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
132 changes: 130 additions & 2 deletions test/gh-aw-lifecycle-upsert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,55 @@ describe('#1916: deterministic lifecycle safe output', () => {
'| Plan | ✅ Done |',
'| Activate | ✅ Done |',
].join('\n');
const liveScopeAcceptanceBody = [
'## 🧭 Squad Planning Lifecycle',
'',
'**State:** Scope Accepted',
'',
'| Field | Value |',
'|---|---|',
'| Research | ✅ Done |',
'| Triage | ✅ Done |',
'| Program Plan | ✅ Done (revised) |',
'| Implementation Plan | ✅ Done |',
'| Validation | ✅ Done (PASS) |',
'| Scope | ✅ Done |',
'| Implementation Acceptance | ⬜ Pending |',
'',
'**Last command:** `/squad plan accept scope`',
'',
'**Next action:** `/squad plan accept implementation`',
].join('\n');
const liveImplementationAcceptanceBody = [
'## Planning Lifecycle',
'',
'| Phase | Status | Artifact | Updated |',
'|-------|--------|----------|---------|',
'| Intent | ✅ Done | (issue body) | 2026-09-15 |',
'| Scope Accepted | ✅ Done | comment | 2026-09-15 |',
'| Impl Accepted | ✅ Done | (this run) | 2026-09-15 |',
'| Activated | ⬚ Pending | — | — |',
'',
'**Current state:** Implementation Accepted',
'**Last command:** `/squad plan accept implementation` by @bradygaster at 2026-09-15 15:16 UTC',
'**Next action:** `/squad plan activate`',
'**Guidance:** Create sub-issues and begin execution.',
].join('\n');
const liveActivatedBody = [
'## 🧭 Squad Planning Lifecycle',
'',
'**State:** Activated',
'',
'| Field | Value |',
'|---|---|',
'| Scope Acceptance | ✅ Done |',
'| Implementation Acceptance | ✅ Done (full, 5 tasks) |',
'| Activation | ✅ Done — 4 epics, 5 tasks created as sub-issues of #4 |',
'',
'**Last command:** `/squad plan activate`',
'',
'**Next action:** No further planning command required — this plan is fully activated. Begin implementation execution starting with the Epic 1.1 spike task.',
].join('\n');

it('creates the first tracker with the fixed structured envelope', async () => {
const result = await runLifecycleUpsert([
Expand Down Expand Up @@ -497,7 +546,10 @@ describe('#1916: deterministic lifecycle safe output', () => {
['progress-table activation with command attribution', terminalTableBody],
['plain progress-list activation', terminalProgressBody],
['activate-stage table row', terminalActivateTableBody],
])('accepts live terminal presentation: %s', async (_name, liveBody) => {
['scope acceptance from the E2E planning run', liveScopeAcceptanceBody],
['implementation acceptance with separate guidance', liveImplementationAcceptanceBody],
['terminal granular activation from run 34987615314', liveActivatedBody],
])('accepts live lifecycle presentation: %s', async (_name, liveBody) => {
const result = await runLifecycleUpsert([
{ type: 'upsert_lifecycle_state', body: liveBody },
]);
Expand Down Expand Up @@ -561,6 +613,25 @@ describe('#1916: deterministic lifecycle safe output', () => {
expect(result.created).toEqual([]);
});

it('rejects the live implementation-acceptance mutation from run 34986842391', async () => {
const result = await runLifecycleUpsert([
{
type: 'upsert_lifecycle_state',
body: liveImplementationAcceptanceBody
.replace(
'**Next action:** `/squad plan activate`',
'**Next action:** `/squad plan activate` — create sub-issues and begin execution.',
)
.replace('\n**Guidance:** Create sub-issues and begin execution.', ''),
},
]);

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.each([
['state', body.replace('**Current state:** Planned\n', '')],
['last command', body.replace('**Last command:** `/squad plan`\n', '')],
Expand Down Expand Up @@ -607,6 +678,19 @@ describe('#1928: deterministic terminal lifecycle repair', () => {
created_at: '2026-08-28T01:00:00Z',
user: { login: 'github-actions[bot]' },
};
const activated = {
id: 11,
body: [
'## Plan activated',
'',
'Structured data:',
'```json',
'{"squad_artifact":"activated","schema_version":"1","origin_issue":5,"phases":[1,2]}',
'```',
].join('\n'),
created_at: '2026-08-28T01:30:00Z',
user: { login: 'github-actions[bot]' },
};
const lifecycleEnvelope =
'{"squad_artifact":"lifecycle-state","schema_version":"1","origin_issue":5,"phases":[]}';
const stale = {
Expand Down Expand Up @@ -640,6 +724,19 @@ describe('#1928: deterministic terminal lifecycle repair', () => {
expect(result.updated[0].body).toContain(lifecycleEnvelope);
});

it('repairs granular /squad plan activate lifecycle state from a trusted phased activation', async () => {
const result = await runLifecycleRepair(
[activated, stale],
'/squad plan activate',
);

expect(result.failures).toEqual([]);
expect(result.created).toEqual([]);
expect(result.updated).toHaveLength(1);
expect(result.updated[0].body).toContain('- **State:** Activated');
expect(result.updated[0].body).toContain('- **Last command:** `/squad plan activate`');
});

it('does nothing when the newest tracker is already terminal', async () => {
const terminal = {
...stale,
Expand All @@ -657,6 +754,37 @@ describe('#1928: deterministic terminal lifecycle repair', () => {
);
});

it('preserves a live terminal tracker with detailed activation and command attribution', async () => {
const terminal = {
...stale,
body: [
'## 🧭 Squad Lifecycle State — Issue #5',
'',
'**State:** Activated',
'',
'| Stage | Status |',
'| --- | --- |',
'| Activation | ✅ Done — created 3 task issues and milestone v1 |',
'',
'**Last command:** `/squad plan activate` by @maintainer',
'**Next action:** Track progress on the created task issues; no further planning action is required.',
'',
'Structured data:',
'```json',
lifecycleEnvelope,
'```',
].join('\n'),
};
const result = await runLifecycleRepair([activated, terminal], '/squad plan activate');

expect(result.failures).toEqual([]);
expect(result.created).toEqual([]);
expect(result.updated).toEqual([]);
expect(result.info).toContain(
'The newest lifecycle tracker already records terminal activation.',
);
});

it('does not trust a user-authored acceptance artifact', async () => {
const result = await runLifecycleRepair([
{ ...accepted, user: { login: 'untrusted-user' } },
Expand All @@ -667,7 +795,7 @@ describe('#1928: deterministic terminal lifecycle repair', () => {
expect(result.created).toEqual([]);
expect(result.updated).toEqual([]);
expect(result.info).toContain(
'No trusted whole-plan acceptance artifact; lifecycle repair is not applicable.',
'No trusted whole-plan acceptance or activation artifact; lifecycle repair is not applicable.',
);
});

Expand Down
1 change: 1 addition & 0 deletions test/gh-aw-plan-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,7 @@ describe('#1916: fast-path commands maintain the planning lifecycle state', () =
expect(lifecycle).toContain('Activation = `✅ Done`');
expect(lifecycle).toContain('state =\n Activated');
expect(lifecycle).toContain('This is terminal');
expect(lifecycle).toContain('explicit terminal prose');
});

it('repairs stale lifecycle state on an idempotent activate rerun', () => {
Expand Down
6 changes: 5 additions & 1 deletion test/gh-aw-quality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ describe('#1916: lifecycle comment updates use a deterministic safe-output job',
"!contains(needs.agent.outputs.output_types, 'upsert_lifecycle_state')",
);
expect(shared).toContain("github.event.comment.body == '/squad activate'");
expect(shared).toContain('envelope?.squad_artifact === "plan-accepted"');
expect(shared).toContain('["plan-accepted", "activated"].includes(e?.squad_artifact)');
expect(shared).toContain('name: Repair terminal lifecycle after idempotent activation');
});

Expand Down Expand Up @@ -1661,6 +1661,10 @@ describe('gh-aw: compiled workflow shell input security contract', () => {
);
expect(compiled).toContain('github.rest.repos.getCollaboratorPermissionLevel');
expect(compiled).toContain('["admin", "maintain", "write"].includes(permission)');
expect(compiled).toContain("github.event.comment.body == '/squad plan activate'");
expect(compiled).toContain(
'["/squad activate", "/squad plan accept", "/squad plan activate"].includes(command)',
);
}, 20000);

it('compiles Cast failure into a queryable post-agent job that fails the run', () => {
Expand Down
7 changes: 6 additions & 1 deletion workflows/shared/squad-planning-ontology.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,15 @@ This comment is created on first transition and updated on every subsequent tran

**Current state:** Triaged
**Last command:** `/squad triage` by @user at <timestamp>
**Next action:** `/squad plan program` — create a program plan from triage dispositions
**Next action:** `/squad plan program`
**Guidance:** Create a program plan from triage dispositions.
**Also available:** `/squad triage revise <feedback>` — adjust triage before planning
```

For nonterminal states, `Next action` is only the backticked `/squad` command;
put prose elsewhere. `Activated` is terminal and may use terminal prose there.
Its canonical granular command is `/squad plan activate`.

Status icons: `✅ Done` · `⏳ In Progress` · `⬚ Pending` · `❌ Failed` · `⏭ Skipped`

---
Expand Down
32 changes: 17 additions & 15 deletions workflows/shared/squad.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,11 @@ safe-outputs:
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);
/^\|\s*Activat(?:e|ion|ed)\s*\|\s*✅\s+Done\b[^|]*\|/im.test(body);
const hasTerminalState =
/^(?:[-*]\s+)?\*\*(?:Current state|State):\*\*\s+Activated\s*$/im.test(body) &&
hasActivationDone &&
/^(?:[-*]\s+)?\*\*Last command:\*\*\s+`\/squad (?:activate|plan accept)(?: phase \d+)?`(?:\s+.*)?$/im.test(body) &&
/^(?:[-*]\s+)?\*\*Last command:\*\*\s+`\/squad (?:activate|plan accept|plan activate)(?: phase \d+)?`(?:\s+.*)?$/im.test(body) &&
/^(?:[-*]\s+)?\*\*Next (?:action|command|recommended):\*\*\s+\S.+$/im.test(body);
const hasNextAction = hasNextCommand || hasTerminalState;
if (!hasLifecycleHeading || !hasState || !hasLastCommand || !hasNextAction) {
Expand Down Expand Up @@ -315,7 +315,8 @@ jobs:
!contains(needs.agent.outputs.output_types, 'upsert_lifecycle_state') &&
github.event_name == 'issue_comment' &&
(github.event.comment.body == '/squad activate' ||
github.event.comment.body == '/squad plan accept')
github.event.comment.body == '/squad plan accept' ||
github.event.comment.body == '/squad plan activate')
}}
runs-on: ubuntu-slim
permissions:
Expand All @@ -334,7 +335,7 @@ jobs:
if (
!Number.isInteger(issueNumber) ||
issueNumber <= 0 ||
!["/squad activate", "/squad plan accept"].includes(command)
!["/squad activate", "/squad plan accept", "/squad plan activate"].includes(command)
) {
core.setFailed("A valid whole-plan activation command and issue number are required.");
return;
Expand Down Expand Up @@ -387,16 +388,16 @@ jobs:
comment,
envelope: envelopeFor(comment),
}));
const accepted = artifacts.some(
({ envelope }) =>
envelope?.squad_artifact === "plan-accepted" &&
envelope?.schema_version === "1" &&
envelope?.origin_issue === issueNumber &&
Array.isArray(envelope?.phases) &&
envelope.phases.length === 0,
const ok = artifacts.some(
({ envelope: e }) =>
["plan-accepted", "activated"].includes(e?.squad_artifact) &&
e?.schema_version === "1" &&
e?.origin_issue === issueNumber &&
Array.isArray(e?.phases) &&
(e.squad_artifact === "activated" || e.phases.length === 0),
);
if (!accepted) {
core.info("No trusted whole-plan acceptance artifact; lifecycle repair is not applicable.");
if (!ok) {
core.info("No trusted whole-plan acceptance or activation artifact; lifecycle repair is not applicable.");
return;
}

Expand All @@ -414,8 +415,9 @@ jobs:
const lifecycleBody = String(lifecycle?.body || "");
const terminal =
/^(?:[-*]\s+)?\*\*(?:Current state|State):\*\*\s+Activated\s*$/im.test(lifecycleBody) &&
/^(?:[-*]\s+)?\*\*Activation:\*\*\s+✅\s+Done\s*$/im.test(lifecycleBody) &&
/^(?:[-*]\s+)?\*\*Last command:\*\*\s+`\/squad (?:activate|plan accept)`\s*$/im.test(lifecycleBody);
(/^(?:[-*]\s+)?(?:\*\*)?Activation:(?:\*\*)?\s+✅\s+Done\b/im.test(lifecycleBody) ||
/^\|\s*Activat(?:e|ion|ed)\s*\|\s*✅\s+Done\b[^|]*\|/im.test(lifecycleBody)) &&
/^(?:[-*]\s+)?\*\*Last command:\*\*\s+`\/squad (?:activate|plan accept|plan activate)(?: phase \d+)?`(?:\s+.*)?$/im.test(lifecycleBody);
if (terminal) {
core.info("The newest lifecycle tracker already records terminal activation.");
return;
Expand Down
5 changes: 4 additions & 1 deletion workflows/squad.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ matching the exact structured fields, and choosing the newest match.

For each lifecycle-state write, call `upsert_lifecycle_state` once with the
complete body. It updates the newest trusted tracker or creates the first one.
For nonterminal states, `Next action` MUST be only the backticked `/squad`
command; put prose elsewhere. `Activated` may use terminal prose instead.

# Squad — `/squad` Slash Command

Expand Down Expand Up @@ -2553,7 +2555,8 @@ accepted label — that manufactures a defect.
##### Step 5: Update Lifecycle

Phase: `🔄 Phase {N} of {total} activated`. Next: accept/activate next phase.
Full/last: `✅ Done`, state = Activated. Terminal — no next action needed.
Full/last: `✅ Done`, state = Activated, last command = invoked
`/squad plan activate`. Terminal — use terminal prose for Next action.

## end skill: `squad-plan-activate`

Expand Down
Loading