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];