ax-agents 0.1.5 → 0.1.7

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 (2) hide show
  1. package/ax.js +625 -212
  2. package/package.json +1 -1
package/ax.js CHANGED
@@ -36,7 +36,9 @@ import { parseArgs, styleText } from "node:util";
36
36
 
37
37
  const __filename = fileURLToPath(import.meta.url);
38
38
  const __dirname = path.dirname(__filename);
39
- const packageJson = JSON.parse(readFileSync(path.join(__dirname, "package.json"), "utf-8"));
39
+ const packageJson = JSON.parse(
40
+ readFileSync(path.join(__dirname, "package.json"), "utf-8")
41
+ );
40
42
  const VERSION = packageJson.version;
41
43
 
42
44
  /**
@@ -228,7 +230,7 @@ function debugError(context, err) {
228
230
  if (DEBUG) {
229
231
  const msg = err instanceof Error ? err.message : err;
230
232
  console.error(
231
- `${COLORS.bright}${COLORS.red}[error:${context}]${COLORS.reset} ${COLORS.magenta}${msg}${COLORS.reset}`,
233
+ `${COLORS.bright}${COLORS.red}[error:${context}]${COLORS.reset} ${COLORS.magenta}${msg}${COLORS.reset}`
232
234
  );
233
235
  }
234
236
  }
@@ -241,7 +243,7 @@ function debugError(context, err) {
241
243
  function debug(tag, message) {
242
244
  if (DEBUG) {
243
245
  console.error(
244
- `${COLORS.bright}${COLORS.cyan}[${tag}]${COLORS.reset} ${COLORS.yellow}${message}${COLORS.reset}`,
246
+ `${COLORS.bright}${COLORS.cyan}[${tag}]${COLORS.reset} ${COLORS.yellow}${message}${COLORS.reset}`
245
247
  );
246
248
  }
247
249
  }
@@ -291,7 +293,9 @@ function getDoProgressPath(name = "default") {
291
293
  */
292
294
  function buildDoPrompt(userPrompt, name) {
293
295
  const progressPath = getDoProgressPath(name);
294
- const progress = existsSync(progressPath) ? readFileSync(progressPath, "utf-8") : "";
296
+ const progress = existsSync(progressPath)
297
+ ? readFileSync(progressPath, "utf-8")
298
+ : "";
295
299
 
296
300
  const relProgressPath = `.ai/do/${name}/progress.txt`;
297
301
  const preamble = DO_PREAMBLE.replace(/\{progressPath\}/g, relProgressPath);
@@ -379,9 +383,14 @@ function tmuxSendLiteral(session, text) {
379
383
  * @param {string} text
380
384
  */
381
385
  function tmuxPasteLiteral(session, text) {
382
- debug("tmux", `pasteLiteral session=${session}, text=${text.slice(0, 50)}...`);
386
+ debug(
387
+ "tmux",
388
+ `pasteLiteral session=${session}, text=${text.slice(0, 50)}...`
389
+ );
383
390
  // Use unique buffer name per invocation to avoid races (even to same session)
384
- const bufferName = `ax-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
391
+ const bufferName = `ax-${process.pid}-${Date.now()}-${Math.random()
392
+ .toString(36)
393
+ .slice(2, 8)}`;
385
394
  // Load text into named tmux buffer from stdin
386
395
  const loadResult = spawnSync("tmux", ["load-buffer", "-b", bufferName, "-"], {
387
396
  input: text,
@@ -423,7 +432,10 @@ async function tmuxSendText(session, text) {
423
432
  // For multiline text in Claude, use adaptive delay based on paste size
424
433
  if (isClaude && newlineCount > 0) {
425
434
  const delay = Math.min(1500, 50 + 3 * text.length + 20 * newlineCount);
426
- debug("sendText", `multiline paste (${text.length} chars, ${newlineCount} lines), waiting ${delay}ms`);
435
+ debug(
436
+ "sendText",
437
+ `multiline paste (${text.length} chars, ${newlineCount} lines), waiting ${delay}ms`
438
+ );
427
439
  await sleep(delay);
428
440
  }
429
441
  tmuxSend(session, "Enter");
@@ -464,9 +476,13 @@ function tmuxRenameSession(oldName, newName) {
464
476
  function tmuxNewSession(session, command) {
465
477
  debug("tmux", `newSession: ${session}, command: ${command.slice(0, 80)}...`);
466
478
  // Use spawnSync to avoid command injection via session/command
467
- const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
468
- encoding: "utf-8",
469
- });
479
+ const result = spawnSync(
480
+ "tmux",
481
+ ["new-session", "-d", "-s", session, command],
482
+ {
483
+ encoding: "utf-8",
484
+ }
485
+ );
470
486
  if (result.status !== 0) {
471
487
  debug("tmux", `newSession failed: ${result.stderr}`);
472
488
  throw new Error(result.stderr || "tmux new-session failed");
@@ -557,22 +573,36 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
557
573
 
558
574
  const POLL_MS = parseInt(process.env.AX_POLL_MS || "200", 10);
559
575
  const DEFAULT_TIMEOUT_MS = parseInt(process.env.AX_TIMEOUT_MS || "120000", 10);
560
- const REVIEW_TIMEOUT_MS = parseInt(process.env.AX_REVIEW_TIMEOUT_MS || "900000", 10); // 15 minutes
561
- const STARTUP_TIMEOUT_MS = parseInt(process.env.AX_STARTUP_TIMEOUT_MS || "30000", 10);
576
+ const REVIEW_TIMEOUT_MS = parseInt(
577
+ process.env.AX_REVIEW_TIMEOUT_MS || "900000",
578
+ 10
579
+ ); // 15 minutes
580
+ const STARTUP_TIMEOUT_MS = parseInt(
581
+ process.env.AX_STARTUP_TIMEOUT_MS || "30000",
582
+ 10
583
+ );
562
584
  const ARCHANGEL_STARTUP_TIMEOUT_MS = parseInt(
563
585
  process.env.AX_ARCHANGEL_STARTUP_TIMEOUT_MS || "60000",
564
- 10,
586
+ 10
565
587
  );
566
588
  const ARCHANGEL_RESPONSE_TIMEOUT_MS = parseInt(
567
589
  process.env.AX_ARCHANGEL_RESPONSE_TIMEOUT_MS || "300000",
568
- 10,
590
+ 10
569
591
  ); // 5 minutes
570
- const ARCHANGEL_HEALTH_CHECK_MS = parseInt(process.env.AX_ARCHANGEL_HEALTH_CHECK_MS || "30000", 10);
592
+ const ARCHANGEL_HEALTH_CHECK_MS = parseInt(
593
+ process.env.AX_ARCHANGEL_HEALTH_CHECK_MS || "30000",
594
+ 10
595
+ );
571
596
  const STABLE_MS = parseInt(process.env.AX_STABLE_MS || "1000", 10);
572
597
  const APPROVE_DELAY_MS = parseInt(process.env.AX_APPROVE_DELAY_MS || "100", 10);
573
- const MAILBOX_MAX_AGE_MS = parseInt(process.env.AX_MAILBOX_MAX_AGE_MS || "3600000", 10); // 1 hour
574
- const CLAUDE_CONFIG_DIR = process.env.AX_CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
575
- const CODEX_CONFIG_DIR = process.env.AX_CODEX_CONFIG_DIR || path.join(os.homedir(), ".codex");
598
+ const MAILBOX_MAX_AGE_MS = parseInt(
599
+ process.env.AX_MAILBOX_MAX_AGE_MS || "3600000",
600
+ 10
601
+ ); // 1 hour
602
+ const CLAUDE_CONFIG_DIR =
603
+ process.env.AX_CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
604
+ const CODEX_CONFIG_DIR =
605
+ process.env.AX_CODEX_CONFIG_DIR || path.join(os.homedir(), ".codex");
576
606
  const TRUNCATE_USER_LEN = 500;
577
607
  const TRUNCATE_THINKING_LEN = 300;
578
608
  const ARCHANGEL_GIT_CONTEXT_HOURS = 4;
@@ -609,20 +639,23 @@ const RFP_PREAMBLE = `## Guidelines
609
639
  // Note: DO_PREAMBLE is a template - {progressPath} gets replaced at runtime
610
640
  const DO_PREAMBLE = `You are an autonomous coding agent in a loop. Each iteration:
611
641
 
612
- 1. Read {progressPath} to see what's done
613
- 2. Choose the highest priority remaining task
614
- 3. Implement ONE small feature/fix
615
- 4. Run feedback loops (tests, types, lint)
616
- 5. Commit your changes with a clear message
617
- 6. Append to {progressPath} what you did
618
- 7. If ALL tasks are complete, output: <promise>COMPLETE</promise>
642
+ 1. Read {progressPath} to see what's done.
643
+ 2. Choose the next task:
644
+ - Start with trivial/mechanistic work. It banks progress, builds context, and constrains nothing.
645
+ - Then do foundational work that makes harder problems easier and safer to approach.
646
+ - Defer risky/architectural decisions until they resolve themselves or there's no other way.
647
+ - If a change is hard to reverse, stop. Surface it as a decision for review.
648
+ 3. Read existing code before modifying it.
649
+ 4. Implement ONE small change - minimal diff.
650
+ 5. Verify the change works. Run typechecking, linting and relevant tests.
651
+ 6. Append to {progressPath}: task done + files changed.
652
+ 7. If ALL tasks complete, output: <promise>COMPLETE</promise>.
619
653
 
620
654
  Guidelines:
621
- - Work on ONE task per iteration, keep changes small
622
- - Always run tests before committing - do NOT commit if tests fail
623
- - Update {progressPath} BEFORE outputting COMPLETE
624
- - Prioritize risky/architectural work first
625
- - If stuck, document the blocker in {progressPath}`;
655
+ - Make minimal changes. Don't refactor surrounding code.
656
+ - DO extract a shared abstraction when you're repeating a decision that's an invariant, not tidying.
657
+ - If stuck after 2-3 attempts, document the blocker in {progressPath} and move on.
658
+ - Update {progressPath} BEFORE outputting COMPLETE.`;
626
659
 
627
660
  /**
628
661
  * @param {string} session
@@ -670,9 +703,13 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
670
703
  function findCallerAgent() {
671
704
  let pid = process.ppid;
672
705
  while (pid > 1) {
673
- const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
674
- encoding: "utf-8",
675
- });
706
+ const result = spawnSync(
707
+ "ps",
708
+ ["-p", pid.toString(), "-o", "ppid=,comm="],
709
+ {
710
+ encoding: "utf-8",
711
+ }
712
+ );
676
713
  if (result.status !== 0) break;
677
714
  const parts = result.stdout.trim().split(/\s+/);
678
715
  const ppid = parseInt(parts[0], 10);
@@ -843,14 +880,18 @@ function parseCliArgs(args) {
843
880
  help: Boolean(values.help),
844
881
  tool: /** @type {string | undefined} */ (values.tool),
845
882
  session: /** @type {string | undefined} */ (values.session),
846
- timeout: values.timeout !== undefined ? Number(values.timeout) : undefined,
883
+ timeout:
884
+ values.timeout !== undefined ? Number(values.timeout) : undefined,
847
885
  tail: values.tail !== undefined ? Number(values.tail) : undefined,
848
886
  limit: values.limit !== undefined ? Number(values.limit) : undefined,
849
887
  branch: /** @type {string | undefined} */ (values.branch),
850
888
  archangels: /** @type {string | undefined} */ (values.archangels),
851
889
  autoApprove: /** @type {string | undefined} */ (values["auto-approve"]),
852
890
  name: /** @type {string | undefined} */ (values.name),
853
- maxLoops: values["max-loops"] !== undefined ? Number(values["max-loops"]) : undefined,
891
+ maxLoops:
892
+ values["max-loops"] !== undefined
893
+ ? Number(values["max-loops"])
894
+ : undefined,
854
895
  loop: Boolean(values.loop),
855
896
  reset: Boolean(values.reset),
856
897
  },
@@ -862,7 +903,8 @@ function parseCliArgs(args) {
862
903
  // =============================================================================
863
904
 
864
905
  // Regex pattern strings for session name parsing
865
- const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
906
+ const UUID_PATTERN =
907
+ "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
866
908
  const PERM_HASH_PATTERN = "[0-9a-f]{8}";
867
909
 
868
910
  /**
@@ -877,7 +919,10 @@ function parseSessionName(session) {
877
919
  const rest = match[2];
878
920
 
879
921
  // Archangel: {tool}-archangel-{name}-{uuid}
880
- const archangelPattern = new RegExp(`^archangel-(.+)-(${UUID_PATTERN})$`, "i");
922
+ const archangelPattern = new RegExp(
923
+ `^archangel-(.+)-(${UUID_PATTERN})$`,
924
+ "i"
925
+ );
881
926
  const archangelMatch = rest.match(archangelPattern);
882
927
  if (archangelMatch) {
883
928
  return { tool, archangelName: archangelMatch[1], uuid: archangelMatch[2] };
@@ -886,7 +931,7 @@ function parseSessionName(session) {
886
931
  // Partner: {tool}-partner-{uuid}[-p{hash}|-yolo]
887
932
  const partnerPattern = new RegExp(
888
933
  `^partner-(${UUID_PATTERN})(?:-p(${PERM_HASH_PATTERN})|-(yolo))?$`,
889
- "i",
934
+ "i"
890
935
  );
891
936
  const partnerMatch = rest.match(partnerPattern);
892
937
  if (partnerMatch) {
@@ -977,7 +1022,7 @@ function getTmuxSessionCwd(sessionName) {
977
1022
  ["display-message", "-t", sessionName, "-p", "#{pane_current_path}"],
978
1023
  {
979
1024
  encoding: "utf-8",
980
- },
1025
+ }
981
1026
  );
982
1027
  if (result.status === 0) return result.stdout.trim();
983
1028
  } catch (err) {
@@ -995,8 +1040,15 @@ function findClaudeLogPath(sessionId, sessionName) {
995
1040
  // Get cwd from tmux session, fall back to process.cwd()
996
1041
  const cwd = (sessionName && getTmuxSessionCwd(sessionName)) || process.cwd();
997
1042
  const projectPath = getClaudeProjectPath(cwd);
998
- const claudeProjectDir = path.join(CLAUDE_CONFIG_DIR, "projects", projectPath);
999
- debug("log", `findClaudeLogPath: sessionId=${sessionId}, projectDir=${claudeProjectDir}`);
1043
+ const claudeProjectDir = path.join(
1044
+ CLAUDE_CONFIG_DIR,
1045
+ "projects",
1046
+ projectPath
1047
+ );
1048
+ debug(
1049
+ "log",
1050
+ `findClaudeLogPath: sessionId=${sessionId}, projectDir=${claudeProjectDir}`
1051
+ );
1000
1052
 
1001
1053
  // Check sessions-index.json first
1002
1054
  const indexPath = path.join(claudeProjectDir, "sessions-index.json");
@@ -1004,7 +1056,8 @@ function findClaudeLogPath(sessionId, sessionName) {
1004
1056
  try {
1005
1057
  const index = JSON.parse(readFileSync(indexPath, "utf-8"));
1006
1058
  const entry = index.entries?.find(
1007
- /** @param {{sessionId: string, fullPath?: string}} e */ (e) => e.sessionId === sessionId,
1059
+ /** @param {{sessionId: string, fullPath?: string}} e */ (e) =>
1060
+ e.sessionId === sessionId
1008
1061
  );
1009
1062
  if (entry?.fullPath) {
1010
1063
  debug("log", `findClaudeLogPath: found via index -> ${entry.fullPath}`);
@@ -1034,7 +1087,11 @@ function findClaudeLogPath(sessionId, sessionName) {
1034
1087
  function findNewestClaudeSessionUuid(sessionName) {
1035
1088
  const cwd = getTmuxSessionCwd(sessionName) || process.cwd();
1036
1089
  const projectPath = getClaudeProjectPath(cwd);
1037
- const claudeProjectDir = path.join(CLAUDE_CONFIG_DIR, "projects", projectPath);
1090
+ const claudeProjectDir = path.join(
1091
+ CLAUDE_CONFIG_DIR,
1092
+ "projects",
1093
+ projectPath
1094
+ );
1038
1095
  const indexPath = path.join(claudeProjectDir, "sessions-index.json");
1039
1096
 
1040
1097
  if (!existsSync(indexPath)) {
@@ -1076,7 +1133,7 @@ function findCodexLogPath(sessionName) {
1076
1133
  ["display-message", "-t", sessionName, "-p", "#{session_created}"],
1077
1134
  {
1078
1135
  encoding: "utf-8",
1079
- },
1136
+ }
1080
1137
  );
1081
1138
  if (result.status !== 0) {
1082
1139
  debug("log", `findCodexLogPath: tmux display-message failed`);
@@ -1114,7 +1171,9 @@ function findCodexLogPath(sessionName) {
1114
1171
 
1115
1172
  for (const file of files) {
1116
1173
  // Parse timestamp from filename: rollout-2026-01-22T13-05-15-UUID.jsonl
1117
- const match = file.match(/^rollout-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-/);
1174
+ const match = file.match(
1175
+ /^rollout-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-/
1176
+ );
1118
1177
  if (!match) continue;
1119
1178
 
1120
1179
  const [, y, mo, d, h, mi, s] = match;
@@ -1140,7 +1199,7 @@ function findCodexLogPath(sessionName) {
1140
1199
  candidates.sort((a, b) => a.diff - b.diff);
1141
1200
  debug(
1142
1201
  "log",
1143
- `findCodexLogPath: found ${candidates.length} candidates, best: ${candidates[0].path}`,
1202
+ `findCodexLogPath: found ${candidates.length} candidates, best: ${candidates[0].path}`
1144
1203
  );
1145
1204
  return candidates[0].path;
1146
1205
  } catch {
@@ -1230,7 +1289,12 @@ function formatTodos(todos) {
1230
1289
  if (!todos || todos.length === 0) return "";
1231
1290
  return todos
1232
1291
  .map((t) => {
1233
- const status = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[>]" : "[ ]";
1292
+ const status =
1293
+ t.status === "completed"
1294
+ ? "[x]"
1295
+ : t.status === "in_progress"
1296
+ ? "[>]"
1297
+ : "[ ]";
1234
1298
  return `${status} ${t.content || "(no content)"}`;
1235
1299
  })
1236
1300
  .join("\n");
@@ -1254,7 +1318,11 @@ function getAssistantText(logPath, index = 0) {
1254
1318
  const assistantTexts = [];
1255
1319
  const needed = Math.abs(index) + 1;
1256
1320
 
1257
- for (let i = lines.length - 1; i >= 0 && assistantTexts.length < needed; i--) {
1321
+ for (
1322
+ let i = lines.length - 1;
1323
+ i >= 0 && assistantTexts.length < needed;
1324
+ i--
1325
+ ) {
1258
1326
  try {
1259
1327
  const entry = JSON.parse(lines[i]);
1260
1328
  if (entry.type === "assistant") {
@@ -1361,7 +1429,10 @@ function formatClaudeLogEntry(entry) {
1361
1429
  let summary;
1362
1430
  if (name === "Bash" && input.command) {
1363
1431
  summary = input.command.slice(0, 50);
1364
- } else if (name === "Task" && (input.description || input.subagent_type)) {
1432
+ } else if (
1433
+ name === "Task" &&
1434
+ (input.description || input.subagent_type)
1435
+ ) {
1365
1436
  // Task tool: show description or subagent type
1366
1437
  summary = input.description || input.subagent_type || "";
1367
1438
  summary = summary.slice(0, 40);
@@ -1388,12 +1459,18 @@ function formatClaudeLogEntry(entry) {
1388
1459
  */
1389
1460
  function formatCodexLogEntry(entry) {
1390
1461
  // Skip function_call_output entries (equivalent to tool_result - can be verbose)
1391
- if (entry.type === "response_item" && entry.payload?.type === "function_call_output") {
1462
+ if (
1463
+ entry.type === "response_item" &&
1464
+ entry.payload?.type === "function_call_output"
1465
+ ) {
1392
1466
  return null;
1393
1467
  }
1394
1468
 
1395
1469
  // Handle function calls
1396
- if (entry.type === "response_item" && entry.payload?.type === "function_call") {
1470
+ if (
1471
+ entry.type === "response_item" &&
1472
+ entry.payload?.type === "function_call"
1473
+ ) {
1397
1474
  const name = entry.payload.name || "tool";
1398
1475
  let summary = "";
1399
1476
  try {
@@ -1454,7 +1531,10 @@ function formatCodexLogEntry(entry) {
1454
1531
  * @returns {TerminalLine[]}
1455
1532
  */
1456
1533
  function parseJsonlEntry(entry, format) {
1457
- const segments = format === "claude" ? formatClaudeLogEntry(entry) : formatCodexLogEntry(entry);
1534
+ const segments =
1535
+ format === "claude"
1536
+ ? formatClaudeLogEntry(entry)
1537
+ : formatCodexLogEntry(entry);
1458
1538
  if (!segments) return [];
1459
1539
 
1460
1540
  // Convert segments to TerminalLines, splitting multiline content
@@ -1686,7 +1766,9 @@ function findMatch(lines, query) {
1686
1766
  const styleMatches = line.spans.some((span) => {
1687
1767
  if (!span.style) return false;
1688
1768
  const spanMatchesPattern =
1689
- typeof pattern === "string" ? span.text.includes(pattern) : pattern.test(span.text);
1769
+ typeof pattern === "string"
1770
+ ? span.text.includes(pattern)
1771
+ : pattern.test(span.text);
1690
1772
  if (!spanMatchesPattern) return false;
1691
1773
 
1692
1774
  // Check each requested style property
@@ -2058,7 +2140,7 @@ function extractPendingToolFromScreen(screen) {
2058
2140
  const line = lines[i];
2059
2141
  // Match tool confirmation patterns like "Bash: command" or "Write: /path/file"
2060
2142
  const match = line.match(
2061
- /^\s*(Bash|Write|Edit|Read|Glob|Grep|Task|WebFetch|WebSearch|NotebookEdit|Skill|TodoWrite|TodoRead):\s*(.{1,40})/,
2143
+ /^\s*(Bash|Write|Edit|Read|Glob|Grep|Task|WebFetch|WebSearch|NotebookEdit|Skill|TodoWrite|TodoRead):\s*(.{1,40})/
2062
2144
  );
2063
2145
  if (match) {
2064
2146
  return `${match[1]}: ${match[2].trim()}`;
@@ -2108,7 +2190,10 @@ function resolveSessionName(partial) {
2108
2190
 
2109
2191
  const sessions = tmuxListSessions();
2110
2192
  const agentSessions = sessions.filter((s) => parseSessionName(s));
2111
- debug("session", `resolving "${partial}" from ${agentSessions.length} agent sessions`);
2193
+ debug(
2194
+ "session",
2195
+ `resolving "${partial}" from ${agentSessions.length} agent sessions`
2196
+ );
2112
2197
 
2113
2198
  // Exact match
2114
2199
  if (agentSessions.includes(partial)) {
@@ -2187,7 +2272,11 @@ function loadAgentConfigs() {
2187
2272
  }
2188
2273
  if (config) configs.push(config);
2189
2274
  } catch (err) {
2190
- console.error(`ERROR: Failed to read ${file}: ${err instanceof Error ? err.message : err}`);
2275
+ console.error(
2276
+ `ERROR: Failed to read ${file}: ${
2277
+ err instanceof Error ? err.message : err
2278
+ }`
2279
+ );
2191
2280
  }
2192
2281
  }
2193
2282
 
@@ -2206,7 +2295,9 @@ function parseAgentConfig(filename, content) {
2206
2295
  const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
2207
2296
 
2208
2297
  // Parse frontmatter
2209
- const frontmatterMatch = normalized.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
2298
+ const frontmatterMatch = normalized.match(
2299
+ /^---\n([\s\S]*?)\n---\n([\s\S]*)$/
2300
+ );
2210
2301
  if (!frontmatterMatch) {
2211
2302
  if (!normalized.startsWith("---")) {
2212
2303
  return { error: `Missing frontmatter. File must start with '---'` };
@@ -2230,17 +2321,22 @@ function parseAgentConfig(filename, content) {
2230
2321
  const knownFields = ["tool", "interval", "watch"];
2231
2322
 
2232
2323
  // Check for unknown fields (likely typos)
2233
- const fieldLines = frontmatter.split("\n").filter((line) => /^\w+:/.test(line.trim()));
2324
+ const fieldLines = frontmatter
2325
+ .split("\n")
2326
+ .filter((line) => /^\w+:/.test(line.trim()));
2234
2327
  for (const line of fieldLines) {
2235
2328
  const fieldName = line.trim().match(/^(\w+):/)?.[1];
2236
2329
  if (fieldName && !knownFields.includes(fieldName)) {
2237
2330
  // Suggest closest match
2238
2331
  const suggestions = knownFields.filter(
2239
- (f) => f[0] === fieldName[0] || fieldName.includes(f.slice(0, 3)),
2332
+ (f) => f[0] === fieldName[0] || fieldName.includes(f.slice(0, 3))
2240
2333
  );
2241
- const hint = suggestions.length > 0 ? ` Did you mean '${suggestions[0]}'?` : "";
2334
+ const hint =
2335
+ suggestions.length > 0 ? ` Did you mean '${suggestions[0]}'?` : "";
2242
2336
  return {
2243
- error: `Unknown field '${fieldName}'.${hint} Valid fields: ${knownFields.join(", ")}`,
2337
+ error: `Unknown field '${fieldName}'.${hint} Valid fields: ${knownFields.join(
2338
+ ", "
2339
+ )}`,
2244
2340
  };
2245
2341
  }
2246
2342
  }
@@ -2283,7 +2379,9 @@ function parseAgentConfig(filename, content) {
2283
2379
  error: `Empty watch array. Add at least one pattern: watch: ["**/*"]`,
2284
2380
  };
2285
2381
  }
2286
- watchPatterns = inner.split(",").map((p) => p.trim().replace(/^["']|["']$/g, ""));
2382
+ watchPatterns = inner
2383
+ .split(",")
2384
+ .map((p) => p.trim().replace(/^["']|["']$/g, ""));
2287
2385
  // Validate patterns aren't empty
2288
2386
  if (watchPatterns.some((p) => !p)) {
2289
2387
  return {
@@ -2346,7 +2444,9 @@ function generateRfpId(parent) {
2346
2444
  const mi = String(now.getMinutes()).padStart(2, "0");
2347
2445
  const s = String(now.getSeconds()).padStart(2, "0");
2348
2446
  const ts = `${y}-${mo}-${d}-${h}-${mi}-${s}`;
2349
- const base = parent?.uuid ? parent.uuid.split("-")[0] : randomUUID().split("-")[0];
2447
+ const base = parent?.uuid
2448
+ ? parent.uuid.split("-")[0]
2449
+ : randomUUID().split("-")[0];
2350
2450
  const suffix = randomUUID().split("-")[0].slice(0, 4);
2351
2451
  return `rfp-${base}-${ts}-${suffix}`.toLowerCase();
2352
2452
  }
@@ -2398,11 +2498,18 @@ function writeToMailbox(payload, type = "observation") {
2398
2498
  * @param {number} [options.limit]
2399
2499
  * @returns {MailboxEntry[]}
2400
2500
  */
2401
- function readMailbox({ maxAge = MAILBOX_MAX_AGE_MS, branch = null, limit = 10 } = {}) {
2501
+ function readMailbox({
2502
+ maxAge = MAILBOX_MAX_AGE_MS,
2503
+ branch = null,
2504
+ limit = 10,
2505
+ } = {}) {
2402
2506
  if (!existsSync(MAILBOX_PATH)) return [];
2403
2507
 
2404
2508
  const now = Date.now();
2405
- const lines = readFileSync(MAILBOX_PATH, "utf-8").trim().split("\n").filter(Boolean);
2509
+ const lines = readFileSync(MAILBOX_PATH, "utf-8")
2510
+ .trim()
2511
+ .split("\n")
2512
+ .filter(Boolean);
2406
2513
  /** @type {MailboxEntry[]} */
2407
2514
  const entries = [];
2408
2515
 
@@ -2436,7 +2543,10 @@ function gcMailbox(maxAgeHours = 24) {
2436
2543
 
2437
2544
  const now = Date.now();
2438
2545
  const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
2439
- const lines = readFileSync(MAILBOX_PATH, "utf-8").trim().split("\n").filter(Boolean);
2546
+ const lines = readFileSync(MAILBOX_PATH, "utf-8")
2547
+ .trim()
2548
+ .split("\n")
2549
+ .filter(Boolean);
2440
2550
  const kept = [];
2441
2551
 
2442
2552
  for (const line of lines) {
@@ -2526,14 +2636,21 @@ function getRecentCommitsDiff(hoursAgo = 4) {
2526
2636
  const since = `--since="${hoursAgo} hours ago"`;
2527
2637
 
2528
2638
  // Get list of commits in range
2529
- const commits = execSync(`git log ${mainBranch}..HEAD ${since} --oneline 2>/dev/null`, {
2530
- encoding: "utf-8",
2531
- }).trim();
2639
+ const commits = execSync(
2640
+ `git log ${mainBranch}..HEAD ${since} --oneline 2>/dev/null`,
2641
+ {
2642
+ encoding: "utf-8",
2643
+ }
2644
+ ).trim();
2532
2645
 
2533
2646
  if (!commits) return "";
2534
2647
 
2535
2648
  // Get diff for those commits
2536
- const firstCommit = commits.split("\n").filter(Boolean).pop()?.split(" ")[0];
2649
+ const firstCommit = commits
2650
+ .split("\n")
2651
+ .filter(Boolean)
2652
+ .pop()
2653
+ ?.split(" ")[0];
2537
2654
  if (!firstCommit) return "";
2538
2655
  return execSync(`git diff ${firstCommit}^..HEAD 2>/dev/null`, {
2539
2656
  encoding: "utf-8",
@@ -2568,17 +2685,30 @@ function buildGitContext(hoursAgo = 4, maxLinesPerSection = 200) {
2568
2685
 
2569
2686
  const staged = truncateDiff(getStagedDiff(), maxLinesPerSection);
2570
2687
  if (staged) {
2571
- sections.push("## Staged Changes (about to be committed)\n```diff\n" + staged + "\n```");
2688
+ sections.push(
2689
+ "## Staged Changes (about to be committed)\n```diff\n" + staged + "\n```"
2690
+ );
2572
2691
  }
2573
2692
 
2574
2693
  const uncommitted = truncateDiff(getUncommittedDiff(), maxLinesPerSection);
2575
2694
  if (uncommitted) {
2576
- sections.push("## Uncommitted Changes (work in progress)\n```diff\n" + uncommitted + "\n```");
2695
+ sections.push(
2696
+ "## Uncommitted Changes (work in progress)\n```diff\n" +
2697
+ uncommitted +
2698
+ "\n```"
2699
+ );
2577
2700
  }
2578
2701
 
2579
- const recent = truncateDiff(getRecentCommitsDiff(hoursAgo), maxLinesPerSection);
2702
+ const recent = truncateDiff(
2703
+ getRecentCommitsDiff(hoursAgo),
2704
+ maxLinesPerSection
2705
+ );
2580
2706
  if (recent) {
2581
- sections.push(`## Recent Commits (last ${hoursAgo} hours)\n\`\`\`diff\n` + recent + "\n```");
2707
+ sections.push(
2708
+ `## Recent Commits (last ${hoursAgo} hours)\n\`\`\`diff\n` +
2709
+ recent +
2710
+ "\n```"
2711
+ );
2582
2712
  }
2583
2713
 
2584
2714
  return sections.join("\n\n");
@@ -2637,10 +2767,16 @@ function findCurrentClaudeSession() {
2637
2767
 
2638
2768
  // Also check non-tmux Claude sessions by scanning the project's log directory
2639
2769
  const projectPath = getClaudeProjectPath(cwd);
2640
- const claudeProjectDir = path.join(CLAUDE_CONFIG_DIR, "projects", projectPath);
2770
+ const claudeProjectDir = path.join(
2771
+ CLAUDE_CONFIG_DIR,
2772
+ "projects",
2773
+ projectPath
2774
+ );
2641
2775
  if (existsSync(claudeProjectDir)) {
2642
2776
  try {
2643
- const files = readdirSync(claudeProjectDir).filter((f) => f.endsWith(".jsonl"));
2777
+ const files = readdirSync(claudeProjectDir).filter((f) =>
2778
+ f.endsWith(".jsonl")
2779
+ );
2644
2780
  for (const file of files) {
2645
2781
  const uuid = file.replace(".jsonl", "");
2646
2782
  // Skip if we already have this from tmux sessions
@@ -2718,7 +2854,9 @@ function getParentSessionContext(maxEntries = 20) {
2718
2854
 
2719
2855
  // Look for plan file path in the log content
2720
2856
  if (!planPath) {
2721
- const planMatch = line.match(/\/Users\/[^"]+\/\.claude\/plans\/[^"]+\.md/);
2857
+ const planMatch = line.match(
2858
+ /\/Users\/[^"]+\/\.claude\/plans\/[^"]+\.md/
2859
+ );
2722
2860
  if (planMatch) planPath = planMatch[0];
2723
2861
  }
2724
2862
 
@@ -2729,7 +2867,8 @@ function getParentSessionContext(maxEntries = 20) {
2729
2867
  entries.push({ type: "user", text: c });
2730
2868
  } else if (Array.isArray(c)) {
2731
2869
  const text = c.find(
2732
- /** @param {{type: string, text?: string}} x */ (x) => x.type === "text",
2870
+ /** @param {{type: string, text?: string}} x */ (x) =>
2871
+ x.type === "text"
2733
2872
  )?.text;
2734
2873
  if (text && text.length > 10) {
2735
2874
  entries.push({ type: "user", text });
@@ -2821,12 +2960,15 @@ function extractFileEditContext(logPath, filePath) {
2821
2960
  const toolCalls = msgContent.filter(
2822
2961
  (/** @type {any} */ c) =>
2823
2962
  (c.type === "tool_use" || c.type === "tool_call") &&
2824
- (c.name === "Write" || c.name === "Edit"),
2963
+ (c.name === "Write" || c.name === "Edit")
2825
2964
  );
2826
2965
 
2827
2966
  for (const tc of toolCalls) {
2828
2967
  const input = tc.input || tc.arguments || {};
2829
- if (input.file_path === filePath || input.file_path?.endsWith("/" + filePath)) {
2968
+ if (
2969
+ input.file_path === filePath ||
2970
+ input.file_path?.endsWith("/" + filePath)
2971
+ ) {
2830
2972
  editEntry = { entry, toolCall: tc, content: msgContent };
2831
2973
  editIdx = i;
2832
2974
  break;
@@ -2874,7 +3016,7 @@ function extractFileEditContext(logPath, filePath) {
2874
3016
  const msgContent = entry.message?.content || [];
2875
3017
  const readCalls = msgContent.filter(
2876
3018
  (/** @type {any} */ c) =>
2877
- (c.type === "tool_use" || c.type === "tool_call") && c.name === "Read",
3019
+ (c.type === "tool_use" || c.type === "tool_call") && c.name === "Read"
2878
3020
  );
2879
3021
 
2880
3022
  for (const rc of readCalls) {
@@ -2892,11 +3034,14 @@ function extractFileEditContext(logPath, filePath) {
2892
3034
  const edits = msgContent.filter(
2893
3035
  (/** @type {any} */ c) =>
2894
3036
  (c.type === "tool_use" || c.type === "tool_call") &&
2895
- (c.name === "Write" || c.name === "Edit"),
3037
+ (c.name === "Write" || c.name === "Edit")
2896
3038
  );
2897
3039
  for (const e of edits) {
2898
3040
  const input = e.input || e.arguments || {};
2899
- if (input.file_path === filePath || input.file_path?.endsWith("/" + filePath)) {
3041
+ if (
3042
+ input.file_path === filePath ||
3043
+ input.file_path?.endsWith("/" + filePath)
3044
+ ) {
2900
3045
  editSequence++;
2901
3046
  }
2902
3047
  }
@@ -2964,7 +3109,9 @@ const DEFAULT_EXCLUDE_PATTERNS = [
2964
3109
  function watchForChanges(patterns, callback) {
2965
3110
  // Separate include and exclude patterns
2966
3111
  const includePatterns = patterns.filter((p) => !p.startsWith("!"));
2967
- const userExcludePatterns = patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
3112
+ const userExcludePatterns = patterns
3113
+ .filter((p) => p.startsWith("!"))
3114
+ .map((p) => p.slice(1));
2968
3115
  const excludePatterns = [...DEFAULT_EXCLUDE_PATTERNS, ...userExcludePatterns];
2969
3116
 
2970
3117
  /** @type {import('node:fs').FSWatcher[]} */
@@ -2980,28 +3127,36 @@ function watchForChanges(patterns, callback) {
2980
3127
  watchedDirs.add(dir);
2981
3128
 
2982
3129
  try {
2983
- const watcher = watch(dir, { recursive: true }, (_eventType, filename) => {
2984
- if (!filename) return;
2985
- const fullPath = path.join(dir, filename);
2986
-
2987
- // Check exclusions first
2988
- for (const ex of excludePatterns) {
2989
- if (matchesPattern(fullPath, ex) || matchesPattern(filename, ex)) {
2990
- return; // Excluded
3130
+ const watcher = watch(
3131
+ dir,
3132
+ { recursive: true },
3133
+ (_eventType, filename) => {
3134
+ if (!filename) return;
3135
+ const fullPath = path.join(dir, filename);
3136
+
3137
+ // Check exclusions first
3138
+ for (const ex of excludePatterns) {
3139
+ if (matchesPattern(fullPath, ex) || matchesPattern(filename, ex)) {
3140
+ return; // Excluded
3141
+ }
2991
3142
  }
2992
- }
2993
3143
 
2994
- // Check if this file matches any include pattern
2995
- for (const p of includePatterns) {
2996
- if (matchesPattern(fullPath, p) || matchesPattern(filename, p)) {
2997
- callback(fullPath);
2998
- break;
3144
+ // Check if this file matches any include pattern
3145
+ for (const p of includePatterns) {
3146
+ if (matchesPattern(fullPath, p) || matchesPattern(filename, p)) {
3147
+ callback(fullPath);
3148
+ break;
3149
+ }
2999
3150
  }
3000
3151
  }
3001
- });
3152
+ );
3002
3153
  watchers.push(watcher);
3003
3154
  } catch (err) {
3004
- console.error(`Warning: Failed to watch ${dir}: ${err instanceof Error ? err.message : err}`);
3155
+ console.error(
3156
+ `Warning: Failed to watch ${dir}: ${
3157
+ err instanceof Error ? err.message : err
3158
+ }`
3159
+ );
3005
3160
  }
3006
3161
  }
3007
3162
 
@@ -3105,11 +3260,17 @@ function detectState(screen, config) {
3105
3260
  if (typeof pattern === "function") {
3106
3261
  // Functions check lastLines first (most specific), then recentLines
3107
3262
  if (pattern(lastLines)) {
3108
- debug("state", "confirmPattern function matched lastLines -> CONFIRMING");
3263
+ debug(
3264
+ "state",
3265
+ "confirmPattern function matched lastLines -> CONFIRMING"
3266
+ );
3109
3267
  return State.CONFIRMING;
3110
3268
  }
3111
3269
  if (pattern(recentLines)) {
3112
- debug("state", "confirmPattern function matched recentLines -> CONFIRMING");
3270
+ debug(
3271
+ "state",
3272
+ "confirmPattern function matched recentLines -> CONFIRMING"
3273
+ );
3113
3274
  return State.CONFIRMING;
3114
3275
  }
3115
3276
  } else {
@@ -3124,7 +3285,8 @@ function detectState(screen, config) {
3124
3285
  // Check for active work patterns first (agent shows prompt even while working)
3125
3286
  const activeWorkPatterns = config.activeWorkPatterns || [];
3126
3287
  for (const p of activeWorkPatterns) {
3127
- const matched = p instanceof RegExp ? p.test(lastLines) : lastLines.includes(p);
3288
+ const matched =
3289
+ p instanceof RegExp ? p.test(lastLines) : lastLines.includes(p);
3128
3290
  if (matched) {
3129
3291
  debug("state", `activeWorkPattern "${p}" matched -> THINKING`);
3130
3292
  return State.THINKING;
@@ -3138,10 +3300,16 @@ function detectState(screen, config) {
3138
3300
  // This prevents false positives from output containing the prompt symbol
3139
3301
  if (config.requireStyledPrompt && config.session) {
3140
3302
  if (hasStyledPrompt(config.session, config.promptSymbol)) {
3141
- debug("state", `promptSymbol "${config.promptSymbol}" found with bold styling -> READY`);
3303
+ debug(
3304
+ "state",
3305
+ `promptSymbol "${config.promptSymbol}" found with bold styling -> READY`
3306
+ );
3142
3307
  return State.READY;
3143
3308
  }
3144
- debug("state", `promptSymbol "${config.promptSymbol}" found but not bold, continuing checks`);
3309
+ debug(
3310
+ "state",
3311
+ `promptSymbol "${config.promptSymbol}" found but not bold, continuing checks`
3312
+ );
3145
3313
  } else {
3146
3314
  debug("state", `promptSymbol "${config.promptSymbol}" found -> READY`);
3147
3315
  return State.READY;
@@ -3172,7 +3340,12 @@ function detectState(screen, config) {
3172
3340
  // Update prompt
3173
3341
  if (config.updatePromptPatterns) {
3174
3342
  const { screen: sp, lastLines: lp } = config.updatePromptPatterns;
3175
- if (sp && sp.some((p) => screen.includes(p)) && lp && lp.some((p) => lastLines.includes(p))) {
3343
+ if (
3344
+ sp &&
3345
+ sp.some((p) => screen.includes(p)) &&
3346
+ lp &&
3347
+ lp.some((p) => lastLines.includes(p))
3348
+ ) {
3176
3349
  debug("state", "updatePromptPatterns matched -> UPDATE_PROMPT");
3177
3350
  return State.UPDATE_PROMPT;
3178
3351
  }
@@ -3284,7 +3457,9 @@ class Agent {
3284
3457
  } else if (customAllowedTools) {
3285
3458
  // Custom permissions from --auto-approve flag
3286
3459
  // Escape for shell: backslashes first, then double quotes
3287
- const escaped = customAllowedTools.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
3460
+ const escaped = customAllowedTools
3461
+ .replace(/\\/g, "\\\\")
3462
+ .replace(/"/g, '\\"');
3288
3463
  base = `${this.startCommand} --allowedTools "${escaped}"`;
3289
3464
  debug("command", `mode=custom, allowedTools=${customAllowedTools}`);
3290
3465
  } else if (this.safeAllowedTools) {
@@ -3322,7 +3497,7 @@ class Agent {
3322
3497
  // Match sessions: {tool}-(partner-)?{uuid}[-p{hash}|-yolo]?
3323
3498
  const childPattern = new RegExp(
3324
3499
  `^${this.name}-(partner-)?${UUID_PATTERN}(-p${PERM_HASH_PATTERN}|-yolo)?$`,
3325
- "i",
3500
+ "i"
3326
3501
  );
3327
3502
  const requestedHash = computePermissionHash(allowedTools);
3328
3503
 
@@ -3354,13 +3529,17 @@ class Agent {
3354
3529
  if (matchingSessions.length === 0) return null;
3355
3530
 
3356
3531
  // Cache session cwds to avoid repeated tmux calls
3357
- const sessionCwds = new Map(matchingSessions.map((s) => [s, getTmuxSessionCwd(s)]));
3532
+ const sessionCwds = new Map(
3533
+ matchingSessions.map((s) => [s, getTmuxSessionCwd(s)])
3534
+ );
3358
3535
 
3359
3536
  let searchDir = cwd;
3360
3537
  const homeDir = os.homedir();
3361
3538
 
3362
3539
  while (searchDir !== homeDir && searchDir !== "/") {
3363
- const existing = matchingSessions.find((s) => sessionCwds.get(s) === searchDir);
3540
+ const existing = matchingSessions.find(
3541
+ (s) => sessionCwds.get(s) === searchDir
3542
+ );
3364
3543
  if (existing) return existing;
3365
3544
 
3366
3545
  // Stop at git root (don't leak across projects)
@@ -3427,7 +3606,11 @@ class Agent {
3427
3606
  if (this.logPathFinder) {
3428
3607
  /** @type {'claude' | 'codex'} */
3429
3608
  const format = this.name === "claude" ? "claude" : "codex";
3430
- return new JsonlTerminalStream(() => this.findLogPath(sessionName), format, opts);
3609
+ return new JsonlTerminalStream(
3610
+ () => this.findLogPath(sessionName),
3611
+ format,
3612
+ opts
3613
+ );
3431
3614
  }
3432
3615
  // Fall back to screen capture
3433
3616
  return new ScreenTerminalStream(sessionName);
@@ -3511,7 +3694,12 @@ class Agent {
3511
3694
  // Logo/branding characters (block drawing)
3512
3695
  if (/[▐▛▜▌▝▘█▀▄]/.test(trimmed) && trimmed.length < 50) return true;
3513
3696
  // Version strings, model info
3514
- if (/^(Claude Code|OpenAI Codex|Opus|gpt-|model:|directory:|cwd:)/i.test(trimmed)) return true;
3697
+ if (
3698
+ /^(Claude Code|OpenAI Codex|Opus|gpt-|model:|directory:|cwd:)/i.test(
3699
+ trimmed
3700
+ )
3701
+ )
3702
+ return true;
3515
3703
  // Path-only lines (working directory display)
3516
3704
  if (/^~\/[^\s]*$/.test(trimmed)) return true;
3517
3705
  // Explicit chrome patterns from agent config
@@ -3569,7 +3757,7 @@ class Agent {
3569
3757
  // Fallback: extract after last prompt
3570
3758
  if (filtered.length === 0) {
3571
3759
  const lastPromptIdx = lines.findLastIndex((/** @type {string} */ l) =>
3572
- l.startsWith(this.promptSymbol),
3760
+ l.startsWith(this.promptSymbol)
3573
3761
  );
3574
3762
  if (lastPromptIdx >= 0 && lastPromptIdx < lines.length - 1) {
3575
3763
  const afterPrompt = lines
@@ -3585,13 +3773,16 @@ class Agent {
3585
3773
  if (lastPromptIdx >= 0) {
3586
3774
  const lastPromptLine = lines[lastPromptIdx];
3587
3775
  const isEmptyPrompt =
3588
- lastPromptLine.trim() === this.promptSymbol || lastPromptLine.match(/^❯\s*$/);
3776
+ lastPromptLine.trim() === this.promptSymbol ||
3777
+ lastPromptLine.match(/^❯\s*$/);
3589
3778
  if (isEmptyPrompt) {
3590
3779
  // Find the previous prompt (user's input) and extract content between
3591
3780
  // Note: [Pasted text is Claude's truncated output indicator, NOT a prompt
3592
3781
  const prevPromptIdx = lines
3593
3782
  .slice(0, lastPromptIdx)
3594
- .findLastIndex((/** @type {string} */ l) => l.startsWith(this.promptSymbol));
3783
+ .findLastIndex((/** @type {string} */ l) =>
3784
+ l.startsWith(this.promptSymbol)
3785
+ );
3595
3786
  if (prevPromptIdx >= 0) {
3596
3787
  const betweenPrompts = lines
3597
3788
  .slice(prevPromptIdx + 1, lastPromptIdx)
@@ -3615,7 +3806,10 @@ class Agent {
3615
3806
  return (
3616
3807
  response
3617
3808
  // Remove tool call lines (Search, Read, Grep, etc.)
3618
- .replace(/^[⏺•]\s*(Search|Read|Grep|Glob|Write|Edit|Bash)\([^)]*\).*$/gm, "")
3809
+ .replace(
3810
+ /^[⏺•]\s*(Search|Read|Grep|Glob|Write|Edit|Bash)\([^)]*\).*$/gm,
3811
+ ""
3812
+ )
3619
3813
  // Remove tool result lines
3620
3814
  .replace(/^⎿\s+.*$/gm, "")
3621
3815
  // Remove "Sautéed for Xs" timing lines
@@ -3761,7 +3955,10 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
3761
3955
  const start = Date.now();
3762
3956
  const initialScreen = tmuxCapture(session);
3763
3957
  const initialState = agent.getState(initialScreen, session);
3764
- debug("waitUntilReady", `start: initialState=${initialState}, timeout=${timeoutMs}ms`);
3958
+ debug(
3959
+ "waitUntilReady",
3960
+ `start: initialState=${initialState}, timeout=${timeoutMs}ms`
3961
+ );
3765
3962
 
3766
3963
  // Dismiss feedback modal if present
3767
3964
  if (initialState === State.FEEDBACK_MODAL) {
@@ -3791,8 +3988,15 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
3791
3988
  continue;
3792
3989
  }
3793
3990
 
3794
- if (state === State.RATE_LIMITED || state === State.CONFIRMING || state === State.READY) {
3795
- debug("waitUntilReady", `reached state=${state} after ${Date.now() - start}ms`);
3991
+ if (
3992
+ state === State.RATE_LIMITED ||
3993
+ state === State.CONFIRMING ||
3994
+ state === State.READY
3995
+ ) {
3996
+ debug(
3997
+ "waitUntilReady",
3998
+ `reached state=${state} after ${Date.now() - start}ms`
3999
+ );
3796
4000
  return { state, screen };
3797
4001
  }
3798
4002
  }
@@ -3852,7 +4056,8 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
3852
4056
  lastScreen = screen;
3853
4057
  stableAt = Date.now();
3854
4058
  if (screen !== initialScreen) {
3855
- if (!sawActivity) debug("poll", "sawActivity=true (screen changed from initial)");
4059
+ if (!sawActivity)
4060
+ debug("poll", "sawActivity=true (screen changed from initial)");
3856
4061
  sawActivity = true;
3857
4062
  }
3858
4063
  }
@@ -3863,7 +4068,10 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
3863
4068
  // Require sawThinking OR enough time has passed (fallback for fast responses)
3864
4069
  const elapsed = Date.now() - start;
3865
4070
  if (sawThinking || elapsed >= THINKING_FALLBACK_MS) {
3866
- debug("poll", `returning READY after ${elapsed}ms (sawThinking=${sawThinking})`);
4071
+ debug(
4072
+ "poll",
4073
+ `returning READY after ${elapsed}ms (sawThinking=${sawThinking})`
4074
+ );
3867
4075
  if (onReady) onReady(screen);
3868
4076
  return { state, screen };
3869
4077
  }
@@ -3926,7 +4134,8 @@ async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
3926
4134
 
3927
4135
  // Dedupe messages within sliding window (Codex logs can contain duplicates)
3928
4136
  // Tool calls are exempt: lineType === "tool" for JSONL streams, or starts with ">" for screen streams
3929
- const isToolLine = line.lineType === "tool" || (!line.lineType && text.startsWith(">"));
4137
+ const isToolLine =
4138
+ line.lineType === "tool" || (!line.lineType && text.startsWith(">"));
3930
4139
  if (!isToolLine) {
3931
4140
  if (recentMessages.includes(text)) continue;
3932
4141
  recentMessages.push(text);
@@ -3950,7 +4159,10 @@ async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
3950
4159
  } else if (state === State.CONFIRMING) {
3951
4160
  const pendingTool = extractPendingToolFromScreen(screen);
3952
4161
  console.log(
3953
- styleText("yellow", pendingTool ? `[CONFIRMING] ${pendingTool}` : "[CONFIRMING]"),
4162
+ styleText(
4163
+ "yellow",
4164
+ pendingTool ? `[CONFIRMING] ${pendingTool}` : "[CONFIRMING]"
4165
+ )
3954
4166
  );
3955
4167
  }
3956
4168
  if (lastState === State.THINKING && state !== State.THINKING) {
@@ -4007,7 +4219,11 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
4007
4219
  * @param {string | null} [options.allowedTools]
4008
4220
  * @returns {Promise<string>}
4009
4221
  */
4010
- async function cmdStart(agent, session, { yolo = false, allowedTools = null } = {}) {
4222
+ async function cmdStart(
4223
+ agent,
4224
+ session,
4225
+ { yolo = false, allowedTools = null } = {}
4226
+ ) {
4011
4227
  // Generate session name if not provided
4012
4228
  if (!session) {
4013
4229
  session = agent.generateSession({ allowedTools, yolo });
@@ -4126,11 +4342,23 @@ function cmdAgents() {
4126
4342
  const maxPlan = Math.max(4, ...agents.map((a) => a.plan.length));
4127
4343
 
4128
4344
  console.log(
4129
- `${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(maxTarget)} ${"TYPE".padEnd(maxType)} ${"MODE".padEnd(maxMode)} ${"PLAN".padEnd(maxPlan)} BRANCH`,
4345
+ `${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(
4346
+ maxTool
4347
+ )} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(
4348
+ maxTarget
4349
+ )} ${"TYPE".padEnd(maxType)} ${"MODE".padEnd(maxMode)} ${"PLAN".padEnd(
4350
+ maxPlan
4351
+ )} BRANCH`
4130
4352
  );
4131
4353
  for (const a of agents) {
4132
4354
  console.log(
4133
- `${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.target.padEnd(maxTarget)} ${a.type.padEnd(maxType)} ${a.mode.padEnd(maxMode)} ${a.plan.padEnd(maxPlan)} ${a.branch}`,
4355
+ `${a.session.padEnd(maxSession)} ${a.tool.padEnd(
4356
+ maxTool
4357
+ )} ${a.state.padEnd(maxState)} ${a.target.padEnd(
4358
+ maxTarget
4359
+ )} ${a.type.padEnd(maxType)} ${a.mode.padEnd(maxMode)} ${a.plan.padEnd(
4360
+ maxPlan
4361
+ )} ${a.branch}`
4134
4362
  );
4135
4363
  }
4136
4364
 
@@ -4191,7 +4419,9 @@ function startArchangel(config, parentSession = null) {
4191
4419
  ? parentSession.session || parentSession.uuid?.slice(0, 8)
4192
4420
  : null;
4193
4421
  console.log(
4194
- `Summoning: ${config.name} (pid ${child.pid})${watchingLabel ? ` [watching: ${watchingLabel}]` : ""}`,
4422
+ `Summoning: ${config.name} (pid ${child.pid})${
4423
+ watchingLabel ? ` [watching: ${watchingLabel}]` : ""
4424
+ }`
4195
4425
  );
4196
4426
  }
4197
4427
 
@@ -4200,7 +4430,10 @@ function startArchangel(config, parentSession = null) {
4200
4430
  * @param {number} [timeoutMs]
4201
4431
  * @returns {Promise<string | undefined>}
4202
4432
  */
4203
- async function waitForArchangelSession(pattern, timeoutMs = ARCHANGEL_STARTUP_TIMEOUT_MS) {
4433
+ async function waitForArchangelSession(
4434
+ pattern,
4435
+ timeoutMs = ARCHANGEL_STARTUP_TIMEOUT_MS
4436
+ ) {
4204
4437
  const start = Date.now();
4205
4438
  while (Date.now() - start < timeoutMs) {
4206
4439
  const session = findArchangelSession(pattern);
@@ -4244,7 +4477,7 @@ async function cmdArchangel(agentName) {
4244
4477
  const cliCheck = spawnSync("which", [agent.name], { encoding: "utf-8" });
4245
4478
  if (cliCheck.status !== 0) {
4246
4479
  console.error(
4247
- `[archangel:${agentName}] ERROR: ${agent.name} CLI is not installed or not in PATH`,
4480
+ `[archangel:${agentName}] ERROR: ${agent.name} CLI is not installed or not in PATH`
4248
4481
  );
4249
4482
  process.exit(1);
4250
4483
  }
@@ -4265,8 +4498,13 @@ async function cmdArchangel(agentName) {
4265
4498
  }
4266
4499
 
4267
4500
  // Handle bypass permissions confirmation dialog (Claude Code shows this for --dangerously-skip-permissions)
4268
- if (screen.includes("Bypass Permissions mode") && screen.includes("Yes, I accept")) {
4269
- console.log(`[archangel:${agentName}] Accepting bypass permissions dialog`);
4501
+ if (
4502
+ screen.includes("Bypass Permissions mode") &&
4503
+ screen.includes("Yes, I accept")
4504
+ ) {
4505
+ console.log(
4506
+ `[archangel:${agentName}] Accepting bypass permissions dialog`
4507
+ );
4270
4508
  tmuxSend(sessionName, "2"); // Select "Yes, I accept"
4271
4509
  await sleep(300);
4272
4510
  tmuxSend(sessionName, "Enter");
@@ -4319,7 +4557,9 @@ async function cmdArchangel(agentName) {
4319
4557
  try {
4320
4558
  // Get parent session log path for JSONL extraction
4321
4559
  const parent = findParentSession();
4322
- const logPath = parent ? findClaudeLogPath(parent.uuid, parent.session) : null;
4560
+ const logPath = parent
4561
+ ? findClaudeLogPath(parent.uuid, parent.session)
4562
+ : null;
4323
4563
 
4324
4564
  // Get orientation context (plan and todos) from parent session
4325
4565
  const meta = parent?.session ? getSessionMeta(parent.session) : null;
@@ -4357,7 +4597,8 @@ async function cmdArchangel(agentName) {
4357
4597
  prompt += (prompt ? "\n\n" : "") + "## Current Plan\n\n" + planContent;
4358
4598
  }
4359
4599
  if (includeTodos && todosContent) {
4360
- prompt += (prompt ? "\n\n" : "") + "## Current Todos\n\n" + todosContent;
4600
+ prompt +=
4601
+ (prompt ? "\n\n" : "") + "## Current Todos\n\n" + todosContent;
4361
4602
  }
4362
4603
 
4363
4604
  if (fileContexts.length > 0) {
@@ -4373,20 +4614,26 @@ async function cmdArchangel(agentName) {
4373
4614
  }
4374
4615
 
4375
4616
  if (ctx.subsequentErrors.length > 0) {
4376
- prompt += `**Errors after:** ${ctx.subsequentErrors[0].slice(0, 200)}\n`;
4617
+ prompt += `**Errors after:** ${ctx.subsequentErrors[0].slice(
4618
+ 0,
4619
+ 200
4620
+ )}\n`;
4377
4621
  }
4378
4622
 
4379
4623
  if (ctx.readsBefore.length > 0) {
4380
- const reads = ctx.readsBefore.map((f) => f.split("/").pop()).join(", ");
4624
+ const reads = ctx.readsBefore
4625
+ .map((f) => f.split("/").pop())
4626
+ .join(", ");
4381
4627
  prompt += `**Files read before:** ${reads}\n`;
4382
4628
  }
4383
4629
  }
4384
4630
 
4385
- prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
4631
+ prompt +=
4632
+ "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
4386
4633
 
4387
4634
  const gitContext = buildGitContext(
4388
4635
  ARCHANGEL_GIT_CONTEXT_HOURS,
4389
- ARCHANGEL_GIT_CONTEXT_MAX_LINES,
4636
+ ARCHANGEL_GIT_CONTEXT_MAX_LINES
4390
4637
  );
4391
4638
  if (gitContext) {
4392
4639
  prompt += "\n\n## Git Context\n\n" + gitContext;
@@ -4395,18 +4642,22 @@ async function cmdArchangel(agentName) {
4395
4642
  prompt += "\n\nReview these changes.";
4396
4643
  } else {
4397
4644
  // Fallback: no JSONL context available, use conversation + git context
4398
- const parentContext = getParentSessionContext(ARCHANGEL_PARENT_CONTEXT_ENTRIES);
4645
+ const parentContext = getParentSessionContext(
4646
+ ARCHANGEL_PARENT_CONTEXT_ENTRIES
4647
+ );
4399
4648
  const gitContext = buildGitContext(
4400
4649
  ARCHANGEL_GIT_CONTEXT_HOURS,
4401
- ARCHANGEL_GIT_CONTEXT_MAX_LINES,
4650
+ ARCHANGEL_GIT_CONTEXT_MAX_LINES
4402
4651
  );
4403
4652
 
4404
4653
  if (parentContext) {
4405
4654
  prompt +=
4406
- "\n\n## Main Session Context\n\nThe user is currently working on:\n\n" + parentContext;
4655
+ "\n\n## Main Session Context\n\nThe user is currently working on:\n\n" +
4656
+ parentContext;
4407
4657
  }
4408
4658
 
4409
- prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
4659
+ prompt +=
4660
+ "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
4410
4661
 
4411
4662
  if (gitContext) {
4412
4663
  prompt += "\n\n## Git Context\n\n" + gitContext;
@@ -4431,7 +4682,9 @@ async function cmdArchangel(agentName) {
4431
4682
  }
4432
4683
 
4433
4684
  if (state !== State.READY) {
4434
- console.log(`[archangel:${agentName}] Agent not ready (${state}), skipping`);
4685
+ console.log(
4686
+ `[archangel:${agentName}] Agent not ready (${state}), skipping`
4687
+ );
4435
4688
  isProcessing = false;
4436
4689
  return;
4437
4690
  }
@@ -4447,7 +4700,7 @@ async function cmdArchangel(agentName) {
4447
4700
  const { state: endState, screen: afterScreen } = await waitForResponse(
4448
4701
  agent,
4449
4702
  sessionName,
4450
- ARCHANGEL_RESPONSE_TIMEOUT_MS,
4703
+ ARCHANGEL_RESPONSE_TIMEOUT_MS
4451
4704
  );
4452
4705
 
4453
4706
  if (endState === State.RATE_LIMITED) {
@@ -4457,7 +4710,8 @@ async function cmdArchangel(agentName) {
4457
4710
 
4458
4711
  const cleanedResponse = agent.getResponse(sessionName, afterScreen) || "";
4459
4712
 
4460
- const isSkippable = !cleanedResponse || cleanedResponse.trim() === "EMPTY_RESPONSE";
4713
+ const isSkippable =
4714
+ !cleanedResponse || cleanedResponse.trim() === "EMPTY_RESPONSE";
4461
4715
 
4462
4716
  if (!isSkippable) {
4463
4717
  writeToMailbox({
@@ -4468,10 +4722,15 @@ async function cmdArchangel(agentName) {
4468
4722
  files,
4469
4723
  message: cleanedResponse,
4470
4724
  });
4471
- console.log(`[archangel:${agentName}] Wrote observation for ${files.length} file(s)`);
4725
+ console.log(
4726
+ `[archangel:${agentName}] Wrote observation for ${files.length} file(s)`
4727
+ );
4472
4728
  }
4473
4729
  } catch (err) {
4474
- console.error(`[archangel:${agentName}] Error:`, err instanceof Error ? err.message : err);
4730
+ console.error(
4731
+ `[archangel:${agentName}] Error:`,
4732
+ err instanceof Error ? err.message : err
4733
+ );
4475
4734
  }
4476
4735
 
4477
4736
  isProcessing = false;
@@ -4481,7 +4740,7 @@ async function cmdArchangel(agentName) {
4481
4740
  processChanges().catch((err) => {
4482
4741
  console.error(
4483
4742
  `[archangel:${agentName}] Unhandled error:`,
4484
- err instanceof Error ? err.message : err,
4743
+ err instanceof Error ? err.message : err
4485
4744
  );
4486
4745
  });
4487
4746
  }
@@ -4550,7 +4809,9 @@ async function cmdSummon(name = null) {
4550
4809
  const exists = configs.some((c) => c.name === name);
4551
4810
  if (!exists) {
4552
4811
  if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
4553
- console.log("ERROR: Name must contain only letters, numbers, dashes, and underscores");
4812
+ console.log(
4813
+ "ERROR: Name must contain only letters, numbers, dashes, and underscores"
4814
+ );
4554
4815
  process.exit(1);
4555
4816
  }
4556
4817
 
@@ -4585,7 +4846,11 @@ Review changed files for bugs, type errors, and edge cases.
4585
4846
 
4586
4847
  const parentSession = findCurrentClaudeSession();
4587
4848
  if (parentSession) {
4588
- console.log(`Parent session: ${parentSession.session || "(non-tmux)"} [${parentSession.uuid}]`);
4849
+ console.log(
4850
+ `Parent session: ${parentSession.session || "(non-tmux)"} [${
4851
+ parentSession.uuid
4852
+ }]`
4853
+ );
4589
4854
  }
4590
4855
 
4591
4856
  for (const config of targetConfigs) {
@@ -4830,7 +5095,9 @@ function ensureClaudeHookConfig() {
4830
5095
  const hookExists = settings.hooks[eventName].some(
4831
5096
  /** @param {{hooks?: Array<{command: string}>}} entry */
4832
5097
  (entry) =>
4833
- entry.hooks?.some(/** @param {{command: string}} h */ (h) => h.command === hookCommand),
5098
+ entry.hooks?.some(
5099
+ /** @param {{command: string}} h */ (h) => h.command === hookCommand
5100
+ )
4834
5101
  );
4835
5102
 
4836
5103
  if (!hookExists) {
@@ -4866,7 +5133,10 @@ function ensureClaudeHookConfig() {
4866
5133
  * @param {string | null | undefined} session
4867
5134
  * @param {{all?: boolean, orphans?: boolean, force?: boolean}} [options]
4868
5135
  */
4869
- function cmdKill(session, { all = false, orphans = false, force = false } = {}) {
5136
+ function cmdKill(
5137
+ session,
5138
+ { all = false, orphans = false, force = false } = {}
5139
+ ) {
4870
5140
  // Handle orphaned processes
4871
5141
  if (orphans) {
4872
5142
  const orphanedProcesses = findOrphanedProcesses();
@@ -4885,14 +5155,18 @@ function cmdKill(session, { all = false, orphans = false, force = false } = {})
4885
5155
  killed++;
4886
5156
  }
4887
5157
  }
4888
- console.log(`Killed ${killed} orphaned process(es)${force ? " (forced)" : ""}`);
5158
+ console.log(
5159
+ `Killed ${killed} orphaned process(es)${force ? " (forced)" : ""}`
5160
+ );
4889
5161
  return;
4890
5162
  }
4891
5163
 
4892
5164
  // If specific session provided, kill just that one
4893
5165
  if (session) {
4894
5166
  if (!tmuxHasSession(session)) {
4895
- console.log("ERROR: session not found. Run 'ax agents' to list sessions.");
5167
+ console.log(
5168
+ "ERROR: session not found. Run 'ax agents' to list sessions."
5169
+ );
4896
5170
  process.exit(1);
4897
5171
  }
4898
5172
  tmuxKill(session);
@@ -4919,7 +5193,9 @@ function cmdKill(session, { all = false, orphans = false, force = false } = {})
4919
5193
 
4920
5194
  if (sessionsToKill.length === 0) {
4921
5195
  console.log(`No agents running in ${currentProject}`);
4922
- console.log(`(Use --all to kill all ${agentSessions.length} agent(s) across all projects)`);
5196
+ console.log(
5197
+ `(Use --all to kill all ${agentSessions.length} agent(s) across all projects)`
5198
+ );
4923
5199
  return;
4924
5200
  }
4925
5201
  }
@@ -4958,7 +5234,10 @@ function cmdAttach(session) {
4958
5234
  * @param {string | null | undefined} sessionName
4959
5235
  * @param {{tail?: number, reasoning?: boolean, follow?: boolean}} [options]
4960
5236
  */
4961
- function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } = {}) {
5237
+ function cmdLog(
5238
+ sessionName,
5239
+ { tail = 50, reasoning = false, follow = false } = {}
5240
+ ) {
4962
5241
  if (!sessionName) {
4963
5242
  console.log("ERROR: no session specified. Run 'agents' to list sessions.");
4964
5243
  process.exit(1);
@@ -5003,7 +5282,9 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
5003
5282
  }
5004
5283
 
5005
5284
  // For initial print, take last N. For follow, take only new lines.
5006
- const startIdx = isInitial ? Math.max(0, lines.length - tail) : lastLineCount;
5285
+ const startIdx = isInitial
5286
+ ? Math.max(0, lines.length - tail)
5287
+ : lastLineCount;
5007
5288
  const newLines = lines.slice(startIdx);
5008
5289
  lastLineCount = lines.length;
5009
5290
 
@@ -5115,7 +5396,9 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
5115
5396
  if (reasoning) {
5116
5397
  const thinking = extractThinking(content);
5117
5398
  if (thinking) {
5118
- parts.push(`> *Thinking*: ${truncate(thinking, TRUNCATE_THINKING_LEN)}\n`);
5399
+ parts.push(
5400
+ `> *Thinking*: ${truncate(thinking, TRUNCATE_THINKING_LEN)}\n`
5401
+ );
5119
5402
  }
5120
5403
  }
5121
5404
 
@@ -5170,7 +5453,11 @@ function extractToolCalls(content) {
5170
5453
  const input = c.input || c.arguments || {};
5171
5454
  // Extract a reasonable target from the input
5172
5455
  const target =
5173
- input.file_path || input.path || input.command?.slice(0, 30) || input.pattern || "";
5456
+ input.file_path ||
5457
+ input.path ||
5458
+ input.command?.slice(0, 30) ||
5459
+ input.pattern ||
5460
+ "";
5174
5461
  const shortTarget = target.split("/").pop() || target.slice(0, 20);
5175
5462
  return { name, target: shortTarget, error: c.error };
5176
5463
  });
@@ -5207,7 +5494,9 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
5207
5494
  const entries = readMailbox({ maxAge, branch, limit });
5208
5495
 
5209
5496
  if (entries.length === 0) {
5210
- console.log("No mailbox entries" + (branch ? ` for branch '${branch}'` : ""));
5497
+ console.log(
5498
+ "No mailbox entries" + (branch ? ` for branch '${branch}'` : "")
5499
+ );
5211
5500
  return;
5212
5501
  }
5213
5502
 
@@ -5260,7 +5549,10 @@ function getProposalFromMailbox(rfpId, archangel) {
5260
5549
  if (!existsSync(MAILBOX_PATH)) return null;
5261
5550
  let result = null;
5262
5551
  try {
5263
- const lines = readFileSync(MAILBOX_PATH, "utf-8").trim().split("\n").filter(Boolean);
5552
+ const lines = readFileSync(MAILBOX_PATH, "utf-8")
5553
+ .trim()
5554
+ .split("\n")
5555
+ .filter(Boolean);
5264
5556
  for (const line of lines) {
5265
5557
  try {
5266
5558
  const entry = JSON.parse(line);
@@ -5283,7 +5575,10 @@ function getProposalFromMailbox(rfpId, archangel) {
5283
5575
  * @param {string} prompt
5284
5576
  * @param {{archangels?: string, fresh?: boolean, noWait?: boolean}} [options]
5285
5577
  */
5286
- async function cmdRfp(prompt, { archangels, fresh = false, noWait = false } = {}) {
5578
+ async function cmdRfp(
5579
+ prompt,
5580
+ { archangels, fresh = false, noWait = false } = {}
5581
+ ) {
5287
5582
  const configs = loadAgentConfigs();
5288
5583
  if (configs.length === 0) {
5289
5584
  console.log(`No archangels found in ${AGENTS_DIR}/`);
@@ -5302,7 +5597,9 @@ async function cmdRfp(prompt, { archangels, fresh = false, noWait = false } = {}
5302
5597
  process.exit(1);
5303
5598
  }
5304
5599
 
5305
- const missing = requested.filter((name) => !configs.some((c) => c.name === name));
5600
+ const missing = requested.filter(
5601
+ (name) => !configs.some((c) => c.name === name)
5602
+ );
5306
5603
  if (missing.length > 0) {
5307
5604
  console.log(`ERROR: unknown archangel(s): ${missing.join(", ")}`);
5308
5605
  process.exit(1);
@@ -5335,7 +5632,11 @@ async function cmdRfp(prompt, { archangels, fresh = false, noWait = false } = {}
5335
5632
  tmuxSend(session, "Enter");
5336
5633
  }
5337
5634
 
5338
- const ready = await waitUntilReady(agent, session, ARCHANGEL_STARTUP_TIMEOUT_MS);
5635
+ const ready = await waitUntilReady(
5636
+ agent,
5637
+ session,
5638
+ ARCHANGEL_STARTUP_TIMEOUT_MS
5639
+ );
5339
5640
  if (ready.state !== State.READY) {
5340
5641
  console.log(`[rfp] ${name} not ready (${ready.state}), skipping`);
5341
5642
  continue;
@@ -5355,7 +5656,8 @@ async function cmdRfp(prompt, { archangels, fresh = false, noWait = false } = {}
5355
5656
  if (noWait) {
5356
5657
  // Truncate prompt for display (first line, max 60 chars)
5357
5658
  const firstLine = prompt.split("\n")[0];
5358
- const taskPreview = firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
5659
+ const taskPreview =
5660
+ firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
5359
5661
 
5360
5662
  let output = `Task: ${taskPreview}
5361
5663
 
@@ -5379,7 +5681,10 @@ e.g.
5379
5681
  * @param {string} rfpId
5380
5682
  * @param {{archangels?: string, timeoutMs?: number}} [options]
5381
5683
  */
5382
- async function cmdRfpWait(rfpId, { archangels, timeoutMs = ARCHANGEL_RESPONSE_TIMEOUT_MS } = {}) {
5684
+ async function cmdRfpWait(
5685
+ rfpId,
5686
+ { archangels, timeoutMs = ARCHANGEL_RESPONSE_TIMEOUT_MS } = {}
5687
+ ) {
5383
5688
  const resolvedRfpId = resolveRfpId(rfpId);
5384
5689
  const configs = loadAgentConfigs();
5385
5690
  if (configs.length === 0) {
@@ -5399,7 +5704,9 @@ async function cmdRfpWait(rfpId, { archangels, timeoutMs = ARCHANGEL_RESPONSE_TI
5399
5704
  process.exit(1);
5400
5705
  }
5401
5706
 
5402
- const missing = requested.filter((name) => !configs.some((c) => c.name === name));
5707
+ const missing = requested.filter(
5708
+ (name) => !configs.some((c) => c.name === name)
5709
+ );
5403
5710
  if (missing.length > 0) {
5404
5711
  console.log(`ERROR: unknown archangel(s): ${missing.join(", ")}`);
5405
5712
  process.exit(1);
@@ -5437,7 +5744,9 @@ async function cmdRfpWait(rfpId, { archangels, timeoutMs = ARCHANGEL_RESPONSE_TI
5437
5744
  if (err instanceof TimeoutError) {
5438
5745
  console.log(`[rfp] ${name} timed out`);
5439
5746
  } else {
5440
- console.log(`[rfp] ${name} error: ${err instanceof Error ? err.message : err}`);
5747
+ console.log(
5748
+ `[rfp] ${name} error: ${err instanceof Error ? err.message : err}`
5749
+ );
5441
5750
  }
5442
5751
  continue;
5443
5752
  }
@@ -5467,7 +5776,7 @@ async function cmdRfpWait(rfpId, { archangels, timeoutMs = ARCHANGEL_RESPONSE_TI
5467
5776
  rfpId: resolvedRfpId,
5468
5777
  archangel: name,
5469
5778
  },
5470
- "proposal",
5779
+ "proposal"
5471
5780
  );
5472
5781
  if (printedAny) console.log("");
5473
5782
  console.log(`[${name}]`);
@@ -5489,15 +5798,25 @@ async function cmdAsk(
5489
5798
  agent,
5490
5799
  session,
5491
5800
  message,
5492
- { noWait = false, yolo = false, allowedTools = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
5801
+ {
5802
+ noWait = false,
5803
+ yolo = false,
5804
+ allowedTools = null,
5805
+ timeoutMs = DEFAULT_TIMEOUT_MS,
5806
+ } = {}
5493
5807
  ) {
5494
5808
  const sessionExists = session != null && tmuxHasSession(session);
5495
- const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
5809
+ const nativeYolo =
5810
+ sessionExists && isYoloSession(/** @type {string} */ (session));
5496
5811
 
5497
5812
  // Cannot use --yolo --no-wait on a safe session: we need to stay and auto-approve
5498
5813
  if (yolo && noWait && sessionExists && !nativeYolo) {
5499
- console.log("ERROR: --yolo requires waiting on a session not started with --yolo");
5500
- console.log("Restart the session with --yolo, or allow waiting for auto-approval");
5814
+ console.log(
5815
+ "ERROR: --yolo requires waiting on a session not started with --yolo"
5816
+ );
5817
+ console.log(
5818
+ "Restart the session with --yolo, or allow waiting for auto-approval"
5819
+ );
5501
5820
  process.exit(1);
5502
5821
  }
5503
5822
 
@@ -5519,7 +5838,8 @@ async function cmdAsk(
5519
5838
  const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
5520
5839
  // Truncate message for display (first line, max 60 chars)
5521
5840
  const firstLine = message.split("\n")[0];
5522
- const taskPreview = firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
5841
+ const taskPreview =
5842
+ firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
5523
5843
 
5524
5844
  let output = `Sent to: ${shortId}
5525
5845
  Task: ${taskPreview}
@@ -5577,10 +5897,18 @@ async function cmdDo(agent, prompt, options = {}) {
5577
5897
  }
5578
5898
 
5579
5899
  // Use provided session or start a new one
5900
+ const sessionExists = options.session && tmuxHasSession(options.session);
5580
5901
  const session = options.session
5581
5902
  ? await cmdStart(agent, options.session, { yolo })
5582
5903
  : await cmdStart(agent, null, { yolo });
5583
5904
 
5905
+ // If reusing existing session, wait for ready and clear stale input
5906
+ if (sessionExists) {
5907
+ await waitUntilReady(agent, session, timeoutMs);
5908
+ tmuxSend(session, "C-u");
5909
+ await sleep(50);
5910
+ }
5911
+
5584
5912
  // Print session ID for targeting approvals when not in yolo mode
5585
5913
  if (!yolo) {
5586
5914
  const parsed = parseSessionName(session);
@@ -5616,7 +5944,9 @@ async function cmdDo(agent, prompt, options = {}) {
5616
5944
  if (state === State.CONFIRMING) {
5617
5945
  const parsed = parseSessionName(session);
5618
5946
  const shortId = parsed?.uuid?.slice(0, 8) || session;
5619
- console.log(`\nAwaiting confirmation: ${formatConfirmationOutput(screen, agent)}`);
5947
+ console.log(
5948
+ `\nAwaiting confirmation: ${formatConfirmationOutput(screen, agent)}`
5949
+ );
5620
5950
  console.log(`Add --session=${shortId} if you have multiple sessions`);
5621
5951
  console.log("Use 'ax approve --wait' or 'ax reject' to continue");
5622
5952
  process.exit(3);
@@ -5632,7 +5962,9 @@ async function cmdDo(agent, prompt, options = {}) {
5632
5962
 
5633
5963
  // Single iteration mode (default): exit with code 5 to signal "more work"
5634
5964
  if (!loop) {
5635
- console.log(`\nIteration complete. Re-run to continue, or --reset to start over.`);
5965
+ console.log(
5966
+ `\nIteration complete. Re-run to continue, or --reset to start over.`
5967
+ );
5636
5968
  process.exit(5);
5637
5969
  }
5638
5970
 
@@ -5726,7 +6058,12 @@ async function cmdReview(
5726
6058
  session,
5727
6059
  option,
5728
6060
  customInstructions,
5729
- { wait = true, yolo = false, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
6061
+ {
6062
+ wait = true,
6063
+ yolo = false,
6064
+ fresh = false,
6065
+ timeoutMs = REVIEW_TIMEOUT_MS,
6066
+ } = {}
5730
6067
  ) {
5731
6068
  const validOptions = ["uncommitted", "custom", "branch", "commit"];
5732
6069
  if (option && !validOptions.includes(option)) {
@@ -5742,7 +6079,11 @@ async function cmdReview(
5742
6079
  tmuxSendLiteral(/** @type {string} */ (session), "/new");
5743
6080
  await sleep(50);
5744
6081
  tmuxSend(/** @type {string} */ (session), "Enter");
5745
- await waitUntilReady(agent, /** @type {string} */ (session), STARTUP_TIMEOUT_MS);
6082
+ await waitUntilReady(
6083
+ agent,
6084
+ /** @type {string} */ (session),
6085
+ STARTUP_TIMEOUT_MS
6086
+ );
5746
6087
  }
5747
6088
 
5748
6089
  // Claude: use prompt-based review (no /review command)
@@ -5758,25 +6099,35 @@ async function cmdReview(
5758
6099
  : "Review the most recent commit.",
5759
6100
  custom: customInstructions || "Review the code.",
5760
6101
  };
5761
- const prompt = (option && reviewPrompts[option]) || reviewPrompts.uncommitted;
6102
+ const prompt =
6103
+ (option && reviewPrompts[option]) || reviewPrompts.uncommitted;
5762
6104
  debug("review", `Claude path: noWait=${!wait}, timeoutMs=${timeoutMs}`);
5763
6105
  return cmdAsk(agent, session, prompt, { noWait: !wait, yolo, timeoutMs });
5764
6106
  }
5765
6107
 
5766
6108
  // AX_REVIEW_MODE=exec: bypass /review command, send instructions directly
5767
- if (process.env.AX_REVIEW_MODE === "exec" && option === "custom" && customInstructions) {
6109
+ if (
6110
+ process.env.AX_REVIEW_MODE === "exec" &&
6111
+ option === "custom" &&
6112
+ customInstructions
6113
+ ) {
5768
6114
  return cmdAsk(agent, session, customInstructions, {
5769
6115
  noWait: !wait,
5770
6116
  yolo,
5771
6117
  timeoutMs,
5772
6118
  });
5773
6119
  }
5774
- const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
6120
+ const nativeYolo =
6121
+ sessionExists && isYoloSession(/** @type {string} */ (session));
5775
6122
 
5776
6123
  // Cannot use --yolo without --wait on a safe session: we need to stay and auto-approve
5777
6124
  if (yolo && !wait && sessionExists && !nativeYolo) {
5778
- console.log("ERROR: --yolo requires waiting on a session not started with --yolo");
5779
- console.log("Restart the session with --yolo, or allow waiting for auto-approval");
6125
+ console.log(
6126
+ "ERROR: --yolo requires waiting on a session not started with --yolo"
6127
+ );
6128
+ console.log(
6129
+ "Restart the session with --yolo, or allow waiting for auto-approval"
6130
+ );
5780
6131
  process.exit(1);
5781
6132
  }
5782
6133
 
@@ -5797,7 +6148,10 @@ async function cmdReview(
5797
6148
  tmuxSend(activeSession, "Enter");
5798
6149
 
5799
6150
  debug("review", `waiting for review menu`);
5800
- await waitFor(activeSession, (s) => s.includes("Select a review preset") || s.includes("review"));
6151
+ await waitFor(
6152
+ activeSession,
6153
+ (s) => s.includes("Select a review preset") || s.includes("review")
6154
+ );
5801
6155
 
5802
6156
  if (option) {
5803
6157
  const key = agent.reviewOptions[option] || option;
@@ -5806,13 +6160,19 @@ async function cmdReview(
5806
6160
 
5807
6161
  if (option === "custom" && customInstructions) {
5808
6162
  debug("review", `waiting for custom instructions prompt`);
5809
- await waitFor(activeSession, (s) => s.includes("custom") || s.includes("instructions"));
6163
+ await waitFor(
6164
+ activeSession,
6165
+ (s) => s.includes("custom") || s.includes("instructions")
6166
+ );
5810
6167
  tmuxSendLiteral(activeSession, customInstructions);
5811
6168
  await sleep(50);
5812
6169
  tmuxSend(activeSession, "Enter");
5813
6170
  } else if (option === "branch") {
5814
6171
  debug("review", `waiting for branch picker`);
5815
- await waitFor(activeSession, (s) => !s.includes("Select a review preset"));
6172
+ await waitFor(
6173
+ activeSession,
6174
+ (s) => !s.includes("Select a review preset")
6175
+ );
5816
6176
  await sleep(200);
5817
6177
  if (customInstructions) {
5818
6178
  debug("review", `typing branch filter: ${customInstructions}`);
@@ -5822,18 +6182,28 @@ async function cmdReview(
5822
6182
  tmuxSend(activeSession, "Enter");
5823
6183
  } else if (option === "commit") {
5824
6184
  debug("review", `waiting for commit picker`);
5825
- await waitFor(activeSession, (s) => !s.includes("Select a review preset"));
6185
+ await waitFor(
6186
+ activeSession,
6187
+ (s) => !s.includes("Select a review preset")
6188
+ );
5826
6189
  await sleep(200);
5827
6190
  if (customInstructions) {
5828
6191
  // Codex commit picker shows messages, not hashes - resolve ref to message
5829
6192
  let searchTerm = customInstructions;
5830
- const gitResult = spawnSync("git", ["log", "--format=%s", "-n", "1", customInstructions], {
5831
- encoding: "utf-8",
5832
- });
6193
+ const gitResult = spawnSync(
6194
+ "git",
6195
+ ["log", "--format=%s", "-n", "1", customInstructions],
6196
+ {
6197
+ encoding: "utf-8",
6198
+ }
6199
+ );
5833
6200
  if (gitResult.status === 0 && gitResult.stdout.trim()) {
5834
6201
  // Use first few words of commit message for search
5835
6202
  searchTerm = gitResult.stdout.trim().slice(0, 40);
5836
- debug("review", `resolved commit ${customInstructions} -> "${searchTerm}"`);
6203
+ debug(
6204
+ "review",
6205
+ `resolved commit ${customInstructions} -> "${searchTerm}"`
6206
+ );
5837
6207
  }
5838
6208
  debug("review", `typing commit filter: ${searchTerm}`);
5839
6209
  tmuxSendLiteral(activeSession, searchTerm);
@@ -5872,7 +6242,7 @@ async function cmdOutput(
5872
6242
  agent,
5873
6243
  session,
5874
6244
  index = 0,
5875
- { wait = false, stale = false, timeoutMs } = {},
6245
+ { wait = false, stale = false, timeoutMs } = {}
5876
6246
  ) {
5877
6247
  if (!session || !tmuxHasSession(session)) {
5878
6248
  console.log("ERROR: no session");
@@ -5901,7 +6271,9 @@ async function cmdOutput(
5901
6271
 
5902
6272
  if (state === State.THINKING) {
5903
6273
  if (!stale) {
5904
- console.log("THINKING: Use --wait to block, or --stale for old response.");
6274
+ console.log(
6275
+ "THINKING: Use --wait to block, or --stale for old response."
6276
+ );
5905
6277
  process.exit(1);
5906
6278
  }
5907
6279
  // --stale: fall through to show previous response
@@ -6099,8 +6471,10 @@ function resolveAgent({ toolFlag, sessionName } = {}) {
6099
6471
 
6100
6472
  // 3. CLI invocation name
6101
6473
  const invoked = path.basename(process.argv[1], ".js");
6102
- if (invoked === "axclaude" || invoked === "claude") return { agent: ClaudeAgent };
6103
- if (invoked === "axcodex" || invoked === "codex") return { agent: CodexAgent };
6474
+ if (invoked === "axclaude" || invoked === "claude")
6475
+ return { agent: ClaudeAgent };
6476
+ if (invoked === "axcodex" || invoked === "codex")
6477
+ return { agent: CodexAgent };
6104
6478
 
6105
6479
  // 4. Infer from parent process (running from within claude/codex)
6106
6480
  const caller = findCallerAgent();
@@ -6112,7 +6486,9 @@ function resolveAgent({ toolFlag, sessionName } = {}) {
6112
6486
  if (defaultTool === "claude") return { agent: ClaudeAgent };
6113
6487
  if (defaultTool === "codex" || !defaultTool) return { agent: CodexAgent };
6114
6488
 
6115
- console.error(`WARNING: invalid AX_DEFAULT_TOOL="${defaultTool}", using codex`);
6489
+ console.error(
6490
+ `WARNING: invalid AX_DEFAULT_TOOL="${defaultTool}", using codex`
6491
+ );
6116
6492
  return { agent: CodexAgent };
6117
6493
  }
6118
6494
 
@@ -6167,11 +6543,15 @@ Flags:
6167
6543
  --auto-approve=TOOLS Auto-approve specific tools (e.g. 'Bash("cargo *")')
6168
6544
  --wait Wait for response (default for messages; required for approve/reject)
6169
6545
  --no-wait Fire-and-forget: send message, print session ID, exit immediately
6170
- --timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
6546
+ --timeout=N Set timeout in seconds (default: ${
6547
+ DEFAULT_TIMEOUT_MS / 1000
6548
+ }, reviews: ${REVIEW_TIMEOUT_MS / 1000})
6171
6549
 
6172
6550
  Examples:
6173
6551
  ${name} "explain this codebase"
6174
- ${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
6552
+ ${name} "review the error handling" # Auto custom review (${
6553
+ REVIEW_TIMEOUT_MS / 60000
6554
+ }min timeout)
6175
6555
  ${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
6176
6556
  ${name} --auto-approve='Bash("cargo *")' "run tests" # Session with specific permissions
6177
6557
  ${name} review uncommitted --wait
@@ -6195,7 +6575,9 @@ async function main() {
6195
6575
  const tmuxCheck = spawnSync("tmux", ["-V"], { encoding: "utf-8" });
6196
6576
  if (tmuxCheck.error || tmuxCheck.status !== 0) {
6197
6577
  console.error("ERROR: tmux is not installed or not in PATH");
6198
- console.error("Install with: brew install tmux (macOS) or apt install tmux (Linux)");
6578
+ console.error(
6579
+ "Install with: brew install tmux (macOS) or apt install tmux (Linux)"
6580
+ );
6199
6581
  process.exit(1);
6200
6582
  }
6201
6583
 
@@ -6211,8 +6593,19 @@ async function main() {
6211
6593
  }
6212
6594
 
6213
6595
  // Extract flags into local variables for convenience
6214
- const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force, stale, autoApprove } =
6215
- flags;
6596
+ const {
6597
+ wait,
6598
+ noWait,
6599
+ yolo,
6600
+ fresh,
6601
+ reasoning,
6602
+ follow,
6603
+ all,
6604
+ orphans,
6605
+ force,
6606
+ stale,
6607
+ autoApprove,
6608
+ } = flags;
6216
6609
 
6217
6610
  // Session resolution (must happen before agent resolution so we can infer tool from session name)
6218
6611
  let session = null;
@@ -6242,7 +6635,9 @@ async function main() {
6242
6635
 
6243
6636
  // Validate --auto-approve is only used with Claude (Codex doesn't support --allowedTools)
6244
6637
  if (autoApprove && agent.name === "codex") {
6245
- console.log("ERROR: --auto-approve is not supported by Codex. Use --yolo instead.");
6638
+ console.log(
6639
+ "ERROR: --auto-approve is not supported by Codex. Use --yolo instead."
6640
+ );
6246
6641
  process.exit(1);
6247
6642
  }
6248
6643
 
@@ -6276,7 +6671,10 @@ async function main() {
6276
6671
  // Dispatch commands
6277
6672
  if (cmd === "agents" || cmd === "list") return cmdAgents();
6278
6673
  if (cmd === "target") {
6279
- const defaultSession = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
6674
+ const defaultSession = agent.getDefaultSession({
6675
+ allowedTools: autoApprove,
6676
+ yolo,
6677
+ });
6280
6678
  if (defaultSession) {
6281
6679
  console.log(defaultSession);
6282
6680
  } else {
@@ -6290,11 +6688,15 @@ async function main() {
6290
6688
  if (cmd === "archangel") return cmdArchangel(positionals[1]);
6291
6689
  if (cmd === "kill") return cmdKill(session, { all, orphans, force });
6292
6690
  if (cmd === "attach") {
6293
- const attachSession = positionals[1] ? resolveSessionName(positionals[1]) : session;
6691
+ const attachSession = positionals[1]
6692
+ ? resolveSessionName(positionals[1])
6693
+ : session;
6294
6694
  return cmdAttach(attachSession);
6295
6695
  }
6296
6696
  if (cmd === "log") {
6297
- const logSession = positionals[1] ? resolveSessionName(positionals[1]) : session;
6697
+ const logSession = positionals[1]
6698
+ ? resolveSessionName(positionals[1])
6699
+ : session;
6298
6700
  return cmdLog(logSession, { tail, reasoning, follow });
6299
6701
  }
6300
6702
  if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
@@ -6336,15 +6738,23 @@ async function main() {
6336
6738
  if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
6337
6739
  if (cmd === "review") {
6338
6740
  const customInstructions = await readStdinIfNeeded(positionals[2]);
6339
- return cmdReview(agent, session, positionals[1], customInstructions ?? undefined, {
6340
- wait: !noWait,
6341
- fresh,
6342
- timeoutMs: flags.timeout !== undefined ? timeoutMs : REVIEW_TIMEOUT_MS,
6343
- });
6741
+ return cmdReview(
6742
+ agent,
6743
+ session,
6744
+ positionals[1],
6745
+ customInstructions ?? undefined,
6746
+ {
6747
+ wait: !noWait,
6748
+ fresh,
6749
+ timeoutMs: flags.timeout !== undefined ? timeoutMs : REVIEW_TIMEOUT_MS,
6750
+ }
6751
+ );
6344
6752
  }
6345
6753
  if (cmd === "status") return cmdStatus(agent, session);
6346
6754
  if (cmd === "debug") {
6347
- const debugSession = positionals[1] ? resolveSessionName(positionals[1]) : session;
6755
+ const debugSession = positionals[1]
6756
+ ? resolveSessionName(positionals[1])
6757
+ : session;
6348
6758
  return cmdDebug(agent, debugSession);
6349
6759
  }
6350
6760
  if (cmd === "output") {
@@ -6354,7 +6764,8 @@ async function main() {
6354
6764
  }
6355
6765
  if (cmd === "send" && positionals.length > 1)
6356
6766
  return cmdSend(session, positionals.slice(1).join(" "));
6357
- if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
6767
+ if (cmd === "compact")
6768
+ return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
6358
6769
  if (cmd === "reset") {
6359
6770
  // Send /new and wait for completion
6360
6771
  await cmdAsk(agent, session, "/new", { timeoutMs });
@@ -6419,7 +6830,9 @@ if (isDirectRun) {
6419
6830
  main().catch((err) => {
6420
6831
  console.log(`ERROR: ${err.message}`);
6421
6832
  if (err instanceof TimeoutError && err.session) {
6422
- console.log(`Hint: Use 'ax debug --session=${err.session}' to see current screen state`);
6833
+ console.log(
6834
+ `Hint: Use 'ax debug --session=${err.session}' to see current screen state`
6835
+ );
6423
6836
  }
6424
6837
  process.exit(1);
6425
6838
  });