Conversation
tpavay
force-pushed
the
fm/fix-fleetview-bash32-g8
branch
from
July 28, 2026 18:51
16b5136 to
0ce11e5
Compare
This was referenced Jul 29, 2026
tpavay
force-pushed
the
fm/fix-fleetview-bash32-g8
branch
from
July 30, 2026 02:40
0ce11e5 to
85289c7
Compare
tpavay
force-pushed
the
fm/fix-fleetview-bash32-g8
branch
from
July 31, 2026 03:55
85289c7 to
38259d2
Compare
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
VAR=$(cat <<EOF ...)construct inbin/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.shand converts the registry reader, jq filters, and parent-activity reader inbin/fm-fleet-snapshot.shplus the x-mode cadence body now owned byfmx_cadence_contentinbin/fm-x-lib.sh; generated brief prose andconfig/x-mode.envoutput 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 underset -e; it now renders the body first and aborts with a diagnostic if publication fails.bin/fm-lint.shdeclares the canonical file set once asCANONICAL_ROOTSand publishes it through a new--listflag, 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/bash3.2 binary, and requires an exact nonzero parsed-file count.tests/fm-lint.test.shguards the list against drift and fails anybin/script that reintroduces the heredoc-in-command-substitution construct; CONTRIBUTING documents the 3.2 floor, the--listownership rule, and a pre-push syntax check that uses/bin/bashrather than PATHbash.The sweep found no other supported-platform defects in
bin/: no Bash 4+ constructs and no unguarded$BASHPID. The surviving instances of the construct intests/were left alone because the authorized sweep was scoped tobin/, and CI's stock-Bash lane already parses those files.$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; andtests/fm-kimi-harness.test.shgate-skips whenpython3lackstomllib(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: 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.mdEvidence: 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=0Evidence: 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 proxyEvidence: 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 )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 contentEvidence: 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: [] }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 renderedEvidence: 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 identicalEvidence: Generated briefs for all three delivery modes
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 lineEvidence: 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=1Evidence: 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 errorEvidence: 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/var/folders/m3/qgj4gq85293_kqsxz93qlp3m0000gn/T/no-mistakes-evidence/01KYKD5T0ZCKN866TNFN28AAMX/09-full-suite.txt) - Outcome: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 <<'EOF') with a free-prose body, and in bin/fm-bootstrap.sh:690 (cadence_body=$(cat <<'EOF', 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 runsbash -n, which resolves through PATH to Homebrew Bash 5.x on a typical dev Mac. Bash 5 parses all sixVAR=$(cat <<...)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 -nso 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; awhile IFS= read -rloop matching the CI sweep is safer.bin/fm-lint.sh:91- The canonical file set is now spelled literally in two places:printf '%s\n' bin/*.sh bin/backends/*.sh tests/*.shfor --list andROOTS=(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 withsed -n 's/^ *ROOTS=(\(.*\))$/\1/p'and thenevalit - the test is fail-closed so drift is caught, but the whole mechanism exists only because the definition is duplicated. Declaring it once aftercd "$ROOT"(e.g.CANONICAL_ROOTS=(bin/*.sh bin/backends/*.sh tests/*.sh)) and having both --list and ROOTS expand"${CANONICAL_ROOTS[@]}"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 "find bin -type f -name '*.sh'" "$CI"forbids exactly one alternative spelling of the file set. Any other re-spelling -for f in bin/*.sh,git ls-files 'bin/*.sh', 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--listconsumption 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 waitwhile [ ! -s "$handoff/$i" ]; do sleep 0.01; donehas no bound. If the parent dies between forking the subshell and completing themv(or $home/handoff is not writable), all 40 contenders spin forever and the subsequentfor 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( ... ) &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 explicitfailwould 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 tobin/*and skips every other path--listpublishes, but the CI stock-Bash lane parses the whole canonical set - all 197 files includingtests/*.sh- through the real 3.2 binary. I counted 53 survivingVAR=$(cat <<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 breaksbash -non 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 "anybin/script" - so the doc is accurate about the guard but the enforcement is narrower than the floor it protects. Dropping thebin/*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 hoistsherdr_section_not_enabledto file scope and bin/fm-x-lib.sh hoistsfmx_cadence_contentto file scope, but hereregistry_reader_script,registry_parse_filter, andregistry_output_filterare defined insideregistry_secondmates_json(), andparent_activities_scriptinsidebounded_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 firstregistry_secondmates_jsoncall. 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.
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 machineBaseline 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.shReplayed 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 matchCI-guard mutation 1: restored the pre-fix base bin/fm-brief.sh, sweep exits 1CI-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 okCI-guard mutation 3: made CANONICAL_ROOTS publish a nonexistent path, sweep exits 1End-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 assertsTruncation transcript under 3.2: control prefix parses alone, byte cut mid-line discards the partial line, byte cut before any newline empties the windowTruncation 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 gateCompared every here-document body in bin/fm-brief.sh between base and head: byte-identicalCompared the generated config/x-mode.env body, base inline heredoc vs fmx_cadence_content executed under 3.2: identicalfm-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 pathBash 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 probeConfirmed BASHPID is unset on 3.2, justifying the lock-concurrency pid-handoff changebin/fm-test-run.sh --all produced FM_TEST_SUMMARY total=99 failed=1 skipped_gate=10With python3.12 first on PATH, /bin/bash tests/fm-kimi-harness.test.sh passes all 19 assertions, exit 0Post-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 binarybase b29621b: bin/fm-brief.sh fm-demo-1 firstmate under stock Bash 3.2 reproduces the unexpected-EOF failure and writes no brieftarget 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-identicalbin/fm-brief.sh for no-mistakes, direct-PR and local-only modes plus the unguarded-Herdr blockci.yml job macos-stock-bash step extracted verbatim and executed: 197 scripts parsed, 16 snapshot/view plus 42 Bearings tests, exit 0same sweep against the base tree: 1 of 197 canonical scripts fails to parseCI 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 passesmutation A: partial-final-line discard removed from bin/fm-fleet-snapshot.sh, new test failsmutation B: empty-when-no-newline branch removed, new test failsfm-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 fixturebin/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 casesheredoc-in-command-substitution reintroduced into bin/fm-x-lib.sh in a scratch tree: tests/fm-lint.test.sh failsminimal repro of the Bash 3.2 defect with quoted and unquoted heredoc delimitersgrep sweep of all 92 bin scripts for Bash 4+ constructs, unguarded BASHPID and the issue #166 construct: all cleanevery 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=truebin/fm-test-run.sh --all: full behavior suite, still running at report time (13 of 99 scripts, 0 failures).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.✅ **Push** - passed
✅ No issues found.