diff --git a/CHANGELOG.md b/CHANGELOG.md index 855622a..f7d4ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,49 @@ For each entry, **Upgrade impact** classifies the change against the [three-buck ## Unreleased +### Claude Code's own worktrees are workers too (parallel-agents) + +([0015](docs/decisions/0015-tool-worktrees-are-workers.md), amending +[0008](docs/decisions/0008-dispatcher-and-worker-worktrees.md)) + +Teams that work in the desktop app start tasks in a new session with its worktree option. Two +projects with the module had such worktrees in use while the framework treated them as foreign. + +#### Changed +- **Every linked worktree is a worker.** A task gets its worktree one of two ways: + - **Claude Code's worktree, by default:** a new session with the desktop app's worktree option, + or `claude --worktree`. + - **The scripts (`/dispatch`):** when the worktree needs a port, setup or start commands, or a + base branch other than the default. +- **The session-context hook names the route and the gaps.** In the main checkout, it says which + route this project's tasks take. In a worktree the scripts didn't set up, it says what that + worktree lacks: a generated branch name to rename after triage, the env file, the scripts' setup, + or the right base branch. The role no longer depends on where the worktree sits. +- **`worktree-new.sh` marks the worktrees it sets up,** with a file in the worktree's own git + directory. Worktrees from before the marker are recognized by their folder name. +- **`worktree-ls.sh` lists Claude Code's worktrees as workers.** It flags the ones on a generated + branch or a detached HEAD, instead of asking to move task work out of them. +- **`/dispatch`** opens with which route a task takes. `protect-hub.sh`'s message names both routes. + +#### Added +- **`.worktreeinclude`** (with the module): the env file Claude Code copies from the main checkout + into each worktree it creates. + +#### Upgrade impact +- **Overwrite:** + - Core: `.claude/hooks/session-context.sh`, `.claude/hooks/protect-hub.sh`, + `.claude/hooks/README.md`. + - With the module: `scripts/agent/{_worktree-lib,worktree-new,worktree-ls}.sh` and + `.claude/skills/dispatch/SKILL.md`. +- **Merge** (with the module): `docs/PARALLEL-AGENTS.md`. Take the new § Two routes, the worker row, + and § Claude Code's worktrees, which replaces § Claude Code's built-in worktrees. Keep your + § Shared services. +- **Additive** (with the module): `.worktreeinclude`. List the same file as `ENV_FILE` in + `scripts/agent/worktree.conf`; if the project already has a `.worktreeinclude`, add that line to + it. +- **No migration:** existing script worktrees are recognized by their folder name, and rerunning + `worktree-new.sh` on one adds the marker. + ### `/upgrade` keeps the hub clean when it installs the dispatcher hub Choosing `parallel-agents` moves the upgrade into a worktree created from the last commit, so the diff --git a/README.md b/README.md index 6e8d1b4..6c855dd 100644 --- a/README.md +++ b/README.md @@ -317,7 +317,7 @@ flowchart TD | The requester needs an update on a task | `/stakeholder-update` ("update the client") — in the client's terms, shown in chat, posted on the pull request for the team to relay; on the tracker only on your yes to the exact text | `sonnet` | [Tracker integration](skeleton/docs/TRACKER-INTEGRATION.md) | | Passing work on — a teammate, another machine, a fresh session | `/handoff` — the state committed to the record first, then a short message of pointers to it | session model | [Skills catalog](docs/SKILLS-REFERENCE.md) | | A change to how the team works | `/record-decision` → a PDR in `docs/process/` | `sonnet` | — | -| Several tasks at once | `/dispatch` from the main checkout; each task in its own worktree (`parallel-agents` module) | per task | [Parallel agents](docs/scenarios/parallel-agents.md) | +| Several tasks at once | Each task in its own worktree (`parallel-agents` module): a new session with Claude Code's worktree option, or `/dispatch` from the main checkout when the worktree needs the project's setup | per task | [Parallel agents](docs/scenarios/parallel-agents.md) | | High stakes or a broad sweep | `/deep-review`, `/deep-spec-analysis`, `/deep-context-audit`, `/deep-drift-sweep` | each agent its own | [Skills catalog](docs/SKILLS-REFERENCE.md) | ### Effort, model, and cost diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index b4c8868..a1dd0ac 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -310,11 +310,14 @@ if there's no tracker task). Check for business language, verified claims, and y Each agent session gets its own git worktree — never two in one checkout. With the [`parallel-agents` module](../modules/parallel-agents/MODULE.md), the main checkout **only -dispatches**: `/dispatch` creates the task's worktree and branch and hands off a three-line prompt; the -worker there does everything from triage on. The main checkout is shared — an edit or a dev server +dispatches**. A task starts in a new session with Claude Code's worktree option (the desktop app's +worktree toggle, or `claude --worktree`), or — when its worktree needs the project's setup, such as a +port — `/dispatch` creates the worktree and branch and hands off a three-line prompt. The worker there +does everything from triage on. The main checkout is shared — an edit or a dev server there collides with every other session — and analysis there is wasted: the dispatcher can't run the app or the tests, so the worker re-reads everything where it can verify it. -([decision 0008](decisions/0008-dispatcher-and-worker-worktrees.md)) +([decisions 0008](decisions/0008-dispatcher-and-worker-worktrees.md) and +[0015](decisions/0015-tool-worktrees-are-workers.md)) **Exercise (module installed):** dispatch the typo and the topics estimate. The main checkout's `git status` stays clean, each worker's session context says WORKER, and diff --git a/docs/SKILLS-REFERENCE.md b/docs/SKILLS-REFERENCE.md index 0cf7ab4..eaddd3d 100644 --- a/docs/SKILLS-REFERENCE.md +++ b/docs/SKILLS-REFERENCE.md @@ -51,7 +51,7 @@ In order, for a change with something to decide. Commits: `spec:` → `docs:` | Skill | Module | Purpose | |---|---|---| -| `/dispatch` | `parallel-agents` | In the main checkout: name the task, create its worktree (`worktree-new.sh --no-start`), and hand off to a worker session with a three-line prompt. Reads only; no analysis, no edits. | +| `/dispatch` | `parallel-agents` | In the main checkout, for a task whose worktree needs the project's setup (a port, setup or start commands, a non-default base branch): name the task, create its worktree (`worktree-new.sh --no-start`), and hand off to a worker session with a three-line prompt. Other tasks start in Claude Code's own worktree. Reads only; no analysis, no edits. | ## Dynamic workflows diff --git a/docs/decisions/0008-dispatcher-and-worker-worktrees.md b/docs/decisions/0008-dispatcher-and-worker-worktrees.md index d3bb0ac..4df2a10 100644 --- a/docs/decisions/0008-dispatcher-and-worker-worktrees.md +++ b/docs/decisions/0008-dispatcher-and-worker-worktrees.md @@ -1,6 +1,6 @@ # 0008: The main checkout dispatches; worktrees do the work (optional module) -- **Status:** accepted +- **Status:** accepted; amended by [0015](0015-tool-worktrees-are-workers.md) (Claude Code's own worktrees are workers too) - **Date:** 2026-10-01 ## Context diff --git a/docs/decisions/0015-tool-worktrees-are-workers.md b/docs/decisions/0015-tool-worktrees-are-workers.md new file mode 100644 index 0000000..8fc5775 --- /dev/null +++ b/docs/decisions/0015-tool-worktrees-are-workers.md @@ -0,0 +1,110 @@ +# 0015: Worktrees that Claude Code creates are workers too (parallel-agents module) + +- **Status:** accepted +- **Date:** 2026-10-02 +- **Amends:** [0008](0008-dispatcher-and-worker-worktrees.md) — how a task gets its worktree + +## Context + +Decision 0008 gives each task its own worktree, branch, pull request, and session, and keeps the main +checkout as a hub that only dispatches. It creates worktrees one way: `/dispatch` runs +`scripts/agent/worktree-new.sh`, which seeds the env file from the main checkout and, when a project +needs it, reserves a port and runs setup and start commands. Worktrees that Claude Code creates +itself are treated as foreign. In the desktop app those come from a new session with the **worktree** +option; in the terminal, from `claude --worktree`. The session-context hook gives a session in one +"no role" and asks for a dispatch, and `worktree-ls.sh` flags task branches in them. + +Two projects show that developers use those worktrees for task work anyway: + +- **The project the module came from** had four of them parked, one on a task branch the scripts + never set up. That finding is recorded in 0008 and was met by the "no role" message. +- **A second project — a marketing site, upgraded to `d5934b3` with the module chosen —** had four + more under `.claude/worktrees/`: two on task branches and two on detached HEADs. The team works in + the desktop app. Its worktree option already gives one task its own worktree, branch, and session. + With the hub on, the framework would send those developers to a second tool for the same result. + +Claude Code now covers most of what the scripts were added for: + +- **`.worktreeinclude`** copies gitignored files, such as `.env`, from the main checkout into every + worktree Claude Code creates — the desktop app's included. Seeding from the main checkout is the + same rule 0008 set for the scripts. +- **Project-scope plugins and the main checkout's local settings** reach every worktree of the + repository (Claude Code 2.1.200 and 2.1.211). +- **The desktop app removes its worktrees** when a session is archived, or on its own once the pull + request merges, with auto-archive on. + +What it doesn't cover: + +- **Ports, env overrides, and setup or start commands** — what a worktree that runs a server needs. +- **A base branch other than the default.** App worktrees branch from the default branch; the + `worktree.baseRef` setting takes `"fresh"` or `"head"`, never a branch name. That's wrong for a + project whose tasks start from an integration branch (Model B). +- **The branch name.** It is generated (`worktree-`, or the app's prefix plus a name), so it + doesn't carry the task's `/`, which is how a branch joins its spec folder (0001). + +The "no role" rule also depends on where the worktree is. It matches `.claude/worktrees/`, and the +desktop app's **Worktree location** setting can move them anywhere, where the same kind of worktree +is called a worker. + +## Decision + +With the `parallel-agents` module installed, **every linked worktree is a worker**, whichever tool +created it. A task gets its worktree by one of two routes: + +- **Claude Code's worktree** — a new desktop session with the worktree option, or + `claude --worktree`. This is the default when the project's worktrees run no server and tasks start + from the default branch. The worker renames the branch to `/` after triage + (`git branch -m`), before its first commit. +- **The scripts** — `/dispatch` and `worktree-new.sh`, as in 0008. Use them when a worktree needs what + only they provide: a port, `ENV_OVERRIDES`, `SETUP_CMD` or `START_CMD`, or a `BASE_BRANCH` other than + the default branch. + +What changes: + +- **`session-context.sh`** (core) gives a session in any linked worktree the WORKER role. In a + worktree the scripts didn't create, it adds one line for each thing that worktree lacks: + - a generated branch name: rename it after triage; + - no env file: `.worktreeinclude` should list it; + - a project whose worktrees need a port or start command: dispatch through the scripts for task + work that runs the app; + - a `BASE_BRANCH` other than the default branch: this worktree started from the wrong base. +- **`worktree-new.sh`** marks the worktrees it sets up, with a file in the worktree's own git + directory, so the hook and `worktree-ls.sh` can tell them apart wherever they sit. Worktrees from + before the marker are recognized by their folder, which is named after the branch. +- **The module** ships a `.worktreeinclude` listing `ENV_FILE`, so Claude Code's worktrees get the + env file. Its `MODULE.md` adds a customize step to keep the two in step. +- **`worktree-ls.sh`** lists Claude Code's worktrees as workers. It flags the ones on a generated + branch or a detached HEAD, which are candidates to archive in the app. +- **`docs/PARALLEL-AGENTS.md` and `/dispatch`** describe the two routes and when each fits. +- **`protect-hub.sh`** still stops every edit in the main checkout; its message names both routes. + +## Consequences + +- **Positive:** + - Developers dispatch with the tool they already use, and the desktop flow needs no pasted prompt. + - For the common case it needs no dispatcher session at all: a new session with the worktree + option replaces the second session 0008 counted as a cost. + - The role no longer depends on where the worktree sits. +- **Negative / cost:** + - Branch naming moves from creation to after triage. A worker that forgets leaves a branch that + joins no spec folder. The session-context line is the reminder, and `/open-pr` already + stops on a branch that isn't `/`. + - Two routes to explain instead of one, and the choice depends on facts in `worktree.conf`. + - A project that runs a server still needs the scripts for that task work. Starting it from the app + gives a worktree without a port, and the hook can only say so. + - Claude Code's worktrees pile up unless sessions are archived; the pilot had two on detached HEADs. + `worktree-ls.sh` flags them, and auto-archive in the desktop app removes them. + +## Alternatives considered + +- **Keep "no role" (today).** Two projects worked around it, and the cost lands on desktop users: a + second tool, a dispatcher session, and a pasted prompt for what the app already does. +- **Route Claude Code's worktree creation through the scripts with a `WorktreeCreate` hook.** Every + worktree would get the full setup, but the hook replaces creation for every worktree Claude Code + makes, including subagent and background-session worktrees. Those would each reserve a port and run + setup. It also turns off `.worktreeinclude`. Its input is a generated name like `bold-oak-a3f2`, + since the task isn't known yet, so the scripts' `/` contract breaks. Cleanup moves to a + `WorktreeRemove` hook. Worth revisiting for a project that wants server setup on every worktree, as + an opt-in. +- **Drop the scripts.** A project whose worktrees run servers needs ports, env overrides, and start + and stop commands, and Claude Code has no equivalent. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index e76de11..f526c20 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -18,13 +18,14 @@ projects. | [0005](0005-outward-actions-and-draft-prs.md) | Outward actions only on request; pull requests stay drafts until a human QCs them | accepted | | [0006](0006-guardrails-as-configuration.md) | Guardrails that must hold are configuration and code, not prose | accepted | | [0007](0007-process-decision-records.md) | Process decisions are recorded as PDRs; the constitution is amended through them | accepted | -| [0008](0008-dispatcher-and-worker-worktrees.md) | The main checkout dispatches; worktrees do the work (optional module) | accepted | +| [0008](0008-dispatcher-and-worker-worktrees.md) | The main checkout dispatches; worktrees do the work (optional module) | accepted; amended by 0015 | | [0009](0009-optional-modules.md) | Host- and team-specific harness ships as optional modules | accepted | | [0010](0010-model-aliases.md) | Configure models with version-less aliases | accepted | | [0011](0011-lanes-ceremony-follows-risk.md) | Three lanes — ceremony follows risk and uncertainty, not size | accepted; partly superseded by 0014 | | [0012](0012-choose-the-model-by-the-work.md) | Choose the model by the work — Sonnet for well-specified work, Opus for judgment | accepted | | [0013](0013-adapt-practices-not-a-second-workflow.md) | Adapt practices from other skill collections into our skills — never a second workflow | accepted | | [0014](0014-test-first-in-every-lane.md) | Test first in every lane | accepted | +| [0015](0015-tool-worktrees-are-workers.md) | Worktrees that Claude Code creates are workers too (parallel-agents module) | accepted | Changes that follow from these records are listed, with their upgrade impact, in [`CHANGELOG.md`](../../CHANGELOG.md). diff --git a/docs/scenarios/parallel-agents.md b/docs/scenarios/parallel-agents.md index b4ce2e7..d1524c2 100644 --- a/docs/scenarios/parallel-agents.md +++ b/docs/scenarios/parallel-agents.md @@ -20,7 +20,7 @@ a second session starts, it gets its own worktree. | | Main checkout — **dispatcher** | A worktree — **worker** | |---|---|---| -| Does | Names the task, creates the worktree, hands off (`/dispatch`) | Everything from triage on: spec folder, gate, TDD, commits | +| Does | Routes each task to its worktree — Claude Code's own, or the scripts' through `/dispatch` | Everything from triage on: spec folder, gate, TDD, commits | | Never | Reads code, analyzes, edits, starts environments | Goes back to the main checkout to work | The `session-context` hook tells each session its role when it starts. @@ -35,7 +35,9 @@ again anyway, where it can check it. The dispatcher's context stays cheap: a bra ### In the main checkout — dispatch -`/dispatch ` does four things and nothing else: +A task whose worktree needs the project's setup — here, anything that runs the app — goes through +the scripts. (Other tasks can start in Claude Code's own worktree: see below.) `/dispatch ` does four things and nothing else: 1. **Names the task** — its title and type only; no code reading, no requirements analysis. A short kebab-case slug. A task that names a delivered feature starts its slug with that feature's @@ -92,15 +94,16 @@ running a content-model migration against the shared development environment cha every other worker's app. Content-model work gets its own Contentful environment, set in that worktree's env file. -### Tools that create their own worktrees +### Claude Code's own worktrees -Claude Code can create worktrees itself — `claude --worktree`, subagents with `isolation: worktree`, -and some desktop flows such as suggested-task chips — under `.claude/worktrees/`, on branches it -names. They're fine for read-only exploration and isolated subagent work. The project's scripts never -set them up — no copy of its env file, no branch convention — so **don't use them for task work**: -dispatch instead. A session that starts in one is told it has no role, and `worktree-ls.sh` flags -task branches found in them. -`.claude/worktrees/` stays in `.gitignore` and `.claudeignore`. +A task can also start in a new session with Claude Code's worktree option — the desktop app's +worktree toggle, or `claude --worktree` ([0015](../decisions/0015-tool-worktrees-are-workers.md)). +It's a worker like any other: `.worktreeinclude` copies the env file, and the session renames its +generated branch to `/` after triage. On the newsletter site that's how copy changes and +investigations start. Tasks that run the app go through `/dispatch`, because their worktree needs a +port and the dev server's start command. The session-context hook names what a worktree is missing +when it starts, and `worktree-ls.sh` flags the ones left on a generated branch. Claude Code's +worktrees live under `.claude/worktrees/`, which stays in `.gitignore` and `.claudeignore`. ### Cleanup diff --git a/evals/static/check-skills.sh b/evals/static/check-skills.sh index 07a3caf..9b3f7d6 100755 --- a/evals/static/check-skills.sh +++ b/evals/static/check-skills.sh @@ -606,7 +606,9 @@ check_practices() { file_contains "$SKILLS_DIR/triage/SKILL.md" 'Declined before' || missing+=("/triage: declined-before check") file_contains "$SKELETON/docs/COST-MODEL.md" '^## Between phases' || missing+=("COST-MODEL.md: between phases") file_contains "$SKILLS_DIR/handoff/SKILL.md" 'never a copy' || missing+=("/handoff: pointers, not copies") - file_contains "$HOOKS_DIR/session-context.sh" 'Role: NONE' || missing+=("session-context.sh: no role in Claude Code's own worktrees") + file_contains "$HOOKS_DIR/session-context.sh" 'branch name is generated' || missing+=("session-context.sh: Claude Code's own worktrees are workers, told what they lack (0015)") + [ -f "$MODULES_DIR/parallel-agents/files/.worktreeinclude" ] || missing+=("parallel-agents: .worktreeinclude") + file_contains "$MODULES_DIR/parallel-agents/files/.claude/skills/dispatch/SKILL.md" '## Which route' || missing+=("/dispatch: which route a task takes") file_contains "$MODULES_DIR/parallel-agents/files/scripts/agent/worktree.conf" '^PORT_SLOTS=0 ' || missing+=("worktree.conf: ports off by default") file_contains "$AGENTS_MD" 'write or update the test that asserts the new behavior and watch it fail' || missing+=("AGENTS.md: the fast lane is test-first") file_contains "$SKELETON/.claude/rules/testing.md" '^## Red, then green — every change, in every lane' || missing+=("testing rule: red then green in every lane") diff --git a/evals/static/test-hooks.sh b/evals/static/test-hooks.sh index 17f702d..d1b2ac3 100755 --- a/evals/static/test-hooks.sh +++ b/evals/static/test-hooks.sh @@ -164,19 +164,50 @@ else FAIL=$((FAIL+1)); echo "✘ session-context.sh: protected-branch warning missing"; echo " $out" fi -# With the parallel-agents module, each session learns its role from where it runs. +# With the parallel-agents module, each session learns its role from where it runs: dispatcher in +# the main checkout, worker in any linked worktree (decision 0015). In a worktree the scripts didn't +# set up, it also learns what that worktree lacks. mkdir -p "$T/scripts/agent" && printf '#!/bin/sh\n' > "$T/scripts/agent/worktree-new.sh" && chmod +x "$T/scripts/agent/worktree-new.sh" -git -C "$T" add scripts/agent/worktree-new.sh && git -C "$T" commit -qm "add the worktree script" +cp "$REPO_ROOT/modules/parallel-agents/files/scripts/agent/_worktree-lib.sh" "$T/scripts/agent/" +printf 'BASE_BRANCH="main"\nENV_FILE=".env"\n' > "$T/scripts/agent/worktree.conf" +git -C "$T" add scripts/agent && git -C "$T" commit -qm "add the worktree scripts" +git -C "$T" update-ref refs/remotes/origin/main HEAD && git -C "$T" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main +echo 'SECRET=1' > "$T/.env" git -C "$T" worktree add -q -b feat/role-check "$WORK/feat-role-check" git -C "$T" worktree add -q -b claude/eager-lamport "$T/.claude/worktrees/eager-lamport" -role() { printf '{"cwd":"%s","hook_event_name":"SessionStart"}' "$1" | "$H/session-context.sh" | grep 'Role:'; } -r_main="$(role "$T")"; r_worker="$(role "$WORK/feat-role-check")"; r_builtin="$(role "$T/.claude/worktrees/eager-lamport")" -if echo "$r_main" | grep -q DISPATCHER && echo "$r_worker" | grep -q WORKER && echo "$r_builtin" | grep -q 'Role: NONE'; then - PASS=$((PASS+1)); echo "✓ session-context.sh: dispatcher in the main checkout, worker in a worktree, none in Claude Code's own worktree" -else - FAIL=$((FAIL+1)); echo "✘ session-context.sh: roles wrong"; echo " main: $r_main"; echo " worker: $r_worker"; echo " built-in: $r_builtin" -fi -git -C "$T" worktree remove --force "$WORK/feat-role-check"; git -C "$T" worktree remove --force "$T/.claude/worktrees/eager-lamport" +git -C "$T" worktree add -q --detach "$T/.claude/worktrees/calm-turing" +git -C "$T" worktree add -q -b feat/marked "$WORK/somewhere-else" +echo marker > "$(git -C "$WORK/somewhere-else" rev-parse --absolute-git-dir)/agent-worktree" +ctx() { printf '{"cwd":"%s","hook_event_name":"SessionStart"}' "$1" | "$H/session-context.sh"; } +check_ctx() { # check_ctx [] + local out; out="$(ctx "$2")" + if echo "$out" | grep -q -- "$3" && { [ -z "${4:-}" ] || ! echo "$out" | grep -q -- "$4"; }; then + PASS=$((PASS+1)); echo "✓ session-context.sh: $1" + else + FAIL=$((FAIL+1)); echo "✘ session-context.sh: $1"; echo "$out" | sed 's/^/ /' + fi +} +check_ctx "dispatcher in the main checkout, offering Claude Code's worktree option" "$T" "Start each task in a new session with Claude Code's worktree option" +check_ctx "worker in a worktree the scripts set up, with nothing missing" "$WORK/feat-role-check" "Role: WORKER" "generated\|No .env\|Not set up" +check_ctx "worker in Claude Code's worktree, told to rename its generated branch" "$T/.claude/worktrees/eager-lamport" "branch name is generated (claude/eager-lamport)" +check_ctx "worker in Claude Code's worktree, told the env file is missing" "$T/.claude/worktrees/eager-lamport" "No .env here" +check_ctx "worker on a detached HEAD, told to create the task's branch" "$T/.claude/worktrees/calm-turing" "Detached HEAD" +check_ctx "a marked worktree counts as the scripts' wherever it is" "$WORK/somewhere-else" "Role: WORKER" "generated\|No .env\|Not set up" +git -C "$T/.claude/worktrees/eager-lamport" branch -q -m feat/renamed +cp "$T/.env" "$T/.claude/worktrees/eager-lamport/.env" +check_ctx "a renamed branch with its env file needs nothing more" "$T/.claude/worktrees/eager-lamport" "Role: WORKER" "generated\|No .env\|Not set up" +printf 'BASE_BRANCH="main"\nENV_FILE=".env"\nPORT_SLOTS=180\nSTART_CMD="npm run dev"\n' > "$T/.claude/worktrees/eager-lamport/scripts/agent/worktree.conf" +cp "$T/.claude/worktrees/eager-lamport/scripts/agent/worktree.conf" "$T/scripts/agent/worktree.conf" +check_ctx "a project whose worktrees run a server: dispatch for app work" "$T" "A task that runs the app starts with /dispatch" +check_ctx "Claude Code's worktree there lacks the port and start command" "$T/.claude/worktrees/eager-lamport" "lacks a port and setup or start commands" +printf 'BASE_BRANCH="staging"\nENV_FILE=".env"\n' > "$T/.claude/worktrees/eager-lamport/scripts/agent/worktree.conf" +cp "$T/.claude/worktrees/eager-lamport/scripts/agent/worktree.conf" "$T/scripts/agent/worktree.conf" +check_ctx "tasks that start from another branch always dispatch" "$T" "Start each task with /dispatch: tasks here start from staging" +check_ctx "Claude Code's worktree there started from the wrong base" "$T/.claude/worktrees/eager-lamport" "started this worktree from main, but tasks here start from staging" +git -C "$T" checkout -q -- scripts/agent/worktree.conf && rm -f "$T/.env" +for w in "$WORK/feat-role-check" "$T/.claude/worktrees/eager-lamport" "$T/.claude/worktrees/calm-turing" "$WORK/somewhere-else"; do + git -C "$T" worktree remove --force "$w" +done # protect-hub — with the module, the main checkout is the hub and edits nothing git -C "$T" worktree add -q -b feat/hub-check "$WORK/feat-hub-check" @@ -186,7 +217,7 @@ edit_event() { printf '{"tool_name":"Edit","cwd":"%s","tool_input":{"file_path": run protect-hub.sh 2 "$(edit_event "$T/src/app.ts")" "stops an edit in the main checkout" run protect-hub.sh 2 "$(edit_event "$T/src/new/file.ts")" "stops a new file in the main checkout" run protect-hub.sh 0 "$(edit_event "$WORK/feat-hub-check/src/app.ts")" "lets edits in a task worktree through" -run protect-hub.sh 0 "$(edit_event "$T/.claude/worktrees/calm-hopper/notes.md")" "leaves Claude Code's own worktrees to the session role" +run protect-hub.sh 0 "$(edit_event "$T/.claude/worktrees/calm-hopper/notes.md")" "lets edits in Claude Code's own worktrees through" run protect-hub.sh 0 "$(edit_event "$WORK/no-module/src/app.ts")" "does nothing in a repository without the module" echo 'HUB_READONLY=""' >> "$T/.claude/hooks/config.sh" run protect-hub.sh 0 "$(edit_event "$T/src/app.ts")" "does nothing when HUB_READONLY is empty" diff --git a/evals/static/test-modules.sh b/evals/static/test-modules.sh index ecdd9a7..301d782 100755 --- a/evals/static/test-modules.sh +++ b/evals/static/test-modules.sh @@ -143,10 +143,13 @@ out=$(scripts/agent/worktree-ls.sh 2>&1) check "worktree-ls: lists every worktree and marks the main checkout" "echo \"\$out\" | grep -q feat/newsletter-signup && echo \"\$out\" | grep -q fix/other-thing && echo \"\$out\" | grep -q 'main checkout'" out=$(scripts/agent/worktree-ls.sh --info 2>&1) check "worktree-ls --info: runs ENV_INFO_CMD in each worktree, with its placeholders" "echo \"\$out\" | grep -q 'info-feat-newsletter-signup-$P1'" -git -C "$M" worktree add -q -b fix/in-builtin "$M/.claude/worktrees/eager-lamport" main 2>/dev/null +check "worktree-new: marks the worktrees it sets up, in their own git directory" "[ -f \"\$(git -C '$W1' rev-parse --absolute-git-dir)/agent-worktree\" ] && [ -z \"\$(git -C '$W1' status --porcelain)\" ]" +git -C "$M" worktree add -q -b claude/eager-lamport "$M/.claude/worktrees/eager-lamport" main 2>/dev/null +git -C "$M" worktree add -q -b fix/in-app "$M/.claude/worktrees/calm-hopper" main 2>/dev/null out=$(scripts/agent/worktree-ls.sh 2>&1) -check "worktree-ls: flags task work in Claude Code's own worktrees" "echo \"\$out\" | grep -q 'fix/in-builtin is task work'" -git -C "$M" worktree remove --force "$M/.claude/worktrees/eager-lamport" +check "worktree-ls: lists Claude Code's worktrees, and flags one on a generated branch" "echo \"\$out\" | grep -q 'eager-lamport is on a generated branch (claude/eager-lamport)' && echo \"\$out\" | grep -q 'fix/in-app'" +check "worktree-ls: no flag for a renamed branch, and the scripts' worktrees count as theirs" "! echo \"\$out\" | grep -q 'calm-hopper is on' && ! echo \"\$out\" | grep 'feat-newsletter-signup' | grep -q 'not from the scripts'" +git -C "$M" worktree remove --force "$M/.claude/worktrees/eager-lamport"; git -C "$M" worktree remove --force "$M/.claude/worktrees/calm-hopper" echo work > "$W2/work.txt" && git -C "$W2" add work.txt && git -C "$W2" commit -qm work echo dirty > "$W2/dirty.txt" diff --git a/modules/parallel-agents/MODULE.md b/modules/parallel-agents/MODULE.md index 8f1a805..ec05c97 100644 --- a/modules/parallel-agents/MODULE.md +++ b/modules/parallel-agents/MODULE.md @@ -10,18 +10,22 @@ project where the main checkout dispatched tasks and workers in sibling worktree | File | Purpose | |---|---| | `scripts/agent/worktree-new.sh` | A worktree per task: branch `/`, the env file seeded from the main checkout (when the project has one), optional setup and start commands, and — for projects that run a server — a port reserved under a lock. Idempotent; `--no-start`, `--setup-only`, `--refresh-env`, `--from ` | -| `scripts/agent/worktree-ls.sh` | Every worktree's branch and uncommitted changes (and port and state, when worktrees run a server); flags task work in Claude Code's own worktrees. `--info` adds what `ENV_INFO_CMD` prints for each | +| `scripts/agent/worktree-ls.sh` | Every worktree's branch and uncommitted changes (and port and state, when worktrees run a server); flags worktrees the scripts didn't set up while they sit on a generated branch or a detached HEAD. `--info` adds what `ENV_INFO_CMD` prints for each | | `scripts/agent/worktree-rm.sh` | Stop (when a stop command is set), remove, and safely delete the branch | | `scripts/agent/worktree.conf` | The project's settings — base branch, env file, setup/start/stop commands, environment info, and the server-only port settings | | `scripts/agent/_worktree-lib.sh` | Shared helpers | -| `.claude/skills/dispatch/SKILL.md` | `/dispatch`: the four-step handoff from the main checkout | +| `.claude/skills/dispatch/SKILL.md` | `/dispatch`: which route a task takes, and the four-step handoff through the scripts | +| `.worktreeinclude` | The env file Claude Code copies into the worktrees it creates — the desktop app's worktree option, `claude --worktree` | | `docs/PARALLEL-AGENTS.md` | The dispatcher/worker process, the scripts, shared vs isolated services | -With the module installed, the core skeleton's session-context hook announces each session's role: -dispatcher in the main checkout, worker in a worktree, and none in one of Claude Code's own worktrees -(never set up by these scripts — fine for reading, not for task work). The core `protect-hub.sh` -hook stops file edits in the main checkout once the module is there. The core `/handoff` skill covers passing -work in progress on; `/dispatch` is the first handoff of every task. +A task gets its worktree one of two ways (decision 0015): **Claude Code's worktree** — a new session +with the desktop app's worktree option, or `claude --worktree` — by default, or **the scripts** +(`/dispatch`) when the worktree needs a port, setup or start commands, or a base branch other than +the default. With the module installed, the core session-context hook announces each session's role +— dispatcher in the main checkout, worker in any worktree — and, in a worktree the scripts didn't set +up, what it lacks: a task branch name, the env file, or the scripts' setup. The core `protect-hub.sh` +hook stops file edits in the main checkout once the module is there. The core `/handoff` skill +covers passing work in progress on. ## Install @@ -30,6 +34,8 @@ cp -R modules/parallel-agents/files/. /path/to/your-repo/ chmod +x scripts/agent/*.sh ``` +A repository that already has a `.worktreeinclude` keeps it: add the env file's line to it. + ## Customize 1. **`scripts/agent/worktree.conf`** — at least `BASE_BRANCH`; `ENV_FILE` / `ENV_TEMPLATE` if the @@ -43,7 +49,10 @@ chmod +x scripts/agent/*.sh services); optionally set `ENV_INFO_CMD` so `worktree-ls.sh --info` prints what someone needs to use each environment. 4. **`.gitignore`** — the env file, and `.claude/worktrees/`. +5. **`.worktreeinclude`** — the same env file as `ENV_FILE`, plus any other gitignored file a worktree + needs (gitignore syntax). Claude Code copies only files that are gitignored. ## Requirements -`bash`, `git` 2.31+; `curl` only for `READY_URL`. macOS and Linux. +`bash`, `git` 2.31+; `curl` only for `READY_URL`. macOS and Linux. `.worktreeinclude` needs a Claude Code +version that supports it. diff --git a/modules/parallel-agents/files/.claude/skills/dispatch/SKILL.md b/modules/parallel-agents/files/.claude/skills/dispatch/SKILL.md index cfc4322..21725f2 100644 --- a/modules/parallel-agents/files/.claude/skills/dispatch/SKILL.md +++ b/modules/parallel-agents/files/.claude/skills/dispatch/SKILL.md @@ -1,6 +1,6 @@ --- name: dispatch -description: From the main checkout, hand a task to its own worktree — name the task, create the worktree and branch with scripts/agent/worktree-new.sh --no-start, and start (or hand the developer) a worker session there with a short prompt that points at the process. Does no analysis and edits nothing. Use when a task arrives in the main checkout. +description: From the main checkout, hand a task to its own worktree — name the task, create the worktree and branch with scripts/agent/worktree-new.sh --no-start, and start (or hand the developer) a worker session there with a short prompt that points at the process. Does no analysis and edits nothing. Use when a task arrives in the main checkout and needs the scripts' worktree (a port, setup or start commands, a base branch other than the default) — or when the developer asks for it. argument-hint: "[tracker link or task description]" --- @@ -15,6 +15,21 @@ dispatcher's context stays cheap: a branch name, not a plan. A dispatcher **writes nothing outside this machine** and takes no outward-facing action. Its only network calls are reads: the task in step 1, and the `git fetch` inside the worktree script. +## Which route + +A task gets its worktree one of two ways (decision 0015): + +- **Claude Code's worktree** — a new session with the worktree option: the desktop app's worktree + toggle, or `claude --worktree `. No dispatcher needed: the worker renames the branch to + `/` after triage, and `.worktreeinclude` copies the env file. The default when the + project's worktrees run no server and tasks start from the default branch. +- **The scripts — this skill** — when the worktree needs what only they set up: a port, setup or + start commands, or a base branch other than the default branch (`scripts/agent/worktree.conf`). + The session-context hook says which applies when this session starts. + +When a task arrives here and Claude Code's worktree fits, say so and stop: the developer starts a new +session with the worktree option and gives it the task. Otherwise, follow the steps. + ## Steps 1. **Name the task — nothing more.** Read only enough of the task to know its title and type @@ -36,10 +51,9 @@ network calls are reads: the task in step 1, and the `git fetch` inside the work 3. **Start the worker session in the worktree.** - Terminal: `cd && claude`, then paste the prompt from step 4. - - Where the tooling can't root a session in a folder you choose — for example, an app whose - suggested-task chips always create their own worktree elsewhere, without this worktree's env - file and port — **don't use those chips for task work.** Give the developer the prompt from - step 4 and ask them to open a new session with the worktree as its folder. + - Desktop app: a new session with the worktree as its folder — not the worktree option, which + would create a second worktree without this one's port and setup. Give the developer the prompt + from step 4. 4. **Write the worker's prompt** — pointers, not instructions: ``` @@ -71,6 +85,7 @@ network calls are reads: the task in step 1, and the `git fetch` inside the work ## Verification - [ ] Only the task's title and type were read +- [ ] The route fits: the scripts only when the task needs their setup, or the developer asked for them - [ ] The worktree was created (or reported) by `worktree-new.sh --no-start` — never by raw `git worktree add` - [ ] Nothing was edited, committed, or posted from the main checkout - [ ] The worker prompt has the task link, worktree path, branch, and "follow AGENTS.md, starting with triage" — nothing else diff --git a/modules/parallel-agents/files/.worktreeinclude b/modules/parallel-agents/files/.worktreeinclude new file mode 100644 index 0000000..b72840b --- /dev/null +++ b/modules/parallel-agents/files/.worktreeinclude @@ -0,0 +1,5 @@ +# Gitignored files Claude Code copies from the main checkout into each worktree it creates — a new +# desktop session with the worktree option, or claude --worktree (gitignore syntax). Keep it in step +# with ENV_FILE in scripts/agent/worktree.conf; worktrees from scripts/agent/worktree-new.sh get +# their env file from the script instead. +.env diff --git a/modules/parallel-agents/files/docs/PARALLEL-AGENTS.md b/modules/parallel-agents/files/docs/PARALLEL-AGENTS.md index c64cbae..acc182c 100644 --- a/modules/parallel-agents/files/docs/PARALLEL-AGENTS.md +++ b/modules/parallel-agents/files/docs/PARALLEL-AGENTS.md @@ -10,8 +10,8 @@ the project needs them, its own copy of the env file and a port of its own. | | Main checkout — **dispatcher** | A worktree — **worker** | |---|---|---| -| Where | The clone itself | A sibling of the main checkout, named after the branch: `../feat-newsletter-signup` | -| Does | Names the task, creates the worktree, hands off (`/dispatch`) | Everything else: triage, spec folder, plan, approval gate, TDD, commits | +| Where | The clone itself | Any linked worktree: Claude Code's own, or a sibling the scripts create (`../feat-newsletter-signup`) | +| Does | Routes the task to its worktree (§ Two routes); creates it with `/dispatch` when the scripts are needed | Everything else: triage, spec folder, plan, approval gate, TDD, commits | | Never | Reads code, analyzes, edits, starts anything | Goes back to the hub to work | | Network | Reads only (the task, `git fetch`) | Whatever the workflow allows — outward actions only when asked | @@ -20,6 +20,22 @@ hook holds the dispatcher to it: a file edit in the main checkout is stopped (`H `.claude/hooks/config.sh`). The same goes for maintenance such as a framework upgrade — dispatch it to a worktree of its own like any other task. +## Two routes to a worktree + +| | Claude Code's worktree | The scripts (`/dispatch`) | +|---|---|---| +| How | A new session with the worktree option — the desktop app's worktree toggle, or `claude --worktree ` | `/dispatch` in the main checkout runs `scripts/agent/worktree-new.sh / --no-start` | +| Branch | A generated name — the worker renames it after triage: `git branch -m /` | `/` from the start | +| Env file | Copied from the main checkout by `.worktreeinclude` | Seeded from the main checkout by the script | +| Port, setup, start commands | None | What `worktree.conf` sets | +| Starts from | The default branch | `BASE_BRANCH`, or `--from ` | +| Use it when | The default: the worktree runs no server and tasks start from the default branch | The worktree needs a port or setup or start commands, the base branch isn't the default, or the developer asks | + +Both are workers, and the session-context hook names what a worktree lacks when it starts: a +generated branch, the env file, or the scripts' setup. Keep `.worktreeinclude` in step with +`ENV_FILE`. The desktop app removes its worktrees when a session is archived — or once the pull +request merges, with auto-archive on; `worktree-rm.sh` removes the scripts'. + **Why split them.** The main checkout is shared: an edit, a running process, or a half-finished change there gets in the way of everyone who starts next. And analysis done in the hub is thrown away — the dispatcher can't run the tests, so its conclusions are unverified, and the worker re-reads @@ -31,12 +47,12 @@ a task can be picked up by an agent on any machine. Passing work on later in a t | Command | Does | |---|---| | `scripts/agent/worktree-new.sh / [--no-start \| --setup-only] [--refresh-env] [--from ]` | Creates `../-` on branch `/` (from `BASE_BRANCH`, or `--from` a tag for a hotfix) and, when the project has an env file, seeds the worktree's copy from the **main checkout's**. Then runs `SETUP_CMD` and `START_CMD` when the project sets them — unless `--no-start` (create only) or `--setup-only` (just what the git hooks and tests need). Rerunning on an existing worktree never touches its branch, and `--refresh-env` rewrites its env file | -| `scripts/agent/worktree-ls.sh [--info]` | Lists every worktree: branch, uncommitted changes, and — when worktrees run a server — port and whether it's up. Warns when two claim one port, or when task work sits in one of Claude Code's own worktrees. `--info` adds what `ENV_INFO_CMD` prints for each | +| `scripts/agent/worktree-ls.sh [--info]` | Lists every worktree: branch, uncommitted changes, and — when worktrees run a server — port and whether it's up. Warns when two claim one port, and flags worktrees the scripts didn't set up while they sit on a generated branch or a detached HEAD. `--info` adds what `ENV_INFO_CMD` prints for each | | `scripts/agent/worktree-rm.sh / [--force]` | Runs `STOP_CMD` when set, removes the worktree, deletes the branch only if git sees it as merged | Settings live in `scripts/agent/worktree.conf`, and the defaults assume nothing: no ports, no -containers, no commands. Never create or remove task worktrees with raw `git worktree add` — the -scripts keep branches, env files, and ports consistent. +containers, no commands. Never create or remove the scripts' worktrees with raw `git worktree add` — +the scripts keep branches, env files, and ports consistent. **Match the environment to the lane.** Most tasks need only what the git hooks and the tests use (`--setup-only`); start anything heavier when a test or check actually needs it. Everything a @@ -88,16 +104,11 @@ one — the place for whatever someone needs to use that environment: its URLs, in with. Derive these every time: environments come and go, so a table committed to the repository would be wrong by the time anyone read it. -## Claude Code's built-in worktrees - -Claude Code can create worktrees itself (`claude --worktree `, subagents with -`isolation: worktree`, and some desktop flows), under `.claude/worktrees/` on branches it names. -Those are fine for read-only exploration or isolated subagent work, but this project's scripts never -set them up — so task work goes through `worktree-new.sh`, and `.claude/worktrees/` stays in -`.gitignore` and `.claudeignore`. +## Claude Code's worktrees -Task work still ends up in them: a session started from a suggested task, or a developer reusing a -parked one. So a session that starts in one is told it has **no role** — read and explore, but ask -for a dispatch before working (the session-context hook) — and `worktree-ls.sh` flags any -`/` branch living in one, with how to move it. The app may park these worktrees rather -than delete them; remove the ones you don't need with `git worktree remove`. +Claude Code also creates worktrees for subagents with `isolation: worktree` and for background +sessions, under `.claude/worktrees/`. Those are its own, short-lived ones; `.claude/worktrees/` stays +in `.gitignore` and `.claudeignore`. A task worktree from the desktop app or `claude --worktree` +lands there too, unless the app's **Worktree location** setting moves it: wherever it is, it's a +worker (§ Two routes). Archive sessions you're done with, so their worktrees don't pile up — +`worktree-ls.sh` flags the ones left on a generated branch or a detached HEAD. diff --git a/modules/parallel-agents/files/scripts/agent/_worktree-lib.sh b/modules/parallel-agents/files/scripts/agent/_worktree-lib.sh index cd4b854..078d258 100644 --- a/modules/parallel-agents/files/scripts/agent/_worktree-lib.sh +++ b/modules/parallel-agents/files/scripts/agent/_worktree-lib.sh @@ -66,11 +66,24 @@ project_name() { to_slug "$prefix-$2" } -# builtin_worktree
— one of Claude Code's own worktrees (.claude/worktrees/), -# which get no env file, no port, and a generated branch name. -builtin_worktree() { - case "$1" in "$2"/.claude/worktrees/*) return 0 ;; esac - return 1 +# scripts_worktree — a worktree worktree-new.sh set up. It leaves a marker in the +# worktree's own git directory; worktrees from before the marker are named after their branch. +# Any other linked worktree — Claude Code's own, from the desktop app or `claude --worktree` — is a +# worker too, without the scripts' env overrides, port, or setup (decision 0015). +scripts_worktree() { + local git_dir + git_dir="$(git -C "$1" rev-parse --absolute-git-dir 2>/dev/null)" || return 1 + [ -f "$git_dir/agent-worktree" ] || [ "$(basename "$1")" = "$(to_slug "$2")" ] +} + +# generated_branch — a name nobody chose for the task: Claude Code's (worktree-, +# claude/), any name without a / prefix, or a detached HEAD. +generated_branch() { + case "$1" in + "(detached)" | worktree-* | claude/*) return 0 ;; + */*) return 1 ;; + esac + return 0 } # The path where a branch is checked out, if any. diff --git a/modules/parallel-agents/files/scripts/agent/worktree-ls.sh b/modules/parallel-agents/files/scripts/agent/worktree-ls.sh index 688ed2a..5c71074 100755 --- a/modules/parallel-agents/files/scripts/agent/worktree-ls.sh +++ b/modules/parallel-agents/files/scripts/agent/worktree-ls.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash # List every worktree of this repository: branch, uncommitted changes, and — when worktrees run a # server — its port and whether something is listening on it. With --info, also what ENV_INFO_CMD -# prints for each one (its URLs, the accounts to sign in with). Everything is derived on each run: -# environments come and go, so a written-down copy would be wrong by the time anyone read it. +# prints for each one (its URLs, the accounts to sign in with). Worktrees the scripts didn't set up — +# Claude Code's own, from the desktop app or `claude --worktree` — are listed as workers too, and +# flagged while they sit on a generated branch or a detached HEAD. Everything is derived on each +# run: environments come and go, so a written-down copy would be wrong by the time anyone read it. # # Usage: scripts/agent/worktree-ls.sh [--info] set -uo pipefail @@ -21,7 +23,7 @@ rows=() ports="" seen=" " duplicates="" -builtin_tasks="" +unnamed="" details="" while IFS= read -r path; do branch="$(git -C "$path" branch --show-current 2>/dev/null)" @@ -35,17 +37,18 @@ while IFS= read -r path; do fi changes="$(git -C "$path" status --porcelain 2>/dev/null | wc -l | tr -d ' ')" label="$path" - builtin="" + outside="" if [ "$path" = "$MAIN_CHECKOUT" ]; then label="$path (main checkout)" - elif builtin_worktree "$path" "$MAIN_CHECKOUT"; then - builtin=1 - label="$path (Claude Code's own worktree)" - case "$branch" in claude/* | "(detached)") ;; */*) builtin_tasks="$builtin_tasks $branch" ;; esac + elif ! scripts_worktree "$path" "$branch"; then + outside=1 + label="$path (not from the scripts)" + if generated_branch "$branch"; then unnamed="$unnamed +$branch|$path"; fi fi rows+=("$branch|${port:--}|$state|$changes|$label") - if [ -n "$INFO" ] && [ -n "$ENV_INFO_CMD" ] && [ "$path" != "$MAIN_CHECKOUT" ] && [ -z "$builtin" ]; then + if [ -n "$INFO" ] && [ -n "$ENV_INFO_CMD" ] && [ "$path" != "$MAIN_CHECKOUT" ] && [ -z "$outside" ]; then APP_PORT="$port" SLUG="$(basename "$path")" PROJECT="$(project_name "$MAIN_CHECKOUT" "$SLUG")" @@ -73,9 +76,10 @@ done for port in $duplicates; do printf '\n !! Two worktrees claim port %s — one of them is talking to the other'"'"'s app.\n Fix one with: scripts/agent/worktree-new.sh --refresh-env\n' "$port" done -for branch in $builtin_tasks; do - printf '\n !! %s is task work in one of Claude Code'"'"'s own worktrees, which this project'"'"'s scripts\n never set up. Move it: commit there, git worktree remove , then\n scripts/agent/worktree-new.sh %s\n' "$branch" "$branch" -done +while IFS='|' read -r branch path; do + [ -n "$path" ] || continue + printf '\n !! %s is on %s. Give the task its branch after triage\n (git branch -m /, or git switch -c from a detached HEAD); if its work is done, archive\n the session in the desktop app or git worktree remove it.\n' "$path" "$( [ "$branch" = "(detached)" ] && echo "a detached HEAD" || echo "a generated branch ($branch)")" +done <<<"$unnamed" if [ -n "$INFO" ]; then if [ -z "$ENV_INFO_CMD" ]; then diff --git a/modules/parallel-agents/files/scripts/agent/worktree-new.sh b/modules/parallel-agents/files/scripts/agent/worktree-new.sh index 1d57197..3d976d9 100755 --- a/modules/parallel-agents/files/scripts/agent/worktree-new.sh +++ b/modules/parallel-agents/files/scripts/agent/worktree-new.sh @@ -114,6 +114,11 @@ else fi fi +# Marks the worktree as set up here, in its own git directory (nothing git status shows), so the +# session-context hook and worktree-ls.sh can tell it from Claude Code's own worktrees. +marker="$(git -C "$WORKTREE_DIR" rev-parse --absolute-git-dir)/agent-worktree" +[ -f "$marker" ] || echo "Set up by scripts/agent/worktree-new.sh — read by session-context.sh and worktree-ls.sh." >"$marker" + # --- Port, under a lock ------------------------------------------------------------------------ # "Taken" means listening now OR reserved in a sibling worktree's env file whose environment isn't # up yet. Checking only the first is a race: two agents starting together both see a free port. diff --git a/skeleton/.claude/hooks/README.md b/skeleton/.claude/hooks/README.md index a10cec9..19df4eb 100644 --- a/skeleton/.claude/hooks/README.md +++ b/skeleton/.claude/hooks/README.md @@ -6,7 +6,7 @@ depending on what the model decides. They are wired in `../settings.json`. | Hook | Event | What it does | |---|---|---| -| `session-context.sh` | SessionStart | Adds a few lines to the session: main checkout or worktree, branch, uncommitted changes, the spec folder for the branch and its status, and — with the parallel-agents module — whether this session is a dispatcher or a worker | +| `session-context.sh` | SessionStart | Adds a few lines to the session: main checkout or worktree, branch, uncommitted changes, the spec folder for the branch and its status, and — with the parallel-agents module — whether this session is a dispatcher or a worker, and what a worktree the scripts didn't set up lacks (a task branch name, the env file, the scripts' setup) | | `guard-git.sh` | PreToolUse · Bash | Blocks `--no-verify` (and `git commit -n`), commits on protected branches, and pushes, force-pushes, or deletes targeting protected branches | | `protect-paths.sh` | PreToolUse · Edit/Write | Blocks hand-edits to generated files (lockfiles, generated types) and modifications to existing files in append-only history (migrations) | | `careful-paths.sh` | PreToolUse · Edit/Write | The first edit in each sensitive area (`CAREFUL_GLOBS`) is stopped once per session, so the agent confirms the change is in the careful or full lane before going on. Empty `CAREFUL_GLOBS` turns it off | diff --git a/skeleton/.claude/hooks/protect-hub.sh b/skeleton/.claude/hooks/protect-hub.sh index 35eee3b..ab0e999 100755 --- a/skeleton/.claude/hooks/protect-hub.sh +++ b/skeleton/.claude/hooks/protect-hub.sh @@ -21,4 +21,4 @@ git_dir="$(cd "$root" && cd "$(git rev-parse --git-dir)" && pwd -P)" common_dir="$(cd "$root" && cd "$(git rev-parse --git-common-dir)" && pwd -P)" [ "$git_dir" = "$common_dir" ] || exit 0 -block "$root is the main checkout — the shared hub, where the dispatcher edits nothing. Hand the task to its own worktree (/dispatch, or scripts/agent/worktree-new.sh / --no-start) and make this change from a session there." +block "$root is the main checkout — the shared hub, where the dispatcher edits nothing. Give the task its own worktree — a new session with Claude Code's worktree option, or /dispatch when it needs the project's worktree setup — and make this change from a session there." diff --git a/skeleton/.claude/hooks/session-context.sh b/skeleton/.claude/hooks/session-context.sh index 76f2169..86c5ebd 100755 --- a/skeleton/.claude/hooks/session-context.sh +++ b/skeleton/.claude/hooks/session-context.sh @@ -53,18 +53,58 @@ if [ "$slug" != "$branch" ] && [ -d "$root/$SPECS_DIR" ]; then fi if [ -x "$root/scripts/agent/worktree-new.sh" ]; then + # What this project's worktrees need beyond what Claude Code gives its own — a port, setup or start + # commands, a base branch other than the default — read from scripts/agent/worktree.conf. + needs="" base_branch="" env_file=".env" + if [ -f "$root/scripts/agent/_worktree-lib.sh" ]; then + IFS='|' read -r needs base_branch env_file < <(bash -c '. "$1" >/dev/null 2>&1 || exit 0 + n=""; [ "${PORT_SLOTS:-0}" -gt 0 ] 2>/dev/null && n="a port" + [ -z "$SETUP_CMD$START_CMD" ] || n="${n:+$n and }setup or start commands" + printf "%s|%s|%s\n" "$n" "$BASE_BRANCH" "$ENV_FILE"' _ "$root/scripts/agent/_worktree-lib.sh") + fi + default_branch="$(git -C "$root" symbolic-ref --short -q refs/remotes/origin/HEAD 2>/dev/null)" + default_branch="${default_branch#origin/}" + other_base="" + if [ -n "$base_branch" ] && [ -n "$default_branch" ] && [ "$base_branch" != "$default_branch" ]; then + other_base=1 + fi main="$(cd "$common_dir/.." && pwd -P)" + if [ "$checkout" = "main checkout" ]; then - echo "- Role: DISPATCHER. This is the shared main checkout — hand each task to its own worktree (/dispatch); never edit code here." + echo "- Role: DISPATCHER. This is the shared main checkout — never edit here. Each task gets its own worktree, branch, and session." + if [ -n "$other_base" ]; then + echo "- Start each task with /dispatch: tasks here start from $base_branch, and Claude Code's own worktrees start from $default_branch." + elif [ -n "$needs" ]; then + echo "- A task that runs the app starts with /dispatch (its worktree needs $needs). Any other task can start in a new session with Claude Code's worktree option — the desktop app's worktree toggle, or claude --worktree." + else + echo "- Start each task in a new session with Claude Code's worktree option — the desktop app's worktree toggle, or claude --worktree — or with /dispatch." + fi else - case "$(cd "$root" && pwd -P)" in - "$main"/.claude/worktrees/*) - echo "- Role: NONE. This is one of Claude Code's own worktrees, which this project's scripts never set up (a generated branch, none of the project's env). Fine for reading and exploring; for task work, ask the developer to dispatch the task from the main checkout (/dispatch) and open a session in the worktree it creates." - ;; - *) - echo "- Role: WORKER. This worktree is yours for one task — start with triage (/triage)." - ;; - esac + echo "- Role: WORKER. This worktree is yours for one task — start with triage (/triage)." + # The scripts mark the worktrees they set up; ones made before the marker are named after their branch. + folder="$(printf '%s' "$branch" | tr '[:upper:]' '[:lower:]' | tr '/' '-' | tr -cs 'a-z0-9_-' '-' | sed -E 's/^-+//; s/-+$//')" + if [ ! -f "$git_dir/agent-worktree" ] && [ "$(basename "$root")" != "$folder" ]; then + generated="" + case "$branch" in + "detached HEAD") ;; + worktree-* | claude/*) generated=1 ;; # Claude Code's own names + */*) ;; # already / + *) generated=1 ;; + esac + if [ "$branch" = "detached HEAD" ]; then + echo "- Detached HEAD: after triage, create the task's branch — git switch -c /." + elif [ -n "$generated" ]; then + echo "- The branch name is generated ($branch): after triage, rename it — git branch -m / — so it joins its spec folder and /open-pr takes it." + fi + if [ -n "$env_file" ] && [ -f "$main/$env_file" ] && [ ! -e "$root/$env_file" ]; then + echo "- No $env_file here. Claude Code copies it into the worktrees it creates when .worktreeinclude lists it." + fi + if [ -n "$other_base" ]; then + echo "- Claude Code started this worktree from $default_branch, but tasks here start from $base_branch: ask the developer to /dispatch the task before the first commit." + elif [ -n "$needs" ]; then + echo "- Not set up by scripts/agent/worktree-new.sh, so it lacks $needs: fine for work that doesn't run the app. To run it, ask the developer to /dispatch the task." + fi + fi fi fi