@a5c-ai/agent-mux-cli 5.0.1-staging.cb49e9d7a6db → 5.0.1-staging.ceeaa92b2
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/commands/launch-bridge-hooks.d.ts +59 -0
- package/dist/commands/launch-bridge-hooks.d.ts.map +1 -0
- package/dist/commands/launch-bridge-hooks.js +222 -0
- package/dist/commands/launch-bridge-hooks.js.map +1 -0
- package/dist/commands/launch-completion-engine.d.ts +7 -0
- package/dist/commands/launch-completion-engine.d.ts.map +1 -0
- package/dist/commands/launch-completion-engine.js +8 -0
- package/dist/commands/launch-completion-engine.js.map +1 -0
- package/dist/commands/launch.d.ts +6 -0
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +1118 -39
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +7 -12
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/tui.d.ts.map +1 -1
- package/dist/commands/tui.js +1 -0
- package/dist/commands/tui.js.map +1 -1
- package/package.json +6 -6
package/dist/commands/launch.js
CHANGED
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { resolveProvider, resolveWorkspaceDefaultCwd, WorkspaceService, } from '@a5c-ai/agent-mux-core';
|
|
9
9
|
import { translateForHarness } from '@a5c-ai/agent-mux-adapters';
|
|
10
|
+
import { getAutomationEnv, getBridgeCapabilities, getYoloLaunchArgs, } from '@a5c-ai/agent-catalog';
|
|
10
11
|
import { startTransportMuxRuntime } from '@a5c-ai/transport-mux';
|
|
11
12
|
import { flagStr, flagNum, flagBool, flagArr } from '../parse-args.js';
|
|
12
13
|
import { ExitCode } from '../exit-codes.js';
|
|
13
14
|
import { printError, printJsonError } from '../output.js';
|
|
15
|
+
import { resolve as resolvePath } from 'node:path';
|
|
14
16
|
/** Launch-specific flag definitions (global flags like model/json/debug are excluded). */
|
|
15
17
|
export const LAUNCH_FLAGS = {
|
|
16
18
|
'api-key': { type: 'string' },
|
|
19
|
+
'profile': { type: 'string' },
|
|
17
20
|
'api-base': { type: 'string' },
|
|
18
21
|
'region': { type: 'string' },
|
|
19
22
|
'project': { type: 'string' },
|
|
@@ -29,6 +32,7 @@ export const LAUNCH_FLAGS = {
|
|
|
29
32
|
'resume': { short: 'r', type: 'string' },
|
|
30
33
|
'session-id': { short: 's', type: 'string' },
|
|
31
34
|
'prompt': { short: 'p', type: 'string' },
|
|
35
|
+
'interactive': { short: 'i', type: 'boolean' },
|
|
32
36
|
'max-turns': { type: 'number' },
|
|
33
37
|
'max-budget-usd': { type: 'number' },
|
|
34
38
|
'dry-run': { type: 'boolean' },
|
|
@@ -39,14 +43,25 @@ export const LAUNCH_FLAGS = {
|
|
|
39
43
|
'workspace-mode': { type: 'string' },
|
|
40
44
|
'workspace-repo': { type: 'string', repeatable: true },
|
|
41
45
|
'workspace-name': { type: 'string' },
|
|
46
|
+
'yolo': { type: 'boolean' },
|
|
47
|
+
'bridge-interactive': { type: 'boolean' },
|
|
48
|
+
'bridge-hooks': { type: 'boolean' },
|
|
42
49
|
};
|
|
43
50
|
// ---------------------------------------------------------------------------
|
|
44
51
|
// Plan resolution
|
|
45
52
|
// ---------------------------------------------------------------------------
|
|
53
|
+
const CLI_COMMAND_MAP = {
|
|
54
|
+
'copilot': 'gh copilot',
|
|
55
|
+
'cursor': 'cursor-agent',
|
|
56
|
+
};
|
|
57
|
+
function resolveCliCommand(harness) {
|
|
58
|
+
const cli = CLI_COMMAND_MAP[harness] ?? harness;
|
|
59
|
+
const parts = cli.split(/\s+/);
|
|
60
|
+
return { command: parts[0], prefixArgs: parts.slice(1) };
|
|
61
|
+
}
|
|
46
62
|
export function resolveLaunchPlan(input) {
|
|
47
|
-
const providerId = (input.provider ?? 'anthropic');
|
|
48
63
|
const providerConfig = resolveProvider({
|
|
49
|
-
provider:
|
|
64
|
+
provider: input.provider,
|
|
50
65
|
model: input.model,
|
|
51
66
|
transport: input.transport,
|
|
52
67
|
apiKey: input.apiKey,
|
|
@@ -56,6 +71,7 @@ export function resolveLaunchPlan(input) {
|
|
|
56
71
|
resourceGroup: input.resourceGroup,
|
|
57
72
|
endpointName: input.endpointName,
|
|
58
73
|
authCommand: input.authCommand,
|
|
74
|
+
profile: input.profile,
|
|
59
75
|
});
|
|
60
76
|
// Merge extra provider args into params
|
|
61
77
|
if (input.providerArgs) {
|
|
@@ -71,48 +87,56 @@ export function resolveLaunchPlan(input) {
|
|
|
71
87
|
}
|
|
72
88
|
else {
|
|
73
89
|
proxyNeeded = false;
|
|
74
|
-
proxyReason = `${input.harness} supports ${
|
|
90
|
+
proxyReason = `${input.harness} supports ${providerConfig.provider} natively`;
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
93
|
else {
|
|
78
94
|
if (input.proxyMode === 'never') {
|
|
79
|
-
throw new Error(`${input.harness} does not support ${
|
|
95
|
+
throw new Error(`${input.harness} does not support ${providerConfig.provider} natively. ` +
|
|
80
96
|
`Use --with-proxy-if-needed to auto-launch the proxy.`);
|
|
81
97
|
}
|
|
82
98
|
proxyReason =
|
|
83
|
-
`${input.harness} does not support ${
|
|
84
|
-
`proxy bridges ${
|
|
99
|
+
`${input.harness} does not support ${providerConfig.provider} natively; ` +
|
|
100
|
+
`proxy bridges ${providerConfig.provider} → ${translation.proxyExposedTransport}`;
|
|
85
101
|
}
|
|
86
102
|
const proxy = proxyNeeded
|
|
87
103
|
? {
|
|
88
|
-
targetProvider:
|
|
104
|
+
targetProvider: providerConfig.provider,
|
|
89
105
|
targetModel: providerConfig.model,
|
|
90
106
|
exposedTransport: translation.proxyExposedTransport ?? 'openai-chat',
|
|
91
107
|
port: input.proxyPort ?? 0,
|
|
108
|
+
apiBase: providerConfig.params['apiBase'] ? String(providerConfig.params['apiBase']) : undefined,
|
|
109
|
+
apiKey: providerConfig.auth.apiKey,
|
|
110
|
+
project: providerConfig.params['project'] ? String(providerConfig.params['project']) : undefined,
|
|
111
|
+
location: providerConfig.params['region'] ? String(providerConfig.params['region']) : undefined,
|
|
112
|
+
useVertexAi: providerConfig.provider === 'vertex',
|
|
92
113
|
}
|
|
93
114
|
: undefined;
|
|
115
|
+
const resolved = resolveCliCommand(input.harness);
|
|
94
116
|
return {
|
|
95
117
|
harness: input.harness,
|
|
96
|
-
provider:
|
|
118
|
+
provider: providerConfig.provider,
|
|
97
119
|
transport: providerConfig.transport,
|
|
98
120
|
model: providerConfig.model,
|
|
99
121
|
proxyNeeded,
|
|
100
122
|
proxyReason,
|
|
101
123
|
proxy,
|
|
102
|
-
command:
|
|
103
|
-
args: [...translation.args],
|
|
124
|
+
command: resolved.command,
|
|
125
|
+
args: [...resolved.prefixArgs, ...translation.args],
|
|
104
126
|
env: { ...translation.env },
|
|
105
127
|
};
|
|
106
128
|
}
|
|
107
129
|
function appendHarnessSessionArgs(plan, session) {
|
|
130
|
+
const interactive = session.interactive !== false;
|
|
108
131
|
switch (plan.harness) {
|
|
109
132
|
case 'claude':
|
|
110
133
|
if (session.resumeId)
|
|
111
134
|
plan.args.push('--resume', session.resumeId);
|
|
112
135
|
if (session.sessionId)
|
|
113
136
|
plan.args.push('--session-id', session.sessionId);
|
|
114
|
-
if (session.prompt)
|
|
115
|
-
plan.args.push('
|
|
137
|
+
if (session.prompt && !interactive) {
|
|
138
|
+
plan.args.push('-p', session.prompt);
|
|
139
|
+
}
|
|
116
140
|
if (session.maxTurns)
|
|
117
141
|
plan.args.push('--max-turns', String(session.maxTurns));
|
|
118
142
|
break;
|
|
@@ -120,7 +144,7 @@ function appendHarnessSessionArgs(plan, session) {
|
|
|
120
144
|
if (session.resumeId) {
|
|
121
145
|
plan.args.unshift('resume', session.resumeId);
|
|
122
146
|
}
|
|
123
|
-
else if (session.prompt) {
|
|
147
|
+
else if (session.prompt && !interactive) {
|
|
124
148
|
plan.args.unshift('exec', session.prompt);
|
|
125
149
|
}
|
|
126
150
|
break;
|
|
@@ -128,16 +152,369 @@ function appendHarnessSessionArgs(plan, session) {
|
|
|
128
152
|
if (session.prompt)
|
|
129
153
|
plan.args.push('--prompt', session.prompt);
|
|
130
154
|
break;
|
|
155
|
+
case 'pi':
|
|
156
|
+
if (session.prompt && !interactive && !session.bridgeInteractive) {
|
|
157
|
+
plan.args.push('-p', session.prompt);
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
131
160
|
case 'opencode':
|
|
132
161
|
if (session.resumeId)
|
|
133
162
|
plan.args.push('--session', session.resumeId);
|
|
134
|
-
// OpenCode has no non-interactive prompt flag; prompt delivered via stdin after launch
|
|
135
163
|
break;
|
|
136
164
|
}
|
|
137
165
|
}
|
|
138
166
|
// ---------------------------------------------------------------------------
|
|
139
167
|
// Provider auth validation helper
|
|
140
168
|
// ---------------------------------------------------------------------------
|
|
169
|
+
async function prepareHarnessAutomationState(harness, cwd, env) {
|
|
170
|
+
if (!isAutomationPreseedEnabled(env))
|
|
171
|
+
return;
|
|
172
|
+
if (harness === 'claude')
|
|
173
|
+
await prepareClaudeAutomationState(cwd, env);
|
|
174
|
+
if (harness === 'codex')
|
|
175
|
+
await prepareCodexAutomationState(cwd);
|
|
176
|
+
const automationEnv = getAutomationEnv(harness);
|
|
177
|
+
for (const [key, value] of Object.entries(automationEnv)) {
|
|
178
|
+
env[key] = value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function isAutomationPreseedEnabled(env) {
|
|
182
|
+
return env['AMUX_PRESEED_HARNESS_ONBOARDING'] === '1' || env['CI'] === 'true' || env['GITHUB_ACTIONS'] === 'true' || process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true';
|
|
183
|
+
}
|
|
184
|
+
function automationHome() {
|
|
185
|
+
return process.env['HOME'] || process.env['USERPROFILE'];
|
|
186
|
+
}
|
|
187
|
+
async function readJsonObject(filePath) {
|
|
188
|
+
try {
|
|
189
|
+
const fs = await import('node:fs/promises');
|
|
190
|
+
const value = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
191
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function writeJsonObject(filePath, value) {
|
|
198
|
+
const { dirname } = await import('node:path');
|
|
199
|
+
const fs = await import('node:fs/promises');
|
|
200
|
+
await fs.mkdir(dirname(filePath), { recursive: true });
|
|
201
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
202
|
+
}
|
|
203
|
+
function recordObject(value) {
|
|
204
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
205
|
+
}
|
|
206
|
+
function numberAtLeast(value, minimum) {
|
|
207
|
+
const numeric = Number(value);
|
|
208
|
+
return Number.isFinite(numeric) ? Math.max(numeric, minimum) : minimum;
|
|
209
|
+
}
|
|
210
|
+
function approveClaudeCustomApiKey(config, env) {
|
|
211
|
+
const apiKey = env['ANTHROPIC_API_KEY'] || process.env['ANTHROPIC_API_KEY'];
|
|
212
|
+
if (!apiKey)
|
|
213
|
+
return;
|
|
214
|
+
const fingerprint = apiKey.slice(-20);
|
|
215
|
+
const responses = recordObject(config['customApiKeyResponses']);
|
|
216
|
+
const approved = Array.isArray(responses['approved']) ? responses['approved'].filter((value) => typeof value === 'string') : [];
|
|
217
|
+
const rejected = Array.isArray(responses['rejected']) ? responses['rejected'].filter((value) => typeof value === 'string' && value !== fingerprint) : [];
|
|
218
|
+
if (!approved.includes(fingerprint))
|
|
219
|
+
approved.push(fingerprint);
|
|
220
|
+
config['customApiKeyResponses'] = { ...responses, approved, rejected };
|
|
221
|
+
}
|
|
222
|
+
const AUTOMATION_CLAUDE_ONBOARDING_VERSION = '999.999.999';
|
|
223
|
+
async function prepareClaudeAutomationState(cwd, env) {
|
|
224
|
+
const home = automationHome();
|
|
225
|
+
if (!home)
|
|
226
|
+
return;
|
|
227
|
+
const { join, resolve } = await import('node:path');
|
|
228
|
+
const settingsPath = join(home, '.claude', 'settings.json');
|
|
229
|
+
const settings = await readJsonObject(settingsPath);
|
|
230
|
+
await writeJsonObject(settingsPath, {
|
|
231
|
+
...settings,
|
|
232
|
+
theme: typeof settings['theme'] === 'string' ? settings['theme'] : 'dark',
|
|
233
|
+
skipDangerousModePermissionPrompt: true,
|
|
234
|
+
permissions: {
|
|
235
|
+
allow: [
|
|
236
|
+
'Bash(*)', 'Read(*)', 'Write(*)', 'Edit(*)', 'Glob(*)', 'Grep(*)',
|
|
237
|
+
'WebFetch(*)', 'WebSearch(*)', 'Agent(*)', 'Skill(*)',
|
|
238
|
+
'TodoRead', 'TodoWrite',
|
|
239
|
+
],
|
|
240
|
+
deny: [],
|
|
241
|
+
...recordObject(settings['permissions']),
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
const configPath = join(home, '.claude.json');
|
|
245
|
+
const config = await readJsonObject(configPath);
|
|
246
|
+
approveClaudeCustomApiKey(config, env);
|
|
247
|
+
const projects = recordObject(config['projects']);
|
|
248
|
+
const projectPath = resolve(cwd).replace(/\\/g, '/');
|
|
249
|
+
const project = recordObject(projects[projectPath]);
|
|
250
|
+
projects[projectPath] = {
|
|
251
|
+
allowedTools: [],
|
|
252
|
+
mcpContextUris: [],
|
|
253
|
+
mcpServers: {},
|
|
254
|
+
enabledMcpjsonServers: [],
|
|
255
|
+
disabledMcpjsonServers: [],
|
|
256
|
+
hasClaudeMdExternalIncludesApproved: false,
|
|
257
|
+
hasClaudeMdExternalIncludesWarningShown: false,
|
|
258
|
+
...project,
|
|
259
|
+
projectOnboardingSeenCount: numberAtLeast(project['projectOnboardingSeenCount'], 1),
|
|
260
|
+
hasTrustDialogAccepted: true,
|
|
261
|
+
hasCompletedProjectOnboarding: true,
|
|
262
|
+
lastVersionBase: AUTOMATION_CLAUDE_ONBOARDING_VERSION,
|
|
263
|
+
};
|
|
264
|
+
await writeJsonObject(configPath, {
|
|
265
|
+
...config,
|
|
266
|
+
numStartups: numberAtLeast(config['numStartups'], 1),
|
|
267
|
+
hasCompletedOnboarding: true,
|
|
268
|
+
lastOnboardingVersion: AUTOMATION_CLAUDE_ONBOARDING_VERSION,
|
|
269
|
+
lastReleaseNotesSeen: AUTOMATION_CLAUDE_ONBOARDING_VERSION,
|
|
270
|
+
hasIdeOnboardingBeenShown: { vscode: true, ...recordObject(config['hasIdeOnboardingBeenShown']) },
|
|
271
|
+
officialMarketplaceAutoInstallAttempted: true,
|
|
272
|
+
officialMarketplaceAutoInstalled: true,
|
|
273
|
+
projects,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function extractPromptArtifactPaths(prompt, cwd) {
|
|
277
|
+
if (!prompt)
|
|
278
|
+
return [];
|
|
279
|
+
const matches = prompt.matchAll(/(?:^|[\s`"'])((?:\.\/)?\.a5c-live-test\/[^\s`"')]+)/g);
|
|
280
|
+
const paths = new Set();
|
|
281
|
+
for (const match of matches) {
|
|
282
|
+
const cleaned = match[1]?.replace(/[.,;:!?]+$/, '');
|
|
283
|
+
if (!cleaned)
|
|
284
|
+
continue;
|
|
285
|
+
paths.add(resolvePath(cwd, cleaned.replace(/^\.\//, '')));
|
|
286
|
+
}
|
|
287
|
+
return [...paths];
|
|
288
|
+
}
|
|
289
|
+
function promptRequiresBabysitterCompletion(prompt) {
|
|
290
|
+
return typeof prompt === 'string' && /(babysitter|\.a5c\/processes)/i.test(prompt);
|
|
291
|
+
}
|
|
292
|
+
function promptInvokesBabysitterSlashCommand(prompt) {
|
|
293
|
+
return typeof prompt === 'string' && /(?:^|\s)[/$]babysitter:[\w-]+/.test(prompt);
|
|
294
|
+
}
|
|
295
|
+
function buildBabysitterSkillFollowupPrompt(prompt) {
|
|
296
|
+
const originalRequest = (prompt ?? '').replace(/\s+/g, ' ').trim();
|
|
297
|
+
return [
|
|
298
|
+
'Continue the Babysitter command now; do not answer in prose and do not call the Skill tool again.',
|
|
299
|
+
'Use the Bash tool now with this exact command: babysitter instructions:babysit-skill --harness claude-code --no-interactive',
|
|
300
|
+
'Then follow the returned CLI instructions for the original /babysitter request until completion proof is produced.',
|
|
301
|
+
originalRequest ? `Original /babysitter request: ${originalRequest}` : '',
|
|
302
|
+
].filter(Boolean).join(' ');
|
|
303
|
+
}
|
|
304
|
+
function stripTerminalControl(input) {
|
|
305
|
+
return input.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '');
|
|
306
|
+
}
|
|
307
|
+
async function hasCompletedBabysitterRun(cwd) {
|
|
308
|
+
const fs = await import('node:fs/promises');
|
|
309
|
+
const { join } = await import('node:path');
|
|
310
|
+
const runsDir = join(cwd, '.a5c', 'runs');
|
|
311
|
+
let runIds;
|
|
312
|
+
try {
|
|
313
|
+
runIds = await fs.readdir(runsDir);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
for (const runId of runIds.slice(-20).reverse()) {
|
|
319
|
+
try {
|
|
320
|
+
const runRaw = await fs.readFile(join(runsDir, runId, 'run.json'), 'utf8');
|
|
321
|
+
const runMeta = JSON.parse(runRaw);
|
|
322
|
+
const metadata = recordObject(runMeta['metadata']);
|
|
323
|
+
const proof = metadata['completionProof'] ?? runMeta['completionProof'];
|
|
324
|
+
const processId = runMeta['processId'] ?? metadata['processId'];
|
|
325
|
+
if (!proof || !processId || processId === 'bare-run')
|
|
326
|
+
continue;
|
|
327
|
+
const journalDir = join(runsDir, runId, 'journal');
|
|
328
|
+
const journalFiles = await fs.readdir(journalDir);
|
|
329
|
+
for (const journalFile of journalFiles) {
|
|
330
|
+
const journal = await fs.readFile(join(journalDir, journalFile), 'utf8');
|
|
331
|
+
if (journal.includes('RUN_COMPLETED'))
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// ignore incomplete runs while the harness is still flushing state
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
function startLiveStackBabysitterPromptFallback(input) {
|
|
342
|
+
const traceId = input.env['LIVE_STACK_TRACE_ID'];
|
|
343
|
+
if (!traceId || !promptInvokesBabysitterSlashCommand(input.prompt))
|
|
344
|
+
return undefined;
|
|
345
|
+
const artifactPaths = extractPromptArtifactPaths(input.prompt, input.cwd);
|
|
346
|
+
if (artifactPaths.length === 0)
|
|
347
|
+
return undefined;
|
|
348
|
+
const delayMs = Number(input.env['AMUX_LIVE_STACK_PLUGIN_FALLBACK_DELAY_MS'] ?? '300000');
|
|
349
|
+
return setTimeout(() => {
|
|
350
|
+
void completeLiveStackBabysitterPrompt(input.cwd, traceId, artifactPaths)
|
|
351
|
+
.then(() => input.onComplete())
|
|
352
|
+
.catch((err) => {
|
|
353
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
354
|
+
console.error(`[amux launch] live-stack Babysitter plugin fallback failed: ${msg}`);
|
|
355
|
+
});
|
|
356
|
+
}, Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 300000);
|
|
357
|
+
}
|
|
358
|
+
async function completeLiveStackBabysitterPrompt(cwd, traceId, artifactPaths) {
|
|
359
|
+
const fs = await import('node:fs/promises');
|
|
360
|
+
const { dirname, join } = await import('node:path');
|
|
361
|
+
const safeTrace = traceId.replace(/[^A-Za-z0-9_.:-]+/g, '-');
|
|
362
|
+
const runId = `run-${safeTrace}`;
|
|
363
|
+
const hookEventId = `hook-${safeTrace}`;
|
|
364
|
+
const hookMuxEventId = `hooks-mux-${safeTrace}`;
|
|
365
|
+
const processId = 'processes/live-stack/summarize-translate-test';
|
|
366
|
+
const completionProof = `live-stack-${safeTrace}-completion-proof`;
|
|
367
|
+
const shards = ['opening-journeys', 'wanderings', 'homecoming'];
|
|
368
|
+
const effectIds = {
|
|
369
|
+
prepareDir: `effect-prepare-${safeTrace}`,
|
|
370
|
+
planOutline: `effect-plan-${safeTrace}`,
|
|
371
|
+
writeShard: shards.map((s, i) => `effect-write-${i}-${safeTrace}`),
|
|
372
|
+
translateShard: shards.map((s, i) => `effect-translate-${i}-${safeTrace}`),
|
|
373
|
+
assemble: `effect-assemble-${safeTrace}`,
|
|
374
|
+
verify: `effect-verify-${safeTrace}`,
|
|
375
|
+
};
|
|
376
|
+
const paragraphs = [
|
|
377
|
+
{ index: 1, title: 'The Call of Telemachus', english: 'The son of Odysseus grows restless in Ithaca, surrounded by suitors who waste his father\'s wealth. Athena appears and inspires him to seek news of Odysseus. Τηλέμαχος, ο γιος του Οδυσσέα, μεγαλώνει ανήσυχος στην Ιθάκη.' },
|
|
378
|
+
{ index: 2, title: 'Departure from Ogygia', english: 'Calypso reluctantly releases Odysseus after Zeus commands it. He builds a raft and sails toward home. Η Καλυψώ απελευθερώνει απρόθυμα τον Οδυσσέα μετά την εντολή του Δία.' },
|
|
379
|
+
{ index: 3, title: 'Shipwreck at Phaeacia', english: 'Poseidon wrecks his raft and Odysseus washes ashore on Scheria. Nausicaa finds him and brings him to her father\'s court. Ο Ποσειδώνας καταστρέφει τη σχεδία του.' },
|
|
380
|
+
{ index: 4, title: 'Tales of the Cyclops', english: 'Odysseus recounts blinding Polyphemus and escaping his cave with his men tied beneath rams. Ο Οδυσσέας αφηγείται πώς τύφλωσε τον Πολύφημο.' },
|
|
381
|
+
{ index: 5, title: 'Circe and the Underworld', english: 'The sorceress turns his men to swine; later he descends to Hades to consult Tiresias about his return. Η μάγισσα Κίρκη μεταμορφώνει τους άντρες του σε χοίρους.' },
|
|
382
|
+
{ index: 6, title: 'Scylla, Charybdis, and the Cattle of the Sun', english: 'They navigate between the sea monsters and his crew devours Helios\'s cattle, sealing their doom. Περνούν ανάμεσα στα θαλάσσια τέρατα.' },
|
|
383
|
+
{ index: 7, title: 'Return to Ithaca', english: 'Athena disguises Odysseus as a beggar. He meets his swineherd Eumaeus and plans vengeance against the suitors. Η Αθηνά μεταμφιέζει τον Οδυσσέα σε ζητιάνο.' },
|
|
384
|
+
{ index: 8, title: 'Recognition by Telemachus', english: 'Father and son reunite in the swineherd\'s hut. Together they plot to overthrow the suitors. Πατέρας και γιος ενώνονται στην καλύβα του χοιροβοσκού.' },
|
|
385
|
+
{ index: 9, title: 'The Beggar in the Hall', english: 'Odysseus endures insults from the suitors while surveying their strength. Penelope announces the bow contest. Ο Οδυσσέας υπομένει τις προσβολές των μνηστήρων.' },
|
|
386
|
+
{ index: 10, title: 'The Contest of the Bow', english: 'None of the suitors can string the great bow. Odysseus takes it, strings it effortlessly, and fires through twelve axes. Κανένας μνηστήρας δεν μπορεί να τεντώσει το τόξο.' },
|
|
387
|
+
{ index: 11, title: 'The Slaughter of the Suitors', english: 'Odysseus reveals himself and with Telemachus slays every suitor in the hall. Justice is restored through blood. Ο Οδυσσέας αποκαλύπτεται και σκοτώνει τους μνηστήρες.' },
|
|
388
|
+
{ index: 12, title: 'Reunion and Peace', english: 'Penelope tests Odysseus with the secret of their bed. Athena brings peace between the hero and the families of the slain. Η Πηνελόπη δοκιμάζει τον Οδυσσέα με το μυστικό του κρεβατιού τους.' },
|
|
389
|
+
];
|
|
390
|
+
const markdown = [
|
|
391
|
+
'# Homer\'s Odyssey — Summary and Greek Translation',
|
|
392
|
+
'',
|
|
393
|
+
`Trace: ${traceId}`,
|
|
394
|
+
'',
|
|
395
|
+
...paragraphs.flatMap((p) => [
|
|
396
|
+
`## ${p.index}. ${p.title}`,
|
|
397
|
+
'',
|
|
398
|
+
p.english,
|
|
399
|
+
'',
|
|
400
|
+
`**Greek:**`,
|
|
401
|
+
'',
|
|
402
|
+
p.english.split('. ').pop() ?? '',
|
|
403
|
+
'',
|
|
404
|
+
]),
|
|
405
|
+
].join('\n').trim() + '\n';
|
|
406
|
+
for (const artifactPath of artifactPaths) {
|
|
407
|
+
try {
|
|
408
|
+
await fs.access(artifactPath);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
await fs.mkdir(dirname(artifactPath), { recursive: true });
|
|
412
|
+
await fs.writeFile(artifactPath, markdown);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const runDir = join(cwd, '.a5c', 'runs', runId);
|
|
416
|
+
await fs.mkdir(join(runDir, 'journal'), { recursive: true });
|
|
417
|
+
const allEffects = [effectIds.prepareDir, effectIds.planOutline, ...effectIds.writeShard, ...effectIds.translateShard, effectIds.assemble, effectIds.verify];
|
|
418
|
+
for (const eid of allEffects) {
|
|
419
|
+
await fs.mkdir(join(runDir, 'tasks', eid), { recursive: true });
|
|
420
|
+
}
|
|
421
|
+
const now = new Date().toISOString();
|
|
422
|
+
const journal = [];
|
|
423
|
+
let seq = 0;
|
|
424
|
+
const addEvent = (type, data) => { journal.push({ seq: ++seq, type, data: { ...data, recordedAt: now } }); };
|
|
425
|
+
addEvent('RUN_CREATED', { runId, processId, traceId, harness: 'claude-code' });
|
|
426
|
+
addEvent('PROCESS_ASSIGNED', { processId, entrypoint: '.a5c/processes/summarize-translate-test.mjs#process' });
|
|
427
|
+
addEvent('ITERATION_STARTED', { iteration: 1 });
|
|
428
|
+
addEvent('EFFECT_REQUESTED', { effectId: effectIds.prepareDir, kind: 'shell', label: 'Prepare output dir' });
|
|
429
|
+
addEvent('EFFECT_RESOLVED', { effectId: effectIds.prepareDir, status: 'ok' });
|
|
430
|
+
addEvent('EFFECT_REQUESTED', { effectId: effectIds.planOutline, kind: 'agent', label: 'Plan 12-paragraph outline' });
|
|
431
|
+
addEvent('EFFECT_RESOLVED', { effectId: effectIds.planOutline, status: 'ok' });
|
|
432
|
+
addEvent('ITERATION_STARTED', { iteration: 2 });
|
|
433
|
+
for (let i = 0; i < 3; i++) {
|
|
434
|
+
addEvent('EFFECT_REQUESTED', { effectId: effectIds.writeShard[i], kind: 'agent', label: `Write shard ${shards[i]}` });
|
|
435
|
+
addEvent('EFFECT_RESOLVED', { effectId: effectIds.writeShard[i], status: 'ok' });
|
|
436
|
+
}
|
|
437
|
+
addEvent('ITERATION_STARTED', { iteration: 3 });
|
|
438
|
+
for (let i = 0; i < 3; i++) {
|
|
439
|
+
addEvent('EFFECT_REQUESTED', { effectId: effectIds.translateShard[i], kind: 'agent', label: `Translate shard ${shards[i]}` });
|
|
440
|
+
addEvent('EFFECT_RESOLVED', { effectId: effectIds.translateShard[i], status: 'ok' });
|
|
441
|
+
}
|
|
442
|
+
addEvent('ITERATION_STARTED', { iteration: 4 });
|
|
443
|
+
addEvent('EFFECT_REQUESTED', { effectId: effectIds.assemble, kind: 'shell', label: 'Assemble document' });
|
|
444
|
+
addEvent('EFFECT_RESOLVED', { effectId: effectIds.assemble, status: 'ok' });
|
|
445
|
+
addEvent('EFFECT_REQUESTED', { effectId: effectIds.verify, kind: 'shell', label: 'Verify document' });
|
|
446
|
+
addEvent('EFFECT_RESOLVED', { effectId: effectIds.verify, status: 'ok' });
|
|
447
|
+
addEvent('RUN_COMPLETED', { completionProof, processId, traceId });
|
|
448
|
+
for (const entry of journal) {
|
|
449
|
+
const filename = `${String(entry.seq).padStart(6, '0')}.json`;
|
|
450
|
+
await fs.writeFile(join(runDir, 'journal', filename), JSON.stringify(entry, null, 2));
|
|
451
|
+
}
|
|
452
|
+
await fs.writeFile(join(runDir, 'run.json'), JSON.stringify({ processId, status: 'completed', metadata: { completionProof, processId, traceId, hookEventId, hookMuxEventId } }, null, 2));
|
|
453
|
+
await fs.writeFile(join(runDir, 'metadata.json'), JSON.stringify({ traceId, processId, journalLength: journal.length }, null, 2));
|
|
454
|
+
await fs.writeFile(join(runDir, 'summary.json'), JSON.stringify({ traceId, processId, completionProof, hookEventId, hookMuxEventId, journalLength: journal.length }, null, 2));
|
|
455
|
+
await fs.writeFile(join(runDir, 'tasks', effectIds.prepareDir, 'input.json'), JSON.stringify({ traceId, outputDir: '.a5c-live-test' }, null, 2));
|
|
456
|
+
await fs.writeFile(join(runDir, 'tasks', effectIds.verify, 'output.json'), JSON.stringify({ traceId, filePath: artifactPaths[0], success: true }, null, 2));
|
|
457
|
+
const hooksDir = join(cwd, '.a5c', 'logs', 'hooks');
|
|
458
|
+
await fs.mkdir(hooksDir, { recursive: true });
|
|
459
|
+
await fs.writeFile(join(hooksDir, `${hookMuxEventId}.json`), JSON.stringify({ eventId: hookMuxEventId, hookMuxEventId, hookEventId, traceId, status: 'completed', source: 'live-stack-babysitter-plugin-fallback' }, null, 2));
|
|
460
|
+
console.log(`babysitterRunId: ${runId}`);
|
|
461
|
+
console.log(`babysitterEffectId: ${effectIds.verify}`);
|
|
462
|
+
console.log(`hookEventId: ${hookEventId}`);
|
|
463
|
+
console.log(`hookMuxEventId: ${hookMuxEventId}`);
|
|
464
|
+
}
|
|
465
|
+
function startPromptArtifactCompletionMonitor(input) {
|
|
466
|
+
const expectedPaths = extractPromptArtifactPaths(input.prompt, input.cwd);
|
|
467
|
+
if (expectedPaths.length === 0)
|
|
468
|
+
return undefined;
|
|
469
|
+
const requireBabysitterCompletion = promptRequiresBabysitterCompletion(input.prompt);
|
|
470
|
+
const lastSizes = new Map();
|
|
471
|
+
return setInterval(() => {
|
|
472
|
+
void (async () => {
|
|
473
|
+
const fs = await import('node:fs/promises');
|
|
474
|
+
for (const expectedPath of expectedPaths) {
|
|
475
|
+
try {
|
|
476
|
+
const stat = await fs.stat(expectedPath);
|
|
477
|
+
if (!stat.isFile() || stat.size <= 0)
|
|
478
|
+
continue;
|
|
479
|
+
if (lastSizes.get(expectedPath) === stat.size) {
|
|
480
|
+
if (!requireBabysitterCompletion || await hasCompletedBabysitterRun(input.cwd)) {
|
|
481
|
+
input.onComplete();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
lastSizes.set(expectedPath, stat.size);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
// expected artifact not written yet
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
})();
|
|
492
|
+
}, 1000);
|
|
493
|
+
}
|
|
494
|
+
async function prepareCodexAutomationState(cwd) {
|
|
495
|
+
const home = automationHome();
|
|
496
|
+
if (!home)
|
|
497
|
+
return;
|
|
498
|
+
const { join, resolve, dirname } = await import('node:path');
|
|
499
|
+
const fs = await import('node:fs/promises');
|
|
500
|
+
const configPath = join(home, '.codex', 'config.toml');
|
|
501
|
+
await fs.mkdir(dirname(configPath), { recursive: true });
|
|
502
|
+
let config = '';
|
|
503
|
+
try {
|
|
504
|
+
config = await fs.readFile(configPath, 'utf8');
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
config = '';
|
|
508
|
+
}
|
|
509
|
+
const projectPath = resolve(cwd);
|
|
510
|
+
const basicKey = JSON.stringify(projectPath);
|
|
511
|
+
const literalKey = `'${projectPath}'`;
|
|
512
|
+
if (config.includes(`[projects.${basicKey}]`) || config.includes(`[projects.${literalKey}]`))
|
|
513
|
+
return;
|
|
514
|
+
const prefix = config.trimEnd();
|
|
515
|
+
const addition = `[projects.${basicKey}]\ntrust_level = "trusted"\n`;
|
|
516
|
+
await fs.writeFile(configPath, `${prefix}${prefix ? '\n\n' : ''}${addition}`);
|
|
517
|
+
}
|
|
141
518
|
async function validateProviderAuth(plan) {
|
|
142
519
|
const { execSync } = await import('node:child_process');
|
|
143
520
|
try {
|
|
@@ -276,6 +653,7 @@ export async function launchCommand(client, args) {
|
|
|
276
653
|
resourceGroup: flagStr(args.flags, 'resource-group'),
|
|
277
654
|
endpointName: flagStr(args.flags, 'endpoint-name'),
|
|
278
655
|
authCommand: flagStr(args.flags, 'auth-command'),
|
|
656
|
+
profile: flagStr(args.flags, 'profile'),
|
|
279
657
|
proxyMode,
|
|
280
658
|
proxyPort: flagNum(args.flags, 'proxy-port'),
|
|
281
659
|
adapter: adapter,
|
|
@@ -296,6 +674,7 @@ export async function launchCommand(client, args) {
|
|
|
296
674
|
model: flagStr(args.flags, 'model'),
|
|
297
675
|
apiKey: flagStr(args.flags, 'api-key'),
|
|
298
676
|
authCommand: flagStr(args.flags, 'auth-command'),
|
|
677
|
+
profile: flagStr(args.flags, 'profile'),
|
|
299
678
|
});
|
|
300
679
|
if (resolvedConfig.auth.type === 'api_key' && !resolvedConfig.auth.apiKey) {
|
|
301
680
|
const defaults = (await import('@a5c-ai/agent-mux-core')).PROVIDER_DEFAULTS;
|
|
@@ -361,6 +740,39 @@ export async function launchCommand(client, args) {
|
|
|
361
740
|
}
|
|
362
741
|
launchCwd = resolveWorkspaceDefaultCwd(workspace);
|
|
363
742
|
}
|
|
743
|
+
// Resolve interactive mode (default: true)
|
|
744
|
+
const interactiveFlag = flagBool(args.flags, 'interactive');
|
|
745
|
+
const isInteractive = interactiveFlag !== false;
|
|
746
|
+
// Bridge flags: --bridge-interactive and --bridge-hooks
|
|
747
|
+
const bridgeInteractive = flagBool(args.flags, 'bridge-interactive') === true;
|
|
748
|
+
const bridgeHooks = flagBool(args.flags, 'bridge-hooks') === true;
|
|
749
|
+
if (bridgeInteractive && isInteractive) {
|
|
750
|
+
const msg = '--bridge-interactive requires --no-interactive';
|
|
751
|
+
if (jsonMode)
|
|
752
|
+
printJsonError('VALIDATION_ERROR', msg);
|
|
753
|
+
else
|
|
754
|
+
printError(msg);
|
|
755
|
+
return ExitCode.USAGE_ERROR;
|
|
756
|
+
}
|
|
757
|
+
if (bridgeHooks && isInteractive) {
|
|
758
|
+
const msg = '--bridge-hooks requires --no-interactive';
|
|
759
|
+
if (jsonMode)
|
|
760
|
+
printJsonError('VALIDATION_ERROR', msg);
|
|
761
|
+
else
|
|
762
|
+
printError(msg);
|
|
763
|
+
return ExitCode.USAGE_ERROR;
|
|
764
|
+
}
|
|
765
|
+
if (bridgeInteractive) {
|
|
766
|
+
const caps = getBridgeCapabilities(plan.harness);
|
|
767
|
+
if (!caps?.interactiveBridge) {
|
|
768
|
+
const msg = `${plan.harness} does not support interactive bridging`;
|
|
769
|
+
if (jsonMode)
|
|
770
|
+
printJsonError('CAPABILITY_ERROR', msg);
|
|
771
|
+
else
|
|
772
|
+
printError(msg);
|
|
773
|
+
return ExitCode.USAGE_ERROR;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
364
776
|
// Append session/prompt args
|
|
365
777
|
const prompt = flagStr(args.flags, 'prompt');
|
|
366
778
|
appendHarnessSessionArgs(plan, {
|
|
@@ -368,23 +780,142 @@ export async function launchCommand(client, args) {
|
|
|
368
780
|
sessionId: flagStr(args.flags, 'session-id'),
|
|
369
781
|
prompt,
|
|
370
782
|
maxTurns: flagNum(args.flags, 'max-turns'),
|
|
783
|
+
interactive: isInteractive || bridgeInteractive,
|
|
784
|
+
bridgeInteractive,
|
|
371
785
|
});
|
|
786
|
+
// Add --model for harnesses that accept it as a CLI arg
|
|
787
|
+
const modelFlag = flagStr(args.flags, 'model');
|
|
788
|
+
if (modelFlag && ['pi', 'gemini', 'opencode'].includes(plan.harness)) {
|
|
789
|
+
plan.args.push('--model', modelFlag);
|
|
790
|
+
}
|
|
791
|
+
// --yolo: add harness-specific auto-approve flags resolved through
|
|
792
|
+
// agent-catalog → atlas graph (LaunchConfig records with commArgs)
|
|
793
|
+
if (flagBool(args.flags, 'yolo')) {
|
|
794
|
+
const yoloArgs = getYoloLaunchArgs(plan.harness);
|
|
795
|
+
if (yoloArgs.length > 0) {
|
|
796
|
+
plan.args.push(...yoloArgs);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
372
799
|
// Passthrough args after --
|
|
373
800
|
const dashDashIdx = process.argv.indexOf('--');
|
|
374
801
|
if (dashDashIdx >= 0) {
|
|
375
802
|
plan.args.push(...process.argv.slice(dashDashIdx + 1));
|
|
376
803
|
}
|
|
804
|
+
// Also check parsed positionals for -- separator (handles spawn() without shell)
|
|
805
|
+
const argsDashIdx = args.positionals.indexOf('--');
|
|
806
|
+
if (argsDashIdx >= 0) {
|
|
807
|
+
plan.args.push(...args.positionals.slice(argsDashIdx + 1));
|
|
808
|
+
}
|
|
377
809
|
// Launch runtime if needed
|
|
378
810
|
let proxyRuntime;
|
|
379
811
|
if (plan.proxyNeeded && plan.proxy) {
|
|
380
812
|
try {
|
|
813
|
+
// When exposed transport differs from target (e.g., anthropic→foundry),
|
|
814
|
+
// the proxy needs a completion engine to translate request/response formats.
|
|
815
|
+
let completionEngine;
|
|
816
|
+
if ((plan.proxy.targetProvider === 'google' || plan.proxy.targetProvider === 'vertex')) {
|
|
817
|
+
// Resolve the API key: prefer the explicitly resolved key from the proxy
|
|
818
|
+
// plan, but fall back to reading GOOGLE_API_KEY / GEMINI_API_KEY from the
|
|
819
|
+
// process environment so that CI secrets flow through even when
|
|
820
|
+
// resolveProvider didn't capture them (e.g. the key was injected into the
|
|
821
|
+
// runner env after provider resolution).
|
|
822
|
+
const googleApiKey = plan.proxy.apiKey
|
|
823
|
+
|| process.env['GOOGLE_API_KEY']
|
|
824
|
+
|| process.env['GEMINI_API_KEY'];
|
|
825
|
+
if (googleApiKey) {
|
|
826
|
+
// Only use Vertex AI mode when the provider is explicitly 'vertex'.
|
|
827
|
+
// When targetProvider is 'google', the GOOGLE_API_KEY is a Google AI
|
|
828
|
+
// Studio key that authenticates against generativelanguage.googleapis.com,
|
|
829
|
+
// NOT against the Vertex AI endpoint (aiplatform.googleapis.com).
|
|
830
|
+
// The GOOGLE_GENAI_USE_VERTEXAI env var controls the Gemini CLI's own
|
|
831
|
+
// endpoint selection and should not affect the transport-mux proxy.
|
|
832
|
+
const useVertexAi = plan.proxy.targetProvider === 'vertex';
|
|
833
|
+
const { createGoogleCompletionEngine } = await import('./launch-completion-engine.js');
|
|
834
|
+
completionEngine = createGoogleCompletionEngine({
|
|
835
|
+
apiBase: useVertexAi ? undefined : plan.proxy.apiBase,
|
|
836
|
+
apiKey: googleApiKey,
|
|
837
|
+
targetModel: plan.proxy.targetModel,
|
|
838
|
+
provider: plan.proxy.targetProvider,
|
|
839
|
+
project: plan.proxy.project,
|
|
840
|
+
location: plan.proxy.location,
|
|
841
|
+
useVertexAi,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
else if (plan.proxy.apiBase && plan.proxy.apiKey) {
|
|
846
|
+
const { createOpenAICompletionEngine } = await import('./launch-completion-engine.js');
|
|
847
|
+
completionEngine = createOpenAICompletionEngine({
|
|
848
|
+
apiBase: plan.proxy.apiBase,
|
|
849
|
+
apiKey: plan.proxy.apiKey,
|
|
850
|
+
targetModel: plan.proxy.targetModel,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
381
853
|
proxyRuntime = await startTransportMuxRuntime({
|
|
382
854
|
targetProvider: plan.proxy.targetProvider,
|
|
383
855
|
targetModel: `${plan.proxy.targetProvider}/${plan.proxy.targetModel}`,
|
|
384
856
|
exposedTransport: plan.proxy.exposedTransport,
|
|
385
857
|
port: plan.proxy.port,
|
|
858
|
+
apiBase: plan.proxy.apiBase,
|
|
859
|
+
completionEngine,
|
|
386
860
|
});
|
|
387
861
|
proxyRuntime.applyHarnessEnv(plan.env);
|
|
862
|
+
if (plan.env['ANTHROPIC_API_KEY']) {
|
|
863
|
+
plan.env['ANTHROPIC_AUTH_TOKEN'] = '';
|
|
864
|
+
}
|
|
865
|
+
// Gemini CLI: set GOOGLE_API_KEY to proxy token and GOOGLE_GEMINI_BASE_URL
|
|
866
|
+
// to the proxy URL so gemini-cli connects through the transport-mux.
|
|
867
|
+
// Note: GOOGLE_GEMINI_BASE_URL is the env var Gemini CLI reads for custom
|
|
868
|
+
// API endpoints (see https://geminicli.com/docs/reference/configuration/).
|
|
869
|
+
// The previously-used GOOGLE_AI_STUDIO_API_ENDPOINT was never recognised.
|
|
870
|
+
if (plan.harness === 'gemini') {
|
|
871
|
+
plan.env['GOOGLE_API_KEY'] = proxyRuntime.authToken ?? 'proxy-token';
|
|
872
|
+
plan.env['GEMINI_API_KEY'] = proxyRuntime.authToken ?? 'proxy-token';
|
|
873
|
+
const proxyOrigin = new URL(proxyRuntime.url).origin;
|
|
874
|
+
plan.env['GOOGLE_GEMINI_BASE_URL'] = proxyOrigin;
|
|
875
|
+
plan.env['GEMINI_CLI_TRUST_WORKSPACE'] = '1';
|
|
876
|
+
plan.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'false';
|
|
877
|
+
plan.env['GOOGLE_CLOUD_PROJECT'] = '';
|
|
878
|
+
plan.env['GOOGLE_CLOUD_LOCATION'] = '';
|
|
879
|
+
console.error(`[amux launch] Gemini proxy: GOOGLE_API_KEY=proxy-token, endpoint=${proxyOrigin}`);
|
|
880
|
+
}
|
|
881
|
+
// Generic OpenAI-compatible harnesses: set OPENAI_API_KEY + OPENAI_BASE_URL
|
|
882
|
+
// to route through the proxy for harnesses that use the openai-chat/responses transport.
|
|
883
|
+
if (['codex', 'cursor', 'hermes', 'omp', 'openclaw', 'opencode'].includes(plan.harness)) {
|
|
884
|
+
plan.env['OPENAI_API_KEY'] = proxyRuntime.authToken ?? 'proxy-token';
|
|
885
|
+
plan.env['OPENAI_BASE_URL'] = `${proxyRuntime.url}/v1`;
|
|
886
|
+
plan.env['OPENAI_API_BASE'] = `${proxyRuntime.url}/v1`;
|
|
887
|
+
console.error(`[amux launch] ${plan.harness} proxy: OPENAI_BASE_URL=${proxyRuntime.url}/v1`);
|
|
888
|
+
}
|
|
889
|
+
// Pi ignores OPENAI_BASE_URL — write a models.json config that registers
|
|
890
|
+
// a custom provider pointing to the local proxy.
|
|
891
|
+
if (plan.harness === 'pi') {
|
|
892
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
893
|
+
const { join } = await import('node:path');
|
|
894
|
+
const piConfigDir = process.env['PI_CODING_AGENT_DIR']
|
|
895
|
+
?? join(process.env['HOME'] ?? process.env['USERPROFILE'] ?? '/tmp', '.pi', 'agent');
|
|
896
|
+
mkdirSync(piConfigDir, { recursive: true });
|
|
897
|
+
const modelsConfig = {
|
|
898
|
+
providers: {
|
|
899
|
+
'amux-proxy': {
|
|
900
|
+
baseUrl: `${proxyRuntime.url}/v1`,
|
|
901
|
+
api: 'openai-completions',
|
|
902
|
+
apiKey: proxyRuntime.authToken ?? 'proxy-token',
|
|
903
|
+
models: [{
|
|
904
|
+
id: plan.model,
|
|
905
|
+
reasoning: false,
|
|
906
|
+
input: ['text'],
|
|
907
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
908
|
+
contextWindow: 128000,
|
|
909
|
+
maxTokens: 16384,
|
|
910
|
+
}],
|
|
911
|
+
},
|
|
912
|
+
},
|
|
913
|
+
};
|
|
914
|
+
const modelsPath = join(piConfigDir, 'models.json');
|
|
915
|
+
writeFileSync(modelsPath, JSON.stringify(modelsConfig, null, 2));
|
|
916
|
+
plan.args.push('--provider', 'amux-proxy');
|
|
917
|
+
console.error(`[amux launch] Pi proxy config written to ${modelsPath}, proxy at ${proxyRuntime.url}`);
|
|
918
|
+
}
|
|
388
919
|
}
|
|
389
920
|
catch (err) {
|
|
390
921
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -395,14 +926,62 @@ export async function launchCommand(client, args) {
|
|
|
395
926
|
return ExitCode.GENERAL_ERROR;
|
|
396
927
|
}
|
|
397
928
|
}
|
|
929
|
+
// Cursor: pre-create ~/.cursor/auth.json so cursor-agent skips browser OAuth.
|
|
930
|
+
// Runs outside the proxy block because cursor always needs auth, regardless
|
|
931
|
+
// of whether the proxy was started.
|
|
932
|
+
if (plan.harness === 'cursor') {
|
|
933
|
+
const token = plan.env['CURSOR_API_KEY'] || 'proxy-token';
|
|
934
|
+
plan.env['CURSOR_API_KEY'] = token;
|
|
935
|
+
const { writeFileSync: wf, mkdirSync: md } = await import('node:fs');
|
|
936
|
+
const { join: pj } = await import('node:path');
|
|
937
|
+
const cursorDir = pj(process.env['HOME'] ?? process.env['USERPROFILE'] ?? '/tmp', '.cursor');
|
|
938
|
+
md(cursorDir, { recursive: true });
|
|
939
|
+
const auth = JSON.stringify({ accessToken: token, refreshToken: token, userId: 'ci-proxy', email: 'ci@proxy.local' });
|
|
940
|
+
wf(pj(cursorDir, 'auth.json'), auth);
|
|
941
|
+
wf(pj(cursorDir, 'credentials.json'), auth);
|
|
942
|
+
console.error(`[amux launch] Cursor auth pre-seeded at ${cursorDir}/auth.json`);
|
|
943
|
+
}
|
|
944
|
+
// Bridge hooks: emulate lifecycle hooks when --bridge-hooks is set
|
|
945
|
+
let bridgeHookEmulator;
|
|
946
|
+
if (bridgeHooks) {
|
|
947
|
+
const { BridgeHookEmulator } = await import('./launch-bridge-hooks.js');
|
|
948
|
+
bridgeHookEmulator = new BridgeHookEmulator({
|
|
949
|
+
harness: plan.harness,
|
|
950
|
+
cwd: launchCwd,
|
|
951
|
+
env: plan.env,
|
|
952
|
+
sessionId: flagStr(args.flags, 'session-id'),
|
|
953
|
+
runsDir: plan.env['BABYSITTER_RUNS_DIR'] || undefined,
|
|
954
|
+
verbose: flagBool(args.flags, 'debug') === true,
|
|
955
|
+
});
|
|
956
|
+
await bridgeHookEmulator.emulateSessionStart();
|
|
957
|
+
}
|
|
958
|
+
await prepareHarnessAutomationState(plan.harness, launchCwd, plan.env);
|
|
398
959
|
// Spawn harness
|
|
399
|
-
|
|
400
|
-
let child;
|
|
960
|
+
let child = null;
|
|
401
961
|
let ptyProcess = null;
|
|
962
|
+
let ptyTerminationExpected = false;
|
|
963
|
+
const ptyCleanup = [];
|
|
964
|
+
const capturedOutputChunks = [];
|
|
965
|
+
const completePtyPrompt = () => {
|
|
966
|
+
if (!ptyProcess || ptyTerminationExpected)
|
|
967
|
+
return;
|
|
968
|
+
ptyTerminationExpected = true;
|
|
969
|
+
try {
|
|
970
|
+
ptyProcess.kill('SIGTERM');
|
|
971
|
+
}
|
|
972
|
+
catch { /* */ }
|
|
973
|
+
setTimeout(() => {
|
|
974
|
+
try {
|
|
975
|
+
ptyProcess?.kill('SIGKILL');
|
|
976
|
+
}
|
|
977
|
+
catch { /* */ }
|
|
978
|
+
}, 2000);
|
|
979
|
+
};
|
|
402
980
|
if (isInteractive) {
|
|
403
|
-
//
|
|
981
|
+
// Interactive mode: full TTY passthrough. If a prompt is provided, it's
|
|
982
|
+
// injected as initial stdin after the harness starts (like typing it in).
|
|
404
983
|
try {
|
|
405
|
-
const nodePty =
|
|
984
|
+
const nodePty = await import('node-pty');
|
|
406
985
|
ptyProcess = nodePty.spawn(plan.command, plan.args, {
|
|
407
986
|
name: 'xterm-256color',
|
|
408
987
|
cols: process.stdout.columns || 80,
|
|
@@ -410,8 +989,86 @@ export async function launchCommand(client, args) {
|
|
|
410
989
|
cwd: launchCwd,
|
|
411
990
|
env: { ...process.env, ...plan.env },
|
|
412
991
|
});
|
|
413
|
-
//
|
|
414
|
-
|
|
992
|
+
// End-of-turn detection: parse PTY output through adapter's event system
|
|
993
|
+
let turnDetected = false;
|
|
994
|
+
let lineBuf = '';
|
|
995
|
+
let assembler = null;
|
|
996
|
+
let adapter = null;
|
|
997
|
+
try {
|
|
998
|
+
const core = await import('@a5c-ai/agent-mux-core');
|
|
999
|
+
assembler = new core.StreamAssembler();
|
|
1000
|
+
// Resolve the adapter for this harness to use its parseEvent
|
|
1001
|
+
const adaptersModule = await import('@a5c-ai/agent-mux-adapters');
|
|
1002
|
+
const factory = adaptersModule.getAdapterFactory?.(plan.harness);
|
|
1003
|
+
adapter = factory ? factory() : null;
|
|
1004
|
+
}
|
|
1005
|
+
catch { /* core/adapters not available */ }
|
|
1006
|
+
// Pipe PTY to stdout + feed through event parser for turn detection
|
|
1007
|
+
let interactiveOutputBuf = '';
|
|
1008
|
+
let interactiveApiKeyHandled = false;
|
|
1009
|
+
let interactiveBypassHandled = false;
|
|
1010
|
+
let babysitterSkillFollowupInjected = false;
|
|
1011
|
+
const maybeInjectBabysitterSkillFollowup = (output) => {
|
|
1012
|
+
if (babysitterSkillFollowupInjected || !promptInvokesBabysitterSlashCommand(prompt))
|
|
1013
|
+
return;
|
|
1014
|
+
if (!stripTerminalControl(output).includes('Skill(babysitter:babysit)'))
|
|
1015
|
+
return;
|
|
1016
|
+
babysitterSkillFollowupInjected = true;
|
|
1017
|
+
setTimeout(() => {
|
|
1018
|
+
if (!ptyTerminationExpected) {
|
|
1019
|
+
ptyProcess.write(buildBabysitterSkillFollowupPrompt(prompt));
|
|
1020
|
+
setTimeout(() => ptyProcess.write('\r'), 500);
|
|
1021
|
+
}
|
|
1022
|
+
}, 1000);
|
|
1023
|
+
};
|
|
1024
|
+
ptyProcess.onData((data) => {
|
|
1025
|
+
process.stdout.write(data);
|
|
1026
|
+
interactiveOutputBuf += data;
|
|
1027
|
+
capturedOutputChunks.push(data);
|
|
1028
|
+
// Auto-respond to Claude Code onboarding prompts
|
|
1029
|
+
const stripped = interactiveOutputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
1030
|
+
if (!interactiveApiKeyHandled && stripped.includes('usethisAPIkey')) {
|
|
1031
|
+
interactiveApiKeyHandled = true;
|
|
1032
|
+
setTimeout(() => ptyProcess.write('\x1b[A\r'), 200);
|
|
1033
|
+
}
|
|
1034
|
+
if (!interactiveBypassHandled && stripped.includes('BypassPermissionsmode')) {
|
|
1035
|
+
interactiveBypassHandled = true;
|
|
1036
|
+
setTimeout(() => ptyProcess.write('\x1b[B\r'), 200);
|
|
1037
|
+
}
|
|
1038
|
+
maybeInjectBabysitterSkillFollowup(interactiveOutputBuf);
|
|
1039
|
+
if (!assembler || !adapter || turnDetected)
|
|
1040
|
+
return;
|
|
1041
|
+
// Strip ANSI escapes, then feed lines to the event parser
|
|
1042
|
+
const clean = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
|
|
1043
|
+
lineBuf += clean;
|
|
1044
|
+
let idx;
|
|
1045
|
+
while ((idx = lineBuf.indexOf('\n')) !== -1) {
|
|
1046
|
+
const line = lineBuf.slice(0, idx).replace(/\r$/, '');
|
|
1047
|
+
lineBuf = lineBuf.slice(idx + 1);
|
|
1048
|
+
if (line.length === 0)
|
|
1049
|
+
continue;
|
|
1050
|
+
const assembled = assembler.feed(line);
|
|
1051
|
+
if (assembled === null)
|
|
1052
|
+
continue;
|
|
1053
|
+
try {
|
|
1054
|
+
const ctx = { runId: 'launch', agent: plan.harness, sessionId: undefined, turnIndex: 0, debug: false, outputFormat: 'text', source: 'stdout', assembler, eventCount: 0, lastEventType: null, adapterState: {} };
|
|
1055
|
+
const result = adapter.parseEvent(assembled, ctx);
|
|
1056
|
+
if (result === null)
|
|
1057
|
+
continue;
|
|
1058
|
+
const events = Array.isArray(result) ? result : [result];
|
|
1059
|
+
for (const ev of events) {
|
|
1060
|
+
// Detect turn completion events
|
|
1061
|
+
if (ev.type === 'message_stop' || ev.type === 'turn_end' || ev.type === 'session_end') {
|
|
1062
|
+
turnDetected = true;
|
|
1063
|
+
// Give the harness a moment to flush output, then end the PTY.
|
|
1064
|
+
setTimeout(completePtyPrompt, 1000);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
catch { /* parse error — ignore */ }
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
415
1072
|
if (process.stdin.isTTY) {
|
|
416
1073
|
process.stdin.setRawMode(true);
|
|
417
1074
|
}
|
|
@@ -421,27 +1078,354 @@ export async function launchCommand(client, args) {
|
|
|
421
1078
|
process.stdout.on('resize', () => {
|
|
422
1079
|
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
423
1080
|
});
|
|
1081
|
+
if (prompt && plan.args.some(a => a === prompt)) {
|
|
1082
|
+
let artifactMonitor;
|
|
1083
|
+
artifactMonitor = startPromptArtifactCompletionMonitor({
|
|
1084
|
+
prompt,
|
|
1085
|
+
cwd: launchCwd,
|
|
1086
|
+
onComplete: () => {
|
|
1087
|
+
if (artifactMonitor)
|
|
1088
|
+
clearInterval(artifactMonitor);
|
|
1089
|
+
completePtyPrompt();
|
|
1090
|
+
},
|
|
1091
|
+
});
|
|
1092
|
+
ptyCleanup.push(() => { if (artifactMonitor)
|
|
1093
|
+
clearInterval(artifactMonitor); });
|
|
1094
|
+
const liveStackFallbackTimer = startLiveStackBabysitterPromptFallback({ prompt, cwd: launchCwd, env: { ...process.env, ...plan.env }, onComplete: completePtyPrompt });
|
|
1095
|
+
ptyCleanup.push(() => { if (liveStackFallbackTimer)
|
|
1096
|
+
clearTimeout(liveStackFallbackTimer); });
|
|
1097
|
+
}
|
|
1098
|
+
// Inject prompt after observed onboarding prompts are dismissed.
|
|
1099
|
+
if (prompt && !plan.args.some(a => a === prompt)) {
|
|
1100
|
+
const startedAt = Date.now();
|
|
1101
|
+
let promptInjected = false;
|
|
1102
|
+
let artifactMonitor;
|
|
1103
|
+
const injectPrompt = () => {
|
|
1104
|
+
if (promptInjected)
|
|
1105
|
+
return;
|
|
1106
|
+
promptInjected = true;
|
|
1107
|
+
ptyProcess.write(prompt);
|
|
1108
|
+
setTimeout(() => ptyProcess.write('\r'), 500);
|
|
1109
|
+
artifactMonitor = startPromptArtifactCompletionMonitor({
|
|
1110
|
+
prompt,
|
|
1111
|
+
cwd: launchCwd,
|
|
1112
|
+
onComplete: () => {
|
|
1113
|
+
if (artifactMonitor)
|
|
1114
|
+
clearInterval(artifactMonitor);
|
|
1115
|
+
completePtyPrompt();
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
ptyCleanup.push(() => { if (artifactMonitor)
|
|
1119
|
+
clearInterval(artifactMonitor); });
|
|
1120
|
+
const liveStackFallbackTimer = startLiveStackBabysitterPromptFallback({ prompt, cwd: launchCwd, env: { ...process.env, ...plan.env }, onComplete: completePtyPrompt });
|
|
1121
|
+
ptyCleanup.push(() => { if (liveStackFallbackTimer)
|
|
1122
|
+
clearTimeout(liveStackFallbackTimer); });
|
|
1123
|
+
};
|
|
1124
|
+
const checkAndInject = () => {
|
|
1125
|
+
if (promptInjected)
|
|
1126
|
+
return;
|
|
1127
|
+
if (interactiveOutputBuf.length === 0) {
|
|
1128
|
+
if (Date.now() - startedAt >= 1000)
|
|
1129
|
+
injectPrompt();
|
|
1130
|
+
else
|
|
1131
|
+
setTimeout(checkAndInject, 100);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const s = interactiveOutputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
1135
|
+
if (interactiveApiKeyHandled || interactiveBypassHandled) {
|
|
1136
|
+
setTimeout(injectPrompt, 2000);
|
|
1137
|
+
}
|
|
1138
|
+
else if (s.includes('APIkey') || s.includes('Bypass')) {
|
|
1139
|
+
setTimeout(checkAndInject, 500);
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
setTimeout(injectPrompt, 3000);
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
checkAndInject();
|
|
1146
|
+
}
|
|
424
1147
|
// Create a fake ChildProcess-like for signal handling
|
|
425
1148
|
child = { pid: ptyProcess.pid, kill: (sig) => ptyProcess.kill(sig) };
|
|
426
1149
|
}
|
|
427
1150
|
catch {
|
|
428
|
-
// node-pty not available, fall back to stdio inherit
|
|
1151
|
+
// node-pty not available, fall back to stdio inherit with stdin pipe for prompt injection
|
|
429
1152
|
const { spawn } = await import('node:child_process');
|
|
430
1153
|
child = spawn(plan.command, plan.args, {
|
|
431
|
-
stdio: 'inherit',
|
|
1154
|
+
stdio: prompt ? ['pipe', 'inherit', 'inherit'] : 'inherit',
|
|
432
1155
|
env: { ...process.env, ...plan.env },
|
|
433
1156
|
cwd: launchCwd,
|
|
434
|
-
shell:
|
|
1157
|
+
shell: process.platform === 'win32',
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
else if (bridgeInteractive) {
|
|
1162
|
+
// Bridge-interactive: spawn via PTY like interactive mode, but:
|
|
1163
|
+
// - No human stdin forwarding
|
|
1164
|
+
// - Parse PTY output via adapter for structured events
|
|
1165
|
+
// - Emit events as NDJSON to stdout
|
|
1166
|
+
// - Auto-kill on turn completion
|
|
1167
|
+
// - Buffer PTY output to avoid pipe deadlock (stdout is piped)
|
|
1168
|
+
// Pre-create full Claude Code automation state to skip all onboarding prompts
|
|
1169
|
+
if (plan.harness === 'claude') {
|
|
1170
|
+
await prepareClaudeAutomationState(launchCwd, plan.env);
|
|
1171
|
+
}
|
|
1172
|
+
let nodePty;
|
|
1173
|
+
try {
|
|
1174
|
+
nodePty = await import('node-pty');
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
const msg = '--bridge-interactive requires node-pty but it is not available. Install it with: npm install node-pty';
|
|
1178
|
+
if (jsonMode)
|
|
1179
|
+
printJsonError('SPAWN_ERROR', msg);
|
|
1180
|
+
else
|
|
1181
|
+
printError(msg);
|
|
1182
|
+
return ExitCode.GENERAL_ERROR;
|
|
1183
|
+
}
|
|
1184
|
+
ptyProcess = nodePty.spawn(plan.command, plan.args, {
|
|
1185
|
+
name: 'xterm-256color',
|
|
1186
|
+
cols: 120,
|
|
1187
|
+
rows: 40,
|
|
1188
|
+
cwd: launchCwd,
|
|
1189
|
+
env: { ...process.env, ...plan.env },
|
|
1190
|
+
});
|
|
1191
|
+
// Set up adapter + assembler for parsing PTY output into structured events
|
|
1192
|
+
let assembler = null;
|
|
1193
|
+
let adapter = null;
|
|
1194
|
+
try {
|
|
1195
|
+
const core = await import('@a5c-ai/agent-mux-core');
|
|
1196
|
+
assembler = new core.StreamAssembler();
|
|
1197
|
+
const adaptersModule = await import('@a5c-ai/agent-mux-adapters');
|
|
1198
|
+
const factory = adaptersModule.getAdapterFactory?.(plan.harness);
|
|
1199
|
+
adapter = factory ? factory() : null;
|
|
1200
|
+
}
|
|
1201
|
+
catch { /* core/adapters not available — raw output only */ }
|
|
1202
|
+
/** Emit a bridge event as NDJSON, deferred to avoid blocking PTY callback. */
|
|
1203
|
+
function emitBridgeEvent(event) {
|
|
1204
|
+
const line = JSON.stringify(event) + '\n';
|
|
1205
|
+
setImmediate(() => {
|
|
1206
|
+
try {
|
|
1207
|
+
process.stdout.write(line);
|
|
1208
|
+
}
|
|
1209
|
+
catch { /* stdout closed */ }
|
|
435
1210
|
});
|
|
436
1211
|
}
|
|
1212
|
+
let turnComplete = false;
|
|
1213
|
+
let lineBuf = '';
|
|
1214
|
+
let outputBuf = '';
|
|
1215
|
+
let eventCount = 0;
|
|
1216
|
+
let apiKeyPromptHandled = false;
|
|
1217
|
+
let bypassPromptHandled = false;
|
|
1218
|
+
let babysitterSkillFollowupInjected = false;
|
|
1219
|
+
const maybeInjectBabysitterSkillFollowup = (output) => {
|
|
1220
|
+
if (babysitterSkillFollowupInjected || !promptInvokesBabysitterSlashCommand(prompt))
|
|
1221
|
+
return;
|
|
1222
|
+
if (!stripTerminalControl(output).includes('Skill(babysitter:babysit)'))
|
|
1223
|
+
return;
|
|
1224
|
+
babysitterSkillFollowupInjected = true;
|
|
1225
|
+
setTimeout(() => {
|
|
1226
|
+
if (!ptyTerminationExpected) {
|
|
1227
|
+
ptyProcess.write(buildBabysitterSkillFollowupPrompt(prompt));
|
|
1228
|
+
setTimeout(() => ptyProcess.write('\r'), 500);
|
|
1229
|
+
}
|
|
1230
|
+
}, 1000);
|
|
1231
|
+
};
|
|
1232
|
+
let idleTimer = null;
|
|
1233
|
+
const IDLE_TIMEOUT_MS = 30_000;
|
|
1234
|
+
const harnessesWithEndEvents = new Set(['claude', 'codex', 'gemini', 'opencode']);
|
|
1235
|
+
const useIdleTimeout = !harnessesWithEndEvents.has(plan.harness);
|
|
1236
|
+
const parseCtx = {
|
|
1237
|
+
runId: 'bridge',
|
|
1238
|
+
agent: plan.harness,
|
|
1239
|
+
sessionId: undefined,
|
|
1240
|
+
turnIndex: 0,
|
|
1241
|
+
debug: false,
|
|
1242
|
+
outputFormat: 'text',
|
|
1243
|
+
source: 'stdout',
|
|
1244
|
+
assembler: assembler,
|
|
1245
|
+
eventCount: 0,
|
|
1246
|
+
lastEventType: null,
|
|
1247
|
+
adapterState: {},
|
|
1248
|
+
};
|
|
1249
|
+
ptyProcess.onData((data) => {
|
|
1250
|
+
// Buffer all PTY output — never write synchronously to stdout (pipe deadlock)
|
|
1251
|
+
outputBuf += data;
|
|
1252
|
+
capturedOutputChunks.push(data);
|
|
1253
|
+
// Auto-respond to Claude Code interactive prompts that block automation.
|
|
1254
|
+
// ANSI cursor-move codes replace spaces, so stripped text is concatenated.
|
|
1255
|
+
const stripped = outputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
1256
|
+
if (!apiKeyPromptHandled && stripped.includes('usethisAPIkey')) {
|
|
1257
|
+
apiKeyPromptHandled = true;
|
|
1258
|
+
// Default is "No (recommended)". Send Up arrow + Enter to select "Yes".
|
|
1259
|
+
setTimeout(() => ptyProcess.write('\x1b[A\r'), 200);
|
|
1260
|
+
}
|
|
1261
|
+
if (!bypassPromptHandled && stripped.includes('BypassPermissionsmode')) {
|
|
1262
|
+
bypassPromptHandled = true;
|
|
1263
|
+
// Default is "No, exit". Send Down arrow + Enter to select "Yes, I accept".
|
|
1264
|
+
setTimeout(() => ptyProcess.write('\x1b[B\r'), 200);
|
|
1265
|
+
}
|
|
1266
|
+
maybeInjectBabysitterSkillFollowup(outputBuf);
|
|
1267
|
+
if (!assembler || !adapter || turnComplete)
|
|
1268
|
+
return;
|
|
1269
|
+
// Strip ANSI escapes, then feed lines to the event parser
|
|
1270
|
+
const clean = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
|
|
1271
|
+
lineBuf += clean;
|
|
1272
|
+
let idx;
|
|
1273
|
+
while ((idx = lineBuf.indexOf('\n')) !== -1) {
|
|
1274
|
+
const line = lineBuf.slice(0, idx).replace(/\r$/, '');
|
|
1275
|
+
lineBuf = lineBuf.slice(idx + 1);
|
|
1276
|
+
if (line.length === 0)
|
|
1277
|
+
continue;
|
|
1278
|
+
const assembled = assembler.feed(line);
|
|
1279
|
+
if (assembled === null)
|
|
1280
|
+
continue;
|
|
1281
|
+
try {
|
|
1282
|
+
parseCtx.eventCount = eventCount;
|
|
1283
|
+
const result = adapter.parseEvent(assembled, parseCtx);
|
|
1284
|
+
if (result === null)
|
|
1285
|
+
continue;
|
|
1286
|
+
const events = Array.isArray(result) ? result : [result];
|
|
1287
|
+
for (const ev of events) {
|
|
1288
|
+
eventCount++;
|
|
1289
|
+
parseCtx.lastEventType = ev.type;
|
|
1290
|
+
// Emit as NDJSON bridge event
|
|
1291
|
+
emitBridgeEvent({
|
|
1292
|
+
type: ev.type,
|
|
1293
|
+
timestamp: new Date().toISOString(),
|
|
1294
|
+
data: ev,
|
|
1295
|
+
});
|
|
1296
|
+
// Detect turn completion events — schedule PTY termination
|
|
1297
|
+
if (ev.type === 'message_stop' || ev.type === 'turn_end' || ev.type === 'session_end') {
|
|
1298
|
+
turnComplete = true;
|
|
1299
|
+
if (idleTimer)
|
|
1300
|
+
clearTimeout(idleTimer);
|
|
1301
|
+
setTimeout(completePtyPrompt, 1000);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
// Idle timeout fallback for harnesses without structured end events (Pi).
|
|
1305
|
+
if (useIdleTimeout) {
|
|
1306
|
+
if (idleTimer)
|
|
1307
|
+
clearTimeout(idleTimer);
|
|
1308
|
+
idleTimer = setTimeout(() => {
|
|
1309
|
+
if (!turnComplete && eventCount > 0) {
|
|
1310
|
+
turnComplete = true;
|
|
1311
|
+
completePtyPrompt();
|
|
1312
|
+
}
|
|
1313
|
+
}, IDLE_TIMEOUT_MS);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
catch { /* parse error — ignore */ }
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
// Inject prompt after observed onboarding prompts are dismissed.
|
|
1321
|
+
// If the PTY stays silent, inject after a short startup grace period because
|
|
1322
|
+
// some harnesses wait for input without rendering an initial prompt.
|
|
1323
|
+
if (prompt) {
|
|
1324
|
+
const startedAt = Date.now();
|
|
1325
|
+
let promptInjected = false;
|
|
1326
|
+
let artifactMonitor;
|
|
1327
|
+
const injectPrompt = () => {
|
|
1328
|
+
if (promptInjected)
|
|
1329
|
+
return;
|
|
1330
|
+
promptInjected = true;
|
|
1331
|
+
ptyProcess.write(prompt);
|
|
1332
|
+
setTimeout(() => ptyProcess.write('\r'), 500);
|
|
1333
|
+
artifactMonitor = startPromptArtifactCompletionMonitor({
|
|
1334
|
+
prompt,
|
|
1335
|
+
cwd: launchCwd,
|
|
1336
|
+
onComplete: () => {
|
|
1337
|
+
if (artifactMonitor)
|
|
1338
|
+
clearInterval(artifactMonitor);
|
|
1339
|
+
completePtyPrompt();
|
|
1340
|
+
},
|
|
1341
|
+
});
|
|
1342
|
+
ptyCleanup.push(() => { if (artifactMonitor)
|
|
1343
|
+
clearInterval(artifactMonitor); });
|
|
1344
|
+
const liveStackFallbackTimer = startLiveStackBabysitterPromptFallback({ prompt, cwd: launchCwd, env: { ...process.env, ...plan.env }, onComplete: completePtyPrompt });
|
|
1345
|
+
ptyCleanup.push(() => { if (liveStackFallbackTimer)
|
|
1346
|
+
clearTimeout(liveStackFallbackTimer); });
|
|
1347
|
+
};
|
|
1348
|
+
const checkAndInject = () => {
|
|
1349
|
+
if (promptInjected)
|
|
1350
|
+
return;
|
|
1351
|
+
if (outputBuf.length === 0) {
|
|
1352
|
+
if (Date.now() - startedAt >= 1000)
|
|
1353
|
+
injectPrompt();
|
|
1354
|
+
else
|
|
1355
|
+
setTimeout(checkAndInject, 100);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const stripped = outputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
1359
|
+
if (apiKeyPromptHandled || bypassPromptHandled) {
|
|
1360
|
+
setTimeout(injectPrompt, 2000);
|
|
1361
|
+
}
|
|
1362
|
+
else if (stripped.includes('APIkey') || stripped.includes('Bypass')) {
|
|
1363
|
+
setTimeout(checkAndInject, 500);
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
setTimeout(injectPrompt, 3000);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
checkAndInject();
|
|
1370
|
+
}
|
|
1371
|
+
// Create a fake ChildProcess-like for signal handling
|
|
1372
|
+
child = { pid: ptyProcess.pid, kill: (sig) => ptyProcess.kill(sig) };
|
|
1373
|
+
// On PTY exit, flush remaining buffered text as a final output event
|
|
1374
|
+
const origOnExit = ptyProcess.onExit.bind(ptyProcess);
|
|
1375
|
+
const exitPromise = new Promise((resolve) => {
|
|
1376
|
+
origOnExit(({ exitCode: code }) => {
|
|
1377
|
+
// Flush any remaining output as a final bridge event
|
|
1378
|
+
if (outputBuf.length > 0) {
|
|
1379
|
+
emitBridgeEvent({
|
|
1380
|
+
type: 'output',
|
|
1381
|
+
timestamp: new Date().toISOString(),
|
|
1382
|
+
data: { text: outputBuf },
|
|
1383
|
+
});
|
|
1384
|
+
outputBuf = '';
|
|
1385
|
+
}
|
|
1386
|
+
resolve(code);
|
|
1387
|
+
});
|
|
1388
|
+
});
|
|
1389
|
+
// Store the exit promise so main exit handler can use it
|
|
1390
|
+
child.__bridgeExitPromise = exitPromise;
|
|
437
1391
|
}
|
|
438
1392
|
else {
|
|
1393
|
+
// Non-interactive: plain spawn. Each harness handles non-interactive mode
|
|
1394
|
+
// internally (claude -p, codex exec, gemini --prompt, pi stdin).
|
|
439
1395
|
const { spawn } = await import('node:child_process');
|
|
440
1396
|
child = spawn(plan.command, plan.args, {
|
|
441
|
-
stdio: ['pipe', '
|
|
1397
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
442
1398
|
env: { ...process.env, ...plan.env },
|
|
443
1399
|
cwd: launchCwd,
|
|
444
|
-
shell:
|
|
1400
|
+
shell: process.platform === 'win32',
|
|
1401
|
+
});
|
|
1402
|
+
child.stderr?.on('data', (chunk) => {
|
|
1403
|
+
process.stderr.write(chunk);
|
|
1404
|
+
capturedOutputChunks.push(chunk.toString('utf8'));
|
|
1405
|
+
});
|
|
1406
|
+
// Pipe stdout through + idle-timeout kill for harnesses that don't exit
|
|
1407
|
+
// after completing a non-interactive task (e.g., Pi doesn't exit on its own).
|
|
1408
|
+
// Harnesses with proper exit behavior (claude -p, codex exec) don't need this.
|
|
1409
|
+
const niUseIdleKill = !new Set(['claude', 'codex', 'gemini', 'opencode']).has(plan.harness);
|
|
1410
|
+
let niIdleTimer = null;
|
|
1411
|
+
let niHasOutput = false;
|
|
1412
|
+
const NI_IDLE_TIMEOUT_MS = 30_000;
|
|
1413
|
+
child.stdout?.on('data', (chunk) => {
|
|
1414
|
+
process.stdout.write(chunk);
|
|
1415
|
+
capturedOutputChunks.push(chunk.toString('utf8'));
|
|
1416
|
+
niHasOutput = true;
|
|
1417
|
+
if (niUseIdleKill) {
|
|
1418
|
+
if (niIdleTimer)
|
|
1419
|
+
clearTimeout(niIdleTimer);
|
|
1420
|
+
niIdleTimer = setTimeout(() => {
|
|
1421
|
+
if (niHasOutput) {
|
|
1422
|
+
try {
|
|
1423
|
+
child.kill('SIGTERM');
|
|
1424
|
+
}
|
|
1425
|
+
catch { /* */ }
|
|
1426
|
+
}
|
|
1427
|
+
}, NI_IDLE_TIMEOUT_MS);
|
|
1428
|
+
}
|
|
445
1429
|
});
|
|
446
1430
|
}
|
|
447
1431
|
if (flagBool(args.flags, 'observe')) {
|
|
@@ -472,32 +1456,127 @@ export async function launchCommand(client, args) {
|
|
|
472
1456
|
}
|
|
473
1457
|
catch { /* process may already be dead */ }
|
|
474
1458
|
}
|
|
1459
|
+
else if (ptyProcess) {
|
|
1460
|
+
// PTY child runs in its own session — kill the process group to avoid orphans
|
|
1461
|
+
try {
|
|
1462
|
+
process.kill(-ptyProcess.pid, sig);
|
|
1463
|
+
}
|
|
1464
|
+
catch { /* */ }
|
|
1465
|
+
try {
|
|
1466
|
+
ptyProcess.kill(sig);
|
|
1467
|
+
}
|
|
1468
|
+
catch { /* */ }
|
|
1469
|
+
}
|
|
475
1470
|
else {
|
|
476
1471
|
child.kill(sig);
|
|
477
1472
|
}
|
|
478
1473
|
};
|
|
479
1474
|
process.on('SIGINT', forwardSignal);
|
|
480
1475
|
process.on('SIGTERM', forwardSignal);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
1476
|
+
// Ensure PTY cleanup on exit
|
|
1477
|
+
if (ptyProcess) {
|
|
1478
|
+
process.on('exit', () => { try {
|
|
1479
|
+
ptyProcess.kill('SIGKILL');
|
|
1480
|
+
}
|
|
1481
|
+
catch { /* */ } });
|
|
484
1482
|
}
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
resolve(code);
|
|
491
|
-
});
|
|
1483
|
+
const promptPassedAsFlag = (plan.harness === 'pi' && !isInteractive && plan.args.includes('-p'));
|
|
1484
|
+
if (prompt && child.stdin && !ptyProcess && !promptPassedAsFlag) {
|
|
1485
|
+
child.stdin.write(prompt + '\n');
|
|
1486
|
+
if (!isInteractive) {
|
|
1487
|
+
child.stdin.end();
|
|
492
1488
|
}
|
|
493
1489
|
else {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
1490
|
+
// Interactive with stdin pipe (no PTY): reconnect terminal stdin after prompt injection
|
|
1491
|
+
process.stdin.resume();
|
|
1492
|
+
process.stdin.pipe(child.stdin);
|
|
497
1493
|
}
|
|
498
|
-
}
|
|
1494
|
+
}
|
|
1495
|
+
// Close stdin for harnesses where prompt was passed as a CLI flag (not via stdin)
|
|
1496
|
+
// to prevent the process from hanging waiting for interactive input.
|
|
1497
|
+
if (promptPassedAsFlag && child.stdin && !ptyProcess) {
|
|
1498
|
+
child.stdin.end();
|
|
1499
|
+
}
|
|
1500
|
+
let exitCode = await (child.__bridgeExitPromise
|
|
1501
|
+
? child.__bridgeExitPromise
|
|
1502
|
+
: new Promise((resolve) => {
|
|
1503
|
+
if (ptyProcess) {
|
|
1504
|
+
ptyProcess.onExit(({ exitCode: code }) => {
|
|
1505
|
+
if (process.stdin.isTTY)
|
|
1506
|
+
process.stdin.setRawMode(false);
|
|
1507
|
+
resolve(code);
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
else {
|
|
1511
|
+
child.on('exit', (code, signal) => {
|
|
1512
|
+
resolve(signal ? 128 + (signal === 'SIGINT' ? 2 : 15) : (code ?? 1));
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
}));
|
|
1516
|
+
for (const cleanup of ptyCleanup.splice(0))
|
|
1517
|
+
cleanup();
|
|
1518
|
+
if (ptyTerminationExpected && exitCode !== 0)
|
|
1519
|
+
exitCode = 0;
|
|
1520
|
+
// Output-to-file bridge: write captured output to expected artifact path
|
|
1521
|
+
// for agents without native file-writing tools (Pi, Hermes, etc.)
|
|
1522
|
+
const capturedLen = capturedOutputChunks.reduce((a, c) => a + c.length, 0);
|
|
1523
|
+
console.error(`[amux launch] exit=${exitCode} captured=${capturedLen} chunks=${capturedOutputChunks.length} prompt=${(prompt ?? '').slice(0, 60)}`);
|
|
1524
|
+
if (capturedOutputChunks.length > 0) {
|
|
1525
|
+
const bridgeArtifactPaths = extractPromptArtifactPaths(prompt, launchCwd);
|
|
1526
|
+
console.error(`[amux launch] bridge paths: ${JSON.stringify(bridgeArtifactPaths)}`);
|
|
1527
|
+
if (bridgeArtifactPaths.length > 0) {
|
|
1528
|
+
const rawOutput = capturedOutputChunks.join('');
|
|
1529
|
+
const cleanOutput = rawOutput.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
1530
|
+
if (cleanOutput.length >= 200) {
|
|
1531
|
+
const fsBridge = await import('node:fs/promises');
|
|
1532
|
+
const { dirname: dirnameBridge } = await import('node:path');
|
|
1533
|
+
for (const artifactPath of bridgeArtifactPaths) {
|
|
1534
|
+
try {
|
|
1535
|
+
await fsBridge.access(artifactPath);
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
catch { /* doesn't exist yet */ }
|
|
1539
|
+
await fsBridge.mkdir(dirnameBridge(artifactPath), { recursive: true });
|
|
1540
|
+
await fsBridge.writeFile(artifactPath, cleanOutput);
|
|
1541
|
+
console.error(`[amux launch] Output bridged to ${artifactPath} (${cleanOutput.length} bytes)`);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
console.error(`[amux launch] bridge skipped: cleanOutput too short (${cleanOutput.length} < 200)`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
499
1549
|
process.off('SIGINT', forwardSignal);
|
|
500
1550
|
process.off('SIGTERM', forwardSignal);
|
|
1551
|
+
// Bridge hooks: emulate stop hook and re-spawn if shouldContinue
|
|
1552
|
+
if (bridgeHookEmulator) {
|
|
1553
|
+
let stopResult = await bridgeHookEmulator.emulateStop();
|
|
1554
|
+
while (stopResult.shouldContinue && stopResult.resumeId) {
|
|
1555
|
+
// Re-spawn with --resume to continue the session
|
|
1556
|
+
const resumePlan = { ...plan, args: [...plan.args] };
|
|
1557
|
+
appendHarnessSessionArgs(resumePlan, {
|
|
1558
|
+
resumeId: stopResult.resumeId,
|
|
1559
|
+
interactive: false,
|
|
1560
|
+
});
|
|
1561
|
+
const { spawn: resumeSpawn } = await import('node:child_process');
|
|
1562
|
+
const resumeChild = resumeSpawn(resumePlan.command, resumePlan.args, {
|
|
1563
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
1564
|
+
env: { ...process.env, ...resumePlan.env },
|
|
1565
|
+
cwd: launchCwd,
|
|
1566
|
+
shell: process.platform === 'win32',
|
|
1567
|
+
});
|
|
1568
|
+
if (resumeChild.stdin) {
|
|
1569
|
+
resumeChild.stdin.end();
|
|
1570
|
+
}
|
|
1571
|
+
await new Promise((resolve) => {
|
|
1572
|
+
resumeChild.on('exit', (code, signal) => {
|
|
1573
|
+
resolve(signal ? 128 + (signal === 'SIGINT' ? 2 : 15) : (code ?? 1));
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
stopResult = await bridgeHookEmulator.emulateStop();
|
|
1577
|
+
}
|
|
1578
|
+
await bridgeHookEmulator.emulateSessionEnd();
|
|
1579
|
+
}
|
|
501
1580
|
if (proxyRuntime) {
|
|
502
1581
|
await proxyRuntime.stop();
|
|
503
1582
|
}
|