From 8e2827646a36866559a13b9ae49a7584513866b2 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 19 Mar 2026 18:57:59 -0400 Subject: [PATCH] fix: check project root .env when secrets gate runs in worktree (#1387) (#1470) In worktree isolation mode, the secrets gate checked for .env at the worktree path (process.cwd()), but the user's .env lives at the project root. Keys that existed in the project root's .env were reported as missing, causing repeated blocking key collection prompts. Fix: getManifestStatus() now accepts an optional projectRoot parameter. When provided (worktree mode), it checks both the worktree .env AND the project root .env. All callers in auto.ts and auto-start.ts updated to pass s.originalBasePath. Fixes #1387 --- src/resources/extensions/gsd/auto-loop.ts | 3 ++- src/resources/extensions/gsd/auto-start.ts | 2 +- src/resources/extensions/gsd/files.ts | 11 ++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/gsd/auto-loop.ts b/src/resources/extensions/gsd/auto-loop.ts index 933f1a5b8..3b333ba95 100644 --- a/src/resources/extensions/gsd/auto-loop.ts +++ b/src/resources/extensions/gsd/auto-loop.ts @@ -353,6 +353,7 @@ export interface LoopDeps { getManifestStatus: ( basePath: string, mid: string | undefined, + projectRoot?: string, ) => Promise<{ pending: unknown[] } | null>; collectSecretsFromManifest: ( basePath: string, @@ -992,7 +993,7 @@ export async function autoLoop( // Secrets re-check gate try { - const manifestStatus = await deps.getManifestStatus(s.basePath, mid); + const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath); if (manifestStatus && manifestStatus.pending.length > 0) { const result = await deps.collectSecretsFromManifest( s.basePath, diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index b7515b137..852b3954b 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -484,7 +484,7 @@ export async function bootstrapAutoSession( // Secrets collection gate const mid = state.activeMilestone!.id; try { - const manifestStatus = await getManifestStatus(base, mid); + const manifestStatus = await getManifestStatus(base, mid, s.originalBasePath || base); if (manifestStatus && manifestStatus.pending.length > 0) { const result = await collectSecretsFromManifest(base, mid, ctx); if ( diff --git a/src/resources/extensions/gsd/files.ts b/src/resources/extensions/gsd/files.ts index 0c26ab100..568b19ed5 100644 --- a/src/resources/extensions/gsd/files.ts +++ b/src/resources/extensions/gsd/files.ts @@ -805,7 +805,7 @@ export async function inlinePriorMilestoneSummary(mid: string, base: string): Pr * file not on disk) - callers can distinguish "no manifest" from "empty manifest". */ export async function getManifestStatus( - base: string, milestoneId: string, + base: string, milestoneId: string, projectRoot?: string, ): Promise { const resolvedPath = resolveMilestoneFile(base, milestoneId, 'SECRETS'); if (!resolvedPath) return null; @@ -815,9 +815,18 @@ export async function getManifestStatus( const manifest = parseSecretsManifest(content); const keys = manifest.entries.map(e => e.key); + + // Check both the base path .env AND the project root .env (#1387). + // In worktree mode, base is the worktree path which may not have .env. + // The project root's .env is where the user actually defined their keys. const existingKeys = await checkExistingEnvKeys(keys, resolve(base, '.env')); const existingSet = new Set(existingKeys); + if (projectRoot && projectRoot !== base) { + const rootKeys = await checkExistingEnvKeys(keys, resolve(projectRoot, '.env')); + for (const k of rootKeys) existingSet.add(k); + } + const result: ManifestStatus = { pending: [], collected: [],