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-*.share entrypoints, callable by you or by the first mate.fm-*-lib.share sourced libraries. Never run them directly.- Every script locates itself (
SCRIPT_DIR=...), so you can call it by absolute path from anywhere. docs/scripts.mdgives 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.
--relaunchreuses the existing worktree.fm-control.sh relaunchuses 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.
--forcemeans "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=1makes it read-only.FM_BOOTSTRAP_NETWORK=all|skip|onlysplits 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 instate/<id>.inbox/and a "doorbell" line is typed into the pane. The watcher re-rings a message nobody acknowledged.--resolve-keyroutes 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 atstate/.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,mainandbranch(Pi's supervision branch). The lease file isstate/.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 atstate/<id>.check.sh. It is pinned by SHA-256 instate/<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. Adone: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/bearingsandfm-fleet-view.sh. It covers backlog records with hold buckets, live tasks withcurrent_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=0disables pruning). It reportsSTUCK: ... N commits behindrather 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.