
Worktree
- 77 installs
- 1 repo stars
- Updated August 3, 2026
- davidlee/doctrine
Isolates work on its own git branch by forking a worktree via doctrine worktree fork, running commit-before-spawn and baseline guards before handing back the fork branch.
About
Creates an isolated git worktree fork so a phase can run on its own branch without touching the main working tree. A developer uses it when execute or dispatch flows need workspace isolation before spawning work.
- Detects existing isolation and creates a fork via doctrine worktree fork
- Runs commit-before-spawn, branch-point, and baseline guards before handing back the branch
Worktree by the numbers
- 77 all-time installs (skills.sh)
- Ranked #255 of 733 Git & Pull Requests skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidlee/doctrine --skill worktreeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 77 |
|---|---|
| repo stars | ★ 1 |
| Last updated | August 3, 2026 |
| Repository | davidlee/doctrine ↗ |
What it does
Isolates work on its own git branch by forking a worktree via doctrine worktree fork, running commit-before-spawn and baseline guards before handing back the fork branch.
Files
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, optionally stamps the worker marker, and emits the per-worktree env contract — all with compensating rollback), 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; /dispatch-subprocess invokes it for codex/pi workers; the /dispatch router invokes the coordination path (worktree coordinate, below) for the per-run dispatch/<slice> tree. (The claude worker arm, /dispatch-agent, does not use fork — Claude default-creates the worktree and a SubagentStart hook stamps it; see that skill.) 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 for `solo`- requested branch / path
base— the ref the fork is created from. **solodefaults toHEAD;
worker REQUIRES an explicit base (the orchestrator's coordination HEAD `B`), because the session HEAD is not** B — the orchestrator drives from the coordination branch while the session repo may sit on main. Forking from the implicit session HEAD imports a divergent base and corrupts the B..S delta.
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 not B and breaks S.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:
- Markerless. The coordination tree is the orchestrator (worker-mode OFF),
so — unlike fork --worker — it stamps no worker marker. 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 /handover picks 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, no import, no marker. 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:
1. git worktree add -b <branch> <dir> <base> — refuses if <dir>/<branch> exist or <base> is not a commit. Pass <base> explicitly: HEAD for solo, B for a worker — the explicit base is why this path is controlled. 2. provisions the fork (the sole copier — coordination/runtime tier withheld at the copy seam, D9; see Provisioning). 3. with --worker, stamps the worker marker before any spawn window (solo omits --worker). 4. emits the per-worktree env contract on stdout (KEY=value per line; human status to stderr) — the project declares its per-wt env (doctrine-the-repo declares CARGO_TARGET_DIR, §8); generalisable, not a framework primitive.
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: 1. Existing isolation (detection above) → skip creation, adopt the fork. 2. Harness-native creation if present and invocable creation-only (no auto-copy) — opportunistic only. (Claude Code's WorktreeCreate hook is the instance, but its payload lacks base/path/type so doctrine cannot drive it — the claude arm instead default-creates + SubagentStart-stamps, see /dispatch-agent.) Design around it; never depend on it. 3. `doctrine worktree fork` — the blessed tested default. Prefer this. 4. Work-in-place (solo only, allow_work_in_place=true) on sandbox denial — no fork, the trunk path. worker aborts instead.
Worktree directory selection
Pick the fork directory before calling fork, in priority order (superpowers:using-git-worktrees):
1. Reuse an existing dir — .worktrees wins over worktrees if both exist. 2. Check CLAUDE.md for a stated preference — use it without asking. 3. 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-allowlist only, and the project must keep .worktreeinclude precise. This is why the fork verb (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-spawn B, 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: just check). The command is project-provided — never a hardcoded cargo …. An unbuildable fork is fixed in provisioning, never handed off.
Worker mode (the funnel half)
mode=worker is the path a codex/pi worker runs inside its fork --worker worktree (claude workers are stamped by the SubagentStart hook instead — see /dispatch-agent). 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).
Self-arm first (D2a, fail-open — C-I). The worker's first act is export DOCTRINE_WORKER=1. This arms the doctrine guard so any doctrine-mediated authored write (slice, memory record, backlog, minting) refuses. Nothing enforces the line — the Agent tool exposes no env seam, so it is a self-armed prompt contract that fails open if omitted (the orchestrator's import-time .doctrine//.claude/-reject belt, not this var, is the real protection). Arm it regardless.
The constrained loop:
1. 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). 2. Verify. Run the orchestrator-supplied verify command (passed in the worker prompt — not assumed just check). A red verify is reported back; the worker does not commit a red delta. 3. Commit exactly one `S`. git add -A && git commit — raw git, not a doctrine verb (so DOCTRINE_WORKER=1 does not refuse it, D2a). S is the importable unit (the orchestrator imports B..S): exactly one non-merge commit on top of B — no multi-commit, merge, or rebase that re-parents off B (each is a contract violation rejected before import). 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, plus { fork_branch, head_sha_after }. 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> — markerless, create-or-resume, off resolved trunk; NOT a fork mode |
| Default fork (solo or codex/pi 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, skip `export
DOCTRINE_WORKER=1, or land more than one non-merge commit S`.
- 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 not B, a divergent base that breaks S.parent == B.
- Author or edit this skill in
.doctrine/skills/(the gitignored install copy);
the source of truth is here under plugins/.
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.
<!-- Attribution: see NOTICE.md (adapted from superpowers:using-git-worktrees, MIT). -->
NOTICE — /worktree skill attribution
The directory-selection and safety-verification patterns in SKILL.md (the existing-directory priority, the CLAUDE.md preference check, the git check-ignore safety gate, and the "fix broken things immediately" framing) are adapted from `superpowers:using-git-worktrees` by Jesse Vincent (<https://github.com/obra/superpowers>), licensed MIT.
Doctrine replaces that skill's project-setup auto-detect with doctrine worktree provision (the sole copier, coordination-tier exclusion at the copy seam) and adds the detection step, the creation backend ladder, the mode = solo | worker contract, and the commit-before-spawn / branch-point / baseline guards.
MIT License — Copyright (c) 2025 Jesse Vincent
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.