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: [],