Many agents work one repo in parallel worktrees, so "who is on this ticket, and what is really verified" needs one answer every checkout can see. Each feature gets its own status file docs/status/<ID>.md, claimed on the remote before any worktree exists and edited only through one script.
status.pymeanspython3 <this skill>/scripts/status.py; thefeature-status-guardhook's deny messages name the exact path on this machine.- Every subcommand has
--help, and every writing one takes--dry-run. references/format.mdowns the file format, the tokens, and the legal state transitions.<ID>is the ticket id (WEST-123), or a kebab slug when there is no ticket;<int>is the integration branch fromworktree-flowsection 0.
0. Is the repo opted in?
git fetch origin --prune
git ls-tree -d --name-only origin/<int> docs/status
git show origin/<int>:AGENTS.md 2>/dev/null | grep -c feature-status- The first prints
docs/status, or the second prints a number above 0: the repo is opted in, and thefeature-status-guardhook enforces the steps below. - Neither: this skill does not apply; follow
worktree-flowalone, and offer the opt-in fromproject-init.
1. Pick before any worktree exists
Who picks:
- Ticket work always picks, with the ticket id as
<ID>. - A no-ticket feature that other agents could collide on picks too, with a kebab slug as
<ID>. - A small no-ticket change (a typo, a one-file fix) may skip
pickand followworktree-flowalone on a<type>/<slug>branch. The hook judges a slug branch only once aclaim/<ID>exists for it. - The hook recognizes only
ABC-123ticket ids (hooks/README.md); ticket work under any other id format (west-12, a numeric issue) still picks, by rule rather than by hook.
Run from the checkout you stand in, usually the main checkout; pick never touches its working tree or index:
status.py pick <ID> --title "<title>" --type <feat|fix|refactor|chore|docs|test> --slug <slug>- It claims the ID on the remote (
claim/<ID>and the feature branch, pushed atomically, so every other checkout sees it at once), creates the worktree, and prints its path as the last line;cdthere. - It replaces
worktree-flowsection 1 steps 1-3, existing-work checks included; on--resumestill read the worktree's progress file (worktree-flowsection 3). - Refused because someone else holds it: the message names who; stop and ask, never pick a second branch for the same ID.
- Refused because the claim names your git identity: parallel agents share one identity, so that alone does not make it yours. Rerun with
--resume: it prints the existing worktree path, recreates it when missing locally, and refuses when that worktree changed in the last 30 minutes, naming who holds it and where. On that refusal, ask the user whether the other session is still running; add--takeoveronly after the user confirms it is dead. - Refused because the claim is stale (its pick is
merged,droppedordeferred): run thestatus.py release <ID>the message names, then pick again; never--resumea stale claim. - A harness already started you in a clean detached worktree: add
--no-worktree, thengit switch -c <branch> --track origin/<branch>in place.
2. Update as you go, inside the feature worktree
| When | Command | ||
|---|---|---|---|
| Work starts (exploration done, at the latest with the first commit) | status.py set <ID> in-progress | ||
| ACs are known | `status.py ac <ID> AC<n> --text "<AC text>" --tier <ut\ | it\ | e2e> --result todo, once per tier the AC needs, and --result na` for tiers it does not |
| A tier of an AC passes or fails | `status.py ac <ID> AC<n> --tier <tier> --result <pass\ | fail>` | |
| An E2E pass | status.py ac <ID> AC<n> --tier e2e --result pass --evidence <e2e-verify JSON report>; only that report is accepted (references/format.md), and no flag skips it | ||
| The spec changes under verified ACs (before the merge) | status.py stale <ID> --acs AC1,AC3 --note "<what changed, link>", then status.py ac <ID> AC3 --text "<new AC text>" for each reworded AC (this resets all its tiers to todo, so record each tier again, na included), add new ACs as above, and re-verify | ||
| Something is cut from scope | status.py defer <ID> --what "<what>" --why "<why>" --owner "<who>" | ||
| Blocked | status.py set <ID> blocked --note "<one line, link>", and open a draft PR/MR per worktree-flow | ||
| A fact worth keeping | status.py note <ID> "<one line, link>"; longer reasoning goes in the PR/MR body or a flow-trace file |
- Commit each status change with the code it describes, so the file on the branch is always true for that commit.
set,ac,stale,defer, andnoterefuse to run in the main checkout.
3. PR/MR, merge, release
The merged-state rule: the PR/MR opens in in-review, and its last push before merging sets merged, so the file on <int> is true the moment it lands, and a PR/MR closed unmerged never reaches <int>.
- Before a non-draft PR/MR, in this order:
status.py set <ID> in-review,status.py check <ID>, commit, push,status.py check <ID> --ref HEAD, then open it. The last check reads the committed file, so it fails when the state change or an E2E evidence file is not committed; the hook runs it too, and denies while the branch has unpushed commits. A draft PR/MR only needs the file to exist. - When the user approves the merge (or asks you to merge):
status.py set <ID> merged --pr <url>, commit, push, and only then merge.checkrefusesmergedwhile any AC tier is todo, fail, or stale, or an E2E pass lacks its evidence; fix those, do not argue with it. - After the merge:
status.py release <ID>deletes the remote claim. It judges only that pick's own file:mergedmust be onorigin/<int>(so it cannot run while the PR/MR is open), and an earlier pick's merged file never releases a re-pick. - Dropped:
status.py set <ID> dropped --note "<why>", commit, push,status.py release <ID>, then merge or close per the user. Deferring the whole feature: the same withset <ID> deferred --note "<why, link>", so anyone can pick it again later. The old worktree can no longer reopen it after the release; resuming means a newpick <ID> --slug <new-slug>, which keeps the deferred file's ACs, and merging the given-up branch in when its work is still needed. - Failed QA or a spec change after merge has one path:
status.py release <ID>(when the claim still stands), thenstatus.py pick <ID> --slug <new-slug>from the main checkout (it keeps the ACs and resets the state topicked), then in the new worktreestalethe affected ACs andset <ID> in-progress --note "<why, link>".staleandsetrefuse amergedfile, so the old worktree cannot reopen it;--slugis needed only while the merged branch still exists, andpicksays so.
Recovery, when the user merged the PR/MR while its file still said in-review (release and pick say so, and board --live shows in-review (PR merged)): in the feature worktree (recreate it with pick <ID> --resume while the claim stands), run git fetch origin && git merge origin/<int>, status.py set <ID> merged --pr <url>, commit, push, open a PR/MR, and release once it merges. Merging origin/<int> first makes the push a fast-forward, also after a squash merge.
4. Board
status.py board --live # --out <file> writes itIt merges docs/status/*.md on <int> with every claim/<ID> branch, so unmerged work shows up; --live adds PR/MR and deploy state when gh/glab and the environments version status are available. Print it or write it to a scratch file; never commit it, since parallel PRs/MRs would conflict on it.
Never
- Never edit
docs/status/*.mdby hand, with an edit tool or with shell (>,sed -i,tee,cp,ln,git checkout <ref> -- <file>,git restore --source); the hook denies the common forms on a best-effort basis, and the script is the only way edits stay surgical and validated. - Never create a branch or worktree for ticket work, or for an ID that has a claim, before
pick; the hook denies it, and also denies a second branch for a claimed ID. "It is a tiny change" is not an exception for a ticket: a tiny change still collides with another agent on the same ticket. - Never record a pass you did not run, and never copy Jira or deploy state into the file; the board derives deploy state.
- Never leave a
claim/<ID>behind aftermerged,dropped, ordeferred;pickrefuses everyone while it exists.
Report
The status file's state, AC table, and deferred items; the PR/MR URL; and whether the claim was released.