ax-agents 0.0.1-alpha.3 → 0.0.1-alpha.5
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 +15 -2
- package/ax.js +162 -189
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A CLI for orchestrating AI coding agents via `tmux`.
|
|
4
4
|
|
|
5
|
+
Running agents in tmux sessions makes it easy to monitor multiple agents, review their work, and interact with them when needed.
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```
|
|
@@ -19,9 +21,20 @@ ax --yolo "fix the login bug"
|
|
|
19
21
|
|
|
20
22
|
Run `ax --help` for all options.
|
|
21
23
|
|
|
22
|
-
##
|
|
24
|
+
## Archangels
|
|
25
|
+
|
|
26
|
+
Archangels are background agents that watch your codebase and surface observations to your main coding session.
|
|
27
|
+
|
|
28
|
+
Configure them in `.ai/agents/*.md`, then:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
ax summon # summon all archangels
|
|
32
|
+
ax summon reviewer # summon one by name
|
|
33
|
+
ax recall # recall all
|
|
34
|
+
ax recall reviewer # recall one
|
|
35
|
+
```
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
When you next prompt Claude, any observations from your archangels will be injected automatically.
|
|
25
38
|
|
|
26
39
|
## License
|
|
27
40
|
|
package/ax.js
CHANGED
|
@@ -32,12 +32,12 @@ const VERSION = packageJson.version;
|
|
|
32
32
|
/**
|
|
33
33
|
* @typedef {Object} ParsedSession
|
|
34
34
|
* @property {string} tool
|
|
35
|
-
* @property {string} [
|
|
35
|
+
* @property {string} [archangelName]
|
|
36
36
|
* @property {string} [uuid]
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* @typedef {Object}
|
|
40
|
+
* @typedef {Object} ArchangelConfig
|
|
41
41
|
* @property {string} name
|
|
42
42
|
* @property {ToolName} tool
|
|
43
43
|
* @property {string[]} watch
|
|
@@ -264,9 +264,9 @@ const POLL_MS = parseInt(process.env.AX_POLL_MS || "200", 10);
|
|
|
264
264
|
const DEFAULT_TIMEOUT_MS = parseInt(process.env.AX_TIMEOUT_MS || "120000", 10);
|
|
265
265
|
const REVIEW_TIMEOUT_MS = parseInt(process.env.AX_REVIEW_TIMEOUT_MS || "900000", 10); // 15 minutes
|
|
266
266
|
const STARTUP_TIMEOUT_MS = parseInt(process.env.AX_STARTUP_TIMEOUT_MS || "30000", 10);
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const
|
|
267
|
+
const ARCHANGEL_STARTUP_TIMEOUT_MS = parseInt(process.env.AX_ARCHANGEL_STARTUP_TIMEOUT_MS || "60000", 10);
|
|
268
|
+
const ARCHANGEL_RESPONSE_TIMEOUT_MS = parseInt(process.env.AX_ARCHANGEL_RESPONSE_TIMEOUT_MS || "300000", 10); // 5 minutes
|
|
269
|
+
const ARCHANGEL_HEALTH_CHECK_MS = parseInt(process.env.AX_ARCHANGEL_HEALTH_CHECK_MS || "30000", 10);
|
|
270
270
|
const STABLE_MS = parseInt(process.env.AX_STABLE_MS || "1000", 10);
|
|
271
271
|
const APPROVE_DELAY_MS = parseInt(process.env.AX_APPROVE_DELAY_MS || "100", 10);
|
|
272
272
|
const MAILBOX_MAX_AGE_MS = parseInt(process.env.AX_MAILBOX_MAX_AGE_MS || "3600000", 10); // 1 hour
|
|
@@ -274,9 +274,9 @@ const CLAUDE_CONFIG_DIR = process.env.AX_CLAUDE_CONFIG_DIR || path.join(os.homed
|
|
|
274
274
|
const CODEX_CONFIG_DIR = process.env.AX_CODEX_CONFIG_DIR || path.join(os.homedir(), ".codex");
|
|
275
275
|
const TRUNCATE_USER_LEN = 500;
|
|
276
276
|
const TRUNCATE_THINKING_LEN = 300;
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
const
|
|
277
|
+
const ARCHANGEL_GIT_CONTEXT_HOURS = 4;
|
|
278
|
+
const ARCHANGEL_GIT_CONTEXT_MAX_LINES = 200;
|
|
279
|
+
const ARCHANGEL_PARENT_CONTEXT_ENTRIES = 10;
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
282
|
* @param {string} session
|
|
@@ -359,10 +359,10 @@ function parseSessionName(session) {
|
|
|
359
359
|
const tool = match[1].toLowerCase();
|
|
360
360
|
const rest = match[2];
|
|
361
361
|
|
|
362
|
-
//
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
365
|
-
return { tool,
|
|
362
|
+
// Archangel: {tool}-archangel-{name}-{uuid}
|
|
363
|
+
const archangelMatch = rest.match(/^archangel-(.+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i);
|
|
364
|
+
if (archangelMatch) {
|
|
365
|
+
return { tool, archangelName: archangelMatch[1], uuid: archangelMatch[2] };
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
// Partner: {tool}-partner-{uuid}
|
|
@@ -567,15 +567,15 @@ function resolveSessionName(partial) {
|
|
|
567
567
|
// Exact match
|
|
568
568
|
if (agentSessions.includes(partial)) return partial;
|
|
569
569
|
|
|
570
|
-
//
|
|
571
|
-
const
|
|
570
|
+
// Archangel name match (e.g., "reviewer" matches "claude-archangel-reviewer-uuid")
|
|
571
|
+
const archangelMatches = agentSessions.filter((s) => {
|
|
572
572
|
const parsed = parseSessionName(s);
|
|
573
|
-
return parsed?.
|
|
573
|
+
return parsed?.archangelName === partial;
|
|
574
574
|
});
|
|
575
|
-
if (
|
|
576
|
-
if (
|
|
577
|
-
console.log("ERROR: ambiguous
|
|
578
|
-
for (const m of
|
|
575
|
+
if (archangelMatches.length === 1) return archangelMatches[0];
|
|
576
|
+
if (archangelMatches.length > 1) {
|
|
577
|
+
console.log("ERROR: ambiguous archangel name. Matches:");
|
|
578
|
+
for (const m of archangelMatches) console.log(` ${m}`);
|
|
579
579
|
process.exit(1);
|
|
580
580
|
}
|
|
581
581
|
|
|
@@ -604,18 +604,18 @@ function resolveSessionName(partial) {
|
|
|
604
604
|
}
|
|
605
605
|
|
|
606
606
|
// =============================================================================
|
|
607
|
-
// Helpers -
|
|
607
|
+
// Helpers - archangels
|
|
608
608
|
// =============================================================================
|
|
609
609
|
|
|
610
610
|
/**
|
|
611
|
-
* @returns {
|
|
611
|
+
* @returns {ArchangelConfig[]}
|
|
612
612
|
*/
|
|
613
613
|
function loadAgentConfigs() {
|
|
614
614
|
const agentsDir = AGENTS_DIR;
|
|
615
615
|
if (!existsSync(agentsDir)) return [];
|
|
616
616
|
|
|
617
617
|
const files = readdirSync(agentsDir).filter((f) => f.endsWith(".md"));
|
|
618
|
-
/** @type {
|
|
618
|
+
/** @type {ArchangelConfig[]} */
|
|
619
619
|
const configs = [];
|
|
620
620
|
|
|
621
621
|
for (const file of files) {
|
|
@@ -638,7 +638,7 @@ function loadAgentConfigs() {
|
|
|
638
638
|
/**
|
|
639
639
|
* @param {string} filename
|
|
640
640
|
* @param {string} content
|
|
641
|
-
* @returns {
|
|
641
|
+
* @returns {ArchangelConfig | {error: string} | null}
|
|
642
642
|
*/
|
|
643
643
|
function parseAgentConfig(filename, content) {
|
|
644
644
|
const name = filename.replace(/\.md$/, "");
|
|
@@ -723,11 +723,11 @@ function parseAgentConfig(filename, content) {
|
|
|
723
723
|
}
|
|
724
724
|
|
|
725
725
|
/**
|
|
726
|
-
* @param {
|
|
726
|
+
* @param {ArchangelConfig} config
|
|
727
727
|
* @returns {string}
|
|
728
728
|
*/
|
|
729
|
-
function
|
|
730
|
-
return `${config.tool}-
|
|
729
|
+
function getArchangelSessionPattern(config) {
|
|
730
|
+
return `${config.tool}-archangel-${config.name}`;
|
|
731
731
|
}
|
|
732
732
|
|
|
733
733
|
// =============================================================================
|
|
@@ -950,9 +950,9 @@ function buildGitContext(hoursAgo = 4, maxLinesPerSection = 200) {
|
|
|
950
950
|
// Helpers - parent session context
|
|
951
951
|
// =============================================================================
|
|
952
952
|
|
|
953
|
-
// Environment variables used to pass parent session info to
|
|
954
|
-
const
|
|
955
|
-
const
|
|
953
|
+
// Environment variables used to pass parent session info to archangels
|
|
954
|
+
const AX_ARCHANGEL_PARENT_SESSION_ENV = "AX_ARCHANGEL_PARENT_SESSION";
|
|
955
|
+
const AX_ARCHANGEL_PARENT_UUID_ENV = "AX_ARCHANGEL_PARENT_UUID";
|
|
956
956
|
|
|
957
957
|
/**
|
|
958
958
|
* @returns {ParentSession | null}
|
|
@@ -962,7 +962,7 @@ function findCurrentClaudeSession() {
|
|
|
962
962
|
const current = tmuxCurrentSession();
|
|
963
963
|
if (current) {
|
|
964
964
|
const parsed = parseSessionName(current);
|
|
965
|
-
if (parsed?.tool === "claude" && !parsed.
|
|
965
|
+
if (parsed?.tool === "claude" && !parsed.archangelName && parsed.uuid) {
|
|
966
966
|
return { session: current, uuid: parsed.uuid };
|
|
967
967
|
}
|
|
968
968
|
}
|
|
@@ -979,7 +979,7 @@ function findCurrentClaudeSession() {
|
|
|
979
979
|
for (const session of sessions) {
|
|
980
980
|
const parsed = parseSessionName(session);
|
|
981
981
|
if (!parsed || parsed.tool !== "claude") continue;
|
|
982
|
-
if (parsed.
|
|
982
|
+
if (parsed.archangelName) continue;
|
|
983
983
|
if (!parsed.uuid) continue;
|
|
984
984
|
|
|
985
985
|
const sessionCwd = getTmuxSessionCwd(session);
|
|
@@ -1035,15 +1035,15 @@ function findCurrentClaudeSession() {
|
|
|
1035
1035
|
* @returns {ParentSession | null}
|
|
1036
1036
|
*/
|
|
1037
1037
|
function findParentSession() {
|
|
1038
|
-
// First check if parent session was passed via environment (for
|
|
1039
|
-
const envUuid = process.env[
|
|
1038
|
+
// First check if parent session was passed via environment (for archangels)
|
|
1039
|
+
const envUuid = process.env[AX_ARCHANGEL_PARENT_UUID_ENV];
|
|
1040
1040
|
if (envUuid) {
|
|
1041
1041
|
// Session name is optional (may be null for non-tmux sessions)
|
|
1042
|
-
const envSession = process.env[
|
|
1042
|
+
const envSession = process.env[AX_ARCHANGEL_PARENT_SESSION_ENV] || null;
|
|
1043
1043
|
return { session: envSession, uuid: envUuid };
|
|
1044
1044
|
}
|
|
1045
1045
|
|
|
1046
|
-
// Fallback to detecting current session (shouldn't be needed for
|
|
1046
|
+
// Fallback to detecting current session (shouldn't be needed for archangels)
|
|
1047
1047
|
return findCurrentClaudeSession();
|
|
1048
1048
|
}
|
|
1049
1049
|
|
|
@@ -2045,7 +2045,7 @@ function cmdAgents() {
|
|
|
2045
2045
|
const screen = tmuxCapture(session);
|
|
2046
2046
|
const state = agent.getState(screen);
|
|
2047
2047
|
const logPath = agent.findLogPath(session);
|
|
2048
|
-
const type = parsed.
|
|
2048
|
+
const type = parsed.archangelName ? "archangel" : "-";
|
|
2049
2049
|
|
|
2050
2050
|
return {
|
|
2051
2051
|
session,
|
|
@@ -2073,87 +2073,84 @@ function cmdAgents() {
|
|
|
2073
2073
|
}
|
|
2074
2074
|
|
|
2075
2075
|
// =============================================================================
|
|
2076
|
-
// Command:
|
|
2076
|
+
// Command: summon/recall
|
|
2077
2077
|
// =============================================================================
|
|
2078
2078
|
|
|
2079
2079
|
/**
|
|
2080
2080
|
* @param {string} pattern
|
|
2081
2081
|
* @returns {string | undefined}
|
|
2082
2082
|
*/
|
|
2083
|
-
function
|
|
2083
|
+
function findArchangelSession(pattern) {
|
|
2084
2084
|
const sessions = tmuxListSessions();
|
|
2085
2085
|
return sessions.find((s) => s.startsWith(pattern));
|
|
2086
2086
|
}
|
|
2087
2087
|
|
|
2088
2088
|
/**
|
|
2089
|
-
* @param {
|
|
2089
|
+
* @param {ArchangelConfig} config
|
|
2090
2090
|
* @returns {string}
|
|
2091
2091
|
*/
|
|
2092
|
-
function
|
|
2093
|
-
return `${config.tool}-
|
|
2092
|
+
function generateArchangelSessionName(config) {
|
|
2093
|
+
return `${config.tool}-archangel-${config.name}-${randomUUID()}`;
|
|
2094
2094
|
}
|
|
2095
2095
|
|
|
2096
2096
|
/**
|
|
2097
|
-
* @param {
|
|
2097
|
+
* @param {ArchangelConfig} config
|
|
2098
2098
|
* @param {ParentSession | null} [parentSession]
|
|
2099
2099
|
*/
|
|
2100
|
-
function
|
|
2101
|
-
// Build environment with parent session info if available
|
|
2100
|
+
function startArchangel(config, parentSession = null) {
|
|
2102
2101
|
/** @type {NodeJS.ProcessEnv} */
|
|
2103
2102
|
const env = { ...process.env };
|
|
2104
2103
|
if (parentSession?.uuid) {
|
|
2105
|
-
// Session name may be null for non-tmux sessions, but uuid is required
|
|
2106
2104
|
if (parentSession.session) {
|
|
2107
|
-
env[
|
|
2105
|
+
env[AX_ARCHANGEL_PARENT_SESSION_ENV] = parentSession.session;
|
|
2108
2106
|
}
|
|
2109
|
-
env[
|
|
2107
|
+
env[AX_ARCHANGEL_PARENT_UUID_ENV] = parentSession.uuid;
|
|
2110
2108
|
}
|
|
2111
2109
|
|
|
2112
|
-
|
|
2113
|
-
const child = spawn("node", [process.argv[1], "daemon", config.name], {
|
|
2110
|
+
const child = spawn("node", [process.argv[1], "archangel", config.name], {
|
|
2114
2111
|
detached: true,
|
|
2115
2112
|
stdio: "ignore",
|
|
2116
2113
|
cwd: process.cwd(),
|
|
2117
2114
|
env,
|
|
2118
2115
|
});
|
|
2119
2116
|
child.unref();
|
|
2120
|
-
console.log(`
|
|
2117
|
+
console.log(`Summoning: ${config.name} (pid ${child.pid})${parentSession ? ` [parent: ${parentSession.session}]` : ""}`);
|
|
2121
2118
|
}
|
|
2122
2119
|
|
|
2123
2120
|
// =============================================================================
|
|
2124
|
-
// Command:
|
|
2121
|
+
// Command: archangel (runs as the archangel process itself)
|
|
2125
2122
|
// =============================================================================
|
|
2126
2123
|
|
|
2127
2124
|
/**
|
|
2128
2125
|
* @param {string | undefined} agentName
|
|
2129
2126
|
*/
|
|
2130
|
-
async function
|
|
2127
|
+
async function cmdArchangel(agentName) {
|
|
2131
2128
|
if (!agentName) {
|
|
2132
|
-
console.error("Usage: ./ax.js
|
|
2129
|
+
console.error("Usage: ./ax.js archangel <name>");
|
|
2133
2130
|
process.exit(1);
|
|
2134
2131
|
}
|
|
2135
2132
|
// Load agent config
|
|
2136
2133
|
const configPath = path.join(AGENTS_DIR, `${agentName}.md`);
|
|
2137
2134
|
if (!existsSync(configPath)) {
|
|
2138
|
-
console.error(`[
|
|
2135
|
+
console.error(`[archangel:${agentName}] Config not found: ${configPath}`);
|
|
2139
2136
|
process.exit(1);
|
|
2140
2137
|
}
|
|
2141
2138
|
|
|
2142
2139
|
const content = readFileSync(configPath, "utf-8");
|
|
2143
2140
|
const configResult = parseAgentConfig(`${agentName}.md`, content);
|
|
2144
2141
|
if (!configResult || "error" in configResult) {
|
|
2145
|
-
console.error(`[
|
|
2142
|
+
console.error(`[archangel:${agentName}] Invalid config`);
|
|
2146
2143
|
process.exit(1);
|
|
2147
2144
|
}
|
|
2148
2145
|
const config = configResult;
|
|
2149
2146
|
|
|
2150
2147
|
const agent = config.tool === "claude" ? ClaudeAgent : CodexAgent;
|
|
2151
|
-
const sessionName =
|
|
2148
|
+
const sessionName = generateArchangelSessionName(config);
|
|
2152
2149
|
|
|
2153
2150
|
// Check agent CLI is installed before trying to start
|
|
2154
2151
|
const cliCheck = spawnSync("which", [agent.name], { encoding: "utf-8" });
|
|
2155
2152
|
if (cliCheck.status !== 0) {
|
|
2156
|
-
console.error(`[
|
|
2153
|
+
console.error(`[archangel:${agentName}] ERROR: ${agent.name} CLI is not installed or not in PATH`);
|
|
2157
2154
|
process.exit(1);
|
|
2158
2155
|
}
|
|
2159
2156
|
|
|
@@ -2163,7 +2160,7 @@ async function cmdDaemon(agentName) {
|
|
|
2163
2160
|
|
|
2164
2161
|
// Wait for agent to be ready
|
|
2165
2162
|
const start = Date.now();
|
|
2166
|
-
while (Date.now() - start <
|
|
2163
|
+
while (Date.now() - start < ARCHANGEL_STARTUP_TIMEOUT_MS) {
|
|
2167
2164
|
const screen = tmuxCapture(sessionName);
|
|
2168
2165
|
const state = agent.getState(screen);
|
|
2169
2166
|
|
|
@@ -2174,7 +2171,7 @@ async function cmdDaemon(agentName) {
|
|
|
2174
2171
|
|
|
2175
2172
|
// Handle bypass permissions confirmation dialog (Claude Code shows this for --dangerously-skip-permissions)
|
|
2176
2173
|
if (screen.includes("Bypass Permissions mode") && screen.includes("Yes, I accept")) {
|
|
2177
|
-
console.log(`[
|
|
2174
|
+
console.log(`[archangel:${agentName}] Accepting bypass permissions dialog`);
|
|
2178
2175
|
tmuxSend(sessionName, "2"); // Select "Yes, I accept"
|
|
2179
2176
|
await sleep(300);
|
|
2180
2177
|
tmuxSend(sessionName, "Enter");
|
|
@@ -2183,7 +2180,7 @@ async function cmdDaemon(agentName) {
|
|
|
2183
2180
|
}
|
|
2184
2181
|
|
|
2185
2182
|
if (state === State.READY) {
|
|
2186
|
-
console.log(`[
|
|
2183
|
+
console.log(`[archangel:${agentName}] Started session: ${sessionName}`);
|
|
2187
2184
|
break;
|
|
2188
2185
|
}
|
|
2189
2186
|
|
|
@@ -2258,7 +2255,7 @@ async function cmdDaemon(agentName) {
|
|
|
2258
2255
|
|
|
2259
2256
|
prompt += "\n\n## Files Changed\n - " + files.slice(0, 10).join("\n - ");
|
|
2260
2257
|
|
|
2261
|
-
const gitContext = buildGitContext(
|
|
2258
|
+
const gitContext = buildGitContext(ARCHANGEL_GIT_CONTEXT_HOURS, ARCHANGEL_GIT_CONTEXT_MAX_LINES);
|
|
2262
2259
|
if (gitContext) {
|
|
2263
2260
|
prompt += "\n\n## Git Context\n\n" + gitContext;
|
|
2264
2261
|
}
|
|
@@ -2266,8 +2263,8 @@ async function cmdDaemon(agentName) {
|
|
|
2266
2263
|
prompt += '\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
2264
|
} else {
|
|
2268
2265
|
// Fallback: no JSONL context available, use conversation + git context
|
|
2269
|
-
const parentContext = getParentSessionContext(
|
|
2270
|
-
const gitContext = buildGitContext(
|
|
2266
|
+
const parentContext = getParentSessionContext(ARCHANGEL_PARENT_CONTEXT_ENTRIES);
|
|
2267
|
+
const gitContext = buildGitContext(ARCHANGEL_GIT_CONTEXT_HOURS, ARCHANGEL_GIT_CONTEXT_MAX_LINES);
|
|
2271
2268
|
|
|
2272
2269
|
if (parentContext) {
|
|
2273
2270
|
prompt += "\n\n## Main Session Context\n\nThe user is currently working on:\n\n" + parentContext;
|
|
@@ -2285,7 +2282,7 @@ async function cmdDaemon(agentName) {
|
|
|
2285
2282
|
|
|
2286
2283
|
// Check session still exists
|
|
2287
2284
|
if (!tmuxHasSession(sessionName)) {
|
|
2288
|
-
console.log(`[
|
|
2285
|
+
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
2289
2286
|
process.exit(0);
|
|
2290
2287
|
}
|
|
2291
2288
|
|
|
@@ -2294,12 +2291,12 @@ async function cmdDaemon(agentName) {
|
|
|
2294
2291
|
const state = agent.getState(screen);
|
|
2295
2292
|
|
|
2296
2293
|
if (state === State.RATE_LIMITED) {
|
|
2297
|
-
console.error(`[
|
|
2294
|
+
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2298
2295
|
process.exit(2);
|
|
2299
2296
|
}
|
|
2300
2297
|
|
|
2301
2298
|
if (state !== State.READY) {
|
|
2302
|
-
console.log(`[
|
|
2299
|
+
console.log(`[archangel:${agentName}] Agent not ready (${state}), skipping`);
|
|
2303
2300
|
isProcessing = false;
|
|
2304
2301
|
return;
|
|
2305
2302
|
}
|
|
@@ -2311,10 +2308,10 @@ async function cmdDaemon(agentName) {
|
|
|
2311
2308
|
await sleep(100); // Ensure Enter is processed
|
|
2312
2309
|
|
|
2313
2310
|
// Wait for response
|
|
2314
|
-
const { state: endState, screen: afterScreen } = await waitForResponse(agent, sessionName,
|
|
2311
|
+
const { state: endState, screen: afterScreen } = await waitForResponse(agent, sessionName, ARCHANGEL_RESPONSE_TIMEOUT_MS);
|
|
2315
2312
|
|
|
2316
2313
|
if (endState === State.RATE_LIMITED) {
|
|
2317
|
-
console.error(`[
|
|
2314
|
+
console.error(`[archangel:${agentName}] Rate limited - stopping`);
|
|
2318
2315
|
process.exit(2);
|
|
2319
2316
|
}
|
|
2320
2317
|
|
|
@@ -2335,12 +2332,12 @@ async function cmdDaemon(agentName) {
|
|
|
2335
2332
|
files,
|
|
2336
2333
|
message: cleanedResponse.slice(0, 1000),
|
|
2337
2334
|
});
|
|
2338
|
-
console.log(`[
|
|
2335
|
+
console.log(`[archangel:${agentName}] Wrote observation for ${files.length} file(s)`);
|
|
2339
2336
|
} else if (isGarbage) {
|
|
2340
|
-
console.log(`[
|
|
2337
|
+
console.log(`[archangel:${agentName}] Skipped garbage response`);
|
|
2341
2338
|
}
|
|
2342
2339
|
} catch (err) {
|
|
2343
|
-
console.error(`[
|
|
2340
|
+
console.error(`[archangel:${agentName}] Error:`, err instanceof Error ? err.message : err);
|
|
2344
2341
|
}
|
|
2345
2342
|
|
|
2346
2343
|
isProcessing = false;
|
|
@@ -2348,7 +2345,7 @@ async function cmdDaemon(agentName) {
|
|
|
2348
2345
|
|
|
2349
2346
|
function scheduleProcessChanges() {
|
|
2350
2347
|
processChanges().catch((err) => {
|
|
2351
|
-
console.error(`[
|
|
2348
|
+
console.error(`[archangel:${agentName}] Unhandled error:`, err instanceof Error ? err.message : err);
|
|
2352
2349
|
});
|
|
2353
2350
|
}
|
|
2354
2351
|
|
|
@@ -2369,16 +2366,16 @@ async function cmdDaemon(agentName) {
|
|
|
2369
2366
|
// Check if session still exists periodically
|
|
2370
2367
|
const sessionCheck = setInterval(() => {
|
|
2371
2368
|
if (!tmuxHasSession(sessionName)) {
|
|
2372
|
-
console.log(`[
|
|
2369
|
+
console.log(`[archangel:${agentName}] Session gone, exiting`);
|
|
2373
2370
|
stopWatching();
|
|
2374
2371
|
clearInterval(sessionCheck);
|
|
2375
2372
|
process.exit(0);
|
|
2376
2373
|
}
|
|
2377
|
-
},
|
|
2374
|
+
}, ARCHANGEL_HEALTH_CHECK_MS);
|
|
2378
2375
|
|
|
2379
2376
|
// Handle graceful shutdown
|
|
2380
2377
|
process.on("SIGTERM", () => {
|
|
2381
|
-
console.log(`[
|
|
2378
|
+
console.log(`[archangel:${agentName}] Received SIGTERM, shutting down`);
|
|
2382
2379
|
stopWatching();
|
|
2383
2380
|
clearInterval(sessionCheck);
|
|
2384
2381
|
tmuxSend(sessionName, "C-c");
|
|
@@ -2389,7 +2386,7 @@ async function cmdDaemon(agentName) {
|
|
|
2389
2386
|
});
|
|
2390
2387
|
|
|
2391
2388
|
process.on("SIGINT", () => {
|
|
2392
|
-
console.log(`[
|
|
2389
|
+
console.log(`[archangel:${agentName}] Received SIGINT, shutting down`);
|
|
2393
2390
|
stopWatching();
|
|
2394
2391
|
clearInterval(sessionCheck);
|
|
2395
2392
|
tmuxSend(sessionName, "C-c");
|
|
@@ -2399,48 +2396,33 @@ async function cmdDaemon(agentName) {
|
|
|
2399
2396
|
}, 500);
|
|
2400
2397
|
});
|
|
2401
2398
|
|
|
2402
|
-
console.log(`[
|
|
2399
|
+
console.log(`[archangel:${agentName}] Watching: ${config.watch.join(", ")}`);
|
|
2403
2400
|
|
|
2404
2401
|
// Keep the process alive
|
|
2405
2402
|
await new Promise(() => {});
|
|
2406
2403
|
}
|
|
2407
2404
|
|
|
2408
2405
|
/**
|
|
2409
|
-
* @param {string}
|
|
2410
|
-
* @param {string | null} [daemonName]
|
|
2406
|
+
* @param {string | null} [name]
|
|
2411
2407
|
*/
|
|
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
|
-
}
|
|
2408
|
+
async function cmdSummon(name = null) {
|
|
2409
|
+
const configs = loadAgentConfigs();
|
|
2431
2410
|
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2411
|
+
// If name provided but doesn't exist, create it
|
|
2412
|
+
if (name) {
|
|
2413
|
+
const exists = configs.some((c) => c.name === name);
|
|
2414
|
+
if (!exists) {
|
|
2415
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
2416
|
+
console.log("ERROR: Name must contain only letters, numbers, dashes, and underscores");
|
|
2417
|
+
process.exit(1);
|
|
2418
|
+
}
|
|
2437
2419
|
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
}
|
|
2420
|
+
if (!existsSync(AGENTS_DIR)) {
|
|
2421
|
+
mkdirSync(AGENTS_DIR, { recursive: true });
|
|
2422
|
+
}
|
|
2442
2423
|
|
|
2443
|
-
|
|
2424
|
+
const agentPath = path.join(AGENTS_DIR, `${name}.md`);
|
|
2425
|
+
const template = `---
|
|
2444
2426
|
tool: claude
|
|
2445
2427
|
watch: ["**/*.{ts,tsx,js,jsx,mjs,mts}"]
|
|
2446
2428
|
interval: 30
|
|
@@ -2448,75 +2430,76 @@ interval: 30
|
|
|
2448
2430
|
|
|
2449
2431
|
Review changed files for bugs, type errors, and edge cases.
|
|
2450
2432
|
`;
|
|
2433
|
+
writeFileSync(agentPath, template);
|
|
2434
|
+
console.log(`Created: ${agentPath}`);
|
|
2435
|
+
console.log(`Edit the file to customize, then run: ax summon ${name}`);
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2451
2439
|
|
|
2452
|
-
|
|
2453
|
-
console.log(`
|
|
2454
|
-
console.log(`Edit the file to customize the daemon, then run: ./ax.js daemons start ${daemonName}`);
|
|
2440
|
+
if (configs.length === 0) {
|
|
2441
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
2455
2442
|
return;
|
|
2456
2443
|
}
|
|
2457
2444
|
|
|
2445
|
+
const targetConfigs = name ? configs.filter((c) => c.name === name) : configs;
|
|
2446
|
+
|
|
2447
|
+
ensureMailboxHookScript();
|
|
2448
|
+
|
|
2449
|
+
const parentSession = findCurrentClaudeSession();
|
|
2450
|
+
if (parentSession) {
|
|
2451
|
+
console.log(`Parent session: ${parentSession.session || "(non-tmux)"} [${parentSession.uuid}]`);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
for (const config of targetConfigs) {
|
|
2455
|
+
const sessionPattern = getArchangelSessionPattern(config);
|
|
2456
|
+
const existing = findArchangelSession(sessionPattern);
|
|
2457
|
+
|
|
2458
|
+
if (!existing) {
|
|
2459
|
+
startArchangel(config, parentSession);
|
|
2460
|
+
} else {
|
|
2461
|
+
console.log(`Already running: ${config.name} (${existing})`);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
gcMailbox(24);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/**
|
|
2469
|
+
* @param {string | null} [name]
|
|
2470
|
+
*/
|
|
2471
|
+
async function cmdRecall(name = null) {
|
|
2458
2472
|
const configs = loadAgentConfigs();
|
|
2459
2473
|
|
|
2460
2474
|
if (configs.length === 0) {
|
|
2461
|
-
console.log(`No
|
|
2475
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
2462
2476
|
return;
|
|
2463
2477
|
}
|
|
2464
2478
|
|
|
2465
|
-
|
|
2466
|
-
const targetConfigs = daemonName
|
|
2467
|
-
? configs.filter((c) => c.name === daemonName)
|
|
2468
|
-
: configs;
|
|
2479
|
+
const targetConfigs = name ? configs.filter((c) => c.name === name) : configs;
|
|
2469
2480
|
|
|
2470
|
-
if (
|
|
2471
|
-
console.log(`ERROR:
|
|
2481
|
+
if (name && targetConfigs.length === 0) {
|
|
2482
|
+
console.log(`ERROR: archangel '${name}' not found in ${AGENTS_DIR}/`);
|
|
2472
2483
|
process.exit(1);
|
|
2473
2484
|
}
|
|
2474
2485
|
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
}
|
|
2486
|
+
for (const config of targetConfigs) {
|
|
2487
|
+
const sessionPattern = getArchangelSessionPattern(config);
|
|
2488
|
+
const existing = findArchangelSession(sessionPattern);
|
|
2479
2489
|
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
console.log(`
|
|
2490
|
+
if (existing) {
|
|
2491
|
+
tmuxSend(existing, "C-c");
|
|
2492
|
+
await sleep(300);
|
|
2493
|
+
tmuxKill(existing);
|
|
2494
|
+
console.log(`Recalled: ${config.name} (${existing})`);
|
|
2485
2495
|
} else {
|
|
2486
|
-
console.log(
|
|
2496
|
+
console.log(`Not running: ${config.name}`);
|
|
2487
2497
|
}
|
|
2488
2498
|
}
|
|
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
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
// GC mailbox on start
|
|
2513
|
-
if (action === "start") {
|
|
2514
|
-
gcMailbox(24);
|
|
2515
|
-
}
|
|
2516
2499
|
}
|
|
2517
2500
|
|
|
2518
2501
|
// Version of the hook script template - bump when making changes
|
|
2519
|
-
const HOOK_SCRIPT_VERSION = "
|
|
2502
|
+
const HOOK_SCRIPT_VERSION = "3";
|
|
2520
2503
|
|
|
2521
2504
|
function ensureMailboxHookScript() {
|
|
2522
2505
|
const hooksDir = HOOKS_DIR;
|
|
@@ -2534,26 +2517,21 @@ function ensureMailboxHookScript() {
|
|
|
2534
2517
|
mkdirSync(hooksDir, { recursive: true });
|
|
2535
2518
|
}
|
|
2536
2519
|
|
|
2537
|
-
// Inject absolute paths into the generated script
|
|
2538
|
-
const mailboxPath = path.join(AI_DIR, "mailbox.jsonl");
|
|
2539
|
-
const lastSeenPath = path.join(AI_DIR, "mailbox-last-seen");
|
|
2540
|
-
|
|
2541
2520
|
const hookCode = `#!/usr/bin/env node
|
|
2542
2521
|
${versionMarker}
|
|
2543
|
-
// Auto-generated hook script - do not edit manually
|
|
2544
|
-
|
|
2545
2522
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2523
|
+
import { dirname, join } from "node:path";
|
|
2524
|
+
import { fileURLToPath } from "node:url";
|
|
2546
2525
|
|
|
2526
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2527
|
+
const AI_DIR = join(__dirname, "..");
|
|
2547
2528
|
const DEBUG = process.env.AX_DEBUG === "1";
|
|
2548
|
-
const MAILBOX = "
|
|
2549
|
-
const LAST_SEEN = "
|
|
2550
|
-
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
2529
|
+
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
2530
|
+
const LAST_SEEN = join(AI_DIR, "mailbox-last-seen");
|
|
2531
|
+
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
2551
2532
|
|
|
2552
2533
|
if (!existsSync(MAILBOX)) process.exit(0);
|
|
2553
2534
|
|
|
2554
|
-
// Note: commit filtering removed - age + lastSeen is sufficient
|
|
2555
|
-
|
|
2556
|
-
// Read last seen timestamp
|
|
2557
2535
|
let lastSeen = 0;
|
|
2558
2536
|
try {
|
|
2559
2537
|
if (existsSync(LAST_SEEN)) {
|
|
@@ -2572,11 +2550,7 @@ for (const line of lines) {
|
|
|
2572
2550
|
const entry = JSON.parse(line);
|
|
2573
2551
|
const ts = new Date(entry.timestamp).getTime();
|
|
2574
2552
|
const age = now - ts;
|
|
2575
|
-
|
|
2576
|
-
// Only show observations within max age and not yet seen
|
|
2577
|
-
// (removed commit filter - too strict when HEAD moves during a session)
|
|
2578
2553
|
if (age < MAX_AGE_MS && ts > lastSeen) {
|
|
2579
|
-
// Extract session prefix (without UUID) for shorter log command
|
|
2580
2554
|
const session = entry.payload.session || "";
|
|
2581
2555
|
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, "");
|
|
2582
2556
|
relevant.push({ agent: entry.payload.agent, sessionPrefix, message: entry.payload.message });
|
|
@@ -2601,7 +2575,6 @@ if (relevant.length > 0) {
|
|
|
2601
2575
|
}
|
|
2602
2576
|
const sessionList = [...sessionPrefixes].map(s => "\\\`./ax.js log " + s + "\\\`").join(" or ");
|
|
2603
2577
|
console.log("> For more context: \\\`./ax.js mailbox\\\`" + (sessionList ? " or " + sessionList : ""));
|
|
2604
|
-
// Update last seen timestamp
|
|
2605
2578
|
writeFileSync(LAST_SEEN, now.toString());
|
|
2606
2579
|
}
|
|
2607
2580
|
|
|
@@ -2614,7 +2587,6 @@ process.exit(0);
|
|
|
2614
2587
|
// Configure the hook in .claude/settings.json at the same time
|
|
2615
2588
|
const configuredHook = ensureClaudeHookConfig();
|
|
2616
2589
|
if (!configuredHook) {
|
|
2617
|
-
const hookScriptPath = path.join(HOOKS_DIR, "mailbox-inject.js");
|
|
2618
2590
|
console.log(`\nTo enable manually, add to .claude/settings.json:\n`);
|
|
2619
2591
|
console.log(`{
|
|
2620
2592
|
"hooks": {
|
|
@@ -2624,7 +2596,7 @@ process.exit(0);
|
|
|
2624
2596
|
"hooks": [
|
|
2625
2597
|
{
|
|
2626
2598
|
"type": "command",
|
|
2627
|
-
"command": "node
|
|
2599
|
+
"command": "node .ai/hooks/mailbox-inject.js",
|
|
2628
2600
|
"timeout": 5
|
|
2629
2601
|
}
|
|
2630
2602
|
]
|
|
@@ -2638,8 +2610,7 @@ process.exit(0);
|
|
|
2638
2610
|
function ensureClaudeHookConfig() {
|
|
2639
2611
|
const settingsDir = ".claude";
|
|
2640
2612
|
const settingsPath = path.join(settingsDir, "settings.json");
|
|
2641
|
-
const
|
|
2642
|
-
const hookCommand = `node ${hookScriptPath}`;
|
|
2613
|
+
const hookCommand = "node .ai/hooks/mailbox-inject.js";
|
|
2643
2614
|
|
|
2644
2615
|
try {
|
|
2645
2616
|
/** @type {ClaudeSettings} */
|
|
@@ -3458,9 +3429,9 @@ Commands:
|
|
|
3458
3429
|
agents List all running agents with state and log paths
|
|
3459
3430
|
attach [SESSION] Attach to agent session interactively
|
|
3460
3431
|
log SESSION View conversation log (--tail=N, --follow, --reasoning)
|
|
3461
|
-
mailbox View
|
|
3462
|
-
|
|
3463
|
-
|
|
3432
|
+
mailbox View archangel observations (--limit=N, --branch=X, --all)
|
|
3433
|
+
summon [name] Summon archangels (all, or by name)
|
|
3434
|
+
recall [name] Recall archangels (all, or by name)
|
|
3464
3435
|
kill Kill sessions in current project (--all for all, --session=NAME for one)
|
|
3465
3436
|
status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
|
|
3466
3437
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
@@ -3476,7 +3447,7 @@ Commands:
|
|
|
3476
3447
|
|
|
3477
3448
|
Flags:
|
|
3478
3449
|
--tool=NAME Use specific agent (codex, claude)
|
|
3479
|
-
--session=NAME Target session by name,
|
|
3450
|
+
--session=NAME Target session by name, archangel name, or UUID prefix (self = current)
|
|
3480
3451
|
--wait Wait for response (for review, approve, etc)
|
|
3481
3452
|
--no-wait Don't wait (for messages, which wait by default)
|
|
3482
3453
|
--timeout=N Set timeout in seconds (default: 120)
|
|
@@ -3502,11 +3473,12 @@ Examples:
|
|
|
3502
3473
|
./${name}.js send "1[Enter]" # Recovery: select option 1 and press Enter
|
|
3503
3474
|
./${name}.js send "[Escape][Escape]" # Recovery: escape out of a dialog
|
|
3504
3475
|
|
|
3505
|
-
|
|
3506
|
-
./${name}.js
|
|
3507
|
-
./${name}.js
|
|
3508
|
-
./${name}.js
|
|
3509
|
-
./${name}.js
|
|
3476
|
+
Archangels:
|
|
3477
|
+
./${name}.js summon # Summon all archangels from .ai/agents/*.md
|
|
3478
|
+
./${name}.js summon reviewer # Summon by name (creates config if new)
|
|
3479
|
+
./${name}.js recall # Recall all archangels
|
|
3480
|
+
./${name}.js recall reviewer # Recall one by name
|
|
3481
|
+
./${name}.js agents # List all agents (shows TYPE=archangel)`);
|
|
3510
3482
|
}
|
|
3511
3483
|
|
|
3512
3484
|
async function main() {
|
|
@@ -3560,7 +3532,7 @@ async function main() {
|
|
|
3560
3532
|
}
|
|
3561
3533
|
session = current;
|
|
3562
3534
|
} else {
|
|
3563
|
-
// Resolve partial names,
|
|
3535
|
+
// Resolve partial names, archangel names, and UUID prefixes
|
|
3564
3536
|
session = resolveSessionName(val);
|
|
3565
3537
|
}
|
|
3566
3538
|
}
|
|
@@ -3616,8 +3588,9 @@ async function main() {
|
|
|
3616
3588
|
|
|
3617
3589
|
// Dispatch commands
|
|
3618
3590
|
if (cmd === "agents") return cmdAgents();
|
|
3619
|
-
if (cmd === "
|
|
3620
|
-
if (cmd === "
|
|
3591
|
+
if (cmd === "summon") return cmdSummon(filteredArgs[1]);
|
|
3592
|
+
if (cmd === "recall") return cmdRecall(filteredArgs[1]);
|
|
3593
|
+
if (cmd === "archangel") return cmdArchangel(filteredArgs[1]);
|
|
3621
3594
|
if (cmd === "kill") return cmdKill(session, { all });
|
|
3622
3595
|
if (cmd === "attach") return cmdAttach(filteredArgs[1] || session);
|
|
3623
3596
|
if (cmd === "log") return cmdLog(filteredArgs[1] || session, { tail, reasoning, follow });
|