Skip to content

fix(bin): enforce steer length, brief path, and red-PR safety rules - #1432

Closed
Boxyboxy wants to merge 9 commits into
kunchenguid:mainfrom
Boxyboxy:fm/af-fm
Closed

fix(bin): enforce steer length, brief path, and red-PR safety rules#1432
Boxyboxy wants to merge 9 commits into
kunchenguid:mainfrom
Boxyboxy:fm/af-fm

Conversation

@Boxyboxy

Copy link
Copy Markdown

Intent

Mechanize three firstmate safety rules that were prose-only with no enforcement, each of which already cost the fleet real turns. (1) bin/fm-send.sh must refuse an over-long steer instead of losing it silently: two ~1,400-character steers to a busy worker pane both reported an unconfirmed send and genuinely never arrived, while a ~230-character pointer to the same pane landed first try. The requested threshold was exactly 400 bytes, defined as a named constant with a comment citing that failure, with an explicit opt-out flag (--allow-long) named in the refusal text, and the refusal must name the working pattern (append the content to data//brief.md, then send a short pointer such as 're-read your brief, section X amended'). No other send path was to change: --key sends, key sequences, and the submit-verification logic are deliberately untouched. (2) bin/fm-brief.sh must bake the ABSOLUTE firstmate path into both the ship and the --scout scaffolds for any report or evidence output, and say plainly that a path inside the worktree does not survive cleanup: three separate workers were told to write evidence to a bare relative data// path, wrote it inside their disposable worktree, and lost it at teardown. Wording was deliberately kept short. (3) bin/fm-pr-merge.sh must refuse a red PR, since AGENTS.md states 'Never merge a red PR' as an absolute rule yet the blessed merge path performed no check on the PR's checks. It reads check state via the gh-axi CLI already used in this repo. The pending/missing/unreadable behaviour was a deliberate decision documented in the script header: 'no checks configured at all' is treated as NOT red (several fleet repos have no required checks and blocking them would break ordinary merges), pending is NOT red (the merge proceeds with a note on stderr), but a check state that cannot be read IS a refusal because 'not red' must be a positive finding rather than the absence of evidence. --allow-red-checks is the captain-authorized override and records merge_checks_override= in the task's metadata so the decision is durable; that record is deliberately written ABOVE the canonical pr= line because bin/fm-pr-lib.sh's metadata identity parse rejects unknown keys after pr=, and placing it after would break the watcher's merge poll. Scope was deliberately limited to exactly these three scripts plus their tests, because a live fleet of ~19 workers is being supervised by this repo's scripts right now; the supervision path (fm-watch.sh, fm-watch-arm.sh, fm-wake-*) was explicitly not to be touched. Tests were deliberately added to the existing colocated suites (tests/fm-send-strict.test.sh, tests/fm-brief.test.sh, tests/fm-pr-merge.test.sh) rather than as new files, per the repo convention of extending an existing script; tests/fm-pr-check-security.test.sh's gh-axi mock also needed a 'pr checks' answer because it drives fm-pr-merge. Two related omp-specific fixes from the same audit were explicitly out of scope: they live on a local-only branch and are blocked on a captain decision. The branch is cut from origin/main and targets main.

What Changed

  • bin/fm-send.sh now refuses a steer longer than a 400-byte constant before any send or pending-reply record is created, since over-long steers were reported as unconfirmed and never arrived at a busy pane. The refusal names the sanctioned pattern (append the content to the task's data/<id>/brief.md, then steer a short absolute-path pointer), and a per-call --allow-long flag is the only opt-out - deliberately not an FM_SEND_* environment tunable. --key sends, key sequences, and submit verification are untouched.
  • bin/fm-brief.sh resolves the firstmate root, data, and state directories to real absolute paths and refuses to scaffold rather than bake in one it could not resolve. Both the ship and --scout scaffolds now state the absolute report/evidence path and say plainly that a relative data/... path resolves inside the disposable worktree and is destroyed at cleanup; the ship scaffold permits that one outside write only when the task section explicitly asks for a report or evidence file, and never over brief.md.
  • bin/fm-pr-merge.sh reads the PR check rollup as structured data through gh pr view --json statusCheckRollup and classifies each entry itself, consulting both check-run conclusions and commit-status states so a failing external status is no longer misread as pending. Failing checks refuse to merge, and so does a check state that cannot be read; no configured checks, pending, and an all-skipped rollup are deliberately not red, with the latter two merging under a note on stderr. --allow-red-checks is the captain-authorized exception and records merge_checks_override=<reason> above the canonical pr= line via the same guarded atomic meta write fm-pr-check.sh uses.
  • Docs (AGENTS.md, docs/architecture.md, docs/configuration.md, docs/scripts.md) record the new gate-decision-via-brief contract and the three refusals. Tests extend the existing colocated suites rather than adding new files, driven by recorded gh rollup payloads under tests/fixtures/gh-status-check-rollup/, and bin/fm-test-run.sh maps fixture paths to their consuming suite.

Risk Assessment

✅ Low: The change is well-bounded to three scripts plus their tests, fails closed in the safety-relevant direction, has been hardened across seven prior review rounds, and now carries fixture-backed coverage of its only forge-facing logic; the two remaining findings are message-accuracy issues that do not affect the merge or send decisions.

Testing

I ran the five directly-affected suites (all pass) and then the repo's full changed-file selection, 46/46 suites with zero failures and one unrelated environmental skip because the Pi harness package is not installed on this machine. Beyond the suites I drove each guard the way an operator would and captured CLI transcripts: fm-send.sh against a real tmux pane running cat -u on a private socket, so a refused 1.6KB steer provably typed nothing while the short absolute-path pointer echoed back as submitted, --allow-long sent with the flag consumed, and --key untouched - the same transcript also reproduces the unconfirmed-delivery failure that motivated the cap; fm-brief.sh generated into one home with both the base-commit and branch scripts and diffed, showing the new absolute-path wording in the ship rules and scout definition-of-done plus the refusal when a firstmate dir cannot be resolved; and fm-pr-merge.sh run against live GitHub pull requests with real gh auth and only the merge command stubbed, refusing a genuinely red PR by check name with no metadata written and no poll armed, merging the no-checks and green PRs, refusing an unreadable state with GitHub's own diagnostic attached, recording the captain-authorized override durably above the canonical pr= line, and covering the pending/all-skipped/external-commit-status/unreadable states from the recorded payload fixtures through the script's own filter. I also confirmed the override's placement is load-bearing by showing the shared metadata identity gate accepts it as written and rejects it moved below pr=. No pull request was merged - PR 1404 and 1420 are still open. There is no UI, HTML, or rendered surface in this change, so CLI transcripts and the generated-brief diff are the end-user artifacts; no screenshot applies. The worktree is clean and all scratch directories were removed.

Evidence: fm-send over-long steer refusal - real tmux pane transcript

$ fm-send.sh af-demo "<1624-byte gate-decision text>" error: message is 1624 bytes, over the 400-byte limit; a steer this long is lost by a busy pane instead of landing. Append the content to /tmp/fm-send-ev/home/data/af-demo/brief.md, then send a short pointer such as 'READ /tmp/fm-send-ev/home/data/af-demo/brief.md, section X amended' - the absolute path, because a worker cannot find a gitignored brief by searching from its own worktree. Pass --allow-long right after the target for a deliberate long send. exit status: 1 --- pane contents after the refusal --- [pane is empty: nothing was typed, so nothing could be half-delivered] $ fm-send.sh af-demo "READ /tmp/fm-send-ev/home/data/af-demo/brief.md, section 3 amended" exit status: 0 --- pane contents: typed once, then echoed back by cat, i.e. it submitted --- READ /tmp/fm-send-ev/home/data/af-demo/brief.md, section 3 amended READ /tmp/fm-send-ev/home/data/af-demo/brief.md, section 3 amended $ fm-send.sh af-demo --allow-long "<1624-byte text>" error: text not submitted to fmdemo:fm-af-demo (delivery unconfirmed; verdict=pending; ...) --- that is the exact outcome the cap prevents ---

Firstmate steer length guard - real CLI transcript (bin/fm-send.sh)

Target is a REAL tmux pane on a private tmux server socket, so the captain's
live fleet server is untouched. The pane runs 'cat -u', so anything that
actually lands is echoed straight back and is visible in the capture.
FM_GATE_REFUSE_BYPASS=1 is the same escape hatch this repo's own test helpers
export; it is needed only because this validation itself runs inside a
no-mistakes gate worktree. The recurring "watcher down" supervision banner
fm-send prints for an unsupervised demo home is filtered out for readability.

Task metadata: /tmp/fm-send-ev/home/state/af-demo.meta -> window=fmdemo:fm-af-demo

============================================================================
1. The failure this change mechanizes: a ~1.6KB steer to a worker pane
============================================================================
$ fm-send.sh af-demo "<1624-byte gate-decision text>"
error: message is 1624 bytes, over the 400-byte limit; a steer this long is lost by a busy pane instead of landing. Append the content to /tmp/fm-send-ev/home/data/af-demo/brief.md, then send a short pointer such as 'READ /tmp/fm-send-ev/home/data/af-demo/brief.md, section X amended' - the absolute path, because a worker cannot find a gitignored brief by searching from its own worktree. Pass --allow-long right after the target for a deliberate long send.
exit status: 1

--- pane contents after the refusal ---
[pane is empty: nothing was typed, so nothing could be half-delivered]


============================================================================
2. The working pattern the refusal itself names: a short absolute-path pointer
============================================================================
$ fm-send.sh af-demo "READ /tmp/fm-send-ev/home/data/af-demo/brief.md, section 3 amended"
exit status: 0
--- pane contents: typed once, then echoed back by cat, i.e. it submitted ---
READ /tmp/fm-send-ev/home/data/af-demo/brief.md, section 3 amended
READ /tmp/fm-send-ev/home/data/af-demo/brief.md, section 3 amended


============================================================================
3a. The explicit opt-out still sends: --allow-long with a 520-byte steer
============================================================================
$ fm-send.sh af-demo --allow-long "<520-byte text>"
exit status: 0
--- pane contents (first 140 cols) ---
READ /tmp/fm-send-ev/home/data/af-demo/brief.md section 3 amended; it names the exact response command. READ /tmp/fm-send-ev/home/data/af-de
t response command.
READ /tmp/fm-send-ev/home/data/af-demo/brief.md section 3 amended; it names the exact response command. READ /tmp/fm-send-ev/home/data/af-de
t response command.
occurrences of the literal flag "--allow-long" in the pane: 0
(the flag is consumed by fm-send, never typed into the worker's composer)


============================================================================
3b. Why the cap exists, reproduced live: --allow-long with the 1624-byte steer
============================================================================
$ fm-send.sh af-demo --allow-long "<1624-byte text>"
error: text not submitted to fmdemo:fm-af-demo (delivery unconfirmed; verdict=pending; tried meta=/tmp/fm-send-ev/home/state/af-demo.meta; backend=from-meta)
exit status: 1
--- that is the exact outcome the cap prevents: the send is typed, but
    delivery comes back UNCONFIRMED rather than landing. ---


============================================================================
4. Deliberately untouched paths: --key
============================================================================
$ fm-send.sh af-demo --key Enter
exit status: 0
Evidence: Generated crewmate brief - absolute report/evidence paths, before vs after

--- ship brief --- # Rules 1. Never push to the default branch. Never merge a PR. -2. Stay inside this worktree; modify nothing outside it. +2. Stay inside this worktree; the status file below is the only file you write outside it. + The single exception: if the # Task section above explicitly asks you for a report or evidence file, write it at the absolute path /tmp/fm-evidence-home/data/af-ship1/report.md, or at the absolute path of an evidence file that section names in that same directory - never /tmp/fm-evidence-home/data/af-ship1/brief.md, which is these instructions and may be amended with a decision you must read - and nowhere else outside this worktree. + Never use a relative data/... path for it: a relative path lands inside this worktree, and the worktree is destroyed at cleanup. --- scout brief --- # Definition of done -Write your findings to /tmp/fm-evidence-home/data/af-scout1/report.md. +Write your findings to the absolute path /tmp/fm-evidence-home/data/af-scout1/report.md. +Use that absolute path, never a relative data/... one: a relative path lands inside this worktree, and the worktree is destroyed at cleanup. $ FM_DATA_OVERRIDE=relative/does-not-exist fm-brief.sh af-x firstmate --scout error: firstmate data dir 'relative/does-not-exist' is relative and does not exist, so a brief cannot name a path the crewmate can reach from its own worktree; set FM_HOME or FM_DATA_OVERRIDE to an absolute path exit status: 1

Firstmate crewmate brief - absolute report/evidence paths (bin/fm-brief.sh)

Both briefs below are REAL output of bin/fm-brief.sh into the same firstmate
home (FM_HOME=/tmp/fm-evidence-home), generated once with the base-commit
script and once with this branch's script, then diffed. This is the file a
crewmate actually reads.

Read the two "# Rules" / "# Definition of done" hunks; the two "$FM_ROOT"
hunks are noise, present only because the two script copies live in different
roots on disk.

### Generated crewmate brief: BEFORE vs AFTER (FM_HOME=/tmp/fm-evidence-home, project 'firstmate')

--- ship brief ---
--- /var/folders/6s/n0fx1k3j3w1_8b__kgyt0kyr0000gn/T//fm-ev-base/before-ship.md	2026-07-31 14:27:31
+++ /tmp/fm-evidence-home/data/af-ship1/brief.md	2026-07-31 14:27:31
@@ -20,7 +20,9 @@
 
 # Rules
 1. Never push to the default branch. Never merge a PR.
-2. Stay inside this worktree; modify nothing outside it.
+2. Stay inside this worktree; the status file below is the only file you write outside it.
+   The single exception: if the `# Task` section above explicitly asks you for a report or evidence file, write it at the absolute path `/tmp/fm-evidence-home/data/af-ship1/report.md`, or at the absolute path of an evidence file that section names in that same directory - never `/tmp/fm-evidence-home/data/af-ship1/brief.md`, which is these instructions and may be amended with a decision you must read - and nowhere else outside this worktree.
+   Never use a relative `data/...` path for it: a relative path lands inside this worktree, and the worktree is destroyed at cleanup.
 3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations.
 4. Report status by appending one line:
    `echo "{state}: {one short line}" >> '/tmp/fm-evidence-home/state/af-ship1.status'`
@@ -44,10 +46,10 @@
    daemon error, append `blocked: {the daemon error}` and stop; only firstmate manages the daemon.
 
 # Project memory
-If `AGENTS.md` or `CLAUDE.md` already exists, or if this task produced durable project-intrinsic knowledge, run `/var/folders/6s/n0fx1k3j3w1_8b__kgyt0kyr0000gn/T/fm-ev-base/bin/fm-ensure-agents-md.sh .` in the worktree.
+If `AGENTS.md` or `CLAUDE.md` already exists, or if this task produced durable project-intrinsic knowledge, run `/Users/eugene/.no-mistakes/worktrees/815d1e47484d/01KYPEXJYBK5H1NSH6YDJDF88F/bin/fm-ensure-agents-md.sh .` in the worktree.
 Record only project knowledge useful to almost every future session.
 For anything the codebase already shows, prefer a pointer to the authoritative file, command, or doc over copying the detail.
-If you touch a project `AGENTS.md` that lacks `## Maintaining this file`, add that short self-governance section from `/var/folders/6s/n0fx1k3j3w1_8b__kgyt0kyr0000gn/T/fm-ev-base/bin/fm-ensure-agents-md.sh` in the same pass.
+If you touch a project `AGENTS.md` that lacks `## Maintaining this file`, add that short self-governance section from `/Users/eugene/.no-mistakes/worktrees/815d1e47484d/01KYPEXJYBK5H1NSH6YDJDF88F/bin/fm-ensure-agents-md.sh` in the same pass.
 Keep it proportionate: skip `AGENTS.md` edits for trivial tasks that produced no durable project knowledge.
 
 # Definition of done

--- scout brief ---
--- /var/folders/6s/n0fx1k3j3w1_8b__kgyt0kyr0000gn/T//fm-ev-base/before-scout.md	2026-07-31 14:27:31
+++ /tmp/fm-evidence-home/data/af-scout1/brief.md	2026-07-31 14:27:31
@@ -37,8 +37,9 @@
    daemon error, append `blocked: {the daemon error}` and stop; only firstmate manages the daemon.
 
 # Definition of done
-Write your findings to `/tmp/fm-evidence-home/data/af-scout1/report.md`.
+Write your findings to the absolute path `/tmp/fm-evidence-home/data/af-scout1/report.md`.
+Use that absolute path, never a relative `data/...` one: a relative path lands inside this worktree, and the worktree is destroyed at cleanup.
 The report must stand alone: what you did, what you found, the evidence (commands run, output, file:line references), and what you recommend.
-Before reporting done, read and follow `/var/folders/6s/n0fx1k3j3w1_8b__kgyt0kyr0000gn/T/fm-ev-base/.agents/skills/decision-hold-lifecycle/SKILL.md` and pass its shared completion gate for the report and any visual review.
+Before reporting done, read and follow `/Users/eugene/.no-mistakes/worktrees/815d1e47484d/01KYPEXJYBK5H1NSH6YDJDF88F/.agents/skills/decision-hold-lifecycle/SKILL.md` and pass its shared completion gate for the report and any visual review.
 When the report is complete, append `done: {one-line conclusion}` to the status file and stop.
 If your findings reveal work that should ship (e.g. you reproduced a bug and the fix is clear), say so in the report; firstmate may promote this task in place, and you would then receive mode-specific ship instructions as a follow-up message.

############################################################################
# Refusal: a firstmate dir that cannot be made absolute is refused rather
# than baked into a brief as a path the crewmate cannot reach.
############################################################################
$ FM_DATA_OVERRIDE=relative/does-not-exist fm-brief.sh af-x firstmate --scout
error: firstmate data dir 'relative/does-not-exist' is relative and does not exist, so a brief cannot name a path the crewmate can reach from its own worktree; set FM_HOME or FM_DATA_OVERRIDE to an absolute path
exit status: 1

$ FM_ROOT_OVERRIDE=relative/does-not-exist fm-brief.sh af-x firstmate
error: firstmate root dir 'relative/does-not-exist' is relative and does not exist, so a brief cannot name a path the crewmate can reach from its own worktree; set FM_ROOT_OVERRIDE to an absolute path
exit status: 1

############################################################################
# For reference: the two sections of a real generated brief, as a crewmate
# reads them (FM_HOME=/tmp/fm-evidence-home).
############################################################################
--- ship brief, # Rules ---
# Rules
1. Never push to the default branch. Never merge a PR.
2. Stay inside this worktree; the status file below is the only file you write outside it.
   The single exception: if the `# Task` section above explicitly asks you for a report or evidence file, write it at the absolute path `/tmp/fm-evidence-home/data/af-ship1/report.md`, or at the absolute path of an evidence file that section names in that same directory - never `/tmp/fm-evidence-home/data/af-ship1/brief.md`, which is these instructions and may be amended with a decision you must read - and nowhere else outside this worktree.
   Never use a relative `data/...` path for it: a relative path lands inside this worktree, and the worktree is destroyed at cleanup.
3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations.
--- scout brief, # Definition of done ---
# Definition of done
Write your findings to the absolute path `/tmp/fm-evidence-home/data/af-scout1/report.md`.
Use that absolute path, never a relative `data/...` one: a relative path lands inside this worktree, and the worktree is destroyed at cleanup.
The report must stand alone: what you did, what you found, the evidence (commands run, output, file:line references), and what you recommend.
Evidence: fm-pr-merge red-PR refusal - live GitHub check states, merge stubbed

# CASE 1 - a genuinely red PR (real checks read from GitHub) FAILURE PR must be raised via no-mistakes FAILURE Stock macOS Bash snapshot compatibility SUCCESS (8 others) $ fm-pr-merge.sh task-red #1404: PR #1404 has failing checks, refusing to merge: PR must be raised via no-mistakes Stock macOS Bash snapshot compatibility Fix the checks, or pass --allow-red-checks for a captain-authorized exception. exit status: 1 --- merge commands actually issued --- [none: gh-axi pr merge was never invoked] # CASE 4 - a check state that CANNOT be read (real gh failure) error: could not read the check state of PR ... (gh could not return the PR's check rollup: GraphQL: Could not resolve to a Repository with the name 'example/no-such-repo-fm'. (repository)), refusing to merge; "not red" must be established, not assumed. # CASE 5 - captain-authorized override on the SAME real red PR 5:mode=no-mistakes 6:merge_checks_override=failing checks: PR must be raised via no-mistakes; Stock macOS Bash snapshot compatibility 7:pr=https://github.com/kunchenguid/firstmate/pull/1404&#10;8:pr_head=13193229bd27b62ea11603cec0b112d6da457dc7&#10;&#10;# CASE 6 - recorded forge payloads through the script's own filter note: 1 check(s) are still pending; pending is not failing, so the merge proceeds note: all 4 check(s) are skipped or cancelled, so no check actually passed; skipped is not failing, so the merge proceeds error: PR ... has failing checks, refusing to merge: vercel/preview # CASE 7 - override placement override ABOVE pr= -> identity parse: ACCEPTED override BELOW pr= -> identity parse: REJECTED

Firstmate "never merge a red PR" guard - real CLI transcript (bin/fm-pr-merge.sh)

The check state below is read from GitHub FOR REAL: `gh pr view --json
statusCheckRollup` against live pull requests in kunchenguid/firstmate, using
this machine's real gh auth. Only the merge itself is stubbed - a `gh-axi`
shim earlier on PATH records the merge command and exits 0 without forwarding,
so no pull request can actually land while producing this evidence.
Each case gets its own task metadata file so the recorded state is visible.

The unsupervised-demo-home "WATCHER DOWN" banner fm-pr-merge prints is filtered
out of this transcript for readability; nothing else is edited.

Post-run check: PR 1404 and PR 1420 are both still OPEN on GitHub, and every
merge line below is the stub's own "[stub] ... recorded, NOT executed" output,
so no pull request was merged to produce this evidence. (PR 1425 was closed,
not merged, by its own author at 21:28:59Z, independently of this run -
mergedAt is null.)

############################################################
# CASE 1 - a genuinely red PR (2 failing checks on the real PR)
############################################################
$ gh pr view 1404 --repo kunchenguid/firstmate --json statusCheckRollup   [real, abbreviated]
  FAILURE	PR must be raised via no-mistakes
  FAILURE	Stock macOS Bash snapshot compatibility
  SUCCESS	Behavior portable parallel 1
  SUCCESS	Behavior portable parallel 2
  SUCCESS	Behavior portable serial
  SUCCESS	Behavior tests (Herdr)
  SUCCESS	Behavior timing aggregate
  SUCCESS	Lint shell scripts
  SUCCESS	Repo invariants
  SUCCESS	Test coverage guard

$ fm-pr-merge.sh task-red https://github.com/kunchenguid/firstmate/pull/1404
error: PR https://github.com/kunchenguid/firstmate/pull/1404 has failing checks, refusing to merge:
  PR must be raised via no-mistakes
  Stock macOS Bash snapshot compatibility
Fix the checks, or pass --allow-red-checks for a captain-authorized exception.
exit status: 1

--- merge commands actually issued ---
[none: gh-axi pr merge was never invoked]
--- task metadata after the refusal (no pr= recorded, nothing armed) ---
window=fm-task-red
worktree=/tmp/fm-prmerge-ev/red/wt
project=/tmp/fm-prmerge-ev/red/project
kind=ship
mode=no-mistakes
task-red.meta

############################################################
# CASE 2 - a real PR in a state with NO configured checks (deliberately not red)
############################################################
$ gh pr view 1425 --repo kunchenguid/firstmate --json statusCheckRollup   [real]
{"statusCheckRollup":[]}

$ fm-pr-merge.sh task-nc https://github.com/kunchenguid/firstmate/pull/1425
armed: state/task-nc.check.sh
[stub] gh-axi pr merge 1425 --repo kunchenguid/firstmate --squash -- recorded, NOT executed
exit status: 0
--- merge command the stub recorded ---
pr merge 1425 --repo kunchenguid/firstmate --squash
--- task metadata (pr= recorded, no override recorded) ---
window=fm-task-nc
worktree=/tmp/fm-prmerge-ev/nochecks/wt
project=/tmp/fm-prmerge-ev/nochecks/project
kind=ship
mode=no-mistakes
pr=https://github.com/kunchenguid/firstmate/pull/1425
pr_head=b1d184a92d17cfb0b197067695221b218590815e

############################################################
# CASE 3 - a real, fully green PR
############################################################
$ gh pr view 1420 --repo kunchenguid/firstmate --json statusCheckRollup   [real, abbreviated]
  SUCCESS	Behavior portable parallel 1
  SUCCESS	Behavior portable parallel 2
  SUCCESS	Behavior portable serial
  SUCCESS	Behavior tests (Herdr)
  SUCCESS	Behavior timing aggregate
  SUCCESS	Lint shell scripts
  SUCCESS	PR must be raised via no-mistakes
  SUCCESS	Repo invariants
  SUCCESS	Stock macOS Bash snapshot compatibility
  SUCCESS	Test coverage guard

$ fm-pr-merge.sh task-gr https://github.com/kunchenguid/firstmate/pull/1420
armed: state/task-gr.check.sh
[stub] gh-axi pr merge 1420 --repo kunchenguid/firstmate --squash -- recorded, NOT executed
exit status: 0
--- merge command the stub recorded (this is what WOULD have run) ---
pr merge 1420 --repo kunchenguid/firstmate --squash
--- task metadata ---
window=fm-task-gr
worktree=/tmp/fm-prmerge-ev/green/wt
project=/tmp/fm-prmerge-ev/green/project
kind=ship
mode=no-mistakes
pr=https://github.com/kunchenguid/firstmate/pull/1420
pr_head=11cbda9ca509ce4645b98024e9d0008192f8e60f

############################################################
# CASE 4 - a check state that CANNOT be read (real gh failure against a repo
#          this token cannot resolve). 'Not red' must be a positive finding.
############################################################
$ fm-pr-merge.sh task-un https://github.com/example/no-such-repo-fm/pull/7
error: could not read the check state of PR https://github.com/example/no-such-repo-fm/pull/7 (gh could not return the PR's check rollup: GraphQL: Could not resolve to a Repository with the name 'example/no-such-repo-fm'. (repository)), refusing to merge; "not red" must be established, not assumed.
Retry once gh can reach the PR, or pass --allow-red-checks for a captain-authorized exception.
exit status: 1
--- merge commands issued ---
[none]
--- task metadata (untouched: no pr= recorded) ---
window=fm-task-un
worktree=/tmp/fm-prmerge-ev/unread/wt
project=/tmp/fm-prmerge-ev/unread/project
kind=ship
mode=no-mistakes

############################################################
# CASE 5 - the captain-authorized override on the SAME real red PR
############################################################
$ fm-pr-merge.sh task-ov https://github.com/kunchenguid/firstmate/pull/1404 --allow-red-checks
armed: state/task-ov.check.sh
[stub] gh-axi pr merge 1404 --repo kunchenguid/firstmate --squash -- recorded, NOT executed
exit status: 0
--- merge command the stub recorded ---
pr merge 1404 --repo kunchenguid/firstmate --squash
--- task metadata: the decision is durable, and merge_checks_override sits ABOVE pr= ---
     1	window=fm-task-ov
     2	worktree=/tmp/fm-prmerge-ev/override/wt
     3	project=/tmp/fm-prmerge-ev/override/project
     4	kind=ship
     5	mode=no-mistakes
     6	merge_checks_override=failing checks: PR must be raised via no-mistakes; Stock macOS Bash snapshot compatibility
     7	pr=https://github.com/kunchenguid/firstmate/pull/1404
     8	pr_head=13193229bd27b62ea11603cec0b112d6da457dc7

############################################################
# CASE 6 - check states no open PR exhibits right now, replayed from RECORDED
#          GitHub payloads through fm-pr-merge.sh's OWN rollup filter
############################################################

--- fixture: tests/fixtures/gh-status-check-rollup/expected-required-status.json ---
[{"t":"CheckRun","v":"SUCCESS","n":"Lint shell scripts"},{"t":"StatusContext","v":"EXPECTED","n":"ci/required-signoff"}]
$ fm-pr-merge.sh task-fx https://github.com/example/repo/pull/9
armed: state/task-fx.check.sh
note: 1 check(s) on https://github.com/example/repo/pull/9 are still pending; pending is not failing, so the merge proceeds
[stub] gh-axi pr merge 9 --repo example/repo --squash -- recorded, NOT executed
exit status: 0
merge command issued: pr merge 9 --repo example/repo --squash

--- fixture: tests/fixtures/gh-status-check-rollup/all-skipped.json ---
[{"t":"CheckRun","v":"SKIPPED","n":"Lint shell scripts"},{"t":"CheckRun","v":"SKIPPED","n":"Behavior tests"},{"t":"CheckRun","v":"CANCELLED","n":"Docs check"},{"t":"CheckRun","v":"CANCELLED","n":"Typecheck"}]
$ fm-pr-merge.sh task-fx https://github.com/example/repo/pull/9
armed: state/task-fx.check.sh
note: all 4 check(s) on https://github.com/example/repo/pull/9 are skipped or cancelled, so no check actually passed; skipped is not failing, so the merge proceeds
[stub] gh-axi pr merge 9 --repo example/repo --squash -- recorded, NOT executed
exit status: 0
merge command issued: pr merge 9 --repo example/repo --squash

--- fixture: tests/fixtures/gh-status-check-rollup/commit-status-failure.json ---
[{"t":"CheckRun","v":"SUCCESS","n":"Lint shell scripts"},{"t":"StatusContext","v":"FAILURE","n":"vercel/preview"}]
$ fm-pr-merge.sh task-fx https://github.com/example/repo/pull/9
error: PR https://github.com/example/repo/pull/9 has failing checks, refusing to merge:
  vercel/preview
Fix the checks, or pass --allow-red-checks for a captain-authorized exception.
exit status: 1
merge command issued: [none]

--- fixture: tests/fixtures/gh-status-check-rollup/unreadable.json ---
[]
$ fm-pr-merge.sh task-fx https://github.com/example/repo/pull/9
error: could not read the check state of PR https://github.com/example/repo/pull/9 (gh could not return the PR's check rollup: jq: error (at /Users/eugene/.no-mistakes/worktrees/815d1e47484d/01KYPEXJYBK5H1NSH6YDJDF88F/tests/fixtures/gh-status-check-rollup/unreadable.json:3): statusCheckRollup is missing from the PR view), refusing to merge; "not red" must be established, not assumed.
Retry once gh can reach the PR, or pass --allow-red-checks for a captain-authorized exception.
exit status: 1
merge command issued: [none]



############################################################
# CASE 7 - why the override line is written ABOVE the canonical pr= line
#          fm_pr_metadata_identity_parse (bin/fm-pr-lib.sh) is the shared gate
#          that fm-pr-check.sh's meta write, fm-pr-merge.sh's own override
#          write, and the legacy poll migration all pass a task meta through.
#          It rejects any unrecognised key that appears AFTER pr=.
############################################################
--- as fm-pr-merge actually writes it (override above pr=) ---
1:window=fm-task-ov
2:worktree=/tmp/fm-prmerge-ev/override/wt
3:project=/tmp/fm-prmerge-ev/override/project
4:kind=ship
5:mode=no-mistakes
6:merge_checks_override=failing checks: PR must be raised via no-mistakes; Stock macOS Bash snapshot compatibility
7:pr=https://github.com/kunchenguid/firstmate/pull/1404
8:pr_head=13193229bd27b62ea11603cec0b112d6da457dc7
identity parse: ACCEPTED  (url=https://github.com/kunchenguid/firstmate/pull/1404 number=1404)

--- the same content with the override moved below pr= (what this deliberately avoids) ---
1:window=fm-task-ov
2:worktree=/tmp/fm-prmerge-ev/override/wt
3:project=/tmp/fm-prmerge-ev/override/project
4:kind=ship
5:mode=no-mistakes
6:pr=https://github.com/kunchenguid/firstmate/pull/1404
7:merge_checks_override=failing checks: PR must be raised via no-mistakes; Stock macOS Bash snapshot compatibility
8:pr_head=13193229bd27b62ea11603cec0b112d6da457dc7
identity parse: REJECTED - the task's PR identity would no longer be readable,
                so the armed merge poll and any later meta write would fail.
Evidence: Full changed-file regression summary
FM_TEST_SUMMARY total=46 failed=0 skipped_gate=1 duration_ms=862621
(the single gate skip is tests/fm-pi-primary-types.test.sh: "installed @earendil-works/pi-coding-agent package not found" - environmental, unrelated to this change)

Pipeline

Updates from git push no-mistakes

... (4 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)

⚠️ **Review** - 2 infos

🔧 Fix: read PR check rollup as structured data; resolve brief paths
6 issues (3 warnings, 3 infos) still open:

  • ⚠️ bin/fm-pr-merge.sh:197 - record_checks_override rebuilds the task's meta with two greps and || true, which is weaker than the while-read reconstruction bin/fm-pr-check.sh:95-101 already uses for the identical job on the identical file. || true cannot distinguish grep's exit 1 (no matching line, expected) from exit >=2 (a real read error), and GNU grep that decides the file is binary prints "Binary file ... matches" on stdout instead of the lines. Either way the tmp is built from partial content, and the validation that follows does not catch it: fm_pr_metadata_identity_parse only rejects unknown keys that appear AFTER pr=, so a tmp holding one junk line plus merge_checks_override= plus the pr block parses clean, gets moved over the meta, and silently drops window=, worktree=, project=, harness=, and mode= - which is what teardown's landed-work check and the watcher read. Reuse fm-pr-check.sh's while IFS= read -r line loop over $META instead; it is immune to both failure modes and removes the second grep pass.
  • ⚠️ bin/fm-brief.sh:98 - DATA is now resolved to a real absolute path (or refused), but STATE on the next line is still ${FM_STATE_OVERRIDE:-$FM_HOME/state} with no resolution, and STATE is what gets baked into every scaffold as the status file at line 134/389. Run the change's own scenario - cd &lt;home&gt; &amp;&amp; FM_HOME=. bin/fm-brief.sh &lt;id&gt; proj, which tests/fm-brief.test.sh:test_relative_home_is_resolved_or_refused exercises - and the emitted brief names an absolute /abs/home/data/&lt;id&gt;/report.md for evidence but a relative ./state/&lt;id&gt;.status for status, directly under new rule 2's claim that "the status file below is the only file you write outside it". The crewmate's status appends then land inside its disposable worktree and vanish at cleanup, which is a worse instance of the exact loss this change exists to prevent: a lost report costs the findings, a lost status file costs every wake firstmate supervises the task by. Resolve STATE through the same cd -- &amp;&amp; pwd / refuse-if-relative block as DATA.
  • ⚠️ bin/fm-pr-merge.sh:118 - read_check_state sends gh's stderr to /dev/null, so the hard refusal it feeds reports only "gh could not return the PR's check rollup" with no cause. This gate now blocks every task PR merge, and the distinct causes need distinct operator actions: an expired token needs gh auth login, a 403 needs a scope, a rate limit needs a wait, a jq filter error needs a code fix, and a missing gh needs an install - the message advises "retry once gh can reach the PR" for all five. fm-pr-check.sh can afford 2&gt;/dev/null there because its pr_head lookup degrades to not recording a field; here the same suppression drops the only evidence for a stop-the-line refusal. Capture the CLI's stderr and append it to the refusal text.
  • ℹ️ bin/fm-pr-merge.sh:146 - The commit-status branch maps EXPECTED to skip, but GitHub's EXPECTED means a required status context that has been declared and has not reported yet - semantically pending, not skipped. The merge decision is the same either way, but the evidence is not: with a rollup of one SUCCESS check run plus one EXPECTED required status, CHECK_PENDING stays 0 and CHECK_SKIP != CHECK_TOTAL, so neither note at lines 276-286 fires and the merge proceeds with no output at all. That is the one outcome the header's own design rules out ("it merges with its own note on stderr rather than silently"). Move EXPECTED into the pending class so the note fires. Note also that NEUTRAL in that same branch is dead: NEUTRAL is a CheckConclusionState, never a StatusState.
  • ℹ️ bin/fm-pr-merge.sh:195 - record_checks_override mirrors fm-pr-check.sh's meta-write idiom but omits three of its guards: there is no EXIT trap, so an interrupt between mktemp and mv leaves a mode-0600 .fm-pr-merge-meta.XXXXXX orphaned in state/ (fm-pr-check.sh:80-86 traps exactly this); there is no [ &#34;$META_DEVICE&#34; = &#34;$STATE_DEVICE&#34; ] assertion, so if the meta ever sits on a different device from state/ the mv degrades from an atomic rename to a copy, which is the window this whole idiom exists to close; and there is no post-mv re-validation of $META (fm-pr-check.sh:112-113 re-runs fm_pr_private_file_valid and the identity parse on the destination). Practically these are covered because fm-pr-check.sh asserted device parity moments earlier, so this is a parity gap in a security-hardened file rather than a live hole.
  • ℹ️ docs/architecture.md:195 - Both new refusals are absent from the docs that describe these scripts. docs/architecture.md:195 still says fm-pr-merge.sh "records pr= and any available pr_head= through bin/fm-pr-check.sh before calling gh-axi pr merge" with no mention of the red-check gate or --allow-red-checks, docs/scripts.md:85 summarizes it the same way, and docs/architecture.md:86 describes fm-send.sh's settle behavior with no mention of the length cap or --allow-long. An agent that reads architecture.md to learn the merge path will not learn that merging a red PR is now mechanically blocked, which is the one fact most worth discovering there.

🔧 Fix: adopt sibling meta-write guards; resolve every brief path
3 issues (1 warning, 2 infos) still open:

  • ⚠️ tests/fm-brief.test.sh:578 - The two negative guards in test_relative_home_is_resolved_or_refused can never match, so they prove nothing. assert_no_grep is a fixed-string grep (tests/lib.sh:206, grep -F --), but the patterns are written as regexes: line 578 is '\./data/&#39; and line 580 is &#34;&#39;\./state/&#34; - both carry a literal backslash (in double quotes bash keeps .intact, since.is not one of $ " \ newline). A brief that regressed to a relative home would contain ./data/ and &#39;./state/, never ./data/ or './state/, so both assertions pass whether or not the defect is present. The positive assertions two lines above ($abs/data/$id/report.md and $abs/state/$id.status) are what actually cover the property today; these two read as extra safety and deliver none. Drop the backslashes so the fixed strings are '`./data/' and "'./state/".
  • ℹ️ bin/fm-brief.sh:78 - FM_ROOT is the third firstmate path the scaffolds bake in, and it is the one that did not get the new treatment. The header now claims "Every firstmate path a scaffold bakes in - the report or evidence dir and the status file alike - is resolved to a real absolute path first", and DATA/STATE go through resolve_home_dir at lines 104-105, but line 78 leaves FM_ROOT as ${FM_ROOT_OVERRIDE:-<absolute>} unresolved. That value is baked into the scaffolds at lines 231, 303, 417 and 420 as paths the crewmate must read or execute from inside its worktree ($FM_ROOT/.agents/skills/decision-hold-lifecycle/SKILL.md, $FM_ROOT/bin/fm-herdr-lab.sh, $FM_ROOT/bin/fm-ensure-agents-md.sh). With a relative FM_ROOT_OVERRIDE the scout brief tells the worker to read a relative skill path that resolves inside its disposable worktree and is not there - the same failure class this change exists to close, on the path that gates scout completion. Every production setter (fm-fleet-snapshot.sh, fm-bootstrap.sh, fm-config-push.sh) passes an absolute $FM_ROOT today, so this is a consistency gap rather than a live break, but the same was true of FM_HOME before this change. Route FM_ROOT through the same resolve_home_dir call.
  • ℹ️ bin/fm-pr-merge.sh:122 - CHECK_ROLLUP_FILTER is the only forge-facing logic this change adds, and no test exercises it. Every case in tests/fm-pr-merge.test.sh mocks gh at the wrong layer: GH_ROLLUP_MOCK_BODY emits already-formatted "<conclusion>|<state>|<name>" rows, so the jq expression that produces them - the .state // &#34;&#34; arm that fixes the original commit-status bug, the .name // .context fallback, the error(...) branch that makes a missing rollup unreadable - is never evaluated. I ran the filter against real gh 2.92.0 and it is correct today (a red PR yields rollup|10 plus one row per check; a repo with no workflows yields statusCheckRollup: [] and so rollup|0, confirming the documented no-checks path). But the defect this change was written to fix was precisely a consumed representation drifting from the forge's data, and the current suite would not notice that happening again. A case that pipes a recorded gh pr view --json statusCheckRollup JSON fixture through the filter would close it; note that gh evaluates -q with its embedded jq, so a system-jq test is close but not identical.

🔧 Fix: cover rollup filter with recorded payloads; resolve brief root
3 issues (1 error, 2 infos) still open:

  • 🚨 tests/fixtures/gh-status-check-rollup/README.md:1 - The new fixture README is a tracked markdown file inside the maintained-prose scope, but it is not classified in docs/documentation-audiences.json, so bin/fm-doc-audience-check.sh fails. That checker's scope is the fixed pathspec *.md *.mdx *.rst *.txt docs/examples/* (bin/fm-doc-audience-check.sh:145 and REQUIRED_TRACKED_PATTERNS), resolved through git ls-files -- &lt;patterns&gt; (line 40-50), and a git pathspec of *.md matches at any depth. Reproducing its own set logic (missing = tracked - classified, line 175-186) against this worktree yields exactly one entry: tests/fixtures/gh-status-check-rollup/README.md. The check therefore exits with unclassified: tests/fixtures/gh-status-check-rollup/README.md, and tests/fm-documentation-audiences.test.sh:56 (test_repository_inventory_passes) runs that check against the real repo root, so the suite fails. This will not surface in a --changed run either: the new tests/fixtures/* branch in bin/fm-test-run.sh:717 resolves the fixture only to tests/fm-pr-merge.test.sh, so the doc-audience suite is never selected and the break lands in the full suite and CI instead. Note the sibling fixture dir tests/fixtures/quota-array-dispatch/ carries only .json files, which is why no prior fixture ever needed a classification. Fix: add {"path": "tests/fixtures/gh-status-check-rollup/README.md", "audience": "maintainer-verification"} to docs/documentation-audiences.json - that class is defined as recording repeatable evidence for an active guarantee, which is exactly what the file does.
  • ℹ️ bin/fm-pr-merge.sh:303 - The rollup is read at line 303, then bin/fm-pr-check.sh runs (line 337 - guard, non-executing legacy migration, poll prepare/publish, several forge and filesystem round trips), and only then does gh-axi pr merge fire at line 369. Nothing binds the merge to the commit whose checks were read, so a push that lands in that window merges a head this gate never inspected - the one outcome the rule exists to prevent. The window is seconds and needs a concurrent push, so it is narrow rather than likely. The material is already at hand to close it: fm-pr-check.sh records the forge's exact head as pr_head= in the task's meta, and gh pr merge accepts --match-head-commit <sha>, which refuses server-side when the head moved. Reading pr_head= back after line 338 and passing it through (when recorded; GitLab tasks and gh's degraded head lookup record none) would make the merge apply to the commit the gate actually cleared. Worth a captain call rather than a mechanical edit, since it adds a flag to the blessed merge path.
  • ℹ️ bin/fm-pr-merge.sh:251 - record_checks_override (lines 251-283) is now a near-line-for-line copy of bin/fm-pr-check.sh:92-118: same device-parity assertion, same mktemp under $STATE, same while-read reconstruction, same chmod 0600, same fm_pr_private_file_valid + fm_pr_metadata_identity_parse on the temp, same fm_pr_regular_destination_on_device_or_absent before the mv, same post-mv revalidation. Two independent copies of a security-hardened atomic write on the identical file mean the next guard added to one has to be remembered for the other - which is exactly how this copy came to be missing three guards in round 3 and needed a follow-up round to regain them. bin/fm-pr-lib.sh already owns every primitive both callers use and would be the natural home for one fm_pr_meta_rewrite helper taking a filter callback or a key/value pair. Flagging as info rather than proposing the edit: the script header at lines 244-250 states the duplication was deliberate ("that writer's reconstruction and its whole guard set rather than a lookalike"), and consolidating touches a security-sensitive shared library, so it is the captain's call whether the coupling is worth it.

🔧 Fix: fold fixture README provenance into consuming test header
3 infos still open:

  • ℹ️ bin/fm-pr-merge.sh:313 - When the rollup is partly readable (CHECK_SEEN != CHECK_TOTAL at line 306) AND the rows that were read include failures, the UNREADABLE branch is tested first, so the refusal prints only "could not read the check state ... Retry once gh can reach the PR" and discards the failing check names tally_check_rows already extracted into FAILING. The operator is told to retry the CLI when the actual problem is a red PR. The same precedence costs the durable record: under --allow-red-checks, OVERRIDE_REASON becomes "check state unreadable: ..." instead of naming the checks that were overridden, so the captain-authorized exception is recorded without the evidence that motivated it. Both paths still refuse, so there is no safety hole. Suggest appending the extracted failing names to the unreadable message (and to OVERRIDE_REASON) when CHECK_FAIL > 0, so a partly-read red rollup reports both facts.
  • ℹ️ bin/fm-brief.sh:98 - resolve_home_dir's refusal text is shared by all three call sites and ends with "set FM_HOME or $override to an absolute path". That is correct for the data and state dirs, which default to $FM_HOME/data and $FM_HOME/state, but wrong for the firstmate root: FM_ROOT is resolved from ${FM_ROOT_OVERRIDE:-<script dir>} at line 105 and FM_HOME is derived FROM it on the next line, so setting FM_HOME cannot clear a root refusal. An operator following the message's first suggestion gets the identical refusal again. Suggest passing the applicable variable name(s) per call site, or dropping FM_HOME from the shared text and letting the $override argument carry it.
  • ℹ️ bin/fm-brief.sh:395 - The ship scaffold's permitted outside-worktree write is "under $DATA/$ID/ - at the absolute path $DATA/$ID/report.md, or a named file beside it in that directory". That directory also holds $DATA/$ID/brief.md, and this same branch changes AGENTS.md section 7 to deliver every gate decision by appending it to that file and steering the worker to re-read it. A worker asked for an evidence file, writing "a named file beside it", can therefore overwrite the one channel firstmate now uses to hand it a captain decision, with no error and no trace. The scout scaffold does not have this exposure: its rule 2 names "the report and the status file" specifically rather than a directory. Probability is low (a worker would have to pick that exact name), but the collision is new to this branch and one clause closes it - e.g. name the permitted files as report.md or an evidence file, never brief.md. Flagging rather than editing because this sentence has been deliberately retuned across several rounds.

🔧 Fix: report red checks in unreadable rollups; fix brief refusal and write region
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-send.sh:262 - The length refusal teaches a pointer pattern that the same branch's AGENTS.md says structurally does not work. AGENTS.md:299-300 (changed in this branch) now requires the steer to give "that brief's ABSOLUTE path and tell the worker to READ it, never to find or look for it", with the stated reason that "data/ is gitignored, so a worker searching from its own worktree structurally cannot find the file". The refusal's suggested pointer is "re-read your brief, section X amended" - no path, and "re-read" rather than READ. That is exactly the shape AGENTS.md rules out. It matters because the brief reaches most workers as piped CONTENT, not a path: bin/fm-spawn.sh:429-452 launches claude/codex/opencode/pi/grok with &#34;$(... encode launch-brief &lt; __BRIEF__)&#34;, so only the kimi adapter (fm-spawn.sh:1525) ever hands over a path. A scout worker following the refusal's advice has no absolute path anywhere in its brief (the scout scaffold names only report.md) and cannot find a gitignored file outside its worktree. The refusal already computes the absolute path in BRIEF_HINT one line above, so it has the value it omits from the example. I am flagging rather than editing because this exact wording was your round-1 instruction; the conflict is that AGENTS.md §7 was rewritten afterwards. Suggest aligning the example with the contract, e.g. "...then send a short pointer such as 'READ <that path>, section 3 amended'".
  • ℹ️ tests/fm-brief.test.sh:636 - The state-dir refusal case runs without FM_HOME, so it does not test what its guard claims. With FM_HOME unset, fm-brief.sh derives FM_HOME from FM_ROOT (bin/fm-brief.sh:111-112), making DATA resolve to the firstmate repo's own $ROOT/data. Today the STATE refusal fires first so nothing is written, but if that refusal ever regressed the brief would be scaffolded into $ROOT/data/brief-relative-state-refused/brief.md - inside the checkout, in a directory .gitignore:3 hides, so the stray write leaves no trace - while the test's assert_absent &#34;$home/data/brief-relative-state-refused/brief.md&#34; watches a path that is never created under either behaviour and passes regardless. The expect_code 1 above it still catches the regression, so this is a blind guard plus a checkout-pollution hazard rather than a hole. The FM_DATA_OVERRIDE case at line 624 has the same missing FM_HOME (its own assert_absent happens to point at the right relative path). Fix: pass FM_HOME="$home" to both cases, as the root-refusal case at line 615 already does, and point the state case's assert_absent at "$home/data/...".
  • ℹ️ bin/fm-test-run.sh:717 - The new fixture mapping resolves families from basename $(dirname $path), which is only correct for a fixture exactly one directory deep. A fixture placed directly at tests/fixtures/&lt;file&gt;.json yields the reference string "fixtures", and families_for_test_reference (line 586-597) then greps every test script for that word - matching tests/fm-test-run.test.sh:6 ("exercise the runner with fixtures") and any other suite whose prose mentions fixtures, while quite possibly missing the suite that actually reads the file. A fixture nested deeper (tests/fixtures/a/b/c.json) resolves to "b" with the same failure mode. Both existing fixture dirs happen to be one level deep, so nothing is broken today; the mapping just quietly stops meaning what its comment says outside that shape. Suggest matching tests/fixtures/*/* for the dirname rule and leaving a direct child of tests/fixtures/ as __unmapped__, so a misplaced fixture fails loudly at selection time instead of selecting an arbitrary family set.

🔧 Fix: align send refusal pointer with contract; fix brief test guards
2 infos still open:

  • ℹ️ bin/fm-pr-merge.sh:190 - NEUTRAL is grouped with SKIPPED|CANCELLED in classify_check, so a rollup whose entries are all NEUTRAL trips the all-skip branch and prints "all N check(s) on <url> are skipped or cancelled, so no check actually passed". Nothing was skipped or cancelled, and GitHub treats a NEUTRAL conclusion as a passing, non-blocking outcome for branch protection - so the note tells the captain no check passed when in fact every check completed acceptably. The merge decision is unaffected (NEUTRAL is correctly not red either way); only the evidence line is wrong. The adjacent comment already reasons carefully about why EXPECTED belongs in pending rather than skip; NEUTRAL deserves the same separation - either its own pass-adjacent class, or wording for the note that covers "skipped, cancelled, or neutral".
  • ℹ️ bin/fm-send.sh:240 - The new MAX_TEXT_BYTES=400 cap is a hard refusal on a path that has an automated internal caller: bin/fm-pending-reply-lib.sh:685 resends fm_pending_reply_recovery_message through fm-send.sh when a marked secondmate request goes unanswered. I measured that message's worst case at 324 bytes (160-byte fixed prefix + the 120-char request_summary bound from fm_pending_reply_summarize + the 22-byte FM_FROMFIRST_MARK and correlation token added by fm_pending_reply_embed_corr), so it fits today with 76 bytes of headroom. Nothing ties the two constants together, though, and the failure mode if that headroom is ever consumed - a longer prefix, or raising the 120-char summary bound - is silent and misleading: fm-send refuses with "message is N bytes, over the 400-byte limit", fm_pending_reply_send_recovery reads only a non-zero status and records recovery_failed, and the escalation reports a delivery failure rather than a message that was never allowed to be sent. Pin the relationship: assert the recovery message's bound against MAX_TEXT_BYTES in tests/fm-send-strict.test.sh or the pending-reply suite, or have the recovery resend pass --allow-long since it is a firstmate-generated message of known bounded size, not an operator steer.
✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-send-strict.test.sh tests/fm-brief.test.sh tests/fm-pr-merge.test.sh tests/fm-pr-check-security.test.sh tests/fm-secondmate-safety.test.sh - 5/5 suites pass
  • bin/fm-test-run.sh --changed --base 99533c5d - 46/46 suites pass, 0 failures, 1 unrelated environmental gate skip (Pi harness package absent)
  • Manual E2E: bin/fm-send.sh &lt;task&gt; &#34;&lt;1624-byte text&gt;&#34; against a REAL tmux pane running cat -u on a private tmux socket - refused, pane verified empty
  • Manual E2E: bin/fm-send.sh &lt;task&gt; &#34;READ &lt;abs&gt;/brief.md, section 3 amended&#34; - typed once and echoed back by the pane (submitted)
  • Manual E2E: bin/fm-send.sh &lt;task&gt; --allow-long &#34;&lt;520-byte text&gt;&#34; - exit 0, text typed, flag consumed not typed; and with the 1624-byte text, reproduced the unconfirmed-delivery outcome the cap exists to prevent
  • Manual E2E: bin/fm-send.sh &lt;task&gt; --key Enter - unaffected by the length guard
  • Manual E2E: bin/fm-brief.sh af-ship1 firstmate and bin/fm-brief.sh af-scout1 firstmate --scout under one FM_HOME, generated with both the base-commit and branch scripts and diffed
  • Manual E2E: FM_DATA_OVERRIDE=relative/does-not-exist bin/fm-brief.sh ... --scout and FM_ROOT_OVERRIDE=relative/does-not-exist bin/fm-brief.sh ... - both refuse with the settable variable named
  • Manual E2E against LIVE GitHub (real gh auth, merge command stubbed): bin/fm-pr-merge.sh task-red https://github.com/kunchenguid/firstmate/pull/1404 - refused, both failing checks named, no pr= recorded, no poll armed
  • Manual E2E against LIVE GitHub: PR 1425 (no configured checks) and PR 1420 (fully green) both proceed to the merge call with pr=/pr_head= recorded and no override line
  • Manual E2E: bin/fm-pr-merge.sh task-un https://github.com/example/no-such-repo-fm/pull/7 - real gh failure, refusal carries GitHub's own GraphQL diagnostic
  • Manual E2E: bin/fm-pr-merge.sh task-ov https://github.com/kunchenguid/firstmate/pull/1404 --allow-red-checks - merges and writes merge_checks_override=<failing check names> above the canonical pr= line
  • Manual E2E: recorded payloads tests/fixtures/gh-status-check-rollup/{expected-required-status,all-skipped,commit-status-failure,unreadable}.json replayed through fm-pr-merge.sh's own -q rollup filter - pending note, all-skipped note, external-commit-status refusal, unreadable refusal
  • Manual check: fm_pr_metadata_identity_parse (bin/fm-pr-lib.sh) ACCEPTS the meta as written and REJECTS the same content with the override line moved below pr=
  • Post-run verification via gh pr view that PR 1404 and PR 1420 remain OPEN and unmerged
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Boxyboxy added 9 commits July 31, 2026 15:06
Each rule already cost the fleet real turns, and each existed only as prose.

fm-send: refuse text over 400 bytes before any send or pending-reply record.
Two ~1,400-byte steers to one busy pane both reported an unconfirmed send and
never arrived, while a ~230-byte pointer to the same pane landed first try.
The refusal names the working pattern (append to the brief, send a pointer)
and --allow-long is the explicit opt-out. The --key path is untouched.

fm-brief: bake the absolute firstmate path into both the ship and the scout
scaffold for any report or evidence output, and say plainly that a path inside
the worktree does not survive cleanup. Three crewmates wrote evidence to a bare
relative data/<id>/ path and lost it when the worktree was returned.

fm-pr-merge: read the PR's check state and refuse a red PR, naming the failing
checks. No configured checks and pending checks are deliberately not red; a
check state that cannot be read is a refusal, because "not red" must be
established rather than assumed. --allow-red-checks is the captain-authorized
exception and records merge_checks_override= in the task's metadata, above the
canonical pr= line so the metadata identity parse still accepts the file.

Tests are colocated in the existing suites for the three scripts.
@kunchenguid

Copy link
Copy Markdown
Owner

Automated reminder: thanks for the PR! This branch currently has a merge conflict with the base branch.

When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again.

Noted for firstmate#1432 at 83657e90.

@Boxyboxy

Copy link
Copy Markdown
Author

Closing deliberately, not abandoning the work.

These changes have been merged into feat/omp-adaptor (now at 5178613b8c7faf6e039878f56a5690fc6dc1ab80), which is the branch actually in use. The canonical-repo route was not viable for this change: CI here sits at action_required behind a large queue of gated fork runs, so no green signal was reachable.

The work itself is unchanged and fully reviewed - seven review rounds through the no-mistakes pipeline.

@Boxyboxy Boxyboxy closed this Jul 31, 2026
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.

2 participants