Two compounding bugs caused auto-mode to re-dispatch run-uat indefinitely after UAT passed: 1. markSliceDoneInRoadmap regex required dash at line start (^-) but the roadmap parser accepts optional leading whitespace (^\s*-). When LLMs indented checklist items, the doctor could never mark them done. 2. After run-uat completed, handleAgentEnd ran doctor with fixLevel:"task" which explicitly excluded slice-level completion transitions. Since run-uat is the terminal unit for a slice, the roadmap checkbox stayed unchecked, causing deriveState to return the same slice indefinitely. Fix: Update markSliceDoneInRoadmap and markTaskDoneInPlan regexes to accept leading whitespace (matching the parser), preserving indentation in the replacement. Add run-uat to the set of unit types that use fixLevel:"all" in handleAgentEnd closeout.
This commit is contained in:
parent
55769392af
commit
306c205dfc
3 changed files with 96 additions and 5 deletions
|
|
@ -1245,12 +1245,16 @@ export async function handleAgentEnd(
|
|||
// fixLevel:"task" ensures doctor only fixes task-level issues (e.g. marking
|
||||
// checkboxes). Slice/milestone completion transitions (summary stubs,
|
||||
// roadmap [x] marking) are left for the complete-slice dispatch unit.
|
||||
// Exception: after complete-slice itself, use fixLevel:"all" so roadmap
|
||||
// checkboxes get fixed even if complete-slice crashed (#839).
|
||||
// Exception: after complete-slice and run-uat, use fixLevel:"all" so roadmap
|
||||
// checkboxes get fixed. run-uat is the terminal unit for a slice — if the
|
||||
// roadmap checkbox wasn't marked done by complete-slice (e.g. edit failure),
|
||||
// fixing it here prevents the state machine from re-dispatching run-uat
|
||||
// indefinitely (#839, #1063).
|
||||
try {
|
||||
const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
|
||||
const doctorScope = scopeParts.join("/");
|
||||
const effectiveFixLevel = s.currentUnit.type === "complete-slice" ? "all" as const : "task" as const;
|
||||
const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
|
||||
const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" as const : "task" as const;
|
||||
const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
|
||||
if (report.fixesApplied.length > 0) {
|
||||
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
||||
|
|
|
|||
|
|
@ -308,7 +308,13 @@ async function markTaskDoneInPlan(basePath: string, milestoneId: string, sliceId
|
|||
if (!planPath) return;
|
||||
const content = await loadFile(planPath);
|
||||
if (!content) return;
|
||||
const updated = content.replace(new RegExp(`^-\\s+\\[ \\]\\s+\\*\\*${taskId}:`, "m"), `- [x] **${taskId}:`);
|
||||
// Allow optional leading whitespace to match the same patterns the plan parser
|
||||
// accepts. Capture the leading whitespace + "- " so the replacement preserves
|
||||
// indentation instead of collapsing it (#1063).
|
||||
const updated = content.replace(
|
||||
new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${taskId}:`, "m"),
|
||||
`$1[x] **${taskId}:`,
|
||||
);
|
||||
if (updated !== content) {
|
||||
await saveFile(planPath, updated);
|
||||
fixesApplied.push(`marked ${taskId} done in ${planPath}`);
|
||||
|
|
@ -320,7 +326,13 @@ async function markSliceDoneInRoadmap(basePath: string, milestoneId: string, sli
|
|||
if (!roadmapPath) return;
|
||||
const content = await loadFile(roadmapPath);
|
||||
if (!content) return;
|
||||
const updated = content.replace(new RegExp(`^-\\s+\\[ \\]\\s+\\*\\*${sliceId}:`, "m"), `- [x] **${sliceId}:`);
|
||||
// Allow optional leading whitespace to match the same patterns the roadmap
|
||||
// parser accepts (^\s*-\s+ in roadmap-slices.ts). Capture the prefix so the
|
||||
// replacement preserves original indentation (#1063).
|
||||
const updated = content.replace(
|
||||
new RegExp(`^(\\s*-\\s+)\\[ \\]\\s+\\*\\*${sliceId}:`, "m"),
|
||||
`$1[x] **${sliceId}:`,
|
||||
);
|
||||
if (updated !== content) {
|
||||
await saveFile(roadmapPath, updated);
|
||||
fixesApplied.push(`marked ${sliceId} done in ${roadmapPath}`);
|
||||
|
|
|
|||
|
|
@ -115,6 +115,81 @@ test("fixLevel:all (default) — detects AND fixes completion issues", async ()
|
|||
}
|
||||
});
|
||||
|
||||
test("fixLevel:all — marks indented roadmap checkboxes done (#1063)", async () => {
|
||||
const tmp = makeTmp("indented-roadmap");
|
||||
try {
|
||||
buildScaffold(tmp);
|
||||
|
||||
// Overwrite roadmap with indented checkbox (LLM formatting drift)
|
||||
writeFileSync(join(tmp, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), `# M001: Test
|
||||
|
||||
## Slices
|
||||
|
||||
- [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
|
||||
> Demo text
|
||||
`);
|
||||
|
||||
const report = await runGSDDoctor(tmp, { fix: true });
|
||||
|
||||
const roadmapContent = readFileSync(join(tmp, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "utf8");
|
||||
// Should mark [x] while preserving the leading whitespace
|
||||
assert.ok(roadmapContent.includes(" - [x] **S01"), "indented roadmap checkbox should be marked done");
|
||||
// Verify indentation is preserved: line should start with " -", not just "-"
|
||||
const checkedLine = roadmapContent.split("\n").find(l => l.includes("[x] **S01"));
|
||||
assert.ok(checkedLine?.startsWith(" -"), `should preserve leading whitespace, got: "${checkedLine}"`);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("fixLevel:all — marks indented task checkboxes done (#1063)", async () => {
|
||||
const tmp = makeTmp("indented-task");
|
||||
try {
|
||||
const gsd = join(tmp, ".gsd");
|
||||
const m = join(gsd, "milestones", "M001");
|
||||
const s = join(m, "slices", "S01", "tasks");
|
||||
mkdirSync(s, { recursive: true });
|
||||
|
||||
writeFileSync(join(m, "M001-ROADMAP.md"), `# M001: Test
|
||||
|
||||
## Slices
|
||||
|
||||
- [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
|
||||
`);
|
||||
|
||||
// Plan with indented checkbox
|
||||
writeFileSync(join(m, "slices", "S01", "S01-PLAN.md"), `# S01: Test Slice
|
||||
|
||||
**Goal:** test
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] **T01: Do stuff** \`est:5m\`
|
||||
`);
|
||||
|
||||
writeFileSync(join(s, "T01-SUMMARY.md"), `---
|
||||
id: T01
|
||||
parent: S01
|
||||
milestone: M001
|
||||
duration: 5m
|
||||
verification_result: passed
|
||||
completed_at: 2026-01-01
|
||||
---
|
||||
|
||||
# T01: Do stuff
|
||||
|
||||
Done.
|
||||
`);
|
||||
|
||||
const report = await runGSDDoctor(tmp, { fix: true, fixLevel: "task" });
|
||||
|
||||
const planContent = readFileSync(join(m, "slices", "S01", "S01-PLAN.md"), "utf8");
|
||||
assert.ok(planContent.includes(" - [x] **T01"), "indented task checkbox should be marked done");
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("fixLevel:task — still fixes task-level bookkeeping (checkbox marking)", async () => {
|
||||
const tmp = makeTmp("task-checkbox");
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue