From c2b0e61aadb26af1f691d08e95a5536a89ebb9f7 Mon Sep 17 00:00:00 2001 From: brady gaster Date: Sun, 23 Aug 2026 10:06:35 -0700 Subject: [PATCH] fix(gh-aw): scan the first non-empty line in PC-0, not record 1 Closes #1835 PC-0 normalized the dispatched command with an awk program gated on NR==1, so a value whose first character is a newline put an EMPTY string on the scanned record: PC0($'\nimplement') returned EMPTY_DISPATCH. That routes to the activation guard, which halts with a ::warning:: and no comment by design (PR #1777, junk issues #12/#14) -- so a structurally valid implement dispatch would halt silently instead of running. Silent loss of a valid command is the same defect class #1824 and #1832 exist to close. Scan the first non-empty line instead: skip blank records, take the first one carrying content, and fall through to EMPTY_DISPATCH via END only when every record was empty or whitespace-only. Genuinely empty input still reaches the silent activation-guard halt, so #1777 stays closed. The one-liner is written without inter-statement spaces deliberately. workflows/squad.md sits ~37 bytes above the 5 KB floor of the 100 KB prompt budget asserted by gh-aw-quality's "reports combined bytes and headroom", so the readable spacing and an explanatory paragraph both overran the gate. The behavior is documented in the tests instead. That headroom is now ~65 bytes and is a blocker for the next change to this file; it needs its own issue. Tests: 27 -> 36 in gh-aw-command-parse. Reverting the awk to NR==1 turns four of the new cases red, and each failure message names the offending input, so a truncating parser cannot pass them. The whitespace-only cases ('', ' ', '\n', '\n\n', ' \n \t \n ') pin EMPTY_DISPATCH so the fix cannot overshoot, and the load-bearing PC-1 cases are untouched -- a bare token still fails on the comment path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/gh-aw-command-parse.test.ts | 43 ++++++++++++++++++++++++++++++++ workflows/squad.md | 4 +-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/test/gh-aw-command-parse.test.ts b/test/gh-aw-command-parse.test.ts index 8df1093df..a74b30da5 100644 --- a/test/gh-aw-command-parse.test.ts +++ b/test/gh-aw-command-parse.test.ts @@ -197,6 +197,49 @@ describe('gh-aw: /squad command parsing (#1824)', () => { expect(normalize('')).not.toBe('NO_COMMAND'); }); + // #1835: PC-0 anchored on NR==1, so a value whose first character is a + // newline put an EMPTY string on the scanned record and a structurally + // valid command halted silently. Silent-loss-of-a-valid-command is the + // defect class #1824 and #1832 exist to close, so it is closed here too. + describe('leading blank lines are skipped, not read as an empty command (#1835)', () => { + const leading: Array<{ input: string; expected: string }> = [ + { input: '\nimplement', expected: 'implement' }, + { input: '\n\n\ncast', expected: 'cast' }, + { input: '\n connect org/repo ', expected: 'connect org/repo' }, + { input: '\n/squad status', expected: 'status' }, + ]; + + it.each(leading)('dispatching $input reaches its mode', ({ input, expected }) => { + expect( + normalize(input), + `PC-0 must skip leading blank lines and normalize ${JSON.stringify(input)} ` + + `to "/squad ${expected}". Scanning only record 1 reads the leading newline ` + + 'as an empty command and emits EMPTY_DISPATCH, halting a valid dispatch ' + + 'silently via the activation guard.' + ).toBe(`/squad ${expected}`); + + expect( + parseDispatch(input), + `The full dispatch path (PC-0 then PC-1) must resolve ${JSON.stringify(input)} ` + + `to ${JSON.stringify(expected)}.` + ).toBe(expected); + }); + + it.each(['', ' ', '\n', '\n\n', ' \n \t \n '])( + 'genuinely empty input %j still yields EMPTY_DISPATCH', + blank => { + // The fix must not overshoot: whitespace-only input carries no command, + // so it must still reach the silent activation-guard halt rather than + // being normalized into a bogus "/squad " with an empty argument. + expect( + normalize(blank), + `${JSON.stringify(blank)} carries no command, so PC-0 must still yield ` + + 'EMPTY_DISPATCH for the silent activation-guard halt (PR #1777).' + ).toBe('EMPTY_DISPATCH'); + } + ); + }); + it('PC-1 is NOT loosened — a bare token still fails on the comment path', () => { // The tempting wrong fix is to teach PC-1 to accept bare words. That reopens // #1824: prose merely containing "cast" would silently mint a team. The diff --git a/workflows/squad.md b/workflows/squad.md index 3c07f6c06..dd9596962 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -312,12 +312,12 @@ command** to `SQUAD_DISPATCH_COMMAND` per hop 1 and run exactly this. Its output is the `SQUAD_TRIGGER_BODY` PC-1 consumes: ```bash -printf '%s\n' "$SQUAD_DISPATCH_COMMAND" | awk 'NR==1{sub(/\r$/,""); sub(/^[[:space:]]+/,""); sub(/[[:space:]]+$/,""); if($0==""){print "EMPTY_DISPATCH"; exit} if($0 ~ /^\/squad([[:space:]]|$)/) print; else print "/squad " $0}' +printf '%s\n' "$SQUAD_DISPATCH_COMMAND" | awk '{sub(/\r$/,"");sub(/^[[:space:]]+/,"");sub(/[[:space:]]+$/,"");if($0=="")next;f=1;if($0~/^\/squad([[:space:]]|$)/)print;else print "/squad " $0;exit}END{if(!f)print "EMPTY_DISPATCH"}' ``` Normalization is idempotent: `implement` and `/squad implement` both yield `/squad implement`, so typing the slash prefix into the dispatch box is not -penalized. +penalized. It scans the first non-empty line (#1835). `EMPTY_DISPATCH` means the activation guard above should already have halted the run. Halt with that guard's `::warning::`; never route it to PC-3, which posts a