ax-agents 0.0.1-alpha.4 → 0.0.1-alpha.6
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 +585 -376
- 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,13 +9,21 @@
|
|
|
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 {
|
|
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
|
+
} from "node:fs";
|
|
18
27
|
import { randomUUID } from "node:crypto";
|
|
19
28
|
import { fileURLToPath } from "node:url";
|
|
20
29
|
import path from "node:path";
|
|
@@ -32,12 +41,12 @@ const VERSION = packageJson.version;
|
|
|
32
41
|
/**
|
|
33
42
|
* @typedef {Object} ParsedSession
|
|
34
43
|
* @property {string} tool
|
|
35
|
-
* @property {string} [
|
|
44
|
+
* @property {string} [archangelName]
|
|
36
45
|
* @property {string} [uuid]
|
|
37
46
|
*/
|
|
38
47
|
|
|
39
48
|
/**
|
|
40
|
-
* @typedef {Object}
|
|
49
|
+
* @typedef {Object} ArchangelConfig
|
|
41
50
|
* @property {string} name
|
|
42
51
|
* @property {ToolName} tool
|
|
43
52
|
* @property {string[]} watch
|
|
@@ -110,8 +119,9 @@ const VERSION = packageJson.version;
|
|
|
110
119
|
*/
|
|
111
120
|
|
|
112
121
|
/**
|
|
122
|
+
* @typedef {{matcher: string, hooks: Array<{type: string, command: string, timeout?: number}>}} ClaudeHookEntry
|
|
113
123
|
* @typedef {Object} ClaudeSettings
|
|
114
|
-
* @property {{UserPromptSubmit?:
|
|
124
|
+
* @property {{UserPromptSubmit?: ClaudeHookEntry[], PostToolUse?: ClaudeHookEntry[], Stop?: ClaudeHookEntry[], [key: string]: ClaudeHookEntry[] | undefined}} [hooks]
|
|
115
125
|
*/
|
|
116
126
|
|
|
117
127
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
@@ -221,7 +231,9 @@ function tmuxKill(session) {
|
|
|
221
231
|
*/
|
|
222
232
|
function tmuxNewSession(session, command) {
|
|
223
233
|
// Use spawnSync to avoid command injection via session/command
|
|
224
|
-
const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
|
|
234
|
+
const result = spawnSync("tmux", ["new-session", "-d", "-s", session, command], {
|
|
235
|
+
encoding: "utf-8",
|
|
236
|
+
});
|
|
225
237
|
if (result.status !== 0) throw new Error(result.stderr || "tmux new-session failed");
|
|
226
238
|
}
|
|
227
239
|
|
|
@@ -230,7 +242,9 @@ function tmuxNewSession(session, command) {
|
|
|
230
242
|
*/
|
|
231
243
|
function tmuxCurrentSession() {
|
|
232
244
|
if (!process.env.TMUX) return null;
|
|
233
|
-
const result = spawnSync("tmux", ["display-message", "-p", "#S"], {
|
|
245
|
+
const result = spawnSync("tmux", ["display-message", "-p", "#S"], {
|
|
246
|
+
encoding: "utf-8",
|
|
247
|
+
});
|
|
234
248
|
if (result.status !== 0) return null;
|
|
235
249
|
return result.stdout.trim();
|
|
236
250
|
}
|
|
@@ -242,9 +256,13 @@ function tmuxCurrentSession() {
|
|
|
242
256
|
*/
|
|
243
257
|
function isYoloSession(session) {
|
|
244
258
|
try {
|
|
245
|
-
const result = spawnSync(
|
|
246
|
-
|
|
247
|
-
|
|
259
|
+
const result = spawnSync(
|
|
260
|
+
"tmux",
|
|
261
|
+
["display-message", "-t", session, "-p", "#{pane_start_command}"],
|
|
262
|
+
{
|
|
263
|
+
encoding: "utf-8",
|
|
264
|
+
},
|
|
265
|
+
);
|
|
248
266
|
if (result.status !== 0) return false;
|
|
249
267
|
const cmd = result.stdout.trim();
|
|
250
268
|
return cmd.includes("--dangerously-");
|
|
@@ -264,9 +282,15 @@ const POLL_MS = parseInt(process.env.AX_POLL_MS || "200", 10);
|
|
|
264
282
|
const DEFAULT_TIMEOUT_MS = parseInt(process.env.AX_TIMEOUT_MS || "120000", 10);
|
|
265
283
|
const REVIEW_TIMEOUT_MS = parseInt(process.env.AX_REVIEW_TIMEOUT_MS || "900000", 10); // 15 minutes
|
|
266
284
|
const STARTUP_TIMEOUT_MS = parseInt(process.env.AX_STARTUP_TIMEOUT_MS || "30000", 10);
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
285
|
+
const ARCHANGEL_STARTUP_TIMEOUT_MS = parseInt(
|
|
286
|
+
process.env.AX_ARCHANGEL_STARTUP_TIMEOUT_MS || "60000",
|
|
287
|
+
10,
|
|
288
|
+
);
|
|
289
|
+
const ARCHANGEL_RESPONSE_TIMEOUT_MS = parseInt(
|
|
290
|
+
process.env.AX_ARCHANGEL_RESPONSE_TIMEOUT_MS || "300000",
|
|
291
|
+
10,
|
|
292
|
+
); // 5 minutes
|
|
293
|
+
const ARCHANGEL_HEALTH_CHECK_MS = parseInt(process.env.AX_ARCHANGEL_HEALTH_CHECK_MS || "30000", 10);
|
|
270
294
|
const STABLE_MS = parseInt(process.env.AX_STABLE_MS || "1000", 10);
|
|
271
295
|
const APPROVE_DELAY_MS = parseInt(process.env.AX_APPROVE_DELAY_MS || "100", 10);
|
|
272
296
|
const MAILBOX_MAX_AGE_MS = parseInt(process.env.AX_MAILBOX_MAX_AGE_MS || "3600000", 10); // 1 hour
|
|
@@ -274,9 +298,9 @@ const CLAUDE_CONFIG_DIR = process.env.AX_CLAUDE_CONFIG_DIR || path.join(os.homed
|
|
|
274
298
|
const CODEX_CONFIG_DIR = process.env.AX_CODEX_CONFIG_DIR || path.join(os.homedir(), ".codex");
|
|
275
299
|
const TRUNCATE_USER_LEN = 500;
|
|
276
300
|
const TRUNCATE_THINKING_LEN = 300;
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const
|
|
301
|
+
const ARCHANGEL_GIT_CONTEXT_HOURS = 4;
|
|
302
|
+
const ARCHANGEL_GIT_CONTEXT_MAX_LINES = 200;
|
|
303
|
+
const ARCHANGEL_PARENT_CONTEXT_ENTRIES = 10;
|
|
280
304
|
|
|
281
305
|
/**
|
|
282
306
|
* @param {string} session
|
|
@@ -304,7 +328,9 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
304
328
|
function findCallerPid() {
|
|
305
329
|
let pid = process.ppid;
|
|
306
330
|
while (pid > 1) {
|
|
307
|
-
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
331
|
+
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
332
|
+
encoding: "utf-8",
|
|
333
|
+
});
|
|
308
334
|
if (result.status !== 0) break;
|
|
309
335
|
const parts = result.stdout.trim().split(/\s+/);
|
|
310
336
|
const ppid = parseInt(parts[0], 10);
|
|
@@ -359,14 +385,18 @@ function parseSessionName(session) {
|
|
|
359
385
|
const tool = match[1].toLowerCase();
|
|
360
386
|
const rest = match[2];
|
|
361
387
|
|
|
362
|
-
//
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
388
|
+
// Archangel: {tool}-archangel-{name}-{uuid}
|
|
389
|
+
const archangelMatch = rest.match(
|
|
390
|
+
/^archangel-(.+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i,
|
|
391
|
+
);
|
|
392
|
+
if (archangelMatch) {
|
|
393
|
+
return { tool, archangelName: archangelMatch[1], uuid: archangelMatch[2] };
|
|
366
394
|
}
|
|
367
395
|
|
|
368
396
|
// Partner: {tool}-partner-{uuid}
|
|
369
|
-
const partnerMatch = rest.match(
|
|
397
|
+
const partnerMatch = rest.match(
|
|
398
|
+
/^partner-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i,
|
|
399
|
+
);
|
|
370
400
|
if (partnerMatch) {
|
|
371
401
|
return { tool, uuid: partnerMatch[1] };
|
|
372
402
|
}
|
|
@@ -399,9 +429,13 @@ function getClaudeProjectPath(cwd) {
|
|
|
399
429
|
*/
|
|
400
430
|
function getTmuxSessionCwd(sessionName) {
|
|
401
431
|
try {
|
|
402
|
-
const result = spawnSync(
|
|
403
|
-
|
|
404
|
-
|
|
432
|
+
const result = spawnSync(
|
|
433
|
+
"tmux",
|
|
434
|
+
["display-message", "-t", sessionName, "-p", "#{pane_current_path}"],
|
|
435
|
+
{
|
|
436
|
+
encoding: "utf-8",
|
|
437
|
+
},
|
|
438
|
+
);
|
|
405
439
|
if (result.status === 0) return result.stdout.trim();
|
|
406
440
|
} catch (err) {
|
|
407
441
|
debugError("getTmuxSessionCwd", err);
|
|
@@ -425,7 +459,9 @@ function findClaudeLogPath(sessionId, sessionName) {
|
|
|
425
459
|
if (existsSync(indexPath)) {
|
|
426
460
|
try {
|
|
427
461
|
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
428
|
-
const entry = index.entries?.find(
|
|
462
|
+
const entry = index.entries?.find(
|
|
463
|
+
/** @param {{sessionId: string, fullPath?: string}} e */ (e) => e.sessionId === sessionId,
|
|
464
|
+
);
|
|
429
465
|
if (entry?.fullPath) return entry.fullPath;
|
|
430
466
|
} catch (err) {
|
|
431
467
|
debugError("findClaudeLogPath", err);
|
|
@@ -447,9 +483,13 @@ function findCodexLogPath(sessionName) {
|
|
|
447
483
|
// For Codex, we need to match by timing since we can't control the session ID
|
|
448
484
|
// Get tmux session creation time
|
|
449
485
|
try {
|
|
450
|
-
const result = spawnSync(
|
|
451
|
-
|
|
452
|
-
|
|
486
|
+
const result = spawnSync(
|
|
487
|
+
"tmux",
|
|
488
|
+
["display-message", "-t", sessionName, "-p", "#{session_created}"],
|
|
489
|
+
{
|
|
490
|
+
encoding: "utf-8",
|
|
491
|
+
},
|
|
492
|
+
);
|
|
453
493
|
if (result.status !== 0) return null;
|
|
454
494
|
const createdTs = parseInt(result.stdout.trim(), 10) * 1000; // tmux gives seconds, we need ms
|
|
455
495
|
if (isNaN(createdTs)) return null;
|
|
@@ -483,7 +523,11 @@ function findCodexLogPath(sessionName) {
|
|
|
483
523
|
// Log file should be created shortly after session start
|
|
484
524
|
// Allow small negative diff (-2s) for clock skew, up to 60s for slow starts
|
|
485
525
|
if (diff >= -2000 && diff < 60000) {
|
|
486
|
-
candidates.push({
|
|
526
|
+
candidates.push({
|
|
527
|
+
file,
|
|
528
|
+
diff: Math.abs(diff),
|
|
529
|
+
path: path.join(dayDir, file),
|
|
530
|
+
});
|
|
487
531
|
}
|
|
488
532
|
}
|
|
489
533
|
|
|
@@ -496,7 +540,6 @@ function findCodexLogPath(sessionName) {
|
|
|
496
540
|
}
|
|
497
541
|
}
|
|
498
542
|
|
|
499
|
-
|
|
500
543
|
/**
|
|
501
544
|
* Extract assistant text responses from a JSONL log file.
|
|
502
545
|
* This provides clean text without screen-scraped artifacts.
|
|
@@ -567,15 +610,15 @@ function resolveSessionName(partial) {
|
|
|
567
610
|
// Exact match
|
|
568
611
|
if (agentSessions.includes(partial)) return partial;
|
|
569
612
|
|
|
570
|
-
//
|
|
571
|
-
const
|
|
613
|
+
// Archangel name match (e.g., "reviewer" matches "claude-archangel-reviewer-uuid")
|
|
614
|
+
const archangelMatches = agentSessions.filter((s) => {
|
|
572
615
|
const parsed = parseSessionName(s);
|
|
573
|
-
return parsed?.
|
|
616
|
+
return parsed?.archangelName === partial;
|
|
574
617
|
});
|
|
575
|
-
if (
|
|
576
|
-
if (
|
|
577
|
-
console.log("ERROR: ambiguous
|
|
578
|
-
for (const m of
|
|
618
|
+
if (archangelMatches.length === 1) return archangelMatches[0];
|
|
619
|
+
if (archangelMatches.length > 1) {
|
|
620
|
+
console.log("ERROR: ambiguous archangel name. Matches:");
|
|
621
|
+
for (const m of archangelMatches) console.log(` ${m}`);
|
|
579
622
|
process.exit(1);
|
|
580
623
|
}
|
|
581
624
|
|
|
@@ -604,25 +647,25 @@ function resolveSessionName(partial) {
|
|
|
604
647
|
}
|
|
605
648
|
|
|
606
649
|
// =============================================================================
|
|
607
|
-
// Helpers -
|
|
650
|
+
// Helpers - archangels
|
|
608
651
|
// =============================================================================
|
|
609
652
|
|
|
610
653
|
/**
|
|
611
|
-
* @returns {
|
|
654
|
+
* @returns {ArchangelConfig[]}
|
|
612
655
|
*/
|
|
613
656
|
function loadAgentConfigs() {
|
|
614
657
|
const agentsDir = AGENTS_DIR;
|
|
615
658
|
if (!existsSync(agentsDir)) return [];
|
|
616
659
|
|
|
617
660
|
const files = readdirSync(agentsDir).filter((f) => f.endsWith(".md"));
|
|
618
|
-
/** @type {
|
|
661
|
+
/** @type {ArchangelConfig[]} */
|
|
619
662
|
const configs = [];
|
|
620
663
|
|
|
621
664
|
for (const file of files) {
|
|
622
665
|
try {
|
|
623
666
|
const content = readFileSync(path.join(agentsDir, file), "utf-8");
|
|
624
667
|
const config = parseAgentConfig(file, content);
|
|
625
|
-
if (config &&
|
|
668
|
+
if (config && "error" in config) {
|
|
626
669
|
console.error(`ERROR: ${file}: ${config.error}`);
|
|
627
670
|
continue;
|
|
628
671
|
}
|
|
@@ -638,7 +681,7 @@ function loadAgentConfigs() {
|
|
|
638
681
|
/**
|
|
639
682
|
* @param {string} filename
|
|
640
683
|
* @param {string} content
|
|
641
|
-
* @returns {
|
|
684
|
+
* @returns {ArchangelConfig | {error: string} | null}
|
|
642
685
|
*/
|
|
643
686
|
function parseAgentConfig(filename, content) {
|
|
644
687
|
const name = filename.replace(/\.md$/, "");
|
|
@@ -653,7 +696,9 @@ function parseAgentConfig(filename, content) {
|
|
|
653
696
|
return { error: `Missing frontmatter. File must start with '---'` };
|
|
654
697
|
}
|
|
655
698
|
if (!normalized.includes("\n---\n")) {
|
|
656
|
-
return {
|
|
699
|
+
return {
|
|
700
|
+
error: `Frontmatter not closed. Add '---' on its own line after the YAML block`,
|
|
701
|
+
};
|
|
657
702
|
}
|
|
658
703
|
return { error: `Invalid frontmatter format` };
|
|
659
704
|
}
|
|
@@ -674,9 +719,13 @@ function parseAgentConfig(filename, content) {
|
|
|
674
719
|
const fieldName = line.trim().match(/^(\w+):/)?.[1];
|
|
675
720
|
if (fieldName && !knownFields.includes(fieldName)) {
|
|
676
721
|
// Suggest closest match
|
|
677
|
-
const suggestions = knownFields.filter(
|
|
722
|
+
const suggestions = knownFields.filter(
|
|
723
|
+
(f) => f[0] === fieldName[0] || fieldName.includes(f.slice(0, 3)),
|
|
724
|
+
);
|
|
678
725
|
const hint = suggestions.length > 0 ? ` Did you mean '${suggestions[0]}'?` : "";
|
|
679
|
-
return {
|
|
726
|
+
return {
|
|
727
|
+
error: `Unknown field '${fieldName}'.${hint} Valid fields: ${knownFields.join(", ")}`,
|
|
728
|
+
};
|
|
680
729
|
}
|
|
681
730
|
}
|
|
682
731
|
|
|
@@ -694,7 +743,9 @@ function parseAgentConfig(filename, content) {
|
|
|
694
743
|
const rawValue = intervalMatch[1].trim();
|
|
695
744
|
const parsed = parseInt(rawValue, 10);
|
|
696
745
|
if (isNaN(parsed)) {
|
|
697
|
-
return {
|
|
746
|
+
return {
|
|
747
|
+
error: `Invalid interval '${rawValue}'. Must be a number (seconds)`,
|
|
748
|
+
};
|
|
698
749
|
}
|
|
699
750
|
interval = Math.max(10, Math.min(3600, parsed)); // Clamp to 10s - 1hr
|
|
700
751
|
}
|
|
@@ -706,16 +757,22 @@ function parseAgentConfig(filename, content) {
|
|
|
706
757
|
const rawWatch = watchLine[1].trim();
|
|
707
758
|
// Must be array format
|
|
708
759
|
if (!rawWatch.startsWith("[") || !rawWatch.endsWith("]")) {
|
|
709
|
-
return {
|
|
760
|
+
return {
|
|
761
|
+
error: `Invalid watch format. Must be an array: watch: ["src/**/*.ts"]`,
|
|
762
|
+
};
|
|
710
763
|
}
|
|
711
764
|
const inner = rawWatch.slice(1, -1).trim();
|
|
712
765
|
if (!inner) {
|
|
713
|
-
return {
|
|
766
|
+
return {
|
|
767
|
+
error: `Empty watch array. Add at least one pattern: watch: ["**/*"]`,
|
|
768
|
+
};
|
|
714
769
|
}
|
|
715
770
|
watchPatterns = inner.split(",").map((p) => p.trim().replace(/^["']|["']$/g, ""));
|
|
716
771
|
// Validate patterns aren't empty
|
|
717
772
|
if (watchPatterns.some((p) => !p)) {
|
|
718
|
-
return {
|
|
773
|
+
return {
|
|
774
|
+
error: `Invalid watch pattern. Check for trailing commas or empty values`,
|
|
775
|
+
};
|
|
719
776
|
}
|
|
720
777
|
}
|
|
721
778
|
|
|
@@ -723,11 +780,11 @@ function parseAgentConfig(filename, content) {
|
|
|
723
780
|
}
|
|
724
781
|
|
|
725
782
|
/**
|
|
726
|
-
* @param {
|
|
783
|
+
* @param {ArchangelConfig} config
|
|
727
784
|
* @returns {string}
|
|
728
785
|
*/
|
|
729
|
-
function
|
|
730
|
-
return `${config.tool}-
|
|
786
|
+
function getArchangelSessionPattern(config) {
|
|
787
|
+
return `${config.tool}-archangel-${config.name}`;
|
|
731
788
|
}
|
|
732
789
|
|
|
733
790
|
// =============================================================================
|
|
@@ -831,7 +888,9 @@ function gcMailbox(maxAgeHours = 24) {
|
|
|
831
888
|
/** @returns {string} */
|
|
832
889
|
function getCurrentBranch() {
|
|
833
890
|
try {
|
|
834
|
-
return execSync("git branch --show-current 2>/dev/null", {
|
|
891
|
+
return execSync("git branch --show-current 2>/dev/null", {
|
|
892
|
+
encoding: "utf-8",
|
|
893
|
+
}).trim();
|
|
835
894
|
} catch {
|
|
836
895
|
return "unknown";
|
|
837
896
|
}
|
|
@@ -840,7 +899,9 @@ function getCurrentBranch() {
|
|
|
840
899
|
/** @returns {string} */
|
|
841
900
|
function getCurrentCommit() {
|
|
842
901
|
try {
|
|
843
|
-
return execSync("git rev-parse --short HEAD 2>/dev/null", {
|
|
902
|
+
return execSync("git rev-parse --short HEAD 2>/dev/null", {
|
|
903
|
+
encoding: "utf-8",
|
|
904
|
+
}).trim();
|
|
844
905
|
} catch {
|
|
845
906
|
return "unknown";
|
|
846
907
|
}
|
|
@@ -864,7 +925,9 @@ function getMainBranch() {
|
|
|
864
925
|
/** @returns {string} */
|
|
865
926
|
function getStagedDiff() {
|
|
866
927
|
try {
|
|
867
|
-
return execSync("git diff --cached 2>/dev/null", {
|
|
928
|
+
return execSync("git diff --cached 2>/dev/null", {
|
|
929
|
+
encoding: "utf-8",
|
|
930
|
+
}).trim();
|
|
868
931
|
} catch {
|
|
869
932
|
return "";
|
|
870
933
|
}
|
|
@@ -889,20 +952,18 @@ function getRecentCommitsDiff(hoursAgo = 4) {
|
|
|
889
952
|
const since = `--since="${hoursAgo} hours ago"`;
|
|
890
953
|
|
|
891
954
|
// Get list of commits in range
|
|
892
|
-
const commits = execSync(
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
).trim();
|
|
955
|
+
const commits = execSync(`git log ${mainBranch}..HEAD ${since} --oneline 2>/dev/null`, {
|
|
956
|
+
encoding: "utf-8",
|
|
957
|
+
}).trim();
|
|
896
958
|
|
|
897
959
|
if (!commits) return "";
|
|
898
960
|
|
|
899
961
|
// Get diff for those commits
|
|
900
962
|
const firstCommit = commits.split("\n").filter(Boolean).pop()?.split(" ")[0];
|
|
901
963
|
if (!firstCommit) return "";
|
|
902
|
-
return execSync(
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
).trim();
|
|
964
|
+
return execSync(`git diff ${firstCommit}^..HEAD 2>/dev/null`, {
|
|
965
|
+
encoding: "utf-8",
|
|
966
|
+
}).trim();
|
|
906
967
|
} catch {
|
|
907
968
|
return "";
|
|
908
969
|
}
|
|
@@ -917,7 +978,10 @@ function truncateDiff(diff, maxLines = 200) {
|
|
|
917
978
|
if (!diff) return "";
|
|
918
979
|
const lines = diff.split("\n");
|
|
919
980
|
if (lines.length <= maxLines) return diff;
|
|
920
|
-
return
|
|
981
|
+
return (
|
|
982
|
+
lines.slice(0, maxLines).join("\n") +
|
|
983
|
+
`\n\n... (truncated, ${lines.length - maxLines} more lines)`
|
|
984
|
+
);
|
|
921
985
|
}
|
|
922
986
|
|
|
923
987
|
/**
|
|
@@ -950,9 +1014,9 @@ function buildGitContext(hoursAgo = 4, maxLinesPerSection = 200) {
|
|
|
950
1014
|
// Helpers - parent session context
|
|
951
1015
|
// =============================================================================
|
|
952
1016
|
|
|
953
|
-
// Environment variables used to pass parent session info to
|
|
954
|
-
const
|
|
955
|
-
const
|
|
1017
|
+
// Environment variables used to pass parent session info to archangels
|
|
1018
|
+
const AX_ARCHANGEL_PARENT_SESSION_ENV = "AX_ARCHANGEL_PARENT_SESSION";
|
|
1019
|
+
const AX_ARCHANGEL_PARENT_UUID_ENV = "AX_ARCHANGEL_PARENT_UUID";
|
|
956
1020
|
|
|
957
1021
|
/**
|
|
958
1022
|
* @returns {ParentSession | null}
|
|
@@ -962,7 +1026,7 @@ function findCurrentClaudeSession() {
|
|
|
962
1026
|
const current = tmuxCurrentSession();
|
|
963
1027
|
if (current) {
|
|
964
1028
|
const parsed = parseSessionName(current);
|
|
965
|
-
if (parsed?.tool === "claude" && !parsed.
|
|
1029
|
+
if (parsed?.tool === "claude" && !parsed.archangelName && parsed.uuid) {
|
|
966
1030
|
return { session: current, uuid: parsed.uuid };
|
|
967
1031
|
}
|
|
968
1032
|
}
|
|
@@ -979,7 +1043,7 @@ function findCurrentClaudeSession() {
|
|
|
979
1043
|
for (const session of sessions) {
|
|
980
1044
|
const parsed = parseSessionName(session);
|
|
981
1045
|
if (!parsed || parsed.tool !== "claude") continue;
|
|
982
|
-
if (parsed.
|
|
1046
|
+
if (parsed.archangelName) continue;
|
|
983
1047
|
if (!parsed.uuid) continue;
|
|
984
1048
|
|
|
985
1049
|
const sessionCwd = getTmuxSessionCwd(session);
|
|
@@ -1002,18 +1066,23 @@ function findCurrentClaudeSession() {
|
|
|
1002
1066
|
const claudeProjectDir = path.join(CLAUDE_CONFIG_DIR, "projects", projectPath);
|
|
1003
1067
|
if (existsSync(claudeProjectDir)) {
|
|
1004
1068
|
try {
|
|
1005
|
-
const files = readdirSync(claudeProjectDir).filter(f => f.endsWith(".jsonl"));
|
|
1069
|
+
const files = readdirSync(claudeProjectDir).filter((f) => f.endsWith(".jsonl"));
|
|
1006
1070
|
for (const file of files) {
|
|
1007
1071
|
const uuid = file.replace(".jsonl", "");
|
|
1008
1072
|
// Skip if we already have this from tmux sessions
|
|
1009
|
-
if (candidates.some(c => c.uuid === uuid)) continue;
|
|
1073
|
+
if (candidates.some((c) => c.uuid === uuid)) continue;
|
|
1010
1074
|
|
|
1011
1075
|
const logPath = path.join(claudeProjectDir, file);
|
|
1012
1076
|
try {
|
|
1013
1077
|
const stat = statSync(logPath);
|
|
1014
1078
|
// Only consider logs modified in the last hour (active sessions)
|
|
1015
1079
|
if (Date.now() - stat.mtimeMs < MAILBOX_MAX_AGE_MS) {
|
|
1016
|
-
candidates.push({
|
|
1080
|
+
candidates.push({
|
|
1081
|
+
session: null,
|
|
1082
|
+
uuid,
|
|
1083
|
+
mtime: stat.mtimeMs,
|
|
1084
|
+
logPath,
|
|
1085
|
+
});
|
|
1017
1086
|
}
|
|
1018
1087
|
} catch (err) {
|
|
1019
1088
|
debugError("findCurrentClaudeSession:logStat", err);
|
|
@@ -1035,15 +1104,15 @@ function findCurrentClaudeSession() {
|
|
|
1035
1104
|
* @returns {ParentSession | null}
|
|
1036
1105
|
*/
|
|
1037
1106
|
function findParentSession() {
|
|
1038
|
-
// First check if parent session was passed via environment (for
|
|
1039
|
-
const envUuid = process.env[
|
|
1107
|
+
// First check if parent session was passed via environment (for archangels)
|
|
1108
|
+
const envUuid = process.env[AX_ARCHANGEL_PARENT_UUID_ENV];
|
|
1040
1109
|
if (envUuid) {
|
|
1041
1110
|
// Session name is optional (may be null for non-tmux sessions)
|
|
1042
|
-
const envSession = process.env[
|
|
1111
|
+
const envSession = process.env[AX_ARCHANGEL_PARENT_SESSION_ENV] || null;
|
|
1043
1112
|
return { session: envSession, uuid: envUuid };
|
|
1044
1113
|
}
|
|
1045
1114
|
|
|
1046
|
-
// Fallback to detecting current session (shouldn't be needed for
|
|
1115
|
+
// Fallback to detecting current session (shouldn't be needed for archangels)
|
|
1047
1116
|
return findCurrentClaudeSession();
|
|
1048
1117
|
}
|
|
1049
1118
|
|
|
@@ -1085,7 +1154,9 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1085
1154
|
if (typeof c === "string" && c.length > 10) {
|
|
1086
1155
|
entries.push({ type: "user", text: c });
|
|
1087
1156
|
} else if (Array.isArray(c)) {
|
|
1088
|
-
const text = c.find(
|
|
1157
|
+
const text = c.find(
|
|
1158
|
+
/** @param {{type: string, text?: string}} x */ (x) => x.type === "text",
|
|
1159
|
+
)?.text;
|
|
1089
1160
|
if (text && text.length > 10) {
|
|
1090
1161
|
entries.push({ type: "user", text });
|
|
1091
1162
|
}
|
|
@@ -1093,7 +1164,10 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1093
1164
|
} else if (entry.type === "assistant") {
|
|
1094
1165
|
/** @type {{type: string, text?: string}[]} */
|
|
1095
1166
|
const parts = entry.message?.content || [];
|
|
1096
|
-
const text = parts
|
|
1167
|
+
const text = parts
|
|
1168
|
+
.filter((p) => p.type === "text")
|
|
1169
|
+
.map((p) => p.text || "")
|
|
1170
|
+
.join("\n");
|
|
1097
1171
|
// Only include assistant responses with meaningful text
|
|
1098
1172
|
if (text && text.length > 20) {
|
|
1099
1173
|
entries.push({ type: "assistant", text });
|
|
@@ -1105,7 +1179,7 @@ function getParentSessionContext(maxEntries = 20) {
|
|
|
1105
1179
|
}
|
|
1106
1180
|
|
|
1107
1181
|
// Format recent conversation
|
|
1108
|
-
const formatted = entries.slice(-maxEntries).map(e => {
|
|
1182
|
+
const formatted = entries.slice(-maxEntries).map((e) => {
|
|
1109
1183
|
const preview = e.text.slice(0, 500).replace(/\n/g, " ");
|
|
1110
1184
|
return `**${e.type === "user" ? "User" : "Assistant"}**: ${preview}`;
|
|
1111
1185
|
});
|
|
@@ -1148,10 +1222,16 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1148
1222
|
|
|
1149
1223
|
// Parse all entries
|
|
1150
1224
|
/** @type {any[]} */
|
|
1151
|
-
const entries = lines
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1225
|
+
const entries = lines
|
|
1226
|
+
.map((line, idx) => {
|
|
1227
|
+
try {
|
|
1228
|
+
return { idx, ...JSON.parse(line) };
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
debugError("extractFileEditContext:parse", err);
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
})
|
|
1234
|
+
.filter(Boolean);
|
|
1155
1235
|
|
|
1156
1236
|
// Find Write/Edit tool calls for this file (scan backwards, want most recent)
|
|
1157
1237
|
/** @type {any} */
|
|
@@ -1164,9 +1244,10 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1164
1244
|
|
|
1165
1245
|
/** @type {any[]} */
|
|
1166
1246
|
const msgContent = entry.message?.content || [];
|
|
1167
|
-
const toolCalls = msgContent.filter(
|
|
1168
|
-
(
|
|
1169
|
-
|
|
1247
|
+
const toolCalls = msgContent.filter(
|
|
1248
|
+
(/** @type {any} */ c) =>
|
|
1249
|
+
(c.type === "tool_use" || c.type === "tool_call") &&
|
|
1250
|
+
(c.name === "Write" || c.name === "Edit"),
|
|
1170
1251
|
);
|
|
1171
1252
|
|
|
1172
1253
|
for (const tc of toolCalls) {
|
|
@@ -1217,8 +1298,9 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1217
1298
|
|
|
1218
1299
|
/** @type {any[]} */
|
|
1219
1300
|
const msgContent = entry.message?.content || [];
|
|
1220
|
-
const readCalls = msgContent.filter(
|
|
1221
|
-
(
|
|
1301
|
+
const readCalls = msgContent.filter(
|
|
1302
|
+
(/** @type {any} */ c) =>
|
|
1303
|
+
(c.type === "tool_use" || c.type === "tool_call") && c.name === "Read",
|
|
1222
1304
|
);
|
|
1223
1305
|
|
|
1224
1306
|
for (const rc of readCalls) {
|
|
@@ -1233,9 +1315,10 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1233
1315
|
if (entry.type !== "assistant") continue;
|
|
1234
1316
|
/** @type {any[]} */
|
|
1235
1317
|
const msgContent = entry.message?.content || [];
|
|
1236
|
-
const edits = msgContent.filter(
|
|
1237
|
-
(
|
|
1238
|
-
|
|
1318
|
+
const edits = msgContent.filter(
|
|
1319
|
+
(/** @type {any} */ c) =>
|
|
1320
|
+
(c.type === "tool_use" || c.type === "tool_call") &&
|
|
1321
|
+
(c.name === "Write" || c.name === "Edit"),
|
|
1239
1322
|
);
|
|
1240
1323
|
for (const e of edits) {
|
|
1241
1324
|
const input = e.input || e.arguments || {};
|
|
@@ -1250,11 +1333,11 @@ function extractFileEditContext(logPath, filePath) {
|
|
|
1250
1333
|
toolCall: {
|
|
1251
1334
|
name: editEntry.toolCall.name,
|
|
1252
1335
|
input: editEntry.toolCall.input || editEntry.toolCall.arguments,
|
|
1253
|
-
id: editEntry.toolCall.id
|
|
1336
|
+
id: editEntry.toolCall.id,
|
|
1254
1337
|
},
|
|
1255
1338
|
subsequentErrors,
|
|
1256
1339
|
readsBefore: [...new Set(readsBefore)].slice(0, 10),
|
|
1257
|
-
editSequence
|
|
1340
|
+
editSequence,
|
|
1258
1341
|
};
|
|
1259
1342
|
}
|
|
1260
1343
|
|
|
@@ -1348,10 +1431,11 @@ function watchForChanges(patterns, callback) {
|
|
|
1348
1431
|
}
|
|
1349
1432
|
}
|
|
1350
1433
|
|
|
1351
|
-
return () => {
|
|
1434
|
+
return () => {
|
|
1435
|
+
for (const w of watchers) w.close();
|
|
1436
|
+
};
|
|
1352
1437
|
}
|
|
1353
1438
|
|
|
1354
|
-
|
|
1355
1439
|
// =============================================================================
|
|
1356
1440
|
// State
|
|
1357
1441
|
// =============================================================================
|
|
@@ -1429,7 +1513,7 @@ function detectState(screen, config) {
|
|
|
1429
1513
|
// Check if any line has the prompt followed by pasted content indicator
|
|
1430
1514
|
const linesArray = lastLines.split("\n");
|
|
1431
1515
|
const promptWithPaste = linesArray.some(
|
|
1432
|
-
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text")
|
|
1516
|
+
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text"),
|
|
1433
1517
|
);
|
|
1434
1518
|
if (!promptWithPaste) {
|
|
1435
1519
|
return State.READY;
|
|
@@ -1640,7 +1724,12 @@ class Agent {
|
|
|
1640
1724
|
if (/^(run|execute|create|delete|modify|write)/i.test(line)) return line;
|
|
1641
1725
|
}
|
|
1642
1726
|
|
|
1643
|
-
return
|
|
1727
|
+
return (
|
|
1728
|
+
lines
|
|
1729
|
+
.filter((l) => l && !l.match(/^[╭╮╰╯│─]+$/))
|
|
1730
|
+
.slice(0, 2)
|
|
1731
|
+
.join(" | ") || "action"
|
|
1732
|
+
);
|
|
1644
1733
|
}
|
|
1645
1734
|
|
|
1646
1735
|
/**
|
|
@@ -1716,7 +1805,9 @@ class Agent {
|
|
|
1716
1805
|
|
|
1717
1806
|
// Fallback: extract after last prompt
|
|
1718
1807
|
if (filtered.length === 0) {
|
|
1719
|
-
const lastPromptIdx = lines.findLastIndex((/** @type {string} */ l) =>
|
|
1808
|
+
const lastPromptIdx = lines.findLastIndex((/** @type {string} */ l) =>
|
|
1809
|
+
l.startsWith(this.promptSymbol),
|
|
1810
|
+
);
|
|
1720
1811
|
if (lastPromptIdx >= 0 && lastPromptIdx < lines.length - 1) {
|
|
1721
1812
|
const afterPrompt = lines
|
|
1722
1813
|
.slice(lastPromptIdx + 1)
|
|
@@ -1730,14 +1821,14 @@ class Agent {
|
|
|
1730
1821
|
// This handles the case where Claude finished and shows a new empty prompt
|
|
1731
1822
|
if (lastPromptIdx >= 0) {
|
|
1732
1823
|
const lastPromptLine = lines[lastPromptIdx];
|
|
1733
|
-
const isEmptyPrompt =
|
|
1734
|
-
|
|
1824
|
+
const isEmptyPrompt =
|
|
1825
|
+
lastPromptLine.trim() === this.promptSymbol || lastPromptLine.match(/^❯\s*$/);
|
|
1735
1826
|
if (isEmptyPrompt) {
|
|
1736
1827
|
// Find the previous prompt (user's input) and extract content between
|
|
1737
1828
|
// Note: [Pasted text is Claude's truncated output indicator, NOT a prompt
|
|
1738
|
-
const prevPromptIdx = lines
|
|
1739
|
-
(
|
|
1740
|
-
|
|
1829
|
+
const prevPromptIdx = lines
|
|
1830
|
+
.slice(0, lastPromptIdx)
|
|
1831
|
+
.findLastIndex((/** @type {string} */ l) => l.startsWith(this.promptSymbol));
|
|
1741
1832
|
if (prevPromptIdx >= 0) {
|
|
1742
1833
|
const betweenPrompts = lines
|
|
1743
1834
|
.slice(prevPromptIdx + 1, lastPromptIdx)
|
|
@@ -1758,23 +1849,25 @@ class Agent {
|
|
|
1758
1849
|
* @returns {string}
|
|
1759
1850
|
*/
|
|
1760
1851
|
cleanResponse(response) {
|
|
1761
|
-
return
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1852
|
+
return (
|
|
1853
|
+
response
|
|
1854
|
+
// Remove tool call lines (Search, Read, Grep, etc.)
|
|
1855
|
+
.replace(/^[⏺•]\s*(Search|Read|Grep|Glob|Write|Edit|Bash)\([^)]*\).*$/gm, "")
|
|
1856
|
+
// Remove tool result lines
|
|
1857
|
+
.replace(/^⎿\s+.*$/gm, "")
|
|
1858
|
+
// Remove "Sautéed for Xs" timing lines
|
|
1859
|
+
.replace(/^✻\s+Sautéed for.*$/gm, "")
|
|
1860
|
+
// Remove expand hints
|
|
1861
|
+
.replace(/\(ctrl\+o to expand\)/g, "")
|
|
1862
|
+
// Clean up multiple blank lines
|
|
1863
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1864
|
+
// Original cleanup
|
|
1865
|
+
.replace(/^[•⏺-]\s*/, "")
|
|
1866
|
+
.replace(/^\*\*(.+)\*\*/, "$1")
|
|
1867
|
+
.replace(/\n /g, "\n")
|
|
1868
|
+
.replace(/─+\s*$/, "")
|
|
1869
|
+
.trim()
|
|
1870
|
+
);
|
|
1778
1871
|
}
|
|
1779
1872
|
|
|
1780
1873
|
/**
|
|
@@ -1857,9 +1950,18 @@ const ClaudeAgent = new Agent({
|
|
|
1857
1950
|
],
|
|
1858
1951
|
updatePromptPatterns: null,
|
|
1859
1952
|
responseMarkers: ["⏺", "•", "- ", "**"],
|
|
1860
|
-
chromePatterns: [
|
|
1953
|
+
chromePatterns: [
|
|
1954
|
+
"↵ send",
|
|
1955
|
+
"Esc to cancel",
|
|
1956
|
+
"shortcuts",
|
|
1957
|
+
"for more options",
|
|
1958
|
+
"docs.anthropic.com",
|
|
1959
|
+
"⏵⏵",
|
|
1960
|
+
"bypass permissions",
|
|
1961
|
+
"shift+Tab to cycle",
|
|
1962
|
+
],
|
|
1861
1963
|
reviewOptions: null,
|
|
1862
|
-
safeAllowedTools: "Bash(git:*) Read Glob Grep",
|
|
1964
|
+
safeAllowedTools: "Bash(git:*) Read Glob Grep", // Default: auto-approve read-only tools
|
|
1863
1965
|
envVar: "AX_SESSION",
|
|
1864
1966
|
approveKey: "1",
|
|
1865
1967
|
rejectKey: "Escape",
|
|
@@ -1883,7 +1985,11 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1883
1985
|
const initialState = agent.getState(initialScreen);
|
|
1884
1986
|
|
|
1885
1987
|
// Already in terminal state
|
|
1886
|
-
if (
|
|
1988
|
+
if (
|
|
1989
|
+
initialState === State.RATE_LIMITED ||
|
|
1990
|
+
initialState === State.CONFIRMING ||
|
|
1991
|
+
initialState === State.READY
|
|
1992
|
+
) {
|
|
1887
1993
|
return { state: initialState, screen: initialScreen };
|
|
1888
1994
|
}
|
|
1889
1995
|
|
|
@@ -2045,7 +2151,7 @@ function cmdAgents() {
|
|
|
2045
2151
|
const screen = tmuxCapture(session);
|
|
2046
2152
|
const state = agent.getState(screen);
|
|
2047
2153
|
const logPath = agent.findLogPath(session);
|
|
2048
|
-
const type = parsed.
|
|
2154
|
+
const type = parsed.archangelName ? "archangel" : "-";
|
|
2049
2155
|
|
|
2050
2156
|
return {
|
|
2051
2157
|
session,
|
|
@@ -2063,97 +2169,98 @@ function cmdAgents() {
|
|
|
2063
2169
|
const maxType = Math.max(4, ...agents.map((a) => a.type.length));
|
|
2064
2170
|
|
|
2065
2171
|
console.log(
|
|
2066
|
-
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TYPE".padEnd(maxType)} LOG
|
|
2172
|
+
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TYPE".padEnd(maxType)} LOG`,
|
|
2067
2173
|
);
|
|
2068
2174
|
for (const a of agents) {
|
|
2069
2175
|
console.log(
|
|
2070
|
-
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.type.padEnd(maxType)} ${a.log}
|
|
2176
|
+
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.type.padEnd(maxType)} ${a.log}`,
|
|
2071
2177
|
);
|
|
2072
2178
|
}
|
|
2073
2179
|
}
|
|
2074
2180
|
|
|
2075
2181
|
// =============================================================================
|
|
2076
|
-
// Command:
|
|
2182
|
+
// Command: summon/recall
|
|
2077
2183
|
// =============================================================================
|
|
2078
2184
|
|
|
2079
2185
|
/**
|
|
2080
2186
|
* @param {string} pattern
|
|
2081
2187
|
* @returns {string | undefined}
|
|
2082
2188
|
*/
|
|
2083
|
-
function
|
|
2189
|
+
function findArchangelSession(pattern) {
|
|
2084
2190
|
const sessions = tmuxListSessions();
|
|
2085
2191
|
return sessions.find((s) => s.startsWith(pattern));
|
|
2086
2192
|
}
|
|
2087
2193
|
|
|
2088
2194
|
/**
|
|
2089
|
-
* @param {
|
|
2195
|
+
* @param {ArchangelConfig} config
|
|
2090
2196
|
* @returns {string}
|
|
2091
2197
|
*/
|
|
2092
|
-
function
|
|
2093
|
-
return `${config.tool}-
|
|
2198
|
+
function generateArchangelSessionName(config) {
|
|
2199
|
+
return `${config.tool}-archangel-${config.name}-${randomUUID()}`;
|
|
2094
2200
|
}
|
|
2095
2201
|
|
|
2096
2202
|
/**
|
|
2097
|
-
* @param {
|
|
2203
|
+
* @param {ArchangelConfig} config
|
|
2098
2204
|
* @param {ParentSession | null} [parentSession]
|
|
2099
2205
|
*/
|
|
2100
|
-
function
|
|
2101
|
-
// Build environment with parent session info if available
|
|
2206
|
+
function startArchangel(config, parentSession = null) {
|
|
2102
2207
|
/** @type {NodeJS.ProcessEnv} */
|
|
2103
2208
|
const env = { ...process.env };
|
|
2104
2209
|
if (parentSession?.uuid) {
|
|
2105
|
-
// Session name may be null for non-tmux sessions, but uuid is required
|
|
2106
2210
|
if (parentSession.session) {
|
|
2107
|
-
env[
|
|
2211
|
+
env[AX_ARCHANGEL_PARENT_SESSION_ENV] = parentSession.session;
|
|
2108
2212
|
}
|
|
2109
|
-
env[
|
|
2213
|
+
env[AX_ARCHANGEL_PARENT_UUID_ENV] = parentSession.uuid;
|
|
2110
2214
|
}
|
|
2111
2215
|
|
|
2112
|
-
|
|
2113
|
-
const child = spawn("node", [process.argv[1], "daemon", config.name], {
|
|
2216
|
+
const child = spawn("node", [process.argv[1], "archangel", config.name], {
|
|
2114
2217
|
detached: true,
|
|
2115
2218
|
stdio: "ignore",
|
|
2116
2219
|
cwd: process.cwd(),
|
|
2117
2220
|
env,
|
|
2118
2221
|
});
|
|
2119
2222
|
child.unref();
|
|
2120
|
-
console.log(
|
|
2223
|
+
console.log(
|
|
2224
|
+
`Summoning: ${config.name} (pid ${child.pid})${parentSession ? ` [parent: ${parentSession.session}]` : ""}`,
|
|
2225
|
+
);
|
|
2121
2226
|
}
|
|
2122
2227
|
|
|
2123
2228
|
// =============================================================================
|
|
2124
|
-
// Command:
|
|
2229
|
+
// Command: archangel (runs as the archangel process itself)
|
|
2125
2230
|
// =============================================================================
|
|
2126
2231
|
|
|
2127
2232
|
/**
|
|
2128
2233
|
* @param {string | undefined} agentName
|
|
2129
2234
|
*/
|
|
2130
|
-
async function
|
|
2235
|
+
async function cmdArchangel(agentName) {
|
|
2131
2236
|
if (!agentName) {
|
|
2132
|
-
console.error("Usage: ./ax.js
|
|
2237
|
+
console.error("Usage: ./ax.js archangel <name>");
|
|
2133
2238
|
process.exit(1);
|
|
2134
2239
|
}
|
|
2135
2240
|
// Load agent config
|
|
2136
2241
|
const configPath = path.join(AGENTS_DIR, `${agentName}.md`);
|
|
2137
2242
|
if (!existsSync(configPath)) {
|
|
2138
|
-
console.error(`[
|
|
2243
|
+
console.error(`[archangel:${agentName}] Config not found: ${configPath}`);
|
|
2139
2244
|
process.exit(1);
|
|
2140
2245
|
}
|
|
2141
2246
|
|
|
2142
2247
|
const content = readFileSync(configPath, "utf-8");
|
|
2143
2248
|
const configResult = parseAgentConfig(`${agentName}.md`, content);
|
|
2144
2249
|
if (!configResult || "error" in configResult) {
|
|
2145
|
-
console.error(`[
|
|
2250
|
+
console.error(`[archangel:${agentName}] Invalid config`);
|
|
2146
2251
|
process.exit(1);
|
|
2147
2252
|
}
|
|
2148
2253
|
const config = configResult;
|
|
2149
2254
|
|
|
2150
2255
|
const agent = config.tool === "claude" ? ClaudeAgent : CodexAgent;
|
|
2151
|
-
const sessionName =
|
|
2256
|
+
const sessionName = generateArchangelSessionName(config);
|
|
2152
2257
|
|
|
2153
2258
|
// Check agent CLI is installed before trying to start
|
|
2154
2259
|
const cliCheck = spawnSync("which", [agent.name], { encoding: "utf-8" });
|
|
2155
2260
|
if (cliCheck.status !== 0) {
|
|
2156
|
-
console.error(
|
|
2261
|
+
console.error(
|
|
2262
|
+
`[archangel:${agentName}] ERROR: ${agent.name} CLI is not installed or not in PATH`,
|
|
2263
|
+
);
|
|
2157
2264
|
process.exit(1);
|
|
2158
2265
|
}
|
|
2159
2266
|
|
|
@@ -2163,7 +2270,7 @@ async function cmdDaemon(agentName) {
|
|
|
2163
2270
|
|
|
2164
2271
|
// Wait for agent to be ready
|
|
2165
2272
|
const start = Date.now();
|
|
2166
|
-
while (Date.now() - start <
|
|
2273
|
+
while (Date.now() - start < ARCHANGEL_STARTUP_TIMEOUT_MS) {
|
|
2167
2274
|
const screen = tmuxCapture(sessionName);
|
|
2168
2275
|
const state = agent.getState(screen);
|
|
2169
2276
|
|
|
@@ -2174,7 +2281,7 @@ async function cmdDaemon(agentName) {
|
|
|
2174
2281
|
|
|
2175
2282
|
// Handle bypass permissions confirmation dialog (Claude Code shows this for --dangerously-skip-permissions)
|
|
2176
2283
|
if (screen.includes("Bypass Permissions mode") && screen.includes("Yes, I accept")) {
|
|
2177
|
-
console.log(`[
|
|
2284
|
+
console.log(`[archangel:${agentName}] Accepting bypass permissions dialog`);
|
|
2178
2285
|
tmuxSend(sessionName, "2"); // Select "Yes, I accept"
|
|
2179
2286
|
await sleep(300);
|
|
2180
2287
|
tmuxSend(sessionName, "Enter");
|
|
@@ -2183,7 +2290,7 @@ async function cmdDaemon(agentName) {
|
|
|
2183
2290
|
}
|
|
2184
2291
|
|
|
2185
2292
|
if (state === State.READY) {
|
|
2186
|
-
console.log(`[
|
|
2293
|
+
console.log(`[archangel:${agentName}] Started session: ${sessionName}`);
|
|
2187
2294
|
break;
|
|
2188
2295
|
}
|
|
2189
2296
|
|
|
@@ -2215,7 +2322,7 @@ async function cmdDaemon(agentName) {
|
|
|
2215
2322
|
isProcessing = true;
|
|
2216
2323
|
|
|
2217
2324
|
const files = [...changedFiles];
|
|
2218
|
-
changedFiles = new Set();
|
|
2325
|
+
changedFiles = new Set(); // atomic swap to avoid losing changes during processing
|
|
2219
2326
|
|
|
2220
2327
|
try {
|
|
2221
2328
|
// Get parent session log path for JSONL extraction
|
|
@@ -2224,7 +2331,8 @@ async function cmdDaemon(agentName) {
|
|
|
2224
2331
|
|
|
2225
2332
|
// Build file-specific context from JSONL
|
|
2226
2333
|
const fileContexts = [];
|
|
2227
|
-
for (const file of files.slice(0, 5)) {
|
|
2334
|
+
for (const file of files.slice(0, 5)) {
|
|
2335
|
+
// Limit to 5 files
|
|
2228
2336
|
const ctx = extractFileEditContext(logPath, file);
|
|
2229
2337
|
if (ctx) {
|
|
2230
2338
|
fileContexts.push({ file, ...ctx });
|
|
@@ -2251,26 +2359,34 @@ async function cmdDaemon(agentName) {
|
|
|
2251
2359
|
}
|
|
2252
2360
|
|
|
2253
2361
|
if (ctx.readsBefore.length > 0) {
|
|
2254
|
-
const reads = ctx.readsBefore.map(f => f.split("/").pop()).join(", ");
|
|
2362
|
+
const reads = ctx.readsBefore.map((f) => f.split("/").pop()).join(", ");
|
|
2255
2363
|
prompt += `**Files read before:** ${reads}\n`;
|
|
2256
2364
|
}
|
|
2257
2365
|
}
|
|
2258
2366
|
|
|
2259
2367
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
2260
2368
|
|
|
2261
|
-
const gitContext = buildGitContext(
|
|
2369
|
+
const gitContext = buildGitContext(
|
|
2370
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2371
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2372
|
+
);
|
|
2262
2373
|
if (gitContext) {
|
|
2263
2374
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2264
2375
|
}
|
|
2265
2376
|
|
|
2266
|
-
prompt +=
|
|
2377
|
+
prompt +=
|
|
2378
|
+
'\n\nReview these changes in the context of what the user is working on. Report any issues found. Keep your response concise.\nIf there are no significant issues, respond with just "No issues found."';
|
|
2267
2379
|
} else {
|
|
2268
2380
|
// Fallback: no JSONL context available, use conversation + git context
|
|
2269
|
-
const parentContext = getParentSessionContext(
|
|
2270
|
-
const gitContext = buildGitContext(
|
|
2381
|
+
const parentContext = getParentSessionContext(ARCHANGEL_PARENT_CONTEXT_ENTRIES);
|
|
2382
|
+
const gitContext = buildGitContext(
|
|
2383
|
+
ARCHANGEL_GIT_CONTEXT_HOURS,
|
|
2384
|
+
ARCHANGEL_GIT_CONTEXT_MAX_LINES,
|
|
2385
|
+
);
|
|
2271
2386
|
|
|
2272
2387
|
if (parentContext) {
|
|
2273
|
-
prompt +=
|
|
2388
|
+
prompt +=
|
|
2389
|
+
"\n\n## Main Session Context\n\nThe user is currently working on:\n\n" + parentContext;
|
|
2274
2390
|
}
|
|
2275
2391
|
|
|
2276
2392
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
@@ -2279,13 +2395,13 @@ async function cmdDaemon(agentName) {
|
|
|
2279
2395
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2280
2396
|
}
|
|
2281
2397
|
|
|
2282
|
-
prompt +=
|
|
2398
|
+
prompt +=
|
|
2399
|
+
'\n\nReview these changes in the context of what the user is working on. Report any issues found. Keep your response concise.\nIf there are no significant issues, respond with just "No issues found."';
|
|
2283
2400
|
}
|
|
2284
2401
|
|
|
2285
|
-
|
|
2286
2402
|
// Check session still exists
|
|
2287
2403
|
if (!tmuxHasSession(sessionName)) {
|
|
2288
|
-
console.log(`[
|
|
2404
|
+
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
2289
2405
|
process.exit(0);
|
|
2290
2406
|
}
|
|
2291
2407
|
|
|
@@ -2294,12 +2410,12 @@ async function cmdDaemon(agentName) {
|
|
|
2294
2410
|
const state = agent.getState(screen);
|
|
2295
2411
|
|
|
2296
2412
|
if (state === State.RATE_LIMITED) {
|
|
2297
|
-
console.error(`[
|
|
2413
|
+
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2298
2414
|
process.exit(2);
|
|
2299
2415
|
}
|
|
2300
2416
|
|
|
2301
2417
|
if (state !== State.READY) {
|
|
2302
|
-
console.log(`[
|
|
2418
|
+
console.log(`[archangel:${agentName}] Agent not ready (${state}), skipping`);
|
|
2303
2419
|
isProcessing = false;
|
|
2304
2420
|
return;
|
|
2305
2421
|
}
|
|
@@ -2311,22 +2427,30 @@ async function cmdDaemon(agentName) {
|
|
|
2311
2427
|
await sleep(100); // Ensure Enter is processed
|
|
2312
2428
|
|
|
2313
2429
|
// Wait for response
|
|
2314
|
-
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2430
|
+
const { state: endState, screen: afterScreen } = await waitForResponse(
|
|
2431
|
+
agent,
|
|
2432
|
+
sessionName,
|
|
2433
|
+
ARCHANGEL_RESPONSE_TIMEOUT_MS,
|
|
2434
|
+
);
|
|
2315
2435
|
|
|
2316
2436
|
if (endState === State.RATE_LIMITED) {
|
|
2317
|
-
console.error(`[
|
|
2437
|
+
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2318
2438
|
process.exit(2);
|
|
2319
2439
|
}
|
|
2320
2440
|
|
|
2321
|
-
|
|
2322
2441
|
const cleanedResponse = agent.getResponse(sessionName, afterScreen) || "";
|
|
2323
2442
|
|
|
2324
2443
|
// Sanity check: skip garbage responses (screen scraping artifacts)
|
|
2325
|
-
const isGarbage =
|
|
2444
|
+
const isGarbage =
|
|
2445
|
+
cleanedResponse.includes("[Pasted text") ||
|
|
2326
2446
|
cleanedResponse.match(/^\+\d+ lines\]/) ||
|
|
2327
2447
|
cleanedResponse.length < 20;
|
|
2328
2448
|
|
|
2329
|
-
if (
|
|
2449
|
+
if (
|
|
2450
|
+
cleanedResponse &&
|
|
2451
|
+
!isGarbage &&
|
|
2452
|
+
!cleanedResponse.toLowerCase().includes("no issues found")
|
|
2453
|
+
) {
|
|
2330
2454
|
writeToMailbox({
|
|
2331
2455
|
agent: /** @type {string} */ (agentName),
|
|
2332
2456
|
session: sessionName,
|
|
@@ -2335,12 +2459,12 @@ async function cmdDaemon(agentName) {
|
|
|
2335
2459
|
files,
|
|
2336
2460
|
message: cleanedResponse.slice(0, 1000),
|
|
2337
2461
|
});
|
|
2338
|
-
console.log(`[
|
|
2462
|
+
console.log(`[archangel:${agentName}] Wrote observation for ${files.length} file(s)`);
|
|
2339
2463
|
} else if (isGarbage) {
|
|
2340
|
-
console.log(`[
|
|
2464
|
+
console.log(`[archangel:${agentName}] Skipped garbage response`);
|
|
2341
2465
|
}
|
|
2342
2466
|
} catch (err) {
|
|
2343
|
-
console.error(`[
|
|
2467
|
+
console.error(`[archangel:${agentName}] Error:`, err instanceof Error ? err.message : err);
|
|
2344
2468
|
}
|
|
2345
2469
|
|
|
2346
2470
|
isProcessing = false;
|
|
@@ -2348,7 +2472,10 @@ async function cmdDaemon(agentName) {
|
|
|
2348
2472
|
|
|
2349
2473
|
function scheduleProcessChanges() {
|
|
2350
2474
|
processChanges().catch((err) => {
|
|
2351
|
-
console.error(
|
|
2475
|
+
console.error(
|
|
2476
|
+
`[archangel:${agentName}] Unhandled error:`,
|
|
2477
|
+
err instanceof Error ? err.message : err,
|
|
2478
|
+
);
|
|
2352
2479
|
});
|
|
2353
2480
|
}
|
|
2354
2481
|
|
|
@@ -2369,16 +2496,16 @@ async function cmdDaemon(agentName) {
|
|
|
2369
2496
|
// Check if session still exists periodically
|
|
2370
2497
|
const sessionCheck = setInterval(() => {
|
|
2371
2498
|
if (!tmuxHasSession(sessionName)) {
|
|
2372
|
-
console.log(`[
|
|
2499
|
+
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
2373
2500
|
stopWatching();
|
|
2374
2501
|
clearInterval(sessionCheck);
|
|
2375
2502
|
process.exit(0);
|
|
2376
2503
|
}
|
|
2377
|
-
},
|
|
2504
|
+
}, ARCHANGEL_HEALTH_CHECK_MS);
|
|
2378
2505
|
|
|
2379
2506
|
// Handle graceful shutdown
|
|
2380
2507
|
process.on("SIGTERM", () => {
|
|
2381
|
-
console.log(`[
|
|
2508
|
+
console.log(`[archangel:${agentName}] Received SIGTERM, shutting down`);
|
|
2382
2509
|
stopWatching();
|
|
2383
2510
|
clearInterval(sessionCheck);
|
|
2384
2511
|
tmuxSend(sessionName, "C-c");
|
|
@@ -2389,7 +2516,7 @@ async function cmdDaemon(agentName) {
|
|
|
2389
2516
|
});
|
|
2390
2517
|
|
|
2391
2518
|
process.on("SIGINT", () => {
|
|
2392
|
-
console.log(`[
|
|
2519
|
+
console.log(`[archangel:${agentName}] Received SIGINT, shutting down`);
|
|
2393
2520
|
stopWatching();
|
|
2394
2521
|
clearInterval(sessionCheck);
|
|
2395
2522
|
tmuxSend(sessionName, "C-c");
|
|
@@ -2399,48 +2526,33 @@ async function cmdDaemon(agentName) {
|
|
|
2399
2526
|
}, 500);
|
|
2400
2527
|
});
|
|
2401
2528
|
|
|
2402
|
-
console.log(`[
|
|
2529
|
+
console.log(`[archangel:${agentName}] Watching: ${config.watch.join(", ")}`);
|
|
2403
2530
|
|
|
2404
2531
|
// Keep the process alive
|
|
2405
2532
|
await new Promise(() => {});
|
|
2406
2533
|
}
|
|
2407
2534
|
|
|
2408
2535
|
/**
|
|
2409
|
-
* @param {string}
|
|
2410
|
-
* @param {string | null} [daemonName]
|
|
2536
|
+
* @param {string | null} [name]
|
|
2411
2537
|
*/
|
|
2412
|
-
async function
|
|
2413
|
-
|
|
2414
|
-
console.log("Usage: ./ax.js daemons <start|stop|init> [name]");
|
|
2415
|
-
process.exit(1);
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
// Handle init action separately
|
|
2419
|
-
if (action === "init") {
|
|
2420
|
-
if (!daemonName) {
|
|
2421
|
-
console.log("Usage: ./ax.js daemons init <name>");
|
|
2422
|
-
console.log("Example: ./ax.js daemons init reviewer");
|
|
2423
|
-
process.exit(1);
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
// Validate name (alphanumeric, dashes, underscores only)
|
|
2427
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(daemonName)) {
|
|
2428
|
-
console.log("ERROR: Daemon name must contain only letters, numbers, dashes, and underscores");
|
|
2429
|
-
process.exit(1);
|
|
2430
|
-
}
|
|
2538
|
+
async function cmdSummon(name = null) {
|
|
2539
|
+
const configs = loadAgentConfigs();
|
|
2431
2540
|
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2541
|
+
// If name provided but doesn't exist, create it
|
|
2542
|
+
if (name) {
|
|
2543
|
+
const exists = configs.some((c) => c.name === name);
|
|
2544
|
+
if (!exists) {
|
|
2545
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
2546
|
+
console.log("ERROR: Name must contain only letters, numbers, dashes, and underscores");
|
|
2547
|
+
process.exit(1);
|
|
2548
|
+
}
|
|
2437
2549
|
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
}
|
|
2550
|
+
if (!existsSync(AGENTS_DIR)) {
|
|
2551
|
+
mkdirSync(AGENTS_DIR, { recursive: true });
|
|
2552
|
+
}
|
|
2442
2553
|
|
|
2443
|
-
|
|
2554
|
+
const agentPath = path.join(AGENTS_DIR, `${name}.md`);
|
|
2555
|
+
const template = `---
|
|
2444
2556
|
tool: claude
|
|
2445
2557
|
watch: ["**/*.{ts,tsx,js,jsx,mjs,mts}"]
|
|
2446
2558
|
interval: 30
|
|
@@ -2448,75 +2560,76 @@ interval: 30
|
|
|
2448
2560
|
|
|
2449
2561
|
Review changed files for bugs, type errors, and edge cases.
|
|
2450
2562
|
`;
|
|
2563
|
+
writeFileSync(agentPath, template);
|
|
2564
|
+
console.log(`Created: ${agentPath}`);
|
|
2565
|
+
console.log(`Edit the file to customize, then run: ax summon ${name}`);
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2451
2569
|
|
|
2452
|
-
|
|
2453
|
-
console.log(`
|
|
2454
|
-
console.log(`Edit the file to customize the daemon, then run: ./ax.js daemons start ${daemonName}`);
|
|
2570
|
+
if (configs.length === 0) {
|
|
2571
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
2455
2572
|
return;
|
|
2456
2573
|
}
|
|
2457
2574
|
|
|
2575
|
+
const targetConfigs = name ? configs.filter((c) => c.name === name) : configs;
|
|
2576
|
+
|
|
2577
|
+
ensureMailboxHookScript();
|
|
2578
|
+
|
|
2579
|
+
const parentSession = findCurrentClaudeSession();
|
|
2580
|
+
if (parentSession) {
|
|
2581
|
+
console.log(`Parent session: ${parentSession.session || "(non-tmux)"} [${parentSession.uuid}]`);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
for (const config of targetConfigs) {
|
|
2585
|
+
const sessionPattern = getArchangelSessionPattern(config);
|
|
2586
|
+
const existing = findArchangelSession(sessionPattern);
|
|
2587
|
+
|
|
2588
|
+
if (!existing) {
|
|
2589
|
+
startArchangel(config, parentSession);
|
|
2590
|
+
} else {
|
|
2591
|
+
console.log(`Already running: ${config.name} (${existing})`);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
gcMailbox(24);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
/**
|
|
2599
|
+
* @param {string | null} [name]
|
|
2600
|
+
*/
|
|
2601
|
+
async function cmdRecall(name = null) {
|
|
2458
2602
|
const configs = loadAgentConfigs();
|
|
2459
2603
|
|
|
2460
2604
|
if (configs.length === 0) {
|
|
2461
|
-
console.log(`No
|
|
2605
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
2462
2606
|
return;
|
|
2463
2607
|
}
|
|
2464
2608
|
|
|
2465
|
-
|
|
2466
|
-
const targetConfigs = daemonName
|
|
2467
|
-
? configs.filter((c) => c.name === daemonName)
|
|
2468
|
-
: configs;
|
|
2609
|
+
const targetConfigs = name ? configs.filter((c) => c.name === name) : configs;
|
|
2469
2610
|
|
|
2470
|
-
if (
|
|
2471
|
-
console.log(`ERROR:
|
|
2611
|
+
if (name && targetConfigs.length === 0) {
|
|
2612
|
+
console.log(`ERROR: archangel '${name}' not found in ${AGENTS_DIR}/`);
|
|
2472
2613
|
process.exit(1);
|
|
2473
2614
|
}
|
|
2474
2615
|
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
}
|
|
2616
|
+
for (const config of targetConfigs) {
|
|
2617
|
+
const sessionPattern = getArchangelSessionPattern(config);
|
|
2618
|
+
const existing = findArchangelSession(sessionPattern);
|
|
2479
2619
|
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
console.log(`
|
|
2620
|
+
if (existing) {
|
|
2621
|
+
tmuxSend(existing, "C-c");
|
|
2622
|
+
await sleep(300);
|
|
2623
|
+
tmuxKill(existing);
|
|
2624
|
+
console.log(`Recalled: ${config.name} (${existing})`);
|
|
2485
2625
|
} else {
|
|
2486
|
-
console.log(
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
for (const config of targetConfigs) {
|
|
2491
|
-
const sessionPattern = getDaemonSessionPattern(config);
|
|
2492
|
-
const existing = findDaemonSession(sessionPattern);
|
|
2493
|
-
|
|
2494
|
-
if (action === "stop") {
|
|
2495
|
-
if (existing) {
|
|
2496
|
-
tmuxSend(existing, "C-c");
|
|
2497
|
-
await sleep(300);
|
|
2498
|
-
tmuxKill(existing);
|
|
2499
|
-
console.log(`Stopped daemon: ${config.name} (${existing})`);
|
|
2500
|
-
} else {
|
|
2501
|
-
console.log(`Daemon not running: ${config.name}`);
|
|
2502
|
-
}
|
|
2503
|
-
} else if (action === "start") {
|
|
2504
|
-
if (!existing) {
|
|
2505
|
-
startDaemonAgent(config, parentSession);
|
|
2506
|
-
} else {
|
|
2507
|
-
console.log(`Daemon already running: ${config.name} (${existing})`);
|
|
2508
|
-
}
|
|
2626
|
+
console.log(`Not running: ${config.name}`);
|
|
2509
2627
|
}
|
|
2510
2628
|
}
|
|
2511
|
-
|
|
2512
|
-
// GC mailbox on start
|
|
2513
|
-
if (action === "start") {
|
|
2514
|
-
gcMailbox(24);
|
|
2515
|
-
}
|
|
2516
2629
|
}
|
|
2517
2630
|
|
|
2518
2631
|
// Version of the hook script template - bump when making changes
|
|
2519
|
-
const HOOK_SCRIPT_VERSION = "
|
|
2632
|
+
const HOOK_SCRIPT_VERSION = "4";
|
|
2520
2633
|
|
|
2521
2634
|
function ensureMailboxHookScript() {
|
|
2522
2635
|
const hooksDir = HOOKS_DIR;
|
|
@@ -2539,24 +2652,49 @@ ${versionMarker}
|
|
|
2539
2652
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2540
2653
|
import { dirname, join } from "node:path";
|
|
2541
2654
|
import { fileURLToPath } from "node:url";
|
|
2655
|
+
import { createHash } from "node:crypto";
|
|
2542
2656
|
|
|
2543
2657
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2544
2658
|
const AI_DIR = join(__dirname, "..");
|
|
2545
2659
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
2546
2660
|
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
2547
|
-
const LAST_SEEN = join(AI_DIR, "mailbox-last-seen");
|
|
2548
2661
|
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
2549
2662
|
|
|
2663
|
+
// Read hook input from stdin
|
|
2664
|
+
let hookInput = {};
|
|
2665
|
+
try {
|
|
2666
|
+
const stdinData = readFileSync(0, "utf-8").trim();
|
|
2667
|
+
if (stdinData) hookInput = JSON.parse(stdinData);
|
|
2668
|
+
} catch (err) {
|
|
2669
|
+
if (DEBUG) console.error("[hook] stdin parse:", err.message);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
const sessionId = hookInput.session_id || "";
|
|
2673
|
+
const hookEvent = hookInput.hook_event_name || "";
|
|
2674
|
+
|
|
2675
|
+
if (DEBUG) console.error("[hook] session:", sessionId, "event:", hookEvent);
|
|
2676
|
+
|
|
2677
|
+
// NO-OP for archangel or partner sessions
|
|
2678
|
+
if (sessionId.includes("-archangel-") || sessionId.includes("-partner-")) {
|
|
2679
|
+
if (DEBUG) console.error("[hook] skipping non-parent session");
|
|
2680
|
+
process.exit(0);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// Per-session last-seen tracking (single JSON file, self-cleaning)
|
|
2684
|
+
const sessionHash = sessionId ? createHash("md5").update(sessionId).digest("hex").slice(0, 8) : "default";
|
|
2685
|
+
const LAST_SEEN_FILE = join(AI_DIR, "mailbox-last-seen.json");
|
|
2686
|
+
|
|
2550
2687
|
if (!existsSync(MAILBOX)) process.exit(0);
|
|
2551
2688
|
|
|
2552
|
-
let
|
|
2689
|
+
let lastSeenMap = {};
|
|
2553
2690
|
try {
|
|
2554
|
-
if (existsSync(
|
|
2555
|
-
|
|
2691
|
+
if (existsSync(LAST_SEEN_FILE)) {
|
|
2692
|
+
lastSeenMap = JSON.parse(readFileSync(LAST_SEEN_FILE, "utf-8"));
|
|
2556
2693
|
}
|
|
2557
2694
|
} catch (err) {
|
|
2558
2695
|
if (DEBUG) console.error("[hook] readLastSeen:", err.message);
|
|
2559
2696
|
}
|
|
2697
|
+
const lastSeen = lastSeenMap[sessionHash] || 0;
|
|
2560
2698
|
|
|
2561
2699
|
const now = Date.now();
|
|
2562
2700
|
const lines = readFileSync(MAILBOX, "utf-8").trim().split("\\n").filter(Boolean);
|
|
@@ -2578,21 +2716,39 @@ for (const line of lines) {
|
|
|
2578
2716
|
}
|
|
2579
2717
|
|
|
2580
2718
|
if (relevant.length > 0) {
|
|
2581
|
-
console.log("## Background Agents");
|
|
2582
|
-
console.log("");
|
|
2583
|
-
console.log("Background agents watching your files found:");
|
|
2584
|
-
console.log("");
|
|
2585
2719
|
const sessionPrefixes = new Set();
|
|
2720
|
+
let messageLines = [];
|
|
2721
|
+
messageLines.push("## Background Agents");
|
|
2722
|
+
messageLines.push("");
|
|
2723
|
+
messageLines.push("Background agents watching your files found:");
|
|
2724
|
+
messageLines.push("");
|
|
2586
2725
|
for (const { agent, sessionPrefix, message } of relevant) {
|
|
2587
2726
|
if (sessionPrefix) sessionPrefixes.add(sessionPrefix);
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2727
|
+
messageLines.push("**[" + agent + "]**");
|
|
2728
|
+
messageLines.push("");
|
|
2729
|
+
messageLines.push(message);
|
|
2730
|
+
messageLines.push("");
|
|
2592
2731
|
}
|
|
2593
2732
|
const sessionList = [...sessionPrefixes].map(s => "\\\`./ax.js log " + s + "\\\`").join(" or ");
|
|
2594
|
-
|
|
2595
|
-
|
|
2733
|
+
messageLines.push("> For more context: \\\`./ax.js mailbox\\\`" + (sessionList ? " or " + sessionList : ""));
|
|
2734
|
+
|
|
2735
|
+
const formattedMessage = messageLines.join("\\n");
|
|
2736
|
+
|
|
2737
|
+
// For Stop hook, return blocking JSON to force acknowledgment
|
|
2738
|
+
if (hookEvent === "Stop") {
|
|
2739
|
+
console.log(JSON.stringify({ decision: "block", reason: formattedMessage }));
|
|
2740
|
+
} else {
|
|
2741
|
+
// For other hooks, just output the context
|
|
2742
|
+
console.log(formattedMessage);
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// Update last-seen and prune entries older than 24 hours
|
|
2746
|
+
const PRUNE_AGE_MS = 24 * 60 * 60 * 1000;
|
|
2747
|
+
lastSeenMap[sessionHash] = now;
|
|
2748
|
+
for (const key of Object.keys(lastSeenMap)) {
|
|
2749
|
+
if (now - lastSeenMap[key] > PRUNE_AGE_MS) delete lastSeenMap[key];
|
|
2750
|
+
}
|
|
2751
|
+
writeFileSync(LAST_SEEN_FILE, JSON.stringify(lastSeenMap));
|
|
2596
2752
|
}
|
|
2597
2753
|
|
|
2598
2754
|
process.exit(0);
|
|
@@ -2607,18 +2763,9 @@ process.exit(0);
|
|
|
2607
2763
|
console.log(`\nTo enable manually, add to .claude/settings.json:\n`);
|
|
2608
2764
|
console.log(`{
|
|
2609
2765
|
"hooks": {
|
|
2610
|
-
"UserPromptSubmit": [
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
"hooks": [
|
|
2614
|
-
{
|
|
2615
|
-
"type": "command",
|
|
2616
|
-
"command": "node .ai/hooks/mailbox-inject.js",
|
|
2617
|
-
"timeout": 5
|
|
2618
|
-
}
|
|
2619
|
-
]
|
|
2620
|
-
}
|
|
2621
|
-
]
|
|
2766
|
+
"UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
2767
|
+
"PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }],
|
|
2768
|
+
"Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "node .ai/hooks/mailbox-inject.js", "timeout": 5 }] }]
|
|
2622
2769
|
}
|
|
2623
2770
|
}`);
|
|
2624
2771
|
}
|
|
@@ -2628,6 +2775,7 @@ function ensureClaudeHookConfig() {
|
|
|
2628
2775
|
const settingsDir = ".claude";
|
|
2629
2776
|
const settingsPath = path.join(settingsDir, "settings.json");
|
|
2630
2777
|
const hookCommand = "node .ai/hooks/mailbox-inject.js";
|
|
2778
|
+
const hookEvents = ["UserPromptSubmit", "PostToolUse", "Stop"];
|
|
2631
2779
|
|
|
2632
2780
|
try {
|
|
2633
2781
|
/** @type {ClaudeSettings} */
|
|
@@ -2646,33 +2794,41 @@ function ensureClaudeHookConfig() {
|
|
|
2646
2794
|
|
|
2647
2795
|
// Ensure hooks structure exists
|
|
2648
2796
|
if (!settings.hooks) settings.hooks = {};
|
|
2649
|
-
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
2650
|
-
|
|
2651
|
-
// Check if our hook is already configured
|
|
2652
|
-
const hookExists = settings.hooks.UserPromptSubmit.some(
|
|
2653
|
-
/** @param {{hooks?: Array<{command: string}>}} entry */
|
|
2654
|
-
(entry) => entry.hooks?.some(/** @param {{command: string}} h */ (h) => h.command === hookCommand)
|
|
2655
|
-
);
|
|
2656
2797
|
|
|
2657
|
-
|
|
2658
|
-
|
|
2798
|
+
let anyAdded = false;
|
|
2799
|
+
|
|
2800
|
+
for (const eventName of hookEvents) {
|
|
2801
|
+
if (!settings.hooks[eventName]) settings.hooks[eventName] = [];
|
|
2802
|
+
|
|
2803
|
+
// Check if our hook is already configured for this event
|
|
2804
|
+
const hookExists = settings.hooks[eventName].some(
|
|
2805
|
+
/** @param {{hooks?: Array<{command: string}>}} entry */
|
|
2806
|
+
(entry) =>
|
|
2807
|
+
entry.hooks?.some(/** @param {{command: string}} h */ (h) => h.command === hookCommand),
|
|
2808
|
+
);
|
|
2809
|
+
|
|
2810
|
+
if (!hookExists) {
|
|
2811
|
+
// Add the hook for this event
|
|
2812
|
+
settings.hooks[eventName].push({
|
|
2813
|
+
matcher: "",
|
|
2814
|
+
hooks: [
|
|
2815
|
+
{
|
|
2816
|
+
type: "command",
|
|
2817
|
+
command: hookCommand,
|
|
2818
|
+
timeout: 5,
|
|
2819
|
+
},
|
|
2820
|
+
],
|
|
2821
|
+
});
|
|
2822
|
+
anyAdded = true;
|
|
2823
|
+
}
|
|
2659
2824
|
}
|
|
2660
2825
|
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
hooks:
|
|
2665
|
-
|
|
2666
|
-
type: "command",
|
|
2667
|
-
command: hookCommand,
|
|
2668
|
-
timeout: 5,
|
|
2669
|
-
},
|
|
2670
|
-
],
|
|
2671
|
-
});
|
|
2826
|
+
if (anyAdded) {
|
|
2827
|
+
// Write settings
|
|
2828
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2829
|
+
console.log(`Configured hooks in: ${settingsPath}`);
|
|
2830
|
+
}
|
|
2672
2831
|
|
|
2673
|
-
// Write settings
|
|
2674
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
2675
|
-
console.log(`Configured hook in: ${settingsPath}`);
|
|
2676
2832
|
return true;
|
|
2677
2833
|
} catch {
|
|
2678
2834
|
// If we can't configure automatically, return false so manual instructions are shown
|
|
@@ -2744,7 +2900,9 @@ function cmdAttach(session) {
|
|
|
2744
2900
|
}
|
|
2745
2901
|
|
|
2746
2902
|
// Hand over to tmux attach
|
|
2747
|
-
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
2903
|
+
const result = spawnSync("tmux", ["attach", "-t", resolved], {
|
|
2904
|
+
stdio: "inherit",
|
|
2905
|
+
});
|
|
2748
2906
|
process.exit(result.status || 0);
|
|
2749
2907
|
}
|
|
2750
2908
|
|
|
@@ -2803,13 +2961,15 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2803
2961
|
|
|
2804
2962
|
if (newLines.length === 0) return;
|
|
2805
2963
|
|
|
2806
|
-
const entries = newLines
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2964
|
+
const entries = newLines
|
|
2965
|
+
.map((line) => {
|
|
2966
|
+
try {
|
|
2967
|
+
return JSON.parse(line);
|
|
2968
|
+
} catch {
|
|
2969
|
+
return null;
|
|
2970
|
+
}
|
|
2971
|
+
})
|
|
2972
|
+
.filter(Boolean);
|
|
2813
2973
|
|
|
2814
2974
|
const output = [];
|
|
2815
2975
|
if (isInitial) {
|
|
@@ -2822,7 +2982,10 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
2822
2982
|
const ts = entry.timestamp || entry.ts || entry.createdAt;
|
|
2823
2983
|
if (ts && ts !== lastTimestamp) {
|
|
2824
2984
|
const date = new Date(ts);
|
|
2825
|
-
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
2985
|
+
const timeStr = date.toLocaleTimeString("en-GB", {
|
|
2986
|
+
hour: "2-digit",
|
|
2987
|
+
minute: "2-digit",
|
|
2988
|
+
});
|
|
2826
2989
|
if (formatted.isUserMessage) {
|
|
2827
2990
|
output.push(`\n### ${timeStr}\n`);
|
|
2828
2991
|
}
|
|
@@ -2872,7 +3035,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2872
3035
|
if (type === "user" || type === "human") {
|
|
2873
3036
|
const text = extractTextContent(content);
|
|
2874
3037
|
if (text) {
|
|
2875
|
-
return {
|
|
3038
|
+
return {
|
|
3039
|
+
text: `**User**: ${truncate(text, TRUNCATE_USER_LEN)}\n`,
|
|
3040
|
+
isUserMessage: true,
|
|
3041
|
+
};
|
|
2876
3042
|
}
|
|
2877
3043
|
}
|
|
2878
3044
|
|
|
@@ -2888,10 +3054,12 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2888
3054
|
// Extract tool calls (compressed)
|
|
2889
3055
|
const tools = extractToolCalls(content);
|
|
2890
3056
|
if (tools.length > 0) {
|
|
2891
|
-
const toolSummary = tools
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
3057
|
+
const toolSummary = tools
|
|
3058
|
+
.map((t) => {
|
|
3059
|
+
if (t.error) return `${t.name}(${t.target}) ✗`;
|
|
3060
|
+
return `${t.name}(${t.target})`;
|
|
3061
|
+
})
|
|
3062
|
+
.join(", ");
|
|
2895
3063
|
parts.push(`> ${toolSummary}\n`);
|
|
2896
3064
|
}
|
|
2897
3065
|
|
|
@@ -2913,7 +3081,10 @@ function formatLogEntry(entry, { reasoning = false } = {}) {
|
|
|
2913
3081
|
const error = entry.error || entry.is_error;
|
|
2914
3082
|
if (error) {
|
|
2915
3083
|
const name = entry.tool_name || entry.name || "tool";
|
|
2916
|
-
return {
|
|
3084
|
+
return {
|
|
3085
|
+
text: `> ${name} ✗ (${truncate(String(error), 100)})\n`,
|
|
3086
|
+
isUserMessage: false,
|
|
3087
|
+
};
|
|
2917
3088
|
}
|
|
2918
3089
|
}
|
|
2919
3090
|
|
|
@@ -2950,7 +3121,8 @@ function extractToolCalls(content) {
|
|
|
2950
3121
|
const name = c.name || c.tool || "tool";
|
|
2951
3122
|
const input = c.input || c.arguments || {};
|
|
2952
3123
|
// Extract a reasonable target from the input
|
|
2953
|
-
const target =
|
|
3124
|
+
const target =
|
|
3125
|
+
input.file_path || input.path || input.command?.slice(0, 30) || input.pattern || "";
|
|
2954
3126
|
const shortTarget = target.split("/").pop() || target.slice(0, 20);
|
|
2955
3127
|
return { name, target: shortTarget, error: c.error };
|
|
2956
3128
|
});
|
|
@@ -2995,8 +3167,14 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
2995
3167
|
|
|
2996
3168
|
for (const entry of entries) {
|
|
2997
3169
|
const ts = new Date(entry.timestamp);
|
|
2998
|
-
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
2999
|
-
|
|
3170
|
+
const timeStr = ts.toLocaleTimeString("en-GB", {
|
|
3171
|
+
hour: "2-digit",
|
|
3172
|
+
minute: "2-digit",
|
|
3173
|
+
});
|
|
3174
|
+
const dateStr = ts.toLocaleDateString("en-GB", {
|
|
3175
|
+
month: "short",
|
|
3176
|
+
day: "numeric",
|
|
3177
|
+
});
|
|
3000
3178
|
const p = entry.payload || {};
|
|
3001
3179
|
|
|
3002
3180
|
console.log(`### [${p.agent || "unknown"}] ${dateStr} ${timeStr}\n`);
|
|
@@ -3037,7 +3215,9 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3037
3215
|
}
|
|
3038
3216
|
|
|
3039
3217
|
/** @type {string} */
|
|
3040
|
-
const activeSession = sessionExists
|
|
3218
|
+
const activeSession = sessionExists
|
|
3219
|
+
? /** @type {string} */ (session)
|
|
3220
|
+
: await cmdStart(agent, session, { yolo });
|
|
3041
3221
|
|
|
3042
3222
|
tmuxSendLiteral(activeSession, message);
|
|
3043
3223
|
await sleep(50);
|
|
@@ -3138,7 +3318,13 @@ async function cmdReject(agent, session, { wait = false, timeoutMs } = {}) {
|
|
|
3138
3318
|
* @param {string | null | undefined} customInstructions
|
|
3139
3319
|
* @param {{wait?: boolean, yolo?: boolean, fresh?: boolean, timeoutMs?: number}} [options]
|
|
3140
3320
|
*/
|
|
3141
|
-
async function cmdReview(
|
|
3321
|
+
async function cmdReview(
|
|
3322
|
+
agent,
|
|
3323
|
+
session,
|
|
3324
|
+
option,
|
|
3325
|
+
customInstructions,
|
|
3326
|
+
{ wait = true, yolo = true, fresh = false, timeoutMs = REVIEW_TIMEOUT_MS } = {},
|
|
3327
|
+
) {
|
|
3142
3328
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3143
3329
|
|
|
3144
3330
|
// Reset conversation if --fresh and session exists
|
|
@@ -3164,7 +3350,11 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3164
3350
|
|
|
3165
3351
|
// AX_REVIEW_MODE=exec: bypass /review command, send instructions directly
|
|
3166
3352
|
if (process.env.AX_REVIEW_MODE === "exec" && option === "custom" && customInstructions) {
|
|
3167
|
-
return cmdAsk(agent, session, customInstructions, {
|
|
3353
|
+
return cmdAsk(agent, session, customInstructions, {
|
|
3354
|
+
noWait: !wait,
|
|
3355
|
+
yolo,
|
|
3356
|
+
timeoutMs,
|
|
3357
|
+
});
|
|
3168
3358
|
}
|
|
3169
3359
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3170
3360
|
|
|
@@ -3176,7 +3366,9 @@ async function cmdReview(agent, session, option, customInstructions, { wait = tr
|
|
|
3176
3366
|
}
|
|
3177
3367
|
|
|
3178
3368
|
/** @type {string} */
|
|
3179
|
-
const activeSession = sessionExists
|
|
3369
|
+
const activeSession = sessionExists
|
|
3370
|
+
? /** @type {string} */ (session)
|
|
3371
|
+
: await cmdStart(agent, session, { yolo });
|
|
3180
3372
|
|
|
3181
3373
|
tmuxSendLiteral(activeSession, "/review");
|
|
3182
3374
|
await sleep(50);
|
|
@@ -3437,7 +3629,7 @@ function getAgentFromInvocation() {
|
|
|
3437
3629
|
*/
|
|
3438
3630
|
function printHelp(agent, cliName) {
|
|
3439
3631
|
const name = cliName;
|
|
3440
|
-
const backendName = agent.name === "codex" ? "
|
|
3632
|
+
const backendName = agent.name === "codex" ? "Codex" : "Claude";
|
|
3441
3633
|
const hasReview = !!agent.reviewOptions;
|
|
3442
3634
|
|
|
3443
3635
|
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
@@ -3446,14 +3638,18 @@ Commands:
|
|
|
3446
3638
|
agents List all running agents with state and log paths
|
|
3447
3639
|
attach [SESSION] Attach to agent session interactively
|
|
3448
3640
|
log SESSION View conversation log (--tail=N, --follow, --reasoning)
|
|
3449
|
-
mailbox View
|
|
3450
|
-
|
|
3451
|
-
|
|
3641
|
+
mailbox View archangel observations (--limit=N, --branch=X, --all)
|
|
3642
|
+
summon [name] Summon archangels (all, or by name)
|
|
3643
|
+
recall [name] Recall archangels (all, or by name)
|
|
3452
3644
|
kill Kill sessions in current project (--all for all, --session=NAME for one)
|
|
3453
3645
|
status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
|
|
3454
3646
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
3455
|
-
debug Show raw screen output and detected state${
|
|
3456
|
-
|
|
3647
|
+
debug Show raw screen output and detected state${
|
|
3648
|
+
hasReview
|
|
3649
|
+
? `
|
|
3650
|
+
review [TYPE] Review code: pr, uncommitted, commit, custom`
|
|
3651
|
+
: ""
|
|
3652
|
+
}
|
|
3457
3653
|
select N Select menu option N
|
|
3458
3654
|
approve Approve pending action (send 'y')
|
|
3459
3655
|
reject Reject pending action (send 'n')
|
|
@@ -3464,7 +3660,7 @@ Commands:
|
|
|
3464
3660
|
|
|
3465
3661
|
Flags:
|
|
3466
3662
|
--tool=NAME Use specific agent (codex, claude)
|
|
3467
|
-
--session=NAME Target session by name,
|
|
3663
|
+
--session=NAME Target session by name, archangel name, or UUID prefix (self = current)
|
|
3468
3664
|
--wait Wait for response (for review, approve, etc)
|
|
3469
3665
|
--no-wait Don't wait (for messages, which wait by default)
|
|
3470
3666
|
--timeout=N Set timeout in seconds (default: 120)
|
|
@@ -3472,29 +3668,28 @@ Flags:
|
|
|
3472
3668
|
--fresh Reset conversation before review
|
|
3473
3669
|
|
|
3474
3670
|
Environment:
|
|
3475
|
-
AX_DEFAULT_TOOL
|
|
3476
|
-
${agent.envVar}
|
|
3477
|
-
AX_CLAUDE_CONFIG_DIR
|
|
3478
|
-
AX_CODEX_CONFIG_DIR
|
|
3479
|
-
AX_REVIEW_MODE=exec
|
|
3480
|
-
AX_DEBUG=1
|
|
3671
|
+
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
3672
|
+
${agent.envVar} Override default session name
|
|
3673
|
+
AX_CLAUDE_CONFIG_DIR Override Claude config directory (default: ~/.claude)
|
|
3674
|
+
AX_CODEX_CONFIG_DIR Override Codex config directory (default: ~/.codex)
|
|
3675
|
+
AX_REVIEW_MODE=exec Bypass /review, send instructions directly (codex only)
|
|
3676
|
+
AX_DEBUG=1 Enable debug logging
|
|
3481
3677
|
|
|
3482
3678
|
Examples:
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
./${name}.js agents # List all agents (shows TYPE=daemon)`);
|
|
3679
|
+
${name} "explain this codebase"
|
|
3680
|
+
${name} "please review the error handling" # Auto custom review
|
|
3681
|
+
${name} review uncommitted --wait
|
|
3682
|
+
${name} approve --wait
|
|
3683
|
+
${name} kill # Kill agents in current project
|
|
3684
|
+
${name} kill --all # Kill all agents across all projects
|
|
3685
|
+
${name} kill --session=NAME # Kill specific session
|
|
3686
|
+
${name} send "1[Enter]" # Recovery: select option 1 and press Enter
|
|
3687
|
+
${name} send "[Escape][Escape]" # Recovery: escape out of a dialog
|
|
3688
|
+
${name} summon # Summon all archangels from .ai/agents/*.md
|
|
3689
|
+
${name} summon reviewer # Summon by name (creates config if new)
|
|
3690
|
+
${name} recall # Recall all archangels
|
|
3691
|
+
${name} recall reviewer # Recall one by name
|
|
3692
|
+
${name} agents # List all agents (shows TYPE=archangel)`);
|
|
3498
3693
|
}
|
|
3499
3694
|
|
|
3500
3695
|
async function main() {
|
|
@@ -3548,7 +3743,7 @@ async function main() {
|
|
|
3548
3743
|
}
|
|
3549
3744
|
session = current;
|
|
3550
3745
|
} else {
|
|
3551
|
-
// Resolve partial names,
|
|
3746
|
+
// Resolve partial names, archangel names, and UUID prefixes
|
|
3552
3747
|
session = resolveSessionName(val);
|
|
3553
3748
|
}
|
|
3554
3749
|
}
|
|
@@ -3598,21 +3793,28 @@ async function main() {
|
|
|
3598
3793
|
!a.startsWith("--tool") &&
|
|
3599
3794
|
!a.startsWith("--tail") &&
|
|
3600
3795
|
!a.startsWith("--limit") &&
|
|
3601
|
-
!a.startsWith("--branch")
|
|
3796
|
+
!a.startsWith("--branch"),
|
|
3602
3797
|
);
|
|
3603
3798
|
const cmd = filteredArgs[0];
|
|
3604
3799
|
|
|
3605
3800
|
// Dispatch commands
|
|
3606
3801
|
if (cmd === "agents") return cmdAgents();
|
|
3607
|
-
if (cmd === "
|
|
3608
|
-
if (cmd === "
|
|
3802
|
+
if (cmd === "summon") return cmdSummon(filteredArgs[1]);
|
|
3803
|
+
if (cmd === "recall") return cmdRecall(filteredArgs[1]);
|
|
3804
|
+
if (cmd === "archangel") return cmdArchangel(filteredArgs[1]);
|
|
3609
3805
|
if (cmd === "kill") return cmdKill(session, { all });
|
|
3610
3806
|
if (cmd === "attach") return cmdAttach(filteredArgs[1] || session);
|
|
3611
3807
|
if (cmd === "log") return cmdLog(filteredArgs[1] || session, { tail, reasoning, follow });
|
|
3612
3808
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
3613
3809
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
3614
3810
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
3615
|
-
if (cmd === "review")
|
|
3811
|
+
if (cmd === "review")
|
|
3812
|
+
return cmdReview(agent, session, filteredArgs[1], filteredArgs[2], {
|
|
3813
|
+
wait,
|
|
3814
|
+
yolo,
|
|
3815
|
+
fresh,
|
|
3816
|
+
timeoutMs,
|
|
3817
|
+
});
|
|
3616
3818
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
3617
3819
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
3618
3820
|
if (cmd === "output") {
|
|
@@ -3620,10 +3822,12 @@ async function main() {
|
|
|
3620
3822
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
3621
3823
|
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
3622
3824
|
}
|
|
3623
|
-
if (cmd === "send" && filteredArgs.length > 1)
|
|
3825
|
+
if (cmd === "send" && filteredArgs.length > 1)
|
|
3826
|
+
return cmdSend(session, filteredArgs.slice(1).join(" "));
|
|
3624
3827
|
if (cmd === "compact") return cmdAsk(agent, session, "/compact", { noWait: true, timeoutMs });
|
|
3625
3828
|
if (cmd === "reset") return cmdAsk(agent, session, "/new", { noWait: true, timeoutMs });
|
|
3626
|
-
if (cmd === "select" && filteredArgs[1])
|
|
3829
|
+
if (cmd === "select" && filteredArgs[1])
|
|
3830
|
+
return cmdSelect(agent, session, filteredArgs[1], { wait, timeoutMs });
|
|
3627
3831
|
|
|
3628
3832
|
// Default: send message
|
|
3629
3833
|
let message = filteredArgs.join(" ");
|
|
@@ -3640,7 +3844,11 @@ async function main() {
|
|
|
3640
3844
|
const reviewMatch = message.match(/^please review\s*(.*)/i);
|
|
3641
3845
|
if (reviewMatch && agent.reviewOptions) {
|
|
3642
3846
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
3643
|
-
return cmdReview(agent, session, "custom", customInstructions, {
|
|
3847
|
+
return cmdReview(agent, session, "custom", customInstructions, {
|
|
3848
|
+
wait: !noWait,
|
|
3849
|
+
yolo,
|
|
3850
|
+
timeoutMs,
|
|
3851
|
+
});
|
|
3644
3852
|
}
|
|
3645
3853
|
|
|
3646
3854
|
return cmdAsk(agent, session, message, { noWait, yolo, timeoutMs });
|
|
@@ -3648,13 +3856,15 @@ async function main() {
|
|
|
3648
3856
|
|
|
3649
3857
|
// Run main() only when executed directly (not when imported for testing)
|
|
3650
3858
|
// Use realpathSync to handle symlinks (e.g., axclaude, axcodex bin entries)
|
|
3651
|
-
const isDirectRun =
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3859
|
+
const isDirectRun =
|
|
3860
|
+
process.argv[1] &&
|
|
3861
|
+
(() => {
|
|
3862
|
+
try {
|
|
3863
|
+
return realpathSync(process.argv[1]) === __filename;
|
|
3864
|
+
} catch {
|
|
3865
|
+
return false;
|
|
3866
|
+
}
|
|
3867
|
+
})();
|
|
3658
3868
|
if (isDirectRun) {
|
|
3659
3869
|
main().catch((err) => {
|
|
3660
3870
|
console.log(`ERROR: ${err.message}`);
|
|
@@ -3678,4 +3888,3 @@ export {
|
|
|
3678
3888
|
detectState,
|
|
3679
3889
|
State,
|
|
3680
3890
|
};
|
|
3681
|
-
|