Skip to content

Latest commit

 

History

History
528 lines (477 loc) · 72.1 KB

File metadata and controls

528 lines (477 loc) · 72.1 KB

Configuration

The files and environment variables you set to operate firstmate.

Orchestrator behavior (AGENTS.md)

The shared orchestrator behavior lives in AGENTS.md - edit it like any prompt when the fleet is empty, or dispatch shared-repo edits to a crewmate while tasks are in flight.

Operational home layout and state

This section is the single owner of the top-level operational-home layout; producer script headers and their help own exact child-file fields and mutation contracts. The tracked code root contains the shared instruction, skill, documentation, workflow, and bin/ surfaces, while each effective FM_HOME contains private operational directories. data/ holds durable private fleet records such as the project and secondmate registries, captain preferences, optional shared captain preferences, learnings, backlog, briefs, and scout reports. state/ holds volatile runtime records such as task metadata, append-only status events, endpoint signals, watcher and wake-queue coordination, away-mode state, generated X-mode artifacts, private secondmate config-reread generations with their retry and quarantine state, and parent-owned secondmate pending-reply records under state/pending-replies/ (bin/fm-pending-reply-lib.sh). config/ holds local gitignored operating choices, and projects/ holds the local project clones that Firstmate reads but changes only through the narrow guarded and concrete captain-approved exceptions in AGENTS.md.

bin/fm-spawn.sh owns the base task-metadata fields it emits, while the runtime-backend section below owns backend-specific fields and selector interpretation. The producing PR and X helpers own the fields they append, bin/fm-classify-lib.sh owns status-event vocabulary, and bin/fm-crew-state.sh owns current-state reconciliation. Wake, watcher, away-mode, and X-specific state mechanics remain with their named scripts and reference sections rather than being duplicated into one exhaustive state tree here.

bin/fm-session-start.sh's header is the single owner of session-start ordering, composed commands, digest contents, and the digest's startup mechanism. docs/sessionstart-nudge.md owns the native session-open adapter mechanics that nudge the digest command. AGENTS.md retains the run-once and read-once operator rules, lock-refusal safety, installation consent, and direct-report recovery boundaries because those facts apply at every session start. Ordinary dead-direct-report recovery is owned by stuck-crewmate-recovery, while persistent-secondmate recovery is owned by secondmate-provisioning.

Pi Calm preference (config/calm)

The Pi Calm extension stores the captain's home-local presentation choice in gitignored config/calm under the effective Firstmate home, resolved from FM_HOME, then FM_ROOT_OVERRIDE, then the tracked code root derived from the extension path, or under FM_CONFIG_OVERRIDE when that test and specialized-setup override is present. The only values it writes are on and off, each followed by one newline; an absent, unreadable, or unrecognized value defaults to off. The /calm command replaces the file atomically before changing live presentation, so a failed write leaves the current choice unchanged rather than claiming persistence. The extension reloads this preference on every Pi session_start, including startup, new, resume, fork, and reload reasons. This preference is local to each Firstmate home and is not part of secondmate inherited configuration.

Backlog backend (.tasks.toml / config/backlog-backend)

The tracked .tasks.toml pins the default tasks-axi markdown backend to data/backlog.md, with done_keep = 10 and an archive at data/done-archive.md. When the default backend is selected and compatible tasks-axi is on PATH, firstmate uses its verbs for routine backlog mutations. Secondmate handoffs are separate and unconditional: fm-backlog-handoff.sh keeps only its own fleet-level validation and always delegates the item move to tasks-axi mv, the single owner of the backlog format. It moves in-scope ## Queued items only and refuses ## In flight and historical ## Done records, which stay with their home for pruning or archiving. Handoff item bodies must use at least two leading spaces, and the helper refuses a selected item with a single-space or tab-indented continuation rather than risk orphaning it. Because bootstrap requires tasks-axi on PATH on every profile, that delegation works fleet-wide, and the config/backlog-backend=manual knob governs firstmate's own hand-editing of its backlog, not this validated helper. Compatible means the shared bootstrap probe accepts tasks-axi --version as 0.1.1 or newer, tasks-axi update --help exposes --archive-body, and tasks-axi mv --help exposes [<id>...] for the atomic multi-ID move introduced in 0.2.2 and required by handoff delegation. That sentence is the single owner of the tasks-axi compatibility definition; every other document points here instead of restating the version gates. Bootstrap requires compatible tasks-axi on every profile; see "Toolchain" below for missing-tool reporting and silent default-backend behavior. Set the local, gitignored config/backlog-backend file to manual to force manual backlog editing and suppress the verbose BOOTSTRAP_INFO: tasks-axi available fact, not missing-tool reporting. Absent or tasks-axi selects the default tasks-axi backend. The file format is unchanged in both modes; tasks-axi and manual edits produce the same ## In flight, ## Queued, and ## Done sections.

Runtime backend (config/backend / FM_BACKEND)

For spawn-capable adapters, the runtime session-provider backend controls where task windows/endpoints are created, captured, sent to, watched, and killed. tmux is the verified reference backend (see docs/tmux-backend.md); herdr, zellij, orca, and cmux are experimental spawn backends (see docs/herdr-backend.md, docs/zellij-backend.md, docs/orca-backend.md, and docs/cmux-backend.md). Treehouse remains the worktree provider for tmux, herdr, zellij, and cmux, since herdr, zellij, and cmux are session providers only; Orca provides both the task worktree and terminal endpoint. New spawns choose the backend in this order: an explicit --backend flag that current authority for that exact task alone has authorized (a present captain instruction or the task's own accepted brief; never later-task precedent by analogy), then FM_BACKEND, then the first non-empty line of local gitignored config/backend, then runtime auto-detection from $TMUX, HERDR_ENV=1, or cmux runtime signals, then default tmux. If more than one runtime marker is present, detection resolves innermost-first: $TMUX is checked before HERDR_ENV=1, which is checked before cmux's primary CMUX_WORKSPACE_ID marker and its documented fallback signals - tmux or herdr started from inside a cmux terminal is the innermost, currently-executing layer, while cmux itself (a terminal application, not a nestable multiplexer) is always checked last. See docs/cmux-backend.md for why cmux can be selected when CMUX_WORKSPACE_ID is absent. Auto-detected herdr or cmux prints a stderr notice naming config/backend and --backend tmux as opt-outs; auto-detected tmux stays silent to preserve existing default behavior. Zellij and Orca are never auto-detected; select them by putting the name in a local config/backend file, by exporting FM_BACKEND=<name>, or by telling the first mate in chat. Any value other than tmux, herdr, zellij, orca, or cmux is rejected until another adapter is implemented and verified. fm-spawn.sh accepts tmux, herdr, zellij, orca, and cmux for ship and scout tasks; backend=orca and backend=cmux both still refuse --secondmate until secondmate launch semantics are designed for each. codex-app is not an accepted runtime backend yet; docs/codex-app-backend.md owns the Codex App boundary. The session-start secondmate liveness sweep uses the recovery-grade fm_backend_agent_state classifier where verified. The comment above that function in bin/fm-backend.sh is the single owner of its detailed state contract and recovery authorization. The compatibility helper fm_backend_agent_alive continues to collapse those detailed results to alive, dead, or unknown for older callers. A herdr spawn additionally version-gates against the installed herdr binary's protocol and requires jq, refusing loudly on an incompatible or missing installation. A zellij spawn additionally version-gates against the installed zellij binary's version and requires jq, refusing loudly when either is missing or the version is older than 0.44. A cmux spawn additionally version-gates against the installed cmux binary's version, requires jq, and requires the control socket to be reachable and accessible (see docs/cmux-backend.md "Setup" for the one-time socket-access configuration this needs; Automation mode is the recommended socket control mode, with Password mode supported via config/cmux-socket-password), refusing loudly and non-retryably on a cmuxOnly/unauthenticated socket. A backend spawn refusal from a missing dependency, version gate, or unauthenticated socket is terminal for that selected backend; firstmate surfaces it as a blocker instead of silently retrying another backend. Task meta records backend= only for a non-default backend; an absent backend= means tmux, preserving existing default-path meta files. Every new task records endpoint_task_id= as the cleanup binding between the metadata filename and its opaque runtime endpoint. A herdr task additionally records herdr_session=, herdr_workspace_id=, herdr_tab_id=, and herdr_pane_id=. A zellij task additionally records zellij_session=, zellij_tab_id=, and zellij_pane_id=. An Orca task additionally records orca_worktree_id= and terminal=, with window=fm-<id> kept as the shared firstmate alias. A cmux task additionally records cmux_workspace_id= and cmux_surface_id=. Task selectors for fm-peek.sh, fm-send.sh, and fm-crew-state.sh resolve centrally through fm_backend_resolve_selector. A selector containing : is passed through as an explicit backend endpoint escape hatch. Otherwise an exact task id matching state/<id>.meta wins before the legacy fm-<id> label fallback, so task ids that themselves start with fm- route to their own metadata instead of being stripped. A metadata-routed selector returns the recorded backend target (terminal= for Orca, otherwise window=), and matching explicit targets can still recover the recorded backend when metadata contains the same endpoint. Only metadata-routed task selectors carry secondmate-marker and Codex-harness context; explicit endpoint escape hatches do not. These five sentences are the single owner of the task-selector vocabulary; backend guides and other documents point here instead of restating the resolution order. fm-teardown.sh <id> takes a task id directly and validates the complete metadata-only endpoint identity before any runtime dispatch or cleanup mutation. Missing, empty, duplicate, malformed, backend-inconsistent, or task-mismatched endpoint records are preserved and refused. Legacy tmux metadata remains cleanup-compatible when its exact window name is fm-<id>; opaque non-tmux endpoints refuse only when a recorded endpoint_task_id= binding is present and mismatches the task id, so legacy metadata predating that field also stays cleanup-compatible. FM_HOME determines Herdr's home label: the primary home uses firstmate, and a secondmate home marked by .fm-secondmate-home uses 2ndmate-<secondmate-id>. herdr-backend.md owns launcher-bound workspace placement, the label-only fallback, collision handling, and recovery behavior. The optional local config/herdr-presentation-spaces presence flag instead enables Herdr's default-off disposable single-task visual projection; Optional presentation spaces owns its behavior, safety limits, recovery contract, and narrow locked session-start cleanup of exact restored idle-shell children. The flag is default-off and inherited into secondmate homes under the primary-authoritative contract owned by secondmate-provisioning. For normal herdr operations, HERDR_SESSION selects the named session, but destructive test cleanup must not rely on HERDR_SESSION alone. Use the explicit guarded cleanup path described in docs/herdr-backend.md instead of herdr server stop. For normal zellij operations, FM_ZELLIJ_SESSION selects the named session and defaults to firstmate. Zellij has no per-home workspace split: primary and secondmate tasks share that one session, and visible tab titles are scoped by the active FM_HOME readable label plus a short hash of the resolved FM_ROOT path as fm-<home-label>-<id>. Use the guarded cleanup path described in docs/zellij-backend.md instead of kill-all-sessions or delete-all-sessions. cmux has no session layer at all - one workspace per task, in whatever cmux window is open - and its socket password (when configured) is read from local, gitignored config/cmux-socket-password under the effective config directory, never committed. The caller-facing label remains fm-<id>, but the actual cmux workspace title is scoped by the active FM_HOME readable label plus a short hash of the resolved FM_ROOT path as fm-<home-label>-<id>. Test cleanup must use the guarded path in docs/cmux-backend.md, never enumerate-and-close every workspace. config/backend is inherited into secondmate homes under the primary-authoritative contract owned by secondmate-provisioning.

Away-mode supervisor backend (FM_SUPERVISOR_BACKEND / FM_SUPERVISOR_TARGET)

The /afk sub-supervisor injects escalation digests into firstmate's own pane independently of where new task endpoints are spawned. It currently supports only tmux and herdr supervisor panes. Set FM_SUPERVISOR_BACKEND=tmux|herdr and FM_SUPERVISOR_TARGET=<target> to override both axes explicitly; for herdr the target is "<session>:<pane-id>". Without overrides, backend detection uses $TMUX_PANE first, then HERDR_ENV=1 with HERDR_PANE_ID, then falls back to tmux. That keeps a tmux pane nested inside herdr on the tmux transport, matching the runtime backend's innermost-first rule. Target detection uses FM_SUPERVISOR_TARGET, then $TMUX_PANE, then "${HERDR_SESSION:-default}:${HERDR_PANE_ID}" under herdr, then the legacy firstmate:0 tmux fallback with a warning. Selecting any other supervisor backend, including zellij, orca, or cmux, refuses at daemon startup instead of trying tmux injection primitives against a non-tmux pane.

Away-mode wedge alarm channels (config/wedge-alarm)

When away-mode injection wedges past FM_MAX_DEFER_SECS, the sub-supervisor raises a loud, rate-limited alarm. Beyond the durable state/.subsuper-inject-wedged marker and the tmux status-line flash, it attempts a configured backend-independent active alert that can reach the captain even when every pane and its backend status-line is unreadable. config/wedge-alarm (local, gitignored) lists channel directives, one per non-empty, non-comment line; every listed non-off channel fires, best-effort. FM_WEDGE_ALARM_CHANNEL overrides the file with a single directive. Directives are off (a position-independent kill switch that disables every active alert), auto/default, osascript (macOS Notification Center banner), herdr (herdr UI notification), and command:<cmd> (run <cmd> via sh -c, summary on $1 and stdin). An absent file means auto, i.e. default-on on macOS: the alarm exists precisely so a wedged away-mode primary is never silent, and it fires at most once per max-defer window after a genuine wedge. A missing or failing channel logs and falls through to the next, never crashing the daemon. See wedge-alarm.md for the current channel reference, verification/supervision.md for active evidence, and examples/wedge-alarm for a copyable config.

Gate defaults (.no-mistakes.yaml)

The tracked .no-mistakes.yaml keeps test evidence outside the repo and pins commands.lint to bin/fm-lint.sh so local lint matches CI. That evidence policy is specific to the firstmate repo: target projects may legitimately commit .no-mistakes/evidence/ from their own no-mistakes pipeline, but firstmate keeps .no-mistakes/ local and CI rejects tracked entries under that path. It does not set commands.test to a complete tests/*.test.sh walk. See CONTRIBUTING.md for the firstmate-specific local test policy and entry points. Portable shard evidence and coverage rules are in fm-test-portable-shards.md; herdr-backend.md owns the real-Herdr lane's isolation boundary, and runtime-backends.md owns active evidence.

Captain Preferences (data/captain.md / data/captain-shared.md)

Domain-local preferences for one captain's fleet live locally in each home's data/captain.md; it is gitignored and printed in the session-start context digest after data/projects.md and optional data/secondmates.md. Before changing it, inspect the current file and rewrite or prune the matching bullet in place; add a new bullet only for a genuinely new durable preference. Shared captain preferences that apply across secondmate domains live only in the primary home's optional data/captain-shared.md. secondmate-provisioning owns its propagation contract, including the required header, read-only secondmate copies, quarantine diagnostics, and the rollout rule that existing homes trim data/captain.md by hand after first propagation rather than deleting private content automatically.

Operational learnings (data/learnings.md)

Fleet-local operational facts and gotchas live locally in data/learnings.md; it is gitignored and printed after the captain-preference files in the session-start context digest. The file is created lazily on first learning and follows the same dated, evidence-backed, curated style as data/captain.md: inspect the current file first, then rewrite or prune stale entries instead of appending forever. There is no shared learnings file by captain decision.

Startup memory budget (config/startup-memory-budget)

config/startup-memory-budget is the primary-authoritative per-home allowance for the startup prompt-memory surface: data/captain.md, data/captain-shared.md, and data/learnings.md together. The locked mutable bootstrap path materializes its visible default of 7500 estimated tokens in a primary home when the file is absent. To select another allowance, replace the primary home's file with one valid positive value in the exact format below; the next locked bootstrap convergence or bin/fm-config-push.sh propagates it to registered secondmates. A secondmate does not create an independent default and instead receives the primary value through the inherited-local-material contract in secondmate-provisioning. The file must be one positive base-10 integer followed by exactly one newline in a regular, single-linked file beneath a non-symlinked config/ directory. Malformed, multi-line, symlinked, hardlinked, special, or otherwise unsafe values are rejected rather than treated as a default. Use bin/fm-startup-memory-budget.sh read to validate and print the effective value, or bin/fm-startup-memory-budget.sh report to account for the three files. The stable local estimate is ceil(UTF-8 bytes / 3) per file, a conservative portable approximation rather than a provider-exact tokenizer. An inherited data/captain-shared.md counts in a secondmate's total but remains primary-owned and read-only there. The internal /stow skill curates only the editable local files in that case and reports the primary-owned shared file as a concrete exception if it alone exceeds the budget. The helper's header owns exact parsing, publication, and report output mechanics.

Secondmate routes (data/secondmates.md)

Persistent secondmate routes live locally in data/secondmates.md. The concise single-line route contract is owned by the secondmate-provisioning skill, including the parser-compatible fields, one-sentence summary requirement, home: pointer to the seeded charter, and limit on extra registry prose. Use fm-home-seed.sh validate to check the complete operational registry contract documented by the command itself. The main first mate routes by reading those scopes with judgment; the project list is provisioning data, not exclusive ownership. Use fm-home-seed.sh <id> - {<project>...|--no-projects} to lease a fresh firstmate worktree for the secondmate home. Use the deliberate --no-projects signal only for a firstmate-repo domain that needs no separate project clones. It cannot be combined with a project list, and omitting both still fails loudly. A project-less seed requires no existing project clones or data/projects.md entries in the home, so it refuses a populated-home conversion without changing that home. A preexisting project-bearing charter is also refused until it is re-scaffolded with --no-projects or removed. The lease is held under the secondmate id until explicit retirement or seed rollback returns it, so normal restarts do not free or recycle the home. Teardown of a leased home fails closed if treehouse return cannot release the lease; plain-clone homes with no treehouse pool slot are removed directly. Secondmate routes cover no-mistakes and direct-PR projects; local-only projects remain main-firstmate work. For no-mistakes projects, seeding initializes only projects newly cloned into a secondmate home and refuses to mutate a preexisting clone that is not already initialized. After creating a secondmate, move existing main-backlog queued items that you have judged in-scope with fm-backlog-handoff.sh <secondmate-id> <item-key>...; it is idempotent and refuses In flight, Done, or non-secondmate homes. Set FM_SECONDMATE_CHARTER to seed from inline charter text when no filled charter brief exists; set FM_SECONDMATE_SCOPE when the routing scope should differ from the charter text. The seeded home's data/charter.md owns the standard secondmate lifecycle and escalation contract; the route file points to it through the existing home: field instead of adding another pointer. Each seed writes an .fm-secondmate-home identity marker at the home root. The tracked root .gitignore ignores that marker, so validation can read it without making a freshly seeded home appear dirty to porcelain-based safety checks. This does not relax protection for any other untracked file. An existing linked-worktree home that predates this rule advances through its marker-only state during its next bootstrap or spawn local sync, after which Git ignores the marker normally. A standalone-clone home cannot receive a primary-local commit through that no-fetch sync, so it receives the rule through /updatefirstmate's origin refresh instead.

FM_HOME

FM_HOME selects the operational home for one firstmate instance. When it is unset, most scripts use the repo root as the home; when it is set, scripts still run from this repo's bin/, but state/, data/, config/, and projects/ come from $FM_HOME. FM_ROOT_OVERRIDE overrides the firstmate repo root used by scripts, including the primary checkout watched by the worktree-tangle guard. When FM_HOME is unset, it also behaves as the old whole-root override. bin/fm-send.sh is intentionally stricter than that general fallback: it requires FM_HOME to be set before resolving a target, so operator steers cannot silently resolve against the wrong home. FM_STATE_OVERRIDE, FM_DATA_OVERRIDE, FM_PROJECTS_OVERRIDE, and FM_CONFIG_OVERRIDE override individual operational directories for tests and specialized harness setup. Before fm-brief.sh, fm-spawn.sh, or fm-afk-launch.sh persists a path or passes it to another process, it resolves each applicable relative FM_HOME, FM_STATE_OVERRIDE, or FM_DATA_OVERRIDE directory against the caller's working directory, preserves absolute spellings unchanged, and rejects an unresolvable relative directory with the offending variable named. Bootstrap applies the same relative FM_HOME resolution only when embedding that home in the generated X-mode poll shim; other transient consumers retain their existing shell-relative behavior. For the herdr backend, FM_HOME also determines the workspace label used by the adapter. For the zellij backend, FM_HOME does not split containers, but it determines the readable home prefix embedded in visible tab titles; use FM_ZELLIJ_SESSION when a separate zellij session is needed. The full zellij home label also includes a short hash of the resolved FM_ROOT path. For the cmux backend, FM_CONFIG_OVERRIDE overrides where config/cmux-socket-password is read from, while FM_HOME determines the default config path and readable home prefix embedded in workspace titles. The full cmux home label also includes a short hash of the resolved FM_ROOT path, and there is no per-home container split.

Harness support

claude, codex, opencode, pi, pi-signed, grok, and kimi are empirically verified for crewmate and secondmate launches; agy is empirically verified for crewmate launches only (no secondmate support, experimental) - README requirements own the set supported for the primary session. New harnesses get verified through a supervised trial task before joining the set. The verified adapter knowledge - each harness's busy-state source, interrupt and exit commands, skill-invocation syntax, and per-harness quirks - lives in .agents/skills/harness-adapters/SKILL.md. Launch mechanics, including the verified command templates, live in bin/fm-spawn.sh. Enabled primary-session turn-end guard integrations are tracked as repo-level hook files and documented in docs/turnend-guard.md. Kimi remains outside the primary turn-end guard integrations; docs/turnend-guard.md owns its separate captain-approved crew wake hook. Primary-session watcher wake protocols are rendered at session start by bin/fm-supervision-instructions.sh from docs/supervision-protocols/. Claude's Stop asyncRewake hook owns tokenless re-arm cycles, Grok uses background-notify cycles, Codex uses bounded foreground checkpoints, Pi and pi-signed use the same two tracked primary extensions, and OpenCode uses its TUI plugin. config/crew-harness is a local, gitignored file containing one adapter name for crewmate and scout launches. When pi-signed is selected, Firstmate launches the executable named pi-signed from PATH with FM_PI_HARNESS=pi-signed and refuses the launch if it is unavailable rather than falling back to pi. Plain Pi launches set FM_PI_HARNESS=pi, so a signed primary's environment cannot relabel a plain Pi worker. When it is absent or contains default, crewmates mirror the firstmate's own harness. config/secondmate-harness is a separate local, gitignored file containing the adapter the primary uses to launch secondmate agents, optionally followed by model and effort tokens on the same line. The first non-empty, non-comment line is parsed as <harness> [<model>] [<effort>]. A bare <harness> preserves the previous behavior: harness only, with no model or effort launch flag. When the harness token is absent or default, secondmate launch falls back through config/crew-harness and then the primary's own harness, and no model or effort is read from that file. fm-harness.sh secondmate-model and fm-harness.sh secondmate-effort expose only the optional tokens from config/secondmate-harness; config/crew-harness remains a bare adapter-name file. An explicit harness argument to fm-spawn.sh still overrides either config file for that spawn only. An explicit --model or --effort overrides the matching token from config/secondmate-harness; an explicit harness or raw launch command starts with clean model and effort defaults unless those flags are also passed. When config/crew-dispatch.json exists, crewmate and scout spawns require an explicit resolved harness instead of automatically falling back to config/crew-harness. The inherited-local-material contract is owned by secondmate-provisioning; its harness-relevant consequence is that a secondmate's own crewmates use the primary's dispatch profiles and static harness value. Those inherited values are defaults and rules only; fm-spawn still permits a consciously chosen explicit runtime outside the config. config/secondmate-harness is not inherited because secondmates do not launch secondmates. For grok, fm-spawn.sh installs one firstmate-owned global turn-end hook under $GROK_HOME/hooks/, or ~/.grok/hooks/ when GROK_HOME is unset, and drops a per-task .fm-grok-turnend pointer in the worktree, with teardown removing the task token and pointer. For Kimi crews, fm-spawn.sh runs fm-kimi-turnend-hook.sh install, drops a per-task .fm-kimi-turnend pointer in the worktree, and records the matching private registry token for teardown. Kimi continues to use the captain's normal Kimi home, including the existing config, skills, and memory; Firstmate does not create an isolated Kimi home. The Kimi installer requires an existing regular non-symlink ~/.kimi-code/config.toml, python3 with tomllib, and jq; it validates but never serializes the captain's TOML and refuses before writing when the config is missing, malformed, or surprising or when either tool requirement is unavailable. Its remove action excises only the marker-delimited Firstmate region and removes Firstmate's hook files. For Pi and pi-signed secondmate launches, fm-spawn.sh starts the selected executable with -e pointed at the secondmate home's own tracked .pi/extensions/fm-primary-pi-watch.ts and .pi/extensions/fm-primary-turnend-guard.ts, both already present from the secondmate home's git worktree.

Crew dispatch profiles (config/crew-dispatch.json)

config/crew-dispatch.json is an optional local, gitignored file containing natural-language rules that firstmate reads before dispatching a crewmate or scout. The shell scripts do not match those rules; firstmate chooses the best matching rule with judgment, resolves its profile object or array under the operating contract in AGENTS.md section 4 and quota-array-dispatch, and passes only concrete --harness, --model, and --effort flags to fm-spawn.sh. When the file exists, fm-spawn.sh enforces that contract by refusing crewmate and scout spawns that lack an explicit harness (--harness, a positional adapter, or a raw launch command). Batch spawns satisfy the same requirement with a shared --harness. Secondmate spawns are exempt and still resolve through config/secondmate-harness and its optional model and effort tokens. This section is the single owner of the canonical schema and its per-field semantics. AGENTS.md section 4 owns the always-loaded dispatch intake boundary, and quota-array-dispatch owns the completion-aware profile-array selection procedure.

{
  "rules": [
    {
      "when": "<natural-language condition describing a kind of task>",
      "use": [
        { "harness": "<adapter>", "model": "<optional model>", "effort": "<low|medium|high|xhigh|max, optional>" }
      ],
      "why": "<optional rationale that helps firstmate choose>"
    }
  ],
  "default": [
    { "harness": "<adapter>", "model": "<optional model>", "effort": "<optional effort>" }
  ]
}

Per rule, when and use are required. Both use and the optional top-level default accept either one profile object or a non-empty array of profile objects. The single-object form stays fully backward-compatible, and every profile needs harness. Profile model and effort fields and rule why are optional. An omitted model or effort means the selected harness uses its own default for that axis. Every profile array is an implicit quota-aware choice resolved through quota-array-dispatch. If no dispatch rule fits, firstmate resolves default through the same object-or-array path before falling back to config/crew-harness. If a selected profile carries an effort value the chosen harness does not accept, fm-spawn.sh records the requested effort= in task meta for traceability but omits the launch flag, and bootstrap reports the invalid harness/effort pair as a CREW_DISPATCH diagnostic when it is visible in the file. See docs/examples/crew-dispatch.json for a starting point to copy into local config/crew-dispatch.json. When the file exists, bootstrap validates it with jq. Valid files stay silent by default; with FM_BOOTSTRAP_VERBOSE_FACTS=1, bootstrap emits BOOTSTRAP_INFO: crew dispatch active config/crew-dispatch.json, one BOOTSTRAP_INFO: fact per rule, and one fact for the optional default profile set. Malformed JSON, an empty or malformed rule/default array, an unverified harness, or an effort value unsupported by that harness is reported as CREW_DISPATCH: invalid config/crew-dispatch.json - ...; missing jq is reported through the normal MISSING: jq install-consent flow. While the file remains present, no crewmate or scout spawn may proceed without an explicit resolved harness; malformed configuration must be reported and corrected rather than selected around. Secondmate homes inherit this file from the primary, so a secondmate's own crewmates apply the same dispatch profile behavior.

Toolchain

On session start the first mate detects what its required toolchain is missing or too old and lists each problem with either an exact install command or manual instructions. It installs automatically supported tools only after you say go; manual-only tools remain for you to install from the printed instructions. Required tools come in two parts: a universal toolchain every home needs regardless of backend, and a per-backend delta that follows the runtime backend actually resolved for this home. The universal toolchain is node, git, gh with GitHub auth via gh auth login, no-mistakes v1.31.2 or newer, gh-axi, chrome-devtools-axi, lavish-axi, compatible tasks-axi per "Backlog backend" above, and quota-axi v0.1.16 or newer. This section is the single owner of that universal toolchain list; backend guides' prerequisites point here and add only their backend-specific tools. In that list, no-mistakes runs the validation pipeline, gh-axi, chrome-devtools-axi, and lavish-axi cover GitHub, browser, and rich-review operations, and tasks-axi plus quota-axi back backlog mutations and quota-aware array dispatch. The per-backend delta is required only for the backend resolved from FM_BACKEND, then config/backend, then runtime auto-detection, then default tmux, so a home is never told to install a tool an inactive backend or feature would need. That delta is owned in code by fm_backend_required_tools in bin/fm-backend.sh: the resolved backend's own session-provider CLI (tmux, herdr, zellij, orca, or cmux), jq for the JSON-emitting experimental adapters (herdr, zellij, cmux) whose spawn and liveness paths parse the backend's JSON output, and the treehouse worktree provider for every session-provider-only backend (tmux, herdr, zellij, cmux). Backend tool availability uses the adapter's own executable resolver, so bootstrap and spawn agree on supported non-PATH locations such as cmux's bundled CLI. An unknown resolved backend emits BACKEND_INVALID and blocks dispatch instead of silently dropping its dependency delta or falling back to tmux. Orca provides both the task worktree and terminal endpoint (see "Runtime backend" above), so backend=orca requires only orca on top of the universal toolchain and skips both treehouse and every other backend's session CLI. A herdr, zellij, or cmux home is therefore never told tmux is missing, and the treehouse durable-lease upgrade check runs only for the backends that actually use treehouse. When config/crew-dispatch.json exists, bootstrap also requires jq for dispatch profile validation. When X mode is opted in, bootstrap also requires curl and jq before arming the relay poll shim. tasks-axi and quota-axi are required bootstrap tools in every profile, the same class as lavish-axi. An absent or incompatible tasks-axi reports MISSING: tasks-axi (install: npm install -g tasks-axi); when config/backlog-backend is not manual and compatible tasks-axi is on PATH, bootstrap stays silent and firstmate uses its verbs for routine backlog mutations, otherwise it hand-edits data/backlog.md until installation is approved and completed. An absent or too-old quota-axi reports MISSING: quota-axi (install: npm install -g quota-axi); firstmate cannot resolve a profile array without a compatible binary. That floor exists because it is the first build reporting per-credential auth sources, without which a candidate cannot be judged against the authentication surface it actually uses. Bootstrap also reports a TANGLE: line when FM_ROOT is on a named non-default branch; follow the printed checkout remediation rather than treating it as an installable tool problem. In a read-only session that did not get the fleet lock, the same line is advisory and omits the checkout command. The locked session-start bootstrap step also runs a best-effort project clone refresh through fm-fleet-sync.sh. It emits FLEET_SYNC: for skipped refreshes that may matter, recovered self-heals, and STUCK: alarms. Normal completed runs keep local-only and no-origin skips silent. If bootstrap kills a timed-out refresh, it replays any completed fm-fleet-sync.sh output before the aggregate timeout skip so no finished result is lost. A killed refresh (or a teardown process kill) can leave an orphaned .git/packed-refs.lock in a clone, which makes the next refresh's fetch fail with Git's Unable to create '...packed-refs.lock': File exists. On that signature only, fm-fleet-sync.sh retries the fetch with a bounded wait for the lock to self-clear, then removes the lock and retries once more only when it can prove the lock stale, exactly like the fm-teardown.sh index.lock recovery. It never removes a live lock, leaves any other failure shape untouched, and prints every wait, retry, and removal to stderr plus a one-line recovered: summary to stdout on success so that this session-start relay still surfaces the recovery. The locked session-start bootstrap step also runs the guarded local secondmate sync for recorded live secondmate homes, then propagates declared inherited local material into each validated live home. It emits SECONDMATE_SYNC: only when a home was skipped for an actionable sync reason, inheritance failed, or a divergent shared captain-preference copy was quarantined. When a running home advances and its loaded instruction surface (AGENTS.md, bin/, or .agents/skills/) changed, bootstrap sends the re-read nudge itself through the stable fm-<id> selector and reports the exact completed send as BOOTSTRAP_INFO:. If that send fails, bootstrap keeps an idempotent retry marker and emits NUDGE_SECONDMATES: with the failure reason. The same bootstrap run emits SECONDMATE_LIVENESS: only when a registered secondmate is skipped or its relaunch fails; already-live and successfully relaunched secondmates are handled silently. For a mid-session inherited local-material edit where tracked-file sync is not needed, run bin/fm-config-push.sh. It uses the same live secondmate discovery and propagation helper as bootstrap, prints each live home's crew-dispatch.json, crew-harness, backlog-backend, backend, herdr-presentation-spaces, startup-memory-budget, and data/captain-shared.md result as pushed, unchanged, skipped, or error, and exits non-zero for real propagation errors or config-reread send failures. When an allowlisted config item changes for an already-running home, it sends the literal-content reread pointer described in secondmate-provisioning; unchanged allowlisted config sends no pointer unless a previous delivery is pending. The locked bootstrap inheritance pass uses the same per-home changed-set and reread path for already-running homes; see secondmate-provisioning for the single contract owner. That live discovery starts from state/*.meta records with kind=secondmate; data/secondmates.md only backfills home= for older or incomplete meta records. Skipped items, such as a destination checkout that does not yet gitignore the item, are visible warnings but not hard failures.

X mode (.env)

X mode lets a firstmate instance answer public @myfirstmate mentions and act on normal reversible mention requests through firstmate's normal lifecycle. It is off unless the firstmate home's gitignored .env contains a non-empty FMX_PAIRING_TOKEN. The pairing token both identifies the relay tenant and records opt-in consent for autonomous public replies and eligible lifecycle actions. Destructive, irreversible, or security-sensitive asks are flagged for trusted-channel confirmation instead of being executed from a public mention. The relay uses owner-only routing: a mention delivered to a home is from that home's owner/captain, while parent-thread context may still include other public accounts. FMX_RELAY_URL is optional and defaults to https://myfirstmate.io, mainly for developers pointing at a local relay. For direct client invocations, environment values override .env; bootstrap activation still keys off .env presence so watcher artifacts are explicit local opt-in state. FMX_ENV_FILE can point direct poll/reply client invocations at another .env-style file, but it does not change bootstrap activation.

The locked session-start bootstrap step turns the token into local generated state. It writes state/x-watch.check.sh, a byte-static identity shim for bin/fm-x-poll.sh, and config/x-mode.env, which exports FM_CHECK_INTERVAL=30 for watcher processes in that home. The watcher accepts the shim only when its bytes match the expected generated content, then invokes the trusted repository poll script directly instead of executing state-file source. This section is the single owner of the X-mode cadence contract: an X instance polls every 30 seconds instead of the default 300, only an X instance speeds up because a non-X home has no config/x-mode.env, and the session-start supervision operating block includes the cadence instruction when that file exists. The active primary-harness supervision protocol owns how that sourced cadence reaches the watcher process. Because bin/fm-watch.sh reads FM_CHECK_INTERVAL only at process start, a cadence transition - opt-in while a watcher is already running, or opt-out - is applied by restarting the home-scoped watcher through the emitted harness protocol; bootstrap deliberately never restarts the watcher itself. While away mode is active the daemon owns the watcher and its default cadence applies; away-mode X cadence is a deferred follow-up. When the token is removed or empty, the next locked session-start bootstrap step removes those artifacts. Steady-state off is silent and writes nothing. X mode remains additive to non-X lifecycle behavior: homes without the generated artifacts keep the default watcher cadence and do not run the X poll. Its request handling remains in X-specific bin/ scripts and the fmx-respond skill, while the watcher owns authenticated dispatch from the generated local identity shim.

bin/fm-x-poll.sh calls GET /connector/poll with Authorization: Bearer <FMX_PAIRING_TOKEN>. HTTP 204 is silent. A newly offered pending mention with non-empty text is stored at state/x-inbox/<request_id>.json and wakes firstmate exactly once with x-mention <request_id>. The poll atomically claims state/x-context/<request_id>.offered.json before emitting that wake, and subsequent offers of the same request stay silent even after the inbox is drained following an answer or dismiss. Offer markers share the context registry's bounded seven-day retention, so losing or expiring the local marker lets a relay offer wake firstmate again. The full relay object is preserved, including in_reply_to: {author_handle, text} when the mention is a reply in a conversation or null for fresh mentions. At the same time the poll records a durable per-request reply context at state/x-context/<request_id>.json ({request_id, platform, reply_max_chars, recorded_at}) from the same authoritative relay payload, best-effort and keyed by request_id so concurrent requests never overwrite each other; it survives the inbox cleanup that follows the acknowledgement, so a delayed follow-up can recover the original platform and split budget even with no task link. recorded_at begins as the locally observed first-seen Unix epoch and remains unchanged when the same request is polled again. A successful live initial answer refreshes it to the time that the relay establishes the follow-up binding; dry-runs, failed answers, and follow-ups do not refresh it. Configured polls prune records beyond the local follow-up window, capped at the relay's seven-day window; legacy or malformed records fall back to their file modification time so they cannot remain indefinitely. The record is written only when a platform or explicit budget is actually known, so an unknown-platform mention leaves no useless entry. The fmx-respond skill decides whether the stashed mention is an actionable request, a question, or a pure acknowledgment. Actionable reversible requests are run through intake, backlog, dispatch, investigation, or ship flow as appropriate. If the work completes in that turn, the public reply reports the outcome. If the request spawns a longer-running task, firstmate posts an acknowledgement through the normal answer endpoint, links the task to the mention with bin/fm-x-link.sh, and posts up to three completion follow-ups on genuine milestones, finishing with a --final one for ordinary X-linked work. When a typed promised-final commitment is registered, bin/fm-public-followup.sh owns the terminal reply and clears the legacy link after its receipt is validated. That link stores optional reply-platform context so Discord-originated follow-ups keep Discord's larger message budget after the inbox file has been drained. Platform/budget resolution is layered and independent of the task link: a per-axis FMX_REPLY_PLATFORM / FMX_REPLY_MAX_CHARS override (how bin/fm-x-followup.sh passes a recorded link's context) wins. For either axis without an override, bin/fm-x-lib.sh:fmx_resolve_reply_context owns the source order: the durable per-request registry is consulted first, then the still-present inbox payload, then - for a follow-up posted live by request_id - an authoritative relay lookup via POST /connector/request-context ({request_id} in, {platform, reply_max_chars} back). This is what keeps a delayed request-id follow-up on the original platform's budget even after the inbox is drained and with no task link surviving; the relay step is confined to the live follow-up path so the answer path and every dry-run stay network-free. bin/fm-x-link.sh follows the same ordering when recording a fresh link's context and requires jq; its request-context lookup is best-effort: no token or curl; a non-2xx response; an unresolved response; or a relay version without that endpoint leaves the context unknown. In that case the link is still recorded but bin/fm-x-link.sh prints a loud warning; and when either a follow-up's platform or explicit budget cannot be authoritatively resolved from any source, bin/fm-x-reply.sh refuses it (fail-safe exit 8) rather than posting with a local default - firstmate holds and retries it once both values are recoverable. Fresh links start with x_followups=0 and the current timestamp; when relinking the same relay request onto a successor task, pass paired --carry-count <n> --carry-ts <epoch> flags plus any prior x_platform= and x_reply_max_chars= as --carry-platform <x|discord> --carry-max <n> so the successor preserves the already-consumed follow-up count, original 7-day window, and reply split budget. Pure acknowledgments or mentions with nothing to answer are dismissed through bin/fm-x-dismiss.sh before the local inbox file is cleared. Dismiss sends POST /connector/dismiss with {request_id}, posts no text, and tells the relay to drop the request instead of re-offering it or falling back to an offline auto-reply; on success it clears that request's durable reply-context record, while the separate offer marker remains for its bounded retention so a brief relay re-offer stays silent. Relay auth or config problems are reported once as x-mode-error ... until recovery. A failed durable offer claim is likewise reported once as x-mode-error cannot record mention offer and remains deduplicated through quiet no-pending polls until a later offer confirms an existing valid marker or claims a new one. Live replies are posted by bin/fm-x-reply.sh, which sends POST /connector/answer with {request_id,text} for one-message replies. Add --image <path> to attach one local PNG, JPEG, GIF, WebP, BMP, or TIFF as {media_type,data_base64} in the relay's optional image object. Completion follow-ups use bin/fm-x-followup.sh, which checks the local state/<id>.meta link and sends the same payload shape through POST /connector/followup by calling bin/fm-x-reply.sh --followup, up to three times per link within the window. Add --image <path> there too when a completion follow-up should carry an image. A successful post increments the local x_followups= counter and keeps the link, unless --final was passed or the new count reaches the cap, in which case the link is cleared instead; a failed post leaves the link and counter untouched so it can be retried. The relay itself rejects a follow-up past its own cap or window with HTTP 409 and may include {"error":"followup_unavailable"} in the response body; the client surfaces any follow-up 409 as a distinguishable exit code and uses the body marker only for a sharper diagnostic. fm-x-followup.sh treats that exit exactly like a locally-detected expiry - clearing the link and skipping quietly rather than retrying - so an older single-follow-up relay or an already-exhausted binding degrades gracefully. It treats fm-x-reply.sh's fail-safe refusal (exit 8: platform or explicit budget unresolved) differently: that is a retryable hold, so the link is KEPT and the follow-up is retried once both values can be recovered, never posted with a local default. Past-window relay rejections are only guaranteed while the expired binding row still exists on the relay side; after its cleanup sweep, a very-late follow-up call may instead see a benign no-op 200, which is why the local window and cap pruning remains the primary guard. Reply splitting is platform-aware: an explicit relay platform field (reply_platform, platform, target_platform, source_platform, or provider) wins, otherwise a legacy tweet_id beginning with discord: selects Discord and a numeric tweet_id selects X. An explicit relay limit field (reply_max_chars, reply_max_characters, message_max_chars, message_limit, or max_chars) wins over the platform defaults. If the reply exceeds the selected budget, the client splits it into a numbered thread on fenced-code, paragraph, line, and word boundaries and sends {request_id,text,texts}, where texts is the ordered chunk list and text remains the first chunk for older relays. When --image <path> is present on a split reply, the image rides the first/opener message and later chunks stay text-only. FMX_X_REPLY_MAX_CHARS defaults to 280 and clamps to a minimum of 50; FMX_DISCORD_REPLY_MAX_CHARS defaults to 1900, clamps to a minimum of 50, and resets values above Discord's 2000-character limit back to 1900. FMX_X_THREAD_MAX defaults to 25 and caps oversized reply threads for every platform, marking the last retained message with an ellipsis when truncation is needed. FMX_FOLLOWUP_MAX_AGE_SECS defaults to 604800 (7 days) and controls the local completion follow-up window; FMX_FOLLOWUP_MAX_COUNT defaults to 3 and controls the local follow-up cap.

Set FMX_DRY_RUN to preview replies and dismissals without posting. Truthy means anything except unset, empty, 0, false, no, or off; an explicit environment value wins over .env. In dry-run, fm-x-reply.sh records the would-be payload to state/x-outbox/<request_id>.json, including texts for a thread and an endpoint marker for follow-up previews, prints a DRY RUN summary to stderr, echoes the request_id, and exits 0. When an image is attached, the dry-run record uses compact {media_type, bytes, source_path} metadata instead of writing the base64 bytes. In dry-run, fm-x-dismiss.sh records {request_id, endpoint:"dismiss"} to the same outbox path, prints a DRY RUN summary, echoes the request_id, and exits 0. The live answer and follow-up bodies intentionally stay the same shape, including optional image; the relay distinguishes them by endpoint, and dismiss stays {request_id}. These paths need jq to build the JSON payload, but they run before token and network checks, so they need neither FMX_PAIRING_TOKEN nor curl.

Promised public replies (state/public-followup)

A relay request that spawns real work can leave firstmate owing a specific public reply in a specific thread. That promise is a typed kind=public-followup obligation owned entirely by tasks-axi public-followup, with the full private request context staying in state/x-context/; firstmate keeps no parallel copy of either. bin/fm-public-followup.sh is firstmate's side: it registers a commitment, reconciles typed terminal work results into it, and posts the final reply through bin/fm-x-reply.sh --followup. Run bin/fm-public-followup.sh --help for the exact subcommands and flags.

Registration is what creates this home's private transport under state/public-followup/ (mode 0700): registry/ for the bounded public-safe binding of each live commitment, events/ for typed terminal results awaiting reconciliation, consumed/ for the accepted-event ledger, rejected/ for refusals kept with a one-line reason, and surfaced for the poll's last-surfaced signature. The home that owns the commitment also owns the outward post, because only it holds the relay consent, the request context, and the opaque thread binding. Work routed elsewhere reports a typed terminal result with bin/fm-public-followup-emit.sh and never looks for the thread; that emitter refuses to write into a home with no registration for the named obligation. A terminal event's id is derived from its identity tuple, so a duplicate report, a retry, or a replay after restart resolves to the same event and changes nothing.

Activation is the same .env FMX_PAIRING_TOKEN contract as the rest of X mode, with no second flag. A home without that token runs one file test and stops: no tasks-axi call, no backlog or request-context scan, and no state/public-followup/ directory. Ordinary startup, polling, cleanup, and silent read-side subcommands also produce no output; commands that require an active relay report that configuration error after the same gate. A relay-enabled home with no registered commitment stops at an O(1) directory presence check, so the empty state costs no CLI call and adds no periodic scan. Unreconciled terminal results ride the existing 30-second relay poll rather than a new process or timer: bin/fm-x-poll.sh compares the pending-event signature against surfaced and wakes firstmate once per new result set. The session-start digest separately prints an "Public commitments awaiting delivery" subsection from disk when, and only when, this home is relay-active and still owes a reply, so compaction and restart are non-events. bin/fm-teardown.sh refuses to clean up a task while this home still owes a public reply for exactly that work, unless --force carries explicit discard approval. FM_PF_RETRY_BACKOFF_SECS (default 900) sets the next-attempt time recorded with a retryable delivery error. See verification/public-followup.md for the current maintainer evidence behind the restart end-to-end and the relay-disabled zero-overhead guarantee.

Environment variables

Runtime tuning via environment variables (defaults shown):

FM_HOME=                 # optional operational home for most scripts, unset means this repo root; fm-send requires it explicitly
FM_ROOT_OVERRIDE=        # override firstmate repo root, tangle-guard target, and zellij/cmux home-title hash; also legacy whole-root override when FM_HOME is unset
FM_STATE_OVERRIDE=       # alternate state dir, mainly for tests
FM_DATA_OVERRIDE=        # alternate data dir, mainly for tests
FM_PROJECTS_OVERRIDE=    # alternate projects dir, mainly for tests
FM_CONFIG_OVERRIDE=      # alternate config dir, mainly for tests
FM_PROC_ROOT_OVERRIDE=   # alternate /proc root for the Linux process-identity read in fm-wake-lib.sh, mainly for tests
FM_BACKEND=             # optional runtime backend override for new spawns; tmux/herdr/zellij/orca/cmux support ship/scout spawns, codex-app is not accepted
HERDR_SESSION=default  # herdr-only: named session for normal backend ops; not enough for destructive cleanup (docs/herdr-backend.md)
FM_BACKEND_HERDR_COMPOSER_LINES=20  # herdr-only: tail lines scanned by composer-state guard/fallback paths; idle-baseline submit confirmation uses agent-state
FM_BACKEND_HERDR_IDLE_RE='^Type a message\.\.\.$'  # herdr-only: empty-composer placeholder regex after shared ghost extraction plus border and prompt stripping
FM_BACKEND_HERDR_BARE_PROMPT_RE='^(❯|›)'  # herdr-only: verified agent glyphs recognized as an UNBORDERED (bare) composer row, e.g. Claude's ❯ or Codex's ›; an alternation, not a `[...]` bracket expression, so a C-locale byte-decomposed match can never misfire on an unrelated multibyte glyph; shell glyphs remain unknown rather than empty, and de-emphasised ghost/placeholder text reads empty through shared fm_composer_strip_ghost (docs/herdr-backend.md "Composer and injection safety")
FM_BACKEND_HERDR_PI_COMPOSER_MAX_LINES=8  # herdr-only: maximum rows admitted between Pi's native-identity-corroborated separator pair; taller or ambiguous candidates stay unknown (docs/herdr-backend.md "Composer and injection safety")
FM_BACKEND_HERDR_SUBMIT_POLLS=6  # herdr-only: agent-state samples spread across each Enter attempt's budget when confirming a submit (docs/herdr-backend.md "Current transport behavior")
FM_BACKEND_HERDR_SUBMIT_MIN_SLEEP=0.6  # herdr-only: minimum per-Enter confirmation budget before polling agent-state after an idle baseline
FM_BACKEND_ORCA_COMPOSER_LINES=200  # orca-only: terminal-read lines scanned to locate the composer row for submit verification
FM_BACKEND_ORCA_IDLE_RE='^Type a message\.\.\.$'  # orca-only: empty-composer placeholder regex after border/prompt stripping
FM_ZELLIJ_SESSION=firstmate  # zellij-only: named session for normal backend ops and test isolation (docs/zellij-backend.md)
FM_BACKEND_CMUX_COMPOSER_LINES=20  # cmux-only: tail lines scanned to locate the composer row for submit verification
FM_BACKEND_CMUX_IDLE_RE='^Type a message\.\.\.$'  # cmux-only: empty-composer placeholder regex after border/prompt stripping
CMUX_SOCKET_PASSWORD=   # cmux-only: socket password fallback when config/cmux-socket-password is absent (docs/cmux-backend.md)
FM_SESSION_START_STATUS_TAIL=5   # state/*.status lines printed per task in the session-start digest
FM_BOOTSTRAP_DETECT_ONLY=0   # internal/read-only session-start mode: skip bootstrap's mutating sweeps and print advisory TANGLE wording
FM_GUARD_READ_ONLY=0    # internal/read-only guard mode: keep alarms but suppress drain, supervision repair, and checkout repair commands
FM_GUARD_CONTINUE_LINE='This is a supervision warning only; the guarded operation WILL still run.'   # banner continuation line; fm-send.sh overrides it to name the requested message specifically
FM_POLL=15              # seconds between watcher poll cycles
FM_HEARTBEAT=600        # base seconds between heartbeat scans; no-change heartbeats are absorbed while idle
FM_HEARTBEAT_MAX=7200   # heartbeat backoff cap
FM_CHECK_INTERVAL=300   # seconds between slow checks (authenticated merge polls, custom checks, or X-mode dispatch)
FM_CHECK_TIMEOUT=30     # seconds allowed per slow check script
FM_CODEX_WATCH_CHECKPOINT=180   # seconds per foreground watcher checkpoint in Codex primary supervision
FM_CREW_STATE_NM_TIMEOUT=10   # seconds allowed per no-mistakes query inside fm-crew-state.sh
FM_CREW_STATE_RUNS_LIMIT=200  # recent no-mistakes run rows scanned when axi status cannot be attributed to the current code
FM_CREW_STATE_BIN=bin/fm-crew-state.sh   # test override for the current-state reader used by working/paused watcher triage
FMX_PAIRING_TOKEN=      # X mode pairing token; .env opt-in authorizes replies and eligible lifecycle actions
FMX_RELAY_URL=https://myfirstmate.io   # optional X relay override, mainly for local relay development
FMX_ENV_FILE=           # optional alternate .env file for direct X client invocations; bootstrap still checks $FM_HOME/.env
FMX_DRY_RUN=            # truthy previews X replies and dismissals to state/x-outbox/ without posting or requiring a token
FMX_X_REPLY_MAX_CHARS=280   # X reply per-message split budget; values below 50 clamp to 50
FMX_DISCORD_REPLY_MAX_CHARS=1900   # Discord reply per-message split budget; values below 50 clamp to 50, values above 2000 reset to 1900
FMX_X_THREAD_MAX=25     # maximum messages in one auto-split reply thread
FMX_FOLLOWUP_MAX_AGE_SECS=604800   # local window for posting X-mode completion follow-ups (7 days)
FMX_FOLLOWUP_MAX_COUNT=3   # local cap on X-mode completion follow-ups per linked mention
FM_PF_RETRY_BACKOFF_SECS=900   # seconds before the next attempt after a retryable promised-public-reply delivery error
FM_LOCK_STALE_AFTER=2   # seconds before dead-pid lock records can be reclaimed; mid-acquire locks keep at least 2s grace
FM_GUARD_GRACE=300      # seconds before guard warnings, arm health checks, and the primary turn-end guard treat a watcher beacon as stale
FM_CLAUDE_AUTOARM_SYNC_WAIT_MS=800   # milliseconds the --claude turn-end guard waits for the Stop auto-arm's claim, health, or fresh rewake epoch before re-blocking
FM_CLAUDE_AUTOARM_EPOCH_FRESH=15   # seconds a recorded auto-arm rewake outcome counts as this event epoch's owned recovery
FM_CLAUDE_TURNEND_BLOCK_BUDGET=3   # consecutive --claude guard re-blocks before a degraded allow; safely below Claude Code's 8-block override
FM_ARM_CONFIRM_TIMEOUT=10   # seconds fm-watch-arm waits to confirm a fresh watcher before reporting FAILED; default 30 on Git Bash/MSYS
FM_ARM_ATTACH_POLL=0.5  # seconds between checks while fm-watch-arm is attached to an existing healthy watcher cycle
FM_OPENCODE_ARM_READY_TIMEOUT_MS=12000   # milliseconds the OpenCode primary watcher plugin waits for an arm attempt to report started, healthy, wake, or failure; default 35000 on Windows to stay above the MSYS confirm budget
FM_PI_ARM_READY_TIMEOUT_MS=12000   # milliseconds the Pi watcher extension waits for a successor arm to report started or attached; default 35000 on Windows to stay above the MSYS confirm budget
FM_WATCH_ARM_RETIRE_TIMEOUT_MS=1000   # milliseconds Pi/OpenCode wait for an unready successor arm to exit before abandoning retries
FM_WATCH_REARM_RETRY_BASE_MS=250   # Pi/OpenCode adapter base delay for continuity restoration retries
FM_WATCH_REARM_RETRY_MAX_MS=4000   # Pi/OpenCode adapter cap for exponential continuity retry delay
FM_WATCH_REARM_RETRY_LIMIT=5   # Pi/OpenCode adapter launch-failure retries before surfacing restoration failure
FM_WATCH_CYCLE_LOG_MAX_BYTES=262144   # size cap for the arm-owned watcher lifecycle ledger
FM_WATCH_CYCLE_LOG_KEEP_LINES=1000   # newest complete lifecycle rows considered when the ledger is capped
FM_WATCHER_STALE_GRACE=300   # defaults to FM_GUARD_GRACE; seconds a live watcher lock may have a stale beacon before re-arm errors
FM_SIGNAL_GRACE=30      # seconds to coalesce nearby status and turn-end signals into one wake
FM_CAPTAIN_RE='done:|needs-decision:|blocked:|failed:|PR ready|checks green|ready in branch|merged'   # captain-relevant status regex; nonterminal progress verbs remain excluded even when their prose matches
FM_CLASSIFY_PAUSED_VERB=paused     # leading status verb for a declared external wait; excluded from FM_CAPTAIN_RE and distinct from blocked
FM_STALE_ESCALATE_SECS=240         # idle seconds before a provably-working stale pane escalates; stale panes whose crew is not provably working surface immediately unless they declare the pause verb
FM_BUSY_TURN_MAX_SECS=3600         # maximum age of a busy pane's latest state/<id>.turn-ended marker, or its state/<id>.meta spawn record before any turn completes, before the same wedge escalation used for a provably-working non-busy stale takes over; inspection-only, never an automatic interrupt or restart
FM_PAUSE_RESURFACE_SECS=3600       # seconds before an idle declared external wait re-surfaces for a recheck in the watcher or away-mode daemon
FM_WEDGE_DEMAND_INSPECT_COUNT=3    # consecutive provably-working stale escalations on the same unchanged pane before demand-deep-inspection is added
FM_WATCH_TRIAGE_LOG_MAX_BYTES=262144   # size cap for the watcher's absorbed-wake debug log
FM_FLEET_SYNC_BOOTSTRAP_TIMEOUT=     # optional seconds allowed for bootstrap's best-effort clone refresh; unset/blank defaults to max(20, 5 + 3 * origin-backed-project-count)
FM_FLEET_PRUNE=1        # set to 0 to skip pruning local branches whose upstream is gone
FM_STALE_WORKTREE_LOCK_AGE_SECS=30       # min mtime age before fm-teardown.sh treats a leftover worktree git index.lock as provably stale
FM_TREEHOUSE_RETURN_LOCK_RETRIES=3        # retries after a treehouse return fails on the transient git index.lock signature
FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS=1 # seconds fm-teardown.sh waits before each retry after that signature
FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS=   # legacy alias for FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS when the new variable is unset
FM_FLEET_SYNC_PACKED_REFS_LOCK_RETRIES=3        # fetch retries after fm-fleet-sync.sh hits the orphaned .git/packed-refs.lock signature
FM_FLEET_SYNC_PACKED_REFS_LOCK_RETRY_WAIT_SECS=1 # seconds fm-fleet-sync.sh waits before each of those retries
FM_FLEET_SYNC_PACKED_REFS_LOCK_AGE_SECS=30       # min mtime age before fm-fleet-sync.sh treats a leftover packed-refs.lock as provably stale
FM_BUSY_REGEX=          # optional override for rendered delivery guards and Grok's isolated task-state fallback; converted worker state ignores it
FM_COMPOSER_IDLE_RE=    # optional empty-composer regex, applied after ghost and border stripping
FM_COMPOSER_GHOST_LUMA_MAX=128   # fleet-wide: max perceived luminance (0.299R+0.587G+0.114B, 0-255) for a TRUECOLOR foreground to count as de-emphasised ghost/placeholder text and be stripped; dim/faint (SGR 2) is stripped regardless. Assumes a dark terminal theme (bin/fm-composer-lib.sh's fm_composer_strip_ghost, shared by the tmux and herdr composer readers)
GROK_HOME=              # optional Grok config home for firstmate's global grok turn-end hook; defaults to ~/.grok
FM_SEND_RETRIES=3       # fm-send Enter-retry attempts after typing the line once
FM_SEND_SLEEP=0.4       # seconds between fm-send submit checks
FM_SEND_SETTLE=1        # seconds fm-send waits after a successful text submit; 0 disables
FM_PENDING_REPLY_GRACE_SECS=120   # seconds after marked-request delivery before a completed turn without a correlated parent report is eligible for its one recovery repost
# sub-supervisor (bin/fm-supervise-daemon.sh); presence-gated via /afk
FM_SUPERVISOR_BACKEND=             # optional supervisor pane backend override; tmux/herdr only, otherwise detects $TMUX_PANE then HERDR_ENV/HERDR_PANE_ID before tmux fallback
FM_SUPERVISOR_TARGET=              # optional supervisor pane target override; tmux target or herdr <session>:<pane-id>, otherwise auto-detected
FM_INJECT_SKIP=heartbeat           # |-prefixes force-self-handled bypassing classification; empty disables
FM_ESCALATE_BATCH_SECS=90          # buffer window for batched escalation digests; 0 = flush immediately
FM_MAX_DEFER_SECS=300              # max buffered escalation age before retry plus wedge alarm; 0 disables
FM_WEDGE_ALARM_CHANNEL=            # override config/wedge-alarm with one active-alert directive for the wedge alarm; off|auto|osascript|herdr|command:<cmd>; absent = auto (macOS -> an OS notification)
FM_WEDGE_ALARM_EXEC=              # notifier seam: route every channel (osascript, herdr, command:) through this command as `<cmd> <channel> <summary>`; "discard" fires nothing; unset in production; the daemon defaults it to "discard" when sourced so no test posts a real notification (docs/wedge-alarm.md)
FM_WEDGE_ALARM_TIMEOUT_SECS=10    # maximum seconds for each osascript, herdr, override, or command: notifier before its watchdog terminates it and continues to the next channel; invalid or zero values use 10
FM_INJECT_FAIL_SLEEP=30            # seconds to back off when the supervisor pane is unavailable
FM_INJECT_CONFIRM_RETRIES=3        # daemon Enter-retry attempts after typing a digest once
FM_INJECT_CONFIRM_SLEEP=0.5        # seconds between daemon submit checks
FM_HEARTBEAT_SCAN_SECS=300         # cadence of the catch-all status scan for missed captain verbs
FM_HOUSEKEEPING_TICK=15            # seconds between batch-flush, stale/pause-recheck, and scan passes
FM_CRASH_THRESHOLD=10              # watcher crashes allowed inside FM_CRASH_WINDOW before daemon backoff
FM_CRASH_WINDOW=60                 # seconds in the crash-loop detection window
FM_CRASH_BACKOFF=60                # seconds to wait after crossing the crash threshold
FM_CRASH_NORMAL_SLEEP=5            # seconds to wait after an isolated watcher crash
FM_LOG_MAX_BYTES=1048576           # daemon log size that triggers trimming
FM_LOG_KEEP_LINES=2000             # daemon log lines kept when trimming

fm-teardown.sh retries only Git's Unable to create '...index.lock': File exists return failure up to FM_TREEHOUSE_RETURN_LOCK_RETRIES times. FM_TREEHOUSE_RETURN_LOCK_RETRIES accepts a nonnegative integer, and an unset, blank, or invalid value uses the default of 3. FM_TREEHOUSE_RETURN_LOCK_RETRY_WAIT_SECS accepts nonnegative whole or fractional seconds between attempts. When it is unset or blank, FM_STALE_WORKTREE_LOCK_RETRY_WAIT_SECS remains a compatible fallback, and a blank fallback uses the 1-second default. An invalid nonblank wait falls back to 1 second rather than interrupting teardown. Teardown never removes a lock during the retry window, and after that window it attempts stale-lock cleanup only for a still-present lock that passes the configured age and live-holder checks.

fm-fleet-sync.sh applies the same shape to an orphaned .git/packed-refs.lock: it retries only Git's Unable to create '...packed-refs.lock': File exists fetch failure up to FM_FLEET_SYNC_PACKED_REFS_LOCK_RETRIES times (nonnegative integer; unset, blank, or invalid uses the default of 3), waiting FM_FLEET_SYNC_PACKED_REFS_LOCK_RETRY_WAIT_SECS seconds (nonnegative whole or fractional; invalid falls back to 1 second) before each. Only after those retries exhaust does it remove the lock, and only when it is provably stale - still present, mtime age at least FM_FLEET_SYNC_PACKED_REFS_LOCK_AGE_SECS (default 30), and no lsof holder of the lock file or of the clone worktree itself (a live git keeps that as its cwd even in the window after it closes the lock and before it exits). A live lock, a missing lsof, any failed check, or any other fetch failure keeps today's behavior. Every wait, retry, and removal is printed to stderr, and a successful recovery also prints one recovered: summary line to stdout so a session-start refresh - which discards fleet-sync stderr and relays only stdout - still surfaces it. The shared staleness proof lives in bin/fm-lock-lib.sh, which both fm-teardown.sh and fm-fleet-sync.sh use.