Skip to content
2 changes: 1 addition & 1 deletion .agents/skills/ask-user-authority/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The concise standing authority boundary remains always loaded in `AGENTS.md` sec
Destructive, irreversible, and genuinely security-sensitive choices always escalate regardless of whether they also expand the contract.

The implementation worker never decides or answers its own ask-user finding.
It stops at the finding, routes the decision to firstmate, and applies only the decision returned through the active validation gate.
It reports the finding, routes the decision to firstmate, waits in the same turn, and applies only the decision returned through the active validation gate.

## Captain-facing escalation

Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/firstmate-codexapp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ For a Firstmate-managed task, include an explicit status instruction:

```text
Append supervisor-visible status lines to <absolute-firstmate-home>/state/<task-id>.status.
Use only these prefixes for status changes: working:, needs-decision:, blocked:, paused:, done:, failed:.
Use only these prefixes for status changes: working:, needs-decision:, blocked:, paused:, resolved:, done:, failed:.
Use paused: only for a deliberate known external wait that should be rechecked later, never for a blocker that needs firstmate to act.
Before doing substantive work, append "working: Codex Desktop thread started".
```
Expand Down
8 changes: 6 additions & 2 deletions .agents/skills/harness-adapters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ The supported launch-profile flags below are verified locally; each row records
| Harness | Model flag | Effort flag | Notes |
|---|---|---|---|
| claude | `--model <model>` | `--effort <low\|medium\|high\|xhigh\|max>` | Verified on Claude Code 2.1.196. |
| codex | `--model <model>` | `-c 'model_reasoning_effort="<low\|medium\|high\|xhigh>"'` | Verified on codex-cli 0.142.1. The installed binary schema contains `model_reasoning_effort`, the active config uses it, and the bundled model catalog advertises only low/medium/high/xhigh. `max` is omitted. |
| codex | `--model <model>` | `-c 'model_reasoning_effort="<low\|medium\|high\|xhigh\|max>"'` | Verified 2026-08-20 on codex-cli 0.147.0; see the [Codex CLI effort evidence](../../../docs/verification/runtime-backends.md#codex-cli-effort). |
| grok | `--model <model>` | `--reasoning-effort <low\|medium\|high>` | Verified on grok 0.2.99 (2026-07-13). `--effort` is an alias, but firstmate's profile axis is reasoning effort. As of 0.2.99 the ceiling is `high`; both `xhigh` and `max` are rejected with `use one of: high, medium, low`, so firstmate omits them. |
| pi / pi-signed | `--model <model>` | `--thinking <low\|medium\|high\|xhigh\|max>` | Verified 2026-07-27 on Pi and pi-signed 0.82.0. Both expose the same accepted thinking levels and completed the same model-qualified max-thinking smoke. |
| opencode | `--model <provider/model>` | none for firstmate's interactive launch | Verified on opencode 1.17.6. `opencode run` has `--variant`, but firstmate launches the interactive `opencode --prompt` path, which has no verified effort flag. |
Expand Down Expand Up @@ -210,7 +210,7 @@ A project-level `.claude/settings.json` only takes effect when Claude Code's pro
After those settings are loaded, hook command resolution is still cwd-sensitive because Claude Code runs commands through `/bin/sh` against the session's current cwd; keep the tracked commands anchored through `"$CLAUDE_PROJECT_DIR"/bin/...` and see `docs/turnend-guard.md` for the verified Stop-hook details.
Claude Code's primary watcher protocol is Stop-owned: the auto-arm hook fires on every Stop and foregrounds `bin/fm-watch-arm.sh` when the home is eligible and still needs supervision, and its exit-2 `asyncRewake` rewake is the wake; the model drains and handles wakes but never runs a routine re-arm command.

## codex (VERIFIED 2026-06-11, codex-cli 0.139.0)
## codex (VERIFIED 2026-08-20, codex-cli 0.147.0)

| Fact | Value |
|---|---|
Expand Down Expand Up @@ -241,6 +241,10 @@ The tracked hook anchors to `pwd -P`, verifies that root is firstmate-shaped and
Codex's primary watcher protocol is `bin/fm-watch-checkpoint.sh --seconds "${FM_CODEX_WATCH_CHECKPOINT:-180}"`, not `bin/fm-watch-arm.sh`.
The checkpoint is deliberately foreground and bounded so Codex regains control regularly to process user messages and queued wakes.

The effort axis was independently verified on 2026-08-20 with codex-cli 0.147.0.
The exact Codex effort probes and observed results are recorded in the [Codex CLI effort evidence](../../../docs/verification/runtime-backends.md#codex-cli-effort).
The vendor catalog includes `ultra` for `gpt-5.6-sol` and `gpt-5.6-terra`, but Firstmate maps only the shared vocabulary through `max`.

## opencode (VERIFIED 2026-06-11, v1.15.7-1.17.6; 1.18.4 busy-queue re-verified 2026-07-20)

| Fact | Value |
Expand Down
34 changes: 32 additions & 2 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1442,21 +1442,51 @@ fm_backend_herdr_projection_order_best_effort() { # <session> <created-workspac
return 0
}

# Bash command substitutions may expose their result pipe on a descriptor above
# 2, so the server child closes every inherited non-stdio descriptor before exec.
fm_backend_herdr_close_inherited_fds() {
local fd
for fd in /dev/fd/*; do
fd=${fd##*/}
case "$fd" in
''|*[!0-9]*|0|1|2) continue ;;
esac
eval "exec ${fd}>&-"
done
}

# fm_backend_herdr_server_ensure: start the herdr server for <session>
# headless (no TUI client) if not already running, mirroring tmux's `tmux
# has-session || tmux new-session -d`. Verified: a bare socket CLI call does
# NOT auto-start the server, so this must run before any workspace/tab/pane
# call. Bounded poll for the server to report running.
# A readiness failure terminates and reaps only the direct start child; successful
# readiness leaves the server running without adding general liveness ownership.
fm_backend_herdr_server_ensure() { # <session>
local session=$1 running out i
local session=$1 running i start_pid
running=$(fm_backend_herdr_cli "$session" status --json 2>/dev/null | jq -r '.server.running // false' 2>/dev/null)
[ "$running" = "true" ] && return 0
( fm_backend_herdr_cli "$session" server >/dev/null 2>&1 & ) || return 1
(
fm_backend_herdr_close_inherited_fds
HERDR_SESSION="$session" exec herdr server --session "$session"
) </dev/null >/dev/null 2>&1 &
start_pid=$!
for i in $(seq 1 20); do
running=$(fm_backend_herdr_cli "$session" status --json 2>/dev/null | jq -r '.server.running // false' 2>/dev/null)
[ "$running" = "true" ] && return 0
sleep 0.5
done
if kill -0 "$start_pid" 2>/dev/null; then
kill -TERM "$start_pid" 2>/dev/null || true
for _ in $(seq 1 20); do
kill -0 "$start_pid" 2>/dev/null || break
sleep 0.1
done
if kill -0 "$start_pid" 2>/dev/null; then
kill -KILL "$start_pid" 2>/dev/null || true
fi
fi
wait "$start_pid" 2>/dev/null || true
echo "error: herdr server for session '$session' did not report running within 10s" >&2
return 1
}
Expand Down
2 changes: 1 addition & 1 deletion bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ crew_dispatch_validate() {
if $e == null then true
elif ($e | type) != "string" then false
elif $h == "claude" then (["low","medium","high","xhigh","max"] | index($e))
elif $h == "codex" then (["low","medium","high","xhigh"] | index($e))
elif $h == "codex" then (["low","medium","high","xhigh","max"] | index($e))
elif $h == "grok" then (["low","medium","high"] | index($e))
elif $h == "pi" or $h == "pi-signed" then (["low","medium","high","xhigh","max"] | index($e))
elif $h == "muse" then (["low","medium","high","xhigh","max"] | index($e))
Expand Down
48 changes: 36 additions & 12 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,17 @@ shell_quote() {

STATUS_FILE=$(shell_quote "$STATE/$ID.status")

IFS= read -r -d '' PROCESS_CLEANUP_SECTION <<'EOF' || true
# Synthetic load and helper-process cleanup
If you create synthetic load or another long-lived helper process, record each process's exact identity immediately when it starts, using the exact PID returned by the launcher or a helper-provided identity that uniquely identifies it.
If a launcher creates additional child processes and their identities are available, record each child identity as well.
Stop every recorded process in the same turn that created or used it, using a bounded cleanup path that signals only those recorded identities, and verify that every recorded process has exited before continuing or ending the turn.
This applies even when a helper runs outside the task worktree, including in scratch space or a pipeline-owned worktree, because ordinary task teardown is not a fallback and cannot reliably reach those processes.
Never use broad `pkill -f`, restart the shared no-mistakes daemon, guess a PID, or stop a process you did not create.
If a recorded process does not exit within the bound, report the cleanup failure and continue using only its recorded identity rather than broadening the match or deferring cleanup to teardown.
EOF
PROCESS_CLEANUP_SECTION=${PROCESS_CLEANUP_SECTION%$'\n'}

if [ "$KIND" = secondmate ]; then
SECONDMATE_PROJECTS=""
idx=1
Expand Down Expand Up @@ -211,6 +222,8 @@ $SECONDMATE_SCOPE
# Project clones
$PROJECT_CLONES_BODY

$PROCESS_CLEANUP_SECTION

# Operating model
You are in an isolated firstmate home. The local \`AGENTS.md\` is your job description, and your local \`data/\`, \`state/\`, \`config/\`, and \`projects/\` dirs are yours to operate.
$PROJECT_CLONES_NOTE
Expand Down Expand Up @@ -307,6 +320,8 @@ You are a crewmate: an autonomous worker agent managed by firstmate. Work on you

$HERDR_SECTION

$PROCESS_CLEANUP_SECTION

# Setup
You are in a disposable git worktree of $REPO, at a detached HEAD on a clean default branch.
This is a SCOUT task: the deliverable is a written report, not a PR.
Expand Down Expand Up @@ -384,19 +399,20 @@ EOF
IFS= read -r -d '' DOD <<EOF || true
# Definition of done
Delivery contract: mode=no-mistakes
The task is complete only when committed on your branch.
When you believe it is complete, append \`done: {summary}\` to the status file and stop.
Firstmate will then instruct you to run /no-mistakes to validate and ship a PR.
The implementation milestone is complete only when committed on your branch; report it with
\`working: {summary}\`, never \`done:\`, and continue in the same turn into the no-mistakes pipeline.
Immediately invoke /no-mistakes to validate and ship a PR; do not wait for another instruction.

You drive no-mistakes by responding to its gates, not by implementing fixes.
Follow the guidance no-mistakes itself provides for the mechanics: it loads when you invoke /no-mistakes, and \`no-mistakes axi run --help\` plus the \`help\` lines in each \`axi\` response are authoritative and version-matched to the installed binary.
When starting no-mistakes, make \`--intent\` preserve all relevant content from this brief's \`# Task\` section plus every later accepted Firstmate requirement, clarification, constraint, exclusion, and supersession, carrying only each requirement's current accepted form; retain direct requirements instead of substituting a diff summary, and exclude generic operational, status, delivery, and other scaffold boilerplate unless it is task-specific.
Do not hand-edit, commit, or fix findings yourself while a run is active - the pipeline applies every fix.

Two firstmate-specific rules layer on top of that guidance:
- ask-user findings are never yours to answer: escalate to firstmate (rule 6) and stop.
- ask-user findings are never yours to answer: escalate to firstmate (rule 6), then enter the same-turn wait for its decision.
Firstmate applies the authority contract in its \`AGENTS.md\` and obtains any required captain decision.
When the decision comes back, feed it to the gate with \`no-mistakes axi respond\` and let the pipeline apply it - do not route the question to "the user" or implement the fix yourself.
Wait in the same turn for \`no-mistakes axi respond\` itself to return, then immediately process its returned gate or outcome and continue driving the pipeline; do not end the turn or wait again for another return.
- Avoid \`--yes\`: it would silently bypass firstmate's authority check and any required captain escalation.

After /no-mistakes reports CI green (the CI-ready return point - do not wait for it to keep monitoring in the background until merge), append \`done: PR {url} checks green\` and stop. You are finished.
Expand All @@ -417,12 +433,14 @@ You are a crewmate: an autonomous worker agent managed by firstmate. Work on you

$HERDR_SECTION

$PROCESS_CLEANUP_SECTION

# Setup
You are in a disposable git worktree of $REPO, at a detached HEAD on a clean default branch.

**Verify isolation before anything else.** Run \`pwd -P\` and \`git rev-parse --show-toplevel\`; both must resolve to the disposable task worktree you were launched in, such as a treehouse pool path or an Orca-managed worktree, not the primary checkout firstmate operates from.
The path check is authoritative: \`git rev-parse --git-dir\` and \`git rev-parse --git-common-dir\` can help inspect the repo, but they do not prove you are outside the primary checkout.
If the top-level path is the primary checkout or not the worktree you were launched in, STOP - do not branch or commit here - append \`blocked: launched in primary checkout, not an isolated worktree\` to the status file and stop.
If the top-level path is the primary checkout or not the worktree you were launched in, STOP - do not branch or commit here - append \`blocked: launched in primary checkout, not an isolated worktree\` to the status file and wait in the same turn for firstmate's help.

1. First action: create your branch: \`git checkout -b fm/$ID\`$SETUP2

Expand All @@ -432,25 +450,31 @@ $RULE1
3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations.
4. Report status by appending one line:
\`echo "{state}: {one short line}" >> $STATUS_FILE\`
States: working, needs-decision, blocked, $PAUSED_VERB, done, failed.
States: working, needs-decision, blocked, $PAUSED_VERB, resolved, done, failed.
Each append wakes firstmate, so report sparingly: only phase changes a supervisor
would act on (setup done, bug reproduced, fix implemented, validation passed) and the
needs-decision/blocked/paused/done/failed states. No step-by-step FYI progress lines;
needs-decision/blocked/$PAUSED_VERB/resolved/done/failed states. No step-by-step FYI progress lines;
firstmate reads your pane for that.
A mid-task \`working:\` line (including setup complete) is nonterminal: do not end the
turn after it; continue the same stage until a defined \`done:\` gate under Definition of done.
Every status line except \`done:\` and \`failed:\` is nonterminal, including \`working:\`,
\`needs-decision:\`, \`blocked:\`, \`$PAUSED_VERB:\`, and \`resolved:\`; after any such line,
continue the same turn's work or enter its prescribed wait, and never end the task merely
because that status line was written.
Use \`$PAUSED_VERB: {why}\` - distinct from \`blocked:\` - ONLY when you are deliberately idling on a
known external wait you expect to clear on its own (an upstream release, a rate-limit reset,
a scheduled window): firstmate then leaves your idle pane alone and rechecks it on a long
cadence instead of treating it as a possible wedge. Use \`blocked:\` when you are stuck and need help.
5. If you hit the same obstacle twice, append \`blocked: {why}\` and stop; firstmate will help.
5. If you hit the same obstacle twice, append \`blocked: {why}\` and wait in the same turn for firstmate's help.
6. If a decision belongs above the implementation worker (product choices, destructive actions, ask-user findings),
append \`needs-decision: {summary of options}\` and stop. Firstmate will apply the configured authority and reply with the decision.
append \`needs-decision: {summary of options}\` and wait in the same turn. Firstmate will apply the configured authority and reply with the decision.
A decision or blocker you opened stays open until a \`resolved\` line carrying its exact key lands; a later \`done:\` or \`working:\` line never closes it, even when the answer is what started that work.
Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append \`resolved: {how it cleared}\` yourself (same \`[key=<slug>]\` if you opened it with one) as you resume.
Describe each decision as coming from firstmate unless firstmate explicitly says the captain made it; standing yolo authority is firstmate's authority and must never be rewritten as a direct captain decision.
7. Never stop, restart, or update the shared \`no-mistakes\` daemon - it is one instance serving
every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes
daemon error, append \`blocked: {the daemon error}\` and stop; only firstmate manages the daemon.
daemon error, append \`blocked: {the daemon error}\` and wait in the same turn for firstmate; only firstmate manages the daemon.
8. When the task requires evidence from multiple execution paths, generate each artifact through
its own path and make the producing path visible in the artifact; copied or byte-identical
artifacts are not sufficient evidence.

# Project memory
If \`AGENTS.md\` or \`CLAUDE.md\` already exists, or if this task produced durable project-intrinsic knowledge, run \`$FM_ROOT/bin/fm-ensure-agents-md.sh .\` in the worktree.
Expand Down
7 changes: 5 additions & 2 deletions bin/fm-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
# it in the tool output of whatever it was doing - the one channel every harness
# has. Supervision health is MODEL-AWARE (fm_watcher_supervision_verdict in
# bin/fm-wake-lib.sh): under the Claude Stop auto-arm model the watcher runs only
# between turns, so mid-turn a fresh beacon with no live watcher is healthy and
# only a stale beacon (beyond FM_GUARD_GRACE) is a genuine lapse; under the Pi
# between turns, so mid-turn a fresh beacon with no live watcher is healthy, and
# a stale beacon remains healthy only while the lock-owning Claude session has a
# durable pending handling generation and no auto-arm failure evidence; under the Pi
# extension model the extension tears the watcher down and respawns it on every
# actionable wake, so a fresh beacon with a genuinely unheld lock is healthy
# while that live Pi session provably owns continuity; any held but unhealthy
Expand Down Expand Up @@ -48,6 +49,8 @@ STALE_BANNER_MARKER="$STATE/.guard-watcher-stale-banner"

# shellcheck source=bin/fm-wake-lib.sh
. "$SCRIPT_DIR/fm-wake-lib.sh"
# shellcheck source=bin/fm-session-lock-lib.sh
. "$SCRIPT_DIR/fm-session-lock-lib.sh"
# shellcheck source=bin/fm-tangle-lib.sh
. "$SCRIPT_DIR/fm-tangle-lib.sh"
# shellcheck source=bin/fm-supervision-lib.sh
Expand Down
11 changes: 9 additions & 2 deletions bin/fm-remote-job-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,11 @@ fm_remote_job_worker_owned_alive() {
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
identity_file=$(fm_remote_job_worker_identity_path)
fm_remote_job_regular_bounded "$identity_file" 256 || return 1
fm_remote_job_probe "$account_home" || return 1
# Ownership and readiness are separate predicates. A worker with a stale
# heartbeat is still the owned process that must be stopped before a
# replacement can acquire the lock; treating stale readiness as absence
# launches a second supervisor behind the first one and leaves the queue
# without a path to recover.
if fm_remote_job_lock_owner_matches_process "$account_home"; then
[ "$pid" = "$FM_REMOTE_JOB_OWNER_PID" ] || return 1
return 0
Expand Down Expand Up @@ -939,7 +943,10 @@ fm_remote_job_start_linux_worker() { # <remote-root> <account-home>
}
fm_remote_job_prepare_state "$account_home" || return 1
if fm_remote_job_worker_owned_alive "$root" "$account_home"; then
if fm_remote_job_worker_identity_matches "$root" "$account_home"; then return 0; fi
if fm_remote_job_probe "$account_home" &&
fm_remote_job_worker_identity_matches "$root" "$account_home"; then
return 0
fi
# The owner pid is the serving child; its restart supervisor sits above it
# and would immediately replace a lone process kill, so stop the whole
# worker tree through its isolated group.
Expand Down
Loading