The task lifecycle
Intake → brief → dispatch → spawn → supervise → validate → PR-ready → land → teardown, with the script that owns each step and the guard that protects it.
captain request
│
▼
┌──────────┐ resolve project, route to secondmate?, ship or scout?,
│ Intake │ mode + yolo + branch prefix
└────┬─────┘
▼
┌──────────┐ bin/fm-brief.sh → data/<id>/brief.md
│ Brief │ ## Captain's intent (verbatim) + ## Firstmate spec
└────┬─────┘
▼
┌──────────┐ dispatch profile → harness/model/effort
│ Spawn │ bin/fm-spawn.sh → treehouse worktree + fm-<id> window
└────┬─────┘ backlog item → In flight
▼
┌──────────┐ bin/fm-watch.sh (zero tokens) ⇄ state/<id>.status
│Supervise │ steer: fm-send.sh · control: fm-control.sh
└────┬─────┘
▼
┌──────────┐ no-mistakes pipeline driven by the worker;
│ Validate │ ask-user findings → first mate → maybe you
└────┬─────┘
▼
┌──────────┐ done: PR <url> checks green → fm-pr-check.sh
│ PR ready │ (reachability gate, draft refusal)
└────┬─────┘
▼
┌──────────┐ your word or +yolo → fm-pr-merge.sh / fm-merge-local.sh
│ Land │
└────┬─────┘
▼
┌──────────┐ bin/fm-teardown.sh (refuses unlanded work)
│ Teardown │ backlog → Done, worktree → pool
└──────────┘
1. Intake#
Resolve the project. An explicit name wins. A follow-up inherits the project it refers to. Otherwise the first mate matches the request against the registry, the work under way, and the code. If that is ambiguous, it asks one concise question.
Route. If a registered secondmate's scope fits the nature of the work, the work goes there. Scope decides, not which projects that secondmate happens to have cloned. Local-only work stays in the main home.
Classify. The task is ship by default. It is scout when you explicitly want a separate knowledge deliverable, or when unresolved uncertainty could change whether or what to build.
Resolve the delivery contract. Mode, yolo, and ship-branch prefix come from bin/fm-project-mode.sh <project> (--branch-prefix, default fm/). They are passed explicitly to the brief and the spawn. Your current explicit instruction beats the registry. Dropping below the registered rigor "needs a reason you can state", and the reason is recorded in the backlog note.
2. Brief#
bin/fm-brief.sh <id> <repo> --mode <mode> (or --scout) writes data/<id>/brief.md. The first mate fills two placeholders, and fm-spawn.sh refuses to launch if either is left over:
## Captain's intent— your ask, verbatim, without a "Captain:" label. It is never widened. It becomes the--intentfor no-mistakes.## Firstmate spec— only the build instructions the ask requires.
The rest of the brief is generated: isolation checks, rules, the status protocol, the steering inbox, and a mode-specific definition of done. Full anatomy: Briefs & the status protocol.
3. Spawn#
bin/fm-spawn.sh is the only spawn path. It:
- resolves harness, model and effort from your dispatch profiles,
config/crew-harness, or the first mate's own harness, - validates the backend and harness adapter. It never dispatches on an unverified adapter,
- acquires an isolated worktree from treehouse, or from Orca when
backend=orca. A failed isolation assertion stops the task, - unless
config/keep-ai-trailersis set, installs a per-task gitcore.hooksPaththat strips AI commit trailers, and Claude attribution settings that suppress co-author lines, - launches the agent in a new
fm-<id>window or tab and moves the backlog item to In flight.
The first mate then confirms the worker is reading the brief and clears any trust dialog (see Harness integration).
4. Supervise#
The worker appends one-line events to state/<id>.status (working, needs-decision, blocked, paused, done, failed). The watcher wakes the first mate for the ones that matter.
It steers through two separate planes:
- Text:
bin/fm-send.sh <id> '<message>'. This is a durable inbox record instate/<id>.inbox/plus a doorbell line typed into the pane. An unacknowledged message is re-rung up to 3 times, then escalated. - Lifecycle:
bin/fm-control.sh <id> interrupt|exit|relaunch. Relaunch keeps the worktree and any uncommitted work.
A status line is not current state
state/<id>.status is an append-only log of wake events. Only bin/fm-crew-state.sh <id> gives current truth. It reconciles the no-mistakes run, the log, open decisions and pane busy-state into one line.
5. Validate#
In no-mistakes mode, the same worker drives the pipeline. It answers gates and does not implement fixes by hand, and it is forbidden to pass --yes/-y. An ask-user finding goes back up as needs-decision. The first mate decides it or escalates it to you (see Decisions), then sends exactly one decision down.
"Never hold work outside no-mistakes for a manual clean verdict, stack serial manual reviews, or infer authority... from security, architecture, or risk alone." If a fast-path project needs more rigor, the answer is to switch that task to no-mistakes, not to invent a manual gate.
6. PR ready#
| Mode | Worker's final report |
|---|---|
no-mistakes |
done: <summary> hands off to the pipeline and is not gated. Later, done: PR <url> checks green is gated. |
direct-PR |
done: PR <url> after pushing and opening a non-draft PR |
local-only |
done: ready in branch <branch> |
forge=gerrit |
done: PR <change url> published for review |
The reachability gate (fm_dod_accept_ship_done) accepts a ship done: only if the named commit exists outside the disposable worktree: on a remote-tracking ref, a recorded PR head, or a Gerrit patch set. Otherwise the task shows as blocked. bin/fm-pr-check.sh <id> <url> records pr= and pr_head=, arms the merge poll, and refuses drafts.
7. Land#
- You say "merge it", or the project has
+yoloand everything is green and in scope. - PRs:
bin/fm-pr-merge.shdoes a live read and merges with--match-head-commit(squash by default). It confirms the merge landed and records the outcome. It will not merge red unless your current instruction names the check to waive (--allow-red <check>). It refuses Gerrit. - Local-only:
bin/fm-merge-local.sh <id>fast-forwards only, on a clean default branch, and not while the task is held for you.
8. Teardown#
bin/fm-teardown.sh <id> runs only after landing is proven:
- It returns the worktree, kills the endpoint, clears state, moves the backlog item to Done (the last 10 are kept, per
done_keepin.tasks.toml), and refreshes the clone. - It refuses on uncommitted or unlanded work. "A refusal to discard is a finding, not an obstacle." Only
--force, on your explicit word, discards. - If the task was the subject of an open captain call, teardown keeps the backlog row, attaches the deliverable, and re-queues it held. Finishing the work never silently answers your question.
Scouts#
A scout follows the same path, with three differences:
- Rule 1 of its brief is "never push, never open a PR".
- Its deliverable is
data/<id>/report.md, which survives teardown. - Its scratch worktree is discarded only after the report exists and the decision-inventory gate passes (
fm-captain-hold.sh complete <origin>). No captain question can be dropped by ending an investigation.
Promotion: bin/fm-promote.sh <id> --mode <m> --yolo <on|off> flips kind=scout to kind=ship in state/<id>.meta, appends a superseding contract to the brief, and keeps the window, worktree and context. Only the intended fix is carried forward. Scratch and debug edits are dropped, and a reproduced bug becomes the regression test.