From ce0b5e9b0e3cf7efec723ffbf7023f37f0f35738 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Tue, 15 Sep 2026 01:17:11 -0700 Subject: [PATCH 1/2] fix(workflows): normalize validation retry action Require lifecycle next actions to contain only a backticked Squad command and move retry guidance to a separate field. Cover the malformed output observed in run 34944550565. Refs octodemo/eshop-bradyg-dev#4 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/gh-aw-lifecycle-upsert.test.ts | 43 +++++++++++++++++++++++++++-- test/gh-aw-plan-lifecycle.test.ts | 7 +++++ workflows/shared/squad.md | 6 ++-- workflows/squad.md | 16 ++++++++++- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/test/gh-aw-lifecycle-upsert.test.ts b/test/gh-aw-lifecycle-upsert.test.ts index a752852d4..cd8316d23 100644 --- a/test/gh-aw-lifecycle-upsert.test.ts +++ b/test/gh-aw-lifecycle-upsert.test.ts @@ -519,7 +519,44 @@ describe('#1916: deterministic lifecycle safe output', () => { ]); expect(result.failures).toEqual([ - 'Lifecycle body must include an H2 lifecycle heading plus state, last-command, and next-action fields.', + 'Lifecycle body must include an H2 lifecycle heading plus state, last-command, and a nonterminal next-action value consisting of a backticked /squad command.', + ]); + expect(result.created).toEqual([]); + }); + + it('rejects the prose-prefixed retry action emitted by failed run 34944550565', async () => { + const result = await runLifecycleUpsert([ + { + type: 'upsert_lifecycle_state', + body: body + .replace('**Current state:** Planned', '**Current state:** Validation failed') + .replace('**Last command:** `/squad plan`', '**Last command:** `/squad plan validate`') + .replace( + '**Next action:** `/squad activate`', + '**Next action:** Re-run `/squad plan validate`. If the sub-agent fails again, track the infrastructure defect.', + ), + }, + ]); + + expect(result.failures).toEqual([ + 'Lifecycle body must include an H2 lifecycle heading plus state, last-command, and a nonterminal next-action value consisting of a backticked /squad command.', + ]); + expect(result.created).toEqual([]); + }); + + it('rejects explanatory prose after a valid nonterminal next command', async () => { + const result = await runLifecycleUpsert([ + { + type: 'upsert_lifecycle_state', + body: body.replace( + '**Next action:** `/squad activate`', + '**Next action:** `/squad activate` after reviewing the validation results.', + ), + }, + ]); + + expect(result.failures).toEqual([ + 'Lifecycle body must include an H2 lifecycle heading plus state, last-command, and a nonterminal next-action value consisting of a backticked /squad command.', ]); expect(result.created).toEqual([]); }); @@ -534,7 +571,7 @@ describe('#1916: deterministic lifecycle safe output', () => { ]); expect(result.failures).toEqual([ - 'Lifecycle body must include an H2 lifecycle heading plus state, last-command, and next-action fields.', + 'Lifecycle body must include an H2 lifecycle heading plus state, last-command, and a nonterminal next-action value consisting of a backticked /squad command.', ]); expect(result.created).toEqual([]); expect(result.updated).toEqual([]); @@ -550,7 +587,7 @@ describe('#1916: deterministic lifecycle safe output', () => { ]); expect(malformed.failures).toEqual([ - 'Lifecycle body must include an H2 lifecycle heading plus state, last-command, and next-action fields.', + 'Lifecycle body must include an H2 lifecycle heading plus state, last-command, and a nonterminal next-action value consisting of a backticked /squad command.', ]); expect(duplicate.failures).toEqual(['Expected exactly one lifecycle update, found 2.']); }); diff --git a/test/gh-aw-plan-lifecycle.test.ts b/test/gh-aw-plan-lifecycle.test.ts index cc99879c9..3a786d608 100644 --- a/test/gh-aw-plan-lifecycle.test.ts +++ b/test/gh-aw-plan-lifecycle.test.ts @@ -883,6 +883,13 @@ describe('#1757: squad-plan-validate has adversarial teeth', () => { expect(validation).toMatch(/Structural PASS alone cannot produce overall PASS/); }); + it('emits lifecycle next actions in the deterministic writer format', () => { + expect(validation).toContain('**Next action:** `/squad plan accept scope`'); + expect(validation).toContain('**Next action:** `/squad plan validate`'); + expect(validation).toMatch(/backticked command must\s+be the entire field value/); + expect(validation).toMatch(/retry\s+context in a separate\s+`\*\*Guidance:\*\*` field/); + }); + it('distinguishes a neatly formatted bad plan from a genuinely validated plan', () => { const validationHeader = [ '## ✅ Squad Plan Validation — PASSED', diff --git a/workflows/shared/squad.md b/workflows/shared/squad.md index c49193748..9484384e2 100644 --- a/workflows/shared/squad.md +++ b/workflows/shared/squad.md @@ -196,7 +196,7 @@ safe-outputs: output: Lifecycle state updated. inputs: body: - description: Complete lifecycle Markdown with an H2 lifecycle heading plus state, last-command, and next-action fields; structured data is normalized by the writer. + description: Complete lifecycle Markdown with an H2 lifecycle heading plus state, last-command, and next-action fields. For a nonterminal state, the next-action value must consist of a backticked /squad command; put explanatory prose in a separate field. Structured data is normalized by the writer. required: true type: string steps: @@ -244,7 +244,7 @@ safe-outputs: (/\bsquad\b/i.test(firstLine) || /\bplanning\b/i.test(firstLine)); const hasState = /^(?:[-*]\s+)?\*\*(?:Current state|State):\*\*\s+\S+/im.test(body); const hasLastCommand = /^(?:[-*]\s+)?\*\*Last command:\*\*\s+`\/squad\b[^`]*`/im.test(body); - const hasNextCommand = /^(?:[-*]\s+)?\*\*Next (?:action|command|recommended):\*\*\s+`\/squad\b[^`]*`/im.test(body); + const hasNextCommand = /^(?:[-*]\s+)?\*\*Next (?:action|command|recommended):\*\*\s+`\/squad\b[^`]*`[ \t]*$/im.test(body); const hasActivationDone = /^(?:[-*]\s+)?(?:\*\*)?Activation:(?:\*\*)?\s+✅\s+Done\b/im.test(body) || /^\|\s*Activat(?:e|ion|ed)\s*\|\s*✅\s+Done\s*\|/im.test(body); @@ -255,7 +255,7 @@ safe-outputs: /^(?:[-*]\s+)?\*\*Next (?:action|command|recommended):\*\*\s+\S.+$/im.test(body); const hasNextAction = hasNextCommand || hasTerminalState; if (!hasLifecycleHeading || !hasState || !hasLastCommand || !hasNextAction) { - core.setFailed("Lifecycle body must include an H2 lifecycle heading plus state, last-command, and next-action fields."); + core.setFailed("Lifecycle body must include an H2 lifecycle heading plus state, last-command, and a nonterminal next-action value consisting of a backticked /squad command."); return; } if (body.includes("Structured data:") || body.replace(/\s/g, "").includes('"squad_artifact":"lifecycle-state"')) { diff --git a/workflows/squad.md b/workflows/squad.md index e8ea8a4df..4e43a0074 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -2167,7 +2167,21 @@ acceptance, and validator-owned synthesis. ##### Step 4: Update Lifecycle -Set Validation = `✅ Done` or `❌ Failed`. Next on pass: `/squad plan accept scope`. On fail: fix + re-run. +Set Validation = `✅ Done` or `❌ Failed`. Next on pass: +`/squad plan accept scope`. The lifecycle field MUST be exactly this on pass: + +```markdown +**Next action:** `/squad plan accept scope` +``` + +Or exactly this on fail: + +```markdown +**Next action:** `/squad plan validate` +``` + +The backticked command must be the entire field value; put remediation or retry +context in a separate `**Guidance:**` field. ##### Step 5: Surface Next Action From 56a970d9ffbc65873d8edc020b80bca49be3349c Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Tue, 15 Sep 2026 01:42:35 -0700 Subject: [PATCH 2/2] test(gh-aw): account for lifecycle contract growth Raise the authored-source regression budget by 1 KB after the command-only lifecycle next-action contract. The independently enforced ambient prompt budget remains healthy. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/gh-aw-quality.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index 1317024b7..9151cd879 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -1161,7 +1161,10 @@ describe('gh-aw: prompt budget & planning import regression', () => { // source independently verifies the triggering comment author's live repository // permission before the deterministic repair job can write a terminal state. // The ambient prompt remains below its separate 40 KB delivery guard. - const SOURCE_GROWTH_BUDGET_KB = 193; + // Raised 193 -> 194 KB for the command-only lifecycle next-action contract. + // Its validation and retry guidance are inside the on-demand plan-validation + // skill; the ambient prompt remains below the independently enforced 40 KB cap. + const SOURCE_GROWTH_BUDGET_KB = 194; const SOURCE_GROWTH_BUDGET_BYTES = SOURCE_GROWTH_BUDGET_KB * 1024; it('squad-planning-ontology.md is in the imports list', () => {