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/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', () => { 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