orquesta-agent 0.2.246 → 0.2.249

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.
Files changed (42) hide show
  1. package/dist/cli-auth-detect.js +1 -1
  2. package/dist/cli-auth-detect.js.map +1 -1
  3. package/dist/coordination.d.ts +21 -1
  4. package/dist/coordination.d.ts.map +1 -1
  5. package/dist/coordination.js +123 -0
  6. package/dist/coordination.js.map +1 -1
  7. package/dist/executor.d.ts +11 -4
  8. package/dist/executor.d.ts.map +1 -1
  9. package/dist/executor.js +279 -42
  10. package/dist/executor.js.map +1 -1
  11. package/dist/index.js +13 -8
  12. package/dist/index.js.map +1 -1
  13. package/dist/interactive-prompt.d.ts +65 -0
  14. package/dist/interactive-prompt.d.ts.map +1 -0
  15. package/dist/interactive-prompt.js +115 -0
  16. package/dist/interactive-prompt.js.map +1 -0
  17. package/dist/sandbox.d.ts.map +1 -1
  18. package/dist/sandbox.js +5 -0
  19. package/dist/sandbox.js.map +1 -1
  20. package/dist/secret-files.d.ts +19 -0
  21. package/dist/secret-files.d.ts.map +1 -0
  22. package/dist/secret-files.js +77 -0
  23. package/dist/secret-files.js.map +1 -0
  24. package/dist/supabase.d.ts +13 -8
  25. package/dist/supabase.d.ts.map +1 -1
  26. package/dist/supabase.js +27 -1
  27. package/dist/supabase.js.map +1 -1
  28. package/dist/ui/onboarding.js +1 -1
  29. package/dist/ui/onboarding.js.map +1 -1
  30. package/dist/ui/public/app.js +2 -0
  31. package/dist/ui/public/index.html +1 -0
  32. package/dist/ui/public/style.css +1 -0
  33. package/dist/ui/public/welcome.html +2 -0
  34. package/dist/ui/server.d.ts +1 -1
  35. package/dist/ui/server.d.ts.map +1 -1
  36. package/dist/ui/server.js +2 -2
  37. package/dist/ui/server.js.map +1 -1
  38. package/dist/ui/types.d.ts +1 -1
  39. package/dist/ui/types.d.ts.map +1 -1
  40. package/dist/ws-client.d.ts +1 -1
  41. package/dist/ws-client.d.ts.map +1 -1
  42. package/package.json +1 -1
package/dist/executor.js CHANGED
@@ -119,12 +119,14 @@ import { fileURLToPath, pathToFileURL } from 'url';
119
119
  import { createRequire } from 'module';
120
120
  import * as logger from './logger.js';
121
121
  import { redactSecrets } from './redact.js';
122
+ import { materializeSecretFiles } from './secret-files.js';
123
+ import { InteractiveStallDetector, describeInteractiveStall, stripAnsi as stripAnsiForResult } from './interactive-prompt.js';
122
124
  // ESM require for resolving the bundled orquesta-cli entry on Windows (see the
123
125
  // interactive-session spawn — node-pty can't launch the `.cmd` shim).
124
126
  const nodeRequire = createRequire(import.meta.url);
125
127
  import { isSandboxAvailable, buildBwrapArgs, shQuote, ensureStrictProjectDirs, encodeClaudeProjectDir, findStaleBinds, criticalBindPaths } from './sandbox.js';
126
128
  import { isLoggedOutMessage } from './cli-auth-detect.js';
127
- import { parseCoordSpec, runCoordination } from './coordination.js';
129
+ import { parseCoordSpec, runCoordination, runVideoCall } from './coordination.js';
128
130
  import { parseSudosudoInstallSpec, runSudosudoInstall } from './sudosudo.js';
129
131
  import { sendOutput, sendComplete, sendError, sendSupervisionRequest, sendExecutionResumed, updatePromptStatus, persistOutputLogs, clearOutputBuffer, sendRequirement, persistRequirement, sendQAInstructions, persistQAInstructions, sendPlanItemsGenerated, sendSessionOutput, sendSessionStarted, sendSessionEnded, sendSessionError, sendSessionLog, reportAgentError } from './supabase.js';
130
132
  import { startTranscriptTail } from './claude-transcript.js';
@@ -155,6 +157,22 @@ function cleanKimiText(str) {
155
157
  .map(line => line.replace(/^(\s*)•\s/, '$1'))
156
158
  .join('\n');
157
159
  }
160
+ // kiro-cli's headless `chat` output is a TUI transcript, not a plain answer:
161
+ // ANSI colour throughout, a trust banner when --trust-all-tools is on ("All
162
+ // tools are now trusted (!). …", "Agents can sometimes do unexpected things…",
163
+ // "Learn more at https://kiro.dev/docs/cli/chat/security/…"), the answer
164
+ // prefixed with the `> ` prompt marker, and a "▸ Credits: 0.05 • Time: 2s"
165
+ // footer. Strip the chrome so the timeline shows the answer.
166
+ function cleanKiroText(str) {
167
+ return stripAnsiCodes(str)
168
+ .split('\n')
169
+ .filter(line => !/^\s*All tools are now trusted/.test(line))
170
+ .filter(line => !/^\s*Agents can sometimes do unexpected things/.test(line))
171
+ .filter(line => !/^\s*Learn more at https:\/\/kiro\.dev\//.test(line))
172
+ .filter(line => !/^\s*▸?\s*Credits:\s*[\d.]+\s*•\s*Time:/.test(line))
173
+ .map(line => line.replace(/^>\s?/, ''))
174
+ .join('\n');
175
+ }
158
176
  function stripAnsiCodes(str) {
159
177
  // Remove OSC sequences first (ESC ] ... BEL or ESC ] ... ESC \)
160
178
  // Windows Terminal / ConEmu emit OSC 9;4;<state>;<progress> for progress markers.
@@ -236,6 +254,18 @@ const promptCostCents = new Map();
236
254
  // ever saw logs, and panels that read prompts.result (architecture extraction,
237
255
  // auto-agent polling) found it empty.
238
256
  const promptFinalResult = new Map();
257
+ // promptId -> failure text when a run was cut for waiting on a keyboard prompt
258
+ // (see interactive-prompt.ts). Read by the close handler.
259
+ const promptInteractiveStall = new Map();
260
+ // How long a prompt-looking line may sit with no further output before the
261
+ // run is killed as "waiting for input". 0 disables the watchdog.
262
+ function interactivePromptTimeoutMs() {
263
+ const raw = process.env.ORQUESTA_INTERACTIVE_PROMPT_TIMEOUT_MS;
264
+ if (raw === undefined || raw === '')
265
+ return 60_000;
266
+ const n = Number(raw);
267
+ return Number.isFinite(n) && n >= 0 ? n : 60_000;
268
+ }
239
269
  // The CLI's own verdict for a run, from its `end` event. Kept per prompt because
240
270
  // several prompts stream through the same process concurrently. See QA-76: an
241
271
  // exit code is not evidence that the work succeeded.
@@ -343,6 +373,18 @@ export function setInjectedCredentials(credentials) {
343
373
  export function getInjectedCredentials() {
344
374
  return injectedCredentials;
345
375
  }
376
+ // Write the project's secret files to disk (relative paths resolve against
377
+ // `cwd`) and return the credentials plus one `ORQUESTA_FILE_<NAME>` var per
378
+ // file — listed in the manifest too, so orquesta-cli's env whitelist lets the
379
+ // shell tools see them.
380
+ function withSecretFileEnv(credentials, cwd) {
381
+ const fileEnv = materializeSecretFiles(cwd);
382
+ const fileKeys = Object.keys(fileEnv);
383
+ if (fileKeys.length === 0)
384
+ return credentials;
385
+ const manifest = [credentials[INJECTED_ENV_MANIFEST], ...fileKeys].filter(Boolean).join(',');
386
+ return { ...credentials, ...fileEnv, [INJECTED_ENV_MANIFEST]: manifest };
387
+ }
346
388
  let globalPermissionMode = 'auto';
347
389
  // Agent instructions (from Orquesta dashboard)
348
390
  let agentInstructions = null;
@@ -1037,6 +1079,87 @@ export function checkCursorAuth() {
1037
1079
  return cache({ available: true, authenticated: true, method: null });
1038
1080
  }
1039
1081
  }
1082
+ // Resolve AWS Kiro's CLI (`kiro-cli`). Its installer drops the binary in
1083
+ // ~/.local/bin (alongside `kiro-cli-chat`/`kiro-cli-term`) and, like cursor's,
1084
+ // only reminds the user to put that on PATH — so probe the install dir too.
1085
+ // Order: KIRO_CLI_PATH → on PATH → ~/.local/bin/kiro-cli.
1086
+ // macOS + Linux only: Kiro ships no native Windows CLI, so on win32 this is
1087
+ // always null and the engine never appears as available there.
1088
+ let kiroBinaryCache = null;
1089
+ export function resolveKiroBinary() {
1090
+ const now = Date.now();
1091
+ if (kiroBinaryCache && now - kiroBinaryCache.at < CLI_DETECTION_TTL_MS)
1092
+ return kiroBinaryCache.path;
1093
+ let resolved = null;
1094
+ if (process.platform !== 'win32') {
1095
+ const envPath = process.env.KIRO_CLI_PATH;
1096
+ if (envPath) {
1097
+ try {
1098
+ fs.accessSync(envPath, fs.constants.X_OK);
1099
+ resolved = envPath;
1100
+ }
1101
+ catch { /* bad override */ }
1102
+ }
1103
+ if (!resolved) {
1104
+ try {
1105
+ execSync('kiro-cli --version', { stdio: 'pipe', timeout: 15000 });
1106
+ resolved = 'kiro-cli';
1107
+ }
1108
+ catch { /* not on PATH */ }
1109
+ }
1110
+ if (!resolved) {
1111
+ const candidate = path.join(os.homedir(), '.local', 'bin', 'kiro-cli');
1112
+ try {
1113
+ fs.accessSync(candidate, fs.constants.X_OK);
1114
+ resolved = candidate;
1115
+ }
1116
+ catch { /* not installed there */ }
1117
+ }
1118
+ }
1119
+ kiroBinaryCache = { path: resolved, at: now };
1120
+ return resolved;
1121
+ }
1122
+ // Kiro CLI (the `kiro-cli` binary). Authenticated separately with
1123
+ // `kiro-cli login` against the user's own AWS Builder ID / IAM Identity Center
1124
+ // — Orquesta never sees that credential.
1125
+ export function isKiroCliAvailable() {
1126
+ return resolveKiroBinary() !== null;
1127
+ }
1128
+ // Is kiro-cli actually LOGGED IN, not just installed? Same contract as
1129
+ // checkCursorAuth: `kiro-cli whoami` exits 0 with the identity when logged in
1130
+ // and exits 1 printing "Not logged in" otherwise. Only that wording flips the
1131
+ // flag; anything else (timeout, older build) reports authenticated with a null
1132
+ // method, because execution never depends on this flag.
1133
+ let kiroAuthCache = null;
1134
+ export function checkKiroAuth() {
1135
+ const now = Date.now();
1136
+ if (kiroAuthCache && now - kiroAuthCache.at < CLI_DETECTION_TTL_MS)
1137
+ return kiroAuthCache.result;
1138
+ const cache = (result) => {
1139
+ kiroAuthCache = { result, at: now };
1140
+ return result;
1141
+ };
1142
+ const binary = resolveKiroBinary();
1143
+ if (!binary)
1144
+ return cache({ available: false, authenticated: false, method: null });
1145
+ try {
1146
+ const out = execSync(`"${binary}" whoami`, {
1147
+ stdio: ['ignore', 'pipe', 'pipe'],
1148
+ timeout: 15000,
1149
+ encoding: 'utf-8',
1150
+ });
1151
+ if (isLoggedOutMessage(out))
1152
+ return cache({ available: true, authenticated: false, method: null });
1153
+ return cache({ available: true, authenticated: true, method: 'subscription' });
1154
+ }
1155
+ catch (error) {
1156
+ const err = error;
1157
+ const combined = `${err.stdout ?? ''}${err.stderr ?? ''}`;
1158
+ if (isLoggedOutMessage(combined))
1159
+ return cache({ available: true, authenticated: false, method: null });
1160
+ return cache({ available: true, authenticated: true, method: null });
1161
+ }
1162
+ }
1040
1163
  // Dedupe the fallback warning: selectCli() runs on every heartbeat (the agent
1041
1164
  // reports activeCli), so logging unconditionally spammed thousands of identical
1042
1165
  // WARN lines. Only log when the message actually changes.
@@ -1051,12 +1174,13 @@ function warnCliOnce(msg) {
1051
1174
  // the pinned CLI turns out not to be installed. Table-driven rather than a nest
1052
1175
  // of pairwise ifs: every CLI added here otherwise costs another N branches, and
1053
1176
  // the pair that got missed is exactly how a preference silently no-ops.
1054
- const CLI_AUTO_ORDER = ['orquesta', 'claude', 'kimi', 'cursor'];
1177
+ const CLI_AUTO_ORDER = ['orquesta', 'claude', 'kimi', 'cursor', 'kiro'];
1055
1178
  const CLI_AUTO_REASON = {
1056
1179
  orquesta: 'Auto: orquesta available (local LLM)',
1057
1180
  claude: 'Auto: claude available (Anthropic API)',
1058
1181
  kimi: 'Auto: kimi available',
1059
1182
  cursor: 'Auto: cursor-agent available',
1183
+ kiro: 'Auto: kiro-cli available',
1060
1184
  };
1061
1185
  // Select which CLI to use based on availability and preference
1062
1186
  export function selectCli() {
@@ -1065,6 +1189,7 @@ export function selectCli() {
1065
1189
  claude: isClaudeCliAvailable(),
1066
1190
  kimi: isKimiCliAvailable(),
1067
1191
  cursor: isCursorCliAvailable(),
1192
+ kiro: isKiroCliAvailable(),
1068
1193
  };
1069
1194
  if (!CLI_AUTO_ORDER.some(cli => available[cli])) {
1070
1195
  return { cli: null, reason: `No CLI found (${CLI_AUTO_ORDER.join(', ')})` };
@@ -2083,12 +2208,14 @@ export async function execute(options) {
2083
2208
  // the spec is absent or malformed.
2084
2209
  const coordSpec = parseCoordSpec(content);
2085
2210
  if (coordSpec) {
2086
- addLog(promptId, 'info', 'system', `Native coordination (${coordSpec.reason}) — channel ${coordSpec.channelId}`);
2211
+ const isCall = coordSpec.mode === 'call';
2212
+ addLog(promptId, 'info', 'system', `Native ${isCall ? 'video call' : 'coordination'} (${coordSpec.reason}) — channel ${coordSpec.channelId}`);
2087
2213
  try {
2088
2214
  // The agent token isn't in process.env — it's injected per-execution
2089
2215
  // (index.ts sets ORQUESTA_TOKEN on injectedCredentials before execute()).
2090
2216
  const orquestaToken = getInjectedCredentials()['ORQUESTA_TOKEN'] || process.env.ORQUESTA_TOKEN || '';
2091
- const summary = await runCoordination(coordSpec, orquestaToken, (line) => {
2217
+ const runner = isCall ? runVideoCall : runCoordination;
2218
+ const summary = await runner(coordSpec, orquestaToken, (line) => {
2092
2219
  addLog(promptId, 'info', 'output', line);
2093
2220
  sendOutput(channel, commandId, 'stdout', line + '\n').catch(() => undefined);
2094
2221
  });
@@ -2098,7 +2225,7 @@ export async function execute(options) {
2098
2225
  await sendComplete(channel, commandId, 0, startTime);
2099
2226
  }
2100
2227
  catch (err) {
2101
- const msg = `Coordination failed: ${err.message}`;
2228
+ const msg = `${coordSpec.mode === 'call' ? 'Video call' : 'Coordination'} failed: ${err.message}`;
2102
2229
  addLog(promptId, 'error', 'error', msg);
2103
2230
  await updatePromptStatus(promptId, 'failed', { text: msg, key_actions: [], files_modified: [] });
2104
2231
  await sendComplete(channel, commandId, 1, startTime);
@@ -2201,7 +2328,7 @@ ${userRequestBody}`;
2201
2328
  // Build environment with authentication
2202
2329
  const env = {
2203
2330
  ...process.env,
2204
- ...injectedCredentials, // Inject credentials from Orquesta
2331
+ ...withSecretFileEnv(injectedCredentials, workingDirectory || process.cwd()), // Inject credentials from Orquesta
2205
2332
  CI: 'true', // Ensure non-interactive mode
2206
2333
  // Expose the current prompt id so Claude can link generated files back
2207
2334
  // to this prompt via /api/agent/files (prompt_id=$ORQUESTA_PROMPT_ID).
@@ -2258,9 +2385,11 @@ ${userRequestBody}`;
2258
2385
  ? (resolveKimiBinary() || 'kimi')
2259
2386
  : cliCommand === 'cursor'
2260
2387
  ? (resolveCursorBinary() || 'cursor-agent')
2261
- : cliCommand === 'claude'
2262
- ? (resolveClaudeBinary() || 'claude')
2263
- : (cliCommand || 'claude');
2388
+ : cliCommand === 'kiro'
2389
+ ? (resolveKiroBinary() || 'kiro-cli')
2390
+ : cliCommand === 'claude'
2391
+ ? (resolveClaudeBinary() || 'claude')
2392
+ : (cliCommand || 'claude');
2264
2393
  const cwd = workingDirectory || process.cwd();
2265
2394
  // Which branch this prompt runs on — captured up front so it's persisted even
2266
2395
  // if the run fails, and broadcast live below once sendSpecialEvent exists.
@@ -2272,16 +2401,16 @@ ${userRequestBody}`;
2272
2401
  let detectedCommitSha = null;
2273
2402
  let detectedCommitUrl = null;
2274
2403
  logger.info(`CLI Selection: ${cliCommand} (${reason})`);
2275
- logger.info(`Spawning: ${cliBinary} -p "${fullContent.slice(0, 50)}..." in ${cwd}${currentBranch ? ` on branch ${currentBranch}` : ''}`);
2404
+ logger.info(`Spawning: ${cliBinary} ${cliCommand === 'kiro' ? 'chat' : '-p'} "${fullContent.slice(0, 50)}..." in ${cwd}${currentBranch ? ` on branch ${currentBranch}` : ''}`);
2276
2405
  // Hoist the static block into the CLI system prompt (claude/orquesta support
2277
2406
  // --append-system-prompt-file; kimi has no such flag and stays inline). On any
2278
2407
  // write failure we fall back to the inline fullContent, so a dispatched prompt
2279
2408
  // can never regress to a worse state than before.
2280
2409
  let promptContent = fullContent;
2281
2410
  let sysPromptFilePath = null;
2282
- // kimi and cursor-agent have no --append-system-prompt-file, so their static
2283
- // context stays inline in the prompt.
2284
- if (cliCommand !== 'kimi' && cliCommand !== 'cursor') {
2411
+ // kimi, cursor-agent and kiro-cli have no --append-system-prompt-file, so
2412
+ // their static context stays inline in the prompt.
2413
+ if (cliCommand !== 'kimi' && cliCommand !== 'cursor' && cliCommand !== 'kiro') {
2285
2414
  try {
2286
2415
  const promptDir = process.platform === 'win32'
2287
2416
  ? os.tmpdir()
@@ -2314,22 +2443,29 @@ ${userRequestBody}`;
2314
2443
  // below only understands Claude's and orquesta-cli's,
2315
2444
  // so asking for it would give us an unparseable
2316
2445
  // stream instead of an answer.
2317
- : cliCommand === 'kimi'
2318
- ? '--output-format text' // Kimi: plain text. NOTE: -p (one-shot) CANNOT be
2319
- // combined with -y ("Cannot combine --prompt with
2320
- // --yolo"), so dispatched kimi prompts respond as
2321
- // chat (no autonomous tool use). Interactive kimi
2322
- // sessions DO get -y (see ptyArgs below).
2323
- : '--verbose --output-format stream-json'; // Claude: stream-json format
2446
+ : cliCommand === 'kiro'
2447
+ ? '--no-interactive --wrap never' // Kiro: `chat` is interactive by default;
2448
+ // --no-interactive answers once and exits, --wrap
2449
+ // never stops it re-flowing the answer to the
2450
+ // (absent) terminal width.
2451
+ : cliCommand === 'kimi'
2452
+ ? '--output-format text' // Kimi: plain text. NOTE: -p (one-shot) CANNOT be
2453
+ // combined with -y ("Cannot combine --prompt with
2454
+ // --yolo"), so dispatched kimi prompts respond as
2455
+ // chat (no autonomous tool use). Interactive kimi
2456
+ // sessions DO get -y (see ptyArgs below).
2457
+ : '--verbose --output-format stream-json'; // Claude: stream-json format
2324
2458
  // Kimi has no --dangerously-skip-permissions, and -p forbids -y. Cursor spells
2325
- // the same thing --force (alias --yolo).
2459
+ // the same thing --force (alias --yolo); kiro spells it --trust-all-tools.
2326
2460
  const permFlags = cliCommand === 'kimi'
2327
2461
  ? ''
2328
2462
  : mode !== 'auto'
2329
2463
  ? ''
2330
2464
  : cliCommand === 'cursor'
2331
2465
  ? '--force'
2332
- : '--dangerously-skip-permissions';
2466
+ : cliCommand === 'kiro'
2467
+ ? '--trust-all-tools'
2468
+ : '--dangerously-skip-permissions';
2333
2469
  // Per-project endpoint override (orquesta CLI only). Single-quoted for the
2334
2470
  // shell-wrapped (Linux) path; pushed as a separate arg on Win/macOS below.
2335
2471
  const useEndpoint = cliCommand === 'orquesta' && !!globalCliEndpoint;
@@ -2341,7 +2477,10 @@ ${userRequestBody}`;
2341
2477
  // Claude CLI's headless `-p` mode does not accept positional file args as
2342
2478
  // attachments — image refs are surfaced inline in the prompt as absolute
2343
2479
  // paths so Claude's Read tool can pick them up.
2344
- command = `${cliBinary} -p '${escapedContent}' ${permFlags} ${baseFlags} ${endpointFlag} ${sysPromptFlag}`;
2480
+ // kiro-cli has no `-p`: the one-shot form is `kiro-cli chat "<prompt>"` with
2481
+ // --no-interactive (baseFlags), the prompt as the verb's positional arg.
2482
+ const promptVerb = cliCommand === 'kiro' ? 'chat' : '-p';
2483
+ command = `${cliBinary} ${promptVerb} '${escapedContent}' ${permFlags} ${baseFlags} ${endpointFlag} ${sysPromptFlag}`;
2345
2484
  if (imageReferences.length > 0) {
2346
2485
  logger.info(`Image attachments referenced inline: ${imageReferences.length}`);
2347
2486
  }
@@ -2354,7 +2493,7 @@ ${userRequestBody}`;
2354
2493
  const isDarwin = process.platform === 'darwin';
2355
2494
  let claude;
2356
2495
  if (isWindows || isDarwin) {
2357
- const args = ['-p'];
2496
+ const args = [promptVerb];
2358
2497
  // Windows cmd.exe caps the whole command line at ~8191 chars. The prompt is
2359
2498
  // prefixed with large context blocks (capabilities, project CLAUDE.md,
2360
2499
  // coordination…), so passing it as an argv overflows that limit and the CLI
@@ -2414,6 +2553,16 @@ ${userRequestBody}`;
2414
2553
  let thinkingBuffer = '';
2415
2554
  let textBuffer = '';
2416
2555
  let toolInputBuffer = '';
2556
+ // Fallbacks for the stored result when the CLI dies without a result event
2557
+ // (killed, crashed, cut for an interactive prompt): the last prose the model
2558
+ // produced, else the tail of whatever raw output there was.
2559
+ let lastAssistantText = '';
2560
+ let rawOutputTail = '';
2561
+ const noteRawOutput = (text) => {
2562
+ const clean = stripAnsiForResult(text);
2563
+ if (clean.trim())
2564
+ rawOutputTail = (rawOutputTail + clean).slice(-4000);
2565
+ };
2417
2566
  // Parse stream-json output format
2418
2567
  // Returns { output: string, hadValidJson: boolean, logs: AgentLogEntry[] }
2419
2568
  const parseStreamJson = (text) => {
@@ -2495,6 +2644,7 @@ ${userRequestBody}`;
2495
2644
  output += `\n${content}\n`;
2496
2645
  logs.push(createOutputLog(content, 'markdown', 'info', promptId));
2497
2646
  promptFinalResult.set(promptId, String(content));
2647
+ lastAssistantText = String(content);
2498
2648
  }
2499
2649
  }
2500
2650
  else if (json.event === 'error') {
@@ -2537,6 +2687,8 @@ ${userRequestBody}`;
2537
2687
  // separate the model's prose from a preceding tool result
2538
2688
  // (the blob otherwise leaves them adjacent with no delimiter).
2539
2689
  output += `\n💬 ${block.text}\n`;
2690
+ if (typeof block.text === 'string' && block.text.trim())
2691
+ lastAssistantText = block.text;
2540
2692
  // Create structured output log
2541
2693
  if (block.text.trim()) {
2542
2694
  logs.push(createOutputLog(block.text, 'markdown', 'info', promptId));
@@ -2639,6 +2791,7 @@ ${userRequestBody}`;
2639
2791
  }
2640
2792
  if (textBuffer.trim()) {
2641
2793
  logs.push(createOutputLog(textBuffer, 'markdown', 'info', promptId));
2794
+ lastAssistantText = textBuffer;
2642
2795
  textBuffer = '';
2643
2796
  }
2644
2797
  if (toolInputBuffer.trim() && currentToolName) {
@@ -2739,9 +2892,44 @@ ${userRequestBody}`;
2739
2892
  sendSpecialEvent('git_branch', { branch: currentBranch });
2740
2893
  }
2741
2894
  // Stream stdout
2895
+ // Watchdog for a CLI (or a tool it ran with a real TTY) that stops to ask a
2896
+ // question. A headless run can never answer, so after `silenceMs` of quiet
2897
+ // following a prompt-looking line we kill it and record WHY — the old
2898
+ // behaviour was to sit until the dispatch timeout and store an empty
2899
+ // result over a log of TUI redraw garbage (`eas login`, 2026-09).
2900
+ const stallSilenceMs = interactivePromptTimeoutMs();
2901
+ const stallDetector = new InteractiveStallDetector({
2902
+ silenceMs: stallSilenceMs,
2903
+ onStall: (label, tail) => {
2904
+ if (!runningProcesses.has(commandId))
2905
+ return;
2906
+ const message = describeInteractiveStall(label, tail, stallSilenceMs);
2907
+ logger.warn(`Interactive prompt detected (${label}) with no output for ${Math.round(stallSilenceMs / 1000)}s — killing run`);
2908
+ promptInteractiveStall.set(promptId, message);
2909
+ addLog(promptId, 'error', 'system', message);
2910
+ sendOutput(channel, commandId, 'stderr', `\n❌ ${message}\n`);
2911
+ try {
2912
+ claude.kill('SIGTERM');
2913
+ }
2914
+ catch { /* already gone */ }
2915
+ setTimeout(() => {
2916
+ if (runningProcesses.has(commandId)) {
2917
+ try {
2918
+ claude.kill('SIGKILL');
2919
+ }
2920
+ catch { /* already gone */ }
2921
+ }
2922
+ }, 5000).unref?.();
2923
+ },
2924
+ });
2742
2925
  if (claude.stdout) {
2743
2926
  claude.stdout.on('data', (data) => {
2744
- const text = cliCommand === 'kimi' ? cleanKimiText(data.toString()) : data.toString();
2927
+ const text = cliCommand === 'kimi'
2928
+ ? cleanKimiText(data.toString())
2929
+ : cliCommand === 'kiro'
2930
+ ? cleanKiroText(data.toString())
2931
+ : data.toString();
2932
+ stallDetector.feed(data.toString());
2745
2933
  // Skip logging if it looks like base64/binary data (long strings without spaces)
2746
2934
  const preview = text.slice(0, 150);
2747
2935
  const looksLikeBinary = preview.length > 50 && !preview.includes(' ') && /^[A-Za-z0-9+/=]+$/.test(preview.replace(/\s/g, ''));
@@ -2756,6 +2944,8 @@ ${userRequestBody}`;
2756
2944
  // Only fallback to raw text if it wasn't valid JSON
2757
2945
  // If it was valid JSON but produced no output (e.g., system init), don't send anything
2758
2946
  const outputToSend = hadValidJson ? parsed : text;
2947
+ if (!hadValidJson)
2948
+ noteRawOutput(text);
2759
2949
  if (outputToSend.trim()) {
2760
2950
  logger.output('stdout', outputToSend.slice(0, 200));
2761
2951
  sendOutput(channel, commandId, 'stdout', outputToSend);
@@ -2867,9 +3057,11 @@ ${userRequestBody}`;
2867
3057
  if (claude.stderr) {
2868
3058
  claude.stderr.on('data', (data) => {
2869
3059
  const raw = data.toString();
3060
+ stallDetector.feed(raw);
2870
3061
  const text = stripAnsiCodes(raw);
2871
3062
  if (!text.trim())
2872
3063
  return;
3064
+ noteRawOutput(text);
2873
3065
  logger.info(`[STDERR] ${text.slice(0, 100)}`);
2874
3066
  logger.output('stderr', text);
2875
3067
  sendOutput(channel, commandId, 'stderr', text);
@@ -2885,6 +3077,7 @@ ${userRequestBody}`;
2885
3077
  // Handle completion
2886
3078
  claude.on('close', async (code) => {
2887
3079
  runningProcesses.delete(commandId);
3080
+ stallDetector.stop();
2888
3081
  // Remove the hoisted system-prompt temp file now that the CLI has read it.
2889
3082
  if (sysPromptFilePath) {
2890
3083
  try {
@@ -2946,7 +3139,12 @@ ${userRequestBody}`;
2946
3139
  if (cliReportedFailure && (code ?? 0) === 0) {
2947
3140
  logger.warn('CLI reported a failed run but exited 0 — recording it as failed');
2948
3141
  }
2949
- const exitCode = (claudeApiError || cliReportedFailure) ? (code || 1) : (code ?? 0);
3142
+ // A run cut for waiting on a keyboard prompt is a failure whatever the
3143
+ // signal-death exit code says (a SIGTERM'd child reports code null).
3144
+ const interactiveStall = promptId ? promptInteractiveStall.get(promptId) : undefined;
3145
+ if (promptId)
3146
+ promptInteractiveStall.delete(promptId);
3147
+ const exitCode = (claudeApiError || cliReportedFailure || interactiveStall) ? (code || 1) : (code ?? 0);
2950
3148
  const duration = Date.now() - startTime;
2951
3149
  const status = exitCode === 0 ? 'completed' : 'failed';
2952
3150
  if (exitCode === 0) {
@@ -2994,8 +3192,25 @@ ${userRequestBody}`;
2994
3192
  promptCostCents.delete(promptId);
2995
3193
  }
2996
3194
  // Update prompt status in database with summary and usage (backend fix - don't rely on frontend)
2997
- const finalResult = promptFinalResult.get(promptId);
3195
+ // Prefer the CLI's own result event; when the run died without one
3196
+ // (killed, crashed, cut for a prompt) still store SOMETHING readable —
3197
+ // the stall explanation, the last assistant prose, or the raw tail —
3198
+ // so the dashboard never shows an empty result over a dead run.
3199
+ let finalResult = promptFinalResult.get(promptId);
2998
3200
  promptFinalResult.delete(promptId);
3201
+ if (!finalResult || !finalResult.trim()) {
3202
+ if (interactiveStall) {
3203
+ finalResult = lastAssistantText.trim()
3204
+ ? `${interactiveStall}\n\nLast assistant message:\n${lastAssistantText.trim()}`
3205
+ : interactiveStall;
3206
+ }
3207
+ else if (lastAssistantText.trim()) {
3208
+ finalResult = lastAssistantText.trim();
3209
+ }
3210
+ else if (rawOutputTail.trim()) {
3211
+ finalResult = `${status === 'failed' ? `Run failed (exit code ${exitCode}) without a result. ` : ''}Last output:\n${rawOutputTail.trim().split('\n').slice(-40).join('\n')}`;
3212
+ }
3213
+ }
2999
3214
  // Record WHICH CLI + model executed it (timeline/history). Model is
3000
3215
  // best-effort — the CLI/proxy pick the true upstream model internally
3001
3216
  // (batuta-auto fans out per prompt), so we report the CLI's MODEL CONFIG:
@@ -3005,13 +3220,16 @@ ${userRequestBody}`;
3005
3220
  ? 'kimi-cli'
3006
3221
  : cliCommand === 'cursor'
3007
3222
  ? 'cursor-cli'
3008
- : 'claude-cli';
3223
+ : cliCommand === 'kiro'
3224
+ ? 'kiro-cli'
3225
+ : 'claude-cli';
3009
3226
  let modelForDb;
3010
3227
  if (cliCommand === 'kimi')
3011
3228
  modelForDb = 'kimi-for-coding';
3012
3229
  // cursor-agent picks the model from the user's own Cursor settings and does
3013
- // not report it in text mode, so leave it unset rather than guess.
3014
- else if (cliCommand === 'cursor')
3230
+ // not report it in text mode, so leave it unset rather than guess. Same for
3231
+ // kiro-cli (its default model is set in the user's Kiro agent config).
3232
+ else if (cliCommand === 'cursor' || cliCommand === 'kiro')
3015
3233
  modelForDb = undefined;
3016
3234
  else if (cliCommand === 'orquesta')
3017
3235
  modelForDb = (globalCliEndpoint === 'batuta' || !globalCliEndpoint) ? 'batuta-auto' : globalCliEndpoint;
@@ -3038,6 +3256,7 @@ ${userRequestBody}`;
3038
3256
  // Handle errors
3039
3257
  claude.on('error', async (err) => {
3040
3258
  runningProcesses.delete(commandId);
3259
+ stallDetector.stop();
3041
3260
  if (promptId)
3042
3261
  promptIdToCommandId.delete(promptId);
3043
3262
  pendingSupervisionCallbacks.delete(commandId);
@@ -3858,7 +4077,7 @@ export async function startSession(options) {
3858
4077
  // Build environment with authentication
3859
4078
  const env = {
3860
4079
  ...process.env,
3861
- ...injectedCredentials,
4080
+ ...withSecretFileEnv(injectedCredentials, cwd),
3862
4081
  // Don't set CI=true for interactive mode - we want full TTY behavior
3863
4082
  };
3864
4083
  if (anthropicApiKey) {
@@ -3888,10 +4107,12 @@ export async function startSession(options) {
3888
4107
  ? (resolveKimiBinary() || 'kimi')
3889
4108
  : cliCommand === 'cursor'
3890
4109
  ? (resolveCursorBinary() || 'cursor-agent')
3891
- : cliCommand === 'claude'
3892
- ? (resolveClaudeBinary() || 'claude')
3893
- : cliCommand;
3894
- const resolvedByPath = cliCommand === 'kimi' || cliCommand === 'cursor';
4110
+ : cliCommand === 'kiro'
4111
+ ? (resolveKiroBinary() || 'kiro-cli')
4112
+ : cliCommand === 'claude'
4113
+ ? (resolveClaudeBinary() || 'claude')
4114
+ : cliCommand;
4115
+ const resolvedByPath = cliCommand === 'kimi' || cliCommand === 'cursor' || cliCommand === 'kiro';
3895
4116
  logger.info(`Interactive session CLI: ${cliCommand}${resolvedByPath ? ` (${cliBinary})` : ''}`);
3896
4117
  // Best-effort model + cli label reported in session:started so the dashboard
3897
4118
  // can tag the interactive prompt (same mapping dispatched prompts use on
@@ -3899,8 +4120,8 @@ export async function startSession(options) {
3899
4120
  // back to ANTHROPIC_MODEL when set, else undefined.
3900
4121
  const sessionModel = cliCommand === 'kimi'
3901
4122
  ? 'kimi-for-coding'
3902
- : cliCommand === 'cursor'
3903
- // cursor-agent uses whatever model the user selected in their own Cursor
4123
+ : cliCommand === 'cursor' || cliCommand === 'kiro'
4124
+ // cursor-agent / kiro-cli use whatever model the user selected in their own
3904
4125
  // account; it isn't knowable from the PTY, so report nothing rather than lie.
3905
4126
  ? undefined
3906
4127
  : cliCommand === 'orquesta'
@@ -3909,7 +4130,8 @@ export async function startSession(options) {
3909
4130
  const sessionCliType = cliCommand === 'orquesta' ? 'orquesta'
3910
4131
  : cliCommand === 'kimi' ? 'kimi'
3911
4132
  : cliCommand === 'cursor' ? 'cursor'
3912
- : 'claude';
4133
+ : cliCommand === 'kiro' ? 'kiro'
4134
+ : 'claude';
3913
4135
  // Filter undefined env values — node-pty requires Record<string, string>.
3914
4136
  // In strict sandbox mode, sandboxEnv() first reduces to the allowlist so the
3915
4137
  // interactive CLI can't read unrelated host secrets from its own env.
@@ -3997,7 +4219,7 @@ export async function startSession(options) {
3997
4219
  // `ps aux`. Default-off because a file unreadable inside bwrap would silently
3998
4220
  // strip the system prompt (degraded session, not a crash) — flip the default
3999
4221
  // only after E2E confirms the sandbox reads it. Rollback = unset the var.
4000
- const wantSysPromptFile = cliCommand !== 'kimi' && cliCommand !== 'cursor' && ((process.platform === 'win32' && cliCommand === 'orquesta') ||
4222
+ const wantSysPromptFile = cliCommand !== 'kimi' && cliCommand !== 'cursor' && cliCommand !== 'kiro' && ((process.platform === 'win32' && cliCommand === 'orquesta') ||
4001
4223
  /^(1|true|yes|on)$/i.test(process.env.ORQUESTA_SYSPROMPT_FILE || ''));
4002
4224
  if (wantSysPromptFile) {
4003
4225
  try {
@@ -4016,7 +4238,22 @@ export async function startSession(options) {
4016
4238
  }
4017
4239
  }
4018
4240
  let ptyArgs;
4019
- if (cliCommand === 'cursor') {
4241
+ if (cliCommand === 'kiro') {
4242
+ // kiro-cli has no --append-system-prompt either; it reads AGENTS.md /
4243
+ // steering files from the working dir. `chat` is the interactive verb and
4244
+ // `--trust-all-tools` its --dangerously-skip-permissions. Resume flags go
4245
+ // AFTER the verb (see below) — the generic unshift would put them before it.
4246
+ ptyArgs = ['chat', '--trust-all-tools'];
4247
+ if (resumeSessionId) {
4248
+ ptyArgs.push('--resume-id', resumeSessionId);
4249
+ logger.info(`[Session] Resuming conversation ${resumeSessionId} (--resume-id) for kiro`);
4250
+ }
4251
+ else if (resume) {
4252
+ ptyArgs.push('--resume');
4253
+ logger.info('[Session] Resuming most recent conversation in this directory (--resume) for kiro');
4254
+ }
4255
+ }
4256
+ else if (cliCommand === 'cursor') {
4020
4257
  // cursor-agent has no --append-system-prompt; it reads AGENTS.md / CLAUDE.md
4021
4258
  // from the working dir (which the agent already syncs), so the capabilities
4022
4259
  // block isn't injected. `--force` is its --dangerously-skip-permissions.
@@ -4039,7 +4276,7 @@ export async function startSession(options) {
4039
4276
  // cli.ts) both reattach the latest session non-interactively; kimi has no
4040
4277
  // resume flag, so the option is silently ignored there. Prepended so it's an
4041
4278
  // unambiguous standalone flag, separate from --append-system-prompt's value.
4042
- if ((resume || resumeSessionId) && cliCommand !== 'kimi') {
4279
+ if ((resume || resumeSessionId) && cliCommand !== 'kimi' && cliCommand !== 'kiro') {
4043
4280
  // cursor-agent spells these the same way claude does (--continue / --resume <id>).
4044
4281
  // claude can target a SPECIFIC past conversation by id (--resume <id>);
4045
4282
  // orquesta-cli only reattaches the latest (--continue). So when a specific
@@ -4183,7 +4420,7 @@ export async function startSession(options) {
4183
4420
  startTime,
4184
4421
  subAgentId,
4185
4422
  subAgentName,
4186
- cliType: cliCommand === 'orquesta' ? 'orquesta' : cliCommand === 'kimi' ? 'kimi' : 'claude',
4423
+ cliType: sessionCliType,
4187
4424
  isActive: true,
4188
4425
  pendingPhoneBlock: '',
4189
4426
  pendingPhoneTimer: null,