worktree-isolation
Isolated development using git worktrees so the main branch stays clean. Use when starting any non-trivial task (execute state), doing parallel work on multiple tasks, making risky changes you might want to discard, or delegating to a sub-agent (automatic via ai-hats agent --isolation).
- License 许可
- MIT
- 所属仓库
- muratovv/ai-hats
- 最近更新
- 2026-11-05
- Stars
- ★ 3
- Forks
- 1
01 前言 Front Matter
- name
- worktree-isolation
- ai_hats
- { "type": "skill", "schema_version": 1 }
- license
- MIT
- description
- Isolated development using git worktrees so the main branch stays clean. Use when starting any non-trivial task (execute state), doing parallel work on multiple tasks, making risky changes you might want to discard, or delegating to a sub-agent (automatic via ai-hats agent --isolation).
02 资源文件 8
- 脚本hooks/bypass_journal.py35 B
- 参考文档hooks/code_extensions.json985 B
- 脚本hooks/shell_walk.py31 B
- 参考文档hooks/test_runners.json35 B
- 脚本hooks/wt_entry_gate.py3.6 KB
- 脚本hooks/wt_gate.py8.9 KB
- 脚本hooks/wt_git_gate.py10.3 KB
- 脚本hooks/wt_interpreter_gate.py13.2 KB
03 SKILL.md 正文
Worktree Isolation
Isolated development using git worktrees. Each task gets its own working copy — main branch stays clean.
Worktree Python Environment & Interpreter Trap. The main venv's editable install points at the MAIN checkout, so any command run with that interpreter observes the main checkout — not only tests.
ai-hatsCLI invocations and compose smokes count, and so does anything else you would call verification.
- Run everything through the worktree's own interpreter:
./.venv/bin/python -m pytest …, where a project provisions one per worktree. If it is missing, mint it:uv venv .venv && VIRTUAL_ENV=.venv uv pip install -e .(plus any workspace sub-packages-e packages/...).- Verifying a library-DATA change is the dangerous case. For a code change the session tripwire (
tests/conftest.py) refuses the run, so the trap is loud. ForSKILL.md/ trait / roleconfig.yamlthere is no tripwire: a compose smoke run against the main venv succeeds, exits 0, and validates a tree your change never touched. Validate the worktree file directly, or put the worktree venv on PATH.
Workflow
-
Start task → create worktree:
ai-hats wt create feat/PROJ-004 cd <worktree-path>Claude surface only — enter with
cd, never theEnterWorktreetool. It builds a rival worktree outside ai-hats (no state, no locks, no venv, wrong branch name), or raises an approval prompt nothing can suppress. Thewt_entry_gate.pyPreToolUse hook denies it and repeats this recipe. No other surface has the tool — skip this paragraph there.Claude surface only — delegate with
ai-hats agent --isolation, never the Agent tool'sisolation: "worktree". A subagent that writes leaves its worktree on disk on aworktree-agent-<hex>branch — registered in git, invisible toai-hats wt list(measured). A read-only subagent needs no isolation at all. No other surface has the parameter — skip this paragraph there.A PreToolUse gate (
hooks/wt_gate.py) hard-denies a code/config Edit/Write in the main checkout — interactive and headless (the old nudge was ignored). On a deny, move into a worktree and re-apply:ai-hats wt statusfor an active one, elseai-hats wt create <type>/<name>frommaster. Don't ask to skip a worktree (making one is one command); ask the supervisor only for a genuine direct-master change. Bypass is supervisor-only (AI_HATS_WT_GATE_OFF=1, never agent-set). Docs, non-trigger extensions, and gitignored paths (tracker,ai-hats.yaml) are exempt (trigger set:hooks/code_extensions.json).A second gate (
hooks/wt_git_gate.py) does the same for git state: a command that discards uncommitted work or moves a branch ref is denied where it would act on the main checkout. A leadingcdand agit -Care read first, so the same command aimed at a worktree runs, and the refusal names the recipe. Bypass is supervisor-only (AI_HATS_WT_GIT_OFF=1, never agent-set). -
Work — commit freely in the worktree. Main tree is untouched.
-
Finish → hand off for review; the merge is review-gated. In an interactive session, the external consent wrapper turns the canonical
rack transition … doneor directai-hats wt mergecommand into a supervisor question. The agent never exports an acknowledgement flag and never grants itself consent. The canonical close:cd <project-dir> rack transition <id> review # then STOP — supervisor reviews the diff # after review passed, the reviewer drives the consent-gated edge; # its one authorization also covers the nested worktree merge. rack transition <id> doneThe supervisor may instead run a direct merge; it is a separate top-level consent operation and therefore gets its own question.
The merge happens only on the review→done road. The review diff is computed against the merge-base with the base branch, so a
--no-ffmerge of the task branch into that base before review empties the diff — merge-base becomes the branch tip.- A raw
git mergeof atask/<prefix>-<n>ref is hard-denied at PreToolUse when the checkout that merge runs in ismasterormain.git -Cand a leadingcdcount. The refusal namesai-hats wt merge. Merging one task branch into another is allowed. ai-hats wt mergerefuses while the card is beforereview. A card cannot enterreviewwhen its tip is already a non-first parent of a merge on the base recorded at worktree create (--no-ff, the roadwt mergetakes). A branch that never carried a commit still enters review.
These two do not catch a command that hides the branch name in a variable, a cherry-pick, a fast-forward that already happened outside the guard, or a human merging from their own terminal.
If
wt mergerefuses withRefused (drift), the base holds commits your branch never took in (another agent's worktree merged, ororigin/<base>received commits). Re-verify your changes against the new base (re-run grep-verify, re-check moved/renamed paths), then rebase in the worktree —git rebase <base>clears the guard, no flag needed, sorack transition <id> doneworks again.--accept-driftis for the other case: merging a stale baseline you accept knowingly. Do not pass--forcefor drift —--forceonly bypasses uncommitted changes; drift has its own override. Neither--forcenor--accept-driftbypasses the session wrapper. - A raw
-
Abandon → discard:
cd <project-dir> ai-hats wt discard
Pre-merge checklist (long-lived tasks)
For tasks that span multiple sessions OR multiple review rounds,
the base branch (usually master) can drift forward while you work
— other agents' worktrees merge in parallel, or origin/<base>
receives commits. rack transition <id> done will then
fail with WorktreeDriftError and you'll need an ad-hoc recovery.
Rebase before closing so drift never accumulates past your verification window. Do the rebase IN the worktree, but run the close from the MAIN checkout:
# Rebase the task branch in the worktree (git work is fine here):
cd <worktree-path>
git fetch --all
git rebase <base-branch> # usually master
# resolve any conflicts; re-run the affected tests
# Close from the MAIN checkout — NOT from inside the worktree. A
# worktree-backed `transition done` issued from inside its own worktree
# is refused: the merge runs `git worktree remove` on the cwd
# you are standing in, which would orphan your shell and desync the tracker.
cd <project-dir>
rack transition <id> done # ack-free once the branch is merged; do NOT `git merge` by hand
The merge itself is review-gated (step 3 above) — never git merge <task-branch>
yourself (a manual pre-merge collides with the FSM merge, HYP-023). If the close
still reports drift, you skipped the rebase above — do it, then re-run the
transition. Only a baseline you knowingly leave stale needs
ai-hats wt merge --accept-drift from the main repo.
Skip this for short tasks (< ~2 hours wall-clock, no review rounds). Drift typically only matters on multi-session work; for a 30-minute task the base hasn't moved.
If you skipped the checklist and hit WorktreeDriftError anyway,
the recovery is the same rebase — see the drift block in step 3 of
the Workflow above.
Commands
| Command | What it does |
|---|---|
ai-hats wt create <branch> |
Create worktree on new branch |
ai-hats wt merge |
Squash-merge changes back, clean up |
ai-hats wt discard |
Delete worktree and branch |
ai-hats wt list |
Show all worktrees |
ai-hats wt status |
Show active worktree |
ai-hats wt exec -- <cmd> |
Run command in worktree, where you stand (+ PYTHONPATH) |
ai-hats wt env [<branch>] |
Print export WT=... PYTHONPATH=... for eval |
Teardown runs lifecycle hooks
ai-hats wt merge / wt discard / cleanup run any composed wt_out
lifecycle hooks before removing the worktree — e.g. the hunk-review-comments
skill drains its review sidecar so notes aren't lost. They are fail-closed: if a
hook fails, the teardown aborts and the worktree + branch are preserved (nothing
gitignored is destroyed). Force past a genuinely broken hook with
ai-hats wt … --skip-hooks only if you accept the loss. (wt create likewise runs
wt_in hooks to seed gitignored data — e.g. .env — into the fresh worktree.) These
are component-declared hooks, distinct from the FSM merge-locks/drift guards; to
author one see docs/how-to-extend.md → "Worktree lifecycle hooks".
Running Commands in Worktree
Use wt exec instead of manual WT=/PYTHONPATH= boilerplate. It is an
environment wrapper, not a teleporter: it runs your command where you stand
when your cwd is inside the worktree, and at the worktree root otherwise.
It swaps the import path, not the interpreter. Measured:
ai-hats wt exec task/hats-1 -- python -c 'import sys, ai_hats; print(sys.executable, ai_hats.__file__)'
# sys.executable -> <MAIN checkout>/.venv/bin/python
# ai_hats.__file__ -> <worktree>/src/ai_hats/... (via PYTHONPATH)
That is enough for in-process imports and nothing else. Any subprocess that does
not inherit PYTHONPATH, and any ai-hats binary found on PATH, sees the MAIN
checkout. So for pytest — and for anything that spawns subprocesses — use the
worktree's own interpreter; the cost of not doing so is not a quiet false green
but a loud false red, a wall of Test suite would test the WRONG checkout
tripwire errors about a mismatch you did not cause.
A PreToolUse gate (hooks/wt_interpreter_gate.py) nudges — never blocks — when it
can prove the interpreter resolves outside the worktree; kill switch
AI_HATS_WT_INTERP_OFF=1.
# CORRECT — pytest and anything spawning subprocesses, from inside the worktree:
./.venv/bin/python -m pytest tests/test_foo.py -xvs
# CORRECT — wt exec for in-process, single-shot commands:
ai-hats wt exec -- ruff check src/
ai-hats wt exec -- python -c 'import ai_hats; print(ai_hats.__file__)'
# WRONG — hand-rolled paths, and one dead permission grant per worktree:
WT=/var/folders/.../ai-hats-wt-...
PYTHONPATH=$WT/src python -m pytest tests/test_foo.py -xvs
Subprojects. A worktree may hold a subproject with its own pyproject.toml
and venv. cd into it and carry on, or name it with -C from outside — never
cd to an absolute worktree path:
cd packages/ai-hats-wt && ../../.venv/bin/python -m pytest # from inside the worktree
ai-hats wt exec task/hats-1 -C packages/ai-hats-wt -- ruff check . # from the main checkout
Reaching a subproject's tests from the main checkout means standing in the
worktree first — -C moves the cwd, not the interpreter.
PYTHONPATH follows the project that owns the directory you run in: the
nearest ancestor with a pyproject.toml, bounded by the worktree root. So a
subproject gets its own src, and a plain subdirectory keeps the worktree-root
workspace.
Pass -- before any command that has its own -C (e.g. make -C).
Reaching another worktree. A leading token naming an active branch is a
selector and always beats cwd, so this works from inside a different worktree
too. Without a selector the worktree comes from cwd, else the sole
active one; with several active and no selector, wt exec refuses and lists them.
For interactive shell work (rare) — bare, or named to reach another worktree:
eval "$(ai-hats wt env)"
eval "$(ai-hats wt env task/hats-1)"
cd $WT
When to Use
The cost (a separate working copy) is justified by parallelism, risky-discard, or sub-agent isolation — not by every edit. Skip it for read-only exploration or a trivial single-file fix on a throwaway branch. The git operations inside the worktree (rebase, conflict resolution, commit conventions) are git-mastery's remit; this skill owns the worktree's create → merge → discard lifecycle.
Conventions
- Branch naming:
type/TICKET-ID(e.g.,feat/PROJ-004,fix/PROJ-012) - One active worktree at a time (per project)
- Always
cdback to project dir before merge/discard - Commit your work in the worktree before merging
Anti-Patterns
- Working directly on main branch for non-trivial changes — use a worktree
- Forgetting to
cdback to project dir before merge/discard — commands fail silently - Multiple active worktrees without tracking — leads to forgotten branches
- Running
ai-hats wt create/wt merge/wt discard/rack transition <id> done|failed|cancelledfrom inside a linked worktree — all blocked. The teardown commands rungit worktree removeon the very cwd you are standing in, orphaning your shell so every laterai-hatsmis-resolves the tracker. Alwayscdback to the main repo first; useai-hats wt exec/ai-hats wt envto act on a worktree without leaving it. - Mixing manual
wt createwithrack transition <id> executefrom the main repo — if you created a worktree manually and want the task to use it,cdinto the worktree first, then transition. Otherwise the transition errors out with a clear remediation message. - Invoking
ai-hats wt create(orrack transition <ID> execute) while the main repo's HEAD is not on the worktree merge target — blocked with a "Refused: … not the worktree merge target" error. The worktree inherits its merge target from the current branch, so creating from the wrong branch causeswt mergeto silently land on it. The target ismaster/mainby default, or a configuredworktree.merge_target(see the fork-workflow note below). Recovery: the error names the branch —git checkout <that branch>in the main repo, then retry. - Working without committing inside a worktree — uncommitted work in a worktree is NOT protected. The worktree is a filesystem directory that parallel sessions, cleanup hooks, or
git worktree remove --forcecan destroy without warning, and there is no recovery for uncommitted changes. Commit at every meaningful checkpoint (every passing test run, every completed sub-task). If a step could be reverted withgit checkout HEAD -- ., you've waited too long to commit. - Finishing the worktree cycle with raw git — running
git merge --no-ff <task-branch>,git worktree remove, or a manualgit pushto the base branch instead ofai-hats wt merge/rack transition <id> done. The CLI wrappers run the FSM lifecycle hooks — per-branch + base-branch merge-locks, drift-check, stale-lock recovery, state cleanup. Raw git skips every one of them and re-opens the race/drift bugs those epics closed; a manual merge before FSM-donealso produces a double-merge conflict that then needs--force(HYP-023). Scope: this targets the lifecycle transitions only (merge-to-base + cleanup + done). Raw git for inspection (git status/log/diff,git worktree list) and for in-worktree conflict resolution during a rebase stays fine.
Specific base or target branch
Fork/dogfood repos can cut worktrees from one branch and merge them into another
(base ≠ merge-target) via the worktree block in ai-hats.yaml. Rarely needed;
full contract in docs/how-to-configure.md, section "The worktree block"
— for a project whose dev trunk is not its upstream default branch.
If You End Up With a Stray Worktree
git worktree list # audit
git worktree remove <path> # remove a stray linked worktree
git worktree prune # clean stale metadata
rm <ai_hats_dir>/sessions/worktree.json # if ai-hats state is stale
Rule of thumb: one task, one worktree, one <ai_hats_dir>/sessions/worktree.json (in the main repo).
Shipped on Master (Retrospective Close)
Work shipped on the base out-of-band? From brainstorm/plan, fast-close with a
forced terminal transition: rack transition <id> --state done --force --reason "…".
From execute/document/review: rack transition <id> done finalizes an
already-merged branch even if the worktree/state is gone.
04 同仓库更多 Skills
tool-evaluation-protocol
Time-bounded protocol for evaluating new tools, libraries, or methodologies before adoption. Use when evaluating a candidate dependency for the project, researching a methodology (e.g. ML/LLM frameworks, testing approaches), or following up on a hype-driven recommendation.
systematic-debugging
4-phase bug-fix protocol (evidence, pattern, hypothesis, verify). Use when investigating any error, failure, or unexpected behavior, handling bug reports from users or monitoring, or diagnosing test failures with non-obvious causes.
skill-template
Canonical template and validation guide for ai-hats skills. Use when creating a new skill, reviewing an existing one for structural compliance, or deciding which pattern fits a new behavior.
skill-optimization
Audit and refactor library components to eliminate redundancy and staleness. Use during periodic library maintenance, after adding multiple new skills or rules, when a role's token budget is too high, or after retrospectives identify component issues.
silent-fallback-gate
PreToolUse hook — refuses an Edit or Write that adds an inert broad except. Use when a Python edit was blocked for a silent fallback.
retro-to-framework
Convert an observation about agent behavior — from a retrospective or from a single incident — into a framework-level change (hook, rule, skill, skill update). Use after a retrospective identifies problems that are not project-specific, after an incident where an agent misapplied a component it had read, when CLAUDE.md band-aids accumulate (more than 3 per-project rules that could be generic), or when the same problem recurs across multiple projects.