ax-agents 0.0.1-alpha.1 → 0.0.1-alpha.10
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/README.md +20 -3
- package/ax.js +1332 -572
- package/package.json +1 -1
package/ax.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// ax
|
|
3
|
+
// ax - CLI for interacting with AI agents (Codex, Claude) via `tmux`.
|
|
4
|
+
// Usage: ax --help
|
|
4
5
|
//
|
|
5
6
|
// Exit codes:
|
|
6
7
|
// 0 - success / ready
|
|
@@ -8,17 +9,34 @@
|
|
|
8
9
|
// 2 - rate limited
|
|
9
10
|
// 3 - awaiting confirmation
|
|
10
11
|
// 4 - thinking
|
|
11
|
-
//
|
|
12
|
-
// Usage: ./ax.js --help
|
|
13
|
-
// ./axcodex.js --help (symlink)
|
|
14
|
-
// ./axclaude.js --help (symlink)
|
|
15
12
|
|
|
16
13
|
import { execSync, spawnSync, spawn } from "node:child_process";
|
|
17
|
-
import {
|
|
18
|
-
|
|
14
|
+
import {
|
|
15
|
+
fstatSync,
|
|
16
|
+
statSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
appendFileSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
renameSync,
|
|
24
|
+
realpathSync,
|
|
25
|
+
watch,
|
|
26
|
+
openSync,
|
|
27
|
+
readSync,
|
|
28
|
+
closeSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
19
31
|
import { fileURLToPath } from "node:url";
|
|
20
32
|
import path from "node:path";
|
|
21
33
|
import os from "node:os";
|
|
34
|
+
import { parseArgs } from "node:util";
|
|
35
|
+
|
|
36
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
37
|
+
const __dirname = path.dirname(__filename);
|
|
38
|
+
const packageJson = JSON.parse(readFileSync(path.join(__dirname, "package.json"), "utf-8"));
|
|
39
|
+
const VERSION = packageJson.version;
|
|
22
40
|
|
|
23
41
|
/**
|
|
24
42
|
* @typedef {'claude' | 'codex'} ToolName
|
|
@@ -27,12 +45,12 @@ import os from "node:os";
|
|
|
27
45
|
/**
|
|
28
46
|
* @typedef {Object} ParsedSession
|
|
29
47
|
* @property {string} tool
|
|
30
|
-
* @property {string} [
|
|
48
|
+
* @property {string} [archangelName]
|
|
31
49
|
* @property {string} [uuid]
|
|
32
50
|
*/
|
|
33
51
|
|
|
34
52
|
/**
|
|
35
|
-
* @typedef {Object}
|
|
53
|
+
* @typedef {Object} ArchangelConfig
|
|
36
54
|
* @property {string} name
|
|
37
55
|
* @property {ToolName} tool
|
|
38
56
|
* @property {string[]} watch
|
|
@@ -105,8 +123,9 @@ import os from "node:os";
|
|
|
105
123
|
*/
|
|
106
124
|
|
|
107
125
|
/**
|
|
126
|
+
* @typedef {{matcher: string, hooks: Array<{type: string, command: string, timeout?: number}>}} ClaudeHookEntry
|
|
108
127
|
* @typedef {Object} ClaudeSettings
|
|
109
|
-
* @property {{UserPromptSubmit?:
|
|
128
|
+
* @property {{UserPromptSubmit?: ClaudeHookEntry[], PostToolUse?: ClaudeHookEntry[], Stop?: ClaudeHookEntry[], [key: string]: ClaudeHookEntry[] | undefined}} [hooks]
|
|
110
129
|
*/
|
|
111
130
|
|
|
112
131
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
@@ -216,7 +235,9 @@ function tmuxKill(session) {
|
|
|
216
235
|
*/
|
|
217
236
|
function tmuxNewSession(session, command) {
|
|
218
237
|
// Use spawnSync to avoid command injection via session/command
|
|
219
|
-
const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
|
|
238
|
+
const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
|
|
239
|
+
encoding: "utf-8",
|
|
240
|
+
});
|
|
220
241
|
if (result.status !== 0) throw new Error(result.stderr || "tmux new-session failed");
|
|
221
242
|
}
|
|
222
243
|
|
|
@@ -225,7 +246,9 @@ function tmuxNewSession(session, command) {
|
|
|
225
246
|
*/
|
|
226
247
|
function tmuxCurrentSession() {
|
|
227
248
|
if (!process.env.TMUX) return null;
|
|
228
|
-
const result = spawnSync("tmux", ["display-message", "-p", "#S"], {
|
|
249
|
+
const result = spawnSync("tmux", ["display-message", "-p", "#S"], {
|
|
250
|
+
encoding: "utf-8",
|
|
251
|
+
});
|
|
229
252
|
if (result.status !== 0) return null;
|
|
230
253
|
return result.stdout.trim();
|
|
231
254
|
}
|
|
@@ -237,9 +260,13 @@ function tmuxCurrentSession() {
|
|
|
237
260
|
*/
|
|
238
261
|
function isYoloSession(session) {
|
|
239
262
|
try {
|
|
240
|
-
const result = spawnSync(
|
|
241
|
-
|
|
242
|
-
|
|
263
|
+
const result = spawnSync(
|
|
264
|
+
"tmux",
|
|
265
|
+
["display-message", "-t", session, "-p", "#{pane_start_command}"],
|
|
266
|
+
{
|
|
267
|
+
encoding: "utf-8",
|
|
268
|
+
},
|
|
269
|
+
);
|
|
243
270
|
if (result.status !== 0) return false;
|
|
244
271
|
const cmd = result.stdout.trim();
|
|
245
272
|
return cmd.includes("--dangerously-");
|
|
@@ -259,9 +286,15 @@ const POLL_MS = parseInt(process.env.AX_POLL_MS || "200", 10);
|
|
|
259
286
|
const DEFAULT_TIMEOUT_MS = parseInt(process.env.AX_TIMEOUT_MS || "120000", 10);
|
|
260
287
|
const REVIEW_TIMEOUT_MS = parseInt(process.env.AX_REVIEW_TIMEOUT_MS || "900000", 10); // 15 minutes
|
|
261
288
|
const STARTUP_TIMEOUT_MS = parseInt(process.env.AX_STARTUP_TIMEOUT_MS || "30000", 10);
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
289
|
+
const ARCHANGEL_STARTUP_TIMEOUT_MS = parseInt(
|
|
290
|
+
process.env.AX_ARCHANGEL_STARTUP_TIMEOUT_MS || "60000",
|
|
291
|
+
10,
|
|
292
|
+
);
|
|
293
|
+
const ARCHANGEL_RESPONSE_TIMEOUT_MS = parseInt(
|
|
294
|
+
process.env.AX_ARCHANGEL_RESPONSE_TIMEOUT_MS || "300000",
|
|
295
|
+
10,
|
|
296
|
+
); // 5 minutes
|
|
297
|
+
const ARCHANGEL_HEALTH_CHECK_MS = parseInt(process.env.AX_ARCHANGEL_HEALTH_CHECK_MS || "30000", 10);
|
|
265
298
|
const STABLE_MS = parseInt(process.env.AX_STABLE_MS || "1000", 10);
|
|
266
299
|
const APPROVE_DELAY_MS = parseInt(process.env.AX_APPROVE_DELAY_MS || "100", 10);
|
|
267
300
|
const MAILBOX_MAX_AGE_MS = parseInt(process.env.AX_MAILBOX_MAX_AGE_MS || "3600000", 10); // 1 hour
|
|
@@ -269,9 +302,35 @@ const CLAUDE_CONFIG_DIR = process.env.AX_CLAUDE_CONFIG_DIR || path.join(os.homed
|
|
|
269
302
|
const CODEX_CONFIG_DIR = process.env.AX_CODEX_CONFIG_DIR || path.join(os.homedir(), ".codex");
|
|
270
303
|
const TRUNCATE_USER_LEN = 500;
|
|
271
304
|
const TRUNCATE_THINKING_LEN = 300;
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
const
|
|
305
|
+
const ARCHANGEL_GIT_CONTEXT_HOURS = 4;
|
|
306
|
+
const ARCHANGEL_GIT_CONTEXT_MAX_LINES = 200;
|
|
307
|
+
const ARCHANGEL_PARENT_CONTEXT_ENTRIES = 10;
|
|
308
|
+
const ARCHANGEL_PREAMBLE = `## Guidelines
|
|
309
|
+
|
|
310
|
+
- If you have nothing to report, you MUST respond with ONLY "EMPTY_RESPONSE".
|
|
311
|
+
- Investigate before speaking. If uncertain, read more code and trace the logic until you're confident.
|
|
312
|
+
- Explain WHY something is an issue, not just that it is.
|
|
313
|
+
- Focus on your area of expertise.
|
|
314
|
+
- Calibrate to the task or plan. Don't suggest refactors during a bug fix.
|
|
315
|
+
- Be clear. Brief is fine, but never sacrifice clarity.
|
|
316
|
+
- For critical issues, request for them to be added to the todo list.
|
|
317
|
+
- Don't repeat observations you've already made unless you have more to say or better clarity.
|
|
318
|
+
- Make judgment calls - don't ask questions.`;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @param {string} session
|
|
322
|
+
* @param {(screen: string) => boolean} predicate
|
|
323
|
+
* @param {number} [timeoutMs]
|
|
324
|
+
* @returns {Promise<string>}
|
|
325
|
+
*/
|
|
326
|
+
class TimeoutError extends Error {
|
|
327
|
+
/** @param {string} [session] */
|
|
328
|
+
constructor(session) {
|
|
329
|
+
super("timeout");
|
|
330
|
+
this.name = "TimeoutError";
|
|
331
|
+
this.session = session;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
275
334
|
|
|
276
335
|
/**
|
|
277
336
|
* @param {string} session
|
|
@@ -286,7 +345,7 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
286
345
|
if (predicate(screen)) return screen;
|
|
287
346
|
await sleep(POLL_MS);
|
|
288
347
|
}
|
|
289
|
-
throw new
|
|
348
|
+
throw new TimeoutError(session);
|
|
290
349
|
}
|
|
291
350
|
|
|
292
351
|
// =============================================================================
|
|
@@ -299,7 +358,9 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
299
358
|
function findCallerPid() {
|
|
300
359
|
let pid = process.ppid;
|
|
301
360
|
while (pid > 1) {
|
|
302
|
-
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
361
|
+
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
362
|
+
encoding: "utf-8",
|
|
363
|
+
});
|
|
303
364
|
if (result.status !== 0) break;
|
|
304
365
|
const parts = result.stdout.trim().split(/\s+/);
|
|
305
366
|
const ppid = parseInt(parts[0], 10);
|
|
@@ -312,6 +373,38 @@ function findCallerPid() {
|
|
|
312
373
|
return null;
|
|
313
374
|
}
|
|
314
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Find orphaned claude/codex processes (PPID=1, reparented to init/launchd)
|
|
378
|
+
* @returns {{pid: string, command: string}[]}
|
|
379
|
+
*/
|
|
380
|
+
function findOrphanedProcesses() {
|
|
381
|
+
const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf-8" });
|
|
382
|
+
|
|
383
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const orphans = [];
|
|
388
|
+
for (const line of result.stdout.trim().split("\n")) {
|
|
389
|
+
// Parse: " PID PPID command args..."
|
|
390
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(.+)$/);
|
|
391
|
+
if (!match) continue;
|
|
392
|
+
|
|
393
|
+
const [, pid, ppid, args] = match;
|
|
394
|
+
|
|
395
|
+
// Must have PPID=1 (orphaned/reparented to init)
|
|
396
|
+
if (ppid !== "1") continue;
|
|
397
|
+
|
|
398
|
+
// Command must START with claude or codex (excludes tmux which also has PPID=1)
|
|
399
|
+
const cmd = args.split(/\s+/)[0];
|
|
400
|
+
if (cmd !== "claude" && cmd !== "codex") continue;
|
|
401
|
+
|
|
402
|
+
orphans.push({ pid, command: args.slice(0, 60) });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return orphans;
|
|
406
|
+
}
|
|
407
|
+
|
|
315
408
|
// =============================================================================
|
|
316
409
|
// Helpers - stdin
|
|
317
410
|
// =============================================================================
|
|
@@ -340,6 +433,86 @@ async function readStdin() {
|
|
|
340
433
|
}
|
|
341
434
|
|
|
342
435
|
// =============================================================================
|
|
436
|
+
// =============================================================================
|
|
437
|
+
// Helpers - CLI argument parsing
|
|
438
|
+
// =============================================================================
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Parse CLI arguments using Node.js built-in parseArgs.
|
|
442
|
+
* @param {string[]} args - Command line arguments (without node and script path)
|
|
443
|
+
* @returns {{ flags: ParsedFlags, positionals: string[] }}
|
|
444
|
+
*
|
|
445
|
+
* @typedef {Object} ParsedFlags
|
|
446
|
+
* @property {boolean} wait
|
|
447
|
+
* @property {boolean} noWait
|
|
448
|
+
* @property {boolean} yolo
|
|
449
|
+
* @property {boolean} fresh
|
|
450
|
+
* @property {boolean} reasoning
|
|
451
|
+
* @property {boolean} follow
|
|
452
|
+
* @property {boolean} all
|
|
453
|
+
* @property {boolean} orphans
|
|
454
|
+
* @property {boolean} force
|
|
455
|
+
* @property {boolean} version
|
|
456
|
+
* @property {boolean} help
|
|
457
|
+
* @property {string} [tool]
|
|
458
|
+
* @property {string} [session]
|
|
459
|
+
* @property {number} [timeout]
|
|
460
|
+
* @property {number} [tail]
|
|
461
|
+
* @property {number} [limit]
|
|
462
|
+
* @property {string} [branch]
|
|
463
|
+
*/
|
|
464
|
+
function parseCliArgs(args) {
|
|
465
|
+
const { values, positionals } = parseArgs({
|
|
466
|
+
args,
|
|
467
|
+
options: {
|
|
468
|
+
// Boolean flags
|
|
469
|
+
wait: { type: "boolean", default: false },
|
|
470
|
+
"no-wait": { type: "boolean", default: false },
|
|
471
|
+
yolo: { type: "boolean", default: false },
|
|
472
|
+
fresh: { type: "boolean", default: false },
|
|
473
|
+
reasoning: { type: "boolean", default: false },
|
|
474
|
+
follow: { type: "boolean", short: "f", default: false },
|
|
475
|
+
all: { type: "boolean", default: false },
|
|
476
|
+
orphans: { type: "boolean", default: false },
|
|
477
|
+
force: { type: "boolean", default: false },
|
|
478
|
+
version: { type: "boolean", short: "V", default: false },
|
|
479
|
+
help: { type: "boolean", short: "h", default: false },
|
|
480
|
+
// Value flags
|
|
481
|
+
tool: { type: "string" },
|
|
482
|
+
session: { type: "string" },
|
|
483
|
+
timeout: { type: "string" },
|
|
484
|
+
tail: { type: "string" },
|
|
485
|
+
limit: { type: "string" },
|
|
486
|
+
branch: { type: "string" },
|
|
487
|
+
},
|
|
488
|
+
allowPositionals: true,
|
|
489
|
+
strict: false, // Don't error on unknown flags
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
flags: {
|
|
494
|
+
wait: Boolean(values.wait),
|
|
495
|
+
noWait: Boolean(values["no-wait"]),
|
|
496
|
+
yolo: Boolean(values.yolo),
|
|
497
|
+
fresh: Boolean(values.fresh),
|
|
498
|
+
reasoning: Boolean(values.reasoning),
|
|
499
|
+
follow: Boolean(values.follow),
|
|
500
|
+
all: Boolean(values.all),
|
|
501
|
+
orphans: Boolean(values.orphans),
|
|
502
|
+
force: Boolean(values.force),
|
|
503
|
+
version: Boolean(values.version),
|
|
504
|
+
help: Boolean(values.help),
|
|
505
|
+
tool: /** @type {string | undefined} */ (values.tool),
|
|
506
|
+
session: /** @type {string | undefined} */ (values.session),
|
|
507
|
+
timeout: values.timeout !== undefined ? Number(values.timeout) : undefined,
|
|
508
|
+
tail: values.tail !== undefined ? Number(values.tail) : undefined,
|
|
509
|
+
limit: values.limit !== undefined ? Number(values.limit) : undefined,
|
|
510
|
+
branch: /** @type {string | undefined} */ (values.branch),
|
|
511
|
+
},
|
|
512
|
+
positionals,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
343
516
|
// Helpers - session tracking
|
|
344
517
|
// =============================================================================
|
|
345
518
|
|
|
@@ -354,14 +527,18 @@ function parseSessionName(session) {
|
|
|
354
527
|
const tool = match[1].toLowerCase();
|
|
355
528
|
const rest = match[2];
|
|
356
529
|
|
|
357
|
-
//
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
530
|
+
// Archangel: {tool}-archangel-{name}-{uuid}
|
|
531
|
+
const archangelMatch = rest.match(
|
|
532
|
+
/^archangel-(.+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i,
|
|
533
|
+
);
|
|
534
|
+
if (archangelMatch) {
|
|
535
|
+
return { tool, archangelName: archangelMatch[1], uuid: archangelMatch[2] };
|
|
361
536
|
}
|
|
362
537
|
|
|
363
538
|
// Partner: {tool}-partner-{uuid}
|
|
364
|
-
const partnerMatch = rest.match(
|
|
539
|
+
const partnerMatch = rest.match(
|
|
540
|
+
/^partner-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i,
|
|
541
|
+
);
|
|
365
542
|
if (partnerMatch) {
|
|
366
543
|
return { tool, uuid: partnerMatch[1] };
|
|
367
544
|
}
|
|
@@ -378,6 +555,16 @@ function generateSessionName(tool) {
|
|
|
378
555
|
return `${tool}-partner-${randomUUID()}`;
|
|
379
556
|
}
|
|
380
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Quick hash for change detection (not cryptographic).
|
|
560
|
+
* @param {string | null | undefined} str
|
|
561
|
+
* @returns {string | null}
|
|
562
|
+
*/
|
|
563
|
+
function quickHash(str) {
|
|
564
|
+
if (!str) return null;
|
|
565
|
+
return createHash("md5").update(str).digest("hex").slice(0, 8);
|
|
566
|
+
}
|
|
567
|
+
|
|
381
568
|
/**
|
|
382
569
|
* @param {string} cwd
|
|
383
570
|
* @returns {string}
|
|
@@ -394,9 +581,13 @@ function getClaudeProjectPath(cwd) {
|
|
|
394
581
|
*/
|
|
395
582
|
function getTmuxSessionCwd(sessionName) {
|
|
396
583
|
try {
|
|
397
|
-
const result = spawnSync(
|
|
398
|
-
|
|
399
|
-
|
|
584
|
+
const result = spawnSync(
|
|
585
|
+
"tmux",
|
|
586
|
+
["display-message", "-t", sessionName, "-p", "#{pane_current_path}"],
|
|
587
|
+
{
|
|
588
|
+
encoding: "utf-8",
|
|
589
|
+
},
|
|
590
|
+
);
|
|
400
591
|
if (result.status === 0) return result.stdout.trim();
|
|
401
592
|
} catch (err) {
|
|
402
593
|
debugError("getTmuxSessionCwd", err);
|
|
@@ -420,7 +611,9 @@ function findClaudeLogPath(sessionId, sessionName) {
|
|
|
420
611
|
if (existsSync(indexPath)) {
|
|
421
612
|
try {
|
|
422
613
|
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
423
|
-
const entry = index.entries?.find(
|
|
614
|
+
const entry = index.entries?.find(
|
|
615
|
+
/** @param {{sessionId: string, fullPath?: string}} e */ (e) => e.sessionId === sessionId,
|
|
616
|
+
);
|
|
424
617
|
if (entry?.fullPath) return entry.fullPath;
|
|
425
618
|
} catch (err) {
|
|
426
619
|
debugError("findClaudeLogPath", err);
|
|
@@ -442,9 +635,13 @@ function findCodexLogPath(sessionName) {
|
|
|
442
635
|
// For Codex, we need to match by timing since we can't control the session ID
|
|
443
636
|
// Get tmux session creation time
|
|
444
637
|
try {
|
|
445
|
-
const result = spawnSync(
|
|
446
|
-
|
|
447
|
-
|
|
638
|
+
const result = spawnSync(
|
|
639
|
+
"tmux",
|
|
640
|
+
["display-message", "-t", sessionName, "-p", "#{session_created}"],
|
|
641
|
+
{
|
|
642
|
+
encoding: "utf-8",
|
|
643
|
+
},
|
|
644
|
+
);
|
|
448
645
|
if (result.status !== 0) return null;
|
|
449
646
|
const createdTs = parseInt(result.stdout.trim(), 10) * 1000; // tmux gives seconds, we need ms
|
|
450
647
|
if (isNaN(createdTs)) return null;
|
|
@@ -478,7 +675,11 @@ function findCodexLogPath(sessionName) {
|
|
|
478
675
|
// Log file should be created shortly after session start
|
|
479
676
|
// Allow small negative diff (-2s) for clock skew, up to 60s for slow starts
|
|
480
677
|
if (diff >= -2000 && diff < 60000) {
|
|
481
|
-
candidates.push({
|
|
678
|
+
candidates.push({
|
|
679
|
+
file,
|
|
680
|
+
diff: Math.abs(diff),
|
|
681
|
+
path: path.join(dayDir, file),
|
|
682
|
+
});
|
|
482
683
|
}
|
|
483
684
|
}
|
|
484
685
|
|
|
@@ -491,6 +692,92 @@ function findCodexLogPath(sessionName) {
|
|
|
491
692
|
}
|
|
492
693
|
}
|
|
493
694
|
|
|
695
|
+
/**
|
|
696
|
+
* @typedef {Object} SessionMeta
|
|
697
|
+
* @property {string | null} slug - Plan identifier (if plan is active)
|
|
698
|
+
* @property {Array<{content: string, status: string, id?: string}> | null} todos - Current todos
|
|
699
|
+
* @property {string | null} permissionMode - "default", "acceptEdits", "plan"
|
|
700
|
+
* @property {string | null} gitBranch - Current git branch
|
|
701
|
+
* @property {string | null} cwd - Working directory
|
|
702
|
+
*/
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Get metadata from a Claude session's JSONL file.
|
|
706
|
+
* Returns null for Codex sessions (different format, no equivalent metadata).
|
|
707
|
+
* @param {string} sessionName - The tmux session name
|
|
708
|
+
* @returns {SessionMeta | null}
|
|
709
|
+
*/
|
|
710
|
+
function getSessionMeta(sessionName) {
|
|
711
|
+
const parsed = parseSessionName(sessionName);
|
|
712
|
+
if (!parsed) return null;
|
|
713
|
+
|
|
714
|
+
// Only Claude sessions have this metadata
|
|
715
|
+
if (parsed.tool !== "claude") return null;
|
|
716
|
+
if (!parsed.uuid) return null;
|
|
717
|
+
|
|
718
|
+
const logPath = findClaudeLogPath(parsed.uuid, sessionName);
|
|
719
|
+
if (!logPath || !existsSync(logPath)) return null;
|
|
720
|
+
|
|
721
|
+
try {
|
|
722
|
+
const content = readFileSync(logPath, "utf-8");
|
|
723
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
724
|
+
|
|
725
|
+
// Read from end to find most recent entry with metadata
|
|
726
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
727
|
+
try {
|
|
728
|
+
const entry = JSON.parse(lines[i]);
|
|
729
|
+
// User entries typically have the metadata fields
|
|
730
|
+
if (entry.type === "user" || entry.slug || entry.gitBranch) {
|
|
731
|
+
return {
|
|
732
|
+
slug: entry.slug || null,
|
|
733
|
+
todos: entry.todos || null,
|
|
734
|
+
permissionMode: entry.permissionMode || null,
|
|
735
|
+
gitBranch: entry.gitBranch || null,
|
|
736
|
+
cwd: entry.cwd || null,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
} catch {
|
|
740
|
+
// Skip malformed lines
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
744
|
+
} catch (err) {
|
|
745
|
+
debugError("getSessionMeta", err);
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Read a plan file by its slug.
|
|
752
|
+
* @param {string} slug - The plan slug (e.g., "curious-roaming-pascal")
|
|
753
|
+
* @returns {string | null} The plan content or null if not found
|
|
754
|
+
*/
|
|
755
|
+
function readPlanFile(slug) {
|
|
756
|
+
const planPath = path.join(CLAUDE_CONFIG_DIR, "plans", `${slug}.md`);
|
|
757
|
+
try {
|
|
758
|
+
if (existsSync(planPath)) {
|
|
759
|
+
return readFileSync(planPath, "utf-8");
|
|
760
|
+
}
|
|
761
|
+
} catch (err) {
|
|
762
|
+
debugError("readPlanFile", err);
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Format todos for display in a prompt.
|
|
769
|
+
* @param {Array<{content: string, status: string, id?: string}>} todos
|
|
770
|
+
* @returns {string}
|
|
771
|
+
*/
|
|
772
|
+
function formatTodos(todos) {
|
|
773
|
+
if (!todos || todos.length === 0) return "";
|
|
774
|
+
return todos
|
|
775
|
+
.map((t) => {
|
|
776
|
+
const status = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[>]" : "[ ]";
|
|
777
|
+
return `${status} ${t.content || "(no content)"}`;
|
|
778
|
+
})
|
|
779
|
+
.join("\n");
|
|
780
|
+
}
|
|
494
781
|
|
|
495
782
|
/**
|
|
496
783
|
* Extract assistant text responses from a JSONL log file.
|
|
@@ -537,6 +824,130 @@ function getAssistantText(logPath, index = 0) {
|
|
|
537
824
|
}
|
|
538
825
|
}
|
|
539
826
|
|
|
827
|
+
/**
|
|
828
|
+
* Read new complete JSON lines from a log file since the given offset.
|
|
829
|
+
* @param {string | null} logPath
|
|
830
|
+
* @param {number} fromOffset
|
|
831
|
+
* @returns {{ entries: object[], newOffset: number }}
|
|
832
|
+
*/
|
|
833
|
+
function tailJsonl(logPath, fromOffset) {
|
|
834
|
+
if (!logPath || !existsSync(logPath)) {
|
|
835
|
+
return { entries: [], newOffset: fromOffset };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const stats = statSync(logPath);
|
|
839
|
+
if (stats.size <= fromOffset) {
|
|
840
|
+
return { entries: [], newOffset: fromOffset };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const fd = openSync(logPath, "r");
|
|
844
|
+
const buffer = Buffer.alloc(stats.size - fromOffset);
|
|
845
|
+
readSync(fd, buffer, 0, buffer.length, fromOffset);
|
|
846
|
+
closeSync(fd);
|
|
847
|
+
|
|
848
|
+
const text = buffer.toString("utf-8");
|
|
849
|
+
const lines = text.split("\n");
|
|
850
|
+
|
|
851
|
+
// Last line may be incomplete - don't parse it yet
|
|
852
|
+
const complete = lines.slice(0, -1).filter(Boolean);
|
|
853
|
+
const incomplete = lines[lines.length - 1];
|
|
854
|
+
|
|
855
|
+
const entries = [];
|
|
856
|
+
for (const line of complete) {
|
|
857
|
+
try {
|
|
858
|
+
entries.push(JSON.parse(line));
|
|
859
|
+
} catch {
|
|
860
|
+
// Skip malformed lines
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Offset advances by complete lines only
|
|
865
|
+
const newOffset = fromOffset + text.length - incomplete.length;
|
|
866
|
+
return { entries, newOffset };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* @typedef {{command?: string, file_path?: string, path?: string, pattern?: string}} ToolInput
|
|
871
|
+
*/
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Format a JSONL entry for streaming display.
|
|
875
|
+
* @param {{type?: string, message?: {content?: Array<{type?: string, text?: string, name?: string, input?: ToolInput, tool?: string, arguments?: ToolInput}>}}} entry
|
|
876
|
+
* @returns {string | null}
|
|
877
|
+
*/
|
|
878
|
+
function formatEntry(entry) {
|
|
879
|
+
// Skip tool_result entries (they can be very verbose)
|
|
880
|
+
if (entry.type === "tool_result") return null;
|
|
881
|
+
|
|
882
|
+
// Only process assistant entries
|
|
883
|
+
if (entry.type !== "assistant") return null;
|
|
884
|
+
|
|
885
|
+
const parts = entry.message?.content || [];
|
|
886
|
+
const output = [];
|
|
887
|
+
|
|
888
|
+
for (const part of parts) {
|
|
889
|
+
if (part.type === "text" && part.text) {
|
|
890
|
+
output.push(part.text);
|
|
891
|
+
} else if (part.type === "tool_use" || part.type === "tool_call") {
|
|
892
|
+
const name = part.name || part.tool || "tool";
|
|
893
|
+
const input = part.input || part.arguments || {};
|
|
894
|
+
let summary;
|
|
895
|
+
if (name === "Bash" && input.command) {
|
|
896
|
+
summary = input.command.slice(0, 50);
|
|
897
|
+
} else {
|
|
898
|
+
const target = input.file_path || input.path || input.pattern || "";
|
|
899
|
+
summary = target.split("/").pop() || target.slice(0, 30);
|
|
900
|
+
}
|
|
901
|
+
output.push(`> ${name}(${summary})`);
|
|
902
|
+
}
|
|
903
|
+
// Skip thinking blocks - internal reasoning
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return output.length > 0 ? output.join("\n") : null;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Extract pending tool from confirmation screen.
|
|
911
|
+
* @param {string} screen
|
|
912
|
+
* @returns {string | null}
|
|
913
|
+
*/
|
|
914
|
+
function extractPendingToolFromScreen(screen) {
|
|
915
|
+
const lines = screen.split("\n");
|
|
916
|
+
|
|
917
|
+
// Check recent lines for tool confirmation patterns
|
|
918
|
+
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 15); i--) {
|
|
919
|
+
const line = lines[i];
|
|
920
|
+
// Match tool confirmation patterns like "Bash: command" or "Write: /path/file"
|
|
921
|
+
const match = line.match(
|
|
922
|
+
/^\s*(Bash|Write|Edit|Read|Glob|Grep|Task|WebFetch|WebSearch|NotebookEdit|Skill|TodoWrite|TodoRead):\s*(.{1,40})/,
|
|
923
|
+
);
|
|
924
|
+
if (match) {
|
|
925
|
+
return `${match[1]}: ${match[2].trim()}`;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Format confirmation output with helpful commands
|
|
934
|
+
* @param {string} screen
|
|
935
|
+
* @param {Agent} _agent
|
|
936
|
+
* @returns {string}
|
|
937
|
+
*/
|
|
938
|
+
function formatConfirmationOutput(screen, _agent) {
|
|
939
|
+
const pendingTool = extractPendingToolFromScreen(screen);
|
|
940
|
+
const cli = path.basename(process.argv[1], ".js");
|
|
941
|
+
|
|
942
|
+
let output = pendingTool || "Confirmation required";
|
|
943
|
+
output += "\n\ne.g.";
|
|
944
|
+
output += `\n ${cli} approve # for y/n prompts`;
|
|
945
|
+
output += `\n ${cli} reject`;
|
|
946
|
+
output += `\n ${cli} select N # for numbered menus`;
|
|
947
|
+
|
|
948
|
+
return output;
|
|
949
|
+
}
|
|
950
|
+
|
|
540
951
|
/**
|
|
541
952
|
* @returns {string[]}
|
|
542
953
|
*/
|
|
@@ -562,15 +973,15 @@ function resolveSessionName(partial) {
|
|
|
562
973
|
// Exact match
|
|
563
974
|
if (agentSessions.includes(partial)) return partial;
|
|
564
975
|
|
|
565
|
-
//
|
|
566
|
-
const
|
|
976
|
+
// Archangel name match (e.g., "reviewer" matches "claude-archangel-reviewer-uuid")
|
|
977
|
+
const archangelMatches = agentSessions.filter((s) => {
|
|
567
978
|
const parsed = parseSessionName(s);
|
|
568
|
-
return parsed?.
|
|
979
|
+
return parsed?.archangelName === partial;
|
|
569
980
|
});
|
|
570
|
-
if (
|
|
571
|
-
if (
|
|
572
|
-
console.log("ERROR: ambiguous
|
|
573
|
-
for (const m of
|
|
981
|
+
if (archangelMatches.length === 1) return archangelMatches[0];
|
|
982
|
+
if (archangelMatches.length > 1) {
|
|
983
|
+
console.log("ERROR: ambiguous archangel name. Matches:");
|
|
984
|
+
for (const m of archangelMatches) console.log(` ${m}`);
|
|
574
985
|
process.exit(1);
|
|
575
986
|
}
|
|
576
987
|
|
|
@@ -599,25 +1010,25 @@ function resolveSessionName(partial) {
|
|
|
599
1010
|
}
|
|
600
1011
|
|
|
601
1012
|
// =============================================================================
|
|
602
|
-
// Helpers -
|
|
1013
|
+
// Helpers - archangels
|
|
603
1014
|
// =============================================================================
|
|
604
1015
|
|
|
605
1016
|
/**
|
|
606
|
-
* @returns {
|
|
1017
|
+
* @returns {ArchangelConfig[]}
|
|
607
1018
|
*/
|
|
608
1019
|
function loadAgentConfigs() {
|
|
609
1020
|
const agentsDir = AGENTS_DIR;
|
|
610
1021
|
if (!existsSync(agentsDir)) return [];
|
|
611
1022
|
|
|
612
1023
|
const files = readdirSync(agentsDir).filter((f) => f.endsWith(".md"));
|
|
613
|
-
/** @type {
|
|
1024
|
+
/** @type {ArchangelConfig[]} */
|
|
614
1025
|
const configs = [];
|
|
615
1026
|
|
|
616
1027
|
for (const file of files) {
|
|
617
1028
|
try {
|
|
618
1029
|
const content = readFileSync(path.join(agentsDir, file), "utf-8");
|
|
619
1030
|
const config = parseAgentConfig(file, content);
|
|
620
|
-
if (config &&
|
|
1031
|
+
if (config && "error" in config) {
|
|
621
1032
|
console.error(`ERROR: ${file}: ${config.error}`);
|
|
622
1033
|
continue;
|
|
623
1034
|
}
|
|
@@ -633,7 +1044,7 @@ function loadAgentConfigs() {
|
|
|
633
1044
|
/**
|
|
634
1045
|
* @param {string} filename
|
|
635
1046
|
* @param {string} content
|
|
636
|
-
* @returns {
|
|
1047
|
+
* @returns {ArchangelConfig | {error: string} | null}
|
|
637
1048
|
*/
|
|
638
1049
|
function parseAgentConfig(filename, content) {
|
|
639
1050
|
const name = filename.replace(/\.md$/, "");
|
|
@@ -648,7 +1059,9 @@ function parseAgentConfig(filename, content) {
|
|
|
648
1059
|
return { error: `Missing frontmatter. File must start with '---'` };
|
|
649
1060
|
}
|
|
650
1061
|
if (!normalized.includes("\n---\n")) {
|
|
651
|
-
return {
|
|
1062
|
+
return {
|
|
1063
|
+
error: `Frontmatter not closed. Add '---' on its own line after the YAML block`,
|
|
1064
|
+
};
|
|
652
1065
|
}
|
|
653
1066
|
return { error: `Invalid frontmatter format` };
|
|
654
1067
|
}
|
|
@@ -669,9 +1082,13 @@ function parseAgentConfig(filename, content) {
|
|
|
669
1082
|
const fieldName = line.trim().match(/^(\w+):/)?.[1];
|
|
670
1083
|
if (fieldName && !knownFields.includes(fieldName)) {
|
|
671
1084
|
// Suggest closest match
|
|
672
|
-
const suggestions = knownFields.filter(
|
|
1085
|
+
const suggestions = knownFields.filter(
|
|
1086
|
+
(f) => f[0] === fieldName[0] || fieldName.includes(f.slice(0, 3)),
|
|
1087
|
+
);
|
|
673
1088
|
const hint = suggestions.length > 0 ? ` Did you mean '${suggestions[0]}'?` : "";
|
|
674
|
-
return {
|
|
1089
|
+
return {
|
|
1090
|
+
error: `Unknown field '${fieldName}'.${hint} Valid fields: ${knownFields.join(", ")}`,
|
|
1091
|
+
};
|
|
675
1092
|
}
|
|
676
1093
|
}
|
|
677
1094
|
|
|
@@ -689,7 +1106,9 @@ function parseAgentConfig(filename, content) {
|
|
|
689
1106
|
const rawValue = intervalMatch[1].trim();
|
|
690
1107
|
const parsed = parseInt(rawValue, 10);
|
|
691
1108
|
if (isNaN(parsed)) {
|
|
692
|
-
return {
|
|
1109
|
+
return {
|
|
1110
|
+
error: `Invalid interval '${rawValue}'. Must be a number (seconds)`,
|
|
1111
|
+
};
|
|
693
1112
|
}
|
|
694
1113
|
interval = Math.max(10, Math.min(3600, parsed)); // Clamp to 10s - 1hr
|
|
695
1114
|
}
|
|
@@ -701,16 +1120,22 @@ function parseAgentConfig(filename, content) {
|
|
|
701
1120
|
const rawWatch = watchLine[1].trim();
|
|
702
1121
|
// Must be array format
|
|
703
1122
|
if (!rawWatch.startsWith("[") || !rawWatch.endsWith("]")) {
|
|
704
|
-
return {
|
|
1123
|
+
return {
|
|
1124
|
+
error: `Invalid watch format. Must be an array: watch: ["src/**/*.ts"]`,
|
|
1125
|
+
};
|
|
705
1126
|
}
|
|
706
1127
|
const inner = rawWatch.slice(1, -1).trim();
|
|
707
1128
|
if (!inner) {
|
|
708
|
-
return {
|
|
1129
|
+
return {
|
|
1130
|
+
error: `Empty watch array. Add at least one pattern: watch: ["**/*"]`,
|
|
1131
|
+
};
|
|
709
1132
|
}
|
|
710
1133
|
watchPatterns = inner.split(",").map((p) => p.trim().replace(/^["']|["']$/g, ""));
|
|
711
1134
|
// Validate patterns aren't empty
|
|
712
1135
|
if (watchPatterns.some((p) => !p)) {
|
|
713
|
-
return {
|
|
1136
|
+
return {
|
|
1137
|
+
error: `Invalid watch pattern. Check for trailing commas or empty values`,
|
|
1138
|
+
};
|
|
714
1139
|
}
|
|
715
1140
|
}
|
|
716
1141
|
|
|
@@ -718,11 +1143,11 @@ function parseAgentConfig(filename, content) {
|
|
|
718
1143
|
}
|
|
719
1144
|
|
|
720
1145
|
/**
|
|
721
|
-
* @param {
|
|
1146
|
+
* @param {ArchangelConfig} config
|
|
722
1147
|
* @returns {string}
|
|
723
1148
|
*/
|
|
724
|
-
function
|
|
725
|
-
return `${config.tool}-
|
|
1149
|
+
function getArchangelSessionPattern(config) {
|
|
1150
|
+
return `${config.tool}-archangel-${config.name}`;
|
|
726
1151
|
}
|
|
727
1152
|
|
|
728
1153
|
// =============================================================================
|
|
@@ -826,7 +1251,9 @@ function gcMailbox(maxAgeHours = 24) {
|
|
|
826
1251
|
/** @returns {string} */
|
|
827
1252
|
function getCurrentBranch() {
|
|
828
1253
|
try {
|
|
829
|
-
return execSync("git branch --show-current 2>/dev/null", {
|
|
1254
|
+
return execSync("git branch --show-current 2>/dev/null", {
|
|
1255
|
+
encoding: "utf-8",
|
|
1256
|
+
}).trim();
|
|
830
1257
|
} catch {
|
|
831
1258
|
return "unknown";
|
|
832
1259
|
}
|
|
@@ -835,7 +1262,9 @@ function getCurrentBranch() {
|
|
|
835
1262
|
/** @returns {string} */
|
|
836
1263
|
function getCurrentCommit() {
|
|
837
1264
|
try {
|
|
838
|
-
return execSync("git rev-parse --short HEAD 2>/dev/null", {
|
|
1265
|
+
return execSync("git rev-parse --short HEAD 2>/dev/null", {
|
|
1266
|
+
encoding: "utf-8",
|
|
1267
|
+
}).trim();
|
|
839
1268
|
} catch {
|
|
840
1269
|
return "unknown";
|
|
841
1270
|
}
|
|
@@ -859,7 +1288,9 @@ function getMainBranch() {
|
|
|
859
1288
|
/** @returns {string} */
|
|
860
1289
|
function getStagedDiff() {
|
|
861
1290
|
try {
|
|
862
|
-
return execSync("git diff --cached 2>/dev/null", {
|
|
1291
|
+
return execSync("git diff --cached 2>/dev/null", {
|
|
1292
|
+
encoding: "utf-8",
|
|
1293
|
+
}).trim();
|
|
863
1294
|
} catch {
|
|
864
1295
|
return "";
|
|
865
1296
|
}
|
|
@@ -884,20 +1315,18 @@ function getRecentCommitsDiff(hoursAgo = 4) {
|
|
|
884
1315
|
const since = `--since="${hoursAgo} hours ago"`;
|
|
885
1316
|
|
|
886
1317
|
// Get list of commits in range
|
|
887
|
-
const commits = execSync(
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
).trim();
|
|
1318
|
+
const commits = execSync(`git log ${mainBranch}..HEAD ${since} --oneline 2>/dev/null`, {
|
|
1319
|
+
encoding: "utf-8",
|
|
1320
|
+
}).trim();
|
|
891
1321
|
|
|
892
1322
|
if (!commits) return "";
|
|
893
1323
|
|
|
894
1324
|
// Get diff for those commits
|
|
895
1325
|
const firstCommit = commits.split("\n").filter(Boolean).pop()?.split(" ")[0];
|
|
896
1326
|
if (!firstCommit) return "";
|
|
897
|
-
return execSync(
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
).trim();
|
|
1327
|
+
return execSync(`git diff ${firstCommit}^..HEAD 2>/dev/null`, {
|
|
1328
|
+
encoding: "utf-8",
|
|
1329
|
+
}).trim();
|
|
901
1330
|
} catch {
|
|
902
1331
|
return "";
|
|
903
1332
|
}
|
|
@@ -912,7 +1341,10 @@ function truncateDiff(diff, maxLines = 200) {
|
|
|
912
1341
|
if (!diff) return "";
|
|
913
1342
|
const lines = diff.split("\n");
|
|
914
1343
|
if (lines.length <= maxLines) return diff;
|
|
915
|
-
return
|
|
1344
|
+
return (
|
|
1345
|
+
lines.slice(0, maxLines).join("\n") +
|
|
1346
|
+
`\n\n... (truncated, ${lines.length - maxLines} more lines)`
|
|
1347
|
+
);
|
|
916
1348
|
}
|
|
917
1349
|
|
|
918
1350
|
/**
|
|
@@ -945,9 +1377,9 @@ function buildGitContext(hoursAgo = 4, maxLinesPerSection = 200) {
|
|
|
945
1377
|
// Helpers - parent session context
|
|
946
1378
|
// =============================================================================
|
|
947
1379
|
|
|
948
|
-
// Environment variables used to pass parent session info to
|
|
949
|
-
const
|
|
950
|
-
const
|
|
1380
|
+
// Environment variables used to pass parent session info to archangels
|
|
1381
|
+
const AX_ARCHANGEL_PARENT_SESSION_ENV = "AX_ARCHANGEL_PARENT_SESSION";
|
|
1382
|
+
const AX_ARCHANGEL_PARENT_UUID_ENV = "AX_ARCHANGEL_PARENT_UUID";
|
|
951
1383
|
|
|
952
1384
|
/**
|
|
953
1385
|
* @returns {ParentSession | null}
|
|
@@ -957,7 +1389,7 @@ function findCurrentClaudeSession() {
|
|
|
957
1389
|
const current = tmuxCurrentSession();
|
|
958
1390
|
if (current) {
|
|
959
1391
|
const parsed = parseSessionName(current);
|
|
960
|
-
if (parsed?.tool === "claude" && !parsed.
|
|
1392
|
+
if (parsed?.tool === "claude" && !parsed.archangelName && parsed.uuid) {
|
|
961
1393
|
return { session: current, uuid: parsed.uuid };
|
|
962
1394
|
}
|
|
963
1395
|
}
|
|
@@ -974,7 +1406,7 @@ function findCurrentClaudeSession() {
|
|
|
974
1406
|
for (const session of sessions) {
|
|
975
1407
|
const parsed = parseSessionName(session);
|
|
976
1408
|
if (!parsed || parsed.tool !== "claude") continue;
|
|
977
|
-
if (parsed.
|
|
1409
|
+
if (parsed.archangelName) continue;
|
|
978
1410
|
if (!parsed.uuid) continue;
|
|
979
1411
|
|
|
980
1412
|
const sessionCwd = getTmuxSessionCwd(session);
|
|
@@ -997,18 +1429,23 @@ function findCurrentClaudeSession() {
|
|
|
997
1429
|
const claudeProjectDir = path.join(CLAUDE_CONFIG_DIR, "projects", projectPath);
|
|
998
1430
|
if (existsSync(claudeProjectDir)) {
|
|
999
1431
|
try {
|
|
1000
|
-
const files = readdirSync(claudeProjectDir).filter(f => f.endsWith(".jsonl"));
|
|
1432
|
+
const files = readdirSync(claudeProjectDir).filter((f) => f.endsWith(".jsonl"));
|
|
1001
1433
|
for (const file of files) {
|
|
1002
1434
|
const uuid = file.replace(".jsonl", "");
|
|
1003
1435
|
// Skip if we already have this from tmux sessions
|
|
1004
|
-
if (candidates.some(c => c.uuid === uuid)) continue;
|
|
1436
|
+
if (candidates.some((c) => c.uuid === uuid)) continue;
|
|
1005
1437
|
|
|
1006
1438
|
const logPath = path.join(claudeProjectDir, file);
|
|
1007
1439
|
try {
|
|
1008
1440
|
const stat = statSync(logPath);
|
|
1009
1441
|
// Only consider logs modified in the last hour (active sessions)
|
|
1010
1442
|
if (Date.now() - stat.mtimeMs < MAILBOX_MAX_AGE_MS) {
|
|
1011
|
-
candidates.push({
|
|
1443
|
+
candidates.push({
|
|
1444
|
+
session: null,
|
|
1445
|
+
uuid,
|
|
1446
|
+
mtime: stat.mtimeMs,
|
|
1447
|
+
logPath,
|
|
1448
|
+
});
|
|
1012
1449
|
}
|
|
1013
1450
|
} catch (err) {
|
|
1014
1451
|
debugError("findCurrentClaudeSession:logStat", err);
|
|
@@ -1030,15 +1467,15 @@ function findCurrentClaudeSession() {
|
|
|
1030
1467
|
* @returns {ParentSession | null}
|
|
1031
1468
|
*/
|
|
1032
1469
|
function findParentSession() {
|
|
1033
|
-
// First check if parent session was passed via environment (for
|
|
1034
|
-
const envUuid = process.env[
|
|
1470
|
+
// First check if parent session was passed via environment (for archangels)
|
|
1471
|
+
const envUuid = process.env[AX_ARCHANGEL_PARENT_UUID_ENV];
|
|
1035
1472
|
if (envUuid) {
|
|
1036
1473
|
// Session name is optional (may be null for non-tmux sessions)
|
|
1037
|
-
const envSession = process.env[
|
|
1474
|
+
const envSession = process.env[AX_ARCHANGEL_PARENT_SESSION_ENV] || null;
|
|
1038
1475
|
return { session: envSession, uuid: envUuid };
|
|
1039
1476
|
}
|
|
1040
1477
|
|
|
1041
|
-
// Fallback to detecting current session (shouldn't be needed for
|
|
1478
|
+
// Fallback to detecting current session (shouldn't be needed for archangels)
|
|
1042
1479
|
return findCurrentClaudeSession();
|
|
1043
1480
|
}
|
|
1044
1481
|
|
|
@@ -1080,7 +1517,9 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1080
1517
|
if (typeof c === "string" && c.length > 10) {
|
|
1081
1518
|
entries.push({ type: "user", text: c });
|
|
1082
1519
|
} else if (Array.isArray(c)) {
|
|
1083
|
-
const text = c.find(
|
|
1520
|
+
const text = c.find(
|
|
1521
|
+
/** @param {{type: string, text?: string}} x */ (x) => x.type === "text",
|
|
1522
|
+
)?.text;
|
|
1084
1523
|
if (text && text.length > 10) {
|
|
1085
1524
|
entries.push({ type: "user", text });
|
|
1086
1525
|
}
|
|
@@ -1088,7 +1527,10 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1088
1527
|
} else if (entry.type === "assistant") {
|
|
1089
1528
|
/** @type {{type: string, text?: string}[]} */
|
|
1090
1529
|
const parts = entry.message?.content || [];
|
|
1091
|
-
const text = parts
|
|
1530
|
+
const text = parts
|
|
1531
|
+
.filter((p) => p.type === "text")
|
|
1532
|
+
.map((p) => p.text || "")
|
|
1533
|
+
.join("\n");
|
|
1092
1534
|
// Only include assistant responses with meaningful text
|
|
1093
1535
|
if (text && text.length > 20) {
|
|
1094
1536
|
entries.push({ type: "assistant", text });
|
|
@@ -1100,7 +1542,7 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1100
1542
|
}
|
|
1101
1543
|
|
|
1102
1544
|
// Format recent conversation
|
|
1103
|
-
const formatted = entries.slice(-maxEntries).map(e => {
|
|
1545
|
+
const formatted = entries.slice(-maxEntries).map((e) => {
|
|
1104
1546
|
const preview = e.text.slice(0, 500).replace(/\n/g, " ");
|
|
1105
1547
|
return `**${e.type === "user" ? "User" : "Assistant"}**: ${preview}`;
|
|
1106
1548
|
});
|
|
@@ -1143,10 +1585,16 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1143
1585
|
|
|
1144
1586
|
// Parse all entries
|
|
1145
1587
|
/** @type {any[]} */
|
|
1146
|
-
const entries = lines
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1588
|
+
const entries = lines
|
|
1589
|
+
.map((line, idx) => {
|
|
1590
|
+
try {
|
|
1591
|
+
return { idx, ...JSON.parse(line) };
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
debugError("extractFileEditContext:parse", err);
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
})
|
|
1597
|
+
.filter(Boolean);
|
|
1150
1598
|
|
|
1151
1599
|
// Find Write/Edit tool calls for this file (scan backwards, want most recent)
|
|
1152
1600
|
/** @type {any} */
|
|
@@ -1159,9 +1607,10 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1159
1607
|
|
|
1160
1608
|
/** @type {any[]} */
|
|
1161
1609
|
const msgContent = entry.message?.content || [];
|
|
1162
|
-
const toolCalls = msgContent.filter(
|
|
1163
|
-
(
|
|
1164
|
-
|
|
1610
|
+
const toolCalls = msgContent.filter(
|
|
1611
|
+
(/** @type {any} */ c) =>
|
|
1612
|
+
(c.type === "tool_use" || c.type === "tool_call") &&
|
|
1613
|
+
(c.name === "Write" || c.name === "Edit"),
|
|
1165
1614
|
);
|
|
1166
1615
|
|
|
1167
1616
|
for (const tc of toolCalls) {
|
|
@@ -1212,8 +1661,9 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1212
1661
|
|
|
1213
1662
|
/** @type {any[]} */
|
|
1214
1663
|
const msgContent = entry.message?.content || [];
|
|
1215
|
-
const readCalls = msgContent.filter(
|
|
1216
|
-
(
|
|
1664
|
+
const readCalls = msgContent.filter(
|
|
1665
|
+
(/** @type {any} */ c) =>
|
|
1666
|
+
(c.type === "tool_use" || c.type === "tool_call") && c.name === "Read",
|
|
1217
1667
|
);
|
|
1218
1668
|
|
|
1219
1669
|
for (const rc of readCalls) {
|
|
@@ -1228,9 +1678,10 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1228
1678
|
if (entry.type !== "assistant") continue;
|
|
1229
1679
|
/** @type {any[]} */
|
|
1230
1680
|
const msgContent = entry.message?.content || [];
|
|
1231
|
-
const edits = msgContent.filter(
|
|
1232
|
-
(
|
|
1233
|
-
|
|
1681
|
+
const edits = msgContent.filter(
|
|
1682
|
+
(/** @type {any} */ c) =>
|
|
1683
|
+
(c.type === "tool_use" || c.type === "tool_call") &&
|
|
1684
|
+
(c.name === "Write" || c.name === "Edit"),
|
|
1234
1685
|
);
|
|
1235
1686
|
for (const e of edits) {
|
|
1236
1687
|
const input = e.input || e.arguments || {};
|
|
@@ -1245,11 +1696,11 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1245
1696
|
toolCall: {
|
|
1246
1697
|
name: editEntry.toolCall.name,
|
|
1247
1698
|
input: editEntry.toolCall.input || editEntry.toolCall.arguments,
|
|
1248
|
-
id: editEntry.toolCall.id
|
|
1699
|
+
id: editEntry.toolCall.id,
|
|
1249
1700
|
},
|
|
1250
1701
|
subsequentErrors,
|
|
1251
1702
|
readsBefore: [...new Set(readsBefore)].slice(0, 10),
|
|
1252
|
-
editSequence
|
|
1703
|
+
editSequence,
|
|
1253
1704
|
};
|
|
1254
1705
|
}
|
|
1255
1706
|
|
|
@@ -1343,10 +1794,11 @@ function watchForChanges(patterns, callback) {
|
|
|
1343
1794
|
}
|
|
1344
1795
|
}
|
|
1345
1796
|
|
|
1346
|
-
return () => {
|
|
1797
|
+
return () => {
|
|
1798
|
+
for (const w of watchers) w.close();
|
|
1799
|
+
};
|
|
1347
1800
|
}
|
|
1348
1801
|
|
|
1349
|
-
|
|
1350
1802
|
// =============================================================================
|
|
1351
1803
|
// State
|
|
1352
1804
|
// =============================================================================
|
|
@@ -1368,7 +1820,7 @@ const State = {
|
|
|
1368
1820
|
* @param {string} config.promptSymbol - Symbol indicating ready state
|
|
1369
1821
|
* @param {string[]} [config.spinners] - Spinner characters indicating thinking
|
|
1370
1822
|
* @param {RegExp} [config.rateLimitPattern] - Pattern for rate limit detection
|
|
1371
|
-
* @param {string[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
1823
|
+
* @param {(string | RegExp | ((lines: string) => boolean))[]} [config.thinkingPatterns] - Text patterns indicating thinking
|
|
1372
1824
|
* @param {(string | ((lines: string) => boolean))[]} [config.confirmPatterns] - Patterns for confirmation dialogs
|
|
1373
1825
|
* @param {{screen: string[], lastLines: string[]} | null} [config.updatePromptPatterns] - Patterns for update prompts
|
|
1374
1826
|
* @returns {string} The detected state
|
|
@@ -1381,31 +1833,12 @@ function detectState(screen, config) {
|
|
|
1381
1833
|
// Larger range for confirmation detection (catches dialogs that scrolled slightly)
|
|
1382
1834
|
const recentLines = lines.slice(-15).join("\n");
|
|
1383
1835
|
|
|
1384
|
-
// Rate limited - check full screen
|
|
1385
|
-
if (config.rateLimitPattern && config.rateLimitPattern.test(
|
|
1836
|
+
// Rate limited - check recent lines (not full screen to avoid matching historical output)
|
|
1837
|
+
if (config.rateLimitPattern && config.rateLimitPattern.test(recentLines)) {
|
|
1386
1838
|
return State.RATE_LIMITED;
|
|
1387
1839
|
}
|
|
1388
1840
|
|
|
1389
|
-
//
|
|
1390
|
-
const spinners = config.spinners || [];
|
|
1391
|
-
if (spinners.some((s) => screen.includes(s))) {
|
|
1392
|
-
return State.THINKING;
|
|
1393
|
-
}
|
|
1394
|
-
// Thinking - text patterns (last lines)
|
|
1395
|
-
const thinkingPatterns = config.thinkingPatterns || [];
|
|
1396
|
-
if (thinkingPatterns.some((p) => lastLines.includes(p))) {
|
|
1397
|
-
return State.THINKING;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// Update prompt
|
|
1401
|
-
if (config.updatePromptPatterns) {
|
|
1402
|
-
const { screen: sp, lastLines: lp } = config.updatePromptPatterns;
|
|
1403
|
-
if (sp && sp.some((p) => screen.includes(p)) && lp && lp.some((p) => lastLines.includes(p))) {
|
|
1404
|
-
return State.UPDATE_PROMPT;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
// Confirming - check recent lines (not full screen to avoid history false positives)
|
|
1841
|
+
// Confirming - check before THINKING because "Running…" in tool output matches thinking patterns
|
|
1409
1842
|
const confirmPatterns = config.confirmPatterns || [];
|
|
1410
1843
|
for (const pattern of confirmPatterns) {
|
|
1411
1844
|
if (typeof pattern === "function") {
|
|
@@ -1418,13 +1851,14 @@ function detectState(screen, config) {
|
|
|
1418
1851
|
}
|
|
1419
1852
|
}
|
|
1420
1853
|
|
|
1421
|
-
// Ready -
|
|
1422
|
-
//
|
|
1854
|
+
// Ready - check BEFORE thinking to avoid false positives from timing messages like "✻ Worked for 45s"
|
|
1855
|
+
// If the prompt symbol is visible, the agent is ready regardless of spinner characters in timing messages
|
|
1423
1856
|
if (lastLines.includes(config.promptSymbol)) {
|
|
1424
1857
|
// Check if any line has the prompt followed by pasted content indicator
|
|
1858
|
+
// "[Pasted text" indicates user has pasted content and Claude is still processing
|
|
1425
1859
|
const linesArray = lastLines.split("\n");
|
|
1426
1860
|
const promptWithPaste = linesArray.some(
|
|
1427
|
-
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text")
|
|
1861
|
+
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text"),
|
|
1428
1862
|
);
|
|
1429
1863
|
if (!promptWithPaste) {
|
|
1430
1864
|
return State.READY;
|
|
@@ -1432,6 +1866,31 @@ function detectState(screen, config) {
|
|
|
1432
1866
|
// If prompt has pasted content, Claude is still processing - not ready yet
|
|
1433
1867
|
}
|
|
1434
1868
|
|
|
1869
|
+
// Thinking - spinners (check last lines only)
|
|
1870
|
+
const spinners = config.spinners || [];
|
|
1871
|
+
if (spinners.some((s) => lastLines.includes(s))) {
|
|
1872
|
+
return State.THINKING;
|
|
1873
|
+
}
|
|
1874
|
+
// Thinking - text patterns (last lines) - supports strings, regexes, and functions
|
|
1875
|
+
const thinkingPatterns = config.thinkingPatterns || [];
|
|
1876
|
+
if (
|
|
1877
|
+
thinkingPatterns.some((p) => {
|
|
1878
|
+
if (typeof p === "function") return p(lastLines);
|
|
1879
|
+
if (p instanceof RegExp) return p.test(lastLines);
|
|
1880
|
+
return lastLines.includes(p);
|
|
1881
|
+
})
|
|
1882
|
+
) {
|
|
1883
|
+
return State.THINKING;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// Update prompt
|
|
1887
|
+
if (config.updatePromptPatterns) {
|
|
1888
|
+
const { screen: sp, lastLines: lp } = config.updatePromptPatterns;
|
|
1889
|
+
if (sp && sp.some((p) => screen.includes(p)) && lp && lp.some((p) => lastLines.includes(p))) {
|
|
1890
|
+
return State.UPDATE_PROMPT;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1435
1894
|
return State.STARTING;
|
|
1436
1895
|
}
|
|
1437
1896
|
|
|
@@ -1452,12 +1911,13 @@ function detectState(screen, config) {
|
|
|
1452
1911
|
/**
|
|
1453
1912
|
* @typedef {Object} AgentConfigInput
|
|
1454
1913
|
* @property {string} name
|
|
1914
|
+
* @property {string} displayName
|
|
1455
1915
|
* @property {string} startCommand
|
|
1456
1916
|
* @property {string} yoloCommand
|
|
1457
1917
|
* @property {string} promptSymbol
|
|
1458
1918
|
* @property {string[]} [spinners]
|
|
1459
1919
|
* @property {RegExp} [rateLimitPattern]
|
|
1460
|
-
* @property {string[]} [thinkingPatterns]
|
|
1920
|
+
* @property {(string | RegExp | ((lines: string) => boolean))[]} [thinkingPatterns]
|
|
1461
1921
|
* @property {ConfirmPattern[]} [confirmPatterns]
|
|
1462
1922
|
* @property {UpdatePromptPatterns | null} [updatePromptPatterns]
|
|
1463
1923
|
* @property {string[]} [responseMarkers]
|
|
@@ -1467,6 +1927,8 @@ function detectState(screen, config) {
|
|
|
1467
1927
|
* @property {string} [approveKey]
|
|
1468
1928
|
* @property {string} [rejectKey]
|
|
1469
1929
|
* @property {string} [safeAllowedTools]
|
|
1930
|
+
* @property {string | null} [sessionIdFlag]
|
|
1931
|
+
* @property {((sessionName: string) => string | null) | null} [logPathFinder]
|
|
1470
1932
|
*/
|
|
1471
1933
|
|
|
1472
1934
|
class Agent {
|
|
@@ -1477,6 +1939,8 @@ class Agent {
|
|
|
1477
1939
|
/** @type {string} */
|
|
1478
1940
|
this.name = config.name;
|
|
1479
1941
|
/** @type {string} */
|
|
1942
|
+
this.displayName = config.displayName;
|
|
1943
|
+
/** @type {string} */
|
|
1480
1944
|
this.startCommand = config.startCommand;
|
|
1481
1945
|
/** @type {string} */
|
|
1482
1946
|
this.yoloCommand = config.yoloCommand;
|
|
@@ -1486,7 +1950,7 @@ class Agent {
|
|
|
1486
1950
|
this.spinners = config.spinners || [];
|
|
1487
1951
|
/** @type {RegExp | undefined} */
|
|
1488
1952
|
this.rateLimitPattern = config.rateLimitPattern;
|
|
1489
|
-
/** @type {string[]} */
|
|
1953
|
+
/** @type {(string | RegExp | ((lines: string) => boolean))[]} */
|
|
1490
1954
|
this.thinkingPatterns = config.thinkingPatterns || [];
|
|
1491
1955
|
/** @type {ConfirmPattern[]} */
|
|
1492
1956
|
this.confirmPatterns = config.confirmPatterns || [];
|
|
@@ -1506,6 +1970,10 @@ class Agent {
|
|
|
1506
1970
|
this.rejectKey = config.rejectKey || "n";
|
|
1507
1971
|
/** @type {string | undefined} */
|
|
1508
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;
|
|
1509
1977
|
}
|
|
1510
1978
|
|
|
1511
1979
|
/**
|
|
@@ -1523,12 +1991,9 @@ class Agent {
|
|
|
1523
1991
|
} else {
|
|
1524
1992
|
base = this.startCommand;
|
|
1525
1993
|
}
|
|
1526
|
-
//
|
|
1527
|
-
if (this.
|
|
1528
|
-
|
|
1529
|
-
if (parsed?.uuid) {
|
|
1530
|
-
return `${base} --session-id ${parsed.uuid}`;
|
|
1531
|
-
}
|
|
1994
|
+
// Some agents support session ID flags for deterministic session tracking
|
|
1995
|
+
if (this.sessionIdFlag && sessionName) {
|
|
1996
|
+
return `${base} ${this.sessionIdFlag} ${sessionName}`;
|
|
1532
1997
|
}
|
|
1533
1998
|
return base;
|
|
1534
1999
|
}
|
|
@@ -1540,7 +2005,7 @@ class Agent {
|
|
|
1540
2005
|
}
|
|
1541
2006
|
|
|
1542
2007
|
const cwd = process.cwd();
|
|
1543
|
-
const childPattern = new RegExp(`^${this.name}-[0-9a-f-]{36}$`, "i");
|
|
2008
|
+
const childPattern = new RegExp(`^${this.name}-(partner-)?[0-9a-f-]{36}$`, "i");
|
|
1544
2009
|
|
|
1545
2010
|
// If inside tmux, look for existing agent session in same cwd
|
|
1546
2011
|
const current = tmuxCurrentSession();
|
|
@@ -1585,13 +2050,8 @@ class Agent {
|
|
|
1585
2050
|
* @returns {string | null}
|
|
1586
2051
|
*/
|
|
1587
2052
|
findLogPath(sessionName) {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
const uuid = parsed?.uuid;
|
|
1591
|
-
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
1592
|
-
}
|
|
1593
|
-
if (this.name === "codex") {
|
|
1594
|
-
return findCodexLogPath(sessionName);
|
|
2053
|
+
if (this.logPathFinder) {
|
|
2054
|
+
return this.logPathFinder(sessionName);
|
|
1595
2055
|
}
|
|
1596
2056
|
return null;
|
|
1597
2057
|
}
|
|
@@ -1635,7 +2095,12 @@ class Agent {
|
|
|
1635
2095
|
if (/^(run|execute|create|delete|modify|write)/i.test(line)) return line;
|
|
1636
2096
|
}
|
|
1637
2097
|
|
|
1638
|
-
return
|
|
2098
|
+
return (
|
|
2099
|
+
lines
|
|
2100
|
+
.filter((l) => l && !l.match(/^[╭╮╰╯│─]+$/))
|
|
2101
|
+
.slice(0, 2)
|
|
2102
|
+
.join(" | ") || "action"
|
|
2103
|
+
);
|
|
1639
2104
|
}
|
|
1640
2105
|
|
|
1641
2106
|
/**
|
|
@@ -1711,7 +2176,9 @@ class Agent {
|
|
|
1711
2176
|
|
|
1712
2177
|
// Fallback: extract after last prompt
|
|
1713
2178
|
if (filtered.length === 0) {
|
|
1714
|
-
const lastPromptIdx = lines.findLastIndex((/** @type {string} */ l) =>
|
|
2179
|
+
const lastPromptIdx = lines.findLastIndex((/** @type {string} */ l) =>
|
|
2180
|
+
l.startsWith(this.promptSymbol),
|
|
2181
|
+
);
|
|
1715
2182
|
if (lastPromptIdx >= 0 && lastPromptIdx < lines.length - 1) {
|
|
1716
2183
|
const afterPrompt = lines
|
|
1717
2184
|
.slice(lastPromptIdx + 1)
|
|
@@ -1725,14 +2192,14 @@ class Agent {
|
|
|
1725
2192
|
// This handles the case where Claude finished and shows a new empty prompt
|
|
1726
2193
|
if (lastPromptIdx >= 0) {
|
|
1727
2194
|
const lastPromptLine = lines[lastPromptIdx];
|
|
1728
|
-
const isEmptyPrompt =
|
|
1729
|
-
|
|
2195
|
+
const isEmptyPrompt =
|
|
2196
|
+
lastPromptLine.trim() === this.promptSymbol || lastPromptLine.match(/^❯\s*$/);
|
|
1730
2197
|
if (isEmptyPrompt) {
|
|
1731
2198
|
// Find the previous prompt (user's input) and extract content between
|
|
1732
2199
|
// Note: [Pasted text is Claude's truncated output indicator, NOT a prompt
|
|
1733
|
-
const prevPromptIdx = lines
|
|
1734
|
-
(
|
|
1735
|
-
|
|
2200
|
+
const prevPromptIdx = lines
|
|
2201
|
+
.slice(0, lastPromptIdx)
|
|
2202
|
+
.findLastIndex((/** @type {string} */ l) => l.startsWith(this.promptSymbol));
|
|
1736
2203
|
if (prevPromptIdx >= 0) {
|
|
1737
2204
|
const betweenPrompts = lines
|
|
1738
2205
|
.slice(prevPromptIdx + 1, lastPromptIdx)
|
|
@@ -1753,23 +2220,25 @@ class Agent {
|
|
|
1753
2220
|
* @returns {string}
|
|
1754
2221
|
*/
|
|
1755
2222
|
cleanResponse(response) {
|
|
1756
|
-
return
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2223
|
+
return (
|
|
2224
|
+
response
|
|
2225
|
+
// Remove tool call lines (Search, Read, Grep, etc.)
|
|
2226
|
+
.replace(/^[⏺•]\s*(Search|Read|Grep|Glob|Write|Edit|Bash)\([^)]*\).*$/gm, "")
|
|
2227
|
+
// Remove tool result lines
|
|
2228
|
+
.replace(/^⎿\s+.*$/gm, "")
|
|
2229
|
+
// Remove "Sautéed for Xs" timing lines
|
|
2230
|
+
.replace(/^✻\s+Sautéed for.*$/gm, "")
|
|
2231
|
+
// Remove expand hints
|
|
2232
|
+
.replace(/\(ctrl\+o to expand\)/g, "")
|
|
2233
|
+
// Clean up multiple blank lines
|
|
2234
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
2235
|
+
// Original cleanup
|
|
2236
|
+
.replace(/^[•⏺-]\s*/, "")
|
|
2237
|
+
.replace(/^\*\*(.+)\*\*/, "$1")
|
|
2238
|
+
.replace(/\n /g, "\n")
|
|
2239
|
+
.replace(/─+\s*$/, "")
|
|
2240
|
+
.trim()
|
|
2241
|
+
);
|
|
1773
2242
|
}
|
|
1774
2243
|
|
|
1775
2244
|
/**
|
|
@@ -1810,6 +2279,7 @@ class Agent {
|
|
|
1810
2279
|
|
|
1811
2280
|
const CodexAgent = new Agent({
|
|
1812
2281
|
name: "codex",
|
|
2282
|
+
displayName: "Codex",
|
|
1813
2283
|
startCommand: "codex --sandbox read-only",
|
|
1814
2284
|
yoloCommand: "codex --dangerously-bypass-approvals-and-sandbox",
|
|
1815
2285
|
promptSymbol: "›",
|
|
@@ -1829,6 +2299,7 @@ const CodexAgent = new Agent({
|
|
|
1829
2299
|
chromePatterns: ["context left", "for shortcuts"],
|
|
1830
2300
|
reviewOptions: { pr: "1", uncommitted: "2", commit: "3", custom: "4" },
|
|
1831
2301
|
envVar: "AX_SESSION",
|
|
2302
|
+
logPathFinder: findCodexLogPath,
|
|
1832
2303
|
});
|
|
1833
2304
|
|
|
1834
2305
|
// =============================================================================
|
|
@@ -1837,12 +2308,15 @@ const CodexAgent = new Agent({
|
|
|
1837
2308
|
|
|
1838
2309
|
const ClaudeAgent = new Agent({
|
|
1839
2310
|
name: "claude",
|
|
2311
|
+
displayName: "Claude",
|
|
1840
2312
|
startCommand: "claude",
|
|
1841
2313
|
yoloCommand: "claude --dangerously-skip-permissions",
|
|
1842
2314
|
promptSymbol: "❯",
|
|
1843
|
-
|
|
2315
|
+
// Claude Code spinners: ·✢✳✶✻✽ (from cli.js source)
|
|
2316
|
+
spinners: ["·", "✢", "✳", "✶", "✻", "✽"],
|
|
1844
2317
|
rateLimitPattern: /rate.?limit/i,
|
|
1845
|
-
|
|
2318
|
+
// Claude uses whimsical verbs like "Wibbling…", "Dancing…", etc. Match any capitalized -ing word + ellipsis (… or ...)
|
|
2319
|
+
thinkingPatterns: ["Thinking", /[A-Z][a-z]+ing(…|\.\.\.)/],
|
|
1846
2320
|
confirmPatterns: [
|
|
1847
2321
|
"Do you want to make this edit",
|
|
1848
2322
|
"Do you want to run this command",
|
|
@@ -1852,12 +2326,28 @@ const ClaudeAgent = new Agent({
|
|
|
1852
2326
|
],
|
|
1853
2327
|
updatePromptPatterns: null,
|
|
1854
2328
|
responseMarkers: ["⏺", "•", "- ", "**"],
|
|
1855
|
-
chromePatterns: [
|
|
2329
|
+
chromePatterns: [
|
|
2330
|
+
"↵ send",
|
|
2331
|
+
"Esc to cancel",
|
|
2332
|
+
"shortcuts",
|
|
2333
|
+
"for more options",
|
|
2334
|
+
"docs.anthropic.com",
|
|
2335
|
+
"⏵⏵",
|
|
2336
|
+
"bypass permissions",
|
|
2337
|
+
"shift+Tab to cycle",
|
|
2338
|
+
],
|
|
1856
2339
|
reviewOptions: null,
|
|
1857
|
-
safeAllowedTools: "Bash(git:*) Read Glob Grep",
|
|
2340
|
+
safeAllowedTools: "Bash(git:*) Read Glob Grep", // Default: auto-approve read-only tools
|
|
1858
2341
|
envVar: "AX_SESSION",
|
|
1859
2342
|
approveKey: "1",
|
|
1860
2343
|
rejectKey: "Escape",
|
|
2344
|
+
sessionIdFlag: "--session-id",
|
|
2345
|
+
logPathFinder: (sessionName) => {
|
|
2346
|
+
const parsed = parseSessionName(sessionName);
|
|
2347
|
+
const uuid = parsed?.uuid;
|
|
2348
|
+
if (uuid) return findClaudeLogPath(uuid, sessionName);
|
|
2349
|
+
return null;
|
|
2350
|
+
},
|
|
1861
2351
|
});
|
|
1862
2352
|
|
|
1863
2353
|
// =============================================================================
|
|
@@ -1878,7 +2368,11 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1878
2368
|
const initialState = agent.getState(initialScreen);
|
|
1879
2369
|
|
|
1880
2370
|
// Already in terminal state
|
|
1881
|
-
if (
|
|
2371
|
+
if (
|
|
2372
|
+
initialState === State.RATE_LIMITED ||
|
|
2373
|
+
initialState === State.CONFIRMING ||
|
|
2374
|
+
initialState === State.READY
|
|
2375
|
+
) {
|
|
1882
2376
|
return { state: initialState, screen: initialScreen };
|
|
1883
2377
|
}
|
|
1884
2378
|
|
|
@@ -1891,30 +2385,38 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1891
2385
|
return { state, screen };
|
|
1892
2386
|
}
|
|
1893
2387
|
}
|
|
1894
|
-
throw new
|
|
2388
|
+
throw new TimeoutError(session);
|
|
1895
2389
|
}
|
|
1896
2390
|
|
|
1897
2391
|
/**
|
|
1898
|
-
*
|
|
1899
|
-
* Waits for screen activity before considering the response complete.
|
|
2392
|
+
* Core polling loop for waiting on agent responses.
|
|
1900
2393
|
* @param {Agent} agent
|
|
1901
2394
|
* @param {string} session
|
|
1902
|
-
* @param {number}
|
|
2395
|
+
* @param {number} timeoutMs
|
|
2396
|
+
* @param {{onPoll?: (screen: string, state: string) => void, onStateChange?: (state: string, lastState: string | null, screen: string) => void, onReady?: (screen: string) => void}} [hooks]
|
|
1903
2397
|
* @returns {Promise<{state: string, screen: string}>}
|
|
1904
2398
|
*/
|
|
1905
|
-
async function
|
|
2399
|
+
async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
2400
|
+
const { onPoll, onStateChange, onReady } = hooks;
|
|
1906
2401
|
const start = Date.now();
|
|
1907
2402
|
const initialScreen = tmuxCapture(session);
|
|
1908
2403
|
|
|
1909
2404
|
let lastScreen = initialScreen;
|
|
2405
|
+
let lastState = null;
|
|
1910
2406
|
let stableAt = null;
|
|
1911
2407
|
let sawActivity = false;
|
|
1912
2408
|
|
|
1913
2409
|
while (Date.now() - start < timeoutMs) {
|
|
1914
|
-
await sleep(POLL_MS);
|
|
1915
2410
|
const screen = tmuxCapture(session);
|
|
1916
2411
|
const state = agent.getState(screen);
|
|
1917
2412
|
|
|
2413
|
+
if (onPoll) onPoll(screen, state);
|
|
2414
|
+
|
|
2415
|
+
if (state !== lastState) {
|
|
2416
|
+
if (onStateChange) onStateChange(state, lastState, screen);
|
|
2417
|
+
lastState = state;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
1918
2420
|
if (state === State.RATE_LIMITED || state === State.CONFIRMING) {
|
|
1919
2421
|
return { state, screen };
|
|
1920
2422
|
}
|
|
@@ -1929,6 +2431,7 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1929
2431
|
|
|
1930
2432
|
if (sawActivity && stableAt && Date.now() - stableAt >= STABLE_MS) {
|
|
1931
2433
|
if (state === State.READY) {
|
|
2434
|
+
if (onReady) onReady(screen);
|
|
1932
2435
|
return { state, screen };
|
|
1933
2436
|
}
|
|
1934
2437
|
}
|
|
@@ -1936,26 +2439,86 @@ async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1936
2439
|
if (state === State.THINKING) {
|
|
1937
2440
|
sawActivity = true;
|
|
1938
2441
|
}
|
|
2442
|
+
|
|
2443
|
+
await sleep(POLL_MS);
|
|
1939
2444
|
}
|
|
1940
|
-
throw new
|
|
2445
|
+
throw new TimeoutError(session);
|
|
1941
2446
|
}
|
|
1942
2447
|
|
|
1943
2448
|
/**
|
|
1944
|
-
*
|
|
1945
|
-
* Used by callers to implement yolo mode on sessions not started with native --yolo.
|
|
2449
|
+
* Wait for agent response without streaming output.
|
|
1946
2450
|
* @param {Agent} agent
|
|
1947
2451
|
* @param {string} session
|
|
1948
2452
|
* @param {number} [timeoutMs]
|
|
1949
2453
|
* @returns {Promise<{state: string, screen: string}>}
|
|
1950
2454
|
*/
|
|
1951
|
-
async function
|
|
2455
|
+
async function waitForResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2456
|
+
return pollForResponse(agent, session, timeoutMs);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
/**
|
|
2460
|
+
* Wait for agent response with streaming output to console.
|
|
2461
|
+
* @param {Agent} agent
|
|
2462
|
+
* @param {string} session
|
|
2463
|
+
* @param {number} [timeoutMs]
|
|
2464
|
+
* @returns {Promise<{state: string, screen: string}>}
|
|
2465
|
+
*/
|
|
2466
|
+
async function streamResponse(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2467
|
+
let logPath = agent.findLogPath(session);
|
|
2468
|
+
let logOffset = logPath && existsSync(logPath) ? statSync(logPath).size : 0;
|
|
2469
|
+
let printedThinking = false;
|
|
2470
|
+
|
|
2471
|
+
const streamNewEntries = () => {
|
|
2472
|
+
if (!logPath) {
|
|
2473
|
+
logPath = agent.findLogPath(session);
|
|
2474
|
+
if (logPath && existsSync(logPath)) {
|
|
2475
|
+
logOffset = statSync(logPath).size;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
if (logPath) {
|
|
2479
|
+
const { entries, newOffset } = tailJsonl(logPath, logOffset);
|
|
2480
|
+
logOffset = newOffset;
|
|
2481
|
+
for (const entry of entries) {
|
|
2482
|
+
const formatted = formatEntry(entry);
|
|
2483
|
+
if (formatted) console.log(formatted);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
|
|
2488
|
+
return pollForResponse(agent, session, timeoutMs, {
|
|
2489
|
+
onPoll: () => streamNewEntries(),
|
|
2490
|
+
onStateChange: (state, lastState, screen) => {
|
|
2491
|
+
if (state === State.THINKING && !printedThinking) {
|
|
2492
|
+
console.log("[THINKING]");
|
|
2493
|
+
printedThinking = true;
|
|
2494
|
+
} else if (state === State.CONFIRMING) {
|
|
2495
|
+
const pendingTool = extractPendingToolFromScreen(screen);
|
|
2496
|
+
console.log(pendingTool ? `[CONFIRMING] ${pendingTool}` : "[CONFIRMING]");
|
|
2497
|
+
}
|
|
2498
|
+
if (lastState === State.THINKING && state !== State.THINKING) {
|
|
2499
|
+
printedThinking = false;
|
|
2500
|
+
}
|
|
2501
|
+
},
|
|
2502
|
+
onReady: () => streamNewEntries(),
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/**
|
|
2507
|
+
* Auto-approve loop that keeps approving confirmations until the agent is ready or rate limited.
|
|
2508
|
+
* @param {Agent} agent
|
|
2509
|
+
* @param {string} session
|
|
2510
|
+
* @param {number} timeoutMs
|
|
2511
|
+
* @param {Function} waitFn - waitForResponse or streamResponse
|
|
2512
|
+
* @returns {Promise<{state: string, screen: string}>}
|
|
2513
|
+
*/
|
|
2514
|
+
async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
1952
2515
|
const deadline = Date.now() + timeoutMs;
|
|
1953
2516
|
|
|
1954
2517
|
while (Date.now() < deadline) {
|
|
1955
2518
|
const remaining = deadline - Date.now();
|
|
1956
2519
|
if (remaining <= 0) break;
|
|
1957
2520
|
|
|
1958
|
-
const { state, screen } = await
|
|
2521
|
+
const { state, screen } = await waitFn(agent, session, remaining);
|
|
1959
2522
|
|
|
1960
2523
|
if (state === State.RATE_LIMITED || state === State.READY) {
|
|
1961
2524
|
return { state, screen };
|
|
@@ -1967,11 +2530,10 @@ async function autoApproveLoop(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1967
2530
|
continue;
|
|
1968
2531
|
}
|
|
1969
2532
|
|
|
1970
|
-
// Unexpected state - log and continue polling
|
|
1971
2533
|
debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
|
|
1972
2534
|
}
|
|
1973
2535
|
|
|
1974
|
-
throw new
|
|
2536
|
+
throw new TimeoutError(session);
|
|
1975
2537
|
}
|
|
1976
2538
|
|
|
1977
2539
|
/**
|
|
@@ -2030,125 +2592,158 @@ function cmdAgents() {
|
|
|
2030
2592
|
|
|
2031
2593
|
if (agentSessions.length === 0) {
|
|
2032
2594
|
console.log("No agents running");
|
|
2595
|
+
// Still check for orphans
|
|
2596
|
+
const orphans = findOrphanedProcesses();
|
|
2597
|
+
if (orphans.length > 0) {
|
|
2598
|
+
console.log(`\nOrphaned (${orphans.length}):`);
|
|
2599
|
+
for (const { pid, command } of orphans) {
|
|
2600
|
+
console.log(` PID ${pid}: ${command}`);
|
|
2601
|
+
}
|
|
2602
|
+
console.log(`\n Run 'ax kill --orphans' to clean up`);
|
|
2603
|
+
}
|
|
2033
2604
|
return;
|
|
2034
2605
|
}
|
|
2035
2606
|
|
|
2607
|
+
// Get default session for each agent type
|
|
2608
|
+
const claudeDefault = ClaudeAgent.getDefaultSession();
|
|
2609
|
+
const codexDefault = CodexAgent.getDefaultSession();
|
|
2610
|
+
|
|
2036
2611
|
// Get info for each agent
|
|
2037
2612
|
const agents = agentSessions.map((session) => {
|
|
2038
2613
|
const parsed = /** @type {ParsedSession} */ (parseSessionName(session));
|
|
2039
2614
|
const agent = parsed.tool === "claude" ? ClaudeAgent : CodexAgent;
|
|
2040
2615
|
const screen = tmuxCapture(session);
|
|
2041
2616
|
const state = agent.getState(screen);
|
|
2042
|
-
const
|
|
2043
|
-
const
|
|
2617
|
+
const type = parsed.archangelName ? "archangel" : "-";
|
|
2618
|
+
const isDefault =
|
|
2619
|
+
(parsed.tool === "claude" && session === claudeDefault) ||
|
|
2620
|
+
(parsed.tool === "codex" && session === codexDefault);
|
|
2621
|
+
|
|
2622
|
+
// Get session metadata (Claude only)
|
|
2623
|
+
const meta = getSessionMeta(session);
|
|
2044
2624
|
|
|
2045
2625
|
return {
|
|
2046
2626
|
session,
|
|
2047
2627
|
tool: parsed.tool,
|
|
2048
2628
|
state: state || "unknown",
|
|
2629
|
+
target: isDefault ? "*" : "",
|
|
2049
2630
|
type,
|
|
2050
|
-
|
|
2631
|
+
plan: meta?.slug || "-",
|
|
2632
|
+
branch: meta?.gitBranch || "-",
|
|
2051
2633
|
};
|
|
2052
2634
|
});
|
|
2053
2635
|
|
|
2054
|
-
// Print table
|
|
2636
|
+
// Print sessions table
|
|
2055
2637
|
const maxSession = Math.max(7, ...agents.map((a) => a.session.length));
|
|
2056
2638
|
const maxTool = Math.max(4, ...agents.map((a) => a.tool.length));
|
|
2057
2639
|
const maxState = Math.max(5, ...agents.map((a) => a.state.length));
|
|
2640
|
+
const maxTarget = Math.max(6, ...agents.map((a) => a.target.length));
|
|
2058
2641
|
const maxType = Math.max(4, ...agents.map((a) => a.type.length));
|
|
2642
|
+
const maxPlan = Math.max(4, ...agents.map((a) => a.plan.length));
|
|
2059
2643
|
|
|
2060
2644
|
console.log(
|
|
2061
|
-
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TYPE".padEnd(maxType)}
|
|
2645
|
+
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(maxTarget)} ${"TYPE".padEnd(maxType)} ${"PLAN".padEnd(maxPlan)} BRANCH`,
|
|
2062
2646
|
);
|
|
2063
2647
|
for (const a of agents) {
|
|
2064
2648
|
console.log(
|
|
2065
|
-
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.type.padEnd(maxType)} ${a.
|
|
2649
|
+
`${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}`,
|
|
2066
2650
|
);
|
|
2067
2651
|
}
|
|
2652
|
+
|
|
2653
|
+
// Print orphaned processes if any
|
|
2654
|
+
const orphans = findOrphanedProcesses();
|
|
2655
|
+
if (orphans.length > 0) {
|
|
2656
|
+
console.log(`\nOrphaned (${orphans.length}):`);
|
|
2657
|
+
for (const { pid, command } of orphans) {
|
|
2658
|
+
console.log(` PID ${pid}: ${command}`);
|
|
2659
|
+
}
|
|
2660
|
+
console.log(`\n Run 'ax kill --orphans' to clean up`);
|
|
2661
|
+
}
|
|
2068
2662
|
}
|
|
2069
2663
|
|
|
2070
2664
|
// =============================================================================
|
|
2071
|
-
// Command:
|
|
2665
|
+
// Command: summon/recall
|
|
2072
2666
|
// =============================================================================
|
|
2073
2667
|
|
|
2074
2668
|
/**
|
|
2075
2669
|
* @param {string} pattern
|
|
2076
2670
|
* @returns {string | undefined}
|
|
2077
2671
|
*/
|
|
2078
|
-
function
|
|
2672
|
+
function findArchangelSession(pattern) {
|
|
2079
2673
|
const sessions = tmuxListSessions();
|
|
2080
2674
|
return sessions.find((s) => s.startsWith(pattern));
|
|
2081
2675
|
}
|
|
2082
2676
|
|
|
2083
2677
|
/**
|
|
2084
|
-
* @param {
|
|
2678
|
+
* @param {ArchangelConfig} config
|
|
2085
2679
|
* @returns {string}
|
|
2086
2680
|
*/
|
|
2087
|
-
function
|
|
2088
|
-
return `${config.tool}-
|
|
2681
|
+
function generateArchangelSessionName(config) {
|
|
2682
|
+
return `${config.tool}-archangel-${config.name}-${randomUUID()}`;
|
|
2089
2683
|
}
|
|
2090
2684
|
|
|
2091
2685
|
/**
|
|
2092
|
-
* @param {
|
|
2686
|
+
* @param {ArchangelConfig} config
|
|
2093
2687
|
* @param {ParentSession | null} [parentSession]
|
|
2094
2688
|
*/
|
|
2095
|
-
function
|
|
2096
|
-
// Build environment with parent session info if available
|
|
2689
|
+
function startArchangel(config, parentSession = null) {
|
|
2097
2690
|
/** @type {NodeJS.ProcessEnv} */
|
|
2098
2691
|
const env = { ...process.env };
|
|
2099
2692
|
if (parentSession?.uuid) {
|
|
2100
|
-
// Session name may be null for non-tmux sessions, but uuid is required
|
|
2101
2693
|
if (parentSession.session) {
|
|
2102
|
-
env[
|
|
2694
|
+
env[AX_ARCHANGEL_PARENT_SESSION_ENV] = parentSession.session;
|
|
2103
2695
|
}
|
|
2104
|
-
env[
|
|
2696
|
+
env[AX_ARCHANGEL_PARENT_UUID_ENV] = parentSession.uuid;
|
|
2105
2697
|
}
|
|
2106
2698
|
|
|
2107
|
-
|
|
2108
|
-
const child = spawn("node", [process.argv[1], "daemon", config.name], {
|
|
2699
|
+
const child = spawn("node", [process.argv[1], "archangel", config.name], {
|
|
2109
2700
|
detached: true,
|
|
2110
2701
|
stdio: "ignore",
|
|
2111
2702
|
cwd: process.cwd(),
|
|
2112
2703
|
env,
|
|
2113
2704
|
});
|
|
2114
2705
|
child.unref();
|
|
2115
|
-
console.log(
|
|
2706
|
+
console.log(
|
|
2707
|
+
`Summoning: ${config.name} (pid ${child.pid})${parentSession ? ` [parent: ${parentSession.session}]` : ""}`,
|
|
2708
|
+
);
|
|
2116
2709
|
}
|
|
2117
2710
|
|
|
2118
2711
|
// =============================================================================
|
|
2119
|
-
// Command:
|
|
2712
|
+
// Command: archangel (runs as the archangel process itself)
|
|
2120
2713
|
// =============================================================================
|
|
2121
2714
|
|
|
2122
2715
|
/**
|
|
2123
2716
|
* @param {string | undefined} agentName
|
|
2124
2717
|
*/
|
|
2125
|
-
async function
|
|
2718
|
+
async function cmdArchangel(agentName) {
|
|
2126
2719
|
if (!agentName) {
|
|
2127
|
-
console.error("Usage: ./ax.js
|
|
2720
|
+
console.error("Usage: ./ax.js archangel <name>");
|
|
2128
2721
|
process.exit(1);
|
|
2129
2722
|
}
|
|
2130
2723
|
// Load agent config
|
|
2131
2724
|
const configPath = path.join(AGENTS_DIR, `${agentName}.md`);
|
|
2132
2725
|
if (!existsSync(configPath)) {
|
|
2133
|
-
console.error(`[
|
|
2726
|
+
console.error(`[archangel:${agentName}] Config not found: ${configPath}`);
|
|
2134
2727
|
process.exit(1);
|
|
2135
2728
|
}
|
|
2136
2729
|
|
|
2137
2730
|
const content = readFileSync(configPath, "utf-8");
|
|
2138
2731
|
const configResult = parseAgentConfig(`${agentName}.md`, content);
|
|
2139
2732
|
if (!configResult || "error" in configResult) {
|
|
2140
|
-
console.error(`[
|
|
2733
|
+
console.error(`[archangel:${agentName}] Invalid config`);
|
|
2141
2734
|
process.exit(1);
|
|
2142
2735
|
}
|
|
2143
2736
|
const config = configResult;
|
|
2144
2737
|
|
|
2145
2738
|
const agent = config.tool === "claude" ? ClaudeAgent : CodexAgent;
|
|
2146
|
-
const sessionName =
|
|
2739
|
+
const sessionName = generateArchangelSessionName(config);
|
|
2147
2740
|
|
|
2148
2741
|
// Check agent CLI is installed before trying to start
|
|
2149
2742
|
const cliCheck = spawnSync("which", [agent.name], { encoding: "utf-8" });
|
|
2150
2743
|
if (cliCheck.status !== 0) {
|
|
2151
|
-
console.error(
|
|
2744
|
+
console.error(
|
|
2745
|
+
`[archangel:${agentName}] ERROR: ${agent.name} CLI is not installed or not in PATH`,
|
|
2746
|
+
);
|
|
2152
2747
|
process.exit(1);
|
|
2153
2748
|
}
|
|
2154
2749
|
|
|
@@ -2158,7 +2753,7 @@ async function cmdDaemon(agentName) {
|
|
|
2158
2753
|
|
|
2159
2754
|
// Wait for agent to be ready
|
|
2160
2755
|
const start = Date.now();
|
|
2161
|
-
while (Date.now() - start <
|
|
2756
|
+
while (Date.now() - start < ARCHANGEL_STARTUP_TIMEOUT_MS) {
|
|
2162
2757
|
const screen = tmuxCapture(sessionName);
|
|
2163
2758
|
const state = agent.getState(screen);
|
|
2164
2759
|
|
|
@@ -2169,7 +2764,7 @@ async function cmdDaemon(agentName) {
|
|
|
2169
2764
|
|
|
2170
2765
|
// Handle bypass permissions confirmation dialog (Claude Code shows this for --dangerously-skip-permissions)
|
|
2171
2766
|
if (screen.includes("Bypass Permissions mode") && screen.includes("Yes, I accept")) {
|
|
2172
|
-
console.log(`[
|
|
2767
|
+
console.log(`[archangel:${agentName}] Accepting bypass permissions dialog`);
|
|
2173
2768
|
tmuxSend(sessionName, "2"); // Select "Yes, I accept"
|
|
2174
2769
|
await sleep(300);
|
|
2175
2770
|
tmuxSend(sessionName, "Enter");
|
|
@@ -2178,7 +2773,7 @@ async function cmdDaemon(agentName) {
|
|
|
2178
2773
|
}
|
|
2179
2774
|
|
|
2180
2775
|
if (state === State.READY) {
|
|
2181
|
-
console.log(`[
|
|
2776
|
+
console.log(`[archangel:${agentName}] Started session: ${sessionName}`);
|
|
2182
2777
|
break;
|
|
2183
2778
|
}
|
|
2184
2779
|
|
|
@@ -2200,6 +2795,13 @@ async function cmdDaemon(agentName) {
|
|
|
2200
2795
|
let isProcessing = false;
|
|
2201
2796
|
const intervalMs = config.interval * 1000;
|
|
2202
2797
|
|
|
2798
|
+
// Hash tracking for incremental context updates
|
|
2799
|
+
/** @type {string | null} */
|
|
2800
|
+
let lastPlanHash = null;
|
|
2801
|
+
/** @type {string | null} */
|
|
2802
|
+
let lastTodosHash = null;
|
|
2803
|
+
let isFirstTrigger = true;
|
|
2804
|
+
|
|
2203
2805
|
async function processChanges() {
|
|
2204
2806
|
clearTimeout(debounceTimer);
|
|
2205
2807
|
clearTimeout(maxWaitTimer);
|
|
@@ -2210,16 +2812,32 @@ async function cmdDaemon(agentName) {
|
|
|
2210
2812
|
isProcessing = true;
|
|
2211
2813
|
|
|
2212
2814
|
const files = [...changedFiles];
|
|
2213
|
-
changedFiles = new Set();
|
|
2815
|
+
changedFiles = new Set(); // atomic swap to avoid losing changes during processing
|
|
2214
2816
|
|
|
2215
2817
|
try {
|
|
2216
2818
|
// Get parent session log path for JSONL extraction
|
|
2217
2819
|
const parent = findParentSession();
|
|
2218
2820
|
const logPath = parent ? findClaudeLogPath(parent.uuid, parent.session) : null;
|
|
2219
2821
|
|
|
2822
|
+
// Get orientation context (plan and todos) from parent session
|
|
2823
|
+
const meta = parent?.session ? getSessionMeta(parent.session) : null;
|
|
2824
|
+
const planContent = meta?.slug ? readPlanFile(meta.slug) : null;
|
|
2825
|
+
const todosContent = meta?.todos?.length ? formatTodos(meta.todos) : null;
|
|
2826
|
+
|
|
2827
|
+
// Check if plan/todos have changed since last trigger
|
|
2828
|
+
const planHash = quickHash(planContent);
|
|
2829
|
+
const todosHash = quickHash(todosContent);
|
|
2830
|
+
const includePlan = planHash !== lastPlanHash;
|
|
2831
|
+
const includeTodos = todosHash !== lastTodosHash;
|
|
2832
|
+
|
|
2833
|
+
// Update tracking for next trigger
|
|
2834
|
+
lastPlanHash = planHash;
|
|
2835
|
+
lastTodosHash = todosHash;
|
|
2836
|
+
|
|
2220
2837
|
// Build file-specific context from JSONL
|
|
2221
2838
|
const fileContexts = [];
|
|
2222
|
-
for (const file of files.slice(0, 5)) {
|
|
2839
|
+
for (const file of files.slice(0, 5)) {
|
|
2840
|
+
// Limit to 5 files
|
|
2223
2841
|
const ctx = extractFileEditContext(logPath, file);
|
|
2224
2842
|
if (ctx) {
|
|
2225
2843
|
fileContexts.push({ file, ...ctx });
|
|
@@ -2227,7 +2845,18 @@ async function cmdDaemon(agentName) {
|
|
|
2227
2845
|
}
|
|
2228
2846
|
|
|
2229
2847
|
// Build the prompt
|
|
2230
|
-
|
|
2848
|
+
// First trigger: include intro, guidelines, and focus (archangel has memory)
|
|
2849
|
+
let prompt = isFirstTrigger
|
|
2850
|
+
? `You are the archangel of ${agentName}.\n\n${ARCHANGEL_PREAMBLE}\n\n## Focus\n\n${basePrompt}\n\n---`
|
|
2851
|
+
: "";
|
|
2852
|
+
|
|
2853
|
+
// Add orientation context (plan and todos) only if changed since last trigger
|
|
2854
|
+
if (includePlan && planContent) {
|
|
2855
|
+
prompt += (prompt ? "\n\n" : "") + "## Current Plan\n\n" + planContent;
|
|
2856
|
+
}
|
|
2857
|
+
if (includeTodos && todosContent) {
|
|
2858
|
+
prompt += (prompt ? "\n\n" : "") + "## Current Todos\n\n" + todosContent;
|
|
2859
|
+
}
|
|
2231
2860
|
|
|
2232
2861
|
if (fileContexts.length > 0) {
|
|
2233
2862
|
prompt += "\n\n## Recent Edits (from parent session)\n";
|
|
@@ -2246,26 +2875,33 @@ async function cmdDaemon(agentName) {
|
|
|
2246
2875
|
}
|
|
2247
2876
|
|
|
2248
2877
|
if (ctx.readsBefore.length > 0) {
|
|
2249
|
-
const reads = ctx.readsBefore.map(f => f.split("/").pop()).join(", ");
|
|
2878
|
+
const reads = ctx.readsBefore.map((f) => f.split("/").pop()).join(", ");
|
|
2250
2879
|
prompt += `**Files read before:** ${reads}\n`;
|
|
2251
2880
|
}
|
|
2252
2881
|
}
|
|
2253
2882
|
|
|
2254
2883
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
2255
2884
|
|
|
2256
|
-
const gitContext = buildGitContext(
|
|
2885
|
+
const gitContext = buildGitContext(
|
|
2886
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2887
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2888
|
+
);
|
|
2257
2889
|
if (gitContext) {
|
|
2258
2890
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2259
2891
|
}
|
|
2260
2892
|
|
|
2261
|
-
prompt +=
|
|
2893
|
+
prompt += "\n\nReview these changes.";
|
|
2262
2894
|
} else {
|
|
2263
2895
|
// Fallback: no JSONL context available, use conversation + git context
|
|
2264
|
-
const parentContext = getParentSessionContext(
|
|
2265
|
-
const gitContext = buildGitContext(
|
|
2896
|
+
const parentContext = getParentSessionContext(ARCHANGEL_PARENT_CONTEXT_ENTRIES);
|
|
2897
|
+
const gitContext = buildGitContext(
|
|
2898
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2899
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2900
|
+
);
|
|
2266
2901
|
|
|
2267
2902
|
if (parentContext) {
|
|
2268
|
-
prompt +=
|
|
2903
|
+
prompt +=
|
|
2904
|
+
"\n\n## Main Session Context\n\nThe user is currently working on:\n\n" + parentContext;
|
|
2269
2905
|
}
|
|
2270
2906
|
|
|
2271
2907
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
@@ -2274,13 +2910,12 @@ async function cmdDaemon(agentName) {
|
|
|
2274
2910
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2275
2911
|
}
|
|
2276
2912
|
|
|
2277
|
-
prompt +=
|
|
2913
|
+
prompt += "\n\nReview these changes.";
|
|
2278
2914
|
}
|
|
2279
2915
|
|
|
2280
|
-
|
|
2281
2916
|
// Check session still exists
|
|
2282
2917
|
if (!tmuxHasSession(sessionName)) {
|
|
2283
|
-
console.log(`[
|
|
2918
|
+
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
2284
2919
|
process.exit(0);
|
|
2285
2920
|
}
|
|
2286
2921
|
|
|
@@ -2289,12 +2924,12 @@ async function cmdDaemon(agentName) {
|
|
|
2289
2924
|
const state = agent.getState(screen);
|
|
2290
2925
|
|
|
2291
2926
|
if (state === State.RATE_LIMITED) {
|
|
2292
|
-
console.error(`[
|
|
2927
|
+
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2293
2928
|
process.exit(2);
|
|
2294
2929
|
}
|
|
2295
2930
|
|
|
2296
2931
|
if (state !== State.READY) {
|
|
2297
|
-
console.log(`[
|
|
2932
|
+
console.log(`[archangel:${agentName}] Agent not ready (${state}), skipping`);
|
|
2298
2933
|
isProcessing = false;
|
|
2299
2934
|
return;
|
|
2300
2935
|
}
|
|
@@ -2304,38 +2939,37 @@ async function cmdDaemon(agentName) {
|
|
|
2304
2939
|
await sleep(200); // Allow time for large prompts to be processed
|
|
2305
2940
|
tmuxSend(sessionName, "Enter");
|
|
2306
2941
|
await sleep(100); // Ensure Enter is processed
|
|
2942
|
+
isFirstTrigger = false;
|
|
2307
2943
|
|
|
2308
2944
|
// Wait for response
|
|
2309
|
-
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2945
|
+
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2946
|
+
agent,
|
|
2947
|
+
sessionName,
|
|
2948
|
+
ARCHANGEL_RESPONSE_TIMEOUT_MS,
|
|
2949
|
+
);
|
|
2310
2950
|
|
|
2311
2951
|
if (endState === State.RATE_LIMITED) {
|
|
2312
|
-
console.error(`[
|
|
2952
|
+
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2313
2953
|
process.exit(2);
|
|
2314
2954
|
}
|
|
2315
2955
|
|
|
2316
|
-
|
|
2317
2956
|
const cleanedResponse = agent.getResponse(sessionName, afterScreen) || "";
|
|
2318
2957
|
|
|
2319
|
-
|
|
2320
|
-
const isGarbage = cleanedResponse.includes("[Pasted text") ||
|
|
2321
|
-
cleanedResponse.match(/^\+\d+ lines\]/) ||
|
|
2322
|
-
cleanedResponse.length < 20;
|
|
2958
|
+
const isSkippable = !cleanedResponse || cleanedResponse.trim() === "EMPTY_RESPONSE";
|
|
2323
2959
|
|
|
2324
|
-
if (
|
|
2960
|
+
if (!isSkippable) {
|
|
2325
2961
|
writeToMailbox({
|
|
2326
2962
|
agent: /** @type {string} */ (agentName),
|
|
2327
2963
|
session: sessionName,
|
|
2328
2964
|
branch: getCurrentBranch(),
|
|
2329
2965
|
commit: getCurrentCommit(),
|
|
2330
2966
|
files,
|
|
2331
|
-
message: cleanedResponse
|
|
2967
|
+
message: cleanedResponse,
|
|
2332
2968
|
});
|
|
2333
|
-
console.log(`[
|
|
2334
|
-
} else if (isGarbage) {
|
|
2335
|
-
console.log(`[daemon:${agentName}] Skipped garbage response`);
|
|
2969
|
+
console.log(`[archangel:${agentName}] Wrote observation for ${files.length} file(s)`);
|
|
2336
2970
|
}
|
|
2337
2971
|
} catch (err) {
|
|
2338
|
-
console.error(`[
|
|
2972
|
+
console.error(`[archangel:${agentName}] Error:`, err instanceof Error ? err.message : err);
|
|
2339
2973
|
}
|
|
2340
2974
|
|
|
2341
2975
|
isProcessing = false;
|
|
@@ -2343,7 +2977,10 @@ async function cmdDaemon(agentName) {
|
|
|
2343
2977
|
|
|
2344
2978
|
function scheduleProcessChanges() {
|
|
2345
2979
|
processChanges().catch((err) => {
|
|
2346
|
-
console.error(
|
|
2980
|
+
console.error(
|
|
2981
|
+
`[archangel:${agentName}] Unhandled error:`,
|
|
2982
|
+
err instanceof Error ? err.message : err,
|
|
2983
|
+
);
|
|
2347
2984
|
});
|
|
2348
2985
|
}
|
|
2349
2986
|
|
|
@@ -2364,16 +3001,16 @@ async function cmdDaemon(agentName) {
|
|
|
2364
3001
|
// Check if session still exists periodically
|
|
2365
3002
|
const sessionCheck = setInterval(() => {
|
|
2366
3003
|
if (!tmuxHasSession(sessionName)) {
|
|
2367
|
-
console.log(`[
|
|
3004
|
+
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
2368
3005
|
stopWatching();
|
|
2369
3006
|
clearInterval(sessionCheck);
|
|
2370
3007
|
process.exit(0);
|
|
2371
3008
|
}
|
|
2372
|
-
},
|
|
3009
|
+
}, ARCHANGEL_HEALTH_CHECK_MS);
|
|
2373
3010
|
|
|
2374
3011
|
// Handle graceful shutdown
|
|
2375
3012
|
process.on("SIGTERM", () => {
|
|
2376
|
-
console.log(`[
|
|
3013
|
+
console.log(`[archangel:${agentName}] Received SIGTERM, shutting down`);
|
|
2377
3014
|
stopWatching();
|
|
2378
3015
|
clearInterval(sessionCheck);
|
|
2379
3016
|
tmuxSend(sessionName, "C-c");
|
|
@@ -2384,7 +3021,7 @@ async function cmdDaemon(agentName) {
|
|
|
2384
3021
|
});
|
|
2385
3022
|
|
|
2386
3023
|
process.on("SIGINT", () => {
|
|
2387
|
-
console.log(`[
|
|
3024
|
+
console.log(`[archangel:${agentName}] Received SIGINT, shutting down`);
|
|
2388
3025
|
stopWatching();
|
|
2389
3026
|
clearInterval(sessionCheck);
|
|
2390
3027
|
tmuxSend(sessionName, "C-c");
|
|
@@ -2394,48 +3031,33 @@ async function cmdDaemon(agentName) {
|
|
|
2394
3031
|
}, 500);
|
|
2395
3032
|
});
|
|
2396
3033
|
|
|
2397
|
-
console.log(`[
|
|
3034
|
+
console.log(`[archangel:${agentName}] Watching: ${config.watch.join(", ")}`);
|
|
2398
3035
|
|
|
2399
3036
|
// Keep the process alive
|
|
2400
3037
|
await new Promise(() => {});
|
|
2401
3038
|
}
|
|
2402
3039
|
|
|
2403
3040
|
/**
|
|
2404
|
-
* @param {string}
|
|
2405
|
-
* @param {string | null} [daemonName]
|
|
3041
|
+
* @param {string | null} [name]
|
|
2406
3042
|
*/
|
|
2407
|
-
async function
|
|
2408
|
-
|
|
2409
|
-
console.log("Usage: ./ax.js daemons <start|stop|init> [name]");
|
|
2410
|
-
process.exit(1);
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
// Handle init action separately
|
|
2414
|
-
if (action === "init") {
|
|
2415
|
-
if (!daemonName) {
|
|
2416
|
-
console.log("Usage: ./ax.js daemons init <name>");
|
|
2417
|
-
console.log("Example: ./ax.js daemons init reviewer");
|
|
2418
|
-
process.exit(1);
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
// Validate name (alphanumeric, dashes, underscores only)
|
|
2422
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(daemonName)) {
|
|
2423
|
-
console.log("ERROR: Daemon name must contain only letters, numbers, dashes, and underscores");
|
|
2424
|
-
process.exit(1);
|
|
2425
|
-
}
|
|
3043
|
+
async function cmdSummon(name = null) {
|
|
3044
|
+
const configs = loadAgentConfigs();
|
|
2426
3045
|
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
3046
|
+
// If name provided but doesn't exist, create it
|
|
3047
|
+
if (name) {
|
|
3048
|
+
const exists = configs.some((c) => c.name === name);
|
|
3049
|
+
if (!exists) {
|
|
3050
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
3051
|
+
console.log("ERROR: Name must contain only letters, numbers, dashes, and underscores");
|
|
3052
|
+
process.exit(1);
|
|
3053
|
+
}
|
|
2432
3054
|
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
}
|
|
3055
|
+
if (!existsSync(AGENTS_DIR)) {
|
|
3056
|
+
mkdirSync(AGENTS_DIR, { recursive: true });
|
|
3057
|
+
}
|
|
2437
3058
|
|
|
2438
|
-
|
|
3059
|
+
const agentPath = path.join(AGENTS_DIR, `${name}.md`);
|
|
3060
|
+
const template = `---
|
|
2439
3061
|
tool: claude
|
|
2440
3062
|
watch: ["**/*.{ts,tsx,js,jsx,mjs,mts}"]
|
|
2441
3063
|
interval: 30
|
|
@@ -2443,75 +3065,76 @@ interval: 30
|
|
|
2443
3065
|
|
|
2444
3066
|
Review changed files for bugs, type errors, and edge cases.
|
|
2445
3067
|
`;
|
|
3068
|
+
writeFileSync(agentPath, template);
|
|
3069
|
+
console.log(`Created: ${agentPath}`);
|
|
3070
|
+
console.log(`Edit the file to customize, then run: ax summon ${name}`);
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
2446
3074
|
|
|
2447
|
-
|
|
2448
|
-
console.log(`
|
|
2449
|
-
console.log(`Edit the file to customize the daemon, then run: ./ax.js daemons start ${daemonName}`);
|
|
3075
|
+
if (configs.length === 0) {
|
|
3076
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
2450
3077
|
return;
|
|
2451
3078
|
}
|
|
2452
3079
|
|
|
3080
|
+
const targetConfigs = name ? configs.filter((c) => c.name === name) : configs;
|
|
3081
|
+
|
|
3082
|
+
ensureMailboxHookScript();
|
|
3083
|
+
|
|
3084
|
+
const parentSession = findCurrentClaudeSession();
|
|
3085
|
+
if (parentSession) {
|
|
3086
|
+
console.log(`Parent session: ${parentSession.session || "(non-tmux)"} [${parentSession.uuid}]`);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
for (const config of targetConfigs) {
|
|
3090
|
+
const sessionPattern = getArchangelSessionPattern(config);
|
|
3091
|
+
const existing = findArchangelSession(sessionPattern);
|
|
3092
|
+
|
|
3093
|
+
if (!existing) {
|
|
3094
|
+
startArchangel(config, parentSession);
|
|
3095
|
+
} else {
|
|
3096
|
+
console.log(`Already running: ${config.name} (${existing})`);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
gcMailbox(24);
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
/**
|
|
3104
|
+
* @param {string | null} [name]
|
|
3105
|
+
*/
|
|
3106
|
+
async function cmdRecall(name = null) {
|
|
2453
3107
|
const configs = loadAgentConfigs();
|
|
2454
3108
|
|
|
2455
3109
|
if (configs.length === 0) {
|
|
2456
|
-
console.log(`No
|
|
3110
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
2457
3111
|
return;
|
|
2458
3112
|
}
|
|
2459
3113
|
|
|
2460
|
-
|
|
2461
|
-
const targetConfigs = daemonName
|
|
2462
|
-
? configs.filter((c) => c.name === daemonName)
|
|
2463
|
-
: configs;
|
|
3114
|
+
const targetConfigs = name ? configs.filter((c) => c.name === name) : configs;
|
|
2464
3115
|
|
|
2465
|
-
if (
|
|
2466
|
-
console.log(`ERROR:
|
|
3116
|
+
if (name && targetConfigs.length === 0) {
|
|
3117
|
+
console.log(`ERROR: archangel '${name}' not found in ${AGENTS_DIR}/`);
|
|
2467
3118
|
process.exit(1);
|
|
2468
3119
|
}
|
|
2469
3120
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
}
|
|
3121
|
+
for (const config of targetConfigs) {
|
|
3122
|
+
const sessionPattern = getArchangelSessionPattern(config);
|
|
3123
|
+
const existing = findArchangelSession(sessionPattern);
|
|
2474
3124
|
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
console.log(`
|
|
3125
|
+
if (existing) {
|
|
3126
|
+
tmuxSend(existing, "C-c");
|
|
3127
|
+
await sleep(300);
|
|
3128
|
+
tmuxKill(existing);
|
|
3129
|
+
console.log(`Recalled: ${config.name} (${existing})`);
|
|
2480
3130
|
} else {
|
|
2481
|
-
console.log(
|
|
3131
|
+
console.log(`Not running: ${config.name}`);
|
|
2482
3132
|
}
|
|
2483
3133
|
}
|
|
2484
|
-
|
|
2485
|
-
for (const config of targetConfigs) {
|
|
2486
|
-
const sessionPattern = getDaemonSessionPattern(config);
|
|
2487
|
-
const existing = findDaemonSession(sessionPattern);
|
|
2488
|
-
|
|
2489
|
-
if (action === "stop") {
|
|
2490
|
-
if (existing) {
|
|
2491
|
-
tmuxSend(existing, "C-c");
|
|
2492
|
-
await sleep(300);
|
|
2493
|
-
tmuxKill(existing);
|
|
2494
|
-
console.log(`Stopped daemon: ${config.name} (${existing})`);
|
|
2495
|
-
} else {
|
|
2496
|
-
console.log(`Daemon not running: ${config.name}`);
|
|
2497
|
-
}
|
|
2498
|
-
} else if (action === "start") {
|
|
2499
|
-
if (!existing) {
|
|
2500
|
-
startDaemonAgent(config, parentSession);
|
|
2501
|
-
} else {
|
|
2502
|
-
console.log(`Daemon already running: ${config.name} (${existing})`);
|
|
2503
|
-
}
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
// GC mailbox on start
|
|
2508
|
-
if (action === "start") {
|
|
2509
|
-
gcMailbox(24);
|
|
2510
|
-
}
|
|
2511
3134
|
}
|
|
2512
3135
|
|
|
2513
3136
|
// Version of the hook script template - bump when making changes
|
|
2514
|
-
const HOOK_SCRIPT_VERSION = "
|
|
3137
|
+
const HOOK_SCRIPT_VERSION = "4";
|
|
2515
3138
|
|
|
2516
3139
|
function ensureMailboxHookScript() {
|
|
2517
3140
|
const hooksDir = HOOKS_DIR;
|
|
@@ -2529,34 +3152,54 @@ function ensureMailboxHookScript() {
|
|
|
2529
3152
|
mkdirSync(hooksDir, { recursive: true });
|
|
2530
3153
|
}
|
|
2531
3154
|
|
|
2532
|
-
// Inject absolute paths into the generated script
|
|
2533
|
-
const mailboxPath = path.join(AI_DIR, "mailbox.jsonl");
|
|
2534
|
-
const lastSeenPath = path.join(AI_DIR, "mailbox-last-seen");
|
|
2535
|
-
|
|
2536
3155
|
const hookCode = `#!/usr/bin/env node
|
|
2537
3156
|
${versionMarker}
|
|
2538
|
-
// Auto-generated hook script - do not edit manually
|
|
2539
|
-
|
|
2540
3157
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
3158
|
+
import { dirname, join } from "node:path";
|
|
3159
|
+
import { fileURLToPath } from "node:url";
|
|
3160
|
+
import { createHash } from "node:crypto";
|
|
2541
3161
|
|
|
3162
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3163
|
+
const AI_DIR = join(__dirname, "..");
|
|
2542
3164
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
2543
|
-
const MAILBOX = "
|
|
2544
|
-
const
|
|
2545
|
-
const MAX_AGE_MS = 60 * 60 * 1000; // 1 hour (matches MAILBOX_MAX_AGE_MS)
|
|
3165
|
+
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
3166
|
+
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
2546
3167
|
|
|
2547
|
-
|
|
3168
|
+
// Read hook input from stdin
|
|
3169
|
+
let hookInput = {};
|
|
3170
|
+
try {
|
|
3171
|
+
const stdinData = readFileSync(0, "utf-8").trim();
|
|
3172
|
+
if (stdinData) hookInput = JSON.parse(stdinData);
|
|
3173
|
+
} catch (err) {
|
|
3174
|
+
if (DEBUG) console.error("[hook] stdin parse:", err.message);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
const sessionId = hookInput.session_id || "";
|
|
3178
|
+
const hookEvent = hookInput.hook_event_name || "";
|
|
2548
3179
|
|
|
2549
|
-
|
|
3180
|
+
if (DEBUG) console.error("[hook] session:", sessionId, "event:", hookEvent);
|
|
2550
3181
|
|
|
2551
|
-
//
|
|
2552
|
-
|
|
3182
|
+
// NO-OP for archangel or partner sessions
|
|
3183
|
+
if (sessionId.includes("-archangel-") || sessionId.includes("-partner-")) {
|
|
3184
|
+
if (DEBUG) console.error("[hook] skipping non-parent session");
|
|
3185
|
+
process.exit(0);
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// Per-session last-seen tracking (single JSON file, self-cleaning)
|
|
3189
|
+
const sessionHash = sessionId ? createHash("md5").update(sessionId).digest("hex").slice(0, 8) : "default";
|
|
3190
|
+
const LAST_SEEN_FILE = join(AI_DIR, "mailbox-last-seen.json");
|
|
3191
|
+
|
|
3192
|
+
if (!existsSync(MAILBOX)) process.exit(0);
|
|
3193
|
+
|
|
3194
|
+
let lastSeenMap = {};
|
|
2553
3195
|
try {
|
|
2554
|
-
if (existsSync(
|
|
2555
|
-
|
|
3196
|
+
if (existsSync(LAST_SEEN_FILE)) {
|
|
3197
|
+
lastSeenMap = JSON.parse(readFileSync(LAST_SEEN_FILE, "utf-8"));
|
|
2556
3198
|
}
|
|
2557
3199
|
} catch (err) {
|
|
2558
3200
|
if (DEBUG) console.error("[hook] readLastSeen:", err.message);
|
|
2559
3201
|
}
|
|
3202
|
+
const lastSeen = lastSeenMap[sessionHash] || 0;
|
|
2560
3203
|
|
|
2561
3204
|
const now = Date.now();
|
|
2562
3205
|
const lines = readFileSync(MAILBOX, "utf-8").trim().split("\\n").filter(Boolean);
|
|
@@ -2567,11 +3210,7 @@ for (const line of lines) {
|
|
|
2567
3210
|
const entry = JSON.parse(line);
|
|
2568
3211
|
const ts = new Date(entry.timestamp).getTime();
|
|
2569
3212
|
const age = now - ts;
|
|
2570
|
-
|
|
2571
|
-
// Only show observations within max age and not yet seen
|
|
2572
|
-
// (removed commit filter - too strict when HEAD moves during a session)
|
|
2573
3213
|
if (age < MAX_AGE_MS && ts > lastSeen) {
|
|
2574
|
-
// Extract session prefix (without UUID) for shorter log command
|
|
2575
3214
|
const session = entry.payload.session || "";
|
|
2576
3215
|
const sessionPrefix = session.replace(/-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "");
|
|
2577
3216
|
relevant.push({ agent: entry.payload.agent, sessionPrefix, message: entry.payload.message });
|
|
@@ -2582,22 +3221,39 @@ for (const line of lines) {
|
|
|
2582
3221
|
}
|
|
2583
3222
|
|
|
2584
3223
|
if (relevant.length > 0) {
|
|
2585
|
-
console.log("## Background Agents");
|
|
2586
|
-
console.log("");
|
|
2587
|
-
console.log("Background agents watching your files found:");
|
|
2588
|
-
console.log("");
|
|
2589
3224
|
const sessionPrefixes = new Set();
|
|
3225
|
+
let messageLines = [];
|
|
3226
|
+
messageLines.push("## Background Agents");
|
|
3227
|
+
messageLines.push("");
|
|
3228
|
+
messageLines.push("Background agents watching your files found:");
|
|
3229
|
+
messageLines.push("");
|
|
2590
3230
|
for (const { agent, sessionPrefix, message } of relevant) {
|
|
2591
3231
|
if (sessionPrefix) sessionPrefixes.add(sessionPrefix);
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
3232
|
+
messageLines.push("**[" + agent + "]**");
|
|
3233
|
+
messageLines.push("");
|
|
3234
|
+
messageLines.push(message);
|
|
3235
|
+
messageLines.push("");
|
|
2596
3236
|
}
|
|
2597
3237
|
const sessionList = [...sessionPrefixes].map(s => "\\\`./ax.js log " + s + "\\\`").join(" or ");
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3238
|
+
messageLines.push("> For more context: \\\`./ax.js mailbox\\\`" + (sessionList ? " or " + sessionList : ""));
|
|
3239
|
+
|
|
3240
|
+
const formattedMessage = messageLines.join("\\n");
|
|
3241
|
+
|
|
3242
|
+
// For Stop hook, return blocking JSON to force acknowledgment
|
|
3243
|
+
if (hookEvent === "Stop") {
|
|
3244
|
+
console.log(JSON.stringify({ decision: "block", reason: formattedMessage }));
|
|
3245
|
+
} else {
|
|
3246
|
+
// For other hooks, just output the context
|
|
3247
|
+
console.log(formattedMessage);
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// Update last-seen and prune entries older than 24 hours
|
|
3251
|
+
const PRUNE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
3252
|
+
lastSeenMap[sessionHash] = now;
|
|
3253
|
+
for (const key of Object.keys(lastSeenMap)) {
|
|
3254
|
+
if (now - lastSeenMap[key] > PRUNE_AGE_MS) delete lastSeenMap[key];
|
|
3255
|
+
}
|
|
3256
|
+
writeFileSync(LAST_SEEN_FILE, JSON.stringify(lastSeenMap));
|
|
2601
3257
|
}
|
|
2602
3258
|
|
|
2603
3259
|
process.exit(0);
|
|
@@ -2609,22 +3265,12 @@ process.exit(0);
|
|
|
2609
3265
|
// Configure the hook in .claude/settings.json at the same time
|
|
2610
3266
|
const configuredHook = ensureClaudeHookConfig();
|
|
2611
3267
|
if (!configuredHook) {
|
|
2612
|
-
const hookScriptPath = path.join(HOOKS_DIR, "mailbox-inject.js");
|
|
2613
3268
|
console.log(`\nTo enable manually, add to .claude/settings.json:\n`);
|
|
2614
3269
|
console.log(`{
|
|
2615
3270
|
"hooks": {
|
|
2616
|
-
"UserPromptSubmit": [
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
"hooks": [
|
|
2620
|
-
{
|
|
2621
|
-
"type": "command",
|
|
2622
|
-
"command": "node ${hookScriptPath}",
|
|
2623
|
-
"timeout": 5
|
|
2624
|
-
}
|
|
2625
|
-
]
|
|
2626
|
-
}
|
|
2627
|
-
]
|
|
3271
|
+
"UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
3272
|
+
"PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
3273
|
+
"Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }]
|
|
2628
3274
|
}
|
|
2629
3275
|
}`);
|
|
2630
3276
|
}
|
|
@@ -2633,8 +3279,8 @@ process.exit(0);
|
|
|
2633
3279
|
function ensureClaudeHookConfig() {
|
|
2634
3280
|
const settingsDir = ".claude";
|
|
2635
3281
|
const settingsPath = path.join(settingsDir, "settings.json");
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
3282
|
+
const hookCommand = "node .ai/hooks/mailbox-inject.js";
|
|
3283
|
+
const hookEvents = ["UserPromptSubmit", "PostToolUse", "Stop"];
|
|
2638
3284
|
|
|
2639
3285
|
try {
|
|
2640
3286
|
/** @type {ClaudeSettings} */
|
|
@@ -2653,33 +3299,41 @@ function ensureClaudeHookConfig() {
|
|
|
2653
3299
|
|
|
2654
3300
|
// Ensure hooks structure exists
|
|
2655
3301
|
if (!settings.hooks) settings.hooks = {};
|
|
2656
|
-
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
2657
|
-
|
|
2658
|
-
// Check if our hook is already configured
|
|
2659
|
-
const hookExists = settings.hooks.UserPromptSubmit.some(
|
|
2660
|
-
/** @param {{hooks?: Array<{command: string}>}} entry */
|
|
2661
|
-
(entry) => entry.hooks?.some(/** @param {{command: string}} h */ (h) => h.command === hookCommand)
|
|
2662
|
-
);
|
|
2663
3302
|
|
|
2664
|
-
|
|
2665
|
-
|
|
3303
|
+
let anyAdded = false;
|
|
3304
|
+
|
|
3305
|
+
for (const eventName of hookEvents) {
|
|
3306
|
+
if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
|
|
3307
|
+
|
|
3308
|
+
// Check if our hook is already configured for this event
|
|
3309
|
+
const hookExists = settings.hooks[eventName].some(
|
|
3310
|
+
/** @param {{hooks?: Array<{command: string}>}} entry */
|
|
3311
|
+
(entry) =>
|
|
3312
|
+
entry.hooks?.some(/** @param {{command: string}} h */ (h) => h.command === hookCommand),
|
|
3313
|
+
);
|
|
3314
|
+
|
|
3315
|
+
if (!hookExists) {
|
|
3316
|
+
// Add the hook for this event
|
|
3317
|
+
settings.hooks[eventName].push({
|
|
3318
|
+
matcher: "",
|
|
3319
|
+
hooks: [
|
|
3320
|
+
{
|
|
3321
|
+
type: "command",
|
|
3322
|
+
command: hookCommand,
|
|
3323
|
+
timeout: 5,
|
|
3324
|
+
},
|
|
3325
|
+
],
|
|
3326
|
+
});
|
|
3327
|
+
anyAdded = true;
|
|
3328
|
+
}
|
|
2666
3329
|
}
|
|
2667
3330
|
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
hooks:
|
|
2672
|
-
|
|
2673
|
-
type: "command",
|
|
2674
|
-
command: hookCommand,
|
|
2675
|
-
timeout: 5,
|
|
2676
|
-
},
|
|
2677
|
-
],
|
|
2678
|
-
});
|
|
3331
|
+
if (anyAdded) {
|
|
3332
|
+
// Write settings
|
|
3333
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3334
|
+
console.log(`Configured hooks in: ${settingsPath}`);
|
|
3335
|
+
}
|
|
2679
3336
|
|
|
2680
|
-
// Write settings
|
|
2681
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2682
|
-
console.log(`Configured hook in: ${settingsPath}`);
|
|
2683
3337
|
return true;
|
|
2684
3338
|
} catch {
|
|
2685
3339
|
// If we can't configure automatically, return false so manual instructions are shown
|
|
@@ -2689,9 +3343,31 @@ function ensureClaudeHookConfig() {
|
|
|
2689
3343
|
|
|
2690
3344
|
/**
|
|
2691
3345
|
* @param {string | null | undefined} session
|
|
2692
|
-
* @param {{all?: boolean}} [options]
|
|
3346
|
+
* @param {{all?: boolean, orphans?: boolean, force?: boolean}} [options]
|
|
2693
3347
|
*/
|
|
2694
|
-
function cmdKill(session, { all = false } = {}) {
|
|
3348
|
+
function cmdKill(session, { all = false, orphans = false, force = false } = {}) {
|
|
3349
|
+
// Handle orphaned processes
|
|
3350
|
+
if (orphans) {
|
|
3351
|
+
const orphanedProcesses = findOrphanedProcesses();
|
|
3352
|
+
|
|
3353
|
+
if (orphanedProcesses.length === 0) {
|
|
3354
|
+
console.log("No orphaned processes found");
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
const signal = force ? "-9" : "-15"; // SIGKILL vs SIGTERM
|
|
3359
|
+
let killed = 0;
|
|
3360
|
+
for (const { pid, command } of orphanedProcesses) {
|
|
3361
|
+
const result = spawnSync("kill", [signal, pid]);
|
|
3362
|
+
if (result.status === 0) {
|
|
3363
|
+
console.log(`Killed: PID ${pid} (${command.slice(0, 40)})`);
|
|
3364
|
+
killed++;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
console.log(`Killed ${killed} orphaned process(es)${force ? " (forced)" : ""}`);
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
|
|
2695
3371
|
// If specific session provided, kill just that one
|
|
2696
3372
|
if (session) {
|
|
2697
3373
|
if (!tmuxHasSession(session)) {
|
|
@@ -2751,7 +3427,9 @@ function cmdAttach(session) {
|
|
|
2751
3427
|
}
|
|
2752
3428
|
|
|
2753
3429
|
// Hand over to tmux attach
|
|
2754
|
-
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
3430
|
+
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
3431
|
+
stdio: "inherit",
|
|
3432
|
+
});
|
|
2755
3433
|
process.exit(result.status || 0);
|
|
2756
3434
|
}
|
|
2757
3435
|
|
|
@@ -2810,13 +3488,15 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2810
3488
|
|
|
2811
3489
|
if (newLines.length === 0) return;
|
|
2812
3490
|
|
|
2813
|
-
const entries = newLines
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3491
|
+
const entries = newLines
|
|
3492
|
+
.map((line) => {
|
|
3493
|
+
try {
|
|
3494
|
+
return JSON.parse(line);
|
|
3495
|
+
} catch {
|
|
3496
|
+
return null;
|
|
3497
|
+
}
|
|
3498
|
+
})
|
|
3499
|
+
.filter(Boolean);
|
|
2820
3500
|
|
|
2821
3501
|
const output = [];
|
|
2822
3502
|
if (isInitial) {
|
|
@@ -2829,7 +3509,10 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2829
3509
|
const ts = entry.timestamp || entry.ts || entry.createdAt;
|
|
2830
3510
|
if (ts && ts !== lastTimestamp) {
|
|
2831
3511
|
const date = new Date(ts);
|
|
2832
|
-
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
3512
|
+
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
3513
|
+
hour: "2-digit",
|
|
3514
|
+
minute: "2-digit",
|
|
3515
|
+
});
|
|
2833
3516
|
if (formatted.isUserMessage) {
|
|
2834
3517
|
output.push(`\n### ${timeStr}\n`);
|
|
2835
3518
|
}
|
|
@@ -2879,7 +3562,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2879
3562
|
if (type === "user" || type === "human") {
|
|
2880
3563
|
const text = extractTextContent(content);
|
|
2881
3564
|
if (text) {
|
|
2882
|
-
return {
|
|
3565
|
+
return {
|
|
3566
|
+
text: `**User**: ${truncate(text, TRUNCATE_USER_LEN)}\n`,
|
|
3567
|
+
isUserMessage: true,
|
|
3568
|
+
};
|
|
2883
3569
|
}
|
|
2884
3570
|
}
|
|
2885
3571
|
|
|
@@ -2895,10 +3581,12 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2895
3581
|
// Extract tool calls (compressed)
|
|
2896
3582
|
const tools = extractToolCalls(content);
|
|
2897
3583
|
if (tools.length > 0) {
|
|
2898
|
-
const toolSummary = tools
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3584
|
+
const toolSummary = tools
|
|
3585
|
+
.map((t) => {
|
|
3586
|
+
if (t.error) return `${t.name}(${t.target}) ✗`;
|
|
3587
|
+
return `${t.name}(${t.target})`;
|
|
3588
|
+
})
|
|
3589
|
+
.join(", ");
|
|
2902
3590
|
parts.push(`> ${toolSummary}\n`);
|
|
2903
3591
|
}
|
|
2904
3592
|
|
|
@@ -2920,7 +3608,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2920
3608
|
const error = entry.error || entry.is_error;
|
|
2921
3609
|
if (error) {
|
|
2922
3610
|
const name = entry.tool_name || entry.name || "tool";
|
|
2923
|
-
return {
|
|
3611
|
+
return {
|
|
3612
|
+
text: `> ${name} ✗ (${truncate(String(error), 100)})\n`,
|
|
3613
|
+
isUserMessage: false,
|
|
3614
|
+
};
|
|
2924
3615
|
}
|
|
2925
3616
|
}
|
|
2926
3617
|
|
|
@@ -2957,7 +3648,8 @@ function extractToolCalls(content) {
|
|
|
2957
3648
|
const name = c.name || c.tool || "tool";
|
|
2958
3649
|
const input = c.input || c.arguments || {};
|
|
2959
3650
|
// Extract a reasonable target from the input
|
|
2960
|
-
const target =
|
|
3651
|
+
const target =
|
|
3652
|
+
input.file_path || input.path || input.command?.slice(0, 30) || input.pattern || "";
|
|
2961
3653
|
const shortTarget = target.split("/").pop() || target.slice(0, 20);
|
|
2962
3654
|
return { name, target: shortTarget, error: c.error };
|
|
2963
3655
|
});
|
|
@@ -3002,8 +3694,14 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3002
3694
|
|
|
3003
3695
|
for (const entry of entries) {
|
|
3004
3696
|
const ts = new Date(entry.timestamp);
|
|
3005
|
-
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
3006
|
-
|
|
3697
|
+
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
3698
|
+
hour: "2-digit",
|
|
3699
|
+
minute: "2-digit",
|
|
3700
|
+
});
|
|
3701
|
+
const dateStr = ts.toLocaleDateString("en-GB", {
|
|
3702
|
+
month: "short",
|
|
3703
|
+
day: "numeric",
|
|
3704
|
+
});
|
|
3007
3705
|
const p = entry.payload || {};
|
|
3008
3706
|
|
|
3009
3707
|
console.log(`### [${p.agent || "unknown"}] ${dateStr} ${timeStr}\n`);
|
|
@@ -3032,7 +3730,7 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3032
3730
|
* @param {string} message
|
|
3033
3731
|
* @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
|
|
3034
3732
|
*/
|
|
3035
|
-
async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs } = {}) {
|
|
3733
|
+
async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
3036
3734
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3037
3735
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3038
3736
|
|
|
@@ -3044,20 +3742,31 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3044
3742
|
}
|
|
3045
3743
|
|
|
3046
3744
|
/** @type {string} */
|
|
3047
|
-
const activeSession = sessionExists
|
|
3745
|
+
const activeSession = sessionExists
|
|
3746
|
+
? /** @type {string} */ (session)
|
|
3747
|
+
: await cmdStart(agent, session, { yolo });
|
|
3048
3748
|
|
|
3049
3749
|
tmuxSendLiteral(activeSession, message);
|
|
3050
3750
|
await sleep(50);
|
|
3051
3751
|
tmuxSend(activeSession, "Enter");
|
|
3052
3752
|
|
|
3053
|
-
if (noWait)
|
|
3753
|
+
if (noWait) {
|
|
3754
|
+
const parsed = parseSessionName(activeSession);
|
|
3755
|
+
const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
|
|
3756
|
+
const cli = path.basename(process.argv[1], ".js");
|
|
3757
|
+
console.log(`Sent to: ${shortId}
|
|
3758
|
+
|
|
3759
|
+
e.g.
|
|
3760
|
+
${cli} status --session=${shortId}
|
|
3761
|
+
${cli} output --session=${shortId}`);
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3054
3764
|
|
|
3055
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3056
3765
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3057
3766
|
|
|
3058
3767
|
const { state, screen } = useAutoApprove
|
|
3059
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3060
|
-
: await
|
|
3768
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3769
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3061
3770
|
|
|
3062
3771
|
if (state === State.RATE_LIMITED) {
|
|
3063
3772
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3065,14 +3774,9 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3065
3774
|
}
|
|
3066
3775
|
|
|
3067
3776
|
if (state === State.CONFIRMING) {
|
|
3068
|
-
console.log(`CONFIRM: ${
|
|
3777
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3069
3778
|
process.exit(3);
|
|
3070
3779
|
}
|
|
3071
|
-
|
|
3072
|
-
const output = agent.getResponse(activeSession, screen);
|
|
3073
|
-
if (output) {
|
|
3074
|
-
console.log(output);
|
|
3075
|
-
}
|
|
3076
3780
|
}
|
|
3077
3781
|
|
|
3078
3782
|
/**
|
|
@@ -3087,9 +3791,10 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3087
3791
|
}
|
|
3088
3792
|
|
|
3089
3793
|
const before = tmuxCapture(session);
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3794
|
+
const beforeState = agent.getState(before);
|
|
3795
|
+
if (beforeState !== State.CONFIRMING) {
|
|
3796
|
+
console.log(`Already ${beforeState}`);
|
|
3797
|
+
return;
|
|
3093
3798
|
}
|
|
3094
3799
|
|
|
3095
3800
|
tmuxSend(session, agent.approveKey);
|
|
@@ -3104,7 +3809,7 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3104
3809
|
}
|
|
3105
3810
|
|
|
3106
3811
|
if (state === State.CONFIRMING) {
|
|
3107
|
-
console.log(`CONFIRM: ${
|
|
3812
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3108
3813
|
process.exit(3);
|
|
3109
3814
|
}
|
|
3110
3815
|
|
|
@@ -3123,6 +3828,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3123
3828
|
process.exit(1);
|
|
3124
3829
|
}
|
|
3125
3830
|
|
|
3831
|
+
const before = tmuxCapture(session);
|
|
3832
|
+
const beforeState = agent.getState(before);
|
|
3833
|
+
if (beforeState !== State.CONFIRMING) {
|
|
3834
|
+
console.log(`Already ${beforeState}`);
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3126
3838
|
tmuxSend(session, agent.rejectKey);
|
|
3127
3839
|
|
|
3128
3840
|
if (!wait) return;
|
|
@@ -3145,7 +3857,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3145
3857
|
* @param {string | null | undefined} customInstructions
|
|
3146
3858
|
* @param {{wait?: boolean, yolo?: boolean, fresh?: boolean, timeoutMs?: number}} [options]
|
|
3147
3859
|
*/
|
|
3148
|
-
async function cmdReview(
|
|
3860
|
+
async function cmdReview(
|
|
3861
|
+
agent,
|
|
3862
|
+
session,
|
|
3863
|
+
option,
|
|
3864
|
+
customInstructions,
|
|
3865
|
+
{ wait = true, yolo = false, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
|
|
3866
|
+
) {
|
|
3149
3867
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3150
3868
|
|
|
3151
3869
|
// Reset conversation if --fresh and session exists
|
|
@@ -3171,7 +3889,11 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3171
3889
|
|
|
3172
3890
|
// AX_REVIEW_MODE=exec: bypass /review command, send instructions directly
|
|
3173
3891
|
if (process.env.AX_REVIEW_MODE === "exec" && option === "custom" && customInstructions) {
|
|
3174
|
-
return cmdAsk(agent, session, customInstructions, {
|
|
3892
|
+
return cmdAsk(agent, session, customInstructions, {
|
|
3893
|
+
noWait: !wait,
|
|
3894
|
+
yolo,
|
|
3895
|
+
timeoutMs,
|
|
3896
|
+
});
|
|
3175
3897
|
}
|
|
3176
3898
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3177
3899
|
|
|
@@ -3183,7 +3905,9 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3183
3905
|
}
|
|
3184
3906
|
|
|
3185
3907
|
/** @type {string} */
|
|
3186
|
-
const activeSession = sessionExists
|
|
3908
|
+
const activeSession = sessionExists
|
|
3909
|
+
? /** @type {string} */ (session)
|
|
3910
|
+
: await cmdStart(agent, session, { yolo });
|
|
3187
3911
|
|
|
3188
3912
|
tmuxSendLiteral(activeSession, "/review");
|
|
3189
3913
|
await sleep(50);
|
|
@@ -3205,12 +3929,11 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3205
3929
|
|
|
3206
3930
|
if (!wait) return;
|
|
3207
3931
|
|
|
3208
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3209
3932
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3210
3933
|
|
|
3211
3934
|
const { state, screen } = useAutoApprove
|
|
3212
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3213
|
-
: await
|
|
3935
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3936
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3214
3937
|
|
|
3215
3938
|
if (state === State.RATE_LIMITED) {
|
|
3216
3939
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3218,12 +3941,9 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3218
3941
|
}
|
|
3219
3942
|
|
|
3220
3943
|
if (state === State.CONFIRMING) {
|
|
3221
|
-
console.log(`CONFIRM: ${
|
|
3944
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3222
3945
|
process.exit(3);
|
|
3223
3946
|
}
|
|
3224
|
-
|
|
3225
|
-
const response = agent.getResponse(activeSession, screen);
|
|
3226
|
-
console.log(response || "");
|
|
3227
3947
|
}
|
|
3228
3948
|
|
|
3229
3949
|
/**
|
|
@@ -3254,7 +3974,7 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3254
3974
|
}
|
|
3255
3975
|
|
|
3256
3976
|
if (state === State.CONFIRMING) {
|
|
3257
|
-
console.log(`CONFIRM: ${
|
|
3977
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3258
3978
|
process.exit(3);
|
|
3259
3979
|
}
|
|
3260
3980
|
|
|
@@ -3266,6 +3986,8 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3266
3986
|
const output = agent.getResponse(session, screen, index);
|
|
3267
3987
|
if (output) {
|
|
3268
3988
|
console.log(output);
|
|
3989
|
+
} else {
|
|
3990
|
+
console.log("READY_NO_CONTENT");
|
|
3269
3991
|
}
|
|
3270
3992
|
}
|
|
3271
3993
|
|
|
@@ -3288,7 +4010,7 @@ function cmdStatus(agent, session) {
|
|
|
3288
4010
|
}
|
|
3289
4011
|
|
|
3290
4012
|
if (state === State.CONFIRMING) {
|
|
3291
|
-
console.log(`CONFIRM: ${
|
|
4013
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3292
4014
|
process.exit(3);
|
|
3293
4015
|
}
|
|
3294
4016
|
|
|
@@ -3296,6 +4018,10 @@ function cmdStatus(agent, session) {
|
|
|
3296
4018
|
console.log("THINKING");
|
|
3297
4019
|
process.exit(4);
|
|
3298
4020
|
}
|
|
4021
|
+
|
|
4022
|
+
// READY (or STARTING/UPDATE_PROMPT which are transient)
|
|
4023
|
+
console.log("READY");
|
|
4024
|
+
process.exit(0);
|
|
3299
4025
|
}
|
|
3300
4026
|
|
|
3301
4027
|
/**
|
|
@@ -3409,7 +4135,7 @@ async function cmdSelect(agent, session, n, { wait = false, timeoutMs } = {}) {
|
|
|
3409
4135
|
}
|
|
3410
4136
|
|
|
3411
4137
|
if (state === State.CONFIRMING) {
|
|
3412
|
-
console.log(`CONFIRM: ${
|
|
4138
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3413
4139
|
process.exit(3);
|
|
3414
4140
|
}
|
|
3415
4141
|
|
|
@@ -3422,20 +4148,43 @@ async function cmdSelect(agent, session, n, { wait = false, timeoutMs } = {}) {
|
|
|
3422
4148
|
// =============================================================================
|
|
3423
4149
|
|
|
3424
4150
|
/**
|
|
3425
|
-
*
|
|
4151
|
+
* Resolve the agent to use based on (in priority order):
|
|
4152
|
+
* 1. Explicit --tool flag
|
|
4153
|
+
* 2. Session name (e.g., "claude-archangel-..." → ClaudeAgent)
|
|
4154
|
+
* 3. CLI invocation name (axclaude, axcodex)
|
|
4155
|
+
* 4. AX_DEFAULT_TOOL environment variable
|
|
4156
|
+
* 5. Default to CodexAgent
|
|
4157
|
+
*
|
|
4158
|
+
* @param {{toolFlag?: string, sessionName?: string | null}} options
|
|
4159
|
+
* @returns {{agent: Agent, error?: string}}
|
|
3426
4160
|
*/
|
|
3427
|
-
function
|
|
4161
|
+
function resolveAgent({ toolFlag, sessionName } = {}) {
|
|
4162
|
+
// 1. Explicit --tool flag takes highest priority
|
|
4163
|
+
if (toolFlag) {
|
|
4164
|
+
if (toolFlag === "claude") return { agent: ClaudeAgent };
|
|
4165
|
+
if (toolFlag === "codex") return { agent: CodexAgent };
|
|
4166
|
+
return { agent: CodexAgent, error: `unknown tool '${toolFlag}'` };
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
// 2. Infer from session name (e.g., "claude-archangel-..." or "codex-partner-...")
|
|
4170
|
+
if (sessionName) {
|
|
4171
|
+
const parsed = parseSessionName(sessionName);
|
|
4172
|
+
if (parsed?.tool === "claude") return { agent: ClaudeAgent };
|
|
4173
|
+
if (parsed?.tool === "codex") return { agent: CodexAgent };
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
// 3. CLI invocation name
|
|
3428
4177
|
const invoked = path.basename(process.argv[1], ".js");
|
|
3429
|
-
if (invoked === "axclaude" || invoked === "claude") return ClaudeAgent;
|
|
3430
|
-
if (invoked === "axcodex" || invoked === "codex") return CodexAgent;
|
|
4178
|
+
if (invoked === "axclaude" || invoked === "claude") return { agent: ClaudeAgent };
|
|
4179
|
+
if (invoked === "axcodex" || invoked === "codex") return { agent: CodexAgent };
|
|
3431
4180
|
|
|
3432
|
-
//
|
|
4181
|
+
// 4. AX_DEFAULT_TOOL environment variable
|
|
3433
4182
|
const defaultTool = process.env.AX_DEFAULT_TOOL;
|
|
3434
|
-
if (defaultTool === "claude") return ClaudeAgent;
|
|
3435
|
-
if (defaultTool === "codex" || !defaultTool) return CodexAgent;
|
|
4183
|
+
if (defaultTool === "claude") return { agent: ClaudeAgent };
|
|
4184
|
+
if (defaultTool === "codex" || !defaultTool) return { agent: CodexAgent };
|
|
3436
4185
|
|
|
3437
4186
|
console.error(`WARNING: invalid AX_DEFAULT_TOOL="${defaultTool}", using codex`);
|
|
3438
|
-
return CodexAgent;
|
|
4187
|
+
return { agent: CodexAgent };
|
|
3439
4188
|
}
|
|
3440
4189
|
|
|
3441
4190
|
/**
|
|
@@ -3444,23 +4193,30 @@ function getAgentFromInvocation() {
|
|
|
3444
4193
|
*/
|
|
3445
4194
|
function printHelp(agent, cliName) {
|
|
3446
4195
|
const name = cliName;
|
|
3447
|
-
const backendName = agent.
|
|
4196
|
+
const backendName = agent.displayName;
|
|
3448
4197
|
const hasReview = !!agent.reviewOptions;
|
|
3449
4198
|
|
|
3450
|
-
console.log(`${name}
|
|
4199
|
+
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
4200
|
+
|
|
4201
|
+
Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
3451
4202
|
|
|
3452
4203
|
Commands:
|
|
3453
4204
|
agents List all running agents with state and log paths
|
|
4205
|
+
target Show default target session for current tool
|
|
3454
4206
|
attach [SESSION] Attach to agent session interactively
|
|
3455
4207
|
log SESSION View conversation log (--tail=N, --follow, --reasoning)
|
|
3456
|
-
mailbox View
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
kill Kill sessions
|
|
4208
|
+
mailbox View archangel observations (--limit=N, --branch=X, --all)
|
|
4209
|
+
summon [name] Summon archangels (all, or by name)
|
|
4210
|
+
recall [name] Recall archangels (all, or by name)
|
|
4211
|
+
kill Kill sessions (--all, --session=NAME, --orphans [--force])
|
|
3460
4212
|
status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
|
|
3461
4213
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
3462
|
-
debug Show raw screen output and detected state${
|
|
3463
|
-
|
|
4214
|
+
debug Show raw screen output and detected state${
|
|
4215
|
+
hasReview
|
|
4216
|
+
? `
|
|
4217
|
+
review [TYPE] Review code: pr, uncommitted, commit, custom`
|
|
4218
|
+
: ""
|
|
4219
|
+
}
|
|
3464
4220
|
select N Select menu option N
|
|
3465
4221
|
approve Approve pending action (send 'y')
|
|
3466
4222
|
reject Reject pending action (send 'n')
|
|
@@ -3471,37 +4227,42 @@ Commands:
|
|
|
3471
4227
|
|
|
3472
4228
|
Flags:
|
|
3473
4229
|
--tool=NAME Use specific agent (codex, claude)
|
|
3474
|
-
--session=NAME Target session by name,
|
|
3475
|
-
--wait Wait for response (for
|
|
3476
|
-
--no-wait
|
|
3477
|
-
--timeout=N Set timeout in seconds (default:
|
|
4230
|
+
--session=NAME Target session by name, archangel name, or UUID prefix (self = current)
|
|
4231
|
+
--wait Wait for response (default for messages; required for approve/reject)
|
|
4232
|
+
--no-wait Fire-and-forget: send message, print session ID, exit immediately
|
|
4233
|
+
--timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
|
|
3478
4234
|
--yolo Skip all confirmations (dangerous)
|
|
3479
4235
|
--fresh Reset conversation before review
|
|
4236
|
+
--orphans Kill orphaned claude/codex processes (PPID=1)
|
|
4237
|
+
--force Use SIGKILL instead of SIGTERM (with --orphans)
|
|
3480
4238
|
|
|
3481
4239
|
Environment:
|
|
3482
|
-
AX_DEFAULT_TOOL
|
|
3483
|
-
${agent.envVar}
|
|
3484
|
-
AX_CLAUDE_CONFIG_DIR
|
|
3485
|
-
AX_CODEX_CONFIG_DIR
|
|
3486
|
-
AX_REVIEW_MODE=exec
|
|
3487
|
-
AX_DEBUG=1
|
|
4240
|
+
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
4241
|
+
${agent.envVar} Override default session name
|
|
4242
|
+
AX_CLAUDE_CONFIG_DIR Override Claude config directory (default: ~/.claude)
|
|
4243
|
+
AX_CODEX_CONFIG_DIR Override Codex config directory (default: ~/.codex)
|
|
4244
|
+
AX_REVIEW_MODE=exec Bypass /review, send instructions directly (codex only)
|
|
4245
|
+
AX_DEBUG=1 Enable debug logging
|
|
3488
4246
|
|
|
3489
4247
|
Examples:
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
4248
|
+
${name} "explain this codebase"
|
|
4249
|
+
${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
|
|
4250
|
+
${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
|
|
4251
|
+
${name} review uncommitted --wait
|
|
4252
|
+
${name} approve --wait
|
|
4253
|
+
${name} kill # Kill agents in current project
|
|
4254
|
+
${name} kill --all # Kill all agents across all projects
|
|
4255
|
+
${name} kill --session=NAME # Kill specific session
|
|
4256
|
+
${name} send "1[Enter]" # Recovery: select option 1 and press Enter
|
|
4257
|
+
${name} send "[Escape][Escape]" # Recovery: escape out of a dialog
|
|
4258
|
+
${name} summon # Summon all archangels from .ai/agents/*.md
|
|
4259
|
+
${name} summon reviewer # Summon by name (creates config if new)
|
|
4260
|
+
${name} recall # Recall all archangels
|
|
4261
|
+
${name} recall reviewer # Recall one by name
|
|
4262
|
+
${name} agents # List all agents (shows TYPE=archangel)
|
|
4263
|
+
|
|
4264
|
+
Note: Reviews and complex tasks may take several minutes.
|
|
4265
|
+
Use Bash run_in_background for long operations (not --no-wait).`);
|
|
3505
4266
|
}
|
|
3506
4267
|
|
|
3507
4268
|
async function main() {
|
|
@@ -3516,33 +4277,21 @@ async function main() {
|
|
|
3516
4277
|
const args = process.argv.slice(2);
|
|
3517
4278
|
const cliName = path.basename(process.argv[1], ".js");
|
|
3518
4279
|
|
|
3519
|
-
// Parse flags
|
|
3520
|
-
const
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
const follow = args.includes("--follow") || args.includes("-f");
|
|
3526
|
-
|
|
3527
|
-
// Agent selection
|
|
3528
|
-
let agent = getAgentFromInvocation();
|
|
3529
|
-
const toolArg = args.find((a) => a.startsWith("--tool="));
|
|
3530
|
-
if (toolArg) {
|
|
3531
|
-
const tool = toolArg.split("=")[1];
|
|
3532
|
-
if (tool === "claude") agent = ClaudeAgent;
|
|
3533
|
-
else if (tool === "codex") agent = CodexAgent;
|
|
3534
|
-
else {
|
|
3535
|
-
console.log(`ERROR: unknown tool '${tool}'`);
|
|
3536
|
-
process.exit(1);
|
|
3537
|
-
}
|
|
4280
|
+
// Parse all flags and positionals in one place
|
|
4281
|
+
const { flags, positionals } = parseCliArgs(args);
|
|
4282
|
+
|
|
4283
|
+
if (flags.version) {
|
|
4284
|
+
console.log(VERSION);
|
|
4285
|
+
process.exit(0);
|
|
3538
4286
|
}
|
|
3539
4287
|
|
|
3540
|
-
//
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
4288
|
+
// Extract flags into local variables for convenience
|
|
4289
|
+
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force } = flags;
|
|
4290
|
+
|
|
4291
|
+
// Session resolution (must happen before agent resolution so we can infer tool from session name)
|
|
4292
|
+
let session = null;
|
|
4293
|
+
if (flags.session) {
|
|
4294
|
+
if (flags.session === "self") {
|
|
3546
4295
|
const current = tmuxCurrentSession();
|
|
3547
4296
|
if (!current) {
|
|
3548
4297
|
console.log("ERROR: --session=self requires running inside tmux");
|
|
@@ -3550,99 +4299,106 @@ async function main() {
|
|
|
3550
4299
|
}
|
|
3551
4300
|
session = current;
|
|
3552
4301
|
} else {
|
|
3553
|
-
// Resolve partial names,
|
|
3554
|
-
session = resolveSessionName(
|
|
4302
|
+
// Resolve partial names, archangel names, and UUID prefixes
|
|
4303
|
+
session = resolveSessionName(flags.session);
|
|
3555
4304
|
}
|
|
3556
4305
|
}
|
|
3557
4306
|
|
|
3558
|
-
//
|
|
4307
|
+
// Agent resolution (considers --tool flag, session name, invocation, and env vars)
|
|
4308
|
+
const { agent, error: agentError } = resolveAgent({ toolFlag: flags.tool, sessionName: session });
|
|
4309
|
+
if (agentError) {
|
|
4310
|
+
console.log(`ERROR: ${agentError}`);
|
|
4311
|
+
process.exit(1);
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
// If no explicit session, use agent's default
|
|
4315
|
+
if (!session) {
|
|
4316
|
+
session = agent.getDefaultSession();
|
|
4317
|
+
}
|
|
4318
|
+
|
|
4319
|
+
// Timeout (convert seconds to milliseconds)
|
|
3559
4320
|
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
const val = parseInt(timeoutArg.split("=")[1], 10);
|
|
3563
|
-
if (isNaN(val) || val <= 0) {
|
|
4321
|
+
if (flags.timeout !== undefined) {
|
|
4322
|
+
if (isNaN(flags.timeout) || flags.timeout <= 0) {
|
|
3564
4323
|
console.log("ERROR: invalid timeout");
|
|
3565
4324
|
process.exit(1);
|
|
3566
4325
|
}
|
|
3567
|
-
timeoutMs =
|
|
4326
|
+
timeoutMs = flags.timeout * 1000;
|
|
3568
4327
|
}
|
|
3569
4328
|
|
|
3570
4329
|
// Tail (for log command)
|
|
3571
|
-
|
|
3572
|
-
const tailArg = args.find((a) => a.startsWith("--tail="));
|
|
3573
|
-
if (tailArg) {
|
|
3574
|
-
tail = parseInt(tailArg.split("=")[1], 10) || 50;
|
|
3575
|
-
}
|
|
4330
|
+
const tail = flags.tail ?? 50;
|
|
3576
4331
|
|
|
3577
4332
|
// Limit (for mailbox command)
|
|
3578
|
-
|
|
3579
|
-
const limitArg = args.find((a) => a.startsWith("--limit="));
|
|
3580
|
-
if (limitArg) {
|
|
3581
|
-
limit = parseInt(limitArg.split("=")[1], 10) || 20;
|
|
3582
|
-
}
|
|
4333
|
+
const limit = flags.limit ?? 20;
|
|
3583
4334
|
|
|
3584
4335
|
// Branch filter (for mailbox command)
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
}
|
|
3590
|
-
|
|
3591
|
-
// All flag (for mailbox command - show all regardless of age)
|
|
3592
|
-
const all = args.includes("--all");
|
|
3593
|
-
|
|
3594
|
-
// Filter out flags
|
|
3595
|
-
const filteredArgs = args.filter(
|
|
3596
|
-
(a) =>
|
|
3597
|
-
!["--wait", "--no-wait", "--yolo", "--reasoning", "--follow", "-f", "--all"].includes(a) &&
|
|
3598
|
-
!a.startsWith("--timeout") &&
|
|
3599
|
-
!a.startsWith("--session") &&
|
|
3600
|
-
!a.startsWith("--tool") &&
|
|
3601
|
-
!a.startsWith("--tail") &&
|
|
3602
|
-
!a.startsWith("--limit") &&
|
|
3603
|
-
!a.startsWith("--branch")
|
|
3604
|
-
);
|
|
3605
|
-
const cmd = filteredArgs[0];
|
|
4336
|
+
const branch = flags.branch ?? null;
|
|
4337
|
+
|
|
4338
|
+
// Command is first positional
|
|
4339
|
+
const cmd = positionals[0];
|
|
3606
4340
|
|
|
3607
4341
|
// Dispatch commands
|
|
3608
4342
|
if (cmd === "agents") return cmdAgents();
|
|
3609
|
-
if (cmd === "
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
4343
|
+
if (cmd === "target") {
|
|
4344
|
+
const defaultSession = agent.getDefaultSession();
|
|
4345
|
+
if (defaultSession) {
|
|
4346
|
+
console.log(defaultSession);
|
|
4347
|
+
} else {
|
|
4348
|
+
console.log("NO_TARGET");
|
|
4349
|
+
process.exit(1);
|
|
4350
|
+
}
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
if (cmd === "summon") return cmdSummon(positionals[1]);
|
|
4354
|
+
if (cmd === "recall") return cmdRecall(positionals[1]);
|
|
4355
|
+
if (cmd === "archangel") return cmdArchangel(positionals[1]);
|
|
4356
|
+
if (cmd === "kill") return cmdKill(session, { all, orphans, force });
|
|
4357
|
+
if (cmd === "attach") return cmdAttach(positionals[1] || session);
|
|
4358
|
+
if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
|
|
3614
4359
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
3615
4360
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
3616
4361
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
3617
|
-
if (cmd === "review")
|
|
4362
|
+
if (cmd === "review")
|
|
4363
|
+
return cmdReview(agent, session, positionals[1], positionals[2], {
|
|
4364
|
+
wait,
|
|
4365
|
+
fresh,
|
|
4366
|
+
timeoutMs,
|
|
4367
|
+
});
|
|
3618
4368
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
3619
4369
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
3620
4370
|
if (cmd === "output") {
|
|
3621
|
-
const indexArg =
|
|
4371
|
+
const indexArg = positionals[1];
|
|
3622
4372
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
3623
4373
|
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
3624
4374
|
}
|
|
3625
|
-
if (cmd === "send" &&
|
|
4375
|
+
if (cmd === "send" && positionals.length > 1)
|
|
4376
|
+
return cmdSend(session, positionals.slice(1).join(" "));
|
|
3626
4377
|
if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
|
|
3627
4378
|
if (cmd === "reset") return cmdAsk(agent, session, "/new", { noWait: true, timeoutMs });
|
|
3628
|
-
if (cmd === "select" &&
|
|
4379
|
+
if (cmd === "select" && positionals[1])
|
|
4380
|
+
return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
|
|
3629
4381
|
|
|
3630
4382
|
// Default: send message
|
|
3631
|
-
let message =
|
|
4383
|
+
let message = positionals.join(" ");
|
|
3632
4384
|
if (!message && hasStdinData()) {
|
|
3633
4385
|
message = await readStdin();
|
|
3634
4386
|
}
|
|
3635
4387
|
|
|
3636
|
-
if (!message ||
|
|
4388
|
+
if (!message || flags.help) {
|
|
3637
4389
|
printHelp(agent, cliName);
|
|
3638
4390
|
process.exit(0);
|
|
3639
4391
|
}
|
|
3640
4392
|
|
|
3641
|
-
// Detect "please review" and route to custom review mode
|
|
3642
|
-
const reviewMatch = message.match(/^please review\s*(.*)/i);
|
|
4393
|
+
// Detect "review ..." or "please review ..." and route to custom review mode
|
|
4394
|
+
const reviewMatch = message.match(/^(?:please )?review\s*(.*)/i);
|
|
3643
4395
|
if (reviewMatch && agent.reviewOptions) {
|
|
3644
4396
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
3645
|
-
return cmdReview(agent, session, "custom", customInstructions, {
|
|
4397
|
+
return cmdReview(agent, session, "custom", customInstructions, {
|
|
4398
|
+
wait: !noWait,
|
|
4399
|
+
yolo,
|
|
4400
|
+
timeoutMs: flags.timeout !== undefined ? timeoutMs : REVIEW_TIMEOUT_MS,
|
|
4401
|
+
});
|
|
3646
4402
|
}
|
|
3647
4403
|
|
|
3648
4404
|
return cmdAsk(agent, session, message, { noWait, yolo, timeoutMs });
|
|
@@ -3650,17 +4406,21 @@ async function main() {
|
|
|
3650
4406
|
|
|
3651
4407
|
// Run main() only when executed directly (not when imported for testing)
|
|
3652
4408
|
// Use realpathSync to handle symlinks (e.g., axclaude, axcodex bin entries)
|
|
3653
|
-
const
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
}
|
|
4409
|
+
const isDirectRun =
|
|
4410
|
+
process.argv[1] &&
|
|
4411
|
+
(() => {
|
|
4412
|
+
try {
|
|
4413
|
+
return realpathSync(process.argv[1]) === __filename;
|
|
4414
|
+
} catch {
|
|
4415
|
+
return false;
|
|
4416
|
+
}
|
|
4417
|
+
})();
|
|
3661
4418
|
if (isDirectRun) {
|
|
3662
4419
|
main().catch((err) => {
|
|
3663
4420
|
console.log(`ERROR: ${err.message}`);
|
|
4421
|
+
if (err instanceof TimeoutError && err.session) {
|
|
4422
|
+
console.log(`Hint: Use 'ax debug --session=${err.session}' to see current screen state`);
|
|
4423
|
+
}
|
|
3664
4424
|
process.exit(1);
|
|
3665
4425
|
});
|
|
3666
4426
|
}
|
|
@@ -3670,6 +4430,7 @@ export {
|
|
|
3670
4430
|
parseSessionName,
|
|
3671
4431
|
parseAgentConfig,
|
|
3672
4432
|
parseKeySequence,
|
|
4433
|
+
parseCliArgs,
|
|
3673
4434
|
getClaudeProjectPath,
|
|
3674
4435
|
matchesPattern,
|
|
3675
4436
|
getBaseDir,
|
|
@@ -3681,4 +4442,3 @@ export {
|
|
|
3681
4442
|
detectState,
|
|
3682
4443
|
State,
|
|
3683
4444
|
};
|
|
3684
|
-
|