fix(doctor): prevent cleanup from deleting user work files (#1825)
The doctor's orphaned_completed_units check was running at fixLevel="task" during every auto-mode post-unit cycle. When artifact files were temporarily unreachable (worktree sync timing, path resolution), the doctor removed their completion keys from completed-units.json. This caused deriveState to consider those tasks incomplete, reverting the user to an earlier slice and discarding all work past that point. Add orphaned_completed_units to GLOBAL_STATE_CODES so it is never auto-fixed at fixLevel="task". Only explicit manual doctor runs (fixLevel="all") can now remove completion keys. Fixes #1809 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
42f23630e7
commit
b741090870
2 changed files with 49 additions and 2 deletions
|
|
@ -84,12 +84,19 @@ export const COMPLETION_TRANSITION_CODES = new Set<DoctorIssueCode>([
|
|||
]);
|
||||
|
||||
/**
|
||||
* Issue codes that represent global (cross-project) state.
|
||||
* Issue codes that represent global or completion-critical state.
|
||||
* These must NOT be auto-fixed when fixLevel is "task" — automated
|
||||
* post-task health checks must never delete external project state directories.
|
||||
* post-task health checks must never delete external project state directories
|
||||
* or remove completed-unit keys (which causes state reversion / data loss).
|
||||
*
|
||||
* orphaned_completed_units: Removing completed-unit keys causes deriveState to
|
||||
* consider those tasks incomplete, reverting the user to an earlier slice and
|
||||
* effectively discarding all work past that point (#1809). This must only be
|
||||
* fixed by an explicit manual doctor run (fixLevel="all").
|
||||
*/
|
||||
export const GLOBAL_STATE_CODES = new Set<DoctorIssueCode>([
|
||||
"orphaned_project_state",
|
||||
"orphaned_completed_units",
|
||||
]);
|
||||
|
||||
export interface DoctorIssue {
|
||||
|
|
|
|||
|
|
@ -346,6 +346,46 @@ node_modules/
|
|||
console.log("\n=== stranded_lock_directory (skipped on Windows) ===");
|
||||
}
|
||||
|
||||
// ─── Test: orphaned_completed_units NOT auto-fixed at fixLevel="task" (#1809) ──
|
||||
// Regression: task-level doctor was removing completed-unit keys whose artifacts
|
||||
// were temporarily missing, causing deriveState to revert the user to S01 and
|
||||
// effectively discarding hours of work.
|
||||
console.log("\n=== orphaned_completed_units protected at fixLevel=task (#1809) ===");
|
||||
{
|
||||
const dir = createMinimalProject();
|
||||
cleanups.push(dir);
|
||||
|
||||
// Write completed-units.json with keys that reference non-existent artifacts.
|
||||
// At fixLevel="task" (auto-mode post-unit), these must NOT be removed.
|
||||
const completedKeys = [
|
||||
"execute-task/M001/S01/T99", // artifact missing
|
||||
"complete-slice/M001/S99", // artifact missing
|
||||
];
|
||||
writeFileSync(join(dir, ".gsd", "completed-units.json"), JSON.stringify(completedKeys));
|
||||
|
||||
// fixLevel="task" — the level used by auto-post-unit after every task
|
||||
const taskLevelFix = await runGSDDoctor(dir, { fix: true, fixLevel: "task" });
|
||||
const taskLevelOrphan = taskLevelFix.issues.filter(i => i.code === "orphaned_completed_units");
|
||||
assertTrue(taskLevelOrphan.length > 0, "orphaned_completed_units detected at task fixLevel");
|
||||
|
||||
// Verify keys were NOT removed — the fix must be suppressed at task level
|
||||
const afterTaskFix = JSON.parse(readFileSync(join(dir, ".gsd", "completed-units.json"), "utf-8"));
|
||||
assertEq(afterTaskFix.length, 2, "completed-unit keys preserved at fixLevel=task (data loss prevention)");
|
||||
assertTrue(
|
||||
!taskLevelFix.fixesApplied.some(f => f.includes("orphaned")),
|
||||
"no orphaned-units fix applied at fixLevel=task",
|
||||
);
|
||||
|
||||
// fixLevel="all" (explicit manual doctor) — fix SHOULD apply
|
||||
const allLevelFix = await runGSDDoctor(dir, { fix: true, fixLevel: "all" });
|
||||
assertTrue(
|
||||
allLevelFix.fixesApplied.some(f => f.includes("orphaned")),
|
||||
"orphaned-units fix applied at fixLevel=all (manual doctor)",
|
||||
);
|
||||
const afterAllFix = JSON.parse(readFileSync(join(dir, ".gsd", "completed-units.json"), "utf-8"));
|
||||
assertEq(afterAllFix.length, 0, "orphaned keys removed at fixLevel=all");
|
||||
}
|
||||
|
||||
} finally {
|
||||
for (const dir of cleanups) {
|
||||
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue