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