Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions test/gh-aw-lifecycle-upsert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});
Expand All @@ -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([]);
Expand All @@ -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.']);
});
Expand Down
7 changes: 7 additions & 0 deletions test/gh-aw-plan-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 4 additions & 1 deletion test/gh-aw-quality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
6 changes: 3 additions & 3 deletions workflows/shared/squad.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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);
Expand All @@ -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"')) {
Expand Down
16 changes: 15 additions & 1 deletion workflows/squad.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading