Skip to content

Research 009 — Worktree at scaffold ​

Status: complete

Verified 2026-08-03 against superpowers 6.1.1 (the active version; 6.2.0 is also cached — see F9), system git, Node v26.5.0, and the repo at branch 009-worktree-at-scaffold (commit ea70076). Local main is level with origin/main at the time of writing (git rev-list --count main..origin/main = 0, and the reverse = 0).

One entry per [NEEDS VERIFICATION] marker in spec.md (V1–V5), plus unprompted findings (F6–F9).

V1 — Does EnterWorktree (and its git worktree add backing) fail cleanly when the branch already exists? ​

Question. R1 gives branch creation to the native tool and R5 keeps a guard. If both the guard and the tool could create the branch, they must not collide silently.

Verdict. Confirmed. Creating a worktree on a pre-existing branch fails loudly and leaves the shared tree untouched — so the failure is safe, and R5's guard becomes belt-and-braces rather than load-bearing.

Evidence. Throwaway spike (run outside the repo, since discarded): git worktree add .wt/009-foo-2 -b 009-foo when 009-foo already exists →

Preparing worktree (new branch '009-foo')
fatal: a branch named '009-foo' already exists

The main tree's HEAD stayed on main before and after (git rev-parse --abbrev-ref HEAD = main). EnterWorktree with name is documented to "create a new git worktree … on a new branch" (EnterWorktree tool schema), i.e. it wraps exactly this git worktree add -b, so it surfaces the same error.

Caveat. In the new flow the branch name is always the freshly-allocated NNN-slug, so this path is reached only if a stale branch of that number exists with no matching specs/ directory — rare. Worth a friendly pre-check in allocate mode, but not a correctness dependency.

V2 — Numbering is read from local specs/, but the worktree branches from origin/main. What breaks when local main is behind origin? ​

Question. EnterWorktree's default base ref is fresh = origin/main (EnterWorktree schema / worktree.baseRef). Allocation reads the local working tree. If those two disagree, the branch number and the worktree's contents can disagree.

Verdict. Confirmed as a real edge, with a clean mitigation. It cannot silently corrupt: the specs/NNN-<slug>/ existence guard (R5), evaluated inside the worktree, catches a collision.

Evidence. If a spec merged on origin/main that local main has not pulled, local allocation under-counts and picks an NNN already taken on origin/main. The worktree, branched from origin/main, then contains that specs/NNN-existing/; scaffold-mode's dir-exists guard fires there rather than overwriting. Today the case is inert (local main is level with origin, measured above).

Resolution for the plan. Make allocation agree with the base ref instead of relying on the tree being current: either derive NNN from origin/main (e.g. git ls-tree origin/main specs/), or have /new-spec git fetch and refuse when main is behind. This tightens R3 — recorded here and folded into R3's wording in spec.md.

V3 — How does finishing-a-development-branch remove a worktree created in an earlier session? ​

Question. R9 routes cleanup through step 6 / finishing-a-development-branch, for a worktree that often outlives the session that made it and is removed only after a remote PR merge.

Verdict. Refuted — for our layout and our merge path, that skill does not do the cleanup. The mechanism itself works; the routing named in R9 is wrong. This sends R9 back to step 1 (see Refuted claims) rather than being patched here.

Evidence. finishing-a-development-branch/SKILL.md (6.1.1):

  • Step 6 cleans up only worktrees under .worktrees/ or worktrees/ (line 173). Ours live under .claude/worktrees/ (native EnterWorktree location, gitignored at .gitignore:24). For those, the skill says the harness owns them and to "use [the] workspace-exit tool" (line 182) — that is ExitWorktree, which by its own schema refuses cross-session worktrees ("will NOT touch … worktrees from a previous session").
  • Its cleanup runs only for Option 1 (local merge) and Option 4 (discard) (line 163). The user's flow is a GitHub merge, i.e. Option 2 ("Push and create a PR"), which explicitly preserves the worktree (line 128). Nothing in the skill removes it after the remote merge.

What does work. Plain git from the main tree, spike-confirmed: git worktree remove .claude/worktrees/NNN-slug then git branch -D NNN-slug removed both, and git worktree list no longer showed it. This is the mechanism a corrected R9 should name (a small /finish-spec command or documented commands), not the skill.

V4 — Does any existing test or CI step assert the old checkout -b behaviour? ​

Question. SC7/R2 remove the branch switch from new-spec.ts. If tests or CI pin the old behaviour, they must change with it.

Verdict. Refuted (the "nothing asserts it" assumption is false). Tests do assert it; CI does not add its own assertion.

Evidence.

  • tests/workflow/scaffold.test.ts:199 — P3 #2: scaffold leaves the working branch at NNN-<slug> asserts git rev-parse --abbrev-ref HEAD == the branch after scaffoldSpec. :221 — SC4: opening a new feature takes one command asserts the same HEAD == result.branch. Both encode the checkout-in-the-shared-tree behaviour R2 removes.
  • tests/workflow/scaffold.test.ts:211 — "scaffold refuses rather than reusing an existing branch" tests the branch-exists guard; relevant but its meaning shifts once the tool owns the branch.
  • CI (.github/workflows/ci.yml) runs pnpm lint/typecheck/test/build/verify:contract/docs:build; the only "checkout" there is actions/checkout@v7 (line 19), unrelated. So the coupling lives entirely in the test suite, and changes there flow through CI automatically.

V5 — Which success criteria are automatable in the new-spec.ts suite versus verified by doc-assertion or manual acceptance? ​

Question. The Definition of Done wants a named test per criterion, but SC1/SC3/SC5/SC6 touch session state, the remote, and a GitHub merge — none reachable from a unit test.

Verdict. Confirmed, and the project already has the pattern for it: (proxy) tests that assert the instruction exists in CLAUDE.md / the command doc, paired with a human-verified row in the traceability table. tests/workflow/constitution.test.ts uses exactly this (its header, lines 9–13, and the many (proxy) tests, e.g. :116, :133, :145).

Mapping.

  • Unit-testable against a throwaway repo (as in scaffold.test.ts): SC7 / R2 (scaffold runs no checkout; shared-tree HEAD unchanged), R3 (allocate prints NNN-slug, writes nothing), R4 (scaffold-mode writes the files), R5 (dir-exists guard), SC4 (four files exist).
  • Doc-assertion (as in constitution.test.ts): R6, R7 (commit+push-before-discovery stated), R8 (step 3.5 removed, using-git-worktrees at step 0), R11 (command orchestration stated), R1.
  • (proxy) + human-verified: SC1 (two coexisting worktrees), SC3 (session cwd == worktree), SC5 (pushed to origin), SC6 (merge cleanup).

F6 — Removing step 3.5 breaks constitution.test.ts as written ​

Why it matters. R8. tests/workflow/constitution.test.ts:95 requires the bindings table to contain ['0','1','1.5','2','3','3.5','4','5','6']. Deleting the 3.5 row fails that test until 3.5 is removed from the required list. A task must update this test alongside CLAUDE.md.

F7 — Step 0 stops being technique-free ​

Why it matters. R1/R8. constitution.test.ts:64 allowlists step 0 as the only step exempt from naming a technique ("bookkeeping"). Under the new flow step 0 names superpowers:using-git-worktrees. The allowlist stays valid (exempt = "not required to name one"; naming one is fine), and the "only step exempt" guard (:87) still holds at size 1. No test breaks, but the plan should note step 0 now legitimately carries a technique.

F8 — scaffoldSpec's single-call API is being split ​

Why it matters. R3/R4. Today scaffoldSpec does numbering + file-writing + git checkout -b in one call (scripts/new-spec.ts:126, checkout at :169), and scaffold.test.ts drives it that way. The new allocate/scaffold split plus "no checkout" means the exported surface and its tests are reworked, not just tweaked. This is the largest code task and should be planned as such (TDD: rewrite the failing branch-assertion tests first).

F9 — Superpowers is dual-cached (6.1.1 active, 6.2.0 present) ​

Why it matters. The plugin floats (per docs/architecture/workflow-tooling.md). Both 6.1.1 and 6.2.0 are cached under ~/.claude/plugins/. This session runs 6.1.1 (the launched skills resolved there). All skill-text evidence above is from 6.1.1; a bump to 6.2.0 could move the finishing-a-development-branch details cited in V3. Not a blocker, but the reason V3's corrected mechanism should be our own command rather than a dependency on that skill's internals.

Refuted claims ​

R9 (routing). Believed: the worktree is removed on merge "via step 6 / finishing-a-development-branch". True: that skill does not clean up .claude/worktrees/ worktrees, and its cleanup does not run on the Option-2 (push-and-PR) path the user's GitHub merge takes (V3). The mechanism that does work is git worktree remove .claude/worktrees/NNN-slug + git branch -D NNN-slug from the main tree. Per the constitution this returns R9 to step 1 for the human to re-decide, rather than being patched in place — pending the human's revision (proposed: name explicit commands or a small /finish-spec command; drop the reliance on that skill). This is the one open item blocking a clean approval.

Graduation ​

Candidates to move to docs/architecture/ at step 6:

  • The worktree facts for this project: base ref fresh = origin/main; worktrees live under .claude/worktrees/ (gitignored); cross-session cleanup is git worktree remove + git branch -D from the main tree; finishing-a-development-branch does not cover this layout (V3, F9). Likely a short docs/architecture/worktrees.md, referenced from CLAUDE.md step 0 and the Cleanup step.