diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index db6e7dfce..1d9abe10a 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -238,6 +238,12 @@ describe('gh-aw: safe-output configuration', () => { expect(safeOutputs['create-issue'], 'create-issue should exist').toBeDefined(); expect(safeOutputs['add-comment'], 'add-comment should exist').toBeDefined(); }); + + it('create-issue max is 75 (supports large plans, forward-port of #1683)', () => { + const ci = safeOutputs['create-issue']; + expect(ci, 'create-issue block must exist').toBeDefined(); + expect(ci['max'], 'create-issue max must be 75 — do not reduce below this').toBe(75); + }); }); // --------------------------------------------------------------------------- @@ -528,4 +534,63 @@ describe('gh-aw: prompt budget & planning import regression', () => { `Combined prompt is ${totalKB} KB — exceeds the gh-aw ${GH_AW_PROMPT_CEILING_KB} KB ceiling. Headroom: ${headroomKB} KB.` ).toBeLessThan(GH_AW_PROMPT_CEILING_BYTES); }); + + it('reports combined bytes and headroom', () => { + let totalBytes = Buffer.byteLength(squadContent, 'utf8'); + for (const importPath of imports) { + const fullPath = join(WORKFLOWS_DIR, importPath); + if (existsSync(fullPath)) totalBytes += Buffer.byteLength(readFileSync(fullPath, 'utf8'), 'utf8'); + } + const headroomBytes = GH_AW_PROMPT_CEILING_BYTES - totalBytes; + // Informational — log bytes/headroom; fail only if headroom < 5 KB (regression guard) + expect( + headroomBytes, + `Headroom too low: ${(headroomBytes / 1024).toFixed(1)} KB remaining of ${GH_AW_PROMPT_CEILING_KB} KB ceiling` + ).toBeGreaterThan(5 * 1024); + }); +}); + +// --------------------------------------------------------------------------- +// Test: Plan Activate hardening behaviors (forward-port #1683) +// --------------------------------------------------------------------------- + +describe('gh-aw: Plan Activate hardening behaviors', () => { + const content = readFileSync(SQUAD_WORKFLOW, 'utf8'); + + it('includes output budget awareness guidance', () => { + expect(content).toContain('Output Budget Awareness'); + expect(content).toMatch(/total.*>\s*50.*phased activation|phased.*activation.*>\s*50/i); + }); + + it('includes label pre-flight step before issue creation', () => { + expect(content).toContain('Label Pre-flight'); + expect(content).toMatch(/squad.*label.*exist|label.*squad.*exist/i); + }); + + it('label pre-flight does not claim impossible label creation', () => { + // Workflow has issues: read and no create-label safe-output; must not claim it can create labels + const preflight = content.match(/##### Label Pre-flight\n([\s\S]*?)(?=\n#####|\n####)/)?.[1] ?? ''; + expect(preflight).not.toMatch(/create them|safe-output permissions handle the write|no additional token scope/i); + }); + + it('label pre-flight reports missing labels as prerequisite gap', () => { + expect(content).toMatch(/prerequisite gap|prerequisite/i); + expect(content).toMatch(/issues: write/i); + expect(content).toMatch(/create-label.*safe-output|safe-output.*create-label/i); + }); + + it('label pre-flight continues activation and omits unavailable labels with report', () => { + expect(content).toMatch(/unavailable labels are omitted and reported|omitted and reported/i); + }); + + it('includes transient failure handling with single retry', () => { + expect(content).toContain('Transient Failure Handling'); + expect(content).toMatch(/5xx.*retry|retry.*5xx/i); + }); + + it('includes graceful sub-issue fallback for 404/422', () => { + expect(content).toContain('Sub-issue Fallback'); + expect(content).toMatch(/404.*422|422.*404/); + expect(content).toMatch(/degrade gracefully|graceful/i); + }); }); diff --git a/workflows/squad.md b/workflows/squad.md index d817ae76e..a1889904e 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -50,7 +50,7 @@ safe-outputs: expires: 14d create-issue: labels: [squad] - max: 50 + max: 75 add-comment: max: 20 --- @@ -711,6 +711,22 @@ After phase acceptance, check if ready for automatic activation: After EVERY `create-issue` call: verify returned issue number, stop on failure, NEVER predict issue numbers. +##### Output Budget Awareness + +Count expected issues before starting. If total > 50: recommend phased activation (`/squad plan activate phase {N}`) and proceed with the current phase only. If total > 30: use compact issue bodies (scope + acceptance criteria only; omit elaboration). + +##### Label Pre-flight + +Before the first `create-issue`, verify labels `squad` and any `squad:{agent}` exist. If missing, record them in the activation summary as a prerequisite gap (label creation requires `issues: write` + `create-label` safe-output — not configured in this workflow). Continue activation — `create-issue` will apply any existing labels normally; unavailable labels are omitted and reported, not silently applied. + +##### Transient Failure Handling + +On `5xx` response from `create-issue`: wait briefly and retry once. On second failure or `4xx`: record the issue title as skipped in the activation summary, continue with remaining issues. Never abort the full run for a single transient failure. + +##### Sub-issue Fallback + +When setting a `parent` sub-issue relationship returns `404` or `422` (feature disabled or repo plan): degrade gracefully — record the intended parent as a body reference (`Parent: #{issue_number}`), then continue. Never fail activation over sub-issue API unavailability. + ##### Step 2: Create Issues — Full Hierarchy Root → Epics → Tasks. Phase-specific: filter to matching phase heading.