Skip to content

fix(bin): keep firstmate parsing under stock macOS Bash 3.2 - #1200

Closed
tpavay wants to merge 10 commits into
kunchenguid:mainfrom
tpavay:fm/fix-fleetview-bash32-g8
Closed

tpavay wants to merge 10 commits into
kunchenguid:mainfrom
tpavay:fm/fix-fleetview-bash32-g8

Conversation

@tpavay

@tpavay tpavay commented Jul 28, 2026

Copy link
Copy Markdown

Intent

Keep Firstmate fully supported on stock macOS Bash 3.2. Fix the hard parse failure in fm-fleet-snapshot without raising the Bash floor, preserve byte-truncation behavior by discarding a final partial line and emptying content when no newline exists, and cover both cases with a non-vacuous regression test. Sweep every script under bin for Bash 3.2 parse and runtime hazards, fix real supported-platform defects, and report all findings and non-changes with reasoning in the PR body. Add an honest CI guard that runs every canonical bin script from fm-lint.sh --list through the genuine /bin/bash 3.2 binary on macOS, verifies every listed path and an exact nonzero parsed-file count, and do not add a vacuous proxy. The captain explicitly authorized fm-lint.sh --list as the single canonical CI file-list owner with exact count matching and authorized fixing the fm-brief.sh apostrophe-in-heredoc parser failure within this change while preserving generated brief wording and behavior. Validate fm-fleet-view end to end under /bin/bash 3.2, all bin scripts with bash -n under Bash 3.2, truncation behavior, the full suite, and pinned lint. Preserve unrelated behavior and existing safety contracts.

What Changed

  • Replaced every VAR=$(cat <<EOF ...) construct in bin/ with file-scope emitter functions, which is the shape stock macOS Bash 3.2 parses correctly once a body contains an apostrophe (issue firstmate's fm-brief.sh scaffold script has a bug #166).
    This fixes the hard parse failure in bin/fm-brief.sh and converts the registry reader, jq filters, and parent-activity reader in bin/fm-fleet-snapshot.sh plus the x-mode cadence body now owned by fmx_cadence_content in bin/fm-x-lib.sh; generated brief prose and config/x-mode.env output are byte-identical to before.
    The same 3.2 sweep also fixed bin/fm-spawn.sh, where a brace-group redirect let a failed metadata write continue under set -e; it now renders the body first and aborts with a diagnostic if publication fails.
  • bin/fm-lint.sh declares the canonical file set once as CANONICAL_ROOTS and publishes it through a new --list flag, and the macOS CI lane (renamed to "Stock macOS Bash compatibility") now consumes that list instead of spelling its own globs: it existence-checks every published path, runs each through the real /bin/bash 3.2 binary, and requires an exact nonzero parsed-file count.
    tests/fm-lint.test.sh guards the list against drift and fails any bin/ script that reintroduces the heredoc-in-command-substitution construct; CONTRIBUTING documents the 3.2 floor, the --list ownership rule, and a pre-push syntax check that uses /bin/bash rather than PATH bash.
    The sweep found no other supported-platform defects in bin/: no Bash 4+ constructs and no unguarded $BASHPID. The surviving instances of the construct in tests/ were left alone because the authorized sweep was scoped to bin/, and CI's stock-Bash lane already parses those files.
  • Test suite fixes surfaced by running the suite under real 3.2: a new non-vacuous regression test covers registry byte truncation (final partial line discarded, window empty when no newline exists) with an explicit control proving the truncated prefix would otherwise parse; the session-start lock-concurrency test replaces $BASHPID, which does not exist on 3.2, with an atomic parent-to-subshell pid handoff and bounds every contender barrier so a broken handoff fails with a diagnosis instead of hanging; harness and path env vars leaking from the surrounding firstmate session are scrubbed in three test files; and tests/fm-kimi-harness.test.sh gate-skips when python3 lacks tomllib (Python 3.11+) rather than hard-failing, which is what the pipeline Test lane hit on this host.

Risk Assessment

✅ Low: The incremental change is a purely structural hoist whose heredoc bodies I verified byte-identical, whose definitions all precede their call sites, and whose runtime behavior I confirmed unchanged by executing fm-fleet-snapshot.sh end to end under the real /bin/bash 3.2 binary across all three registry truncation paths.

Testing

On a host whose /bin/bash and PATH bash are both the genuine stock 3.2.57, I reproduced the end-user failure at base (fm-brief.sh dies with unexpected EOF and writes no brief), confirmed the target fixes it with byte-identical generated wording, ran the CI macos-stock-bash job step verbatim (197 canonical scripts parsed through real Bash 3.2 plus the snapshot/view and Bearings suites, exit 0), proved that step fails on the base tree and that each of its path/count/parse assertions is load-bearing, killed the new byte-truncation test with both behavioral mutations, rendered fm-fleet-view end to end under 3.2, swept all 92 bin scripts for Bash 4+ hazards, and confirmed the round-1 Kimi tomllib failure now gate-skips. Everything checked passed. The full bin/fm-test-run.sh --all regression was still running when this report was produced (13 of 99 scripts, zero failures); the prior identical run on this host failed only on the Kimi tomllib case that this change fixes. Lint was not run per the no-linters rule and ShellCheck is not installed here. This change has no rendered UI surface - it is shell CLI behavior, so the evidence is CLI transcripts and generated product output rather than screenshots.

Evidence: Evidence index (start here)
# Evidence index - Bash 3.2 compatibility change (b29621b -> e864ac0)

Host: macOS 26.5.2, arm64. `/bin/bash` and PATH `bash` are both GNU bash **3.2.57(1)-release**,
so every command below ran on the genuine stock macOS interpreter this change targets - not a proxy.

| # | File | What it shows |
| --- | --- | --- |
| 01 | `01-fm-brief-bash32-before-after.txt` | End-user repro: at base, `fm-brief.sh` dies with `unexpected EOF while looking for matching ')'` and scaffolds no brief. At target, same command on the same interpreter scaffolds the brief and exits 0. |
| 02 | `02-generated-wording-unchanged.txt` | The 43 lines of generated brief prose and the generated `config/x-mode.env` body are byte-identical to base - the fix moved the heredocs, not the wording. |
| 03 | `03-fm-brief-generated-output.txt` | The actual generated briefs for all three delivery modes plus the unguarded-Herdr safety block, with zero leaked `EOF` markers. |
| 04 | `04-ci-macos-stock-bash-job.txt` | The CI `macos-stock-bash` step run verbatim: 197 canonical scripts parsed through real `/bin/bash` 3.2, then 16 snapshot/view + 42 Bearings tests, exit 0. |
| 05 | `05-ci-guard-fails-on-base.txt` | The same sweep against the base tree: 1 of 197 fails. The guard is not vacuous. |
| 06 | `06-ci-sweep-assertions-are-load-bearing.txt` | The sweep's three assertions each fire: missing listed path, empty list, and a listed script that will not parse. |
| 07 | `07-truncation-regression-mutation.txt` | The new byte-truncation test passes unmutated and is killed by both mutations (drop the partial-line discard; keep content when the window has no newline). |
| 08 | `08-fm-fleet-view-render-bash32.txt` | `fm-fleet-view.sh` rendered end to end under `/bin/bash` 3.2 - Under Way, Queued, Done, Secondmates. |
| 10 | `10-fm-lint-list-cli.txt` | The new `fm-lint.sh --list` surface: 197 paths, same set as on disk, documented in `--help`, identical under the stock interpreter. |
| 11 | `11-kimi-gate-skip.txt` | The round-1 finding is fixed: the Kimi harness now gate-skips on a python3 without tomllib (`gate_skip=true`) instead of hard-failing as it did at base. |
| 12 | `12-bin-bash32-hazard-sweep.txt` | Independent sweep of all 92 `bin/` scripts for Bash 4+ constructs, unguarded `$BASHPID`, and the issue #166 construct - all clean; every `--help` path executed under 3.2. |
| 13 | `13-heredoc-guard-non-vacuous.txt` | Reintroducing the banned construct fails `tests/fm-lint.test.sh`, plus a minimal repro of the underlying 3.2 defect. |
| 14 | `14-truncation-json-contract.txt` | The truncation contract as a consumer sees it: `fm-fleet-snapshot.sh --json` under 3.2, including the control proving the discarded prefix would otherwise parse as a record. |
| 09 | `09-full-suite.txt` | `bin/fm-test-run.sh --all` - the complete behavior suite on this 3.2 host. |
Evidence: End-user repro and fix: fm-brief.sh under stock Bash 3.2

GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) ### BEFORE (base b29621b) $ FM_HOME=... bin/fm-brief.sh fm-demo-1 firstmate .../bin/fm-brief.sh: line 314: unexpected EOF while looking for matching ) exit=2 (no brief.md produced) ### AFTER (target e864ac0) - same command, same stock Bash 3.2 $ FM_HOME=... bin/fm-brief.sh fm-demo-1 firstmate scaffolded: .../data/fm-demo-1/brief.md (ship, mode=no-mistakes; replace {TASK}) exit=0 -rw-r--r-- 1 tylerpavay staff 5907 brief.md

$ sw_vers -productVersion; /bin/bash --version | head -1; command -v bash
26.5.2
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
/bin/bash

### BEFORE (base b29621b) - stock macOS Bash 3.2 end-user run of fm-brief.sh
$ FM_HOME=... /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-base-b29621b/bin/fm-brief.sh fm-demo-1 firstmate
warn: no registry at /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-e2e/base/data/projects.md; defaulting firstmate to no-mistakes off
/var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-base-b29621b/bin/fm-brief.sh: line 314: unexpected EOF while looking for matching `)'
exit=2

$ /bin/bash -n /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-base-b29621b/bin/fm-brief.sh
/var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-base-b29621b/bin/fm-brief.sh: line 314: unexpected EOF while looking for matching `)'
/var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-base-b29621b/bin/fm-brief.sh: line 388: syntax error: unexpected end of file
exit=2

### AFTER (target e864ac0) - same command, same stock Bash 3.2
$ FM_HOME=... /Users/tylerpavay/.no-mistakes/worktrees/79e139438bd4/01KYKD5T0ZCKN866TNFN28AAMX/bin/fm-brief.sh fm-demo-1 firstmate
warn: no registry at /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-e2e/target/data/projects.md; defaulting firstmate to no-mistakes off
scaffolded: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-e2e/target/data/fm-demo-1/brief.md (ship, mode=no-mistakes; replace {TASK})
exit=0

$ /bin/bash -n /Users/tylerpavay/.no-mistakes/worktrees/79e139438bd4/01KYKD5T0ZCKN866TNFN28AAMX/bin/fm-brief.sh
exit=0

$ ls -l base/data/fm-demo-1/ target/data/fm-demo-1/
total 0
total 16
-rw-r--r--@ 1 tylerpavay  staff  5907 Jul 28 11:41 brief.md
Evidence: CI macos-stock-bash job run verbatim on real Bash 3.2

GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25) Bash 3.2 parsed 197 canonical shell scripts ok - empty fleet snapshot and view use explicit absence markers ... (16 snapshot/fleet-view + 42 Bearings tests) ... CI STEP EXIT=0

Running .github/workflows/ci.yml job 'macos-stock-bash' verbatim on a real macOS host
with the genuine stock interpreter (/bin/bash 3.2.57, arm64-apple-darwin25).

=== TARGET e864ac0 ===
$ /bin/bash <ci macos-stock-bash step>
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
Bash 3.2 parsed 197 canonical shell scripts
ok - empty fleet snapshot and view use explicit absence markers
ok - fixture snapshot covers task rows, backlog rows, pointers, and stable ordering
ok - main_inventory discloses orphan/unstructured and clears when inventory is consistent
ok - backlog normalization preserves strict roles and resolves every blocker compatibly
ok - snapshot event hints follow reconciled current state
ok - durable fold keeps an open decision past a later unrelated event
ok - a live secondmate endpoint preserves unrelated open decisions
ok - durable captain-held transfer closes the duplicate live status decision
ok - durable fold clears a decision only on a keyed resolution
ok - a completed scout's stale decision surfaces as a report pointer, not pending
ok - a scout still parked at a decision stays pending (terminal clear does not over-fire)
ok - snapshot includes durable scout reports after teardown
ok - registry byte truncation discards the partial final line and content without a newline
ok - snapshot parses tasks-axi rows and respects operational overrides
ok - fleet view renders the snapshot without secondmate peek guidance
ok - fleet view renders secondmate agent liveness
ok - Domain Alpha structured state overrides a stale parent Phase 7 event
ok - GNU stat file reads select -c without BSD filesystem-report pollution
ok - parent activity evidence is bounded and disclosed
ok - Bearings excludes a status-only child decision
ok - a structured child captain hold reaches Captain's Call
ok - missing, invalid, unreadable, malformed, and timed-out homes stay explicit unknowns
ok - an oversized secondmate summary retains the strict empty unknown fallback
ok - secondmate and per-home child counts are bounded, disclosed, and explicitly expandable
ok - parent decisions remain untrusted contradiction evidence
ok - parent evidence reconciliation distinguishes matching holds, blocks, and decisions
ok - nonprogressing child states are explicit and inconsistent terminal rows invalidate
ok - registry unavailability and bounded truncation remain explicit
ok - repeated snapshots keep the same current landed baseline and ignore prior reports
ok - default output is bounded, local-only, and marks omitted surfaces
ok - TOON and JSON are parity representations of the same model
ok - landed includes secondmate-managed merges alongside main-home merges
ok - default landed selection balances one dominant home with sparse homes
ok - landed selection refills capacity after sparse homes exhaust
ok - landed selection uses deterministic home order when homes exceed the cap
ok - landed selection preserves deterministic home and internal tie ordering
ok - landed selection handles no landed items
ok - --all-landed keeps the complete global landed output
ok - landed stays bounded with per-home + overall caps and omitted[] disclosure
ok - Bearings keeps a live blocker in structured live state and never converts it to Charted Next queue work
ok - action-free items (working/done/queued/landed) do not leak into Captain's Call
ok - main orphan in-flight stays out of Underway and is disclosed in omitted/gates
ok - main unstructured current is disclosed while structured siblings still project
ok - counterfactual meta clears main inventory warning and projects the live task
ok - mixed secondmate roles, partial state, and captain readiness project independently
ok - main and secondmate captain actionability use the same blocker readiness
ok - the /bearings skill states the four-section chat contract in order, with empty-states and the At Anchor exclusion
ok - a completed scout with decision-like report prose is a pointer, not pending
ok - an authoritative captain hold surfaces end-to-end
ok - current report pointers surface
ok - superseded queued items are dropped by default and restored with --all-queued
ok - --include-prs is the only path that fetches, and it enriches correctly
ok - a partial GitHub failure degrades gracefully
ok - Perl fallback bounds stalled GitHub calls without coreutils timeout
ok - all fleet-sized sections are capped with counted opt-in expansion
ok - live PR enrichment caps repositories with counted expansion
ok - per-repository open-PR caps are disclosed with an expansion knob
ok - projection and TOON rendering failures exit nonzero with diagnostics
CI STEP EXIT=0
Evidence: The CI guard is not vacuous: same sweep fails on the base tree

::error::FAIL bin/fm-brief.sh bin/fm-brief.sh: line 314: unexpected EOF while looking for matching ) base: 1 of 197 canonical scripts failed to parse under stock /bin/bash 3.2.57 => the CI guard would have failed the base tree, so it is not a vacuous proxy

=== Honesty check: the same CI sweep run against the BASE tree (b29621b) ===
The base tree has no --list, so the sweep is fed the identical canonical globs directly.
$ cd <base tree>; for f in bin/*.sh bin/backends/*.sh tests/*.sh; do /bin/bash -n $f; done
::error::FAIL bin/fm-brief.sh
bin/fm-brief.sh: line 314: unexpected EOF while looking for matching `)'
bin/fm-brief.sh: line 388: syntax error: unexpected end of file
base: 1 of 197 canonical scripts failed to parse under stock /bin/bash 3.2.57
=> the CI guard would have failed the base tree, so it is not a vacuous proxy
Evidence: CI sweep assertions each fire (missing path, empty list, unparseable script)

-- case 1: listed path does not exist -- CI STEP EXIT=1 ::error::canonical shell script bin/fm-ghost.sh does not exist -- case 2: list is empty -- CI STEP EXIT=1 ::error::bin/fm-lint.sh --list published no shell scripts -- case 3: a listed script does not parse under Bash 3.2 -- CI STEP EXIT=2 bin/fm-brief.sh: line 314: unexpected EOF while looking for matching )

=== The exact-count / path-existence assertions in the CI sweep are load-bearing ===
(target tree e864ac0, with bin/fm-lint.sh --list stubbed to simulate each failure mode)

-- case 1: the canonical list names a path that does not exist --
CI STEP EXIT=1
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
::error::canonical shell script bin/fm-ghost.sh does not exist

-- case 2: the canonical list is empty --
CI STEP EXIT=1
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
::error::bin/fm-lint.sh --list published no shell scripts

-- case 3: a listed script does not parse under Bash 3.2 (base fm-brief.sh dropped in) --
CI STEP EXIT=2
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
bin/fm-brief.sh: line 314: unexpected EOF while looking for matching `)'
bin/fm-brief.sh: line 388: syntax error: unexpected end of file
Evidence: Byte-truncation regression test killed by both behavioral mutations

--- unmutated target --- ok - registry byte truncation discards the partial final line and content without a newline --- mutation A: drop the partial-final-line discard --- not ok - registry byte truncation must keep only complete lines --- mutation B: keep the partial line when the window has no newline --- not ok - registry truncation before the first newline must discard all content

=== Byte-truncation regression test, end to end under stock /bin/bash 3.2.57 ===

--- unmutated target: the new case passes ---
$ /bin/bash tests/fm-fleet-snapshot-view.test.sh
ok - registry byte truncation discards the partial final line and content without a newline
exit=0

--- mutation A: drop the partial-final-line discard (keep the half-read record) ---
      byte_truncated=true
      content=$(printf "%s" "$content" | LC_ALL=C head -c "$max_bytes")
      complete=${content%$'\n'*}
      :
    fi
      byte_truncated=true
      complete=${content#*$'\n'}
      if [ "$complete" != "$content" ]; then
not ok - registry byte truncation must keep only complete lines: {

--- mutation B: keep the partial line when the window has no newline at all ---
      byte_truncated=true
      content=$(printf "%s" "$content" | LC_ALL=C head -c "$max_bytes")
      complete=${content%$'\n'*}
      if [ "$complete" != "$content" ]; then
        content=$complete
      fi
    fi
      byte_truncated=true
not ok - registry truncation before the first newline must discard all content: {
Evidence: Truncation contract through the product's own JSON under Bash 3.2

$ FM_SNAPSHOT_REGISTRY_BYTES=187 fm-fleet-snapshot.sh --json # cut lands mid-second-line { input_truncated: true, complete: false, reasons: [byte_limit], lines_in_window: 1, records: [first] } control: the discarded prefix parses as a whole record on its own: [{ id: second, registered: true, registry_error: null }] $ FM_SNAPSHOT_REGISTRY_BYTES=87 fm-fleet-snapshot.sh --json # no newline anywhere in the window { input_truncated: true, complete: false, reasons: [byte_limit], lines_in_window: 0, records: [] }

=== fm-fleet-snapshot byte-truncation contract, observed through the product's own JSON ===
interpreter: /bin/bash 3.2.57(1)-release

registry data/secondmates.md:
  - first (home: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-trunc-demo/first; scope: alpha)
  - second (home: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-trunc-demo/second; scope: beta)

$ FM_SNAPSHOT_REGISTRY_BYTES=187 fm-fleet-snapshot.sh --json   # cut lands mid-second-line
{
  "input_truncated": true,
  "complete": false,
  "reasons": [
    "byte_limit"
  ],
  "lines_in_window": 1,
  "records": [
    "first"
  ]
}

control: that discarded prefix parses as a whole record on its own, so the discard is real:
[{"id":"second","home":"/var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-trunc-demo/second","registered":true,"registry_error":null}]

$ FM_SNAPSHOT_REGISTRY_BYTES=87 fm-fleet-snapshot.sh --json   # no newline anywhere in the window
{
  "input_truncated": true,
  "complete": false,
  "reasons": [
    "byte_limit"
  ],
  "lines_in_window": 0,
  "records": []
}
Evidence: fm-fleet-view rendered end to end under stock Bash 3.2

bash: 3.2.57(1)-release # Fleet View Schema: fm-fleet-snapshot.v1 ## Under Way | ID | Current | Kind | Repo/Project | Backend | Endpoint | Artifact | Path | Watch / return channel | | cmux-task | unknown / none | ship | alpha | cmux | absent | - | ... (absent) | bin/fm-peek.sh fm-cmux-task | | scout-task | done / status-log | scout | alpha | tmux | present | .../report.md | ... | | secondmate-task | working / status-log | secondmate | ... | tmux | present / alive | - | ... | | ship-task | working / pane | ship | alpha | tmux | present | #9 | ... | ## Queued / ## Done / ## Secondmates rendered

=== fm-fleet-view.sh rendered end to end under the stock macOS interpreter ===
$ /bin/bash bin/fm-fleet-view.sh   (fleet fixture: ship, scout, secondmate, cmux tasks)

bash: 3.2.57(1)-release

# Fleet View

Schema: fm-fleet-snapshot.v1
Home: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-fleet-view-demo.PzrTxP/demo

## Under Way
| ID | Current | Kind | Repo/Project | Backend | Endpoint | Artifact | Path | Watch / return channel |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| cmux-task | unknown / none | ship | alpha | cmux | absent | - | /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-fleet-view-demo.PzrTxP/demo/projects/missing-cmux (absent) | bin/fm-peek.sh fm-cmux-task |
| scout-task | done / status-log | scout | alpha | tmux | present | /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-fleet-view-demo.PzrTxP/demo/data/scout-task/report.md | /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-fleet-view-demo.PzrTxP/demo/projects/scout-worktree | bin/fm-peek.sh fm-scout-task |
| secondmate-task | working / status-log | secondmate | /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-fleet-view-demo.PzrTxP/demo/secondmate-home | tmux | present / alive | - | /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-fleet-view-demo.PzrTxP/demo/secondmate-home | bin/fm-send.sh fm-secondmate-task '<request>' - read status/doc return channel; do not routinely fm-peek a secondmate for answers |
| ship-task | working / pane | ship | alpha | tmux | present | https://github.com/kunchenguid/firstmate/pull/9 | /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-fleet-view-demo.PzrTxP/demo/projects/alpha-worktree | bin/fm-peek.sh fm-ship-task |

## Queued
| ID | Title | Repo | Kind | Blocked By | Artifact |
| --- | --- | --- | --- | --- | --- |
| queued-task | Queued Task | alpha | ship | ship-task | - |
| - | handoff note without canonical syntax | - | - | - | - |

## Done
| ID | Title | Repo | Kind | Blocked By | Artifact |
| --- | --- | --- | --- | --- | --- |
| done-task | Done Task | alpha | ship | - | https://github.com/kunchenguid/firstmate/pull/7 |

## Secondmates
For kind=secondmate, bearings selects validated structured state from that registered home; parent events and bounded terminal evidence are fallback-only supplements and never current-state authority.

exit=0
Evidence: Generated brief wording byte-identical to base

$ diff base-vs-target generated brief wording identical (43 prose lines) $ diff base-vs-target generated config/x-mode.env identical

$ diff <(base heredoc prose) <(target heredoc prose)   # generated brief wording
identical (43 prose lines)

$ diff <(base fmx cadence body) <(target fmx cadence body)   # generated config/x-mode.env
identical
Evidence: Generated briefs for all three delivery modes
All three ship modes scaffolded end-to-end under stock /bin/bash 3.2.57.

$ FM_HOME=$W bin/fm-brief.sh d-nomistakes nm-proj
warn: project "nm-proj" not in registry; defaulting to no-mistakes off
scaffolded: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-nomistakes/brief.md (ship, mode=no-mistakes; replace {TASK})
--- Definition of done section of /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-nomistakes/brief.md ---
# Definition of done
The task is complete only when committed on your branch.
When you believe it is complete, append `done: {summary}` to the status file and stop.
Firstmate will then instruct you to run /no-mistakes to validate and ship a PR.

You drive no-mistakes by responding to its gates, not by implementing fixes.
Follow the guidance no-mistakes itself provides for the mechanics: it loads when you invoke /no-mistakes, and `no-mistakes axi run --help` plus the `help` lines in each `axi` response are authoritative and version-matched to the installed binary.
Do not hand-edit, commit, or fix findings yourself while a run is active - the pipeline applies every fix.

Two firstmate-specific rules layer on top of that guidance:
- ask-user findings are never yours to answer: escalate to firstmate (rule 6) and stop.
  Firstmate applies the authority contract in its `AGENTS.md` and obtains any required captain decision.
  When the decision comes back, feed it to the gate with `no-mistakes axi respond` and let the pipeline apply it - do not route the question to "the user" or implement the fix yourself.
- Avoid `--yes`: it would silently bypass firstmate's authority check and any required captain escalation.

After /no-mistakes reports CI green (the CI-ready return point - do not wait for it to keep monitoring in the background until merge), append `done: PR {url} checks green` and stop. You are finished.

$ FM_HOME=$W bin/fm-brief.sh d-directpr direct-proj
scaffolded: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-directpr/brief.md (ship, mode=direct-PR; replace {TASK})
--- Definition of done section of /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-directpr/brief.md ---
# Definition of done
This project ships **direct-PR**: you raise the PR yourself, without the no-mistakes pipeline.
The task is complete only when committed on your branch.
When it is implemented and committed, push your branch and open a PR with `gh-axi`, then append `done: PR {url}` to the status file and stop.
Do NOT run /no-mistakes. The configured merge authority decides whether to merge the PR; firstmate relays the outcome.

$ FM_HOME=$W bin/fm-brief.sh d-localonly local-proj
scaffolded: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-localonly/brief.md (ship, mode=local-only; replace {TASK})
--- Definition of done section of /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-localonly/brief.md ---
# Definition of done
This project ships **local-only**: no remote, no PR, no pipeline.
The task is complete only when committed on your branch `fm/d-localonly`. Do NOT push, do NOT open a PR, do NOT merge.
Keep your branch a clean fast-forward onto the current default branch - if `main` has advanced, rebase onto it so the eventual merge stays a fast-forward.
When it is implemented and committed, append `done: ready in branch fm/d-localonly` to the status file and stop.
The configured merge authority approves the ready branch, then firstmate merges it into local `main` through the guarded fast-forward path.

$ FM_HOME=$W bin/fm-brief.sh d-herdrguard someproj   # unguarded Herdr safety section
warn: project "someproj" not in registry; defaulting to no-mistakes off
scaffolded: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-herdrguard/brief.md (ship, mode=no-mistakes; replace {TASK})
# Herdr lifecycle declaration - NOT ENABLED
**HARD SAFETY GATE:** this scaffold cannot inspect the task text that replaces `{TASK}` later.
If the task will start, stop, delete, restart, profile, or otherwise drive Herdr lifecycle behavior, stop and regenerate the brief with `--herdr-lab` before dispatch.
Do not add Herdr lifecycle commands to this unguarded brief by hand.


$ grep -c EOF across generated briefs (must be 0 - no leaked heredoc markers)
/var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-herdrguard/brief.md:0
/var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-directpr/brief.md:0
/var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-localonly/brief.md:0
/var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T//fm-brief-modes/data/d-nomistakes/brief.md:0
Evidence: fm-lint.sh --list: the new canonical file-list surface

$ bin/fm-lint.sh --list | wc -l 197 $ diff sorted --list vs sorted on-disk set identical (order differs only by glob group) $ bin/fm-lint.sh --help fm-lint.sh --list print the canonical file set, one per line

=== bin/fm-lint.sh --list: the single canonical file-list owner (new CLI surface) ===
$ bin/fm-lint.sh --list | head -5
bin/fm-afk-launch.sh
bin/fm-afk-return.sh
bin/fm-afk-start.sh
bin/fm-arm-pretool-check.sh
bin/fm-backend-hometag-lib.sh
...
$ bin/fm-lint.sh --list | wc -l
197
$ bin/fm-lint.sh --list | sed 's:/[^/]*$::' | sort -u   # directories covered
bin
bin/backends
tests
$ diff <(bin/fm-lint.sh --list | sort) <(ls bin/*.sh bin/backends/*.sh tests/*.sh | sort)  # same set as on disk
identical (order differs only by glob group)

$ bin/fm-lint.sh --help   (usage must document --list)
Usage:
  fm-lint.sh                         lint the canonical file set
  fm-lint.sh <path>...               lint explicit roots with the same config
  fm-lint.sh --jobs <1|2> [path]...  override bounded worker count
  fm-lint.sh --telemetry <path> ...  write a quiet metrics snapshot
  fm-lint.sh --required-version      print the ShellCheck pin
  fm-lint.sh --list                  print the canonical file set, one per line
  fm-lint.sh --help                  print this usage

$ bin/fm-lint.sh --required-version
0.11.0

$ bin/fm-lint.sh --list  (under the stock interpreter, as CI consumes it)
197
lines published under /bin/bash 3.2.57(1)-release
Evidence: Round-1 Kimi finding fixed: gate-skips instead of hard-failing

host python3: 3.10.0 (no tomllib) $ tests/fm-kimi-harness.test.sh (target) skip: python3 lacks tomllib (required by fm-kimi-turnend-hook.sh; needs Python 3.11+) exit=0 $ tests/fm-kimi-harness.test.sh (base b29621b, same interpreter) not ok - Kimi hook install refused a realistic config $ bin/fm-test-run.sh tests/fm-kimi-harness.test.sh FM_TEST_END ... exit=0 duration_ms=40 gate_skip=true FM_TEST_SUMMARY total=1 failed=0 skipped_gate=1

=== Kimi harness test: gate-skip on a python3 without tomllib (this host: Python 3.10.0) ===
$ grep -n tomllib bin/fm-kimi-turnend-hook.sh
37:  printf 'fm-kimi-turnend-hook: refused: python3 with tomllib is required to validate config.toml.\n' >&2
54:    import tomllib
57:        "fm-kimi-turnend-hook: refused: python3 with tomllib is required to validate config.toml.",
120:        parsed = tomllib.loads(text)
121:    except tomllib.TOMLDecodeError as error:

$ tests/fm-kimi-harness.test.sh   (target)
skip: python3 lacks tomllib (required by fm-kimi-turnend-hook.sh; needs Python 3.11+)
exit=0

$ tests/fm-kimi-harness.test.sh   (base b29621b, same interpreter) - hard failure this change removes
ok - repository: tracked files contain no user-specific absolute paths
ok - fm-spawn: the five pre-existing adapters' launch templates stay byte-pinned
fm-kimi-turnend-hook: refused: python3 with tomllib is required to validate config.toml.
not ok - Kimi hook install refused a realistic config

-- the skip is a recognized gate skip for the suite runner, not a silent pass --
$ bin/fm-test-run.sh tests/fm-kimi-harness.test.sh
FM_TEST_BEGIN 2026-07-28T16:46:24Z tests/fm-kimi-harness.test.sh family=pure-contract-unit expected_gate_skip=none
skip: python3 lacks tomllib (required by fm-kimi-turnend-hook.sh; needs Python 3.11+)
FM_TEST_END 2026-07-28T16:46:24Z tests/fm-kimi-harness.test.sh exit=0 duration_ms=40 gate_skip=true
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=1 duration_ms=84

-- and it still fails loudly when tomllib IS available but the hook is broken --
Evidence: Independent Bash 3.2 hazard sweep of all 92 bin scripts

declare -A / local -A / mapfile / readarray / case-conversion expansions / globstar / append-both / wait -n / coproc : none unguarded BASHPID: none unguarded heredoc inside command substitution anywhere in bin/: none 92 bin scripts executed with --help under /bin/bash 3.2; 0 hit a parse/substitution error

=== Independent sweep of bin/ for Bash 4+ constructs against the 3.2 floor ===
scripts scanned: 92
declare -A                                 none
local -A                                   none
mapfile                                    none
readarray                                  none
\$\{[A-Za-z_][A-Za-z0-9_]*,,               none
\$\{[A-Za-z_][A-Za-z0-9_]*\^\^             none
shopt -s globstar                          none
&>>                                        none
wait -n                                    none
coproc                                     none

-- unguarded $BASHPID (3.2 has none; every use must be ${BASHPID:-$$}) --
none unguarded

-- heredoc inside command substitution anywhere in bin/ (the issue #166 construct) --
none

-- runtime smoke: every bin script's --help path executed by stock /bin/bash 3.2 (10s cap each) --
92 bin scripts executed with --help under /bin/bash 3.2; 0 hit a parse/substitution error
scripts with no --help fast path (alarmed at 10s, not a parse failure): bin/fm-supervise-daemon.sh
Evidence: Heredoc guard non-vacuity plus minimal Bash 3.2 repro

target: ok - no bin script builds a here-document inside a command substitution (92 scanned) mutated: not ok - bin/fm-x-lib.sh builds a here-document inside a command substitution ... minimal repro: one apostrophe inside a heredoc wrapped in a command substitution breaks parsing of the whole file under Bash 3.2, with quoted and unquoted delimiters alike

=== The new heredoc-in-command-substitution guard is non-vacuous ===
$ tests/fm-lint.test.sh   (target, unmutated)
ok - fm-lint.sh --list publishes the canonical file set for other gates
ok - CI's Bash 3.2 sweep is driven by the canonical file list
ok - no bin script builds a here-document inside a command substitution (92 scanned)

-- reintroduce the exact issue #166 construct in bin/fm-x-lib.sh, prose with an apostrophe --
$ /bin/bash -n bin/fm-x-lib.sh   (stock 3.2, mutated tree)
exit=0
$ tests/fm-lint.test.sh   (mutated tree)
not ok - bin/fm-x-lib.sh builds a here-document inside a command substitution, which stock Bash 3.2 mis-parses: 1002:  body=$(cat <<'EOF'

-- minimal repro of the underlying stock-Bash-3.2 defect (quoted and unquoted delimiter alike) --
$ cat r1.sh
V=$(cat <<EOF
firstmate's apostrophe
EOF
)
echo ok
$ /bin/bash -n r1.sh -> r1.sh: line 2: unexpected EOF while looking for matching `''
r1.sh: line 6: syntax error: unexpected end of file

$ cat r2.sh
V=$(cat <<'EOF'
firstmate's apostrophe
EOF
)
echo ok
$ /bin/bash -n r2.sh -> r2.sh: line 2: unexpected EOF while looking for matching `''
r2.sh: line 6: syntax error: unexpected end of file

Bash 3.2 leaks the apostrophe's quote state out of the here-document while it scans
for the command substitution's closing ')', so the breakage depends on apostrophe parity
across the file - which is why the guard bans the construct rather than only its symptom.
- Evidence: Full behavior suite log (bin/fm-test-run.sh --all, still running) (local file: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T/no-mistakes-evidence/01KYKD5T0ZCKN866TNFN28AAMX/09-full-suite.txt) - Outcome: ⚠️ 1 info across 2 runs (1h17m53s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 6 issues found → auto-fixed (2) ✅
  • ⚠️ bin/fm-brief.sh:220 - The change eliminates the issue-firstmate's fm-brief.sh scaffold script has a bug #166 heredoc-in-command-substitution hazard only for the three Definition-of-done blocks, but the identical pattern survives 60 lines above it in the same file (HERDR_SECTION=$(cat &lt;&lt;&#39;EOF&#39;) with a free-prose body, and in bin/fm-bootstrap.sh:690 (cadence_body=$(cat &lt;&lt;&#39;EOF&#39;, also prose) plus bin/fm-fleet-snapshot.sh:787/837/849/885. I reproduced on stock /bin/bash 3.2.57 that this construct fails to parse with a quoted delimiter too, so a single apostrophe added to any of these prose bodies re-breaks the whole file exactly as firstmate's fm-brief.sh scaffold script has a bug #166 did. These parse today only because their bodies happen to have balanced apostrophes. Meanwhile CONTRIBUTING.md:48 now states the rule absolutely ("never build a here-document inside a command substitution"), so the repo documents a rule its own bin/ scripts broadly violate. The new CI sweep would catch a regression, so this is a latent risk rather than a shipping defect - but the prose blocks are the ones most likely to gain an apostrophe, and converting them to the same function shape is mechanical and content-preserving. The intent permits reporting non-changes with reasoning, so confirm whether leaving these was deliberate.
  • ⚠️ CONTRIBUTING.md:76 - The documented pre-push syntax check runs bash -n, which resolves through PATH to Homebrew Bash 5.x on a typical dev Mac. Bash 5 parses all six VAR=$(cat &lt;&lt;...) blocks in this repo without complaint, so the exact failure this branch fixes (bin/fm-brief.sh under 3.2) passes this local check and only surfaces in CI - the snippet cannot guard the floor the same section declares two bullets earlier. Use /bin/bash -n so the check exercises the stock 3.2 interpreter on macOS (it degrades to a harmless no-op on Linux where /bin/bash is 5.x). Separately, the unquoted $(bin/fm-lint.sh --list) word-splits, so the snippet would break on any path containing whitespace; a while IFS= read -r loop matching the CI sweep is safer.
  • ℹ️ bin/fm-lint.sh:91 - The canonical file set is now spelled literally in two places: printf &#39;%s\n&#39; bin/*.sh bin/backends/*.sh tests/*.sh for --list and ROOTS=(bin/*.sh bin/backends/*.sh tests/*.sh) at line 163. The drift guard added in tests/fm-lint.test.sh:70 has to reverse-engineer the second spelling with sed -n &#39;s/^ *ROOTS=(\(.*\))$/\1/p&#39; and then eval it - the test is fail-closed so drift is caught, but the whole mechanism exists only because the definition is duplicated. Declaring it once after cd &#34;$ROOT&#34; (e.g. CANONICAL_ROOTS=(bin/*.sh bin/backends/*.sh tests/*.sh)) and having both --list and ROOTS expand &#34;${CANONICAL_ROOTS[@]}&#34; makes the one-owner claim structural instead of test-enforced, and lets the test drop the sed/eval reconstruction.
  • ℹ️ tests/fm-lint.test.sh:84 - assert_no_grep &#34;find bin -type f -name &#39;*.sh&#39;&#34; &#34;$CI&#34; forbids exactly one alternative spelling of the file set. Any other re-spelling - for f in bin/*.sh, git ls-files &#39;bin/*.sh&#39;, a hardcoded list - passes this assertion untouched, so it reads as a guard against re-spelling while enforcing nothing. The two positive assertions on the same test (the --list consumption and the exact parsed-count match) are the ones carrying real weight; this line mainly adds false confidence. Either drop it or replace it with an assertion that the macos-stock-bash step block contains no glob/find file-set expression at all.
  • ℹ️ tests/fm-session-start.test.sh:731 - The new pid-handoff wait while [ ! -s &#34;$handoff/$i&#34; ]; do sleep 0.01; done has no bound. If the parent dies between forking the subshell and completing the mv (or $home/handoff is not writable), all 40 contenders spin forever and the subsequent for pid in $pids; do wait ... blocks until the 10-minute CI job timeout rather than failing with a diagnosis. The handoff mechanism itself is correct - for ( ... ) &amp; the parent's $! is exactly the pid $BASHPID would have reported, the tmp+mv publication is atomic, and $handoff is a separate directory from $ready so it cannot perturb the existing readiness count. This matches the file's existing unbounded-wait style, so it is consistent rather than novel, but a bounded retry with an explicit fail would keep a broken handoff from presenting as a hang.
  • ℹ️ bin/fm-fleet-snapshot.sh:787 - Context for the reviewer, not a defect: the intent names fm-fleet-snapshot as the site of the hard parse failure, but bin/fm-fleet-snapshot.sh is untouched by this branch. I confirmed it already parses under real /bin/bash 3.2.57 at the base commit - that fix landed earlier in eb9ee2f (fix: restore fleet snapshots on stock macOS Bash #578). At base, the only file in the canonical set that fails 3.2 parsing is bin/fm-brief.sh, which this branch fixes, so the new CI gate is genuinely load-bearing rather than retroactively green. The intent's fm-fleet-snapshot clauses are satisfied: byte-truncation behavior is unchanged, and the new test_registry_byte_truncation_discards_partial_line covers both the discard-final-partial-line and empty-when-no-newline cases with an explicit control proving the truncated prefix parses as a record on its own. No required behavior is missing from the codebase.

🔧 Fix: eliminate bin heredoc-in-cmdsubst hazard, bound test barriers
2 issues (1 warning, 1 info) still open:

  • ⚠️ tests/fm-lint.test.sh:112 - The new guard filters to bin/* and skips every other path --list publishes, but the CI stock-Bash lane parses the whole canonical set - all 197 files including tests/*.sh - through the real 3.2 binary. I counted 53 surviving VAR=$(cat &lt;&lt;DELIM ...) blocks in tests/, and 6 of them already contain apostrophes: tests/fm-x-mode.test.sh:1071, :1126, :2234, :2361, :2704 and tests/fm-calm-pi-extension.test.sh:268. Those are free-form prose fixtures (X-mode reply text), exactly the bodies most likely to gain a seventh apostrophe. They parse today only because each has an even count; one more breaks bash -n on the whole test file, fails the macOS lane with a cryptic "unexpected EOF while looking for matching )" pointing at the wrong line, and the guard stays silent. CONTRIBUTING.md:48 declares the 3.2 floor for "both" bin/ and tests/, while line 49 narrows the guard's promise to "any bin/ script" - so the doc is accurate about the guard but the enforcement is narrower than the floor it protects. Dropping the bin/* filter would cover the full set the CI lane already parses. Flagging rather than fixing because the intent scoped the sweep to bin ("Sweep every script under bin"), so restricting the guard may be deliberate.
  • ℹ️ bin/fm-fleet-snapshot.sh:793 - The same fix was applied three different ways across this change. bin/fm-brief.sh hoists herdr_section_not_enabled to file scope and bin/fm-x-lib.sh hoists fmx_cadence_content to file scope, but here registry_reader_script, registry_parse_filter, and registry_output_filter are defined inside registry_secondmates_json(), and parent_activities_script inside bounded_parent_activities_json() at line 901. This is correct - bash defines a nested function globally when the enclosing function runs, and each is called immediately after its definition - but it re-executes the definitions on every snapshot call and leaks the four names into the global namespace after the first invocation, where they are callable but were never intended to be. It also makes the helpers invisible to anything that runs before the first registry_secondmates_json call. Hoisting them next to the other two file-scope emitters makes the whole change read uniformly and removes the per-call redefinition; the emitted bodies are already byte-identical so the move is purely structural.

🔧 Fix: hoist fleet-snapshot heredoc emitters to file scope
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 info
  • ⚠️ tests/fm-kimi-harness.test.sh - tests/fm-kimi-harness.test.sh fails because this machine default python3 is 3.10.0 and bin/fm-kimi-turnend-hook.sh needs tomllib from Python 3.11+. Unrelated to this change; no Kimi file is in the diff and tomllib is used only by that hook and its test. Retried with python3.12 on PATH it passes all 19 assertions. It hard-fails rather than gate-skipping on an unsupported interpreter, unlike other optional-dependency tests here.
  • ℹ️ bin/fm-lint.sh - The pinned-lint leg of the intent was not exercised: bin/fm-lint.sh runs ShellCheck and this run may not execute linters or static analysis. Only bin/fm-lint.sh --list was used, which just prints the canonical file set. The CI lint job still covers it.
  • /bin/bash --version confirms genuine stock 3.2.57 release with no Homebrew bash on this machine
  • Baseline sweep: extracted all 92 base-commit bin shell scripts and ran /bin/bash -n on each, yielding 1 hard parse failure in bin/fm-brief.sh
  • Replayed the ci.yml macos-stock-bash sweep verbatim: bin/fm-lint.sh --list published 197 paths, all existence-checked and parsed, exact parsed vs expected count match
  • CI-guard mutation 1: restored the pre-fix base bin/fm-brief.sh, sweep exits 1
  • CI-guard mutation 2: re-inlined the fleet-snapshot emitter as a heredoc inside a command substitution with one apostrophe; fm-fleet-view exits 2, sweep exits 1, tests/fm-lint.test.sh reports not ok
  • CI-guard mutation 3: made CANONICAL_ROOTS publish a nonexistent path, sweep exits 1
  • End-to-end: built a realistic FM_HOME with backlog, ship/scout/secondmate metas, secondmate registry and fake tmux, then ran /bin/bash bin/fm-fleet-view.sh and fm-fleet-view.sh --json, full rendered fleet, exit 0
  • /bin/bash tests/fm-fleet-snapshot-view.test.sh produced exactly 16 ok lines, matching the count CI asserts
  • Truncation transcript under 3.2: control prefix parses alone, byte cut mid-line discards the partial line, byte cut before any newline empties the window
  • Truncation non-vacuity: deleting the partial-line discard, and separately dropping the no-newline-empties branch, each fail the new regression test
  • /bin/bash -n bin/fm-brief.sh plus generated briefs for no-mistakes, direct-PR and local-only, and the Herdr NOT-ENABLED safety gate
  • Compared every here-document body in bin/fm-brief.sh between base and head: byte-identical
  • Compared the generated config/x-mode.env body, base inline heredoc vs fmx_cadence_content executed under 3.2: identical
  • fm-spawn.sh metadata: old brace-group redirect continues past a failed write under set -e with exit 0, new form aborts with could not publish task metadata and exit 1, byte-identical output on the happy path
  • Bash 4+ construct sweep over every bin path from --list covering associative arrays, case modification, mapfile, stderr pipe, append-both, coproc, negative index, globstar and printf time format: clean, every BASHPID guarded, greps verified non-vacuous against a Bash-4-only probe
  • Confirmed BASHPID is unset on 3.2, justifying the lock-concurrency pid-handoff change
  • bin/fm-test-run.sh --all produced FM_TEST_SUMMARY total=99 failed=1 skipped_gate=10
  • With python3.12 first on PATH, /bin/bash tests/fm-kimi-harness.test.sh passes all 19 assertions, exit 0
  • Post-run hygiene: git status --porcelain empty including --ignored, and no leftover fm-lab herdr sessions

🔧 Fix: gate-skip Kimi harness test when python3 lacks tomllib
1 info still open:

  • ℹ️ tests - The full bin/fm-test-run.sh --all regression run was still in progress when this report was produced - 13 of 99 test scripts complete with zero failures, currently in the real-Herdr E2E lane, which makes the local run take roughly 32 minutes. Every other intent leg was validated to completion. The previous round's identical full run on this same host produced exactly one failure, the Kimi tomllib hard-fail, which this change fixes and which I verified directly in isolation (it now reports gate_skip=true). Live log: /var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T/no-mistakes-evidence/01KYKD5T0ZCKN866TNFN28AAMX/09-full-suite.txt
  • /bin/bash --version on host: GNU bash 3.2.57(1)-release, and PATH bash resolves to the same binary
  • base b29621b: bin/fm-brief.sh fm-demo-1 firstmate under stock Bash 3.2 reproduces the unexpected-EOF failure and writes no brief
  • target e864ac0: bin/fm-brief.sh fm-demo-1 firstmate scaffolds brief.md, exit 0
  • /bin/bash -n bin/fm-brief.sh at base (exit 2) vs target (exit 0)
  • diff of base vs target generated brief prose and x-mode.env cadence body: byte-identical
  • bin/fm-brief.sh for no-mistakes, direct-PR and local-only modes plus the unguarded-Herdr block
  • ci.yml job macos-stock-bash step extracted verbatim and executed: 197 scripts parsed, 16 snapshot/view plus 42 Bearings tests, exit 0
  • same sweep against the base tree: 1 of 197 canonical scripts fails to parse
  • CI sweep with fm-lint.sh --list stubbed: nonexistent path, empty list, and unparseable listed script all fail the step
  • /bin/bash tests/fm-fleet-snapshot-view.test.sh: test_registry_byte_truncation_discards_partial_line passes
  • mutation A: partial-final-line discard removed from bin/fm-fleet-snapshot.sh, new test fails
  • mutation B: empty-when-no-newline branch removed, new test fails
  • fm-fleet-snapshot.sh --json with FM_SNAPSHOT_REGISTRY_BYTES at and below the second record boundary, plus the parseable-prefix control
  • /bin/bash bin/fm-fleet-view.sh end to end against a ship/scout/secondmate/cmux fixture
  • bin/fm-lint.sh --list (197 paths, matches on-disk set), --help, --required-version, and --list under /bin/bash
  • ./tests/fm-lint.test.sh: new --list, CI-sweep and heredoc-guard cases
  • heredoc-in-command-substitution reintroduced into bin/fm-x-lib.sh in a scratch tree: tests/fm-lint.test.sh fails
  • minimal repro of the Bash 3.2 defect with quoted and unquoted heredoc delimiters
  • grep sweep of all 92 bin scripts for Bash 4+ constructs, unguarded BASHPID and the issue #166 construct: all clean
  • every bin script executed with --help under /bin/bash 3.2: no parse or substitution errors
  • ./tests/fm-kimi-harness.test.sh on Python 3.10 (target gate-skips, base hard-fails) and bin/fm-test-run.sh tests/fm-kimi-harness.test.sh showing gate_skip=true
  • bin/fm-test-run.sh --all: full behavior suite, still running at report time (13 of 99 scripts, 0 failures)
⚠️ **Document** - 1 info
  • ℹ️ .agents/skills/firstmate-coding-guidelines/SKILL.md:97 - Placement judgment call, left unchanged and proposed as follow-up: the new hard invariant "never build a here-document inside a command substitution" lives only in CONTRIBUTING.md:48-49, but CONTRIBUTING.md:63 mandates that agents load .agents/skills/firstmate-coding-guidelines/SKILL.md before any tracked change, and that skill's "Repo style rules" section (line 97-98) carries the sibling shell rules (shellcheck-clean bin scripts, run fm-lint.sh) without mentioning the Bash 3.2 floor. ShellCheck and Bash 5 both accept the construct, so the skill-only path gives no warning. This is a gap rather than staleness - the construct is enforced by tests/fm-lint.test.sh with a self-explanatory failure message, so nothing is currently wrong - and adding a pointer line to an always-loaded skill is an addition the placement policy asks to weigh deliberately. Suggested follow-up: one pointer sentence in SKILL.md's repo style rules to CONTRIBUTING's shell portability rules, not a copy of them.
⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

@tpavay
tpavay force-pushed the fm/fix-fleetview-bash32-g8 branch from 85289c7 to 38259d2 Compare July 31, 2026 03:55
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: closing this as stale. It has been waiting on a contributor update for 14+ days with no author push or comment. Reopen if you want to pick it back up.

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