⚓ firstmate field guide
Reference

The bin/ toolbelt

The ~200 scripts that own firstmate's mechanics, grouped by job, with the handful you will actually run by hand.

Everything exact in firstmate lives in bin/, per the VISION rule "scripts own the mechanics, agents own the judgment". There are about 200 scripts. You will run fewer than ten yourself. The first mate calls the rest.

  • fm-*.sh are entrypoints, callable by you or by the first mate.
  • fm-*-lib.sh are sourced libraries. Never run them directly.
  • Every script locates itself (SCRIPT_DIR=...), so you can call it by absolute path from anywhere.
  • docs/scripts.md gives one line per script. Each script's header comment is its canonical spec. Read the header before trusting any summary, including this one.

Set FM_HOME when poking by hand

Several scripts refuse to guess which home you mean. bin/fm-send.sh fails closed without an explicit FM_HOME. In a single-home setup, export FM_HOME=$PWD from the firstmate checkout first.

The ones you'll run yourself#

Command What it answers
bin/fm-fleet-view.sh "What is the whole fleet doing?" It renders Markdown with Under Way / Queued / Done / Secondmates tables. Needs jq. --json gives the raw snapshot.
bin/fm-crew-state.sh <id> "What state is this task actually in?" It prints one line: state: <working\|parked\|done\|blocked\|paused\|failed\|unknown> · source: <run-step\|pane\|status-log\|remote-endpoint\|none> · <detail>.
bin/fm-peek.sh <id> [lines=40] Look at a worker's pane without disturbing it. It also works for remote secondmates.
bin/fm-watch-arm.sh / --restart / --stop Check or restart this home's watcher. It prints watcher: started pid=N (beacon fresh), attached, or FAILED - ....
bin/fm-lock.sh status "Who owns this home's session?" Use it when the first mate says it is read-only.
bin/fm-lease.sh check <id> "Which actor (main or the Pi supervision branch) holds this task?"
bin/fm-pr-state.sh "What is blocking this PR right now?" It is read-only.
bin/fm-inbox.sh status / list / note <text> See what is queued for you, or leave the first mate a note while it is mid-turn.
bin/fm-project-mode.sh <project> Prints the registered <mode> <yolo> for a project. Add --branch-prefix, --forge or --raw for the other fields.

Never pkill -f bin/fm-watch.sh

A broad kill hits the watchers of every firstmate home on the machine, secondmates included. Use bin/fm-watch-arm.sh --restart, which is scoped to one home. Also never &-background fm-watch-arm.sh inside another command. It must run as a tracked background task. The repo records a ~30-minute supervision outage caused by exactly that.

Spawn and lifecycle#

fm-spawn.sh is the only way a worker is created.

fm-spawn.sh <task-id> <project-dir> --mode <no-mistakes|direct-PR|local-only> --yolo <on|off> \
    [--branch-prefix <p>] [--harness <name>] [--model <m>] [--effort <e>] [--backend <b>]
fm-spawn.sh <task-id> <project-dir> --scout [--harness ...] [--model ...] [--effort ...] [--backend ...]
fm-spawn.sh <task-id> [<firstmate-home>] --secondmate [...]
fm-spawn.sh <task-id> --relaunch [--harness <h>] [--model <m>] [--effort <e>]
  • It creates three kinds of worker: ship (a branch plus a merge contract), scout (a report and a scratch worktree), and secondmate (a whole persistent home).
  • Harness adapters: claude codex opencode pi pi-signed grok kimi cursor gemini muse rovo omp agy devin, or a raw launch command string.
  • A per-project lock serialises treehouse slot allocation across the machine.
  • --relaunch reuses the existing worktree. fm-control.sh relaunch uses this path.

fm-brief.sh writes data/<id>/brief.md. It refuses to overwrite. See Briefs & status protocol.

fm-brief.sh <task-id> <repo> --mode <mode> [--branch-prefix <p>] [--forge <none|gerrit> [--shape squash]] [--herdr-lab]
fm-brief.sh <task-id> <repo> --scout [--herdr-lab]
fm-brief.sh <task-id> --secondmate {<project>...|--no-projects}

fm-promote.sh <id> --mode <m> --yolo <on|off> [--branch-prefix <p>] turns a scout into a ship task in place. It keeps the same window, worktree and context. Both --mode and --yolo are mandatory, because a scout has no delivery posture.

fm-teardown.sh <id> [--force] [--legacy-record] is the fail-closed cleanup:

  • It returns the worktree, kills the endpoint, clears state, moves the backlog item, and refreshes the clone.
  • It refuses if work is unlanded. It understands squash-merge-then-delete-branch.
  • It checks no other task record names the same worktree before destroying anything.
  • --force means "discard". Only use it on the captain's explicit word.

fm-bootstrap.sh runs at every session start:

  • With no arguments it detects problems and prints one line each (MISSING:, NEEDS_GH_AUTH, TANGLE:, ...). It prints nothing when everything is healthy.
  • install <tool>... installs tools you have approved.
  • FM_BOOTSTRAP_DETECT_ONLY=1 makes it read-only. FM_BOOTSTRAP_NETWORK=all|skip|only splits the local and network stages.

Supervision#

Script Role
fm-watch.sh The zero-token supervisor loop. It absorbs benign wakes and exits with one reason line on an actionable one: signal:, stale:, check: or heartbeat. Absorbed wakes go to state/.watch-triage.log.
fm-watch-arm.sh Safe re-arm. It forks the watcher and verifies a fresh state/.last-watcher-beat before reporting success. It logs every cycle to state/.watch-cycle-exits.log.
fm-watch-checkpoint.sh [--seconds n] One bounded foreground cycle, used by Codex. The default is 180s (FM_CODEX_WATCH_CHECKPOINT). It exits 124 when quiet.
fm-wake-drain.sh Presents the durable wake queue plus the OPEN DECISIONS, UNREAD STATUS and RECORD DIVERGENCE sections. You acknowledge with --ack-through.
fm-turnend-guard.sh The "no turn ends blind" predicate that every harness hook calls.
fm-supervise-daemon.sh The away-mode sub-supervisor (see /afk).
fm-session-start.sh The ordered session-start digest (see Install).
fm-guard.sh Mid-turn warnings: primary-checkout tangles, pending wakes, unhealthy supervision.

Steering and control#

These are two separate planes, on purpose.

  • Text steering uses fm-send.sh <id> '<text>'. The message becomes a durable record in state/<id>.inbox/ and a "doorbell" line is typed into the pane. The watcher re-rings a message nobody acknowledged. --resolve-key routes a decision answer.
  • Lifecycle control uses fm-control.sh <id> interrupt|exit|relaunch [--note ...] [--harness/--model/--effort]. Keeping it separate stops a control command from arriving as chat that the worker reasons about instead of executing.

PR and merge#

Script Role
fm-pr-check.sh <id> <url> Records pr= and pr_head= in state/<id>.meta and arms the watcher's merge poll. Accepts GitHub PR, GitLab MR and Gerrit change URLs. Refuses a draft PR.
fm-pr-merge.sh <id> <url> [--attended-override] [--allow-red <check>] [--allow-missing <check>] Live-reads the PR at merge time: open, not draft, MERGEABLE, not DIRTY, every required check green at the current head. Merges with --match-head-commit, defaults to --squash, and refuses Gerrit (it would need a faked Code-Review+2). It confirms the merge landed before reporting.
fm-merge-local.sh <id> The one sanctioned git write into projects/: a fast-forward of a local-only project's default branch. It refuses divergence and refuses if the task is still held for the captain.
fm-review-diff.sh Diffs a crewmate's branch or PR head against the real base.
fm-pr-reviewers.sh Suggests reviewers from commit authorship. It never requests them.

State, locks, leases, holds#

  • fm-lock.sh — the per-home session lock at state/.lock. Line 1 is the harness's anchor PID. Only one live session per home.
  • fm-lease.sh claim|release|check|release-actor|sweep — per-task leases between the two actors that can exist in one home, main and branch (Pi's supervision branch). The lease file is state/.lease-<task>. Exit code 6 means another actor holds it. Merging, local landing, spawning and answering decisions are main-only while you are attended.
  • fm-captain-hold.sh — every "needs the captain" item. See Decisions & holds.
  • fm-check-register.sh <id> / fm-check-unregister.sh <id> — a task's custom watcher check at state/<id>.check.sh. It is pinned by SHA-256 in state/<id>.check-trust, so nobody can swap it silently.
  • fm-dod-lib.sh — the definition of done per mode and the named-head reachability gate. A done: claim is only accepted if the commit exists outside the disposable worktree.

Fleet#

  • fm-fleet-snapshot.sh --json — the structured source (schema: fm-fleet-snapshot.v1) behind /bearings and fm-fleet-view.sh. It covers backlog records with hold buckets, live tasks with current_state, scout reports, and secondmate summaries.
  • fm-fleet-sync.sh [<project>] — fast-forwards clones when safe and prunes branches whose upstream is gone (FM_FLEET_PRUNE=0 disables pruning). It reports STUCK: ... N commits behind rather than forcing anything.
  • fm-fleet-ledger.sh — the opt-in JSONL activity log (see Relay, voice & extras).

Captain-facing channels#

fm-inbox.sh is your out-of-band capture surface.

fm-inbox.sh note [--request-id <id>] <text>   # queue an idea while firstmate is busy (durable + one wake)
fm-inbox.sh status                            # read-only, no network, safe to loop
fm-inbox.sh list | drain [--ack <id>...]
fm-inbox.sh ask <question>                    # one-shot side question; never touches the fleet
fm-inbox.sh say [file.wav]                    # transcribe audio, then note (needs AWS Bedrock config)
fm-inbox.sh ready                             # primary readiness: lock, wake consumer, away posture

fm-mail.sh read|send|poll|status is an optional IMAP/SMTP plane. poll surfaces unseen mail as check wakes, keyed by IMAP UID, using BODY.PEEK so nothing is marked read early. fm-mail-check.sh arm runs it on the watcher's cadence. Credentials go in .env (FM_MAIL_USER, FM_MAIL_PASS, FM_IMAP_HOST, ...).

Backends#

fm-backend.sh resolves and dispatches to bin/backends/{tmux,herdr,zellij,orca,cmux}.sh. Auto-detect checks innermost first: $TMUX, then HERDR_ENV=1, then cmux markers. Zellij and Orca are never auto-detected. See Runtime backends.

Other families#

  • fm-home-seed.sh, fm-remote-home-seed.sh, fm-remote-doctor.sh — secondmate provisioning.
  • fm-x-*.sh, fm-public-followup.sh — Relay.
  • fm-voice-relay.py, fm-voice-client.py — the spoken interface.
  • fm-harness.sh — harness detection and model/effort resolution.
  • fm-dispatch-resolve.sh — the typed dispatch resolver.
  • fm-update.sh, fm-secondmate-restart.sh — self-update.
  • fm-lint.sh, fm-test-run.sh — the repo's own lint and tests.
Unofficial guide built 2026-09-26 from kunchenguid/firstmate, the AXI repos, and Kun Chen's public posts. The repo moves daily; when this guide and the repo disagree, the repo wins.