orquesta-agent 0.2.142 → 0.2.144

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/executor.js CHANGED
@@ -1258,83 +1258,84 @@ export async function execute(options) {
1258
1258
  // orquesta-cli. Holds the upstream error text for logging.
1259
1259
  let claudeApiError = null;
1260
1260
  _activePromptId = promptId;
1261
- // Clear per-execution buffers for new execution (concurrent-safe)
1262
- outputBufferMap.delete(commandId);
1263
- clearDetectedRequirements(commandId);
1264
- clearQABuffer(commandId);
1265
- // Start log persistence for this execution
1266
- startLogPersistence(promptId);
1267
- addLog(promptId, 'info', 'system', `Starting execution: ${content.slice(0, 100)}...`);
1268
- // Native coordination fast-path: if this is a RogerThat rejoin/wake dispatch
1269
- // (carries an <<ORQ_COORD>> spec), run the plumbing in code + one LLM call to
1270
- // draft the reply, instead of making the model orchestrate curls turn-by-turn
1271
- // (slow + fragile on weak models). Falls through to the normal prompt path if
1272
- // the spec is absent or malformed.
1273
- const coordSpec = parseCoordSpec(content);
1274
- if (coordSpec) {
1275
- addLog(promptId, 'info', 'system', `Native coordination (${coordSpec.reason}) — channel ${coordSpec.channelId}`);
1276
- try {
1277
- // The agent token isn't in process.env — it's injected per-execution
1278
- // (index.ts sets ORQUESTA_TOKEN on injectedCredentials before execute()).
1279
- const orquestaToken = getInjectedCredentials()['ORQUESTA_TOKEN'] || process.env.ORQUESTA_TOKEN || '';
1280
- const summary = await runCoordination(coordSpec, orquestaToken, (line) => {
1281
- addLog(promptId, 'info', 'output', line);
1282
- sendOutput(channel, commandId, 'stdout', line + '\n').catch(() => undefined);
1283
- });
1284
- addLog(promptId, 'info', 'system', summary);
1285
- await sendOutput(channel, commandId, 'stdout', `\n${summary}\n`);
1286
- await updatePromptStatus(promptId, 'completed', { text: summary, key_actions: [], files_modified: [] });
1287
- await sendComplete(channel, commandId, 0, startTime);
1261
+ try {
1262
+ // Clear per-execution buffers for new execution (concurrent-safe)
1263
+ outputBufferMap.delete(commandId);
1264
+ clearDetectedRequirements(commandId);
1265
+ clearQABuffer(commandId);
1266
+ // Start log persistence for this execution
1267
+ startLogPersistence(promptId);
1268
+ addLog(promptId, 'info', 'system', `Starting execution: ${content.slice(0, 100)}...`);
1269
+ // Native coordination fast-path: if this is a RogerThat rejoin/wake dispatch
1270
+ // (carries an <<ORQ_COORD>> spec), run the plumbing in code + one LLM call to
1271
+ // draft the reply, instead of making the model orchestrate curls turn-by-turn
1272
+ // (slow + fragile on weak models). Falls through to the normal prompt path if
1273
+ // the spec is absent or malformed.
1274
+ const coordSpec = parseCoordSpec(content);
1275
+ if (coordSpec) {
1276
+ addLog(promptId, 'info', 'system', `Native coordination (${coordSpec.reason}) — channel ${coordSpec.channelId}`);
1277
+ try {
1278
+ // The agent token isn't in process.env — it's injected per-execution
1279
+ // (index.ts sets ORQUESTA_TOKEN on injectedCredentials before execute()).
1280
+ const orquestaToken = getInjectedCredentials()['ORQUESTA_TOKEN'] || process.env.ORQUESTA_TOKEN || '';
1281
+ const summary = await runCoordination(coordSpec, orquestaToken, (line) => {
1282
+ addLog(promptId, 'info', 'output', line);
1283
+ sendOutput(channel, commandId, 'stdout', line + '\n').catch(() => undefined);
1284
+ });
1285
+ addLog(promptId, 'info', 'system', summary);
1286
+ await sendOutput(channel, commandId, 'stdout', `\n${summary}\n`);
1287
+ await updatePromptStatus(promptId, 'completed', { text: summary, key_actions: [], files_modified: [] });
1288
+ await sendComplete(channel, commandId, 0, startTime);
1289
+ }
1290
+ catch (err) {
1291
+ const msg = `Coordination failed: ${err.message}`;
1292
+ addLog(promptId, 'error', 'error', msg);
1293
+ await updatePromptStatus(promptId, 'failed', { text: msg, key_actions: [], files_modified: [] });
1294
+ await sendComplete(channel, commandId, 1, startTime);
1295
+ }
1296
+ return;
1288
1297
  }
1289
- catch (err) {
1290
- const msg = `Coordination failed: ${err.message}`;
1291
- addLog(promptId, 'error', 'error', msg);
1292
- await updatePromptStatus(promptId, 'failed', { text: msg, key_actions: [], files_modified: [] });
1293
- await sendComplete(channel, commandId, 1, startTime);
1298
+ logger.info(`Executing prompt: ${content.slice(0, 60)}${content.length > 60 ? '...' : ''}`);
1299
+ logger.info(`Permission mode: ${mode}`);
1300
+ if (attachments && attachments.length > 0) {
1301
+ logger.info(`Attachments: ${attachments.length} file(s)`);
1294
1302
  }
1295
- return;
1296
- }
1297
- logger.info(`Executing prompt: ${content.slice(0, 60)}${content.length > 60 ? '...' : ''}`);
1298
- logger.info(`Permission mode: ${mode}`);
1299
- if (attachments && attachments.length > 0) {
1300
- logger.info(`Attachments: ${attachments.length} file(s)`);
1301
- }
1302
- // Process attachments if present
1303
- let attachmentContext = '';
1304
- let attachmentCleanup = null;
1305
- let imageReferences = [];
1306
- if (attachments && attachments.length > 0) {
1307
- try {
1308
- const processed = await processAttachments(attachments);
1309
- attachmentContext = processed.contextText;
1310
- attachmentCleanup = processed.cleanup;
1311
- imageReferences = processed.fileReferences;
1312
- logger.info(`Processed ${attachments.length} attachment(s)`);
1303
+ // Process attachments if present
1304
+ let attachmentContext = '';
1305
+ let attachmentCleanup = null;
1306
+ let imageReferences = [];
1307
+ if (attachments && attachments.length > 0) {
1308
+ try {
1309
+ const processed = await processAttachments(attachments);
1310
+ attachmentContext = processed.contextText;
1311
+ attachmentCleanup = processed.cleanup;
1312
+ imageReferences = processed.fileReferences;
1313
+ logger.info(`Processed ${attachments.length} attachment(s)`);
1314
+ }
1315
+ catch (error) {
1316
+ logger.error(`Failed to process attachments: ${error}`);
1317
+ addLog(promptId, 'error', 'system', `Failed to process attachments: ${error}`);
1318
+ }
1313
1319
  }
1314
- catch (error) {
1315
- logger.error(`Failed to process attachments: ${error}`);
1316
- addLog(promptId, 'error', 'system', `Failed to process attachments: ${error}`);
1317
- }
1318
- }
1319
- // Build full prompt with settings, agent instructions, and attachments
1320
- let fullContent = content + attachmentContext;
1321
- const settingsHeader = generateSettingsHeader();
1322
- const browserBlock = BROWSER_TOOLS_PROMPT_BLOCK;
1323
- const coordinationBlock = await fetchCoordinationContextBlock();
1324
- const capabilitiesBlock = `${ORQUESTA_CAPABILITIES_PROMPT_BLOCK}${coordinationBlock}`;
1325
- // Auto-inject any rogerthat phone messages received since the last prompt /
1326
- // interactive turn. Shared state with the interactive path — see
1327
- // readNewRogerthatMessages(). No-op when no listener is active.
1328
- const phoneBlock = readNewRogerthatMessages();
1329
- if (phoneBlock) {
1330
- logger.info(`Injecting ${phoneBlock.length} chars of rogerthat phone messages into prompt`);
1331
- }
1332
- const userRequestBody = phoneBlock
1333
- ? `${phoneBlock}\n${content}${attachmentContext}`
1334
- : `${content}${attachmentContext}`;
1335
- if (agentInstructions && agentInstructions.trim()) {
1336
- // Prepend settings + agent instructions to the prompt
1337
- fullContent = `[Orquesta Configuration]
1320
+ // Build full prompt with settings, agent instructions, and attachments
1321
+ let fullContent = content + attachmentContext;
1322
+ const settingsHeader = generateSettingsHeader();
1323
+ const browserBlock = BROWSER_TOOLS_PROMPT_BLOCK;
1324
+ const coordinationBlock = await fetchCoordinationContextBlock();
1325
+ const capabilitiesBlock = `${ORQUESTA_CAPABILITIES_PROMPT_BLOCK}${coordinationBlock}`;
1326
+ // Auto-inject any rogerthat phone messages received since the last prompt /
1327
+ // interactive turn. Shared state with the interactive path — see
1328
+ // readNewRogerthatMessages(). No-op when no listener is active.
1329
+ const phoneBlock = readNewRogerthatMessages();
1330
+ if (phoneBlock) {
1331
+ logger.info(`Injecting ${phoneBlock.length} chars of rogerthat phone messages into prompt`);
1332
+ }
1333
+ const userRequestBody = phoneBlock
1334
+ ? `${phoneBlock}\n${content}${attachmentContext}`
1335
+ : `${content}${attachmentContext}`;
1336
+ if (agentInstructions && agentInstructions.trim()) {
1337
+ // Prepend settings + agent instructions to the prompt
1338
+ fullContent = `[Orquesta Configuration]
1338
1339
  ${settingsHeader}
1339
1340
  [Agent Instructions from Orquesta]
1340
1341
  ${agentInstructions}
@@ -1345,11 +1346,11 @@ ${browserBlock}
1345
1346
 
1346
1347
  [User Request]
1347
1348
  ${userRequestBody}`;
1348
- logger.info('Including settings and agent instructions in prompt');
1349
- }
1350
- else if (settingsHeader) {
1351
- // Just settings, no agent instructions
1352
- fullContent = `[Orquesta Configuration]
1349
+ logger.info('Including settings and agent instructions in prompt');
1350
+ }
1351
+ else if (settingsHeader) {
1352
+ // Just settings, no agent instructions
1353
+ fullContent = `[Orquesta Configuration]
1353
1354
  ${settingsHeader}
1354
1355
 
1355
1356
  ${capabilitiesBlock}
@@ -1358,692 +1359,699 @@ ${browserBlock}
1358
1359
 
1359
1360
  [User Request]
1360
1361
  ${userRequestBody}`;
1361
- logger.info('Including settings in prompt');
1362
- }
1363
- else {
1364
- // No settings — still include capabilities + browser blocks.
1365
- fullContent = `${capabilitiesBlock}
1362
+ logger.info('Including settings in prompt');
1363
+ }
1364
+ else {
1365
+ // No settings — still include capabilities + browser blocks.
1366
+ fullContent = `${capabilitiesBlock}
1366
1367
 
1367
1368
  ${browserBlock}
1368
1369
 
1369
1370
  [User Request]
1370
1371
  ${userRequestBody}`;
1371
- }
1372
- // Build environment with authentication
1373
- const env = {
1374
- ...process.env,
1375
- ...injectedCredentials, // Inject credentials from Orquesta
1376
- CI: 'true', // Ensure non-interactive mode
1377
- // Expose the current prompt id so Claude can link generated files back
1378
- // to this prompt via /api/agent/files (prompt_id=$ORQUESTA_PROMPT_ID).
1379
- ORQUESTA_PROMPT_ID: promptId,
1380
- };
1381
- // Add API key if configured (overrides injected if present)
1382
- if (anthropicApiKey) {
1383
- env.ANTHROPIC_API_KEY = anthropicApiKey;
1384
- }
1385
- // Strip Claude Code session detection env vars so the spawned claude process
1386
- // doesn't refuse with "nested sessions" when the agent itself runs inside a
1387
- // Claude Code session (local dev or systemd inherits the launching shell's env).
1388
- delete env.CLAUDECODE;
1389
- delete env.CLAUDE_CODE_ENTRYPOINT;
1390
- delete env.CLAUDE_CODE_VERSION;
1391
- delete env.CLAUDE_CODE_SESSION_ID;
1392
- delete env.CLAUDE_CODE_PROJECT_DIR;
1393
- delete env.CLAUDE_CODE_HEADLESS;
1394
- // Select CLI based on availability and preference. _forceCli overrides
1395
- // this on the claude→orquesta failure-retry path.
1396
- let { cli: selectedCli, reason } = selectCli();
1397
- if (options._forceCli && (options._forceCli === 'orquesta' ? isOrquestaCliAvailable() : isClaudeCliAvailable())) {
1398
- selectedCli = options._forceCli;
1399
- reason = `Forced: ${options._forceCli} (retry after claude failure)`;
1400
- }
1401
- if (!selectedCli) {
1402
- throw new Error('No CLI available. Please install either orquesta-cli or claude CLI.');
1403
- }
1404
- // Preflight check: if orquesta CLI is selected, verify its LLM endpoint works
1405
- // This prevents wasting time on prompts that will fail with 404s
1406
- if (selectedCli === 'orquesta') {
1407
- try {
1408
- const testResult = execSync('orquesta --status 2>&1 || true', { encoding: 'utf-8', timeout: 5000, env });
1409
- // If orquesta-cli reports no endpoint or connection issues, fall back to claude
1410
- if (testResult.includes('No endpoint') || testResult.includes('not configured') || testResult.includes('No LLM')) {
1411
- const hasClaude = isClaudeCliAvailable();
1412
- if (hasClaude) {
1413
- logger.warn('Orquesta CLI has no LLM endpoint configured — falling back to Claude CLI');
1414
- selectedCli = 'claude';
1415
- reason = 'Fallback: orquesta has no LLM endpoint';
1372
+ }
1373
+ // Build environment with authentication
1374
+ const env = {
1375
+ ...process.env,
1376
+ ...injectedCredentials, // Inject credentials from Orquesta
1377
+ CI: 'true', // Ensure non-interactive mode
1378
+ // Expose the current prompt id so Claude can link generated files back
1379
+ // to this prompt via /api/agent/files (prompt_id=$ORQUESTA_PROMPT_ID).
1380
+ ORQUESTA_PROMPT_ID: promptId,
1381
+ };
1382
+ // Add API key if configured (overrides injected if present)
1383
+ if (anthropicApiKey) {
1384
+ env.ANTHROPIC_API_KEY = anthropicApiKey;
1385
+ }
1386
+ // Strip Claude Code session detection env vars so the spawned claude process
1387
+ // doesn't refuse with "nested sessions" when the agent itself runs inside a
1388
+ // Claude Code session (local dev or systemd inherits the launching shell's env).
1389
+ delete env.CLAUDECODE;
1390
+ delete env.CLAUDE_CODE_ENTRYPOINT;
1391
+ delete env.CLAUDE_CODE_VERSION;
1392
+ delete env.CLAUDE_CODE_SESSION_ID;
1393
+ delete env.CLAUDE_CODE_PROJECT_DIR;
1394
+ delete env.CLAUDE_CODE_HEADLESS;
1395
+ // Select CLI based on availability and preference. _forceCli overrides
1396
+ // this on the claude→orquesta failure-retry path.
1397
+ let { cli: selectedCli, reason } = selectCli();
1398
+ if (options._forceCli && (options._forceCli === 'orquesta' ? isOrquestaCliAvailable() : isClaudeCliAvailable())) {
1399
+ selectedCli = options._forceCli;
1400
+ reason = `Forced: ${options._forceCli} (retry after claude failure)`;
1401
+ }
1402
+ if (!selectedCli) {
1403
+ throw new Error('No CLI available. Please install either orquesta-cli or claude CLI.');
1404
+ }
1405
+ // Preflight check: if orquesta CLI is selected, verify its LLM endpoint works
1406
+ // This prevents wasting time on prompts that will fail with 404s
1407
+ if (selectedCli === 'orquesta') {
1408
+ try {
1409
+ const testResult = execSync('orquesta --status 2>&1 || true', { encoding: 'utf-8', timeout: 5000, env });
1410
+ // If orquesta-cli reports no endpoint or connection issues, fall back to claude
1411
+ if (testResult.includes('No endpoint') || testResult.includes('not configured') || testResult.includes('No LLM')) {
1412
+ const hasClaude = isClaudeCliAvailable();
1413
+ if (hasClaude) {
1414
+ logger.warn('Orquesta CLI has no LLM endpoint configured — falling back to Claude CLI');
1415
+ selectedCli = 'claude';
1416
+ reason = 'Fallback: orquesta has no LLM endpoint';
1417
+ }
1416
1418
  }
1417
1419
  }
1420
+ catch {
1421
+ // --status not supported or timeout, skip check
1422
+ }
1418
1423
  }
1419
- catch {
1420
- // --status not supported or timeout, skip check
1424
+ const cliCommand = selectedCli;
1425
+ const cwd = workingDirectory || process.cwd();
1426
+ logger.info(`CLI Selection: ${cliCommand} (${reason})`);
1427
+ logger.info(`Spawning: ${cliCommand} -p "${fullContent.slice(0, 50)}..." in ${cwd}`);
1428
+ // Escape content for shell - replace single quotes
1429
+ const escapedContent = fullContent.replace(/'/g, "'\\''");
1430
+ // Build command based on permission mode
1431
+ // Use stream-json format to get structured output including thinking
1432
+ let command;
1433
+ // Different flags for different CLIs
1434
+ // Claude CLI supports --output-format stream-json
1435
+ // Orquesta CLI doesn't need special format flags - it outputs directly
1436
+ const baseFlags = cliCommand === 'orquesta'
1437
+ ? '--verbose' // Orquesta: simple verbose output
1438
+ : '--verbose --output-format stream-json'; // Claude: stream-json format
1439
+ const permFlags = mode === 'auto' ? '--dangerously-skip-permissions' : '';
1440
+ // Claude CLI's headless `-p` mode does not accept positional file args as
1441
+ // attachments — image refs are surfaced inline in the prompt as absolute
1442
+ // paths so Claude's Read tool can pick them up.
1443
+ command = `${cliCommand} -p '${escapedContent}' ${permFlags} ${baseFlags}`;
1444
+ if (imageReferences.length > 0) {
1445
+ logger.info(`Image attachments referenced inline: ${imageReferences.length}`);
1446
+ }
1447
+ // Windows has no `script` binary; macOS BSD `script` requires a parent TTY
1448
+ // (which the agent daemon doesn't have, causing tcgetattr/ioctl failure).
1449
+ // Direct-spawn the CLI on those platforms — Claude CLI's `-p` stream-json
1450
+ // mode doesn't need a PTY. Linux util-linux `script` works without a parent
1451
+ // TTY, so we keep PTY wrapping there.
1452
+ const isWindows = process.platform === 'win32';
1453
+ const isDarwin = process.platform === 'darwin';
1454
+ let claude;
1455
+ if (isWindows || isDarwin) {
1456
+ const args = [];
1457
+ args.push('-p', fullContent);
1458
+ if (permFlags)
1459
+ args.push(...permFlags.split(/\s+/).filter(Boolean));
1460
+ if (baseFlags)
1461
+ args.push(...baseFlags.split(/\s+/).filter(Boolean));
1462
+ claude = spawn(cliCommand, args, {
1463
+ cwd,
1464
+ env,
1465
+ stdio: ['pipe', 'pipe', 'pipe'],
1466
+ shell: isWindows, // Windows needs shell:true for .cmd shim; macOS spawns directly
1467
+ });
1421
1468
  }
1422
- }
1423
- const cliCommand = selectedCli;
1424
- const cwd = workingDirectory || process.cwd();
1425
- logger.info(`CLI Selection: ${cliCommand} (${reason})`);
1426
- logger.info(`Spawning: ${cliCommand} -p "${fullContent.slice(0, 50)}..." in ${cwd}`);
1427
- // Escape content for shell - replace single quotes
1428
- const escapedContent = fullContent.replace(/'/g, "'\\''");
1429
- // Build command based on permission mode
1430
- // Use stream-json format to get structured output including thinking
1431
- let command;
1432
- // Different flags for different CLIs
1433
- // Claude CLI supports --output-format stream-json
1434
- // Orquesta CLI doesn't need special format flags - it outputs directly
1435
- const baseFlags = cliCommand === 'orquesta'
1436
- ? '--verbose' // Orquesta: simple verbose output
1437
- : '--verbose --output-format stream-json'; // Claude: stream-json format
1438
- const permFlags = mode === 'auto' ? '--dangerously-skip-permissions' : '';
1439
- // Claude CLI's headless `-p` mode does not accept positional file args as
1440
- // attachments — image refs are surfaced inline in the prompt as absolute
1441
- // paths so Claude's Read tool can pick them up.
1442
- command = `${cliCommand} -p '${escapedContent}' ${permFlags} ${baseFlags}`;
1443
- if (imageReferences.length > 0) {
1444
- logger.info(`Image attachments referenced inline: ${imageReferences.length}`);
1445
- }
1446
- // Windows has no `script` binary; macOS BSD `script` requires a parent TTY
1447
- // (which the agent daemon doesn't have, causing tcgetattr/ioctl failure).
1448
- // Direct-spawn the CLI on those platforms — Claude CLI's `-p` stream-json
1449
- // mode doesn't need a PTY. Linux util-linux `script` works without a parent
1450
- // TTY, so we keep PTY wrapping there.
1451
- const isWindows = process.platform === 'win32';
1452
- const isDarwin = process.platform === 'darwin';
1453
- let claude;
1454
- if (isWindows || isDarwin) {
1455
- const args = [];
1456
- args.push('-p', fullContent);
1457
- if (permFlags)
1458
- args.push(...permFlags.split(/\s+/).filter(Boolean));
1459
- if (baseFlags)
1460
- args.push(...baseFlags.split(/\s+/).filter(Boolean));
1461
- claude = spawn(cliCommand, args, {
1462
- cwd,
1463
- env,
1464
- stdio: ['pipe', 'pipe', 'pipe'],
1465
- shell: isWindows, // Windows needs shell:true for .cmd shim; macOS spawns directly
1466
- });
1467
- }
1468
- else {
1469
- // Sandbox wraps the `script` invocation in bwrap when active (Linux only);
1470
- // no-op otherwise.
1471
- const { file: spawnFile, args: spawnArgs } = sandboxArgv('script', scriptWrapArgs(command), cwd);
1472
- if (spawnFile === 'bwrap') {
1473
- logger.info(`Sandbox active — confining writes to ${cwd}`);
1469
+ else {
1470
+ // Sandbox wraps the `script` invocation in bwrap when active (Linux only);
1471
+ // no-op otherwise.
1472
+ const { file: spawnFile, args: spawnArgs } = sandboxArgv('script', scriptWrapArgs(command), cwd);
1473
+ if (spawnFile === 'bwrap') {
1474
+ logger.info(`Sandbox active confining writes to ${cwd}`);
1475
+ }
1476
+ claude = spawn(spawnFile, spawnArgs, {
1477
+ cwd,
1478
+ env: sandboxEnv(env),
1479
+ stdio: ['pipe', 'pipe', 'pipe'],
1480
+ });
1474
1481
  }
1475
- claude = spawn(spawnFile, spawnArgs, {
1476
- cwd,
1477
- env: sandboxEnv(env),
1478
- stdio: ['pipe', 'pipe', 'pipe'],
1479
- });
1480
- }
1481
- logger.info(`Process spawned with PID: ${claude.pid}`);
1482
- // Track process for cancellation
1483
- runningProcesses.set(commandId, claude);
1484
- // Also map promptId -> commandId so cancel can find by either ID
1485
- if (promptId) {
1486
- promptIdToCommandId.set(promptId, commandId);
1487
- }
1488
- // Buffer for incomplete JSON lines
1489
- let jsonBuffer = '';
1490
- // Track current tool use for pairing with results
1491
- let currentToolUseId = null;
1492
- let currentToolName = null;
1493
- // Track streaming content for complete log entries
1494
- let thinkingBuffer = '';
1495
- let textBuffer = '';
1496
- let toolInputBuffer = '';
1497
- // Parse stream-json output format
1498
- // Returns { output: string, hadValidJson: boolean, logs: AgentLogEntry[] }
1499
- const parseStreamJson = (text) => {
1500
- jsonBuffer += text;
1501
- let output = '';
1502
- let hadValidJson = false;
1503
- const logs = [];
1504
- // Split by newlines and process complete JSON objects
1505
- const lines = jsonBuffer.split('\n');
1506
- jsonBuffer = lines.pop() || ''; // Keep incomplete line in buffer
1507
- for (const line of lines) {
1508
- if (!line.trim())
1509
- continue;
1510
- try {
1511
- const json = JSON.parse(line);
1512
- hadValidJson = true; // Successfully parsed JSON
1513
- // Handle Orquesta CLI eval format (has 'event' field)
1514
- if (json.event) {
1515
- if (json.event === 'start') {
1516
- output += `\n🎯 Starting task...\n`;
1517
- logs.push(createSystemLog('init', 'Task started', 'info', promptId, json.data));
1518
- }
1519
- else if (json.event === 'todo') {
1520
- const { action, id, title } = json.data;
1521
- if (action === 'created') {
1522
- output += `\n📝 Task ${id}: ${title}\n`;
1482
+ logger.info(`Process spawned with PID: ${claude.pid}`);
1483
+ // Track process for cancellation
1484
+ runningProcesses.set(commandId, claude);
1485
+ // Also map promptId -> commandId so cancel can find by either ID
1486
+ if (promptId) {
1487
+ promptIdToCommandId.set(promptId, commandId);
1488
+ }
1489
+ // Buffer for incomplete JSON lines
1490
+ let jsonBuffer = '';
1491
+ // Track current tool use for pairing with results
1492
+ let currentToolUseId = null;
1493
+ let currentToolName = null;
1494
+ // Track streaming content for complete log entries
1495
+ let thinkingBuffer = '';
1496
+ let textBuffer = '';
1497
+ let toolInputBuffer = '';
1498
+ // Parse stream-json output format
1499
+ // Returns { output: string, hadValidJson: boolean, logs: AgentLogEntry[] }
1500
+ const parseStreamJson = (text) => {
1501
+ jsonBuffer += text;
1502
+ let output = '';
1503
+ let hadValidJson = false;
1504
+ const logs = [];
1505
+ // Split by newlines and process complete JSON objects
1506
+ const lines = jsonBuffer.split('\n');
1507
+ jsonBuffer = lines.pop() || ''; // Keep incomplete line in buffer
1508
+ for (const line of lines) {
1509
+ if (!line.trim())
1510
+ continue;
1511
+ try {
1512
+ const json = JSON.parse(line);
1513
+ hadValidJson = true; // Successfully parsed JSON
1514
+ // Handle Orquesta CLI eval format (has 'event' field)
1515
+ if (json.event) {
1516
+ if (json.event === 'start') {
1517
+ output += `\n🎯 Starting task...\n`;
1518
+ logs.push(createSystemLog('init', 'Task started', 'info', promptId, json.data));
1523
1519
  }
1524
- else if (action === 'started') {
1525
- output += `\n▶️ Working on: ${title}\n`;
1520
+ else if (json.event === 'todo') {
1521
+ const { action, id, title } = json.data;
1522
+ if (action === 'created') {
1523
+ output += `\n📝 Task ${id}: ${title}\n`;
1524
+ }
1525
+ else if (action === 'started') {
1526
+ output += `\n▶️ Working on: ${title}\n`;
1527
+ }
1528
+ else if (action === 'completed') {
1529
+ output += `\n✅ Completed: ${title}\n`;
1530
+ }
1531
+ else if (action === 'failed') {
1532
+ output += `\n❌ Failed: ${title}\n`;
1533
+ }
1526
1534
  }
1527
- else if (action === 'completed') {
1528
- output += `\n✅ Completed: ${title}\n`;
1535
+ else if (json.event === 'tool_call') {
1536
+ const { tool, args } = json.data;
1537
+ output += `\n🔧 Tool: ${tool}\n`;
1538
+ const argsStr = JSON.stringify(args, null, 2);
1539
+ if (argsStr.length < 500) {
1540
+ output += `${argsStr}\n`;
1541
+ }
1542
+ logs.push(createToolCallLog(tool, args, 'info', promptId));
1543
+ currentToolName = tool;
1529
1544
  }
1530
- else if (action === 'failed') {
1531
- output += `\n❌ Failed: ${title}\n`;
1545
+ else if (json.event === 'tool_result') {
1546
+ const { tool, success, result, error } = json.data;
1547
+ if (success && result) {
1548
+ const preview = result.slice(0, 1000);
1549
+ output += `\n📋 Result: ${preview}${result.length > 1000 ? '...' : ''}\n`;
1550
+ logs.push(createToolResultLog(tool, true, result.slice(0, 5000), undefined, 'success', promptId));
1551
+ }
1552
+ else if (error) {
1553
+ output += `\n❌ Error: ${error}\n`;
1554
+ logs.push(createToolResultLog(tool, false, undefined, error, 'error', promptId));
1555
+ }
1532
1556
  }
1533
- }
1534
- else if (json.event === 'tool_call') {
1535
- const { tool, args } = json.data;
1536
- output += `\n🔧 Tool: ${tool}\n`;
1537
- const argsStr = JSON.stringify(args, null, 2);
1538
- if (argsStr.length < 500) {
1539
- output += `${argsStr}\n`;
1557
+ else if (json.event === 'response') {
1558
+ const { content } = json.data;
1559
+ if (content) {
1560
+ output += `\n${content}\n`;
1561
+ logs.push(createOutputLog(content, 'markdown', 'info', promptId));
1562
+ }
1540
1563
  }
1541
- logs.push(createToolCallLog(tool, args, 'info', promptId));
1542
- currentToolName = tool;
1564
+ else if (json.event === 'end') {
1565
+ const { success, duration_ms, usage } = json.data;
1566
+ const duration = (duration_ms / 1000).toFixed(1);
1567
+ output += `\n${success ? '✅' : '❌'} Completed in ${duration}s\n`;
1568
+ if (usage) {
1569
+ output += `📊 Tokens: ${usage.total_tokens || 0}\n`;
1570
+ }
1571
+ logs.push(createSystemLog('complete', `Execution ${success ? 'succeeded' : 'failed'}`, success ? 'success' : 'error', promptId, json.data));
1572
+ }
1573
+ // Continue to next line - don't process as Claude format
1574
+ continue;
1543
1575
  }
1544
- else if (json.event === 'tool_result') {
1545
- const { tool, success, result, error } = json.data;
1546
- if (success && result) {
1547
- const preview = result.slice(0, 1000);
1548
- output += `\n📋 Result: ${preview}${result.length > 1000 ? '...' : ''}\n`;
1549
- logs.push(createToolResultLog(tool, true, result.slice(0, 5000), undefined, 'success', promptId));
1576
+ // Handle different message types from Claude CLI stream-json
1577
+ if (json.type === 'assistant') {
1578
+ // Assistant message with content blocks
1579
+ if (json.message?.content) {
1580
+ for (const block of json.message.content) {
1581
+ if (block.type === 'thinking') {
1582
+ output += `\n💭 Thinking:\n${block.thinking}\n`;
1583
+ // Create structured thinking log
1584
+ logs.push(createThinkingLog(block.thinking, 'info', promptId));
1585
+ }
1586
+ else if (block.type === 'text') {
1587
+ output += block.text;
1588
+ // Create structured output log
1589
+ if (block.text.trim()) {
1590
+ logs.push(createOutputLog(block.text, 'markdown', 'info', promptId));
1591
+ }
1592
+ }
1593
+ else if (block.type === 'tool_use') {
1594
+ output += `\n🔧 Tool: ${block.name}\n`;
1595
+ const inputStr = block.input
1596
+ ? (typeof block.input === 'string' ? block.input : JSON.stringify(block.input, null, 2))
1597
+ : '';
1598
+ if (inputStr) {
1599
+ output += `${inputStr.slice(0, 500)}${inputStr.length > 500 ? '...' : ''}\n`;
1600
+ }
1601
+ // Create structured tool call log
1602
+ logs.push(createToolCallLog(block.name, block.input || {}, 'info', promptId, block.id));
1603
+ currentToolUseId = block.id;
1604
+ currentToolName = block.name;
1605
+ // Track file modifications for memory summaries
1606
+ const toolInput = block.input;
1607
+ if (toolInput && typeof toolInput.file_path === 'string') {
1608
+ if (['Edit', 'Write', 'NotebookEdit'].includes(block.name)) {
1609
+ trackFileModified(promptId, toolInput.file_path);
1610
+ }
1611
+ }
1612
+ }
1613
+ else if (block.type === 'tool_result') {
1614
+ const content = typeof block.content === 'string'
1615
+ ? block.content
1616
+ : JSON.stringify(block.content);
1617
+ output += `\n📋 Result:\n${content.slice(0, 1000)}${content.length > 1000 ? '...' : ''}\n`;
1618
+ // Create structured tool result log
1619
+ const isError = block.is_error === true;
1620
+ logs.push(createToolResultLog(currentToolName || 'Unknown', !isError, isError ? undefined : content.slice(0, 5000), isError ? content : undefined, isError ? 'error' : 'success', promptId, block.tool_use_id));
1621
+ }
1622
+ }
1550
1623
  }
1551
- else if (error) {
1552
- output += `\n❌ Error: ${error}\n`;
1553
- logs.push(createToolResultLog(tool, false, undefined, error, 'error', promptId));
1624
+ }
1625
+ else if (json.type === 'user') {
1626
+ // User message (tool results from Claude CLI)
1627
+ if (json.message?.content) {
1628
+ for (const block of json.message.content) {
1629
+ if (block.type === 'tool_result') {
1630
+ const content = typeof block.content === 'string'
1631
+ ? block.content
1632
+ : JSON.stringify(block.content);
1633
+ // Show tool results but keep them concise
1634
+ if (content.length > 50) {
1635
+ output += `\n📋 ${content.slice(0, 800)}${content.length > 800 ? '...' : ''}\n`;
1636
+ }
1637
+ // Create structured tool result log
1638
+ const isError = block.is_error === true;
1639
+ logs.push(createToolResultLog(currentToolName || 'Unknown', !isError, isError ? undefined : content.slice(0, 5000), isError ? content : undefined, isError ? 'error' : 'success', promptId, block.tool_use_id));
1640
+ }
1641
+ }
1554
1642
  }
1555
1643
  }
1556
- else if (json.event === 'response') {
1557
- const { content } = json.data;
1558
- if (content) {
1559
- output += `\n${content}\n`;
1560
- logs.push(createOutputLog(content, 'markdown', 'info', promptId));
1644
+ else if (json.type === 'content_block_delta') {
1645
+ // Streaming content delta - accumulate in buffers
1646
+ if (json.delta?.type === 'thinking_delta') {
1647
+ const thinking = json.delta.thinking || '';
1648
+ output += thinking;
1649
+ thinkingBuffer += thinking;
1650
+ }
1651
+ else if (json.delta?.type === 'text_delta') {
1652
+ const text = json.delta.text || '';
1653
+ output += text;
1654
+ textBuffer += text;
1655
+ }
1656
+ else if (json.delta?.type === 'input_json_delta') {
1657
+ // Tool input streaming
1658
+ const partialJson = json.delta.partial_json || '';
1659
+ output += partialJson;
1660
+ toolInputBuffer += partialJson;
1561
1661
  }
1562
1662
  }
1563
- else if (json.event === 'end') {
1564
- const { success, duration_ms, usage } = json.data;
1565
- const duration = (duration_ms / 1000).toFixed(1);
1566
- output += `\n${success ? '✅' : '❌'} Completed in ${duration}s\n`;
1567
- if (usage) {
1568
- output += `📊 Tokens: ${usage.total_tokens || 0}\n`;
1663
+ else if (json.type === 'content_block_start') {
1664
+ // Start of a content block - reset buffers
1665
+ if (json.content_block?.type === 'thinking') {
1666
+ output += '\n💭 Thinking:\n';
1667
+ thinkingBuffer = '';
1668
+ }
1669
+ else if (json.content_block?.type === 'tool_use') {
1670
+ output += `\n🔧 Tool: ${json.content_block.name || 'unknown'}\n`;
1671
+ currentToolName = json.content_block.name;
1672
+ currentToolUseId = json.content_block.id;
1673
+ toolInputBuffer = '';
1674
+ }
1675
+ else if (json.content_block?.type === 'text') {
1676
+ textBuffer = '';
1569
1677
  }
1570
- logs.push(createSystemLog('complete', `Execution ${success ? 'succeeded' : 'failed'}`, success ? 'success' : 'error', promptId, json.data));
1571
1678
  }
1572
- // Continue to next line - don't process as Claude format
1573
- continue;
1574
- }
1575
- // Handle different message types from Claude CLI stream-json
1576
- if (json.type === 'assistant') {
1577
- // Assistant message with content blocks
1578
- if (json.message?.content) {
1579
- for (const block of json.message.content) {
1580
- if (block.type === 'thinking') {
1581
- output += `\n💭 Thinking:\n${block.thinking}\n`;
1582
- // Create structured thinking log
1583
- logs.push(createThinkingLog(block.thinking, 'info', promptId));
1584
- }
1585
- else if (block.type === 'text') {
1586
- output += block.text;
1587
- // Create structured output log
1588
- if (block.text.trim()) {
1589
- logs.push(createOutputLog(block.text, 'markdown', 'info', promptId));
1590
- }
1591
- }
1592
- else if (block.type === 'tool_use') {
1593
- output += `\n🔧 Tool: ${block.name}\n`;
1594
- const inputStr = block.input
1595
- ? (typeof block.input === 'string' ? block.input : JSON.stringify(block.input, null, 2))
1596
- : '';
1597
- if (inputStr) {
1598
- output += `${inputStr.slice(0, 500)}${inputStr.length > 500 ? '...' : ''}\n`;
1599
- }
1600
- // Create structured tool call log
1601
- logs.push(createToolCallLog(block.name, block.input || {}, 'info', promptId, block.id));
1602
- currentToolUseId = block.id;
1603
- currentToolName = block.name;
1679
+ else if (json.type === 'content_block_stop') {
1680
+ // Block ended - emit structured logs from buffers
1681
+ output += '\n';
1682
+ if (thinkingBuffer.trim()) {
1683
+ logs.push(createThinkingLog(thinkingBuffer, 'info', promptId));
1684
+ thinkingBuffer = '';
1685
+ }
1686
+ if (textBuffer.trim()) {
1687
+ logs.push(createOutputLog(textBuffer, 'markdown', 'info', promptId));
1688
+ textBuffer = '';
1689
+ }
1690
+ if (toolInputBuffer.trim() && currentToolName) {
1691
+ try {
1692
+ const parsedInput = JSON.parse(toolInputBuffer);
1693
+ logs.push(createToolCallLog(currentToolName, parsedInput, 'info', promptId, currentToolUseId || undefined));
1604
1694
  // Track file modifications for memory summaries
1605
- const toolInput = block.input;
1606
- if (toolInput && typeof toolInput.file_path === 'string') {
1607
- if (['Edit', 'Write', 'NotebookEdit'].includes(block.name)) {
1608
- trackFileModified(promptId, toolInput.file_path);
1695
+ if (parsedInput && typeof parsedInput.file_path === 'string') {
1696
+ if (['Edit', 'Write', 'NotebookEdit'].includes(currentToolName)) {
1697
+ trackFileModified(promptId, parsedInput.file_path);
1609
1698
  }
1610
1699
  }
1611
1700
  }
1612
- else if (block.type === 'tool_result') {
1613
- const content = typeof block.content === 'string'
1614
- ? block.content
1615
- : JSON.stringify(block.content);
1616
- output += `\n📋 Result:\n${content.slice(0, 1000)}${content.length > 1000 ? '...' : ''}\n`;
1617
- // Create structured tool result log
1618
- const isError = block.is_error === true;
1619
- logs.push(createToolResultLog(currentToolName || 'Unknown', !isError, isError ? undefined : content.slice(0, 5000), isError ? content : undefined, isError ? 'error' : 'success', promptId, block.tool_use_id));
1701
+ catch {
1702
+ // Invalid JSON input - emit as-is
1703
+ logs.push(createToolCallLog(currentToolName, { raw: toolInputBuffer }, 'info', promptId, currentToolUseId || undefined));
1620
1704
  }
1705
+ toolInputBuffer = '';
1621
1706
  }
1622
1707
  }
1623
- }
1624
- else if (json.type === 'user') {
1625
- // User message (tool results from Claude CLI)
1626
- if (json.message?.content) {
1627
- for (const block of json.message.content) {
1628
- if (block.type === 'tool_result') {
1629
- const content = typeof block.content === 'string'
1630
- ? block.content
1631
- : JSON.stringify(block.content);
1632
- // Show tool results but keep them concise
1633
- if (content.length > 50) {
1634
- output += `\n📋 ${content.slice(0, 800)}${content.length > 800 ? '...' : ''}\n`;
1635
- }
1636
- // Create structured tool result log
1637
- const isError = block.is_error === true;
1638
- logs.push(createToolResultLog(currentToolName || 'Unknown', !isError, isError ? undefined : content.slice(0, 5000), isError ? content : undefined, isError ? 'error' : 'success', promptId, block.tool_use_id));
1708
+ else if (json.type === 'result') {
1709
+ // Final result from Claude CLI.
1710
+ //
1711
+ // Claude reports an upstream account/API failure here by exiting 0
1712
+ // with is_error set and the error string as `result` (e.g. "API
1713
+ // Error: 400 This organization has been disabled."). Treating that
1714
+ // as a successful result is wrong — the prompt silently
1715
+ // "completes" with the error text. Detect it and flag for the
1716
+ // claude→orquesta fallback in the close handler instead of emitting
1717
+ // it as success.
1718
+ const apiErrStatus = typeof json.api_error_status === 'number' ? json.api_error_status : 0;
1719
+ const isAccountFailure = json.is_error === true &&
1720
+ (apiErrStatus >= 400 || CLAUDE_PROVIDER_FATAL_PATTERN.test(json.result || ''));
1721
+ if (isAccountFailure && selectedCli === 'claude') {
1722
+ claudeApiError = json.result || `claude api_error_status ${apiErrStatus}`;
1723
+ // Don't emit the error as a success result; the close handler
1724
+ // logs the failure + the orquesta-cli fallback.
1725
+ }
1726
+ else {
1727
+ if (json.result) {
1728
+ output += `\n✅ ${json.result}\n`;
1729
+ }
1730
+ if (json.cost_usd) {
1731
+ output += `💰 Cost: $${json.cost_usd.toFixed(4)}\n`;
1732
+ }
1733
+ // Track usage for persistence to DB
1734
+ if (json.total_tokens) {
1735
+ promptTokensUsed.set(promptId, json.total_tokens);
1736
+ }
1737
+ if (json.cost_usd) {
1738
+ promptCostCents.set(promptId, Math.round(json.cost_usd * 100));
1639
1739
  }
1740
+ // Emit system log for completion
1741
+ logs.push(createSystemLog('complete', json.result || 'Execution completed', 'success', promptId, {
1742
+ cost_cents: json.cost_usd ? Math.round(json.cost_usd * 100) : undefined,
1743
+ tokens_used: json.total_tokens,
1744
+ }));
1640
1745
  }
1641
1746
  }
1642
- }
1643
- else if (json.type === 'content_block_delta') {
1644
- // Streaming content delta - accumulate in buffers
1645
- if (json.delta?.type === 'thinking_delta') {
1646
- const thinking = json.delta.thinking || '';
1647
- output += thinking;
1648
- thinkingBuffer += thinking;
1649
- }
1650
- else if (json.delta?.type === 'text_delta') {
1651
- const text = json.delta.text || '';
1652
- output += text;
1653
- textBuffer += text;
1654
- }
1655
- else if (json.delta?.type === 'input_json_delta') {
1656
- // Tool input streaming
1657
- const partialJson = json.delta.partial_json || '';
1658
- output += partialJson;
1659
- toolInputBuffer += partialJson;
1747
+ else if (json.type === 'system') {
1748
+ // System messages from Claude CLI
1749
+ if (json.message && !json.message.includes('initialization')) {
1750
+ output += `ℹ️ ${json.message}\n`;
1751
+ logs.push(createSystemLog('heartbeat', json.message, 'info', promptId));
1752
+ }
1660
1753
  }
1754
+ // Silently ignore: message_start, message_stop, ping, etc.
1661
1755
  }
1662
- else if (json.type === 'content_block_start') {
1663
- // Start of a content block - reset buffers
1664
- if (json.content_block?.type === 'thinking') {
1665
- output += '\n💭 Thinking:\n';
1666
- thinkingBuffer = '';
1667
- }
1668
- else if (json.content_block?.type === 'tool_use') {
1669
- output += `\n🔧 Tool: ${json.content_block.name || 'unknown'}\n`;
1670
- currentToolName = json.content_block.name;
1671
- currentToolUseId = json.content_block.id;
1672
- toolInputBuffer = '';
1673
- }
1674
- else if (json.content_block?.type === 'text') {
1675
- textBuffer = '';
1676
- }
1756
+ catch {
1757
+ // Not valid JSON, output as plain text
1758
+ output += line + '\n';
1677
1759
  }
1678
- else if (json.type === 'content_block_stop') {
1679
- // Block ended - emit structured logs from buffers
1680
- output += '\n';
1681
- if (thinkingBuffer.trim()) {
1682
- logs.push(createThinkingLog(thinkingBuffer, 'info', promptId));
1683
- thinkingBuffer = '';
1684
- }
1685
- if (textBuffer.trim()) {
1686
- logs.push(createOutputLog(textBuffer, 'markdown', 'info', promptId));
1687
- textBuffer = '';
1688
- }
1689
- if (toolInputBuffer.trim() && currentToolName) {
1690
- try {
1691
- const parsedInput = JSON.parse(toolInputBuffer);
1692
- logs.push(createToolCallLog(currentToolName, parsedInput, 'info', promptId, currentToolUseId || undefined));
1693
- // Track file modifications for memory summaries
1694
- if (parsedInput && typeof parsedInput.file_path === 'string') {
1695
- if (['Edit', 'Write', 'NotebookEdit'].includes(currentToolName)) {
1696
- trackFileModified(promptId, parsedInput.file_path);
1697
- }
1698
- }
1699
- }
1700
- catch {
1701
- // Invalid JSON input - emit as-is
1702
- logs.push(createToolCallLog(currentToolName, { raw: toolInputBuffer }, 'info', promptId, currentToolUseId || undefined));
1760
+ }
1761
+ return { output, hadValidJson, logs };
1762
+ };
1763
+ // Helper to send special events (git commits, deployments)
1764
+ const sendSpecialEvent = async (eventType, details) => {
1765
+ try {
1766
+ await channel.send({
1767
+ type: 'broadcast',
1768
+ event: eventType,
1769
+ payload: {
1770
+ id: commandId,
1771
+ promptId,
1772
+ ...details,
1773
+ timestamp: Date.now(),
1774
+ },
1775
+ });
1776
+ }
1777
+ catch (err) {
1778
+ logger.warn(`Failed to send ${eventType} event: ${err}`);
1779
+ }
1780
+ };
1781
+ // Stream stdout
1782
+ if (claude.stdout) {
1783
+ claude.stdout.on('data', (data) => {
1784
+ const text = data.toString();
1785
+ // Skip logging if it looks like base64/binary data (long strings without spaces)
1786
+ const preview = text.slice(0, 150);
1787
+ const looksLikeBinary = preview.length > 50 && !preview.includes(' ') && /^[A-Za-z0-9+/=]+$/.test(preview.replace(/\s/g, ''));
1788
+ if (!looksLikeBinary) {
1789
+ logger.info(`[STDOUT] ${text.slice(0, 100)}`);
1790
+ }
1791
+ // Parse stream-json and send formatted output
1792
+ const { output: parsed, hadValidJson, logs: structuredLogs } = parseStreamJson(text);
1793
+ // Only fallback to raw text if it wasn't valid JSON
1794
+ // If it was valid JSON but produced no output (e.g., system init), don't send anything
1795
+ const outputToSend = hadValidJson ? parsed : text;
1796
+ if (outputToSend.trim()) {
1797
+ logger.output('stdout', outputToSend.slice(0, 200));
1798
+ sendOutput(channel, commandId, 'stdout', outputToSend);
1799
+ }
1800
+ // Add structured logs to accumulator for persistence
1801
+ for (const log of structuredLogs) {
1802
+ addStructuredLog(promptId, log);
1803
+ }
1804
+ // Fallback: if no structured logs but we have output, create output log
1805
+ if (structuredLogs.length === 0 && outputToSend.trim() && !hadValidJson) {
1806
+ addLog(promptId, 'info', 'output', outputToSend);
1807
+ }
1808
+ // Detect git commits in the output
1809
+ // Matches various formats:
1810
+ // - [f0f66f3] commit message (git log)
1811
+ // - commit f0f66f3 (git show)
1812
+ // - **Commit**: `f0f66f3` (Claude formatted)
1813
+ // - Commit: f0f66f3 (plain text)
1814
+ const commitMatch = text.match(/\[([a-f0-9]{7,40})\]\s+(.+)/i) ||
1815
+ text.match(/commit\s+([a-f0-9]{7,40})/i) ||
1816
+ text.match(/\*\*Commit\*\*:\s*`([a-f0-9]{7,40})`/i) ||
1817
+ text.match(/Commit:\s*`?([a-f0-9]{7,40})`?/i);
1818
+ if (commitMatch) {
1819
+ // Try to get GitHub URL from git remote
1820
+ let commitUrl = null;
1821
+ try {
1822
+ const remoteUrl = execSync('git config --get remote.origin.url', {
1823
+ cwd: workingDirectory,
1824
+ encoding: 'utf-8',
1825
+ timeout: 5000,
1826
+ }).trim();
1827
+ // Convert git remote URL to GitHub commit URL
1828
+ const githubMatch = remoteUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
1829
+ if (githubMatch) {
1830
+ const repoPath = githubMatch[1].replace(/\.git$/, '');
1831
+ commitUrl = `https://github.com/${repoPath}/commit/${commitMatch[1]}`;
1703
1832
  }
1704
- toolInputBuffer = '';
1705
1833
  }
1706
- }
1707
- else if (json.type === 'result') {
1708
- // Final result from Claude CLI.
1709
- //
1710
- // Claude reports an upstream account/API failure here by exiting 0
1711
- // with is_error set and the error string as `result` (e.g. "API
1712
- // Error: 400 This organization has been disabled."). Treating that
1713
- // as a successful ✅ result is wrong — the prompt silently
1714
- // "completes" with the error text. Detect it and flag for the
1715
- // claude→orquesta fallback in the close handler instead of emitting
1716
- // it as success.
1717
- const apiErrStatus = typeof json.api_error_status === 'number' ? json.api_error_status : 0;
1718
- const isAccountFailure = json.is_error === true &&
1719
- (apiErrStatus >= 400 || CLAUDE_PROVIDER_FATAL_PATTERN.test(json.result || ''));
1720
- if (isAccountFailure && selectedCli === 'claude') {
1721
- claudeApiError = json.result || `claude api_error_status ${apiErrStatus}`;
1722
- // Don't emit the error as a ✅ success result; the close handler
1723
- // logs the failure + the orquesta-cli fallback.
1834
+ catch {
1835
+ // Ignore errors getting git remote
1724
1836
  }
1725
- else {
1726
- if (json.result) {
1727
- output += `\n✅ ${json.result}\n`;
1728
- }
1729
- if (json.cost_usd) {
1730
- output += `💰 Cost: $${json.cost_usd.toFixed(4)}\n`;
1731
- }
1732
- // Track usage for persistence to DB
1733
- if (json.total_tokens) {
1734
- promptTokensUsed.set(promptId, json.total_tokens);
1735
- }
1736
- if (json.cost_usd) {
1737
- promptCostCents.set(promptId, Math.round(json.cost_usd * 100));
1738
- }
1739
- // Emit system log for completion
1740
- logs.push(createSystemLog('complete', json.result || 'Execution completed', 'success', promptId, {
1741
- cost_cents: json.cost_usd ? Math.round(json.cost_usd * 100) : undefined,
1742
- tokens_used: json.total_tokens,
1743
- }));
1837
+ sendSpecialEvent('git_commit', {
1838
+ commitSha: commitMatch[1],
1839
+ message: commitMatch[2] || '',
1840
+ commitUrl,
1841
+ });
1842
+ // Track commit as key action for memory summary
1843
+ const commitMsg = commitMatch[2] ? commitMatch[2].slice(0, 50) : 'changes';
1844
+ trackAction(promptId, `Committed: ${commitMsg}`);
1845
+ }
1846
+ // Detect deployments in the output
1847
+ if (text.includes('Deployed to') || text.includes('Production:') || text.includes('vercel.app')) {
1848
+ const urlMatch = text.match(/(https:\/\/[^\s]+\.vercel\.app[^\s]*)/i);
1849
+ if (urlMatch) {
1850
+ sendSpecialEvent('deploy', {
1851
+ url: urlMatch[1],
1852
+ provider: 'vercel',
1853
+ });
1854
+ // Track deployment as key action for memory summary
1855
+ trackAction(promptId, `Deployed to Vercel`);
1744
1856
  }
1745
1857
  }
1746
- else if (json.type === 'system') {
1747
- // System messages from Claude CLI
1748
- if (json.message && !json.message.includes('initialization')) {
1749
- output += `ℹ️ ${json.message}\n`;
1750
- logs.push(createSystemLog('heartbeat', json.message, 'info', promptId));
1858
+ // Detect requirements (API keys, credentials, decisions needed)
1859
+ const requirementResult = detectRequirement(text, commandId);
1860
+ if (requirementResult?.detected && requirementResult.requirement) {
1861
+ logger.warn(`Requirement detected: ${requirementResult.requirement.title}`);
1862
+ // Send via realtime channel
1863
+ sendRequirement(channel, commandId, promptId, requirementResult.requirement);
1864
+ // Also persist to database (creates Linear ticket if connected)
1865
+ persistRequirement(promptId, requirementResult.requirement).catch(err => {
1866
+ logger.warn(`Failed to persist requirement: ${err}`);
1867
+ });
1868
+ }
1869
+ // Detect QA instructions
1870
+ const qaInstructions = detectQAInstructions(text, commandId);
1871
+ if (qaInstructions) {
1872
+ logger.success(`QA Instructions detected: ${qaInstructions.feature}`);
1873
+ // Send via realtime channel
1874
+ sendQAInstructions(channel, commandId, promptId, qaInstructions);
1875
+ // Persist to database
1876
+ persistQAInstructions(promptId, qaInstructions).catch(err => {
1877
+ logger.warn(`Failed to persist QA instructions: ${err}`);
1878
+ });
1879
+ }
1880
+ // In supervised mode, check for permission requests
1881
+ if (mode === 'supervised') {
1882
+ const { detected, content: permissionContent } = detectPermissionRequest(text, commandId);
1883
+ if (detected) {
1884
+ handlePermissionRequest(commandId, promptId, channel, claude, permissionContent);
1751
1885
  }
1752
1886
  }
1753
- // Silently ignore: message_start, message_stop, ping, etc.
1754
- }
1755
- catch {
1756
- // Not valid JSON, output as plain text
1757
- output += line + '\n';
1758
- }
1759
- }
1760
- return { output, hadValidJson, logs };
1761
- };
1762
- // Helper to send special events (git commits, deployments)
1763
- const sendSpecialEvent = async (eventType, details) => {
1764
- try {
1765
- await channel.send({
1766
- type: 'broadcast',
1767
- event: eventType,
1768
- payload: {
1769
- id: commandId,
1770
- promptId,
1771
- ...details,
1772
- timestamp: Date.now(),
1773
- },
1774
1887
  });
1775
1888
  }
1776
- catch (err) {
1777
- logger.warn(`Failed to send ${eventType} event: ${err}`);
1889
+ else {
1890
+ logger.warn('No stdout pipe available');
1891
+ }
1892
+ // Stream stderr
1893
+ if (claude.stderr) {
1894
+ claude.stderr.on('data', (data) => {
1895
+ const raw = data.toString();
1896
+ const text = stripAnsiCodes(raw);
1897
+ if (!text.trim())
1898
+ return;
1899
+ logger.info(`[STDERR] ${text.slice(0, 100)}`);
1900
+ logger.output('stderr', text);
1901
+ sendOutput(channel, commandId, 'stderr', text);
1902
+ // Add to log accumulator for persistence backup
1903
+ addLog(promptId, 'error', 'stderr', text);
1904
+ });
1778
1905
  }
1779
- };
1780
- // Stream stdout
1781
- if (claude.stdout) {
1782
- claude.stdout.on('data', (data) => {
1783
- const text = data.toString();
1784
- // Skip logging if it looks like base64/binary data (long strings without spaces)
1785
- const preview = text.slice(0, 150);
1786
- const looksLikeBinary = preview.length > 50 && !preview.includes(' ') && /^[A-Za-z0-9+/=]+$/.test(preview.replace(/\s/g, ''));
1787
- if (!looksLikeBinary) {
1788
- logger.info(`[STDOUT] ${text.slice(0, 100)}`);
1789
- }
1790
- // Parse stream-json and send formatted output
1791
- const { output: parsed, hadValidJson, logs: structuredLogs } = parseStreamJson(text);
1792
- // Only fallback to raw text if it wasn't valid JSON
1793
- // If it was valid JSON but produced no output (e.g., system init), don't send anything
1794
- const outputToSend = hadValidJson ? parsed : text;
1795
- if (outputToSend.trim()) {
1796
- logger.output('stdout', outputToSend.slice(0, 200));
1797
- sendOutput(channel, commandId, 'stdout', outputToSend);
1798
- }
1799
- // Add structured logs to accumulator for persistence
1800
- for (const log of structuredLogs) {
1801
- addStructuredLog(promptId, log);
1802
- }
1803
- // Fallback: if no structured logs but we have output, create output log
1804
- if (structuredLogs.length === 0 && outputToSend.trim() && !hadValidJson) {
1805
- addLog(promptId, 'info', 'output', outputToSend);
1806
- }
1807
- // Detect git commits in the output
1808
- // Matches various formats:
1809
- // - [f0f66f3] commit message (git log)
1810
- // - commit f0f66f3 (git show)
1811
- // - **Commit**: `f0f66f3` (Claude formatted)
1812
- // - Commit: f0f66f3 (plain text)
1813
- const commitMatch = text.match(/\[([a-f0-9]{7,40})\]\s+(.+)/i) ||
1814
- text.match(/commit\s+([a-f0-9]{7,40})/i) ||
1815
- text.match(/\*\*Commit\*\*:\s*`([a-f0-9]{7,40})`/i) ||
1816
- text.match(/Commit:\s*`?([a-f0-9]{7,40})`?/i);
1817
- if (commitMatch) {
1818
- // Try to get GitHub URL from git remote
1819
- let commitUrl = null;
1820
- try {
1821
- const remoteUrl = execSync('git config --get remote.origin.url', {
1822
- cwd: workingDirectory,
1823
- encoding: 'utf-8',
1824
- timeout: 5000,
1825
- }).trim();
1826
- // Convert git remote URL to GitHub commit URL
1827
- const githubMatch = remoteUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
1828
- if (githubMatch) {
1829
- const repoPath = githubMatch[1].replace(/\.git$/, '');
1830
- commitUrl = `https://github.com/${repoPath}/commit/${commitMatch[1]}`;
1906
+ else {
1907
+ logger.warn('No stderr pipe available');
1908
+ }
1909
+ // Wrap process lifecycle in a Promise so callers can await completion
1910
+ return new Promise((resolveExecution) => {
1911
+ // Handle completion
1912
+ claude.on('close', async (code) => {
1913
+ runningProcesses.delete(commandId);
1914
+ if (promptId)
1915
+ promptIdToCommandId.delete(promptId);
1916
+ pendingSupervisionCallbacks.delete(commandId);
1917
+ // Clean up per-execution buffers (concurrent-safe)
1918
+ outputBufferMap.delete(commandId);
1919
+ detectedRequirementsMap.delete(commandId);
1920
+ qaBufferMap.delete(commandId);
1921
+ qaDetectedMap.delete(commandId);
1922
+ // claude→orquesta fallback: if Claude CLI failed at the account/API
1923
+ // level (org disabled, auth) and orquesta-cli is available, re-run the
1924
+ // SAME prompt through it ( Batuta proxy → Kimi) instead of completing
1925
+ // with the error text. _forceCli guards against re-recursing. The
1926
+ // recursive call owns final completion (sendComplete/flush), so we skip
1927
+ // the rest of this handler and resolve once it returns.
1928
+ if (claudeApiError && selectedCli === 'claude' && !options._forceCli && isOrquestaCliAvailable()) {
1929
+ logger.warn(`Claude CLI unavailable (${claudeApiError.slice(0, 80)}) — retrying via orquesta-cli`);
1930
+ addLog(promptId, 'warning', 'system', 'Claude CLI unavailable falling back to orquesta-cli');
1931
+ try {
1932
+ await execute({ ...options, _forceCli: 'orquesta' });
1933
+ }
1934
+ catch (err) {
1935
+ logger.error(`orquesta-cli fallback failed: ${err}`);
1936
+ stopLogPersistence(promptId);
1937
+ if (_activePromptId === promptId)
1938
+ _activePromptId = null;
1939
+ try {
1940
+ await flushLogs(promptId);
1941
+ }
1942
+ catch { /* noop */ }
1943
+ executionLogs.delete(promptId);
1944
+ sendComplete(channel, commandId, 1, startTime);
1831
1945
  }
1946
+ resolveExecution();
1947
+ return;
1832
1948
  }
1833
- catch {
1834
- // Ignore errors getting git remote
1949
+ // If Claude failed at the account level but we could NOT fall back
1950
+ // (no orquesta-cli, or this IS the forced run), surface it as a real
1951
+ // failure instead of a silent exit-0 "completed" with empty output.
1952
+ // claude exits 0 even on "organization has been disabled".
1953
+ if (claudeApiError) {
1954
+ addLog(promptId, 'error', 'system', `Claude CLI unavailable (no fallback): ${claudeApiError}`);
1955
+ sendOutput(channel, commandId, 'stderr', `\n❌ Claude CLI unavailable: ${claudeApiError}\n`);
1835
1956
  }
1836
- sendSpecialEvent('git_commit', {
1837
- commitSha: commitMatch[1],
1838
- message: commitMatch[2] || '',
1839
- commitUrl,
1840
- });
1841
- // Track commit as key action for memory summary
1842
- const commitMsg = commitMatch[2] ? commitMatch[2].slice(0, 50) : 'changes';
1843
- trackAction(promptId, `Committed: ${commitMsg}`);
1844
- }
1845
- // Detect deployments in the output
1846
- if (text.includes('Deployed to') || text.includes('Production:') || text.includes('vercel.app')) {
1847
- const urlMatch = text.match(/(https:\/\/[^\s]+\.vercel\.app[^\s]*)/i);
1848
- if (urlMatch) {
1849
- sendSpecialEvent('deploy', {
1850
- url: urlMatch[1],
1851
- provider: 'vercel',
1852
- });
1853
- // Track deployment as key action for memory summary
1854
- trackAction(promptId, `Deployed to Vercel`);
1957
+ const exitCode = claudeApiError ? (code || 1) : (code ?? 0);
1958
+ const duration = Date.now() - startTime;
1959
+ const status = exitCode === 0 ? 'completed' : 'failed';
1960
+ if (exitCode === 0) {
1961
+ logger.success(`Completed in ${(duration / 1000).toFixed(1)}s`);
1855
1962
  }
1856
- }
1857
- // Detect requirements (API keys, credentials, decisions needed)
1858
- const requirementResult = detectRequirement(text, commandId);
1859
- if (requirementResult?.detected && requirementResult.requirement) {
1860
- logger.warn(`Requirement detected: ${requirementResult.requirement.title}`);
1861
- // Send via realtime channel
1862
- sendRequirement(channel, commandId, promptId, requirementResult.requirement);
1863
- // Also persist to database (creates Linear ticket if connected)
1864
- persistRequirement(promptId, requirementResult.requirement).catch(err => {
1865
- logger.warn(`Failed to persist requirement: ${err}`);
1866
- });
1867
- }
1868
- // Detect QA instructions
1869
- const qaInstructions = detectQAInstructions(text, commandId);
1870
- if (qaInstructions) {
1871
- logger.success(`QA Instructions detected: ${qaInstructions.feature}`);
1872
- // Send via realtime channel
1873
- sendQAInstructions(channel, commandId, promptId, qaInstructions);
1874
- // Persist to database
1875
- persistQAInstructions(promptId, qaInstructions).catch(err => {
1876
- logger.warn(`Failed to persist QA instructions: ${err}`);
1877
- });
1878
- }
1879
- // In supervised mode, check for permission requests
1880
- if (mode === 'supervised') {
1881
- const { detected, content: permissionContent } = detectPermissionRequest(text, commandId);
1882
- if (detected) {
1883
- handlePermissionRequest(commandId, promptId, channel, claude, permissionContent);
1963
+ else {
1964
+ logger.warn(`Exited with code ${exitCode}`);
1884
1965
  }
1885
- }
1886
- });
1887
- }
1888
- else {
1889
- logger.warn('No stdout pipe available');
1890
- }
1891
- // Stream stderr
1892
- if (claude.stderr) {
1893
- claude.stderr.on('data', (data) => {
1894
- const raw = data.toString();
1895
- const text = stripAnsiCodes(raw);
1896
- if (!text.trim())
1897
- return;
1898
- logger.info(`[STDERR] ${text.slice(0, 100)}`);
1899
- logger.output('stderr', text);
1900
- sendOutput(channel, commandId, 'stderr', text);
1901
- // Add to log accumulator for persistence backup
1902
- addLog(promptId, 'error', 'stderr', text);
1903
- });
1904
- }
1905
- else {
1906
- logger.warn('No stderr pipe available');
1907
- }
1908
- // Wrap process lifecycle in a Promise so callers can await completion
1909
- return new Promise((resolveExecution) => {
1910
- // Handle completion
1911
- claude.on('close', async (code) => {
1912
- runningProcesses.delete(commandId);
1913
- if (promptId)
1914
- promptIdToCommandId.delete(promptId);
1915
- pendingSupervisionCallbacks.delete(commandId);
1916
- // Clean up per-execution buffers (concurrent-safe)
1917
- outputBufferMap.delete(commandId);
1918
- detectedRequirementsMap.delete(commandId);
1919
- qaBufferMap.delete(commandId);
1920
- qaDetectedMap.delete(commandId);
1921
- // claude→orquesta fallback: if Claude CLI failed at the account/API
1922
- // level (org disabled, auth) and orquesta-cli is available, re-run the
1923
- // SAME prompt through it (→ Batuta proxy → Kimi) instead of completing
1924
- // with the error text. _forceCli guards against re-recursing. The
1925
- // recursive call owns final completion (sendComplete/flush), so we skip
1926
- // the rest of this handler and resolve once it returns.
1927
- if (claudeApiError && selectedCli === 'claude' && !options._forceCli && isOrquestaCliAvailable()) {
1928
- logger.warn(`Claude CLI unavailable (${claudeApiError.slice(0, 80)}) — retrying via orquesta-cli`);
1929
- addLog(promptId, 'warning', 'system', 'Claude CLI unavailable — falling back to orquesta-cli');
1966
+ // Add completion log entry
1967
+ addLog(promptId, exitCode === 0 ? 'success' : 'error', 'system', `Execution ${status} with exit code ${exitCode} in ${(duration / 1000).toFixed(1)}s`);
1968
+ // Stop periodic log persistence and flush any remaining logs
1969
+ stopLogPersistence(promptId);
1970
+ if (_activePromptId === promptId)
1971
+ _activePromptId = null;
1930
1972
  try {
1931
- await execute({ ...options, _forceCli: 'orquesta' });
1973
+ await flushLogs(promptId);
1974
+ executionLogs.delete(promptId);
1975
+ logger.debug('All logs flushed to database');
1932
1976
  }
1933
1977
  catch (err) {
1934
- logger.error(`orquesta-cli fallback failed: ${err}`);
1935
- stopLogPersistence(promptId);
1936
- if (_activePromptId === promptId)
1937
- _activePromptId = null;
1938
- try {
1939
- await flushLogs(promptId);
1940
- }
1941
- catch { /* noop */ }
1942
- sendComplete(channel, commandId, 1, startTime);
1978
+ logger.error(`Failed to flush final logs: ${err}`);
1979
+ }
1980
+ // Clear output buffer for this command
1981
+ clearOutputBuffer(commandId);
1982
+ // Clean up attachment temp files
1983
+ if (attachmentCleanup) {
1984
+ attachmentCleanup();
1985
+ }
1986
+ // Send completion via WebSocket (for real-time UI update)
1987
+ sendComplete(channel, commandId, exitCode, startTime);
1988
+ // Generate execution summary for project memory
1989
+ const summary = generateExecutionSummary(promptId, content, exitCode);
1990
+ if (summary) {
1991
+ logger.info(`Generated summary: ${summary.text.slice(0, 100)}...`);
1992
+ logger.debug(`Files modified: ${summary.files_modified.length}, Actions: ${summary.key_actions.length}`);
1993
+ }
1994
+ // Collect token/cost usage tracked during streaming
1995
+ const usage = {};
1996
+ if (promptTokensUsed.has(promptId)) {
1997
+ usage.tokens_used = promptTokensUsed.get(promptId);
1998
+ promptTokensUsed.delete(promptId);
1943
1999
  }
2000
+ if (promptCostCents.has(promptId)) {
2001
+ usage.cost_cents = promptCostCents.get(promptId);
2002
+ promptCostCents.delete(promptId);
2003
+ }
2004
+ // Update prompt status in database with summary and usage (backend fix - don't rely on frontend)
2005
+ updatePromptStatus(promptId, status, summary || undefined, Object.keys(usage).length > 0 ? usage : undefined).catch(err => {
2006
+ logger.error(`Failed to update prompt status: ${err}`);
2007
+ });
1944
2008
  resolveExecution();
1945
- return;
1946
- }
1947
- // If Claude failed at the account level but we could NOT fall back
1948
- // (no orquesta-cli, or this IS the forced run), surface it as a real
1949
- // failure instead of a silent exit-0 "completed" with empty output.
1950
- // claude exits 0 even on "organization has been disabled".
1951
- if (claudeApiError) {
1952
- addLog(promptId, 'error', 'system', `Claude CLI unavailable (no fallback): ${claudeApiError}`);
1953
- sendOutput(channel, commandId, 'stderr', `\n❌ Claude CLI unavailable: ${claudeApiError}\n`);
1954
- }
1955
- const exitCode = claudeApiError ? (code || 1) : (code ?? 0);
1956
- const duration = Date.now() - startTime;
1957
- const status = exitCode === 0 ? 'completed' : 'failed';
1958
- if (exitCode === 0) {
1959
- logger.success(`Completed in ${(duration / 1000).toFixed(1)}s`);
1960
- }
1961
- else {
1962
- logger.warn(`Exited with code ${exitCode}`);
1963
- }
1964
- // Add completion log entry
1965
- addLog(promptId, exitCode === 0 ? 'success' : 'error', 'system', `Execution ${status} with exit code ${exitCode} in ${(duration / 1000).toFixed(1)}s`);
1966
- // Stop periodic log persistence and flush any remaining logs
1967
- stopLogPersistence(promptId);
1968
- if (_activePromptId === promptId)
1969
- _activePromptId = null;
1970
- try {
1971
- await flushLogs(promptId);
1972
- logger.debug('All logs flushed to database');
1973
- }
1974
- catch (err) {
1975
- logger.error(`Failed to flush final logs: ${err}`);
1976
- }
1977
- // Clear output buffer for this command
1978
- clearOutputBuffer(commandId);
1979
- // Clean up attachment temp files
1980
- if (attachmentCleanup) {
1981
- attachmentCleanup();
1982
- }
1983
- // Send completion via WebSocket (for real-time UI update)
1984
- sendComplete(channel, commandId, exitCode, startTime);
1985
- // Generate execution summary for project memory
1986
- const summary = generateExecutionSummary(promptId, content, exitCode);
1987
- if (summary) {
1988
- logger.info(`Generated summary: ${summary.text.slice(0, 100)}...`);
1989
- logger.debug(`Files modified: ${summary.files_modified.length}, Actions: ${summary.key_actions.length}`);
1990
- }
1991
- // Collect token/cost usage tracked during streaming
1992
- const usage = {};
1993
- if (promptTokensUsed.has(promptId)) {
1994
- usage.tokens_used = promptTokensUsed.get(promptId);
1995
- promptTokensUsed.delete(promptId);
1996
- }
1997
- if (promptCostCents.has(promptId)) {
1998
- usage.cost_cents = promptCostCents.get(promptId);
1999
- promptCostCents.delete(promptId);
2000
- }
2001
- // Update prompt status in database with summary and usage (backend fix - don't rely on frontend)
2002
- updatePromptStatus(promptId, status, summary || undefined, Object.keys(usage).length > 0 ? usage : undefined).catch(err => {
2003
- logger.error(`Failed to update prompt status: ${err}`);
2004
2009
  });
2005
- resolveExecution();
2006
- });
2007
- // Handle errors
2008
- claude.on('error', async (err) => {
2009
- runningProcesses.delete(commandId);
2010
- if (promptId)
2011
- promptIdToCommandId.delete(promptId);
2012
- pendingSupervisionCallbacks.delete(commandId);
2013
- logger.error(`Execution error: ${err.message}`);
2014
- // Add error log entry
2015
- addLog(promptId, 'error', 'system', `Execution error: ${err.message}`);
2016
- // Stop log persistence and flush remaining logs
2017
- stopLogPersistence(promptId);
2018
- if (_activePromptId === promptId)
2019
- _activePromptId = null;
2020
- try {
2021
- await flushLogs(promptId);
2022
- }
2023
- catch (flushErr) {
2024
- logger.error(`Failed to flush logs on error: ${flushErr}`);
2025
- }
2026
- // Clear output buffer
2027
- clearOutputBuffer(commandId);
2028
- // Clean up attachment temp files
2029
- if (attachmentCleanup) {
2030
- attachmentCleanup();
2031
- }
2032
- if (err.message.includes('ENOENT')) {
2033
- const errPath = err.path;
2034
- if (errPath === 'script') {
2035
- sendError(channel, commandId, `Failed to spawn PTY wrapper 'script' (Unix-only). On Windows this code path should be unreachable — please report this bug.`, 'SPAWN_SCRIPT_FAILED');
2010
+ // Handle errors
2011
+ claude.on('error', async (err) => {
2012
+ runningProcesses.delete(commandId);
2013
+ if (promptId)
2014
+ promptIdToCommandId.delete(promptId);
2015
+ pendingSupervisionCallbacks.delete(commandId);
2016
+ logger.error(`Execution error: ${err.message}`);
2017
+ // Add error log entry
2018
+ addLog(promptId, 'error', 'system', `Execution error: ${err.message}`);
2019
+ // Stop log persistence and flush remaining logs
2020
+ stopLogPersistence(promptId);
2021
+ if (_activePromptId === promptId)
2022
+ _activePromptId = null;
2023
+ try {
2024
+ await flushLogs(promptId);
2025
+ }
2026
+ catch (flushErr) {
2027
+ logger.error(`Failed to flush logs on error: ${flushErr}`);
2028
+ }
2029
+ // Clear output buffer
2030
+ clearOutputBuffer(commandId);
2031
+ // Clean up attachment temp files
2032
+ if (attachmentCleanup) {
2033
+ attachmentCleanup();
2034
+ }
2035
+ if (err.message.includes('ENOENT')) {
2036
+ const errPath = err.path;
2037
+ if (errPath === 'script') {
2038
+ sendError(channel, commandId, `Failed to spawn PTY wrapper 'script' (Unix-only). On Windows this code path should be unreachable — please report this bug.`, 'SPAWN_SCRIPT_FAILED');
2039
+ }
2040
+ else {
2041
+ sendError(channel, commandId, `CLI not found: '${errPath || cliCommand}'. Install with: npm install -g @anthropic-ai/claude-code`, 'CLI_NOT_FOUND');
2042
+ }
2036
2043
  }
2037
2044
  else {
2038
- sendError(channel, commandId, `CLI not found: '${errPath || cliCommand}'. Install with: npm install -g @anthropic-ai/claude-code`, 'CLI_NOT_FOUND');
2045
+ sendError(channel, commandId, err.message);
2039
2046
  }
2040
- }
2041
- else {
2042
- sendError(channel, commandId, err.message);
2043
- }
2044
- resolveExecution();
2047
+ resolveExecution();
2048
+ });
2045
2049
  });
2046
- });
2050
+ }
2051
+ catch (err) {
2052
+ _activePromptId = null;
2053
+ throw err;
2054
+ }
2047
2055
  }
2048
2056
  // Handle a detected permission request
2049
2057
  function handlePermissionRequest(commandId, promptId, channel, process, content) {