# Three ways of working, and how to use each The working directory decides which one is in effect. Nothing infers intent, so standing in the wrong place is not a mistake that gets corrected later — it is a different mode, with different rules about where writes may land. | Standing in | Mode | The work is | | --- | --- | --- | | A worktree under `/.worktrees/` | Track | Writing code for one unit of work | | A primary checkout that declares `tracks.json` | Coordinator | Starting, reviewing, and landing tracks | | The same, with `specWatch` declared and a schedule installed | Spec-driven coordinator | Approving what a tick produced | `hooks/session-place.sh` states the mode at session start, and `statusline.sh` keeps it on screen. A repo that declares no tracks and holds no worktrees gets neither, deliberately. Mechanism and rationale live in `tracks.md`; worktree layout in `worktrees.md`. This page is how to use it. ## Mode 1 — working a track One unit of work, its own worktree, its own branch, lasting as many sessions as it takes. **Start it** from the repo's primary checkout: ``` /track ``` That creates `/.worktrees/` on `track/`, runs the declared `install` in it, writes the worktree's `handoff.md`, and starts the first writer there. The coordinator remains in the primary checkout; the writer reaches the track by its worktree path. **If a person is taking over or watching it, give it a shell of its own**, and use that shell for every session on this track: ``` tmux new-window -n -c /.worktrees/ claude ``` This is the form to prefer. `EnterWorktree` moves the session but not the shell that launched it, so a track entered that way lasts exactly one session: the next `exit` and relaunch lands back in the primary checkout. A shell standing in the worktree makes every session in that window a track session from its first token. **Each session after the first** starts with `/pickup`, with no argument. The handoff at the root of the tree is the source of truth for where the work stands. **End a session** with `/handoff`, which rewrites that file for whoever picks it up next. Ending a session is not ending the track; the tree and branch persist. **End the track** from the primary checkout with `/land `, which verifies, rebases, merges, removes the worktree, and deletes the branch. ## Mode 2 — coordinating by hand A session in a primary checkout that declares `tracks.json` is a coordinator. Interactively, it may edit project files in that checkout directly: `trackRepoRootGuard()` in chook binds only while `CHOOK_UNATTENDED` is set, which a person's own session never is. A subagent it spawns is fenced regardless, by `primaryCheckoutGuard()`, so work that needs a writing subagent still gets a track. Root edits get committed promptly, since a dirty primary checkout blocks `/land` and a spec-driven tick's own roster commit. - `/tracks` — where every track actually stands. Read-only, and derived fresh from git rather than from the coordinator handoff, which records decisions and next steps and drifts on anything git already tracks. - `/track ` — start one. - `/land ` — land one. Serialized: one at a time. - `/pickup ` — hand a track to a `track-worker` agent; the coordinator stays in the primary checkout. It moves the session into the worktree only when the person explicitly asks for that, and the move prompts in a way no rule or hook can suppress; a track worked across many sessions wants its own shell instead. Run with no argument from the primary checkout, it acts on the coordinator handoff's own Next steps before it scans tracks. `/handoff.md` is the coordinator's own handoff — the same Resume Here structure as a track's (Goal, Current state, Next steps, Key files & symbols, How to verify), plus decisions made interactively at this level. It says why each track exists and in what order they land, one line per track plus the cross-track intent no per-track handoff can hold. It is maintained by hand at both ends for that track list — `/track` adds a line, `/land` removes one — so `/tracks` is what catches the drift on anything git-derived. ## Mode 3 — a coordinator driven by specs The same primary checkout, with the work described in files instead of typed in. The schedule drives every tick, which reads every spec, moves each track one step, and stops short of merging. **Merging is a person's decision** unless the repo has declared a landing script (below); a tick's most advanced outcome is a track sitting at `awaiting-approval` with a line in the roster saying so. **The schedule drives the repo, not a session.** `bin/spec-watch-schedule.sh install ` installs a cron entry (a LaunchAgent on macOS) that runs `bin/spec-watch-tick.sh ` every `specWatch.intervalSeconds`. No interactive session drives ticks, so opening, closing, or restarting a session in the repo never starts or stops that work — writers (`claude --bg` sessions and lane writers) were already independent of any session, and now so is the tick itself. `bin/spec-watch-tick.sh` is the only way a tick runs, scheduled or by hand: it holds a per-repo flock, exports `SPEC_WATCH_TICK_LOCK`, and writes a pid file at `~/.local/state/spec-watch/.tick.pid` for as long as it runs. `/spec-watch` (`commands/spec-watch.md`) refuses when `SPEC_WATCH_TICK_LOCK` is unset and points at `spec-watch-tick.sh`, so a tick typed into a session can never race the scheduled one. `hooks/session-place.sh` tells a session standing in a spec-driven repo's primary checkout that the schedule drives it, not to run `/spec-watch` or any loop or wait, and that the status line shows each track's stage. **The status line shows what the schedule is doing.** `statusline.sh` reads each due check's saved stage for a repo's tracks and shows one beside each track's line, and shows "tick running" beside the repo's own line while a tick's pid file names a live process. That is how a session standing in the repo sees what is running for it, since nothing in the session itself drives or watches a tick. **No model for a snapshot already ticked.** Each tick action changes the spec snapshot, so a tick on an unchanged one repeats work already done. `bin/spec-watch-due.mjs` fingerprints every spec's derived state, without a model, and reports a tick due only when that changed since the last due tick, or an hour has passed so a tick that died is retried. `bin/spec-watch-tick.sh` runs it first and starts no model when nothing is due, whether the tick was scheduled or run by hand. Every due check also saves each track's stage — spec state, writer state, lane index — to `~/.local/state/spec-watch/stages/.json`, which is what the status line reads. `docs/spec-watch-token-cost.md` (on `track/token-cost-design` until landed) has the measurements behind the due check. ### Declare it Add `specWatch` to the repo's `tracks.json`: ```json { "worktrees": ".worktrees", "base": "main", "verify": "node verify.mjs", "specWatch": { "specs": "specs", "maxLive": 3, "maxRounds": 1, "writerModel": "claude-sonnet-5" } } ``` - `specs` — directory of spec files, relative to the repo root. Writable at the root even though the rest of the checkout is not, since specs are how work is commissioned. - `maxLive` — how many tracks may exist at once. Backpressure: with no slots free, a tick starts nothing and says which specs are waiting. A parked track counts, so parking is a way to run out of room — and the fix is never to raise this number, because a parked track is waiting on a person editing a spec and starting work around it buries the thing that needs doing. - `maxRounds` — failed reviews allowed before a track parks. - `writerModel` — the model a writer runs on when its spec names none. Optional; defaults to `claude-sonnet-5`. The tick and its reviewers always run on the model `spec-watch-tick.sh` sets, whatever this says. - `landScript` — optional; path, relative to the repo root, to an executable that lands one approved track. Absent, a tick never merges and every track waits at `awaiting-approval` for `/land`. See "Let a tick land tracks". ### Write a spec One file per unit of work, front matter plus prose: ```markdown --- enabled: true touches: [src/greet.mjs, verify.mjs] depends-on: [greeting] --- # Shouting Add a `shout` function to `src/greet.mjs` that upper-cases a greeting. - `shout(text)` returns `text` upper-cased with a trailing `!`. - `verify.mjs` gains an assertion covering it. ``` - `enabled: false` takes a spec out of consideration without deleting it. - `model` — the model this spec's writer runs on, passed verbatim to `claude --model` (`opus`, `claude-opus-5`, …). Omitted, it is the repo's `writerModel`. Name a stronger model for work that needs judgment rather than raising the default, so cheap work stays cheap. - `touches` — the files the work is expected to land in. Anything committed outside these globs is derived from the diff and reported at the approval gate; it is not fatal, but it is never dropped for brevity, because a file beyond the list is the work having widened its own brief. - `depends-on` — spec names that must have landed first. The gate reopens when a dependency lands and closes again if that dependency's spec is later edited. Write the requirements so they can all hold at once. A spec that contradicts itself does not reliably park — it converges on whichever reading the first review settled, and the resulting code encodes a reviewer's preference rather than a decision. `tracks.md` has both failure modes in full. Editing a spec that already landed makes it `stale`, and the next tick starts fresh work for the difference. ### Run a tick unattended `bin/spec-watch-tick.sh ` runs one tick with nobody watching, whether that is by hand or from the installed schedule. Nothing is pushed at you. A tick has no terminal, so it leaves its message in two files that exist anyway: each writer states its outcome in its own `handoff.md` under a section headed `**Result**`, and the tick rewrites a `## Waiting on a decision` section in the roster at `/handoff.md` naming every track that needs you and pointing at its worktree. That section is rewritten whole every tick, so it is never stale and never keeps yesterday's entries — and when nothing is waiting it says so in one line. `bin/spec-watch-tick.sh` sets what an interactive shell already has and a bare invocation would not: `PATH` including the mise shim directory, since node is version-managed and reaches `PATH` only through a login shell; `--add-dir` for `$HOME/.claude/bin`, since a session's sandbox otherwise covers only its working directory and cannot reach the status script; and `CHOOK_UNATTENDED=1`, which turns every approval into a hard refusal so a writer with no terminal refuses rather than stalling. Output appends to `$HOME/.local/state/spec-watch/spec-watch.log`. To run one tick now, run `bin/spec-watch-tick.sh `; its log entry lands in the same file. `/spec-watch` typed into a session refuses, because only the script holds the repo's tick lock. ### What a tick does It reads `bin/spec-status.mjs`, derives every spec's state from git and the filesystem, and moves each at most one step per tick: - **unstarted** — creates a worktree and launches a detached writer, if a slot is free. - **implementing** — nothing. A writer goes idle rather than exiting when its turn ends, so liveness is read from session state, never from process death. - **needs-review** — rebases if behind, runs the gate, spawns a read-only reviewer, records a verdict pinned to the branch tip. On a failure it appends the findings to the handoff and relaunches, until the round limit. - **awaiting-approval** — nothing, unless the repo declares `landScript`; see the next section. - **landed**, **disabled**, **blocked**, **parked** — nothing at all. No re-review, no re-tidying. It aborts without changing anything if the primary checkout is dirty, if the declared `verify` does not run, or if it cannot read the status — and says so in the roster, because a loop that refuses identically on every beat otherwise looks exactly like one with nothing to do. ### Let a tick land tracks Without this, approved tracks keep their slots until a person runs `/land`, and any spec that `depends-on` one stays `blocked` until then. A repo that wants unattended feature work to keep moving declares `specWatch.landScript` in `tracks.json`. Nothing else changes for a repo that does not. The tick runs the script once per track sitting at `awaiting-approval`, passing the spec name, the spec's sha and the branch tip — the exact pair the passing review was recorded for — and stops after the first one that lands, so tracks land one at a time. The script refuses when the spec on the base branch or the branch tip no longer matches that pair, so nothing lands that the review did not see, and it refuses a call that passes only the name. The script decides everything else: what has to be true before a track may land, the rebase, the merge, the `spec//` tag, and removing the worktree. The tick does not repeat any of it. The script prints one line and exits with a status the tick acts on: | Exit | Meaning | The tick | | --- | --- | --- | | 0 | landed | reports the line; a spec depending on it unblocks on the next tick, since the tag now exists | | 1 | not landed; the line names the condition | leaves the track at `awaiting-approval` and reports the line | | 2 | the branch is behind base and rebasing conflicts; the line lists the paths | resolves a `handoff.md`-only conflict itself, and the rebased branch goes back through review before the script runs again; relaunches the writer for any other path | **A conflict in `handoff.md` alone is not a conflict.** `handoff.md` is gitignored and untracked in every repository that uses tracks: the roster and each track's handoff are working files git never carries, because a tracked copy on any branch makes every fast-forward silently overwrite another worktree's ignored copy. A conflict on it can only come from a branch that still tracks the file, replayed onto a base that does not. The tick drops it from the branch's index (`git rm --cached handoff.md`, which keeps the working copy), continues the rebase, and repeats for each replayed commit that conflicts on it. No writer is relaunched for it, so a moving base does not spend the relaunch cap. **When a rebase conflicts in any other path**, the tick aborts the rebase so the branch is left as it was, and relaunches the track's writer through the same launch a dead writer gets, with a paragraph added to its brief: rebase onto the base branch, resolve the conflicting paths (the list never names `handoff.md` alone, and the brief says to untrack `handoff.md` if it conflicts) so that what landed there and what this spec asks for both survive, rerun the gate, commit. Each such relaunch counts against the same three-relaunch cap as any other. Past it the track is `parked`, with the conflicting paths recorded and named in the roster, and stays that way until a commit lands on the branch or the spec is amended. A conflict found while rebasing a track that is still being reviewed takes the same path. The script lives in the repo, so each repo sets its own bar. A generic one checks that the gate passes on the tip, that nothing changed outside the spec's `touches`, that the base is an ancestor of the branch (rebasing when it is not), and that every command in a `specWatch.landChecks` list exits 0, then lands the way `/land` does. Whatever a script checks, a clean rebase is still not a passing one, so it should run the gate after rebasing. ### Approve, or edit the spec `/tracks` puts the evidence in front of you for every track a tick has parked or left awaiting approval: the gate rerun on the current tip, the review verdict, the files outside `touches`, and the writer's recorded Decisions. Read the Decisions last and most carefully. An unattended writer resolves ambiguity by choosing and recording rather than asking, so that section is the complete list of places the spec did not determine the answer — and approving the track approves those readings. A Decision saying the spec contradicts itself or cannot be satisfied is not something to approve; the answer is an edit to the spec, which reopens the track on the next tick. Then `/land ` merges it, and writes the `spec//` tag that tells the coordinator the spec is done. **A spec-driven track merged without that tag is one the coordinator starts again from scratch on its next tick**, so landing by hand with a plain `git merge` is not a shortcut.