Worktree
Drive the lifecycle of an isolated git worktree: detect existing isolation,
create a fork through doctrine worktree fork (the verb that adds the
worktree, provisions it, and binds it to its phase when it can — all with
compensating rollback; the fork builds into its own in-tree target/), run the
spawn guards, and verify a green baseline before handing off.
Announce at start: "Using the worktree skill to set up an isolated workspace."
This is a sub-skill. /execute (mode=solo) invokes it for opt-in isolation;
the dispatch spawn script calls fork for every worker, whatever the harness;
the /dispatch router invokes the coordination path (worktree coordinate,
below) for the per-run dispatch/<slice> tree. It is not a /route destination
— reach it through the caller.
Mode contract
The skill is parameterised so the funnel reuses it without inheriting solo semantics (design §5, the OQ-1 split seam).
Inputs:
mode = solo | workerallow_work_in_place: bool— true only forsolo- requested branch / path
base— the ref the fork is created from.solodefaults toHEAD;workerREQUIRES an explicit base (the orchestrator's coordination HEADB), because the session HEAD is notB— the orchestrator drives from the coordination branch while the session repo may sit onmain. Forking from the implicit session HEAD imports a divergent base and corrupts theB..Sdelta.
Behaviour:
solo— MAY degrade to the work-in-place rung on sandbox denial (no fork; the blessed trunk path).worker— MUST NOT degrade. A worker with no real fork is a hard failure; the funnel's isolation is mandatory.fork --worker --base <B>pins the base explicitly — never the implicit session HEAD, which for a worker is notBand breaksS.parent == B. Beyond creation, the worker half runs a constrained edit→verify→commit-one-S-to-fork loop; see Worker mode (SL-031 §5.2).
Outputs: { fork_path, branch, head_sha, provision_report, baseline_result }.
worker adds { fork_branch, head_sha_after } once it has committed its single
delta S — what the orchestrator imports B..head_sha_after from.
Coordination mode — doctrine worktree coordinate (SL-064, a THIRD path)
solo/worker above both go through fork. The dispatch coordination worktree
is a separate creation verb, not a fork mode — the /dispatch router calls it
once per run, before batch 1:
doctrine worktree coordinate --slice <N> --dir <path>
It creates (or resumes) dispatch/<slice> in its own worktree off the resolved
trunk — the funnel's sole write target (design §2; ADR-012). It differs from
fork on every axis that matters:
- The orchestrator's own tree. The coordination tree is the orchestrator
(worker-mode OFF — worker-ness is a property of the process, carried by
DOCTRINE_WORKERin the worker's jail, never of a directory). Orchestrator-classed, refused under worker-mode like the rest of the verb class. - Create-or-resume, never a second branch. A live worktree already on
dispatch/<slice>is refused (coordination-live— concurrent same-slice dispatch is illegitimate). A branch that exists with no live worktree resumes (reattach) — so a fresh orchestrator after/handoverpicks up the same branch (resume-stable; a per-run discriminator would break resume). - Regenerates the runtime phase sheets from the committed
plan.toml(the SL-056 provision axis), via the sole copier — no coordination-tier copy.
Not a worker delta path: there is no S and no import. Lifecycle is
worktree-life < branch-life — the directory is removed at conclude, but
dispatch/<slice> + the projected review/phase refs are kept as deliverables
(see /dispatch). Solo/worker creation below is unchanged.
Detection (adapt, don't re-create — D1)
If the CWD is already inside a linked worktree, adapt to it; do not create another.
git rev-parse --git-dir # differs from common-dir ⇒ linked worktree
git rev-parse --git-common-dir
--git-dir ≠ --git-common-dir ⇒ already in a linked worktree → skip creation.
Submodule guard. A submodule also trips that inequality. Disambiguate:
- the
.gitgitdir resolves under…/worktrees/⇒ worktree; under…/modules/⇒ submodule; or - a non-empty
git rev-parse --show-superproject-working-tree⇒ submodule.
A submodule is not the isolation we want — treat it as "not yet forked".
Creation — doctrine worktree fork (the controlled path — D5/F1)
Pick the fork directory, run the commit-before-spawn guard, then call the verb:
doctrine worktree fork --base <B> --branch <name> --dir <path> [--worker]
fork is the single create+provision+(mark)+emit verb, atomic via
compensating rollback (any failure after git worktree add triggers a best-effort
worktree remove --force / branch -D / dir reap; a rollback that itself fails
names the leftover and exits non-zero — never a silent half-rollback). It:
git worktree add -b <branch> <dir> <base>— refuses if<dir>/<branch>exist or<base>is not a commit. Pass<base>explicitly:HEADfor solo,Bfor a worker — the explicit base is why this path is controlled.- provisions the fork (the sole copier — coordination/runtime tier withheld at the copy seam, D9; see Provisioning).
- with
--worker— plus both--sliceand--phase, and a<coord>/.worktrees/<name>dir — binds the fork to its(slice, phase). Any of those missing and the fork is simply unbound, which downstream verbs report honestly rather than guess around (solo omits--worker). - reports human status to stderr; stdout stays empty (machine-clean). The fork
builds into its own in-tree
<dir>/target— no env contract is emitted (SL-156: the platform exited the build-env business).
fork is Orchestrator-classed — refused under worker-mode (a worker never
forks; the orchestrator/solo, worker-mode OFF, calls it).
Creation backend ladder — degrade in order:
- Existing isolation (detection above) → skip creation, adopt the fork.
- Harness-native creation if present and invocable creation-only (no
auto-copy) — opportunistic only. (Claude Code's
WorktreeCreatehook is the instance, but its payload lacks base/path/type, so doctrine cannot drive it and dispatch does not: it forks explicitly instead.) Design around it; never depend on it. doctrine worktree fork— the blessed tested default. Prefer this.- Work-in-place (
soloonly,allow_work_in_place=true) on sandbox denial — no fork, the trunk path.workeraborts instead.
Worktree directory selection
Pick the fork directory before calling fork, in priority order
(superpowers:using-git-worktrees):
- Reuse an existing dir —
.worktreeswins overworktreesif both exist. - Check CLAUDE.md for a stated preference — use it without asking.
- Ask the user if no directory exists and no preference is stated.
The fork path is <dir>/<branch>. Safety: the dir MUST be gitignored
(git check-ignore -q .worktrees) before creating, else the fork contents get
tracked — if not ignored, add the line to .gitignore and commit first. (A
global dir outside the project needs no such check.)
Provisioning (D9)
fork provisions automatically; never run a second copier. doctrine worktree provision <fork> exists as the standalone sole copier (run from the
source root, never inside the fork) for the rare adopt-existing path that
skipped fork. It reads .worktreeinclude, fails closed on a tier-naming
pattern, and copies only allowlisted gitignored files — withholding the
coordination/runtime tier at the copy seam even under a broad ** pattern.
Honest invariant framing (F7 — do not overstate)
The copy-seam guarantee holds because provision is the only copier.
doctrine worktree check-allowlistis a static smell test — green means no pattern names the tier; it is NOT completeness.select_copies(inside provision) is the actual guarantee.- If a harness force-copies on creation and cannot be run creation-only,
doctrine cannot prevent that copy — the guarantee degrades to
check-allowlistonly, and the project must keep.worktreeincludeprecise. This is why theforkverb (fully controlled) is the default and the harness-native rung is never depended on.
Guards
Run these around the spawn. A guard that fails aborts — report, do not improvise past it.
- Commit-before-spawn (D5). Before
fork, the source tree must be clean so the fork sees only committed HEAD:git status --porcelain -z. Abort on any dirty tracked file or any untracked non-ignored file (it would be silently absent from the fork). Ignored files are fine — provision handles the allowlisted subset. - Branch-point check (D5).
forklands the worktree at<base>by construction; the concurrency extension is the batch-commit boundary:doctrine worktree branch-point-check --base <B>exits 0 iff coordination HEAD still equals the orchestrator's pre-spawnB, 1 otherwise (→ re-dispatch). It is the orchestrator's guard at import time, worker-mode OFF — not the worker's. A HEAD-stationarity compare, not a merge-base (C-V). - Baseline-verify (D9). After creation, run the project's regenerate-and-verify
command inside the fork and gate handoff on green (this repo:
doctrine check gate). The command is project-provided — never a hardcodedcargo …. An unbuildable fork is fixed in provisioning, never handed off.
Worker mode (the funnel half)
mode=worker is the path a dispatch worker runs inside its fork --worker
worktree. Creation, provision, and the guards above run first unchanged; this
is what happens after a green baseline, in place of solo handoff. The worker is
a constrained writer: one importable delta, then return, never touching the
coordination/runtime tier (the fork already withholds it, D9).
Armed by the jail, not by cooperation. DOCTRINE_WORKER=1 is set inside the
confinement namespace by the spawn script, so the doctrine guard is armed before
the worker's first command: any doctrine-mediated authored write (slice,
memory record, backlog, minting) refuses. Worker-ness is a property of
the process, not of the directory. Outside a dispatch spawn — a hand-driven
worker turn — export it yourself first.
The constrained loop:
- Mutate source only. Edit source files in the fork. Do not write
.doctrine/authored trees, runtime state, or memory — an import touching them is rejected (report+halt). - Verify. Run the orchestrator-supplied verify command (passed in the
worker prompt — not assumed
doctrine check gate). A red verify is reported back; the worker does not hand back a red delta as if it were green. - Leave the delta uncommitted. A confined worker cannot commit — the
linked worktree's real git dir is read-only inside the jail, so there is no
self-commit path and none is wanted. The importable unit is the working tree
itself, which the orchestrator gathers with
worktree import --from-worktree. Neverreset,checkout --,stashorcleanyour own work away — that destroys the only copy. Stay within your declared file set; straying breaks the file-disjoint batch.
MUST NOT degrade to work-in-place. A worker with no real fork is a hard
abort, never a silent in-tree edit (contrast solo rung 4). If creation failed,
report and stop.
Return a structured report (held in orchestrator context, never a doctrine artifact): what changed, the verify result, memory-worthy notes, and the fork branch. Knowledge trails the orchestrator's confirmed commit, not the fork (record-on-trunk, below).
Squash-orphan caveat (record-on-trunk)
Memory recorded inside a worktree branch is orphaned by a squash-merge (the content survives but the git anchor points at a commit that never lands; SL-008 staleness fires). When durable memory must outlive the fork, record it on trunk, not inside the worktree branch.
The .worktreeinclude template (F2 — project-owned, not installed)
Doctrine has no secrets / irreducible local files → its own default is nothing
to copy, and the installer ships no .worktreeinclude (a root-file install
would clobber a consuming project's file). Provision tolerates its absence (copies
nothing, exit 0).
A project that needs to carry gitignored local files into forks may adopt a
repo-root .worktreeinclude: blank lines, # comments, literal repo-relative
paths, simple glob patterns (*, **, ?). No ! negation, no anchoring
(the parser rejects them). The coordination/runtime tier is withheld regardless.
Validate statically (smell test, NOT completeness): doctrine worktree check-allowlist.
Quick Reference
| Situation | Action |
|---|---|
--git-dir ≠ --git-common-dir, not a submodule |
Already forked → adopt, skip creation |
Submodule (modules/ gitdir / superproject) |
Not isolation → treat as not-forked |
| Dispatch coordination tree (per run) | doctrine worktree coordinate --slice <N> --dir <path> — create-or-resume, off resolved trunk; NOT a fork mode |
| Default fork (solo or dispatch worker) | doctrine worktree fork --base <B> --branch <name> --dir <path> [--worker] |
| Worker fork base | --base <B> explicit, never the implicit session HEAD (it is not B) |
| Worktree dir not ignored | Add to .gitignore + commit before fork |
Sandbox denies fork, mode=solo |
Work-in-place (no fork) |
Sandbox denies fork, mode=worker |
Abort (isolation mandatory) |
Adopted a fork that skipped fork |
doctrine worktree provision <fork> from the source root (sole copier) |
| Tree dirty / untracked-non-ignored | Abort commit-before-spawn |
| Batch-commit boundary | doctrine worktree branch-point-check --base <B> → 1 = re-dispatch |
| Baseline red in fork | Fix in provisioning; never hand off |
worker start |
export DOCTRINE_WORKER=1 (self-arm, fail-open) |
worker verify green |
Commit ONE non-merge S; return {fork_branch, head_sha_after} |
worker >1 commit / merge / rebased fork |
Contract violation — orchestrator rejects pre-import |
worker verify red |
Report; do not commit a red delta |
Red Flags
Never:
- Copy gitignored files into a fork by any path other than
fork/provision(the sole copiers — the exclusion guarantee depends on them). - Run a second copier after
fork, or runprovisionfrom inside the fork. - Imply
check-allowlistgreen means the allowlist is complete (it is a smell test). - Let a
workerdegrade to work-in-place. - In
workermode: write.doctrine/authored trees, skipexport DOCTRINE_WORKER=1, or land more than one non-merge commitS. - Fork from a dirty tree or hand off a red baseline.
- Fork a
workerfrom the implicit session HEAD instead of--base <B>— for a worker the session HEAD is notB, a divergent base that breaksS.parent == B. - Author or edit this skill in
.doctrine/skills/(the gitignored install copy); the source of truth is here underplugins/.
Always:
- Detect before creating; adapt to existing isolation.
- Prefer the
forkverb; treat the harness-native rung as opportunistic. - Run all three guards; verify a green baseline before handoff.
- Record durable memory on trunk, not inside the fork branch.
Outcome
Report the outputs: fork_path, branch, head_sha, the provision report
(copied / withheld files), and the baseline result. The fork branch is the
deliverable handed back to the caller.