diff --git a/src/commands/autopilot.ts b/src/commands/autopilot.ts index 54a8af37d..323ff1115 100644 --- a/src/commands/autopilot.ts +++ b/src/commands/autopilot.ts @@ -1324,6 +1324,15 @@ export function writeWrapperScript(repoPath: string, intervalSeconds?: number): # OPENAI/ANTHROPIC keys exported in zshenv reach autopilot. [ -f ~/.zshenv ] && source ~/.zshenv 2>/dev/null source ~/.zshrc 2>/dev/null || source ~/.bashrc 2>/dev/null || true +# Belt-and-suspenders PATH fix. ~/.bashrc ships with a non-interactive guard +# (\`case $- in *i*) ;; *) return;; esac\`) that exits early when launched from +# cron/systemd/launchd — so its PATH exports never reach this subprocess. +# Without bun on PATH, the exec'd gbrain (a \`#!/usr/bin/env bun\` script) fails +# silently with "env: bun: No such file or directory" and leaves a stale +# lockfile that blocks every subsequent tick. Prepending ~/.bun/bin here +# keeps the wrapper self-contained regardless of which init file the OS +# loaded. +export PATH="$HOME/.bun/bin:$PATH" exec '${safeGbrainPath}' autopilot --repo '${safeRepoPath}'${intervalSeconds !== undefined ? ` --interval '${intervalSeconds}'` : ''} `; writeFileSync(wrapperPath, wrapper, { mode: 0o755 }); diff --git a/test/autopilot-install.test.ts b/test/autopilot-install.test.ts index 4ed286b73..30e314004 100644 --- a/test/autopilot-install.test.ts +++ b/test/autopilot-install.test.ts @@ -134,3 +134,29 @@ describe('autopilot wrapper script — env source order (v0.36.1.x #966)', () => expect(src).toMatch(/source\s+~\/\.zshrc/); }); }); + +// v0.42.x: the wrapper must export PATH with ~/.bun/bin before exec'ing +// gbrain. The exec'd gbrain has a `#!/usr/bin/env bun` shebang, and the +// standard Debian ~/.bashrc ships a non-interactive guard +// (`case $- in *i*) ;; *) return;; esac`) that exits early when cron/launchd/ +// systemd invokes bash non-interactively — so the PATH exports that +// operators put in ~/.bashrc never reach this subprocess. Without the +// explicit export the wrapper silently dies with `env: bun: No such file +// or directory`, leaves a stale lockfile, and blocks every subsequent tick +// for the 10-min stale-lock window. Regression: see Hermes `cron doctor` +// reports — this caused a 1-week nightly-cycle outage on at least one +// operator machine before being diagnosed. +describe('autopilot wrapper script — bun PATH export (v0.42.x regression)', () => { + test('wrapper exports ~/.bun/bin onto PATH before the exec', async () => { + const { readFileSync } = await import('fs'); + const src = readFileSync('src/commands/autopilot.ts', 'utf8'); + // The export line must appear inside the writeWrapperScript heredoc. + expect(src).toMatch(/export\s+PATH="\$HOME\/\.bun\/bin:\$PATH"/); + // The export must precede the exec line, otherwise env never sees it. + const exportIdx = src.search(/export\s+PATH="\$HOME\/\.bun\/bin/); + const execIdx = src.search(/exec\s+'\${safeGbrainPath}'/); + expect(exportIdx).toBeGreaterThan(0); + expect(execIdx).toBeGreaterThan(0); + expect(exportIdx).toBeLessThan(execIdx); + }); +});