@a5c-ai/launch-adapter 5.1.1-staging.00ceebd28cf2

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