From 851b69a774f3e9909ce92035c23f04ce73bdeb69 Mon Sep 17 00:00:00 2001 From: Tony Galati Date: Sun, 26 Jul 2026 13:40:18 -0500 Subject: [PATCH] fix(cli): drop Bun SFE virtual argv[1] from detached re-invoke (#2248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `buildDetachedRunCmd` assumed a compiled binary has no entry-script argv[1] and sliced user args from argv[1] in binary mode. Bun single-file executables DO have an argv[1] — the virtual entry path (`/$bunfs/root/`, or `B:/~BUN/root/.exe` on Windows) — and report argv[0] as `bun`, not execPath. The virtual path therefore leaked in as the child's first token. Since cli.ts parses `process.argv.slice(2)` unconditionally, the detached child read it as the command and exited with `Unknown command: B:/~BUN/root/archon-windows-x64.exe`, creating no run and no worktree while the parent still reported `{ ok: true }`. User args start at argv[2] in both modes; only the command prefix differs. Verified against a real `bun build --compile` artifact: argv = ['bun', '/$bunfs/root/', ...userArgs] The existing binary-mode test modelled a compiled argv with no argv[1] at all, so it certified the broken behaviour. Its fixture is corrected to the real Bun SFE shape and a Windows-shaped regression test is added; both fail against the previous implementation. Reported on Windows x64 and independently reproduced on Ubuntu 24.04 x64, so this affects every compiled binary, not just Windows. Closes #2248 Co-Authored-By: Claude Opus 5 (1M context) --- packages/cli/src/commands/workflow.test.ts | 39 ++++++++++++++++++++-- packages/cli/src/commands/workflow.ts | 16 ++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/workflow.test.ts b/packages/cli/src/commands/workflow.test.ts index 11cf75a4a0..f03533d324 100644 --- a/packages/cli/src/commands/workflow.test.ts +++ b/packages/cli/src/commands/workflow.test.ts @@ -3222,11 +3222,18 @@ describe('buildDetachedRunCmd', () => { expect(cmd).toContain('--conversation-id'); }); - it('binary mode: uses [execPath] only (no duplicated entry arg), slices argv(1)', () => { + // A Bun single-file executable's argv is NOT [binary, ...userArgs]. Bun + // injects a virtual entry path at argv[1] and reports argv[0] as 'bun': + // ['bun', '/$bunfs/root/archon', 'workflow', 'run', ...] + // Verified against a real `bun build --compile` artifact. The previous + // fixture modelled a compiled argv with no argv[1] at all, which is why + // #2248 (detached child dies with `Unknown command: B:/~BUN/root/...`) + // shipped green. + it('binary mode: uses [execPath] only (no duplicated entry arg), drops the Bun SFE virtual argv[1]', () => { const cmd = buildDetachedRunCmd( true, '/usr/local/bin/archon', - ['/usr/local/bin/archon', 'workflow', 'run', 'assist', 'hello', '--detach', '--json'], + ['bun', '/$bunfs/root/archon', 'workflow', 'run', 'assist', 'hello', '--detach', '--json'], '/abs/cwd', ['--branch', 'assist-123'] ); @@ -3234,6 +3241,9 @@ describe('buildDetachedRunCmd', () => { expect(cmd[0]).toBe('/usr/local/bin/archon'); // The binary path must appear exactly once — never duplicated as argv[1]. expect(cmd.filter(arg => arg === '/usr/local/bin/archon')).toHaveLength(1); + // The virtual entry path must never reach the child: cli.ts parses + // process.argv.slice(2), so a leaked argv[1] becomes the child's command. + expect(cmd.some(arg => arg.includes('$bunfs'))).toBe(false); expect(cmd[1]).toBe('workflow'); expect(cmd).not.toContain('--detach'); expect(cmd).not.toContain('--json'); @@ -3241,6 +3251,31 @@ describe('buildDetachedRunCmd', () => { expect(cmd[cwdIdx + 1]).toBe('/abs/cwd'); expect(cmd.slice(cwdIdx + 2)).toEqual(['--branch', 'assist-123']); }); + + it('binary mode: drops the Windows Bun SFE virtual argv[1] (#2248 repro)', () => { + const cmd = buildDetachedRunCmd( + true, + 'C:\\Users\\dev\\archon.exe', + [ + 'bun', + 'B:/~BUN/root/archon-windows-x64.exe', + 'workflow', + 'run', + 'assist', + 'hello', + '--detach', + '--json', + ], + 'C:\\checkout', + ['--branch', 'assist-123'] + ); + + expect(cmd[0]).toBe('C:\\Users\\dev\\archon.exe'); + // The exact token that appeared as `Unknown command: ...` in the report. + expect(cmd).not.toContain('B:/~BUN/root/archon-windows-x64.exe'); + expect(cmd[1]).toBe('workflow'); + expect(cmd[2]).toBe('run'); + }); }); describe('workflowResumeCommand', () => { diff --git a/packages/cli/src/commands/workflow.ts b/packages/cli/src/commands/workflow.ts index 7107d93154..13064d41c8 100644 --- a/packages/cli/src/commands/workflow.ts +++ b/packages/cli/src/commands/workflow.ts @@ -240,12 +240,18 @@ export function buildDetachedRunCmd( cwd: string, extraArgs: string[] ): string[] { - // In a compiled binary, execPath IS the archon binary and there is no - // entry-script argv[1]; in dev, execPath is bun and argv[1] is the cli entry. + // Only the command prefix differs between modes: in a compiled binary + // execPath IS the archon binary and re-invoking it needs no entry script; in + // dev, execPath is bun and argv[1] is the cli entry that bun must be handed. const baseCmd = isBinary ? [execPath] : [execPath, argv[1]]; - const userArgs = (isBinary ? argv.slice(1) : argv.slice(2)).filter( - arg => arg !== '--detach' && arg !== '--json' - ); + // User args always start at argv[2] in BOTH modes. A Bun single-file + // executable does have an argv[1] — the virtual entry path + // (`/$bunfs/root/`, `B:/~BUN/root/.exe` on Windows) — so slicing + // from 1 in binary mode leaked that path in as the child's first token and + // the child died with `Unknown command: B:/~BUN/root/archon-...exe` (#2248). + // cli.ts's own parser reads `process.argv.slice(2)` unconditionally, which is + // the contract this must match. + const userArgs = argv.slice(2).filter(arg => arg !== '--detach' && arg !== '--json'); // --cwd is appended last (parseArgs last-wins) so the child resolves the same // absolute working dir regardless of any relative --cwd the caller passed. return [...baseCmd, ...userArgs, '--cwd', cwd, ...extraArgs];