ax-agents 0.0.1-alpha.6 → 0.0.1-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ax.js +711 -168
- 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
|
-
import { randomUUID } from "node:crypto";
|
|
30
|
+
import { randomUUID, createHash } 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);
|
|
@@ -301,6 +305,33 @@ const TRUNCATE_THINKING_LEN = 300;
|
|
|
301
305
|
const ARCHANGEL_GIT_CONTEXT_HOURS = 4;
|
|
302
306
|
const ARCHANGEL_GIT_CONTEXT_MAX_LINES = 200;
|
|
303
307
|
const ARCHANGEL_PARENT_CONTEXT_ENTRIES = 10;
|
|
308
|
+
const ARCHANGEL_PREAMBLE = `## Guidelines
|
|
309
|
+
|
|
310
|
+
- Investigate before speaking. If uncertain, read more code and trace the logic until you're confident.
|
|
311
|
+
- Explain WHY something is an issue, not just that it is.
|
|
312
|
+
- Focus on your area of expertise.
|
|
313
|
+
- Calibrate to the task or plan. Don't suggest refactors during a bug fix.
|
|
314
|
+
- Be clear. Brief is fine, but never sacrifice clarity.
|
|
315
|
+
- For critical issues, request for them to be added to the todo list.
|
|
316
|
+
- Don't repeat observations you've already made unless you have more to say or better clarity.
|
|
317
|
+
- Make judgment calls - don't ask questions.
|
|
318
|
+
|
|
319
|
+
"No issues found." is a valid response when there's nothing significant to report.`;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {string} session
|
|
323
|
+
* @param {(screen: string) => boolean} predicate
|
|
324
|
+
* @param {number} [timeoutMs]
|
|
325
|
+
* @returns {Promise<string>}
|
|
326
|
+
*/
|
|
327
|
+
class TimeoutError extends Error {
|
|
328
|
+
/** @param {string} [session] */
|
|
329
|
+
constructor(session) {
|
|
330
|
+
super("timeout");
|
|
331
|
+
this.name = "TimeoutError";
|
|
332
|
+
this.session = session;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
304
335
|
|
|
305
336
|
/**
|
|
306
337
|
* @param {string} session
|
|
@@ -315,7 +346,7 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
315
346
|
if (predicate(screen)) return screen;
|
|
316
347
|
await sleep(POLL_MS);
|
|
317
348
|
}
|
|
318
|
-
throw new
|
|
349
|
+
throw new TimeoutError(session);
|
|
319
350
|
}
|
|
320
351
|
|
|
321
352
|
// =============================================================================
|
|
@@ -343,6 +374,38 @@ function findCallerPid() {
|
|
|
343
374
|
return null;
|
|
344
375
|
}
|
|
345
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Find orphaned claude/codex processes (PPID=1, reparented to init/launchd)
|
|
379
|
+
* @returns {{pid: string, command: string}[]}
|
|
380
|
+
*/
|
|
381
|
+
function findOrphanedProcesses() {
|
|
382
|
+
const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf-8" });
|
|
383
|
+
|
|
384
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const orphans = [];
|
|
389
|
+
for (const line of result.stdout.trim().split("\n")) {
|
|
390
|
+
// Parse: " PID PPID command args..."
|
|
391
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.+)$/);
|
|
392
|
+
if (!match) continue;
|
|
393
|
+
|
|
394
|
+
const [, pid, ppid, args] = match;
|
|
395
|
+
|
|
396
|
+
// Must have PPID=1 (orphaned/reparented to init)
|
|
397
|
+
if (ppid !== "1") continue;
|
|
398
|
+
|
|
399
|
+
// Command must START with claude or codex (excludes tmux which also has PPID=1)
|
|
400
|
+
const cmd = args.split(/\s+/)[0];
|
|
401
|
+
if (cmd !== "claude" && cmd !== "codex") continue;
|
|
402
|
+
|
|
403
|
+
orphans.push({ pid, command: args.slice(0, 60) });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return orphans;
|
|
407
|
+
}
|
|
408
|
+
|
|
346
409
|
// =============================================================================
|
|
347
410
|
// Helpers - stdin
|
|
348
411
|
// =============================================================================
|
|
@@ -371,6 +434,86 @@ async function readStdin() {
|
|
|
371
434
|
}
|
|
372
435
|
|
|
373
436
|
// =============================================================================
|
|
437
|
+
// =============================================================================
|
|
438
|
+
// Helpers - CLI argument parsing
|
|
439
|
+
// =============================================================================
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Parse CLI arguments using Node.js built-in parseArgs.
|
|
443
|
+
* @param {string[]} args - Command line arguments (without node and script path)
|
|
444
|
+
* @returns {{ flags: ParsedFlags, positionals: string[] }}
|
|
445
|
+
*
|
|
446
|
+
* @typedef {Object} ParsedFlags
|
|
447
|
+
* @property {boolean} wait
|
|
448
|
+
* @property {boolean} noWait
|
|
449
|
+
* @property {boolean} yolo
|
|
450
|
+
* @property {boolean} fresh
|
|
451
|
+
* @property {boolean} reasoning
|
|
452
|
+
* @property {boolean} follow
|
|
453
|
+
* @property {boolean} all
|
|
454
|
+
* @property {boolean} orphans
|
|
455
|
+
* @property {boolean} force
|
|
456
|
+
* @property {boolean} version
|
|
457
|
+
* @property {boolean} help
|
|
458
|
+
* @property {string} [tool]
|
|
459
|
+
* @property {string} [session]
|
|
460
|
+
* @property {number} [timeout]
|
|
461
|
+
* @property {number} [tail]
|
|
462
|
+
* @property {number} [limit]
|
|
463
|
+
* @property {string} [branch]
|
|
464
|
+
*/
|
|
465
|
+
function parseCliArgs(args) {
|
|
466
|
+
const { values, positionals } = parseArgs({
|
|
467
|
+
args,
|
|
468
|
+
options: {
|
|
469
|
+
// Boolean flags
|
|
470
|
+
wait: { type: "boolean", default: false },
|
|
471
|
+
"no-wait": { type: "boolean", default: false },
|
|
472
|
+
yolo: { type: "boolean", default: false },
|
|
473
|
+
fresh: { type: "boolean", default: false },
|
|
474
|
+
reasoning: { type: "boolean", default: false },
|
|
475
|
+
follow: { type: "boolean", short: "f", default: false },
|
|
476
|
+
all: { type: "boolean", default: false },
|
|
477
|
+
orphans: { type: "boolean", default: false },
|
|
478
|
+
force: { type: "boolean", default: false },
|
|
479
|
+
version: { type: "boolean", short: "V", default: false },
|
|
480
|
+
help: { type: "boolean", short: "h", default: false },
|
|
481
|
+
// Value flags
|
|
482
|
+
tool: { type: "string" },
|
|
483
|
+
session: { type: "string" },
|
|
484
|
+
timeout: { type: "string" },
|
|
485
|
+
tail: { type: "string" },
|
|
486
|
+
limit: { type: "string" },
|
|
487
|
+
branch: { type: "string" },
|
|
488
|
+
},
|
|
489
|
+
allowPositionals: true,
|
|
490
|
+
strict: false, // Don't error on unknown flags
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
flags: {
|
|
495
|
+
wait: Boolean(values.wait),
|
|
496
|
+
noWait: Boolean(values["no-wait"]),
|
|
497
|
+
yolo: Boolean(values.yolo),
|
|
498
|
+
fresh: Boolean(values.fresh),
|
|
499
|
+
reasoning: Boolean(values.reasoning),
|
|
500
|
+
follow: Boolean(values.follow),
|
|
501
|
+
all: Boolean(values.all),
|
|
502
|
+
orphans: Boolean(values.orphans),
|
|
503
|
+
force: Boolean(values.force),
|
|
504
|
+
version: Boolean(values.version),
|
|
505
|
+
help: Boolean(values.help),
|
|
506
|
+
tool: /** @type {string | undefined} */ (values.tool),
|
|
507
|
+
session: /** @type {string | undefined} */ (values.session),
|
|
508
|
+
timeout: values.timeout !== undefined ? Number(values.timeout) : undefined,
|
|
509
|
+
tail: values.tail !== undefined ? Number(values.tail) : undefined,
|
|
510
|
+
limit: values.limit !== undefined ? Number(values.limit) : undefined,
|
|
511
|
+
branch: /** @type {string | undefined} */ (values.branch),
|
|
512
|
+
},
|
|
513
|
+
positionals,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
374
517
|
// Helpers - session tracking
|
|
375
518
|
// =============================================================================
|
|
376
519
|
|
|
@@ -413,6 +556,16 @@ function generateSessionName(tool) {
|
|
|
413
556
|
return `${tool}-partner-${randomUUID()}`;
|
|
414
557
|
}
|
|
415
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Quick hash for change detection (not cryptographic).
|
|
561
|
+
* @param {string | null | undefined} str
|
|
562
|
+
* @returns {string | null}
|
|
563
|
+
*/
|
|
564
|
+
function quickHash(str) {
|
|
565
|
+
if (!str) return null;
|
|
566
|
+
return createHash("md5").update(str).digest("hex").slice(0, 8);
|
|
567
|
+
}
|
|
568
|
+
|
|
416
569
|
/**
|
|
417
570
|
* @param {string} cwd
|
|
418
571
|
* @returns {string}
|
|
@@ -540,6 +693,93 @@ function findCodexLogPath(sessionName) {
|
|
|
540
693
|
}
|
|
541
694
|
}
|
|
542
695
|
|
|
696
|
+
/**
|
|
697
|
+
* @typedef {Object} SessionMeta
|
|
698
|
+
* @property {string | null} slug - Plan identifier (if plan is active)
|
|
699
|
+
* @property {Array<{content: string, status: string, id?: string}> | null} todos - Current todos
|
|
700
|
+
* @property {string | null} permissionMode - "default", "acceptEdits", "plan"
|
|
701
|
+
* @property {string | null} gitBranch - Current git branch
|
|
702
|
+
* @property {string | null} cwd - Working directory
|
|
703
|
+
*/
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Get metadata from a Claude session's JSONL file.
|
|
707
|
+
* Returns null for Codex sessions (different format, no equivalent metadata).
|
|
708
|
+
* @param {string} sessionName - The tmux session name
|
|
709
|
+
* @returns {SessionMeta | null}
|
|
710
|
+
*/
|
|
711
|
+
function getSessionMeta(sessionName) {
|
|
712
|
+
const parsed = parseSessionName(sessionName);
|
|
713
|
+
if (!parsed) return null;
|
|
714
|
+
|
|
715
|
+
// Only Claude sessions have this metadata
|
|
716
|
+
if (parsed.tool !== "claude") return null;
|
|
717
|
+
if (!parsed.uuid) return null;
|
|
718
|
+
|
|
719
|
+
const logPath = findClaudeLogPath(parsed.uuid, sessionName);
|
|
720
|
+
if (!logPath || !existsSync(logPath)) return null;
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
const content = readFileSync(logPath, "utf-8");
|
|
724
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
725
|
+
|
|
726
|
+
// Read from end to find most recent entry with metadata
|
|
727
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
728
|
+
try {
|
|
729
|
+
const entry = JSON.parse(lines[i]);
|
|
730
|
+
// User entries typically have the metadata fields
|
|
731
|
+
if (entry.type === "user" || entry.slug || entry.gitBranch) {
|
|
732
|
+
return {
|
|
733
|
+
slug: entry.slug || null,
|
|
734
|
+
todos: entry.todos || null,
|
|
735
|
+
permissionMode: entry.permissionMode || null,
|
|
736
|
+
gitBranch: entry.gitBranch || null,
|
|
737
|
+
cwd: entry.cwd || null,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
} catch {
|
|
741
|
+
// Skip malformed lines
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
} catch (err) {
|
|
746
|
+
debugError("getSessionMeta", err);
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Read a plan file by its slug.
|
|
753
|
+
* @param {string} slug - The plan slug (e.g., "curious-roaming-pascal")
|
|
754
|
+
* @returns {string | null} The plan content or null if not found
|
|
755
|
+
*/
|
|
756
|
+
function readPlanFile(slug) {
|
|
757
|
+
const planPath = path.join(CLAUDE_CONFIG_DIR, "plans", `${slug}.md`);
|
|
758
|
+
try {
|
|
759
|
+
if (existsSync(planPath)) {
|
|
760
|
+
return readFileSync(planPath, "utf-8");
|
|
761
|
+
}
|
|
762
|
+
} catch (err) {
|
|
763
|
+
debugError("readPlanFile", err);
|
|
764
|
+
}
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Format todos for display in a prompt.
|
|
770
|
+
* @param {Array<{content: string, status: string, id?: string}>} todos
|
|
771
|
+
* @returns {string}
|
|
772
|
+
*/
|
|
773
|
+
function formatTodos(todos) {
|
|
774
|
+
if (!todos || todos.length === 0) return "";
|
|
775
|
+
return todos
|
|
776
|
+
.map((t) => {
|
|
777
|
+
const status = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[>]" : "[ ]";
|
|
778
|
+
return `${status} ${t.content || "(no content)"}`;
|
|
779
|
+
})
|
|
780
|
+
.join("\n");
|
|
781
|
+
}
|
|
782
|
+
|
|
543
783
|
/**
|
|
544
784
|
* Extract assistant text responses from a JSONL log file.
|
|
545
785
|
* This provides clean text without screen-scraped artifacts.
|
|
@@ -585,6 +825,130 @@ function getAssistantText(logPath, index = 0) {
|
|
|
585
825
|
}
|
|
586
826
|
}
|
|
587
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Read new complete JSON lines from a log file since the given offset.
|
|
830
|
+
* @param {string | null} logPath
|
|
831
|
+
* @param {number} fromOffset
|
|
832
|
+
* @returns {{ entries: object[], newOffset: number }}
|
|
833
|
+
*/
|
|
834
|
+
function tailJsonl(logPath, fromOffset) {
|
|
835
|
+
if (!logPath || !existsSync(logPath)) {
|
|
836
|
+
return { entries: [], newOffset: fromOffset };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const stats = statSync(logPath);
|
|
840
|
+
if (stats.size <= fromOffset) {
|
|
841
|
+
return { entries: [], newOffset: fromOffset };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const fd = openSync(logPath, "r");
|
|
845
|
+
const buffer = Buffer.alloc(stats.size - fromOffset);
|
|
846
|
+
readSync(fd, buffer, 0, buffer.length, fromOffset);
|
|
847
|
+
closeSync(fd);
|
|
848
|
+
|
|
849
|
+
const text = buffer.toString("utf-8");
|
|
850
|
+
const lines = text.split("\n");
|
|
851
|
+
|
|
852
|
+
// Last line may be incomplete - don't parse it yet
|
|
853
|
+
const complete = lines.slice(0, -1).filter(Boolean);
|
|
854
|
+
const incomplete = lines[lines.length - 1];
|
|
855
|
+
|
|
856
|
+
const entries = [];
|
|
857
|
+
for (const line of complete) {
|
|
858
|
+
try {
|
|
859
|
+
entries.push(JSON.parse(line));
|
|
860
|
+
} catch {
|
|
861
|
+
// Skip malformed lines
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Offset advances by complete lines only
|
|
866
|
+
const newOffset = fromOffset + text.length - incomplete.length;
|
|
867
|
+
return { entries, newOffset };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* @typedef {{command?: string, file_path?: string, path?: string, pattern?: string}} ToolInput
|
|
872
|
+
*/
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Format a JSONL entry for streaming display.
|
|
876
|
+
* @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, name?: string, input?: ToolInput, tool?: string, arguments?: ToolInput}>}}} entry
|
|
877
|
+
* @returns {string | null}
|
|
878
|
+
*/
|
|
879
|
+
function formatEntry(entry) {
|
|
880
|
+
// Skip tool_result entries (they can be very verbose)
|
|
881
|
+
if (entry.type === "tool_result") return null;
|
|
882
|
+
|
|
883
|
+
// Only process assistant entries
|
|
884
|
+
if (entry.type !== "assistant") return null;
|
|
885
|
+
|
|
886
|
+
const parts = entry.message?.content || [];
|
|
887
|
+
const output = [];
|
|
888
|
+
|
|
889
|
+
for (const part of parts) {
|
|
890
|
+
if (part.type === "text" && part.text) {
|
|
891
|
+
output.push(part.text);
|
|
892
|
+
} else if (part.type === "tool_use" || part.type === "tool_call") {
|
|
893
|
+
const name = part.name || part.tool || "tool";
|
|
894
|
+
const input = part.input || part.arguments || {};
|
|
895
|
+
let summary;
|
|
896
|
+
if (name === "Bash" && input.command) {
|
|
897
|
+
summary = input.command.slice(0, 50);
|
|
898
|
+
} else {
|
|
899
|
+
const target = input.file_path || input.path || input.pattern || "";
|
|
900
|
+
summary = target.split("/").pop() || target.slice(0, 30);
|
|
901
|
+
}
|
|
902
|
+
output.push(`> ${name}(${summary})`);
|
|
903
|
+
}
|
|
904
|
+
// Skip thinking blocks - internal reasoning
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return output.length > 0 ? output.join("\n") : null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Extract pending tool from confirmation screen.
|
|
912
|
+
* @param {string} screen
|
|
913
|
+
* @returns {string | null}
|
|
914
|
+
*/
|
|
915
|
+
function extractPendingToolFromScreen(screen) {
|
|
916
|
+
const lines = screen.split("\n");
|
|
917
|
+
|
|
918
|
+
// Check recent lines for tool confirmation patterns
|
|
919
|
+
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 15); i--) {
|
|
920
|
+
const line = lines[i];
|
|
921
|
+
// Match tool confirmation patterns like "Bash: command" or "Write: /path/file"
|
|
922
|
+
const match = line.match(
|
|
923
|
+
/^\s*(Bash|Write|Edit|Read|Glob|Grep|Task|WebFetch|WebSearch|NotebookEdit|Skill|TodoWrite|TodoRead):\s*(.{1,40})/,
|
|
924
|
+
);
|
|
925
|
+
if (match) {
|
|
926
|
+
return `${match[1]}: ${match[2].trim()}`;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Format confirmation output with helpful commands
|
|
935
|
+
* @param {string} screen
|
|
936
|
+
* @param {Agent} _agent
|
|
937
|
+
* @returns {string}
|
|
938
|
+
*/
|
|
939
|
+
function formatConfirmationOutput(screen, _agent) {
|
|
940
|
+
const pendingTool = extractPendingToolFromScreen(screen);
|
|
941
|
+
const cli = path.basename(process.argv[1], ".js");
|
|
942
|
+
|
|
943
|
+
let output = pendingTool || "Confirmation required";
|
|
944
|
+
output += "\n\ne.g.";
|
|
945
|
+
output += `\n ${cli} approve # for y/n prompts`;
|
|
946
|
+
output += `\n ${cli} reject`;
|
|
947
|
+
output += `\n ${cli} select N # for numbered menus`;
|
|
948
|
+
|
|
949
|
+
return output;
|
|
950
|
+
}
|
|
951
|
+
|
|
588
952
|
/**
|
|
589
953
|
* @returns {string[]}
|
|
590
954
|
*/
|
|
@@ -1457,7 +1821,7 @@ const State = {
|
|
|
1457
1821
|
* @param {string} config.promptSymbol - Symbol indicating ready state
|
|
1458
1822
|
* @param {string[]} [config.spinners] - Spinner characters indicating thinking
|
|
1459
1823
|
* @param {RegExp} [config.rateLimitPattern] - Pattern for rate limit detection
|
|
1460
|
-
* @param {string[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
1824
|
+
* @param {(string | RegExp | ((lines: string) => boolean))[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
1461
1825
|
* @param {(string | ((lines: string) => boolean))[]} [config.confirmPatterns] - Patterns for confirmation dialogs
|
|
1462
1826
|
* @param {{screen: string[], lastLines: string[]} | null} [config.updatePromptPatterns] - Patterns for update prompts
|
|
1463
1827
|
* @returns {string} The detected state
|
|
@@ -1470,19 +1834,38 @@ function detectState(screen, config) {
|
|
|
1470
1834
|
// Larger range for confirmation detection (catches dialogs that scrolled slightly)
|
|
1471
1835
|
const recentLines = lines.slice(-15).join("\n");
|
|
1472
1836
|
|
|
1473
|
-
// Rate limited - check full screen
|
|
1474
|
-
if (config.rateLimitPattern && config.rateLimitPattern.test(
|
|
1837
|
+
// Rate limited - check recent lines (not full screen to avoid matching historical output)
|
|
1838
|
+
if (config.rateLimitPattern && config.rateLimitPattern.test(recentLines)) {
|
|
1475
1839
|
return State.RATE_LIMITED;
|
|
1476
1840
|
}
|
|
1477
1841
|
|
|
1478
|
-
//
|
|
1842
|
+
// Confirming - check before THINKING because "Running…" in tool output matches thinking patterns
|
|
1843
|
+
const confirmPatterns = config.confirmPatterns || [];
|
|
1844
|
+
for (const pattern of confirmPatterns) {
|
|
1845
|
+
if (typeof pattern === "function") {
|
|
1846
|
+
// Functions check lastLines first (most specific), then recentLines
|
|
1847
|
+
if (pattern(lastLines)) return State.CONFIRMING;
|
|
1848
|
+
if (pattern(recentLines)) return State.CONFIRMING;
|
|
1849
|
+
} else {
|
|
1850
|
+
// String patterns check recentLines (bounded range)
|
|
1851
|
+
if (recentLines.includes(pattern)) return State.CONFIRMING;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// Thinking - spinners (check last lines only to avoid false positives from timing messages like "✻ Crunched for 32s")
|
|
1479
1856
|
const spinners = config.spinners || [];
|
|
1480
|
-
if (spinners.some((s) =>
|
|
1857
|
+
if (spinners.some((s) => lastLines.includes(s))) {
|
|
1481
1858
|
return State.THINKING;
|
|
1482
1859
|
}
|
|
1483
|
-
// Thinking - text patterns (last lines)
|
|
1860
|
+
// Thinking - text patterns (last lines) - supports strings, regexes, and functions
|
|
1484
1861
|
const thinkingPatterns = config.thinkingPatterns || [];
|
|
1485
|
-
if (
|
|
1862
|
+
if (
|
|
1863
|
+
thinkingPatterns.some((p) => {
|
|
1864
|
+
if (typeof p === "function") return p(lastLines);
|
|
1865
|
+
if (p instanceof RegExp) return p.test(lastLines);
|
|
1866
|
+
return lastLines.includes(p);
|
|
1867
|
+
})
|
|
1868
|
+
) {
|
|
1486
1869
|
return State.THINKING;
|
|
1487
1870
|
}
|
|
1488
1871
|
|
|
@@ -1494,19 +1877,6 @@ function detectState(screen, config) {
|
|
|
1494
1877
|
}
|
|
1495
1878
|
}
|
|
1496
1879
|
|
|
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
1880
|
// Ready - only if prompt symbol is visible AND not followed by pasted content
|
|
1511
1881
|
// "[Pasted text" indicates user has pasted content and Claude is still processing
|
|
1512
1882
|
if (lastLines.includes(config.promptSymbol)) {
|
|
@@ -1541,12 +1911,13 @@ function detectState(screen, config) {
|
|
|
1541
1911
|
/**
|
|
1542
1912
|
* @typedef {Object} AgentConfigInput
|
|
1543
1913
|
* @property {string} name
|
|
1914
|
+
* @property {string} displayName
|
|
1544
1915
|
* @property {string} startCommand
|
|
1545
1916
|
* @property {string} yoloCommand
|
|
1546
1917
|
* @property {string} promptSymbol
|
|
1547
1918
|
* @property {string[]} [spinners]
|
|
1548
1919
|
* @property {RegExp} [rateLimitPattern]
|
|
1549
|
-
* @property {string[]} [thinkingPatterns]
|
|
1920
|
+
* @property {(string | RegExp | ((lines: string) => boolean))[]} [thinkingPatterns]
|
|
1550
1921
|
* @property {ConfirmPattern[]} [confirmPatterns]
|
|
1551
1922
|
* @property {UpdatePromptPatterns | null} [updatePromptPatterns]
|
|
1552
1923
|
* @property {string[]} [responseMarkers]
|
|
@@ -1556,6 +1927,8 @@ function detectState(screen, config) {
|
|
|
1556
1927
|
* @property {string} [approveKey]
|
|
1557
1928
|
* @property {string} [rejectKey]
|
|
1558
1929
|
* @property {string} [safeAllowedTools]
|
|
1930
|
+
* @property {string | null} [sessionIdFlag]
|
|
1931
|
+
* @property {((sessionName: string) => string | null) | null} [logPathFinder]
|
|
1559
1932
|
*/
|
|
1560
1933
|
|
|
1561
1934
|
class Agent {
|
|
@@ -1566,6 +1939,8 @@ class Agent {
|
|
|
1566
1939
|
/** @type {string} */
|
|
1567
1940
|
this.name = config.name;
|
|
1568
1941
|
/** @type {string} */
|
|
1942
|
+
this.displayName = config.displayName;
|
|
1943
|
+
/** @type {string} */
|
|
1569
1944
|
this.startCommand = config.startCommand;
|
|
1570
1945
|
/** @type {string} */
|
|
1571
1946
|
this.yoloCommand = config.yoloCommand;
|
|
@@ -1575,7 +1950,7 @@ class Agent {
|
|
|
1575
1950
|
this.spinners = config.spinners || [];
|
|
1576
1951
|
/** @type {RegExp | undefined} */
|
|
1577
1952
|
this.rateLimitPattern = config.rateLimitPattern;
|
|
1578
|
-
/** @type {string[]} */
|
|
1953
|
+
/** @type {(string | RegExp | ((lines: string) => boolean))[]} */
|
|
1579
1954
|
this.thinkingPatterns = config.thinkingPatterns || [];
|
|
1580
1955
|
/** @type {ConfirmPattern[]} */
|
|
1581
1956
|
this.confirmPatterns = config.confirmPatterns || [];
|
|
@@ -1595,6 +1970,10 @@ class Agent {
|
|
|
1595
1970
|
this.rejectKey = config.rejectKey || "n";
|
|
1596
1971
|
/** @type {string | undefined} */
|
|
1597
1972
|
this.safeAllowedTools = config.safeAllowedTools;
|
|
1973
|
+
/** @type {string | null} */
|
|
1974
|
+
this.sessionIdFlag = config.sessionIdFlag || null;
|
|
1975
|
+
/** @type {((sessionName: string) => string | null) | null} */
|
|
1976
|
+
this.logPathFinder = config.logPathFinder || null;
|
|
1598
1977
|
}
|
|
1599
1978
|
|
|
1600
1979
|
/**
|
|
@@ -1612,11 +1991,11 @@ class Agent {
|
|
|
1612
1991
|
} else {
|
|
1613
1992
|
base = this.startCommand;
|
|
1614
1993
|
}
|
|
1615
|
-
//
|
|
1616
|
-
if (this.
|
|
1994
|
+
// Some agents support session ID flags for deterministic session tracking
|
|
1995
|
+
if (this.sessionIdFlag && sessionName) {
|
|
1617
1996
|
const parsed = parseSessionName(sessionName);
|
|
1618
1997
|
if (parsed?.uuid) {
|
|
1619
|
-
return `${base}
|
|
1998
|
+
return `${base} ${this.sessionIdFlag} ${parsed.uuid}`;
|
|
1620
1999
|
}
|
|
1621
2000
|
}
|
|
1622
2001
|
return base;
|
|
@@ -1674,13 +2053,8 @@ class Agent {
|
|
|
1674
2053
|
* @returns {string | null}
|
|
1675
2054
|
*/
|
|
1676
2055
|
findLogPath(sessionName) {
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const uuid = parsed?.uuid;
|
|
1680
|
-
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
1681
|
-
}
|
|
1682
|
-
if (this.name === "codex") {
|
|
1683
|
-
return findCodexLogPath(sessionName);
|
|
2056
|
+
if (this.logPathFinder) {
|
|
2057
|
+
return this.logPathFinder(sessionName);
|
|
1684
2058
|
}
|
|
1685
2059
|
return null;
|
|
1686
2060
|
}
|
|
@@ -1908,6 +2282,7 @@ class Agent {
|
|
|
1908
2282
|
|
|
1909
2283
|
const CodexAgent = new Agent({
|
|
1910
2284
|
name: "codex",
|
|
2285
|
+
displayName: "Codex",
|
|
1911
2286
|
startCommand: "codex --sandbox read-only",
|
|
1912
2287
|
yoloCommand: "codex --dangerously-bypass-approvals-and-sandbox",
|
|
1913
2288
|
promptSymbol: "›",
|
|
@@ -1927,6 +2302,7 @@ const CodexAgent = new Agent({
|
|
|
1927
2302
|
chromePatterns: ["context left", "for shortcuts"],
|
|
1928
2303
|
reviewOptions: { pr: "1", uncommitted: "2", commit: "3", custom: "4" },
|
|
1929
2304
|
envVar: "AX_SESSION",
|
|
2305
|
+
logPathFinder: findCodexLogPath,
|
|
1930
2306
|
});
|
|
1931
2307
|
|
|
1932
2308
|
// =============================================================================
|
|
@@ -1935,12 +2311,15 @@ const CodexAgent = new Agent({
|
|
|
1935
2311
|
|
|
1936
2312
|
const ClaudeAgent = new Agent({
|
|
1937
2313
|
name: "claude",
|
|
2314
|
+
displayName: "Claude",
|
|
1938
2315
|
startCommand: "claude",
|
|
1939
2316
|
yoloCommand: "claude --dangerously-skip-permissions",
|
|
1940
2317
|
promptSymbol: "❯",
|
|
1941
|
-
|
|
2318
|
+
// Claude Code spinners: ·✢✳✶✻✽ (from cli.js source)
|
|
2319
|
+
spinners: ["·", "✢", "✳", "✶", "✻", "✽"],
|
|
1942
2320
|
rateLimitPattern: /rate.?limit/i,
|
|
1943
|
-
|
|
2321
|
+
// Claude uses whimsical verbs like "Wibbling…", "Dancing…", etc. Match any capitalized -ing word + ellipsis (… or ...)
|
|
2322
|
+
thinkingPatterns: ["Thinking", /[A-Z][a-z]+ing(…|\.\.\.)/],
|
|
1944
2323
|
confirmPatterns: [
|
|
1945
2324
|
"Do you want to make this edit",
|
|
1946
2325
|
"Do you want to run this command",
|
|
@@ -1965,6 +2344,13 @@ const ClaudeAgent = new Agent({
|
|
|
1965
2344
|
envVar: "AX_SESSION",
|
|
1966
2345
|
approveKey: "1",
|
|
1967
2346
|
rejectKey: "Escape",
|
|
2347
|
+
sessionIdFlag: "--session-id",
|
|
2348
|
+
logPathFinder: (sessionName) => {
|
|
2349
|
+
const parsed = parseSessionName(sessionName);
|
|
2350
|
+
const uuid = parsed?.uuid;
|
|
2351
|
+
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
2352
|
+
return null;
|
|
2353
|
+
},
|
|
1968
2354
|
});
|
|
1969
2355
|
|
|
1970
2356
|
// =============================================================================
|
|
@@ -2002,30 +2388,38 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2002
2388
|
return { state, screen };
|
|
2003
2389
|
}
|
|
2004
2390
|
}
|
|
2005
|
-
throw new
|
|
2391
|
+
throw new TimeoutError(session);
|
|
2006
2392
|
}
|
|
2007
2393
|
|
|
2008
2394
|
/**
|
|
2009
|
-
*
|
|
2010
|
-
* Waits for screen activity before considering the response complete.
|
|
2395
|
+
* Core polling loop for waiting on agent responses.
|
|
2011
2396
|
* @param {Agent} agent
|
|
2012
2397
|
* @param {string} session
|
|
2013
|
-
* @param {number}
|
|
2398
|
+
* @param {number} timeoutMs
|
|
2399
|
+
* @param {{onPoll?: (screen: string, state: string) => void, onStateChange?: (state: string, lastState: string | null, screen: string) => void, onReady?: (screen: string) => void}} [hooks]
|
|
2014
2400
|
* @returns {Promise<{state: string, screen: string}>}
|
|
2015
2401
|
*/
|
|
2016
|
-
async function
|
|
2402
|
+
async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
2403
|
+
const { onPoll, onStateChange, onReady } = hooks;
|
|
2017
2404
|
const start = Date.now();
|
|
2018
2405
|
const initialScreen = tmuxCapture(session);
|
|
2019
2406
|
|
|
2020
2407
|
let lastScreen = initialScreen;
|
|
2408
|
+
let lastState = null;
|
|
2021
2409
|
let stableAt = null;
|
|
2022
2410
|
let sawActivity = false;
|
|
2023
2411
|
|
|
2024
2412
|
while (Date.now() - start < timeoutMs) {
|
|
2025
|
-
await sleep(POLL_MS);
|
|
2026
2413
|
const screen = tmuxCapture(session);
|
|
2027
2414
|
const state = agent.getState(screen);
|
|
2028
2415
|
|
|
2416
|
+
if (onPoll) onPoll(screen, state);
|
|
2417
|
+
|
|
2418
|
+
if (state !== lastState) {
|
|
2419
|
+
if (onStateChange) onStateChange(state, lastState, screen);
|
|
2420
|
+
lastState = state;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2029
2423
|
if (state === State.RATE_LIMITED || state === State.CONFIRMING) {
|
|
2030
2424
|
return { state, screen };
|
|
2031
2425
|
}
|
|
@@ -2040,6 +2434,7 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2040
2434
|
|
|
2041
2435
|
if (sawActivity && stableAt && Date.now() - stableAt >= STABLE_MS) {
|
|
2042
2436
|
if (state === State.READY) {
|
|
2437
|
+
if (onReady) onReady(screen);
|
|
2043
2438
|
return { state, screen };
|
|
2044
2439
|
}
|
|
2045
2440
|
}
|
|
@@ -2047,26 +2442,86 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2047
2442
|
if (state === State.THINKING) {
|
|
2048
2443
|
sawActivity = true;
|
|
2049
2444
|
}
|
|
2445
|
+
|
|
2446
|
+
await sleep(POLL_MS);
|
|
2050
2447
|
}
|
|
2051
|
-
throw new
|
|
2448
|
+
throw new TimeoutError(session);
|
|
2052
2449
|
}
|
|
2053
2450
|
|
|
2054
2451
|
/**
|
|
2055
|
-
*
|
|
2056
|
-
* Used by callers to implement yolo mode on sessions not started with native --yolo.
|
|
2452
|
+
* Wait for agent response without streaming output.
|
|
2057
2453
|
* @param {Agent} agent
|
|
2058
2454
|
* @param {string} session
|
|
2059
2455
|
* @param {number} [timeoutMs]
|
|
2060
2456
|
* @returns {Promise<{state: string, screen: string}>}
|
|
2061
2457
|
*/
|
|
2062
|
-
async function
|
|
2458
|
+
async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2459
|
+
return pollForResponse(agent, session, timeoutMs);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
/**
|
|
2463
|
+
* Wait for agent response with streaming output to console.
|
|
2464
|
+
* @param {Agent} agent
|
|
2465
|
+
* @param {string} session
|
|
2466
|
+
* @param {number} [timeoutMs]
|
|
2467
|
+
* @returns {Promise<{state: string, screen: string}>}
|
|
2468
|
+
*/
|
|
2469
|
+
async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2470
|
+
let logPath = agent.findLogPath(session);
|
|
2471
|
+
let logOffset = logPath && existsSync(logPath) ? statSync(logPath).size : 0;
|
|
2472
|
+
let printedThinking = false;
|
|
2473
|
+
|
|
2474
|
+
const streamNewEntries = () => {
|
|
2475
|
+
if (!logPath) {
|
|
2476
|
+
logPath = agent.findLogPath(session);
|
|
2477
|
+
if (logPath && existsSync(logPath)) {
|
|
2478
|
+
logOffset = statSync(logPath).size;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
if (logPath) {
|
|
2482
|
+
const { entries, newOffset } = tailJsonl(logPath, logOffset);
|
|
2483
|
+
logOffset = newOffset;
|
|
2484
|
+
for (const entry of entries) {
|
|
2485
|
+
const formatted = formatEntry(entry);
|
|
2486
|
+
if (formatted) console.log(formatted);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
|
|
2491
|
+
return pollForResponse(agent, session, timeoutMs, {
|
|
2492
|
+
onPoll: () => streamNewEntries(),
|
|
2493
|
+
onStateChange: (state, lastState, screen) => {
|
|
2494
|
+
if (state === State.THINKING && !printedThinking) {
|
|
2495
|
+
console.log("[THINKING]");
|
|
2496
|
+
printedThinking = true;
|
|
2497
|
+
} else if (state === State.CONFIRMING) {
|
|
2498
|
+
const pendingTool = extractPendingToolFromScreen(screen);
|
|
2499
|
+
console.log(pendingTool ? `[CONFIRMING] ${pendingTool}` : "[CONFIRMING]");
|
|
2500
|
+
}
|
|
2501
|
+
if (lastState === State.THINKING && state !== State.THINKING) {
|
|
2502
|
+
printedThinking = false;
|
|
2503
|
+
}
|
|
2504
|
+
},
|
|
2505
|
+
onReady: () => streamNewEntries(),
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
/**
|
|
2510
|
+
* Auto-approve loop that keeps approving confirmations until the agent is ready or rate limited.
|
|
2511
|
+
* @param {Agent} agent
|
|
2512
|
+
* @param {string} session
|
|
2513
|
+
* @param {number} timeoutMs
|
|
2514
|
+
* @param {Function} waitFn - waitForResponse or streamResponse
|
|
2515
|
+
* @returns {Promise<{state: string, screen: string}>}
|
|
2516
|
+
*/
|
|
2517
|
+
async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
2063
2518
|
const deadline = Date.now() + timeoutMs;
|
|
2064
2519
|
|
|
2065
2520
|
while (Date.now() < deadline) {
|
|
2066
2521
|
const remaining = deadline - Date.now();
|
|
2067
2522
|
if (remaining <= 0) break;
|
|
2068
2523
|
|
|
2069
|
-
const { state, screen } = await
|
|
2524
|
+
const { state, screen } = await waitFn(agent, session, remaining);
|
|
2070
2525
|
|
|
2071
2526
|
if (state === State.RATE_LIMITED || state === State.READY) {
|
|
2072
2527
|
return { state, screen };
|
|
@@ -2078,11 +2533,10 @@ async function autoApproveLoop(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2078
2533
|
continue;
|
|
2079
2534
|
}
|
|
2080
2535
|
|
|
2081
|
-
// Unexpected state - log and continue polling
|
|
2082
2536
|
debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
|
|
2083
2537
|
}
|
|
2084
2538
|
|
|
2085
|
-
throw new
|
|
2539
|
+
throw new TimeoutError(session);
|
|
2086
2540
|
}
|
|
2087
2541
|
|
|
2088
2542
|
/**
|
|
@@ -2141,41 +2595,73 @@ function cmdAgents() {
|
|
|
2141
2595
|
|
|
2142
2596
|
if (agentSessions.length === 0) {
|
|
2143
2597
|
console.log("No agents running");
|
|
2598
|
+
// Still check for orphans
|
|
2599
|
+
const orphans = findOrphanedProcesses();
|
|
2600
|
+
if (orphans.length > 0) {
|
|
2601
|
+
console.log(`\nOrphaned (${orphans.length}):`);
|
|
2602
|
+
for (const { pid, command } of orphans) {
|
|
2603
|
+
console.log(` PID ${pid}: ${command}`);
|
|
2604
|
+
}
|
|
2605
|
+
console.log(`\n Run 'ax kill --orphans' to clean up`);
|
|
2606
|
+
}
|
|
2144
2607
|
return;
|
|
2145
2608
|
}
|
|
2146
2609
|
|
|
2610
|
+
// Get default session for each agent type
|
|
2611
|
+
const claudeDefault = ClaudeAgent.getDefaultSession();
|
|
2612
|
+
const codexDefault = CodexAgent.getDefaultSession();
|
|
2613
|
+
|
|
2147
2614
|
// Get info for each agent
|
|
2148
2615
|
const agents = agentSessions.map((session) => {
|
|
2149
2616
|
const parsed = /** @type {ParsedSession} */ (parseSessionName(session));
|
|
2150
2617
|
const agent = parsed.tool === "claude" ? ClaudeAgent : CodexAgent;
|
|
2151
2618
|
const screen = tmuxCapture(session);
|
|
2152
2619
|
const state = agent.getState(screen);
|
|
2153
|
-
const logPath = agent.findLogPath(session);
|
|
2154
2620
|
const type = parsed.archangelName ? "archangel" : "-";
|
|
2621
|
+
const isDefault =
|
|
2622
|
+
(parsed.tool === "claude" && session === claudeDefault) ||
|
|
2623
|
+
(parsed.tool === "codex" && session === codexDefault);
|
|
2624
|
+
|
|
2625
|
+
// Get session metadata (Claude only)
|
|
2626
|
+
const meta = getSessionMeta(session);
|
|
2155
2627
|
|
|
2156
2628
|
return {
|
|
2157
2629
|
session,
|
|
2158
2630
|
tool: parsed.tool,
|
|
2159
2631
|
state: state || "unknown",
|
|
2632
|
+
target: isDefault ? "*" : "",
|
|
2160
2633
|
type,
|
|
2161
|
-
|
|
2634
|
+
plan: meta?.slug || "-",
|
|
2635
|
+
branch: meta?.gitBranch || "-",
|
|
2162
2636
|
};
|
|
2163
2637
|
});
|
|
2164
2638
|
|
|
2165
|
-
// Print table
|
|
2639
|
+
// Print sessions table
|
|
2166
2640
|
const maxSession = Math.max(7, ...agents.map((a) => a.session.length));
|
|
2167
2641
|
const maxTool = Math.max(4, ...agents.map((a) => a.tool.length));
|
|
2168
2642
|
const maxState = Math.max(5, ...agents.map((a) => a.state.length));
|
|
2643
|
+
const maxTarget = Math.max(6, ...agents.map((a) => a.target.length));
|
|
2169
2644
|
const maxType = Math.max(4, ...agents.map((a) => a.type.length));
|
|
2645
|
+
const maxPlan = Math.max(4, ...agents.map((a) => a.plan.length));
|
|
2170
2646
|
|
|
2171
2647
|
console.log(
|
|
2172
|
-
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TYPE".padEnd(maxType)}
|
|
2648
|
+
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(maxTarget)} ${"TYPE".padEnd(maxType)} ${"PLAN".padEnd(maxPlan)} BRANCH`,
|
|
2173
2649
|
);
|
|
2174
2650
|
for (const a of agents) {
|
|
2175
2651
|
console.log(
|
|
2176
|
-
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.type.padEnd(maxType)} ${a.
|
|
2652
|
+
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.target.padEnd(maxTarget)} ${a.type.padEnd(maxType)} ${a.plan.padEnd(maxPlan)} ${a.branch}`,
|
|
2177
2653
|
);
|
|
2178
2654
|
}
|
|
2655
|
+
|
|
2656
|
+
// Print orphaned processes if any
|
|
2657
|
+
const orphans = findOrphanedProcesses();
|
|
2658
|
+
if (orphans.length > 0) {
|
|
2659
|
+
console.log(`\nOrphaned (${orphans.length}):`);
|
|
2660
|
+
for (const { pid, command } of orphans) {
|
|
2661
|
+
console.log(` PID ${pid}: ${command}`);
|
|
2662
|
+
}
|
|
2663
|
+
console.log(`\n Run 'ax kill --orphans' to clean up`);
|
|
2664
|
+
}
|
|
2179
2665
|
}
|
|
2180
2666
|
|
|
2181
2667
|
// =============================================================================
|
|
@@ -2312,6 +2798,13 @@ async function cmdArchangel(agentName) {
|
|
|
2312
2798
|
let isProcessing = false;
|
|
2313
2799
|
const intervalMs = config.interval * 1000;
|
|
2314
2800
|
|
|
2801
|
+
// Hash tracking for incremental context updates
|
|
2802
|
+
/** @type {string | null} */
|
|
2803
|
+
let lastPlanHash = null;
|
|
2804
|
+
/** @type {string | null} */
|
|
2805
|
+
let lastTodosHash = null;
|
|
2806
|
+
let isFirstTrigger = true;
|
|
2807
|
+
|
|
2315
2808
|
async function processChanges() {
|
|
2316
2809
|
clearTimeout(debounceTimer);
|
|
2317
2810
|
clearTimeout(maxWaitTimer);
|
|
@@ -2329,6 +2822,21 @@ async function cmdArchangel(agentName) {
|
|
|
2329
2822
|
const parent = findParentSession();
|
|
2330
2823
|
const logPath = parent ? findClaudeLogPath(parent.uuid, parent.session) : null;
|
|
2331
2824
|
|
|
2825
|
+
// Get orientation context (plan and todos) from parent session
|
|
2826
|
+
const meta = parent?.session ? getSessionMeta(parent.session) : null;
|
|
2827
|
+
const planContent = meta?.slug ? readPlanFile(meta.slug) : null;
|
|
2828
|
+
const todosContent = meta?.todos?.length ? formatTodos(meta.todos) : null;
|
|
2829
|
+
|
|
2830
|
+
// Check if plan/todos have changed since last trigger
|
|
2831
|
+
const planHash = quickHash(planContent);
|
|
2832
|
+
const todosHash = quickHash(todosContent);
|
|
2833
|
+
const includePlan = planHash !== lastPlanHash;
|
|
2834
|
+
const includeTodos = todosHash !== lastTodosHash;
|
|
2835
|
+
|
|
2836
|
+
// Update tracking for next trigger
|
|
2837
|
+
lastPlanHash = planHash;
|
|
2838
|
+
lastTodosHash = todosHash;
|
|
2839
|
+
|
|
2332
2840
|
// Build file-specific context from JSONL
|
|
2333
2841
|
const fileContexts = [];
|
|
2334
2842
|
for (const file of files.slice(0, 5)) {
|
|
@@ -2340,7 +2848,18 @@ async function cmdArchangel(agentName) {
|
|
|
2340
2848
|
}
|
|
2341
2849
|
|
|
2342
2850
|
// Build the prompt
|
|
2343
|
-
|
|
2851
|
+
// First trigger: include intro, guidelines, and focus (archangel has memory)
|
|
2852
|
+
let prompt = isFirstTrigger
|
|
2853
|
+
? `You are the archangel of ${agentName}.\n\n${ARCHANGEL_PREAMBLE}\n\n## Focus\n\n${basePrompt}\n\n---`
|
|
2854
|
+
: "";
|
|
2855
|
+
|
|
2856
|
+
// Add orientation context (plan and todos) only if changed since last trigger
|
|
2857
|
+
if (includePlan && planContent) {
|
|
2858
|
+
prompt += (prompt ? "\n\n" : "") + "## Current Plan\n\n" + planContent;
|
|
2859
|
+
}
|
|
2860
|
+
if (includeTodos && todosContent) {
|
|
2861
|
+
prompt += (prompt ? "\n\n" : "") + "## Current Todos\n\n" + todosContent;
|
|
2862
|
+
}
|
|
2344
2863
|
|
|
2345
2864
|
if (fileContexts.length > 0) {
|
|
2346
2865
|
prompt += "\n\n## Recent Edits (from parent session)\n";
|
|
@@ -2374,8 +2893,7 @@ async function cmdArchangel(agentName) {
|
|
|
2374
2893
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2375
2894
|
}
|
|
2376
2895
|
|
|
2377
|
-
prompt +=
|
|
2378
|
-
'\n\nReview these changes in the context of what the user is working on. Report any issues found. Keep your response concise.\nIf there are no significant issues, respond with just "No issues found."';
|
|
2896
|
+
prompt += "\n\nReview these changes.";
|
|
2379
2897
|
} else {
|
|
2380
2898
|
// Fallback: no JSONL context available, use conversation + git context
|
|
2381
2899
|
const parentContext = getParentSessionContext(ARCHANGEL_PARENT_CONTEXT_ENTRIES);
|
|
@@ -2395,8 +2913,7 @@ async function cmdArchangel(agentName) {
|
|
|
2395
2913
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2396
2914
|
}
|
|
2397
2915
|
|
|
2398
|
-
prompt +=
|
|
2399
|
-
'\n\nReview these changes in the context of what the user is working on. Report any issues found. Keep your response concise.\nIf there are no significant issues, respond with just "No issues found."';
|
|
2916
|
+
prompt += "\n\nReview these changes.";
|
|
2400
2917
|
}
|
|
2401
2918
|
|
|
2402
2919
|
// Check session still exists
|
|
@@ -2425,6 +2942,7 @@ async function cmdArchangel(agentName) {
|
|
|
2425
2942
|
await sleep(200); // Allow time for large prompts to be processed
|
|
2426
2943
|
tmuxSend(sessionName, "Enter");
|
|
2427
2944
|
await sleep(100); // Ensure Enter is processed
|
|
2945
|
+
isFirstTrigger = false;
|
|
2428
2946
|
|
|
2429
2947
|
// Wait for response
|
|
2430
2948
|
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
@@ -2838,9 +3356,31 @@ function ensureClaudeHookConfig() {
|
|
|
2838
3356
|
|
|
2839
3357
|
/**
|
|
2840
3358
|
* @param {string | null | undefined} session
|
|
2841
|
-
* @param {{all?: boolean}} [options]
|
|
3359
|
+
* @param {{all?: boolean, orphans?: boolean, force?: boolean}} [options]
|
|
2842
3360
|
*/
|
|
2843
|
-
function cmdKill(session, { all = false } = {}) {
|
|
3361
|
+
function cmdKill(session, { all = false, orphans = false, force = false } = {}) {
|
|
3362
|
+
// Handle orphaned processes
|
|
3363
|
+
if (orphans) {
|
|
3364
|
+
const orphanedProcesses = findOrphanedProcesses();
|
|
3365
|
+
|
|
3366
|
+
if (orphanedProcesses.length === 0) {
|
|
3367
|
+
console.log("No orphaned processes found");
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
const signal = force ? "-9" : "-15"; // SIGKILL vs SIGTERM
|
|
3372
|
+
let killed = 0;
|
|
3373
|
+
for (const { pid, command } of orphanedProcesses) {
|
|
3374
|
+
const result = spawnSync("kill", [signal, pid]);
|
|
3375
|
+
if (result.status === 0) {
|
|
3376
|
+
console.log(`Killed: PID ${pid} (${command.slice(0, 40)})`);
|
|
3377
|
+
killed++;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
console.log(`Killed ${killed} orphaned process(es)${force ? " (forced)" : ""}`);
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
|
|
2844
3384
|
// If specific session provided, kill just that one
|
|
2845
3385
|
if (session) {
|
|
2846
3386
|
if (!tmuxHasSession(session)) {
|
|
@@ -3203,7 +3743,7 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3203
3743
|
* @param {string} message
|
|
3204
3744
|
* @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
|
|
3205
3745
|
*/
|
|
3206
|
-
async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs } = {}) {
|
|
3746
|
+
async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
3207
3747
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3208
3748
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3209
3749
|
|
|
@@ -3223,14 +3763,23 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3223
3763
|
await sleep(50);
|
|
3224
3764
|
tmuxSend(activeSession, "Enter");
|
|
3225
3765
|
|
|
3226
|
-
if (noWait)
|
|
3766
|
+
if (noWait) {
|
|
3767
|
+
const parsed = parseSessionName(activeSession);
|
|
3768
|
+
const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
|
|
3769
|
+
const cli = path.basename(process.argv[1], ".js");
|
|
3770
|
+
console.log(`Sent to: ${shortId}
|
|
3771
|
+
|
|
3772
|
+
e.g.
|
|
3773
|
+
${cli} status --session=${shortId}
|
|
3774
|
+
${cli} output --session=${shortId}`);
|
|
3775
|
+
return;
|
|
3776
|
+
}
|
|
3227
3777
|
|
|
3228
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3229
3778
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3230
3779
|
|
|
3231
3780
|
const { state, screen } = useAutoApprove
|
|
3232
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3233
|
-
: await
|
|
3781
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3782
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3234
3783
|
|
|
3235
3784
|
if (state === State.RATE_LIMITED) {
|
|
3236
3785
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3238,14 +3787,9 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3238
3787
|
}
|
|
3239
3788
|
|
|
3240
3789
|
if (state === State.CONFIRMING) {
|
|
3241
|
-
console.log(`CONFIRM: ${
|
|
3790
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3242
3791
|
process.exit(3);
|
|
3243
3792
|
}
|
|
3244
|
-
|
|
3245
|
-
const output = agent.getResponse(activeSession, screen);
|
|
3246
|
-
if (output) {
|
|
3247
|
-
console.log(output);
|
|
3248
|
-
}
|
|
3249
3793
|
}
|
|
3250
3794
|
|
|
3251
3795
|
/**
|
|
@@ -3260,9 +3804,10 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3260
3804
|
}
|
|
3261
3805
|
|
|
3262
3806
|
const before = tmuxCapture(session);
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3807
|
+
const beforeState = agent.getState(before);
|
|
3808
|
+
if (beforeState !== State.CONFIRMING) {
|
|
3809
|
+
console.log(`Already ${beforeState}`);
|
|
3810
|
+
return;
|
|
3266
3811
|
}
|
|
3267
3812
|
|
|
3268
3813
|
tmuxSend(session, agent.approveKey);
|
|
@@ -3277,7 +3822,7 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3277
3822
|
}
|
|
3278
3823
|
|
|
3279
3824
|
if (state === State.CONFIRMING) {
|
|
3280
|
-
console.log(`CONFIRM: ${
|
|
3825
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3281
3826
|
process.exit(3);
|
|
3282
3827
|
}
|
|
3283
3828
|
|
|
@@ -3296,6 +3841,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3296
3841
|
process.exit(1);
|
|
3297
3842
|
}
|
|
3298
3843
|
|
|
3844
|
+
const before = tmuxCapture(session);
|
|
3845
|
+
const beforeState = agent.getState(before);
|
|
3846
|
+
if (beforeState !== State.CONFIRMING) {
|
|
3847
|
+
console.log(`Already ${beforeState}`);
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3299
3851
|
tmuxSend(session, agent.rejectKey);
|
|
3300
3852
|
|
|
3301
3853
|
if (!wait) return;
|
|
@@ -3323,7 +3875,7 @@ async function cmdReview(
|
|
|
3323
3875
|
session,
|
|
3324
3876
|
option,
|
|
3325
3877
|
customInstructions,
|
|
3326
|
-
{ wait = true, yolo =
|
|
3878
|
+
{ wait = true, yolo = false, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
|
|
3327
3879
|
) {
|
|
3328
3880
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3329
3881
|
|
|
@@ -3390,12 +3942,11 @@ async function cmdReview(
|
|
|
3390
3942
|
|
|
3391
3943
|
if (!wait) return;
|
|
3392
3944
|
|
|
3393
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3394
3945
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3395
3946
|
|
|
3396
3947
|
const { state, screen } = useAutoApprove
|
|
3397
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3398
|
-
: await
|
|
3948
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3949
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3399
3950
|
|
|
3400
3951
|
if (state === State.RATE_LIMITED) {
|
|
3401
3952
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3403,12 +3954,9 @@ async function cmdReview(
|
|
|
3403
3954
|
}
|
|
3404
3955
|
|
|
3405
3956
|
if (state === State.CONFIRMING) {
|
|
3406
|
-
console.log(`CONFIRM: ${
|
|
3957
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3407
3958
|
process.exit(3);
|
|
3408
3959
|
}
|
|
3409
|
-
|
|
3410
|
-
const response = agent.getResponse(activeSession, screen);
|
|
3411
|
-
console.log(response || "");
|
|
3412
3960
|
}
|
|
3413
3961
|
|
|
3414
3962
|
/**
|
|
@@ -3439,7 +3987,7 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3439
3987
|
}
|
|
3440
3988
|
|
|
3441
3989
|
if (state === State.CONFIRMING) {
|
|
3442
|
-
console.log(`CONFIRM: ${
|
|
3990
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3443
3991
|
process.exit(3);
|
|
3444
3992
|
}
|
|
3445
3993
|
|
|
@@ -3451,6 +3999,8 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3451
3999
|
const output = agent.getResponse(session, screen, index);
|
|
3452
4000
|
if (output) {
|
|
3453
4001
|
console.log(output);
|
|
4002
|
+
} else {
|
|
4003
|
+
console.log("READY_NO_CONTENT");
|
|
3454
4004
|
}
|
|
3455
4005
|
}
|
|
3456
4006
|
|
|
@@ -3473,7 +4023,7 @@ function cmdStatus(agent, session) {
|
|
|
3473
4023
|
}
|
|
3474
4024
|
|
|
3475
4025
|
if (state === State.CONFIRMING) {
|
|
3476
|
-
console.log(`CONFIRM: ${
|
|
4026
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3477
4027
|
process.exit(3);
|
|
3478
4028
|
}
|
|
3479
4029
|
|
|
@@ -3481,6 +4031,10 @@ function cmdStatus(agent, session) {
|
|
|
3481
4031
|
console.log("THINKING");
|
|
3482
4032
|
process.exit(4);
|
|
3483
4033
|
}
|
|
4034
|
+
|
|
4035
|
+
// READY (or STARTING/UPDATE_PROMPT which are transient)
|
|
4036
|
+
console.log("READY");
|
|
4037
|
+
process.exit(0);
|
|
3484
4038
|
}
|
|
3485
4039
|
|
|
3486
4040
|
/**
|
|
@@ -3594,7 +4148,7 @@ async function cmdSelect(agent, session, n, { wait = false, timeoutMs } = {}) {
|
|
|
3594
4148
|
}
|
|
3595
4149
|
|
|
3596
4150
|
if (state === State.CONFIRMING) {
|
|
3597
|
-
console.log(`CONFIRM: ${
|
|
4151
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3598
4152
|
process.exit(3);
|
|
3599
4153
|
}
|
|
3600
4154
|
|
|
@@ -3629,19 +4183,22 @@ function getAgentFromInvocation() {
|
|
|
3629
4183
|
*/
|
|
3630
4184
|
function printHelp(agent, cliName) {
|
|
3631
4185
|
const name = cliName;
|
|
3632
|
-
const backendName = agent.
|
|
4186
|
+
const backendName = agent.displayName;
|
|
3633
4187
|
const hasReview = !!agent.reviewOptions;
|
|
3634
4188
|
|
|
3635
4189
|
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
3636
4190
|
|
|
4191
|
+
Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
4192
|
+
|
|
3637
4193
|
Commands:
|
|
3638
4194
|
agents List all running agents with state and log paths
|
|
4195
|
+
target Show default target session for current tool
|
|
3639
4196
|
attach [SESSION] Attach to agent session interactively
|
|
3640
4197
|
log SESSION View conversation log (--tail=N, --follow, --reasoning)
|
|
3641
4198
|
mailbox View archangel observations (--limit=N, --branch=X, --all)
|
|
3642
4199
|
summon [name] Summon archangels (all, or by name)
|
|
3643
4200
|
recall [name] Recall archangels (all, or by name)
|
|
3644
|
-
kill Kill sessions
|
|
4201
|
+
kill Kill sessions (--all, --session=NAME, --orphans [--force])
|
|
3645
4202
|
status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
|
|
3646
4203
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
3647
4204
|
debug Show raw screen output and detected state${
|
|
@@ -3661,11 +4218,13 @@ Commands:
|
|
|
3661
4218
|
Flags:
|
|
3662
4219
|
--tool=NAME Use specific agent (codex, claude)
|
|
3663
4220
|
--session=NAME Target session by name, archangel name, or UUID prefix (self = current)
|
|
3664
|
-
--wait Wait for response (for
|
|
3665
|
-
--no-wait
|
|
3666
|
-
--timeout=N Set timeout in seconds (default:
|
|
4221
|
+
--wait Wait for response (default for messages; required for approve/reject)
|
|
4222
|
+
--no-wait Fire-and-forget: send message, print session ID, exit immediately
|
|
4223
|
+
--timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
|
|
3667
4224
|
--yolo Skip all confirmations (dangerous)
|
|
3668
4225
|
--fresh Reset conversation before review
|
|
4226
|
+
--orphans Kill orphaned claude/codex processes (PPID=1)
|
|
4227
|
+
--force Use SIGKILL instead of SIGTERM (with --orphans)
|
|
3669
4228
|
|
|
3670
4229
|
Environment:
|
|
3671
4230
|
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
@@ -3677,7 +4236,8 @@ Environment:
|
|
|
3677
4236
|
|
|
3678
4237
|
Examples:
|
|
3679
4238
|
${name} "explain this codebase"
|
|
3680
|
-
${name} "
|
|
4239
|
+
${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
|
|
4240
|
+
${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
|
|
3681
4241
|
${name} review uncommitted --wait
|
|
3682
4242
|
${name} approve --wait
|
|
3683
4243
|
${name} kill # Kill agents in current project
|
|
@@ -3689,7 +4249,10 @@ Examples:
|
|
|
3689
4249
|
${name} summon reviewer # Summon by name (creates config if new)
|
|
3690
4250
|
${name} recall # Recall all archangels
|
|
3691
4251
|
${name} recall reviewer # Recall one by name
|
|
3692
|
-
${name} agents # List all agents (shows TYPE=archangel)
|
|
4252
|
+
${name} agents # List all agents (shows TYPE=archangel)
|
|
4253
|
+
|
|
4254
|
+
Note: Reviews and complex tasks may take several minutes.
|
|
4255
|
+
Use Bash run_in_background for long operations (not --no-wait).`);
|
|
3693
4256
|
}
|
|
3694
4257
|
|
|
3695
4258
|
async function main() {
|
|
@@ -3704,38 +4267,32 @@ async function main() {
|
|
|
3704
4267
|
const args = process.argv.slice(2);
|
|
3705
4268
|
const cliName = path.basename(process.argv[1], ".js");
|
|
3706
4269
|
|
|
3707
|
-
|
|
4270
|
+
// Parse all flags and positionals in one place
|
|
4271
|
+
const { flags, positionals } = parseCliArgs(args);
|
|
4272
|
+
|
|
4273
|
+
if (flags.version) {
|
|
3708
4274
|
console.log(VERSION);
|
|
3709
4275
|
process.exit(0);
|
|
3710
4276
|
}
|
|
3711
4277
|
|
|
3712
|
-
//
|
|
3713
|
-
const 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");
|
|
4278
|
+
// Extract flags into local variables for convenience
|
|
4279
|
+
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force } = flags;
|
|
3719
4280
|
|
|
3720
4281
|
// Agent selection
|
|
3721
4282
|
let agent = getAgentFromInvocation();
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
if (tool === "claude") agent = ClaudeAgent;
|
|
3726
|
-
else if (tool === "codex") agent = CodexAgent;
|
|
4283
|
+
if (flags.tool) {
|
|
4284
|
+
if (flags.tool === "claude") agent = ClaudeAgent;
|
|
4285
|
+
else if (flags.tool === "codex") agent = CodexAgent;
|
|
3727
4286
|
else {
|
|
3728
|
-
console.log(`ERROR: unknown tool '${tool}'`);
|
|
4287
|
+
console.log(`ERROR: unknown tool '${flags.tool}'`);
|
|
3729
4288
|
process.exit(1);
|
|
3730
4289
|
}
|
|
3731
4290
|
}
|
|
3732
4291
|
|
|
3733
4292
|
// Session resolution
|
|
3734
4293
|
let session = agent.getDefaultSession();
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
const val = sessionArg.split("=")[1];
|
|
3738
|
-
if (val === "self") {
|
|
4294
|
+
if (flags.session) {
|
|
4295
|
+
if (flags.session === "self") {
|
|
3739
4296
|
const current = tmuxCurrentSession();
|
|
3740
4297
|
if (!current) {
|
|
3741
4298
|
console.log("ERROR: --session=self requires running inside tmux");
|
|
@@ -3744,110 +4301,92 @@ async function main() {
|
|
|
3744
4301
|
session = current;
|
|
3745
4302
|
} else {
|
|
3746
4303
|
// Resolve partial names, archangel names, and UUID prefixes
|
|
3747
|
-
session = resolveSessionName(
|
|
4304
|
+
session = resolveSessionName(flags.session);
|
|
3748
4305
|
}
|
|
3749
4306
|
}
|
|
3750
4307
|
|
|
3751
|
-
// Timeout
|
|
4308
|
+
// Timeout (convert seconds to milliseconds)
|
|
3752
4309
|
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
const val = parseInt(timeoutArg.split("=")[1], 10);
|
|
3756
|
-
if (isNaN(val) || val <= 0) {
|
|
4310
|
+
if (flags.timeout !== undefined) {
|
|
4311
|
+
if (isNaN(flags.timeout) || flags.timeout <= 0) {
|
|
3757
4312
|
console.log("ERROR: invalid timeout");
|
|
3758
4313
|
process.exit(1);
|
|
3759
4314
|
}
|
|
3760
|
-
timeoutMs =
|
|
4315
|
+
timeoutMs = flags.timeout * 1000;
|
|
3761
4316
|
}
|
|
3762
4317
|
|
|
3763
4318
|
// Tail (for log command)
|
|
3764
|
-
|
|
3765
|
-
const tailArg = args.find((a) => a.startsWith("--tail="));
|
|
3766
|
-
if (tailArg) {
|
|
3767
|
-
tail = parseInt(tailArg.split("=")[1], 10) || 50;
|
|
3768
|
-
}
|
|
4319
|
+
const tail = flags.tail ?? 50;
|
|
3769
4320
|
|
|
3770
4321
|
// Limit (for mailbox command)
|
|
3771
|
-
|
|
3772
|
-
const limitArg = args.find((a) => a.startsWith("--limit="));
|
|
3773
|
-
if (limitArg) {
|
|
3774
|
-
limit = parseInt(limitArg.split("=")[1], 10) || 20;
|
|
3775
|
-
}
|
|
4322
|
+
const limit = flags.limit ?? 20;
|
|
3776
4323
|
|
|
3777
4324
|
// Branch filter (for mailbox command)
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
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];
|
|
4325
|
+
const branch = flags.branch ?? null;
|
|
4326
|
+
|
|
4327
|
+
// Command is first positional
|
|
4328
|
+
const cmd = positionals[0];
|
|
3799
4329
|
|
|
3800
4330
|
// Dispatch commands
|
|
3801
4331
|
if (cmd === "agents") return cmdAgents();
|
|
3802
|
-
if (cmd === "
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
4332
|
+
if (cmd === "target") {
|
|
4333
|
+
const defaultSession = agent.getDefaultSession();
|
|
4334
|
+
if (defaultSession) {
|
|
4335
|
+
console.log(defaultSession);
|
|
4336
|
+
} else {
|
|
4337
|
+
console.log("NO_TARGET");
|
|
4338
|
+
process.exit(1);
|
|
4339
|
+
}
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
if (cmd === "summon") return cmdSummon(positionals[1]);
|
|
4343
|
+
if (cmd === "recall") return cmdRecall(positionals[1]);
|
|
4344
|
+
if (cmd === "archangel") return cmdArchangel(positionals[1]);
|
|
4345
|
+
if (cmd === "kill") return cmdKill(session, { all, orphans, force });
|
|
4346
|
+
if (cmd === "attach") return cmdAttach(positionals[1] || session);
|
|
4347
|
+
if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
|
|
3808
4348
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
3809
4349
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
3810
4350
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
3811
4351
|
if (cmd === "review")
|
|
3812
|
-
return cmdReview(agent, session,
|
|
4352
|
+
return cmdReview(agent, session, positionals[1], positionals[2], {
|
|
3813
4353
|
wait,
|
|
3814
|
-
yolo,
|
|
3815
4354
|
fresh,
|
|
3816
4355
|
timeoutMs,
|
|
3817
4356
|
});
|
|
3818
4357
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
3819
4358
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
3820
4359
|
if (cmd === "output") {
|
|
3821
|
-
const indexArg =
|
|
4360
|
+
const indexArg = positionals[1];
|
|
3822
4361
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
3823
4362
|
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
3824
4363
|
}
|
|
3825
|
-
if (cmd === "send" &&
|
|
3826
|
-
return cmdSend(session,
|
|
4364
|
+
if (cmd === "send" && positionals.length > 1)
|
|
4365
|
+
return cmdSend(session, positionals.slice(1).join(" "));
|
|
3827
4366
|
if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
|
|
3828
4367
|
if (cmd === "reset") return cmdAsk(agent, session, "/new", { noWait: true, timeoutMs });
|
|
3829
|
-
if (cmd === "select" &&
|
|
3830
|
-
return cmdSelect(agent, session,
|
|
4368
|
+
if (cmd === "select" && positionals[1])
|
|
4369
|
+
return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
|
|
3831
4370
|
|
|
3832
4371
|
// Default: send message
|
|
3833
|
-
let message =
|
|
4372
|
+
let message = positionals.join(" ");
|
|
3834
4373
|
if (!message && hasStdinData()) {
|
|
3835
4374
|
message = await readStdin();
|
|
3836
4375
|
}
|
|
3837
4376
|
|
|
3838
|
-
if (!message ||
|
|
4377
|
+
if (!message || flags.help) {
|
|
3839
4378
|
printHelp(agent, cliName);
|
|
3840
4379
|
process.exit(0);
|
|
3841
4380
|
}
|
|
3842
4381
|
|
|
3843
|
-
// Detect "please review" and route to custom review mode
|
|
3844
|
-
const reviewMatch = message.match(/^please review\s*(.*)/i);
|
|
4382
|
+
// Detect "review ..." or "please review ..." and route to custom review mode
|
|
4383
|
+
const reviewMatch = message.match(/^(?:please )?review\s*(.*)/i);
|
|
3845
4384
|
if (reviewMatch && agent.reviewOptions) {
|
|
3846
4385
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
3847
4386
|
return cmdReview(agent, session, "custom", customInstructions, {
|
|
3848
4387
|
wait: !noWait,
|
|
3849
4388
|
yolo,
|
|
3850
|
-
timeoutMs,
|
|
4389
|
+
timeoutMs: flags.timeout !== undefined ? timeoutMs : REVIEW_TIMEOUT_MS,
|
|
3851
4390
|
});
|
|
3852
4391
|
}
|
|
3853
4392
|
|
|
@@ -3868,6 +4407,9 @@ const isDirectRun =
|
|
|
3868
4407
|
if (isDirectRun) {
|
|
3869
4408
|
main().catch((err) => {
|
|
3870
4409
|
console.log(`ERROR: ${err.message}`);
|
|
4410
|
+
if (err instanceof TimeoutError && err.session) {
|
|
4411
|
+
console.log(`Hint: Use 'ax debug --session=${err.session}' to see current screen state`);
|
|
4412
|
+
}
|
|
3871
4413
|
process.exit(1);
|
|
3872
4414
|
});
|
|
3873
4415
|
}
|
|
@@ -3877,6 +4419,7 @@ export {
|
|
|
3877
4419
|
parseSessionName,
|
|
3878
4420
|
parseAgentConfig,
|
|
3879
4421
|
parseKeySequence,
|
|
4422
|
+
parseCliArgs,
|
|
3880
4423
|
getClaudeProjectPath,
|
|
3881
4424
|
matchesPattern,
|
|
3882
4425
|
getBaseDir,
|