Skip to content

CI mirror: refactor(bin): give harness launch knowledge one owner in fm-launch-lib.sh - #2

Open
sbracewell64 wants to merge 10 commits into
mainfrom
fm/fm-launch-lib-extract
Open

CI mirror: refactor(bin): give harness launch knowledge one owner in fm-launch-lib.sh#2
sbracewell64 wants to merge 10 commits into
mainfrom
fm/fm-launch-lib-extract

Conversation

@sbracewell64

Copy link
Copy Markdown
Owner

CI mirror of upstream PR kunchenguid#1080, opened inside this fork solely to run the repository's own workflows on the identical commit. Upstream cross-fork PRs do not run workflows without maintainer approval. Not intended to merge; the upstream PR is the real submission. Body reproduced verbatim below so the compliance check tests the real thing.

Intent

Give firstmate's harness launch knowledge exactly one owner, as a pure refactor with zero behavior change.

Background: firstmate is gaining a second caller of its launch knowledge - a fleet launcher that opens a PRIMARY firstmate session, alongside bin/fm-spawn.sh which opens crewmate sessions. Today that knowledge lives only inside bin/fm-spawn.sh. The drift this prevents already happened once: an external platform hand-copied Claude's launch command and dropped the CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false ghost-text suppression variable, which firstmate would otherwise read as real typed input when it captures a pane. One owner, or it happens again.

What was done: extracted launch_template, model_flag_for_harness, and effort_flag_for_harness out of bin/fm-spawn.sh into a new sourced bin/fm-launch-lib.sh, and had fm-spawn.sh source it. No copy of those three functions remains in fm-spawn.sh; the library is their single definition.

Deliberate decision the captain specified up front, not a free choice made during implementation: launch_template additionally gains a 'kind' axis distinguishing 'primary' from the existing crewmate shape, because a primary session has no task, no worktree, no brief, and no status file. The primary branch was added while keeping every existing crewmate call path byte-identical in behavior - a crewmate spawn on every verified harness must compose exactly the command it composes today, ghost-text variable included. The second consumer (sibling task fm-launch-menu, blocked on this one) is already designed against this exact shape, so the axis shape is intentional and any alternative shaping would have required escalation rather than a local judgment call.

Also deliberate: the launcher itself is explicitly out of scope and is not built here. bin/fm-brief.sh, bin/backends/herdr.sh, bin/fm-backend.sh, bin/fm-merge-local.sh, and bin/fm-ff-lib.sh were left untouched because other workers were concurrently live in those files.

Testing: a colocated regression test covers the extracted library directly, including the new primary kind, and proves the crewmate templates are unchanged. The existing spawn test suite passes untouched.

Repo conventions applied per firstmate's coding guidelines: shellcheck-clean bin scripts, one sentence per line in prose, plain dashes, colocated tests, no agent co-author on commits.

This is a relaunch for publication only. The implementation already passed review, tests, documentation, and lint in a prior pipeline run; that run failed solely at the push step because the GitHub account had pull-only access to the upstream repository. That is now cleared: pushes are routed to the sbracewell64/firstmate fork while fetches still track upstream, so the resulting pull request into kunchenguid/firstmate is expected to be a cross-fork PR.

Two review findings from the prior run were escalated and decided by the project owner; both decisions are already committed on this branch, so they are deliberate and should not be re-flagged:

  1. The opencode primary template uses the full verified briefless shape - the OPENCODE_CONFIG_CONTENT allow-permission JSON plus opencode --auto, not --prompt. The crewmate template keeps --prompt because it has a brief to pass; a primary has none. The empirical evidence CONTRIBUTING.md:50 requires is tests/fm-opencode-primary-live-e2e.test.sh:256 and :310, which both launch a primary opencode TUI exactly that way, and it is cited in the code comment.

  2. Kimi binary resolution (resolve_kimi_binary) deliberately stays private to bin/fm-spawn.sh and was NOT extracted. The planned second consumer, fm-launch-menu, reaches Kimi through the pi harness rather than a native kimi binary, so it will never call it and there is no hand-copy drift to prevent; extracting it would have expanded a pure refactor to serve a consumer that will not exist. That rationale is recorded as a comment at the KIMIBIN placeholder so it is not re-raised, to be revisited only if a native kimi launch ever becomes a launcher entry.

Also already applied: the placeholder contract now lives in bin/fm-launch-lib.sh's own header (fm-spawn.sh points at it), AGENTS.md section 4 attributes the launch flags and unverified-adapter guard to bin/fm-launch-lib.sh, and the one-owner test guard in tests/fm-launch-lib.test.sh covers every per-harness autonomy marker with its fail-open || true removed.

Publication routing note: the gate is configured with fork = sbracewell64/firstmate, so branches push to that fork while the pull request opens against kunchenguid/firstmate. A cross-fork PR is the expected and correct outcome here.

What Changed

  • Extracted launch_template, model_flag_for_harness, and effort_flag_for_harness (plus the shell_quote helper they depend on) out of bin/fm-spawn.sh into a new sourced bin/fm-launch-lib.sh, which is now their single definition; fm-spawn.sh sources it and keeps resolve_kimi_binary private, and the placeholder contract moved into the library's own header. Crewmate, scout, and secondmate launch commands are byte-identical to before.
  • launch_template gained a kind axis with a new primary arm for briefless primary sessions on claude, codex, opencode, pi, and grok, while kimi returns non-zero for primary so the guard refuses instead of emitting an unsubstitutable command. Each arm cites its in-repo evidence, the header records a consumer obligation to disclose the permission-bypass posture of every primary template that carries one, and the library distinguishes "unverified adapter" from "verified adapter, unsupported kind" as separate refusal causes.
  • Added tests/fm-launch-lib.test.sh pinning every crewmate and primary template plus a one-owner guard that fails if a launch command is hand-written elsewhere in bin/, mapped the new file into bin/fm-test-run.sh change-based selection, and repointed launch-command ownership in AGENTS.md, CONTRIBUTING.md, docs/configuration.md, docs/scripts.md, and the harness-adapters skill at bin/fm-launch-lib.sh.

Risk Assessment

✅ Low: This round changes only comments, it correctly resolves the prior finding by grounding pi's posture in SKILL.md:272 and widening the consumer obligation to all five primaries rather than exempting one, every citation verifies, and the crewmate call paths remain byte-identical to the base commit.

Testing

I exercised the extraction end to end rather than only through unit assertions: the new tests/fm-launch-lib.test.sh, the touched kimi/pi/captain-translation contract tests, and the three spawn suites all pass, and on top of that I built a differential harness that compares base commit a5fe1bc against HEAD across 438 fully substituted launch commands (zero differences) and a real-script end-to-end run where bin/fm-spawn.sh at each commit types its launch line into a recording tmux (byte-identical for all five task harnesses). The new primary kind was demonstrated as the fleet launcher will consume it, including kimi's refusal and the fail-closed unverified-adapter guard still firing from the real script, and five planted mutations confirmed the one-owner guard and the template pins actually catch drift including the exact historical ghost-text omission. No visual artifact applies - this change is shell library code whose entire end-user surface is the composed launch command string, which the CLI transcripts capture directly. The only failure seen was one node-driven pi watch-extension assertion that reproduces unchanged at the base commit.

Evidence: base vs HEAD launch command differential (438 combinations, 0 differences) + the new primary kind

combinations compared (harness x kind x model x effort, fully substituted): 438 RESULT: 0 differences. Every composed launch command is byte-identical. --- ghost-text suppression survived the extraction (the drift this refactor exists to prevent) --- claude ship carries CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude scout carries CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude secondmate carries CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude primary carries CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false NEW 'primary' KIND: what the fleet launcher will get from the one owner claude CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --model 'opus' --effort 'high' codex codex --model 'opus' -c 'model_reasoning_effort="high"' --dangerously-bypass-approvals-and-sandbox opencode OPENCODE_CONFIG_CONTENT='{"permission":{"*":"allow"}}' opencode --model 'opus' --auto pi pi --model 'opus' --thinking 'high' grok grok --trust --always-approve --model 'opus' --reasoning-effort 'high' kimi REFUSED (exit 1, no output) - not a verified primary harness --- unverified adapter still refused for every kind (the fail-closed guard) --- kind=ship refused (exit 1) kind=scout refused (exit 1) kind=secondmate refused (exit 1) kind=primary refused (exit 1)

==============================================================================
 CREWMATE / SCOUT / SECONDMATE LAUNCH COMMANDS: base a5fe1bc  vs  HEAD b08b230
==============================================================================
combinations compared (harness x kind x model x effort, fully substituted): 438

RESULT: 0 differences. Every composed launch command is byte-identical.

--- the exact commands a crewmate spawn types into the pane (kind=ship, model=opus, effort=high) ---
  claude    CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --model 'opus' --effort 'high' "$('/home/cap/firstmate/bin/fm-operational-input.sh' encode launch-brief < '/home/cap/.firstmate/data/t-42/brief.md')"
  codex     codex --model 'opus' -c 'model_reasoning_effort="high"' --dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch '/home/cap/.firstmate/state/t-42.turn-ended'\"]" "$('/home/cap/firstmate/bin/fm-operational-input.sh' encode launch-brief < '/home/cap/.firstmate/data/t-42/brief.md')"
  opencode  OPENCODE_CONFIG_CONTENT='{"permission":{"*":"allow"}}' opencode --model 'opus' --prompt "$('/home/cap/firstmate/bin/fm-operational-input.sh' encode launch-brief < '/home/cap/.firstmate/data/t-42/brief.md')"
  pi        pi --model 'opus' --thinking 'high' -e '/home/cap/.firstmate/state/t-42.pi-ext.ts' "$('/home/cap/firstmate/bin/fm-operational-input.sh' encode launch-brief < '/home/cap/.firstmate/data/t-42/brief.md')"
  grok      grok --always-approve --model 'opus' --reasoning-effort 'high' "$('/home/cap/firstmate/bin/fm-operational-input.sh' encode launch-brief < '/home/cap/.firstmate/data/t-42/brief.md')"
  kimi      '/home/cap/.local/bin/kimi' --model 'opus' --auto

--- ghost-text suppression survived the extraction (the drift this refactor exists to prevent) ---
  claude ship        carries CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false
  claude scout       carries CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false
  claude secondmate  carries CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false
  claude primary     carries CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false

==============================================================================
 NEW 'primary' KIND: what the fleet launcher will get from the one owner
==============================================================================
  claude    CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --model 'opus' --effort 'high' 
  codex     codex --model 'opus' -c 'model_reasoning_effort="high"' --dangerously-bypass-approvals-and-sandbox
  opencode  OPENCODE_CONFIG_CONTENT='{"permission":{"*":"allow"}}' opencode --model 'opus' --auto
  pi        pi --model 'opus' --thinking 'high' 
  grok      grok --trust --always-approve --model 'opus' --reasoning-effort 'high' 
  kimi      REFUSED (exit 1, no output) - not a verified primary harness

  (base commit had no primary kind at all:)
  base launch_template claude primary -> CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"

--- a primary carries no task-scoped placeholder (no task, no worktree, no brief, no status file) ---
  claude    clean (only flag placeholders): CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__
  codex     clean (only flag placeholders): codex __MODELFLAG____EFFORTFLAG__--dangerously-bypass-approvals-and-sandbox
  opencode  clean (only flag placeholders): OPENCODE_CONFIG_CONTENT='{"permission":{"*":"allow"}}' opencode __MODELFLAG__--auto
  pi        clean (only flag placeholders): pi __MODELFLAG____EFFORTFLAG__
  grok      clean (only flag placeholders): grok --trust --always-approve __MODELFLAG____EFFORTFLAG__

--- unverified adapter still refused for every kind (the fail-closed guard) ---
  kind=ship        refused (exit 1)
  kind=scout       refused (exit 1)
  kind=secondmate  refused (exit 1)
  kind=primary     refused (exit 1)
Evidence: end-to-end: the literal command the real fm-spawn.sh types into the crewmate pane, base vs HEAD

--- claude ------------------------------------------------------------- HEAD b08b230 (bin/fm-launch-lib.sh): CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --model 'opus' --effort 'high' "$('<FM_ROOT>/bin/fm-operational-input.sh' encode launch-brief < '<RUN>/home/data/<TASK>/brief.md')" base a5fe1bc (pre-extraction fm-spawn.sh): IDENTICAL --- codex ------------------------------------------------------------- HEAD b08b230 (bin/fm-launch-lib.sh): codex --model 'opus' -c 'model_reasoning_effort="high"' --dangerously-bypass-approvals-and-sandbox -c "notify=[&#34;bash&#34;,&#34;-c&#34;,&#34;touch '<RUN>/home/state/<TASK>.turn-ended'&#34;]" "$('<FM_ROOT>/bin/fm-operational-input.sh' encode launch-brief < '<RUN>/home/data/<TASK>/brief.md')" base a5fe1bc (pre-extraction fm-spawn.sh): IDENTICAL --- opencode ---------------------------------------------------------- base a5fe1bc: IDENTICAL --- pi ---------------------------------------------------------------- base a5fe1bc: IDENTICAL --- grok -------------------------------------------------------------- base a5fe1bc: IDENTICAL RESULT: real spawned launch lines are byte-identical at base and HEAD --- fail-closed unverified-adapter guard, still enforced through the sourced library --- $ fm-spawn.sh <task> <project> not-a-harness error: unknown harness 'not-a-harness'; pass a raw launch command to use an unverified adapter

==============================================================================
 END TO END: the literal command bin/fm-spawn.sh types into the crewmate pane
 (real fm-spawn.sh, real git worktree, fake tmux recording send-keys -l)
==============================================================================

--- claude -------------------------------------------------------------
  HEAD b08b230 (bin/fm-launch-lib.sh):
    CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --model 'opus' --effort 'high' "$('<FM_ROOT>/bin/fm-operational-input.sh' encode launch-brief < '<RUN>/home/data/<TASK>/brief.md')"
  base a5fe1bc (pre-extraction fm-spawn.sh): IDENTICAL

--- codex -------------------------------------------------------------
  HEAD b08b230 (bin/fm-launch-lib.sh):
    codex --model 'opus' -c 'model_reasoning_effort="high"' --dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch '<RUN>/home/state/<TASK>.turn-ended'\"]" "$('<FM_ROOT>/bin/fm-operational-input.sh' encode launch-brief < '<RUN>/home/data/<TASK>/brief.md')"
  base a5fe1bc (pre-extraction fm-spawn.sh): IDENTICAL

--- opencode -------------------------------------------------------------
  HEAD b08b230 (bin/fm-launch-lib.sh):
    OPENCODE_CONFIG_CONTENT='{"permission":{"*":"allow"}}' opencode --model 'opus' --prompt "$('<FM_ROOT>/bin/fm-operational-input.sh' encode launch-brief < '<RUN>/home/data/<TASK>/brief.md')"
  base a5fe1bc (pre-extraction fm-spawn.sh): IDENTICAL

--- pi -------------------------------------------------------------
  HEAD b08b230 (bin/fm-launch-lib.sh):
    pi --model 'opus' --thinking 'high' -e '<RUN>/home/state/<TASK>.pi-ext.ts' "$('<FM_ROOT>/bin/fm-operational-input.sh' encode launch-brief < '<RUN>/home/data/<TASK>/brief.md')"
  base a5fe1bc (pre-extraction fm-spawn.sh): IDENTICAL

--- grok -------------------------------------------------------------
  HEAD b08b230 (bin/fm-launch-lib.sh):
    grok --always-approve --model 'opus' --reasoning-effort 'high' "$('<FM_ROOT>/bin/fm-operational-input.sh' encode launch-brief < '<RUN>/home/data/<TASK>/brief.md')"
  base a5fe1bc (pre-extraction fm-spawn.sh): IDENTICAL

RESULT: real spawned launch lines are byte-identical at base and HEAD

--- fail-closed unverified-adapter guard, still enforced through the sourced library ---
  $ fm-spawn.sh <task> <project> not-a-harness
  error: unknown harness 'not-a-harness'; pass a raw launch command to use an unverified adapter
Evidence: mutation check: the one-owner guard and template pins catch all five planted drifts

--- mutation 1: a second bin/ consumer hand-copies claude's command --- not ok - a launch command is hand-written outside bin/fm-launch-lib.sh ('--dangerously-skip-permissions'): bin/fm-launch-menu.sh --- mutation 2: fm-spawn.sh keeps its own copy of launch_template --- not ok - bin/fm-spawn.sh defines launch_template again; bin/fm-launch-lib.sh is the single owner --- mutation 3: the grok primary template loses --trust --- not ok - launch_template grok primary drifted: expected: grok --trust --always-approve MODELFLAG____EFFORTFLAG got: grok --always-approve MODELFLAG____EFFORTFLAG --- mutation 4: the opencode primary reverts to the crewmate --prompt shape --- not ok - launch_template opencode primary drifted: expected: ... opencode MODELFLAG--auto got: ... opencode MODELFLAG--prompt --- mutation 5: a shipped crewmate template drifts (claude loses the prefix) --- not ok - launch_template claude ship drifted not ok - existing launch template changed: (tests/fm-kimi-harness.test.sh byte pin) RESULT: all five mutations are caught; the guard and the pins are load-bearing.

==============================================================================
 MUTATION CHECK: are the one-owner guard and the template pins real?
 Each mutation is planted in a throwaway copy of HEAD, the targeted test is
 run, then the mutation is reverted. The real worktree is never touched.
==============================================================================

--- control: unmutated HEAD ---
ok - one owner: bin/fm-spawn.sh sources the library and redefines nothing
ok - one owner: no other script under bin/ hand-writes any verified harness launch command

--- mutation 1: a second bin/ consumer hand-copies claude's command and drops
    CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION (the drift that already happened once) ---
not ok - a launch command is hand-written outside bin/fm-launch-lib.sh ('--dangerously-skip-permissions'): bin/fm-launch-menu.sh

--- mutation 2: fm-spawn.sh keeps its own copy of launch_template ---
not ok - bin/fm-spawn.sh defines launch_template again; bin/fm-launch-lib.sh is the single owner

--- mutation 3: the grok primary template loses --trust (turn-end guard fails open) ---
not ok - launch_template grok primary drifted:
  expected: grok --trust --always-approve __MODELFLAG____EFFORTFLAG__
  got:      grok --always-approve __MODELFLAG____EFFORTFLAG__

--- mutation 4: the opencode primary reverts to the crewmate --prompt shape ---
not ok - launch_template opencode primary drifted:
  expected: OPENCODE_CONFIG_CONTENT='{"permission":{"*":"allow"}}' opencode __MODELFLAG__--auto
  got:      OPENCODE_CONFIG_CONTENT='{"permission":{"*":"allow"}}' opencode __MODELFLAG__--prompt

--- mutation 5: a shipped crewmate template drifts (claude loses the prefix) ---
not ok - launch_template claude ship drifted:
  expected: CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"
  got:      claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"
not ok - existing launch template changed:     claude) printf '%s' 'CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"' ;;

--- copy restored: clean again ---
ok - one owner: no other script under bin/ hand-writes any verified harness launch command

RESULT: all five mutations are caught; the guard and the pins are load-bearing.
Evidence: reproducible differential harness (base vs HEAD launch commands)
#!/usr/bin/env bash
# Differential proof: every crewmate/scout/secondmate launch command firstmate
# composes at HEAD (bin/fm-launch-lib.sh) is byte-identical to the command the
# pre-extraction bin/fm-spawn.sh composed at base commit a5fe1bc.
#
# It does NOT compare the template functions in the abstract: it replays
# fm-spawn.sh's own placeholder substitution (fm-spawn.sh:1355-1370) so what is
# compared is the literal string typed into the agent's pane.
set -u

HEAD_ROOT=$1
BASE_ROOT=$2

# --- pull the three pre-extraction functions out of base fm-spawn.sh ---------
extract_fn() {
  awk -v fn="$2" '
    $0 == fn "() {" { inside = 1 }
    inside { print }
    inside && $0 == "}" { inside = 0 }
  ' "$1"
}

BASE_SPAWN="$BASE_ROOT/bin/fm-spawn.sh"
{
  extract_fn "$BASE_SPAWN" shell_quote
  extract_fn "$BASE_SPAWN" launch_template
  extract_fn "$BASE_SPAWN" model_flag_for_harness
  extract_fn "$BASE_SPAWN" effort_flag_for_harness
} > "$TMPD/base-fns.sh"

for fn in shell_quote launch_template model_flag_for_harness effort_flag_for_harness; do
  grep -q "^$fn() {" "$TMPD/base-fns.sh" || { echo "FATAL: could not extract $fn from base fm-spawn.sh"; exit 2; }
done

# --- fm-spawn.sh's own substitution step, replayed verbatim -----------------
# Representative values in the shape fm-spawn.sh passes them.
BRIEF=/home/cap/.firstmate/data/t-42/brief.md
TURNEND=/home/cap/.firstmate/state/t-42.turn-ended
PIEXT=/home/cap/.firstmate/state/t-42.pi-ext.ts
PITURNEND=/home/cap/.firstmate/projects/sm-1/.pi/extensions/fm-primary-turnend-guard.ts
PIWATCH=/home/cap/.firstmate/projects/sm-1/.pi/extensions/fm-primary-pi-watch.ts
OPINPUT=/home/cap/firstmate/bin/fm-operational-input.sh
KIMIBIN=/home/cap/.local/bin/kimi

compose() { # <harness> <kind> <model> <effort>  -- uses whatever fns are sourced
  local harness=$1 kind=$2 model=$3 effort=$4 launch modelflag effortflag
  launch=$(launch_template "$harness" "$kind") || { printf '<REFUSED exit=%s>' "$?"; return 0; }
  modelflag=$(model_flag_for_harness "$harness" "$model")
  effortflag=$(effort_flag_for_harness "$harness" "$effort")
  launch=${launch//__KIMIBIN__/$(shell_quote "$KIMIBIN")}
  launch=${launch//__MODELFLAG__/$modelflag}
  launch=${launch//__EFFORTFLAG__/$effortflag}
  launch=${launch//__BRIEF__/$(shell_quote "$BRIEF")}
  launch=${launch//__TURNEND__/$(shell_quote "$TURNEND")}
  launch=${launch//__PIEXT__/$(shell_quote "$PIEXT")}
  launch=${launch//__PITURNEND__/$(shell_quote "$PITURNEND")}
  launch=${launch//__PIWATCH__/$(shell_quote "$PIWATCH")}
  launch=${launch//__OPINPUT__/$(shell_quote "$OPINPUT")}
  printf '%s' "$launch"
}

HARNESSES=(claude codex opencode pi grok kimi)
CREW_KINDS=(ship scout secondmate)
MODELS=(default opus "sonnet-4-5" "o'pus")
EFFORTS=(default low medium high xhigh max)

dump_matrix() { # writes one "harness|kind|model|effort<TAB>command" line per combo
  local h k m e
  for h in "${HARNESSES[@]}"; do
    for k in "${CREW_KINDS[@]}"; do
      for m in "${MODELS[@]}"; do
        for e in "${EFFORTS[@]}"; do
          printf '%s|%s|%s|%s\t%s\n' "$h" "$k" "$m" "$e" "$(compose "$h" "$k" "$m" "$e")"
        done
      done
    done
  done
  # the omitted-kind default path fm-spawn also exercises
  for h in "${HARNESSES[@]}"; do
    printf '%s|<kind omitted>|default|default\t%s\n' "$h" "$(launch_template "$h")"
  done
}

# --- base side ---------------------------------------------------------------
( set -u; . "$TMPD/base-fns.sh"; dump_matrix ) > "$TMPD/base-matrix.txt"
# --- head side ---------------------------------------------------------------
( set -u; . "$HEAD_ROOT/bin/fm-launch-lib.sh"; dump_matrix ) > "$TMPD/head-matrix.txt"

echo "=============================================================================="
echo " CREWMATE / SCOUT / SECONDMATE LAUNCH COMMANDS: base a5fe1bc  vs  HEAD b08b230"
echo "=============================================================================="
echo "combinations compared (harness x kind x model x effort, fully substituted): $(wc -l < "$TMPD/head-matrix.txt")"
echo
if diff -u "$TMPD/base-matrix.txt" "$TMPD/head-matrix.txt" > "$TMPD/matrix.diff"; then
  echo "RESULT: 0 differences. Every composed launch command is byte-identical."
else
  echo "RESULT: DRIFT DETECTED"
  cat "$TMPD/matrix.diff"
fi
echo
echo "--- the exact commands a crewmate spawn types into the pane (kind=ship, model=opus, effort=high) ---"
( set -u; . "$HEAD_ROOT/bin/fm-launch-lib.sh"
  for h in "${HARNESSES[@]}"; do printf '  %-9s %s\n' "$h" "$(compose "$h" ship opus high)"; done )
echo
echo "--- ghost-text suppression survived the extraction (the drift this refactor exists to prevent) ---"
( set -u; . "$HEAD_ROOT/bin/fm-launch-lib.sh"
  for k in ship scout secondmate primary; do
    case "$(compose claude "$k" default default)" in
      CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false*) printf '  claude %-11s carries CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false\n' "$k" ;;
      *) printf '  claude %-11s MISSING THE GHOST-TEXT VARIABLE\n' "$k" ;;
    esac
  done )
echo
echo "=============================================================================="
echo " NEW 'primary' KIND: what the fleet launcher will get from the one owner"
echo "=============================================================================="
( set -u; . "$HEAD_ROOT/bin/fm-launch-lib.sh"
  for h in "${HARNESSES[@]}"; do
    if out=$(launch_template "$h" primary); then
      printf '  %-9s %s\n' "$h" "$(compose "$h" primary opus high)"
    else
      printf '  %-9s REFUSED (exit %s, no output) - not a verified primary harness\n' "$h" "$?"
    fi
  done )
echo
echo "  (base commit had no primary kind at all:)"
( set -u; . "$TMPD/base-fns.sh"
  if launch_template claude primary >/dev/null 2>&1; then
    printf '  base launch_template claude primary -> %s\n' "$(launch_template claude primary)"
  fi )
echo
echo "--- a primary carries no task-scoped placeholder (no task, no worktree, no brief, no status file) ---"
( set -u; . "$HEAD_ROOT/bin/fm-launch-lib.sh"
  for h in claude codex opencode pi grok; do
    tpl=$(launch_template "$h" primary)
    case "$tpl" in
      *__BRIEF__*|*__OPINPUT__*|*__TURNEND__*|*__PIEXT__*|*__PITURNEND__*|*__PIWATCH__*)
        printf '  %-9s LEAKS a task-scoped placeholder: %s\n' "$h" "$tpl" ;;
      *) printf '  %-9s clean (only flag placeholders): %s\n' "$h" "$tpl" ;;
    esac
  done )
echo
echo "--- unverified adapter still refused for every kind (the fail-closed guard) ---"
( set -u; . "$HEAD_ROOT/bin/fm-launch-lib.sh"
  for k in ship scout secondmate primary; do
    if launch_template not-a-harness "$k" >/dev/null 2>&1; then
      printf '  kind=%-11s NOT REFUSED\n' "$k"
    else
      printf '  kind=%-11s refused (exit 1)\n' "$k"
    fi
  done )
Evidence: reproducible end-to-end spawn harness
#!/usr/bin/env bash
# End-to-end: run the REAL bin/fm-spawn.sh (not re-sourced helper functions) at
# base a5fe1bc and at HEAD b08b230, against a fake tmux that records the literal
# string sent with `tmux send-keys -l`. That string is the launch command typed
# into the crewmate's pane, i.e. exactly what the end user's agent receives.
set -u

HEAD_ROOT=$1
BASE_ROOT=$2
OUT_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/fm-spawn-e2e.XXXXXX")
export GIT_AUTHOR_NAME=fmtest GIT_AUTHOR_EMAIL=fmtest@example.invalid
export GIT_COMMITTER_NAME=fmtest GIT_COMMITTER_EMAIL=fmtest@example.invalid

make_fake_tmux() {
  local fakebin=$1
  mkdir -p "$fakebin"
  cat > "$fakebin/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "$*" in
  *"#{pane_current_path}"*) printf '%s\n' "${FM_FAKE_PANE_PATH:-}"; exit 0 ;;
esac
case "${1:-}" in
  display-message) printf 'firstmate\n'; exit 0 ;;
  list-windows) exit 0 ;;
  has-session|new-session|new-window|kill-window) exit 0 ;;
  send-keys)
    if [ -n "${FM_FAKE_LAUNCH_LOG:-}" ]; then
      prev=
      for a in "$@"; do
        [ "$prev" = "-l" ] && printf '%s\n' "$a" >> "$FM_FAKE_LAUNCH_LOG"
        prev=$a
      done
    fi
    exit 0 ;;
esac
exit 0
SH
  chmod +x "$fakebin/tmux"
  printf '#!/usr/bin/env bash\nexit 0\n' > "$fakebin/treehouse"
  chmod +x "$fakebin/treehouse"
}

# spawn_once <root> <tag> <harness> <model> <effort>  -> prints the launch line
spawn_once() {
  local root=$1 tag=$2 harness=$3 model=$4 effort=$5
  local dir="$OUT_ROOT/$tag" home="$OUT_ROOT/$tag/home" proj="$OUT_ROOT/$tag/project"
  local wt="$OUT_ROOT/$tag/wt" fakebin="$OUT_ROOT/$tag/fakebin" log="$OUT_ROOT/$tag/launch.log"
  local id="task-$tag"
  mkdir -p "$home/data/$id" "$home/projects" "$home/state" "$home/config" "$proj"
  printf '%s\n' "$harness" > "$home/config/crew-harness"
  printf 'Ship the thing.\n' > "$home/data/$id/brief.md"
  touch "$home/state/.last-watcher-beat"
  make_fake_tmux "$fakebin"
  git init --quiet "$proj"
  ( cd "$proj" && printf 'x\n' > f.txt && git add f.txt && git commit --quiet -m init )
  git -C "$proj" worktree add --quiet -b "wt-$tag" "$wt"
  : > "$log"
  FM_ROOT_OVERRIDE='' FM_HOME="$home" \
    FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
    FM_PROJECTS_OVERRIDE="$home/projects" FM_CONFIG_OVERRIDE="$home/config" \
    FM_GATE_REFUSE_BYPASS=1 FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$wt" TMUX="fake,1,0" \
    FM_FAKE_LAUNCH_LOG="$log" GROK_HOME="$home/grok-home" PATH="$fakebin:$PATH" \
    "$root/bin/fm-spawn.sh" ${model:+--model "$model"} ${effort:+--effort "$effort"} \
    "$id" "$proj" ${FM_E2E_HARNESS_ARG:-} >"$dir/spawn.out" 2>&1
  # the launch command is the last -l line (the GOTMPDIR export is sent first)
  grep -v '^export GOTMPDIR=' "$log" | tail -1
}

HARNESSES="claude codex opencode pi grok"
echo "=============================================================================="
echo " END TO END: the literal command bin/fm-spawn.sh types into the crewmate pane"
echo " (real fm-spawn.sh, real git worktree, fake tmux recording send-keys -l)"
echo "=============================================================================="
echo
status=same
for h in $HARNESSES; do
  base_line=$(spawn_once "$BASE_ROOT" "base-$h" "$h" opus high)
  head_line=$(spawn_once "$HEAD_ROOT" "head-$h" "$h" opus high)
  # normalise the per-run temp paths, checkout root, and task id so only the
  # command shape is compared
  b=${base_line//$OUT_ROOT\/base-$h/<RUN>}; b=${b//$BASE_ROOT/<FM_ROOT>}; b=${b//task-base-$h/<TASK>}
  hh=${head_line//$OUT_ROOT\/head-$h/<RUN>}; hh=${hh//$HEAD_ROOT/<FM_ROOT>}; hh=${hh//task-head-$h/<TASK>}
  printf -- '--- %s -------------------------------------------------------------\n' "$h"
  printf '  HEAD b08b230 (bin/fm-launch-lib.sh):\n    %s\n' "$hh"
  if [ "$b" = "$hh" ]; then
    printf '  base a5fe1bc (pre-extraction fm-spawn.sh): IDENTICAL\n\n'
  else
    status=drift
    printf '  base a5fe1bc (pre-extraction fm-spawn.sh):\n    %s\n  *** DRIFT ***\n\n' "$b"
  fi
done
echo "RESULT: real spawned launch lines are $( [ "$status" = same ] && echo 'byte-identical at base and HEAD' || echo 'DIFFERENT' )"
echo
echo "--- fail-closed unverified-adapter guard, still enforced through the sourced library ---"
FM_E2E_HARNESS_ARG=not-a-harness spawn_once "$HEAD_ROOT" guard-unverified claude '' '' >/dev/null 2>&1
printf '  $ fm-spawn.sh <task> <project> not-a-harness\n  '
sed -n '1,3p' "$OUT_ROOT/guard-unverified/spawn.out"
rm -rf "$OUT_ROOT"
Evidence: changed-file test selection now includes tests/fm-launch-lib.test.sh
  tests/fm-arm-pretool-check.test.sh
  tests/fm-ask-user-authority.test.sh
  tests/fm-brief.test.sh
  tests/fm-calm-pi-extension.test.sh
  tests/fm-captain-translation-contract.test.sh
  tests/fm-cd-pretool-check.test.sh
  tests/fm-composer-ghost.test.sh
  tests/fm-composer-lib.test.sh
  tests/fm-crew-state.test.sh
  tests/fm-decision-hold-lifecycle.test.sh
  tests/fm-documentation-audiences.test.sh
  tests/fm-ensure-agents-md.test.sh
  tests/fm-grok-harness.test.sh
  tests/fm-herdr-lab.test.sh
  tests/fm-install-herdr.test.sh
  tests/fm-instruction-owners.test.sh
  tests/fm-kimi-harness.test.sh
  tests/fm-launch-lib.test.sh
  tests/fm-lint.test.sh
  tests/fm-nm-test-contract.test.sh
  tests/fm-no-mistakes-ownership.test.sh
  tests/fm-operational-input.test.sh
  tests/fm-pi-primary-types.test.sh
  tests/fm-send-popup-settle.test.sh
  tests/fm-send-settle.test.sh
  tests/fm-stow-contract.test.sh
  tests/fm-subagent-pretool-check.test.sh
  tests/fm-supervision-instructions.test.sh
  tests/fm-test-isolation-proof.test.sh
  tests/fm-test-run.test.sh
  tests/fm-tmux-submit-busy.test.sh
  tests/fm-transition-lib.test.sh
  tests/fm-backend-herdr.test.sh
  tests/fm-backend-tmux-smoke.test.sh
  tests/fm-backend.test.sh
  tests/fm-herdr-session-cleanup.test.sh
  tests/fm-send-strict.test.sh
  tests/fm-spawn-batch.test.sh
  tests/fm-spawn-dispatch-profile.test.sh
  tests/fm-spawn-worktree-settle.test.sh
  tests/fm-pi-watch-extension.test.sh
- Outcome: ⚠️ 1 info across 1 run (7m8s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed (4) ✅
  • ⚠️ bin/fm-launch-lib.sh:83 - The new grok primary template is grok --always-approve __MODELFLAG____EFFORTFLAG__, dropping --trust. Every other owner of grok primary knowledge says that flag is load-bearing: README.md:96 documents the primary launch as grok --trust, README.md:105 says "--trust is needed once per clone so project hooks and the turn-end guard load", docs/turnend-guard.md:63 repeats it, and .agents/skills/harness-adapters/SKILL.md:345 states that without folder trust "this primary guard fails open". The empirical evidence for a grok PRIMARY launch is tests/fm-grok-continuity-live-e2e.test.sh:76, which launches it as grok --trust --always-approve --reasoning-effort low - the same class of citation the author correctly used to justify opencode's --auto shape. A launcher inheriting this template on a not-yet-trusted clone silently loses the "no turn ends blind" backstop, which is precisely the hand-copied-command-drops-a-flag failure this whole change exists to prevent. Counter-argument worth the author's call: trust is granted once per clone, so an already-trusted captain home would not notice, and --trust may be considered setup rather than launch knowledge - hence asking rather than fixing. tests/fm-launch-lib.test.sh:90 currently pins the trustless shape, so it would need updating too.
  • ⚠️ bin/fm-launch-lib.sh:86 - launch_template kimi primary returns 0 with __KIMIBIN__ __MODELFLAG__--auto, but README.md:61 lists only Claude Code, Grok, Pi, Codex, and OpenCode as verified primary harnesses (docs/configuration.md:177 explicitly defers the narrower primary set to README), and __KIMIBIN__ is resolvable only inside bin/fm-spawn.sh by design. The result is the one primary arm that cannot produce a runnable command for any consumer other than fm-spawn.sh - and fm-spawn.sh never passes kind=primary (KIND is only ship|scout|secondmate at bin/fm-spawn.sh:145,172,173). A fleet launcher enumerating verified adapters and calling launch_template &#34;$h&#34; primary would get a literal __KIMIBIN__ --auto string instead of the clean non-zero refusal the unverified-adapter guard gives everywhere else, and tests/fm-launch-lib.test.sh:91 pins that shape. This is a separate question from the already-decided "resolve_kimi_binary stays private" call: given that decision, the consistent shape is for the primary kind to return 1 for kimi (matching kimi's absence from the supported primary set), so the guard refuses rather than emitting an unsubstitutable command. Flagging for the author because making launch_template total across all six verified adapters may itself be the intended contract.
  • ℹ️ tests/fm-launch-lib.test.sh:243 - The one-owner guard's --auto([^-a-z]|$) marker greps all of bin/ and fails if any file other than fm-launch-lib.sh matches. --auto is a common flag outside launch commands (gh pr merge --auto is the obvious candidate for the existing bin/fm-pr-*.sh scripts), so an unrelated future change would trip "a launch command is hand-written outside bin/fm-launch-lib.sh" with a misleading message. The guard's own self-check only proves the pattern still matches the library, not that it is specific to a launch command. Anchoring it to its adapter context (e.g. opencode [^&#39;]*--auto and __KIMIBIN__ [^&#39;]*--auto, or --auto&#39;$) keeps the hand-copy detection while removing the false positive. The other five markers are already adapter-specific enough not to have this problem.
  • ℹ️ bin/fm-launch-lib.sh:26 - shell_quote is a general-purpose helper, not launch knowledge, but it now lives in the launch library. bin/fm-spawn.sh calls it from several non-launch sites (lines 1253, 1273, 1355-1360, 1372), so sourcing fm-launch-lib.sh is silently load-bearing for code paths unrelated to launching; bin/fm-brief.sh:102 and bin/fm-supervision-instructions.sh:94 still carry their own copies, so this is now the third definition in bin/. The header comment already acknowledges the placement, and the intent explicitly excludes fm-brief.sh from this change, so noting only - a shared quoting lib would be the natural follow-up once those files are free.

🔧 Fix: add grok --trust, refuse kimi primary, pin every primary shape
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-launch-lib.sh:90 - Raising this for escalation exactly as the owner's instruction CI mirror: feat(bin): distinguish firstmate's own pane from crewmate panes #4 directed ("where a change would alter the AUTONOMY or PERMISSION posture of a primary session ... do NOT silently change it. Raise it as a finding"). The claude primary (line 90) carries --dangerously-skip-permissions and the codex primary (line 95) carries --dangerously-bypass-approvals-and-sandbox, both inherited from the crewmate command, and the author's own arm comments now state the repo has no interactive-primary evidence for either. I swept every in-repo use and confirm that: README.md:90 documents the primary launch as bare claude; the only claude launch carrying the flag is tests/fm-claude-stop-autoarm-live-e2e.test.sh:115 (claude -p ..., headless print mode), matching docs/arm-pretool-check.md:201, docs/cd-guard.md:158, and docs/subagent-guard.md:248 which are all -p too; every codex use (tests/fm-codex-continuity-live-e2e.test.sh:40-42, docs/arm-pretool-check.md:202, docs/cd-guard.md:159, docs/subagent-guard.md:185, docs/verification/supervision.md:17) is codex exec, headless. No doc anywhere states the primary session's permission posture. A COUNTER-ARGUMENT the arm comments miss and that likely settles it: this library's own header (line 38) defines the secondmate kind as "a firstmate PRIMARY launched in a provisioned secondmate home", and the shipped, byte-pinned secondmate templates (lines 142 and 146) launch that interactive primary firstmate session with exactly these two flags. That is real in-repo evidence for an interactive primary shape. The distinction left for the owner is that a secondmate is an unattended firstmate while the fleet launcher's primary is the captain's own attended session, so the flags may or may not belong. Decide and record it; the pins at tests/fm-launch-lib.test.sh:94 and :105 currently freeze the inherited shape either way.
  • ℹ️ bin/fm-launch-lib.sh:32 - The function contract still reads "Returns 1 for a harness with no verified adapter - that non-zero return is the unverified-adapter guard every caller relies on". After this round that is incomplete: kimi IS a verified adapter (its crewmate template is live at line 170 and fm-spawn.sh depends on it) yet line 125 now returns 1 for kind=primary. A caller cannot distinguish "unverified adapter" from "verified adapter, unsupported kind", and bin/fm-spawn.sh:437/441 hardcode the first reading in their error text ("unknown harness '$HARNESS'; pass a raw launch command to use an unverified adapter"). fm-spawn never passes kind=primary so nothing misreports today, but the fleet launcher this library exists to serve would tell a user kimi is an unverified adapter and point them at the raw-launch escape hatch, when the real reason is that kimi is deliberately outside the verified primary set. Extend the header sentence to cover both refusal reasons so the next consumer writes the right message.
  • ℹ️ bin/fm-launch-lib.sh:73 - The new header sentence asserts unconditionally that "Every primary template below is the shape this repo empirically verified for a briefless PRIMARY launch, not the crewmate command with its brief argument subtracted". Two of the five arms directly beneath it say the opposite: line 84-89 records that for claude "the repo pins the prefix but not this flag's place in an interactive primary", and line 91-94 records that for codex "No in-repo primary codex TUI launch exists to pin" with the flag carried over from the crewmate command. In the file that is now the single owner of launch knowledge, a header making an unqualified verification claim that its own arms disclaim is the same knowledge-drift risk this change exists to close - a future reader trusting the header would treat the claude and codex primary shapes as empirically settled. Qualify it (e.g. "each arm cites its evidence, and where the repo has none the arm says so explicitly"), which the arms already support.

🔧 Fix: record primary autonomy evidence and consumer obligation
1 warning still open:

  • ⚠️ bin/fm-launch-lib.sh:91 - The new CONSUMER OBLIGATION enumerates the primary templates whose posture a consumer must disclose: "the claude and codex primary templates launch a session that runs with NO permission prompts, and the opencode primary template allows every permission outright". Four of the five primary templates carry a permission-bypass flag, not three - the grok primary at line 158 is grok --trust --always-approve ..., and .agents/skills/harness-adapters/SKILL.md:304 defines --always-approve as "auto-approves every tool execution, verified to run fully unattended", explicitly equating it with --permission-mode bypassPermissions. That is the same no-permission-prompt posture the obligation exists to disclose. (pi at line 152 is correctly absent - it carries no autonomy flag.) Because the obligation is worded as binding and enumerative, a consumer implementing it literally would surface the posture for claude, codex, and opencode and silently ship grok's, which defeats the point of putting the obligation in the one owner. The same omission shows in the arm comments: the claude arm (lines 121-123) and codex arm both point back at the CONSUMER OBLIGATION, the grok arm does not. This is mechanical to close and matches the author's evident intent - the owner's instruction named only claude and codex, and the author already generalized it to opencode unprompted, so grok is the same generalization.

🔧 Fix: bind consumer obligation to any bypass flag, add grok
1 warning still open:

  • ⚠️ bin/fm-launch-lib.sh:167 - The new justification for excluding pi from the CONSUMER OBLIGATION is contradicted by the repo's own verified adapter knowledge. Lines 165-167 assert "--approve is part of that scaffolding, and leaving it out is why pi is the one primary template the header's CONSUMER OBLIGATION does not catch: a pi primary still prompts", and line 176 in the header repeats "a pi primary still prompts". But .agents/skills/harness-adapters/SKILL.md:272 states flatly: "Pi has no permission system, so crewmates are always autonomous." If pi has no permission system, a pi primary does not prompt - the correct reason pi is excluded is that there is no permission gate to bypass and therefore nothing for a consumer to disclose, not that the template withholds a bypass flag. The conclusion (exclude pi) is right; only the recorded reason is wrong. Two concrete harms, both the exact drift class this library exists to close. First, this file is now the designated single owner of launch knowledge, and it records a per-harness permission fact that its own skill contradicts. Second, the wording actively invites the next maintainer to "fix" it: read against the header's rule, "a pi primary still prompts" reads as pi missing an autonomy flag its siblings have, and the obvious next edit is to add --approve to the template - a silent permission-posture change of exactly the kind the owner has twice required be escalated rather than made locally. Separately, the claim that --approve is test-isolation scaffolding is unsupported: it appears in all nine pi invocations repo-wide (tests/fm-pi-primary-live-e2e.test.sh:124, 220, 233, 266 and tests/fm-calm-pi-extension.test.sh:1064, 1192, 1349, 1673, 1948) alongside completely different --no-* isolation sets, it is the only one of those flags without a --no- prefix, and no in-repo source documents its role - SKILL.md has Autonomy rows for grok (:304) and one at :361 but none for pi. Fix by grounding the exclusion in SKILL.md:272 and dropping the unsupported --approve characterization; do not add --approve to the template.

🔧 Fix: bind consumer obligation to every primary, fix pi rationale
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 info
  • ℹ️ tests/fm-pi-watch-extension.test.sh:119 - tests/fm-pi-watch-extension.test.sh:test_pi_extension_reports_external_healthy_watcher fails in this environment ("expected exit 0, got 1"). I verified it fails identically at base commit a5fe1bc with the branch's changes absent, so it is pre-existing and unrelated to the launch-library extraction. The two assertions in that file that this change actually touches (the Pi secondmate launch wiring now read from bin/fm-launch-lib.sh) both pass.
  • bin/fm-test-run.sh tests/fm-launch-lib.test.sh tests/fm-kimi-harness.test.sh tests/fm-pi-watch-extension.test.sh tests/fm-captain-translation-contract.test.sh
  • bin/fm-test-run.sh tests/fm-spawn-batch.test.sh tests/fm-spawn-dispatch-profile.test.sh tests/fm-spawn-worktree-settle.test.sh (drives real spawns and pins the typed per-harness model/effort flags)
  • Differential sweep: sourced the pre-extraction launch_template/model_flag_for_harness/effort_flag_for_harness out of base a5fe1bc's bin/fm-spawn.sh and HEAD's bin/fm-launch-lib.sh, replayed fm-spawn.sh's own placeholder substitution, and diffed 438 fully composed commands (/tmp/no-mistakes-evidence/01KYFJ46F1VNA026V80MW8WQVD/launch-command-differential.sh)
  • End-to-end spawn: ran the real bin/fm-spawn.sh --model opus --effort high &lt;task&gt; &lt;project&gt; at base and at HEAD with a fake tmux recording send-keys -l, and compared the literal launch line typed into the pane for claude, codex, opencode, pi, grok (/tmp/no-mistakes-evidence/01KYFJ46F1VNA026V80MW8WQVD/spawn-e2e-launchline.sh)
  • Manual guard check: bin/fm-spawn.sh &lt;task&gt; &lt;project&gt; not-a-harness still refuses with error: unknown harness ... through the sourced library
  • Mutation check in a throwaway copy of HEAD: planted a hand-copied claude --dangerously-skip-permissions in a new bin/ script, redefined launch_template inside fm-spawn.sh, dropped grok primary --trust, reverted opencode primary to --prompt, and dropped claude's CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION prefix - each reverted after bash tests/fm-launch-lib.test.sh / bash tests/fm-kimi-harness.test.sh
  • bin/fm-test-run.sh --list --changed --base a5fe1bc to confirm the new bin/fm-launch-lib.sh maps into test selection
  • bash tests/fm-pi-watch-extension.test.sh run against a base-commit (a5fe1bc) checkout to confirm the one failing assertion is pre-existing
⚠️ **Document** - 1 info
  • ℹ️ bin/fm-launch-lib.sh:82 - bin/fm-launch-lib.sh's header and per-arm comments cite evidence by exact line number (README.md:61/90/96/102/105/106, .agents/skills/harness-adapters/SKILL.md:272/276-278/304/345, CONTRIBUTING.md:50, docs/configuration.md:177, docs/turnend-guard.md:63, several tests). Those citations are correct today and I preserved them by keeping every edit to a single-line, in-place rewrite, but nothing enforces them: any future insertion in those files silently slides the citations onto unrelated lines, and the argument each arm rests on becomes unverifiable. Out of scope here; worth a follow-up that either anchors the citations to quoted text/section headings or adds a check that resolves each cited line.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

A fleet launcher will soon open PRIMARY firstmate sessions alongside the
crewmate sessions fm-spawn.sh opens, so both need the same verified launch
commands. Today that knowledge lives only inside bin/fm-spawn.sh, and the
drift a second copy causes is not hypothetical: a downstream registry
hand-copied claude's command as `claude --dangerously-skip-permissions`,
dropping CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false - the ghost-text
suppression that keeps firstmate from reading predicted-prompt text as real
typed input when it captures a pane.

Extract launch_template, model_flag_for_harness, and effort_flag_for_harness
(plus the shell_quote both flag resolvers depend on) into a new sourced
bin/fm-launch-lib.sh, and have fm-spawn.sh source it. Every crewmate, scout,
and secondmate template is byte-identical to before, so spawn behavior is
unchanged on all six verified adapters.

launch_template also gains a `primary` kind for the launcher. A primary
session has no task, no worktree, no brief, and no status file, so it launches
bare and is greeted by the session-start adapters already installed in the
home; each primary template keeps its adapter's verified autonomy flag and
claude's ghost-text prefix. An unrecognized kind still resolves to the
crewmate shape, and an unverified adapter still returns non-zero for every
kind.

tests/fm-launch-lib.test.sh pins both arms directly, including a proof that
fm-spawn.sh redefines none of the functions and that no other script under
bin/ hand-writes a launch command. Existing suites that read the template
bytes now read them from their new owner.
sbracewell64 pushed a commit that referenced this pull request Jul 30, 2026
* fix(bin): handle dash-leading harness process names (#2)

* fix: handle dash-leading harness process names

* no-mistakes(review): Make dash-leading harness regression hermetic

* fix: preserve secondmate reply routes across relative homes

Resolve relative home, data, and state inputs before durable charter generation, and fail when caller-relative directories cannot be resolved.

Use absolute paths at the related spawn, AFK daemon, and X-mode cross-process handoffs so later processes cannot reinterpret them from another working directory.

* no-mistakes(review): Preserve absolute overrides and normalize relative durable paths

* no-mistakes(review): Normalize relative home before deriving durable paths

* no-mistakes(document): Document relative durable-path normalization

* no-mistakes(review): Captain: Ignore inherited CDPATH during relative path normalization

* no-mistakes(lint): Fix empty CDPATH assignments for ShellCheck
sbracewell64 added a commit that referenced this pull request Jul 31, 2026
… briefs to read the marker (#9)

* feat(bin): add verified pi-signed runtime adapter (kunchenguid#1145)

* feat: add verified pi-signed adapter

* no-mistakes(review): Correct pi-signed maintainer verification date

* no-mistakes(review): Correct remaining pi-signed verification dates

* no-mistakes(review): Preserve authoritative pi-signed runtime identity

* no-mistakes(document): Document pi-signed shared adapter semantics

* no-mistakes: apply CI fixes

* fix(pi): rearm watcher across session transitions (kunchenguid#1166)

* fix(pi): rearm watcher across same-process session transitions

Pi emits session_shutdown for ordinary /new, /resume, and /fork replacement
as well as terminal quit. The primary watcher extension latched a module-level
stopping flag on every shutdown, so a replacement session in the same process
could not arm monitoring until Pi restarted.

Own arm authority per session generation so only the active live generation
may start, stop, or rearm the child. Replacement sessions can arm again without
restarting Pi, stale prior-generation callbacks cannot mutate the active cycle,
and real quit still blocks late rearm.

* no-mistakes(review): Preserve Pi generation isolation and exit cleanup

* no-mistakes(document): Correct Pi watcher transition documentation

* feat: route crew dispatch using quota-window pace (kunchenguid#1172)

* Consume quota-axi pace signals in dispatch profile array selection.

Add quota-array-dispatch as the single owner of the pace-aware candidate
choice, keep AGENTS.md to the intake boundary and load trigger, and cover
the acceptance cases with sanitized schemaVersion 3 fixtures.

* no-mistakes(review): Stop and report genuine quota dispatch ties

* no-mistakes(document): Document quota pace freshness and uncertainty

* fix: adapt Grok Stop continuation and harden endpoint cleanup (kunchenguid#1171)

* fix(grok): adapt Stop continuation to runtime capability

* no-mistakes(review): Reject ambiguous Grok Stop payloads

* no-mistakes(review): Reject duplicate Grok fields and accept spaced tmux sessions

* no-mistakes(review): Enforce exact tmux cleanup selectors

* no-mistakes(test): Fix historical tmux fixture and validate Grok Stop

* no-mistakes: apply CI fixes

* fix: restore stock macOS Bash 3.2 brief scaffolding (kunchenguid#1093)

* fix(brief): make DOD scaffolding parse-safe on stock macOS Bash 3.2

fm-brief.sh built each Definition-of-done block and the not-enabled
Herdr declaration with `VAR=$(cat <<EOF ... EOF)`. On Bash 3.2 (macOS
/bin/bash) the lexer scans for the command substitution's closing `)`
textually and tracks quote state through the heredoc body, so a single
apostrophe, unbalanced quote, or unbalanced paren in that prose breaks
parsing of the whole script. Every ship-brief scaffold (no-mistakes,
direct-PR, local-only) failed with `unexpected EOF while looking for
matching )`. Bash 4+ parses it fine, so the breakage stayed invisible
everywhere except stock macOS.

Replace all four command-substitution heredocs with
`IFS= read -r -d '' VAR <<EOF || true`. That removes the `$(...)`
wrapper and the entire defect class regardless of future prose, and
preserves the variable expansion the direct-PR and local-only bodies
need. `read` keeps the heredoc's trailing newline that `$(...)` used to
strip, so trim one newline to keep every generated brief byte-identical
to prior output.

Guard the structure, not one historical phrase: a new test rejects any
heredoc nested in a command substitution anywhere in fm-brief.sh, where
the old assertion pinned a single apostrophe phrase and so missed the
reintroduction. Extend the stock-macOS Bash CI job from parsing one
script to the whole maintained shell surface (bin/*.sh,
bin/backends/*.sh, tests/*.sh), matching bin/fm-lint.sh's canonical file
set so parse scope and lint scope cannot drift apart.

* no-mistakes(review): Captain: harden Bash structure and inventory guards

* no-mistakes(document): Align stock macOS Bash contributor checks

* no-mistakes(lint): Suppress deliberate SC2016 literal fixture warnings

* test: stabilize tmux teardown conformance baseline (kunchenguid#1209)

* fix(test): pin teardown tmux baseline to historical kill selectors

merge-base HEAD main collapses to HEAD after the exact-selector change
lands on the default branch, so the old teardown fixture was accidentally
exercising current exact targets. Resolve a content-historical permissive
tmux adapter from first-parent history and force that post-squash topology
inside the conformance case so main and feature branches keep the same
old-vs-new contract.

* no-mistakes(lint): Suppress intentional literal-pattern ShellCheck warnings

* docs: slim quota-array-dispatch to the pace selection core (kunchenguid#1197)

Cut the runtime skill to the compact pace-aware selection procedure plus
minimum owner pointers. Keep every distinct decision rule and move expanded
acceptance scenarios to deterministic fixture ownership assertions.

Size: 170/1374/10187 -> 63/544/4068 (about 63%/60%/60% reduction).

* feat(bin): inherit backend config into secondmate homes (kunchenguid#1219)

* Inherit config/backend into secondmate homes with deliberate-override preservation

Add backend to the shared inheritable config allowlist so launch, locked
bootstrap, and config-push converge a primary pin into secondmate homes as each
home local future-spawn default. Track last-inherited bytes in a private state
provenance marker so deliberate per-home overrides survive present and absent
primary convergence, keep --backend and FM_BACKEND stronger, and extend the
existing inheritance tests plus docs and skill claims.

* no-mistakes(review): Preserve equal unprovenanced backend overrides

* no-mistakes(review): Preserve symlink overrides and verify spawn precedence

* no-mistakes(review): Snapshot backend inheritance for consistent provenance

* no-mistakes(review): Simplify backend inheritance to primary-authoritative convergence

* no-mistakes(document): Document inherited backend override preservation

* fix: restore primary-authoritative backend inheritance after document regression

The document step reintroduced provenance and deliberate per-home override
semantics after review had simplified config/backend to plain primary-authoritative
allowlist membership. Restore the primary-always-wins path: present overwrites,
absent removes, no provenance marker, and docs/tests match that contract.

* no-mistakes(review): Add divergent backend precedence regression fixtures

* no-mistakes(document): Document backend inheritance contract

* fix(pi): remove Calm's upper version ceiling (kunchenguid#1226)

* fix(pi): remove Calm's exclusive Pi upper-version ceiling

tests/fm-calm-pi-extension.test.sh gated on a closed PI_COMPAT_VERSIONS
allowlist ("0.81.1 0.82.0") that refused any other installed Pi, and docs
described that range as "supported" rather than verified evidence. The
Calm CHANGELOG shows no API introduced at either version, so there is no
evidence for a real minimum; the presentation adapters already probe the
exact method they patch rather than checking a version.

Replace the allowlist with dated version evidence that never rejects a
newer Pi, and make each presentation adapter degrade independently with
a diagnostic if a future Pi removes its API, instead of the whole Calm
extension failing to load. Rewrite the feasibility doc's "Pi 0.81.1
through 0.82.0" phrasing to state it as verified evidence, not a
ceiling.

* no-mistakes(review): Probe missing Calm adapter exports safely

* no-mistakes(document): Document Calm's unbounded Pi compatibility

* fix(bin): allow session-local todo tools in the subagent guard (kunchenguid#1204)

* fix(guard): allow session-local todo tools in the primary

The delegation-shape guard denied TaskCreate and TaskUpdate because their
normalized names contain the `task` stem. Those tools write only the harness's
session-local todo list, which has no executor: it spawns no agent, allocates
no worktree, registers no schedule, and starts nothing that outlives the
session. That is not the unaccounted work the guard exists to stop, so the stem
match was a false positive, and the deny text told the primary to run
bin/fm-brief.sh and bin/fm-spawn.sh to create a todo entry.

Add a separately-reasoned PLAN_ONLY_TOOLS exact-name exclusion rather than
widening OBSERVE_ONLY_TOOLS, whose documented contract is tools that only
observe or stop existing work. Both lists stay exact-name so neither can widen
by substring.

Tests cover the two allowed names and six near-miss names that a substring or
shortened-stem widening would release; both mutations were watched red.

* no-mistakes(review): drop session-local todo tools from recommended deny list

* no-mistakes: apply CI fixes

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid (kunchenguid#1206)

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid

fm_harness_ancestry_pid() previously returned the first ancestor process
whose command matched a verified harness name. Claude Code's Stop hook
fires as a bg-spare worker several levels below the session's actual
lock-owning claude process (hook shell -> claude bg-spare ->
claude bg-pty-host -> claude -> claude(lock)), so the first match was
the bg-spare worker, not the lock owner. fm_session_lock_owned_by_self()
then never matched state/.lock, and the Claude Stop auto-arm silently
treated its own primary session as an unrelated live owner and never
armed the watcher.

The walk now keeps going past a claude-named match, looking for a still
more ancestral claude-named match, and stops the instant a non-match
follows an already-found match (bounding it to a contiguous run rather
than the literal ancestry top, so an unrelated claude-named process
further up the real process tree is never mistaken for part of this
session's own nested chain). Every other harness keeps the original
first-match-wins behavior, since e.g. Pi's shared signed-wrapper
ancestry actually holds the session at the inner engine pid, not an
outer wrapper pid. Hop limit raised from 8 to 16 to cover the deeper
bg-spare chain.

* no-mistakes(review): Add nested-claude-ancestry regression test; fix nudge doc depth claim

* no-mistakes: apply CI fixes

* fix: conferma l'avvio del watcher su Windows/MSYS (kunchenguid#1212)

* fix: confirm watcher startup on MSYS

* no-mistakes(review): gate MSYS arm ready timeout, cache uname, harden locale test

* no-mistakes(review): validate OpenCode ready timeout, make uname cache internal

* fix(spawn): forward CLAUDE_CONFIG_DIR to claude crewmates (kunchenguid#1195)

* fix(spawn): forward firstmate's CLAUDE_CONFIG_DIR to claude crewmates

Crewmate panes are created by a long-lived tmux/herdr daemon that does not
inherit firstmate's current environment. When firstmate runs under a non-default
CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split), a bare
`claude` in the crewmate pane fell back to the default ~/.claude store and
launched unauthenticated, blocking the crewmate before it could do any work.

fm-spawn now prefixes the claude launch with firstmate's own resolved
CLAUDE_CONFIG_DIR when set, so the crewmate uses the same credential/config
store firstmate is authenticated with. An unset value is the single-store
default and adds no prefix; non-claude harnesses are unaffected.

Adds three tests in fm-spawn-dispatch-profile.test.sh (forwarded-when-set,
omitted-when-unset, non-claude-ignored) and pins CLAUDE_CONFIG_DIR in the test
helper so launch assertions no longer depend on the developer's environment.

* no-mistakes: apply CI fixes

* fix: preserve dispatch identity across authentication checks (kunchenguid#1233)

* fix: preserve dispatch harness identity

* no-mistakes(review): Fix Grok counterfactual tuple validation

* no-mistakes(document): Scope dispatch authentication to selected tuple

* fix: restore dispatch instruction budget

* no-mistakes(review): Scope dispatch authentication after candidate selection

* fix(bin): normalize relative durable paths (kunchenguid#1256)

* fix(bin): handle dash-leading harness process names (#2)

* fix: handle dash-leading harness process names

* no-mistakes(review): Make dash-leading harness regression hermetic

* fix: preserve secondmate reply routes across relative homes

Resolve relative home, data, and state inputs before durable charter generation, and fail when caller-relative directories cannot be resolved.

Use absolute paths at the related spawn, AFK daemon, and X-mode cross-process handoffs so later processes cannot reinterpret them from another working directory.

* no-mistakes(review): Preserve absolute overrides and normalize relative durable paths

* no-mistakes(review): Normalize relative home before deriving durable paths

* no-mistakes(document): Document relative durable-path normalization

* no-mistakes(review): Captain: Ignore inherited CDPATH during relative path normalization

* no-mistakes(lint): Fix empty CDPATH assignments for ShellCheck

* refactor(skills): make Bearings chat-only by default (kunchenguid#1136)

* Add internal status skill

* no-mistakes(document): register /status skill in documentation-audiences inventory

* no-mistakes(lint): replace grep|wc -l with grep -c in status skill test

* test: silence literal status skill patterns

* Refactor bearings default to chat-only

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>

* Clarify follow-up routing during validation (kunchenguid#1277)

* fix: honor concrete approval for project operations (kunchenguid#1272)

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* no-mistakes(review): Align project removal preflight with approved exception

* no-mistakes(document): Align project removal documentation with approved exception

* fix: restore removal test byte-for-byte and preserve the default sentence

tests/fm-instruction-owners.test.sh had been changed to assert different
text; restore it byte-for-byte to origin/main. project-management SKILL.md's
Remove section now keeps the exact default "Never issue a raw removal
command from Firstmate." sentence that test still asserts, immediately
followed by the already-approved captain-operation-or-scope exception, so
the default and the exception both stay explicit and consistent.

* no-mistakes(document): Align project-write boundary documentation

* fix(skills): route new project intake through secondmate scopes (kunchenguid#1275)

* Route project intake through secondmate scopes

* no-mistakes(test): Guard all main-home project registry mutations

* no-mistakes(document): Consolidate secondmate routing documentation

* no-mistakes: apply CI fixes

* Restore new-project routing scope

* no-mistakes(document): Clarify secondmate routing for new-project intake

* no-mistakes: apply CI fixes

* fix: scope validation corrections by accepted behavior (kunchenguid#1281)

* fix: scope validation corrections by accepted behavior

* no-mistakes(review): Classify stale delivery evidence as an autonomous correction

* test: replace source assertions with behavioral coverage (kunchenguid#1282)

* test: remove source-content assertions

* no-mistakes(review): Replace source assertions with runtime behavior coverage

* no-mistakes(review): Isolate Kimi task temp runtime coverage

* no-mistakes(document): Refresh test cleanup documentation

* no-mistakes: apply CI fixes

* fix(watch): escalate busy workers with no completed turn (kunchenguid#1286)

* fix(watch): bound how long a busy pane may run with no completed turn

A busy pane (backend busy state or the harness's rendered footer) was
unconditional, unbounded proof of liveness in every escalation path, so a
hung foreground tool call behind a busy signature could run for hours
undetected (2026-07 hibit-agent-focus-nonsteal-r1 incident: a catastrophic-
backtracking regex hung one bash call for 25h behind an unchanging
"Working..." footer).

FM_BUSY_TURN_MAX_SECS (default 3600s) now bounds how long a busy pane may
run with no completed turn (state/<id>.turn-ended, or its spawn record
before any turn has completed). Past the bound, busy_turn_over_age routes
the pane through the existing wedge_timer_check, reusing the identical
stale reason, escalation counter, and demand-deep-inspection marker for
human inspection only - never an automatic interrupt, signal, or restart
of the worker or its tool process. A completed turn resets the age.

Reproduced end-to-end against the real installed Pi TUI: a foreground
`sleep 999999` bash call with no timeout renders the actual busy footer,
and two captures ~15s apart show the elapsed counter changing the pane
hash while the same turn stays unfinished. Running the pre-fix watcher
against the real captures showed it never starts a wedge timer no matter
how long the pane stays busy; the fixed watcher starts and escalates the
timer through the same mechanism, while the real hung process remained
untouched and alive throughout.

* no-mistakes(review): fix: parse enriched AFK stale reasons

* no-mistakes(review): fix: preserve enriched wedges during AFK supervision

* no-mistakes(review): fix: route all enriched AFK wedges

* no-mistakes(document): Clarify busy-turn age supervision documentation

* fix(gitignore): ignore config/ as a directory, not by exact filename (kunchenguid#1261)

A name-by-name list of config/ entries silently stops ignoring any new or
home-local file placed there, which makes the working tree read as dirty and
blocks guarded sync paths that refuse to touch a dirty home. AGENTS.md
already documents config/ as captain-private and gitignored as a category;
this makes .gitignore match that contract.

* fix(tests): replace source-content .gitignore assertion with behavioral coverage (kunchenguid#1304)

The second assertion in fm-gitignore-config.test.sh (added by kunchenguid#1261) greps
.gitignore for a specific spelling of the config/ ignore pattern. It fails
on a semantically equivalent pattern like config/** and does not prove Git
actually ignores anything, per the completed source-content-test audit.

Replace it with a real git check-ignore control test on a generated
unrelated path, and strengthen the existing directory-coverage test with
generated unpredictable direct and nested config/ paths.

* feat: bound and consolidate startup memory during stow (kunchenguid#1303)

* Add bounded startup memory curation

* no-mistakes(review): Record reproducible stow verification evidence

* no-mistakes(review): Validate inherited secondmate stow evidence

* no-mistakes(document): Document editable startup-memory budget propagation

* feat(bin): mark crewmate and scout steers as from-firstmate

A steer lands in the receiving agent's own chat, where nothing else told
firstmate's instructions apart from a human typing into that pane. The gap
was proven in both directions on 2026-07-26: the captain opened a crewmate
pane believing it was firstmate and issued cross-lane instructions there, and
a Pi crewmate at an ask-user gate addressed "Captain, ..." into its own pane
and sat parked - nobody reads a crewmate pane, and a parked pipeline emits no
wake, so that direction fails silently. AGENTS.md section 1 rule 4 already
required workers to honor a distinction the system gave them no means to make.

fm-send now applies the existing from-firstmate carrier to every text steer
whose target resolves through this home's meta, not just kind=secondmate.
A crewmate or scout carries the marker alone; the corr= correlation token and
the parent pending-reply record stay secondmate-only, because a crewmate
already answers on its own status file. Explicit backend targets and the
--key path are unchanged.

Command-shaped text is the one exclusion. A harness recognizes a slash
command, or a codex $<skill> invocation, only at the very start of the
composer line, so any prefix silently demotes it to prose. Verified on claude
2.1.220 and pi 0.82.0: with either marker shape prepended, /no-mistakes stops
opening the completion popup entirely and would submit as ordinary text.
Crewmate sends of that shape therefore stay unmarked and byte-identical, which
also keeps every documented popup hazard out of this change's blast radius:
the only bytes that move are plain text no harness parses specially. The
exclusion deliberately does not reach a secondmate, whose marker is what
creates its reply guarantee.

The ship and scout scaffolds gain a "Who is speaking to you" section teaching
the reader side: marked is firstmate, unmarked is a human who may believe the
pane is firstmate, self-identify as a worker on this task before acting, and
escalation is always the status file. AGENTS.md states the provenance
principle once in rule 4; the away-mode stub and the secondmate charter keep
their own distinct consequences.

* no-mistakes(review): align brief's unmarked-message exception wording to fm-send predicate

* no-mistakes(test): fix stale corr-less assertion in Pi/Herdr marker e2e

* no-mistakes(document): generalize task-selector marker context to from-firstmate

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com>
Co-authored-by: Daniel Kuykendall IV <danielkuykendall23@gmail.com>
Co-authored-by: Trillium Smith <Spiteless@gmail.com>
Co-authored-by: Unknownzed <45267749+Unknownzed@users.noreply.github.com>
Co-authored-by: lhalbert <lucashalbert@users.noreply.github.com>
Co-authored-by: AG <ag@agw3.org>
Co-authored-by: deeto15 <92119640+deeto15@users.noreply.github.com>
sbracewell64 added a commit that referenced this pull request Aug 2, 2026
* feat(bin): add verified pi-signed runtime adapter (kunchenguid#1145)

* feat: add verified pi-signed adapter

* no-mistakes(review): Correct pi-signed maintainer verification date

* no-mistakes(review): Correct remaining pi-signed verification dates

* no-mistakes(review): Preserve authoritative pi-signed runtime identity

* no-mistakes(document): Document pi-signed shared adapter semantics

* no-mistakes: apply CI fixes

* fix(pi): rearm watcher across session transitions (kunchenguid#1166)

* fix(pi): rearm watcher across same-process session transitions

Pi emits session_shutdown for ordinary /new, /resume, and /fork replacement
as well as terminal quit. The primary watcher extension latched a module-level
stopping flag on every shutdown, so a replacement session in the same process
could not arm monitoring until Pi restarted.

Own arm authority per session generation so only the active live generation
may start, stop, or rearm the child. Replacement sessions can arm again without
restarting Pi, stale prior-generation callbacks cannot mutate the active cycle,
and real quit still blocks late rearm.

* no-mistakes(review): Preserve Pi generation isolation and exit cleanup

* no-mistakes(document): Correct Pi watcher transition documentation

* feat: route crew dispatch using quota-window pace (kunchenguid#1172)

* Consume quota-axi pace signals in dispatch profile array selection.

Add quota-array-dispatch as the single owner of the pace-aware candidate
choice, keep AGENTS.md to the intake boundary and load trigger, and cover
the acceptance cases with sanitized schemaVersion 3 fixtures.

* no-mistakes(review): Stop and report genuine quota dispatch ties

* no-mistakes(document): Document quota pace freshness and uncertainty

* fix: adapt Grok Stop continuation and harden endpoint cleanup (kunchenguid#1171)

* fix(grok): adapt Stop continuation to runtime capability

* no-mistakes(review): Reject ambiguous Grok Stop payloads

* no-mistakes(review): Reject duplicate Grok fields and accept spaced tmux sessions

* no-mistakes(review): Enforce exact tmux cleanup selectors

* no-mistakes(test): Fix historical tmux fixture and validate Grok Stop

* no-mistakes: apply CI fixes

* fix: restore stock macOS Bash 3.2 brief scaffolding (kunchenguid#1093)

* fix(brief): make DOD scaffolding parse-safe on stock macOS Bash 3.2

fm-brief.sh built each Definition-of-done block and the not-enabled
Herdr declaration with `VAR=$(cat <<EOF ... EOF)`. On Bash 3.2 (macOS
/bin/bash) the lexer scans for the command substitution's closing `)`
textually and tracks quote state through the heredoc body, so a single
apostrophe, unbalanced quote, or unbalanced paren in that prose breaks
parsing of the whole script. Every ship-brief scaffold (no-mistakes,
direct-PR, local-only) failed with `unexpected EOF while looking for
matching )`. Bash 4+ parses it fine, so the breakage stayed invisible
everywhere except stock macOS.

Replace all four command-substitution heredocs with
`IFS= read -r -d '' VAR <<EOF || true`. That removes the `$(...)`
wrapper and the entire defect class regardless of future prose, and
preserves the variable expansion the direct-PR and local-only bodies
need. `read` keeps the heredoc's trailing newline that `$(...)` used to
strip, so trim one newline to keep every generated brief byte-identical
to prior output.

Guard the structure, not one historical phrase: a new test rejects any
heredoc nested in a command substitution anywhere in fm-brief.sh, where
the old assertion pinned a single apostrophe phrase and so missed the
reintroduction. Extend the stock-macOS Bash CI job from parsing one
script to the whole maintained shell surface (bin/*.sh,
bin/backends/*.sh, tests/*.sh), matching bin/fm-lint.sh's canonical file
set so parse scope and lint scope cannot drift apart.

* no-mistakes(review): Captain: harden Bash structure and inventory guards

* no-mistakes(document): Align stock macOS Bash contributor checks

* no-mistakes(lint): Suppress deliberate SC2016 literal fixture warnings

* test: stabilize tmux teardown conformance baseline (kunchenguid#1209)

* fix(test): pin teardown tmux baseline to historical kill selectors

merge-base HEAD main collapses to HEAD after the exact-selector change
lands on the default branch, so the old teardown fixture was accidentally
exercising current exact targets. Resolve a content-historical permissive
tmux adapter from first-parent history and force that post-squash topology
inside the conformance case so main and feature branches keep the same
old-vs-new contract.

* no-mistakes(lint): Suppress intentional literal-pattern ShellCheck warnings

* docs: slim quota-array-dispatch to the pace selection core (kunchenguid#1197)

Cut the runtime skill to the compact pace-aware selection procedure plus
minimum owner pointers. Keep every distinct decision rule and move expanded
acceptance scenarios to deterministic fixture ownership assertions.

Size: 170/1374/10187 -> 63/544/4068 (about 63%/60%/60% reduction).

* feat(bin): inherit backend config into secondmate homes (kunchenguid#1219)

* Inherit config/backend into secondmate homes with deliberate-override preservation

Add backend to the shared inheritable config allowlist so launch, locked
bootstrap, and config-push converge a primary pin into secondmate homes as each
home local future-spawn default. Track last-inherited bytes in a private state
provenance marker so deliberate per-home overrides survive present and absent
primary convergence, keep --backend and FM_BACKEND stronger, and extend the
existing inheritance tests plus docs and skill claims.

* no-mistakes(review): Preserve equal unprovenanced backend overrides

* no-mistakes(review): Preserve symlink overrides and verify spawn precedence

* no-mistakes(review): Snapshot backend inheritance for consistent provenance

* no-mistakes(review): Simplify backend inheritance to primary-authoritative convergence

* no-mistakes(document): Document inherited backend override preservation

* fix: restore primary-authoritative backend inheritance after document regression

The document step reintroduced provenance and deliberate per-home override
semantics after review had simplified config/backend to plain primary-authoritative
allowlist membership. Restore the primary-always-wins path: present overwrites,
absent removes, no provenance marker, and docs/tests match that contract.

* no-mistakes(review): Add divergent backend precedence regression fixtures

* no-mistakes(document): Document backend inheritance contract

* fix(pi): remove Calm's upper version ceiling (kunchenguid#1226)

* fix(pi): remove Calm's exclusive Pi upper-version ceiling

tests/fm-calm-pi-extension.test.sh gated on a closed PI_COMPAT_VERSIONS
allowlist ("0.81.1 0.82.0") that refused any other installed Pi, and docs
described that range as "supported" rather than verified evidence. The
Calm CHANGELOG shows no API introduced at either version, so there is no
evidence for a real minimum; the presentation adapters already probe the
exact method they patch rather than checking a version.

Replace the allowlist with dated version evidence that never rejects a
newer Pi, and make each presentation adapter degrade independently with
a diagnostic if a future Pi removes its API, instead of the whole Calm
extension failing to load. Rewrite the feasibility doc's "Pi 0.81.1
through 0.82.0" phrasing to state it as verified evidence, not a
ceiling.

* no-mistakes(review): Probe missing Calm adapter exports safely

* no-mistakes(document): Document Calm's unbounded Pi compatibility

* fix(bin): allow session-local todo tools in the subagent guard (kunchenguid#1204)

* fix(guard): allow session-local todo tools in the primary

The delegation-shape guard denied TaskCreate and TaskUpdate because their
normalized names contain the `task` stem. Those tools write only the harness's
session-local todo list, which has no executor: it spawns no agent, allocates
no worktree, registers no schedule, and starts nothing that outlives the
session. That is not the unaccounted work the guard exists to stop, so the stem
match was a false positive, and the deny text told the primary to run
bin/fm-brief.sh and bin/fm-spawn.sh to create a todo entry.

Add a separately-reasoned PLAN_ONLY_TOOLS exact-name exclusion rather than
widening OBSERVE_ONLY_TOOLS, whose documented contract is tools that only
observe or stop existing work. Both lists stay exact-name so neither can widen
by substring.

Tests cover the two allowed names and six near-miss names that a substring or
shortened-stem widening would release; both mutations were watched red.

* no-mistakes(review): drop session-local todo tools from recommended deny list

* no-mistakes: apply CI fixes

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid (kunchenguid#1206)

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid

fm_harness_ancestry_pid() previously returned the first ancestor process
whose command matched a verified harness name. Claude Code's Stop hook
fires as a bg-spare worker several levels below the session's actual
lock-owning claude process (hook shell -> claude bg-spare ->
claude bg-pty-host -> claude -> claude(lock)), so the first match was
the bg-spare worker, not the lock owner. fm_session_lock_owned_by_self()
then never matched state/.lock, and the Claude Stop auto-arm silently
treated its own primary session as an unrelated live owner and never
armed the watcher.

The walk now keeps going past a claude-named match, looking for a still
more ancestral claude-named match, and stops the instant a non-match
follows an already-found match (bounding it to a contiguous run rather
than the literal ancestry top, so an unrelated claude-named process
further up the real process tree is never mistaken for part of this
session's own nested chain). Every other harness keeps the original
first-match-wins behavior, since e.g. Pi's shared signed-wrapper
ancestry actually holds the session at the inner engine pid, not an
outer wrapper pid. Hop limit raised from 8 to 16 to cover the deeper
bg-spare chain.

* no-mistakes(review): Add nested-claude-ancestry regression test; fix nudge doc depth claim

* no-mistakes: apply CI fixes

* fix: conferma l'avvio del watcher su Windows/MSYS (kunchenguid#1212)

* fix: confirm watcher startup on MSYS

* no-mistakes(review): gate MSYS arm ready timeout, cache uname, harden locale test

* no-mistakes(review): validate OpenCode ready timeout, make uname cache internal

* fix(spawn): forward CLAUDE_CONFIG_DIR to claude crewmates (kunchenguid#1195)

* fix(spawn): forward firstmate's CLAUDE_CONFIG_DIR to claude crewmates

Crewmate panes are created by a long-lived tmux/herdr daemon that does not
inherit firstmate's current environment. When firstmate runs under a non-default
CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split), a bare
`claude` in the crewmate pane fell back to the default ~/.claude store and
launched unauthenticated, blocking the crewmate before it could do any work.

fm-spawn now prefixes the claude launch with firstmate's own resolved
CLAUDE_CONFIG_DIR when set, so the crewmate uses the same credential/config
store firstmate is authenticated with. An unset value is the single-store
default and adds no prefix; non-claude harnesses are unaffected.

Adds three tests in fm-spawn-dispatch-profile.test.sh (forwarded-when-set,
omitted-when-unset, non-claude-ignored) and pins CLAUDE_CONFIG_DIR in the test
helper so launch assertions no longer depend on the developer's environment.

* no-mistakes: apply CI fixes

* fix: preserve dispatch identity across authentication checks (kunchenguid#1233)

* fix: preserve dispatch harness identity

* no-mistakes(review): Fix Grok counterfactual tuple validation

* no-mistakes(document): Scope dispatch authentication to selected tuple

* fix: restore dispatch instruction budget

* no-mistakes(review): Scope dispatch authentication after candidate selection

* fix(bin): normalize relative durable paths (kunchenguid#1256)

* fix(bin): handle dash-leading harness process names (#2)

* fix: handle dash-leading harness process names

* no-mistakes(review): Make dash-leading harness regression hermetic

* fix: preserve secondmate reply routes across relative homes

Resolve relative home, data, and state inputs before durable charter generation, and fail when caller-relative directories cannot be resolved.

Use absolute paths at the related spawn, AFK daemon, and X-mode cross-process handoffs so later processes cannot reinterpret them from another working directory.

* no-mistakes(review): Preserve absolute overrides and normalize relative durable paths

* no-mistakes(review): Normalize relative home before deriving durable paths

* no-mistakes(document): Document relative durable-path normalization

* no-mistakes(review): Captain: Ignore inherited CDPATH during relative path normalization

* no-mistakes(lint): Fix empty CDPATH assignments for ShellCheck

* refactor(skills): make Bearings chat-only by default (kunchenguid#1136)

* Add internal status skill

* no-mistakes(document): register /status skill in documentation-audiences inventory

* no-mistakes(lint): replace grep|wc -l with grep -c in status skill test

* test: silence literal status skill patterns

* Refactor bearings default to chat-only

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>

* Clarify follow-up routing during validation (kunchenguid#1277)

* fix: honor concrete approval for project operations (kunchenguid#1272)

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* no-mistakes(review): Align project removal preflight with approved exception

* no-mistakes(document): Align project removal documentation with approved exception

* fix: restore removal test byte-for-byte and preserve the default sentence

tests/fm-instruction-owners.test.sh had been changed to assert different
text; restore it byte-for-byte to origin/main. project-management SKILL.md's
Remove section now keeps the exact default "Never issue a raw removal
command from Firstmate." sentence that test still asserts, immediately
followed by the already-approved captain-operation-or-scope exception, so
the default and the exception both stay explicit and consistent.

* no-mistakes(document): Align project-write boundary documentation

* fix(skills): route new project intake through secondmate scopes (kunchenguid#1275)

* Route project intake through secondmate scopes

* no-mistakes(test): Guard all main-home project registry mutations

* no-mistakes(document): Consolidate secondmate routing documentation

* no-mistakes: apply CI fixes

* Restore new-project routing scope

* no-mistakes(document): Clarify secondmate routing for new-project intake

* no-mistakes: apply CI fixes

* fix: scope validation corrections by accepted behavior (kunchenguid#1281)

* fix: scope validation corrections by accepted behavior

* no-mistakes(review): Classify stale delivery evidence as an autonomous correction

* test: replace source assertions with behavioral coverage (kunchenguid#1282)

* test: remove source-content assertions

* no-mistakes(review): Replace source assertions with runtime behavior coverage

* no-mistakes(review): Isolate Kimi task temp runtime coverage

* no-mistakes(document): Refresh test cleanup documentation

* no-mistakes: apply CI fixes

* fix(watch): escalate busy workers with no completed turn (kunchenguid#1286)

* fix(watch): bound how long a busy pane may run with no completed turn

A busy pane (backend busy state or the harness's rendered footer) was
unconditional, unbounded proof of liveness in every escalation path, so a
hung foreground tool call behind a busy signature could run for hours
undetected (2026-07 hibit-agent-focus-nonsteal-r1 incident: a catastrophic-
backtracking regex hung one bash call for 25h behind an unchanging
"Working..." footer).

FM_BUSY_TURN_MAX_SECS (default 3600s) now bounds how long a busy pane may
run with no completed turn (state/<id>.turn-ended, or its spawn record
before any turn has completed). Past the bound, busy_turn_over_age routes
the pane through the existing wedge_timer_check, reusing the identical
stale reason, escalation counter, and demand-deep-inspection marker for
human inspection only - never an automatic interrupt, signal, or restart
of the worker or its tool process. A completed turn resets the age.

Reproduced end-to-end against the real installed Pi TUI: a foreground
`sleep 999999` bash call with no timeout renders the actual busy footer,
and two captures ~15s apart show the elapsed counter changing the pane
hash while the same turn stays unfinished. Running the pre-fix watcher
against the real captures showed it never starts a wedge timer no matter
how long the pane stays busy; the fixed watcher starts and escalates the
timer through the same mechanism, while the real hung process remained
untouched and alive throughout.

* no-mistakes(review): fix: parse enriched AFK stale reasons

* no-mistakes(review): fix: preserve enriched wedges during AFK supervision

* no-mistakes(review): fix: route all enriched AFK wedges

* no-mistakes(document): Clarify busy-turn age supervision documentation

* fix(gitignore): ignore config/ as a directory, not by exact filename (kunchenguid#1261)

A name-by-name list of config/ entries silently stops ignoring any new or
home-local file placed there, which makes the working tree read as dirty and
blocks guarded sync paths that refuse to touch a dirty home. AGENTS.md
already documents config/ as captain-private and gitignored as a category;
this makes .gitignore match that contract.

* fix(tests): replace source-content .gitignore assertion with behavioral coverage (kunchenguid#1304)

The second assertion in fm-gitignore-config.test.sh (added by kunchenguid#1261) greps
.gitignore for a specific spelling of the config/ ignore pattern. It fails
on a semantically equivalent pattern like config/** and does not prove Git
actually ignores anything, per the completed source-content-test audit.

Replace it with a real git check-ignore control test on a generated
unrelated path, and strengthen the existing directory-coverage test with
generated unpredictable direct and nested config/ paths.

* feat: bound and consolidate startup memory during stow (kunchenguid#1303)

* Add bounded startup memory curation

* no-mistakes(review): Record reproducible stow verification evidence

* no-mistakes(review): Validate inherited secondmate stow evidence

* no-mistakes(document): Document editable startup-memory budget propagation

* fix(herdr): place workers in the launching workspace (kunchenguid#1328)

* fix(herdr): place workers in the launching agent's exact workspace

Herdr enforces no workspace-label uniqueness, and spawn resolved its
container by taking the FIRST workspace whose label matched the home
label. With two workspaces both labeled "firstmate", a worker launched
from the second one was created in the first, so it appeared in a
different space than the Firstmate the captain was watching.

Reproduced end to end on Herdr 0.7.5 protocol 17 by running the real
bin/fm-spawn.sh inside a launcher pane in the second "firstmate"
workspace: the worker landed in w1 while its launcher was in w2, with an
unrelated third workspace focused throughout, which also rules out any
dependence on the focused workspace.

Placement now binds to the launching process's own Herdr identity. Herdr
injects HERDR_PANE_ID, HERDR_SESSION, and HERDR_SOCKET_PATH into every
process it manages a pane for, and fm_backend_herdr_launcher_identity
resolves that pane's current owning tab and workspace live from Herdr,
cross-checking the pane against its tab and confirming the workspace
exists exactly once in the session. The injected HERDR_TAB_ID and
HERDR_WORKSPACE_ID are creation-time snapshots and are deliberately not
read as current identity. Labels are no longer placement authority.

A claimed parent identity that is unreadable, contradictory, stale, or
from another named session or Herdr server stops the spawn before any
worker endpoint exists, rather than degrading to a label search. A
launcher with no Herdr ancestry has no workspace to inherit and keeps
the per-home labeled container, which must now resolve to exactly one
workspace; two same-labeled candidates refuse instead of adopting
either. A --secondmate launch keeps standing up that home's own
workspace by design.

With presentation spaces enabled, the projected child is created and
bound under that same exact parent and anchors its ordering on it, so a
duplicated home label no longer makes the layout ambiguous. Projection,
focus restoration, restart binding, and quarantine rules are unchanged,
and children are never collapsed into the parent. tmux, Zellij, cmux,
Orca, and the away-mode daemon terminal were each inspected and are not
affected: none resolves a container by searching mutable labels.

tests/fm-backend-herdr-launcher-workspace-e2e.test.sh drives the real
spawn and teardown against an isolated Herdr lab, with its headline case
running fm-spawn.sh inside a real Herdr pane so the identity comes from
Herdr's own injection. The refusal matrix and the ordering anchor are
covered deterministically in tests/fm-backend-herdr.test.sh.

Eight existing real-Herdr suites inherited the developer terminal's own
Herdr pane into their isolated lab sessions, which the new cross-session
check correctly refuses. tests/herdr-test-safety.sh now owns
herdr_forget_inherited_pane and those suites call it, so what they assert
no longer depends on where they were launched from.

Two unrelated fixes found along the way. tests/fm-secondmate-harness.test.sh
had the same class of environment leak through CLAUDECODE, which outranks
PI_CODING_AGENT in bin/fm-harness.sh and made its pi-signed ancestry case
resolve "claude" whenever the suite ran inside Claude Code. And
fm-spawn.sh's usage() printed a fixed line range that had already been
truncating its own help mid-sentence.

* no-mistakes(review): Enforce exact Herdr launcher and projection identity

* no-mistakes(document): Document exact Herdr launcher workspace placement

* fix(calm): refine Calm working boat animation (kunchenguid#1339)

* feat(calm): replace Pi's working row with an animated ship while Calm is on

While Calm is active and one logical agent run is under way, Calm now hides
Pi's built-in working row and renders a small two-row SSHHIP-derived boat in
its place. When Calm is off, Pi's stock working row is left untouched.

The presentation uses only public Pi extension API: setWorkingVisible(false)
plus a temporary setWidget() component whose render(width) owns the responsive
geometry and whose timer requests a TUI render. Visibility follows agent_start
through agent_settled, so the boat does not flicker between tool calls,
automatic continuations, retries, or compaction inside the same run, and
settle, abort, and failure all reach the same cleanup.

fm-calm.ts stays the sole owner of the presentation choice and the only caller
of setWorkingVisible(); the new lib owns the sprite geometry and widget.

* no-mistakes(review): Guarded Calm-off lifecycle visibility writes; focused tests pass

* no-mistakes(test): Fixed Calm E2E wait to include tmux scrollback

* no-mistakes(document): Document Calm working boat behavior

* no-mistakes: apply CI fixes

* feat(calm): slow the Calm boat, animate blue water, and make the sail directional

The boat now moves one column every 880ms while a bounded fixed-cell water phase
advances every 220ms, so the water ripples several times between boat steps and
the presentation reads as calm. One scheduler drives both clocks and disposing
the widget stops them together; ticks rather than wall-clock timestamps drive
every state change, so tests seek animation time exactly.

Colors are standard ANSI foreground codes instead of theme lookups: blue for
every water cell and yellow for the complete boat, each run closed with a
default-foreground reset so nothing bleeds into padding or later frames. ANSI
bytes never enter geometry, so visible width stays exact.

The mainsail is directional and trails aft of the mast: <| travelling right and
|> travelling left. Direction reverses the moment the boat lands on an endpoint,
so the endpoint frame already shows the new heading and no frame at or after a
bounce shows the previous sail.

* test(calm): wait for the Ctrl+O expansion redraw this block asserts

* docs(calm): record the revised working-presentation verification evidence

* no-mistakes(document): Fix Calm feasibility document EOF whitespace

* fix(dispatch): preflight candidate auth before quota escalation (kunchenguid#1349)

* fix(dispatch): scope candidate authentication to its own surface

A locally expired timestamp in one credential store was reported to the
captain as a sign-out, including for dispatch candidates that never read
that store. A `harness=pi, model=xai/grok-*` candidate authenticates
through Pi's own xAI credential, but the only Grok quota reading
available was gated on the standalone Grok CLI's separate token, whose
expiry clock drifts independently. The always-loaded intake rule then
turned that unreadable quota into a mandatory captain escalation.

Add `bin/fm-auth-preflight.sh` as the deterministic owner of the parts
that must not depend on agent memory: it resolves a tuple's
authentication surface from quota-axi's own emitted auth sources rather
than from a harness or model name, so another harness's CLI can never
gate a candidate that does not use it. A vendor CLI is launched only
when the tuple's own harness owns the credential store under test and a
non-destructive discovery command is registered for it, which today is
`grok models` alone. That probe runs at most once with stdin closed and
a hard timeout, reads its verdict from the first stdout line because the
command exits 0 either way, treats unrecognized output as indeterminate,
and never invokes login, logout, or the interactive TUI. Quota is read
at most twice, and unknown headroom never makes a candidate ineligible
on its own.

Update the dispatch procedure to match: usable authentication with
unmeasurable headroom stays eligible at lower preference with the
unknown disclosed, and stop-and-report is reserved for unresolved
authentication, an unresolved relationship, or malformed configuration.
Record that Grok's `credits.remaining` is a prepaid balance rather than
window headroom.

Gate quota-axi at 0.1.16 in bootstrap, the first build reporting
per-credential auth sources. A stale install previously passed the
presence check silently, which is why a fix published two days earlier
was still not in effect.

Replace the orphaned quota-array-dispatch fixtures, which encoded a
`provider: "xai"` shape the tool never emits and had no consumer, with
fixtures shaped like real 0.1.16 output that the new suite drives the
script against. The suite asserts the verdict and, separately, which
vendor CLIs were launched, so a Pi/xAI candidate reaching the Grok CLI
fails. Map `tests/fixtures/<dir>` to its consuming suite so a fixture
change selects the right tests instead of refusing.

* refactor(bootstrap): give the quota-axi floor one owner

The floor was stated twice - once in bootstrap's gate and once inline in
the auth preflight - so bumping it needed two edits that could drift.
Move it to bin/fm-quota-axi-lib.sh alongside its rationale, matching the
existing tasks-axi library, and derive the comparison from the constant
so the number appears exactly once. Bootstrap turns a failing check into
the operator diagnostic; the preflight refuses to emit an unscoped
verdict. Map the new library to both consuming suites so a bump re-runs
them, and record that any usable source means the surface authenticates.

* no-mistakes(review): Captain: bound quota checks and removed Python dependency

* no-mistakes(review): Captain: enforce conservative headroom and exact preflight retry

* no-mistakes(review): Captain: preserve OpenCode eligibility without auth-surface guessing

* no-mistakes(review): Captain: reject malformed OpenCode model relationships

* no-mistakes(review): Captain: exempt verified unmodeled tuples from intake escalation

* no-mistakes(document): Updated dispatch authentication documentation

* no-mistakes: apply CI fixes

* feat(x-mode): reconcile promised public replies deterministically (kunchenguid#1350)

* feat(x-mode): reconcile promised public replies deterministically

A promised final reply in an X or Discord thread was only kept while the
primary remembered it. Compaction or restart erased that memory, so a typed
public-followup obligation could sit at pending-work after its PR merged and
the original thread never got its reply.

Make the promise durable state instead:

- bin/fm-public-followup-emit.sh reports a typed terminal work result (source
  home, work id, generation, outcome, safe deliverables, bounded public-safe
  text) into the owning home's private inbox. The event id is derived from
  that identity tuple, so duplicate reports and restart replay converge with
  no coordination, and nothing ever parses a free-form done: sentence.
- bin/fm-public-followup.sh registers a commitment, reconciles events through
  tasks-axi public-followup, and runs the idempotent delivery sequence
  (begin-delivery with the payload hash, post, record the posted receipt or a
  typed error) against the stored platform and opaque thread binding. A
  delivery interrupted between post and receipt refuses rather than risk a
  second public reply.
- Session start surfaces unresolved commitments from disk, the existing relay
  poll surfaces a new terminal-result set once, and teardown refuses while
  this home still owes a public reply for that exact work.

tasks-axi public-followup remains the only owner of the obligation state
machine, state/x-context/ the only owner of the private request context, and
fm-x-reply.sh the only thing that posts. Its new optional --receipt-file is
the one addition there, so a caller can record how many messages were sent.

A home that never opted into the myfirstmate relay gates out on a single
[ -f "$FM_HOME/.env" ] test: no tasks-axi call, no backlog or context scan,
no output, and no artifact. Evidence in docs/verification/public-followup.md.

* no-mistakes(review): Hardened public-followup reconciliation and ownership guards

* no-mistakes(review): Hardened typed terminal cleanup and receipt reconciliation

* no-mistakes(review): Automated typed-delivery cleanup and strict backlog validation

* no-mistakes(review): Fail-closed parent resolution and registration-safe delivery

* no-mistakes(review): Harden relay gating and validate secondmate bindings

* no-mistakes(review): Use owner-aware single-gate teardown protection

* no-mistakes(document): Correct public-followup documentation drift

* no-mistakes(lint): Quote done literals to fix ShellCheck warnings

* no-mistakes: apply CI fixes

* feat(bin): replace busy heuristics with semantic lifecycle state (kunchenguid#1327)

* feat: add semantic busy-state contract owner and event writer

One owner (bin/fm-busy-lib.sh) for the captain-approved semantic
busy-state redesign: a per-task gen-bound record written only by
bin/fm-busy-event.sh, per-harness trusted-source classification with
explicit source attribution, busy/idle/unknown/dead semantics where
missing, malformed, stale, or untrusted semantic data is unknown -
never idle - and endpoint death is the only process-level override.
The Grok-only rendered-tail fallback and the standalone-Kimi
verification gate live behind the same classifier.

* feat: arm busy-state at spawn and convert Pi to the semantic extension path

fm-spawn arms the busy-state contract for converted adapters and seeds
busy/fm-spawn (the launch brief is a submitted turn). The Pi/pi-signed
per-task extension now reports agent_start -> busy and agent_settled ->
idle confirmed by ctx.isIdle(), covering auto-retries, compaction
retries, tool loops, and queued continuations, while turn_end stays a
wake notification touch. Teardown removes the new record, gen sidecar,
and lock. Live-verified on Pi 0.82.0: seed -> agent-start busy ->
agent-settled idle with the marker still touched.

* feat: convert OpenCode to the semantic session.status plugin path

The per-task plugin (renamed .opencode/plugins/fm-busy-state.js) now
classifies from OpenCode's semantic session.status events - busy and
retry are active, idle is inactive - latched to the worker's own
session so a subagent child session can never clear the worker's busy
state. The session.idle marker touch stays a wake notification.
Teardown removes both the new and the legacy plugin filenames.
Live-verified on OpenCode 1.17.18 in a real TUI pane: seed ->
session-busy -> session-status-idle.

* feat: convert Claude to the full lifecycle hooks path

The per-task settings.local.json now wires UserPromptSubmit -> busy
and Stop, StopFailure, and SessionEnd -> idle, so API-error and
shutdown turn ends can never strand a busy record; Stop keeps the
turn-ended notification touch. A refused (stale-gen) event exits 0 and
stays silent so Claude's own lifecycle is never broken. Live-verified
on Claude Code 2.1.220: UserPromptSubmit fires for the argv launch
prompt, Stop closes each turn, a mid-stream Escape interrupt fires no
closing hook, and the firstmate-controlled idle/fm-interrupt clear
resolves it.

* feat: gate Codex busy state behind verified semantic sources

The approved contract prefers Codex's app-server turn lifecycle with
capability negotiation and sanctions its lifecycle hooks as the
intermediate. Live probes on codex-cli 0.145.0 show neither is usable
for a pane worker: the app-server daemon is unreachable for a TUI
thread and refuses to start outside the managed standalone install,
and firstmate-written project hooks never fired (interactive with
directory trust granted, and exec, both with
--dangerously-bypass-hook-trust) while global hooks fired in the same
runs. Codex therefore classifies unknown codex-unverified behind an
explicit probe rather than falling back to idle or footer text, and
fm-spawn installs no unverified Codex wiring.

* feat: gate standalone Kimi busy state on live verification

Standalone Kimi has no installed binary here, so per the approved
contract its semantic path stays guarded and it classifies unknown
kimi-unverified rather than idle - and never from its locale-sensitive
moon-phase spinner, which the redesign forbids inventing as a state
source. The gate records the preferred source order (Wire prompt
request lifetime, which brackets a turn and reports cancellation, then
the documented hooks including Interrupt because Stop does not fire on
interrupts) and the exact evidence required to open it. Arming without
wiring would seed a busy record nothing could clear, so both land
together behind the same gate.

* feat: route busy consumers through the contract and drop the global OR

The watcher, crew-state reader, and away-mode daemon now decide busy
state through bin/fm-busy-lib.sh: only an exact busy verdict counts as
working, and unknown never becomes working or a silent idle, so a crew
whose semantic state is missing, malformed, stale, or unverified
surfaces instead of being absorbed. Crew-state reports the producing
source in its detail. The watcher's global OR regex default is gone;
Grok keeps its isolated fallback inside the contract. The daemon's
supervisor-pane reader stays rendered-text - that pane is not a
recorded task - but is now scoped to firstmate's own detected harness
instead of every vendor signature. Secondmate pending-reply
observation is deliberately unchanged and documented as a
delivery-confirmation signal, not task state.

* docs: point busy-state documentation at the single contract owner

Adds a maintainer-architecture section naming bin/fm-busy-lib.sh as
the owner of what busy means, with per-adapter sources, the
unknown-never-idle rule, the endpoint-death override, and the two
rendered-text readers that deliberately stay outside the contract.
Replaces the stale regex-first prose in architecture, tmux-backend,
herdr-backend, and configuration; converts the harness-adapters
per-harness rows from UI signatures to the semantic source each
harness uses; and records the live verification evidence, including
why Codex and standalone Kimi stay unknown.

* fix: arm away-launch signal handlers before acquiring the lifecycle lock

fm_afk_launch_main acquired its lock and only then installed the EXIT,
INT, and TERM traps. A signal arriving in that window terminated the
process by default action and left the lock directory behind, which
blocks the next away-mode launch until the stale-owner reclaim path
clears it. The release helper only removes a lock this process owns,
so the handlers are now armed first. The accompanying test also killed
the child whether or not the lock had appeared and sampled cleanup the
instant wait returned; it now requires the lock, then allows a bounded
settle, so it proves the guarantee instead of racing it.

* test: align fleet, Kimi, lifecycle, and detection suites with the contract

The fleet snapshot and wake-daemon lifecycle fixtures now prove a
working crew through its own semantic busy-state record instead of
rendered pane text, which is what those consumers read. The Kimi
watcher test asserts the approved contract directly: a standalone Kimi
task classifies unknown rather than matching its moon-phase spinner,
while Grok's isolated fallback still classifies only Grok. The
pi-signed detection cases clear ambient harness markers, fixing a
pre-existing failure where the running session's own CLAUDECODE
outranked the fixture's marker.

* fix: stop teardown from deleting a project's own Codex hooks file

An intermediate revision wired Codex through a firstmate-written
<worktree>/.codex/hooks.json, and teardown removed it alongside the
other generated wiring. The Codex wiring was dropped when its probes
came back unverified, so that removal now targets a file firstmate
never creates - and a project may legitimately track its own
.codex/hooks.json, which teardown would then delete from a pooled
worktree.

* fix: keep busy-record parsing from disturbing its sourcing caller

The record parser split fields with set -- under a temporary noglob,
which clobbers a sourcing caller's positional parameters and restores
glob expansion even when the caller had disabled it. The watcher, the
daemon, and the crew-state reader all source this library, so it now
reads fields with read -a, which never globs and never touches caller
state.

* docs: state exactly which Claude hook paths were reproduced live

The busy-state record listed all four wired Claude hooks in the source
column, which could read as a claim that every one fired during the
pass. UserPromptSubmit and Stop did; StopFailure and SessionEnd are
wired from hook names confirmed present in the installed binary, but
the abnormal turn ends they cover were not reproduced.

* test: let reset_fakes own the crew-state busy-text fixture lifecycle

The Grok fallback case set FM_FAKE_BUSY_TEXT and cleared it inline, so
the variable's lifetime was owned by one test rather than by the
shared reset that every other fake already uses.

* no-mistakes(review): Fix semantic busy-state lifecycle races

* no-mistakes(review): Make busy-state retirement idempotent

* no-mistakes(review): Enforce semantic state boundaries for status and injection

* no-mistakes(review): Restore harness-scoped away-mode busy guard

* no-mistakes(document): Refresh semantic busy-state documentation

* no-mistakes: apply CI fixes

* fix: preserve Calm boat continuity across working periods (kunchenguid#1356)

* fix(calm): resume working boat from frozen column across runs

Keep one extension-owned boat animation for the Pi session so settling
freezes column and direction, the next working period resumes there
without hidden-time jumps, and only a fresh session resets to the left edge.

* no-mistakes(review): Freeze Calm boat from last rendered state

* no-mistakes(document): Document Calm boat continuity contract

* fix: restore evidence-based dispatch eligibility (kunchenguid#1358)

* fix(dispatch): judge candidate provider relations instead of rejecting them

Firstmate deterministically dropped supported Pi candidates in the
openai-codex family. bin/fm-auth-preflight.sh resolved a harness=pi tuple's
credential surface by constructing the source id `pi:<model-prefix>`, so
`pi + openai-codex/gpt-5.6-terra` looked for a `pi:openai-codex` source. That
source does not exist, because Pi's Codex family authenticates through the
Codex store quota-axi already lists as `auth-json`/`cli-rpc`. The tuple
returned `eligible=no reason=surface-unresolved` while the Pi catalog listed
the model and the Codex provider reported fresh, usable credentials with 64
effective percent remaining on its all-model scope.

The prefix construction was only ever valid where Pi holds its own credential
(`pi:xai`, `pi:kimi-coding`), which is why every previously configured Pi tuple
resolved and the defect stayed hidden until a Codex-family Pi model was
configured.

Retire dispatch eligibility from deterministic shell. The dispatching first
mate now establishes model support and provider family from each harness's
authoritative catalog, applies quota at the granularity the vendor supplies,
and shows that reasoning. Provider-level and all-model evidence bounds every
model established in that family; a named-model window bounds only its own
model. Missing model-level quota, a missing auth source, unmeasurable headroom,
and unmodeled authentication are disclosed uncertainty. Only concrete
contradictory evidence blocks a candidate.

Replace the preflight with bin/fm-vendor-auth-probe.sh, which keeps the
captain's approved bounded probe envelope without any routing knowledge: it
takes no harness, model, or provider, reads no quota, renders no verdict, and
holds only a fixed-argv safety allowlist. Its behavior suite proves the absent
identity surface, the untouched quota, the uniform exit status, the fixed argv
with stdin closed, and a real bound even when the configured bound is zero.

Also fixed along the way: a zero FM_*_TIMEOUT silently removed the hard bound,
the pinned Grok version had drifted to 0.2.117, and --changed selection refused
outright on any deleted bin/ script.

AGENTS.md section 4 and quota-array-dispatch own the corrected policy,
harness-adapters gets the catalog-responsibility correction, and
docs/verification/dispatch-auth.md records the 2026-07-30 evidence on
Pi 0.82.0, quota-axi 0.1.16, and grok 0.2.117.

* no-mistakes(review): Reject all-zero vendor probe timeouts

* feat(watch): wake firstmate when a monitored PR goes conflicting

An overtaken pull request sat conflicted until someone noticed the
maintainer bot's comment: PR 1284 waited an hour, and PR 1267 was
overtaken six times in a day. Nothing in the armed merge poll reported a
conflict, so the only signal was a long-cadence recheck or a human read.

The poll now reports conflicts on the same validated path as merges. Its
single GitHub request carries state, mergeability, and the head commit
instead of state alone, so a conflicting open pull request emits
"dirty <head>" at no extra forge call. State is decided first and alone,
so merged and closed pull requests keep exactly the result they had
before. GitLab is untouched: plain glab field output carries no conflict
field, and reading one would need the JSON processor firstmate
deliberately does not depend on.

The watcher dedupes by conflict episode, keyed on that head commit, and
its wake carries the pull request URL so the worker can be steered to
rebase without a lookup first. The head is the key because poll silence
is ambiguous - clean, unknown, closed, and every error look identical -
so silence can never clear it, while a changed head does mean the branch
moved and went conflicting again. An untouched conflict re-surfaces no
more often than FM_PR_DIRTY_RESURFACE_SECS. The marker retires with the
rest of the poll artifacts at teardown and on merge.

GitHub can briefly report mergeability as unknown while it recomputes
after a base push. That is silence here and resolves on the following
sweep, rather than adding a retry and a timing dependency to a static
program; GitHub recomputes on the base push, so a sweep arriving minutes
later normally reads a settled value.

Changing the poll's bytes retires every armed check, which the existing
content-based migration already handles: the next --checks-safe run
quarantines the stale copies and rebuilds each poll from the recorded
pr=, so homes see one PR_CHECK_MIGRATION line and lose no armed watch.

* no-mistakes(review): record PR conflict episode only after wake enqueued

* no-mistakes(document): correct stale gh field-selector claim in GitLab watch doc

* no-mistakes(document): rename merge poll to PR poll in armed-check docs

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com>
Co-authored-by: Daniel Kuykendall IV <danielkuykendall23@gmail.com>
Co-authored-by: Trillium Smith <Spiteless@gmail.com>
Co-authored-by: Unknownzed <45267749+Unknownzed@users.noreply.github.com>
Co-authored-by: lhalbert <lucashalbert@users.noreply.github.com>
Co-authored-by: AG <ag@agw3.org>
Co-authored-by: deeto15 <92119640+deeto15@users.noreply.github.com>
sbracewell64 added a commit that referenced this pull request Aug 2, 2026
* feat(bin): add verified pi-signed runtime adapter (kunchenguid#1145)

* feat: add verified pi-signed adapter

* no-mistakes(review): Correct pi-signed maintainer verification date

* no-mistakes(review): Correct remaining pi-signed verification dates

* no-mistakes(review): Preserve authoritative pi-signed runtime identity

* no-mistakes(document): Document pi-signed shared adapter semantics

* no-mistakes: apply CI fixes

* fix(pi): rearm watcher across session transitions (kunchenguid#1166)

* fix(pi): rearm watcher across same-process session transitions

Pi emits session_shutdown for ordinary /new, /resume, and /fork replacement
as well as terminal quit. The primary watcher extension latched a module-level
stopping flag on every shutdown, so a replacement session in the same process
could not arm monitoring until Pi restarted.

Own arm authority per session generation so only the active live generation
may start, stop, or rearm the child. Replacement sessions can arm again without
restarting Pi, stale prior-generation callbacks cannot mutate the active cycle,
and real quit still blocks late rearm.

* no-mistakes(review): Preserve Pi generation isolation and exit cleanup

* no-mistakes(document): Correct Pi watcher transition documentation

* feat: route crew dispatch using quota-window pace (kunchenguid#1172)

* Consume quota-axi pace signals in dispatch profile array selection.

Add quota-array-dispatch as the single owner of the pace-aware candidate
choice, keep AGENTS.md to the intake boundary and load trigger, and cover
the acceptance cases with sanitized schemaVersion 3 fixtures.

* no-mistakes(review): Stop and report genuine quota dispatch ties

* no-mistakes(document): Document quota pace freshness and uncertainty

* fix: adapt Grok Stop continuation and harden endpoint cleanup (kunchenguid#1171)

* fix(grok): adapt Stop continuation to runtime capability

* no-mistakes(review): Reject ambiguous Grok Stop payloads

* no-mistakes(review): Reject duplicate Grok fields and accept spaced tmux sessions

* no-mistakes(review): Enforce exact tmux cleanup selectors

* no-mistakes(test): Fix historical tmux fixture and validate Grok Stop

* no-mistakes: apply CI fixes

* fix: restore stock macOS Bash 3.2 brief scaffolding (kunchenguid#1093)

* fix(brief): make DOD scaffolding parse-safe on stock macOS Bash 3.2

fm-brief.sh built each Definition-of-done block and the not-enabled
Herdr declaration with `VAR=$(cat <<EOF ... EOF)`. On Bash 3.2 (macOS
/bin/bash) the lexer scans for the command substitution's closing `)`
textually and tracks quote state through the heredoc body, so a single
apostrophe, unbalanced quote, or unbalanced paren in that prose breaks
parsing of the whole script. Every ship-brief scaffold (no-mistakes,
direct-PR, local-only) failed with `unexpected EOF while looking for
matching )`. Bash 4+ parses it fine, so the breakage stayed invisible
everywhere except stock macOS.

Replace all four command-substitution heredocs with
`IFS= read -r -d '' VAR <<EOF || true`. That removes the `$(...)`
wrapper and the entire defect class regardless of future prose, and
preserves the variable expansion the direct-PR and local-only bodies
need. `read` keeps the heredoc's trailing newline that `$(...)` used to
strip, so trim one newline to keep every generated brief byte-identical
to prior output.

Guard the structure, not one historical phrase: a new test rejects any
heredoc nested in a command substitution anywhere in fm-brief.sh, where
the old assertion pinned a single apostrophe phrase and so missed the
reintroduction. Extend the stock-macOS Bash CI job from parsing one
script to the whole maintained shell surface (bin/*.sh,
bin/backends/*.sh, tests/*.sh), matching bin/fm-lint.sh's canonical file
set so parse scope and lint scope cannot drift apart.

* no-mistakes(review): Captain: harden Bash structure and inventory guards

* no-mistakes(document): Align stock macOS Bash contributor checks

* no-mistakes(lint): Suppress deliberate SC2016 literal fixture warnings

* test: stabilize tmux teardown conformance baseline (kunchenguid#1209)

* fix(test): pin teardown tmux baseline to historical kill selectors

merge-base HEAD main collapses to HEAD after the exact-selector change
lands on the default branch, so the old teardown fixture was accidentally
exercising current exact targets. Resolve a content-historical permissive
tmux adapter from first-parent history and force that post-squash topology
inside the conformance case so main and feature branches keep the same
old-vs-new contract.

* no-mistakes(lint): Suppress intentional literal-pattern ShellCheck warnings

* docs: slim quota-array-dispatch to the pace selection core (kunchenguid#1197)

Cut the runtime skill to the compact pace-aware selection procedure plus
minimum owner pointers. Keep every distinct decision rule and move expanded
acceptance scenarios to deterministic fixture ownership assertions.

Size: 170/1374/10187 -> 63/544/4068 (about 63%/60%/60% reduction).

* feat(bin): inherit backend config into secondmate homes (kunchenguid#1219)

* Inherit config/backend into secondmate homes with deliberate-override preservation

Add backend to the shared inheritable config allowlist so launch, locked
bootstrap, and config-push converge a primary pin into secondmate homes as each
home local future-spawn default. Track last-inherited bytes in a private state
provenance marker so deliberate per-home overrides survive present and absent
primary convergence, keep --backend and FM_BACKEND stronger, and extend the
existing inheritance tests plus docs and skill claims.

* no-mistakes(review): Preserve equal unprovenanced backend overrides

* no-mistakes(review): Preserve symlink overrides and verify spawn precedence

* no-mistakes(review): Snapshot backend inheritance for consistent provenance

* no-mistakes(review): Simplify backend inheritance to primary-authoritative convergence

* no-mistakes(document): Document inherited backend override preservation

* fix: restore primary-authoritative backend inheritance after document regression

The document step reintroduced provenance and deliberate per-home override
semantics after review had simplified config/backend to plain primary-authoritative
allowlist membership. Restore the primary-always-wins path: present overwrites,
absent removes, no provenance marker, and docs/tests match that contract.

* no-mistakes(review): Add divergent backend precedence regression fixtures

* no-mistakes(document): Document backend inheritance contract

* fix(pi): remove Calm's upper version ceiling (kunchenguid#1226)

* fix(pi): remove Calm's exclusive Pi upper-version ceiling

tests/fm-calm-pi-extension.test.sh gated on a closed PI_COMPAT_VERSIONS
allowlist ("0.81.1 0.82.0") that refused any other installed Pi, and docs
described that range as "supported" rather than verified evidence. The
Calm CHANGELOG shows no API introduced at either version, so there is no
evidence for a real minimum; the presentation adapters already probe the
exact method they patch rather than checking a version.

Replace the allowlist with dated version evidence that never rejects a
newer Pi, and make each presentation adapter degrade independently with
a diagnostic if a future Pi removes its API, instead of the whole Calm
extension failing to load. Rewrite the feasibility doc's "Pi 0.81.1
through 0.82.0" phrasing to state it as verified evidence, not a
ceiling.

* no-mistakes(review): Probe missing Calm adapter exports safely

* no-mistakes(document): Document Calm's unbounded Pi compatibility

* fix(bin): allow session-local todo tools in the subagent guard (kunchenguid#1204)

* fix(guard): allow session-local todo tools in the primary

The delegation-shape guard denied TaskCreate and TaskUpdate because their
normalized names contain the `task` stem. Those tools write only the harness's
session-local todo list, which has no executor: it spawns no agent, allocates
no worktree, registers no schedule, and starts nothing that outlives the
session. That is not the unaccounted work the guard exists to stop, so the stem
match was a false positive, and the deny text told the primary to run
bin/fm-brief.sh and bin/fm-spawn.sh to create a todo entry.

Add a separately-reasoned PLAN_ONLY_TOOLS exact-name exclusion rather than
widening OBSERVE_ONLY_TOOLS, whose documented contract is tools that only
observe or stop existing work. Both lists stay exact-name so neither can widen
by substring.

Tests cover the two allowed names and six near-miss names that a substring or
shortened-stem widening would release; both mutations were watched red.

* no-mistakes(review): drop session-local todo tools from recommended deny list

* no-mistakes: apply CI fixes

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid (kunchenguid#1206)

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid

fm_harness_ancestry_pid() previously returned the first ancestor process
whose command matched a verified harness name. Claude Code's Stop hook
fires as a bg-spare worker several levels below the session's actual
lock-owning claude process (hook shell -> claude bg-spare ->
claude bg-pty-host -> claude -> claude(lock)), so the first match was
the bg-spare worker, not the lock owner. fm_session_lock_owned_by_self()
then never matched state/.lock, and the Claude Stop auto-arm silently
treated its own primary session as an unrelated live owner and never
armed the watcher.

The walk now keeps going past a claude-named match, looking for a still
more ancestral claude-named match, and stops the instant a non-match
follows an already-found match (bounding it to a contiguous run rather
than the literal ancestry top, so an unrelated claude-named process
further up the real process tree is never mistaken for part of this
session's own nested chain). Every other harness keeps the original
first-match-wins behavior, since e.g. Pi's shared signed-wrapper
ancestry actually holds the session at the inner engine pid, not an
outer wrapper pid. Hop limit raised from 8 to 16 to cover the deeper
bg-spare chain.

* no-mistakes(review): Add nested-claude-ancestry regression test; fix nudge doc depth claim

* no-mistakes: apply CI fixes

* fix: conferma l'avvio del watcher su Windows/MSYS (kunchenguid#1212)

* fix: confirm watcher startup on MSYS

* no-mistakes(review): gate MSYS arm ready timeout, cache uname, harden locale test

* no-mistakes(review): validate OpenCode ready timeout, make uname cache internal

* fix(spawn): forward CLAUDE_CONFIG_DIR to claude crewmates (kunchenguid#1195)

* fix(spawn): forward firstmate's CLAUDE_CONFIG_DIR to claude crewmates

Crewmate panes are created by a long-lived tmux/herdr daemon that does not
inherit firstmate's current environment. When firstmate runs under a non-default
CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split), a bare
`claude` in the crewmate pane fell back to the default ~/.claude store and
launched unauthenticated, blocking the crewmate before it could do any work.

fm-spawn now prefixes the claude launch with firstmate's own resolved
CLAUDE_CONFIG_DIR when set, so the crewmate uses the same credential/config
store firstmate is authenticated with. An unset value is the single-store
default and adds no prefix; non-claude harnesses are unaffected.

Adds three tests in fm-spawn-dispatch-profile.test.sh (forwarded-when-set,
omitted-when-unset, non-claude-ignored) and pins CLAUDE_CONFIG_DIR in the test
helper so launch assertions no longer depend on the developer's environment.

* no-mistakes: apply CI fixes

* fix: preserve dispatch identity across authentication checks (kunchenguid#1233)

* fix: preserve dispatch harness identity

* no-mistakes(review): Fix Grok counterfactual tuple validation

* no-mistakes(document): Scope dispatch authentication to selected tuple

* fix: restore dispatch instruction budget

* no-mistakes(review): Scope dispatch authentication after candidate selection

* fix(bin): normalize relative durable paths (kunchenguid#1256)

* fix(bin): handle dash-leading harness process names (#2)

* fix: handle dash-leading harness process names

* no-mistakes(review): Make dash-leading harness regression hermetic

* fix: preserve secondmate reply routes across relative homes

Resolve relative home, data, and state inputs before durable charter generation, and fail when caller-relative directories cannot be resolved.

Use absolute paths at the related spawn, AFK daemon, and X-mode cross-process handoffs so later processes cannot reinterpret them from another working directory.

* no-mistakes(review): Preserve absolute overrides and normalize relative durable paths

* no-mistakes(review): Normalize relative home before deriving durable paths

* no-mistakes(document): Document relative durable-path normalization

* no-mistakes(review): Captain: Ignore inherited CDPATH during relative path normalization

* no-mistakes(lint): Fix empty CDPATH assignments for ShellCheck

* refactor(skills): make Bearings chat-only by default (kunchenguid#1136)

* Add internal status skill

* no-mistakes(document): register /status skill in documentation-audiences inventory

* no-mistakes(lint): replace grep|wc -l with grep -c in status skill test

* test: silence literal status skill patterns

* Refactor bearings default to chat-only

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>

* Clarify follow-up routing during validation (kunchenguid#1277)

* fix: honor concrete approval for project operations (kunchenguid#1272)

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* no-mistakes(review): Align project removal preflight with approved exception

* no-mistakes(document): Align project removal documentation with approved exception

* fix: restore removal test byte-for-byte and preserve the default sentence

tests/fm-instruction-owners.test.sh had been changed to assert different
text; restore it byte-for-byte to origin/main. project-management SKILL.md's
Remove section now keeps the exact default "Never issue a raw removal
command from Firstmate." sentence that test still asserts, immediately
followed by the already-approved captain-operation-or-scope exception, so
the default and the exception both stay explicit and consistent.

* no-mistakes(document): Align project-write boundary documentation

* fix(skills): route new project intake through secondmate scopes (kunchenguid#1275)

* Route project intake through secondmate scopes

* no-mistakes(test): Guard all main-home project registry mutations

* no-mistakes(document): Consolidate secondmate routing documentation

* no-mistakes: apply CI fixes

* Restore new-project routing scope

* no-mistakes(document): Clarify secondmate routing for new-project intake

* no-mistakes: apply CI fixes

* fix: scope validation corrections by accepted behavior (kunchenguid#1281)

* fix: scope validation corrections by accepted behavior

* no-mistakes(review): Classify stale delivery evidence as an autonomous correction

* test: replace source assertions with behavioral coverage (kunchenguid#1282)

* test: remove source-content assertions

* no-mistakes(review): Replace source assertions with runtime behavior coverage

* no-mistakes(review): Isolate Kimi task temp runtime coverage

* no-mistakes(document): Refresh test cleanup documentation

* no-mistakes: apply CI fixes

* fix(tests): reap background processes on every suite ending

tests/fm-watcher-lock.test.sh launches real watchers and arms in the
background but only killed them on its happy path, so a failing assertion
or a timeout(1) kill left them running. The worst case was
test_watch_restart_attaches_to_healthy_peer: when it aborted, its
bin/fm-watch-arm.sh --restart survived, and because an arm launches a
successor whenever its child cycle ends, killing just the watcher brought
another one back. Those orphans kept polling an already-deleted temp home,
inflated the shell count of whatever checkout ran the suite, and polluted
later liveness reads.

tests/lib.sh now owns a reaper: fm_test_reap registers a pid, and
fm_test_cleanup kills it together with the children it had at teardown
time. The tree is snapshotted before the first kill because a dead
parent's children are reparented beyond the reach of a ppid walk, and the
kill is SIGKILL so a catchable signal cannot hand back a fresh successor.
Cleanup is now installed for HUP/INT/TERM as well as EXIT, since a bare
EXIT trap does not run for a signalled shell - the reason a hung case
leaked. Every background launch in the suite registers its pid at launch.

* no-mistakes(review): gate reaper kills by identity and survives ps loss

* no-mistakes(document): document harness reaper coverage in watcher-lock test header

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com>
Co-authored-by: Daniel Kuykendall IV <danielkuykendall23@gmail.com>
Co-authored-by: Trillium Smith <Spiteless@gmail.com>
Co-authored-by: Unknownzed <45267749+Unknownzed@users.noreply.github.com>
Co-authored-by: lhalbert <lucashalbert@users.noreply.github.com>
Co-authored-by: AG <ag@agw3.org>
Co-authored-by: deeto15 <92119640+deeto15@users.noreply.github.com>
sbracewell64 added a commit that referenced this pull request Aug 2, 2026
…nshown herdr wedge alarms (#14)

* feat(bin): add verified pi-signed runtime adapter (kunchenguid#1145)

* feat: add verified pi-signed adapter

* no-mistakes(review): Correct pi-signed maintainer verification date

* no-mistakes(review): Correct remaining pi-signed verification dates

* no-mistakes(review): Preserve authoritative pi-signed runtime identity

* no-mistakes(document): Document pi-signed shared adapter semantics

* no-mistakes: apply CI fixes

* fix(pi): rearm watcher across session transitions (kunchenguid#1166)

* fix(pi): rearm watcher across same-process session transitions

Pi emits session_shutdown for ordinary /new, /resume, and /fork replacement
as well as terminal quit. The primary watcher extension latched a module-level
stopping flag on every shutdown, so a replacement session in the same process
could not arm monitoring until Pi restarted.

Own arm authority per session generation so only the active live generation
may start, stop, or rearm the child. Replacement sessions can arm again without
restarting Pi, stale prior-generation callbacks cannot mutate the active cycle,
and real quit still blocks late rearm.

* no-mistakes(review): Preserve Pi generation isolation and exit cleanup

* no-mistakes(document): Correct Pi watcher transition documentation

* feat: route crew dispatch using quota-window pace (kunchenguid#1172)

* Consume quota-axi pace signals in dispatch profile array selection.

Add quota-array-dispatch as the single owner of the pace-aware candidate
choice, keep AGENTS.md to the intake boundary and load trigger, and cover
the acceptance cases with sanitized schemaVersion 3 fixtures.

* no-mistakes(review): Stop and report genuine quota dispatch ties

* no-mistakes(document): Document quota pace freshness and uncertainty

* fix: adapt Grok Stop continuation and harden endpoint cleanup (kunchenguid#1171)

* fix(grok): adapt Stop continuation to runtime capability

* no-mistakes(review): Reject ambiguous Grok Stop payloads

* no-mistakes(review): Reject duplicate Grok fields and accept spaced tmux sessions

* no-mistakes(review): Enforce exact tmux cleanup selectors

* no-mistakes(test): Fix historical tmux fixture and validate Grok Stop

* no-mistakes: apply CI fixes

* fix: restore stock macOS Bash 3.2 brief scaffolding (kunchenguid#1093)

* fix(brief): make DOD scaffolding parse-safe on stock macOS Bash 3.2

fm-brief.sh built each Definition-of-done block and the not-enabled
Herdr declaration with `VAR=$(cat <<EOF ... EOF)`. On Bash 3.2 (macOS
/bin/bash) the lexer scans for the command substitution's closing `)`
textually and tracks quote state through the heredoc body, so a single
apostrophe, unbalanced quote, or unbalanced paren in that prose breaks
parsing of the whole script. Every ship-brief scaffold (no-mistakes,
direct-PR, local-only) failed with `unexpected EOF while looking for
matching )`. Bash 4+ parses it fine, so the breakage stayed invisible
everywhere except stock macOS.

Replace all four command-substitution heredocs with
`IFS= read -r -d '' VAR <<EOF || true`. That removes the `$(...)`
wrapper and the entire defect class regardless of future prose, and
preserves the variable expansion the direct-PR and local-only bodies
need. `read` keeps the heredoc's trailing newline that `$(...)` used to
strip, so trim one newline to keep every generated brief byte-identical
to prior output.

Guard the structure, not one historical phrase: a new test rejects any
heredoc nested in a command substitution anywhere in fm-brief.sh, where
the old assertion pinned a single apostrophe phrase and so missed the
reintroduction. Extend the stock-macOS Bash CI job from parsing one
script to the whole maintained shell surface (bin/*.sh,
bin/backends/*.sh, tests/*.sh), matching bin/fm-lint.sh's canonical file
set so parse scope and lint scope cannot drift apart.

* no-mistakes(review): Captain: harden Bash structure and inventory guards

* no-mistakes(document): Align stock macOS Bash contributor checks

* no-mistakes(lint): Suppress deliberate SC2016 literal fixture warnings

* test: stabilize tmux teardown conformance baseline (kunchenguid#1209)

* fix(test): pin teardown tmux baseline to historical kill selectors

merge-base HEAD main collapses to HEAD after the exact-selector change
lands on the default branch, so the old teardown fixture was accidentally
exercising current exact targets. Resolve a content-historical permissive
tmux adapter from first-parent history and force that post-squash topology
inside the conformance case so main and feature branches keep the same
old-vs-new contract.

* no-mistakes(lint): Suppress intentional literal-pattern ShellCheck warnings

* docs: slim quota-array-dispatch to the pace selection core (kunchenguid#1197)

Cut the runtime skill to the compact pace-aware selection procedure plus
minimum owner pointers. Keep every distinct decision rule and move expanded
acceptance scenarios to deterministic fixture ownership assertions.

Size: 170/1374/10187 -> 63/544/4068 (about 63%/60%/60% reduction).

* feat(bin): inherit backend config into secondmate homes (kunchenguid#1219)

* Inherit config/backend into secondmate homes with deliberate-override preservation

Add backend to the shared inheritable config allowlist so launch, locked
bootstrap, and config-push converge a primary pin into secondmate homes as each
home local future-spawn default. Track last-inherited bytes in a private state
provenance marker so deliberate per-home overrides survive present and absent
primary convergence, keep --backend and FM_BACKEND stronger, and extend the
existing inheritance tests plus docs and skill claims.

* no-mistakes(review): Preserve equal unprovenanced backend overrides

* no-mistakes(review): Preserve symlink overrides and verify spawn precedence

* no-mistakes(review): Snapshot backend inheritance for consistent provenance

* no-mistakes(review): Simplify backend inheritance to primary-authoritative convergence

* no-mistakes(document): Document inherited backend override preservation

* fix: restore primary-authoritative backend inheritance after document regression

The document step reintroduced provenance and deliberate per-home override
semantics after review had simplified config/backend to plain primary-authoritative
allowlist membership. Restore the primary-always-wins path: present overwrites,
absent removes, no provenance marker, and docs/tests match that contract.

* no-mistakes(review): Add divergent backend precedence regression fixtures

* no-mistakes(document): Document backend inheritance contract

* fix(pi): remove Calm's upper version ceiling (kunchenguid#1226)

* fix(pi): remove Calm's exclusive Pi upper-version ceiling

tests/fm-calm-pi-extension.test.sh gated on a closed PI_COMPAT_VERSIONS
allowlist ("0.81.1 0.82.0") that refused any other installed Pi, and docs
described that range as "supported" rather than verified evidence. The
Calm CHANGELOG shows no API introduced at either version, so there is no
evidence for a real minimum; the presentation adapters already probe the
exact method they patch rather than checking a version.

Replace the allowlist with dated version evidence that never rejects a
newer Pi, and make each presentation adapter degrade independently with
a diagnostic if a future Pi removes its API, instead of the whole Calm
extension failing to load. Rewrite the feasibility doc's "Pi 0.81.1
through 0.82.0" phrasing to state it as verified evidence, not a
ceiling.

* no-mistakes(review): Probe missing Calm adapter exports safely

* no-mistakes(document): Document Calm's unbounded Pi compatibility

* fix(bin): allow session-local todo tools in the subagent guard (kunchenguid#1204)

* fix(guard): allow session-local todo tools in the primary

The delegation-shape guard denied TaskCreate and TaskUpdate because their
normalized names contain the `task` stem. Those tools write only the harness's
session-local todo list, which has no executor: it spawns no agent, allocates
no worktree, registers no schedule, and starts nothing that outlives the
session. That is not the unaccounted work the guard exists to stop, so the stem
match was a false positive, and the deny text told the primary to run
bin/fm-brief.sh and bin/fm-spawn.sh to create a todo entry.

Add a separately-reasoned PLAN_ONLY_TOOLS exact-name exclusion rather than
widening OBSERVE_ONLY_TOOLS, whose documented contract is tools that only
observe or stop existing work. Both lists stay exact-name so neither can widen
by substring.

Tests cover the two allowed names and six near-miss names that a substring or
shortened-stem widening would release; both mutations were watched red.

* no-mistakes(review): drop session-local todo tools from recommended deny list

* no-mistakes: apply CI fixes

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid (kunchenguid#1206)

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid

fm_harness_ancestry_pid() previously returned the first ancestor process
whose command matched a verified harness name. Claude Code's Stop hook
fires as a bg-spare worker several levels below the session's actual
lock-owning claude process (hook shell -> claude bg-spare ->
claude bg-pty-host -> claude -> claude(lock)), so the first match was
the bg-spare worker, not the lock owner. fm_session_lock_owned_by_self()
then never matched state/.lock, and the Claude Stop auto-arm silently
treated its own primary session as an unrelated live owner and never
armed the watcher.

The walk now keeps going past a claude-named match, looking for a still
more ancestral claude-named match, and stops the instant a non-match
follows an already-found match (bounding it to a contiguous run rather
than the literal ancestry top, so an unrelated claude-named process
further up the real process tree is never mistaken for part of this
session's own nested chain). Every other harness keeps the original
first-match-wins behavior, since e.g. Pi's shared signed-wrapper
ancestry actually holds the session at the inner engine pid, not an
outer wrapper pid. Hop limit raised from 8 to 16 to cover the deeper
bg-spare chain.

* no-mistakes(review): Add nested-claude-ancestry regression test; fix nudge doc depth claim

* no-mistakes: apply CI fixes

* fix: conferma l'avvio del watcher su Windows/MSYS (kunchenguid#1212)

* fix: confirm watcher startup on MSYS

* no-mistakes(review): gate MSYS arm ready timeout, cache uname, harden locale test

* no-mistakes(review): validate OpenCode ready timeout, make uname cache internal

* fix(spawn): forward CLAUDE_CONFIG_DIR to claude crewmates (kunchenguid#1195)

* fix(spawn): forward firstmate's CLAUDE_CONFIG_DIR to claude crewmates

Crewmate panes are created by a long-lived tmux/herdr daemon that does not
inherit firstmate's current environment. When firstmate runs under a non-default
CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split), a bare
`claude` in the crewmate pane fell back to the default ~/.claude store and
launched unauthenticated, blocking the crewmate before it could do any work.

fm-spawn now prefixes the claude launch with firstmate's own resolved
CLAUDE_CONFIG_DIR when set, so the crewmate uses the same credential/config
store firstmate is authenticated with. An unset value is the single-store
default and adds no prefix; non-claude harnesses are unaffected.

Adds three tests in fm-spawn-dispatch-profile.test.sh (forwarded-when-set,
omitted-when-unset, non-claude-ignored) and pins CLAUDE_CONFIG_DIR in the test
helper so launch assertions no longer depend on the developer's environment.

* no-mistakes: apply CI fixes

* fix: preserve dispatch identity across authentication checks (kunchenguid#1233)

* fix: preserve dispatch harness identity

* no-mistakes(review): Fix Grok counterfactual tuple validation

* no-mistakes(document): Scope dispatch authentication to selected tuple

* fix: restore dispatch instruction budget

* no-mistakes(review): Scope dispatch authentication after candidate selection

* fix(bin): normalize relative durable paths (kunchenguid#1256)

* fix(bin): handle dash-leading harness process names (#2)

* fix: handle dash-leading harness process names

* no-mistakes(review): Make dash-leading harness regression hermetic

* fix: preserve secondmate reply routes across relative homes

Resolve relative home, data, and state inputs before durable charter generation, and fail when caller-relative directories cannot be resolved.

Use absolute paths at the related spawn, AFK daemon, and X-mode cross-process handoffs so later processes cannot reinterpret them from another working directory.

* no-mistakes(review): Preserve absolute overrides and normalize relative durable paths

* no-mistakes(review): Normalize relative home before deriving durable paths

* no-mistakes(document): Document relative durable-path normalization

* no-mistakes(review): Captain: Ignore inherited CDPATH during relative path normalization

* no-mistakes(lint): Fix empty CDPATH assignments for ShellCheck

* refactor(skills): make Bearings chat-only by default (kunchenguid#1136)

* Add internal status skill

* no-mistakes(document): register /status skill in documentation-audiences inventory

* no-mistakes(lint): replace grep|wc -l with grep -c in status skill test

* test: silence literal status skill patterns

* Refactor bearings default to chat-only

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>

* Clarify follow-up routing during validation (kunchenguid#1277)

* fix: honor concrete approval for project operations (kunchenguid#1272)

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* no-mistakes(review): Align project removal preflight with approved exception

* no-mistakes(document): Align project removal documentation with approved exception

* fix: restore removal test byte-for-byte and preserve the default sentence

tests/fm-instruction-owners.test.sh had been changed to assert different
text; restore it byte-for-byte to origin/main. project-management SKILL.md's
Remove section now keeps the exact default "Never issue a raw removal
command from Firstmate." sentence that test still asserts, immediately
followed by the already-approved captain-operation-or-scope exception, so
the default and the exception both stay explicit and consistent.

* no-mistakes(document): Align project-write boundary documentation

* fix(skills): route new project intake through secondmate scopes (kunchenguid#1275)

* Route project intake through secondmate scopes

* no-mistakes(test): Guard all main-home project registry mutations

* no-mistakes(document): Consolidate secondmate routing documentation

* no-mistakes: apply CI fixes

* Restore new-project routing scope

* no-mistakes(document): Clarify secondmate routing for new-project intake

* no-mistakes: apply CI fixes

* fix: scope validation corrections by accepted behavior (kunchenguid#1281)

* fix: scope validation corrections by accepted behavior

* no-mistakes(review): Classify stale delivery evidence as an autonomous correction

* test: replace source assertions with behavioral coverage (kunchenguid#1282)

* test: remove source-content assertions

* no-mistakes(review): Replace source assertions with runtime behavior coverage

* no-mistakes(review): Isolate Kimi task temp runtime coverage

* no-mistakes(document): Refresh test cleanup documentation

* no-mistakes: apply CI fixes

* fix(composer): read an NBSP-padded empty composer as empty, not pending

Away-mode escalations sat undelivered for ~9.5 hours in each of three
stretches. Root cause, with the reproduction now pinned as a regression test:

TRIGGER. Real Claude Code 2.1.220 pads its EMPTY composer row with U+00A0
NBSP, so the captured row is exactly `❯` + \xc2\xa0. bash's [[:space:]] does
not match U+00A0, so no trim in bin/fm-composer-lib.sh or in any adapter could
remove it; the leading-glyph strip left a lone NBSP behind, and the shared
classifier concluded "real, unsubmitted content remains" -> `pending` on a
genuinely idle pane. It is a stable property of the idle pane, not a race, so
it recurred on every poll indefinitely. The NBSP originates in claude's own
output (it sits inside claude's own colour run; non-claude panes never carry
it), so the defect is reader-independent: both herdr's ANSI reader and tmux
`capture-pane -e` surface it faithfully.

MASK. Only the consumers that require an AFFIRMATIVE `empty` could see it, and
both fail safe rather than loudly: away-mode escalation injection defers on
anything that is not `empty`, and verified submit reports a swallowed Enter.
Every other composer consumer treats `pending` as ordinary. So the wedge
produced deferral, not an error, and no test covered the shape - the \xc2\xa0
byte pair appeared NOWHERE under tests/, which is precisely why all three
9.5-hour delivery failures passed CI.

SYMPTOM. Buffered captain-relevant escalations (decision gates, blockers,
completions) delivered only by the away-mode return catch-up ~9.5h later, and
false "delivery unconfirmed" errors on steers into an idle pane.

FIX (sufficiency). bin/fm-composer-lib.sh's fm_composer_classify_content now
normalizes the non-ASCII blanks a TUI can use as padding before its trims:
U+00A0, U+2007 and U+202F map to an ASCII space, U+200B and U+FEFF are dropped.
That function is the ONE fleet-wide owner of the empty|pending|unknown verdict,
so this covers both ANSI readers and all four adapters (tmux, herdr, Orca,
cmux) at once and cannot drift back into per-adapter copies.

SAFETY PROPERTY. Every character normalized here RENDERS AS BLANK, and nothing
else is touched, so the change can only ever make an OTHERWISE-BLANK row read
as blank. It is impossible for real typed text to become `empty`: a row holding
any visible glyph keeps that glyph byte for byte, NBSP-joined text stays
`pending`, and a bare NBSP-padded dead-shell prompt stays non-empty (it becomes
the documented `unknown` instead of `pending`). Tests assert both directions.

Also hardens the away-mode wedge alarm's herdr channel, which failed in the
same incident. `herdr notification show` exits 0 even when it showed nothing:
with `[ui.toast] delivery` off (herdr's default) it answers
{"shown":false,"reason":"disabled"}. wedge_alarm_via_herdr read exit 0 as
success, so a configured-but-disabled channel produced a healthy log line and
reached nobody. It now parses the payload, treats an explicit "shown":false as
a channel failure and logs the reported reason; a build that reports no
outcome keeps its exit-status verdict.

Tests: the reproduction becomes the regression test. tests/fm-composer-lib.test.sh
and tests/fm-backend-herdr.test.sh gain fixtures carrying the LITERAL \xc2\xa0
byte pair captured from real Claude Code 2.1.220, each paired with a real-text
counterpart so the safety property is asserted, and tests/fm-daemon.test.sh
covers the unshown, shown, and payload-less herdr channel outcomes. All 171
existing composer and herdr tests stay green (175 with the new ones), and the
daemon suite goes 99 -> 101.

* no-mistakes(document): document herdr wedge-alarm delivery verification in channel reference

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com>
Co-authored-by: Daniel Kuykendall IV <danielkuykendall23@gmail.com>
Co-authored-by: Trillium Smith <Spiteless@gmail.com>
Co-authored-by: Unknownzed <45267749+Unknownzed@users.noreply.github.com>
Co-authored-by: lhalbert <lucashalbert@users.noreply.github.com>
Co-authored-by: AG <ag@agw3.org>
Co-authored-by: deeto15 <92119640+deeto15@users.noreply.github.com>
sbracewell64 added a commit that referenced this pull request Aug 3, 2026
…s detection (#20)

* feat(bin): add verified pi-signed runtime adapter (kunchenguid#1145)

* feat: add verified pi-signed adapter

* no-mistakes(review): Correct pi-signed maintainer verification date

* no-mistakes(review): Correct remaining pi-signed verification dates

* no-mistakes(review): Preserve authoritative pi-signed runtime identity

* no-mistakes(document): Document pi-signed shared adapter semantics

* no-mistakes: apply CI fixes

* fix(pi): rearm watcher across session transitions (kunchenguid#1166)

* fix(pi): rearm watcher across same-process session transitions

Pi emits session_shutdown for ordinary /new, /resume, and /fork replacement
as well as terminal quit. The primary watcher extension latched a module-level
stopping flag on every shutdown, so a replacement session in the same process
could not arm monitoring until Pi restarted.

Own arm authority per session generation so only the active live generation
may start, stop, or rearm the child. Replacement sessions can arm again without
restarting Pi, stale prior-generation callbacks cannot mutate the active cycle,
and real quit still blocks late rearm.

* no-mistakes(review): Preserve Pi generation isolation and exit cleanup

* no-mistakes(document): Correct Pi watcher transition documentation

* feat: route crew dispatch using quota-window pace (kunchenguid#1172)

* Consume quota-axi pace signals in dispatch profile array selection.

Add quota-array-dispatch as the single owner of the pace-aware candidate
choice, keep AGENTS.md to the intake boundary and load trigger, and cover
the acceptance cases with sanitized schemaVersion 3 fixtures.

* no-mistakes(review): Stop and report genuine quota dispatch ties

* no-mistakes(document): Document quota pace freshness and uncertainty

* fix: adapt Grok Stop continuation and harden endpoint cleanup (kunchenguid#1171)

* fix(grok): adapt Stop continuation to runtime capability

* no-mistakes(review): Reject ambiguous Grok Stop payloads

* no-mistakes(review): Reject duplicate Grok fields and accept spaced tmux sessions

* no-mistakes(review): Enforce exact tmux cleanup selectors

* no-mistakes(test): Fix historical tmux fixture and validate Grok Stop

* no-mistakes: apply CI fixes

* fix: restore stock macOS Bash 3.2 brief scaffolding (kunchenguid#1093)

* fix(brief): make DOD scaffolding parse-safe on stock macOS Bash 3.2

fm-brief.sh built each Definition-of-done block and the not-enabled
Herdr declaration with `VAR=$(cat <<EOF ... EOF)`. On Bash 3.2 (macOS
/bin/bash) the lexer scans for the command substitution's closing `)`
textually and tracks quote state through the heredoc body, so a single
apostrophe, unbalanced quote, or unbalanced paren in that prose breaks
parsing of the whole script. Every ship-brief scaffold (no-mistakes,
direct-PR, local-only) failed with `unexpected EOF while looking for
matching )`. Bash 4+ parses it fine, so the breakage stayed invisible
everywhere except stock macOS.

Replace all four command-substitution heredocs with
`IFS= read -r -d '' VAR <<EOF || true`. That removes the `$(...)`
wrapper and the entire defect class regardless of future prose, and
preserves the variable expansion the direct-PR and local-only bodies
need. `read` keeps the heredoc's trailing newline that `$(...)` used to
strip, so trim one newline to keep every generated brief byte-identical
to prior output.

Guard the structure, not one historical phrase: a new test rejects any
heredoc nested in a command substitution anywhere in fm-brief.sh, where
the old assertion pinned a single apostrophe phrase and so missed the
reintroduction. Extend the stock-macOS Bash CI job from parsing one
script to the whole maintained shell surface (bin/*.sh,
bin/backends/*.sh, tests/*.sh), matching bin/fm-lint.sh's canonical file
set so parse scope and lint scope cannot drift apart.

* no-mistakes(review): Captain: harden Bash structure and inventory guards

* no-mistakes(document): Align stock macOS Bash contributor checks

* no-mistakes(lint): Suppress deliberate SC2016 literal fixture warnings

* test: stabilize tmux teardown conformance baseline (kunchenguid#1209)

* fix(test): pin teardown tmux baseline to historical kill selectors

merge-base HEAD main collapses to HEAD after the exact-selector change
lands on the default branch, so the old teardown fixture was accidentally
exercising current exact targets. Resolve a content-historical permissive
tmux adapter from first-parent history and force that post-squash topology
inside the conformance case so main and feature branches keep the same
old-vs-new contract.

* no-mistakes(lint): Suppress intentional literal-pattern ShellCheck warnings

* docs: slim quota-array-dispatch to the pace selection core (kunchenguid#1197)

Cut the runtime skill to the compact pace-aware selection procedure plus
minimum owner pointers. Keep every distinct decision rule and move expanded
acceptance scenarios to deterministic fixture ownership assertions.

Size: 170/1374/10187 -> 63/544/4068 (about 63%/60%/60% reduction).

* feat(bin): inherit backend config into secondmate homes (kunchenguid#1219)

* Inherit config/backend into secondmate homes with deliberate-override preservation

Add backend to the shared inheritable config allowlist so launch, locked
bootstrap, and config-push converge a primary pin into secondmate homes as each
home local future-spawn default. Track last-inherited bytes in a private state
provenance marker so deliberate per-home overrides survive present and absent
primary convergence, keep --backend and FM_BACKEND stronger, and extend the
existing inheritance tests plus docs and skill claims.

* no-mistakes(review): Preserve equal unprovenanced backend overrides

* no-mistakes(review): Preserve symlink overrides and verify spawn precedence

* no-mistakes(review): Snapshot backend inheritance for consistent provenance

* no-mistakes(review): Simplify backend inheritance to primary-authoritative convergence

* no-mistakes(document): Document inherited backend override preservation

* fix: restore primary-authoritative backend inheritance after document regression

The document step reintroduced provenance and deliberate per-home override
semantics after review had simplified config/backend to plain primary-authoritative
allowlist membership. Restore the primary-always-wins path: present overwrites,
absent removes, no provenance marker, and docs/tests match that contract.

* no-mistakes(review): Add divergent backend precedence regression fixtures

* no-mistakes(document): Document backend inheritance contract

* fix(pi): remove Calm's upper version ceiling (kunchenguid#1226)

* fix(pi): remove Calm's exclusive Pi upper-version ceiling

tests/fm-calm-pi-extension.test.sh gated on a closed PI_COMPAT_VERSIONS
allowlist ("0.81.1 0.82.0") that refused any other installed Pi, and docs
described that range as "supported" rather than verified evidence. The
Calm CHANGELOG shows no API introduced at either version, so there is no
evidence for a real minimum; the presentation adapters already probe the
exact method they patch rather than checking a version.

Replace the allowlist with dated version evidence that never rejects a
newer Pi, and make each presentation adapter degrade independently with
a diagnostic if a future Pi removes its API, instead of the whole Calm
extension failing to load. Rewrite the feasibility doc's "Pi 0.81.1
through 0.82.0" phrasing to state it as verified evidence, not a
ceiling.

* no-mistakes(review): Probe missing Calm adapter exports safely

* no-mistakes(document): Document Calm's unbounded Pi compatibility

* fix(bin): allow session-local todo tools in the subagent guard (kunchenguid#1204)

* fix(guard): allow session-local todo tools in the primary

The delegation-shape guard denied TaskCreate and TaskUpdate because their
normalized names contain the `task` stem. Those tools write only the harness's
session-local todo list, which has no executor: it spawns no agent, allocates
no worktree, registers no schedule, and starts nothing that outlives the
session. That is not the unaccounted work the guard exists to stop, so the stem
match was a false positive, and the deny text told the primary to run
bin/fm-brief.sh and bin/fm-spawn.sh to create a todo entry.

Add a separately-reasoned PLAN_ONLY_TOOLS exact-name exclusion rather than
widening OBSERVE_ONLY_TOOLS, whose documented contract is tools that only
observe or stop existing work. Both lists stay exact-name so neither can widen
by substring.

Tests cover the two allowed names and six near-miss names that a substring or
shortened-stem widening would release; both mutations were watched red.

* no-mistakes(review): drop session-local todo tools from recommended deny list

* no-mistakes: apply CI fixes

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid (kunchenguid#1206)

* fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid

fm_harness_ancestry_pid() previously returned the first ancestor process
whose command matched a verified harness name. Claude Code's Stop hook
fires as a bg-spare worker several levels below the session's actual
lock-owning claude process (hook shell -> claude bg-spare ->
claude bg-pty-host -> claude -> claude(lock)), so the first match was
the bg-spare worker, not the lock owner. fm_session_lock_owned_by_self()
then never matched state/.lock, and the Claude Stop auto-arm silently
treated its own primary session as an unrelated live owner and never
armed the watcher.

The walk now keeps going past a claude-named match, looking for a still
more ancestral claude-named match, and stops the instant a non-match
follows an already-found match (bounding it to a contiguous run rather
than the literal ancestry top, so an unrelated claude-named process
further up the real process tree is never mistaken for part of this
session's own nested chain). Every other harness keeps the original
first-match-wins behavior, since e.g. Pi's shared signed-wrapper
ancestry actually holds the session at the inner engine pid, not an
outer wrapper pid. Hop limit raised from 8 to 16 to cover the deeper
bg-spare chain.

* no-mistakes(review): Add nested-claude-ancestry regression test; fix nudge doc depth claim

* no-mistakes: apply CI fixes

* fix: conferma l'avvio del watcher su Windows/MSYS (kunchenguid#1212)

* fix: confirm watcher startup on MSYS

* no-mistakes(review): gate MSYS arm ready timeout, cache uname, harden locale test

* no-mistakes(review): validate OpenCode ready timeout, make uname cache internal

* fix(spawn): forward CLAUDE_CONFIG_DIR to claude crewmates (kunchenguid#1195)

* fix(spawn): forward firstmate's CLAUDE_CONFIG_DIR to claude crewmates

Crewmate panes are created by a long-lived tmux/herdr daemon that does not
inherit firstmate's current environment. When firstmate runs under a non-default
CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split), a bare
`claude` in the crewmate pane fell back to the default ~/.claude store and
launched unauthenticated, blocking the crewmate before it could do any work.

fm-spawn now prefixes the claude launch with firstmate's own resolved
CLAUDE_CONFIG_DIR when set, so the crewmate uses the same credential/config
store firstmate is authenticated with. An unset value is the single-store
default and adds no prefix; non-claude harnesses are unaffected.

Adds three tests in fm-spawn-dispatch-profile.test.sh (forwarded-when-set,
omitted-when-unset, non-claude-ignored) and pins CLAUDE_CONFIG_DIR in the test
helper so launch assertions no longer depend on the developer's environment.

* no-mistakes: apply CI fixes

* fix: preserve dispatch identity across authentication checks (kunchenguid#1233)

* fix: preserve dispatch harness identity

* no-mistakes(review): Fix Grok counterfactual tuple validation

* no-mistakes(document): Scope dispatch authentication to selected tuple

* fix: restore dispatch instruction budget

* no-mistakes(review): Scope dispatch authentication after candidate selection

* fix(bin): normalize relative durable paths (kunchenguid#1256)

* fix(bin): handle dash-leading harness process names (#2)

* fix: handle dash-leading harness process names

* no-mistakes(review): Make dash-leading harness regression hermetic

* fix: preserve secondmate reply routes across relative homes

Resolve relative home, data, and state inputs before durable charter generation, and fail when caller-relative directories cannot be resolved.

Use absolute paths at the related spawn, AFK daemon, and X-mode cross-process handoffs so later processes cannot reinterpret them from another working directory.

* no-mistakes(review): Preserve absolute overrides and normalize relative durable paths

* no-mistakes(review): Normalize relative home before deriving durable paths

* no-mistakes(document): Document relative durable-path normalization

* no-mistakes(review): Captain: Ignore inherited CDPATH during relative path normalization

* no-mistakes(lint): Fix empty CDPATH assignments for ShellCheck

* refactor(skills): make Bearings chat-only by default (kunchenguid#1136)

* Add internal status skill

* no-mistakes(document): register /status skill in documentation-audiences inventory

* no-mistakes(lint): replace grep|wc -l with grep -c in status skill test

* test: silence literal status skill patterns

* Refactor bearings default to chat-only

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>

* Clarify follow-up routing during validation (kunchenguid#1277)

* fix: honor concrete approval for project operations (kunchenguid#1272)

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* docs: add captain-approved project operation exception to hard rule 1

Firstmate stays read-only over projects by default, but when the captain
clearly approves a concrete project operation and scope in the moment,
firstmate may perform exactly that approved operation with its own tools.
The approval is never inferred, broadened, or standing, and it does not
relax the existing force, discard, unlanded-work, or merge-authority
boundaries.

* no-mistakes(review): Clarify captain-approved project operation boundaries

* no-mistakes(document): Clarify captain-approved project operation scope

* docs: cover directories and preserve the operation-or-scope alternative

Widen the captain-approved project operation exception in AGENTS.md to
files or directories, and restore the explicit operation-or-scope
alternative that a prior pipeline auto-fix had collapsed into "and".

Rework project-management SKILL.md's Remove section, which previously
told firstmate to refuse project removal until a guarded helper existed;
that helper was never built, so the text directly contradicted the new
instruction-only exception. It now points at the exception plus the
existing removal preflight it still requires unchanged.

Update the one instruction-owners test assertion that hard-coded the
sentence removed above, so the suite tracks current, not obsolete, text.

* no-mistakes(review): Align project removal preflight with approved exception

* no-mistakes(document): Align project removal documentation with approved exception

* fix: restore removal test byte-for-byte and preserve the default sentence

tests/fm-instruction-owners.test.sh had been changed to assert different
text; restore it byte-for-byte to origin/main. project-management SKILL.md's
Remove section now keeps the exact default "Never issue a raw removal
command from Firstmate." sentence that test still asserts, immediately
followed by the already-approved captain-operation-or-scope exception, so
the default and the exception both stay explicit and consistent.

* no-mistakes(document): Align project-write boundary documentation

* fix(skills): route new project intake through secondmate scopes (kunchenguid#1275)

* Route project intake through secondmate scopes

* no-mistakes(test): Guard all main-home project registry mutations

* no-mistakes(document): Consolidate secondmate routing documentation

* no-mistakes: apply CI fixes

* Restore new-project routing scope

* no-mistakes(document): Clarify secondmate routing for new-project intake

* no-mistakes: apply CI fixes

* fix: scope validation corrections by accepted behavior (kunchenguid#1281)

* fix: scope validation corrections by accepted behavior

* no-mistakes(review): Classify stale delivery evidence as an autonomous correction

* test: replace source assertions with behavioral coverage (kunchenguid#1282)

* test: remove source-content assertions

* no-mistakes(review): Replace source assertions with runtime behavior coverage

* no-mistakes(review): Isolate Kimi task temp runtime coverage

* no-mistakes(document): Refresh test cleanup documentation

* no-mistakes: apply CI fixes

* fix(watch): escalate busy workers with no completed turn (kunchenguid#1286)

* fix(watch): bound how long a busy pane may run with no completed turn

A busy pane (backend busy state or the harness's rendered footer) was
unconditional, unbounded proof of liveness in every escalation path, so a
hung foreground tool call behind a busy signature could run for hours
undetected (2026-07 hibit-agent-focus-nonsteal-r1 incident: a catastrophic-
backtracking regex hung one bash call for 25h behind an unchanging
"Working..." footer).

FM_BUSY_TURN_MAX_SECS (default 3600s) now bounds how long a busy pane may
run with no completed turn (state/<id>.turn-ended, or its spawn record
before any turn has completed). Past the bound, busy_turn_over_age routes
the pane through the existing wedge_timer_check, reusing the identical
stale reason, escalation counter, and demand-deep-inspection marker for
human inspection only - never an automatic interrupt, signal, or restart
of the worker or its tool process. A completed turn resets the age.

Reproduced end-to-end against the real installed Pi TUI: a foreground
`sleep 999999` bash call with no timeout renders the actual busy footer,
and two captures ~15s apart show the elapsed counter changing the pane
hash while the same turn stays unfinished. Running the pre-fix watcher
against the real captures showed it never starts a wedge timer no matter
how long the pane stays busy; the fixed watcher starts and escalates the
timer through the same mechanism, while the real hung process remained
untouched and alive throughout.

* no-mistakes(review): fix: parse enriched AFK stale reasons

* no-mistakes(review): fix: preserve enriched wedges during AFK supervision

* no-mistakes(review): fix: route all enriched AFK wedges

* no-mistakes(document): Clarify busy-turn age supervision documentation

* fix(gitignore): ignore config/ as a directory, not by exact filename (kunchenguid#1261)

A name-by-name list of config/ entries silently stops ignoring any new or
home-local file placed there, which makes the working tree read as dirty and
blocks guarded sync paths that refuse to touch a dirty home. AGENTS.md
already documents config/ as captain-private and gitignored as a category;
this makes .gitignore match that contract.

* fix(tests): replace source-content .gitignore assertion with behavioral coverage (kunchenguid#1304)

The second assertion in fm-gitignore-config.test.sh (added by kunchenguid#1261) greps
.gitignore for a specific spelling of the config/ ignore pattern. It fails
on a semantically equivalent pattern like config/** and does not prove Git
actually ignores anything, per the completed source-content-test audit.

Replace it with a real git check-ignore control test on a generated
unrelated path, and strengthen the existing directory-coverage test with
generated unpredictable direct and nested config/ paths.

* feat: bound and consolidate startup memory during stow (kunchenguid#1303)

* Add bounded startup memory curation

* no-mistakes(review): Record reproducible stow verification evidence

* no-mistakes(review): Validate inherited secondmate stow evidence

* no-mistakes(document): Document editable startup-memory budget propagation

* fix(herdr): place workers in the launching workspace (kunchenguid#1328)

* fix(herdr): place workers in the launching agent's exact workspace

Herdr enforces no workspace-label uniqueness, and spawn resolved its
container by taking the FIRST workspace whose label matched the home
label. With two workspaces both labeled "firstmate", a worker launched
from the second one was created in the first, so it appeared in a
different space than the Firstmate the captain was watching.

Reproduced end to end on Herdr 0.7.5 protocol 17 by running the real
bin/fm-spawn.sh inside a launcher pane in the second "firstmate"
workspace: the worker landed in w1 while its launcher was in w2, with an
unrelated third workspace focused throughout, which also rules out any
dependence on the focused workspace.

Placement now binds to the launching process's own Herdr identity. Herdr
injects HERDR_PANE_ID, HERDR_SESSION, and HERDR_SOCKET_PATH into every
process it manages a pane for, and fm_backend_herdr_launcher_identity
resolves that pane's current owning tab and workspace live from Herdr,
cross-checking the pane against its tab and confirming the workspace
exists exactly once in the session. The injected HERDR_TAB_ID and
HERDR_WORKSPACE_ID are creation-time snapshots and are deliberately not
read as current identity. Labels are no longer placement authority.

A claimed parent identity that is unreadable, contradictory, stale, or
from another named session or Herdr server stops the spawn before any
worker endpoint exists, rather than degrading to a label search. A
launcher with no Herdr ancestry has no workspace to inherit and keeps
the per-home labeled container, which must now resolve to exactly one
workspace; two same-labeled candidates refuse instead of adopting
either. A --secondmate launch keeps standing up that home's own
workspace by design.

With presentation spaces enabled, the projected child is created and
bound under that same exact parent and anchors its ordering on it, so a
duplicated home label no longer makes the layout ambiguous. Projection,
focus restoration, restart binding, and quarantine rules are unchanged,
and children are never collapsed into the parent. tmux, Zellij, cmux,
Orca, and the away-mode daemon terminal were each inspected and are not
affected: none resolves a container by searching mutable labels.

tests/fm-backend-herdr-launcher-workspace-e2e.test.sh drives the real
spawn and teardown against an isolated Herdr lab, with its headline case
running fm-spawn.sh inside a real Herdr pane so the identity comes from
Herdr's own injection. The refusal matrix and the ordering anchor are
covered deterministically in tests/fm-backend-herdr.test.sh.

Eight existing real-Herdr suites inherited the developer terminal's own
Herdr pane into their isolated lab sessions, which the new cross-session
check correctly refuses. tests/herdr-test-safety.sh now owns
herdr_forget_inherited_pane and those suites call it, so what they assert
no longer depends on where they were launched from.

Two unrelated fixes found along the way. tests/fm-secondmate-harness.test.sh
had the same class of environment leak through CLAUDECODE, which outranks
PI_CODING_AGENT in bin/fm-harness.sh and made its pi-signed ancestry case
resolve "claude" whenever the suite ran inside Claude Code. And
fm-spawn.sh's usage() printed a fixed line range that had already been
truncating its own help mid-sentence.

* no-mistakes(review): Enforce exact Herdr launcher and projection identity

* no-mistakes(document): Document exact Herdr launcher workspace placement

* fix(calm): refine Calm working boat animation (kunchenguid#1339)

* feat(calm): replace Pi's working row with an animated ship while Calm is on

While Calm is active and one logical agent run is under way, Calm now hides
Pi's built-in working row and renders a small two-row SSHHIP-derived boat in
its place. When Calm is off, Pi's stock working row is left untouched.

The presentation uses only public Pi extension API: setWorkingVisible(false)
plus a temporary setWidget() component whose render(width) owns the responsive
geometry and whose timer requests a TUI render. Visibility follows agent_start
through agent_settled, so the boat does not flicker between tool calls,
automatic continuations, retries, or compaction inside the same run, and
settle, abort, and failure all reach the same cleanup.

fm-calm.ts stays the sole owner of the presentation choice and the only caller
of setWorkingVisible(); the new lib owns the sprite geometry and widget.

* no-mistakes(review): Guarded Calm-off lifecycle visibility writes; focused tests pass

* no-mistakes(test): Fixed Calm E2E wait to include tmux scrollback

* no-mistakes(document): Document Calm working boat behavior

* no-mistakes: apply CI fixes

* feat(calm): slow the Calm boat, animate blue water, and make the sail directional

The boat now moves one column every 880ms while a bounded fixed-cell water phase
advances every 220ms, so the water ripples several times between boat steps and
the presentation reads as calm. One scheduler drives both clocks and disposing
the widget stops them together; ticks rather than wall-clock timestamps drive
every state change, so tests seek animation time exactly.

Colors are standard ANSI foreground codes instead of theme lookups: blue for
every water cell and yellow for the complete boat, each run closed with a
default-foreground reset so nothing bleeds into padding or later frames. ANSI
bytes never enter geometry, so visible width stays exact.

The mainsail is directional and trails aft of the mast: <| travelling right and
|> travelling left. Direction reverses the moment the boat lands on an endpoint,
so the endpoint frame already shows the new heading and no frame at or after a
bounce shows the previous sail.

* test(calm): wait for the Ctrl+O expansion redraw this block asserts

* docs(calm): record the revised working-presentation verification evidence

* no-mistakes(document): Fix Calm feasibility document EOF whitespace

* fix(dispatch): preflight candidate auth before quota escalation (kunchenguid#1349)

* fix(dispatch): scope candidate authentication to its own surface

A locally expired timestamp in one credential store was reported to the
captain as a sign-out, including for dispatch candidates that never read
that store. A `harness=pi, model=xai/grok-*` candidate authenticates
through Pi's own xAI credential, but the only Grok quota reading
available was gated on the standalone Grok CLI's separate token, whose
expiry clock drifts independently. The always-loaded intake rule then
turned that unreadable quota into a mandatory captain escalation.

Add `bin/fm-auth-preflight.sh` as the deterministic owner of the parts
that must not depend on agent memory: it resolves a tuple's
authentication surface from quota-axi's own emitted auth sources rather
than from a harness or model name, so another harness's CLI can never
gate a candidate that does not use it. A vendor CLI is launched only
when the tuple's own harness owns the credential store under test and a
non-destructive discovery command is registered for it, which today is
`grok models` alone. That probe runs at most once with stdin closed and
a hard timeout, reads its verdict from the first stdout line because the
command exits 0 either way, treats unrecognized output as indeterminate,
and never invokes login, logout, or the interactive TUI. Quota is read
at most twice, and unknown headroom never makes a candidate ineligible
on its own.

Update the dispatch procedure to match: usable authentication with
unmeasurable headroom stays eligible at lower preference with the
unknown disclosed, and stop-and-report is reserved for unresolved
authentication, an unresolved relationship, or malformed configuration.
Record that Grok's `credits.remaining` is a prepaid balance rather than
window headroom.

Gate quota-axi at 0.1.16 in bootstrap, the first build reporting
per-credential auth sources. A stale install previously passed the
presence check silently, which is why a fix published two days earlier
was still not in effect.

Replace the orphaned quota-array-dispatch fixtures, which encoded a
`provider: "xai"` shape the tool never emits and had no consumer, with
fixtures shaped like real 0.1.16 output that the new suite drives the
script against. The suite asserts the verdict and, separately, which
vendor CLIs were launched, so a Pi/xAI candidate reaching the Grok CLI
fails. Map `tests/fixtures/<dir>` to its consuming suite so a fixture
change selects the right tests instead of refusing.

* refactor(bootstrap): give the quota-axi floor one owner

The floor was stated twice - once in bootstrap's gate and once inline in
the auth preflight - so bumping it needed two edits that could drift.
Move it to bin/fm-quota-axi-lib.sh alongside its rationale, matching the
existing tasks-axi library, and derive the comparison from the constant
so the number appears exactly once. Bootstrap turns a failing check into
the operator diagnostic; the preflight refuses to emit an unscoped
verdict. Map the new library to both consuming suites so a bump re-runs
them, and record that any usable source means the surface authenticates.

* no-mistakes(review): Captain: bound quota checks and removed Python dependency

* no-mistakes(review): Captain: enforce conservative headroom and exact preflight retry

* no-mistakes(review): Captain: preserve OpenCode eligibility without auth-surface guessing

* no-mistakes(review): Captain: reject malformed OpenCode model relationships

* no-mistakes(review): Captain: exempt verified unmodeled tuples from intake escalation

* no-mistakes(document): Updated dispatch authentication documentation

* no-mistakes: apply CI fixes

* feat(x-mode): reconcile promised public replies deterministically (kunchenguid#1350)

* feat(x-mode): reconcile promised public replies deterministically

A promised final reply in an X or Discord thread was only kept while the
primary remembered it. Compaction or restart erased that memory, so a typed
public-followup obligation could sit at pending-work after its PR merged and
the original thread never got its reply.

Make the promise durable state instead:

- bin/fm-public-followup-emit.sh reports a typed terminal work result (source
  home, work id, generation, outcome, safe deliverables, bounded public-safe
  text) into the owning home's private inbox. The event id is derived from
  that identity tuple, so duplicate reports and restart replay converge with
  no coordination, and nothing ever parses a free-form done: sentence.
- bin/fm-public-followup.sh registers a commitment, reconciles events through
  tasks-axi public-followup, and runs the idempotent delivery sequence
  (begin-delivery with the payload hash, post, record the posted receipt or a
  typed error) against the stored platform and opaque thread binding. A
  delivery interrupted between post and receipt refuses rather than risk a
  second public reply.
- Session start surfaces unresolved commitments from disk, the existing relay
  poll surfaces a new terminal-result set once, and teardown refuses while
  this home still owes a public reply for that exact work.

tasks-axi public-followup remains the only owner of the obligation state
machine, state/x-context/ the only owner of the private request context, and
fm-x-reply.sh the only thing that posts. Its new optional --receipt-file is
the one addition there, so a caller can record how many messages were sent.

A home that never opted into the myfirstmate relay gates out on a single
[ -f "$FM_HOME/.env" ] test: no tasks-axi call, no backlog or context scan,
no output, and no artifact. Evidence in docs/verification/public-followup.md.

* no-mistakes(review): Hardened public-followup reconciliation and ownership guards

* no-mistakes(review): Hardened typed terminal cleanup and receipt reconciliation

* no-mistakes(review): Automated typed-delivery cleanup and strict backlog validation

* no-mistakes(review): Fail-closed parent resolution and registration-safe delivery

* no-mistakes(review): Harden relay gating and validate secondmate bindings

* no-mistakes(review): Use owner-aware single-gate teardown protection

* no-mistakes(document): Correct public-followup documentation drift

* no-mistakes(lint): Quote done literals to fix ShellCheck warnings

* no-mistakes: apply CI fixes

* feat(bin): replace busy heuristics with semantic lifecycle state (kunchenguid#1327)

* feat: add semantic busy-state contract owner and event writer

One owner (bin/fm-busy-lib.sh) for the captain-approved semantic
busy-state redesign: a per-task gen-bound record written only by
bin/fm-busy-event.sh, per-harness trusted-source classification with
explicit source attribution, busy/idle/unknown/dead semantics where
missing, malformed, stale, or untrusted semantic data is unknown -
never idle - and endpoint death is the only process-level override.
The Grok-only rendered-tail fallback and the standalone-Kimi
verification gate live behind the same classifier.

* feat: arm busy-state at spawn and convert Pi to the semantic extension path

fm-spawn arms the busy-state contract for converted adapters and seeds
busy/fm-spawn (the launch brief is a submitted turn). The Pi/pi-signed
per-task extension now reports agent_start -> busy and agent_settled ->
idle confirmed by ctx.isIdle(), covering auto-retries, compaction
retries, tool loops, and queued continuations, while turn_end stays a
wake notification touch. Teardown removes the new record, gen sidecar,
and lock. Live-verified on Pi 0.82.0: seed -> agent-start busy ->
agent-settled idle with the marker still touched.

* feat: convert OpenCode to the semantic session.status plugin path

The per-task plugin (renamed .opencode/plugins/fm-busy-state.js) now
classifies from OpenCode's semantic session.status events - busy and
retry are active, idle is inactive - latched to the worker's own
session so a subagent child session can never clear the worker's busy
state. The session.idle marker touch stays a wake notification.
Teardown removes both the new and the legacy plugin filenames.
Live-verified on OpenCode 1.17.18 in a real TUI pane: seed ->
session-busy -> session-status-idle.

* feat: convert Claude to the full lifecycle hooks path

The per-task settings.local.json now wires UserPromptSubmit -> busy
and Stop, StopFailure, and SessionEnd -> idle, so API-error and
shutdown turn ends can never strand a busy record; Stop keeps the
turn-ended notification touch. A refused (stale-gen) event exits 0 and
stays silent so Claude's own lifecycle is never broken. Live-verified
on Claude Code 2.1.220: UserPromptSubmit fires for the argv launch
prompt, Stop closes each turn, a mid-stream Escape interrupt fires no
closing hook, and the firstmate-controlled idle/fm-interrupt clear
resolves it.

* feat: gate Codex busy state behind verified semantic sources

The approved contract prefers Codex's app-server turn lifecycle with
capability negotiation and sanctions its lifecycle hooks as the
intermediate. Live probes on codex-cli 0.145.0 show neither is usable
for a pane worker: the app-server daemon is unreachable for a TUI
thread and refuses to start outside the managed standalone install,
and firstmate-written project hooks never fired (interactive with
directory trust granted, and exec, both with
--dangerously-bypass-hook-trust) while global hooks fired in the same
runs. Codex therefore classifies unknown codex-unverified behind an
explicit probe rather than falling back to idle or footer text, and
fm-spawn installs no unverified Codex wiring.

* feat: gate standalone Kimi busy state on live verification

Standalone Kimi has no installed binary here, so per the approved
contract its semantic path stays guarded and it classifies unknown
kimi-unverified rather than idle - and never from its locale-sensitive
moon-phase spinner, which the redesign forbids inventing as a state
source. The gate records the preferred source order (Wire prompt
request lifetime, which brackets a turn and reports cancellation, then
the documented hooks including Interrupt because Stop does not fire on
interrupts) and the exact evidence required to open it. Arming without
wiring would seed a busy record nothing could clear, so both land
together behind the same gate.

* feat: route busy consumers through the contract and drop the global OR

The watcher, crew-state reader, and away-mode daemon now decide busy
state through bin/fm-busy-lib.sh: only an exact busy verdict counts as
working, and unknown never becomes working or a silent idle, so a crew
whose semantic state is missing, malformed, stale, or unverified
surfaces instead of being absorbed. Crew-state reports the producing
source in its detail. The watcher's global OR regex default is gone;
Grok keeps its isolated fallback inside the contract. The daemon's
supervisor-pane reader stays rendered-text - that pane is not a
recorded task - but is now scoped to firstmate's own detected harness
instead of every vendor signature. Secondmate pending-reply
observation is deliberately unchanged and documented as a
delivery-confirmation signal, not task state.

* docs: point busy-state documentation at the single contract owner

Adds a maintainer-architecture section naming bin/fm-busy-lib.sh as
the owner of what busy means, with per-adapter sources, the
unknown-never-idle rule, the endpoint-death override, and the two
rendered-text readers that deliberately stay outside the contract.
Replaces the stale regex-first prose in architecture, tmux-backend,
herdr-backend, and configuration; converts the harness-adapters
per-harness rows from UI signatures to the semantic source each
harness uses; and records the live verification evidence, including
why Codex and standalone Kimi stay unknown.

* fix: arm away-launch signal handlers before acquiring the lifecycle lock

fm_afk_launch_main acquired its lock and only then installed the EXIT,
INT, and TERM traps. A signal arriving in that window terminated the
process by default action and left the lock directory behind, which
blocks the next away-mode launch until the stale-owner reclaim path
clears it. The release helper only removes a lock this process owns,
so the handlers are now armed first. The accompanying test also killed
the child whether or not the lock had appeared and sampled cleanup the
instant wait returned; it now requires the lock, then allows a bounded
settle, so it proves the guarantee instead of racing it.

* test: align fleet, Kimi, lifecycle, and detection suites with the contract

The fleet snapshot and wake-daemon lifecycle fixtures now prove a
working crew through its own semantic busy-state record instead of
rendered pane text, which is what those consumers read. The Kimi
watcher test asserts the approved contract directly: a standalone Kimi
task classifies unknown rather than matching its moon-phase spinner,
while Grok's isolated fallback still classifies only Grok. The
pi-signed detection cases clear ambient harness markers, fixing a
pre-existing failure where the running session's own CLAUDECODE
outranked the fixture's marker.

* fix: stop teardown from deleting a project's own Codex hooks file

An intermediate revision wired Codex through a firstmate-written
<worktree>/.codex/hooks.json, and teardown removed it alongside the
other generated wiring. The Codex wiring was dropped when its probes
came back unverified, so that removal now targets a file firstmate
never creates - and a project may legitimately track its own
.codex/hooks.json, which teardown would then delete from a pooled
worktree.

* fix: keep busy-record parsing from disturbing its sourcing caller

The record parser split fields with set -- under a temporary noglob,
which clobbers a sourcing caller's positional parameters and restores
glob expansion even when the caller had disabled it. The watcher, the
daemon, and the crew-state reader all source this library, so it now
reads fields with read -a, which never globs and never touches caller
state.

* docs: state exactly which Claude hook paths were reproduced live

The busy-state record listed all four wired Claude hooks in the source
column, which could read as a claim that every one fired during the
pass. UserPromptSubmit and Stop did; StopFailure and SessionEnd are
wired from hook names confirmed present in the installed binary, but
the abnormal turn ends they cover were not reproduced.

* test: let reset_fakes own the crew-state busy-text fixture lifecycle

The Grok fallback case set FM_FAKE_BUSY_TEXT and cleared it inline, so
the variable's lifetime was owned by one test rather than by the
shared reset that every other fake already uses.

* no-mistakes(review): Fix semantic busy-state lifecycle races

* no-mistakes(review): Make busy-state retirement idempotent

* no-mistakes(review): Enforce semantic state boundaries for status and injection

* no-mistakes(review): Restore harness-scoped away-mode busy guard

* no-mistakes(document): Refresh semantic busy-state documentation

* no-mistakes: apply CI fixes

* fix: preserve Calm boat continuity across working periods (kunchenguid#1356)

* fix(calm): resume working boat from frozen column across runs

Keep one extension-owned boat animation for the Pi session so settling
freezes column and direction, the next working period resumes there
without hidden-time jumps, and only a fresh session resets to the left edge.

* no-mistakes(review): Freeze Calm boat from last rendered state

* no-mistakes(document): Document Calm boat continuity contract

* fix: restore evidence-based dispatch eligibility (kunchenguid#1358)

* fix(dispatch): judge candidate provider relations instead of rejecting them

Firstmate deterministically dropped supported Pi candidates in the
openai-codex family. bin/fm-auth-preflight.sh resolved a harness=pi tuple's
credential surface by constructing the source id `pi:<model-prefix>`, so
`pi + openai-codex/gpt-5.6-terra` looked for a `pi:openai-codex` source. That
source does not exist, because Pi's Codex family authenticates through the
Codex store quota-axi already lists as `auth-json`/`cli-rpc`. The tuple
returned `eligible=no reason=surface-unresolved` while the Pi catalog listed
the model and the Codex provider reported fresh, usable credentials with 64
effective percent remaining on its all-model scope.

The prefix construction was only ever valid where Pi holds its own credential
(`pi:xai`, `pi:kimi-coding`), which is why every previously configured Pi tuple
resolved and the defect stayed hidden until a Codex-family Pi model was
configured.

Retire dispatch eligibility from deterministic shell. The dispatching first
mate now establishes model support and provider family from each harness's
authoritative catalog, applies quota at the granularity the vendor supplies,
and shows that reasoning. Provider-level and all-model evidence bounds every
model established in that family; a named-model window bounds only its own
model. Missing model-level quota, a missing auth source, unmeasurable headroom,
and unmodeled authentication are disclosed uncertainty. Only concrete
contradictory evidence blocks a candidate.

Replace the preflight with bin/fm-vendor-auth-probe.sh, which keeps the
captain's approved bounded probe envelope without any routing knowledge: it
takes no harness, model, or provider, reads no quota, renders no verdict, and
holds only a fixed-argv safety allowlist. Its behavior suite proves the absent
identity surface, the untouched quota, the uniform exit status, the fixed argv
with stdin closed, and a real bound even when the configured bound is zero.

Also fixed along the way: a zero FM_*_TIMEOUT silently removed the hard bound,
the pinned Grok version had drifted to 0.2.117, and --changed selection refused
outright on any deleted bin/ script.

AGENTS.md section 4 and quota-array-dispatch own the corrected policy,
harness-adapters gets the catalog-responsibility correction, and
docs/verification/dispatch-auth.md records the 2026-07-30 evidence on
Pi 0.82.0, quota-axi 0.1.16, and grok 0.2.117.

* no-mistakes(review): Reject all-zero vendor probe timeouts

* fix(harness): recognize every verified harness process name consistently

Three places classify a process as a harness, and they disagreed. Measured
against current main by running each rule over the same basenames:

  comm            fm-harness   session-lock  tmux-alive
  kimi-nightly    unknown      yes           alive
  pi-launcher     unknown      no            alive
  Pi              unknown      no            alive

Two consequences, both real and both fail-safe rather than dangerous:

kimi was matched EXACTLY in bin/fm-harness.sh while its four markerless
siblings (claude, codex, opencode, grok) are substring globs, and while
bin/fm-session-lock-lib.sh and bin/backends/tmux.sh both matched it as a
substring. A kimi under a variant basename was therefore recognized by the
other two consumers but self-detected as `unknown`.

pi-launcher and Pi - the launcher wrapper and the npm shim basename, both
already known to bin/backends/tmux.sh's pane classifier - were absent from
self-detection and from the session-lock ancestry regex. A firstmate running
under either reported `unknown`, and could not prove it owned its home's
session lock, so bin/fm-claude-stop-autoarm.sh's ownership check would decline
to arm. fm_session_lock_owned_by_self fails closed, so this was a false
negative, never a false ownership claim.

Align the two outliers: kimi becomes a substring match like its siblings, and
the pi family gains the two missing basenames. The pi family stays ANCHORED on
purpose - a bare `pi` substring would also match pip, pipenv, and any path
component containing "pi" - so each launcher basename gets its own exact arm
rather than relaxing the anchor.

Deliberately NOT done: the originally-planned shared process-identity table.
It would have replaced ~15 lines of clear in-place literals with 80-100 lines
of table, dispatcher, three per-consumer generators, and two exceptions to
encode exactly the divergences fixed above - failing the design's own stop
condition, with its justifying defect (pi missing from the tmux alive list)
already fixed on main by kunchenguid#1145. The inconsistencies were the finding worth
shipping; the machinery was not.

Coverage extends tests/fm-kimi-harness.test.sh rather than adding a runner, and
asserts behavior by executing bin/fm-harness.sh and bin/fm-lock.sh against fake
ancestry. Each case was verified to FAIL before the fix: reverting kimi to an
exact arm, dropping the pi-family arms, and reverting the session-lock regex
each reproduce the original miss. pip, pipenv, and piper are asserted to stay
unrecognized so the anti-substring guard cannot be relaxed unnoticed.

---------

Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com>
Co-authored-by: Daniel Kuykendall IV <danielkuykendall23@gmail.com>
Co-authored-by: Trillium Smith <Spiteless@gmail.com>
Co-authored-by: Unknownzed <45267749+Unknownzed@users.noreply.github.com>
Co-authored-by: lhalbert <lucashalbert@users.noreply.github.com>
Co-authored-by: AG <ag@agw3.org>
Co-authored-by: deeto15 <92119640+deeto15@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant