V025 Multi-Worktree Workflow¶
Problem¶
A V025 checkout is not a single working directory. It is several git worktrees sharing one repository, each playing a distinct role — but those roles are nowhere documented. Every new contributor and every Claude session re-derives them from git error messages and history:
worktree-manager.sh createfails becausemainis checked out in some other worktree.- A merge into
mainrequires a "dance" — youcdinto a different worktree to run the merge, because you can't checkmainout where you are. - The active workspace carries a long-lived branch whose name has drifted from its content, and the fix (rename in place) is an unwritten convention.
- Spec numbers assigned on unmerged branches collide with numbers the current worktree can't see.
Each symptom has its own surface explanation, so the shared root cause stays invisible. The
knowledge lives in lived practice and in per-user MEMORY.md, but memory is the wrong home: it
truncates, drifts as state changes, and is per-user not per-repo. These are repo-level toolkit
conventions and belong in the toolkit's documentation surface. This doc is that home.
The three roles¶
V025 uses three worktree roles, each with a distinct lifecycle. A worktree's role is defined by the branch it holds, not by its path.
| Role | Branch it holds | Lifecycle |
|---|---|---|
| Main-Holder | main |
The worktree that currently holds main — often none. main is frequently checked out nowhere; it only needs a holder at merge time, and that holder is usually thrown away afterward. Not a standing role — a transient state. See merge into main and EC-1 below. |
| Active-Workspace | a long-lived chore/* or docs/* branch |
The repo-root checkout where day-to-day tooling commands run and commit (intake-qualify routings, spec births, doc-sync updates). Accumulates commits between merges to main. When its branch name no longer reflects its content, it is renamed in place, not deleted — it is the only checkout this worktree has. See rename the active-workspace branch. |
| Feature | feature/spec-NNN-* |
One per active spec, created from main via worktree-manager.sh create, living under .worktrees/feature/. Built through the spec workflow (/vt-c-2-plan → /vt-c-3-build → …), then merged back into main after /vt-c-complete. Removed once merged. |
Why "Main-Holder" is a transient state, not a standing role. Because main is usually held
nowhere, there is normally no Main-Holder to find. Merges are performed by temporarily giving
some worktree main (or spinning up a throwaway holder), merging, then releasing it. If you go
looking for a permanent main-holder worktree, you often won't find one — and that is correct, not
a broken state.
Edge cases referenced in this doc¶
Three recurring situations are labeled EC-1 / EC-2 / EC-3 throughout the procedures below:
- EC-1 —
mainis held nowhere. The normal resting state: no worktree hasmainchecked out, so a merge intomainneeds a throwaway holder. See merge into main. - EC-2 — the Active-Workspace branch name has drifted from what the branch now contains and needs an in-place rename (not a delete). See rename the active-workspace branch.
- EC-3 — a feature branch forked from
mainbefore a recent merge and is now behindmain. See sync a fresh feature branch after a main-merge.
Standard procedures¶
Create a feature worktree¶
From the Active-Workspace, always use the manager script (never raw git worktree add — the
script copies .env* files, manages .gitignore, and keeps the directory layout consistent):
bash ${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh create feature/spec-NNN-slug
The script creates the branch from main and handles the held-main case gracefully (fixed at
commit b53fed62). /vt-c-activate Step 6.7 offers to do this for you at activation time.
Side effect to undo when
mainis held nowhere (EC-1). The script's currency step runsgit checkout mainin the Active-Workspace before adding the worktree. Whenmainis held elsewhere that checkout fails harmlessly and is skipped; but whenmainis held nowhere it succeeds and leaves the Active-Workspace sitting onmain— HEAD is never restored. After the command returns, confirm withgit -C "<active-workspace-path>" branch --show-currentand, if it now readsmain, restore it:git -C "<active-workspace-path>" checkout <active-workspace-branch>.
Merge into main¶
Never check out main in the Active-Workspace to merge. If main is held in another worktree
you'll get a already used by worktree error (see anti-patterns);
if it's held nowhere you could check it out, but the convention is to keep the Active-Workspace on
its own long-lived branch. Instead:
- Find who holds
main, if anyone: - If a holder path is printed (an absolute path from step 1), merge the source branch in that worktree without changing your own cwd:
- If nothing is printed (
mainheld nowhere — the common case, EC-1), create a throwaway holder worktree onmain, merge in it, then remove it:# Anchor every path on the MAIN worktree root (the first entry git lists), so these # steps work unchanged from any cwd — repo root OR a feature worktree. root="$(git worktree list --porcelain | awk 'NR==1{sub(/^worktree /,"",$0);print;exit}')" # Attach the EXISTING main branch with raw `git worktree add` — the one documented # exception to "always use the manager script". The script can't do this: it always # passes `-b` (→ `fatal: a branch named 'main' already exists`) and its currency step # would `git checkout main` in the Active-Workspace, stranding it on main. git worktree add "$root/.worktrees/main-holder" main git -C "$root/.worktrees/main-holder" merge --no-ff <source-branch> git worktree remove "$root/.worktrees/main-holder"
The pre-commit hook has a MERGE_HEAD exception, so --no-ff merges into main need no
--no-verify. This is a local ephemeral-branch workflow — main is not pushed by default.
Sync a fresh feature branch after a main-merge¶
A feature worktree created from main before the day's Active-Workspace commits were merged is
now behind. Bring it current from inside the feature worktree (EC-3):
Do this right after creation if main moved between when the feature branch forked and when you
started work.
Rename the active-workspace branch¶
When the Active-Workspace branch name no longer fits what's on it (EC-2), rename in place rather than deleting — the branch is this worktree's only checkout:
(Real example: docs/doc-sync-2026-05-18 → chore/scratch once the branch had accumulated more
than doc-sync work.)
Anti-patterns & error signatures¶
| You see / you do | What it means | What to do instead |
|---|---|---|
fatal: '<branch>' is already used by worktree '<path>' |
The branch (often main) is checked out in the worktree at <path>. Git allows a branch in only one worktree at a time. |
Don't check it out here. cd "<path>" and work there, or use a throwaway holder. See merge into main. |
Checking out main in the Active-Workspace to merge |
Breaks the role separation; fails outright if main is held elsewhere. |
Merge from the Main-Holder (or a throwaway holder), never from the Active-Workspace. |
| Deleting the Active-Workspace branch because its name is stale | Loses the worktree's only checkout and its accumulated pre-merge commits. | git branch -m to rename in place. |
New feature worktree's first build sees stale main state |
The branch forked from main before a recent merge. |
git merge main inside the feature worktree (EC-3). |
spec-from-requirements proposes a SPEC number that already exists elsewhere |
The tool sees only the current worktree's registry, not IDs on unmerged branches or in main since you forked. | Compute the true next-free ID first — see spec-numbering across branches. |
Spec-numbering across branches¶
Why this manual step exists:
/vt-c-spec-from-requirementsreads only the current worktree's.design-state.yamlwhen choosing the next SPEC ID. In a fragmented multi-worktree checkout that view is incomplete, so the number it proposes can collide. This section is a manual patch over that single-worktree limitation — ifspec-from-requirementsis ever fixed to scan all branches, this procedure can be retired.
There are two ways the numbering can go wrong. Compute both and take the maximum, then use
max + 1.
Mode A — fragmented-ahead¶
Spec numbers live on unmerged feature branches carrying IDs higher than main's registry. The
true next-free ID is the max across all local branches' committed registry:
# Highest SPEC ID declared on ANY local branch (not just the current worktree)
for b in $(git branch --format='%(refname:short)'); do
git show "$b:.design-state.yaml" 2>/dev/null | grep -oE 'SPEC-[0-9]+'
done | grep -oE '[0-9]+$' | sort -n | tail -1
git show sees only committed registry state. A spec ID can also exist as an on-disk
directory that is uncommitted or not-yet-registered (exactly the drift window that caused the
2026-07-04 incident). Add the companion scan and take the max of the two:
# Run from the repo root that physically contains .worktrees/ (the main worktree).
# One `.` walk covers both the root's own specs/ and every worktree under .worktrees/.
find . -type d -path '*/specs/[0-9]*' 2>/dev/null \
| grep -oE 'specs/[0-9]+' | grep -oE '[0-9]+' | sort -n | tail -1
Mode B — stale-behind-main¶
The current branch forked from main long ago and is many commits behind; its registry does
not reflect IDs main has since assigned. Reading the current branch tells you nothing about
this. Check divergence before creating a spec on any long-lived branch:
If <behind> is non-trivial, reconcile against main's registry before picking a number:
Take the max of Mode A and Mode B results; the next-free ID is that + 1.
Illustrative layout example¶
This is an illustrative example of the role pattern at one point in time — your checkout will differ. It shows the shape, it is not a maintained inventory. Run
git worktree listyourself for the current picture.
A git worktree list might look like this:
V025-claude-toolkit [chore/scratch] ← Active-Workspace (repo root)
.worktrees/feature/spec-149-... [feature/spec-149-...] ← Feature
.worktrees/feature/spec-151-... [feature/spec-151-...] ← Feature
V025-wt-ims-workflow-gates [docs/doc-sync-...] ← a sibling worktree (not currently holding main)
Note what's absent: no line shows [main]. In this snapshot main is held nowhere — the normal
resting state (EC-1). A Main-Holder appears only transiently, at merge time.
Where this came from¶
- SPEC-143 activation session (2026-05-28) surfaced the three symptoms — a
worktree-manager.shfailure under heldmain, the cross-worktree merge dance, and the implicit branch-rename — and recognized them as one missing convention doc. Source proposal:intake/processed/from-projects/2026-05-28-v025-multi-worktree-workflow-conventions.md. - The 2026-07-04 spec-numbering incident is the worked example for Mode B: specs were created
on
chore/scratch, which had forked frommainand was ~60 commits behind, so the tool mis-numbered them; the fix was a surgical re-apply of only the genuinely-new specs onto currentmain. This very SPEC-151 session then hit the fragmented-ahead hazard it documents — a scan of all branches surfaced a droppedSPEC-152still living onchore/scratch, higher thanmain's registry. - The surface bug behind symptom 1 was fixed separately at commit
b53fed62(intake/processed/bugs/2026-05-28-resolved-worktree-manager-fails-when-from-branch-checked-out-elsewhere.md).
Related¶
plugins/core-standards/skills/using-git-worktrees/SKILL.md— how to create an isolated worktreeplugins/core-standards/skills/git-worktree/SKILL.md— theworktree-manager.shwrapperplugins/core-standards/skills/activate/SKILL.md— Step 6.7 detects the held-maincase at activationdocs/solutions/patterns/cross-repo-spec-tracking.md— the sibling "one repo, one spec namespace" ruleintake/projects.yaml— registry of projects for cross-project awareness