ax-agents 0.0.1-alpha.12 → 0.0.1-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ax.js +334 -99
- package/package.json +1 -1
package/ax.js
CHANGED
|
@@ -47,6 +47,8 @@ const VERSION = packageJson.version;
|
|
|
47
47
|
* @property {string} tool
|
|
48
48
|
* @property {string} [archangelName]
|
|
49
49
|
* @property {string} [uuid]
|
|
50
|
+
* @property {string} [permissionHash]
|
|
51
|
+
* @property {boolean} [yolo]
|
|
50
52
|
*/
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -259,25 +261,66 @@ function tmuxCurrentSession() {
|
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
/**
|
|
262
|
-
*
|
|
264
|
+
* @typedef {Object} SessionPermissions
|
|
265
|
+
* @property {'yolo' | 'custom' | 'safe'} mode
|
|
266
|
+
* @property {string | null} allowedTools
|
|
267
|
+
* @property {string | null} hash
|
|
268
|
+
*/
|
|
269
|
+
|
|
270
|
+
const SAFE_PERMISSIONS = /** @type {SessionPermissions} */ ({
|
|
271
|
+
mode: "safe",
|
|
272
|
+
allowedTools: null,
|
|
273
|
+
hash: null,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get permission info from a session based on its name.
|
|
278
|
+
* Session name encodes permission mode: -yolo, -p{hash}, or neither (safe).
|
|
279
|
+
* @param {string} session
|
|
280
|
+
* @returns {SessionPermissions}
|
|
281
|
+
*/
|
|
282
|
+
function getSessionPermissions(session) {
|
|
283
|
+
const parsed = parseSessionName(session);
|
|
284
|
+
if (parsed?.yolo) {
|
|
285
|
+
return { mode: "yolo", allowedTools: null, hash: null };
|
|
286
|
+
}
|
|
287
|
+
if (parsed?.permissionHash) {
|
|
288
|
+
return { mode: "custom", allowedTools: null, hash: parsed.permissionHash };
|
|
289
|
+
}
|
|
290
|
+
return SAFE_PERMISSIONS;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if a session was started in yolo mode.
|
|
263
295
|
* @param {string} session
|
|
264
296
|
* @returns {boolean}
|
|
265
297
|
*/
|
|
266
298
|
function isYoloSession(session) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
299
|
+
return getSessionPermissions(session).mode === "yolo";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Normalize allowed tools string for consistent hashing.
|
|
304
|
+
* Splits on tool boundaries (e.g., 'Bash("...") Read') while preserving quoted content.
|
|
305
|
+
* @param {string} tools
|
|
306
|
+
* @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
function normalizeAllowedTools(tools) {
|
|
309
|
+
// Match tool patterns: ToolName or ToolName("args") or ToolName("args with spaces")
|
|
310
|
+
const toolPattern = /\w+(?:\("[^"]*"\))?/g;
|
|
311
|
+
const matches = tools.match(toolPattern) || [];
|
|
312
|
+
return matches.sort().join(" ");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Compute a short hash of the allowed tools for session naming.
|
|
317
|
+
* @param {string | null | undefined} allowedTools
|
|
318
|
+
* @returns {string | null}
|
|
319
|
+
*/
|
|
320
|
+
function computePermissionHash(allowedTools) {
|
|
321
|
+
if (!allowedTools) return null;
|
|
322
|
+
const normalized = normalizeAllowedTools(allowedTools);
|
|
323
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
281
324
|
}
|
|
282
325
|
|
|
283
326
|
// =============================================================================
|
|
@@ -374,9 +417,9 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
374
417
|
// =============================================================================
|
|
375
418
|
|
|
376
419
|
/**
|
|
377
|
-
* @returns {number | null}
|
|
420
|
+
* @returns {{pid: number, agent: 'claude' | 'codex'} | null}
|
|
378
421
|
*/
|
|
379
|
-
function
|
|
422
|
+
function findCallerAgent() {
|
|
380
423
|
let pid = process.ppid;
|
|
381
424
|
while (pid > 1) {
|
|
382
425
|
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
@@ -386,9 +429,8 @@ function findCallerPid() {
|
|
|
386
429
|
const parts = result.stdout.trim().split(/\s+/);
|
|
387
430
|
const ppid = parseInt(parts[0], 10);
|
|
388
431
|
const cmd = parts.slice(1).join(" ");
|
|
389
|
-
if (cmd.includes("claude")
|
|
390
|
-
|
|
391
|
-
}
|
|
432
|
+
if (cmd.includes("claude")) return { pid, agent: "claude" };
|
|
433
|
+
if (cmd.includes("codex")) return { pid, agent: "codex" };
|
|
392
434
|
pid = ppid;
|
|
393
435
|
}
|
|
394
436
|
return null;
|
|
@@ -486,6 +528,7 @@ async function readStdinIfNeeded(value) {
|
|
|
486
528
|
* @property {boolean} all
|
|
487
529
|
* @property {boolean} orphans
|
|
488
530
|
* @property {boolean} force
|
|
531
|
+
* @property {boolean} stale
|
|
489
532
|
* @property {boolean} version
|
|
490
533
|
* @property {boolean} help
|
|
491
534
|
* @property {string} [tool]
|
|
@@ -495,6 +538,7 @@ async function readStdinIfNeeded(value) {
|
|
|
495
538
|
* @property {number} [limit]
|
|
496
539
|
* @property {string} [branch]
|
|
497
540
|
* @property {string} [archangels]
|
|
541
|
+
* @property {string} [autoApprove]
|
|
498
542
|
*/
|
|
499
543
|
function parseCliArgs(args) {
|
|
500
544
|
const { values, positionals } = parseArgs({
|
|
@@ -510,10 +554,12 @@ function parseCliArgs(args) {
|
|
|
510
554
|
all: { type: "boolean", default: false },
|
|
511
555
|
orphans: { type: "boolean", default: false },
|
|
512
556
|
force: { type: "boolean", default: false },
|
|
557
|
+
stale: { type: "boolean", default: false },
|
|
513
558
|
version: { type: "boolean", short: "V", default: false },
|
|
514
559
|
help: { type: "boolean", short: "h", default: false },
|
|
515
560
|
// Value flags
|
|
516
561
|
tool: { type: "string" },
|
|
562
|
+
"auto-approve": { type: "string" },
|
|
517
563
|
session: { type: "string" },
|
|
518
564
|
timeout: { type: "string" },
|
|
519
565
|
tail: { type: "string" },
|
|
@@ -536,6 +582,7 @@ function parseCliArgs(args) {
|
|
|
536
582
|
all: Boolean(values.all),
|
|
537
583
|
orphans: Boolean(values.orphans),
|
|
538
584
|
force: Boolean(values.force),
|
|
585
|
+
stale: Boolean(values.stale),
|
|
539
586
|
version: Boolean(values.version),
|
|
540
587
|
help: Boolean(values.help),
|
|
541
588
|
tool: /** @type {string | undefined} */ (values.tool),
|
|
@@ -545,6 +592,7 @@ function parseCliArgs(args) {
|
|
|
545
592
|
limit: values.limit !== undefined ? Number(values.limit) : undefined,
|
|
546
593
|
branch: /** @type {string | undefined} */ (values.branch),
|
|
547
594
|
archangels: /** @type {string | undefined} */ (values.archangels),
|
|
595
|
+
autoApprove: /** @type {string | undefined} */ (values["auto-approve"]),
|
|
548
596
|
},
|
|
549
597
|
positionals,
|
|
550
598
|
};
|
|
@@ -553,6 +601,10 @@ function parseCliArgs(args) {
|
|
|
553
601
|
// Helpers - session tracking
|
|
554
602
|
// =============================================================================
|
|
555
603
|
|
|
604
|
+
// Regex pattern strings for session name parsing
|
|
605
|
+
const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
606
|
+
const PERM_HASH_PATTERN = "[0-9a-f]{8}";
|
|
607
|
+
|
|
556
608
|
/**
|
|
557
609
|
* @param {string} session
|
|
558
610
|
* @returns {ParsedSession | null}
|
|
@@ -565,19 +617,27 @@ function parseSessionName(session) {
|
|
|
565
617
|
const rest = match[2];
|
|
566
618
|
|
|
567
619
|
// Archangel: {tool}-archangel-{name}-{uuid}
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
);
|
|
620
|
+
const archangelPattern = new RegExp(`^archangel-(.+)-(${UUID_PATTERN})$`, "i");
|
|
621
|
+
const archangelMatch = rest.match(archangelPattern);
|
|
571
622
|
if (archangelMatch) {
|
|
572
623
|
return { tool, archangelName: archangelMatch[1], uuid: archangelMatch[2] };
|
|
573
624
|
}
|
|
574
625
|
|
|
575
|
-
// Partner: {tool}-partner-{uuid}
|
|
576
|
-
const
|
|
577
|
-
|
|
626
|
+
// Partner: {tool}-partner-{uuid}[-p{hash}|-yolo]
|
|
627
|
+
const partnerPattern = new RegExp(
|
|
628
|
+
`^partner-(${UUID_PATTERN})(?:-p(${PERM_HASH_PATTERN})|-(yolo))?$`,
|
|
629
|
+
"i",
|
|
578
630
|
);
|
|
631
|
+
const partnerMatch = rest.match(partnerPattern);
|
|
579
632
|
if (partnerMatch) {
|
|
580
|
-
|
|
633
|
+
const result = { tool, uuid: partnerMatch[1] };
|
|
634
|
+
if (partnerMatch[2]) {
|
|
635
|
+
return { ...result, permissionHash: partnerMatch[2] };
|
|
636
|
+
}
|
|
637
|
+
if (partnerMatch[3]) {
|
|
638
|
+
return { ...result, yolo: true };
|
|
639
|
+
}
|
|
640
|
+
return result;
|
|
581
641
|
}
|
|
582
642
|
|
|
583
643
|
// Anything else
|
|
@@ -586,10 +646,19 @@ function parseSessionName(session) {
|
|
|
586
646
|
|
|
587
647
|
/**
|
|
588
648
|
* @param {string} tool
|
|
649
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
589
650
|
* @returns {string}
|
|
590
651
|
*/
|
|
591
|
-
function generateSessionName(tool) {
|
|
592
|
-
|
|
652
|
+
function generateSessionName(tool, { allowedTools = null, yolo = false } = {}) {
|
|
653
|
+
const uuid = randomUUID();
|
|
654
|
+
if (yolo) {
|
|
655
|
+
return `${tool}-partner-${uuid}-yolo`;
|
|
656
|
+
}
|
|
657
|
+
const hash = computePermissionHash(allowedTools);
|
|
658
|
+
if (hash) {
|
|
659
|
+
return `${tool}-partner-${uuid}-p${hash}`;
|
|
660
|
+
}
|
|
661
|
+
return `${tool}-partner-${uuid}`;
|
|
593
662
|
}
|
|
594
663
|
|
|
595
664
|
/**
|
|
@@ -1491,8 +1560,8 @@ function findCurrentClaudeSession() {
|
|
|
1491
1560
|
|
|
1492
1561
|
// We might be running from Claude but not inside tmux (e.g., VSCode, Cursor)
|
|
1493
1562
|
// Find Claude sessions in the same cwd and pick the most recently active one
|
|
1494
|
-
const
|
|
1495
|
-
if (!
|
|
1563
|
+
const caller = findCallerAgent();
|
|
1564
|
+
if (!caller) return null;
|
|
1496
1565
|
|
|
1497
1566
|
const cwd = process.cwd();
|
|
1498
1567
|
const sessions = tmuxListSessions();
|
|
@@ -1906,6 +1975,7 @@ const State = {
|
|
|
1906
1975
|
THINKING: "thinking",
|
|
1907
1976
|
CONFIRMING: "confirming",
|
|
1908
1977
|
RATE_LIMITED: "rate_limited",
|
|
1978
|
+
FEEDBACK_MODAL: "feedback_modal",
|
|
1909
1979
|
};
|
|
1910
1980
|
|
|
1911
1981
|
/**
|
|
@@ -1933,6 +2003,17 @@ function detectState(screen, config) {
|
|
|
1933
2003
|
return State.RATE_LIMITED;
|
|
1934
2004
|
}
|
|
1935
2005
|
|
|
2006
|
+
// Feedback modal - Claude CLI's "How is Claude doing this session?" prompt
|
|
2007
|
+
// Match the numbered options pattern (flexible on whitespace)
|
|
2008
|
+
if (
|
|
2009
|
+
/1:\s*Bad/i.test(recentLines) &&
|
|
2010
|
+
/2:\s*Fine/i.test(recentLines) &&
|
|
2011
|
+
/3:\s*Good/i.test(recentLines) &&
|
|
2012
|
+
/0:\s*Dismiss/i.test(recentLines)
|
|
2013
|
+
) {
|
|
2014
|
+
return State.FEEDBACK_MODAL;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1936
2017
|
// Confirming - check before THINKING because "Running…" in tool output matches thinking patterns
|
|
1937
2018
|
const confirmPatterns = config.confirmPatterns || [];
|
|
1938
2019
|
for (const pattern of confirmPatterns) {
|
|
@@ -2074,12 +2155,18 @@ class Agent {
|
|
|
2074
2155
|
/**
|
|
2075
2156
|
* @param {boolean} [yolo]
|
|
2076
2157
|
* @param {string | null} [sessionName]
|
|
2158
|
+
* @param {string | null} [customAllowedTools]
|
|
2077
2159
|
* @returns {string}
|
|
2078
2160
|
*/
|
|
2079
|
-
getCommand(yolo, sessionName = null) {
|
|
2161
|
+
getCommand(yolo, sessionName = null, customAllowedTools = null) {
|
|
2080
2162
|
let base;
|
|
2081
2163
|
if (yolo) {
|
|
2082
2164
|
base = this.yoloCommand;
|
|
2165
|
+
} else if (customAllowedTools) {
|
|
2166
|
+
// Custom permissions from --auto-approve flag
|
|
2167
|
+
// Escape quotes for shell since tmux runs the command through a shell
|
|
2168
|
+
const escaped = customAllowedTools.replace(/"/g, '\\"');
|
|
2169
|
+
base = `${this.startCommand} --allowedTools "${escaped}"`;
|
|
2083
2170
|
} else if (this.safeAllowedTools) {
|
|
2084
2171
|
// Default: auto-approve safe read-only operations
|
|
2085
2172
|
base = `${this.startCommand} --allowedTools "${this.safeAllowedTools}"`;
|
|
@@ -2088,43 +2175,93 @@ class Agent {
|
|
|
2088
2175
|
}
|
|
2089
2176
|
// Some agents support session ID flags for deterministic session tracking
|
|
2090
2177
|
if (this.sessionIdFlag && sessionName) {
|
|
2091
|
-
|
|
2178
|
+
const parsed = parseSessionName(sessionName);
|
|
2179
|
+
if (parsed?.uuid) {
|
|
2180
|
+
return `${base} ${this.sessionIdFlag} ${parsed.uuid}`;
|
|
2181
|
+
}
|
|
2092
2182
|
}
|
|
2093
2183
|
return base;
|
|
2094
2184
|
}
|
|
2095
2185
|
|
|
2096
|
-
|
|
2186
|
+
/**
|
|
2187
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
2188
|
+
* @returns {string | null}
|
|
2189
|
+
*/
|
|
2190
|
+
getDefaultSession({ allowedTools = null, yolo = false } = {}) {
|
|
2097
2191
|
// Check env var for explicit session
|
|
2098
2192
|
if (this.envVar && process.env[this.envVar]) {
|
|
2099
|
-
return process.env[this.envVar];
|
|
2193
|
+
return process.env[this.envVar] ?? null;
|
|
2100
2194
|
}
|
|
2101
2195
|
|
|
2102
2196
|
const cwd = process.cwd();
|
|
2103
|
-
|
|
2197
|
+
// Match sessions: {tool}-(partner-)?{uuid}[-p{hash}|-yolo]?
|
|
2198
|
+
const childPattern = new RegExp(
|
|
2199
|
+
`^${this.name}-(partner-)?${UUID_PATTERN}(-p${PERM_HASH_PATTERN}|-yolo)?$`,
|
|
2200
|
+
"i",
|
|
2201
|
+
);
|
|
2202
|
+
const requestedHash = computePermissionHash(allowedTools);
|
|
2203
|
+
|
|
2204
|
+
/**
|
|
2205
|
+
* Find a matching session by walking up the directory tree.
|
|
2206
|
+
* Checks exact cwd first, then parent directories up to git root or home.
|
|
2207
|
+
* @param {string[]} sessions
|
|
2208
|
+
* @returns {string | null}
|
|
2209
|
+
*/
|
|
2210
|
+
const findSessionInCwdOrParent = (sessions) => {
|
|
2211
|
+
const matchingSessions = sessions.filter((s) => {
|
|
2212
|
+
if (!childPattern.test(s)) return false;
|
|
2213
|
+
|
|
2214
|
+
const perms = getSessionPermissions(s);
|
|
2215
|
+
|
|
2216
|
+
// If yolo requested, only match yolo sessions
|
|
2217
|
+
if (yolo) {
|
|
2218
|
+
return perms.mode === "yolo";
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// If custom permissions requested, match yolo (superset) or same hash
|
|
2222
|
+
if (requestedHash) {
|
|
2223
|
+
return perms.mode === "yolo" || perms.hash === requestedHash;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// If no special permissions, match safe sessions only
|
|
2227
|
+
return perms.mode === "safe";
|
|
2228
|
+
});
|
|
2229
|
+
if (matchingSessions.length === 0) return null;
|
|
2104
2230
|
|
|
2105
|
-
|
|
2231
|
+
// Cache session cwds to avoid repeated tmux calls
|
|
2232
|
+
const sessionCwds = new Map(matchingSessions.map((s) => [s, getTmuxSessionCwd(s)]));
|
|
2233
|
+
|
|
2234
|
+
let searchDir = cwd;
|
|
2235
|
+
const homeDir = os.homedir();
|
|
2236
|
+
|
|
2237
|
+
while (searchDir !== homeDir && searchDir !== "/") {
|
|
2238
|
+
const existing = matchingSessions.find((s) => sessionCwds.get(s) === searchDir);
|
|
2239
|
+
if (existing) return existing;
|
|
2240
|
+
|
|
2241
|
+
// Stop at git root (don't leak across projects)
|
|
2242
|
+
if (existsSync(path.join(searchDir, ".git"))) break;
|
|
2243
|
+
|
|
2244
|
+
searchDir = path.dirname(searchDir);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
return null;
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
// If inside tmux, look for existing agent session in cwd or parent
|
|
2106
2251
|
const current = tmuxCurrentSession();
|
|
2107
2252
|
if (current) {
|
|
2108
2253
|
const sessions = tmuxListSessions();
|
|
2109
|
-
const existing = sessions
|
|
2110
|
-
if (!childPattern.test(s)) return false;
|
|
2111
|
-
const sessionCwd = getTmuxSessionCwd(s);
|
|
2112
|
-
return sessionCwd === cwd;
|
|
2113
|
-
});
|
|
2254
|
+
const existing = findSessionInCwdOrParent(sessions);
|
|
2114
2255
|
if (existing) return existing;
|
|
2115
|
-
// No existing session in this cwd - will generate new one in cmdStart
|
|
2256
|
+
// No existing session in this cwd or parent - will generate new one in cmdStart
|
|
2116
2257
|
return null;
|
|
2117
2258
|
}
|
|
2118
2259
|
|
|
2119
|
-
// Walk up to find claude/codex ancestor and reuse its session
|
|
2120
|
-
const
|
|
2121
|
-
if (
|
|
2260
|
+
// Walk up to find claude/codex ancestor and reuse its session
|
|
2261
|
+
const caller = findCallerAgent();
|
|
2262
|
+
if (caller) {
|
|
2122
2263
|
const sessions = tmuxListSessions();
|
|
2123
|
-
const existing = sessions
|
|
2124
|
-
if (!childPattern.test(s)) return false;
|
|
2125
|
-
const sessionCwd = getTmuxSessionCwd(s);
|
|
2126
|
-
return sessionCwd === cwd;
|
|
2127
|
-
});
|
|
2264
|
+
const existing = findSessionInCwdOrParent(sessions);
|
|
2128
2265
|
if (existing) return existing;
|
|
2129
2266
|
}
|
|
2130
2267
|
|
|
@@ -2133,10 +2270,11 @@ class Agent {
|
|
|
2133
2270
|
}
|
|
2134
2271
|
|
|
2135
2272
|
/**
|
|
2273
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
2136
2274
|
* @returns {string}
|
|
2137
2275
|
*/
|
|
2138
|
-
generateSession() {
|
|
2139
|
-
return generateSessionName(this.name);
|
|
2276
|
+
generateSession(options = {}) {
|
|
2277
|
+
return generateSessionName(this.name, options);
|
|
2140
2278
|
}
|
|
2141
2279
|
|
|
2142
2280
|
/**
|
|
@@ -2462,8 +2600,12 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2462
2600
|
const initialScreen = tmuxCapture(session);
|
|
2463
2601
|
const initialState = agent.getState(initialScreen);
|
|
2464
2602
|
|
|
2465
|
-
//
|
|
2466
|
-
if (
|
|
2603
|
+
// Dismiss feedback modal if present
|
|
2604
|
+
if (initialState === State.FEEDBACK_MODAL) {
|
|
2605
|
+
tmuxSend(session, "0");
|
|
2606
|
+
await sleep(200);
|
|
2607
|
+
} else if (
|
|
2608
|
+
// Already in terminal state
|
|
2467
2609
|
initialState === State.RATE_LIMITED ||
|
|
2468
2610
|
initialState === State.CONFIRMING ||
|
|
2469
2611
|
initialState === State.READY
|
|
@@ -2476,6 +2618,13 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2476
2618
|
const screen = tmuxCapture(session);
|
|
2477
2619
|
const state = agent.getState(screen);
|
|
2478
2620
|
|
|
2621
|
+
// Dismiss feedback modal if it appears
|
|
2622
|
+
if (state === State.FEEDBACK_MODAL) {
|
|
2623
|
+
tmuxSend(session, "0");
|
|
2624
|
+
await sleep(200);
|
|
2625
|
+
continue;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2479
2628
|
if (state === State.RATE_LIMITED || state === State.CONFIRMING || state === State.READY) {
|
|
2480
2629
|
return { state, screen };
|
|
2481
2630
|
}
|
|
@@ -2516,6 +2665,13 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
|
2516
2665
|
return { state, screen };
|
|
2517
2666
|
}
|
|
2518
2667
|
|
|
2668
|
+
// Dismiss feedback modal if it appears
|
|
2669
|
+
if (state === State.FEEDBACK_MODAL) {
|
|
2670
|
+
tmuxSend(session, "0");
|
|
2671
|
+
await sleep(200);
|
|
2672
|
+
continue;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2519
2675
|
if (screen !== lastScreen) {
|
|
2520
2676
|
lastScreen = screen;
|
|
2521
2677
|
stableAt = Date.now();
|
|
@@ -2625,6 +2781,7 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2625
2781
|
continue;
|
|
2626
2782
|
}
|
|
2627
2783
|
|
|
2784
|
+
// FEEDBACK_MODAL is handled by the underlying waitFn (pollForResponse)
|
|
2628
2785
|
debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
|
|
2629
2786
|
}
|
|
2630
2787
|
|
|
@@ -2636,12 +2793,13 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2636
2793
|
* @param {string | null | undefined} session
|
|
2637
2794
|
* @param {Object} [options]
|
|
2638
2795
|
* @param {boolean} [options.yolo]
|
|
2796
|
+
* @param {string | null} [options.allowedTools]
|
|
2639
2797
|
* @returns {Promise<string>}
|
|
2640
2798
|
*/
|
|
2641
|
-
async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
2799
|
+
async function cmdStart(agent, session, { yolo = false, allowedTools = null } = {}) {
|
|
2642
2800
|
// Generate session name if not provided
|
|
2643
2801
|
if (!session) {
|
|
2644
|
-
session = agent.generateSession();
|
|
2802
|
+
session = agent.generateSession({ allowedTools, yolo });
|
|
2645
2803
|
}
|
|
2646
2804
|
|
|
2647
2805
|
if (tmuxHasSession(session)) return session;
|
|
@@ -2653,7 +2811,7 @@ async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
|
2653
2811
|
process.exit(1);
|
|
2654
2812
|
}
|
|
2655
2813
|
|
|
2656
|
-
const command = agent.getCommand(yolo, session);
|
|
2814
|
+
const command = agent.getCommand(yolo, session, allowedTools);
|
|
2657
2815
|
tmuxNewSession(session, command);
|
|
2658
2816
|
|
|
2659
2817
|
const start = Date.now();
|
|
@@ -2666,6 +2824,18 @@ async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
|
2666
2824
|
continue;
|
|
2667
2825
|
}
|
|
2668
2826
|
|
|
2827
|
+
if (state === State.FEEDBACK_MODAL) {
|
|
2828
|
+
tmuxSend(session, "0");
|
|
2829
|
+
await sleep(200);
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
if (state === State.CONFIRMING) {
|
|
2834
|
+
tmuxSend(session, agent.approveKey);
|
|
2835
|
+
await sleep(APPROVE_DELAY_MS);
|
|
2836
|
+
continue;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2669
2839
|
if (state === State.READY) return session;
|
|
2670
2840
|
|
|
2671
2841
|
await sleep(POLL_MS);
|
|
@@ -3271,6 +3441,7 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
|
3271
3441
|
import { dirname, join } from "node:path";
|
|
3272
3442
|
import { fileURLToPath } from "node:url";
|
|
3273
3443
|
import { createHash } from "node:crypto";
|
|
3444
|
+
import { execSync } from "node:child_process";
|
|
3274
3445
|
|
|
3275
3446
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3276
3447
|
const AI_DIR = join(__dirname, "..");
|
|
@@ -3278,6 +3449,15 @@ const DEBUG = process.env.AX_DEBUG === "1";
|
|
|
3278
3449
|
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
3279
3450
|
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
3280
3451
|
|
|
3452
|
+
function getTmuxSessionName() {
|
|
3453
|
+
if (!process.env.TMUX) return null;
|
|
3454
|
+
try {
|
|
3455
|
+
return execSync("tmux display-message -p '#S'", { encoding: "utf-8" }).trim();
|
|
3456
|
+
} catch {
|
|
3457
|
+
return null;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3281
3461
|
// Read hook input from stdin
|
|
3282
3462
|
let hookInput = {};
|
|
3283
3463
|
try {
|
|
@@ -3292,8 +3472,9 @@ const hookEvent = hookInput.hook_event_name || "";
|
|
|
3292
3472
|
|
|
3293
3473
|
if (DEBUG) console.error("[hook] session:", sessionId, "event:", hookEvent);
|
|
3294
3474
|
|
|
3295
|
-
|
|
3296
|
-
if (
|
|
3475
|
+
const tmuxSession = getTmuxSessionName();
|
|
3476
|
+
if (DEBUG) console.error("[hook] tmux session:", tmuxSession);
|
|
3477
|
+
if (tmuxSession && (tmuxSession.includes("-archangel-") || tmuxSession.includes("-partner-"))) {
|
|
3297
3478
|
if (DEBUG) console.error("[hook] skipping non-parent session");
|
|
3298
3479
|
process.exit(0);
|
|
3299
3480
|
}
|
|
@@ -3941,12 +4122,29 @@ async function cmdRfp(prompt, { archangels, fresh = false, noWait = false } = {}
|
|
|
3941
4122
|
|
|
3942
4123
|
writeRfpRecord(rfpId, prompt);
|
|
3943
4124
|
const archangelList = requested.join(",");
|
|
4125
|
+
const base = rfpId.split("-")[1];
|
|
4126
|
+
const shortId = `rfp-${base}`;
|
|
3944
4127
|
console.log(`rfp: ${rfpId} (${archangelList})`);
|
|
3945
4128
|
if (noWait) {
|
|
3946
|
-
|
|
3947
|
-
const
|
|
3948
|
-
const
|
|
3949
|
-
|
|
4129
|
+
// Truncate prompt for display (first line, max 60 chars)
|
|
4130
|
+
const firstLine = prompt.split("\n")[0];
|
|
4131
|
+
const taskPreview = firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
|
|
4132
|
+
|
|
4133
|
+
let output = `Task: ${taskPreview}
|
|
4134
|
+
|
|
4135
|
+
Add this to your task list, then continue with your work.`;
|
|
4136
|
+
|
|
4137
|
+
// Show TaskCreate example if caller is Claude
|
|
4138
|
+
const caller = findCallerAgent();
|
|
4139
|
+
if (caller?.agent === "claude") {
|
|
4140
|
+
output += `
|
|
4141
|
+
|
|
4142
|
+
e.g.
|
|
4143
|
+
TaskCreate({ subject: "ax:${shortId} - ${taskPreview}" })
|
|
4144
|
+
`;
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
console.log(output);
|
|
3950
4148
|
}
|
|
3951
4149
|
}
|
|
3952
4150
|
|
|
@@ -4058,13 +4256,13 @@ async function cmdRfpWait(rfpId, { archangels, timeoutMs = ARCHANGEL_RESPONSE_TI
|
|
|
4058
4256
|
* @param {Agent} agent
|
|
4059
4257
|
* @param {string | null | undefined} session
|
|
4060
4258
|
* @param {string} message
|
|
4061
|
-
* @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
|
|
4259
|
+
* @param {{noWait?: boolean, yolo?: boolean, allowedTools?: string | null, timeoutMs?: number}} [options]
|
|
4062
4260
|
*/
|
|
4063
4261
|
async function cmdAsk(
|
|
4064
4262
|
agent,
|
|
4065
4263
|
session,
|
|
4066
4264
|
message,
|
|
4067
|
-
{ noWait = false, yolo = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
4265
|
+
{ noWait = false, yolo = false, allowedTools = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
4068
4266
|
) {
|
|
4069
4267
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
4070
4268
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
@@ -4079,7 +4277,7 @@ async function cmdAsk(
|
|
|
4079
4277
|
/** @type {string} */
|
|
4080
4278
|
const activeSession = sessionExists
|
|
4081
4279
|
? /** @type {string} */ (session)
|
|
4082
|
-
: await cmdStart(agent, session, { yolo });
|
|
4280
|
+
: await cmdStart(agent, session, { yolo, allowedTools });
|
|
4083
4281
|
|
|
4084
4282
|
tmuxSendLiteral(activeSession, message);
|
|
4085
4283
|
await sleep(50);
|
|
@@ -4088,12 +4286,26 @@ async function cmdAsk(
|
|
|
4088
4286
|
if (noWait) {
|
|
4089
4287
|
const parsed = parseSessionName(activeSession);
|
|
4090
4288
|
const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
|
|
4091
|
-
|
|
4092
|
-
|
|
4289
|
+
// Truncate message for display (first line, max 60 chars)
|
|
4290
|
+
const firstLine = message.split("\n")[0];
|
|
4291
|
+
const taskPreview = firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
|
|
4292
|
+
|
|
4293
|
+
let output = `Sent to: ${shortId}
|
|
4294
|
+
Task: ${taskPreview}
|
|
4295
|
+
|
|
4296
|
+
Add this to your task list, then continue with your work.`;
|
|
4297
|
+
|
|
4298
|
+
// Show TaskCreate example if caller is Claude
|
|
4299
|
+
const caller = findCallerAgent();
|
|
4300
|
+
if (caller?.agent === "claude") {
|
|
4301
|
+
output += `
|
|
4093
4302
|
|
|
4094
4303
|
e.g.
|
|
4095
|
-
|
|
4096
|
-
|
|
4304
|
+
TaskCreate({ subject: "ax:${shortId} - ${taskPreview}" })
|
|
4305
|
+
`;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
console.log(output);
|
|
4097
4309
|
return;
|
|
4098
4310
|
}
|
|
4099
4311
|
|
|
@@ -4285,9 +4497,14 @@ async function cmdReview(
|
|
|
4285
4497
|
* @param {Agent} agent
|
|
4286
4498
|
* @param {string | null | undefined} session
|
|
4287
4499
|
* @param {number} [index]
|
|
4288
|
-
* @param {{wait?: boolean, timeoutMs?: number}} [options]
|
|
4500
|
+
* @param {{wait?: boolean, stale?: boolean, timeoutMs?: number}} [options]
|
|
4289
4501
|
*/
|
|
4290
|
-
async function cmdOutput(
|
|
4502
|
+
async function cmdOutput(
|
|
4503
|
+
agent,
|
|
4504
|
+
session,
|
|
4505
|
+
index = 0,
|
|
4506
|
+
{ wait = false, stale = false, timeoutMs } = {},
|
|
4507
|
+
) {
|
|
4291
4508
|
if (!session || !tmuxHasSession(session)) {
|
|
4292
4509
|
console.log("ERROR: no session");
|
|
4293
4510
|
process.exit(1);
|
|
@@ -4314,8 +4531,11 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
4314
4531
|
}
|
|
4315
4532
|
|
|
4316
4533
|
if (state === State.THINKING) {
|
|
4317
|
-
|
|
4318
|
-
|
|
4534
|
+
if (!stale) {
|
|
4535
|
+
console.log("THINKING: Use --wait to block, or --stale for old response.");
|
|
4536
|
+
process.exit(1);
|
|
4537
|
+
}
|
|
4538
|
+
// --stale: fall through to show previous response
|
|
4319
4539
|
}
|
|
4320
4540
|
|
|
4321
4541
|
const output = agent.getResponse(session, screen, index);
|
|
@@ -4513,7 +4733,12 @@ function resolveAgent({ toolFlag, sessionName } = {}) {
|
|
|
4513
4733
|
if (invoked === "axclaude" || invoked === "claude") return { agent: ClaudeAgent };
|
|
4514
4734
|
if (invoked === "axcodex" || invoked === "codex") return { agent: CodexAgent };
|
|
4515
4735
|
|
|
4516
|
-
// 4.
|
|
4736
|
+
// 4. Infer from parent process (running from within claude/codex)
|
|
4737
|
+
const caller = findCallerAgent();
|
|
4738
|
+
if (caller?.agent === "claude") return { agent: ClaudeAgent };
|
|
4739
|
+
if (caller?.agent === "codex") return { agent: CodexAgent };
|
|
4740
|
+
|
|
4741
|
+
// 5. AX_DEFAULT_TOOL environment variable
|
|
4517
4742
|
const defaultTool = process.env.AX_DEFAULT_TOOL;
|
|
4518
4743
|
if (defaultTool === "claude") return { agent: ClaudeAgent };
|
|
4519
4744
|
if (defaultTool === "codex" || !defaultTool) return { agent: CodexAgent };
|
|
@@ -4534,15 +4759,13 @@ function printHelp(agent, cliName) {
|
|
|
4534
4759
|
|
|
4535
4760
|
Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
4536
4761
|
|
|
4537
|
-
Messaging
|
|
4762
|
+
Messaging:
|
|
4538
4763
|
<message> Send message to ${name}
|
|
4539
4764
|
review [TYPE] Review code: pr, uncommitted, commit, custom
|
|
4540
|
-
status Exit code: ready=0 rate_limit=2 confirm=3 thinking=4
|
|
4541
|
-
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
4542
|
-
compact Summarise session to shrink context size
|
|
4543
|
-
reset Start fresh conversation
|
|
4544
4765
|
|
|
4545
4766
|
Sessions:
|
|
4767
|
+
compact Summarise session to shrink context size
|
|
4768
|
+
reset Start fresh conversation
|
|
4546
4769
|
agents List all running agents
|
|
4547
4770
|
target Show default target session for current tool
|
|
4548
4771
|
attach [SESSION] Attach to agent session interactively
|
|
@@ -4555,36 +4778,40 @@ Archangels:
|
|
|
4555
4778
|
rfp <prompt> Request proposals (--archangels=a,b)
|
|
4556
4779
|
rfp wait <id> Wait for proposals (--archangels=a,b)
|
|
4557
4780
|
|
|
4558
|
-
Recovery:
|
|
4781
|
+
Recovery/State:
|
|
4782
|
+
status Exit code: ready=0 rate_limit=2 confirm=3 thinking=4
|
|
4783
|
+
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
4559
4784
|
debug Show raw screen output and detected state
|
|
4560
4785
|
approve Approve pending action (send 'y')
|
|
4561
4786
|
reject Reject pending action (send 'n')
|
|
4562
4787
|
select N Select menu option N
|
|
4563
4788
|
send KEYS Send key sequence (e.g. "1[Enter]", "[Escape]")
|
|
4564
|
-
log SESSION
|
|
4789
|
+
log [SESSION] View conversation log (--tail=N, --follow, --reasoning)
|
|
4565
4790
|
|
|
4566
4791
|
Flags:
|
|
4567
4792
|
--tool=NAME Use specific agent (codex, claude)
|
|
4568
4793
|
--session=ID name | archangel | uuid-prefix | self
|
|
4569
4794
|
--fresh Reset conversation before review
|
|
4570
4795
|
--yolo Skip all confirmations (dangerous)
|
|
4796
|
+
--auto-approve=TOOLS Auto-approve specific tools (e.g. 'Bash("cargo *")')
|
|
4571
4797
|
--wait Wait for response (default for messages; required for approve/reject)
|
|
4572
4798
|
--no-wait Fire-and-forget: send message, print session ID, exit immediately
|
|
4573
4799
|
--timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
|
|
4574
4800
|
|
|
4575
4801
|
Examples:
|
|
4576
4802
|
${name} "explain this codebase"
|
|
4577
|
-
${name} "review the error handling"
|
|
4578
|
-
${name} "FYI: auth was refactored" --no-wait
|
|
4803
|
+
${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
|
|
4804
|
+
${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
|
|
4805
|
+
${name} --auto-approve='Bash("cargo *")' "run tests" # Session with specific permissions
|
|
4579
4806
|
${name} review uncommitted --wait
|
|
4580
|
-
${name} kill
|
|
4581
|
-
${name} kill --all
|
|
4582
|
-
${name} kill --session=NAME
|
|
4583
|
-
${name} summon
|
|
4584
|
-
${name} summon reviewer
|
|
4585
|
-
${name} recall
|
|
4586
|
-
${name} recall reviewer
|
|
4587
|
-
${name} agents
|
|
4807
|
+
${name} kill # Kill agents in current project
|
|
4808
|
+
${name} kill --all # Kill all agents across all projects
|
|
4809
|
+
${name} kill --session=NAME # Kill specific session
|
|
4810
|
+
${name} summon # Summon all archangels from .ai/agents/*.md
|
|
4811
|
+
${name} summon reviewer # Summon by name (creates config if new)
|
|
4812
|
+
${name} recall # Recall all archangels
|
|
4813
|
+
${name} recall reviewer # Recall one by name
|
|
4814
|
+
${name} agents # List all agents (shows TYPE=archangel)
|
|
4588
4815
|
|
|
4589
4816
|
Note: Reviews and complex tasks may take several minutes.
|
|
4590
4817
|
Use Bash run_in_background for long operations (not --no-wait).`);
|
|
@@ -4611,7 +4838,8 @@ async function main() {
|
|
|
4611
4838
|
}
|
|
4612
4839
|
|
|
4613
4840
|
// Extract flags into local variables for convenience
|
|
4614
|
-
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force } =
|
|
4841
|
+
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force, stale, autoApprove } =
|
|
4842
|
+
flags;
|
|
4615
4843
|
|
|
4616
4844
|
// Session resolution (must happen before agent resolution so we can infer tool from session name)
|
|
4617
4845
|
let session = null;
|
|
@@ -4639,9 +4867,9 @@ async function main() {
|
|
|
4639
4867
|
process.exit(1);
|
|
4640
4868
|
}
|
|
4641
4869
|
|
|
4642
|
-
// If no explicit session, use agent's default
|
|
4870
|
+
// If no explicit session, use agent's default (with permission filtering)
|
|
4643
4871
|
if (!session) {
|
|
4644
|
-
session = agent.getDefaultSession();
|
|
4872
|
+
session = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
4645
4873
|
}
|
|
4646
4874
|
|
|
4647
4875
|
// Timeout (convert seconds to milliseconds)
|
|
@@ -4669,7 +4897,7 @@ async function main() {
|
|
|
4669
4897
|
// Dispatch commands
|
|
4670
4898
|
if (cmd === "agents") return cmdAgents();
|
|
4671
4899
|
if (cmd === "target") {
|
|
4672
|
-
const defaultSession = agent.getDefaultSession();
|
|
4900
|
+
const defaultSession = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
4673
4901
|
if (defaultSession) {
|
|
4674
4902
|
console.log(defaultSession);
|
|
4675
4903
|
} else {
|
|
@@ -4717,7 +4945,7 @@ async function main() {
|
|
|
4717
4945
|
if (cmd === "output") {
|
|
4718
4946
|
const indexArg = positionals[1];
|
|
4719
4947
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
4720
|
-
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
4948
|
+
return cmdOutput(agent, session, index, { wait, stale, timeoutMs });
|
|
4721
4949
|
}
|
|
4722
4950
|
if (cmd === "send" && positionals.length > 1)
|
|
4723
4951
|
return cmdSend(session, positionals.slice(1).join(" "));
|
|
@@ -4747,7 +4975,12 @@ async function main() {
|
|
|
4747
4975
|
});
|
|
4748
4976
|
}
|
|
4749
4977
|
|
|
4750
|
-
return cmdAsk(agent, session, messageText, {
|
|
4978
|
+
return cmdAsk(agent, session, messageText, {
|
|
4979
|
+
noWait,
|
|
4980
|
+
yolo,
|
|
4981
|
+
allowedTools: autoApprove,
|
|
4982
|
+
timeoutMs,
|
|
4983
|
+
});
|
|
4751
4984
|
}
|
|
4752
4985
|
|
|
4753
4986
|
// Run main() only when executed directly (not when imported for testing)
|
|
@@ -4787,4 +5020,6 @@ export {
|
|
|
4787
5020
|
extractThinking,
|
|
4788
5021
|
detectState,
|
|
4789
5022
|
State,
|
|
5023
|
+
normalizeAllowedTools,
|
|
5024
|
+
computePermissionHash,
|
|
4790
5025
|
};
|