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
This commit is contained in:
Tom Boucher 2026-03-19 18:57:59 -04:00 committed by GitHub
parent bc9cfb1992
commit 8e2827646a
3 changed files with 13 additions and 3 deletions

View file

@ -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,

View file

@ -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 (

View file

@ -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<ManifestStatus | null> {
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: [],