fix(autopilot): export ~/.bun/bin onto PATH in cron wrapper (#2013)

The wrapper script that 'gbrain autopilot --install' writes to
~/.gbrain/autopilot-run.sh sources ~/.bashrc to inherit PATH for the
exec'd gbrain binary (which has a '#!/usr/bin/env bun' shebang). The
standard Debian/Ubuntu ~/.bashrc ships a non-interactive guard that
returns early when bash is launched non-interactively (cron, launchd,
systemd) — so PATH exports operators add to ~/.bashrc never reach the
wrapper subprocess.

The result: the wrapper dies silently with 'env: bun: No such file or
directory', leaves a stale lockfile, and every subsequent cron tick
hits the lockfile and bails. The nightly dream cycle hangs waiting on
a worker that never comes back, and the wrapper's own 10-min
stale-lock window is the only thing that can recover it.

This bites every operator whose bashrc is the standard distro default
(which is the default), and there is no warning at install time.

Fix: prepend ~/.bun/bin to PATH directly in the wrapper, so it is
self-contained regardless of which init file the OS loaded. Add a
regression test alongside the existing zshenv/zshrc source-order test
(v0.36.1.x #966) so this class of bug stays caught.
This commit is contained in:
klampatech
2026-07-22 18:49:05 -07:00
committed by GitHub
parent bb5a66942d
commit b928f40bcd
2 changed files with 35 additions and 0 deletions
+9
View File
@@ -1186,6 +1186,15 @@ function writeWrapperScript(repoPath: string): string {
# 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}'
`;
writeFileSync(wrapperPath, wrapper, { mode: 0o755 });
+26
View File
@@ -99,3 +99,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);
});
});