Two bugs fixed:
1. completed-units.json was wiped to [] on milestone transition, losing all
tracking data. Now archived to completed-units-{MID}.json before reset.
2. metrics.json was never synced between worktree and project root. Added to
syncStateToProjectRoot, syncWorktreeStateBack, and syncGsdStateToWorktree
file lists.
Fixes #2313
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
81de9f60c5
commit
5d0c6311f1
4 changed files with 132 additions and 2 deletions
|
|
@ -93,6 +93,11 @@ export function syncStateToProjectRoot(
|
|||
{ force: true },
|
||||
);
|
||||
|
||||
// 3. metrics.json — session cost/token tracking (#2313).
|
||||
// Without this, metrics accumulated in the worktree are invisible from the
|
||||
// project root and never appear in the dashboard or skill-health reports.
|
||||
safeCopy(join(wtGsd, "metrics.json"), join(prGsd, "metrics.json"), { force: true });
|
||||
|
||||
// 4. Runtime records — unit dispatch state used by selfHealRuntimeRecords().
|
||||
// Without this, a crash during a unit leaves the runtime record only in the
|
||||
// worktree. If the next session resolves basePath before worktree re-entry,
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ export function syncGsdStateToWorktree(
|
|||
"OVERRIDES.md",
|
||||
"QUEUE.md",
|
||||
"completed-units.json",
|
||||
"metrics.json",
|
||||
];
|
||||
for (const f of rootFiles) {
|
||||
const src = join(mainGsd, f);
|
||||
|
|
@ -325,8 +326,9 @@ export function syncWorktreeStateBack(
|
|||
// ── 1. Sync root-level .gsd/ files back ──────────────────────────────
|
||||
// The worktree is authoritative — complete-milestone updates REQUIREMENTS,
|
||||
// PROJECT, etc. These must overwrite main's copies so they survive teardown.
|
||||
// Also includes QUEUE.md and completed-units.json which are written during
|
||||
// milestone closeout and lost on teardown without explicit sync (#1787).
|
||||
// Also includes QUEUE.md, completed-units.json, and metrics.json which are
|
||||
// written during milestone closeout and lost on teardown without explicit sync
|
||||
// (#1787, #2313).
|
||||
const rootFiles = [
|
||||
"DECISIONS.md",
|
||||
"REQUIREMENTS.md",
|
||||
|
|
@ -335,6 +337,7 @@ export function syncWorktreeStateBack(
|
|||
"OVERRIDES.md",
|
||||
"QUEUE.md",
|
||||
"completed-units.json",
|
||||
"metrics.json",
|
||||
];
|
||||
for (const f of rootFiles) {
|
||||
const src = join(wtGsd, f);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { gsdRoot } from "../paths.js";
|
|||
import { atomicWriteSync } from "../atomic-write.js";
|
||||
import { PROJECT_FILES } from "../detection.js";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, cpSync } from "node:fs";
|
||||
|
||||
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -263,9 +264,17 @@ export async function runPreDispatch(
|
|||
// Reset completed-units tracking for the new milestone — stale entries
|
||||
// from the previous milestone cause the dispatch loop to skip units
|
||||
// that haven't actually been completed in the new milestone's context.
|
||||
// Archive the old completed-units.json instead of wiping it (#2313).
|
||||
s.completedUnits = [];
|
||||
try {
|
||||
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
||||
if (existsSync(completedKeysPath) && s.currentMilestoneId) {
|
||||
const archivePath = join(
|
||||
gsdRoot(s.basePath),
|
||||
`completed-units-${s.currentMilestoneId}.json`,
|
||||
);
|
||||
cpSync(completedKeysPath, archivePath);
|
||||
}
|
||||
atomicWriteSync(completedKeysPath, JSON.stringify([], null, 2));
|
||||
} catch { /* non-fatal */ }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* completed-units-metrics-sync.test.ts — Regression tests for #2313.
|
||||
*
|
||||
* 1. completed-units.json should be archived (not wiped) on milestone transition
|
||||
* 2. metrics.json should be in the worktree → project root sync file list
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, cpSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
// ─── Bug 1: completed-units.json should be archived, not wiped ─────────────
|
||||
|
||||
const phasesSrcPath = join(import.meta.dirname, "..", "auto", "phases.ts");
|
||||
const phasesSrc = readFileSync(phasesSrcPath, "utf-8");
|
||||
|
||||
test("#2313: completed-units.json should not be blindly wiped to [] on milestone transition", () => {
|
||||
// The milestone transition block should NOT write an empty array to completed-units.json
|
||||
// without first archiving the existing data. Look for the archive/rename pattern.
|
||||
const transitionIdx = phasesSrc.indexOf("Milestone transition");
|
||||
assert.ok(transitionIdx !== -1, "Milestone transition section exists");
|
||||
|
||||
// Find the completed-units handling block
|
||||
const completedUnitsIdx = phasesSrc.indexOf("completed-units", transitionIdx);
|
||||
assert.ok(completedUnitsIdx !== -1, "completed-units handling exists in transition");
|
||||
|
||||
// Get a window around the completed-units handling
|
||||
const windowStart = Math.max(0, completedUnitsIdx - 200);
|
||||
const windowEnd = Math.min(phasesSrc.length, completedUnitsIdx + 500);
|
||||
const window = phasesSrc.slice(windowStart, windowEnd);
|
||||
|
||||
// Should archive/rename the old file before resetting
|
||||
const hasArchive = window.includes("archive") ||
|
||||
window.includes("rename") ||
|
||||
window.includes("cpSync") ||
|
||||
window.includes("safeCopy") ||
|
||||
window.includes("completed-units-");
|
||||
|
||||
assert.ok(
|
||||
hasArchive,
|
||||
"completed-units.json should be archived before reset during milestone transition",
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Bug 2: metrics.json should be in the sync file lists ──────────────────
|
||||
|
||||
test("#2313: syncStateToProjectRoot should sync metrics.json", () => {
|
||||
const syncSrcPath = join(import.meta.dirname, "..", "auto-worktree-sync.ts");
|
||||
const syncSrc = readFileSync(syncSrcPath, "utf-8");
|
||||
|
||||
// syncStateToProjectRoot should copy metrics.json from worktree to project root
|
||||
assert.ok(
|
||||
syncSrc.includes("metrics.json"),
|
||||
"auto-worktree-sync.ts should reference metrics.json for sync",
|
||||
);
|
||||
});
|
||||
|
||||
test("#2313: syncWorktreeStateBack should include metrics.json in root files list", () => {
|
||||
const autoWorktreeSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
|
||||
const autoWorktreeSrc = readFileSync(autoWorktreeSrcPath, "utf-8");
|
||||
|
||||
// Find the rootFiles array in syncWorktreeStateBack
|
||||
const syncBackIdx = autoWorktreeSrc.indexOf("syncWorktreeStateBack");
|
||||
assert.ok(syncBackIdx !== -1, "syncWorktreeStateBack exists");
|
||||
|
||||
const rootFilesIdx = autoWorktreeSrc.indexOf("rootFiles", syncBackIdx);
|
||||
assert.ok(rootFilesIdx !== -1, "rootFiles list exists in syncWorktreeStateBack");
|
||||
|
||||
// Get the rootFiles array content
|
||||
const arrayStart = autoWorktreeSrc.indexOf("[", rootFilesIdx);
|
||||
const arrayEnd = autoWorktreeSrc.indexOf("]", arrayStart);
|
||||
const rootFilesBlock = autoWorktreeSrc.slice(arrayStart, arrayEnd);
|
||||
|
||||
assert.ok(
|
||||
rootFilesBlock.includes("metrics.json"),
|
||||
"metrics.json should be in syncWorktreeStateBack rootFiles list",
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Functional test: completed-units archive ────────────────────────────────
|
||||
|
||||
test("#2313: functional — completed-units archive creates milestone-specific file", () => {
|
||||
const tmpBase = mkdtempSync(join(tmpdir(), "gsd-completed-units-"));
|
||||
const gsdDir = join(tmpBase, ".gsd");
|
||||
mkdirSync(gsdDir, { recursive: true });
|
||||
|
||||
// Simulate existing completed-units.json with data
|
||||
const existing = [
|
||||
{ type: "task", id: "T01" },
|
||||
{ type: "slice", id: "S01" },
|
||||
];
|
||||
const completedKeysPath = join(gsdDir, "completed-units.json");
|
||||
writeFileSync(completedKeysPath, JSON.stringify(existing, null, 2));
|
||||
|
||||
// Simulate the archive behavior: copy to milestone-specific file
|
||||
const milestoneId = "M001";
|
||||
const archivePath = join(gsdDir, `completed-units-${milestoneId}.json`);
|
||||
cpSync(completedKeysPath, archivePath);
|
||||
|
||||
// Reset the main file
|
||||
writeFileSync(completedKeysPath, JSON.stringify([], null, 2));
|
||||
|
||||
// Verify archive exists with original data
|
||||
assert.ok(existsSync(archivePath), "archive file should exist");
|
||||
const archived = JSON.parse(readFileSync(archivePath, "utf-8"));
|
||||
assert.deepEqual(archived, existing, "archived data should match original");
|
||||
|
||||
// Verify main file is reset
|
||||
const current = JSON.parse(readFileSync(completedKeysPath, "utf-8"));
|
||||
assert.deepEqual(current, [], "current completed-units should be empty after transition");
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue