Keep /gsd auto artifact writes scoped to the active milestone worktree (#590)

This commit is contained in:
Copilot 2026-03-16 06:22:59 -06:00 committed by GitHub
parent 2ae4633d05
commit 570f6195be
4 changed files with 65 additions and 4 deletions

View file

@ -14,6 +14,7 @@ import {
removeWorktree,
worktreePath,
} from "./worktree-manager.js";
import { detectWorktreeName } from "./worktree.js";
import {
MergeConflictError,
} from "./git-service.js";
@ -224,6 +225,27 @@ export function getAutoWorktreeOriginalBase(): string | null {
return originalBase;
}
export function getActiveAutoWorktreeContext(): {
originalBase: string;
worktreeName: string;
branch: string;
} | null {
if (!originalBase) return null;
const cwd = process.cwd();
const resolvedBase = existsSync(originalBase) ? realpathSync(originalBase) : originalBase;
const wtDir = join(resolvedBase, ".gsd", "worktrees");
if (!cwd.startsWith(wtDir)) return null;
const worktreeName = detectWorktreeName(cwd);
if (!worktreeName) return null;
const branch = nativeGetCurrentBranch(cwd);
if (!branch.startsWith("milestone/")) return null;
return {
originalBase,
worktreeName,
branch,
};
}
// ─── Merge Milestone -> Main ───────────────────────────────────────────────
/**

View file

@ -591,17 +591,17 @@ export async function startAuto(
ctx.ui.setFooter(hideFooter);
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
// Restore hook state from disk in case session was interrupted
restoreHookState(base);
restoreHookState(basePath);
// Rebuild disk state before resuming — user interaction during pause may have changed files
try { await rebuildState(base); } catch { /* non-fatal */ }
try { await rebuildState(basePath); } catch { /* non-fatal */ }
try {
const report = await runGSDDoctor(base, { fix: true });
const report = await runGSDDoctor(basePath, { fix: true });
if (report.fixesApplied.length > 0) {
ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
}
} catch { /* non-fatal */ }
// Self-heal: clear stale runtime records where artifacts already exist
await selfHealRuntimeRecords(base, ctx, completedKeySet);
await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
invalidateAllCaches();
await dispatchNextUnit(ctx, pi);
return;

View file

@ -28,6 +28,7 @@ import { createBashTool, createWriteTool, createReadTool, createEditTool, isTool
import { registerGSDCommand, loadToolApiKeys } from "./commands.js";
import { registerExitCommand } from "./exit-command.js";
import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
import { getActiveAutoWorktreeContext } from "./auto-worktree.js";
import { saveFile, formatContinue, loadFile, parseContinue, parseSummary, loadActiveOverrides, formatOverridesSection } from "./files.js";
import { loadPrompt } from "./prompt-loader.js";
import { deriveState } from "./state.js";
@ -302,6 +303,7 @@ export default function (pi: ExtensionAPI) {
let worktreeBlock = "";
const worktreeName = getActiveWorktreeName();
const worktreeMainCwd = getWorktreeOriginalCwd();
const autoWorktree = getActiveAutoWorktreeContext();
if (worktreeName && worktreeMainCwd) {
worktreeBlock = [
"",
@ -319,6 +321,23 @@ export default function (pi: ExtensionAPI) {
"All file operations, bash commands, and GSD state resolve against the worktree path above.",
"Use /worktree merge to merge changes back. Use /worktree return to switch back to the main tree.",
].join("\n");
} else if (autoWorktree) {
worktreeBlock = [
"",
"",
"[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
`IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
`The actual current working directory is: ${process.cwd()}`,
"",
"You are working inside a GSD auto-worktree.",
`- Milestone worktree: ${autoWorktree.worktreeName}`,
`- Worktree path (this is the real cwd): ${process.cwd()}`,
`- Main project: ${autoWorktree.originalBase}`,
`- Branch: ${autoWorktree.branch}`,
"",
"All file operations, bash commands, and GSD state resolve against the worktree path above.",
"Write every .gsd artifact in the worktree path above, never in the main project tree.",
].join("\n");
}
return {

View file

@ -17,6 +17,7 @@ import {
getAutoWorktreePath,
enterAutoWorktree,
getAutoWorktreeOriginalBase,
getActiveAutoWorktreeContext,
} from "../auto-worktree.ts";
import { createTestContext } from "./test-helpers.ts";
@ -76,6 +77,15 @@ async function main(): Promise<void> {
// ─── getAutoWorktreeOriginalBase ─────────────────────────────────
assertEq(getAutoWorktreeOriginalBase(), tempDir, "originalBase returns temp dir");
assertEq(
getActiveAutoWorktreeContext(),
{
originalBase: tempDir,
worktreeName: "M003",
branch: "milestone/M003",
},
"active auto-worktree context reflects the worktree cwd",
);
// ─── getAutoWorktreePath ─────────────────────────────────────────
assertEq(getAutoWorktreePath(tempDir, "M003"), wtPath, "getAutoWorktreePath returns correct path");
@ -88,6 +98,7 @@ async function main(): Promise<void> {
assertTrue(!existsSync(wtPath), "worktree directory removed after teardown");
assertTrue(!isInAutoWorktree(tempDir), "isInAutoWorktree returns false after teardown");
assertEq(getAutoWorktreeOriginalBase(), null, "originalBase is null after teardown");
assertEq(getActiveAutoWorktreeContext(), null, "active auto-worktree context clears after teardown");
// ─── Re-entry: create again, exit without teardown, re-enter ─────
console.log("\n=== re-entry ===");
@ -103,6 +114,15 @@ async function main(): Promise<void> {
assertEq(process.cwd(), entered, "re-entered worktree via enterAutoWorktree");
assertEq(getAutoWorktreeOriginalBase(), tempDir, "originalBase restored on re-entry");
assertTrue(isInAutoWorktree(tempDir), "isInAutoWorktree true after re-entry");
assertEq(
getActiveAutoWorktreeContext(),
{
originalBase: tempDir,
worktreeName: "M003",
branch: "milestone/M003",
},
"active auto-worktree context is restored on re-entry",
);
// Cleanup
teardownAutoWorktree(tempDir, "M003");