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
65 changes: 65 additions & 0 deletions test/gh-aw-quality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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);
});
});
18 changes: 17 additions & 1 deletion workflows/squad.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ safe-outputs:
expires: 14d
create-issue:
labels: [squad]
max: 50
max: 75
add-comment:
max: 20
---
Expand Down Expand Up @@ -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.
Comment on lines +726 to +728

##### Step 2: Create Issues — Full Hierarchy

Root → Epics → Tasks. Phase-specific: filter to matching phase heading.
Expand Down