Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/settings/models.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
{
"availableModels": [
"fable",
"sonnet",
"opus",
"haiku",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
* =====================================================================================================
*/
{
// Write permissions for specific files
// Edit permissions for specific files
// This should only ever be running in a devcontainer, so pretty lenient permissions are allowed
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"permissions": {
"allow": ["Write(/tmp/**/*)", "Write(/workspaces/**/tmp/**)", "Write(tmp/**)"],
// A single leading slash anchors at the settings source, so absolute paths need the `//` prefix
"allow": ["Edit(//tmp/**)", "Edit(//workspaces/**/tmp/**)", "Edit(tmp/**)"],
// Agents are allowed to run any task, but this provides some basic protection against editing a task to do something nefarious that's normally forbidden.
"ask": ["Write(Taskfile.yaml)", "Write(.config/taskfiles/**)", "Edit(Taskfile.yaml)", "Edit(.config/taskfiles/**)"],
"ask": ["Edit(Taskfile.yaml)", "Edit(.config/taskfiles/**)"],
},
}
13 changes: 9 additions & 4 deletions .claude/skills/address-pr-comments/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: address-pr-comments
description: Addresses PR review comments by making code changes and posting replies. Takes a PR number or auto-detects from current branch. Use when the user wants to address review feedback, respond to PR comments, fix PR feedback, or says "address comments on PR X".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Grep, Glob, AskUserQuestion
allowed-tools: Bash, Read, Write, Edit, Grep, Glob, AskUserQuestion, Skill
adapted-from: https://github.com/josh-gree/my-claude-skills
---

Expand Down Expand Up @@ -190,9 +190,12 @@ Within a single Phase 2 invocation, the order below is strict. The invariants ap
> **Never commit before the reply body is finalized with the user. Never push before all reply texts in the batch are finalized. Never post a reply before its commit has been pushed. Never skip the commit link in a code-change reply.**

1. For each queued **code change** (in order):
- Follow the `/create-issues` process: create a bd issue with a proper title, description (why this change, context from the PR comment), design (technical approach), and Given-When-Then acceptance criteria. Export after creating.
- Invoke the `Skill` tool with `skill: "create-issues"`, passing the reviewer's comment and the agreed change as `args`. Follow the process it returns to create a bd issue with a proper title, description (why this change, context from the PR comment), design (technical approach), and Given-When-Then acceptance criteria. Export after creating.
- Mark the issue in progress
- If the change involves code: execute the TDD red/green/refactor cycle (`/red` → `/green` → `/refactor`) against the acceptance criteria. If the change is non-code (docs, markdown, config, scripts, etc.): make the change directly — TDD does not apply, but the remaining steps are identical.
- Decide first whether the change is testable code. **If it is not** — documentation, markdown, comments, config, shell scripts where the project has no harness for them, or any other change with no behaviour to assert — make the change directly and move to the next step. Do **not** invoke `red`, `green` or `refactor`, and do not invent a test to justify the change: TDD does not apply here. Everything after this step is identical either way.
- **If it is testable code**, work through the TDD cycle by invoking the `Skill` tool once per phase, in this order — `skill: "red"`, then `skill: "green"`, then `skill: "refactor"` — passing the bd issue ID and the acceptance criterion being driven as `args`. Repeat the three-phase cycle until every acceptance criterion is satisfied.

**Each phase must be a real `Skill` tool call — not a description of one, and not your own recollection of what red/green/refactor mean.** Those skills carry project-specific rules that general TDD practice does not include, and they are re-read fresh each cycle so later cycles do not drift from earlier ones. Writing a test or an implementation without a preceding `Skill` call for that phase is a workflow violation.
- **Finalize the reply body with the user — before committing.** Draft the reply text (everything except the commit link) — **do NOT include the AI attribution footer; `check-footer.py` appends it**. Write to `<reply_file>`, tell the user the absolute path (per [Conventions](#conventions)), and go through the approve/edit loop with the user until they approve. Do not proceed until the user explicitly confirms the text. Leave a clear `[COMMIT LINK]` placeholder where the link will go.

Use AskUserQuestion with this wording (do NOT say "Post now" — the reply is queued for posting after the push, not posted immediately):
Expand Down Expand Up @@ -252,6 +255,8 @@ Summarise what was done:
- Skip comments without asking
- Leave uncommitted changes
- Include the AI attribution footer in generated reply text — `check-footer.py` is solely responsible for it
- Write tests or implementation from your own knowledge of TDD — when a change is testable code, `red`, `green` and `refactor` must each be invoked through the `Skill` tool
- Run the TDD cycle on a change that has no behaviour to assert — docs, markdown, comments, config and unharnessed shell scripts are made directly

## Handling Common Scenarios

Expand All @@ -273,7 +278,7 @@ Discuss with user, then post a reply explaining the reasoning if they want to pu
- [ ] If `--resume`: build inventory, classify reply drafts (placeholder vs finalized vs orphan), prompt user for resume strategy
- [ ] Fetch and display comments
- [ ] Phase 1: collect decisions for all comments (reply-only comments posted in Phase 1)
- [ ] Phase 2: implement code changes, one commit per comment
- [ ] Phase 2: invoke `create-issues` via the `Skill` tool for every queued item; add `red` → `green` → `refactor` only for items that are testable code; one commit per comment
- [ ] Push all commits in a single push
- [ ] Post code-change replies with commit links after push
- [ ] Report what was done
Expand Down
2 changes: 1 addition & 1 deletion .config/.copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
_commit: v0.0.161
_commit: v0.1.5
_src_path: gh:LabAutomationAndScreening/copier-base-template.git
copier_answers_directory: .config
description: Creating libraries in the VueJS ecosystem
Expand Down
9 changes: 6 additions & 3 deletions .config/.copier-managed-files.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
".claude/settings/models.jsonc",
".claude/settings/permissions/additional-dirs.jsonc",
".claude/settings/permissions/bash.jsonc",
".claude/settings/permissions/edit.jsonc",
".claude/settings/permissions/read.jsonc",
".claude/settings/permissions/write.jsonc",
".claude/skills/address-pr-comments/SKILL.md",
".claude/skills/address-pr-comments/check-footer.py",
".claude/skills/address-pr-comments/commit-link.py",
Expand Down Expand Up @@ -98,7 +98,9 @@
"Taskfile.yaml",
"biome.jsonc",
"copier.yml",
"copier_template_resources/eslint.shared.mjs",
"copier_template_resources/eslint/eslint.shared.mjs",
"copier_template_resources/eslint/istanbul-ignore-if-must-throw.mjs",
"copier_template_resources/eslint/istanbul-ignore-if-must-throw.spec.ts",
"copier_template_resources/openapi_schema_simplifier.py",
"extensions/context.py",
"pyproject.toml",
Expand All @@ -108,6 +110,7 @@
"src/copier_tasks/__init__.py",
"src/copier_tasks/copier_provenance.py",
"src/copier_tasks/ensure_pnpm_minimum_release_age_exclude.py",
"src/copier_tasks/normalize_pnpm_minimum_release_age_exclude.py",
"src/copier_tasks/remove_precommit_hooks.py",
"template/.claude/.beads/.gitignore",
"template/.claude/.beads/config.yaml",
Expand Down Expand Up @@ -142,8 +145,8 @@
"template/.claude/settings/models.jsonc",
"template/.claude/settings/permissions/additional-dirs.jsonc",
"template/.claude/settings/permissions/bash.jsonc",
"template/.claude/settings/permissions/edit.jsonc",
"template/.claude/settings/permissions/read.jsonc",
"template/.claude/settings/permissions/write.jsonc",
"template/.claude/skills/address-pr-comments/SKILL.md",
"template/.claude/skills/address-pr-comments/check-footer.py",
"template/.claude/skills/address-pr-comments/commit-link.py",
Expand Down
8 changes: 4 additions & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
"extensions": [
// basic tooling
// "eamodio.gitlens@15.5.1",
"coderabbit.coderabbit-vscode@0.21.5",
"coderabbit.coderabbit-vscode@0.21.7",
"ms-vscode.live-server@0.5.2025051301",
"MS-vsliveshare.vsliveshare@1.0.5905",
"anthropic.claude-code@2.1.252",
"anthropic.claude-code@2.1.273",

// Python
"-ms-python.vscode-pylance", // pyrefly is the language server for this project; ms-python.python auto-installs Pylance via its extension pack, and Pylance then warns on every boot that it's disabled
"ms-python.python@2026.5.2026052901",
"meta.pyrefly@1.2.9002",
"meta.pyrefly@1.3.1",
"ms-vscode-remote.remote-containers@0.414.0",
"charliermarsh.ruff@2026.68.0",

Expand Down Expand Up @@ -67,5 +67,5 @@
"initializeCommand": "sh .devcontainer/initialize-command.sh",
"onCreateCommand": "sh .devcontainer/on-create-command.sh",
"postStartCommand": "sh .devcontainer/post-start-command.sh"
// Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): c4ef9d1e # spellchecker:disable-line
// Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): a4afbd6c # spellchecker:disable-line
}
6 changes: 3 additions & 3 deletions .devcontainer/install-ci-tooling.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
import tempfile
from pathlib import Path

UV_VERSION = "0.12.6"
PNPM_VERSION = "11.22.0"
COPIER_VERSION = "==9.17.1"
UV_VERSION = "0.12.15"
PNPM_VERSION = "12.4.2"
COPIER_VERSION = "==9.18.2"
COPIER_TEMPLATE_EXTENSIONS_VERSION = "==0.3.3"
PRE_COMMIT_VERSION = "4.6.2"
TASK_VERSION = "3.53.1"
Expand Down
41 changes: 25 additions & 16 deletions .github/actions/install_deps/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ inputs:
description: Node version to install
required: false # if this is set, then assume you want node dependencies installed
default: 'notUsing'
package-json-file:
description: Relative path to a package.json to read the pnpm version from (its `packageManager` or `devEngines.packageManager` field). When unset, the pnpm version pinned by the template is used instead.
working-directory:
description: Relative path to the directory holding the package.json to read the pnpm version from (its `packageManager` or `devEngines.packageManager` field). When unset, the pnpm version pinned by the template is used instead.
required: false
default: 'notUsing'
install-deps:
Expand Down Expand Up @@ -68,43 +68,52 @@ runs:
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Check for a pnpm lock file # pnpm/setup can only cache the store when there's a lock file at the repo root to key the cache off of
id: pnpm-lock-probe
- name: Locate the pnpm project # pnpm/setup keys the store cache off a lock file, so it can only cache when one exists in the directory the project lives in
id: pnpm-project
if: ${{ inputs.node-version != 'notUsing' }}
env:
WORKING_DIRECTORY: ${{ inputs.working-directory }}
run: |
if [ -f pnpm-lock.yaml ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
if [ "$WORKING_DIRECTORY" = "notUsing" ]; then
project_dir="."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
project_dir="$WORKING_DIRECTORY"
fi
echo "dir=$project_dir" >> "$GITHUB_OUTPUT"
if [ -f "$project_dir/pnpm-lock.yaml" ]; then
echo "lock_exists=true" >> "$GITHUB_OUTPUT"
else
echo "lock_exists=false" >> "$GITHUB_OUTPUT"
fi
shell: bash

- name: Setup pnpm and node # this action installs both pnpm and the node runtime, so actions/setup-node is not needed
if: ${{ inputs.node-version != 'notUsing' }}
uses: pnpm/setup@v2.0.2
uses: pnpm/setup@v2.1.0
with:
# an empty version makes pnpm/setup read it from package-json-file instead. Passing both a version and a package.json with a conflicting `packageManager` field is a hard error in pnpm/setup.
version: ${{ inputs.package-json-file == 'notUsing' && '11.22.0' || '' }}
package-json-file: ${{ inputs.package-json-file == 'notUsing' && 'package.json' || inputs.package-json-file }}
# an empty version makes pnpm/setup read it from the manifest in working-directory instead. Passing both a version and a package.json with a conflicting `packageManager` field is a hard error in pnpm/setup.
version: ${{ inputs.working-directory == 'notUsing' && '12.4.2' || '' }}
working-directory: ${{ steps.pnpm-project.outputs.dir }}
# cache-dependency-path is left at its default, which is the lock file inside working-directory -- the same one the probe above checks
runtime: node@${{ inputs.node-version }}
cache: ${{ steps.pnpm-lock-probe.outputs.exists }}
cache: ${{ steps.pnpm-project.outputs.lock_exists }}
install: false # dependency installation is handled by manual-setup-deps.py, which knows about every environment in the repo

- name: Install tooling
# the funky syntax is github action ternary
# --no-node is always passed because the only node tooling this script installs is pnpm, which pnpm/setup above already installed
run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} --no-node ${{ inputs.skip-installing-ssm-plugin-manager && '--skip-installing-ssm-plugin' || '' }}
run: python .devcontainer/install-ci-tooling.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} --no-node ${{ inputs.skip-installing-ssm-plugin-manager == 'true' && '--skip-installing-ssm-plugin' || '' }}
shell: pwsh

- name: OIDC Auth for CodeArtifact
if: ${{ inputs.code-artifact-auth-role-name != 'no-code-artifact' }}
uses: aws-actions/configure-aws-credentials@v6.2.3
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: arn:aws:iam::${{ inputs.code-artifact-auth-role-account-id }}:role/${{ inputs.code-artifact-auth-role-name }}
aws-region: ${{ inputs.code-artifact-auth-region }}

- name: Install dependencies
# the funky syntax is github action ternary
if: ${{ inputs.install-deps }}
run: python .devcontainer/manual-setup-deps.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-updating-devcontainer-hash && '--skip-updating-devcontainer-hash' || '' }} ${{ inputs.skip-installing-pulumi-cli && '--skip-installing-pulumi-cli' || '' }}
if: ${{ inputs.install-deps == 'true' }}
run: python .devcontainer/manual-setup-deps.py ${{ inputs.python-version == 'notUsing' && '--no-python' || '' }} ${{ inputs.node-version == 'notUsing' && '--no-node' || '' }} ${{ inputs.skip-updating-devcontainer-hash == 'true' && '--skip-updating-devcontainer-hash' || '' }} ${{ inputs.skip-installing-pulumi-cli == 'true' && '--skip-installing-pulumi-cli' || '' }}
shell: pwsh
6 changes: 3 additions & 3 deletions .github/reusable_workflows/build-docker-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,13 @@ jobs:

- name: Set up Docker Buildx
if: ${{ (inputs.save-as-artifact && inputs.push-role-name == 'no-push') || steps.image-status.outputs.status == 'notfound' }}
uses: docker/setup-buildx-action@v4.0.0
uses: docker/setup-buildx-action@v4.4.1
with:
version: v0.33.0
version: v0.37.1

- name: Build Docker Image
if: ${{ (inputs.save-as-artifact && inputs.push-role-name == 'no-push') || steps.image-status.outputs.status == 'notfound' }}
uses: docker/build-push-action@v7.1.0
uses: docker/build-push-action@v7.4.0
with:
context: ${{ inputs.context }}
build-contexts: ${{ inputs.build-contexts }}
Expand Down
18 changes: 9 additions & 9 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,16 @@ jobs:
if: always()
steps:
- name: fail if prior job failure
# Iterating the needs context keeps this gate from drifting out of sync with the job list above
env:
NEEDS: ${{ toJSON(needs) }}
run: |
success_pattern="^(skipped|success)$" # these are the possibilities: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#needs-context

if [[ ! "${{ needs.get-values.result }}" =~ $success_pattern ]] ||
[[ ! "${{ needs.confirm-on-tagged-copier-template.result }}" =~ $success_pattern ]] ||
[[ ! "${{ needs.check-skip-duplicate.result }}" =~ $success_pattern ]] ||
[[ ! "${{ needs.pre-commit.result }}" =~ $success_pattern ]] ||
[[ ! "${{ needs.unit-test.result }}" =~ $success_pattern ]] ||
[[ ! "${{ needs.copier-create-verification-matrix.result }}" =~ $success_pattern ]]; then
echo "❌ One or more jobs did not finish with skipped or success"
# these are the possibilities: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#needs-context
not_ok=$(jq -r 'to_entries[] | select(.value.result | test("^(skipped|success)$") | not) | .key' <<<"$NEEDS")

if [[ -n "$not_ok" ]]; then
echo "❌ One or more jobs did not finish with skipped or success:"
echo "$not_ok"
exit 1
fi
echo "✅ All jobs finished with skipped or success"
Expand Down
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ repos:
.*tsconfig.json|
)$
- id: check-yaml
# some tools (e.g. pnpm) create multiple-document YAML streams
args: [--allow-multiple-documents]
# .copier-answers.yml is an autogenerated YAML file by Copier that throws 'found unhashable key' errors
# Helm charts contain other non-YAML markers that cause errors here. they should be checked with helm-lint instead
# VCRpy cassettes sometimes contain non-YAML markers that cause errors---but they're autogenerated anyway
Expand Down
22 changes: 22 additions & 0 deletions copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,28 @@ pnpm_minimum_release_age_exclude:
default: ""


_migrations:
# PNPM12-MIGRATION -- this whole block is transient: it carries repos across the pnpm 11 -> 12 boundary and
# nothing else. Delete both entries (and with them `_migrations` entirely, if nothing else has been added)
# once every downstream repo has taken an update that ran them. Grep PNPM12-MIGRATION for the rest.
# Both of these run in the `before` stage, while the repo's own pnpm-workspace.yaml is still on disk and
# before anything in the update reaches for pnpm. pnpm 12 cannot load a manifest whose
# minimumReleaseAgeExclude is a scalar -- it fails every pnpm command, `pnpm --version` included -- so the
# value has to be a list before the pnpm 12 install below, and `pnpm config` cannot do the rewrite itself.
- command: python3 '{{ _copier_conf.src_path }}/src/copier_tasks/normalize_pnpm_minimum_release_age_exclude.py'
when: "{{ _stage == 'before' }}"
# The devcontainer's pnpmVersion only takes effect on a rebuild, so an update applied from an existing
# container still has the old pnpm on PATH while the tasks that read pnpm 12 settings run. The version is
# hardcoded rather than templated because this migration exists to cross the pnpm 11 -> 12 boundary, and
# the check keeps it from downgrading a container that is already on 12 or newer.
- command: |
installed_major="$(pnpm --version 2>/dev/null | cut -d. -f1)"
if [ -z "$installed_major" ] || [ "$installed_major" -lt 12 ]; then
echo "Installing pnpm 12.4.2 (found '${installed_major:-none}') so pnpm 12 workspace settings can be read before a devcontainer rebuild..."
npm install -g pnpm@12.4.2
fi
when: "{{ _stage == 'before' }}"

_tasks:
- command: |
python3 '{{ _copier_conf.src_path }}/src/copier_tasks/remove_precommit_hooks.py' \
Expand Down
Loading