# Configuration Configure where Firstmate keeps its files, which tools launch workers, and how supervision runs. Start with the directory layout, then use the setting reference for the behavior you want to change. ## Find a setting | What you want to configure | Start here | | --- | --- | | Firstmate's code, private files, or project location | [FM_HOME](#fm_home) and [operational home layout](#operational-home-layout-and-state) | | Task windows and worker tools | [Runtime backend](#runtime-backend-configbackend--fm_backend) and [harness support](#harness-support) | | Worker permissions, accounts, or environment | [Codex approval review](#codex-automatic-approval-review), [Claude permission mode](#claude-permission-mode-configclaude-permission-mode), [Codex permission mode](#codex-permission-mode-configcodex-permission-mode), [persistent managed-policy failover](#persistent-managed-policy-failover), [worker account pin](#worker-account-pin-configclaude-account-configpi-account), [several Claude accounts](#several-claude-accounts-configclaude-accounts), [worker tool exclusions](#worker-tool-exclusions-configcrew-exclude-tools), [worker launch environment](#worker-launch-environment-configlaunch-env-allowlist), and its [Orca environment source](#orca-environment-source-configlaunch-env-tmux-session) | | Backlog, preferences, and memory | [Backlog backend](#backlog-backend-taskstoml--configbacklog-backend), [captain preferences](#captain-preferences-datacaptainmd--datacaptain-sharedmd), [external memory tiers](#external-memory-tiers-memory-layering-section), [record export](#record-export-configrecord-export-root), and [startup memory budget](#startup-memory-budget-configstartup-memory-budget) | | Supervision and presentation | [Pi supervision branch](#pi-supervision-branch), [supervision host](#supervision-host-configsupervision-host), [Codex native Stop park](#codex-native-stop-park-configcodex-stop-park), [warm-cache compaction](#warm-cache-compaction-configwarm-compact), and [Calm preference](#calm-preference-configcalm) | | Persistent secondmates | [Secondmate routes](#secondmate-routes-datasecondmatesmd) | | Review bots on owned GitHub pull requests | [Contribution review bots](#contribution-review-bots-configcontribution-review-bots) | | Per-run overrides and tuning | [Environment variables](#environment-variables) | ## FM_HOME `FM_HOME` selects the operational home for one firstmate instance. | Location | What it contains | Default relationship | | --- | --- | --- | | Firstmate repo root | Shared code, including the scripts in this repo's `bin/` | Most scripts also use this as the operational home when `FM_HOME` is unset. | | Operational home | Private `state/`, `data/`, `config/`, and `projects/` | Selected by `FM_HOME`. | | Projects directory | Local project clones | Under the operational home; `FM_PROJECTS_OVERRIDE` can select a different directory for tests and specialized harness setup. | When `FM_HOME` is unset, most scripts use the repo root as the home. When it is set, scripts still run from this repo's `bin/`, while `state/`, `data/`, `config/`, and `projects/` come from `$FM_HOME`. ### Root and directory overrides `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. Normal `bin/fm-send.sh` sends require `FM_HOME` to be set before resolving a target, but `bin/fm-send.sh --help` prints its usage without resolving a target or requiring `FM_HOME`. Normal sends do not use the general fallback, because a steer must not silently resolve against the wrong home. These variables override individual operational directories for tests and specialized harness setup: | Variable | Directory selected | | --- | --- | | `FM_STATE_OVERRIDE` | Runtime state | | `FM_DATA_OVERRIDE` | Durable private records | | `FM_PROJECTS_OVERRIDE` | Local project clones | | `FM_CONFIG_OVERRIDE` | Local configuration | ### Relative paths and lifecycle safety Before `fm-brief.sh`, `fm-spawn.sh`, or `fm-afk-launch.sh` saves a path or passes it to another process, it handles each applicable `FM_HOME`, `FM_STATE_OVERRIDE`, or `FM_DATA_OVERRIDE` directory as follows: - Resolve relative directories against the caller's working directory. - Preserve accepted absolute spellings unchanged. - Reject an unresolvable relative directory and name the offending variable. `fm-spawn.sh` additionally rejects control bytes in those raw directory inputs before shell or filesystem normalization can change which path the backlog gate checks. Lifecycle access to a backlog, task record, or pending-close record must resolve within its configured data or state root, and a final-component symlink is refused even when its target remains within that root. Bootstrap applies the same relative `FM_HOME` resolution only when embedding that home in the generated Relay poll shim. `fm-contributions.sh arm` resolves a relative `FM_CONFIG_OVERRIDE` against the caller's working directory and embeds the resulting absolute config directory in its generated check, so later watcher polls keep the same review-bot policy. Other transient consumers retain their existing shell-relative behavior. ### Backend labels and containers | Backend | Effect of the operational home | | --- | --- | | herdr | `FM_HOME` determines the adapter's workspace label. | | zellij | `FM_HOME` determines the readable home prefix in visible tab titles, but does not split containers; use `FM_ZELLIJ_SESSION` for a separate session; the full home label also includes a short hash of the resolved `FM_ROOT` path. | | cmux | `FM_HOME` determines the default config path and readable home prefix in workspace titles; `FM_CONFIG_OVERRIDE` overrides where `config/cmux-socket-password` is read; the full home label also includes a short hash of the resolved `FM_ROOT` path; there is no per-home container split. | | tern | `FM_HOME` determines the readable home prefix used with the shared home-tag session label (via `bin/fm-backend-hometag-lib.sh`); the full home label also includes a short hash of the resolved `FM_ROOT` path. | | hc | `FM_HOME` determines the home tag in each task terminal's `firstmate.home` label and the default Herdr.app space (via `bin/fm-backend-hometag-lib.sh`); `HC_SESSION` selects the core. | ## Contribution review bots (config/contribution-review-bots) The contribution watcher admits comments, reviews, and inline review comments from configured bot logins on owned GitHub pull requests and routes them to the owning home as fleet triage. Review-bot watching is opt-in: a home with no file, or an empty one, admits no bot, and there is no built-in bot list. The file is local, gitignored, and not inherited by secondmate homes, so each home lists its own bots. List each bot's exact GitHub login to watch it, for example `greptile-apps[bot]`. These signals never become maintainer verdicts or merge authority. A bot signal's text is third-party content for the owning home to triage, never instructions to follow. [`fm-contributions.sh --help`](../bin/fm-contributions.sh) owns the exact allowlist format and polling contract. ## 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 shared instructions, skills, documentation, workflows, and `bin/`. Each effective `FM_HOME` contains private operational directories. `data/` holds durable private fleet records: - Project and secondmate registries. - Captain preferences and optional shared captain preferences. - Learnings, backlog, briefs, scout reports, optional task status archives, and the optional per-task no-mistakes pipeline-spend ledger. - Explicitly installed content-addressed extension packages under `data/extensions/packages/`. `state/` holds runtime records: - Task metadata, append-only status events, and endpoint signals. - Watcher and wake-queue coordination, away-mode state, and generated Relay artifacts. - Inactive terminal-outcome receipts under `state/terminal-outcomes/`. - Enabled extension working namespaces under `state/extensions/`. - Parent-side remote ledger copies under `state/secondmate-summary-cache/`. - One-shot Bearings reconcile requests under `state/reconcile-notify/`. - Private secondmate config-reread generations with their retry and quarantine state. - Per-task steering-inbox records under `state/.inbox/` (`bin/fm-task-inbox-lib.sh`). - Parent-owned secondmate pending-reply records under `state/pending-replies/` (`bin/fm-pending-reply-lib.sh`). `config/` holds local gitignored operating choices, including explicit extension bindings under `config/extensions.d/`. `projects/` holds local project clones. Firstmate reads these clones, but changes them only through the narrow guarded and concrete captain-approved exceptions in `AGENTS.md`. Untracked files and directories whose names begin with `scratchpad` are also gitignored, so temporary scratch does not make porcelain-based secondmate sync guards treat a home as dirty. ### Format and lifecycle references - `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. - [`bin/fm-contributions.sh`](../bin/fm-contributions.sh) owns durable published-contribution records under each task, observation bounds, equivalent triage-label configuration, the [review-bot setting above](#contribution-review-bots-configcontribution-review-bots), the opt-in `FM_CONTRIBUTIONS_REPLY_AGE` setting (seconds, unset by default) that turns on re-raising unanswered review threads after that age, and the authenticated contribution check. - The producing PR and Relay helpers own the fields they append, [`bin/fm-classify-lib.sh`](../bin/fm-classify-lib.sh) owns status-event vocabulary, optional emission-time syntax, and legacy unknown-time handling, and `bin/fm-crew-state.sh` owns current-state reconciliation. - The [`bin/fm-fleet-snapshot.sh` header](../bin/fm-fleet-snapshot.sh) owns the snapshot's event-time and age fields, including secondmate parent-event projections. - Wake, watcher, away-mode, and Relay-specific state mechanics remain with their named scripts and reference sections rather than being duplicated into one exhaustive state tree here. ### Session-start references - `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. - `bin/fm-startup-network.sh`'s header owns the deferred startup stage that keeps every external-network call and the potentially slow inactive-outcome scan off that digest's blocking path, including its state files and the safety argument for running them later. - `docs/sessionstart-nudge.md` owns the native session-open adapter tiers that run or nudge the digest command, and the source routing between them. - `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`. ## Orchestrator behavior (AGENTS.md) The shared orchestrator behavior lives in [`AGENTS.md`](../AGENTS.md). Edit it like any prompt when the fleet is empty. While tasks are in flight, dispatch shared-repo edits to a crewmate. ## Calm preference (config/calm) The Pi Calm extension and the Claude Code Calm mod share the local, gitignored `config/calm` preference under the effective Firstmate home. One `/calm` choice therefore applies on either harness. Both resolve the home in this order: `FM_HOME`, `FM_ROOT_OVERRIDE`, then the tracked code root derived from their own path under it. When `FM_CONFIG_OVERRIDE` is present for tests or specialized setup, it selects the config directory directly. ### Values and default | Value or file state | Result | | --- | --- | | `on` | Calm on. | | `off` | Calm off. | | Absent, unreadable, or unrecognized | Defaults to off. | Both written values end with one newline. `max` is a legacy value from a removed third presentation level. Its behavior is now ordinary Calm, so it is still read as `on`. A home upgraded from `max` keeps Calm on rather than dropping to off. ### Saving and reloading the preference Each `/calm` command saves the new choice before changing live presentation. A failed write leaves the current choice unchanged and is not reported as a saved preference. Pi replaces the file atomically; the Claude Code mod uses the plugin API's plain file write. The Pi extension reloads this preference on every Pi `session_start`, including startup, new, resume, fork, and reload reasons. The Claude Code mod reloads it on every `session.start`, including same-process session replacement. It also loads the preference lazily before any row that can draw ahead of that event, including during `claude --continue` restoration. This preference is local to each Firstmate home and is not part of secondmate inherited configuration. ## Pi repeated-read containment The tracked `.pi/extensions/fm-primary-read-repetition.ts` extension provides an optional read-loop safeguard for a Linux Pi primary. It is off by default, including when configuration is absent or omits `enabled`. When enabled, it stops the owning primary after three identical recognized reads return the same successful result within fifteen minutes. It blocks the next call before execution and requires fresh interactive or RPC user input to resume. Extension input and marked operational follow-ups cannot reset that stop, including the turn-end guard's recovery message. The stopped owner also cancels compaction so the aborted turn cannot start a separate compaction request. Changing results, errors and nonzero exit codes break the repetition sequence. An identical call already in flight is temporarily blocked without being counted as a completed success. Optional `config/read-repetition.json` configures the guard at extension load: ```json {"enabled": true, "repeatLimit": 3, "windowMs": 900000} ``` `repeatLimit` must be an integer from 2 through 32, and `windowMs` an integer from 1000 through 86400000. An absent file uses `enabled: false`, `repeatLimit: 3` and `windowMs: 900000`; malformed JSON, unknown keys or invalid values report an extension-load error instead of silently substituting limits. `enabled: false` registers no guard handlers. Configuration changes require the next extension load. Ownership requires the effective home, current directory, session directory and primary PID marker to match this primary, with the session lock naming its process or a verified ancestor. Workers, supervision branches, foreign owners and nested capability probes remain outside the guard. Linux ancestry uses bounded `ps` queries, including when a wrapper holds the lock. Enabled configuration on macOS or Windows reports an unsupported-platform load error; those platforms have no protection claim. Missing ownership evidence leaves the guard inactive without writing session history or aborting another session. A proven owner records `fm-read-repetition-activation-v1` in its Pi session journal with the effective limits and restored latch state; extension discovery alone is not evidence of activation. The recognizer counts Pi's native `read`, `grep`, `find` and `ls` tools plus a conservative subset of ordinary shell reads. It recognizes a narrow SSH read shape without executing or interpreting its command. Supported shell forms include simple `cat`, bounded `head`/`tail`, `sed -n N,Mp`, pipeline `cut -cN-M`, selected `grep` options, `nl -ba` with optional `-vN` line numbering, `wc -l`/`-c` and `od -An -tx1c`/`od -An -c`. One outer parenthesized group may contain a sequence of recognized reads separated by semicolons or newlines, including `cd /project`, read pipelines and unquoted backslash-newline continuations. Every stage must be recognized; nesting, brace groups, shell substitutions, redirections other than `2>&1`, additional shell interpreters and unknown programs or options are uncounted. Ungrouped multiline shell commands and polling tools are also uncounted. An unrecognized stage makes the entire shell command uncounted, even if its output repeats hundreds of times. This is a repetition detector, not command authorization or comprehensive loop detection. History contains bounded hashes, labels and timestamps rather than read arguments or output; the stopped latch survives reload and compaction. SDK-generated temporary paths in truncated Bash footers are normalized only when matching truncation metadata identifies them. The extension uses the existing tracked-file distribution and trusted Pi project auto-discovery described in the [setup guide](../README.md). The supported interception surface is Pi's own session event pipeline, including RPC sessions; its tool-call, result, input, session and compaction-veto hooks must all execute. Other primary harnesses, worker interruption and tool execution in a separate native adapter are outside this extension's protection. An adapter's `firstmate:native-tools` registration does not forward this interception or abort contract. The stop is stored as a displayed custom message, but a native app may filter that message; an abort alone does not prove that its detailed notice was shown. Current SDK and installed-harness evidence is recorded in [runtime backend verification](verification/runtime-backends.md#pi-repeated-read-containment). ### Migrating an ignored local guard The managed entrypoint and `lib/fm-primary-read-repetition-policy.ts` intentionally differ from the local workaround's `fm-read-repetition-guard.ts` and `lib/fm-read-repetition.ts`. An update therefore preserves those old ignored files and `config/read-repetition.json`, including with an updater from before this feature. If the legacy entrypoint is present, enabling the managed guard refuses to register its handlers and reports the migration needed; a broken symlink at that path also refuses activation. The legacy extension may still run under its own rules until it is migrated. Before enabling the managed guard, preserve both legacy files and any existing configuration in a private backup outside Pi's extension discovery directories, then move the legacy entrypoint outside `.pi/extensions`. Retain the backup and deliberately review the existing configuration before setting `enabled: true` and reloading extensions normally. No automatic migration, removal, overwrite, reload or live-process change occurs. For a locally installed file at either new managed path, preflight the update and use Git's `--no-overwrite-ignore` fast-forward option; an older updater does not protect arbitrary ignored-path collisions. ## Pi supervision branch On a Pi primary, an in-process supervision branch handles eligible task-local wake rows and selected heartbeat reviews. Main-only rows stay on the captain-facing path. [docs/pi-supervision-branch.md](pi-supervision-branch.md) defines its conversation lifecycle, row eligibility, mixed-queue dispatch, heartbeat routing, and pre-drain recheck. Supervision is default-on: once a Pi primary session owns this home's fleet lock, the branch is eligible for every task with no captain grant file required. Bash absorbs a genuinely no-op heartbeat before it reaches Pi. Every watcher-failure alarm stays on the captain-facing main path. If the branch breaks, wakes still fall back to main in both postures. The legacy `state/.afk` daemon flag has no effect on Pi. ### Attended and away authority While the away-posture record `state/.afk-contract` exists: - The branch takes every actionable row. - No processing turn opens on the parked main. - Main's standing authority moves to the branch through the guarded scripts, each retaining its own gate. [docs/pi-supervision-branch.md](pi-supervision-branch.md#postures) defines that posture. While attended, the branch cannot merge a PR, land local work, freshly spawn, or answer a decision. These are the bounds set by the captain-approved architecture. Every existing captain gate remains unchanged in either posture. Homes on other primary harnesses do not load the Pi branch extension; shared per-task lease behavior is owned by `bin/fm-lease-lib.sh`. `AGENTS.md`'s `state/` inventory routes the branch's runtime files to their format and lifecycle owners. ### Outcome delivery and acknowledgement While attended, a captain-facing branch outcome (verdict `captain`) is saved as one exact visible transcript entry keyed by sequence. It then opens one processing turn on main for that sequence. The turn stays open until main acknowledges the sequence through its `fm_branch_processed` tool. While away, the entry is saved, but processing waits until the away-posture record is archived. The branch prompt's "Verdict: routine or captain" section owns the distinction between captain-facing, unsolicited routine, and unchanged-review outcomes. The generated [Pi supervision protocol](supervision-protocols/pi.md) owns main's event ownership, acknowledgement duty, and conversational treatment for merged outcomes, while the persisted entry itself owns captain visibility. A task-level routine no-change outcome or a no-change heartbeat explicitly reported with `silent=true` is delivered without a rendered note; the branch prompt owns task-level eligibility, and every other routine outcome still appends a rendered, sailboat-prefixed note. ## Pi supervision branch model and effort (config/supervision-branch-model, config/supervision-branch-effort) The branch can run on a cheaper model than main because supervision is an easier job than the captain's own conversation. It can also use a lower reasoning effort because supervision needs less reasoning than that conversation. ### Choose a model and effort The Pi `/supervision-model` command settles both in one flow: it opens a selector over the models that Pi reports with configured credentials and that this home's stored credentials let the isolated supervision branch resolve, plus a first "Follow main" entry, and then a second picker for the branch's reasoning effort. In Pi's terminal TUI, the model step uses Pi's bounded scrolling list with its input and fuzzy filtering primitives, the same list primitive Pi's `/model` picker scrolls: typing filters the entries, "Follow main" stays the first entry whenever it still matches, and a long catalog scrolls inside the dialog instead of running off the terminal. The non-TUI RPC, JSON, and print modes have no custom-component surface and keep Pi's generic selector without search, where terminal overflow does not apply. The effort list is a handful of levels and stays on Pi's plain selector dialog. Both picks change the supervision branch alone and never the captain's own conversation model or effort. ### Saved settings and available models The command saves the model pick in gitignored `config/supervision-branch-model` and the effort pick in gitignored `config/supervision-branch-effort`. Both live under the effective Firstmate home, resolved in this order: `FM_HOME`, `FM_ROOT_OVERRIDE`, then the tracked code root derived from the extension path. When `FM_CONFIG_OVERRIDE` is present for tests or specialized setup, it selects the config directory directly. Firstmate keeps no model catalog of its own. The list is the intersection of what Pi reports when the picker opens and what a fresh isolated branch runtime can run. A provider that exists only because an extension registered it inside the captain's session, such as pi-devin-auth's `devin`, is offered and can be pinned or followed like any other; [pi-supervision-branch.md](pi-supervision-branch.md#cost-model-and-the-byte-stable-prefix) owns how that registration reaches the isolated branch runtime. Stored OAuth and API-key credentials retain their native credential type because Firstmate never copies, converts, installs, or overwrites credentials for the branch runtime. ### Model file format and default The model file holds one `/` line followed by one newline. Parsing splits at the first `/`, so a provider-qualified model id such as `openrouter/anthropic/claude-sonnet-4-5` survives intact. An absent, unreadable, or unparseable file means no pin. The branch then follows main's current model, applied explicitly and live whenever main changes models mid-session. ### Following a native Codex model When main uses `codex-native`, following main explicitly selects the same model through ordinary Pi's `openai-codex` provider, so the background branch owns an independent Pi conversation. If that ordinary Pi model is unavailable, the branch refuses to build and returns the notification to main; it never inherits the main native thread or silently selects a different model. Picking "Follow main" under a `codex-native` main reports that same `openai-codex` model, or that same refusal, because the command and the branch build share one follow rule. A `codex-native` branch pin is refused and excluded from the picker. ### Applying and changing a model pin A valid pin wins over main and remains unaffected by main's model changes. Picking "Follow main" removes the file, and the command writes a pin at mode `0600` and replaces it atomically so a failed write leaves the current choice unchanged rather than claiming persistence. The file decides the branch model on every build: the new conversation opened at each main session start, and a reopen after a model or effort change within a session. It overrides the model Pi would otherwise restore from the reopened branch session, so the choice survives both cases. That override is what keeps "Follow main" honest: a branch conversation that ran under an earlier pin still records that model, so clearing the file explicitly applies main's model rather than letting the reopened session restore the old one. ### Unavailable models For ordinary Pi providers, an unpinned build passes no model override only when main's model is unknown or this home's stored credentials cannot run it in the isolated branch runtime. This preserves the behavior from before the file existed. Model choice never loses the wake. If main's model could not be applied, the command reports that failure instead of reporting a change that did not take effect. If a pin names a model Pi cannot return because it is unknown or has no configured credentials, the branch refuses to build. It sends the accepted wake back to the watcher's captain-facing main path, as any other unreachable branch does. It never silently falls back to main's model. Picking also releases the live branch so the next wake reopens this session's own branch conversation under the new model without waiting for a session replacement. ### Effort file format and available levels The effort file holds one Pi thinking level followed by one newline. Model and effort pins are independent: a captain may pin either, both, or neither. The effort step follows the model step because the effective branch model determines which levels exist. The menu uses Pi's own supported-level list. Models without extended levels do not offer them; a non-reasoning model offers only `off`. The picker keeps no effort catalog of its own; when main's model cannot be resolved, it first resolves the model recorded by the most recent branch conversation and uses Pi's supported levels for that effective model. If neither model can be resolved, the picker invents no levels and the command says that the branch's effective effort cannot be determined. ### Applying and changing an effort pin An absent, unreadable, or unrecognized file means no effort pin. The branch then follows main's current effort, applied explicitly and live whenever main changes effort mid-session. A valid pin wins over main and remains unaffected by main's effort changes. Picking "Follow main" removes the file, and the command writes an effort pin at mode `0600` and replaces it atomically, exactly as it writes a model pin. The effort file's current state decides the branch effort on every branch build, on the same create-and-reopen contract as the model pin and for the same reason: a reopened branch conversation records the effort it last ran under, so only an explicit override keeps "Follow main" honest. Only when main's own effort cannot be read either does an unpinned build fall back to passing no effort override at all, which is the behavior from before this file existed. ### Unsupported effort levels Pi maps an unsupported pinned level to the branch model's nearest supported level. Effort never causes the branch to refuse a build. The captain's raw pick is kept so it applies again on a model that supports it, and the command reports the level the branch will actually use. An effort token Pi would not recognize at all is treated as no pin rather than passed to that clamp, which would otherwise collapse a typo into the model's lowest level. ### Cancellation and inheritance Cancelling the model picker cancels the whole command and changes neither choice. Cancelling only the effort picker keeps the standing effort choice and still applies the model pick from the same run. The command's closing message reports both choices as they will take effect. Both choices are local to each Firstmate home and are not part of secondmate inherited configuration, the same as the Calm preference; a secondmate home pins its own supervision model and effort with its own `/supervision-model`. ## Supervision host (config/supervision-host) Two optional local, gitignored files control the supervision host for this home: `config/supervision-host-off` opts the home out, and `config/supervision-host` opts a home in and selects its engine. The host runs the supervision branch's contract on a headless engine session beside a non-Pi primary. [docs/supervision-host.md](supervision-host.md) defines its design, current scope, and verified engines. A Claude, Cursor, OpenCode, omp, Grok, or Codex primary can run the host. A present `config/supervision-host-off`, whatever it holds, opts the home out on every primary. Otherwise a Claude primary runs the host by default: with no `config/supervision-host` it runs exactly as with an empty one, at the Claude engine's default model. A Cursor, OpenCode, omp, Grok, or Codex primary runs the host only while `config/supervision-host` exists and the home is not opted out. A home that does not run the host behaves exactly as it does without it, and a Pi primary keeps its in-process supervision branch whatever either file says. `fm_supervision_host_enabled` in `bin/fm-supervision-engine-lib.sh` implements this gate for every reader. While the home runs the host, the primary's arm owner runs it in place of the watcher arm. The host handles wakes on the engine under the [posture rules](supervision-host.md#postures), including an away record and attended operation on a Claude or Cursor primary with a verified dialog mirror. On that home, `/afk` launches no away daemon; see [Quiet mode](supervision-host.md#quiet-mode) for `/quiet`'s attended statement and fallback. The same gate governs the primary's dialog-mirror hooks (`bin/fm-host-mirror.sh`), which record on a Claude or Cursor primary ([supervision-host.md](supervision-host.md#the-dialog-mirror)). Grok's arm command is rendered at session start, so a change to its host mode takes effect at its next session start; the other arm owners check the gate at every arm. ### Engine selection `config/supervision-host` may be empty or hold one line ` []`: - empty or `default` selects the primary harness's own engine at that engine's default model (`sonnet` for the Claude engine); - ` []` names a verified engine, currently only `claude`, and optionally the engine's own model name or alias; `default ` selects the primary harness's engine with that model. Only Claude has a verified engine of its own, so a Cursor, OpenCode, omp, Grok, or Codex home names `claude` in the file. ### Failures, when changes apply, and inheritance An unverified engine, a primary without a verified engine, or a malformed line leaves the host without an engine. It takes no wake, so every wake reaches main as it would without the host. Each away-posture wake includes a line naming the problem. The running host reads both files at every wake, so an engine change or an opt-out takes effect at the next wake without a restart. The opt-out is inherited into secondmate homes: a primary that opts out also opts its secondmates out, and clearing it restores each mate's own host setting at its next spawn or convergence. The primary-authoritative propagation contract, including removal of a mate's local opt-out when the primary has none, is owned by [`secondmate-provisioning`](../.agents/skills/secondmate-provisioning/SKILL.md). `config/supervision-host` is local to each home and not inherited, because each home's engine and model are its own choice. While the home runs the host, main's lease-checked commands also take the per-task lease lock, so a claim by the host's engine cannot race a mutation main already started (`bin/fm-lease-lib.sh`). ### Acknowledged hand-back (config/supervision-handoff-ack) Create the empty, home-local `config/supervision-handoff-ack` file to enable acknowledged hand-backs on a Claude primary that runs the supervision host. The flag is off when absent, is not inherited, and has no effect on other primaries or the host-off Stop path. The host reads its presence at each hand-back, so enabling it needs no session restart. Removing it stops further retries and leaves the current watcher for the ordinary next park. [Supervision host](supervision-host.md#acknowledged-hand-back) owns the acknowledgement, retry transport, and limits. ## Codex native Stop park (config/codex-stop-park) Create the empty, home-local `config/codex-stop-park` file to let a Codex primary wait for watcher wakes outside the model between turns. The feature is off when the file is absent, and every Codex session then keeps the bounded foreground checkpoint protocol. With the file present, only an interactive native Codex session on the verified CLI version that holds this home's session lock parks; every other session keeps the foreground checkpoint. The supervision protocol is chosen when the session starts, so create or remove the file before starting the Codex session. The file is local to each home and is not inherited by secondmate homes. [The Codex protocol](supervision-protocols/codex.md) owns the parked operating protocol, and `bin/fm-codex-stop-park.sh` owns version selection, registration, and failure fallback. ## Warm-cache compaction (config/warm-compact) Create the home-local `config/warm-compact` file to compact an idle Claude primary and the supervision host's engine conversation before their prompt cache expires. A session that sits idle past the cache lifetime re-writes its whole context at the cache-write price on its next turn. Compacting while the cache is still warm reads the old context at the read price, so the next turn writes only the summary. The feature is off when the file is absent. The file is local to each home and is not inherited. The host reads it at every check, so creating or removing it needs no restart. It acts only on a home that runs the [supervision host](#supervision-host-configsupervision-host) beside a Claude primary, because the host's park is the primary's idle episode. The file may be empty or hold these lines, each optional: - `idle-seconds=` is the idle time before a session is due (default 2700). - `min-context-tokens=` is the context size below which a session is never due (default 80000). Blank lines and lines that start with `#` are ignored, and an unusable value keeps its default. Context size is the last request's `input + cache_read + cache_creation` usage in the session's transcript. A session is compacted when all of these hold: - The home opted in. - The session has been idle for `idle-seconds`: the primary since its turn ended, the engine conversation since its last handled turn. - Its context is at least `min-context-tokens`. - It was not already compacted in this idle episode. The host remembers a primary compaction for one park. It records an engine compaction in the engine record, so a later host does not repeat it. - For the engine conversation, the next turn would resume it: the same engine, model, and prompt key, and below the rotation limit. An abandoned compaction is not recorded, so a later check retries it. - For the primary, the agent reads idle and the composer reads provably empty. A busy agent, a non-empty composer, or an unproven state defers the compaction; nothing is typed. The primary receives `/compact` through the backend's verified submit primitive (`fm_backend_send_text_submit`), the one the away-mode daemon injects through. The engine conversation receives `/compact` as one resumed, bounded headless turn. A compaction is housekeeping: it adds no wake, outcome, or close line, and it is recorded only in the host's log. A compacted primary still picks up pending wakes and decisions from disk, because a wake is a record the next drain reads, not conversation state. This feature does not set `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` or any other autocompact setting. `bin/fm-warm-compact-lib.sh` owns the guards, with coverage in [`tests/fm-warm-compact.test.sh`](../tests/fm-warm-compact.test.sh) and the opt-in live guard [`tests/fm-warm-compact-herdr-live-e2e.test.sh`](../tests/fm-warm-compact-herdr-live-e2e.test.sh). ## 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`. A home may instead select another tasks-axi adapter such as Beads through its own `.tasks.toml` or `TASKS_AXI_BACKEND`; firstmate still uses only tasks-axi verbs for routine backlog reads and mutations, and the adapter maps `start` and evidence-bearing `done` transitions to its native statuses and evidence fields. ### Captain holds on Beads Captain-hold row creation is owned by [`bin/fm-captain-hold.sh`](../bin/fm-captain-hold.sh) `hold`: when no work item exists, it creates an ordinary backlog row (`--kind captain` metadata; Beads native type `task`) and then applies the captain hold. Captain rows have no Beads due semantics, so that create path waives a Beads `due.required` setting rather than passing a synthetic `--due`; `--until` remains the optional hold deferral. Do not register a Beads `types.custom` `captain` type for this: captain is a hold kind, and the fleet Beads `due.required` policy for ordinary work stays in the federated beads config. ### Automatic dispatch and completion When the automatic transition gate applies, dispatch and completion each move the work item in the same run that creates or removes its task record. The ordinary successful path therefore keeps the backlog and live task set in sync ([`bin/fm-backlog-transition-lib.sh`](../bin/fm-backlog-transition-lib.sh)). Under that gate, dispatch accepts only an unheld, unblocked Queued or In flight item in this home; a missing, Done, held, or dependency-blocked item is refused before any endpoint or local copy is created. [`bin/fm-tasks-axi.sh`](../bin/fm-tasks-axi.sh) refuses `add --start` and its `create --start` alias. Either would place a row In flight without a task record, status file, or inbox, counting it as live work that nobody is doing. The wrapper still passes through the documented direct transition `tasks-axi start `. Completion refuses to report success until the item is closed, and session start reconciles this home's own books after an interrupted run. When a spawn is interrupted after launch delivery began, its exit path re-reads the paired task record and the backlog row under the same per-task lock as the commit, repairs a row the commit believed it had moved, and reports only what was verified or honestly attempted, never intent phrased as outcome ([`bin/fm-spawn.sh`](../bin/fm-spawn.sh); [`tests/fm-backlog-atomicity.test.sh`](../tests/fm-backlog-atomicity.test.sh)). ### Which backlog receives a transition Automatic transitions run from the configured data directory's parent, letting that home's effective tasks-axi configuration address its selected adapter while keeping relative scout-report links rooted there. A markdown backlog is additionally addressed by an explicit `--file` at `/backlog.md`, so the change lands in the home that owns the task regardless of the caller's working directory. Any other configured adapter is addressed by that root alone, because `--file` would override the adapter's own workspace path. ### Exemptions and refusal conditions The gate does not apply to persistent secondmates, manual-backend homes, or markdown homes without a backlog file. Those retain their existing persistent-agent, manual, or ad-hoc lifecycle behavior. Configured non-markdown adapters remain active without that file. Migrated-hold resolution on a beads home reads its graph path, binary, and prefix from the root `.tasks.toml` `[beads]` section only, and refuses (rc=2) when the beads backend is selected elsewhere (a `TASKS_AXI_BACKEND` override or user-level config) with no root-level `[beads]` section. On an automatic-backend home, missing or incompatible `tasks-axi`, an unresolvable configured data directory, or one containing a control byte fails lifecycle work before mutation. An unreadable backend configuration can refuse lifecycle work before the no-backlog exemption applies; repair the configuration named in the diagnostic ([backend resolution contract](../bin/fm-tasks-axi-lib.sh)). ### Handoffs between homes Secondmate handoffs bypass that routine-backend choice: `fm-backlog-handoff.sh` keeps only its own fleet-level validation and delegates the item move to `tasks-axi mv`; its [script header](../bin/fm-backlog-handoff.sh) owns route-specific wake outcomes and remote outbox release. 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. ### Required tools and manual mode Compatible means the installed build passes the shared version and feature probe owned by [`bin/fm-tasks-axi-lib.sh`](../bin/fm-tasks-axi-lib.sh), including the atomic multi-ID move required by handoff delegation. Bootstrap requires compatible `tasks-axi` on every profile; see "Toolchain" below for required-tool diagnostics 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 required-tool diagnostics. A `manual` home owns its backlog file outright: the lifecycle transitions above are skipped there, dispatch and completion never fail over the file's contents, and a completed teardown prints the hand edit that is owed instead. Absent or `tasks-axi` selects the tasks-axi path. On the default markdown adapter, tasks-axi and manual edits produce the same `## In flight`, `## Queued`, and `## Done` sections. ### Using a separate operational home The tracked `.tasks.toml` paths resolve against the directory tasks-axi runs in, not `FM_HOME`, so a bare `tasks-axi` run from the code root addresses the code root's `data/` whenever the home lives elsewhere. tasks-axi replaces its target by renaming a temporary file over it. If the target is a symlink, the write replaces it with a regular file. Linking the code-root copy into the home therefore forks the queue on the first write instead of keeping the copies in sync. Run every routine Firstmate backlog command through [`bin/fm-tasks-axi.sh`](../bin/fm-tasks-axi.sh). Like lifecycle transitions, it addresses this home's backlog and archive from any working directory. Bootstrap reports a code-root `data/backlog.md` or `data/done-archive.md` that is not this home's own file as a `BACKLOG_RECONCILE: code-root ...` line, even in a read-only session. ## 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. | Runtime backend | Verification status | Reference | | --- | --- | --- | | `tmux` | Verified reference backend | [`docs/tmux-backend.md`](tmux-backend.md) | | `herdr` | Has its own required CI lane | [`docs/herdr-backend.md`](herdr-backend.md) | | `zellij` | Experimental; no dedicated real-backend CI lane | [`docs/zellij-backend.md`](zellij-backend.md) | | `orca` | Experimental; no dedicated real-backend CI lane | [`docs/orca-backend.md`](orca-backend.md) | | `cmux` | Experimental; no dedicated real-backend CI lane | [`docs/cmux-backend.md`](cmux-backend.md) | | `paseo` | Experimental; no dedicated real-backend CI lane | [`docs/paseo-backend.md`](paseo-backend.md) | | `rex` | Experimental; no dedicated real-backend CI lane | [`docs/rex-backend.md`](rex-backend.md) | | `t3code` | Experimental; no dedicated real-backend CI lane | [`docs/t3code-backend.md`](t3code-backend.md) | | `tern` | Experimental; no dedicated real-backend CI lane | [`docs/tern-backend.md`](tern-backend.md) | | `hc` | Experimental; the `tests-hc` lane runs the hc family against the pinned build | [`docs/hc-backend.md`](hc-backend.md) | Treehouse remains the worktree provider for tmux, herdr, zellij, cmux, paseo, rex, t3code, tern, and hc, since herdr, zellij, cmux, paseo, rex, t3code, tern, and hc are session providers only; Orca provides both the task worktree and terminal endpoint. ### Backend selection order New spawns choose the backend in this order: 1. An explicit `--backend` flag authorized for that exact task by a present captain instruction or the task's own accepted brief. A later task cannot inherit that authority by analogy. 2. `FM_BACKEND`. 3. The first non-empty line of local, gitignored `config/backend`. 4. Runtime auto-detection from `$TMUX`, `HERDR_ENV=1`, `TERM_PROGRAM=rex` with `REX_BLOCK`, cmux runtime signals, or the Herdr app signal. 5. 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 Rex's `TERM_PROGRAM=rex` with `REX_BLOCK`, which is checked before cmux's primary `CMUX_WORKSPACE_ID` marker and its documented fallback signals - tmux, herdr, or Rex started from inside a cmux terminal is the innermost, currently-executing layer, while cmux itself (a terminal application, not a nestable multiplexer) is checked after those nested runtimes. The Herdr app signal is checked after cmux; contradictory marker sets keep the earlier surface's verdict instead of switching. [`docs/hc-backend.md`](hc-backend.md#runtime-auto-detection) owns what the app-terminal signal means and why an open app window alone is not one. See [`docs/cmux-backend.md`](cmux-backend.md#runtime-detection) for why cmux can be selected when `CMUX_WORKSPACE_ID` is absent, and [`docs/rex-backend.md`](rex-backend.md#auto-detection) for why a nested tmux or Herdr inside a Rex block is never read as Rex. Auto-detected Herdr and Rex stay silent like tmux, while auto-detected cmux prints a stderr notice naming `config/backend` and `--backend tmux` because cmux remains experimental. For hc notices, missing-piece handling, and detection metadata, see [Runtime auto-detection](hc-backend.md#runtime-auto-detection). T3 Code, Zellij, Orca, Paseo, and tern are never auto-detected; select them by putting the name in a local `config/backend` file, by exporting `FM_BACKEND=`, or by telling the first mate in chat. Paseo and tern stay explicit-only because their runtime markers leak into every descendant process, so they cannot stand for a selection (see [`docs/paseo-backend.md`](paseo-backend.md#selection-is-explicit) and [`docs/tern-backend.md`](tern-backend.md#selection-is-explicit)); bare `HC_TERMINAL_*` markers leak the same way, so hc detection requires the Herdr app's own signal. ### Accepted backends and secondmate limits Any value other than `tmux`, `herdr`, `zellij`, `orca`, `cmux`, `paseo`, `rex`, `t3code`, `tern`, or `hc` is rejected until another adapter is implemented and verified. `fm-spawn.sh` accepts `tmux`, `herdr`, `zellij`, `orca`, `cmux`, `paseo`, `rex`, `t3code`, `tern`, and `hc` for ship and scout tasks; `backend=orca`, `backend=cmux`, `backend=paseo`, `backend=tern`, and `backend=hc` all still refuse `--secondmate` until secondmate launch semantics are designed for each, while `backend=t3code` supports secondmates and runs only the `claude` and `codex` harnesses. `codex-app` is not an accepted runtime backend yet; [`docs/codex-app-backend.md`](codex-app-backend.md) owns the Codex App boundary. ### Liveness classification The session-start secondmate liveness sweep and the watcher's secondmate liveness tick use 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. ### Dependency and socket checks - 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`](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 paseo spawn additionally version-gates against the installed `paseo` binary's version, requires `jq`, and starts the daemon with `paseo start` only when it is simply not up yet (see [`docs/paseo-backend.md`](paseo-backend.md) "Setup"), refusing loudly on a missing CLI, an old version, or an unreachable daemon. - A rex spawn additionally gates both the `rex` CLI and the running Rex server at API revision 5 or newer and requires `jq` (see [`docs/rex-backend.md`](rex-backend.md) "Setup"), refusing loudly on a missing CLI, an older API revision, or a server that cannot be reached or started. - A tern spawn requires `tern` and `jq`, refusing loudly when either is missing (see [`docs/tern-backend.md`](tern-backend.md) "Setup"); bootstrap reports a missing `tern` as `MISSING_MANUAL` with `https://stencil.so/tern`. - For hc spawn prerequisites, binary resolution, and compatibility gates, see [`docs/hc-backend.md`](hc-backend.md#setup). 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 metadata 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=`. - A paseo task additionally records `paseo_terminal_id=` and `paseo_workspace_id=`, with the terminal NAME as routing authority (see [`docs/paseo-backend.md`](paseo-backend.md) "Task shape and metadata"). - A rex task additionally records `rex_server=`, `rex_session_id=`, `rex_window_id=`, and `rex_block_id=`, with the immutable block id as routing authority (see [`docs/rex-backend.md`](rex-backend.md) "Task shape and metadata"). - An Orca task additionally records `orca_worktree_id=` and `terminal=`, with `window=fm-` kept as the shared firstmate alias. - A cmux task additionally records `cmux_workspace_id=` and `cmux_surface_id=`. - A t3code task additionally records `t3_thread_id=` and `t3_project_id=`, with `window=fm-` kept as the shared firstmate alias. - A tern task additionally records `tern_session_id=`, `tern_tab_id=`, and `tern_block_id=`. - For hc endpoint fields and automatic-selection provenance, see [Task shape and metadata](hc-backend.md#task-shape-and-metadata). ### Task selectors 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/.meta` wins before the legacy `fm-` 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, `t3_thread_id=` for T3 Code, 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 rules are the single owner of the task-selector vocabulary. Backend guides and other documents refer here instead of restating the resolution order. ### Teardown identity checks `fm-teardown.sh ` 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. When the worktree identity is missing, empty, or ambiguous, the refusal also explains how to identify the held copy and restore exactly one `worktree=` line without changing the record automatically. Legacy tmux metadata remains cleanup-compatible when its exact window name is `fm-`; opaque non-tmux endpoints require their recorded `endpoint_task_id=` binding. ### Herdr homes and presentation `FM_HOME` determines Herdr's home label: the primary home uses `firstmate`, and a secondmate home marked by `.fm-secondmate-home` uses `2ndmate-`. [`herdr-backend.md`](herdr-backend.md#watching-and-task-containers) owns launcher-bound workspace placement, the label-only fallback, collision handling, and recovery behavior. The local `config/herdr-presentation-spaces` file instead opts a home out of, or explicitly in to, Herdr's default-on disposable single-task visual projection; [Presentation spaces](herdr-backend.md#presentation-spaces) owns its accepted values, default, Herdr version floor, migration, behavior, safety limits, recovery contract, and narrow locked session-start cleanup of exact restored idle-shell children. The setting is inherited into secondmate homes under the primary-authoritative contract owned by [`secondmate-provisioning`](../.agents/skills/secondmate-provisioning/SKILL.md). The local `config/herdr-project-groups` file opts a home in to display-only Herdr project groups; [Project groups](herdr-backend.md#project-groups) owns its values, rules, tags, inheritance, and limits. ### Rex attention marks The local `config/rex-status-marks` file opts a home in to Rex attention marks: `on` sets a program-status badge and a notification on the pane of a Rex worker that needs the captain, plus validation progress in its hover card, and `off` (the default when absent) shows none of them. [Attention marks](rex-backend.md#attention-marks) owns the status mapping and its limits. The local `config/rex-tint` file opts in to a subtle background tint on waiting and failed workers: `off` (the default when absent), `subtle`, or `subtle-light`; [State tint](rex-backend.md#state-tint) owns its behavior and caveats. 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`](herdr-backend.md) instead of `herdr server stop`. ### Zellij sessions 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--`. Use the guarded cleanup path described in [`docs/zellij-backend.md`](zellij-backend.md) instead of `kill-all-sessions` or `delete-all-sessions`. ### cmux workspaces 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-`, 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--`. Test cleanup must use the guarded path in [`docs/cmux-backend.md`](cmux-backend.md#current-operation-and-safety), never enumerate-and-close every workspace. ### tern sessions tern uses one home-tagged session named `fm-` (or `FM_TERN_SESSION` in tests) and one keep-open tab/block per task. The caller-facing endpoint is `:`; tab titles are `fm--`; ids are 64-bit integers and must keep full precision. Test cleanup must use the guarded path in [`docs/tern-backend.md`](tern-backend.md#current-operation-and-safety) and only sessions named `fm-test-*`. `config/backend` is inherited into secondmate homes under the primary-authoritative contract owned by [`secondmate-provisioning`](../.agents/skills/secondmate-provisioning/SKILL.md). ## Away-mode supervisor backend (FM_SUPERVISOR_BACKEND / FM_SUPERVISOR_TARGET) The `/afk` sub-supervisor injects escalation digests into firstmate's own endpoint independently of where new task endpoints are spawned. It supports `tmux`, `herdr`, and `rex` supervisor panes and `t3code` supervisor threads. Set `FM_SUPERVISOR_BACKEND=tmux|herdr|rex|t3code` and `FM_SUPERVISOR_TARGET=` to override both axes explicitly. For tmux, accepted targets are `%pane-id`, `@window-id`, `session:@window-id`, a bare session, `session:window-name`, `session:index`, `session:index.pane-index`, and the supported `=session:=window` exact-marker forms; the `fm_backend_target_exists` header in `bin/fm-backend.sh` owns this grammar. For herdr, the target is `":"`; for Rex it is `:`, with both kind prefixes removed; and for t3code it is the thread id. Without overrides, backend detection uses `$TMUX_PANE` first, then `HERDR_ENV=1` with `HERDR_PANE_ID`, then `TERM_PROGRAM=rex` with both `REX_SESSION` and `REX_BLOCK`, then `HC_TERMINAL_ID` with `HC_TERMINAL_SOCKET` (resolving `hc`), then a cwd match over T3's live threads only when T3 Code is explicitly selected, then falls back to `tmux`. T3 discovery requires explicit `FM_BACKEND=t3code` or `config/backend=t3code` and the `config/t3code-token` credential from the [T3 sign-in](t3code-backend.md#sign-in) under the effective config directory before making a T3 call. That keeps a nested tmux or Herdr pane on its own 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 prefix-stripped `REX_SESSION` and `REX_BLOCK` under Rex, then `"${HC_SESSION:-default}:${HC_TERMINAL_ID}"` when both hc terminal markers are present, then the thread id from that T3 cwd match, then the legacy `firstmate:0` tmux fallback with a warning. Selecting or discovering any other supervisor backend, including `hc`, `zellij`, `orca`, `cmux`, or `tern`, 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. ### Channels and overrides `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:` (run `` 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`](wedge-alarm.md) for the current channel reference, [`verification/supervision.md`](verification/supervision.md#wedge-alarm-channels) for active evidence, and [`examples/wedge-alarm`](examples/wedge-alarm) for a copyable config. ## Trace context propagation (config/trace-context / FM_TRACE_CONTEXT) The optional local, gitignored `config/trace-context` presence flag enables default-off native W3C trace-context propagation. ### Precedence and session boundary `FM_TRACE_CONTEXT` overrides the file: `1`/`on`/`true`/`yes` enables, any other non-empty value disables, and unset or empty defers to the file. Each locked home session resolves those inputs once, and all spawns from that home use the frozen decision until a new session starts. ### Secondmate propagation When launching a Secondmate, the primary copies the presence flag into its home and passes the primary session's frozen decision as a non-empty `FM_TRACE_CONTEXT=on|off` override for the Secondmate's own session start. A Secondmate on a remote route is covered the same way: the primary resolves and records that task's carrier, and the configured host exports it and receives the same enablement snapshot. The presence flag is session-scoped enablement, so it transfers at launch and is left unchanged by live convergence into a running home. See [`trace-context.md`](trace-context.md) for carrier semantics, supported routes, the manual fleet-restart requirement, the session boundary, and safety limits; `bin/fm-trace-context-lib.sh`'s header owns the exact mechanics, and [`verification/trace-context.md`](verification/trace-context.md) records repeatable evidence. ## Fleet activity ledger (config/fleet-ledger) See [`fleet-ledger.md`](fleet-ledger.md) for the opt-in setup, record contract, and limits. ## Firstmate role (config/firstmate-role) The optional local, gitignored `config/firstmate-role` declares this home's relationship to the firstmate repository itself. Every home defaults to **user/operator** with no firstmate-upstream publication authority: ordinary project work and `/updatefirstmate` pulls are unaffected, but without an explicit grant no ship task on the firstmate repository may carry a publishing contract. For the agent's local diagnosis, workaround, and draft-only response to a firstmate bug, see [`firstmate-self-repo`](../.agents/skills/firstmate-self-repo/SKILL.md#when-a-firstmate-bug-surfaces-in-ordinary-use). The file holds exactly one directive line, with blank lines and `#` comment lines ignored: | Directive | Effect | | --- | --- | | `user` | The default; identical to an absent file. | | `maintainer //` | This home may dispatch publishing work that targets the one named firstmate upstream. | A scope names the publication target repository (where a pull request lands), normalized to lowercase for comparison, not a push destination. A fork maintainer contributing to the parent repository uses per-task consent rather than a second standing scope. Write the primary home's file by hand; firstmate never grants itself a role, and the file never travels through git. Supported secondmate propagation writes or removes the inherited copy as described below. A malformed, unreadable, or symlinked file - including one with a second directive anywhere - resolves to user/operator; the role fails closed rather than guessing. Authority is also available for one task at a time: `--upstream-consent //` on `bin/fm-brief.sh`, `bin/fm-spawn.sh`, and `bin/fm-promote.sh` records the captain's explicit approval in the task's delivery contract and metadata without changing the home's role. Role is never inferred from GitHub identity, forge permissions, remotes, repository ownership, authentication state, environment, or CI. `bin/fm-role.sh report` prints the resolved role with no network call, and the session-start digest shows it in the HOME ROLE subsection. `bin/fm-role-lib.sh` owns the fail-closed parser, self-repository detection, and the gate that `bin/fm-brief.sh`, `bin/fm-spawn.sh`, and `bin/fm-promote.sh` apply. The publication target is selected from the first configured remote whose `remote..gh-resolved` value is exactly `base`, including custom remote names, otherwise from the first configured remote in the order `upstream`, `github`, `origin`; selection is offline, and `bin/fm-role-lib.sh` owns URL and local-path normalization. An unreadable or URL-less marked remote is not replaced by a fallback remote, and an undeterminable target never authorizes a task, even under task consent. A task's project counts as the firstmate repository when it shares the code root's lineage - the same git common dir, or a root commit of the code root's history, which every fork, the parent, and any clone of either share - computed offline; undeterminable lineage (for example a shallow clone sharing no root commit) is refused, even under task consent. Secondmate homes inherit the primary's resolved role, never its raw file: a primary file that resolves to user for any reason propagates as revocation. In a secondmate home the file is honored only while the inheritance receipt `config/.fm-inherit-firstmate-role.generation` proves the exact bytes the primary last published (`bin/fm-config-inherit-lib.sh`); a role file with no matching receipt or a stale grant authorizes nothing. The receipt prevents accidental publication through supported paths; it proves consistency, not provenance, and is not a security boundary against a process that deliberately forges local records. ## Waiting worker spends no turns (config/wait-no-turns) The optional local, gitignored `config/wait-no-turns` presence flag opts this home into keeping a waiting worker from spending turns until it is answered. With it present, ship and scout briefs gain the `# Waiting` section and the foreground no-mistakes drive text, every brief's inbox section keeps the natural-checkpoint check and adds that a waiting worker does not poll or list its inbox because a waiting instruction rings, a pending-reply recovery waits while that mate has its own open decision or blocker, and a fire-and-forget steer whose doorbell did not land gets one later ring. With the file absent, generated briefs omit the waiting section and the no-poll inbox line, the drive text backgrounds the call, recovery sends during an open decision, and a fire-and-forget steer is not owed a retry ring. The flag is a home-local preference and is not inherited by secondmate homes. ## PR health monitor (config/pr-health-monitor) The optional local, gitignored `config/pr-health-monitor` presence flag opts this home into a mechanical monitor of the pull requests it owns. With the flag present, session start arms `state/pr-health.check.sh`, and the watcher's check sweep then prints one wake line for each new conflict, failing check, or change the author must make on an owned PR. Maintainer-gated CI and missing required reviews are recorded in `state/pr-health.json` for fleet review, but are not printed, so they never wake the owning task. CI waits on the maintainer when a check suite on the latest commit concluded `ACTION_REQUIRED`, which is how GitHub holds an outside contributor's workflows for approval; nothing has run, so such a PR is never read as green even when its check rollup is. A non-draft PR needs human approval when GitHub reports `REVIEW_REQUIRED` or has outstanding review requests (`reviewRequests`); the state is recorded without waking the task. The author must act only on an explicit signal: a `CHANGES_REQUESTED` review decision, or a maintainer label naming the wait, such as `pending-contributor-action` or `waiting-on-author` with or without a `namespace:` prefix. Only author-actionable states, conflicts, and failing checks print; the maintainer and approval states remain recorded without waking the owning task. Reads are driven by the GitHub notifications feed: each pass makes one conditional read of it, which GitHub answers with an uncounted 304 while nothing changed, and reads only the owned PRs with new notification activity, PRs not yet read, and PRs whose last read did not settle. Every owned PR is still read when the feed is unavailable or returns a full page of notifications newer than the saved position, and at least every 15 minutes as the fallback sweep, because some conditions, such as a base branch moving into conflict or a check from another app failing, raise no notification. GitLab merge requests are not monitored yet. When no reported check is marked required, a failing reported check still counts, matching `bin/fm-pr-state.sh` for unprotected stacked bases. When a required check has reported, optional failures do not notify. A conflict and an author action each wake once per latest commit, and a missing approval is recorded once per latest commit. A failing check wakes once per latest commit and check name, so a check that newly fails at an already reported commit wakes once more while an already reported check never repeats. A maintainer-gated suite is recorded once per latest commit and workflow name. A home owns a PR it recorded, linked, or observed before, and the open PR on a live task copy's current branch, drafts and adopted branches included, until the PR merges or closes. A PR URL that only appears in a task's status lines is not owned. Retiring a contribution with `bin/fm-contributions.sh retire` ends that task's ownership for the monitor too, and the branch lookup does not reclaim it. When a durable contribution record cannot be read, the monitor reports that once, reads nothing and keeps its fingerprints until the record is readable again. The monitor reads no other PR, so another home's PR never wakes this home, and it never comments on or changes a PR. `bin/fm-pr-health.sh` owns the conditions, the fingerprints in `state/pr-health.json`, and the read budget. The monitor only notifies; worker briefs and waiting rules are unchanged by the flag. With the file absent, nothing is armed and an already armed check prints nothing. Creating the flag takes effect at the next session start, or immediately after `bin/fm-pr-health.sh arm`. The flag is a home-local preference and is not inherited by secondmate homes. ## PR registration scan (config/pr-register-scan) The optional local, gitignored `config/pr-register-scan` presence flag opts this home into recording, without the supervisor, each PR a task reports. With the flag present, the watcher's check sweep runs `bin/fm-pr-check.sh scan` before the merge polls. The scan reads each ship task's newest `done:` line that names a `PR `, and records that PR through the ordinary registration path when the task has no `pr=`, or has that same `pr=` but no published merge poll. It skips persistent secondmate and scout records, a `pr=` that names another PR, and a PR whose merge was already reported or whose poll was retired. A refused registration, such as a draft, prints one `check: pr-register:` wake for each task and PR, and later sweeps retry without a new wake, so a PR marked ready later is recorded. With the file absent, the check sweep does not run the scan, and `pr=` and the merge poll come only from a manual `bin/fm-pr-check.sh ` run. The flag takes effect at the next check sweep, and is a home-local preference that is not inherited by secondmate homes. ## No-mistakes pipeline spend (config/pipeline-spend) The optional local, gitignored `config/pipeline-spend` presence flag opts this home into recording per-task no-mistakes pipeline spend in `data/pipeline-spend.jsonl` during teardown. When the flag is absent, teardown skips recording and the recorder exits before reading task metadata, no-mistakes state, or the spend ledger. An existing ledger is left untouched while recording is disabled. ## No-mistakes home resolver (config/nm-home-resolver) The optional local, gitignored `config/nm-home-resolver` file opts this home into finding each task's no-mistakes home through a site-provided resolver, for sites that run one no-mistakes daemon per isolated workspace, each against its own `NM_HOME`. The home cannot be derived from a task copy's `no-mistakes` remote, because every workspace of a project shares one gate URL. The file's first line is the absolute path of an executable the site provides; Firstmate calls it with the task copy's path as its only argument and never interprets the layout itself. When the resolver exits successfully within 5 seconds and prints exactly one line naming an existing directory by absolute path, that directory is exported as `NM_HOME` for that one call. Only host-side reads use it: current-state reads, pipeline-spend recording, the custody check, and teardown's parked-run lookup and `axi abort --run`, which read `axi status`, the `axi` overview, `axi logs`, `runs`, `daemon status`, and the state database. It never starts a daemon and never changes how runs are launched or driven, account routing, or no-mistakes' own `config.yaml`. An explicit non-empty `NM_HOME` always wins and the resolver is not called. With the file absent, a first line that is not an absolute executable path, or a resolver that fails, times out, or prints anything else, every read uses the ambient home exactly as without the file. The file is a home-local site choice and is not inherited by secondmate homes; `bin/fm-nm-run-lib.sh` owns the exact resolution. ## Lane capacity (config/lane-capacity) The optional local, gitignored `config/lane-capacity` file says how many ship and scout lanes this home should run at once, and when it should stop taking more. `bin/fm-capacity.sh` reads it to report how many lanes are open for queued work, alongside running lanes, 1-minute load against logical cores, free memory, and free disk. With the file absent nothing changes: the report prints counts and host figures only, never invents a target, never advises holding dispatch, and nothing is published; a malformed value is reported as invalid rather than guessed. Persistent secondmates hold no lane. The file is a per-machine choice and is not inherited by secondmate homes, so each home owns its own numbers. The first non-comment line is the cap, one non-negative integer, so a file holding only that integer keeps its earlier meaning. Optional lines follow, one key and its value per line, with `#` starting a comment: ```text 12 # the cap: lanes this home runs at once min-avail-gb 5 # keep this much memory available; a 1 GB lane must fit above it min-disk-gb 60 # every disk row must stay above this many GB pressure-file ~/.cache/lookout-beat/beat.json # repeatable; a lookout.beat 1.x document reserve-flag /var/run/fm-reserved.flag host reserved by the captain # repeatable; admit nothing while the file exists captain-idle 300 # opt-in, macOS: the captain counts as present when input was idle under 300 s quota-provider claude # opt-in: publish that provider's quota runway max-load1 20 # keep a raw 1-minute load rule while beats replace it ``` `pressure-file` and `reserve-flag` may repeat and take an absolute or `~/` path; a `reserve-flag` path may be followed by the label it is reported by. Every other key may appear at most once. An unknown key, a repeated single key, or a malformed value makes the whole file invalid: the report exits 1 naming the line, and the published facts carry the cap as invalid, because the captain's limits are never guessed. A pressure file is any producer of the `lookout.beat` 1.x shape, read by its own file age on this machine; a missing, stale, or unreadable one is skipped with its reason and the host's own probes stand in, so a broken beat never blocks anything. `captain-idle` reads input idle time locally and publishes only `present`, `idle`, or `away` (an away record), never the idle time itself. `quota-provider` is the only key that makes the script reach the network, through `quota-axi`, bounded to five seconds. While the file exists, the watcher runs `bin/fm-capacity.sh --publish` detached at most every 30 seconds, writing this home's lane facts to `state/lane-capacity.json`: its lanes, its cap and limits, its machine's pressure and boots, its watcher's freshness, and its own admission verdict. Publishing is best-effort and never changes what the watcher does; no spawn reads the verdict. `bin/fm-capacity.sh --admit` is an advisory check of the same verdict: it exits 0 when the verdict admits or the file is absent, and reports the reasons on stderr with exit 75 when it does not. No Firstmate script calls it. The script's header owns the report line, the published document's fields, the verdict, the beat freshness rules, the host-constraint thresholds, and the probe fallbacks. ## PR-ready line after green (config/ready-after-green) The optional local, gitignored `config/ready-after-green` presence flag opts a secondmate home into publishing its child's PR-ready parent line only after the child has reported checks green. Create it with `touch "$FM_HOME/config/ready-after-green"`; remove it to restore the default. It is off by default: without the flag, `bin/fm-pr-check.sh` publishes the line when it registers the PR, whatever the state of the checks. With the flag present and a `no-mistakes` task, registration still records the PR and arms the merge poll, but publishes the line only when the last `done` line in the child's status reports `PR checks green` for that same PR, with any text around it. A registration made earlier publishes nothing, and registering again after the green report publishes the line. The `direct-PR` and `local-only` modes and Gerrit changes are unaffected. The child's own `done` line still reaches the parent through `bin/fm-inactive-reconcile.sh`. The flag is a home-local preference and is not inherited by secondmate homes; [`bin/fm-pr-check.sh`](../bin/fm-pr-check.sh) owns the mechanics. ## Task status archive (config/status-archive) The optional local, gitignored `config/status-archive` presence flag opts this home into retaining task status history during teardown. Create it with `touch "$FM_HOME/config/status-archive"`; remove it to disable future archival and pruning. It is off by default: without the flag, teardown deletes status logs as before and leaves existing archives untouched. The flag applies to teardown run by this home, including forced retirement of its secondmate children; it is not inherited by other homes. Each opted-in archival prunes owned logs older than 30 completed days by mtime; housekeeping failures warn and never abort teardown. Age-based retention works independently of the selected backlog backend and keeps histories available for a predictable audit period. [`architecture.md`](architecture.md#task-status-archives) describes archive locations and isolation from live state; [`bin/fm-teardown.sh`](../bin/fm-teardown.sh) owns the mechanics. ## Record export (config/record-export-root) The optional local, gitignored `config/record-export-root` file contains exactly one absolute directory path and opts this home into a private copy of a fixed record allowlist. The setting is off by default and is not inherited by secondmate homes. When the file is absent, sync exits without export work, teardown follows its existing path, and spawn, watcher, presentation, classification, and snapshot readers remain unaware of exports. When the file is present, `python3` is required and an unreadable config directory or file is a hard failure rather than an opt-out. The configured path must name a dedicated root outside and disjoint from the effective home, config, data, and state trees. Its parent must already exist, while the final root may be absent so the exporter can create it. An existing root is accepted only when its strict versioned ownership marker names this canonical home, so one root cannot serve multiple homes. The exporter refuses an unmarked pre-existing directory, an owner mismatch, unknown managed artifacts, or unsafe object types instead of changing a broad or shared directory. Exporter-owned directories use mode `0750`, and exported files use mode `0640`. Run `bin/fm-record-export.sh sync` to publish the currently present allowlisted files through `/live/`: ```text data/{projects.md,backlog.md,done-archive.md} data//{brief.md,report.md,status*.log} state/fleet-ledger.jsonl state/.{meta,status} state/.inbox/{,handled/}NNN.msg ``` The command is one-shot and starts no network transfer, daemon, timer, or watcher. It does not recursively copy `data/` or `state/`, accept arbitrary sources, discover secrets, or redact content. The exporter opens source and destination components relative to verified directory descriptors with no-follow semantics, then rechecks source identity and content before publication. Sync builds a complete immutable generation, flushes its files and directories, and atomically replaces `live` with a relative symlink to that generation. Consumers that need one consistent snapshot resolve `live` once and read that immutable generation rather than resolving the pointer separately for each file. Superseded live generations remain selectable for 30 completed days before a later sync removes them. The published generation is exact, so a later sync retracts allowlisted files that are no longer live and represents a pending-to-handled message move only at its current path. Enabled teardown commits metadata, the live status when present, and pending and handled inbox messages under `/archive///` before those source records are retired. Direct archive calls acquire the task metadata lock, while teardown calls under the same lock it already holds, so the exact source set is re-enumerated immediately before commit. Each attempt builds a fresh sibling staging directory, flushes every copied file, directory, and manifest, and publishes the complete generation with one descriptor-relative rename. `manifest.v1.json` carries the version, capture time, completeness, source-relative names, byte counts, and SHA-256 values. Retries parse that JSON with duplicate-key rejection, require the exact v1 fields and types, and verify a one-to-one mapping between manifest entries and archived regular files. A reused task ID writes its distinct `spawn_gen` rather than overwriting an earlier generation. Only strictly valid committed generations older than 30 completed days are pruned by manifest mtime, matching task status archive retention, and scan failures warn without undoing a successful capture. If the archive cannot commit, teardown reports the reason and stops before deleting metadata, status, or inbox sources. Removing `config/record-export-root` is the explicit escape hatch back to ordinary teardown behavior. Briefs and handled inbox messages carry captain text, and every exported record can contain other sensitive operational data. Keep the export root private and never serve it directly. The schema is versioned for compatible readers, but it is not a Firstmate command API and no Firstmate reader consumes it. [`architecture.md`](architecture.md#read-only-record-exports) describes the mechanism boundary; [`bin/fm-record-export.sh`](../bin/fm-record-export.sh) owns the exact allowlist, schema, path checks, publication, and retention mechanics. ## Turn-end pane-churn absorb (config/turnend-churn-absorb) The optional local, gitignored `config/turnend-churn-absorb` presence flag opts this home into a default-off third form of positive work evidence in watcher triage. With it present, every referenced task must independently show positive work evidence, and an eligible bare turn-ended task that lacks authoritative proof may satisfy that requirement when its pane content changed since the previous poll. ### Evidence and time limit It stays opt-in because the other two proofs read a verdict the harness itself vouches for while this one infers execution from rendered bytes; with the flag absent triage behaves exactly as it did before. `FM_TURNEND_CHURN_ABSORB_SECS` is a positive integer number of seconds, defaults to `900`, and bounds how long one endpoint's turn-ends may ride that evidence before surfacing anyway. An invalid value fails closed and surfaces the wake. The bound is required rather than cosmetic because churn and pane staleness read the same pane. The flag is a home-local supervision-noise preference and is not inherited by secondmate homes, which run their own crew mix. [`architecture.md`](architecture.md) owns the triage contract and `bin/fm-watch.sh`'s `signal_turnend_panes_churned` owns the exact evidence and fail-closed boundaries. ## Parked-gate wait deferral (config/wedge-defer-parked-gate) The optional local, gitignored `config/wedge-defer-parked-gate` presence flag opts this home into a default-off further form of wait evidence in the watcher's wedge timer. ### When a waiting gate defers an alarm With it present, a provably-working pane about to escalate is also deferred to the `FM_PAUSE_RESURFACE_SECS` recheck cadence when its crew's own current state is a validation gate whose answer is owed to the supervisor and whose decision for that run is still open, and the recheck names the supervisor and the action that clears the lane instead of reporting a suspected wedge. It stays opt-in because the other evidence is a written record of the lane's silence - the worker's own declaration or firstmate's backlog captain hold - while this is derived from a pipeline's gate state, so which lanes give up the escalation ladder for it is a home's choice. With the flag absent the wedge timer spends no fold or current-state read for it, writes no record, and keeps the unchanged escalation schedule, reasons, and `demand-deep-inspection` wording. The flag is a home-local supervision-noise preference and is not inherited by secondmate homes, which supervise their own crew and own that trade separately. [`architecture.md`](architecture.md) owns the wait-evidence contract and which records may take the ladder away; `bin/fm-watch.sh`'s `wedge_wait_evidence` owns the exact derivation and its fail-closed boundaries. ## Wedge evidence guards (config/wedge-evidence-guards) The optional local, gitignored `config/wedge-evidence-guards` file opts this home into consulting operator-owned [Jev guards](jev-guards.md) before the watcher wedge-escalates a quiet live pane. It lists one absolute guard executable path per line, and blank lines and `#` comments are skipped. The file and every listed executable must be regular files owned by the watcher's user and not group or world writable, or they are never run. ### When a guard defers an alarm Only at the wedge threshold, after the declared-wait, worktree-write, dead-endpoint, and opt-in validating-run checks have all let the escalation through, the watcher runs each guard as ` --json` with `FM_GUARD_TASK` and `FM_GUARD_ENDPOINT` set, bounded by `FM_WEDGE_GUARD_TIMEOUT_SECS`. The first guard that prints one JSON object with `status` `CRITICAL` and a non-empty `recommendation` defers that escalation by one window, without advancing the escalation count. After `FM_WEDGE_GUARD_DEFER_MAX` consecutive deferrals the escalation fires anyway, and its reason names the guard and the first line of its recommendation. A timeout, non-zero exit, malformed output, `OK`, `WARNING`, or `UNKNOWN` changes nothing, and a dead or missing endpoint is reported before any guard is asked. A guard's answer is evidence only and never authorizes a decision, merge, cleanup, or any other action. With the file absent the watcher runs no guard, writes no record, and keeps the unchanged escalation schedule, reasons, and `demand-deep-inspection` wording. The file is home-local and not inherited by secondmate homes. ### Example operator family Firstmate ships no default guard for this; host-specific families belong to the operator's own layer. For example, an operator whose workers hold numbered tickets in a local queue directory could list a guard like this: ```sh #!/bin/sh # CRITICAL while this task holds a queue ticket whose recorded job is alive. ticket="$HOME/queue/$FM_GUARD_TASK.ticket" if [ -f "$ticket" ] && kill -0 "$(head -n 1 "$ticket")" 2>/dev/null; then echo '{"name":"ticket-dir","status":"CRITICAL","recommendation":"queue ticket held by a live job"}' else echo '{"name":"ticket-dir","status":"OK","recommendation":"no live queue ticket"}' fi ``` `bin/fm-watch.sh`'s `wedge_guard_evidence` and `wedge_defer_guard` own the exact invocation, deferral record, and fail-open boundaries. ## Worker-declared wait deferral (config/wedge-defer-worker-wait) The optional local, gitignored `config/wedge-defer-worker-wait` presence flag opts this home into a default-off form of wait evidence in the watcher's wedge timer. With it present, a provably-working pane about to escalate is deferred to the `FM_PAUSE_RESURFACE_SECS` recheck cadence when its newest status line is the worker's own still-open `blocked:` or `needs-decision:` line, and the recheck names firstmate and that verb's clearing action instead of reporting a suspected wedge. Lines under supervisor-reserved keys do not count, the escalation count does not rise, and any newer status line or a newer acknowledged steering-inbox record ends the deferral, so an answered lane that never resumes returns to the ladder. The away-mode daemon honors the same evidence before it escalates a possible-wedge wake. With the flag absent the wedge timer spends no read for it and keeps the unchanged escalation schedule, reasons, and `demand-deep-inspection` wording. The flag is a home-local supervision-noise preference and is not inherited by secondmate homes. [`architecture.md`](architecture.md) owns the wait-evidence contract and `bin/fm-classify-lib.sh`'s `status_worker_wait_verb` owns which lines count. ## Open-decision reminders (config/open-decision-reminders) The optional local, gitignored `config/open-decision-reminders` presence flag enables a default-off watcher backstop for unanswered keyed `needs-decision` and `blocked` records. On each poll, the watcher queues at most one `check: open decision overdue: key= age=s` wake for the oldest eligible overdue key, independently of pane state or supervision-branch classification. Declared `paused:` waits and tasks already held for the captain are exempt. Resolving a key stops its reminders, and a later opening starts a new reminder episode. Only explicit keys with valid source `[at=]` timestamps can be aged. `FM_OPEN_DECISION_OVERDUE_SECS` defaults to `1800` seconds. Repeats start at that interval, double after each repeat, and cap at `FM_OPEN_DECISION_BACKOFF_MAX_SECS`, which defaults to `14400` seconds. Both settings must be canonical positive integers of at most nine digits, with the cap at least the threshold. Invalid settings stop the watcher with an error when the flag is present. Pass them in the watcher's environment, or export them in `config/watch.env` for watchers started through `bin/fm-watch-arm.sh`. The flag is home-local and is not inherited by secondmate homes. `bin/fm-watch.sh`'s `open_decision_reminder_tick` owns the per-key schedule, which persists across watcher restarts. ## Validating-run deferral (config/wedge-defer-validating) The optional local, gitignored `config/wedge-defer-validating` presence flag opts this home into a default-off deferral in the watcher's wedge timer. With it present, a provably-working pane about to escalate is deferred to the `FM_PAUSE_RESURFACE_SECS` recheck cadence when its crew's attributed no-mistakes run step is `validating (running)` or `validating (fixing)`, because the worker is correctly idle behind its own drive call while the pipeline executes the round. The recheck names that run instead of reporting a suspected wedge, the escalation count does not rise, and a run that is parked, terminal, ci-monitoring, absent or unreadable keeps the ladder. The away-mode daemon has no such deferral of its own. With the flag absent the wedge timer spends no current-state read for it and keeps the unchanged escalation schedule, reasons, and `demand-deep-inspection` wording. The flag is a home-local supervision-noise preference and is not inherited by secondmate homes. [`architecture.md`](architecture.md) owns the deferral contract and `bin/fm-classify-lib.sh`'s `crew_run_is_validating` owns which verdicts count. ## Spawn base from the primary checkout (config/spawn-base-primary-checkout) The optional local, gitignored `config/spawn-base-primary-checkout` presence flag opts this home into a default-off base for fresh task worktrees on projects that land work locally. Without it, every fresh ship or scout worktree on a project with an origin remote resets to `origin/`, or to `origin/` for a named base branch. ### Which tasks it changes With it present, a fresh local-only ship, or a scout spawned without `--base-branch`, starts from the primary checkout's `` when that branch strictly contains origin's tip. A local-only project lands work there through `bin/fm-merge-local.sh` and never pushes, so `origin/` stays at the last push, and a fresh task would otherwise start behind work already landed and load the project's instruction files from that stale tree. A no-mistakes or direct-PR ship and any task with a named base branch still start from origin's tip, because that work is delivered against origin. When the primary checkout's `` and origin's have diverged, the spawn prints a note and starts from origin's tip. A project without an origin remote is unaffected, and a relaunch never changes the base a task already has. `bin/fm-review-diff.sh` reads the same flag, so a local-only ship or a scout is reviewed against the branch it delivers to and its diff shows only its own work, while a no-mistakes or direct-PR ship stays anchored on `origin/`. `bin/fm-promote.sh` reads it too, telling a scout promoted to no-mistakes or direct-PR on a project with an origin remote to return to origin's default branch rather than the primary checkout's. The flag is per home and is not inherited by secondmate homes, which keep their own project clones and opt in separately. Only its presence is read, at each fresh spawn, review, and promotion. The [`fm-spawn.sh` header](../bin/fm-spawn.sh) owns the exact base rule, and the [`fm-review-diff.sh` header](../bin/fm-review-diff.sh) owns the review anchor. ## Gate defaults (.no-mistakes.yaml) The tracked [`.no-mistakes.yaml`](../.no-mistakes.yaml) sets `test.evidence.store_in_repo: true` and runs the canonical [shell/workflow lint](../bin/fm-lint.sh) followed by the [Pi production-consumer typecheck](../tests/fm-pi-primary-types.test.sh) in the compiler-owning lint phase. Storing evidence in the repo publishes each run's test artifacts to the orphan `no-mistakes/evidence` branch and links them from the PR body, instead of keeping them on local disk under the no-mistakes home. That branch shares no history with code branches, so evidence never enters a pushed feature branch or the default branch; the worktree's `.no-mistakes/` stays local and CI rejects tracked entries under that path. The [`firstmate-coding-guidelines` skill](../.agents/skills/firstmate-coding-guidelines/SKILL.md#no-mistakes-test-configuration) owns why `commands.test` stays absent and targeted validation belongs to the evidence path. `commands.test` executes code, so no-mistakes honors it only from the default-branch copy of `.no-mistakes.yaml`; a pushed branch cannot change what the gate runs. See [CONTRIBUTING.md](../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](fm-test-portable-shards.md); [herdr-backend.md](herdr-backend.md#destructive-lab-safety) owns the real-Herdr lane's isolation boundary, and [runtime-backends.md](verification/runtime-backends.md#herdr) 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 curate the matching bullet in place under the internal [`stow` skill's](../.agents/skills/stow/SKILL.md) tiering and archive contract; 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 internal [`stow` skill's](../.agents/skills/stow/SKILL.md) aging-tier and cold-archive contract: inspect the current file first and curate it instead of appending forever. There is no shared learnings file by captain decision. ## External memory tiers (Memory layering section) A home may declare memory stores outside firstmate, such as MCP-served knowledge stores, as authoritative for named classes of knowledge. The declaration lives in a `Memory layering` heading inside `data/captain.md`, or inside the primary home's `data/captain-shared.md` when every secondmate home should inherit it; a local `data/captain.md` declaration wins for that home when both exist. No declaration means firstmate's local files under `data/` are the only memory owners, which is the default. Each declared tier states, in plain prose or a table: - its destination, named precisely enough for an agent to reach it, such as a connector's tool-name prefix; - the knowledge classes it is authoritative for, such as decisions and corrections, or people and references; - its correction rule, such as append-only supersession instead of edit or delete; - any capture etiquette, such as capture first and report what was captured. For example, one home declares a Palimpsest MCP store authoritative for reasoning continuity (decisions, hypotheses, corrections), an OpenBrain MCP store authoritative for retrievable facts (people, projects, references), and its own `data/` as a disposable per-machine cache; those stores are that home's choice, not a template default. The declaration is per-home untracked state under `data/`, like every other captain preference, so no clone or update carries it. A fresh primary home has no declaration until the captain re-declares the `Memory layering` section there from their own records. Until then local files are the only memory owners. [`AGENTS.md`](../AGENTS.md) section 6 owns what a declaration changes about knowledge routing, and the internal [`stow` skill](../.agents/skills/stow/SKILL.md) owns how curation reads the declaration and protects tier-owned entries from pruning. The section counts toward the startup memory budget like the rest of its file, so keep it to the mapping rather than restating these rules. ## 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. ### Set and validate the budget 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`](../.agents/skills/secondmate-provisioning/SKILL.md). 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. ### Accounting and curation 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](../.agents/skills/stow/SKILL.md) owns curation and its automatic secondmate cascade, which accounts every home against this same per-home allowance separately rather than against a fleet total. The helper's header owns exact parsing, publication, and report output mechanics. ### Daily startup growth check A home can arm a lightweight daily growth monitor with `bin/fm-startup-growth-check.sh arm`. It writes `state/startup-growth.check.sh` and binds it through the existing authenticated watcher-check mechanism, so no extra daemon or scheduler is installed. Registering it is a reason to watch on the same terms as the [watched-tool check](#watched-tool-updates-configwatched-toolsjson), so an armed home keeps needing a watcher after its last task is torn down. Use `bin/fm-startup-growth-check.sh disarm` to remove the check and its local report record. The check evaluates at most once per day and stays silent when nothing meaningful changed. A due evaluation uses file metadata and byte sizes before any content inspection: it asks `bin/fm-startup-memory-budget.sh report` for the budget verdict over `data/captain.md`, `data/captain-shared.md`, and `data/learnings.md`, watches the `data/projects.md` and `data/secondmates.md` that session start also prints in full for growth without entering that budget total, and separately watches the tracked startup/instruction owner files described by the script header. `bin/fm-startup-memory-budget.sh` remains the sole owner of the budget total and its verdict, so the check never re-derives either: when that owner annotates an overrun caused by the primary-owned `data/captain-shared.md` alone, a secondmate home is not woken about an overrun it cannot act on. A secondmate home is likewise not notified about per-file growth of that same primary-owned `data/captain-shared.md`, which it receives read-only; the growth is still observed and recorded, and a primary home reports it normally. Those tracked bytes are code and instruction-surface size, not prompt-memory cost. The check does not run session-start, bootstrap, network checks, model calls, repository refreshes, `/stow`, or full preference/learnings rereads. Growth is measured against a per-file baseline retained in the check's own state record, so accumulation that stays under one day's threshold is still caught once it adds up; reporting a file rebases its baseline to the reported size, so accepted growth then stays silent. A surface observed for the first time is baselined silently, including the first content of an optional file that did not exist yet when the check was armed, and an established baseline survives that file disappearing and coming back. The fixed growth thresholds are inspectable in the script header: 2048 bytes for tracked startup/instruction files and 250 estimated tokens for the printed startup-memory files. Budget overrun, unsafe or unreadable inputs, missing required tracked owner files, or material growth are reported once and deduplicated until the finding changes or clears; the report line is delivered before the check advances its own record, so a state-publication failure can repeat a finding but never swallow one. That one line goes out through the shared per-line digest cut, so an over-long finding set carries the repo's `[truncated]` marker instead of ending mid-finding, while deduplication keeps comparing the full uncapped set. Older bulk learning files remain reference-only; this monitor neither loads nor merges them. A reported review need is only a recommendation, not cleanup authority. ## Stow pass horizon (config/stow-pass-horizon) `config/stow-pass-horizon` is an optional local, gitignored presence flag that opts this home in to the pass-count decay horizon in the internal [`/stow` skill](../.agents/skills/stow/SKILL.md). Without it a `/stow` pass decays memory entries on their wall-clock horizons alone - 30 days for `aging`, 7 days for `perishable` - which is the default and unchanged behavior. With it, an entry is also stale after 10 passes (`aging`) or 3 passes (`perishable`) that evaluated it without reinforcing it, whichever horizon it reaches first. Opt in for a home that stows often enough that entries never sit unreinforced for a wall-clock horizon, so memory only grows against the startup-memory budget above; a home that stows rarely already exceeds its date horizon on a single pass and gains nothing. The flag is per home and is not inherited by secondmate homes, because stow cadence is a property of the home doing the stowing. Only the file's presence is read, so its contents are ignored; remove it to return to the default contract on the next pass. The skill text owns the marker spelling, the tick order, and the reinforcement rule. ## Self-update source (config/self-update-source) The optional local, gitignored `config/self-update-source` changes where `/updatefirstmate` takes the running firstmate from, and nothing else. It suits a home that runs its own fixes before upstream merges them, by following a branch that holds upstream's default branch plus those fixes, for example a `carry` branch on a fork. It holds one line, ` `, for example `fork carry`, naming a remote that already exists in this repository and a branch on that remote. When the file is absent, `/updatefirstmate` fast-forwards from `origin` exactly as before. When it is present, [`bin/fm-update.sh`](../bin/fm-update.sh) fetches only that branch and fast-forwards the primary to it under the same guards as the origin path: a dirty, off-default, or diverged primary is skipped and left untouched, so the named branch must only ever move forward. Local secondmate homes then advance to the primary's resulting default-branch commit, the same commit the startup secondmate sync follows, and a standalone-clone home that lacks that commit imports it from the primary first. A remote secondmate's host runs its own update against its own code root, so a remote home follows the named branch only when that code root has the same file; otherwise it keeps following its own origin. Worker spawns, review diffs, the cleanup landed-work test, lint, and default-branch detection keep using `origin`, so work started in this home still branches from, and is judged against, upstream. An unreadable or malformed file, an unknown remote, or an invalid branch name stops the whole update before anything moves and names the file; it never falls back to `origin`. The file is per home and is not inherited by secondmate homes. To stop following the branch, remove the file; a primary already ahead of `origin` is then reported as diverged until it is brought back by hand. The script's header owns the exact parsing and the per-home outcome lines. ## 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](../.agents/skills/secondmate-provisioning/SKILL.md#routing-table), including the parser-compatible fields, one-sentence summary requirement, `home:` pointer to the seeded charter, and limit on extra registry prose. ### Remote routes and validation A remote route adds `host:` and `root:` before the existing fields and places the whole secondmate home on that SSH host; it does not make ordinary workers remotely placeable. [`remote-secondmates.md`](remote-secondmates.md) owns current remote setup, operation, and safety behavior. 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. ### Provision a local home Use `fm-home-seed.sh - {...|--no-projects}` to lease a fresh local firstmate worktree for the secondmate home. For remote provisioning, including supplied project origins, follow [Remote second mates](remote-secondmates.md#provision-a-route). 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. ### Project modes and backlog handoff 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 ...`; it refuses In flight, Done, or non-secondmate homes, and its [script header](../bin/fm-backlog-handoff.sh) owns route-specific wake outcomes and retries. 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. ### Identity markers and upgrades Each seed writes an `.fm-secondmate-home` identity marker at the home root, alongside a durable `.fm-secondmate-parent` record of the home's route to its parent (see "Provision a route" in [`docs/remote-secondmates.md`](remote-secondmates.md)). The tracked root `.gitignore` ignores both markers, so validation can read them 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 local standalone-clone home cannot receive a primary-local commit through that no-fetch sync, so it receives the rule through `/updatefirstmate` instead. ## Harness support claude, codex, opencode, pi, pi-signed, grok, kimi, cursor, and omp are empirically verified for crewmate and secondmate launches; gemini is verified for crewmate and scout launches only, and [README requirements](../README.md#requirements) own the set supported for the primary session. ### Harness restrictions and credentials `fm-spawn.sh` refuses kimi on cmux and Orca at preflight, because answering Kimi's folder-trust dialog needs a verified viewport-only capture those backends lack; [its adapter reference](../.agents/skills/harness-adapters/references/harness/kimi.md#readiness-gated-start) owns the trust-dialog handling. It also never answers a Kimi folder prompt that announces project MCP servers, pages what trusting would activate, or is collapsed for a short terminal, so no project MCP server starts without a consent decision that covers it; the launch fails with that reason, and the same adapter reference owns the exact rules. A cursor secondmate or primary runs the tracked project-scope `.cursor/hooks.json` in its own home and must be launched with `--trust`, or no project hook loads; [`docs/supervision-protocols/cursor.md`](supervision-protocols/cursor.md) owns its supervision protocol. Cursor typed-submit confirmation is verified on tmux and Herdr only. On Zellij, cmux, Orca, Paseo, and tern a typed-plane Cursor send (a harness-native invocation or an explicit backend target; ordinary text steers ride the durable inbox and exit 0 at enqueue) lands, but `fm-send` reports delivery unconfirmed and exits non-zero because their shared submit core does not consult the busy footer; [runtime backend verification](verification/runtime-backends.md#cursor-agent-cli) owns the evidence and transcript-state boundary. muse is verified for crewmate and scout launches ONLY, and `fm-spawn.sh` refuses it for a secondmate, because muse ships no usable hook surface for a primary session's turn-end supervision; [`docs/verification/muse.md`](verification/muse.md) owns that evidence. muse also needs a worker-reachable credential before spawning, and the portable fleet path is the `/muse/auth.json` credential stored by `muse login`, because a caller-only `META_API_KEY` does not cross a long-lived backend daemon. gemini is likewise refused for secondmates because it has no primary supervision protocol; [its adapter reference](../.agents/skills/harness-adapters/references/harness/gemini.md) owns the credential precondition, canonical-launch wiring, and raw-launch limitations. rovo is likewise verified for crewmate and scout launches ONLY, refused for a secondmate for the same reason - no turn-end hook and no primary supervision protocol; [`docs/verification/rovo.md`](verification/rovo.md) owns that evidence, including the OAuth token's silent background refresh from a stored refresh token and both tmux and herdr pane liveness (herdr placement is verified live, with a Herdr-side agent-detection gap left open for recovery classification). An agy secondmate or primary runs the tracked project-scope `.agents/hooks.json` in its own home, and `bin/fm-spawn.sh` pre-registers a secondmate home in agy's workspace trust store through `bin/fm-agy-trust.sh`; [`docs/supervision-protocols/agy.md`](supervision-protocols/agy.md) owns its supervision protocol and [`docs/verification/agy.md`](verification/agy.md) owns the evidence. kiro (Kiro CLI, V2 engine only) is likewise verified for crewmate and scout launches ONLY, refused for a secondmate because it has no primary supervision protocol; it is claude-shaped, so its per-task turn-end and busy hooks live in a firstmate-owned agent config the spawn writes under `state/.kiro-home` and reaches by relocating `KIRO_HOME` (never the worktree's own `.kiro/`), with the `--trust-all-tools` modal suppressed by a seeded `chat.disableTrustAllConfirmation` setting; [`docs/verification/kiro.md`](verification/kiro.md) owns that evidence, and the v3/KAS engine is out of scope and unsupported on Amazon Linux 2. A kiro hook command must be one unquoted token, so a kiro spawn refuses before launch when that per-task home path contains whitespace or a shell-special character (such as a quote or `$`) and asks for an `FM_HOME` or `FM_STATE_OVERRIDE` free of them. devin is verified for crewmate and scout launches only; a secondmate is refused because Devin has no verified primary supervision protocol. Its private worker config disables Claude Code imports (including the captain's hooks) and, unless the home sets `config/keep-ai-trailers` (see "Commit attribution"), Devin commit attribution without editing user or project config; [`fm-devin-config.sh`](../bin/fm-devin-config.sh) owns these enforced settings and [Devin verification](verification/devin.md) owns the live evidence and observed model availability. cortex (Snowflake's Cortex Code) is likewise verified for crewmate and scout launches only; a secondmate is refused because Cortex has no verified primary supervision protocol. It runs only on the tmux and Herdr backends; a zellij, cmux, orca, paseo, rex, or t3code launch is refused because those backends supply no Cortex pane identity to prove its titled composer empty. Its busy-state and turn-end hooks reach the worker through a private per-task plugin directory passed with `--plugin-dir`, so no user or project Cortex or Claude config is edited; [`fm-cortex-plugin.sh`](../bin/fm-cortex-plugin.sh) owns it and teardown removes it. The launch clears `CLAUDECODE` so Herdr's Claude integration hook cannot claim the pane, and it uses `--dangerously-allow-all-tool-calls`, which also skips the project trust dialog without recording the project in Cortex's trust file. Cortex loads Claude-compatible hook files with no switch to disable the import, so a worker in a project with Claude hooks runs them. [Cortex verification](verification/cortex.md) owns the live evidence, the Herdr pane-claim behavior, and the unverified items. commandcode (Command Code) is verified for crewmate and scout launches only; a secondmate is refused because Command Code has no verified primary supervision protocol. It runs only on the tmux and Herdr backends; a zellij, cmux, orca, paseo, or tern launch is refused because those backends supply no Command Code pane identity, without which its idle composer placeholder never reads empty and steering stalls, and a rex launch is refused until Command Code is live-verified there. Its busy-state wiring is a tracked mod loaded per process with `--mod`, so no user or project Command Code settings are edited; [its adapter reference](../.agents/skills/harness-adapters/references/harness/commandcode.md) owns the launch facts and [Command Code verification](verification/commandcode.md) owns the live evidence. Hermes Agent is verified for crewmate and scout TUI launches on tmux only; primary and secondmate use and other backends are refused. Its native profile selects the model and reasoning, and the profile's own gateway may run only if it is idle at dispatch. The [Hermes adapter reference](../.agents/skills/harness-adapters/references/harness/hermes.md) owns operating limits, [`fm-spawn.sh`](../bin/fm-spawn.sh)'s header owns the per-spawn profile flag, and [Hermes verification](verification/hermes.md) owns live evidence. Polytoken is verified for crewmate and scout launches only; [its adapter reference](../.agents/skills/harness-adapters/references/harness/polytoken.md) owns the operating contract and [verification record](verification/polytoken.md) owns the live evidence. It has no primary or secondmate supervision protocol. The launch generates a private per-task config directory that mirrors the operator's provider/model configuration and enforces unattended permissions without modifying global configuration. The first-start license dialog requires an operator decision and is never accepted automatically. ### Verification and primary supervision New harnesses get verified through a supervised trial task before joining the set. The verified adapter evidence - each harness's busy-state source, interrupt and exit behavior, skill-invocation syntax, and per-harness quirks - lives in the skill tree rooted at [`.agents/skills/harness-adapters/SKILL.md`](../.agents/skills/harness-adapters/SKILL.md). The executable interrupt and exit mechanics live in [`bin/fm-control-lib.sh`](../bin/fm-control-lib.sh), and [`docs/agent-control.md`](agent-control.md) owns their lifecycle-control architecture. Launch mechanics, including the verified command templates, live in [`bin/fm-spawn.sh`](../bin/fm-spawn.sh). A Claude worker's launch brief is published as an operational record in the receiving home's state and delivered as a printable doorbell; if publication fails, the spawn reports the failure and launches nothing rather than sending a marker that Claude Code would strip. Other harnesses retain the typed operational-marker launch path. Pi-family launches adapt the regular-TUI safeguard to the installed CLI's capabilities; [`fm-spawn.sh --help`](../bin/fm-spawn.sh) owns the exact version-safe launch mechanics. Enabled primary-session turn-end guard integrations are tracked as repo-level hook files and documented in [`docs/turnend-guard.md`](turnend-guard.md). Kimi remains outside the primary turn-end guard integrations; [`docs/turnend-guard.md`](turnend-guard.md#compatibility-limits) owns its separate captain-approved crew wake hook. Primary-session watcher wake protocols are rendered at session start by [`bin/fm-supervision-instructions.sh`](../bin/fm-supervision-instructions.sh) from [`docs/supervision-protocols/`](supervision-protocols/). Claude's Stop `asyncRewake` hook owns tokenless re-arm cycles, Cursor's stop hook parks on the watcher, Grok uses background-notify cycles, Codex uses bounded foreground checkpoints unless the home opts in to its [native Stop park](#codex-native-stop-park-configcodex-stop-park), Pi and pi-signed use the same two tracked primary extensions, omp uses its own two tracked `.omp/extensions/` files with a blocking `session_stop` turn-end hook, and OpenCode uses its TUI plugin. ### Choose the worker harness `config/crew-harness` is a local, gitignored file containing one adapter name for crewmate and scout launches. When pi-signed is selected, Firstmate preserves `FM_PI_HARNESS=pi-signed` and refuses the launch if the selected executable is unavailable rather than falling back to pi; [`fm-spawn.sh --help`](../bin/fm-spawn.sh) owns executable resolution and launch mechanics. 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. ### Choose the secondmate 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 file belongs to the primary, is not inherited by secondmate homes, and secondmates do not launch secondmates. Without a `mate :` declaration, the first non-empty, non-comment line supplies the default, parsed as ` [] []`; later lines and additional tokens retain their legacy ignored behavior. With an override declaration, at most one non-empty, non-comment line without that prefix supplies the default. Any number of override lines can pin named secondmates rather than the whole home: ```text mate : [] [] ``` Override ids use letters, digits, dots, underscores, and hyphens, and matching is case-sensitive. A mate line replaces the default line for that exact secondmate id, so raising the default pin cannot migrate a mate the captain pinned separately. Precedence is the override for that mate's id, then the default line, then `config/crew-harness`, then the primary's own harness. An override whose harness token is `default` skips the file's default line and falls back to `config/crew-harness`, then the primary's own harness, contributing no model or effort. Blank lines and `#` comments stay legal anywhere, and a mate line is honored wherever it sits in the file. In a file that declares an override, a second default line, two overrides for one id, an override with no tokens or more than three, or a `mate` keyword naming an invalid secondmate id is an actionable error: resolution exits nonzero naming the file's line number and prints no token, so malformed pin config cannot resolve to a fallback in silence. In a file that declares no override, none of that applies: the legacy first-line rule governs, so an existing, already-valid file is read exactly as it was before this grammar existed. A bare `` 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`](../bin/fm-harness.sh)'s header owns the resolver commands and optional secondmate-id argument. Without an id they select the default line only; model and effort output is empty when no token applies. When the file declares an override, every resolver read validates the whole file, even when no id is supplied or the malformed line concerns another mate. Every resolution site that knows which mate it is launching passes that id, so spawn, control-plane relaunch, and secondmate restart all land on one decision. `config/crew-harness` remains a bare adapter-name file and is never parsed for a model. Changing this pin affects the next secondmate spawn or control-plane relaunch; the relaunch profile rules are owned by [`docs/agent-control.md`](agent-control.md#transactional-relaunch). For a separate account choice for one local secondmate, see [per-secondmate account pins](#per-secondmate-account-pins). The regression coverage is in [`fm-secondmate-harness.test.sh`](../tests/fm-secondmate-harness.test.sh), [`fm-control-relaunch.test.sh`](../tests/fm-control-relaunch.test.sh), and [`fm-secondmate-restart.test.sh`](../tests/fm-secondmate-restart.test.sh). ### Per-launch overrides and inherited defaults 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`; for a local route, an explicit harness or raw launch command starts with clean model and effort defaults unless those flags are also passed. An explicit harness also bypasses reading the pin file during spawn or control-plane relaunch, so malformed unused pin configuration cannot block that override. A relaunch supplying explicit `--harness`, `--model`, and `--effort` therefore does not read this configuration. Remote secondmate routes accept verified harness adapters only and reject raw launch commands. 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`](../.agents/skills/secondmate-provisioning/SKILL.md); 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. ### Installed hooks and launch details 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, plus the parent home's `state/.pi-ext.ts` busy extension. Pi-family secondmates can start unattended in Firstmate-seeded homes without accepting project trust manually; [`fm-spawn.sh --help`](../bin/fm-spawn.sh) owns the capability requirement, session-only approval scope, and older-version fallback, with [regression evidence](verification/runtime-backends.md#pi-seeded-secondmate-project-trust). For omp secondmate launches, `fm-spawn.sh` names only the parent home's `state/.omp-ext.ts` busy extension with `-e`: omp auto-discovers the home's tracked `.omp/extensions/` with no trust gate, and naming a discovered file with `-e` as well loads it twice; every omp launch also carries the tracked `.omp/fm-worker-overlay.yml` posture overlay through `--config`, which [`fm-spawn.sh --help`](../bin/fm-spawn.sh) owns. For a Claude secondmate, `fm-spawn.sh` writes the busy hooks into the parent home's `state/.claude-settings.json` and passes that file as Claude Code's one `--settings` value. The same file carries `feedbackDrafts` and, unless `config/keep-ai-trailers` is present, the attribution policy every other Claude launch puts in its inline `--settings` JSON. Claude Code applies `--settings` for that session and does not write the file into the mate home. A key the file sets overrides the same key in user, project, and local settings. A key it omits keeps the file-based value, so a `.claude/settings.local.json` already in the home is left untouched. Hook entries merge across settings files rather than replacing each other, so the home's tracked Stop guard still runs beside these hooks. The CLI reference documents the one file-or-JSON flag, the settings page documents session precedence, and the hooks page documents the hooks merge: , , . [`fm-spawn.sh`](../bin/fm-spawn.sh) owns the file and the flag. ### Codex automatic approval review Codex crewmate, scout, and secondmate launches default to `--dangerously-bypass-approvals-and-sandbox`. The local, gitignored `config/codex-approval-mode` accepts one whitespace-trimmed token: absent or `bypass` retains that default; `approve-for-me` selects `--approve-for-me`. Malformed values and unreadable files refuse before spawn mutation, and the setting applies to relaunches and is inherited into secondmate homes. `backend=t3code` always runs Codex at T3's `full-access`, so it refuses `approve-for-me` before mutation; [`t3code-backend.md`](t3code-backend.md#active-limits) owns that backend limit. The fallback uses a model reviewer in the workspace-write sandbox, which starts with network access disabled and reviews writes outside the working directory. It remedies a managed policy that leaves bypass launches with user-reviewed approvals even when its sandbox bypass applies. Firstmate targets current CLIs rather than detecting versions or emulating older approval flags. Crew and scout launches still request hook disablement and retain their notification wiring, while secondmates retain their primary-session hooks. A managed policy can override the requested hook disablement, so it may still require an operator's hook review. See the [current verification and limits](verification/runtime-backends.md#codex-automatic-approval-review) for observed task-channel and network access. ## Claude permission mode (config/claude-permission-mode) The optional local, gitignored `config/claude-permission-mode` selects the permission flag for Firstmate-built Claude command lines: crewmates, scouts, Claude secondmates, and control-plane relaunches. ### Accepted values and refusals The token is the file's whitespace-trimmed content. | Token | Launch permission flag | | --- | --- | | `auto` | `--permission-mode auto` | | `bypass` | `claude --dangerously-skip-permissions` | An absent file defaults to bypass, preserving `--dangerously-skip-permissions` for unconfigured homes. Auto uses a model reviewer and avoids managed policies that block bypass-permission sessions. It is selected by an explicit setting or the firstmate-invoked failover below. Only the permission flag changes between the two modes. The environment prefix, session settings, model, effort flags, and the task-channel `--add-dir` grant below stay the same in both. Every Firstmate-built Claude command line, in both modes, also passes `--add-dir` for exactly this task's Firstmate channel directories, resolved to real paths: a secondmate gets the parent home's `state/.inbox` it reads its steers from; a ship or scout worker gets this home's `state/operational-inbox` (its launch record), `state/.inbox` (its steers), `data/` (its brief and report), and the code root's `.agents/skills`. The grant exists because Claude Code path-checks the Read/Glob/Grep file tools against cwd plus `--add-dir`, and since 2.1.257 the first outside read in `auto` mode parks the pane on a one-time interactive question, while a "Block" answer there writes `permissions.blockReadsOutsideWorkingDirectories` into user settings and then refuses the same reads under bypass too. It never covers the whole `state/` or anything wider. Any other value or an unreadable file refuses every spawn from that home, whichever harness it would launch. This happens before any endpoint, worktree, or task record exists. The diagnostic names the accepted values; Firstmate never falls back to a permission posture the captain did not choose. ### When changes apply and inheritance `bin/fm-spawn.sh` reads the file on every spawn and relaunch, so a change takes effect at the next launch without a restart. Both approval-mode files follow the primary-authoritative [`secondmate-provisioning`](../.agents/skills/secondmate-provisioning/SKILL.md) inherited-local-material contract. T3 Code owns its provider command and always uses `full-access`, so `backend=t3code` accepts the default `bypass` posture and refuses `auto` before mutation; [`t3code-backend.md`](t3code-backend.md#active-limits) owns that backend limit. The [Claude adapter reference](../.agents/skills/harness-adapters/references/harness/claude.md) records the permission-mode observations and the distinct startup dialogs. ## Codex permission mode (config/codex-permission-mode) The optional local, gitignored `config/codex-permission-mode` selects the permission flags for Codex crewmates, scouts, secondmates, and relaunches started by this home. | File content | Launch permission flags | | --- | --- | | Absent or `bypass` | The [Codex approval mode](#codex-automatic-approval-review) flag, `--dangerously-bypass-approvals-and-sandbox` by default | | `restricted` | `--sandbox workspace-write --ask-for-approval on-request` | Create the file containing exactly one of the two tokens, optionally followed by a newline. An absent file preserves the existing launch command. Restricted mode changes only the permission flags; model, effort, and hook wiring stay the same. It replaces whichever flag `config/codex-approval-mode` selects, and the task record reports `restricted`, so managed approval failover never treats the launch as bypass. It grants no additional writable directories, including the shared Firstmate state directory. Writes outside the worker's worktree, including status reports and steering-inbox acknowledgements, remain subject to operator approval. Codex `--sandbox workspace-write` also disables network access and does not permit writes to shared Git metadata by default, so `git commit`, `git push`, `gh`, and no-mistakes pipeline steps from a restricted worker likewise stop at an approval prompt. Choose restricted mode only when that approval workflow is available. `bin/fm-spawn.sh` reads and validates the setting before any spawn or relaunch mutation, regardless of the selected harness. Any other content, an unreadable or non-regular file, or a dangling symlink refuses the launch instead of falling back to bypass. Changes take effect at the next launch without a restart. The file is not inherited into secondmate homes; configure it separately in each home that should also restrict its own Codex workers. `backend=t3code` always runs Codex at T3's `full-access`, so it refuses `restricted` before mutation; [`t3code-backend.md`](t3code-backend.md#active-limits) owns that backend limit. ## Persistent managed-policy failover After firstmate observes a Codex command-approval prompt under its bypass flag or a Claude managed-policy refusal of a bypass session, it asks the captain for explicit approval to save a lasting worker permission setting and relaunch the worker. Only after that approval does firstmate invoke `bin/fm-approval-failover.sh`; the script's header and help own the command and recovery mechanics. There is no automatic detection, polling, startup window, or approval-prompt answering. The command saves Codex's `approve-for-me` or Claude's `auto` in the owning home's absent mode file, so later launches use the model-reviewed remedy without retrying bypass. Existing explicit settings are never overwritten, including invalid or empty files, directories and dangling symlinks; a concurrent setting wins too. To retain a secondmate's remedy across convergence, select it in the primary home under the [inherited-local-material contract](../.agents/skills/secondmate-provisioning/SKILL.md). Recovery uses the existing [lifecycle control plane](agent-control.md), respecting its leases and supported-runtime limits; incomplete recovery is reported with the saved setting and work preserved. A `backend=t3code` task refuses before any change, because T3 already runs it at `full-access` and its spawns refuse both fallbacks; approve the pending request in T3 Code itself or choose another backend. Generic login failures do not establish a defeated bypass, and managed hook review remains a separate operator decision. Neither fallback is unrestricted execution, and Firstmate does not change vendor or security-agent settings. ## Worker tool exclusions (config/crew-exclude-tools) The optional local, gitignored `config/crew-exclude-tools` hides named tools from this home's ship and scout workers, for example to keep an MCP server's write tools out of reach while its read tools stay available. The contract is runtime-neutral: a runtime must support hiding the listed tool names or refuse the launch, and a non-empty list is never silently ignored. With no file, or a file with no entries, every launch on every runtime is unchanged. Create the file with one tool name per line, such as `mcp____` for an MCP tool. Blank lines and lines beginning with `#` are allowed, and surrounding whitespace on a line is trimmed; a trailing comment on an entry line is not allowed. The file is read from this home's own configuration directory on every launch, so a change reaches the next worker or relaunch without a restart. It is not in the inherited configuration set, so no other home, including a secondmate home, receives it; create the file in each home that wants it. It does not apply to a secondmate's own agent, which neither reads nor refuses on it. ### Runtime support | Runtime | With a non-empty list | | --- | --- | | `pi`, `pi-signed` | Hides listed tool names, MCP tools included, on every ship and scout spawn and relaunch. | | Every other runtime, and a raw launch command | The launch refuses with an error naming `config/crew-exclude-tools`, because that runtime has no verified way to hide tools. | A relaunch validates the list and the replacement runtime's support before stopping the running worker, so an exclusion-list refusal preserves the running agent. ### Validation An entry may use only `A-Z`, `a-z`, `0-9`, `_`, `.`, and `-`. An entry with any other character, including internal whitespace, a comma, or a `*`, refuses the launch and names the offending entry. An unreadable or nonregular file, or a path inspection error, also refuses and names the configuration file. For a new worker, these checks run before its endpoint, local copy, or task record is created; Firstmate never launches with a partial list. Only exact tool names are accepted, not wildcard patterns. Firstmate checks syntax and runtime support before launch but never runs `pi mcp list` or otherwise connects to servers to validate names. When its first agent run starts, after Pi's startup tool-loading boundary, the worker extension compares the launch's exclusion list with its own loaded-tool registry and appends a timestamped warning note to `state/.status` naming the configuration file and every unmatched entry for the supervisor. The check runs before worker actions so it cannot supersede a terminal status emitted during the turn. An unmatched entry is reported as **unverified**, not valid: Pi versions that omit excluded tools from the registry cannot distinguish a correct exclusion from a typo, and a server that has not connected cannot verify its tools either. Names present in the registry produce no report; unknown or unverified names do not refuse the launch. Each relaunch installs a fresh worker extension with the home's current list, so the replacement performs the same check. [`bin/fm-exclude-tools-lib.sh`](../bin/fm-exclude-tools-lib.sh) implements parsing and pre-launch validation for this contract; [`bin/fm-spawn.sh`](../bin/fm-spawn.sh)'s header owns the launch-flag mechanics. ## Worker account pin (config/claude-account, config/pi-account) A home that mixes accounts for one runner, such as a work login and a personal one, can pin the account its own Claude and Pi workers launch on. The pin is opt-in: without a home-wide or per-secondmate pin, pane-backed Claude launches keep receiving firstmate's own `CLAUDE_CONFIG_DIR` when it is set. Unpinned Pi launches use the destination pane's ambient account. Both files are local and gitignored. | Runner | File | Variable the launch receives | `ordinary` means | | --- | --- | --- | --- | | `claude` | `config/claude-account` | `CLAUDE_CONFIG_DIR` | the variable unset, so Claude uses its default login | | `pi`, `pi-signed` | `config/pi-account` | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | ### File format and provider selection `config/claude-account` holds one line: `ordinary`, or the absolute path of an existing Claude config directory. `config/pi-account` holds that same root on line 1 and, on line 2, the providers this home may spend, separated by spaces, for example `openai-codex anthropic`. A final newline is optional; any other line, a relative path, or a control character such as a CR refuses. For Claude, `ordinary` unsets `CLAUDE_CONFIG_DIR` rather than pointing it at `~/.claude`, because Claude reads `$CLAUDE_CONFIG_DIR/.claude.json` and keys its macOS Keychain entry to any directory that is set ([authentication, "Credential management"](https://code.claude.com/docs/en/authentication#credential-management)). A Pi root can hold several provider logins at once, so the root alone does not say which account a launch spends. A pinned Pi launch therefore needs `--model /` naming a declared provider, and Firstmate also passes `--provider ` so Pi cannot resolve the model under another signed-in provider. An unqualified model, an undeclared provider, or a raw Pi launch command, which cannot receive that flag, refuses; Firstmate never guesses a provider. ### Launch scope and sign-in checks When a file is present, supported command-line launches of that runner from this home use it: ships, scouts, local secondmate agents, raw Claude launch commands, and relaunches. A local secondmate can override this default with its own [per-secondmate account pin](#per-secondmate-account-pins). T3 Code selects the account through its provider instance instead, so `backend=t3code` refuses a worker account pin before mutation; [`t3code-backend.md`](t3code-backend.md#active-limits) owns that backend limit. A raw Claude launch command refuses if its leading assignments set `CLAUDE_CONFIG_DIR` or a credential that a pinned launch unsets, such as `ANTHROPIC_API_KEY`. The assignment would override the pin. The refusal names the variable; remove that assignment from the raw command, or change or remove `config/claude-account`. Before any worker endpoint, local copy, or task record exists, and before a relaunch stops the running worker, Firstmate asks the runner itself whether the pinned account is signed in: `claude auth status` for Claude, and `pi auth check --provider --json --no-refresh` for Pi, falling back to `pi --list-models ` for a provider an extension registers. The check runs with only `HOME`, `PATH`, `TMPDIR`, `USER`, `LOGNAME`, and the pinned root in its environment, so a credential variable in firstmate's own environment cannot answer for an empty root. A pinned Claude launch also unsets the environment credentials Claude ranks above a stored login, such as `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, and the Bedrock and Vertex switches ([authentication precedence](https://code.claude.com/docs/en/authentication#authentication-precedence)). Pi ranks a root's stored logins above environment variables, so a pinned Pi launch unsets nothing. A home that authenticates Claude through environment credentials on purpose should leave the pin absent. ### Failures, reporting, and inheritance A malformed file, a root that is not a readable directory, or a signed-out account refuses the launch and names the file to fix; Firstmate never falls back to the ambient account and never changes a global login or copies a credential. The spawn prints the pin as `account=` (plus `account_provider=` for Pi) and records the same fields in the task record, so the session-start digest shows which account each worker launched on. Pins are not inherited into secondmate homes: the secondmate's own workers read that home's `config/claude-account` and `config/pi-account` files independently. A remote secondmate is launched on its host from its own home's configuration, so create the file in that remote home. [`bin/fm-worker-account-lib.sh`](../bin/fm-worker-account-lib.sh) owns parsing, the sign-in check, and the full list of credentials a Claude launch unsets; [runtime backend verification](verification/runtime-backends.md#worker-account-pin-sign-in-check) records the check against the real runners. ### Several Claude accounts (config/claude-accounts) A home with more than one Claude subscription can let each ship and scout launch on whichever declared account has quota to spare, instead of pinning every Claude worker to one account. Create `config/claude-accounts` with one account per line, each `ordinary` or the absolute path of an existing Claude config directory, written exactly as `config/claude-account` would hold it: ```text ordinary /home/me/.claude-second ``` A final newline is optional; a blank line, a relative path, a repeated entry, or a control character refuses. The list is opt-in: without it, every launch, including the single pin above, behaves exactly as before, and `--account` refuses. A home keeps either the list or `config/claude-account`, never both; with both present, every Claude launch refuses. While the list is present, every Claude ship or scout spawn must name one declared entry with `--account `. Firstmate chooses that entry at dispatch intake under the [`quota-array-dispatch`](../.agents/skills/quota-array-dispatch/SKILL.md) procedure, and [`bin/fm-claude-account-quota.sh`](../bin/fm-claude-account-quota.sh) measures each entry for it by running `quota-axi --provider claude --profile-only` under that entry's own config directory (`~/.claude` for `ordinary`). Firstmate never picks an entry by default, by file order, or from the ambient account, and never discovers, adds, or copies a login. The named entry passes the same refusals as a pin before anything launches: its directory must exist, a raw Claude command must not override it, and `claude auth status` must say it is signed in, with the stripped environment and credential unsetting described above. An undeclared, missing, or signed-out entry refuses rather than falling back to another entry or the ambient account. The spawn line and the task record carry the entry as `account=`. A relaunch or recovery of that task keeps its recorded account, re-checking that it is still declared and signed in before the running worker stops. To move a task to another declared account, for example when its own account is exhausted, relaunch it explicitly with `bin/fm-control.sh relaunch --account --note `; nothing moves a running task automatically. A task with no recorded account, or one whose account is no longer declared, refuses a plain relaunch and needs that explicit `--account`. The list covers Claude ships and scouts only. A local secondmate's own agent never draws from it: under a list it needs its own [per-secondmate account pin](#per-secondmate-account-pins), and `--account` refuses for every secondmate, local or remote. Other runners ignore the list, and `--account` refuses for them. Like the single pin, the list is not inherited into secondmate homes. ### Per-secondmate account pins To choose an account for one local secondmate's own agent, create `config/secondmate-accounts//claude-account` or `config/secondmate-accounts//pi-account` in the primary home. Use the secondmate's registered identifier for ``. These private files use the same [file format and provider selection](#file-format-and-provider-selection), sign-in checks, and refusal rules as the home-wide worker pins. The matching per-secondmate file overrides the primary home's matching worker pin for that secondmate alone. An absent per-secondmate file preserves the existing home-wide pin or unpinned behavior exactly. Other secondmates and ordinary workers remain unchanged. A pin for another runner does not apply, and `pi` and `pi-signed` share `pi-account`. An empty, malformed, unreadable, or unusable per-secondmate pin refuses with its path and reason rather than falling back to the primary's account. For example, `config/secondmate-accounts/work/claude-account` containing `/home/me/.claude-work` selects that existing Claude login for the local secondmate named `work`. Removing that file restores the primary home's account selection on the next launch. Every spawn, control-plane relaunch, and automatic recovery reads the durable setting again, never a previous task record or ambient account choice. After changing a live mate's pin, use `bin/fm-control.sh relaunch` to apply it. The relaunch checks the selected pin before stopping the current agent. Per-secondmate pins stay in the primary home and are not inherited or copied into the secondmate's home. A matching primary-side per-secondmate pin for a remote route refuses before remote launch, because its account must be configured on that host instead. ### Per-secondmate omp profile To launch one local omp secondmate under its own isolated omp profile, create `config/secondmate-accounts//omp-profile` in the primary home. Use the secondmate's registered identifier for ``. The file holds one omp profile name on a single line, with an optional final newline. The name must match omp's own profile rule, `^[a-z0-9][a-z0-9._-]{0,63}$`, must not end in `.`, and must not be a Windows reserved device name (`con`, `prn`, `aux`, `nul`, `com0`-`com9`, `lpt0`-`lpt9`), alone or with an extension such as `nul.x`. When the file is present, that secondmate's omp agent launches as `omp --profile `, so it uses that profile's own logins, sessions, settings, and caches rather than the primary's default omp profile. The model catalog check before launch reads the same profile. When the file is absent, the launch carries no `--profile` flag and is unchanged. There is no home-wide fallback file. Only a local secondmate's own omp agent reads this file. Ships, scouts, other runners, and remote secondmates never read it, and another secondmate's file never applies. The profile pin is independent of the [per-secondmate account pins](#per-secondmate-account-pins), which select a Claude or Pi login root rather than an omp profile. For example, `config/secondmate-accounts/speak4/omp-profile` containing `speak4` launches the local secondmate named `speak4` with `omp --profile speak4`. An empty, malformed, or unreadable file refuses with its path before any worker endpoint, local copy, or task record exists, and never falls back to the default profile. A raw omp launch command cannot carry the flag, so a pinned secondmate refuses one; launch it with `--harness omp`. Firstmate does not create, sign in, or copy an omp profile; sign in to it yourself with `omp --profile `. The spawn prints the pin as `omp_profile=` and records the same field in the task record. Every spawn, control-plane relaunch, and automatic recovery reads the file again, and a relaunch checks it before stopping the current agent. After changing a live mate's pin, use `bin/fm-control.sh relaunch` to apply it. The file stays in the primary home and is not inherited or copied into the secondmate's home, because a secondmate does not launch secondmates. [`bin/fm-omp-profile-lib.sh`](../bin/fm-omp-profile-lib.sh) owns parsing and the refusals. ## Lavish server address (config/lavish-axi-host) The optional local, gitignored `config/lavish-axi-host` contains one non-empty address without whitespace for the per-machine Lavish server. `fm-spawn.sh` exports that address into every new worker and relaunch for opening boards, and the file is inherited into secondmate homes through the primary-authoritative configuration contract. Once a board exists, the process-event adapter derives the polling address from that board's own saved Lavish session instead; its header owns the lookup contract. When the file is absent, worker launches do not add a board address and retain the existing ambient-environment behavior. Malformed or unreadable values refuse the launch before the worker starts. The address selects the existing shared server; it does not authorize starting or stopping the server, and the Lavish startup crash remains a vendor-tool concern. ## Home brief include (config/brief-include.md) The optional local, gitignored `config/brief-include.md` adds standing worker instructions to every ship and scout brief. This keeps private brief content out of tracked files. When the file exists, `bin/fm-brief.sh` appends its text verbatim as the scaffold's last section, `# Home brief additions`, which defers to every other section of the brief, including the ship contract a later scout promotion appends below it. An absent or blank file changes nothing, while a present path that is not a readable regular file, or text carrying its own `Delivery contract: mode=` line, stops the scaffold before anything is written. The text is static and never executed or expanded; secondmate charters never take it, and the file is local to each home rather than part of secondmate inherited configuration. `bin/fm-brief.sh`'s header owns the placement rule and its safety argument. ## Home branch prompt include (config/branch-prompt-include.md) The optional local, gitignored `config/branch-prompt-include.md` adds standing home rules to the supervision branch's system prompt, the way `config/brief-include.md` extends worker briefs. The branch's headless engine loads none of the home's own hooks, `CLAUDE.md`, skills, or plugins, so this file is how a home gives the branch a home-specific rule. When the file exists, `bin/fm-branch-prompt.sh` appends its text verbatim as the prompt's last section, `# Home prompt additions`, which defers to every other section of the prompt. An absent or blank file leaves the prompt byte-identical, while a present path that is not a readable regular file stops the generator before it emits anything, so the branch does not start. The text is static and never executed or expanded, and an edit takes effect when the branch next builds its prompt for a new conversation. `bin/fm-branch-prompt.sh`'s header owns the placement rule and how it keeps the prompt cache stable. ## Herdr lifecycle spawn guard (config/herdr-lifecycle-spawn-guard) The optional local, gitignored `config/herdr-lifecycle-spawn-guard` presence flag opts this home into refusing, at spawn, a ship or scout brief that drives Herdr lifecycle without the Herdr isolation contract. Create it with `touch "$FM_HOME/config/herdr-lifecycle-spawn-guard"`; remove it to launch such briefs again. It is off by default: without the flag, `bin/fm-spawn.sh` launches the brief as before, and only the unguarded scaffold's own `# Herdr lifecycle declaration - NOT ENABLED` gate text asks the author to regenerate it. With the flag present, a brief that names `bin/fm-herdr-lab.sh`, or a `herdr session` or `herdr server` command with the verb `start`, `stop`, `delete`, `restart`, `reload-config`, or `live-handoff`, must carry the `# Herdr isolation - HARD SAFETY CONTRACT` section that `bin/fm-brief.sh --herdr-lab` emits. The refusal names the first matching command, so the author can tell a brief that truly drives Herdr lifecycle from one that only quotes a verb incidentally, and points at `bin/fm-brief.sh --herdr-lab` as the remedy. A refused spawn writes no task record, so the brief can be regenerated and the spawn repeated. The flag is read only for ship and scout spawns, secondmate charters never take it, and it is not inherited by secondmate homes. [`herdr-backend.md`](herdr-backend.md) owns the lab isolation contract itself; `bin/fm-spawn.sh`'s header owns the exact matching rule. ## Worker launch environment (config/launch-env-allowlist) The optional local, gitignored `config/launch-env-allowlist` limits the ambient environment passed to newly launched workers, scouts, and secondmates, including relaunches. T3 Code starts the provider from the server's environment and cannot apply this command boundary, so `backend=t3code` refuses when the file exists; [`t3code-backend.md`](t3code-backend.md#active-limits) owns that backend limit. With no file, ambient inheritance remains unfiltered: selected harness markers are cleared, while the provider, long-lived terminal daemon, and shell initialization determine which other variables reach the worker. Do not assume every worker inherits the invoking Firstmate process's current environment. The file is inherited into secondmate homes through the [primary-authoritative configuration contract](../.agents/skills/secondmate-provisioning/SKILL.md). Changes apply to subsequent launches; existing processes keep their environment. ### Allowlist format Create the file with one environment variable **name** per line, never credential values, assignments, wildcards, or shell commands. Blank lines and lines beginning with `#` are allowed. Invalid names, an unreadable or nonregular file, or a path inspection error (including an inaccessible configuration directory) stop the launch. An empty file enables filtering with only Firstmate's operational floor. For example, a provider using `OPENAI_API_KEY` and Git using an SSH agent could use: ```text # Provider credential already available in the destination pane OPENAI_API_KEY # Git over SSH using an existing agent SSH_AUTH_SOCK ``` ### Variables retained and where values come from Firstmate retains basic home, executable search, terminal, locale, temporary-directory, and backend routing variables, plus its explicit launch assignments, its ship and scout task marker, the compact-adviser kill switch described below, and enabled task trace. The allowlist floor also retains Tern block markers `TERN_IDENTITY`, `TERN_PANE`, `TERN_PANE_SOCKET`, `TERN_WINDOW_KEY`, and `TERN_WINDOW_SOCKET` so a worker's own Tern/TSP integration survives `env -i`. It retains `HC_SESSION`, `HC_TERMINAL_ID`, `HC_TERMINAL_SOCKET`, and `HC_SCOPE` the same way, so a worker in an hc terminal still reports its status to herdr-core. [`fm-spawn.sh --help`](../bin/fm-spawn.sh) owns the exact retained names and parsing mechanics. Other ambient names must be listed explicitly, including custom credential-store locations, proxy settings, and certificate overrides when required by the selected tools. The command shell and worker may still create their own variables. Unless the Orca environment source below is configured, allowed values come from the destination pane at execution time; they are neither copied from the invoking Firstmate process nor written into the launch command. Listing a name does not provision it in a daemon's environment or transfer credentials to another machine. On the Orca backend the destination is an Orca terminal whose environment comes from the Orca app and your shell startup files, not from Firstmate or its tmux session. Allowlisted names there resolve to whatever that terminal has, often your personal credentials or nothing, so a spawn with a nonempty allowlist prints a warning unless the Orca environment source below is configured. ### Orca environment source (config/launch-env-tmux-session) An Orca home can opt in to taking allowlisted values from one named tmux session instead of the Orca terminal. Create the local, gitignored `config/launch-env-tmux-session` containing that session's name, and keep the session running for subsequent Orca spawns. The source requires `config/launch-env-allowlist` and reads only its names from the session's local environment (`tmux set-environment -t `), so tmux's automatic values such as `SSH_AUTH_SOCK` are never forwarded unless listed. A listed name missing from the session, an unreadable session, or a malformed selector stops the spawn before Orca creates the worktree or terminal; a name removed with `tmux set-environment -r` stays deliberately unset. Source mode also sets `GIT_CONFIG_NOSYSTEM=1`, so a system Git credential helper cannot answer with personal credentials. Values travel only in the owner-only staged launch file, never terminal input, and the pane deletes that file before the worker starts. If the pane has not run the launch command and deleted the file within 30 seconds, or the spawn is interrupted before then, Firstmate removes the file unless the pane already did, leaves the Orca terminal open, and reports the spawn as failed rather than leaving credentials on disk. The source applies to fresh Orca spawns; `--relaunch` is refused on Orca because it has no recovery-grade agent-state classifier. The selector is not inherited into secondmate homes, and other backends ignore it. [`fm-spawn.sh --help`](../bin/fm-spawn.sh) owns the snapshot and launch-file mechanics. Project and harness sandbox settings still own filesystem access to personal credential stores. ### Authentication requirements Choose the minimum additions for the authentication method actually in use: | Provider or Git transport | Additional names needed | | --- | --- | | Provider login stored under the normal home directory | None for the environment contract; the same user still has access to that provider's stored login. | | Provider configured through environment variables | The exact credential and endpoint names required by that provider, for example `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`; a multi-provider tool needs each provider it will actually use. | | Custom provider store | Its configured location variables, such as `CODEX_HOME`, `GROK_HOME`, or `XDG_CONFIG_HOME`; Firstmate's existing explicit Claude and Muse store assignments still apply. | | Muse environment authentication | `META_API_KEY`, already present in the target tmux session environment; Firstmate's preflight requires the stored-login path on other backends. | | Git over SSH with an agent | `SSH_AUTH_SOCK`; add `GIT_SSH_COMMAND` only if the chosen transport requires that override. | | Git over SSH with a key file | No credential variable when normal SSH configuration selects the key; file permissions and any passphrase handling still apply. | | Git over HTTPS with a credential helper | Whatever the configured helper requires; a GitHub CLI helper using an environment token needs its selected `GH_TOKEN` or `GITHUB_TOKEN`. | ### Validation and security limits Verify the selected provider login and Git transport after opting in; Firstmate does not infer credentials from model names or install a secret manager. Raw launch commands run under noninteractive POSIX `sh` with this option and must use compatible syntax. The filter runs at the worker command boundary, after the terminal daemon and pane shell have started; it does not scrub either of those processes. This is not a sandbox: it cannot revoke same-user access to credential files, prevent tools or later shells from loading credentials again, or isolate processes from the same user's other processes. Regression coverage executes emitted launch commands with synthetic nonsecret values in [`tests/fm-spawn-dispatch-profile.test.sh`](../tests/fm-spawn-dispatch-profile.test.sh), and the Orca environment source in [`tests/fm-spawn-orca-worktree.test.sh`](../tests/fm-spawn-orca-worktree.test.sh). ### Compact adviser setting Every crewmate, scout, and secondmate Firstmate launches starts with `COMPACT_ADVISER_DISABLE=1` in its environment, on a fresh spawn and on a relaunch alike, so an unattended session never activates the compact adviser. This guarantee also covers raw launch commands, remote secondmates, and launches filtered by `config/launch-env-allowlist`; it does not depend on the destination environment already containing the variable. Firstmate provides no configuration or flag to change this value. This applies only to agents Firstmate launches; the captain's own primary Firstmate session is never given the variable. [`fm-spawn.sh --help`](../bin/fm-spawn.sh) owns the delivery mechanics, with focused regression coverage in [`tests/fm-spawn-compact-adviser-disable.test.sh`](../tests/fm-spawn-compact-adviser-disable.test.sh) and [`tests/fm-spawn-compact-adviser-disable-remote.test.sh`](../tests/fm-spawn-compact-adviser-disable-remote.test.sh). ### Commit attribution The optional local, gitignored `config/keep-ai-trailers` presence flag opts this home into keeping AI co-author trailers on its launched workers. With the flag absent, every Claude command line Firstmate builds carries inline `--settings` JSON with `"attribution":{"commit":"","pr":"","sessionUrl":false}`, every Devin worker config sets `"attribution": false`, and every fleet launch appends a worker-scoped `GIT_CONFIG` `core.hooksPath` entry pointing at `state/.git-hooks`, where git's `commit-msg` hook strips known AI trailers even when a runtime injects them after the typed message. The entry follows any `GIT_CONFIG_*` entries the worker inherits, so an inherited scoped `credential.helper` still applies to the worker and to the repository's own hooks, such as a git-lfs `pre-push`; an inherited `GIT_CONFIG_COUNT` that is not a decimal number, or that leaves no room for another entry within Git's signed int, stops the launch before the worker runs. T3 Code owns its provider command and cannot carry Claude's inline setting, but its per-worktree environment overlay selects the same Git hook; [`t3code-backend.md`](t3code-backend.md#active-limits) owns that backend limit. When the flag is present, Claude launches omit those attribution-off settings, Devin worker configs keep the user config's `attribution` setting (Devin's default is on), and fleet launches do not install or select the strip hooks, so Git uses the repository's configured hooks directly. Cortex Code has no attribution switch and its model writes a `Generated with Snowflake CoCo` line and a `Co-authored-by: Snowflake CoCo` trailer itself, so the strip hooks are the only mechanism that removes them from a Cortex worker's commits. `bin/fm-git-strip-ai-trailers.sh` owns the identities, the install, and chaining the hooks of whichever repository git is running in, including when `git -c core.hooksPath` supplies the pane's hook override, so a project hook such as husky still runs when stripping is enabled. A repository whose config sets `core.hooksPath` to the empty string runs no project hook, as in plain git; if the wrapper otherwise cannot resolve that repository's hooks directory, the git operation fails rather than silently skipping a project hook such as a pre-push guard. When stripping is enabled, the hooks directory is read-only, so a hook manager run inside a fleet pane (lefthook's npm postinstall, `pre-commit install`) fails instead of displacing the strip; install a project's hooks from outside the pane, where the wrappers chain them. The flag is a home-wide attribution choice, so it is inherited into secondmate homes under the [`secondmate-provisioning`](../.agents/skills/secondmate-provisioning/SKILL.md) inherited-local-material contract and a secondmate's own workers keep AI trailers too. Per-machine Cursor `cli-config.json` attribution-off is not this contract: it does not travel with Firstmate, defaults back to on when unset, and only feeds the CLI's request to the server, so it suppresses the trailer rather than preventing it. ## Project capacity (config/project-capacity) The optional local, gitignored `config/project-capacity` tells Firstmate how many workers a project can run at once on this machine, for a project whose machine-local resource - a heavy test suite, a local editor stack, a device - only serves a few workers at a time. Without it, dispatch stays uncapped as `AGENTS.md` section 7 describes, and a surplus worker is launched only to spend full-context turns waiting for the resource. The file lives in the machine's root Firstmate home, so every local secondmate home reads the same limit, and it holds one line per project: ```text # heavy suite serves two workers my-project 2 ``` The name is the project's registered name, which is its clone directory name and may contain spaces, and the number, the last field on the line, is a positive integer. A line that is only `#`, or that begins with `#` followed by whitespace, is a comment, as is a `#` line whose last field is not an integer. A project name may begin with `#` when that `#` is written immediately against the rest of the name and the line ends with the project's capacity. A name that is `#`, or that begins with `#` and a space, cannot be declared, because that line is a comment. A place is held by every ship or scout on that project in the root home or any local secondmate home registered under it, including one working in a separate clone of the same origin, until its ready PR is recorded or it is cleaned up; a local-only ship or a scout holds its place until cleanup. The declaration is matched by the spawning clone's directory name, so clones of the same origin share the cap only when they use that same directory name. A clone of that origin under a different directory name finds no declaration and is not capped, though its workers are still counted as holders for a same-origin clone that is capped. When every place is held, `bin/fm-spawn.sh` launches nothing, creates no record, leaves the backlog item queued, prints one `deferred:` line naming the holders, and exits 75, so Firstmate dispatches the item again once a place frees. A malformed or unreadable file refuses every fresh ship or scout spawn until it is fixed, rather than guessing the intended limit, and so does a local home's state directory or task record that cannot be read while counting a capped project's holders. Firstmate cannot see which part of a worker's life uses the resource, so the number bounds whole workers from launch to handoff, and the tightest resource every worker needs should decide it. [`bin/fm-project-capacity-lib.sh`](../bin/fm-project-capacity-lib.sh) owns the file format, what holds a place, and why concurrent spawns cannot both take the last one. ## 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. Firstmate chooses the best matching rule with judgment; shell scripts do not match the natural-language rules. Firstmate resolves the rule's profile object or array under `AGENTS.md` section 4 and `quota-array-dispatch`, then passes only concrete `--harness`, `--model`, and `--effort` flags to `fm-spawn.sh`. **Spawn requirements** - 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 resolve through `config/secondmate-harness` using the registered mate's id; see [Choose the secondmate harness](#choose-the-secondmate-harness) for pin precedence and legacy parsing. **Contract owners** 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. ```json { "preferred_provider": "", "shared_quota": false, "rules": [ { "when": "", "approval": "captain", "min_confidence": 0.85, "floor": { "scope": "", "min_percent": 20, "provider": "" }, "use": [ { "harness": "", "model": "", "effort": "", "provider": "", "quota_scope": "", "floor": { "scope": "", "min_percent": 50 } } ], "why": "" } ], "default": [ { "harness": "", "model": "", "effort": "" } ] } ``` **Required and optional fields** | Field | Requirement | | --- | --- | | `rules` | May be absent or empty for a default-only configuration. | | Rule `when` and `use` | Required for each rule. | | `use` and optional top-level `default` | Accept one profile object or a non-empty array of profile objects; the single-object form remains fully backward-compatible. | | Profile `harness` | Required in every profile. | | Profile `model` and `effort`; rule `why` | Optional. | | Top-level `preferred_provider` | Optional provider-family preference, applied after eligibility checks under `quota-array-dispatch`. | | Top-level `shared_quota` | Optional; see "Shared-quota opt-out" below. | **Preferred provider** `preferred_provider` accepts a provider ID matching `^[a-z0-9]+(-[a-z0-9]+)*\z`, using the same vocabulary as profile `provider`. For example, `"preferred_provider": "codex"` prefers the OpenAI subscription family across its eligible accounts, not one account or harness. Both bootstrap and the enabled resolver reject malformed values, including null, empty strings, uppercase letters, and whitespace. Bootstrap validates this field even without a resolver key because the agent-owned intake also honors it. An absent field preserves the existing routing behavior. The [profile-array procedure](../.agents/skills/quota-array-dispatch/SKILL.md#rank-by-spendpriority) owns family preference, uncertainty, and fallback after the existing gates. **Shared-quota opt-out** An optional top-level `"shared_quota": true` declares this home draws every provider's usage from one single shared budget, such as one workplace AI spend line, so per-provider quota rows do not describe what is left. With it set, typed dispatch resolution makes no `quota-axi` call, applies no quota floor or runway gate, ranks nothing by `spendPriority`, and resolves the matched rule's `use` profiles in declared order; every candidate line still prints, stating quota ranking is off by home declaration. `preferred_provider` is a separate home preference, not a quota signal, so it still applies: `preferred_provider` still narrows the pool; declared order then picks within it. Firstmate's own `quota-array-dispatch` intake honors the same declaration. Without the setting, or with `"shared_quota": false`, behavior is unchanged. The declared value must be a boolean; anything else is an actionable configuration error reported like any other malformed rule. **Fields applied only by typed resolution** Rule `approval`, `min_confidence`, and `floor`, and profile `provider`, `quota_scope`, and `floor` are optional declarations that only [typed dispatch resolution](#typed-dispatch-resolution-env-fm_dispatch_provider-fm_dispatch_model) applies in code; without that opt-in they are inert, and firstmate's own intake reads them as ordinary hints. The resolver supplies the fixed neutral Choice option `No listed rule applies to this task.` for work that matches no listed rule. - `approval` accepts only `"captain"` and means a task the rule matches is never dispatched from the tool's answer alone. `min_confidence` is a number from 0 through 1. The rule's own probability in the answer must reach it, replacing the resolver's global 0.6 floor on the answer's confidence. Set it high when a wrong pick is costly and low when the rule is a safe runner-up. **Rule quota floors** - A rule `floor` names the quota-axi `provider` and `scope` whose `effectivePercentRemaining` must be at least `min_percent` for the rule's profiles to apply. - A provider-only rule floor on an expanded provider binds to its `default` account row. - An absent or unknown row or unmeasured provider makes the floor unverifiable and escalates without authorizing default routing. - A known percentage below the floor makes the tool resolve among `default` profiles instead. **Provider identifiers and mappings** A profile `provider` optionally names the quota-axi provider family whose rows apply to that profile; when present, profile and rule-floor provider IDs must match the strict whole-string pattern `^[a-z0-9]+(-[a-z0-9]+)*\z`. Bootstrap's resolver-only validation gate is described under "Validation and diagnostics" below. Typed resolution additively recognizes `gemini` because AGENTS.md section 4 verifies it for crewmate and scout dispatch. | Harness | Provider declaration on the opted-in resolver path | | --- | --- | | `claude`, `codex`, `grok`, `kimi`, `cursor`, `agy`, `muse` | The resolver has an authoritative single-provider mapping. | | Every other verified harness | Must declare `provider` explicitly; this includes multi-provider `pi`, `pi-signed`, `omp`, and `opencode`, and unmapped `gemini`, `rovo`, `devin`, `commandcode`, `cortex`, and `kiro`; omission is an actionable configuration error before any request. | This single-provider table is separate from the frozen legacy mapping used by `fm-quota-choose.sh`, so additions cannot alter no-key routing. **Profile quota scope** - A profile `quota_scope` is a non-empty string naming one quota-axi row of that profile's provider and matched account, such as an Antigravity tier scope (`gemini` or `claude_gpt`) that no model-name rule can bind. - The named row joins the candidate's applicable rows by exact scope match, so a known zero or `exhausted_now` row makes the candidate ineligible and a healthy row ranks it. - A named row absent from the snapshot leaves the candidate eligible but unranked and is reported as disclosed uncertainty, never as a block. - Firstmate never derives the scope from a model name and never falls back to a profile or rule `floor` scope; a profile without `quota_scope` resolves exactly as before. **Profile quota floors** - A profile `floor` contains only `scope` and `min_percent`, always uses that profile's provider and matched account, and makes that one candidate ineligible below `min_percent` on the named scope. - An absent or unknown named row also makes the candidate unrankable and is reported as an unverifiable floor, not as a known shortfall. **Model, effort, and fallback behavior** - `ultra` is native-only: the model-aware validation contract and launch mapping are owned by `bin/fm-harness.sh validate-native-effort` and `bin/fm-spawn.sh` respectively. - Codex `max` is valid when the profile selects `gpt-5.6-luna`, whose installed catalog entry supports that reasoning level. - On pane backends, an omitted model or effort means the selected harness uses its own default for that axis; [T3 selection](t3code-backend.md#provider-instances-and-models) owns server-backed defaults. - OpenCode receives the effort as its default `build` agent's `variant`, keyed to the resolved model, inside the `OPENCODE_CONFIG_CONTENT` JSON its launch already writes (the per-model reasoning-effort field of the config schema, verified on opencode 1.18.32); with no model resolved, the effort is recorded in task metadata but omitted from the launch. - 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`. - On pane backends, except for `ultra` under the native-effort contract above, an effort value the chosen harness does not accept is recorded as `effort=` in task meta for traceability but omitted from the launch flags. For T3 Code, follow the [catalog validation contract](t3code-backend.md#provider-instances-and-models). - Bootstrap reports unsupported harness/model/effort combinations as a `CREW_DISPATCH` diagnostic when they are visible in the file. See [`docs/examples/crew-dispatch.json`](examples/crew-dispatch.json) for a starting point to copy into local `config/crew-dispatch.json`; its Pi default declares the `claude` provider required for typed resolution of that Anthropic model. **Validation and diagnostics** - 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, malformed rules, an empty or malformed profile array, a malformed `preferred_provider`, an unverified harness, or an effort value unsupported by that harness is reported as `CREW_DISPATCH: invalid config/crew-dispatch.json - ...`. - Bootstrap enables resolver-only checks for `approval`, `min_confidence`, `floor`, and present `provider` declarations when `TYPESAFE_API_KEY` is non-empty in the environment or the home's `.env`, or when the home's valid `config/dispatch-credential-sources.json` names a `typesafe` source, regardless of `FM_DISPATCH_PROVIDER`. Bootstrap only checks that declaration; it never reads the key or runs the source's tool. An OpenRouter key or OpenRouter declaration alone does not enable these bootstrap checks; the resolver validates those declarations itself before any request. Without a typesafe key or a declared typesafe source, bootstrap retains its pre-existing verified-harness baseline. - 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. **Inheritance** Secondmate homes inherit this file from the primary, so a secondmate's own crewmates apply the same dispatch profile behavior. ## Typed dispatch resolution (.env FM_DISPATCH_PROVIDER, FM_DISPATCH_MODEL) `bin/fm-dispatch-resolve.sh` resolves one concrete crewmate or scout profile from a written brief through typesafe.ai, OpenRouter, any System One-compatible host, or OpenAI, so the rule match that firstmate otherwise reasons out in its own context becomes one short tool turn. `FM_DISPATCH_PROVIDER` selects `typesafe`, `openrouter`, `systemone`, or `openai`, and `FM_DISPATCH_MODEL` selects the model. Each setting comes from the calling environment or the home's gitignored `.env` through `bin/fm-env-lib.sh`; a non-empty environment value wins. OpenRouter, `systemone`, and OpenAI each require their explicit `FM_DISPATCH_PROVIDER` value and a non-empty `FM_DISPATCH_MODEL`; an omitted model is an actionable configuration error, and the presence of a provider's key never selects that provider by itself. `systemone` also requires `FM_DISPATCH_BASE_URL`, an `http://` or `https://` base such as a local llama.cpp server or a hosted System One-compatible service, and posts to `/v1/systemone`; a missing or scheme-less base URL is an actionable configuration error. With no provider setting, the original typesafe.ai behavior is unchanged: `jev-latest` is used when `TYPESAFE_API_KEY` is available, `FM_DISPATCH_MODEL` is ignored, and the resolver is off when no key is available and no `typesafe` credential source is declared. Explicit `FM_DISPATCH_PROVIDER=typesafe` also uses `jev-latest` when no model is set. The selected provider needs a non-empty key from the environment, else `.env`, else, for `typesafe` and `openrouter` only, the credential source the home declares for that provider; `systemone` is the exception, where an absent `FM_DISPATCH_API_KEY` means the request carries no `Authorization` header. Off means one `dispatch-resolve: off` line on stderr, nothing on stdout, exit 0, and no network call, so firstmate dispatches exactly as it does without the tool. A declared credential source that cannot supply its key is not off; see "Credential source declaration" below. This section is the single owner of the tool's operator contract; the script header owns its exact flags and output lines, and "Crew dispatch profiles" above owns the declared top-level, rule, and profile fields it applies. Rules come only from the effective home's `config/crew-dispatch.json`; `FM_CONFIG_OVERRIDE` selects the config directory for tests and specialized setup like the other scripts. ```sh bin/fm-dispatch-resolve.sh data//brief.md --project # TOON block on stdout ``` **When firstmate invokes the resolver** Firstmate invokes the resolve path directly after writing the brief, without a preflight; the absent-key off line is handled exactly like every other non-clear outcome. **What the model receives** When on and at least one rule exists, the tool sends the project name and the brief's task-specific text as state and asks one Choice question whose options are every rule's `when` plus the fixed neutral option for no matching rule; the model never sees quota, catalogs, `why`, `use`, approvals, or confidence floors. OpenRouter's Decisions API and a `systemone` host accept the same Choice request shape and return the same typed answer, including confidence and a probability per option. For OpenAI's Decisions API the resolver rewrites that request into OpenAI's shape - the state as JSON text `input` and one `choice` question named `rule` whose `choices` are the same options - and maps the answer's per-option probability array back; a refusal, a missing or duplicated rule answer, or duplicated option values is rejected as malformed. Every provider then uses the same response validation and post-model gates. The task-specific text is the brief's `## Captain's intent` and `## Firstmate spec` sections under `# Task` that `bin/fm-brief.sh` scaffolds, read by the same parser that feeds `fm-spawn.sh` validation and the no-mistakes `--intent` contract; a brief with neither section is sent whole. When the sections are sent from a scout brief, the line `Brief kind: scout (report only)` comes first, taken from the scaffold's scout contract line; ship briefs and briefs sent whole get no kind line. A ship brief's delivery mode is deliberately not sent, because in live runs naming it pushed a routine ship brief toward the hardest tier (see [the verification record](verification/dispatch-resolve.md)). The scaffold's standard setup, rules, and definition-of-done text is the same in every brief, so leaving it out keeps its safety language from reading as a signal about the task. **Never-send list (config/dispatch-never-send)** The optional local, gitignored `config/dispatch-never-send` keeps values you name from ever leaving the machine in a resolver request. It has no default entries, and an absent file changes nothing. Like `config/crew-dispatch.json`, it is inherited into secondmate homes, so a secondmate's resolver withholds the same values. Each non-blank line not beginning with `#` is one literal value, matched case-insensitively. Every entry is trimmed of surrounding whitespace, and any run of whitespace, in the entry or in the checked text, counts as one space, so a value the brief wraps across lines still matches. ```text # Client names Example Client Ltd ``` Before the request is sent, the endpoint URL and every string in the final provider-specific request body are checked, including strings inside JSON text such as OpenAI's `input`: the selected model, the project name, the task text, each rule's `when`, and the fixed question text. A match stops the request: the resolver behaves exactly as when it is off, printing one `dispatch-resolve: off (...; nothing sent)` line on stderr and nothing on stdout, making no network or quota call, and exiting 0, so firstmate dispatches through its existing intake. A list that is present but not a readable regular file also stops the request the same way rather than sending unchecked text. That one diagnostic names the list line number at most and never prints the listed value or the matching text. **Missing or invalid rules** An absent rules file, a default-only file, or `rules: []` returns the non-clear reason `no rules to match` without a model or quota request, leaving firstmate's existing routing in control; an existing but unreadable or malformed rules file, including a broken symlink, remains an actionable exit 2 configuration error. **Checks performed after the answer** After the answer, code applies all remaining checks and ranking: - The confidence floor and the matched rule's `approval` and `floor`. - Each candidate's `provider`, `quota_scope`, and `floor`. - Every applicable account-wide and model/product row from one `quota-axi --json` snapshot. - The optional `preferred_provider` restriction under the [profile-array procedure](../.agents/skills/quota-array-dispatch/SKILL.md#rank-by-spendpriority). - The numeric `spendPriority` argmax within that candidate pool, using each candidate's limiting row. - With the home's `shared_quota: true` declaration ("Crew dispatch profiles"), the quota snapshot, floors, runway gates, and `spendPriority` argmax are skipped entirely; resolution is the matched rule's declared profile order (`preferred_provider` still narrows the pool; declared order then picks within it). The resolver prints its preferred-family decision while retaining evidence for every candidate. If preferred candidates remain eligible but none is rankable, it escalates instead of selecting another family. It never introduces candidates outside the matched rule or its existing default fallback. The [shared quota library](../bin/fm-quota-axi-lib.sh) accepts schema 5 and schema 6 and implements the [account-matching contract](../.agents/skills/quota-array-dispatch/SKILL.md#1-eligibility). - An expanded provider with no matching account row leaves the candidate eligible but unranked. - Known applicable rows from a provider with partial quota semantics remain rankable; rows whose own status is not known remain unrankable. **Confidence and fallback rules** - A rule that declares `min_confidence` is checked against that rule's own probability, whether it is the picked option or a runner-up, so a runner-up never needs weaker support than it would as the pick. - A picked rule without `min_confidence`, and the neutral option, keep the global 0.6 floor on the answer's confidence exactly as before, so a file with no declared floors behaves as it did. When the picked rule declares its own floor but its probability falls below it, the tool checks the other options: 1. Find the most probable other option that clears its own floor: the rule's `min_confidence`, or 0.6 otherwise. 2. Print a `fallback:` line naming both floors and resolve that rule as though it had been picked. No qualifying option, or two equally probable qualifying options, produces `ambiguous`. **Candidate eligibility and evidence** - Any applicable `exhausted_now` row or known zero bound makes that candidate ineligible, and a known profile-floor shortfall does the same before unrelated quota uncertainty is considered. - Missing or nonnumeric `spendPriority` evidence is never ranked, and every candidate is printed beside its evidence or the reason it was not rankable, including on ambiguous and approval-gated outcomes that emit no profile. - On the opted-in path, duplicate concrete profiles with the same harness, model, and effort inside one rule or the default array are configuration errors rather than ties. **Outcomes and exit status** | Result | Meaning | | --- | --- | | `clear` | A `profile:` line ready for `fm-spawn.sh`. | | `ambiguous` | Confidence below the floor with no runner-up taken. | | `escalate` | An approval-gated rule, unverifiable rule floor, nothing rankable, or a genuine tie. | | `error` | API, network, malformed response metadata, rendering, quota-axi failure, or a declared credential source that cannot supply its key. | Every result above exits 0. - Response probabilities must contain exactly every offered choice, use numeric values from 0 through 1, and sum to approximately 1 within 0.01. - Only a usage or configuration error exits 2: an unreadable brief, an existing but unreadable or malformed canonical rules file, an unreadable or invalid credential source declaration, a missing `jq`, an unknown provider, a non-typesafe provider without a model, or `systemone` without a valid base URL, each reported and never selected around. - Missing `curl` is a normal structured `error` outcome with exit 0 so firstmate uses today's routing. **Firstmate retains the dispatch decision** The tool never replaces firstmate's judgment, `quota-array-dispatch`, the captain-approval gate, or `fm-spawn.sh` validation; `AGENTS.md` section 4 owns what firstmate does with each outcome. By accepted design, a `clear` result does not enforce catalog/authentication, reasoning-class, or completion-runway gates. Firstmate still applies those gates before dispatch, including when `preferred_provider` is present. Explicit captain overrides retain precedence over the resolver's suggestion. Firstmate passes its profile line unless it states a reason to override, such as the brief's reasoning class or an eligible-unranked-candidate note; every non-clear result returns to the full existing intake. **Credential source declaration (config/dispatch-credential-sources.json)** The optional local, gitignored `config/dispatch-credential-sources.json` tells the resolver where a provider's key lives when neither the environment nor `.env` supplies one. It names a source and never holds a key. It is read from the effective config directory, which is `FM_CONFIG_OVERRIDE` when set and `$FM_HOME/config` otherwise, so each home reads only its own file. It is not among the settings `bin/fm-config-inherit-lib.sh` propagates, so a secondmate home that wants a declared source keeps its own file. A home without the file behaves exactly as it did before the file existed. It covers the `typesafe` and `openrouter` keys only; the `systemone` and OpenAI keys still come from the environment or `.env`. The file is one JSON object keyed by provider, `typesafe` or `openrouter`, and each value says where that provider's key is: ```json { "typesafe": { "source": "macos-keychain", "service": "typesafe-api-key", "account": "me" } } ``` `source` is required and names the adapter, and `macos-keychain` is the first. It requires `service`, accepts an optional `account`, and reads that generic-password item with `security find-generic-password -s [-a ] -w`. Store the item with `security add-generic-password -s [-a ] -w`, which prompts for the key and lets `security` read it back without an access dialog. Only the service and account reach `security`'s argument list, and the key comes back on a pipe. No other field is accepted, so the file has nowhere to put a key. `bin/fm-credential-source-lib.sh` owns the schema and the adapters, so a further source adds an adapter there rather than a special case in the resolver. The resolver reads the file only after the environment and `.env` have supplied no key for the selected provider, and it uses only that provider's entry. A declaration for one provider never feeds another provider and never selects a provider. Three outcomes stay distinct: - **Off.** No entry for the selected provider and no key keeps today's behavior: the `dispatch-resolve: off` line, nothing on stdout, and exit 0. - **Configuration error.** An unreadable file, malformed JSON, an unknown provider, an unknown or missing field, or a value that is not a plain non-empty string exits 2 with one line that names the problem. The check covers the whole file, so an invalid entry for a provider that is not selected also exits 2. The line names providers and fields and never prints a declared value. - **Unavailable source.** An entry whose source this host cannot run, or that yields no key, ends as the `error` outcome with exit 0 and a reason ending `nothing sent`, and makes no network or quota call. The reason says whether the host cannot run the source, the source holds no readable key, or it did not answer in time, and it never relays the tool's own output. A source that does not answer within 5 seconds ends the same way, so an unanswered Keychain access prompt cannot hold an intake. A configuration installer can check a source before it writes anything: ```sh bin/fm-dispatch-resolve.sh --supports-credential-source macos-keychain ``` The probe prints `supported` and exits 0, or prints `unsupported` and exits 1, for a source name on the host it runs on. `unsupported` covers an unknown name, a host the adapter does not run on, and a missing tool. The probe reads no declaration and no credential and runs no credential tool. An installer should treat any status other than 0 as a refusal, because a resolver that predates the probe rejects the flag with exit 2. **Key handling and provider settings** - The resolver copies every environment-provided key into a non-exported private variable and unsets `TYPESAFE_API_KEY`, `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, and `FM_DISPATCH_API_KEY` before launching child processes; bootstrap also protects the typesafe key. - A key read from a declared source is captured into the same private variable, so it is never exported either. - The resolver sends only the selected key to `curl` as a header read from a file descriptor, never on argv, and never prints, logs, or writes any key. - The typesafe.ai endpoint is `https://api.typesafe.ai/v1/systemone`, the OpenRouter endpoint is `https://openrouter.ai/api/alpha/decisions`, the OpenAI endpoint is `https://api.openai.com/v1/decisions`, and the `systemone` endpoint is `/v1/systemone`. - The default confidence floor remains 0.6; request timeouts are 5 seconds for typesafe.ai and 10 seconds for OpenRouter, `systemone`, and OpenAI. - A CPU-only local `systemone` host may not answer within its 10-second timeout; that is an ordinary `error` outcome, and firstmate keeps its existing intake. The recommended OpenRouter model is `typesafe/jev-1.13` because it is built for fast typed choices and supplies native per-option probabilities without generated text. The dated model catalog observation is recorded in [dispatch verification](verification/dispatch-resolve.md#the-api-the-tool-depends-on). The resolver never chooses this model automatically; use these settings in the calling environment or the home's `.env`: ```dotenv FM_DISPATCH_PROVIDER=openrouter FM_DISPATCH_MODEL=typesafe/jev-1.13 ``` A `systemone` or OpenAI setup names its model the same way; the resolver never picks one for them: ```dotenv FM_DISPATCH_PROVIDER=systemone FM_DISPATCH_MODEL= FM_DISPATCH_BASE_URL=http://127.0.0.1:8080 ``` The live rule-match evidence is recorded in [`verification/dispatch-resolve.md`](verification/dispatch-resolve.md). ## Toolchain On session start the first mate detects what its required toolchain is missing or too old and lists each problem with its exact install or upgrade command, or with manual instructions. It installs missing automatically supported tools only after you say go; for an installed but outdated tool, run the printed upgrade command after consent, while 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. **Universal requirements** Every home requires: - node and git. - gh, with GitHub authentication through `gh auth login`. - no-mistakes v1.46.0 or newer. - Compatible gh-axi. - chrome-devtools-axi. - Compatible tasks-axi, as specified in "Backlog backend" above. - Compatible quota-axi. [`bin/fm-bootstrap.sh`](../bin/fm-bootstrap.sh) owns the axi-family floor policy and the gh-axi and lavish-axi floors, while [`bin/fm-tasks-axi-lib.sh`](../bin/fm-tasks-axi-lib.sh) and [`bin/fm-quota-axi-lib.sh`](../bin/fm-quota-axi-lib.sh) hold their own tools' floor constants. 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 and chrome-devtools-axi cover GitHub and browser operations, and tasks-axi plus quota-axi back backlog mutations and quota-aware array dispatch. Lavish is a presentation-only dependency for visual decisions and reports; nonvisual work can proceed with plain text when it is unavailable. **Backend requirements** 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. `fm_backend_required_tools` in `bin/fm-backend.sh` owns the backend additions: | Resolved backend | Additional tools | | --- | --- | | `tmux` | `tmux`, `treehouse` | | `herdr` | `herdr`, `jq`, `treehouse` | | `zellij` | `zellij`, `jq`, `treehouse` | | `orca` | `orca` | | `cmux` | `cmux`, `jq`, `treehouse` | | `paseo` | `paseo`, `jq`, `treehouse` | | `rex` | `rex`, `jq`, `treehouse` | | `t3code` | `node`, `treehouse` | | `tern` | `tern`, `jq`, `treehouse` | The JSON-emitting adapters (`herdr`, `zellij`, `cmux`, `paseo`, `rex`, `tern`) need `jq` because their spawn and liveness paths parse backend JSON. Every session-provider-only backend (`tmux`, `herdr`, `zellij`, `cmux`, `paseo`, `rex`, `t3code`, `tern`) uses `treehouse` for worktrees. 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 and the Rex server bundle's `rex`. 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, cmux, paseo, rex, t3code, or tern home is therefore never told `tmux` is missing, and the `treehouse` durable-lease upgrade check runs only for the backends that actually use treehouse. **Feature-specific requirements** - When `config/crew-dispatch.json` exists, bootstrap also requires `jq` for dispatch profile validation. - When Relay is opted in, bootstrap also requires `curl` and `jq` before arming the relay poll shim. - When `config/record-export-root` exists, record sync and teardown archival require `python3` for descriptor-relative filesystem transactions and strict manifest parsing. **Missing and outdated tool diagnostics** `tasks-axi` and `quota-axi` are essential bootstrap tools in every profile. An absent required tool with an automated install path reports `MISSING:` with its install command, while a present required tool below its floor or missing a required capability reports `OUTDATED:` with its installed version (or `unparseable`), requirement, and upgrade command; manual-only and presentation-only dependencies retain their dedicated diagnostics, and [`bin/fm-bootstrap.sh`](../bin/fm-bootstrap.sh)'s header owns these exact line formats. - An absent `treehouse` reports `MISSING:` with its install command, while an installed build without `treehouse get --lease` support reports `OUTDATED:` with the `treehouse update` upgrade. - An absent `no-mistakes` reports `MISSING:` with its install command, while a version below 1.46.0 or an unparseable version reports `OUTDATED:` with the `no-mistakes update` upgrade. - An absent `tasks-axi` reports `MISSING: tasks-axi (install: npm install -g tasks-axi)` and a below-floor or feature-incompatible one reports `OUTDATED:` with its required version and any missing feature-probe capability, while retaining that same `npm install -g tasks-axi` upgrade; when `config/backlog-backend` is not `manual`, a home with a configured non-markdown adapter or a markdown backlog refuses lifecycle mutation until compatible `tasks-axi` is on `PATH`, while a manual-backend home keeps its backlog hand-edited. - An absent `gh-axi` reports `MISSING: gh-axi (install: npm install -g gh-axi && gh-axi setup hooks)` and an outdated one reports `OUTDATED:` with its `gh-axi update` upgrade. - An absent or board-incompatible `lavish-axi` reports `PRESENTATION_UNAVAILABLE` with the 0.1.77 compatibility floor, install command, and explicit text fallback; compatible versions below 0.1.80 retain legacy board replies and report an upgrade recommendation for synchronous acceptance, while [`bootstrap-diagnostics`](../.agents/skills/bootstrap-diagnostics/SKILL.md) owns diagnostic handling. - An absent `quota-axi` reports `MISSING: quota-axi (install: npm install -g quota-axi)` and an incompatible one reports `OUTDATED:` with its `quota-axi update` upgrade; firstmate cannot resolve a profile array without a compatible binary. **Checkout diagnostics** 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. **Project refresh at startup** The locked session-start deferred network stage runs bootstrap's best-effort project clone refresh through `fm-fleet-sync.sh`; [`fm-bootstrap.sh`'s header](../bin/fm-bootstrap.sh) owns the exact clone-refresh overlap, liveness-before-convergence, per-mate concurrency, ordered diagnostic replay, and sequential-fallback contract. - 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. **Stale Git lock recovery** 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. **Secondmate sync at startup** The same deferred network stage performs guarded tracked-file sync and propagates declared inherited local material into each validated live home under that sequencing contract. Local routes use direct guarded filesystem operations, while remote routes delegate sync and allowlisted transfer through their configured SSH host without probing any unconfigured fleet. - 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-` 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. **Push inherited configuration during a session** 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; its [help](../bin/fm-config-push.sh) owns reporting and exit semantics, and [`fm_config_inherit_items`](../bin/fm-config-inherit-lib.sh) declares the inherited items. - The [`secondmate-provisioning` skill](../.agents/skills/secondmate-provisioning/SKILL.md#charter-and-seed) owns the local and remote re-read behavior shared with locked bootstrap inheritance, including unchanged retries and interrupted transfers. - 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. ## Forge selection (data/projects.md forge= token) A project with no `forge=` token is served by the GitHub tooling, `gh` and `gh-axi`, exactly as before the token existed, and the token changes nothing for existing entries. A remote-backed project on a Forgejo or Gitea server is registered with `forge=forgejo`, one value of the same closed `forge=` binding that Gerrit uses; `bin/fm-project-mode.sh`'s header owns its grammar, values, and refusals, and `bin/fm-project-mode.sh --forge ` reads it back. Firstmate passes the binding to `bin/fm-brief.sh --forge` at intake the same way it passes `--mode`, and `bin/fm-spawn.sh` refuses a brief whose forge disagrees with the registry; on `forgejo` the generated ship brief names `tea` instead of `gh-axi`. The URL-driven provider dispatch in [`bin/fm-pr-lib.sh`](../bin/fm-pr-lib.sh) recognizes a GitHub, GitLab, Gerrit, or Forgejo PR/MR/change URL once one exists, regardless of this token. A Forgejo (or Gitea) project needs `tea` on `PATH` plus a `tea login` registered for that project's host (`tea login add`); the login is matched to a PR's host automatically, never configured per project. A GitLab project needs no forge token, only `glab` on `PATH`, authenticated via `glab auth login`. [`docs/gitlab-merge-watch.md`](gitlab-merge-watch.md) and [`docs/forgejo-tea-integration.md`](forgejo-tea-integration.md) hold the empirically verified CLI behavior each provider's watch and merge path depends on. A project whose reviews are created by a review tool that uploads its own snapshot of the worker's commits is registered `[direct-PR forge=external]`; it is never detected, ships `direct-PR` only, carries no yolo, and needs the [external review tool](#external-review-tool-configexternal-reviewmd-configexternal-review-url-configexternal-review-backup) configuration below. [`docs/external-review-forge.md`](external-review-forge.md) is the design. ## PR text selection (data/projects.md pr= token) A project with no `pr=` token keeps today's behavior: the no-mistakes pipeline's `pr` step writes the pull request title and body and refreshes them on every validated push. A project whose outside maintainer reviews and merges, and whose hand-written PR text the pipeline would overwrite, is registered `[no-mistakes pr=manual]`. `pr=manual` is the one accepted value, off by default, read like `forge=`: `bin/fm-project-mode.sh --pr ` reads it back, and its header owns the grammar and refusals. It applies to a `no-mistakes` ship on a project with no `forge=` binding only, so the registry refuses it on `direct-PR`, which already leaves the PR text to the worker, on `local-only`, which opens no PR, and beside a forge. Firstmate passes it to `bin/fm-brief.sh --pr manual` at intake the same way it passes `--forge`, `bin/fm-spawn.sh` refuses a brief whose `pr=` disagrees with the registry, and `bin/fm-promote.sh` reads it from the registry. The generated brief then has the worker pass `--skip pr,ci` on every `no-mistakes axi run`, write the PR title and body itself, open or update the PR with `gh-axi`, and wait for the PR's checks; `bin/fm-dod-lib.sh` owns that text. The token is the thin registry form of no-mistakes' own keep-text option, [kunchenguid/no-mistakes#1648](https://github.com/kunchenguid/no-mistakes/issues/1648), and is meant to map onto it once that exists. ## GitHub Enterprise hosts (config/github-hosts) `config/github-hosts` is an optional local, gitignored file naming the GitHub Enterprise hosts whose pull request URLs this home accepts, one lowercase hostname per line, such as `leadsonline.ghe.com`. The PR library reads `github-hosts` from `FM_CONFIG_OVERRIDE` when nonempty, otherwise from `$FM_HOME/config`, falling back when `FM_HOME` is unset or empty to `config/` under the library's own code root, captured when the library is sourced. `github.com` needs no line and is always accepted. Blank lines, `#` comments, and any other line authorize nothing: only a line equal to the host is a match, so the file cannot be made to allow more than it spells out. An absent, unreadable, or symlinked file authorizes no Enterprise hosts. GitHub pull request URLs are otherwise identical on every GitHub host, so a hostname's shape cannot show whether it is a GitHub host or a look-alike such as `github.com.evil`, and [`bin/fm-pr-lib.sh`](../bin/fm-pr-lib.sh)'s `fm_github_host_allowed` accepts an Enterprise host only when this file names it. That covers registration through `bin/fm-pr-check.sh`, the merge poll, `bin/fm-pr-merge.sh`, and the merge-outcome and teardown reads, which all carry the parsed host through `gh` and `gh-axi` via `GH_HOST`. A host named here still needs its own `gh` authentication (`gh auth login --hostname `), exactly as `gh` itself requires. The file is local to each home and not part of secondmate inherited configuration, so a secondmate home that serves such a project names the host in its own copy. The host is part of each recorded PR identity, so removing a host while a task's pull request is still open stops that task's merge monitoring rather than pointing it at another host. ## External review tool (config/external-review.md, config/external-review-url, config/external-review-backup) These optional local, gitignored files configure a home's `forge=external` projects; a home with no such project needs none of them. | File | Required | Meaning | | --- | --- | --- | | `config/external-review.md` | yes, for any `forge=external` scaffold or spawn | Worker instructions for the home's review tool: how to validate, upload, and read review state, and which state is the stopping point. `bin/fm-dod-lib.sh` appends it verbatim to the Definition of done under `## Review tool instructions (home configuration)`. | | `config/external-review-url` | no | One extended regular expression a ready review URL must match in full. Without it, any `https://` URL with no whitespace, control characters, query, or fragment that is not a forge pull request URL is accepted. | | `config/external-review-backup` | no | Line 1 names a git remote in the project clone that is the captain's personal backup remote; an optional line 2, `prefix=`, sets the backup ref prefix, default `backup/`. | A missing, unreadable, or empty adapter stops the scaffold or spawn, as does an adapter carrying its own `Delivery contract: mode=` line, because a worker with no upload instructions would otherwise improvise a push. A backup remote named `origin`, absent from the project clone, or pointing at one of origin's URLs stops the spawn, as does a backup prefix that is empty, invalid, or equal to or starting with the task branch prefix. The files are static, never executed, and local to each home rather than part of secondmate inherited configuration. `bin/fm-external-review-lib.sh`'s header owns the readers, and [`docs/external-review-forge.md`](external-review-forge.md) owns the backup rule and its teardown consequence. ## Watched tool updates (config/watched-tools.json) `config/watched-tools.json` is an optional local, gitignored list of the tools this home depends on. When it is present and the check is armed, [`bin/fm-tool-update-check.sh`](../bin/fm-tool-update-check.sh) reports two conditions, and keeps them deliberately distinct: - ` update available` means a newer version exists at the tool's update source. - ` update not in effect` means a newer copy is already installed on this host, but `PATH` still resolves an older one. The second condition is the reason the check exists. An update can install correctly and stay inert because an earlier `PATH` entry still holds an older copy, and a check that only asks whether a newer version is published reports that host as up to date. The script therefore runs every copy of a watched command found on `PATH` and asks it for its own version, rather than trusting one lookup or reading a version out of a directory name. It only reports; it never installs or updates tools, fetches into a watched git repository, or changes `PATH` or a version manager. This section is the single owner of the canonical schema. `bin/fm-tool-update-check.sh` owns probe mechanics, cadence, and the report record. ```json { "tools": [ { "name": "