fix(gsd): enforce backtick file paths in task plan IO sections (#1548)

* fix(gsd): enforce backtick file paths in task plan IO sections

The reactive task graph (ADR-004) derives dependencies from backtick-wrapped
file paths in ## Inputs and ## Expected Output sections. Without concrete
paths, the graph is ambiguous and falls back to sequential execution.

Changes:
- task-plan.md template: add comments explaining paths are machine-parsed
- plan-slice.md prompt: explicitly instruct planner to write backtick file
  paths in IO sections, add self-audit check for path presence
- observability-validator.ts: new validation rules missing_output_file_paths
  (warning) and missing_input_file_paths (info) catch plans without paths
- plan-quality-validator.test.ts: 4 new test cases for IO path validation

* fix(ci): increase max_tokens and add JSON parse error handling in ai-triage

max_tokens: 300 was too low, causing truncated JSON responses from Claude
that failed to parse. Bumped to 1024 and added try/catch with raw text
logging for easier debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-19 23:28:44 -06:00 committed by GitHub
parent 567751471a
commit e4cd141503
5 changed files with 159 additions and 6 deletions

View file

@ -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

View file

@ -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);

View file

@ -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 25 steps and 38 files per task. 10+ steps or 12+ files — must split. Each task must be completable in a single fresh context window.

View file

@ -42,11 +42,19 @@ estimated_files: {{estimatedFiles}}
## Inputs
<!-- Every input MUST be a backtick-wrapped file path. These paths are machine-parsed to
derive task dependencies — vague descriptions without paths break dependency detection.
For the first task in a slice with no prior task outputs, list the existing source files
this task reads or modifies. -->
- `{{filePath}}` — {{whatThisTaskNeedsFromPriorWork}}
- {{priorTaskSummaryInsight}}
## Expected Output
<!-- This task should produce a real increment toward making the slice goal/demo true. A full slice plan should not be able to mark every task complete while the claimed slice behavior still does not work at the stated proof level. -->
<!-- Every output MUST be a backtick-wrapped file path — the specific files this task creates
or modifies. These paths are machine-parsed to derive task dependencies.
This task should produce a real increment toward making the slice goal/demo true. A full
slice plan should not be able to mark every task complete while the claimed slice behavior
still does not work at the stated proof level. -->
- `{{filePath}}` — {{whatThisTaskShouldProduceOrModify}}
- `{{filePath}}` — {{whatThisTaskCreatesOrModifies}}

View file

@ -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();