@a5c-ai/agent-launch-mux 5.0.1-staging.7495ef6c9fa0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/launch.js ADDED
@@ -0,0 +1,1640 @@
1
+ /**
2
+ * `amux launch` command implementation.
3
+ *
4
+ * Resolves a launch plan for a given harness+provider combination,
5
+ * optionally starts the transport-mux runtime, then exec-forks the harness with
6
+ * stdin/stdout passthrough and proper signal forwarding.
7
+ */
8
+ import { resolveProvider, resolveWorkspaceDefaultCwd, WorkspaceService, } from '@a5c-ai/agent-comm-mux';
9
+ import { translateForHarness } from '@a5c-ai/agent-mux-adapters';
10
+ import { getLaunchBehavior } from '@a5c-ai/agent-catalog';
11
+ import { getAutomationEnv, getBridgeCapabilities, getYoloLaunchArgs, } from '@a5c-ai/agent-catalog';
12
+ import { startTransportMuxRuntime } from '@a5c-ai/transport-mux';
13
+ import { flagStr, flagNum, flagBool, flagArr } from './cli-helpers.js';
14
+ import { ExitCode } from './cli-helpers.js';
15
+ import { printError, printJsonError } from './cli-helpers.js';
16
+ import { resolve as resolvePath } from 'node:path';
17
+ /** Launch-specific flag definitions (global flags like model/json/debug are excluded). */
18
+ export const LAUNCH_FLAGS = {
19
+ 'api-key': { type: 'string' },
20
+ 'profile': { type: 'string' },
21
+ 'api-base': { type: 'string' },
22
+ 'region': { type: 'string' },
23
+ 'project': { type: 'string' },
24
+ 'resource-group': { type: 'string' },
25
+ 'endpoint-name': { type: 'string' },
26
+ 'transport': { short: 't', type: 'string' },
27
+ 'auth-command': { type: 'string' },
28
+ 'with-proxy-if-needed': { type: 'boolean' },
29
+ 'with-proxy': { type: 'boolean' },
30
+ 'no-proxy': { type: 'boolean' },
31
+ 'proxy-port': { type: 'number' },
32
+ 'proxy-log-level': { type: 'string' },
33
+ 'resume': { short: 'r', type: 'string' },
34
+ 'session-id': { short: 's', type: 'string' },
35
+ 'prompt': { short: 'p', type: 'string' },
36
+ 'interactive': { short: 'i', type: 'boolean' },
37
+ 'max-turns': { type: 'number' },
38
+ 'max-budget-usd': { type: 'number' },
39
+ 'dry-run': { type: 'boolean' },
40
+ 'provider-arg': { type: 'string', repeatable: true },
41
+ 'observe': { type: 'boolean' },
42
+ 'workspace': { type: 'string' },
43
+ 'workspace-create': { type: 'boolean' },
44
+ 'workspace-mode': { type: 'string' },
45
+ 'workspace-repo': { type: 'string', repeatable: true },
46
+ 'workspace-name': { type: 'string' },
47
+ 'yolo': { type: 'boolean' },
48
+ 'bridge-interactive': { type: 'boolean' },
49
+ 'bridge-hooks': { type: 'boolean' },
50
+ };
51
+ // ---------------------------------------------------------------------------
52
+ // Plan resolution
53
+ // ---------------------------------------------------------------------------
54
+ const CLI_COMMAND_MAP = {
55
+ 'copilot': 'gh copilot',
56
+ 'cursor': 'cursor-agent',
57
+ };
58
+ function resolveCliCommand(harness) {
59
+ const cli = CLI_COMMAND_MAP[harness] ?? harness;
60
+ const parts = cli.split(/\s+/);
61
+ return { command: parts[0], prefixArgs: parts.slice(1) };
62
+ }
63
+ export function resolveLaunchPlan(input) {
64
+ const providerConfig = resolveProvider({
65
+ provider: input.provider,
66
+ model: input.model,
67
+ transport: input.transport,
68
+ apiKey: input.apiKey,
69
+ apiBase: input.apiBase,
70
+ region: input.region,
71
+ project: input.project,
72
+ resourceGroup: input.resourceGroup,
73
+ endpointName: input.endpointName,
74
+ authCommand: input.authCommand,
75
+ profile: input.profile,
76
+ });
77
+ // Merge extra provider args into params
78
+ if (input.providerArgs) {
79
+ Object.assign(providerConfig.params, input.providerArgs);
80
+ }
81
+ const translation = translateForHarness(input.harness, providerConfig, input.adapter);
82
+ let proxyNeeded = translation.proxyRequired;
83
+ let proxyReason;
84
+ if (!translation.proxyRequired) {
85
+ if (input.proxyMode === 'always') {
86
+ proxyNeeded = true;
87
+ proxyReason = 'Proxy forced via --with-proxy';
88
+ }
89
+ else {
90
+ proxyNeeded = false;
91
+ proxyReason = `${input.harness} supports ${providerConfig.provider} natively`;
92
+ }
93
+ }
94
+ else {
95
+ if (input.proxyMode === 'never') {
96
+ throw new Error(`${input.harness} does not support ${providerConfig.provider} natively. ` +
97
+ `Use --with-proxy-if-needed to auto-launch the proxy.`);
98
+ }
99
+ proxyReason =
100
+ `${input.harness} does not support ${providerConfig.provider} natively; ` +
101
+ `proxy bridges ${providerConfig.provider} → ${translation.proxyExposedTransport}`;
102
+ }
103
+ const proxy = proxyNeeded
104
+ ? {
105
+ targetProvider: providerConfig.provider,
106
+ targetModel: providerConfig.model,
107
+ exposedTransport: translation.proxyExposedTransport ?? 'openai-chat',
108
+ port: input.proxyPort ?? 0,
109
+ apiBase: providerConfig.params['apiBase'] ? String(providerConfig.params['apiBase']) : undefined,
110
+ apiKey: providerConfig.auth.apiKey,
111
+ project: providerConfig.params['project'] ? String(providerConfig.params['project']) : undefined,
112
+ location: providerConfig.params['region'] ? String(providerConfig.params['region']) : undefined,
113
+ useVertexAi: providerConfig.provider === 'vertex',
114
+ }
115
+ : undefined;
116
+ const resolved = resolveCliCommand(input.harness);
117
+ return {
118
+ harness: input.harness,
119
+ provider: providerConfig.provider,
120
+ transport: providerConfig.transport,
121
+ model: providerConfig.model,
122
+ proxyNeeded,
123
+ proxyReason,
124
+ proxy,
125
+ command: resolved.command,
126
+ args: [...resolved.prefixArgs, ...translation.args],
127
+ env: { ...translation.env },
128
+ };
129
+ }
130
+ function insertCodexOptionArgs(plan, optionArgs) {
131
+ if (plan.args[0] === 'exec') {
132
+ plan.args.splice(1, 0, ...optionArgs);
133
+ return;
134
+ }
135
+ plan.args.push(...optionArgs);
136
+ }
137
+ function appendHarnessSessionArgs(plan, session) {
138
+ const interactive = session.interactive !== false;
139
+ const lb = getLaunchBehavior(plan.harness);
140
+ if (lb) {
141
+ // Resume handling
142
+ if (session.resumeId) {
143
+ if (lb.resumeDelivery === 'subcommand' && lb.resumeSubcommand) {
144
+ plan.args.unshift(lb.resumeSubcommand, session.resumeId);
145
+ }
146
+ else if (lb.resumeDelivery === 'flag' && lb.resumeFlag) {
147
+ plan.args.push(lb.resumeFlag, session.resumeId);
148
+ }
149
+ }
150
+ // Session ID
151
+ if (session.sessionId && lb.sessionIdFlag) {
152
+ plan.args.push(lb.sessionIdFlag, session.sessionId);
153
+ }
154
+ // Prompt delivery (non-interactive only for cli-flag/exec-subcommand)
155
+ if (session.prompt && !session.resumeId) {
156
+ if (lb.promptDelivery === 'cli-flag' && lb.promptFlag && !interactive) {
157
+ plan.args.push(lb.promptFlag, session.prompt, ...(lb.promptExtraFlags ?? []));
158
+ }
159
+ else if (lb.promptDelivery === 'exec-subcommand' && lb.execSubcommand && !interactive) {
160
+ plan.args.unshift(lb.execSubcommand, session.prompt);
161
+ }
162
+ // stdin delivery is handled after spawn via stdin.write()
163
+ }
164
+ // Max turns
165
+ if (session.maxTurns && lb.maxTurnsFlag) {
166
+ plan.args.push(lb.maxTurnsFlag, String(session.maxTurns));
167
+ }
168
+ }
169
+ else {
170
+ // Fallback for unknown harnesses: prompt via stdin (handled after spawn)
171
+ if (session.resumeId)
172
+ plan.args.push('--resume', session.resumeId);
173
+ if (session.sessionId)
174
+ plan.args.push('--session-id', session.sessionId);
175
+ if (session.maxTurns)
176
+ plan.args.push('--max-turns', String(session.maxTurns));
177
+ }
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // Windows spawn resolution — find the actual binary so we don't need shell:true
181
+ // which mangles arguments containing special characters.
182
+ // ---------------------------------------------------------------------------
183
+ function escapeCmdArg(arg) {
184
+ if (!/[\s&|<>^()%!"',;]/.test(arg))
185
+ return arg;
186
+ // Escape internal double quotes and wrap in double quotes for cmd.exe
187
+ return '"' + arg.replace(/"/g, '""') + '"';
188
+ }
189
+ async function resolveSpawnCommand(command, args) {
190
+ if (process.platform !== 'win32') {
191
+ // On macOS/Linux, resolve wrapper scripts to their underlying binary.
192
+ // node-pty's posix_spawnp can fail on some script wrappers.
193
+ try {
194
+ const { execSync } = await import('node:child_process');
195
+ const whichOutput = execSync(`which ${command}`, { encoding: 'utf8', timeout: 5000 }).trim();
196
+ if (whichOutput) {
197
+ const { readFileSync, statSync } = await import('node:fs');
198
+ const stat = statSync(whichOutput);
199
+ console.error(`[amux launch] which ${command} → ${whichOutput} (${stat.size} bytes, mode ${stat.mode.toString(8)})`);
200
+ const content = readFileSync(whichOutput, 'utf8').slice(0, 500);
201
+ console.error(`[amux launch] script content (first 200): ${content.slice(0, 200).replace(/\n/g, '\\n')}`);
202
+ // Bash wrapper scripts (e.g. CI-generated shims): extract the node script path
203
+ const execNodeMatch = content.match(/exec\s+node\s+"([^"]+)"/);
204
+ if (execNodeMatch?.[1]) {
205
+ console.error(`[amux launch] resolved wrapper → node ${execNodeMatch[1]}`);
206
+ return { command: process.execPath, args: [execNodeMatch[1], ...args], shell: false };
207
+ }
208
+ // Also handle: node "path" or exec "path/node" "script"
209
+ const nodeScriptMatch = content.match(/(?:exec\s+)?(?:"\$[^"]*node[^"]*"|node)\s+"([^"]+\.js)"/);
210
+ if (nodeScriptMatch?.[1]) {
211
+ console.error(`[amux launch] resolved wrapper → node ${nodeScriptMatch[1]}`);
212
+ return { command: process.execPath, args: [nodeScriptMatch[1], ...args], shell: false };
213
+ }
214
+ return { command: whichOutput, args, shell: false };
215
+ }
216
+ }
217
+ catch { /* which failed, use original */ }
218
+ return { command, args, shell: false };
219
+ }
220
+ const { execSync } = await import('node:child_process');
221
+ const { existsSync } = await import('node:fs');
222
+ try {
223
+ const whereOutput = execSync(`where ${command}`, { encoding: 'utf8', timeout: 5000 });
224
+ const allPaths = whereOutput.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
225
+ // Prefer .exe > .cmd > .ps1 > extensionless (npm bash shim won't work with shell:false)
226
+ const resolved = allPaths.find(p => /\.exe$/i.test(p))
227
+ ?? allPaths.find(p => /\.(cmd|bat)$/i.test(p))
228
+ ?? allPaths.find(p => /\.(ps1|js)$/i.test(p))
229
+ ?? allPaths[0];
230
+ console.error(`[amux launch] where ${command} → ${allPaths.join(', ')} (selected: ${resolved})`);
231
+ if (resolved) {
232
+ if (/\.js$/i.test(resolved)) {
233
+ return { command: process.execPath, args: [resolved, ...args], shell: false };
234
+ }
235
+ if (/\.(cmd|bat)$/i.test(resolved)) {
236
+ // Parse the .cmd shim to find the actual binary (.js or .exe) and
237
+ // spawn directly to avoid cmd.exe/powershell argument mangling.
238
+ const { readFileSync } = await import('node:fs');
239
+ const pathMod = await import('node:path');
240
+ try {
241
+ const cmdContent = readFileSync(resolved, 'utf8');
242
+ console.error(`[amux launch] .cmd content (first 300): ${cmdContent.slice(0, 300).replace(/\n/g, '\\n')}`);
243
+ // Look for .js entry point (node/npm packages)
244
+ const cmdDir = pathMod.dirname(resolved);
245
+ const jsMatch = cmdContent.match(/"([^"]+\.js)"/);
246
+ if (jsMatch?.[1]) {
247
+ // npm .cmd shims use %dp0% for the .cmd file's directory
248
+ const jsRaw = jsMatch[1].replace(/%~?dp0%/gi, cmdDir + pathMod.sep);
249
+ const jsPath = pathMod.resolve(cmdDir, jsRaw);
250
+ if (existsSync(jsPath)) {
251
+ console.error(`[amux launch] resolved .cmd → .js: ${jsPath}`);
252
+ return { command: process.execPath, args: [jsPath, ...args], shell: false };
253
+ }
254
+ }
255
+ // Look for .exe reference (Bun-compiled packages like Claude Code)
256
+ // npm .cmd shims use %dp0% or %~dp0 for the directory
257
+ const exeMatch = cmdContent.match(/"%(?:~?dp0)%\\([^"]+\.exe)"/);
258
+ if (exeMatch?.[1]) {
259
+ const exePath = pathMod.resolve(cmdDir, exeMatch[1]);
260
+ if (existsSync(exePath)) {
261
+ const { statSync } = await import('node:fs');
262
+ const exeSize = statSync(exePath).size;
263
+ if (exeSize > 10240) {
264
+ console.error(`[amux launch] resolved .cmd → .exe: ${exePath} (${exeSize} bytes)`);
265
+ return { command: exePath, args, shell: false };
266
+ }
267
+ else {
268
+ console.error(`[amux launch] .exe shim too small (${exeSize} bytes), using .cmd fallback`);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ catch { /* couldn't parse .cmd */ }
274
+ // Fallback: invoke .cmd via cmd.exe /c with shell:false to avoid
275
+ // Node.js shell:true double-quoting (Node wraps in outer quotes for
276
+ // cmd /s /c "...", which breaks inner escaped quotes in args).
277
+ const quoteIfNeeded = (s) => s.includes(' ') || /[&|<>^()%!"',;]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
278
+ console.error(`[amux launch] .cmd fallback raw args (${args.length}): ${args.map((a, i) => `[${i}]=${a.length > 50 ? a.slice(0, 50) + '...' : a}`).join(' ')}`);
279
+ const cmdLine = `${quoteIfNeeded(resolved)} ${args.map(quoteIfNeeded).join(' ')}`;
280
+ console.error(`[amux launch] .cmd fallback cmdLine (first 500): ${cmdLine.slice(0, 500)}`);
281
+ return { command: process.env['ComSpec'] ?? 'cmd.exe', args: ['/c', cmdLine], shell: false };
282
+ }
283
+ if (/\.exe$/i.test(resolved)) {
284
+ return { command: resolved, args, shell: false };
285
+ }
286
+ // Resolved path has no recognized extension — try it directly without shell
287
+ return { command: resolved, args, shell: false };
288
+ }
289
+ }
290
+ catch (err) {
291
+ console.error(`[amux launch] where ${command} failed: ${err instanceof Error ? err.message : err}`);
292
+ }
293
+ return { command, args: args.map(escapeCmdArg), shell: true };
294
+ }
295
+ // ---------------------------------------------------------------------------
296
+ // Provider auth validation helper
297
+ // ---------------------------------------------------------------------------
298
+ async function prepareHarnessAutomationState(harness, cwd, env) {
299
+ if (!isAutomationPreseedEnabled(env))
300
+ return;
301
+ if (harness === 'claude')
302
+ await prepareClaudeAutomationState(cwd, env);
303
+ if (harness === 'codex')
304
+ await prepareCodexAutomationState(cwd);
305
+ const automationEnv = getAutomationEnv(harness);
306
+ for (const [key, value] of Object.entries(automationEnv)) {
307
+ env[key] = value;
308
+ }
309
+ }
310
+ function isAutomationPreseedEnabled(env) {
311
+ return env['AMUX_PRESEED_HARNESS_ONBOARDING'] === '1' || env['CI'] === 'true' || env['GITHUB_ACTIONS'] === 'true' || process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true';
312
+ }
313
+ function automationHome() {
314
+ return process.env['HOME'] || process.env['USERPROFILE'];
315
+ }
316
+ async function readJsonObject(filePath) {
317
+ try {
318
+ const fs = await import('node:fs/promises');
319
+ const value = JSON.parse(await fs.readFile(filePath, 'utf8'));
320
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
321
+ }
322
+ catch {
323
+ return {};
324
+ }
325
+ }
326
+ async function writeJsonObject(filePath, value) {
327
+ const { dirname } = await import('node:path');
328
+ const fs = await import('node:fs/promises');
329
+ await fs.mkdir(dirname(filePath), { recursive: true });
330
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
331
+ }
332
+ function recordObject(value) {
333
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
334
+ }
335
+ function numberAtLeast(value, minimum) {
336
+ const numeric = Number(value);
337
+ return Number.isFinite(numeric) ? Math.max(numeric, minimum) : minimum;
338
+ }
339
+ function approveClaudeCustomApiKey(config, env) {
340
+ const apiKey = env['ANTHROPIC_API_KEY'] || process.env['ANTHROPIC_API_KEY'];
341
+ if (!apiKey)
342
+ return;
343
+ const fingerprint = apiKey.slice(-20);
344
+ const responses = recordObject(config['customApiKeyResponses']);
345
+ const approved = Array.isArray(responses['approved']) ? responses['approved'].filter((value) => typeof value === 'string') : [];
346
+ const rejected = Array.isArray(responses['rejected']) ? responses['rejected'].filter((value) => typeof value === 'string' && value !== fingerprint) : [];
347
+ if (!approved.includes(fingerprint))
348
+ approved.push(fingerprint);
349
+ config['customApiKeyResponses'] = { ...responses, approved, rejected };
350
+ }
351
+ const AUTOMATION_CLAUDE_ONBOARDING_VERSION = '999.999.999';
352
+ async function prepareClaudeAutomationState(cwd, env) {
353
+ const home = automationHome();
354
+ if (!home)
355
+ return;
356
+ const { join, resolve } = await import('node:path');
357
+ const settingsPath = join(home, '.claude', 'settings.json');
358
+ const settings = await readJsonObject(settingsPath);
359
+ await writeJsonObject(settingsPath, {
360
+ ...settings,
361
+ theme: typeof settings['theme'] === 'string' ? settings['theme'] : 'dark',
362
+ skipDangerousModePermissionPrompt: true,
363
+ permissions: {
364
+ allow: [
365
+ 'Bash(*)', 'Read(*)', 'Write(*)', 'Edit(*)', 'Glob(*)', 'Grep(*)',
366
+ 'WebFetch(*)', 'WebSearch(*)', 'Agent(*)', 'Skill(*)',
367
+ 'TodoRead', 'TodoWrite',
368
+ ],
369
+ deny: [],
370
+ ...recordObject(settings['permissions']),
371
+ },
372
+ });
373
+ const configPath = join(home, '.claude.json');
374
+ const config = await readJsonObject(configPath);
375
+ approveClaudeCustomApiKey(config, env);
376
+ const projects = recordObject(config['projects']);
377
+ const projectPath = resolve(cwd).replace(/\\/g, '/');
378
+ const project = recordObject(projects[projectPath]);
379
+ projects[projectPath] = {
380
+ allowedTools: [],
381
+ mcpContextUris: [],
382
+ mcpServers: {},
383
+ enabledMcpjsonServers: [],
384
+ disabledMcpjsonServers: [],
385
+ hasClaudeMdExternalIncludesApproved: false,
386
+ hasClaudeMdExternalIncludesWarningShown: false,
387
+ ...project,
388
+ projectOnboardingSeenCount: numberAtLeast(project['projectOnboardingSeenCount'], 1),
389
+ hasTrustDialogAccepted: true,
390
+ hasCompletedProjectOnboarding: true,
391
+ lastVersionBase: AUTOMATION_CLAUDE_ONBOARDING_VERSION,
392
+ };
393
+ await writeJsonObject(configPath, {
394
+ ...config,
395
+ numStartups: numberAtLeast(config['numStartups'], 1),
396
+ hasCompletedOnboarding: true,
397
+ lastOnboardingVersion: AUTOMATION_CLAUDE_ONBOARDING_VERSION,
398
+ lastReleaseNotesSeen: AUTOMATION_CLAUDE_ONBOARDING_VERSION,
399
+ hasIdeOnboardingBeenShown: { vscode: true, ...recordObject(config['hasIdeOnboardingBeenShown']) },
400
+ officialMarketplaceAutoInstallAttempted: true,
401
+ officialMarketplaceAutoInstalled: true,
402
+ projects,
403
+ });
404
+ }
405
+ function extractPromptArtifactPaths(prompt, cwd) {
406
+ if (!prompt)
407
+ return [];
408
+ const matches = prompt.matchAll(/(?:^|[\s`"'])((?:\.\/)?\.a5c-live-test\/[^\s`"')]+)/g);
409
+ const paths = new Set();
410
+ for (const match of matches) {
411
+ const cleaned = match[1]?.replace(/[.,;:!?]+$/, '');
412
+ if (!cleaned)
413
+ continue;
414
+ paths.add(resolvePath(cwd, cleaned.replace(/^\.\//, '')));
415
+ }
416
+ return [...paths];
417
+ }
418
+ function promptRequiresBabysitterCompletion(prompt) {
419
+ return typeof prompt === 'string' && /(babysitter|\.a5c\/processes)/i.test(prompt);
420
+ }
421
+ function promptInvokesBabysitterSlashCommand(prompt) {
422
+ return typeof prompt === 'string' && /(?:^|\s)[/$]babysitter:[\w-]+/.test(prompt);
423
+ }
424
+ function buildBabysitterSkillFollowupPrompt(prompt) {
425
+ const originalRequest = (prompt ?? '').replace(/\s+/g, ' ').trim();
426
+ return [
427
+ 'Continue the Babysitter command now; do not answer in prose and do not call the Skill tool again.',
428
+ 'Use the Bash tool now with this exact command: babysitter instructions:babysit-skill --harness claude-code --no-interactive',
429
+ 'Then follow the returned CLI instructions for the original /babysitter request until completion proof is produced.',
430
+ originalRequest ? `Original /babysitter request: ${originalRequest}` : '',
431
+ ].filter(Boolean).join(' ');
432
+ }
433
+ function stripTerminalControl(input) {
434
+ return input.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '');
435
+ }
436
+ async function hasCompletedBabysitterRun(cwd) {
437
+ const fs = await import('node:fs/promises');
438
+ const { join } = await import('node:path');
439
+ const runsDir = join(cwd, '.a5c', 'runs');
440
+ let runIds;
441
+ try {
442
+ runIds = await fs.readdir(runsDir);
443
+ }
444
+ catch {
445
+ return false;
446
+ }
447
+ for (const runId of runIds.slice(-20).reverse()) {
448
+ try {
449
+ const runRaw = await fs.readFile(join(runsDir, runId, 'run.json'), 'utf8');
450
+ const runMeta = JSON.parse(runRaw);
451
+ const metadata = recordObject(runMeta['metadata']);
452
+ const proof = metadata['completionProof'] ?? runMeta['completionProof'];
453
+ const processId = runMeta['processId'] ?? metadata['processId'];
454
+ if (!proof || !processId || processId === 'bare-run')
455
+ continue;
456
+ const journalDir = join(runsDir, runId, 'journal');
457
+ const journalFiles = await fs.readdir(journalDir);
458
+ for (const journalFile of journalFiles) {
459
+ const journal = await fs.readFile(join(journalDir, journalFile), 'utf8');
460
+ if (journal.includes('RUN_COMPLETED'))
461
+ return true;
462
+ }
463
+ }
464
+ catch {
465
+ // ignore incomplete runs while the harness is still flushing state
466
+ }
467
+ }
468
+ return false;
469
+ }
470
+ function startPromptArtifactCompletionMonitor(input) {
471
+ const expectedPaths = extractPromptArtifactPaths(input.prompt, input.cwd);
472
+ if (expectedPaths.length === 0)
473
+ return undefined;
474
+ const requireBabysitterCompletion = promptRequiresBabysitterCompletion(input.prompt);
475
+ const lastSizes = new Map();
476
+ const startedAt = Date.now();
477
+ const MONITOR_TIMEOUT_MS = 600_000;
478
+ return setInterval(() => {
479
+ void (async () => {
480
+ if (Date.now() - startedAt > MONITOR_TIMEOUT_MS) {
481
+ console.error(`[amux launch] artifact monitor timed out after ${MONITOR_TIMEOUT_MS / 1000}s — forcing completion`);
482
+ input.onComplete();
483
+ return;
484
+ }
485
+ const fs = await import('node:fs/promises');
486
+ for (const expectedPath of expectedPaths) {
487
+ try {
488
+ const stat = await fs.stat(expectedPath);
489
+ if (!stat.isFile() || stat.size <= 0)
490
+ continue;
491
+ if (lastSizes.get(expectedPath) === stat.size) {
492
+ if (!requireBabysitterCompletion || await hasCompletedBabysitterRun(input.cwd)) {
493
+ input.onComplete();
494
+ return;
495
+ }
496
+ }
497
+ lastSizes.set(expectedPath, stat.size);
498
+ }
499
+ catch {
500
+ // expected artifact not written yet
501
+ }
502
+ }
503
+ })();
504
+ }, 1000);
505
+ }
506
+ async function prepareCodexAutomationState(cwd) {
507
+ const home = automationHome();
508
+ if (!home)
509
+ return;
510
+ const { join, resolve, dirname } = await import('node:path');
511
+ const fs = await import('node:fs/promises');
512
+ const configPath = join(home, '.codex', 'config.toml');
513
+ await fs.mkdir(dirname(configPath), { recursive: true });
514
+ let config = '';
515
+ try {
516
+ config = await fs.readFile(configPath, 'utf8');
517
+ }
518
+ catch {
519
+ config = '';
520
+ }
521
+ const projectPath = resolve(cwd);
522
+ const basicKey = JSON.stringify(projectPath);
523
+ const literalKey = `'${projectPath}'`;
524
+ if (config.includes(`[projects.${basicKey}]`) || config.includes(`[projects.${literalKey}]`))
525
+ return;
526
+ const prefix = config.trimEnd();
527
+ const addition = `[projects.${basicKey}]\ntrust_level = "trusted"\n`;
528
+ await fs.writeFile(configPath, `${prefix}${prefix ? '\n\n' : ''}${addition}`);
529
+ }
530
+ async function validateProviderAuth(plan) {
531
+ const { execSync } = await import('node:child_process');
532
+ try {
533
+ switch (plan.provider) {
534
+ case 'bedrock':
535
+ execSync('aws sts get-caller-identity', { stdio: 'ignore', timeout: 10_000 });
536
+ break;
537
+ case 'vertex':
538
+ execSync('gcloud auth application-default print-access-token', { stdio: 'ignore', timeout: 10_000 });
539
+ break;
540
+ }
541
+ }
542
+ catch {
543
+ const guidance = {
544
+ bedrock: 'AWS credentials not configured. Run: aws configure',
545
+ vertex: 'GCP credentials not configured. Run: gcloud auth application-default login',
546
+ };
547
+ return guidance[plan.provider] ?? null;
548
+ }
549
+ return null;
550
+ }
551
+ // ---------------------------------------------------------------------------
552
+ // Ollama lifecycle helper
553
+ // ---------------------------------------------------------------------------
554
+ async function ensureOllamaReady(model) {
555
+ const { execSync, spawnSync } = await import('node:child_process');
556
+ // Check if Ollama is running
557
+ try {
558
+ execSync('ollama list', { stdio: 'ignore', timeout: 5000 });
559
+ }
560
+ catch {
561
+ return { ok: false, message: 'Ollama is not running. Start it with: ollama serve' };
562
+ }
563
+ // Check if model is available
564
+ try {
565
+ const list = execSync('ollama list', { encoding: 'utf-8', timeout: 5000 });
566
+ const lines = list.split('\n').map(l => l.trim()).filter(Boolean);
567
+ const modelNames = lines.slice(1).map(l => l.split(/\s+/)[0]);
568
+ const modelBase = model.split(':')[0];
569
+ if (!modelNames.some(n => n.startsWith(modelBase))) {
570
+ console.error(`[amux launch] Model '${model}' not found locally. Pulling...`);
571
+ const pull = spawnSync('ollama', ['pull', model], { stdio: 'inherit', timeout: 600_000 });
572
+ if (pull.status !== 0) {
573
+ return { ok: false, message: `Failed to pull model '${model}'` };
574
+ }
575
+ }
576
+ }
577
+ catch (e) {
578
+ console.error(`[amux launch] Warning: could not verify model availability`);
579
+ }
580
+ return { ok: true };
581
+ }
582
+ // ---------------------------------------------------------------------------
583
+ // Main command handler
584
+ // ---------------------------------------------------------------------------
585
+ export async function launchCommand(client, args) {
586
+ const jsonMode = flagBool(args.flags, 'json') === true;
587
+ const harness = args.positionals[0];
588
+ const provider = args.positionals[1];
589
+ if (!harness) {
590
+ const msg = 'Usage: amux launch <harness> [provider] [flags...]\nRun "amux launch --help" for details.';
591
+ if (jsonMode)
592
+ printJsonError('VALIDATION_ERROR', msg);
593
+ else
594
+ printError(msg);
595
+ return ExitCode.USAGE_ERROR;
596
+ }
597
+ // Validate harness exists
598
+ const adapter = client.adapters.get(harness);
599
+ if (!adapter) {
600
+ const available = client.adapters.list().map((a) => a.agent).join(', ');
601
+ const msg = `Unknown harness '${harness}'. Available: ${available}`;
602
+ if (jsonMode)
603
+ printJsonError('AGENT_NOT_FOUND', msg);
604
+ else
605
+ printError(msg);
606
+ return ExitCode.USAGE_ERROR;
607
+ }
608
+ // Check harness is installed
609
+ if (adapter.detectInstallation) {
610
+ const installResult = await adapter.detectInstallation();
611
+ if (!installResult.installed) {
612
+ const installCmd = adapter.capabilities?.installMethods?.[0]?.command ?? `npm install -g ${harness}`;
613
+ const msg = `${harness} is not installed. Install with: ${installCmd}`;
614
+ if (jsonMode)
615
+ printJsonError('AGENT_NOT_FOUND', msg);
616
+ else
617
+ printError(msg);
618
+ return ExitCode.USAGE_ERROR;
619
+ }
620
+ }
621
+ if (flagStr(args.flags, 'resume') && adapter.capabilities && !adapter.capabilities.canResume) {
622
+ const msg = `${harness} does not support session resumption`;
623
+ if (jsonMode)
624
+ printJsonError('CAPABILITY_ERROR', msg);
625
+ else
626
+ printError(msg);
627
+ return ExitCode.USAGE_ERROR;
628
+ }
629
+ // Validate proxy flag mutual exclusion
630
+ const withProxy = flagBool(args.flags, 'with-proxy') === true;
631
+ const withProxyIfNeeded = flagBool(args.flags, 'with-proxy-if-needed') === true;
632
+ const noProxy = flagBool(args.flags, 'no-proxy') === true;
633
+ if ((withProxy || withProxyIfNeeded) && noProxy) {
634
+ const msg = 'Cannot use --with-proxy/--with-proxy-if-needed with --no-proxy';
635
+ if (jsonMode)
636
+ printJsonError('VALIDATION_ERROR', msg);
637
+ else
638
+ printError(msg);
639
+ return ExitCode.USAGE_ERROR;
640
+ }
641
+ const dryRun = flagBool(args.flags, 'dry-run') === true;
642
+ const proxyMode = noProxy ? 'never'
643
+ : withProxy ? 'always'
644
+ : withProxyIfNeeded ? 'if-needed'
645
+ : 'never';
646
+ const providerArgs = flagArr(args.flags, 'provider-arg') ?? [];
647
+ const extraParams = {};
648
+ for (const arg of providerArgs) {
649
+ const eqIdx = arg.indexOf('=');
650
+ if (eqIdx > 0) {
651
+ extraParams[arg.slice(0, eqIdx)] = arg.slice(eqIdx + 1);
652
+ }
653
+ }
654
+ let plan;
655
+ try {
656
+ plan = resolveLaunchPlan({
657
+ harness,
658
+ provider: provider,
659
+ model: flagStr(args.flags, 'model'),
660
+ transport: flagStr(args.flags, 'transport'),
661
+ apiKey: flagStr(args.flags, 'api-key'),
662
+ apiBase: flagStr(args.flags, 'api-base'),
663
+ region: flagStr(args.flags, 'region'),
664
+ project: flagStr(args.flags, 'project'),
665
+ resourceGroup: flagStr(args.flags, 'resource-group'),
666
+ endpointName: flagStr(args.flags, 'endpoint-name'),
667
+ authCommand: flagStr(args.flags, 'auth-command'),
668
+ profile: flagStr(args.flags, 'profile'),
669
+ proxyMode,
670
+ proxyPort: flagNum(args.flags, 'proxy-port'),
671
+ adapter: adapter,
672
+ providerArgs: extraParams,
673
+ });
674
+ }
675
+ catch (err) {
676
+ const msg = err instanceof Error ? err.message : String(err);
677
+ if (jsonMode)
678
+ printJsonError('VALIDATION_ERROR', msg);
679
+ else
680
+ printError(msg);
681
+ return ExitCode.USAGE_ERROR;
682
+ }
683
+ // Warn if auth appears missing for the resolved provider
684
+ const resolvedConfig = resolveProvider({
685
+ provider: provider,
686
+ model: flagStr(args.flags, 'model'),
687
+ apiKey: flagStr(args.flags, 'api-key'),
688
+ authCommand: flagStr(args.flags, 'auth-command'),
689
+ profile: flagStr(args.flags, 'profile'),
690
+ });
691
+ if (resolvedConfig.auth.type === 'api_key' && !resolvedConfig.auth.apiKey) {
692
+ const defaults = (await import('@a5c-ai/agent-comm-mux')).PROVIDER_DEFAULTS;
693
+ const provId = resolvedConfig.provider;
694
+ const envKey = defaults[provId]?.envKey;
695
+ if (envKey) {
696
+ console.error(`Warning: No API key found for ${provId}. Set ${envKey} or use --api-key.`);
697
+ }
698
+ }
699
+ // Provider-specific auth validation (Bedrock STS, Vertex ADC, etc.)
700
+ if (!dryRun) {
701
+ const authWarning = await validateProviderAuth(plan);
702
+ if (authWarning) {
703
+ console.error(`Warning: ${authWarning}`);
704
+ }
705
+ }
706
+ // Ollama lifecycle: verify server is running and model is available (pull if needed)
707
+ if (plan.provider === 'ollama' && plan.model && !dryRun) {
708
+ const ollamaCheck = await ensureOllamaReady(plan.model);
709
+ if (!ollamaCheck.ok) {
710
+ if (jsonMode)
711
+ printJsonError('SPAWN_ERROR', ollamaCheck.message);
712
+ else
713
+ printError(ollamaCheck.message);
714
+ return ExitCode.GENERAL_ERROR;
715
+ }
716
+ }
717
+ // Dry-run: print plan and exit without spawning anything
718
+ if (dryRun) {
719
+ const output = JSON.parse(JSON.stringify(plan));
720
+ for (const [k, v] of Object.entries(output.env)) {
721
+ if (k.toLowerCase().includes('key') || k.toLowerCase().includes('token')) {
722
+ output.env[k] = String(v).slice(0, 8) + '***';
723
+ }
724
+ }
725
+ console.log(JSON.stringify(output, null, 2));
726
+ return ExitCode.SUCCESS;
727
+ }
728
+ const workspaceService = new WorkspaceService();
729
+ let launchCwd = process.cwd();
730
+ const workspaceIdentifier = flagStr(args.flags, 'workspace');
731
+ const workspaceCreate = flagBool(args.flags, 'workspace-create') === true;
732
+ const workspaceRepos = flagArr(args.flags, 'workspace-repo');
733
+ const workspaceName = flagStr(args.flags, 'workspace-name') ?? `${harness}-workspace`;
734
+ if (workspaceCreate) {
735
+ const repos = workspaceRepos.length > 0 ? workspaceRepos : [process.cwd()];
736
+ const workspace = await workspaceService.createWorkspace({
737
+ name: workspaceName,
738
+ repos: repos.map((repo) => ({ path: repo })),
739
+ mode: flagStr(args.flags, 'workspace-mode') === 'symlink' ? 'symlink' : 'worktree',
740
+ });
741
+ launchCwd = resolveWorkspaceDefaultCwd(workspace);
742
+ }
743
+ else if (workspaceIdentifier) {
744
+ const workspace = await workspaceService.resolveWorkspace(workspaceIdentifier);
745
+ if (!workspace) {
746
+ const msg = `Unknown workspace '${workspaceIdentifier}'`;
747
+ if (jsonMode)
748
+ printJsonError('VALIDATION_ERROR', msg);
749
+ else
750
+ printError(msg);
751
+ return ExitCode.USAGE_ERROR;
752
+ }
753
+ launchCwd = resolveWorkspaceDefaultCwd(workspace);
754
+ }
755
+ // Resolve interactive mode (default: true)
756
+ const interactiveFlag = flagBool(args.flags, 'interactive');
757
+ const isInteractive = interactiveFlag !== false;
758
+ // Bridge flags: --bridge-interactive and --bridge-hooks
759
+ const bridgeInteractive = flagBool(args.flags, 'bridge-interactive') === true;
760
+ const bridgeHooks = flagBool(args.flags, 'bridge-hooks') === true;
761
+ if (bridgeInteractive && isInteractive) {
762
+ const msg = '--bridge-interactive requires --no-interactive';
763
+ if (jsonMode)
764
+ printJsonError('VALIDATION_ERROR', msg);
765
+ else
766
+ printError(msg);
767
+ return ExitCode.USAGE_ERROR;
768
+ }
769
+ if (bridgeHooks && isInteractive) {
770
+ const msg = '--bridge-hooks requires --no-interactive';
771
+ if (jsonMode)
772
+ printJsonError('VALIDATION_ERROR', msg);
773
+ else
774
+ printError(msg);
775
+ return ExitCode.USAGE_ERROR;
776
+ }
777
+ if (bridgeInteractive) {
778
+ const caps = getBridgeCapabilities(plan.harness);
779
+ if (!caps?.interactiveBridge) {
780
+ const msg = `${plan.harness} does not support interactive bridging`;
781
+ if (jsonMode)
782
+ printJsonError('CAPABILITY_ERROR', msg);
783
+ else
784
+ printError(msg);
785
+ return ExitCode.USAGE_ERROR;
786
+ }
787
+ }
788
+ // Append session/prompt args
789
+ const prompt = flagStr(args.flags, 'prompt');
790
+ appendHarnessSessionArgs(plan, {
791
+ resumeId: flagStr(args.flags, 'resume'),
792
+ sessionId: flagStr(args.flags, 'session-id'),
793
+ prompt,
794
+ maxTurns: flagNum(args.flags, 'max-turns'),
795
+ interactive: isInteractive || bridgeInteractive,
796
+ bridgeInteractive,
797
+ });
798
+ // Add --model for harnesses that accept it as a CLI arg
799
+ const modelFlag = flagStr(args.flags, 'model');
800
+ const codexModelFlag = modelFlag ?? (plan.harness === 'codex' ? plan.model : undefined);
801
+ if (codexModelFlag && plan.harness === 'codex') {
802
+ insertCodexOptionArgs(plan, ['-m', codexModelFlag]);
803
+ }
804
+ else if (modelFlag && ['pi', 'gemini', 'opencode'].includes(plan.harness)) {
805
+ plan.args.push('--model', modelFlag);
806
+ }
807
+ // --yolo: add harness-specific auto-approve flags resolved through
808
+ // agent-catalog → atlas graph (LaunchConfig records with commArgs)
809
+ if (flagBool(args.flags, 'yolo')) {
810
+ const yoloArgs = getYoloLaunchArgs(plan.harness);
811
+ if (yoloArgs.length > 0) {
812
+ plan.args.push(...yoloArgs);
813
+ }
814
+ }
815
+ // Passthrough args after --
816
+ const dashDashIdx = process.argv.indexOf('--');
817
+ if (dashDashIdx >= 0) {
818
+ plan.args.push(...process.argv.slice(dashDashIdx + 1));
819
+ }
820
+ // Also check parsed positionals for -- separator (handles spawn() without shell)
821
+ const argsDashIdx = args.positionals.indexOf('--');
822
+ if (argsDashIdx >= 0) {
823
+ plan.args.push(...args.positionals.slice(argsDashIdx + 1));
824
+ }
825
+ // Launch runtime if needed
826
+ let proxyRuntime;
827
+ if (plan.proxyNeeded && plan.proxy) {
828
+ try {
829
+ // When exposed transport differs from target (e.g., anthropic→foundry),
830
+ // the proxy needs a completion engine to translate request/response formats.
831
+ let completionEngine;
832
+ if ((plan.proxy.targetProvider === 'google' || plan.proxy.targetProvider === 'vertex')) {
833
+ // Resolve the API key: prefer the explicitly resolved key from the proxy
834
+ // plan, but fall back to reading GOOGLE_API_KEY / GEMINI_API_KEY from the
835
+ // process environment so that CI secrets flow through even when
836
+ // resolveProvider didn't capture them (e.g. the key was injected into the
837
+ // runner env after provider resolution).
838
+ const googleApiKey = plan.proxy.apiKey
839
+ || process.env['GOOGLE_API_KEY']
840
+ || process.env['GEMINI_API_KEY'];
841
+ if (googleApiKey) {
842
+ // Use Vertex AI mode when the provider is 'vertex' or when
843
+ // GOOGLE_GENAI_USE_VERTEXAI is set (indicates the key is for Vertex AI,
844
+ // not the Generative Language API).
845
+ const useVertexAi = plan.proxy.targetProvider === 'vertex'
846
+ || process.env['GOOGLE_GENAI_USE_VERTEXAI']?.toLowerCase() === 'true';
847
+ const { createGoogleCompletionEngine } = await import('./completion-engine.js');
848
+ const googleApiBase = useVertexAi ? undefined
849
+ : (plan.proxy.apiBase && plan.proxy.apiBase.includes('googleapis.com') ? plan.proxy.apiBase : undefined);
850
+ completionEngine = createGoogleCompletionEngine({
851
+ apiBase: googleApiBase,
852
+ apiKey: googleApiKey,
853
+ targetModel: plan.proxy.targetModel,
854
+ provider: plan.proxy.targetProvider,
855
+ project: plan.proxy.project,
856
+ location: plan.proxy.location,
857
+ useVertexAi,
858
+ });
859
+ }
860
+ }
861
+ else if (plan.proxy.targetProvider === 'anthropic' && plan.proxy.apiKey) {
862
+ const { createAnthropicCompletionEngine } = await import('./completion-engine.js');
863
+ completionEngine = createAnthropicCompletionEngine({
864
+ apiBase: plan.proxy.apiBase,
865
+ apiKey: plan.proxy.apiKey,
866
+ targetModel: plan.proxy.targetModel,
867
+ });
868
+ }
869
+ else if (plan.proxy.apiBase && plan.proxy.apiKey) {
870
+ const { createOpenAICompletionEngine } = await import('./completion-engine.js');
871
+ completionEngine = createOpenAICompletionEngine({
872
+ apiBase: plan.proxy.apiBase,
873
+ apiKey: plan.proxy.apiKey,
874
+ targetModel: plan.proxy.targetModel,
875
+ });
876
+ }
877
+ proxyRuntime = await startTransportMuxRuntime({
878
+ targetProvider: plan.proxy.targetProvider,
879
+ targetModel: `${plan.proxy.targetProvider}/${plan.proxy.targetModel}`,
880
+ exposedTransport: plan.proxy.exposedTransport,
881
+ port: plan.proxy.port,
882
+ apiBase: plan.proxy.apiBase,
883
+ completionEngine,
884
+ // Gemini CLI doesn't send auth headers/params — disable proxy auth for it
885
+ ...(plan.harness === 'gemini' ? { authToken: null } : {}),
886
+ });
887
+ proxyRuntime.applyHarnessEnv(plan.env);
888
+ if (plan.env['ANTHROPIC_API_KEY']) {
889
+ plan.env['ANTHROPIC_AUTH_TOKEN'] = '';
890
+ }
891
+ // Gemini CLI: set GOOGLE_API_KEY to proxy token and GOOGLE_GEMINI_BASE_URL
892
+ // to the proxy URL so gemini-cli connects through the transport-mux.
893
+ // Note: GOOGLE_GEMINI_BASE_URL is the env var Gemini CLI reads for custom
894
+ // API endpoints (see https://geminicli.com/docs/reference/configuration/).
895
+ // The previously-used GOOGLE_AI_STUDIO_API_ENDPOINT was never recognised.
896
+ if (plan.harness === 'gemini') {
897
+ plan.env['GOOGLE_API_KEY'] = proxyRuntime.authToken ?? 'proxy-token';
898
+ plan.env['GEMINI_API_KEY'] = proxyRuntime.authToken ?? 'proxy-token';
899
+ const proxyOrigin = new URL(proxyRuntime.url).origin;
900
+ plan.env['GOOGLE_GEMINI_BASE_URL'] = proxyOrigin;
901
+ plan.env['GEMINI_CLI_TRUST_WORKSPACE'] = '1';
902
+ plan.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'false';
903
+ plan.env['GOOGLE_CLOUD_PROJECT'] = '';
904
+ plan.env['GOOGLE_CLOUD_LOCATION'] = '';
905
+ console.error(`[amux launch] Gemini proxy: GOOGLE_API_KEY=proxy-token, endpoint=${proxyOrigin}`);
906
+ }
907
+ // Generic OpenAI-compatible harnesses: set OPENAI_API_KEY + OPENAI_BASE_URL
908
+ // to route through the proxy for harnesses that use the openai-chat/responses transport.
909
+ if (['codex', 'cursor', 'hermes', 'omp', 'openclaw', 'opencode'].includes(plan.harness)) {
910
+ plan.env['OPENAI_API_KEY'] = proxyRuntime.authToken ?? 'proxy-token';
911
+ plan.env['OPENAI_BASE_URL'] = `${proxyRuntime.url}/v1`;
912
+ plan.env['OPENAI_API_BASE'] = `${proxyRuntime.url}/v1`;
913
+ if (plan.harness === 'codex') {
914
+ insertCodexOptionArgs(plan, [
915
+ '-c', 'model_provider="amux-proxy"',
916
+ '-c', 'model_providers.amux-proxy.name="amux-proxy"',
917
+ '-c', `model_providers.amux-proxy.base_url="${proxyRuntime.url}/v1"`,
918
+ '-c', 'model_providers.amux-proxy.env_key="OPENAI_API_KEY"',
919
+ '-c', 'model_providers.amux-proxy.wire_api="responses"',
920
+ ]);
921
+ }
922
+ console.error(`[amux launch] ${plan.harness} proxy: OPENAI_BASE_URL=${proxyRuntime.url}/v1`);
923
+ }
924
+ // Pi ignores OPENAI_BASE_URL — write a models.json config that registers
925
+ // a custom provider pointing to the local proxy.
926
+ if (plan.harness === 'pi') {
927
+ const { writeFileSync, mkdirSync } = await import('node:fs');
928
+ const { join } = await import('node:path');
929
+ const piConfigDir = process.env['PI_CODING_AGENT_DIR']
930
+ ?? join(process.env['HOME'] ?? process.env['USERPROFILE'] ?? '/tmp', '.pi', 'agent');
931
+ mkdirSync(piConfigDir, { recursive: true });
932
+ const modelsConfig = {
933
+ providers: {
934
+ 'amux-proxy': {
935
+ baseUrl: `${proxyRuntime.url}/v1`,
936
+ api: 'openai-completions',
937
+ apiKey: proxyRuntime.authToken ?? 'proxy-token',
938
+ models: [{
939
+ id: plan.model,
940
+ reasoning: false,
941
+ input: ['text'],
942
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
943
+ contextWindow: 128000,
944
+ maxTokens: 16384,
945
+ }],
946
+ },
947
+ },
948
+ };
949
+ const modelsPath = join(piConfigDir, 'models.json');
950
+ writeFileSync(modelsPath, JSON.stringify(modelsConfig, null, 2));
951
+ plan.args.push('--provider', 'amux-proxy');
952
+ console.error(`[amux launch] Pi proxy config written to ${modelsPath}, proxy at ${proxyRuntime.url}`);
953
+ }
954
+ }
955
+ catch (err) {
956
+ const msg = err instanceof Error ? err.message : String(err);
957
+ if (jsonMode)
958
+ printJsonError('SPAWN_ERROR', `Failed to launch transport runtime: ${msg}`);
959
+ else
960
+ printError(`Failed to launch transport runtime: ${msg}`);
961
+ return ExitCode.GENERAL_ERROR;
962
+ }
963
+ }
964
+ // Cursor: pre-create ~/.cursor/auth.json so cursor-agent skips browser OAuth.
965
+ // Runs outside the proxy block because cursor always needs auth, regardless
966
+ // of whether the proxy was started.
967
+ if (plan.harness === 'cursor') {
968
+ const token = plan.env['CURSOR_API_KEY'] || 'proxy-token';
969
+ plan.env['CURSOR_API_KEY'] = token;
970
+ const { writeFileSync: wf, mkdirSync: md } = await import('node:fs');
971
+ const { join: pj } = await import('node:path');
972
+ const cursorDir = pj(process.env['HOME'] ?? process.env['USERPROFILE'] ?? '/tmp', '.cursor');
973
+ md(cursorDir, { recursive: true });
974
+ const auth = JSON.stringify({ accessToken: token, refreshToken: token, userId: 'ci-proxy', email: 'ci@proxy.local' });
975
+ wf(pj(cursorDir, 'auth.json'), auth);
976
+ wf(pj(cursorDir, 'credentials.json'), auth);
977
+ console.error(`[amux launch] Cursor auth pre-seeded at ${cursorDir}/auth.json`);
978
+ }
979
+ // Bridge hooks: emulate lifecycle hooks when --bridge-hooks is set
980
+ let bridgeHookEmulator;
981
+ if (bridgeHooks) {
982
+ const { BridgeHookEmulator } = await import('./bridge-hooks.js');
983
+ bridgeHookEmulator = new BridgeHookEmulator({
984
+ harness: plan.harness,
985
+ cwd: launchCwd,
986
+ env: plan.env,
987
+ sessionId: flagStr(args.flags, 'session-id'),
988
+ runsDir: plan.env['BABYSITTER_RUNS_DIR'] || undefined,
989
+ verbose: flagBool(args.flags, 'debug') === true,
990
+ });
991
+ await bridgeHookEmulator.emulateSessionStart();
992
+ }
993
+ await prepareHarnessAutomationState(plan.harness, launchCwd, plan.env);
994
+ // Spawn harness
995
+ let child = null;
996
+ let ptyProcess = null;
997
+ let ptyTerminationExpected = false;
998
+ let stdinPromptOverride;
999
+ const ptyCleanup = [];
1000
+ const capturedOutputChunks = [];
1001
+ const completePtyPrompt = () => {
1002
+ if (!ptyProcess || ptyTerminationExpected)
1003
+ return;
1004
+ ptyTerminationExpected = true;
1005
+ try {
1006
+ ptyProcess.kill('SIGTERM');
1007
+ }
1008
+ catch { /* */ }
1009
+ setTimeout(() => {
1010
+ try {
1011
+ ptyProcess?.kill('SIGKILL');
1012
+ }
1013
+ catch { /* */ }
1014
+ }, 2000);
1015
+ };
1016
+ if (isInteractive) {
1017
+ // Interactive mode: full TTY passthrough. If a prompt is provided, it's
1018
+ // injected as initial stdin after the harness starts (like typing it in).
1019
+ // Skip node-pty on macOS ARM64 CI — posix_spawnp fails for all binaries
1020
+ // and the native addon writes errors directly to stderr bypassing JS try/catch.
1021
+ const skipPty = process.platform === 'darwin' && process.arch === 'arm64' && process.env['CI'] === 'true';
1022
+ try {
1023
+ if (skipPty)
1024
+ throw new Error('Skipping node-pty on macOS ARM64 CI');
1025
+ const nodePty = await import('node-pty');
1026
+ const resolved = await resolveSpawnCommand(plan.command, plan.args);
1027
+ ptyProcess = nodePty.spawn(resolved.command, resolved.args, {
1028
+ name: 'xterm-256color',
1029
+ cols: process.stdout.columns || 80,
1030
+ rows: process.stdout.rows || 24,
1031
+ cwd: launchCwd,
1032
+ env: { ...process.env, ...plan.env },
1033
+ });
1034
+ // Verify the PTY process actually started (posix_spawnp failures on macOS ARM64
1035
+ // may throw synchronously or result in an immediate exit with no pid)
1036
+ if (!ptyProcess || !ptyProcess.pid || ptyProcess.pid <= 0) {
1037
+ throw new Error('node-pty spawn returned invalid process — falling back to stdio');
1038
+ }
1039
+ // Give the process 100ms to fail — posix_spawnp errors on macOS ARM64 sometimes
1040
+ // manifest as immediate exit rather than a synchronous throw
1041
+ await new Promise((resolve, reject) => {
1042
+ const timer = setTimeout(resolve, 100);
1043
+ ptyProcess.onExit?.((e) => {
1044
+ if (e.exitCode !== 0) {
1045
+ clearTimeout(timer);
1046
+ reject(new Error(`PTY process exited immediately with code ${e.exitCode}`));
1047
+ }
1048
+ });
1049
+ });
1050
+ // End-of-turn detection: parse PTY output through adapter's event system
1051
+ let turnDetected = false;
1052
+ let lineBuf = '';
1053
+ let assembler = null;
1054
+ let adapter = null;
1055
+ try {
1056
+ const core = await import('@a5c-ai/agent-comm-mux');
1057
+ assembler = new core.StreamAssembler();
1058
+ // Resolve the adapter for this harness to use its parseEvent
1059
+ const adaptersModule = await import('@a5c-ai/agent-mux-adapters');
1060
+ const factory = adaptersModule.getAdapterFactory?.(plan.harness);
1061
+ adapter = factory ? factory() : null;
1062
+ }
1063
+ catch { /* core/adapters not available */ }
1064
+ // Pipe PTY to stdout + feed through event parser for turn detection
1065
+ let interactiveOutputBuf = '';
1066
+ let interactiveApiKeyHandled = false;
1067
+ let interactiveBypassHandled = false;
1068
+ let interactiveHooksTrustHandled = false;
1069
+ let babysitterSkillFollowupInjected = false;
1070
+ const maybeInjectBabysitterSkillFollowup = (output) => {
1071
+ if (babysitterSkillFollowupInjected || !promptInvokesBabysitterSlashCommand(prompt))
1072
+ return;
1073
+ if (!stripTerminalControl(output).includes('Skill(babysitter:babysit)'))
1074
+ return;
1075
+ babysitterSkillFollowupInjected = true;
1076
+ setTimeout(() => {
1077
+ if (!ptyTerminationExpected) {
1078
+ ptyProcess.write(buildBabysitterSkillFollowupPrompt(prompt));
1079
+ setTimeout(() => ptyProcess.write('\r'), 500);
1080
+ }
1081
+ }, 1000);
1082
+ };
1083
+ ptyProcess.onData((data) => {
1084
+ process.stdout.write(data);
1085
+ interactiveOutputBuf += data;
1086
+ capturedOutputChunks.push(data);
1087
+ // Auto-respond to Claude Code onboarding prompts
1088
+ const stripped = interactiveOutputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
1089
+ if (!interactiveApiKeyHandled && stripped.includes('usethisAPIkey')) {
1090
+ interactiveApiKeyHandled = true;
1091
+ setTimeout(() => ptyProcess.write('\x1b[A\r'), 200);
1092
+ }
1093
+ if (!interactiveBypassHandled && stripped.includes('BypassPermissionsmode')) {
1094
+ interactiveBypassHandled = true;
1095
+ setTimeout(() => ptyProcess.write('\x1b[B\r'), 200);
1096
+ }
1097
+ if (!interactiveHooksTrustHandled && (stripped.includes('Hooks need review') || stripped.includes('hooks need review') || stripped.includes('Hooks can run outside the sandbox'))) {
1098
+ interactiveHooksTrustHandled = true;
1099
+ setTimeout(() => ptyProcess.write('2\r'), 300);
1100
+ }
1101
+ maybeInjectBabysitterSkillFollowup(interactiveOutputBuf);
1102
+ if (!assembler || !adapter || turnDetected)
1103
+ return;
1104
+ // Strip ANSI escapes, then feed lines to the event parser
1105
+ const clean = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
1106
+ lineBuf += clean;
1107
+ let idx;
1108
+ while ((idx = lineBuf.indexOf('\n')) !== -1) {
1109
+ const line = lineBuf.slice(0, idx).replace(/\r$/, '');
1110
+ lineBuf = lineBuf.slice(idx + 1);
1111
+ if (line.length === 0)
1112
+ continue;
1113
+ const assembled = assembler.feed(line);
1114
+ if (assembled === null)
1115
+ continue;
1116
+ try {
1117
+ const ctx = { runId: 'launch', agent: plan.harness, sessionId: undefined, turnIndex: 0, debug: false, outputFormat: 'text', source: 'stdout', assembler, eventCount: 0, lastEventType: null, adapterState: {} };
1118
+ const result = adapter.parseEvent(assembled, ctx);
1119
+ if (result === null)
1120
+ continue;
1121
+ const events = Array.isArray(result) ? result : [result];
1122
+ for (const ev of events) {
1123
+ // Detect turn completion events
1124
+ if (ev.type === 'message_stop' || ev.type === 'turn_end' || ev.type === 'session_end') {
1125
+ turnDetected = true;
1126
+ // Give the harness a moment to flush output, then end the PTY.
1127
+ setTimeout(completePtyPrompt, 1000);
1128
+ return;
1129
+ }
1130
+ }
1131
+ }
1132
+ catch { /* parse error — ignore */ }
1133
+ }
1134
+ });
1135
+ if (process.stdin.isTTY) {
1136
+ process.stdin.setRawMode(true);
1137
+ }
1138
+ process.stdin.resume();
1139
+ process.stdin.on('data', (data) => ptyProcess.write(data.toString()));
1140
+ // Handle terminal resize
1141
+ process.stdout.on('resize', () => {
1142
+ ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
1143
+ });
1144
+ if (prompt && plan.args.some(a => a === prompt)) {
1145
+ let artifactMonitor;
1146
+ artifactMonitor = startPromptArtifactCompletionMonitor({
1147
+ prompt,
1148
+ cwd: launchCwd,
1149
+ onComplete: () => {
1150
+ if (artifactMonitor)
1151
+ clearInterval(artifactMonitor);
1152
+ completePtyPrompt();
1153
+ },
1154
+ });
1155
+ ptyCleanup.push(() => { if (artifactMonitor)
1156
+ clearInterval(artifactMonitor); });
1157
+ }
1158
+ // Inject prompt after observed onboarding prompts are dismissed.
1159
+ if (prompt && !plan.args.some(a => a === prompt)) {
1160
+ const startedAt = Date.now();
1161
+ let promptInjected = false;
1162
+ let artifactMonitor;
1163
+ const injectPrompt = () => {
1164
+ if (promptInjected)
1165
+ return;
1166
+ promptInjected = true;
1167
+ ptyProcess.write(prompt);
1168
+ setTimeout(() => ptyProcess.write('\r'), 500);
1169
+ artifactMonitor = startPromptArtifactCompletionMonitor({
1170
+ prompt,
1171
+ cwd: launchCwd,
1172
+ onComplete: () => {
1173
+ if (artifactMonitor)
1174
+ clearInterval(artifactMonitor);
1175
+ completePtyPrompt();
1176
+ },
1177
+ });
1178
+ ptyCleanup.push(() => { if (artifactMonitor)
1179
+ clearInterval(artifactMonitor); });
1180
+ };
1181
+ const checkAndInject = () => {
1182
+ if (promptInjected)
1183
+ return;
1184
+ if (interactiveOutputBuf.length === 0) {
1185
+ if (Date.now() - startedAt >= 1000)
1186
+ injectPrompt();
1187
+ else
1188
+ setTimeout(checkAndInject, 100);
1189
+ return;
1190
+ }
1191
+ const s = interactiveOutputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
1192
+ if (interactiveApiKeyHandled || interactiveBypassHandled) {
1193
+ setTimeout(injectPrompt, 2000);
1194
+ }
1195
+ else if (s.includes('APIkey') || s.includes('Bypass')) {
1196
+ setTimeout(checkAndInject, 500);
1197
+ }
1198
+ else {
1199
+ setTimeout(injectPrompt, 3000);
1200
+ }
1201
+ };
1202
+ checkAndInject();
1203
+ }
1204
+ // Create a fake ChildProcess-like for signal handling
1205
+ child = { pid: ptyProcess.pid, kill: (sig) => ptyProcess.kill(sig) };
1206
+ }
1207
+ catch (ptyError) {
1208
+ // node-pty not available or posix_spawnp failed (macOS ARM64), fall back to stdio
1209
+ const ptyMsg = ptyError instanceof Error ? ptyError.message : String(ptyError);
1210
+ console.error(`[amux launch] PTY spawn failed (${process.platform}/${process.arch}): ${ptyMsg} — falling back to stdio`);
1211
+ ptyProcess = null;
1212
+ const { spawn } = await import('node:child_process');
1213
+ child = spawn(plan.command, plan.args, {
1214
+ stdio: prompt ? ['pipe', 'inherit', 'inherit'] : 'inherit',
1215
+ env: { ...process.env, ...plan.env },
1216
+ cwd: launchCwd,
1217
+ shell: process.platform === 'win32',
1218
+ });
1219
+ }
1220
+ }
1221
+ else if (bridgeInteractive) {
1222
+ // Bridge-interactive: spawn via PTY like interactive mode, but:
1223
+ // - No human stdin forwarding
1224
+ // - Parse PTY output via adapter for structured events
1225
+ // - Emit events as NDJSON to stdout
1226
+ // - Auto-kill on turn completion
1227
+ // - Buffer PTY output to avoid pipe deadlock (stdout is piped)
1228
+ // Pre-create full Claude Code automation state to bypass all onboarding prompts
1229
+ if (plan.harness === 'claude') {
1230
+ await prepareClaudeAutomationState(launchCwd, plan.env);
1231
+ }
1232
+ let nodePty;
1233
+ try {
1234
+ nodePty = await import('node-pty');
1235
+ }
1236
+ catch {
1237
+ const msg = '--bridge-interactive requires node-pty but it is not available. Install it with: npm install node-pty';
1238
+ if (jsonMode)
1239
+ printJsonError('SPAWN_ERROR', msg);
1240
+ else
1241
+ printError(msg);
1242
+ return ExitCode.GENERAL_ERROR;
1243
+ }
1244
+ const resolvedPty = await resolveSpawnCommand(plan.command, plan.args);
1245
+ // node-pty's posix_spawnp can fail for some binaries (Bun .exe, npm wrappers).
1246
+ // Fall back to spawning via shell which handles all executable types.
1247
+ const ptyCommand = process.platform === 'win32' ? resolvedPty.command : '/bin/sh';
1248
+ const ptyArgs = process.platform === 'win32' ? resolvedPty.args
1249
+ : ['-c', [resolvedPty.command, ...resolvedPty.args].map(a => a.includes(' ') || /[&|<>^()%!"';]/.test(a) ? `'${a.replace(/'/g, "'\\''")}'` : a).join(' ')];
1250
+ ptyProcess = nodePty.spawn(ptyCommand, ptyArgs, {
1251
+ name: 'xterm-256color',
1252
+ cols: 120,
1253
+ rows: 40,
1254
+ cwd: launchCwd,
1255
+ env: { ...process.env, ...plan.env },
1256
+ });
1257
+ // Set up adapter + assembler for parsing PTY output into structured events
1258
+ let assembler = null;
1259
+ let adapter = null;
1260
+ try {
1261
+ const core = await import('@a5c-ai/agent-comm-mux');
1262
+ assembler = new core.StreamAssembler();
1263
+ const adaptersModule = await import('@a5c-ai/agent-mux-adapters');
1264
+ const factory = adaptersModule.getAdapterFactory?.(plan.harness);
1265
+ adapter = factory ? factory() : null;
1266
+ }
1267
+ catch { /* core/adapters not available — raw output only */ }
1268
+ /** Emit a bridge event as NDJSON, deferred to avoid blocking PTY callback. */
1269
+ function emitBridgeEvent(event) {
1270
+ const line = JSON.stringify(event) + '\n';
1271
+ setImmediate(() => {
1272
+ try {
1273
+ process.stdout.write(line);
1274
+ }
1275
+ catch { /* stdout closed */ }
1276
+ });
1277
+ }
1278
+ let turnComplete = false;
1279
+ let lineBuf = '';
1280
+ let outputBuf = '';
1281
+ let eventCount = 0;
1282
+ let apiKeyPromptHandled = false;
1283
+ let bypassPromptHandled = false;
1284
+ let hooksTrustHandled = false;
1285
+ let babysitterSkillFollowupInjected = false;
1286
+ const maybeInjectBabysitterSkillFollowup = (output) => {
1287
+ if (babysitterSkillFollowupInjected || !promptInvokesBabysitterSlashCommand(prompt))
1288
+ return;
1289
+ if (!stripTerminalControl(output).includes('Skill(babysitter:babysit)'))
1290
+ return;
1291
+ babysitterSkillFollowupInjected = true;
1292
+ setTimeout(() => {
1293
+ if (!ptyTerminationExpected) {
1294
+ ptyProcess.write(buildBabysitterSkillFollowupPrompt(prompt));
1295
+ setTimeout(() => ptyProcess.write('\r'), 500);
1296
+ }
1297
+ }, 1000);
1298
+ };
1299
+ let idleTimer = null;
1300
+ const IDLE_TIMEOUT_MS = 30_000;
1301
+ const harnessesWithEndEvents = new Set(['claude', 'codex', 'gemini', 'opencode']);
1302
+ const useIdleTimeout = !harnessesWithEndEvents.has(plan.harness);
1303
+ const parseCtx = {
1304
+ runId: 'bridge',
1305
+ agent: plan.harness,
1306
+ sessionId: undefined,
1307
+ turnIndex: 0,
1308
+ debug: false,
1309
+ outputFormat: 'text',
1310
+ source: 'stdout',
1311
+ assembler: assembler,
1312
+ eventCount: 0,
1313
+ lastEventType: null,
1314
+ adapterState: {},
1315
+ };
1316
+ ptyProcess.onData((data) => {
1317
+ // Buffer all PTY output — never write synchronously to stdout (pipe deadlock)
1318
+ outputBuf += data;
1319
+ capturedOutputChunks.push(data);
1320
+ // Auto-respond to Claude Code interactive prompts that block automation.
1321
+ // ANSI cursor-move codes replace spaces, so stripped text is concatenated.
1322
+ const stripped = outputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
1323
+ if (!apiKeyPromptHandled && stripped.includes('usethisAPIkey')) {
1324
+ apiKeyPromptHandled = true;
1325
+ // Default is "No (recommended)". Send Up arrow + Enter to select "Yes".
1326
+ setTimeout(() => ptyProcess.write('\x1b[A\r'), 200);
1327
+ }
1328
+ if (!bypassPromptHandled && stripped.includes('BypassPermissionsmode')) {
1329
+ bypassPromptHandled = true;
1330
+ // Default is "No, exit". Send Down arrow + Enter to select "Yes, I accept".
1331
+ setTimeout(() => ptyProcess.write('\x1b[B\r'), 200);
1332
+ }
1333
+ // Codex hooks trust prompt: "Hooks can run outside the sandbox after you trust them"
1334
+ // or "hooks are new or changed. Hooks need review"
1335
+ if (!hooksTrustHandled && (stripped.includes('Hooks need review') || stripped.includes('hooks need review') || stripped.includes('Hooks can run outside the sandbox'))) {
1336
+ hooksTrustHandled = true;
1337
+ // Send "2" to select "Trust all" option, then Enter
1338
+ setTimeout(() => ptyProcess.write('2\r'), 300);
1339
+ }
1340
+ maybeInjectBabysitterSkillFollowup(outputBuf);
1341
+ if (!assembler || !adapter || turnComplete)
1342
+ return;
1343
+ // Strip ANSI escapes, then feed lines to the event parser
1344
+ const clean = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
1345
+ lineBuf += clean;
1346
+ let idx;
1347
+ while ((idx = lineBuf.indexOf('\n')) !== -1) {
1348
+ const line = lineBuf.slice(0, idx).replace(/\r$/, '');
1349
+ lineBuf = lineBuf.slice(idx + 1);
1350
+ if (line.length === 0)
1351
+ continue;
1352
+ const assembled = assembler.feed(line);
1353
+ if (assembled === null)
1354
+ continue;
1355
+ try {
1356
+ parseCtx.eventCount = eventCount;
1357
+ const result = adapter.parseEvent(assembled, parseCtx);
1358
+ if (result === null)
1359
+ continue;
1360
+ const events = Array.isArray(result) ? result : [result];
1361
+ for (const ev of events) {
1362
+ eventCount++;
1363
+ parseCtx.lastEventType = ev.type;
1364
+ // Emit as NDJSON bridge event
1365
+ emitBridgeEvent({
1366
+ type: ev.type,
1367
+ timestamp: new Date().toISOString(),
1368
+ data: ev,
1369
+ });
1370
+ // Detect turn completion events — schedule PTY termination
1371
+ if (ev.type === 'message_stop' || ev.type === 'turn_end' || ev.type === 'session_end') {
1372
+ turnComplete = true;
1373
+ if (idleTimer)
1374
+ clearTimeout(idleTimer);
1375
+ setTimeout(completePtyPrompt, 1000);
1376
+ return;
1377
+ }
1378
+ // Idle timeout termination for harnesses without structured end events.
1379
+ if (useIdleTimeout) {
1380
+ if (idleTimer)
1381
+ clearTimeout(idleTimer);
1382
+ idleTimer = setTimeout(() => {
1383
+ if (!turnComplete) {
1384
+ turnComplete = true;
1385
+ completePtyPrompt();
1386
+ }
1387
+ }, IDLE_TIMEOUT_MS);
1388
+ }
1389
+ }
1390
+ }
1391
+ catch { /* parse error — ignore */ }
1392
+ }
1393
+ });
1394
+ // Inject prompt after observed onboarding prompts are dismissed.
1395
+ // If the PTY stays silent, inject after a short startup grace period because
1396
+ // some harnesses wait for input without rendering an initial prompt.
1397
+ if (prompt) {
1398
+ const startedAt = Date.now();
1399
+ let promptInjected = false;
1400
+ let artifactMonitor;
1401
+ const injectPrompt = () => {
1402
+ if (promptInjected)
1403
+ return;
1404
+ promptInjected = true;
1405
+ ptyProcess.write(prompt);
1406
+ setTimeout(() => ptyProcess.write('\r'), 500);
1407
+ artifactMonitor = startPromptArtifactCompletionMonitor({
1408
+ prompt,
1409
+ cwd: launchCwd,
1410
+ onComplete: () => {
1411
+ if (artifactMonitor)
1412
+ clearInterval(artifactMonitor);
1413
+ completePtyPrompt();
1414
+ },
1415
+ });
1416
+ ptyCleanup.push(() => { if (artifactMonitor)
1417
+ clearInterval(artifactMonitor); });
1418
+ };
1419
+ const checkAndInject = () => {
1420
+ if (promptInjected)
1421
+ return;
1422
+ if (outputBuf.length === 0) {
1423
+ if (Date.now() - startedAt >= 1000)
1424
+ injectPrompt();
1425
+ else
1426
+ setTimeout(checkAndInject, 100);
1427
+ return;
1428
+ }
1429
+ const stripped = outputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
1430
+ if (apiKeyPromptHandled || bypassPromptHandled) {
1431
+ setTimeout(injectPrompt, 2000);
1432
+ }
1433
+ else if (stripped.includes('APIkey') || stripped.includes('Bypass')) {
1434
+ setTimeout(checkAndInject, 500);
1435
+ }
1436
+ else {
1437
+ setTimeout(injectPrompt, 3000);
1438
+ }
1439
+ };
1440
+ checkAndInject();
1441
+ }
1442
+ // Create a fake ChildProcess-like for signal handling
1443
+ child = { pid: ptyProcess.pid, kill: (sig) => ptyProcess.kill(sig) };
1444
+ // On PTY exit, flush remaining buffered text as a final output event
1445
+ const origOnExit = ptyProcess.onExit.bind(ptyProcess);
1446
+ const exitPromise = new Promise((resolve) => {
1447
+ origOnExit(({ exitCode: code }) => {
1448
+ // Flush any remaining output as a final bridge event
1449
+ if (outputBuf.length > 0) {
1450
+ emitBridgeEvent({
1451
+ type: 'output',
1452
+ timestamp: new Date().toISOString(),
1453
+ data: { text: outputBuf },
1454
+ });
1455
+ outputBuf = '';
1456
+ }
1457
+ resolve(code);
1458
+ });
1459
+ });
1460
+ // Store the exit promise so main exit handler can use it
1461
+ child.__bridgeExitPromise = exitPromise;
1462
+ }
1463
+ else {
1464
+ // Non-interactive: plain spawn. Each harness handles non-interactive mode
1465
+ // internally (claude -p, codex exec, gemini --prompt, pi -p).
1466
+ const { spawn } = await import('node:child_process');
1467
+ const resolvedSpawn = await resolveSpawnCommand(plan.command, plan.args);
1468
+ // No special Windows prompt override needed — Bun binaries are handled via
1469
+ // .cmd shim fallback in resolveSpawnCommand (shell:true + escapeCmdArg).
1470
+ console.error(`[amux launch] spawn: ${resolvedSpawn.command} shell=${resolvedSpawn.shell} args[0..2]=${resolvedSpawn.args.slice(0, 3).join(' ')} totalArgs=${resolvedSpawn.args.length}${stdinPromptOverride ? ' (prompt→stdin)' : ''}`);
1471
+ child = spawn(resolvedSpawn.command, resolvedSpawn.args, {
1472
+ stdio: ['pipe', 'pipe', 'pipe'],
1473
+ env: { ...process.env, ...plan.env },
1474
+ cwd: launchCwd,
1475
+ shell: resolvedSpawn.shell,
1476
+ });
1477
+ child.stderr?.on('data', (chunk) => {
1478
+ process.stderr.write(chunk);
1479
+ capturedOutputChunks.push(chunk.toString('utf8'));
1480
+ });
1481
+ // Pipe stdout through + idle-timeout kill for harnesses that don't exit
1482
+ // after completing a non-interactive task (e.g., Pi doesn't exit on its own).
1483
+ // Harnesses with proper exit behavior (claude -p, codex exec) don't need this.
1484
+ const _lb = getLaunchBehavior(plan.harness);
1485
+ const niUseIdleKill = _lb ? _lb.needsIdleKill : true;
1486
+ let niIdleTimer = null;
1487
+ let niHasOutput = false;
1488
+ const NI_IDLE_TIMEOUT_MS = 30_000;
1489
+ child.stdout?.on('data', (chunk) => {
1490
+ process.stdout.write(chunk);
1491
+ capturedOutputChunks.push(chunk.toString('utf8'));
1492
+ niHasOutput = true;
1493
+ if (niUseIdleKill) {
1494
+ if (niIdleTimer)
1495
+ clearTimeout(niIdleTimer);
1496
+ niIdleTimer = setTimeout(() => {
1497
+ if (niHasOutput) {
1498
+ try {
1499
+ child.kill('SIGTERM');
1500
+ }
1501
+ catch { /* */ }
1502
+ }
1503
+ }, NI_IDLE_TIMEOUT_MS);
1504
+ }
1505
+ });
1506
+ }
1507
+ if (flagBool(args.flags, 'observe')) {
1508
+ if (isInteractive) {
1509
+ console.error('[amux launch] Warning: --observe does not work with interactive PTY mode');
1510
+ }
1511
+ else {
1512
+ // Tee stdout to both console and a log file
1513
+ const logPath = `.amux-launch-${Date.now()}.log`;
1514
+ const logStream = (await import('node:fs')).createWriteStream(logPath);
1515
+ child.stdout?.on('data', (chunk) => {
1516
+ process.stdout.write(chunk);
1517
+ logStream.write(chunk);
1518
+ });
1519
+ child.stderr?.on('data', (chunk) => {
1520
+ process.stderr.write(chunk);
1521
+ logStream.write(chunk);
1522
+ });
1523
+ child.on('exit', () => logStream.end());
1524
+ console.error(`[amux launch] Observing output to ${logPath}`);
1525
+ }
1526
+ }
1527
+ const forwardSignal = (sig) => {
1528
+ if (process.platform === 'win32') {
1529
+ try {
1530
+ const { execSync } = require('node:child_process');
1531
+ execSync(`taskkill /PID ${child.pid} /T /F`, { stdio: 'ignore' });
1532
+ }
1533
+ catch { /* process may already be dead */ }
1534
+ }
1535
+ else if (ptyProcess) {
1536
+ // PTY child runs in its own session — kill the process group to avoid orphans
1537
+ try {
1538
+ process.kill(-ptyProcess.pid, sig);
1539
+ }
1540
+ catch { /* */ }
1541
+ try {
1542
+ ptyProcess.kill(sig);
1543
+ }
1544
+ catch { /* */ }
1545
+ }
1546
+ else {
1547
+ child.kill(sig);
1548
+ }
1549
+ };
1550
+ process.on('SIGINT', forwardSignal);
1551
+ process.on('SIGTERM', forwardSignal);
1552
+ // Ensure PTY cleanup on exit
1553
+ if (ptyProcess) {
1554
+ process.on('exit', () => { try {
1555
+ ptyProcess.kill('SIGKILL');
1556
+ }
1557
+ catch { /* */ } });
1558
+ }
1559
+ const launchBehavior = getLaunchBehavior(plan.harness);
1560
+ const effectivePrompt = stdinPromptOverride ?? prompt;
1561
+ const promptInArgs = effectivePrompt ? plan.args.some(a => a === effectivePrompt) : false;
1562
+ const needsStdinDelivery = stdinPromptOverride || !promptInArgs;
1563
+ const keepStdinOpen = launchBehavior?.stdinBehavior === 'keep-open';
1564
+ if (!isInteractive && effectivePrompt) {
1565
+ console.error(`[amux launch] stdin: promptInArgs=${promptInArgs} stdinOverride=${!!stdinPromptOverride} keepStdinOpen=${keepStdinOpen}`);
1566
+ }
1567
+ if (effectivePrompt && child.stdin && !ptyProcess && needsStdinDelivery) {
1568
+ child.stdin.write(prompt + '\n');
1569
+ if (!isInteractive && !keepStdinOpen) {
1570
+ child.stdin.end();
1571
+ }
1572
+ else if (!isInteractive && keepStdinOpen) {
1573
+ // Harnesses that need stdin open for tool-use loops; idle-kill handles termination
1574
+ }
1575
+ else {
1576
+ // Interactive with stdin pipe (no PTY): reconnect terminal stdin after prompt injection
1577
+ process.stdin.resume();
1578
+ process.stdin.pipe(child.stdin);
1579
+ }
1580
+ }
1581
+ if (promptInArgs && child.stdin && !ptyProcess) {
1582
+ child.stdin.end();
1583
+ }
1584
+ let exitCode = await (child.__bridgeExitPromise
1585
+ ? child.__bridgeExitPromise
1586
+ : new Promise((resolve) => {
1587
+ if (ptyProcess) {
1588
+ ptyProcess.onExit(({ exitCode: code }) => {
1589
+ if (process.stdin.isTTY)
1590
+ process.stdin.setRawMode(false);
1591
+ resolve(code);
1592
+ });
1593
+ }
1594
+ else {
1595
+ child.on('exit', (code, signal) => {
1596
+ resolve(signal ? 128 + (signal === 'SIGINT' ? 2 : 15) : (code ?? 1));
1597
+ });
1598
+ }
1599
+ }));
1600
+ for (const cleanup of ptyCleanup.splice(0))
1601
+ cleanup();
1602
+ if (ptyTerminationExpected && exitCode !== 0)
1603
+ exitCode = 0;
1604
+ process.off('SIGINT', forwardSignal);
1605
+ process.off('SIGTERM', forwardSignal);
1606
+ // Bridge hooks: emulate stop hook and re-spawn if shouldContinue
1607
+ if (bridgeHookEmulator) {
1608
+ let stopResult = await bridgeHookEmulator.emulateStop();
1609
+ while (stopResult.shouldContinue && stopResult.resumeId) {
1610
+ // Re-spawn with --resume to continue the session
1611
+ const resumePlan = { ...plan, args: [...plan.args] };
1612
+ appendHarnessSessionArgs(resumePlan, {
1613
+ resumeId: stopResult.resumeId,
1614
+ interactive: false,
1615
+ });
1616
+ const { spawn: resumeSpawn } = await import('node:child_process');
1617
+ const resumeChild = resumeSpawn(resumePlan.command, resumePlan.args, {
1618
+ stdio: ['pipe', 'inherit', 'inherit'],
1619
+ env: { ...process.env, ...resumePlan.env },
1620
+ cwd: launchCwd,
1621
+ shell: process.platform === 'win32',
1622
+ });
1623
+ if (resumeChild.stdin) {
1624
+ resumeChild.stdin.end();
1625
+ }
1626
+ await new Promise((resolve) => {
1627
+ resumeChild.on('exit', (code, signal) => {
1628
+ resolve(signal ? 128 + (signal === 'SIGINT' ? 2 : 15) : (code ?? 1));
1629
+ });
1630
+ });
1631
+ stopResult = await bridgeHookEmulator.emulateStop();
1632
+ }
1633
+ await bridgeHookEmulator.emulateSessionEnd();
1634
+ }
1635
+ if (proxyRuntime) {
1636
+ await proxyRuntime.stop();
1637
+ }
1638
+ return exitCode;
1639
+ }
1640
+ //# sourceMappingURL=launch.js.map