fix: detect REPLAN-TRIGGER.md in deriveState for triage-initiated replans (#1798)
This commit is contained in:
parent
ad85995108
commit
3e4be6babf
2 changed files with 107 additions and 0 deletions
|
|
@ -740,6 +740,39 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|||
// REPLAN.md exists — loop protection: fall through to normal executing
|
||||
}
|
||||
|
||||
// ── REPLAN-TRIGGER detection: triage-initiated replan ──────────────────
|
||||
// Manual `/gsd triage` writes REPLAN-TRIGGER.md when a capture is classified
|
||||
// as "replan". Detect it here and transition to replanning-slice so the
|
||||
// dispatch loop picks it up (instead of silently advancing past it).
|
||||
if (!blockerTaskId) {
|
||||
const replanTriggerFile = resolveSliceFile(basePath, activeMilestone.id, activeSlice.id, "REPLAN-TRIGGER");
|
||||
if (replanTriggerFile) {
|
||||
// Same loop protection: if REPLAN.md already exists, a replan was
|
||||
// already performed — skip further replanning and continue executing.
|
||||
const replanFile = resolveSliceFile(basePath, activeMilestone.id, activeSlice.id, "REPLAN");
|
||||
if (!replanFile) {
|
||||
return {
|
||||
activeMilestone,
|
||||
activeSlice,
|
||||
activeTask,
|
||||
phase: 'replanning-slice',
|
||||
recentDecisions: [],
|
||||
blockers: ['Triage replan trigger detected — slice replan required'],
|
||||
nextAction: `Triage replan triggered for slice ${activeSlice.id}. Replan before continuing.`,
|
||||
|
||||
activeWorkspace: undefined,
|
||||
registry,
|
||||
requirements,
|
||||
progress: {
|
||||
milestones: milestoneProgress,
|
||||
slices: sliceProgress,
|
||||
tasks: taskProgress,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for interrupted work
|
||||
const sDir = resolveSlicePath(basePath, activeMilestone.id, activeSlice.id);
|
||||
const continueFile = sDir ? resolveSliceFile(basePath, activeMilestone.id, activeSlice.id, "CONTINUE") : null;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,12 @@ function writeReplanFile(base: string, mid: string, sid: string, content: string
|
|||
writeFileSync(join(dir, `${sid}-REPLAN.md`), content);
|
||||
}
|
||||
|
||||
function writeReplanTrigger(base: string, mid: string, sid: string, content: string): void {
|
||||
const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, `${sid}-REPLAN-TRIGGER.md`), content);
|
||||
}
|
||||
|
||||
/** Standard roadmap with one slice having no dependencies */
|
||||
const ROADMAP_ONE_SLICE = `# M001: Test Milestone
|
||||
|
||||
|
|
@ -535,4 +541,72 @@ console.log('\n=== artifact: verifyExpectedArtifact passes when REPLAN.md exists
|
|||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// REPLAN-TRIGGER.md detection (triage-initiated replan, #1701)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// (a) REPLAN-TRIGGER.md exists + no REPLAN.md → replanning-slice
|
||||
console.log('\n=== deriveState: REPLAN-TRIGGER.md exists, no REPLAN → replanning-slice (#1701) ===');
|
||||
{
|
||||
const base = createFixtureBase();
|
||||
writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
|
||||
writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
|
||||
// No blocker in task summary — the trigger comes from triage, not blocker_discovered
|
||||
writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', false));
|
||||
writeReplanTrigger(base, 'M001', 'S01', '# Replan Trigger\n\n**Source:** Capture C001\n');
|
||||
|
||||
const state = await deriveState(base);
|
||||
assertEq(state.phase, 'replanning-slice', 'phase is replanning-slice when REPLAN-TRIGGER.md exists');
|
||||
assertTrue(state.blockers.length > 0, 'blockers array is non-empty for triage replan trigger');
|
||||
assertTrue(state.nextAction.includes('Triage replan'), 'nextAction mentions triage replan');
|
||||
assertEq(state.activeSlice?.id, 'S01', 'activeSlice is S01');
|
||||
assertEq(state.activeTask?.id, 'T02', 'activeTask is T02 (next incomplete task)');
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// (b) REPLAN-TRIGGER.md + REPLAN.md both exist → executing (loop protection)
|
||||
console.log('\n=== deriveState: REPLAN-TRIGGER.md + REPLAN.md → executing (loop protection, #1701) ===');
|
||||
{
|
||||
const base = createFixtureBase();
|
||||
writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
|
||||
writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
|
||||
writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', false));
|
||||
writeReplanTrigger(base, 'M001', 'S01', '# Replan Trigger\n\n**Source:** Capture C001\n');
|
||||
writeReplanFile(base, 'M001', 'S01', '# Replan\n\nAlready replanned.');
|
||||
|
||||
const state = await deriveState(base);
|
||||
assertEq(state.phase, 'executing', 'phase is executing when REPLAN.md exists (loop protection)');
|
||||
assertEq(state.activeTask?.id, 'T02', 'activeTask is T02');
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// (c) No REPLAN-TRIGGER.md, no blocker → executing (no false positive)
|
||||
console.log('\n=== deriveState: no REPLAN-TRIGGER.md, no blocker → executing (#1701) ===');
|
||||
{
|
||||
const base = createFixtureBase();
|
||||
writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
|
||||
writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
|
||||
writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', false));
|
||||
|
||||
const state = await deriveState(base);
|
||||
assertEq(state.phase, 'executing', 'phase is executing when no trigger and no blocker');
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// (d) blocker_discovered takes priority over REPLAN-TRIGGER.md
|
||||
console.log('\n=== deriveState: blocker_discovered takes priority over REPLAN-TRIGGER.md (#1701) ===');
|
||||
{
|
||||
const base = createFixtureBase();
|
||||
writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
|
||||
writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
|
||||
writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', true));
|
||||
writeReplanTrigger(base, 'M001', 'S01', '# Replan Trigger\n\n**Source:** Capture C001\n');
|
||||
|
||||
const state = await deriveState(base);
|
||||
assertEq(state.phase, 'replanning-slice', 'phase is replanning-slice');
|
||||
// blocker_discovered path should fire first (blockerTaskId is set, so REPLAN-TRIGGER check is skipped)
|
||||
assertTrue(state.nextAction.includes('T01'), 'nextAction mentions blocker task T01 (blocker path, not trigger path)');
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
report();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue