fix: use absolute paths for write-target variables in auto-mode prompts (#627)

In worktree contexts, the LLM received relative output paths like
`.gsd/milestones/M002/slices/S01/S01-RESEARCH.md` combined with a
working directory containing `.gsd/worktrees/M002`. The double .gsd
in the resulting path confused the LLM, which resolved the relative
path against the project root instead of the worktree — writing
artifacts to the wrong location and triggering loop detection.

All write-target path variables (outputPath, taskSummaryPath,
sliceSummaryPath, milestoneSummaryPath, replanPath, planPath,
uatResultPath, assessmentPath, secretsOutputPath) are now passed
as absolute paths via join(base, relPath), eliminating the need
for the LLM to do path arithmetic in confusing worktree layouts.
This commit is contained in:
Juan Francisco Lebrero 2026-03-16 11:18:53 -03:00 committed by GitHub
parent e0a309f5b5
commit 88bdf9bc8d

View file

@ -389,7 +389,7 @@ export async function buildResearchMilestonePrompt(mid: string, midTitle: string
milestoneId: mid, milestoneTitle: midTitle,
milestonePath: relMilestonePath(base, mid),
contextPath: contextRel,
outputPath: outputRelPath,
outputPath: join(base, outputRelPath),
inlinedContext,
...buildSkillDiscoveryVars(),
});
@ -432,14 +432,14 @@ export async function buildPlanMilestonePrompt(mid: string, midTitle: string, ba
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
const secretsOutputPath = relMilestoneFile(base, mid, "SECRETS");
const secretsOutputPath = join(base, relMilestoneFile(base, mid, "SECRETS"));
return loadPrompt("plan-milestone", {
workingDirectory: base,
milestoneId: mid, milestoneTitle: midTitle,
milestonePath: relMilestonePath(base, mid),
contextPath: contextRel,
researchPath: researchRel,
outputPath: outputRelPath,
outputPath: join(base, outputRelPath),
secretsOutputPath,
inlinedContext,
});
@ -484,7 +484,7 @@ export async function buildResearchSlicePrompt(
roadmapPath: roadmapRel,
contextPath: contextRel,
milestoneResearchPath: milestoneResearchRel,
outputPath: outputRelPath,
outputPath: join(base, outputRelPath),
inlinedContext,
dependencySummaries: depContent,
...buildSkillDiscoveryVars(),
@ -531,7 +531,7 @@ export async function buildPlanSlicePrompt(
slicePath: relSlicePath(base, mid, sid),
roadmapPath: roadmapRel,
researchPath: researchRel,
outputPath: outputRelPath,
outputPath: join(base, outputRelPath),
inlinedContext,
dependencySummaries: depContent,
});
@ -598,7 +598,7 @@ export async function buildExecuteTaskPrompt(
...(knowledgeInlineET ? [knowledgeInlineET] : []),
].join("\n\n---\n\n");
const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
const taskSummaryPath = join(base, `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`);
const activeOverrides = await loadActiveOverrides(base);
const overridesSection = formatOverridesSection(activeOverrides);
@ -607,7 +607,7 @@ export async function buildExecuteTaskPrompt(
overridesSection,
workingDirectory: base,
milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
planPath: relSliceFile(base, mid, sid, "PLAN"),
planPath: join(base, relSliceFile(base, mid, sid, "PLAN")),
slicePath: relSlicePath(base, mid, sid),
taskPlanPath: taskPlanRelPath,
taskPlanInline,
@ -665,14 +665,14 @@ export async function buildCompleteSlicePrompt(
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
const sliceRel = relSlicePath(base, mid, sid);
const sliceSummaryPath = `${sliceRel}/${sid}-SUMMARY.md`;
const sliceUatPath = `${sliceRel}/${sid}-UAT.md`;
const sliceSummaryPath = join(base, `${sliceRel}/${sid}-SUMMARY.md`);
const sliceUatPath = join(base, `${sliceRel}/${sid}-UAT.md`);
return loadPrompt("complete-slice", {
workingDirectory: base,
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
slicePath: sliceRel,
roadmapPath: roadmapRel,
roadmapPath: join(base, roadmapRel),
inlinedContext,
sliceSummaryPath,
sliceUatPath,
@ -723,7 +723,7 @@ export async function buildCompleteMilestonePrompt(
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
const milestoneSummaryPath = `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`;
const milestoneSummaryPath = join(base, `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`);
return loadPrompt("complete-milestone", {
workingDirectory: base,
@ -775,7 +775,7 @@ export async function buildReplanSlicePrompt(
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
const replanPath = `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`;
const replanPath = join(base, `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`);
// Build capture context for replan prompt (captures that triggered this replan)
let captureContext = "(none)";
@ -797,7 +797,7 @@ export async function buildReplanSlicePrompt(
sliceId: sid,
sliceTitle: sTitle,
slicePath: relSlicePath(base, mid, sid),
planPath: slicePlanRel,
planPath: join(base, slicePlanRel),
blockerTaskId,
inlinedContext,
replanPath,
@ -823,7 +823,7 @@ export async function buildRunUatPrompt(
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
const uatResultPath = relSliceFile(base, mid, sliceId, "UAT-RESULT");
const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
const uatType = extractUatType(uatContent) ?? "human-experience";
return loadPrompt("run-uat", {
@ -862,7 +862,7 @@ export async function buildReassessRoadmapPrompt(
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
const assessmentPath = relSliceFile(base, mid, completedSliceId, "ASSESSMENT");
const assessmentPath = join(base, relSliceFile(base, mid, completedSliceId, "ASSESSMENT"));
// Build deferred captures context for reassess prompt
let deferredCaptures = "(none)";