diff --git a/.github/workflows/ai-triage.yml b/.github/workflows/ai-triage.yml index 60615fca5..b07fc8c46 100644 --- a/.github/workflows/ai-triage.yml +++ b/.github/workflows/ai-triage.yml @@ -105,7 +105,7 @@ jobs: }, body: JSON.stringify({ model: 'claude-haiku-4-5-20251001', - max_tokens: 300, + max_tokens: 1024, messages: [{ role: 'user', content: prompt }] }) }); @@ -126,7 +126,13 @@ jobs: return; } - const result = JSON.parse(jsonMatch[0]); + let result; + try { + result = JSON.parse(jsonMatch[0]); + } catch (e) { + core.setFailed(`JSON parse error: ${e.message}\nRaw text: ${text}`); + return; + } core.info(`Triage result: ${JSON.stringify(result, null, 2)}`); // Apply labels diff --git a/src/resources/extensions/gsd/observability-validator.ts b/src/resources/extensions/gsd/observability-validator.ts index 99d109752..0fb87f5d2 100644 --- a/src/resources/extensions/gsd/observability-validator.ts +++ b/src/resources/extensions/gsd/observability-validator.ts @@ -235,6 +235,33 @@ export function validateTaskPlanContent(file: string, content: string): Validati } } + // Rule: Inputs and Expected Output should contain backtick-wrapped file paths + const inputsSection = getSection(content, "Inputs", 2); + const outputSection = getSection(content, "Expected Output", 2); + const backtickPathPattern = /`[^`]*[./][^`]*`/; + + if (outputSection === null || !backtickPathPattern.test(outputSection)) { + issues.push({ + severity: "warning", + scope: "task-plan", + file, + ruleId: "missing_output_file_paths", + message: "Task plan `## Expected Output` is missing or has no backtick-wrapped file paths.", + suggestion: "List concrete output file paths in backticks (e.g. `src/types.ts`). These are machine-parsed to derive task dependencies.", + }); + } + + if (inputsSection !== null && inputsSection.trim().length > 0 && !backtickPathPattern.test(inputsSection)) { + issues.push({ + severity: "info", + scope: "task-plan", + file, + ruleId: "missing_input_file_paths", + message: "Task plan `## Inputs` has content but no backtick-wrapped file paths.", + suggestion: "List input file paths in backticks (e.g. `src/config.json`). These are machine-parsed to derive task dependencies.", + }); + } + // ── Observability rules (gated by runtime relevance) ── const relevant = textSuggestsObservabilityRelevant(content); diff --git a/src/resources/extensions/gsd/prompts/plan-slice.md b/src/resources/extensions/gsd/prompts/plan-slice.md index 4db1914da..e4ba4aad1 100644 --- a/src/resources/extensions/gsd/prompts/plan-slice.md +++ b/src/resources/extensions/gsd/prompts/plan-slice.md @@ -61,13 +61,14 @@ Then: - a concrete, action-oriented title - the inline task entry fields defined in the plan.md template (Why / Files / Do / Verify / Done when) - a matching task plan file with description, steps, must-haves, verification, inputs, and expected output + - **Inputs and Expected Output must list concrete backtick-wrapped file paths** (e.g. `` `src/types.ts` ``). These are machine-parsed to derive task dependencies — vague prose without paths breaks parallel execution. Every task must have at least one output file path. - Observability Impact section **only if the task touches runtime boundaries, async flows, or error paths** — omit it otherwise 6. Write `{{outputPath}}` 7. Write individual task plans in `{{slicePath}}/tasks/`: `T01-PLAN.md`, `T02-PLAN.md`, etc. 8. **Self-audit the plan.** Walk through each check — if any fail, fix the plan files before moving on: - **Completion semantics:** If every task were completed exactly as written, the slice goal/demo should actually be true. - **Requirement coverage:** Every must-have in the slice maps to at least one task. No must-have is orphaned. If `REQUIREMENTS.md` exists, every Active requirement this slice owns maps to at least one task. - - **Task completeness:** Every task has steps, must-haves, verification, inputs, and expected output — none are blank or vague. + - **Task completeness:** Every task has steps, must-haves, verification, inputs, and expected output — none are blank or vague. Inputs and Expected Output list backtick-wrapped file paths, not prose descriptions. - **Dependency correctness:** Task ordering is consistent. No task references work from a later task. - **Key links planned:** For every pair of artifacts that must connect, there is an explicit step that wires them. - **Scope sanity:** Target 2–5 steps and 3–8 files per task. 10+ steps or 12+ files — must split. Each task must be completable in a single fresh context window. diff --git a/src/resources/extensions/gsd/templates/task-plan.md b/src/resources/extensions/gsd/templates/task-plan.md index 7b1121f15..00aa78955 100644 --- a/src/resources/extensions/gsd/templates/task-plan.md +++ b/src/resources/extensions/gsd/templates/task-plan.md @@ -42,11 +42,19 @@ estimated_files: {{estimatedFiles}} ## Inputs + + - `{{filePath}}` — {{whatThisTaskNeedsFromPriorWork}} -- {{priorTaskSummaryInsight}} ## Expected Output - + -- `{{filePath}}` — {{whatThisTaskShouldProduceOrModify}} +- `{{filePath}}` — {{whatThisTaskCreatesOrModifies}} diff --git a/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts b/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts index ad4429877..fdbc8de0c 100644 --- a/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +++ b/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts @@ -360,4 +360,115 @@ console.log('\n=== Clean slice plan: no plan-quality issues ==='); assertEq(planQualityIssues.length, 0, 'clean slice plan produces no empty_task_entry issues'); } +// ═══════════════════════════════════════════════════════════════════════════ +// validateTaskPlanContent — missing output file paths +// ═══════════════════════════════════════════════════════════════════════════ + +console.log('\n=== validateTaskPlanContent: missing output file paths ==='); +{ + const content = `# T01: Some Task + +## Description + +Do something. + +## Steps + +1. Do the thing + +## Verification + +- Check it works + +## Expected Output + +This task produces the main output. +`; + + const issues = validateTaskPlanContent('T01-PLAN.md', content); + const outputIssues = issues.filter(i => i.ruleId === 'missing_output_file_paths'); + assertTrue(outputIssues.length >= 1, 'Expected Output without file paths triggers missing_output_file_paths'); +} + +console.log('\n=== validateTaskPlanContent: valid output file paths ==='); +{ + const content = `# T01: Some Task + +## Description + +Do something. + +## Steps + +1. Do the thing + +## Verification + +- Check it works + +## Expected Output + +- \`src/types.ts\` — New type definitions +`; + + const issues = validateTaskPlanContent('T01-PLAN.md', content); + const outputIssues = issues.filter(i => i.ruleId === 'missing_output_file_paths'); + assertEq(outputIssues.length, 0, 'Expected Output with file paths does not trigger warning'); +} + +console.log('\n=== validateTaskPlanContent: missing input file paths (info severity) ==='); +{ + const content = `# T01: Some Task + +## Description + +Do something. + +## Steps + +1. Do the thing + +## Verification + +- Check it works + +## Inputs + +Prior task summary insights about the architecture. + +## Expected Output + +- \`src/output.ts\` — Output file +`; + + const issues = validateTaskPlanContent('T01-PLAN.md', content); + const inputIssues = issues.filter(i => i.ruleId === 'missing_input_file_paths'); + assertTrue(inputIssues.length >= 1, 'Inputs without file paths triggers missing_input_file_paths'); + if (inputIssues.length > 0) { + assertEq(inputIssues[0].severity, 'info', 'missing_input_file_paths is info severity (not warning)'); + } +} + +console.log('\n=== validateTaskPlanContent: no Expected Output section at all ==='); +{ + const content = `# T01: Some Task + +## Description + +Do something. + +## Steps + +1. Do the thing + +## Verification + +- Check it works +`; + + const issues = validateTaskPlanContent('T01-PLAN.md', content); + const outputIssues = issues.filter(i => i.ruleId === 'missing_output_file_paths'); + assertTrue(outputIssues.length >= 1, 'Missing Expected Output section triggers missing_output_file_paths'); +} + report();