diff --git a/.squad/agents/procedures/history.md b/.squad/agents/procedures/history.md index 1298528a8..814a7f384 100644 --- a/.squad/agents/procedures/history.md +++ b/.squad/agents/procedures/history.md @@ -80,6 +80,20 @@ Also updated: examples section (showing `name` + `description` pairs), anti-patt **Proposal filed:** `.squad/decisions/inbox/procedures-vscode-routing-fix.md` +### 2026-08-20: Long-path lifecycle repairs (#1758, #1759, #1756) + +Fixed three defects in `workflows/squad.md`, all gating the 2026-08-21 e2e series. Every touched stage was in Sims' "NEVER EXERCISED" bucket, so I anchored each fix in a readable source of truth rather than inference. + +- **#1758.1 (dead-code routing):** `squad-plan-accept` Step 1 unconditionally hard-failed "No plan found" on a missing `plan` artifact, so the Behavior note's `program`/`implementation` routing could never run. Rewrote Step 1 as "Find Plan and Route": check `program`/`implementation` first → run Accept Scope → Accept Impl → Activate; only reply "No plan found" when none of `program`/`implementation`/`plan` exist. +- **#1758.2 (epics dispatched as tasks):** Implement mode found immediate children of root — Epics in a 3-level hierarchy — and dispatched implementation workers on them. Changed Step 1 + Epic Dispatch to descend the sub-issue hierarchy recursively and dispatch only **leaf tasks** (open issues with no open sub-issues). Kept the 3-slot cap and worker contract intact (I do NOT own `squad-implement-worker.md`). +- **#1758.3 (validate ordering):** PROVABLE, not speculative. The planning ontology (`shared/squad-planning-ontology.md:48-87`) is the authoritative state machine and sequences `program → implementation → validate → accept scope → accept implementation → activate`. `squad.md`'s `next=` hints had drifted (program→accept-scope, validate→accept-impl, accept-scope→implementation). Corrected all hints to match the ontology, so validate precedes BOTH accept steps. +- **#1759 (Role strings in Owner/Agent):** Added an explicit Owner/Agent binding rule (resolve to the `Name` column of `.squad/team.md`, never a Role string) at every emission site (squad-plan Step 1/Step 3, squad-plan-implementation Step 2/3/4) and made `squad:{owner}` label minting use the lowercased cast Name, forbidding `squad:lead`. +- **#1756 (char floor → structural contract):** Replaced the research artifact's `≥200-char` floor with a structural contract (required sections: Evidence table, Goals, Non-goals, Load-bearing assumptions, Open decisions, Acceptance framing; `Rn` traceability IDs; one citation token per evidence row) enforced via the MANDATORY verify step. Shipped ONLY the structural half — the "well-formatted bad plan should FAIL" taste-judgment (#1757) stays deferred. + +Tests: `test/gh-aw-plan-lifecycle.test.ts` (23 assertions), incl. a role-leak detector that parses a plan's Owner column against team.md and flags Role strings (`lead`) while passing cast Names (`Procedures`). Build green; gh-aw-quality suite unaffected. No changeset (no `packages/*/src/` touched). + +**Proposal filed:** `.squad/decisions/inbox/procedures-long-path-lifecycle-fix.md` + ### 2026-07: VS Code routing enforcement — Fix 1 + Fix 2 shipped (#613) **Implemented** P0 fixes from the VS Code routing proposal: diff --git a/test/gh-aw-plan-lifecycle.test.ts b/test/gh-aw-plan-lifecycle.test.ts new file mode 100644 index 000000000..8366e9870 --- /dev/null +++ b/test/gh-aw-plan-lifecycle.test.ts @@ -0,0 +1,319 @@ +/** + * gh-aw Plan Lifecycle Contract Tests + * + * Guards the long-path planning lifecycle in `workflows/squad.md` against the + * three defects tracked by #1758, the Owner/Agent cast-Name binding of #1759, + * and the structural research contract of #1756. + * + * These assertions target the workflow's structural contract (labeled sections, + * ordering hints, binding rules) rather than incidental prose — a single + * regression produces a single failing criterion. + */ + +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const WORKFLOWS_DIR = join(process.cwd(), 'workflows'); +const SQUAD_WORKFLOW = join(WORKFLOWS_DIR, 'squad.md'); +const ONTOLOGY = join(WORKFLOWS_DIR, 'shared', 'squad-planning-ontology.md'); +const TEAM = join(process.cwd(), '.squad', 'team.md'); + +function readText(filePath: string): string { + return readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n'); +} + +/** Slice a `## skill: \`name\`` block out of the workflow markdown. */ +function skillBlock(markdown: string, name: string): string { + const start = markdown.indexOf(`## skill: \`${name}\``); + if (start === -1) throw new Error(`skill block "${name}" not found`); + const rest = markdown.slice(start + 1); + const nextIdx = rest.indexOf('\n## skill: `'); + return nextIdx === -1 ? markdown.slice(start) : rest.slice(0, nextIdx); +} + +const squad = readText(SQUAD_WORKFLOW); +const ontology = readText(ONTOLOGY); +const team = readText(TEAM); + +// --------------------------------------------------------------------------- +// team.md parsing — Name column vs Role column +// --------------------------------------------------------------------------- + +/** Cast Names from the `## Members` table's `Name` column. */ +function castNames(): string[] { + const section = team.match(/## Members\n([\s\S]*?)(?=\n## )/)?.[1] ?? ''; + return [...section.matchAll(/^\|\s*([A-Za-z0-9@]+)\s*\|\s*([^|]+?)\s*\|/gm)] + .map(m => m[1]) + .filter(name => name && name !== 'Name' && !/^-+$/.test(name)); +} + +/** Role strings from the `## Members` table's `Role` column. */ +function castRoles(): string[] { + const section = team.match(/## Members\n([\s\S]*?)(?=\n## )/)?.[1] ?? ''; + return [...section.matchAll(/^\|\s*([A-Za-z0-9@]+)\s*\|\s*([^|]+?)\s*\|/gm)] + .map(m => m[2].trim()) + .filter(role => role && role !== 'Role' && !/^-+$/.test(role)); +} + +const NAMES = castNames(); +const NAMES_LC = new Set(NAMES.map(n => n.toLowerCase())); +const ROLES_LC = new Set(castRoles().map(r => r.toLowerCase())); + +/** + * A plan Owner/Agent cell is a "role-string leak" when it fails to resolve to a + * cast Name from team.md. This is the exact defect #1759 describes: Role strings + * (`lead`, `devrel`) reaching an Owner column instead of cast Names. + */ +function isRoleStringLeak(ownerCell: string): boolean { + const value = ownerCell.trim().toLowerCase(); + if (value === '@copilot') return false; // explicit coding-agent fallback + return !NAMES_LC.has(value); +} + +// --------------------------------------------------------------------------- +// #1759 — Owner/Agent columns must resolve to cast Names, never Role strings +// --------------------------------------------------------------------------- + +describe('#1759: Owner/Agent bind to the cast Name column', () => { + it('team.md exposes distinct Name and Role columns to bind against', () => { + expect(NAMES).toContain('Procedures'); + expect(NAMES).toContain('Flight'); + expect(ROLES_LC.has('lead')).toBe(true); // "Lead" is a Role, not a Name + expect(NAMES_LC.has('lead')).toBe(false); // and it is not a valid Owner + }); + + it('the role-leak detector catches a Role string in an Owner column', () => { + // A well-formed plan table whose Owner cells are cast Names. + const goodPlan = [ + '| # | Title | Owner | Size | Depends On |', + '|---|-------|-------|------|-----------|', + '| 1 | Wire adapter | EECOM | M | - |', + '| 2 | Prompt refactor | Procedures | S | 1 |', + ].join('\n'); + + // A plan table that leaked Role strings into the Owner column. + const badPlan = [ + '| # | Title | Owner | Size | Depends On |', + '|---|-------|-------|------|-----------|', + '| 1 | Wire adapter | lead | M | - |', + '| 2 | Prompt refactor | devrel | S | 1 |', + ].join('\n'); + + const owners = (table: string) => + [...table.matchAll(/^\|\s*\d+\s*\|[^|]*\|\s*([^|]+?)\s*\|/gm)].map(m => m[1]); + + expect(owners(goodPlan).some(isRoleStringLeak)).toBe(false); + expect(owners(badPlan).every(isRoleStringLeak)).toBe(true); + // The specific failure mode: "lead" is a Role, "Procedures"/"EECOM" are Names. + expect(isRoleStringLeak('lead')).toBe(true); + expect(isRoleStringLeak('Procedures')).toBe(false); + }); + + it('squad-plan binds the Owner column to the team.md Name column', () => { + const block = skillBlock(squad, 'squad-plan'); + expect(block).toMatch(/Owner\/Agent binding rule/i); + expect(block).toContain('`Name` column'); + // Explicitly forbids the leaking values named in #1759. + expect(block).toMatch(/never a Role string/i); + expect(block).toMatch(/`lead`, `devrel`, `reviewer`/); + }); + + it('squad-plan-accept mints squad:{owner} from the cast Name, not a role', () => { + const block = skillBlock(squad, 'squad-plan-accept'); + expect(block).toContain('cast **Name** lowercased'); + expect(block).toContain('squad:flight'); + expect(block).toMatch(/never mint a role-derived label such as `squad:lead`/); + }); + + it('squad-plan-implementation binds the Agent column to the cast Name', () => { + const block = skillBlock(squad, 'squad-plan-implementation'); + expect(block).toMatch(/Agent binding rule/i); + expect(block).toContain('`Name` column'); + expect(block).toMatch(/agent validity \(every `Agent` resolves to a `Name` row/); + }); +}); + +// --------------------------------------------------------------------------- +// #1758.1 — squad-plan-accept must route before it can hard-fail +// --------------------------------------------------------------------------- + +describe('#1758.1: squad-plan-accept routes granular plans before failing', () => { + const block = skillBlock(squad, 'squad-plan-accept'); + + it('Step 1 checks program/implementation before replying "No plan found"', () => { + const step1 = block.match(/##### Step 1: Find Plan and Route([\s\S]*?)#####/)?.[1] ?? ''; + expect(step1, 'Step 1 must be a routing step').not.toBe(''); + + const programIdx = step1.indexOf('`program`'); + const noPlanIdx = step1.indexOf('No plan found'); + expect(programIdx).toBeGreaterThan(-1); + expect(noPlanIdx).toBeGreaterThan(-1); + // The hard-fail must come AFTER the program/implementation routing check, + // so the routing note is reachable rather than dead code. + expect(programIdx).toBeLessThan(noPlanIdx); + }); + + it('routes to Accept Scope -> Accept Implementation -> Activate when granular artifacts exist', () => { + const step1 = block.match(/##### Step 1: Find Plan and Route([\s\S]*?)#####/)?.[1] ?? ''; + const scopeIdx = step1.indexOf('Accept Scope'); + const implIdx = step1.indexOf('Accept Implementation'); + const activateIdx = step1.indexOf('Activate'); + expect(scopeIdx).toBeGreaterThan(-1); + expect(implIdx).toBeGreaterThan(scopeIdx); + expect(activateIdx).toBeGreaterThan(implIdx); + }); + + it('only replies "No plan found" when none of plan/program/implementation exist', () => { + expect(block).toMatch( + /none of `program`, `implementation`, or `plan` exist, reply "No plan found/, + ); + }); +}); + +// --------------------------------------------------------------------------- +// #1758.2 — Implement descends to leaf tasks, never dispatches epics +// --------------------------------------------------------------------------- + +describe('#1758.2: Implement dispatches leaf tasks, not epics', () => { + it('Step 1 descends the hierarchy recursively to leaf tasks', () => { + expect(squad).toMatch(/descending recursively through \*\*every\*\* level/); + expect(squad).toContain('Identify the **leaf tasks**'); + expect(squad).toContain('Intermediate parents'); + expect(squad).toContain('are never dispatched to a worker'); + }); + + it('Epic Dispatch iterates leaf tasks and matches leaf branch names', () => { + const dispatch = squad.match(/##### Epic Dispatch([\s\S]*?)## skill:/)?.[1] ?? ''; + expect(dispatch).toContain('For each open leaf task'); + expect(dispatch).toContain('squad/implement-{leaf-number}-'); + expect(dispatch).toContain('"issue_number": "{leaf-issue-number}"'); + // The immediate-children language that caused the bug must be gone. + expect(dispatch).not.toContain('For each open child issue'); + }); + + it('preserves the three-slot dispatch cap', () => { + expect(squad).toMatch(/available-slots = max\(0, 3 - active-implementation-count\)/); + }); +}); + +// --------------------------------------------------------------------------- +// #1758.3 — validate precedes BOTH accept steps (ontology-consistent order) +// --------------------------------------------------------------------------- + +describe('#1758.3: validate precedes both accept steps', () => { + /** Canonical command order from the ontology state-transition block. */ + function ontologyCommandOrder(): string[] { + const blockMatch = ontology.match(/```\n(idle[\s\S]*?)```/); + const transitions = blockMatch![1]; + return [...transitions.matchAll(/triggered_by:\s*(\/squad plan [\w ]+)/g)].map(m => + m[1].trim(), + ); + } + + it('ontology sequences validate before accept scope before accept implementation', () => { + const order = ontologyCommandOrder(); + const idx = (cmd: string) => order.indexOf(cmd); + expect(idx('/squad plan validate')).toBeGreaterThan(-1); + expect(idx('/squad plan validate')).toBeLessThan(idx('/squad plan accept scope')); + expect(idx('/squad plan accept scope')).toBeLessThan( + idx('/squad plan accept implementation'), + ); + }); + + it('squad.md next-hints reproduce the ontology order', () => { + // program -> implementation -> validate -> accept scope -> accept implementation -> activate + expect(skillBlock(squad, 'squad-plan-program')).toMatch( + /next = `\/squad plan implementation`/, + ); + expect(skillBlock(squad, 'squad-plan-implementation')).toMatch( + /next = `\/squad plan validate`/, + ); + expect(skillBlock(squad, 'squad-plan-validate')).toMatch( + /Next on pass: `\/squad plan accept scope`/, + ); + expect(skillBlock(squad, 'squad-plan-accept-scope')).toMatch( + /next = `\/squad plan accept implementation`/, + ); + expect(skillBlock(squad, 'squad-plan-accept-implementation')).toMatch( + /next = `\/squad plan activate`/, + ); + }); + + it('validate no longer routes straight to accept implementation', () => { + const block = skillBlock(squad, 'squad-plan-validate'); + expect(block).not.toMatch(/Next on pass: `\/squad plan accept implementation`/); + }); +}); + +// --------------------------------------------------------------------------- +// #1756 — structural research contract replaces the >=200-char floor +// --------------------------------------------------------------------------- + +describe('#1756: research uses a structural contract, not a length floor', () => { + const block = skillBlock(squad, 'squad-research'); + + it('drops the >=200-char length floor entirely', () => { + expect(block).not.toContain('≥200 chars'); + expect(block).not.toMatch(/\b200\b/); + }); + + it('requires the six structural sections', () => { + for (const section of [ + 'Evidence table', + 'Goals', + 'Non-goals', + 'Load-bearing assumptions', + 'Open decisions', + 'Acceptance framing', + ]) { + expect(block, `research contract must require "${section}"`).toContain(section); + } + }); + + it('requires Rn traceability IDs and one citation token per evidence row', () => { + expect(block).toMatch(/`Rn` traceability ID/); + expect(block).toMatch(/exactly one citation token/); + }); + + it('the MANDATORY verify step enumerates the structural checks', () => { + const verify = block.match(/Step 4: Verify Completion \[MANDATORY\]([\s\S]*)$/)?.[1] ?? ''; + expect(verify).toContain('Evidence table'); + expect(verify).toMatch(/unique `Rn` ID and exactly one citation token/); + expect(verify).not.toContain('≥200 chars'); + }); +}); + +// --------------------------------------------------------------------------- +// #1772 (defense-in-depth) — empty workflow_dispatch probe is guarded, not +// turned into a junk issue. Pairs with EECOM's dispatch-workflow max fix +// (PR #1777). +// --------------------------------------------------------------------------- + +describe('#1772: empty workflow_dispatch probe halts without junk issues', () => { + const guard = + squad.match(/### Workflow-dispatch activation guard[\s\S]*?(?=\nResolve the slash command)/)?.[0] ?? + ''; + + it('declares a MANDATORY activation guard that runs before any skill', () => { + expect(guard, 'activation guard section must exist').not.toBe(''); + expect(guard).toMatch(/\[MANDATORY — run before any skill\]/); + }); + + it('halts an empty-command probe with a log annotation and no side effects', () => { + expect(guard).toMatch(/Dispatched command\*\*\s+above is empty or missing/); + expect(guard).toContain('::warning::'); + // The empty probe must STOP without creating an issue, comment, or skill entry. + expect(guard).toMatch(/Do NOT create an issue, do NOT post a comment, do NOT/); + }); + + it('no longer instructs creating a junk "missing command/issue_number" issue', () => { + // The old junk-issue generators (fixture #12/#14 root cause) must be gone. + expect(squad).not.toContain('Squad workflow dispatch missing command'); + expect(squad).not.toContain('Squad workflow dispatch missing issue_number'); + }); + + it('references EECOM PR #1777 so the paired fixes are traceable', () => { + expect(guard).toContain('PR #1777'); + }); +}); diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index 977df8759..bf266df4d 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -1034,10 +1034,14 @@ describe('gh-aw: merge continuation dispatch contract', () => { expect(squadInputs.command.default).toBeUndefined(); }); - it('documents missing workflow_dispatch issue_number as a visible failure', () => { - expect(readText(SQUAD_WORKFLOW)).toMatch(/missing issue_number/i); - expect(readText(SQUAD_WORKFLOW)).toMatch(/workflow_dispatch\.inputs\.issue_number/i); - expect(readText(SQUAD_WORKFLOW)).toMatch(/create a visible issue/i); + it('documents missing workflow_dispatch issue_number as a guarded halt, not a junk issue', () => { + const squadText = readText(SQUAD_WORKFLOW); + expect(squadText).toMatch(/missing issue_number/i); + // The activation guard halts the run with a visible log annotation instead of + // minting a junk issue for an empty/malformed dispatch probe (see PR #1777). + expect(squadText).toMatch(/::warning::/); + expect(squadText).toMatch(/halting with no side effects/i); + expect(squadText).not.toMatch(/Squad workflow dispatch missing issue_number/); }); it('worker continuation dispatch payload nests keys that Squad declares', () => { diff --git a/workflows/squad.md b/workflows/squad.md index 0021bc8be..d281513c4 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -139,14 +139,42 @@ failures, not commands to reinterpret as Cast. - **Dispatched command:** `${{ github.event.inputs.command }}` - **Dispatched issue number:** `${{ github.event.inputs.issue_number }}` +### Workflow-dispatch activation guard [MANDATORY — run before any skill] + +`workflow_dispatch` inputs `command` and `issue_number` are both +`required: false`, so an empty activation probe can reach this workflow. The +`squad-implement-worker` relay fires such a probe before its real dispatch (see +EECOM's `dispatch-workflow` `max` fix in PR #1777). That probe arrives here as a +`workflow_dispatch` with empty inputs. It is NOT a command. Guard against it as +the FIRST action of the run, before resolving any command or entering any skill: + +- When `github.event_name` is `workflow_dispatch` AND the **Dispatched command** + above is empty or missing: this is an empty activation probe, not a real run. + Emit exactly one diagnostic annotation via bash — + `echo "::warning::Squad workflow_dispatch fired with empty command input — empty activation probe (see PR #1777); halting with no side effects"` + — and STOP immediately. Do NOT create an issue, do NOT post a comment, do NOT + enter any skill. Creating an issue here is the junk-issue defect that produced + fixture issues #12 and #14; never do it. +- When `github.event_name` is `workflow_dispatch`, the **Dispatched command** is + non-empty and names an issue-bound mode (`research`, `triage`, `plan*`, or + `implement`), but neither a dispatched nor a triggering `issue_number` is + available: emit + `echo "::warning::Squad workflow_dispatch for the named command is missing issue_number; halting with no side effects"` + and STOP. Do NOT create an issue. + +This guard is defense-in-depth: PR #1777's `max` bump keeps the real relay +dispatch alive, and this guard makes the surviving probe harmless and visible +(a log annotation that survives the run) instead of silently minting junk +issues. If the LLM ever emits a third dispatch entry, `max` alone fails again — +this guard still holds. + Resolve the slash command in this order: 1. **Dispatched command** (above) — when the event name is `workflow_dispatch`, this input must be present for the run to proceed. If it - is empty, create a visible issue titled - `Squad workflow dispatch missing command`, explain that the run cannot - continue without `workflow_dispatch.inputs.command`, and stop. When it is - non-empty, use this value as the command and skip the remaining sources. + is empty, the activation guard above has already halted the run; never reach + this step with an empty dispatched command. When it is non-empty, use this + value as the command and skip the remaining sources. 2. **Issue comment / PR review comment:** `github.event.comment.body` — the full comment text. 3. **Issue body:** `github.event.issue.body` — the full issue description. @@ -160,12 +188,9 @@ Resolve the target issue in this order: 2. The triggering issue or pull request number from the event payload. **Never emit `noop` when the dispatched command is non-empty.** A workflow -dispatch is always actionable: run the named mode against the dispatched issue -number. If the dispatched command is non-empty and names an issue-bound mode -(`research`, `triage`, `plan*`, or `implement`) but no dispatched or triggering -issue number exists, create a visible issue titled -`Squad workflow dispatch missing issue_number`, explain that the run cannot -continue without `workflow_dispatch.inputs.issue_number`, and stop. +dispatch with a non-empty command is always actionable: run the named mode +against the dispatched issue number. The missing-`issue_number` case is handled +by the activation guard above — halt with a log annotation, never an issue. The activation job already ran `squad init --preset default`, which produced a generic 5-agent team (lead, reviewer, devrel, security, docs) in `.squad/`. Cast @@ -500,9 +525,10 @@ description: Dispatch implementation work to the squad-implement-worker workflow --- Implement mode dispatches an isolated implementation worker for a regular issue. -When invoked on an epic, it dispatches workers for up to three currently -unblocked children. The worker relays merged implementation pull requests back -to this mode so it can automatically refill the parent epic's available slots. +When invoked on a parent (initiative or epic), it descends the sub-issue +hierarchy to the **leaf tasks** and dispatches workers for up to three currently +unblocked leaf tasks. The worker relays merged implementation pull requests back +to this mode so it can automatically refill the parent's available slots. **Acknowledge:** Post `🤖 Squad is preparing implementation…` using the `add-comment` safe-output. @@ -514,64 +540,70 @@ to this mode so it can automatically refill the parent epic's available slots. triggering issue. If invoked from a pull request review comment, explain that `/squad implement` must be run from the target issue. 2. Read the target issue title, body, labels, state, and relevant comments. -3. Find open child issues using native GitHub sub-issue relationships. Also - include open issues whose body contains a - `Parent: #{target-issue-number}` line for compatibility with older plans. -4. If child issues exist, treat the target as an epic and follow the Epic - Dispatch procedure below. Do not implement the epic body directly. -5. If no child issues exist, call the workflow-specific - `squad_implement_worker` safe-output tool with `issue_number` set to the - target issue number. -6. Post a comment linking the dispatched worker run. The worker performs +3. Discover the target's open descendant issues using native GitHub sub-issue + relationships, descending recursively through **every** level of the + hierarchy (initiative → epic → task), not just immediate children. Also + include open issues whose body contains a `Parent: #{ancestor-issue-number}` + line for any ancestor, for compatibility with older plans. +4. Identify the **leaf tasks**: open descendants that themselves have no open + sub-issues. Intermediate parents (initiatives and epics that only group other + issues) are never dispatched to a worker — only leaf tasks are implemented. +5. If the target has one or more open leaf descendants, treat the target as a + parent and follow the Epic Dispatch procedure below over the leaf-task set. + Do not implement the parent body directly. +6. If the target has no open descendants (it is itself a leaf), call the + workflow-specific `squad_implement_worker` safe-output tool with `issue_number` + set to the target issue number. +7. Post a comment linking the dispatched worker run. The worker performs dependency, duplicate pull request, routing, implementation, and validation checks. ##### Epic Dispatch -For each open child issue: +For each open leaf task in the target's descendant set: 1. Parse its `Depends on:` line and check the state of every referenced issue. -2. Exclude children with any open dependency. -3. Find children that already have an open pull request whose branch starts - with `squad/implement-{child-number}-` or whose body closes that child. - These are active implementation children. +2. Exclude leaf tasks with any open dependency. +3. Find leaf tasks that already have an open pull request whose branch starts + with `squad/implement-{leaf-number}-` or whose body closes that leaf task. + These are active implementation tasks. 4. Calculate `available-slots = max(0, 3 - active-implementation-count)`. -5. Exclude active implementation children from the ready set. -6. Sort ready children by issue number and select at most `available-slots`. +5. Exclude active implementation tasks from the ready set. +6. Sort ready leaf tasks by issue number and select at most `available-slots`. -For each selected child, call the workflow-specific `squad_implement_worker` +For each selected leaf task, call the workflow-specific `squad_implement_worker` safe-output tool with this input: ```json { - "issue_number": "{child-issue-number}" + "issue_number": "{leaf-issue-number}" } ``` Never call the generic `dispatch_workflow` tool. Never emit a dispatch without a non-empty numeric `issue_number`. Emit exactly one workflow-specific dispatch -per selected child, and only report a child as dispatched after the tool returns -success. +per selected leaf task, and only report a leaf task as dispatched after the tool +returns success. -Post a comment on the epic listing the dispatched children, blocked children, -children with existing implementation pull requests, and any ready children -deferred because all three slots are occupied. If no child is ready or no slot -is available, post the status summary and do not dispatch a workflow. +Post a comment on the target listing the dispatched leaf tasks, blocked leaf +tasks, leaf tasks with existing implementation pull requests, and any ready leaf +tasks deferred because all three slots are occupied. If no leaf task is ready or +no slot is available, post the status summary and do not dispatch a workflow. -**Always leave a visible next step.** Every Implement run against an epic ends -with a comment on that epic — never a silent exit. Cover each terminal case: +**Always leave a visible next step.** Every Implement run against a parent ends +with a comment on that parent — never a silent exit. Cover each terminal case: -- Children dispatched → name them and state how many children remain open. -- All remaining children blocked → name the blocking dependencies. +- Leaf tasks dispatched → name them and state how many leaf tasks remain open. +- All remaining leaf tasks blocked → name the blocking dependencies. - All three slots occupied → name the in-flight pull requests. -- No open children left → state that the epic's implementation is complete. +- No open leaf tasks left → state that the parent's implementation is complete. Never emit `noop` for an Implement run. `noop` is not reported as a comment, so -it strands the epic with no signal about what to do next — the exact failure +it strands the parent with no signal about what to do next — the exact failure this procedure exists to prevent. After each implementation pull request merges, this workflow runs again and -fills newly available slots. Continue until the epic has no open children. +fills newly available slots. Continue until the parent has no open leaf tasks. `/squad implement` remains available as a manual recovery command. ## skill: `squad-research` @@ -600,13 +632,19 @@ Budget-aware breadth-first investigation: architecture mapping, technology audit `add-comment` with `data: {"squad_artifact":"research","schema_version":"1","origin_issue":{issue_number},"phases":[]}`. -Structure: `## 🔬 Squad Research — {Title}` → Summary (2-3 sentences) → Current State → Gap Analysis → Risk & Complexity table (Area|Risk 🟢/🟡/🔴|Complexity S/M/L/XL|Notes) → Key Findings (with evidence) → Recommendations → Next Step (`/squad triage` or `/squad plan`). +Structure: `## 🔬 Squad Research — {Title}` → Summary (2-3 sentences) → **Goals** → **Non-goals** → **Evidence table** (columns `Rn` | Finding | Risk 🟢/🟡/🔴 | Complexity S/M/L/XL | Citation) → **Load-bearing assumptions** → **Open decisions** → **Acceptance framing** → Recommendations (each referencing the `Rn` IDs it rests on) → Next Step (`/squad triage` or `/squad plan`). -Must be ≥200 chars of substantive findings. Tailor sections to scope. +**Structural contract (not a length floor).** The artifact MUST contain every one of these labeled sections: **Evidence table**, **Goals**, **Non-goals**, **Load-bearing assumptions**, **Open decisions**, **Acceptance framing**. Every evidence row carries a stable `Rn` traceability ID (`R1`, `R2`, …) and exactly one citation token — a file path, `path:line`, URL, or `#issue`/`#pr` reference — so each finding is independently checkable. Recommendations and load-bearing assumptions reference the `Rn` IDs they rest on. Assert structure, not length: never pad to hit a size target. ##### Step 4: Verify Completion [MANDATORY] -Confirm: structured artifact data posted, heading present, ≥200 chars substantive content, ≥1 recommendation. If ANY fails, go back and post now. +Confirm ALL of the following, each independently checkable from the posted comment without re-running research. If ANY fails, fix and re-post now: + +1. Structured artifact `data` posted. +2. `## 🔬 Squad Research` heading present. +3. Every required section present: **Evidence table**, **Goals**, **Non-goals**, **Load-bearing assumptions**, **Open decisions**, **Acceptance framing**. +4. Every evidence row has a unique `Rn` ID and exactly one citation token. +5. ≥1 recommendation, each tracing to ≥1 `Rn` ID. ## skill: `squad-plan` --- @@ -623,7 +661,15 @@ Decompose issue into sub-issues as a comment. Does NOT create issues. Works on o 1. Read issue body (the epic/brief). 2. Find latest `research` artifact comment for this issue. If found, use as primary context. If not, do lightweight repo analysis. -3. Read `.squad/team.md` if exists for agent assignments. +3. Read `.squad/team.md` if it exists. **Owner/Agent binding rule:** every + `Owner` and `Agent` value MUST be a cast **Name** taken verbatim from the + `## Members` table's `Name` column of `.squad/team.md` (e.g. `Flight`, + `Procedures`, `EECOM`) — never a Role string (`Lead`, `Prompt Engineer`, + `DevRel`) and never a lowercased role (`lead`, `devrel`, `reviewer`). Map each + work item's domain to an owner via `.squad/routing.md`, then resolve that + owner to its exact `Name`. If no cast member fits, use `@copilot`. This + binding governs every `Owner`/`Agent` column and every `squad:{owner}` label + emitted downstream. 4. Text after `/squad plan` = planning guidance. ##### Step 2: Decompose @@ -636,6 +682,8 @@ Break into discrete work items. **Minimum 3 items** unless genuinely atomic (exp Structure: `## 📋 Squad Plan — {Title}` → reference line → Phase tables (# | Title | Owner | Size | Depends On) → Details per item (Scope, Acceptance criteria, Notes) → Dependency Graph → Execution Notes → Next Steps (`/squad plan accept`, `/squad plan accept phase 1`, `/squad plan revise`, `/squad plan`). +The `Owner` column MUST be a cast **Name** per the Owner/Agent binding rule (Step 1) — a value from the `Name` column of `.squad/team.md`, never a Role string. + Do NOT create issues. ## skill: `squad-plan-accept` @@ -649,9 +697,21 @@ description: Accept a plan (whole plan or a single phase) and record the accepte **Acknowledge:** `🤖 Squad is creating the planned issues…` -##### Step 1: Find Plan +##### Step 1: Find Plan and Route -Find latest `plan` artifact comment for this issue. If none: reply "No plan found. Run `/squad plan` first." +Resolve which planning path this issue is on, in this order: + +1. Find the latest `program` and `implementation` artifacts for this issue. If + **either** exists, this is a granular (long-path) plan. Run the granular + sequence in this exact order — **Accept Scope** (`squad-plan-accept-scope`) → + **Accept Implementation** (`squad-plan-accept-implementation`) → **Activate** + (`squad-plan-activate`) — each honoring its own preconditions (e.g. Accept + Implementation requires a `validation` PASS). Then stop; do NOT run the + legacy fast-path steps below. +2. Otherwise, find the latest `plan` artifact. If found, continue with the + legacy fast-path behavior in the steps below. +3. If none of `program`, `implementation`, or `plan` exist, reply "No plan found. + Run `/squad plan` first." and stop. ##### Step 1a: Phase Resolution @@ -667,7 +727,7 @@ If plan has phases: Root → Phase issues → Task issues. Flat plan: tasks dire For each work item, `create-issue`: - Title: work item title -- Labels: `squad` (color `9B8FCC`), `squad:{owner}` (color `9B8FCC`) +- Labels: `squad` (color `9B8FCC`), `squad:{owner}` (color `9B8FCC`), where `{owner}` is the work item's cast **Name** lowercased (e.g. Owner `Flight` → `squad:flight`). It MUST resolve to a `Name` row in `.squad/team.md`; never mint a role-derived label such as `squad:lead` or `squad:reviewer`. - Body: scope, acceptance criteria, context (parent, phase, size, depends on, owner), notes, footer - Parent: phase issue (hierarchical) or root (flat) - Size: set Project field if available, else body `**Size:**` line @@ -807,11 +867,11 @@ Not created yet — describes what activation will produce. `add-comment` with `data: {"squad_artifact":"program","schema_version":"1","origin_issue":{issue_number},"phases":[]}`. -Structure: `## 📋 Squad Program Plan` → Intent + triage ref → Milestones table (Milestone|Outcome|Contains) → Initiatives & Epics (per initiative: outcome, epic table with Description|Stories|Milestone|Depends On, details per epic with Outcome/Stories/Acceptance criteria) → Unresolved Decisions table → Program Metadata → Dependency Graph → Next: `/squad plan accept scope` or `/squad plan program revise`. +Structure: `## 📋 Squad Program Plan` → Intent + triage ref → Milestones table (Milestone|Outcome|Contains) → Initiatives & Epics (per initiative: outcome, epic table with Description|Stories|Milestone|Depends On, details per epic with Outcome/Stories/Acceptance criteria) → Unresolved Decisions table → Program Metadata → Dependency Graph → Next: `/squad plan implementation` or `/squad plan program revise`. ##### Step 6: Update Lifecycle -Set Program Plan = `✅ Done`, state = Program Planned, next = `/squad plan accept scope`. +Set Program Plan = `✅ Done`, state = Program Planned, next = `/squad plan implementation`. ## skill: `squad-plan-program-revise` --- @@ -850,17 +910,21 @@ Search in order: `scope-accepted` artifact (use as authoritative) → `program` Per task specify: Title, Scope (files/modules/APIs), Acceptance criteria, Size (XS <1h, S 1-3h, M 3-8h, L 1-2d; max per policy default L), Dependencies (task numbers), Agent, Rollout notes. +**Agent binding rule:** every `Agent` value MUST be a cast **Name** from the `Name` column of `.squad/team.md` (resolved via `.squad/routing.md`), never a Role string (`Lead`, `DevRel`) or lowercased role (`lead`, `reviewer`). If no cast member fits, use `@copilot`. + Rules: no task > max_task_size. DAG only. Every task traces to program item. Every epic has ≥1 task. Vertical slices. Group into phases by dependency order (Phase 1 = no deps). ##### Step 3: Validate Structure -Check: sizes ≤ L, no cycles, traceability, coverage, agent validity. Fix before posting. +Check: sizes ≤ L, no cycles, traceability, coverage, agent validity (every `Agent` resolves to a `Name` row in `.squad/team.md`, never a Role string). Fix before posting. ##### Step 4: Post Implementation Plan `add-comment` with `data: {"squad_artifact":"implementation","schema_version":"1","origin_issue":{issue_number},"phases":[]}`. -Structure: `## 🔧 Squad Implementation Plan` → Program ref → Phase tables (Title|Size|Depends On|Agent|Epic) → Details per task (Scope, Acceptance criteria, Dependencies, Rollout, Traces to) → Dependency Graph → Sizing Summary table → Validation Pre-check → Next: `/squad plan validate` or `/squad plan accept implementation`. +Structure: `## 🔧 Squad Implementation Plan` → Program ref → Phase tables (Title|Size|Depends On|Agent|Epic) → Details per task (Scope, Acceptance criteria, Dependencies, Rollout, Traces to) → Dependency Graph → Sizing Summary table → Validation Pre-check → Next: `/squad plan validate`. + +The `Agent` column MUST be a cast **Name** per the Agent binding rule (Step 2) — a value from the `Name` column of `.squad/team.md`, never a Role string. ##### Step 5: Update Lifecycle @@ -909,11 +973,11 @@ Rules: any ❌ = FAILED heading+verdict. Warnings alone ≠ failure. ##### Step 4: Update Lifecycle -Set Validation = `✅ Done` or `❌ Failed`. Next on pass: `/squad plan accept implementation`. On fail: fix + re-run. +Set Validation = `✅ Done` or `❌ Failed`. Next on pass: `/squad plan accept scope`. On fail: fix + re-run. ##### Step 5: Surface Next Action -Pass: suggest accept. Fail: suggest fix + re-validate. +Pass: suggest `/squad plan accept scope`. Fail: suggest fix + re-validate. ## skill: `squad-plan-accept-scope` --- @@ -942,7 +1006,7 @@ Content: `## ✅ Scope Accepted` → program plan version link, accepted by, dat ##### Step 4: Update Lifecycle -Set Scope = `✅ Done`, next = `/squad plan implementation`. +Set Scope = `✅ Done`, next = `/squad plan accept implementation`. ## skill: `squad-plan-accept-implementation` ---