diff --git a/.agents/skills/firstmate-coding-guidelines/SKILL.md b/.agents/skills/firstmate-coding-guidelines/SKILL.md index 83ab1d2a..ef47bca5 100644 --- a/.agents/skills/firstmate-coding-guidelines/SKILL.md +++ b/.agents/skills/firstmate-coding-guidelines/SKILL.md @@ -74,6 +74,9 @@ Firstmate adds this skill's load instruction to firstmate-repo briefs by hand in - Never add an agent name as a commit co-author. - `bin/*.sh` and `bin/backends/*.sh` must pass `shellcheck`. - Run `shellcheck bin/*.sh bin/backends/*.sh tests/*.sh` before treating a script change as done. +- Scripts must parse under macOS `/bin/bash` (bash 3.2), not just modern bash on Linux CI. +- Never assign a heredoc through command substitution (`VAR=$(cat <.test.sh`, and extend an existing script rather than inventing a new runner. - A backend-verification doc (`docs/*-backend.md`) records empirical facts, not assumptions. - Include the date, version, exact commands run, and exact output. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e066fa8..ab49e6f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,7 +88,7 @@ tests/fm-grok-harness.test.sh # grok adapter spawn hook, token guard tests/fm-fleet-sync.test.sh # project clone refresh: safe detached recovery, STUCK drift reports, benign skips, and bootstrap relay tests/fm-x-mode.test.sh # X-mode poll, inbox context round-trip, reply threading, dismiss, completion follow-up counters/caps, dry-run preview, and .env-presence activation tests tests/fm-tangle-guard.test.sh # primary-checkout tangle detection, read-only remediation suppression, and spawn/brief isolation tests -tests/fm-brief.test.sh # fm-brief.sh bash -n parse regression guard (issue #166) and clean no-mistakes/direct-PR/local-only brief generation tests +tests/fm-brief.test.sh # fm-brief.sh bash -n parse regression guard (issue #166, also run under /bin/bash), a structural guard refusing $(cat <<...) across bin/*.sh and bin/backends/*.sh, and clean no-mistakes/direct-PR/local-only brief generation tests tests/fm-spawn-batch.test.sh # batch dispatch and FM_HOME project-path scoping tests tests/fm-spawn-dispatch-profile.test.sh # concrete dispatch profile flags: active-profile backstop, harness/model/effort meta, launch templates, batch forwarding, and secondmate exemption tests/fm-update.test.sh # fast-forward-only self-update, reread, nudge, dedup, and skip-safety tests diff --git a/bin/fm-bootstrap.sh b/bin/fm-bootstrap.sh index c28814dd..c9b57ef8 100755 --- a/bin/fm-bootstrap.sh +++ b/bin/fm-bootstrap.sh @@ -295,25 +295,23 @@ x_mode_setup() { mkdir -p "$STATE" "$CONFIG" 2>/dev/null || { fmx_arm_failed; return 0; } - shim_body=$(cat </dev/null || { fmx_arm_failed; return 0; } - cadence_body=$(cat <<'EOF' + read -r -d '' cadence_body <<'EOF' || true # Auto-generated by fm-bootstrap.sh - X mode watcher cadence. # Source this before arming the watcher (see AGENTS.md "X mode") so fm-watch.sh # polls the X check every 30s. Non-X instances have no such file and keep the # default 300s cadence. export FM_CHECK_INTERVAL=30 EOF -) write_if_changed "$cadence" "$cadence_body" || { fmx_arm_failed; return 0; } echo "FMX: X mode on - relay poll armed via state/x-watch.check.sh; 30s watcher cadence in config/x-mode.env" diff --git a/bin/fm-brief.sh b/bin/fm-brief.sh index e0f2ec78..b1dcf9f0 100755 --- a/bin/fm-brief.sh +++ b/bin/fm-brief.sh @@ -171,23 +171,26 @@ read -r MODE _ < file < file <&1 || fail "bin/fm-brief.sh fails bash -n (heredoc/quote regression)" + if [ -x /bin/bash ]; then + /bin/bash -n "$ROOT/bin/fm-brief.sh" 2>&1 || fail "bin/fm-brief.sh fails /bin/bash -n (bash 3.2 heredoc/quote regression)" + fi pass "fm-brief.sh: bash -n succeeds" } +# Structural guard for issue #166: heredocs nested inside \$(cat <<...) are a +# bash-3.2 parse trap that Linux CI's modern bash cannot detect via `bash -n`, +# so refuse the construct itself anywhere in bin/ (including bin/backends/). +test_no_heredoc_in_command_substitution() { + local script + for script in "$ROOT"/bin/*.sh "$ROOT"/bin/backends/*.sh; do + # shellcheck disable=SC2016 # literal grep pattern; expansion is not wanted + if grep -n '^[^#]*\$(cat <<' "$script"; then + fail "${script#"$ROOT"/} uses \$(cat <<...) - breaks bash 3.2 parsing if the body ever gains an apostrophe; assign with read -r -d '' instead" + fi + done + pass "bin/*.sh, bin/backends/*.sh: no heredoc-in-command-substitution construct" +} + # Registry with one project per delivery mode, so each ship-mode DOD branch is # exercised. A project absent from the registry defaults to no-mistakes. write_registry() { @@ -77,5 +97,6 @@ test_no_mistakes_dod_wording() { } test_script_parses +test_no_heredoc_in_command_substitution test_ship_modes_generate_clean_briefs test_no_mistakes_dod_wording