@a5c-ai/launch-adapter 5.1.1-staging.52898ebfc24f

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