ax-agents 0.0.1-alpha.6 → 0.0.1-alpha.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 +556 -158
  2. package/package.json +1 -1
package/ax.js CHANGED
@@ -23,11 +23,15 @@ import {
23
23
  renameSync,
24
24
  realpathSync,
25
25
  watch,
26
+ openSync,
27
+ readSync,
28
+ closeSync,
26
29
  } from "node:fs";
27
30
  import { randomUUID } from "node:crypto";
28
31
  import { fileURLToPath } from "node:url";
29
32
  import path from "node:path";
30
33
  import os from "node:os";
34
+ import { parseArgs } from "node:util";
31
35
 
32
36
  const __filename = fileURLToPath(import.meta.url);
33
37
  const __dirname = path.dirname(__filename);
@@ -302,6 +306,21 @@ const ARCHANGEL_GIT_CONTEXT_HOURS = 4;
302
306
  const ARCHANGEL_GIT_CONTEXT_MAX_LINES = 200;
303
307
  const ARCHANGEL_PARENT_CONTEXT_ENTRIES = 10;
304
308
 
309
+ /**
310
+ * @param {string} session
311
+ * @param {(screen: string) => boolean} predicate
312
+ * @param {number} [timeoutMs]
313
+ * @returns {Promise<string>}
314
+ */
315
+ class TimeoutError extends Error {
316
+ /** @param {string} [session] */
317
+ constructor(session) {
318
+ super("timeout");
319
+ this.name = "TimeoutError";
320
+ this.session = session;
321
+ }
322
+ }
323
+
305
324
  /**
306
325
  * @param {string} session
307
326
  * @param {(screen: string) => boolean} predicate
@@ -315,7 +334,7 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
315
334
  if (predicate(screen)) return screen;
316
335
  await sleep(POLL_MS);
317
336
  }
318
- throw new Error("timeout");
337
+ throw new TimeoutError(session);
319
338
  }
320
339
 
321
340
  // =============================================================================
@@ -343,6 +362,38 @@ function findCallerPid() {
343
362
  return null;
344
363
  }
345
364
 
365
+ /**
366
+ * Find orphaned claude/codex processes (PPID=1, reparented to init/launchd)
367
+ * @returns {{pid: string, command: string}[]}
368
+ */
369
+ function findOrphanedProcesses() {
370
+ const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf-8" });
371
+
372
+ if (result.status !== 0 || !result.stdout.trim()) {
373
+ return [];
374
+ }
375
+
376
+ const orphans = [];
377
+ for (const line of result.stdout.trim().split("\n")) {
378
+ // Parse: " PID PPID command args..."
379
+ const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.+)$/);
380
+ if (!match) continue;
381
+
382
+ const [, pid, ppid, args] = match;
383
+
384
+ // Must have PPID=1 (orphaned/reparented to init)
385
+ if (ppid !== "1") continue;
386
+
387
+ // Command must START with claude or codex (excludes tmux which also has PPID=1)
388
+ const cmd = args.split(/\s+/)[0];
389
+ if (cmd !== "claude" && cmd !== "codex") continue;
390
+
391
+ orphans.push({ pid, command: args.slice(0, 60) });
392
+ }
393
+
394
+ return orphans;
395
+ }
396
+
346
397
  // =============================================================================
347
398
  // Helpers - stdin
348
399
  // =============================================================================
@@ -371,6 +422,86 @@ async function readStdin() {
371
422
  }
372
423
 
373
424
  // =============================================================================
425
+ // =============================================================================
426
+ // Helpers - CLI argument parsing
427
+ // =============================================================================
428
+
429
+ /**
430
+ * Parse CLI arguments using Node.js built-in parseArgs.
431
+ * @param {string[]} args - Command line arguments (without node and script path)
432
+ * @returns {{ flags: ParsedFlags, positionals: string[] }}
433
+ *
434
+ * @typedef {Object} ParsedFlags
435
+ * @property {boolean} wait
436
+ * @property {boolean} noWait
437
+ * @property {boolean} yolo
438
+ * @property {boolean} fresh
439
+ * @property {boolean} reasoning
440
+ * @property {boolean} follow
441
+ * @property {boolean} all
442
+ * @property {boolean} orphans
443
+ * @property {boolean} force
444
+ * @property {boolean} version
445
+ * @property {boolean} help
446
+ * @property {string} [tool]
447
+ * @property {string} [session]
448
+ * @property {number} [timeout]
449
+ * @property {number} [tail]
450
+ * @property {number} [limit]
451
+ * @property {string} [branch]
452
+ */
453
+ function parseCliArgs(args) {
454
+ const { values, positionals } = parseArgs({
455
+ args,
456
+ options: {
457
+ // Boolean flags
458
+ wait: { type: "boolean", default: false },
459
+ "no-wait": { type: "boolean", default: false },
460
+ yolo: { type: "boolean", default: false },
461
+ fresh: { type: "boolean", default: false },
462
+ reasoning: { type: "boolean", default: false },
463
+ follow: { type: "boolean", short: "f", default: false },
464
+ all: { type: "boolean", default: false },
465
+ orphans: { type: "boolean", default: false },
466
+ force: { type: "boolean", default: false },
467
+ version: { type: "boolean", short: "V", default: false },
468
+ help: { type: "boolean", short: "h", default: false },
469
+ // Value flags
470
+ tool: { type: "string" },
471
+ session: { type: "string" },
472
+ timeout: { type: "string" },
473
+ tail: { type: "string" },
474
+ limit: { type: "string" },
475
+ branch: { type: "string" },
476
+ },
477
+ allowPositionals: true,
478
+ strict: false, // Don't error on unknown flags
479
+ });
480
+
481
+ return {
482
+ flags: {
483
+ wait: Boolean(values.wait),
484
+ noWait: Boolean(values["no-wait"]),
485
+ yolo: Boolean(values.yolo),
486
+ fresh: Boolean(values.fresh),
487
+ reasoning: Boolean(values.reasoning),
488
+ follow: Boolean(values.follow),
489
+ all: Boolean(values.all),
490
+ orphans: Boolean(values.orphans),
491
+ force: Boolean(values.force),
492
+ version: Boolean(values.version),
493
+ help: Boolean(values.help),
494
+ tool: /** @type {string | undefined} */ (values.tool),
495
+ session: /** @type {string | undefined} */ (values.session),
496
+ timeout: values.timeout !== undefined ? Number(values.timeout) : undefined,
497
+ tail: values.tail !== undefined ? Number(values.tail) : undefined,
498
+ limit: values.limit !== undefined ? Number(values.limit) : undefined,
499
+ branch: /** @type {string | undefined} */ (values.branch),
500
+ },
501
+ positionals,
502
+ };
503
+ }
504
+
374
505
  // Helpers - session tracking
375
506
  // =============================================================================
376
507
 
@@ -585,6 +716,130 @@ function getAssistantText(logPath, index = 0) {
585
716
  }
586
717
  }
587
718
 
719
+ /**
720
+ * Read new complete JSON lines from a log file since the given offset.
721
+ * @param {string | null} logPath
722
+ * @param {number} fromOffset
723
+ * @returns {{ entries: object[], newOffset: number }}
724
+ */
725
+ function tailJsonl(logPath, fromOffset) {
726
+ if (!logPath || !existsSync(logPath)) {
727
+ return { entries: [], newOffset: fromOffset };
728
+ }
729
+
730
+ const stats = statSync(logPath);
731
+ if (stats.size <= fromOffset) {
732
+ return { entries: [], newOffset: fromOffset };
733
+ }
734
+
735
+ const fd = openSync(logPath, "r");
736
+ const buffer = Buffer.alloc(stats.size - fromOffset);
737
+ readSync(fd, buffer, 0, buffer.length, fromOffset);
738
+ closeSync(fd);
739
+
740
+ const text = buffer.toString("utf-8");
741
+ const lines = text.split("\n");
742
+
743
+ // Last line may be incomplete - don't parse it yet
744
+ const complete = lines.slice(0, -1).filter(Boolean);
745
+ const incomplete = lines[lines.length - 1];
746
+
747
+ const entries = [];
748
+ for (const line of complete) {
749
+ try {
750
+ entries.push(JSON.parse(line));
751
+ } catch {
752
+ // Skip malformed lines
753
+ }
754
+ }
755
+
756
+ // Offset advances by complete lines only
757
+ const newOffset = fromOffset + text.length - incomplete.length;
758
+ return { entries, newOffset };
759
+ }
760
+
761
+ /**
762
+ * @typedef {{command?: string, file_path?: string, path?: string, pattern?: string}} ToolInput
763
+ */
764
+
765
+ /**
766
+ * Format a JSONL entry for streaming display.
767
+ * @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, name?: string, input?: ToolInput, tool?: string, arguments?: ToolInput}>}}} entry
768
+ * @returns {string | null}
769
+ */
770
+ function formatEntry(entry) {
771
+ // Skip tool_result entries (they can be very verbose)
772
+ if (entry.type === "tool_result") return null;
773
+
774
+ // Only process assistant entries
775
+ if (entry.type !== "assistant") return null;
776
+
777
+ const parts = entry.message?.content || [];
778
+ const output = [];
779
+
780
+ for (const part of parts) {
781
+ if (part.type === "text" && part.text) {
782
+ output.push(part.text);
783
+ } else if (part.type === "tool_use" || part.type === "tool_call") {
784
+ const name = part.name || part.tool || "tool";
785
+ const input = part.input || part.arguments || {};
786
+ let summary;
787
+ if (name === "Bash" && input.command) {
788
+ summary = input.command.slice(0, 50);
789
+ } else {
790
+ const target = input.file_path || input.path || input.pattern || "";
791
+ summary = target.split("/").pop() || target.slice(0, 30);
792
+ }
793
+ output.push(`> ${name}(${summary})`);
794
+ }
795
+ // Skip thinking blocks - internal reasoning
796
+ }
797
+
798
+ return output.length > 0 ? output.join("\n") : null;
799
+ }
800
+
801
+ /**
802
+ * Extract pending tool from confirmation screen.
803
+ * @param {string} screen
804
+ * @returns {string | null}
805
+ */
806
+ function extractPendingToolFromScreen(screen) {
807
+ const lines = screen.split("\n");
808
+
809
+ // Check recent lines for tool confirmation patterns
810
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - 15); i--) {
811
+ const line = lines[i];
812
+ // Match tool confirmation patterns like "Bash: command" or "Write: /path/file"
813
+ const match = line.match(
814
+ /^\s*(Bash|Write|Edit|Read|Glob|Grep|Task|WebFetch|WebSearch|NotebookEdit|Skill|TodoWrite|TodoRead):\s*(.{1,40})/,
815
+ );
816
+ if (match) {
817
+ return `${match[1]}: ${match[2].trim()}`;
818
+ }
819
+ }
820
+
821
+ return null;
822
+ }
823
+
824
+ /**
825
+ * Format confirmation output with helpful commands
826
+ * @param {string} screen
827
+ * @param {Agent} _agent
828
+ * @returns {string}
829
+ */
830
+ function formatConfirmationOutput(screen, _agent) {
831
+ const pendingTool = extractPendingToolFromScreen(screen);
832
+ const cli = path.basename(process.argv[1], ".js");
833
+
834
+ let output = pendingTool || "Confirmation required";
835
+ output += "\n\ne.g.";
836
+ output += `\n ${cli} approve # for y/n prompts`;
837
+ output += `\n ${cli} reject`;
838
+ output += `\n ${cli} select N # for numbered menus`;
839
+
840
+ return output;
841
+ }
842
+
588
843
  /**
589
844
  * @returns {string[]}
590
845
  */
@@ -1457,7 +1712,7 @@ const State = {
1457
1712
  * @param {string} config.promptSymbol - Symbol indicating ready state
1458
1713
  * @param {string[]} [config.spinners] - Spinner characters indicating thinking
1459
1714
  * @param {RegExp} [config.rateLimitPattern] - Pattern for rate limit detection
1460
- * @param {string[]} [config.thinkingPatterns] - Text patterns indicating thinking
1715
+ * @param {(string | RegExp | ((lines: string) => boolean))[]} [config.thinkingPatterns] - Text patterns indicating thinking
1461
1716
  * @param {(string | ((lines: string) => boolean))[]} [config.confirmPatterns] - Patterns for confirmation dialogs
1462
1717
  * @param {{screen: string[], lastLines: string[]} | null} [config.updatePromptPatterns] - Patterns for update prompts
1463
1718
  * @returns {string} The detected state
@@ -1475,14 +1730,33 @@ function detectState(screen, config) {
1475
1730
  return State.RATE_LIMITED;
1476
1731
  }
1477
1732
 
1478
- // Thinking - spinners (full screen, they're unique UI elements)
1733
+ // Confirming - check before THINKING because "Running…" in tool output matches thinking patterns
1734
+ const confirmPatterns = config.confirmPatterns || [];
1735
+ for (const pattern of confirmPatterns) {
1736
+ if (typeof pattern === "function") {
1737
+ // Functions check lastLines first (most specific), then recentLines
1738
+ if (pattern(lastLines)) return State.CONFIRMING;
1739
+ if (pattern(recentLines)) return State.CONFIRMING;
1740
+ } else {
1741
+ // String patterns check recentLines (bounded range)
1742
+ if (recentLines.includes(pattern)) return State.CONFIRMING;
1743
+ }
1744
+ }
1745
+
1746
+ // Thinking - spinners (check last lines only to avoid false positives from timing messages like "✻ Crunched for 32s")
1479
1747
  const spinners = config.spinners || [];
1480
- if (spinners.some((s) => screen.includes(s))) {
1748
+ if (spinners.some((s) => lastLines.includes(s))) {
1481
1749
  return State.THINKING;
1482
1750
  }
1483
- // Thinking - text patterns (last lines)
1751
+ // Thinking - text patterns (last lines) - supports strings, regexes, and functions
1484
1752
  const thinkingPatterns = config.thinkingPatterns || [];
1485
- if (thinkingPatterns.some((p) => lastLines.includes(p))) {
1753
+ if (
1754
+ thinkingPatterns.some((p) => {
1755
+ if (typeof p === "function") return p(lastLines);
1756
+ if (p instanceof RegExp) return p.test(lastLines);
1757
+ return lastLines.includes(p);
1758
+ })
1759
+ ) {
1486
1760
  return State.THINKING;
1487
1761
  }
1488
1762
 
@@ -1494,19 +1768,6 @@ function detectState(screen, config) {
1494
1768
  }
1495
1769
  }
1496
1770
 
1497
- // Confirming - check recent lines (not full screen to avoid history false positives)
1498
- const confirmPatterns = config.confirmPatterns || [];
1499
- for (const pattern of confirmPatterns) {
1500
- if (typeof pattern === "function") {
1501
- // Functions check lastLines first (most specific), then recentLines
1502
- if (pattern(lastLines)) return State.CONFIRMING;
1503
- if (pattern(recentLines)) return State.CONFIRMING;
1504
- } else {
1505
- // String patterns check recentLines (bounded range)
1506
- if (recentLines.includes(pattern)) return State.CONFIRMING;
1507
- }
1508
- }
1509
-
1510
1771
  // Ready - only if prompt symbol is visible AND not followed by pasted content
1511
1772
  // "[Pasted text" indicates user has pasted content and Claude is still processing
1512
1773
  if (lastLines.includes(config.promptSymbol)) {
@@ -1541,12 +1802,13 @@ function detectState(screen, config) {
1541
1802
  /**
1542
1803
  * @typedef {Object} AgentConfigInput
1543
1804
  * @property {string} name
1805
+ * @property {string} displayName
1544
1806
  * @property {string} startCommand
1545
1807
  * @property {string} yoloCommand
1546
1808
  * @property {string} promptSymbol
1547
1809
  * @property {string[]} [spinners]
1548
1810
  * @property {RegExp} [rateLimitPattern]
1549
- * @property {string[]} [thinkingPatterns]
1811
+ * @property {(string | RegExp | ((lines: string) => boolean))[]} [thinkingPatterns]
1550
1812
  * @property {ConfirmPattern[]} [confirmPatterns]
1551
1813
  * @property {UpdatePromptPatterns | null} [updatePromptPatterns]
1552
1814
  * @property {string[]} [responseMarkers]
@@ -1556,6 +1818,8 @@ function detectState(screen, config) {
1556
1818
  * @property {string} [approveKey]
1557
1819
  * @property {string} [rejectKey]
1558
1820
  * @property {string} [safeAllowedTools]
1821
+ * @property {string | null} [sessionIdFlag]
1822
+ * @property {((sessionName: string) => string | null) | null} [logPathFinder]
1559
1823
  */
1560
1824
 
1561
1825
  class Agent {
@@ -1566,6 +1830,8 @@ class Agent {
1566
1830
  /** @type {string} */
1567
1831
  this.name = config.name;
1568
1832
  /** @type {string} */
1833
+ this.displayName = config.displayName;
1834
+ /** @type {string} */
1569
1835
  this.startCommand = config.startCommand;
1570
1836
  /** @type {string} */
1571
1837
  this.yoloCommand = config.yoloCommand;
@@ -1575,7 +1841,7 @@ class Agent {
1575
1841
  this.spinners = config.spinners || [];
1576
1842
  /** @type {RegExp | undefined} */
1577
1843
  this.rateLimitPattern = config.rateLimitPattern;
1578
- /** @type {string[]} */
1844
+ /** @type {(string | RegExp | ((lines: string) => boolean))[]} */
1579
1845
  this.thinkingPatterns = config.thinkingPatterns || [];
1580
1846
  /** @type {ConfirmPattern[]} */
1581
1847
  this.confirmPatterns = config.confirmPatterns || [];
@@ -1595,6 +1861,10 @@ class Agent {
1595
1861
  this.rejectKey = config.rejectKey || "n";
1596
1862
  /** @type {string | undefined} */
1597
1863
  this.safeAllowedTools = config.safeAllowedTools;
1864
+ /** @type {string | null} */
1865
+ this.sessionIdFlag = config.sessionIdFlag || null;
1866
+ /** @type {((sessionName: string) => string | null) | null} */
1867
+ this.logPathFinder = config.logPathFinder || null;
1598
1868
  }
1599
1869
 
1600
1870
  /**
@@ -1612,11 +1882,11 @@ class Agent {
1612
1882
  } else {
1613
1883
  base = this.startCommand;
1614
1884
  }
1615
- // Claude supports --session-id for deterministic session tracking
1616
- if (this.name === "claude" && sessionName) {
1885
+ // Some agents support session ID flags for deterministic session tracking
1886
+ if (this.sessionIdFlag && sessionName) {
1617
1887
  const parsed = parseSessionName(sessionName);
1618
1888
  if (parsed?.uuid) {
1619
- return `${base} --session-id ${parsed.uuid}`;
1889
+ return `${base} ${this.sessionIdFlag} ${parsed.uuid}`;
1620
1890
  }
1621
1891
  }
1622
1892
  return base;
@@ -1674,13 +1944,8 @@ class Agent {
1674
1944
  * @returns {string | null}
1675
1945
  */
1676
1946
  findLogPath(sessionName) {
1677
- const parsed = parseSessionName(sessionName);
1678
- if (this.name === "claude") {
1679
- const uuid = parsed?.uuid;
1680
- if (uuid) return findClaudeLogPath(uuid, sessionName);
1681
- }
1682
- if (this.name === "codex") {
1683
- return findCodexLogPath(sessionName);
1947
+ if (this.logPathFinder) {
1948
+ return this.logPathFinder(sessionName);
1684
1949
  }
1685
1950
  return null;
1686
1951
  }
@@ -1908,6 +2173,7 @@ class Agent {
1908
2173
 
1909
2174
  const CodexAgent = new Agent({
1910
2175
  name: "codex",
2176
+ displayName: "Codex",
1911
2177
  startCommand: "codex --sandbox read-only",
1912
2178
  yoloCommand: "codex --dangerously-bypass-approvals-and-sandbox",
1913
2179
  promptSymbol: "›",
@@ -1927,6 +2193,7 @@ const CodexAgent = new Agent({
1927
2193
  chromePatterns: ["context left", "for shortcuts"],
1928
2194
  reviewOptions: { pr: "1", uncommitted: "2", commit: "3", custom: "4" },
1929
2195
  envVar: "AX_SESSION",
2196
+ logPathFinder: findCodexLogPath,
1930
2197
  });
1931
2198
 
1932
2199
  // =============================================================================
@@ -1935,12 +2202,15 @@ const CodexAgent = new Agent({
1935
2202
 
1936
2203
  const ClaudeAgent = new Agent({
1937
2204
  name: "claude",
2205
+ displayName: "Claude",
1938
2206
  startCommand: "claude",
1939
2207
  yoloCommand: "claude --dangerously-skip-permissions",
1940
2208
  promptSymbol: "❯",
1941
- spinners: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
2209
+ // Claude Code spinners: ·✢✳✶✻✽ (from cli.js source)
2210
+ spinners: ["·", "✢", "✳", "✶", "✻", "✽"],
1942
2211
  rateLimitPattern: /rate.?limit/i,
1943
- thinkingPatterns: ["Thinking"],
2212
+ // Claude uses whimsical verbs like "Wibbling…", "Dancing…", etc. Match any capitalized -ing word + ellipsis (… or ...)
2213
+ thinkingPatterns: ["Thinking", /[A-Z][a-z]+ing(…|\.\.\.)/],
1944
2214
  confirmPatterns: [
1945
2215
  "Do you want to make this edit",
1946
2216
  "Do you want to run this command",
@@ -1965,6 +2235,13 @@ const ClaudeAgent = new Agent({
1965
2235
  envVar: "AX_SESSION",
1966
2236
  approveKey: "1",
1967
2237
  rejectKey: "Escape",
2238
+ sessionIdFlag: "--session-id",
2239
+ logPathFinder: (sessionName) => {
2240
+ const parsed = parseSessionName(sessionName);
2241
+ const uuid = parsed?.uuid;
2242
+ if (uuid) return findClaudeLogPath(uuid, sessionName);
2243
+ return null;
2244
+ },
1968
2245
  });
1969
2246
 
1970
2247
  // =============================================================================
@@ -2002,30 +2279,38 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
2002
2279
  return { state, screen };
2003
2280
  }
2004
2281
  }
2005
- throw new Error("timeout");
2282
+ throw new TimeoutError(session);
2006
2283
  }
2007
2284
 
2008
2285
  /**
2009
- * Wait for agent to process a new message and respond.
2010
- * Waits for screen activity before considering the response complete.
2286
+ * Core polling loop for waiting on agent responses.
2011
2287
  * @param {Agent} agent
2012
2288
  * @param {string} session
2013
- * @param {number} [timeoutMs]
2289
+ * @param {number} timeoutMs
2290
+ * @param {{onPoll?: (screen: string, state: string) => void, onStateChange?: (state: string, lastState: string | null, screen: string) => void, onReady?: (screen: string) => void}} [hooks]
2014
2291
  * @returns {Promise<{state: string, screen: string}>}
2015
2292
  */
2016
- async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
2293
+ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
2294
+ const { onPoll, onStateChange, onReady } = hooks;
2017
2295
  const start = Date.now();
2018
2296
  const initialScreen = tmuxCapture(session);
2019
2297
 
2020
2298
  let lastScreen = initialScreen;
2299
+ let lastState = null;
2021
2300
  let stableAt = null;
2022
2301
  let sawActivity = false;
2023
2302
 
2024
2303
  while (Date.now() - start < timeoutMs) {
2025
- await sleep(POLL_MS);
2026
2304
  const screen = tmuxCapture(session);
2027
2305
  const state = agent.getState(screen);
2028
2306
 
2307
+ if (onPoll) onPoll(screen, state);
2308
+
2309
+ if (state !== lastState) {
2310
+ if (onStateChange) onStateChange(state, lastState, screen);
2311
+ lastState = state;
2312
+ }
2313
+
2029
2314
  if (state === State.RATE_LIMITED || state === State.CONFIRMING) {
2030
2315
  return { state, screen };
2031
2316
  }
@@ -2040,6 +2325,7 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
2040
2325
 
2041
2326
  if (sawActivity && stableAt && Date.now() - stableAt >= STABLE_MS) {
2042
2327
  if (state === State.READY) {
2328
+ if (onReady) onReady(screen);
2043
2329
  return { state, screen };
2044
2330
  }
2045
2331
  }
@@ -2047,26 +2333,86 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
2047
2333
  if (state === State.THINKING) {
2048
2334
  sawActivity = true;
2049
2335
  }
2336
+
2337
+ await sleep(POLL_MS);
2050
2338
  }
2051
- throw new Error("timeout");
2339
+ throw new TimeoutError(session);
2052
2340
  }
2053
2341
 
2054
2342
  /**
2055
- * Auto-approve loop that keeps approving confirmations until the agent is ready or rate limited.
2056
- * Used by callers to implement yolo mode on sessions not started with native --yolo.
2343
+ * Wait for agent response without streaming output.
2057
2344
  * @param {Agent} agent
2058
2345
  * @param {string} session
2059
2346
  * @param {number} [timeoutMs]
2060
2347
  * @returns {Promise<{state: string, screen: string}>}
2061
2348
  */
2062
- async function autoApproveLoop(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
2349
+ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
2350
+ return pollForResponse(agent, session, timeoutMs);
2351
+ }
2352
+
2353
+ /**
2354
+ * Wait for agent response with streaming output to console.
2355
+ * @param {Agent} agent
2356
+ * @param {string} session
2357
+ * @param {number} [timeoutMs]
2358
+ * @returns {Promise<{state: string, screen: string}>}
2359
+ */
2360
+ async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
2361
+ let logPath = agent.findLogPath(session);
2362
+ let logOffset = logPath && existsSync(logPath) ? statSync(logPath).size : 0;
2363
+ let printedThinking = false;
2364
+
2365
+ const streamNewEntries = () => {
2366
+ if (!logPath) {
2367
+ logPath = agent.findLogPath(session);
2368
+ if (logPath && existsSync(logPath)) {
2369
+ logOffset = statSync(logPath).size;
2370
+ }
2371
+ }
2372
+ if (logPath) {
2373
+ const { entries, newOffset } = tailJsonl(logPath, logOffset);
2374
+ logOffset = newOffset;
2375
+ for (const entry of entries) {
2376
+ const formatted = formatEntry(entry);
2377
+ if (formatted) console.log(formatted);
2378
+ }
2379
+ }
2380
+ };
2381
+
2382
+ return pollForResponse(agent, session, timeoutMs, {
2383
+ onPoll: () => streamNewEntries(),
2384
+ onStateChange: (state, lastState, screen) => {
2385
+ if (state === State.THINKING && !printedThinking) {
2386
+ console.log("[THINKING]");
2387
+ printedThinking = true;
2388
+ } else if (state === State.CONFIRMING) {
2389
+ const pendingTool = extractPendingToolFromScreen(screen);
2390
+ console.log(pendingTool ? `[CONFIRMING] ${pendingTool}` : "[CONFIRMING]");
2391
+ }
2392
+ if (lastState === State.THINKING && state !== State.THINKING) {
2393
+ printedThinking = false;
2394
+ }
2395
+ },
2396
+ onReady: () => streamNewEntries(),
2397
+ });
2398
+ }
2399
+
2400
+ /**
2401
+ * Auto-approve loop that keeps approving confirmations until the agent is ready or rate limited.
2402
+ * @param {Agent} agent
2403
+ * @param {string} session
2404
+ * @param {number} timeoutMs
2405
+ * @param {Function} waitFn - waitForResponse or streamResponse
2406
+ * @returns {Promise<{state: string, screen: string}>}
2407
+ */
2408
+ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
2063
2409
  const deadline = Date.now() + timeoutMs;
2064
2410
 
2065
2411
  while (Date.now() < deadline) {
2066
2412
  const remaining = deadline - Date.now();
2067
2413
  if (remaining <= 0) break;
2068
2414
 
2069
- const { state, screen } = await waitForResponse(agent, session, remaining);
2415
+ const { state, screen } = await waitFn(agent, session, remaining);
2070
2416
 
2071
2417
  if (state === State.RATE_LIMITED || state === State.READY) {
2072
2418
  return { state, screen };
@@ -2078,11 +2424,10 @@ async function autoApproveLoop(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
2078
2424
  continue;
2079
2425
  }
2080
2426
 
2081
- // Unexpected state - log and continue polling
2082
2427
  debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
2083
2428
  }
2084
2429
 
2085
- throw new Error("timeout");
2430
+ throw new TimeoutError(session);
2086
2431
  }
2087
2432
 
2088
2433
  /**
@@ -2141,9 +2486,22 @@ function cmdAgents() {
2141
2486
 
2142
2487
  if (agentSessions.length === 0) {
2143
2488
  console.log("No agents running");
2489
+ // Still check for orphans
2490
+ const orphans = findOrphanedProcesses();
2491
+ if (orphans.length > 0) {
2492
+ console.log(`\nOrphaned (${orphans.length}):`);
2493
+ for (const { pid, command } of orphans) {
2494
+ console.log(` PID ${pid}: ${command}`);
2495
+ }
2496
+ console.log(`\n Run 'ax kill --orphans' to clean up`);
2497
+ }
2144
2498
  return;
2145
2499
  }
2146
2500
 
2501
+ // Get default session for each agent type
2502
+ const claudeDefault = ClaudeAgent.getDefaultSession();
2503
+ const codexDefault = CodexAgent.getDefaultSession();
2504
+
2147
2505
  // Get info for each agent
2148
2506
  const agents = agentSessions.map((session) => {
2149
2507
  const parsed = /** @type {ParsedSession} */ (parseSessionName(session));
@@ -2152,30 +2510,45 @@ function cmdAgents() {
2152
2510
  const state = agent.getState(screen);
2153
2511
  const logPath = agent.findLogPath(session);
2154
2512
  const type = parsed.archangelName ? "archangel" : "-";
2513
+ const isDefault =
2514
+ (parsed.tool === "claude" && session === claudeDefault) ||
2515
+ (parsed.tool === "codex" && session === codexDefault);
2155
2516
 
2156
2517
  return {
2157
2518
  session,
2158
2519
  tool: parsed.tool,
2159
2520
  state: state || "unknown",
2521
+ target: isDefault ? "*" : "",
2160
2522
  type,
2161
2523
  log: logPath || "-",
2162
2524
  };
2163
2525
  });
2164
2526
 
2165
- // Print table
2527
+ // Print sessions table
2166
2528
  const maxSession = Math.max(7, ...agents.map((a) => a.session.length));
2167
2529
  const maxTool = Math.max(4, ...agents.map((a) => a.tool.length));
2168
2530
  const maxState = Math.max(5, ...agents.map((a) => a.state.length));
2531
+ const maxTarget = Math.max(6, ...agents.map((a) => a.target.length));
2169
2532
  const maxType = Math.max(4, ...agents.map((a) => a.type.length));
2170
2533
 
2171
2534
  console.log(
2172
- `${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TYPE".padEnd(maxType)} LOG`,
2535
+ `${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(maxTarget)} ${"TYPE".padEnd(maxType)} LOG`,
2173
2536
  );
2174
2537
  for (const a of agents) {
2175
2538
  console.log(
2176
- `${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.type.padEnd(maxType)} ${a.log}`,
2539
+ `${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.target.padEnd(maxTarget)} ${a.type.padEnd(maxType)} ${a.log}`,
2177
2540
  );
2178
2541
  }
2542
+
2543
+ // Print orphaned processes if any
2544
+ const orphans = findOrphanedProcesses();
2545
+ if (orphans.length > 0) {
2546
+ console.log(`\nOrphaned (${orphans.length}):`);
2547
+ for (const { pid, command } of orphans) {
2548
+ console.log(` PID ${pid}: ${command}`);
2549
+ }
2550
+ console.log(`\n Run 'ax kill --orphans' to clean up`);
2551
+ }
2179
2552
  }
2180
2553
 
2181
2554
  // =============================================================================
@@ -2838,9 +3211,31 @@ function ensureClaudeHookConfig() {
2838
3211
 
2839
3212
  /**
2840
3213
  * @param {string | null | undefined} session
2841
- * @param {{all?: boolean}} [options]
3214
+ * @param {{all?: boolean, orphans?: boolean, force?: boolean}} [options]
2842
3215
  */
2843
- function cmdKill(session, { all = false } = {}) {
3216
+ function cmdKill(session, { all = false, orphans = false, force = false } = {}) {
3217
+ // Handle orphaned processes
3218
+ if (orphans) {
3219
+ const orphanedProcesses = findOrphanedProcesses();
3220
+
3221
+ if (orphanedProcesses.length === 0) {
3222
+ console.log("No orphaned processes found");
3223
+ return;
3224
+ }
3225
+
3226
+ const signal = force ? "-9" : "-15"; // SIGKILL vs SIGTERM
3227
+ let killed = 0;
3228
+ for (const { pid, command } of orphanedProcesses) {
3229
+ const result = spawnSync("kill", [signal, pid]);
3230
+ if (result.status === 0) {
3231
+ console.log(`Killed: PID ${pid} (${command.slice(0, 40)})`);
3232
+ killed++;
3233
+ }
3234
+ }
3235
+ console.log(`Killed ${killed} orphaned process(es)${force ? " (forced)" : ""}`);
3236
+ return;
3237
+ }
3238
+
2844
3239
  // If specific session provided, kill just that one
2845
3240
  if (session) {
2846
3241
  if (!tmuxHasSession(session)) {
@@ -3203,7 +3598,7 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
3203
3598
  * @param {string} message
3204
3599
  * @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
3205
3600
  */
3206
- async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs } = {}) {
3601
+ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
3207
3602
  const sessionExists = session != null && tmuxHasSession(session);
3208
3603
  const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
3209
3604
 
@@ -3223,14 +3618,23 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
3223
3618
  await sleep(50);
3224
3619
  tmuxSend(activeSession, "Enter");
3225
3620
 
3226
- if (noWait) return;
3621
+ if (noWait) {
3622
+ const parsed = parseSessionName(activeSession);
3623
+ const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
3624
+ const cli = path.basename(process.argv[1], ".js");
3625
+ console.log(`Sent to: ${shortId}
3626
+
3627
+ e.g.
3628
+ ${cli} status --session=${shortId}
3629
+ ${cli} output --session=${shortId}`);
3630
+ return;
3631
+ }
3227
3632
 
3228
- // Yolo mode on a safe session: auto-approve until done
3229
3633
  const useAutoApprove = yolo && !nativeYolo;
3230
3634
 
3231
3635
  const { state, screen } = useAutoApprove
3232
- ? await autoApproveLoop(agent, activeSession, timeoutMs)
3233
- : await waitForResponse(agent, activeSession, timeoutMs);
3636
+ ? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
3637
+ : await streamResponse(agent, activeSession, timeoutMs);
3234
3638
 
3235
3639
  if (state === State.RATE_LIMITED) {
3236
3640
  console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
@@ -3238,14 +3642,9 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
3238
3642
  }
3239
3643
 
3240
3644
  if (state === State.CONFIRMING) {
3241
- console.log(`CONFIRM: ${agent.parseAction(screen)}`);
3645
+ console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
3242
3646
  process.exit(3);
3243
3647
  }
3244
-
3245
- const output = agent.getResponse(activeSession, screen);
3246
- if (output) {
3247
- console.log(output);
3248
- }
3249
3648
  }
3250
3649
 
3251
3650
  /**
@@ -3260,9 +3659,10 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
3260
3659
  }
3261
3660
 
3262
3661
  const before = tmuxCapture(session);
3263
- if (agent.getState(before) !== State.CONFIRMING) {
3264
- console.log("ERROR: not confirming");
3265
- process.exit(1);
3662
+ const beforeState = agent.getState(before);
3663
+ if (beforeState !== State.CONFIRMING) {
3664
+ console.log(`Already ${beforeState}`);
3665
+ return;
3266
3666
  }
3267
3667
 
3268
3668
  tmuxSend(session, agent.approveKey);
@@ -3277,7 +3677,7 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
3277
3677
  }
3278
3678
 
3279
3679
  if (state === State.CONFIRMING) {
3280
- console.log(`CONFIRM: ${agent.parseAction(screen)}`);
3680
+ console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
3281
3681
  process.exit(3);
3282
3682
  }
3283
3683
 
@@ -3296,6 +3696,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
3296
3696
  process.exit(1);
3297
3697
  }
3298
3698
 
3699
+ const before = tmuxCapture(session);
3700
+ const beforeState = agent.getState(before);
3701
+ if (beforeState !== State.CONFIRMING) {
3702
+ console.log(`Already ${beforeState}`);
3703
+ return;
3704
+ }
3705
+
3299
3706
  tmuxSend(session, agent.rejectKey);
3300
3707
 
3301
3708
  if (!wait) return;
@@ -3323,7 +3730,7 @@ async function cmdReview(
3323
3730
  session,
3324
3731
  option,
3325
3732
  customInstructions,
3326
- { wait = true, yolo = true, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
3733
+ { wait = true, yolo = false, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
3327
3734
  ) {
3328
3735
  const sessionExists = session != null && tmuxHasSession(session);
3329
3736
 
@@ -3390,12 +3797,11 @@ async function cmdReview(
3390
3797
 
3391
3798
  if (!wait) return;
3392
3799
 
3393
- // Yolo mode on a safe session: auto-approve until done
3394
3800
  const useAutoApprove = yolo && !nativeYolo;
3395
3801
 
3396
3802
  const { state, screen } = useAutoApprove
3397
- ? await autoApproveLoop(agent, activeSession, timeoutMs)
3398
- : await waitForResponse(agent, activeSession, timeoutMs);
3803
+ ? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
3804
+ : await streamResponse(agent, activeSession, timeoutMs);
3399
3805
 
3400
3806
  if (state === State.RATE_LIMITED) {
3401
3807
  console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
@@ -3403,12 +3809,9 @@ async function cmdReview(
3403
3809
  }
3404
3810
 
3405
3811
  if (state === State.CONFIRMING) {
3406
- console.log(`CONFIRM: ${agent.parseAction(screen)}`);
3812
+ console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
3407
3813
  process.exit(3);
3408
3814
  }
3409
-
3410
- const response = agent.getResponse(activeSession, screen);
3411
- console.log(response || "");
3412
3815
  }
3413
3816
 
3414
3817
  /**
@@ -3439,7 +3842,7 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
3439
3842
  }
3440
3843
 
3441
3844
  if (state === State.CONFIRMING) {
3442
- console.log(`CONFIRM: ${agent.parseAction(screen)}`);
3845
+ console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
3443
3846
  process.exit(3);
3444
3847
  }
3445
3848
 
@@ -3451,6 +3854,8 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
3451
3854
  const output = agent.getResponse(session, screen, index);
3452
3855
  if (output) {
3453
3856
  console.log(output);
3857
+ } else {
3858
+ console.log("READY_NO_CONTENT");
3454
3859
  }
3455
3860
  }
3456
3861
 
@@ -3473,7 +3878,7 @@ function cmdStatus(agent, session) {
3473
3878
  }
3474
3879
 
3475
3880
  if (state === State.CONFIRMING) {
3476
- console.log(`CONFIRM: ${agent.parseAction(screen)}`);
3881
+ console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
3477
3882
  process.exit(3);
3478
3883
  }
3479
3884
 
@@ -3481,6 +3886,10 @@ function cmdStatus(agent, session) {
3481
3886
  console.log("THINKING");
3482
3887
  process.exit(4);
3483
3888
  }
3889
+
3890
+ // READY (or STARTING/UPDATE_PROMPT which are transient)
3891
+ console.log("READY");
3892
+ process.exit(0);
3484
3893
  }
3485
3894
 
3486
3895
  /**
@@ -3594,7 +4003,7 @@ async function cmdSelect(agent, session, n, { wait = false, timeoutMs } = {}) {
3594
4003
  }
3595
4004
 
3596
4005
  if (state === State.CONFIRMING) {
3597
- console.log(`CONFIRM: ${agent.parseAction(screen)}`);
4006
+ console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
3598
4007
  process.exit(3);
3599
4008
  }
3600
4009
 
@@ -3629,19 +4038,22 @@ function getAgentFromInvocation() {
3629
4038
  */
3630
4039
  function printHelp(agent, cliName) {
3631
4040
  const name = cliName;
3632
- const backendName = agent.name === "codex" ? "Codex" : "Claude";
4041
+ const backendName = agent.displayName;
3633
4042
  const hasReview = !!agent.reviewOptions;
3634
4043
 
3635
4044
  console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
3636
4045
 
4046
+ Usage: ${name} [OPTIONS] <command|message> [ARGS...]
4047
+
3637
4048
  Commands:
3638
4049
  agents List all running agents with state and log paths
4050
+ target Show default target session for current tool
3639
4051
  attach [SESSION] Attach to agent session interactively
3640
4052
  log SESSION View conversation log (--tail=N, --follow, --reasoning)
3641
4053
  mailbox View archangel observations (--limit=N, --branch=X, --all)
3642
4054
  summon [name] Summon archangels (all, or by name)
3643
4055
  recall [name] Recall archangels (all, or by name)
3644
- kill Kill sessions in current project (--all for all, --session=NAME for one)
4056
+ kill Kill sessions (--all, --session=NAME, --orphans [--force])
3645
4057
  status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
3646
4058
  output [-N] Show response (0=last, -1=prev, -2=older)
3647
4059
  debug Show raw screen output and detected state${
@@ -3661,11 +4073,13 @@ Commands:
3661
4073
  Flags:
3662
4074
  --tool=NAME Use specific agent (codex, claude)
3663
4075
  --session=NAME Target session by name, archangel name, or UUID prefix (self = current)
3664
- --wait Wait for response (for review, approve, etc)
3665
- --no-wait Don't wait (for messages, which wait by default)
3666
- --timeout=N Set timeout in seconds (default: 120)
4076
+ --wait Wait for response (default for messages; required for approve/reject)
4077
+ --no-wait Fire-and-forget: send message, print session ID, exit immediately
4078
+ --timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
3667
4079
  --yolo Skip all confirmations (dangerous)
3668
4080
  --fresh Reset conversation before review
4081
+ --orphans Kill orphaned claude/codex processes (PPID=1)
4082
+ --force Use SIGKILL instead of SIGTERM (with --orphans)
3669
4083
 
3670
4084
  Environment:
3671
4085
  AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
@@ -3677,7 +4091,8 @@ Environment:
3677
4091
 
3678
4092
  Examples:
3679
4093
  ${name} "explain this codebase"
3680
- ${name} "please review the error handling" # Auto custom review
4094
+ ${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
4095
+ ${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
3681
4096
  ${name} review uncommitted --wait
3682
4097
  ${name} approve --wait
3683
4098
  ${name} kill # Kill agents in current project
@@ -3689,7 +4104,10 @@ Examples:
3689
4104
  ${name} summon reviewer # Summon by name (creates config if new)
3690
4105
  ${name} recall # Recall all archangels
3691
4106
  ${name} recall reviewer # Recall one by name
3692
- ${name} agents # List all agents (shows TYPE=archangel)`);
4107
+ ${name} agents # List all agents (shows TYPE=archangel)
4108
+
4109
+ Note: Reviews and complex tasks may take several minutes.
4110
+ Use Bash run_in_background for long operations (not --no-wait).`);
3693
4111
  }
3694
4112
 
3695
4113
  async function main() {
@@ -3704,38 +4122,32 @@ async function main() {
3704
4122
  const args = process.argv.slice(2);
3705
4123
  const cliName = path.basename(process.argv[1], ".js");
3706
4124
 
3707
- if (args.includes("--version") || args.includes("-V")) {
4125
+ // Parse all flags and positionals in one place
4126
+ const { flags, positionals } = parseCliArgs(args);
4127
+
4128
+ if (flags.version) {
3708
4129
  console.log(VERSION);
3709
4130
  process.exit(0);
3710
4131
  }
3711
4132
 
3712
- // Parse flags
3713
- const wait = args.includes("--wait");
3714
- const noWait = args.includes("--no-wait");
3715
- const yolo = args.includes("--yolo");
3716
- const fresh = args.includes("--fresh");
3717
- const reasoning = args.includes("--reasoning");
3718
- const follow = args.includes("--follow") || args.includes("-f");
4133
+ // Extract flags into local variables for convenience
4134
+ const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force } = flags;
3719
4135
 
3720
4136
  // Agent selection
3721
4137
  let agent = getAgentFromInvocation();
3722
- const toolArg = args.find((a) => a.startsWith("--tool="));
3723
- if (toolArg) {
3724
- const tool = toolArg.split("=")[1];
3725
- if (tool === "claude") agent = ClaudeAgent;
3726
- else if (tool === "codex") agent = CodexAgent;
4138
+ if (flags.tool) {
4139
+ if (flags.tool === "claude") agent = ClaudeAgent;
4140
+ else if (flags.tool === "codex") agent = CodexAgent;
3727
4141
  else {
3728
- console.log(`ERROR: unknown tool '${tool}'`);
4142
+ console.log(`ERROR: unknown tool '${flags.tool}'`);
3729
4143
  process.exit(1);
3730
4144
  }
3731
4145
  }
3732
4146
 
3733
4147
  // Session resolution
3734
4148
  let session = agent.getDefaultSession();
3735
- const sessionArg = args.find((a) => a.startsWith("--session="));
3736
- if (sessionArg) {
3737
- const val = sessionArg.split("=")[1];
3738
- if (val === "self") {
4149
+ if (flags.session) {
4150
+ if (flags.session === "self") {
3739
4151
  const current = tmuxCurrentSession();
3740
4152
  if (!current) {
3741
4153
  console.log("ERROR: --session=self requires running inside tmux");
@@ -3744,110 +4156,92 @@ async function main() {
3744
4156
  session = current;
3745
4157
  } else {
3746
4158
  // Resolve partial names, archangel names, and UUID prefixes
3747
- session = resolveSessionName(val);
4159
+ session = resolveSessionName(flags.session);
3748
4160
  }
3749
4161
  }
3750
4162
 
3751
- // Timeout
4163
+ // Timeout (convert seconds to milliseconds)
3752
4164
  let timeoutMs = DEFAULT_TIMEOUT_MS;
3753
- const timeoutArg = args.find((a) => a.startsWith("--timeout="));
3754
- if (timeoutArg) {
3755
- const val = parseInt(timeoutArg.split("=")[1], 10);
3756
- if (isNaN(val) || val <= 0) {
4165
+ if (flags.timeout !== undefined) {
4166
+ if (isNaN(flags.timeout) || flags.timeout <= 0) {
3757
4167
  console.log("ERROR: invalid timeout");
3758
4168
  process.exit(1);
3759
4169
  }
3760
- timeoutMs = val * 1000;
4170
+ timeoutMs = flags.timeout * 1000;
3761
4171
  }
3762
4172
 
3763
4173
  // Tail (for log command)
3764
- let tail = 50;
3765
- const tailArg = args.find((a) => a.startsWith("--tail="));
3766
- if (tailArg) {
3767
- tail = parseInt(tailArg.split("=")[1], 10) || 50;
3768
- }
4174
+ const tail = flags.tail ?? 50;
3769
4175
 
3770
4176
  // Limit (for mailbox command)
3771
- let limit = 20;
3772
- const limitArg = args.find((a) => a.startsWith("--limit="));
3773
- if (limitArg) {
3774
- limit = parseInt(limitArg.split("=")[1], 10) || 20;
3775
- }
4177
+ const limit = flags.limit ?? 20;
3776
4178
 
3777
4179
  // Branch filter (for mailbox command)
3778
- let branch = null;
3779
- const branchArg = args.find((a) => a.startsWith("--branch="));
3780
- if (branchArg) {
3781
- branch = branchArg.split("=")[1] || null;
3782
- }
3783
-
3784
- // All flag (for mailbox command - show all regardless of age)
3785
- const all = args.includes("--all");
3786
-
3787
- // Filter out flags
3788
- const filteredArgs = args.filter(
3789
- (a) =>
3790
- !["--wait", "--no-wait", "--yolo", "--reasoning", "--follow", "-f", "--all"].includes(a) &&
3791
- !a.startsWith("--timeout") &&
3792
- !a.startsWith("--session") &&
3793
- !a.startsWith("--tool") &&
3794
- !a.startsWith("--tail") &&
3795
- !a.startsWith("--limit") &&
3796
- !a.startsWith("--branch"),
3797
- );
3798
- const cmd = filteredArgs[0];
4180
+ const branch = flags.branch ?? null;
4181
+
4182
+ // Command is first positional
4183
+ const cmd = positionals[0];
3799
4184
 
3800
4185
  // Dispatch commands
3801
4186
  if (cmd === "agents") return cmdAgents();
3802
- if (cmd === "summon") return cmdSummon(filteredArgs[1]);
3803
- if (cmd === "recall") return cmdRecall(filteredArgs[1]);
3804
- if (cmd === "archangel") return cmdArchangel(filteredArgs[1]);
3805
- if (cmd === "kill") return cmdKill(session, { all });
3806
- if (cmd === "attach") return cmdAttach(filteredArgs[1] || session);
3807
- if (cmd === "log") return cmdLog(filteredArgs[1] || session, { tail, reasoning, follow });
4187
+ if (cmd === "target") {
4188
+ const defaultSession = agent.getDefaultSession();
4189
+ if (defaultSession) {
4190
+ console.log(defaultSession);
4191
+ } else {
4192
+ console.log("NO_TARGET");
4193
+ process.exit(1);
4194
+ }
4195
+ return;
4196
+ }
4197
+ if (cmd === "summon") return cmdSummon(positionals[1]);
4198
+ if (cmd === "recall") return cmdRecall(positionals[1]);
4199
+ if (cmd === "archangel") return cmdArchangel(positionals[1]);
4200
+ if (cmd === "kill") return cmdKill(session, { all, orphans, force });
4201
+ if (cmd === "attach") return cmdAttach(positionals[1] || session);
4202
+ if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
3808
4203
  if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
3809
4204
  if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
3810
4205
  if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
3811
4206
  if (cmd === "review")
3812
- return cmdReview(agent, session, filteredArgs[1], filteredArgs[2], {
4207
+ return cmdReview(agent, session, positionals[1], positionals[2], {
3813
4208
  wait,
3814
- yolo,
3815
4209
  fresh,
3816
4210
  timeoutMs,
3817
4211
  });
3818
4212
  if (cmd === "status") return cmdStatus(agent, session);
3819
4213
  if (cmd === "debug") return cmdDebug(agent, session);
3820
4214
  if (cmd === "output") {
3821
- const indexArg = filteredArgs[1];
4215
+ const indexArg = positionals[1];
3822
4216
  const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
3823
4217
  return cmdOutput(agent, session, index, { wait, timeoutMs });
3824
4218
  }
3825
- if (cmd === "send" && filteredArgs.length > 1)
3826
- return cmdSend(session, filteredArgs.slice(1).join(" "));
4219
+ if (cmd === "send" && positionals.length > 1)
4220
+ return cmdSend(session, positionals.slice(1).join(" "));
3827
4221
  if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
3828
4222
  if (cmd === "reset") return cmdAsk(agent, session, "/new", { noWait: true, timeoutMs });
3829
- if (cmd === "select" && filteredArgs[1])
3830
- return cmdSelect(agent, session, filteredArgs[1], { wait, timeoutMs });
4223
+ if (cmd === "select" && positionals[1])
4224
+ return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
3831
4225
 
3832
4226
  // Default: send message
3833
- let message = filteredArgs.join(" ");
4227
+ let message = positionals.join(" ");
3834
4228
  if (!message && hasStdinData()) {
3835
4229
  message = await readStdin();
3836
4230
  }
3837
4231
 
3838
- if (!message || cmd === "--help" || cmd === "-h") {
4232
+ if (!message || flags.help) {
3839
4233
  printHelp(agent, cliName);
3840
4234
  process.exit(0);
3841
4235
  }
3842
4236
 
3843
- // Detect "please review" and route to custom review mode
3844
- const reviewMatch = message.match(/^please review\s*(.*)/i);
4237
+ // Detect "review ..." or "please review ..." and route to custom review mode
4238
+ const reviewMatch = message.match(/^(?:please )?review\s*(.*)/i);
3845
4239
  if (reviewMatch && agent.reviewOptions) {
3846
4240
  const customInstructions = reviewMatch[1].trim() || null;
3847
4241
  return cmdReview(agent, session, "custom", customInstructions, {
3848
4242
  wait: !noWait,
3849
4243
  yolo,
3850
- timeoutMs,
4244
+ timeoutMs: flags.timeout !== undefined ? timeoutMs : REVIEW_TIMEOUT_MS,
3851
4245
  });
3852
4246
  }
3853
4247
 
@@ -3868,6 +4262,9 @@ const isDirectRun =
3868
4262
  if (isDirectRun) {
3869
4263
  main().catch((err) => {
3870
4264
  console.log(`ERROR: ${err.message}`);
4265
+ if (err instanceof TimeoutError && err.session) {
4266
+ console.log(`Hint: Use 'ax debug --session=${err.session}' to see current screen state`);
4267
+ }
3871
4268
  process.exit(1);
3872
4269
  });
3873
4270
  }
@@ -3877,6 +4274,7 @@ export {
3877
4274
  parseSessionName,
3878
4275
  parseAgentConfig,
3879
4276
  parseKeySequence,
4277
+ parseCliArgs,
3880
4278
  getClaudeProjectPath,
3881
4279
  matchesPattern,
3882
4280
  getBaseDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ax-agents",
3
- "version": "0.0.1-alpha.6",
3
+ "version": "0.0.1-alpha.7",
4
4
  "description": "A CLI for orchestrating AI coding agents via tmux",
5
5
  "bin": {
6
6
  "ax": "ax.js",