ax-agents 0.0.1-alpha.1 → 0.0.1-alpha.11
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 +1335 -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,161 @@ 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
|
-
|
|
2706
|
+
const watchingLabel = parentSession
|
|
2707
|
+
? parentSession.session || parentSession.uuid?.slice(0, 8)
|
|
2708
|
+
: null;
|
|
2709
|
+
console.log(
|
|
2710
|
+
`Summoning: ${config.name} (pid ${child.pid})${watchingLabel ? ` [watching: ${watchingLabel}]` : ""}`,
|
|
2711
|
+
);
|
|
2116
2712
|
}
|
|
2117
2713
|
|
|
2118
2714
|
// =============================================================================
|
|
2119
|
-
// Command:
|
|
2715
|
+
// Command: archangel (runs as the archangel process itself)
|
|
2120
2716
|
// =============================================================================
|
|
2121
2717
|
|
|
2122
2718
|
/**
|
|
2123
2719
|
* @param {string | undefined} agentName
|
|
2124
2720
|
*/
|
|
2125
|
-
async function
|
|
2721
|
+
async function cmdArchangel(agentName) {
|
|
2126
2722
|
if (!agentName) {
|
|
2127
|
-
console.error("Usage: ./ax.js
|
|
2723
|
+
console.error("Usage: ./ax.js archangel <name>");
|
|
2128
2724
|
process.exit(1);
|
|
2129
2725
|
}
|
|
2130
2726
|
// Load agent config
|
|
2131
2727
|
const configPath = path.join(AGENTS_DIR, `${agentName}.md`);
|
|
2132
2728
|
if (!existsSync(configPath)) {
|
|
2133
|
-
console.error(`[
|
|
2729
|
+
console.error(`[archangel:${agentName}] Config not found: ${configPath}`);
|
|
2134
2730
|
process.exit(1);
|
|
2135
2731
|
}
|
|
2136
2732
|
|
|
2137
2733
|
const content = readFileSync(configPath, "utf-8");
|
|
2138
2734
|
const configResult = parseAgentConfig(`${agentName}.md`, content);
|
|
2139
2735
|
if (!configResult || "error" in configResult) {
|
|
2140
|
-
console.error(`[
|
|
2736
|
+
console.error(`[archangel:${agentName}] Invalid config`);
|
|
2141
2737
|
process.exit(1);
|
|
2142
2738
|
}
|
|
2143
2739
|
const config = configResult;
|
|
2144
2740
|
|
|
2145
2741
|
const agent = config.tool === "claude" ? ClaudeAgent : CodexAgent;
|
|
2146
|
-
const sessionName =
|
|
2742
|
+
const sessionName = generateArchangelSessionName(config);
|
|
2147
2743
|
|
|
2148
2744
|
// Check agent CLI is installed before trying to start
|
|
2149
2745
|
const cliCheck = spawnSync("which", [agent.name], { encoding: "utf-8" });
|
|
2150
2746
|
if (cliCheck.status !== 0) {
|
|
2151
|
-
console.error(
|
|
2747
|
+
console.error(
|
|
2748
|
+
`[archangel:${agentName}] ERROR: ${agent.name} CLI is not installed or not in PATH`,
|
|
2749
|
+
);
|
|
2152
2750
|
process.exit(1);
|
|
2153
2751
|
}
|
|
2154
2752
|
|
|
@@ -2158,7 +2756,7 @@ async function cmdDaemon(agentName) {
|
|
|
2158
2756
|
|
|
2159
2757
|
// Wait for agent to be ready
|
|
2160
2758
|
const start = Date.now();
|
|
2161
|
-
while (Date.now() - start <
|
|
2759
|
+
while (Date.now() - start < ARCHANGEL_STARTUP_TIMEOUT_MS) {
|
|
2162
2760
|
const screen = tmuxCapture(sessionName);
|
|
2163
2761
|
const state = agent.getState(screen);
|
|
2164
2762
|
|
|
@@ -2169,7 +2767,7 @@ async function cmdDaemon(agentName) {
|
|
|
2169
2767
|
|
|
2170
2768
|
// Handle bypass permissions confirmation dialog (Claude Code shows this for --dangerously-skip-permissions)
|
|
2171
2769
|
if (screen.includes("Bypass Permissions mode") && screen.includes("Yes, I accept")) {
|
|
2172
|
-
console.log(`[
|
|
2770
|
+
console.log(`[archangel:${agentName}] Accepting bypass permissions dialog`);
|
|
2173
2771
|
tmuxSend(sessionName, "2"); // Select "Yes, I accept"
|
|
2174
2772
|
await sleep(300);
|
|
2175
2773
|
tmuxSend(sessionName, "Enter");
|
|
@@ -2178,7 +2776,7 @@ async function cmdDaemon(agentName) {
|
|
|
2178
2776
|
}
|
|
2179
2777
|
|
|
2180
2778
|
if (state === State.READY) {
|
|
2181
|
-
console.log(`[
|
|
2779
|
+
console.log(`[archangel:${agentName}] Started session: ${sessionName}`);
|
|
2182
2780
|
break;
|
|
2183
2781
|
}
|
|
2184
2782
|
|
|
@@ -2200,6 +2798,13 @@ async function cmdDaemon(agentName) {
|
|
|
2200
2798
|
let isProcessing = false;
|
|
2201
2799
|
const intervalMs = config.interval * 1000;
|
|
2202
2800
|
|
|
2801
|
+
// Hash tracking for incremental context updates
|
|
2802
|
+
/** @type {string | null} */
|
|
2803
|
+
let lastPlanHash = null;
|
|
2804
|
+
/** @type {string | null} */
|
|
2805
|
+
let lastTodosHash = null;
|
|
2806
|
+
let isFirstTrigger = true;
|
|
2807
|
+
|
|
2203
2808
|
async function processChanges() {
|
|
2204
2809
|
clearTimeout(debounceTimer);
|
|
2205
2810
|
clearTimeout(maxWaitTimer);
|
|
@@ -2210,16 +2815,32 @@ async function cmdDaemon(agentName) {
|
|
|
2210
2815
|
isProcessing = true;
|
|
2211
2816
|
|
|
2212
2817
|
const files = [...changedFiles];
|
|
2213
|
-
changedFiles = new Set();
|
|
2818
|
+
changedFiles = new Set(); // atomic swap to avoid losing changes during processing
|
|
2214
2819
|
|
|
2215
2820
|
try {
|
|
2216
2821
|
// Get parent session log path for JSONL extraction
|
|
2217
2822
|
const parent = findParentSession();
|
|
2218
2823
|
const logPath = parent ? findClaudeLogPath(parent.uuid, parent.session) : null;
|
|
2219
2824
|
|
|
2825
|
+
// Get orientation context (plan and todos) from parent session
|
|
2826
|
+
const meta = parent?.session ? getSessionMeta(parent.session) : null;
|
|
2827
|
+
const planContent = meta?.slug ? readPlanFile(meta.slug) : null;
|
|
2828
|
+
const todosContent = meta?.todos?.length ? formatTodos(meta.todos) : null;
|
|
2829
|
+
|
|
2830
|
+
// Check if plan/todos have changed since last trigger
|
|
2831
|
+
const planHash = quickHash(planContent);
|
|
2832
|
+
const todosHash = quickHash(todosContent);
|
|
2833
|
+
const includePlan = planHash !== lastPlanHash;
|
|
2834
|
+
const includeTodos = todosHash !== lastTodosHash;
|
|
2835
|
+
|
|
2836
|
+
// Update tracking for next trigger
|
|
2837
|
+
lastPlanHash = planHash;
|
|
2838
|
+
lastTodosHash = todosHash;
|
|
2839
|
+
|
|
2220
2840
|
// Build file-specific context from JSONL
|
|
2221
2841
|
const fileContexts = [];
|
|
2222
|
-
for (const file of files.slice(0, 5)) {
|
|
2842
|
+
for (const file of files.slice(0, 5)) {
|
|
2843
|
+
// Limit to 5 files
|
|
2223
2844
|
const ctx = extractFileEditContext(logPath, file);
|
|
2224
2845
|
if (ctx) {
|
|
2225
2846
|
fileContexts.push({ file, ...ctx });
|
|
@@ -2227,7 +2848,18 @@ async function cmdDaemon(agentName) {
|
|
|
2227
2848
|
}
|
|
2228
2849
|
|
|
2229
2850
|
// Build the prompt
|
|
2230
|
-
|
|
2851
|
+
// First trigger: include intro, guidelines, and focus (archangel has memory)
|
|
2852
|
+
let prompt = isFirstTrigger
|
|
2853
|
+
? `You are the archangel of ${agentName}.\n\n${ARCHANGEL_PREAMBLE}\n\n## Focus\n\n${basePrompt}\n\n---`
|
|
2854
|
+
: "";
|
|
2855
|
+
|
|
2856
|
+
// Add orientation context (plan and todos) only if changed since last trigger
|
|
2857
|
+
if (includePlan && planContent) {
|
|
2858
|
+
prompt += (prompt ? "\n\n" : "") + "## Current Plan\n\n" + planContent;
|
|
2859
|
+
}
|
|
2860
|
+
if (includeTodos && todosContent) {
|
|
2861
|
+
prompt += (prompt ? "\n\n" : "") + "## Current Todos\n\n" + todosContent;
|
|
2862
|
+
}
|
|
2231
2863
|
|
|
2232
2864
|
if (fileContexts.length > 0) {
|
|
2233
2865
|
prompt += "\n\n## Recent Edits (from parent session)\n";
|
|
@@ -2246,26 +2878,33 @@ async function cmdDaemon(agentName) {
|
|
|
2246
2878
|
}
|
|
2247
2879
|
|
|
2248
2880
|
if (ctx.readsBefore.length > 0) {
|
|
2249
|
-
const reads = ctx.readsBefore.map(f => f.split("/").pop()).join(", ");
|
|
2881
|
+
const reads = ctx.readsBefore.map((f) => f.split("/").pop()).join(", ");
|
|
2250
2882
|
prompt += `**Files read before:** ${reads}\n`;
|
|
2251
2883
|
}
|
|
2252
2884
|
}
|
|
2253
2885
|
|
|
2254
2886
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
2255
2887
|
|
|
2256
|
-
const gitContext = buildGitContext(
|
|
2888
|
+
const gitContext = buildGitContext(
|
|
2889
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2890
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2891
|
+
);
|
|
2257
2892
|
if (gitContext) {
|
|
2258
2893
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2259
2894
|
}
|
|
2260
2895
|
|
|
2261
|
-
prompt +=
|
|
2896
|
+
prompt += "\n\nReview these changes.";
|
|
2262
2897
|
} else {
|
|
2263
2898
|
// Fallback: no JSONL context available, use conversation + git context
|
|
2264
|
-
const parentContext = getParentSessionContext(
|
|
2265
|
-
const gitContext = buildGitContext(
|
|
2899
|
+
const parentContext = getParentSessionContext(ARCHANGEL_PARENT_CONTEXT_ENTRIES);
|
|
2900
|
+
const gitContext = buildGitContext(
|
|
2901
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2902
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2903
|
+
);
|
|
2266
2904
|
|
|
2267
2905
|
if (parentContext) {
|
|
2268
|
-
prompt +=
|
|
2906
|
+
prompt +=
|
|
2907
|
+
"\n\n## Main Session Context\n\nThe user is currently working on:\n\n" + parentContext;
|
|
2269
2908
|
}
|
|
2270
2909
|
|
|
2271
2910
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
@@ -2274,13 +2913,12 @@ async function cmdDaemon(agentName) {
|
|
|
2274
2913
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2275
2914
|
}
|
|
2276
2915
|
|
|
2277
|
-
prompt +=
|
|
2916
|
+
prompt += "\n\nReview these changes.";
|
|
2278
2917
|
}
|
|
2279
2918
|
|
|
2280
|
-
|
|
2281
2919
|
// Check session still exists
|
|
2282
2920
|
if (!tmuxHasSession(sessionName)) {
|
|
2283
|
-
console.log(`[
|
|
2921
|
+
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
2284
2922
|
process.exit(0);
|
|
2285
2923
|
}
|
|
2286
2924
|
|
|
@@ -2289,12 +2927,12 @@ async function cmdDaemon(agentName) {
|
|
|
2289
2927
|
const state = agent.getState(screen);
|
|
2290
2928
|
|
|
2291
2929
|
if (state === State.RATE_LIMITED) {
|
|
2292
|
-
console.error(`[
|
|
2930
|
+
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2293
2931
|
process.exit(2);
|
|
2294
2932
|
}
|
|
2295
2933
|
|
|
2296
2934
|
if (state !== State.READY) {
|
|
2297
|
-
console.log(`[
|
|
2935
|
+
console.log(`[archangel:${agentName}] Agent not ready (${state}), skipping`);
|
|
2298
2936
|
isProcessing = false;
|
|
2299
2937
|
return;
|
|
2300
2938
|
}
|
|
@@ -2304,38 +2942,37 @@ async function cmdDaemon(agentName) {
|
|
|
2304
2942
|
await sleep(200); // Allow time for large prompts to be processed
|
|
2305
2943
|
tmuxSend(sessionName, "Enter");
|
|
2306
2944
|
await sleep(100); // Ensure Enter is processed
|
|
2945
|
+
isFirstTrigger = false;
|
|
2307
2946
|
|
|
2308
2947
|
// Wait for response
|
|
2309
|
-
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2948
|
+
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2949
|
+
agent,
|
|
2950
|
+
sessionName,
|
|
2951
|
+
ARCHANGEL_RESPONSE_TIMEOUT_MS,
|
|
2952
|
+
);
|
|
2310
2953
|
|
|
2311
2954
|
if (endState === State.RATE_LIMITED) {
|
|
2312
|
-
console.error(`[
|
|
2955
|
+
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2313
2956
|
process.exit(2);
|
|
2314
2957
|
}
|
|
2315
2958
|
|
|
2316
|
-
|
|
2317
2959
|
const cleanedResponse = agent.getResponse(sessionName, afterScreen) || "";
|
|
2318
2960
|
|
|
2319
|
-
|
|
2320
|
-
const isGarbage = cleanedResponse.includes("[Pasted text") ||
|
|
2321
|
-
cleanedResponse.match(/^\+\d+ lines\]/) ||
|
|
2322
|
-
cleanedResponse.length < 20;
|
|
2961
|
+
const isSkippable = !cleanedResponse || cleanedResponse.trim() === "EMPTY_RESPONSE";
|
|
2323
2962
|
|
|
2324
|
-
if (
|
|
2963
|
+
if (!isSkippable) {
|
|
2325
2964
|
writeToMailbox({
|
|
2326
2965
|
agent: /** @type {string} */ (agentName),
|
|
2327
2966
|
session: sessionName,
|
|
2328
2967
|
branch: getCurrentBranch(),
|
|
2329
2968
|
commit: getCurrentCommit(),
|
|
2330
2969
|
files,
|
|
2331
|
-
message: cleanedResponse
|
|
2970
|
+
message: cleanedResponse,
|
|
2332
2971
|
});
|
|
2333
|
-
console.log(`[
|
|
2334
|
-
} else if (isGarbage) {
|
|
2335
|
-
console.log(`[daemon:${agentName}] Skipped garbage response`);
|
|
2972
|
+
console.log(`[archangel:${agentName}] Wrote observation for ${files.length} file(s)`);
|
|
2336
2973
|
}
|
|
2337
2974
|
} catch (err) {
|
|
2338
|
-
console.error(`[
|
|
2975
|
+
console.error(`[archangel:${agentName}] Error:`, err instanceof Error ? err.message : err);
|
|
2339
2976
|
}
|
|
2340
2977
|
|
|
2341
2978
|
isProcessing = false;
|
|
@@ -2343,7 +2980,10 @@ async function cmdDaemon(agentName) {
|
|
|
2343
2980
|
|
|
2344
2981
|
function scheduleProcessChanges() {
|
|
2345
2982
|
processChanges().catch((err) => {
|
|
2346
|
-
console.error(
|
|
2983
|
+
console.error(
|
|
2984
|
+
`[archangel:${agentName}] Unhandled error:`,
|
|
2985
|
+
err instanceof Error ? err.message : err,
|
|
2986
|
+
);
|
|
2347
2987
|
});
|
|
2348
2988
|
}
|
|
2349
2989
|
|
|
@@ -2364,16 +3004,16 @@ async function cmdDaemon(agentName) {
|
|
|
2364
3004
|
// Check if session still exists periodically
|
|
2365
3005
|
const sessionCheck = setInterval(() => {
|
|
2366
3006
|
if (!tmuxHasSession(sessionName)) {
|
|
2367
|
-
console.log(`[
|
|
3007
|
+
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
2368
3008
|
stopWatching();
|
|
2369
3009
|
clearInterval(sessionCheck);
|
|
2370
3010
|
process.exit(0);
|
|
2371
3011
|
}
|
|
2372
|
-
},
|
|
3012
|
+
}, ARCHANGEL_HEALTH_CHECK_MS);
|
|
2373
3013
|
|
|
2374
3014
|
// Handle graceful shutdown
|
|
2375
3015
|
process.on("SIGTERM", () => {
|
|
2376
|
-
console.log(`[
|
|
3016
|
+
console.log(`[archangel:${agentName}] Received SIGTERM, shutting down`);
|
|
2377
3017
|
stopWatching();
|
|
2378
3018
|
clearInterval(sessionCheck);
|
|
2379
3019
|
tmuxSend(sessionName, "C-c");
|
|
@@ -2384,7 +3024,7 @@ async function cmdDaemon(agentName) {
|
|
|
2384
3024
|
});
|
|
2385
3025
|
|
|
2386
3026
|
process.on("SIGINT", () => {
|
|
2387
|
-
console.log(`[
|
|
3027
|
+
console.log(`[archangel:${agentName}] Received SIGINT, shutting down`);
|
|
2388
3028
|
stopWatching();
|
|
2389
3029
|
clearInterval(sessionCheck);
|
|
2390
3030
|
tmuxSend(sessionName, "C-c");
|
|
@@ -2394,48 +3034,33 @@ async function cmdDaemon(agentName) {
|
|
|
2394
3034
|
}, 500);
|
|
2395
3035
|
});
|
|
2396
3036
|
|
|
2397
|
-
console.log(`[
|
|
3037
|
+
console.log(`[archangel:${agentName}] Watching: ${config.watch.join(", ")}`);
|
|
2398
3038
|
|
|
2399
3039
|
// Keep the process alive
|
|
2400
3040
|
await new Promise(() => {});
|
|
2401
3041
|
}
|
|
2402
3042
|
|
|
2403
3043
|
/**
|
|
2404
|
-
* @param {string}
|
|
2405
|
-
* @param {string | null} [daemonName]
|
|
3044
|
+
* @param {string | null} [name]
|
|
2406
3045
|
*/
|
|
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
|
-
}
|
|
3046
|
+
async function cmdSummon(name = null) {
|
|
3047
|
+
const configs = loadAgentConfigs();
|
|
2426
3048
|
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
3049
|
+
// If name provided but doesn't exist, create it
|
|
3050
|
+
if (name) {
|
|
3051
|
+
const exists = configs.some((c) => c.name === name);
|
|
3052
|
+
if (!exists) {
|
|
3053
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
3054
|
+
console.log("ERROR: Name must contain only letters, numbers, dashes, and underscores");
|
|
3055
|
+
process.exit(1);
|
|
3056
|
+
}
|
|
2432
3057
|
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
}
|
|
3058
|
+
if (!existsSync(AGENTS_DIR)) {
|
|
3059
|
+
mkdirSync(AGENTS_DIR, { recursive: true });
|
|
3060
|
+
}
|
|
2437
3061
|
|
|
2438
|
-
|
|
3062
|
+
const agentPath = path.join(AGENTS_DIR, `${name}.md`);
|
|
3063
|
+
const template = `---
|
|
2439
3064
|
tool: claude
|
|
2440
3065
|
watch: ["**/*.{ts,tsx,js,jsx,mjs,mts}"]
|
|
2441
3066
|
interval: 30
|
|
@@ -2443,75 +3068,76 @@ interval: 30
|
|
|
2443
3068
|
|
|
2444
3069
|
Review changed files for bugs, type errors, and edge cases.
|
|
2445
3070
|
`;
|
|
3071
|
+
writeFileSync(agentPath, template);
|
|
3072
|
+
console.log(`Created: ${agentPath}`);
|
|
3073
|
+
console.log(`Edit the file to customize, then run: ax summon ${name}`);
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
2446
3077
|
|
|
2447
|
-
|
|
2448
|
-
console.log(`
|
|
2449
|
-
console.log(`Edit the file to customize the daemon, then run: ./ax.js daemons start ${daemonName}`);
|
|
3078
|
+
if (configs.length === 0) {
|
|
3079
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
2450
3080
|
return;
|
|
2451
3081
|
}
|
|
2452
3082
|
|
|
3083
|
+
const targetConfigs = name ? configs.filter((c) => c.name === name) : configs;
|
|
3084
|
+
|
|
3085
|
+
ensureMailboxHookScript();
|
|
3086
|
+
|
|
3087
|
+
const parentSession = findCurrentClaudeSession();
|
|
3088
|
+
if (parentSession) {
|
|
3089
|
+
console.log(`Parent session: ${parentSession.session || "(non-tmux)"} [${parentSession.uuid}]`);
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
for (const config of targetConfigs) {
|
|
3093
|
+
const sessionPattern = getArchangelSessionPattern(config);
|
|
3094
|
+
const existing = findArchangelSession(sessionPattern);
|
|
3095
|
+
|
|
3096
|
+
if (!existing) {
|
|
3097
|
+
startArchangel(config, parentSession);
|
|
3098
|
+
} else {
|
|
3099
|
+
console.log(`Already running: ${config.name} (${existing})`);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
gcMailbox(24);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
/**
|
|
3107
|
+
* @param {string | null} [name]
|
|
3108
|
+
*/
|
|
3109
|
+
async function cmdRecall(name = null) {
|
|
2453
3110
|
const configs = loadAgentConfigs();
|
|
2454
3111
|
|
|
2455
3112
|
if (configs.length === 0) {
|
|
2456
|
-
console.log(`No
|
|
3113
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
2457
3114
|
return;
|
|
2458
3115
|
}
|
|
2459
3116
|
|
|
2460
|
-
|
|
2461
|
-
const targetConfigs = daemonName
|
|
2462
|
-
? configs.filter((c) => c.name === daemonName)
|
|
2463
|
-
: configs;
|
|
3117
|
+
const targetConfigs = name ? configs.filter((c) => c.name === name) : configs;
|
|
2464
3118
|
|
|
2465
|
-
if (
|
|
2466
|
-
console.log(`ERROR:
|
|
3119
|
+
if (name && targetConfigs.length === 0) {
|
|
3120
|
+
console.log(`ERROR: archangel '${name}' not found in ${AGENTS_DIR}/`);
|
|
2467
3121
|
process.exit(1);
|
|
2468
3122
|
}
|
|
2469
3123
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
}
|
|
3124
|
+
for (const config of targetConfigs) {
|
|
3125
|
+
const sessionPattern = getArchangelSessionPattern(config);
|
|
3126
|
+
const existing = findArchangelSession(sessionPattern);
|
|
2474
3127
|
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
console.log(`
|
|
3128
|
+
if (existing) {
|
|
3129
|
+
tmuxSend(existing, "C-c");
|
|
3130
|
+
await sleep(300);
|
|
3131
|
+
tmuxKill(existing);
|
|
3132
|
+
console.log(`Recalled: ${config.name} (${existing})`);
|
|
2480
3133
|
} else {
|
|
2481
|
-
console.log(
|
|
3134
|
+
console.log(`Not running: ${config.name}`);
|
|
2482
3135
|
}
|
|
2483
3136
|
}
|
|
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
3137
|
}
|
|
2512
3138
|
|
|
2513
3139
|
// Version of the hook script template - bump when making changes
|
|
2514
|
-
const HOOK_SCRIPT_VERSION = "
|
|
3140
|
+
const HOOK_SCRIPT_VERSION = "4";
|
|
2515
3141
|
|
|
2516
3142
|
function ensureMailboxHookScript() {
|
|
2517
3143
|
const hooksDir = HOOKS_DIR;
|
|
@@ -2529,34 +3155,54 @@ function ensureMailboxHookScript() {
|
|
|
2529
3155
|
mkdirSync(hooksDir, { recursive: true });
|
|
2530
3156
|
}
|
|
2531
3157
|
|
|
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
3158
|
const hookCode = `#!/usr/bin/env node
|
|
2537
3159
|
${versionMarker}
|
|
2538
|
-
// Auto-generated hook script - do not edit manually
|
|
2539
|
-
|
|
2540
3160
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
3161
|
+
import { dirname, join } from "node:path";
|
|
3162
|
+
import { fileURLToPath } from "node:url";
|
|
3163
|
+
import { createHash } from "node:crypto";
|
|
2541
3164
|
|
|
3165
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3166
|
+
const AI_DIR = join(__dirname, "..");
|
|
2542
3167
|
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)
|
|
3168
|
+
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
3169
|
+
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
2546
3170
|
|
|
2547
|
-
|
|
3171
|
+
// Read hook input from stdin
|
|
3172
|
+
let hookInput = {};
|
|
3173
|
+
try {
|
|
3174
|
+
const stdinData = readFileSync(0, "utf-8").trim();
|
|
3175
|
+
if (stdinData) hookInput = JSON.parse(stdinData);
|
|
3176
|
+
} catch (err) {
|
|
3177
|
+
if (DEBUG) console.error("[hook] stdin parse:", err.message);
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
const sessionId = hookInput.session_id || "";
|
|
3181
|
+
const hookEvent = hookInput.hook_event_name || "";
|
|
2548
3182
|
|
|
2549
|
-
|
|
3183
|
+
if (DEBUG) console.error("[hook] session:", sessionId, "event:", hookEvent);
|
|
2550
3184
|
|
|
2551
|
-
//
|
|
2552
|
-
|
|
3185
|
+
// NO-OP for archangel or partner sessions
|
|
3186
|
+
if (sessionId.includes("-archangel-") || sessionId.includes("-partner-")) {
|
|
3187
|
+
if (DEBUG) console.error("[hook] skipping non-parent session");
|
|
3188
|
+
process.exit(0);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// Per-session last-seen tracking (single JSON file, self-cleaning)
|
|
3192
|
+
const sessionHash = sessionId ? createHash("md5").update(sessionId).digest("hex").slice(0, 8) : "default";
|
|
3193
|
+
const LAST_SEEN_FILE = join(AI_DIR, "mailbox-last-seen.json");
|
|
3194
|
+
|
|
3195
|
+
if (!existsSync(MAILBOX)) process.exit(0);
|
|
3196
|
+
|
|
3197
|
+
let lastSeenMap = {};
|
|
2553
3198
|
try {
|
|
2554
|
-
if (existsSync(
|
|
2555
|
-
|
|
3199
|
+
if (existsSync(LAST_SEEN_FILE)) {
|
|
3200
|
+
lastSeenMap = JSON.parse(readFileSync(LAST_SEEN_FILE, "utf-8"));
|
|
2556
3201
|
}
|
|
2557
3202
|
} catch (err) {
|
|
2558
3203
|
if (DEBUG) console.error("[hook] readLastSeen:", err.message);
|
|
2559
3204
|
}
|
|
3205
|
+
const lastSeen = lastSeenMap[sessionHash] || 0;
|
|
2560
3206
|
|
|
2561
3207
|
const now = Date.now();
|
|
2562
3208
|
const lines = readFileSync(MAILBOX, "utf-8").trim().split("\\n").filter(Boolean);
|
|
@@ -2567,11 +3213,7 @@ for (const line of lines) {
|
|
|
2567
3213
|
const entry = JSON.parse(line);
|
|
2568
3214
|
const ts = new Date(entry.timestamp).getTime();
|
|
2569
3215
|
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
3216
|
if (age < MAX_AGE_MS && ts > lastSeen) {
|
|
2574
|
-
// Extract session prefix (without UUID) for shorter log command
|
|
2575
3217
|
const session = entry.payload.session || "";
|
|
2576
3218
|
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
3219
|
relevant.push({ agent: entry.payload.agent, sessionPrefix, message: entry.payload.message });
|
|
@@ -2582,22 +3224,39 @@ for (const line of lines) {
|
|
|
2582
3224
|
}
|
|
2583
3225
|
|
|
2584
3226
|
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
3227
|
const sessionPrefixes = new Set();
|
|
3228
|
+
let messageLines = [];
|
|
3229
|
+
messageLines.push("## Background Agents");
|
|
3230
|
+
messageLines.push("");
|
|
3231
|
+
messageLines.push("Background agents watching your files found:");
|
|
3232
|
+
messageLines.push("");
|
|
2590
3233
|
for (const { agent, sessionPrefix, message } of relevant) {
|
|
2591
3234
|
if (sessionPrefix) sessionPrefixes.add(sessionPrefix);
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
3235
|
+
messageLines.push("**[" + agent + "]**");
|
|
3236
|
+
messageLines.push("");
|
|
3237
|
+
messageLines.push(message);
|
|
3238
|
+
messageLines.push("");
|
|
2596
3239
|
}
|
|
2597
3240
|
const sessionList = [...sessionPrefixes].map(s => "\\\`./ax.js log " + s + "\\\`").join(" or ");
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3241
|
+
messageLines.push("> For more context: \\\`./ax.js mailbox\\\`" + (sessionList ? " or " + sessionList : ""));
|
|
3242
|
+
|
|
3243
|
+
const formattedMessage = messageLines.join("\\n");
|
|
3244
|
+
|
|
3245
|
+
// For Stop hook, return blocking JSON to force acknowledgment
|
|
3246
|
+
if (hookEvent === "Stop") {
|
|
3247
|
+
console.log(JSON.stringify({ decision: "block", reason: formattedMessage }));
|
|
3248
|
+
} else {
|
|
3249
|
+
// For other hooks, just output the context
|
|
3250
|
+
console.log(formattedMessage);
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// Update last-seen and prune entries older than 24 hours
|
|
3254
|
+
const PRUNE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
3255
|
+
lastSeenMap[sessionHash] = now;
|
|
3256
|
+
for (const key of Object.keys(lastSeenMap)) {
|
|
3257
|
+
if (now - lastSeenMap[key] > PRUNE_AGE_MS) delete lastSeenMap[key];
|
|
3258
|
+
}
|
|
3259
|
+
writeFileSync(LAST_SEEN_FILE, JSON.stringify(lastSeenMap));
|
|
2601
3260
|
}
|
|
2602
3261
|
|
|
2603
3262
|
process.exit(0);
|
|
@@ -2609,22 +3268,12 @@ process.exit(0);
|
|
|
2609
3268
|
// Configure the hook in .claude/settings.json at the same time
|
|
2610
3269
|
const configuredHook = ensureClaudeHookConfig();
|
|
2611
3270
|
if (!configuredHook) {
|
|
2612
|
-
const hookScriptPath = path.join(HOOKS_DIR, "mailbox-inject.js");
|
|
2613
3271
|
console.log(`\nTo enable manually, add to .claude/settings.json:\n`);
|
|
2614
3272
|
console.log(`{
|
|
2615
3273
|
"hooks": {
|
|
2616
|
-
"UserPromptSubmit": [
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
"hooks": [
|
|
2620
|
-
{
|
|
2621
|
-
"type": "command",
|
|
2622
|
-
"command": "node ${hookScriptPath}",
|
|
2623
|
-
"timeout": 5
|
|
2624
|
-
}
|
|
2625
|
-
]
|
|
2626
|
-
}
|
|
2627
|
-
]
|
|
3274
|
+
"UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
3275
|
+
"PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
3276
|
+
"Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }]
|
|
2628
3277
|
}
|
|
2629
3278
|
}`);
|
|
2630
3279
|
}
|
|
@@ -2633,8 +3282,8 @@ process.exit(0);
|
|
|
2633
3282
|
function ensureClaudeHookConfig() {
|
|
2634
3283
|
const settingsDir = ".claude";
|
|
2635
3284
|
const settingsPath = path.join(settingsDir, "settings.json");
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
3285
|
+
const hookCommand = "node .ai/hooks/mailbox-inject.js";
|
|
3286
|
+
const hookEvents = ["UserPromptSubmit", "PostToolUse", "Stop"];
|
|
2638
3287
|
|
|
2639
3288
|
try {
|
|
2640
3289
|
/** @type {ClaudeSettings} */
|
|
@@ -2653,33 +3302,41 @@ function ensureClaudeHookConfig() {
|
|
|
2653
3302
|
|
|
2654
3303
|
// Ensure hooks structure exists
|
|
2655
3304
|
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
3305
|
|
|
2664
|
-
|
|
2665
|
-
|
|
3306
|
+
let anyAdded = false;
|
|
3307
|
+
|
|
3308
|
+
for (const eventName of hookEvents) {
|
|
3309
|
+
if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
|
|
3310
|
+
|
|
3311
|
+
// Check if our hook is already configured for this event
|
|
3312
|
+
const hookExists = settings.hooks[eventName].some(
|
|
3313
|
+
/** @param {{hooks?: Array<{command: string}>}} entry */
|
|
3314
|
+
(entry) =>
|
|
3315
|
+
entry.hooks?.some(/** @param {{command: string}} h */ (h) => h.command === hookCommand),
|
|
3316
|
+
);
|
|
3317
|
+
|
|
3318
|
+
if (!hookExists) {
|
|
3319
|
+
// Add the hook for this event
|
|
3320
|
+
settings.hooks[eventName].push({
|
|
3321
|
+
matcher: "",
|
|
3322
|
+
hooks: [
|
|
3323
|
+
{
|
|
3324
|
+
type: "command",
|
|
3325
|
+
command: hookCommand,
|
|
3326
|
+
timeout: 5,
|
|
3327
|
+
},
|
|
3328
|
+
],
|
|
3329
|
+
});
|
|
3330
|
+
anyAdded = true;
|
|
3331
|
+
}
|
|
2666
3332
|
}
|
|
2667
3333
|
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
hooks:
|
|
2672
|
-
|
|
2673
|
-
type: "command",
|
|
2674
|
-
command: hookCommand,
|
|
2675
|
-
timeout: 5,
|
|
2676
|
-
},
|
|
2677
|
-
],
|
|
2678
|
-
});
|
|
3334
|
+
if (anyAdded) {
|
|
3335
|
+
// Write settings
|
|
3336
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3337
|
+
console.log(`Configured hooks in: ${settingsPath}`);
|
|
3338
|
+
}
|
|
2679
3339
|
|
|
2680
|
-
// Write settings
|
|
2681
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2682
|
-
console.log(`Configured hook in: ${settingsPath}`);
|
|
2683
3340
|
return true;
|
|
2684
3341
|
} catch {
|
|
2685
3342
|
// If we can't configure automatically, return false so manual instructions are shown
|
|
@@ -2689,9 +3346,31 @@ function ensureClaudeHookConfig() {
|
|
|
2689
3346
|
|
|
2690
3347
|
/**
|
|
2691
3348
|
* @param {string | null | undefined} session
|
|
2692
|
-
* @param {{all?: boolean}} [options]
|
|
3349
|
+
* @param {{all?: boolean, orphans?: boolean, force?: boolean}} [options]
|
|
2693
3350
|
*/
|
|
2694
|
-
function cmdKill(session, { all = false } = {}) {
|
|
3351
|
+
function cmdKill(session, { all = false, orphans = false, force = false } = {}) {
|
|
3352
|
+
// Handle orphaned processes
|
|
3353
|
+
if (orphans) {
|
|
3354
|
+
const orphanedProcesses = findOrphanedProcesses();
|
|
3355
|
+
|
|
3356
|
+
if (orphanedProcesses.length === 0) {
|
|
3357
|
+
console.log("No orphaned processes found");
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
const signal = force ? "-9" : "-15"; // SIGKILL vs SIGTERM
|
|
3362
|
+
let killed = 0;
|
|
3363
|
+
for (const { pid, command } of orphanedProcesses) {
|
|
3364
|
+
const result = spawnSync("kill", [signal, pid]);
|
|
3365
|
+
if (result.status === 0) {
|
|
3366
|
+
console.log(`Killed: PID ${pid} (${command.slice(0, 40)})`);
|
|
3367
|
+
killed++;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
console.log(`Killed ${killed} orphaned process(es)${force ? " (forced)" : ""}`);
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
|
|
2695
3374
|
// If specific session provided, kill just that one
|
|
2696
3375
|
if (session) {
|
|
2697
3376
|
if (!tmuxHasSession(session)) {
|
|
@@ -2751,7 +3430,9 @@ function cmdAttach(session) {
|
|
|
2751
3430
|
}
|
|
2752
3431
|
|
|
2753
3432
|
// Hand over to tmux attach
|
|
2754
|
-
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
3433
|
+
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
3434
|
+
stdio: "inherit",
|
|
3435
|
+
});
|
|
2755
3436
|
process.exit(result.status || 0);
|
|
2756
3437
|
}
|
|
2757
3438
|
|
|
@@ -2810,13 +3491,15 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2810
3491
|
|
|
2811
3492
|
if (newLines.length === 0) return;
|
|
2812
3493
|
|
|
2813
|
-
const entries = newLines
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3494
|
+
const entries = newLines
|
|
3495
|
+
.map((line) => {
|
|
3496
|
+
try {
|
|
3497
|
+
return JSON.parse(line);
|
|
3498
|
+
} catch {
|
|
3499
|
+
return null;
|
|
3500
|
+
}
|
|
3501
|
+
})
|
|
3502
|
+
.filter(Boolean);
|
|
2820
3503
|
|
|
2821
3504
|
const output = [];
|
|
2822
3505
|
if (isInitial) {
|
|
@@ -2829,7 +3512,10 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2829
3512
|
const ts = entry.timestamp || entry.ts || entry.createdAt;
|
|
2830
3513
|
if (ts && ts !== lastTimestamp) {
|
|
2831
3514
|
const date = new Date(ts);
|
|
2832
|
-
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
3515
|
+
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
3516
|
+
hour: "2-digit",
|
|
3517
|
+
minute: "2-digit",
|
|
3518
|
+
});
|
|
2833
3519
|
if (formatted.isUserMessage) {
|
|
2834
3520
|
output.push(`\n### ${timeStr}\n`);
|
|
2835
3521
|
}
|
|
@@ -2879,7 +3565,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2879
3565
|
if (type === "user" || type === "human") {
|
|
2880
3566
|
const text = extractTextContent(content);
|
|
2881
3567
|
if (text) {
|
|
2882
|
-
return {
|
|
3568
|
+
return {
|
|
3569
|
+
text: `**User**: ${truncate(text, TRUNCATE_USER_LEN)}\n`,
|
|
3570
|
+
isUserMessage: true,
|
|
3571
|
+
};
|
|
2883
3572
|
}
|
|
2884
3573
|
}
|
|
2885
3574
|
|
|
@@ -2895,10 +3584,12 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2895
3584
|
// Extract tool calls (compressed)
|
|
2896
3585
|
const tools = extractToolCalls(content);
|
|
2897
3586
|
if (tools.length > 0) {
|
|
2898
|
-
const toolSummary = tools
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
3587
|
+
const toolSummary = tools
|
|
3588
|
+
.map((t) => {
|
|
3589
|
+
if (t.error) return `${t.name}(${t.target}) ✗`;
|
|
3590
|
+
return `${t.name}(${t.target})`;
|
|
3591
|
+
})
|
|
3592
|
+
.join(", ");
|
|
2902
3593
|
parts.push(`> ${toolSummary}\n`);
|
|
2903
3594
|
}
|
|
2904
3595
|
|
|
@@ -2920,7 +3611,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2920
3611
|
const error = entry.error || entry.is_error;
|
|
2921
3612
|
if (error) {
|
|
2922
3613
|
const name = entry.tool_name || entry.name || "tool";
|
|
2923
|
-
return {
|
|
3614
|
+
return {
|
|
3615
|
+
text: `> ${name} ✗ (${truncate(String(error), 100)})\n`,
|
|
3616
|
+
isUserMessage: false,
|
|
3617
|
+
};
|
|
2924
3618
|
}
|
|
2925
3619
|
}
|
|
2926
3620
|
|
|
@@ -2957,7 +3651,8 @@ function extractToolCalls(content) {
|
|
|
2957
3651
|
const name = c.name || c.tool || "tool";
|
|
2958
3652
|
const input = c.input || c.arguments || {};
|
|
2959
3653
|
// Extract a reasonable target from the input
|
|
2960
|
-
const target =
|
|
3654
|
+
const target =
|
|
3655
|
+
input.file_path || input.path || input.command?.slice(0, 30) || input.pattern || "";
|
|
2961
3656
|
const shortTarget = target.split("/").pop() || target.slice(0, 20);
|
|
2962
3657
|
return { name, target: shortTarget, error: c.error };
|
|
2963
3658
|
});
|
|
@@ -3002,8 +3697,14 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3002
3697
|
|
|
3003
3698
|
for (const entry of entries) {
|
|
3004
3699
|
const ts = new Date(entry.timestamp);
|
|
3005
|
-
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
3006
|
-
|
|
3700
|
+
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
3701
|
+
hour: "2-digit",
|
|
3702
|
+
minute: "2-digit",
|
|
3703
|
+
});
|
|
3704
|
+
const dateStr = ts.toLocaleDateString("en-GB", {
|
|
3705
|
+
month: "short",
|
|
3706
|
+
day: "numeric",
|
|
3707
|
+
});
|
|
3007
3708
|
const p = entry.payload || {};
|
|
3008
3709
|
|
|
3009
3710
|
console.log(`### [${p.agent || "unknown"}] ${dateStr} ${timeStr}\n`);
|
|
@@ -3032,7 +3733,7 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3032
3733
|
* @param {string} message
|
|
3033
3734
|
* @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
|
|
3034
3735
|
*/
|
|
3035
|
-
async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs } = {}) {
|
|
3736
|
+
async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
3036
3737
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3037
3738
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3038
3739
|
|
|
@@ -3044,20 +3745,31 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3044
3745
|
}
|
|
3045
3746
|
|
|
3046
3747
|
/** @type {string} */
|
|
3047
|
-
const activeSession = sessionExists
|
|
3748
|
+
const activeSession = sessionExists
|
|
3749
|
+
? /** @type {string} */ (session)
|
|
3750
|
+
: await cmdStart(agent, session, { yolo });
|
|
3048
3751
|
|
|
3049
3752
|
tmuxSendLiteral(activeSession, message);
|
|
3050
3753
|
await sleep(50);
|
|
3051
3754
|
tmuxSend(activeSession, "Enter");
|
|
3052
3755
|
|
|
3053
|
-
if (noWait)
|
|
3756
|
+
if (noWait) {
|
|
3757
|
+
const parsed = parseSessionName(activeSession);
|
|
3758
|
+
const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
|
|
3759
|
+
const cli = path.basename(process.argv[1], ".js");
|
|
3760
|
+
console.log(`Sent to: ${shortId}
|
|
3761
|
+
|
|
3762
|
+
e.g.
|
|
3763
|
+
${cli} status --session=${shortId}
|
|
3764
|
+
${cli} output --session=${shortId}`);
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3054
3767
|
|
|
3055
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3056
3768
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3057
3769
|
|
|
3058
3770
|
const { state, screen } = useAutoApprove
|
|
3059
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3060
|
-
: await
|
|
3771
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3772
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3061
3773
|
|
|
3062
3774
|
if (state === State.RATE_LIMITED) {
|
|
3063
3775
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3065,14 +3777,9 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3065
3777
|
}
|
|
3066
3778
|
|
|
3067
3779
|
if (state === State.CONFIRMING) {
|
|
3068
|
-
console.log(`CONFIRM: ${
|
|
3780
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3069
3781
|
process.exit(3);
|
|
3070
3782
|
}
|
|
3071
|
-
|
|
3072
|
-
const output = agent.getResponse(activeSession, screen);
|
|
3073
|
-
if (output) {
|
|
3074
|
-
console.log(output);
|
|
3075
|
-
}
|
|
3076
3783
|
}
|
|
3077
3784
|
|
|
3078
3785
|
/**
|
|
@@ -3087,9 +3794,10 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3087
3794
|
}
|
|
3088
3795
|
|
|
3089
3796
|
const before = tmuxCapture(session);
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3797
|
+
const beforeState = agent.getState(before);
|
|
3798
|
+
if (beforeState !== State.CONFIRMING) {
|
|
3799
|
+
console.log(`Already ${beforeState}`);
|
|
3800
|
+
return;
|
|
3093
3801
|
}
|
|
3094
3802
|
|
|
3095
3803
|
tmuxSend(session, agent.approveKey);
|
|
@@ -3104,7 +3812,7 @@ async function cmdApprove(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3104
3812
|
}
|
|
3105
3813
|
|
|
3106
3814
|
if (state === State.CONFIRMING) {
|
|
3107
|
-
console.log(`CONFIRM: ${
|
|
3815
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3108
3816
|
process.exit(3);
|
|
3109
3817
|
}
|
|
3110
3818
|
|
|
@@ -3123,6 +3831,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3123
3831
|
process.exit(1);
|
|
3124
3832
|
}
|
|
3125
3833
|
|
|
3834
|
+
const before = tmuxCapture(session);
|
|
3835
|
+
const beforeState = agent.getState(before);
|
|
3836
|
+
if (beforeState !== State.CONFIRMING) {
|
|
3837
|
+
console.log(`Already ${beforeState}`);
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3126
3841
|
tmuxSend(session, agent.rejectKey);
|
|
3127
3842
|
|
|
3128
3843
|
if (!wait) return;
|
|
@@ -3145,7 +3860,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3145
3860
|
* @param {string | null | undefined} customInstructions
|
|
3146
3861
|
* @param {{wait?: boolean, yolo?: boolean, fresh?: boolean, timeoutMs?: number}} [options]
|
|
3147
3862
|
*/
|
|
3148
|
-
async function cmdReview(
|
|
3863
|
+
async function cmdReview(
|
|
3864
|
+
agent,
|
|
3865
|
+
session,
|
|
3866
|
+
option,
|
|
3867
|
+
customInstructions,
|
|
3868
|
+
{ wait = true, yolo = false, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
|
|
3869
|
+
) {
|
|
3149
3870
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3150
3871
|
|
|
3151
3872
|
// Reset conversation if --fresh and session exists
|
|
@@ -3171,7 +3892,11 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3171
3892
|
|
|
3172
3893
|
// AX_REVIEW_MODE=exec: bypass /review command, send instructions directly
|
|
3173
3894
|
if (process.env.AX_REVIEW_MODE === "exec" && option === "custom" && customInstructions) {
|
|
3174
|
-
return cmdAsk(agent, session, customInstructions, {
|
|
3895
|
+
return cmdAsk(agent, session, customInstructions, {
|
|
3896
|
+
noWait: !wait,
|
|
3897
|
+
yolo,
|
|
3898
|
+
timeoutMs,
|
|
3899
|
+
});
|
|
3175
3900
|
}
|
|
3176
3901
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3177
3902
|
|
|
@@ -3183,7 +3908,9 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3183
3908
|
}
|
|
3184
3909
|
|
|
3185
3910
|
/** @type {string} */
|
|
3186
|
-
const activeSession = sessionExists
|
|
3911
|
+
const activeSession = sessionExists
|
|
3912
|
+
? /** @type {string} */ (session)
|
|
3913
|
+
: await cmdStart(agent, session, { yolo });
|
|
3187
3914
|
|
|
3188
3915
|
tmuxSendLiteral(activeSession, "/review");
|
|
3189
3916
|
await sleep(50);
|
|
@@ -3205,12 +3932,11 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3205
3932
|
|
|
3206
3933
|
if (!wait) return;
|
|
3207
3934
|
|
|
3208
|
-
// Yolo mode on a safe session: auto-approve until done
|
|
3209
3935
|
const useAutoApprove = yolo && !nativeYolo;
|
|
3210
3936
|
|
|
3211
3937
|
const { state, screen } = useAutoApprove
|
|
3212
|
-
? await autoApproveLoop(agent, activeSession, timeoutMs)
|
|
3213
|
-
: await
|
|
3938
|
+
? await autoApproveLoop(agent, activeSession, timeoutMs, streamResponse)
|
|
3939
|
+
: await streamResponse(agent, activeSession, timeoutMs);
|
|
3214
3940
|
|
|
3215
3941
|
if (state === State.RATE_LIMITED) {
|
|
3216
3942
|
console.log(`RATE_LIMITED: ${agent.parseRetryTime(screen)}`);
|
|
@@ -3218,12 +3944,9 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3218
3944
|
}
|
|
3219
3945
|
|
|
3220
3946
|
if (state === State.CONFIRMING) {
|
|
3221
|
-
console.log(`CONFIRM: ${
|
|
3947
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3222
3948
|
process.exit(3);
|
|
3223
3949
|
}
|
|
3224
|
-
|
|
3225
|
-
const response = agent.getResponse(activeSession, screen);
|
|
3226
|
-
console.log(response || "");
|
|
3227
3950
|
}
|
|
3228
3951
|
|
|
3229
3952
|
/**
|
|
@@ -3254,7 +3977,7 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3254
3977
|
}
|
|
3255
3978
|
|
|
3256
3979
|
if (state === State.CONFIRMING) {
|
|
3257
|
-
console.log(`CONFIRM: ${
|
|
3980
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3258
3981
|
process.exit(3);
|
|
3259
3982
|
}
|
|
3260
3983
|
|
|
@@ -3266,6 +3989,8 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3266
3989
|
const output = agent.getResponse(session, screen, index);
|
|
3267
3990
|
if (output) {
|
|
3268
3991
|
console.log(output);
|
|
3992
|
+
} else {
|
|
3993
|
+
console.log("READY_NO_CONTENT");
|
|
3269
3994
|
}
|
|
3270
3995
|
}
|
|
3271
3996
|
|
|
@@ -3288,7 +4013,7 @@ function cmdStatus(agent, session) {
|
|
|
3288
4013
|
}
|
|
3289
4014
|
|
|
3290
4015
|
if (state === State.CONFIRMING) {
|
|
3291
|
-
console.log(`CONFIRM: ${
|
|
4016
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3292
4017
|
process.exit(3);
|
|
3293
4018
|
}
|
|
3294
4019
|
|
|
@@ -3296,6 +4021,10 @@ function cmdStatus(agent, session) {
|
|
|
3296
4021
|
console.log("THINKING");
|
|
3297
4022
|
process.exit(4);
|
|
3298
4023
|
}
|
|
4024
|
+
|
|
4025
|
+
// READY (or STARTING/UPDATE_PROMPT which are transient)
|
|
4026
|
+
console.log("READY");
|
|
4027
|
+
process.exit(0);
|
|
3299
4028
|
}
|
|
3300
4029
|
|
|
3301
4030
|
/**
|
|
@@ -3409,7 +4138,7 @@ async function cmdSelect(agent, session, n, { wait = false, timeoutMs } = {}) {
|
|
|
3409
4138
|
}
|
|
3410
4139
|
|
|
3411
4140
|
if (state === State.CONFIRMING) {
|
|
3412
|
-
console.log(`CONFIRM: ${
|
|
4141
|
+
console.log(`CONFIRM: ${formatConfirmationOutput(screen, agent)}`);
|
|
3413
4142
|
process.exit(3);
|
|
3414
4143
|
}
|
|
3415
4144
|
|
|
@@ -3422,20 +4151,43 @@ async function cmdSelect(agent, session, n, { wait = false, timeoutMs } = {}) {
|
|
|
3422
4151
|
// =============================================================================
|
|
3423
4152
|
|
|
3424
4153
|
/**
|
|
3425
|
-
*
|
|
4154
|
+
* Resolve the agent to use based on (in priority order):
|
|
4155
|
+
* 1. Explicit --tool flag
|
|
4156
|
+
* 2. Session name (e.g., "claude-archangel-..." → ClaudeAgent)
|
|
4157
|
+
* 3. CLI invocation name (axclaude, axcodex)
|
|
4158
|
+
* 4. AX_DEFAULT_TOOL environment variable
|
|
4159
|
+
* 5. Default to CodexAgent
|
|
4160
|
+
*
|
|
4161
|
+
* @param {{toolFlag?: string, sessionName?: string | null}} options
|
|
4162
|
+
* @returns {{agent: Agent, error?: string}}
|
|
3426
4163
|
*/
|
|
3427
|
-
function
|
|
4164
|
+
function resolveAgent({ toolFlag, sessionName } = {}) {
|
|
4165
|
+
// 1. Explicit --tool flag takes highest priority
|
|
4166
|
+
if (toolFlag) {
|
|
4167
|
+
if (toolFlag === "claude") return { agent: ClaudeAgent };
|
|
4168
|
+
if (toolFlag === "codex") return { agent: CodexAgent };
|
|
4169
|
+
return { agent: CodexAgent, error: `unknown tool '${toolFlag}'` };
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
// 2. Infer from session name (e.g., "claude-archangel-..." or "codex-partner-...")
|
|
4173
|
+
if (sessionName) {
|
|
4174
|
+
const parsed = parseSessionName(sessionName);
|
|
4175
|
+
if (parsed?.tool === "claude") return { agent: ClaudeAgent };
|
|
4176
|
+
if (parsed?.tool === "codex") return { agent: CodexAgent };
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
// 3. CLI invocation name
|
|
3428
4180
|
const invoked = path.basename(process.argv[1], ".js");
|
|
3429
|
-
if (invoked === "axclaude" || invoked === "claude") return ClaudeAgent;
|
|
3430
|
-
if (invoked === "axcodex" || invoked === "codex") return CodexAgent;
|
|
4181
|
+
if (invoked === "axclaude" || invoked === "claude") return { agent: ClaudeAgent };
|
|
4182
|
+
if (invoked === "axcodex" || invoked === "codex") return { agent: CodexAgent };
|
|
3431
4183
|
|
|
3432
|
-
//
|
|
4184
|
+
// 4. AX_DEFAULT_TOOL environment variable
|
|
3433
4185
|
const defaultTool = process.env.AX_DEFAULT_TOOL;
|
|
3434
|
-
if (defaultTool === "claude") return ClaudeAgent;
|
|
3435
|
-
if (defaultTool === "codex" || !defaultTool) return CodexAgent;
|
|
4186
|
+
if (defaultTool === "claude") return { agent: ClaudeAgent };
|
|
4187
|
+
if (defaultTool === "codex" || !defaultTool) return { agent: CodexAgent };
|
|
3436
4188
|
|
|
3437
4189
|
console.error(`WARNING: invalid AX_DEFAULT_TOOL="${defaultTool}", using codex`);
|
|
3438
|
-
return CodexAgent;
|
|
4190
|
+
return { agent: CodexAgent };
|
|
3439
4191
|
}
|
|
3440
4192
|
|
|
3441
4193
|
/**
|
|
@@ -3444,23 +4196,30 @@ function getAgentFromInvocation() {
|
|
|
3444
4196
|
*/
|
|
3445
4197
|
function printHelp(agent, cliName) {
|
|
3446
4198
|
const name = cliName;
|
|
3447
|
-
const backendName = agent.
|
|
4199
|
+
const backendName = agent.displayName;
|
|
3448
4200
|
const hasReview = !!agent.reviewOptions;
|
|
3449
4201
|
|
|
3450
|
-
console.log(`${name}
|
|
4202
|
+
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
4203
|
+
|
|
4204
|
+
Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
3451
4205
|
|
|
3452
4206
|
Commands:
|
|
3453
4207
|
agents List all running agents with state and log paths
|
|
4208
|
+
target Show default target session for current tool
|
|
3454
4209
|
attach [SESSION] Attach to agent session interactively
|
|
3455
4210
|
log SESSION View conversation log (--tail=N, --follow, --reasoning)
|
|
3456
|
-
mailbox View
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
kill Kill sessions
|
|
4211
|
+
mailbox View archangel observations (--limit=N, --branch=X, --all)
|
|
4212
|
+
summon [name] Summon archangels (all, or by name)
|
|
4213
|
+
recall [name] Recall archangels (all, or by name)
|
|
4214
|
+
kill Kill sessions (--all, --session=NAME, --orphans [--force])
|
|
3460
4215
|
status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
|
|
3461
4216
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
3462
|
-
debug Show raw screen output and detected state${
|
|
3463
|
-
|
|
4217
|
+
debug Show raw screen output and detected state${
|
|
4218
|
+
hasReview
|
|
4219
|
+
? `
|
|
4220
|
+
review [TYPE] Review code: pr, uncommitted, commit, custom`
|
|
4221
|
+
: ""
|
|
4222
|
+
}
|
|
3464
4223
|
select N Select menu option N
|
|
3465
4224
|
approve Approve pending action (send 'y')
|
|
3466
4225
|
reject Reject pending action (send 'n')
|
|
@@ -3471,37 +4230,42 @@ Commands:
|
|
|
3471
4230
|
|
|
3472
4231
|
Flags:
|
|
3473
4232
|
--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:
|
|
4233
|
+
--session=NAME Target session by name, archangel name, or UUID prefix (self = current)
|
|
4234
|
+
--wait Wait for response (default for messages; required for approve/reject)
|
|
4235
|
+
--no-wait Fire-and-forget: send message, print session ID, exit immediately
|
|
4236
|
+
--timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
|
|
3478
4237
|
--yolo Skip all confirmations (dangerous)
|
|
3479
4238
|
--fresh Reset conversation before review
|
|
4239
|
+
--orphans Kill orphaned claude/codex processes (PPID=1)
|
|
4240
|
+
--force Use SIGKILL instead of SIGTERM (with --orphans)
|
|
3480
4241
|
|
|
3481
4242
|
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
|
|
4243
|
+
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
4244
|
+
${agent.envVar} Override default session name
|
|
4245
|
+
AX_CLAUDE_CONFIG_DIR Override Claude config directory (default: ~/.claude)
|
|
4246
|
+
AX_CODEX_CONFIG_DIR Override Codex config directory (default: ~/.codex)
|
|
4247
|
+
AX_REVIEW_MODE=exec Bypass /review, send instructions directly (codex only)
|
|
4248
|
+
AX_DEBUG=1 Enable debug logging
|
|
3488
4249
|
|
|
3489
4250
|
Examples:
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
4251
|
+
${name} "explain this codebase"
|
|
4252
|
+
${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
|
|
4253
|
+
${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
|
|
4254
|
+
${name} review uncommitted --wait
|
|
4255
|
+
${name} approve --wait
|
|
4256
|
+
${name} kill # Kill agents in current project
|
|
4257
|
+
${name} kill --all # Kill all agents across all projects
|
|
4258
|
+
${name} kill --session=NAME # Kill specific session
|
|
4259
|
+
${name} send "1[Enter]" # Recovery: select option 1 and press Enter
|
|
4260
|
+
${name} send "[Escape][Escape]" # Recovery: escape out of a dialog
|
|
4261
|
+
${name} summon # Summon all archangels from .ai/agents/*.md
|
|
4262
|
+
${name} summon reviewer # Summon by name (creates config if new)
|
|
4263
|
+
${name} recall # Recall all archangels
|
|
4264
|
+
${name} recall reviewer # Recall one by name
|
|
4265
|
+
${name} agents # List all agents (shows TYPE=archangel)
|
|
4266
|
+
|
|
4267
|
+
Note: Reviews and complex tasks may take several minutes.
|
|
4268
|
+
Use Bash run_in_background for long operations (not --no-wait).`);
|
|
3505
4269
|
}
|
|
3506
4270
|
|
|
3507
4271
|
async function main() {
|
|
@@ -3516,33 +4280,21 @@ async function main() {
|
|
|
3516
4280
|
const args = process.argv.slice(2);
|
|
3517
4281
|
const cliName = path.basename(process.argv[1], ".js");
|
|
3518
4282
|
|
|
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
|
-
}
|
|
4283
|
+
// Parse all flags and positionals in one place
|
|
4284
|
+
const { flags, positionals } = parseCliArgs(args);
|
|
4285
|
+
|
|
4286
|
+
if (flags.version) {
|
|
4287
|
+
console.log(VERSION);
|
|
4288
|
+
process.exit(0);
|
|
3538
4289
|
}
|
|
3539
4290
|
|
|
3540
|
-
//
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
4291
|
+
// Extract flags into local variables for convenience
|
|
4292
|
+
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force } = flags;
|
|
4293
|
+
|
|
4294
|
+
// Session resolution (must happen before agent resolution so we can infer tool from session name)
|
|
4295
|
+
let session = null;
|
|
4296
|
+
if (flags.session) {
|
|
4297
|
+
if (flags.session === "self") {
|
|
3546
4298
|
const current = tmuxCurrentSession();
|
|
3547
4299
|
if (!current) {
|
|
3548
4300
|
console.log("ERROR: --session=self requires running inside tmux");
|
|
@@ -3550,99 +4302,106 @@ async function main() {
|
|
|
3550
4302
|
}
|
|
3551
4303
|
session = current;
|
|
3552
4304
|
} else {
|
|
3553
|
-
// Resolve partial names,
|
|
3554
|
-
session = resolveSessionName(
|
|
4305
|
+
// Resolve partial names, archangel names, and UUID prefixes
|
|
4306
|
+
session = resolveSessionName(flags.session);
|
|
3555
4307
|
}
|
|
3556
4308
|
}
|
|
3557
4309
|
|
|
3558
|
-
//
|
|
4310
|
+
// Agent resolution (considers --tool flag, session name, invocation, and env vars)
|
|
4311
|
+
const { agent, error: agentError } = resolveAgent({ toolFlag: flags.tool, sessionName: session });
|
|
4312
|
+
if (agentError) {
|
|
4313
|
+
console.log(`ERROR: ${agentError}`);
|
|
4314
|
+
process.exit(1);
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
// If no explicit session, use agent's default
|
|
4318
|
+
if (!session) {
|
|
4319
|
+
session = agent.getDefaultSession();
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
// Timeout (convert seconds to milliseconds)
|
|
3559
4323
|
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
const val = parseInt(timeoutArg.split("=")[1], 10);
|
|
3563
|
-
if (isNaN(val) || val <= 0) {
|
|
4324
|
+
if (flags.timeout !== undefined) {
|
|
4325
|
+
if (isNaN(flags.timeout) || flags.timeout <= 0) {
|
|
3564
4326
|
console.log("ERROR: invalid timeout");
|
|
3565
4327
|
process.exit(1);
|
|
3566
4328
|
}
|
|
3567
|
-
timeoutMs =
|
|
4329
|
+
timeoutMs = flags.timeout * 1000;
|
|
3568
4330
|
}
|
|
3569
4331
|
|
|
3570
4332
|
// 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
|
-
}
|
|
4333
|
+
const tail = flags.tail ?? 50;
|
|
3576
4334
|
|
|
3577
4335
|
// 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
|
-
}
|
|
4336
|
+
const limit = flags.limit ?? 20;
|
|
3583
4337
|
|
|
3584
4338
|
// 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];
|
|
4339
|
+
const branch = flags.branch ?? null;
|
|
4340
|
+
|
|
4341
|
+
// Command is first positional
|
|
4342
|
+
const cmd = positionals[0];
|
|
3606
4343
|
|
|
3607
4344
|
// Dispatch commands
|
|
3608
4345
|
if (cmd === "agents") return cmdAgents();
|
|
3609
|
-
if (cmd === "
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
4346
|
+
if (cmd === "target") {
|
|
4347
|
+
const defaultSession = agent.getDefaultSession();
|
|
4348
|
+
if (defaultSession) {
|
|
4349
|
+
console.log(defaultSession);
|
|
4350
|
+
} else {
|
|
4351
|
+
console.log("NO_TARGET");
|
|
4352
|
+
process.exit(1);
|
|
4353
|
+
}
|
|
4354
|
+
return;
|
|
4355
|
+
}
|
|
4356
|
+
if (cmd === "summon") return cmdSummon(positionals[1]);
|
|
4357
|
+
if (cmd === "recall") return cmdRecall(positionals[1]);
|
|
4358
|
+
if (cmd === "archangel") return cmdArchangel(positionals[1]);
|
|
4359
|
+
if (cmd === "kill") return cmdKill(session, { all, orphans, force });
|
|
4360
|
+
if (cmd === "attach") return cmdAttach(positionals[1] || session);
|
|
4361
|
+
if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
|
|
3614
4362
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
3615
4363
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
3616
4364
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
3617
|
-
if (cmd === "review")
|
|
4365
|
+
if (cmd === "review")
|
|
4366
|
+
return cmdReview(agent, session, positionals[1], positionals[2], {
|
|
4367
|
+
wait,
|
|
4368
|
+
fresh,
|
|
4369
|
+
timeoutMs,
|
|
4370
|
+
});
|
|
3618
4371
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
3619
4372
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
3620
4373
|
if (cmd === "output") {
|
|
3621
|
-
const indexArg =
|
|
4374
|
+
const indexArg = positionals[1];
|
|
3622
4375
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
3623
4376
|
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
3624
4377
|
}
|
|
3625
|
-
if (cmd === "send" &&
|
|
4378
|
+
if (cmd === "send" && positionals.length > 1)
|
|
4379
|
+
return cmdSend(session, positionals.slice(1).join(" "));
|
|
3626
4380
|
if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
|
|
3627
4381
|
if (cmd === "reset") return cmdAsk(agent, session, "/new", { noWait: true, timeoutMs });
|
|
3628
|
-
if (cmd === "select" &&
|
|
4382
|
+
if (cmd === "select" && positionals[1])
|
|
4383
|
+
return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
|
|
3629
4384
|
|
|
3630
4385
|
// Default: send message
|
|
3631
|
-
let message =
|
|
4386
|
+
let message = positionals.join(" ");
|
|
3632
4387
|
if (!message && hasStdinData()) {
|
|
3633
4388
|
message = await readStdin();
|
|
3634
4389
|
}
|
|
3635
4390
|
|
|
3636
|
-
if (!message ||
|
|
4391
|
+
if (!message || flags.help) {
|
|
3637
4392
|
printHelp(agent, cliName);
|
|
3638
4393
|
process.exit(0);
|
|
3639
4394
|
}
|
|
3640
4395
|
|
|
3641
|
-
// Detect "please review" and route to custom review mode
|
|
3642
|
-
const reviewMatch = message.match(/^please review\s*(.*)/i);
|
|
4396
|
+
// Detect "review ..." or "please review ..." and route to custom review mode
|
|
4397
|
+
const reviewMatch = message.match(/^(?:please )?review\s*(.*)/i);
|
|
3643
4398
|
if (reviewMatch && agent.reviewOptions) {
|
|
3644
4399
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
3645
|
-
return cmdReview(agent, session, "custom", customInstructions, {
|
|
4400
|
+
return cmdReview(agent, session, "custom", customInstructions, {
|
|
4401
|
+
wait: !noWait,
|
|
4402
|
+
yolo,
|
|
4403
|
+
timeoutMs: flags.timeout !== undefined ? timeoutMs : REVIEW_TIMEOUT_MS,
|
|
4404
|
+
});
|
|
3646
4405
|
}
|
|
3647
4406
|
|
|
3648
4407
|
return cmdAsk(agent, session, message, { noWait, yolo, timeoutMs });
|
|
@@ -3650,17 +4409,21 @@ async function main() {
|
|
|
3650
4409
|
|
|
3651
4410
|
// Run main() only when executed directly (not when imported for testing)
|
|
3652
4411
|
// Use realpathSync to handle symlinks (e.g., axclaude, axcodex bin entries)
|
|
3653
|
-
const
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
}
|
|
4412
|
+
const isDirectRun =
|
|
4413
|
+
process.argv[1] &&
|
|
4414
|
+
(() => {
|
|
4415
|
+
try {
|
|
4416
|
+
return realpathSync(process.argv[1]) === __filename;
|
|
4417
|
+
} catch {
|
|
4418
|
+
return false;
|
|
4419
|
+
}
|
|
4420
|
+
})();
|
|
3661
4421
|
if (isDirectRun) {
|
|
3662
4422
|
main().catch((err) => {
|
|
3663
4423
|
console.log(`ERROR: ${err.message}`);
|
|
4424
|
+
if (err instanceof TimeoutError && err.session) {
|
|
4425
|
+
console.log(`Hint: Use 'ax debug --session=${err.session}' to see current screen state`);
|
|
4426
|
+
}
|
|
3664
4427
|
process.exit(1);
|
|
3665
4428
|
});
|
|
3666
4429
|
}
|
|
@@ -3670,6 +4433,7 @@ export {
|
|
|
3670
4433
|
parseSessionName,
|
|
3671
4434
|
parseAgentConfig,
|
|
3672
4435
|
parseKeySequence,
|
|
4436
|
+
parseCliArgs,
|
|
3673
4437
|
getClaudeProjectPath,
|
|
3674
4438
|
matchesPattern,
|
|
3675
4439
|
getBaseDir,
|
|
@@ -3681,4 +4445,3 @@ export {
|
|
|
3681
4445
|
detectState,
|
|
3682
4446
|
State,
|
|
3683
4447
|
};
|
|
3684
|
-
|