fix: write milestone reports to project root instead of worktree (#2778)

* fix: write milestone reports to project root instead of worktree

During worktree isolation, s.basePath points to the temporary worktree
directory. Reports written there are silently lost when the worktree is
cleaned up. Use s.originalBasePath (falling back to s.basePath when not
in a worktree) so reports persist in the actual project directory.

Closes #2751

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add regression test for milestone report path resolution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-26 20:04:40 -06:00 committed by GitHub
parent f96a26b3c9
commit ce23da6718
2 changed files with 67 additions and 5 deletions

View file

@ -36,6 +36,15 @@ import { writeUnitRuntimeRecord } from "../unit-runtime.js";
// ─── generateMilestoneReport ──────────────────────────────────────────────────
/**
* Resolve the base path for milestone reports.
* Prefers originalBasePath (project root) over basePath (which may be a worktree).
* Exported for testing as _resolveReportBasePath.
*/
export function _resolveReportBasePath(s: Pick<AutoSession, "originalBasePath" | "basePath">): string {
return s.originalBasePath || s.basePath;
}
/**
* Generate and write an HTML milestone report snapshot.
* Extracted from the milestone-transition block in autoLoop.
@ -50,13 +59,15 @@ async function generateMilestoneReport(
const { writeReportSnapshot } = await importExtensionModule<typeof import("../reports.js")>(import.meta.url, "../reports.js");
const { basename } = await import("node:path");
const snapData = await loadVisualizerData(s.basePath);
const reportBasePath = _resolveReportBasePath(s);
const snapData = await loadVisualizerData(reportBasePath);
const completedMs = snapData.milestones.find(
(m: { id: string }) => m.id === milestoneId,
);
const msTitle = completedMs?.title ?? milestoneId;
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
const projName = basename(s.basePath);
const projName = basename(reportBasePath);
const doneSlices = snapData.milestones.reduce(
(acc: number, m: { slices: { done: boolean }[] }) =>
acc + m.slices.filter((sl: { done: boolean }) => sl.done).length,
@ -67,10 +78,10 @@ async function generateMilestoneReport(
0,
);
const outPath = writeReportSnapshot({
basePath: s.basePath,
basePath: reportBasePath,
html: generateHtmlReport(snapData, {
projectName: projName,
projectPath: s.basePath,
projectPath: reportBasePath,
gsdVersion,
milestoneId,
indexRelPath: "index.html",
@ -79,7 +90,7 @@ async function generateMilestoneReport(
milestoneTitle: msTitle,
kind: "milestone",
projectName: projName,
projectPath: s.basePath,
projectPath: reportBasePath,
gsdVersion,
totalCost: snapData.totals?.cost ?? 0,
totalTokens: snapData.totals?.tokens.total ?? 0,

View file

@ -0,0 +1,51 @@
/**
* milestone-report-path.test.ts Regression test for milestone report path resolution.
*
* When running in a worktree, milestone reports must be written to the
* original project root (originalBasePath), not the worktree path (basePath).
*
* Covers: _resolveReportBasePath from auto/phases.ts
*/
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { _resolveReportBasePath } from "../auto/phases.ts";
describe("_resolveReportBasePath", () => {
test("uses originalBasePath when set (worktree scenario)", () => {
const session = {
originalBasePath: "/projects/my-app",
basePath: "/projects/my-app/.claude/worktrees/agent-abc123",
};
assert.equal(_resolveReportBasePath(session), "/projects/my-app");
});
test("falls back to basePath when originalBasePath is empty", () => {
const session = {
originalBasePath: "",
basePath: "/projects/my-app",
};
assert.equal(_resolveReportBasePath(session), "/projects/my-app");
});
test("falls back to basePath when originalBasePath is undefined", () => {
const session = {
originalBasePath: undefined as unknown as string,
basePath: "/projects/my-app",
};
assert.equal(_resolveReportBasePath(session), "/projects/my-app");
});
test("uses originalBasePath even when basePath differs", () => {
const session = {
originalBasePath: "/home/user/repo",
basePath: "/tmp/worktree-xyz",
};
assert.equal(_resolveReportBasePath(session), "/home/user/repo");
});
});