fix(gsd): write safety — atomic writes and randomized tmp paths (wave 4/5)
Three write-safety fixes: 1. json-persistence.ts: Fixed .tmp suffix replaced with randomized suffix using crypto.randomBytes(4). Prevents concurrent-write data loss when two callers write the same JSON file simultaneously (metrics ledger at risk during parallel slice execution). 2. undo.ts: Raw writeFileSync on PLAN.md replaced with atomicWriteSync. Prevents crash mid-write from corrupting PLAN.md permanently. 3. triage-resolution.ts: All 6 writeFileSync calls replaced with atomicWriteSync. Covers PLAN.md inject, REPLAN-TRIGGER.md, REGRESSION.md, and CONTEXT-DRAFT.md writes.
This commit is contained in:
parent
0dd7c31213
commit
c595eb2b22
3 changed files with 17 additions and 12 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
/**
|
||||
* Load a JSON file with validation, returning a default on failure.
|
||||
|
|
@ -51,9 +52,11 @@ export function loadJsonFileOrNull<T>(
|
|||
export function saveJsonFile<T>(filePath: string, data: T): void {
|
||||
try {
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
const tmp = filePath + ".tmp";
|
||||
// Use randomized tmp suffix to prevent concurrent-write data loss
|
||||
const tmp = `${filePath}.tmp.${randomBytes(4).toString("hex")}`;
|
||||
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
||||
renameSync(tmp, filePath);
|
||||
// No cleanup needed — renameSync atomically removes tmp on success
|
||||
} catch {
|
||||
// Non-fatal — don't let persistence failures break operation
|
||||
}
|
||||
|
|
@ -66,7 +69,7 @@ export function saveJsonFile<T>(filePath: string, data: T): void {
|
|||
export function writeJsonFileAtomic<T>(filePath: string, data: T): void {
|
||||
try {
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
const tmp = filePath + ".tmp";
|
||||
const tmp = `${filePath}.tmp.${randomBytes(4).toString("hex")}`;
|
||||
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
||||
renameSync(tmp, filePath);
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
* Also provides detectFileOverlap() for surfacing downstream impact on quick tasks.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
||||
import { atomicWriteSync } from "./atomic-write.js";
|
||||
import { join } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { gsdRoot, milestonesDir } from "./paths.js";
|
||||
|
|
@ -65,10 +66,10 @@ export function executeInject(
|
|||
const filesSection = content.indexOf("## Files Likely Touched");
|
||||
if (filesSection !== -1) {
|
||||
const updated = content.slice(0, filesSection) + newTask + "\n\n" + content.slice(filesSection);
|
||||
writeFileSync(planPath, updated, "utf-8");
|
||||
atomicWriteSync(planPath, updated, "utf-8");
|
||||
} else {
|
||||
// No Files section — append at end
|
||||
writeFileSync(planPath, content.trimEnd() + "\n\n" + newTask + "\n", "utf-8");
|
||||
atomicWriteSync(planPath, content.trimEnd() + "\n\n" + newTask + "\n", "utf-8");
|
||||
}
|
||||
|
||||
return newId;
|
||||
|
|
@ -105,7 +106,7 @@ export function executeReplan(
|
|||
`will detect it and enter the replanning-slice phase.`,
|
||||
].join("\n");
|
||||
|
||||
writeFileSync(triggerPath, content, "utf-8");
|
||||
atomicWriteSync(triggerPath, content, "utf-8");
|
||||
|
||||
// Also write replan_triggered_at column for DB-backed detection
|
||||
try {
|
||||
|
|
@ -183,7 +184,7 @@ export function executeBacktrack(
|
|||
`3. Resume auto-mode — the state machine will re-enter discussion for the target`,
|
||||
].join("\n");
|
||||
|
||||
writeFileSync(triggerPath, content, "utf-8");
|
||||
atomicWriteSync(triggerPath, content, "utf-8");
|
||||
|
||||
// If we have a valid target, also reset that milestone's completion status
|
||||
// so deriveState() will re-enter it as the active milestone.
|
||||
|
|
@ -194,7 +195,7 @@ export function executeBacktrack(
|
|||
// Write a regression marker so the state machine knows this milestone
|
||||
// needs re-discussion, not just re-execution
|
||||
const regressionPath = join(targetDir, `${targetMilestoneId}-REGRESSION.md`);
|
||||
writeFileSync(regressionPath, [
|
||||
atomicWriteSync(regressionPath, [
|
||||
`# Milestone Regression`,
|
||||
``,
|
||||
`**From:** ${currentMilestoneId}`,
|
||||
|
|
@ -361,7 +362,7 @@ export function ensureDeferMilestoneDir(
|
|||
``,
|
||||
].join("\n");
|
||||
|
||||
writeFileSync(
|
||||
atomicWriteSync(
|
||||
join(msDir, `${targetMilestone}-CONTEXT-DRAFT.md`),
|
||||
draftContent,
|
||||
"utf-8",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
// handleResetSlice: Reset a slice and all its tasks, re-rendering plan + roadmap.
|
||||
|
||||
import type { ExtensionCommandContext, ExtensionAPI } from "@gsd/pi-coding-agent";
|
||||
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from "node:fs";
|
||||
import { existsSync, readFileSync, unlinkSync, readdirSync } from "node:fs";
|
||||
import { join, basename } from "node:path";
|
||||
import { nativeRevertCommit, nativeRevertAbort } from "./native-git-bridge.js";
|
||||
import { atomicWriteSync } from "./atomic-write.js";
|
||||
import { parseUnitId } from "./unit-id.js";
|
||||
import { deriveState } from "./state.js";
|
||||
import { invalidateAllCaches } from "./cache.js";
|
||||
|
|
@ -393,7 +394,7 @@ export function uncheckTaskInPlan(basePath: string, mid: string, sid: string, ti
|
|||
const regex = new RegExp(`^(\\s*-\\s*)\\[x\\](\\s*\\**${tid}\\**[:\\s])`, "mi");
|
||||
if (regex.test(content)) {
|
||||
content = content.replace(regex, "$1[ ]$2");
|
||||
writeFileSync(planFile, content, "utf-8");
|
||||
atomicWriteSync(planFile, content);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue