Skip to content

fix(fm-brief): remove heredoc-in-command-substitution that breaks bash 3.2#989

Open
sandersjdd3 wants to merge 1 commit into
kunchenguid:mainfrom
sandersjdd3:fm/fm-brief-bash32-fix
Open

fix(fm-brief): remove heredoc-in-command-substitution that breaks bash 3.2#989
sandersjdd3 wants to merge 1 commit into
kunchenguid:mainfrom
sandersjdd3:fm/fm-brief-bash32-fix

Conversation

@sandersjdd3

Copy link
Copy Markdown

Intent

Fix a bash 3.2 parse regression in bin/fm-brief.sh (issue #958) that broke ALL crewmate/scout brief scaffolding on stock macOS. Symptom: /bin/bash -n bin/fm-brief.sh (macOS /bin/bash is 3.2.57) fails with 'unexpected EOF while looking for matching )'. Root cause: bash 3.2 misreads a lone apostrophe inside a heredoc body nested in $(...) as an unterminated single-quoted string, scans to EOF, and aborts; bash 4+ parses it literally so CI and modern dev machines passed. #945 added a line containing 'firstmate's' to a DOD=$(cat <<EOF ...) block, triggering it.

Deliberate design decisions in this fix: (1) The fix is DURABLE/structural, not a reword. I eliminated the VAR=$(cat <<EOF ... EOF) command-substitution-around-heredoc pattern entirely for the affected assignments rather than just rewording the one apostrophe, because a reword leaves the same trap for the next apostrophe. The mode case now sets only SETUP2/RULE1 scalars; the mode-specific Definition-of-done text is appended after the main brief with a plain 'cat >> "$BRIEF" <<EOF' redirection heredoc (NOT wrapped in $(...)), which parses fine on 3.2. (2) I also converted the unguarded Herdr section from HERDR_SECTION=$(cat <<'EOF') to printf, matching its --herdr-lab counterpart which already used printf. This was intentional even though that block has no apostrophe today: I tested the counterfactual and confirmed BOTH quoted (<<'EOF') and unquoted heredoc-in-$() delimiters trip the bash 3.2 bug, so the quoted delimiter was a latent trap, not safe. (3) Brief CONTENT is byte-for-byte unchanged across all delivery modes (no-mistakes, direct-PR, local-only, scout) and the Herdr sections; #945's wording is preserved verbatim. I am fixing parsing only, not reverting or changing content. Verified by generating real briefs in a throwaway home and confirming identical output and boundaries.

Regression test: extended the existing tests/fm-brief.test.sh (colocated *.test.sh pattern). Strengthened test_script_parses to run 'bash -n' under EVERY available bash on the box (dedup by resolved path), because the #958 variant only surfaces on stock bash 3.2 and a modern-bash bash -n passes the exact input that breaks a stock-macOS firstmate. Added test_no_heredoc_in_command_substitution: a version-independent structural guard grepping for the '=$(cat <<' pattern in code lines (comment lines excluded) so the hazard cannot return even on modern-bash CI where the parse check would pass. Both tests fail on pre-fix code and pass on the fix; verified. bin/fm-lint.sh passes clean (shellcheck 0.11.0 pinned).

What Changed

  • Eliminated the VAR=$(cat <<EOF ... EOF) command-substitution-around-heredoc pattern in bin/fm-brief.sh that bash 3.2 (stock macOS /bin/bash 3.2.57) misparses as an unterminated single-quoted string on a lone apostrophe, aborting brief scaffolding with unexpected EOF while looking for matching ). The mode case now sets only SETUP2/RULE1 scalars, and the mode-specific Definition-of-done text is appended via a plain cat >> "$BRIEF" <<EOF redirection heredoc; the unguarded Herdr section was converted from a quoted-delimiter heredoc-in-$() to printf, matching its --herdr-lab counterpart. Generated brief content is byte-for-byte unchanged across all delivery modes and Herdr sections.
  • Strengthened tests/fm-brief.test.sh: test_script_parses now runs bash -n under every distinct bash on the box (deduped by resolved path) so the 3.2-only variant is caught, and a new test_no_heredoc_in_command_substitution grep guard fails on the =$(cat << hazard pattern in code lines regardless of the running bash version.

Risk Assessment

✅ Low: Parse-only bash 3.2 fix: verified OLD fails and NEW passes bash -n on stock /bin/bash, all Definition-of-done and Herdr content is byte-for-byte identical to the prior version, and the added regression tests correctly fail pre-fix and pass post-fix.

Testing

Reproduced #958 on the box's stock macOS bash 3.2.57 (the only bash present, and the exact broken environment): base commit's bin/fm-brief.sh fails bash -n with 'unexpected EOF while looking for matching )', while the target commit parses cleanly. Ran the full tests/fm-brief.test.sh suite (16/16 pass) and independently confirmed both regression guards catch the bug on pre-fix code (structural grep finds 4 heredoc-in-command-substitution hazard lines on base, 0 on fix). Captured the real end-user artifact — a generated no-mistakes ship brief produced on bash 3.2.57 — showing clean output with no heredoc corruption and the #945 apostrophe wording preserved verbatim. Not a UI change, so no visual artifact; the reviewer-visible artifact is the generated brief markdown.

Evidence: Generated no-mistakes ship brief (produced on stock macOS bash 3.2.57 — the operation that was fully broken pre-fix)
You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task
{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.

# Setup
You are in a disposable git worktree of my-project, at a detached HEAD on a clean default branch.

**Verify isolation before anything else.** Run `pwd -P` and `git rev-parse --show-toplevel`; both must resolve to the disposable task worktree you were launched in, such as a treehouse pool path or an Orca-managed worktree, not the primary checkout firstmate operates from.
The path check is authoritative: `git rev-parse --git-dir` and `git rev-parse --git-common-dir` can help inspect the repo, but they do not prove you are outside the primary checkout.
If the top-level path is the primary checkout or not the worktree you were launched in, STOP - do not branch or commit here - append `blocked: launched in primary checkout, not an isolated worktree` to the status file and stop.

1. First action: create your branch: `git checkout -b fm/demo-task-001`
2. Run `no-mistakes doctor`; if it reports the repo is not initialized here, run `no-mistakes init`.

# Rules
1. Never push to the default branch. Never merge a PR.
2. Stay inside this worktree; modify nothing outside it.
3. Use gh-axi for GitHub operations and chrome-devtools-axi for browser operations.
4. Report status by appending one line:
   `echo "{state}: {one short line}" >> '/var/folders/p7/t5xyy71x0yxf_z56bmgdyrsh0000gn/T/tmp.tnjh3l0KO8/state/demo-task-001.status'`
   States: working, needs-decision, blocked, paused, done, failed.
   Each append wakes firstmate, so report sparingly: only phase changes a supervisor
   would act on (setup done, bug reproduced, fix implemented, validation passed) and the
   needs-decision/blocked/paused/done/failed states. No step-by-step FYI progress lines;
   firstmate reads your pane for that.
   A mid-task `working:` line (including setup complete) is nonterminal: do not end the
   turn after it; continue the same stage until a defined `done:` gate under Definition of done.
   Use `paused: {why}` - distinct from `blocked:` - ONLY when you are deliberately idling on a
   known external wait you expect to clear on its own (an upstream release, a rate-limit reset,
   a scheduled window): firstmate then leaves your idle pane alone and rechecks it on a long
   cadence instead of treating it as a possible wedge. Use `blocked:` when you are stuck and need help.
5. If you hit the same obstacle twice, append `blocked: {why}` and stop; firstmate will help.
6. If a decision belongs above the implementation worker (product choices, destructive actions, ask-user findings),
   append `needs-decision: {summary of options}` and stop. Firstmate will apply the configured authority and reply with the decision.
   When firstmate replies or a blocker clears and you resume, append `resolved: {how it was decided or unblocked}` (add the same `[key=<slug>]` if you opened it with one) so the decision or blocker is durably closed and does not keep resurfacing.
7. Never stop, restart, or update the shared `no-mistakes` daemon - it is one instance serving
   every lane/home, so restarting it kills other lanes' in-flight pipeline runs. On ANY no-mistakes
   daemon error, append `blocked: {the daemon error}` and stop; only firstmate manages the daemon.

# Project memory
If `AGENTS.md` or `CLAUDE.md` already exists, or if this task produced durable project-intrinsic knowledge, run `/Users/jd.sanders/.no-mistakes/worktrees/bd2afaf1a2cc/01KYAF8ZEKXA3WP610CJRX18MK/bin/fm-ensure-agents-md.sh .` in the worktree.
Record only project knowledge useful to almost every future session.
For anything the codebase already shows, prefer a pointer to the authoritative file, command, or doc over copying the detail.
If you touch a project `AGENTS.md` that lacks `## Maintaining this file`, add that short self-governance section from `/Users/jd.sanders/.no-mistakes/worktrees/bd2afaf1a2cc/01KYAF8ZEKXA3WP610CJRX18MK/bin/fm-ensure-agents-md.sh` in the same pass.
Keep it proportionate: skip `AGENTS.md` edits for trivial tasks that produced no durable project knowledge.

# Definition of done
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.
Evidence: Bug reproduction + fix confirmation on bash 3.2.57
=== BASELINE BUG (base commit 10ee779) ===
/tmp/fm-brief-base.sh: line 314: unexpected EOF while looking for matching `)'
/tmp/fm-brief-base.sh: line 388: syntax error: unexpected end of file
base rc=2 (bash 3.2.57)

=== FIXED (target commit HEAD) ===
fixed rc=0 (bash 3.2.57)
Evidence: fm-brief test suite result
ok - fm-brief.sh: bash -n succeeds on every available bash (incl. stock 3.2)
ok - fm-brief.sh: no heredoc wrapped in command substitution
ok - fm-brief.sh: --help renders the complete header
ok - fm-brief.sh: no-mistakes/direct-PR/local-only briefs generate cleanly
... (16/16) ...
EXIT=0

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 on base commit 10ee779 → rc=2, reproduced 'unexpected EOF while looking for matching )' on bash 3.2.57
  • /bin/bash -n bin/fm-brief.sh on target commit → rc=0 (parses clean on stock macOS bash 3.2.57)
  • bash tests/fm-brief.test.sh → all 16 tests pass (EXIT=0)
  • Confirmed test_no_heredoc_in_command_substitution grep finds 4 hazard lines on base (220/288/300/314) and 0 on fix
  • Generated a real no-mistakes ship brief via FM_HOME=&lt;tmp&gt; /bin/bash bin/fm-brief.sh demo-task-001 my-project on bash 3.2.57 → exit 0, 68 lines, 0 leaked EOF markers, #945 'firstmate's' apostrophe wording preserved verbatim
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…h 3.2 (kunchenguid#958)

fm-brief.sh failed to parse under stock macOS bash 3.2.57, so no brief could
be scaffolded on a default macOS firstmate. Bash 3.2 misreads a lone apostrophe
inside a heredoc body nested in $(...) as an unterminated single-quoted string
and aborts with "unexpected EOF while looking for matching `)'". kunchenguid#945 added a
line containing "firstmate's" to a DOD=$(cat <<EOF ...) block, triggering it.
Bash 4+ parses the body literally, so CI and modern-bash dev machines passed.

Durable fix, not just a reword: eliminate the VAR=$(cat <<EOF ... EOF) pattern.
The mode-specific Definition of done is appended with a plain cat >> redirection
heredoc (not wrapped in $(...)), and the unguarded Herdr section is built with
printf like its --herdr-lab counterpart. Both quoted and unquoted heredoc
delimiters trip the 3.2 bug, so the quoted Herdr <<'EOF' was a latent trap too.
Brief content is byte-for-byte unchanged across all modes; only the shell
construction changed.

Regression test parses fm-brief.sh under every available bash (the 3.2 variant
is invisible to a modern-bash bash -n) and adds a version-independent structural
guard that the command-substitution-around-heredoc pattern does not return.
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