Skip to content

fix(bin): repair fm-brief.sh for macOS Bash 3.2 and guard bin/ scripts in CI#979

Open
hnvivek wants to merge 2 commits into
kunchenguid:mainfrom
hnvivek:fm/fm-brief-bash32-fix
Open

fix(bin): repair fm-brief.sh for macOS Bash 3.2 and guard bin/ scripts in CI#979
hnvivek wants to merge 2 commits into
kunchenguid:mainfrom
hnvivek:fm/fm-brief-bash32-fix

Conversation

@hnvivek

@hnvivek hnvivek commented Jul 24, 2026

Copy link
Copy Markdown

Intent

Repair fm-brief.sh so it parses under stock macOS Bash 3.2 (regression introduced in PR #945), and add CI protection so the same class of bash-3.2-only syntax regression in any tracked bin/ shell script is caught before merge.

What Changed

  • Fixed the syntax in bin/fm-brief.sh that failed to parse under stock macOS Bash 3.2 (regression introduced in PR fix: enforce contract boundaries for ask-user findings #945).
  • Added a CI step that runs /bin/bash -n across all tracked bin/*.sh and bin/backends/*.sh scripts so bash-3.2-only syntax regressions are caught before merge.
  • Updated the CONTRIBUTING.md local-validation example to use /bin/bash -n, pinning the check to the macOS stock shell instead of a brew Bash 5.

Risk Assessment

✅ Low: Well-bounded regression fix verified to produce byte-identical brief output under bash 3.2 and bash 5, with the same class of bug now blocked by an expanded macOS-stock-bash CI loop over every tracked bin/*.sh script.

Testing

On macOS stock Bash 3.2.57: regressed base fm-brief.sh reproduces the issue-#958 parse error, target fm-brief.sh parses clean, end-to-end scaffolds for all three delivery modes produce byte-correct DOD output under /bin/bash, and the new CI loop passes all 92 tracked bin/ scripts while faithfully catching the actual pre-fix fm-brief.sh as a regression.

Evidence: Negative CI-loop test (catches the actual pre-fix fm-brief.sh)

bin/fm-brief-regressed.sh: line 314: unexpected EOF while looking for matching `)' bin/fm-brief-regressed.sh: line 388: syntax error: unexpected end of file ::error::bin/fm-brief-regressed.sh fails /bin/bash -n (bash 3.2 parse regression) FAIL (correct: regression caught)

bin/fm-brief-regressed.sh: line 314: unexpected EOF while looking for matching `)'
bin/fm-brief-regressed.sh: line 388: syntax error: unexpected end of file
::error::bin/fm-brief-regressed.sh fails /bin/bash -n (bash 3.2 parse regression)
FAIL (correct: regression caught)
Evidence: fm-brief.sh direct-PR mode stdout (target fix, stock /bin/bash 3.2)
scaffolded: /tmp/fm-brief-evidence-home/data/direct-task/brief.md (ship, mode=direct-PR; replace {TASK})
Evidence: fm-brief.sh local-only mode stdout (target fix, stock /bin/bash 3.2)
scaffolded: /tmp/fm-brief-evidence-home/data/local-task/brief.md (ship, mode=local-only; replace {TASK})
Evidence: fm-brief.sh no-mistakes (default) mode stdout (target fix, stock /bin/bash 3.2)
scaffolded: /tmp/fm-brief-evidence-home/data/default-task/brief.md (ship, mode=no-mistakes; replace {TASK})
Evidence: Extracted DOD body — direct-PR brief
# 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.
Evidence: Extracted DOD body — local-only brief
# 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/local-task`. 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/local-task` 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.
Evidence: Extracted DOD body — no-mistakes brief
# 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.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • /bin/bash -n bin/fm-brief.sh (target) — parses OK under macOS stock Bash 3.2.57
  • /bin/bash -n bin/fm-brief.sh (base commit 10ee779, restored) — reproduces issue #958: line 314: unexpected EOF while looking for matching )`'
  • /bin/bash -c '… loop …' against bin/*.sh bin/backends/*.sh — all 92 tracked scripts parse OK
  • Negative CI-loop test: loop vs. the actual pre-fix fm-brief.sh — caught with ::error::… fails /bin/bash -n and exit 1
  • End-to-end: /bin/bash bin/fm-brief.sh <id> <project> for each of directproj / localproj / defaultproj (via a scratch projects.md registry) — all exit 0, briefs contain correct DOD body per mode, trailing newline correctly stripped (no blank line)
  • FM_HOME=<scratch> /bin/bash bin/fm-project-mode.sh <project> — resolves direct-PR / local-only / no-mistakes correctly for the three fixtures
⚠️ **Document** - 1 info
  • ℹ️ docs/arm-pretool-check.md:249 - docs/arm-pretool-check.md:249, docs/subagent-guard.md:352, and docs/cd-guard.md:133 each show a scoped local-validation snippet bash -n bin/<one-script>.sh. This change establishes a principle in CONTRIBUTING.md:73 that local syntax checks should use /bin/bash -n to pin macOS to stock Bash 3.2. These three snippets still use plain bash -n, which on a macOS machine with brew Bash 5 would not catch a bash-3.2-only regression. They are not factually stale (bash -n still validates syntax) and serve a different purpose than the CONTRIBUTING toolbelt loop, so they are out of this change's scope, but a follow-up could align them with the new principle for consistency.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

hnvivek added 2 commits July 24, 2026 08:53
The three ship-mode Definition-of-done blocks built the brief text with
`DOD=$(cat <<EOF ... EOF)` nested inside `case`. Bash 3.2's lexer loses
quote state across a heredoc-in-$(...) inside `case`, so a single
apostrophe anywhere in the body (reintroduced in kunchenguid#945) aborted the whole
script with "unexpected EOF while looking for matching ')'". The scout
and secondmate paths use a plain `cat > file <<EOF` redirect (no $(...))
and were unaffected.

Rewrite each DOD block as `IFS= read -r -d '' DOD <<EOF || true` plus a
single `${DOD%$'\n'}` strip. Byte-for-byte identical brief output proven
against the bash-5 baseline for no-mistakes, direct-PR, local-only, and
scout scaffolds, under both /bin/bash 3.2 and bash 5.

Also extend the existing macos-stock-bash CI job to loop /bin/bash -n
over every bin/*.sh and bin/backends/*.sh, so any future bash-3.2-only
syntax regression in any tracked shell script fails before merge.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant