From 4889b4d732754b01b2c73f31910d33e96057ec17 Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:03:58 +0200 Subject: [PATCH] feat(harness): per-worktree setup/teardown lifecycle hooks (closes #9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebased onto current main to resolve the conflict on .claude/agents/implementer.md that blocked the approved PR #29 (its branch was cut from a stale base). Content is identical to #29 — merged cleanly onto main: keeps the GitHub-identity + explicit-staging rules already on main and layers the worktree bootstrap step + teardown + renumbering. - .claude/scripts/worktree.sh — gate.sh-style setup/teardown runner (GATES_FILE-aware). - .claude/gates.json + .claude/self/gates.json — new "worktree" {setup,teardown} block. - implementer.md — step 1 bootstrap + teardown after PR; steps renumbered. - test-runner.md — run worktree.sh setup first when gating a fresh worktree. Supersedes #29. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_011HosUeuSvhetARboEfDW6K --- .claude/agents/implementer.md | 13 ++++++----- .claude/agents/test-runner.md | 2 ++ .claude/gates.json | 6 +++++ .claude/scripts/worktree.sh | 44 +++++++++++++++++++++++++++++++++++ .claude/self/gates.json | 6 +++++ 5 files changed, 65 insertions(+), 6 deletions(-) create mode 100755 .claude/scripts/worktree.sh diff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md index c55ee1a..7b0749b 100644 --- a/.claude/agents/implementer.md +++ b/.claude/agents/implementer.md @@ -16,17 +16,18 @@ Never call bare `gh`. EVERY `gh` invocation (PR create/update, comments, `gh api - `CLAUDE.md` — conventions, style, definition of done. ## Workflow -1. **Explore, don't guess.** Delegate codebase discovery to the `Explore` subagent to map the files you'll touch. Stay read-only until you understand the area. -2. **Respect your boundary.** You were assigned a module/path. NEVER edit files outside it. If the task truly requires touching another module, stop and report back to the orchestrator — do not reach across the boundary. -3. **Implement in small commits.** Match surrounding code style. Write/extend tests alongside the change. +1. **Bootstrap your worktree.** Your worktree is a fresh checkout that lacks toolchain state living outside the tree (`node_modules`, Foundry libs from `forge install`, shared caches). Run `bash .claude/scripts/worktree.sh setup` first if present — it runs the adapter's `worktree.setup` hook so that *every* gate is runnable here, not just in the main checkout. Empty/unconfigured = it skips harmlessly. If setup fails, fix it before proceeding — a half-bootstrapped worktree makes gates lie. +2. **Explore, don't guess.** Delegate codebase discovery to the `Explore` subagent to map the files you'll touch. Stay read-only until you understand the area. +3. **Respect your boundary.** You were assigned a module/path. NEVER edit files outside it. If the task truly requires touching another module, stop and report back to the orchestrator — do not reach across the boundary. +4. **Implement in small commits.** Match surrounding code style. Write/extend tests alongside the change. **Stage explicit paths only — never `git add -A` / `git add .` / `git commit -a`.** A sandboxed session masks sensitive config paths (shell rc, `.gitconfig`, `.mcp.json`, `.claude/{hooks,skills,routines}`, editor dirs) as `/dev/null` character-device nodes; `git status` shows them as untracked, and a blanket `git add` can try to index a device node and abort your commit. Add the files you actually changed, by name. Ignore any `crw-` device-node entries `git status` shows — they are sandbox masks, not your work. -4. **Self-gate before declaring done.** Run, in order, the commands from `.claude/gates.json`: `build` → `lint` → `typecheck` → `test_affected` → `coverage`. Use `.claude/scripts/gate.sh ` if present. Fix anything that fails. Do not report done with a red gate. -5. **Open a PR** (or leave the branch ready, per `CLAUDE.md` merge policy). -6. **Report back** in this format: +5. **Self-gate before declaring done.** Run, in order, the commands from `.claude/gates.json`: `build` → `lint` → `typecheck` → `test_affected` → `coverage`. Use `.claude/scripts/gate.sh ` if present. Fix anything that fails. Do not report done with a red gate. +6. **Open a PR** (or leave the branch ready, per `CLAUDE.md` merge policy). Then run `bash .claude/scripts/worktree.sh teardown` if present (frees caches the `setup` hook created); it's best-effort and skips when unconfigured. +7. **Report back** in this format: ``` - Sub-task: - Branch: <name> diff --git a/.claude/agents/test-runner.md b/.claude/agents/test-runner.md index 576154e..fc1cab9 100644 --- a/.claude/agents/test-runner.md +++ b/.claude/agents/test-runner.md @@ -11,6 +11,8 @@ You run the project's gates and report results. You do not fix code — you repo If any gate or check needs `gh` (e.g. fetching CI status via `gh api`/`gh run`), call it through `.claude/scripts/bot-gh.sh`, never bare `gh`, so it runs as the bot. ## What to run +If you're gating a **fresh isolated worktree** (not the main checkout), first run `bash .claude/scripts/worktree.sh setup` if present — it bootstraps toolchain state that lives outside the tree (`node_modules`, `forge install`, caches) so gates don't fail for lack of setup rather than real defects. Empty/unconfigured = skips. + Read `.claude/gates.json` and run the requested gates (or all configured ones) using `.claude/scripts/gate.sh <name>` when available, else the raw command from the file. Typical order: `install` (if needed) → `build` → `lint` → `typecheck` → `test` (or `test_affected`) → `coverage` → `e2e` → `security`. Skip any gate whose command is empty in `gates.json` and note it as "not configured". diff --git a/.claude/gates.json b/.claude/gates.json index afa5f2f..cb04b16 100644 --- a/.claude/gates.json +++ b/.claude/gates.json @@ -30,6 +30,12 @@ "security": "" }, + "worktree": { + "_note": "Optional per-worktree lifecycle. `setup` runs right after an isolated implementer/reviewer worktree is created — bootstrap toolchain state that lives OUTSIDE the tree so every gate is runnable in-worktree (e.g. 'pnpm install', 'forge install', link shared caches). `teardown` runs before the worktree is removed. Empty string = skip. Run via .claude/scripts/worktree.sh setup|teardown.", + "setup": "", + "teardown": "" + }, + "review": { "lenses": ["correctness", "tests", "security", "performance"], "consensus": "all", diff --git a/.claude/scripts/worktree.sh b/.claude/scripts/worktree.sh new file mode 100755 index 0000000..a7d916f --- /dev/null +++ b/.claude/scripts/worktree.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# worktree.sh <setup|teardown> +# Runs the command stored at .worktree.<phase> in the adapter (gates.json) — the +# per-worktree lifecycle hook. `setup` bootstraps a freshly-created isolated +# worktree (install deps, `forge install`, link shared caches) so that EVERY gate +# is runnable in-worktree, not just in the main checkout; `teardown` runs before +# the worktree is removed (free caches, etc.). +# +# Empty/missing command => skip with exit 0 (so unconfigured repos don't block). +# Non-zero command exit => propagates (so a failed bootstrap surfaces, not hides). +# Mirrors gate.sh: honors the GATES_FILE override and resolves the repo root from +# this script's location — which, inside a worktree, IS the worktree root, so the +# hook installs into the worktree the caller is working in. +set -uo pipefail + +phase="${1:?usage: worktree.sh <setup|teardown>}" +case "$phase" in + setup|teardown) ;; + *) echo "worktree.sh: phase must be 'setup' or 'teardown' (got '$phase')"; exit 2 ;; +esac + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(cd "$script_dir/../.." && pwd)" +# Which adapter to read. Defaults to the project adapter; set GATES_FILE to run a +# different one (e.g. GATES_FILE=.claude/self/gates.json). Relative paths resolve +# from the repo root. +gates_ref="${GATES_FILE:-.claude/gates.json}" +case "$gates_ref" in + /*) gates="$gates_ref" ;; + *) gates="$root/$gates_ref" ;; +esac + +if [ ! -f "$gates" ]; then + echo "worktree.sh: no $gates found — skipping '$phase'"; exit 0 +fi + +cmd="$(node -e "try{const g=require('$gates');process.stdout.write((g.worktree&&g.worktree['$phase'])||'')}catch(e){process.stdout.write('')}" 2>/dev/null)" + +if [ -z "$cmd" ]; then + echo "worktree.sh: '$phase' not configured in $(basename "$gates") — skipping"; exit 0 +fi + +echo "▶ worktree '$phase': $cmd" +cd "$root" && eval "$cmd" diff --git a/.claude/self/gates.json b/.claude/self/gates.json index 1d1a393..bbf212d 100644 --- a/.claude/self/gates.json +++ b/.claude/self/gates.json @@ -24,6 +24,12 @@ "security": "" }, + "worktree": { + "_note": "Self-host workers need no external toolchain state (gates are node/bash-only), so both hooks are empty. Present to document the schema and exercise worktree.sh's skip path.", + "setup": "", + "teardown": "" + }, + "review": { "lenses": ["correctness", "tests"], "consensus": "all", "skills": [] }, "budget": {