ax-agents 0.0.1-alpha.9 → 0.1.0
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 +731 -155
- 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
|
/**
|
|
@@ -75,6 +77,10 @@ const VERSION = packageJson.version;
|
|
|
75
77
|
* @property {string[]} files
|
|
76
78
|
* @property {string} [summary]
|
|
77
79
|
* @property {string} [message]
|
|
80
|
+
* @property {string} [rfpId]
|
|
81
|
+
* @property {string} [prompt]
|
|
82
|
+
* @property {string} [archangel]
|
|
83
|
+
* @property {string} [requestedBy]
|
|
78
84
|
*/
|
|
79
85
|
|
|
80
86
|
/**
|
|
@@ -158,6 +164,7 @@ const PROJECT_ROOT = findProjectRoot();
|
|
|
158
164
|
const AI_DIR = path.join(PROJECT_ROOT, ".ai");
|
|
159
165
|
const AGENTS_DIR = path.join(AI_DIR, "agents");
|
|
160
166
|
const HOOKS_DIR = path.join(AI_DIR, "hooks");
|
|
167
|
+
const RFP_DIR = path.join(AI_DIR, "rfps");
|
|
161
168
|
|
|
162
169
|
// =============================================================================
|
|
163
170
|
// Helpers - tmux
|
|
@@ -254,25 +261,66 @@ function tmuxCurrentSession() {
|
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
/**
|
|
257
|
-
*
|
|
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.
|
|
258
295
|
* @param {string} session
|
|
259
296
|
* @returns {boolean}
|
|
260
297
|
*/
|
|
261
298
|
function isYoloSession(session) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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);
|
|
276
324
|
}
|
|
277
325
|
|
|
278
326
|
// =============================================================================
|
|
@@ -307,6 +355,7 @@ const ARCHANGEL_GIT_CONTEXT_MAX_LINES = 200;
|
|
|
307
355
|
const ARCHANGEL_PARENT_CONTEXT_ENTRIES = 10;
|
|
308
356
|
const ARCHANGEL_PREAMBLE = `## Guidelines
|
|
309
357
|
|
|
358
|
+
- If you have nothing to report, you MUST respond with ONLY "EMPTY_RESPONSE".
|
|
310
359
|
- Investigate before speaking. If uncertain, read more code and trace the logic until you're confident.
|
|
311
360
|
- Explain WHY something is an issue, not just that it is.
|
|
312
361
|
- Focus on your area of expertise.
|
|
@@ -314,9 +363,23 @@ const ARCHANGEL_PREAMBLE = `## Guidelines
|
|
|
314
363
|
- Be clear. Brief is fine, but never sacrifice clarity.
|
|
315
364
|
- For critical issues, request for them to be added to the todo list.
|
|
316
365
|
- Don't repeat observations you've already made unless you have more to say or better clarity.
|
|
317
|
-
- Make judgment calls - don't ask questions
|
|
318
|
-
|
|
319
|
-
|
|
366
|
+
- Make judgment calls - don't ask questions.`;
|
|
367
|
+
const RFP_PREAMBLE = `## Guidelines
|
|
368
|
+
|
|
369
|
+
- Your only task is to propose a single idea in response to this RFP. This overrides any other goals or habits.
|
|
370
|
+
- Provide exactly one proposal.
|
|
371
|
+
- Make a persuasive case for why this is a strong idea.
|
|
372
|
+
- Think deeply before you answer; avoid first-impression responses.
|
|
373
|
+
- Aim for 3–4 clear paragraphs.
|
|
374
|
+
- Ground the idea in the actual context you were given; don’t ignore it.
|
|
375
|
+
- If you need context, read the existing project or conversation before proposing.
|
|
376
|
+
- Structure: (1) core insight/value, (2) who benefits & why now, (3) risks/tradeoffs (brief), (4) closing case.
|
|
377
|
+
- Focus on value: what improves, for whom, and why now.
|
|
378
|
+
- Do NOT review code or report bugs.
|
|
379
|
+
- Do NOT describe scope, implementation approach, or plan.
|
|
380
|
+
- You may briefly note tradeoffs, but they are not the focus.
|
|
381
|
+
- Prioritize clarity over brevity.
|
|
382
|
+
- If you have nothing to propose, respond with ONLY "EMPTY_RESPONSE".`;
|
|
320
383
|
|
|
321
384
|
/**
|
|
322
385
|
* @param {string} session
|
|
@@ -354,9 +417,9 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
354
417
|
// =============================================================================
|
|
355
418
|
|
|
356
419
|
/**
|
|
357
|
-
* @returns {number | null}
|
|
420
|
+
* @returns {{pid: number, agent: 'claude' | 'codex'} | null}
|
|
358
421
|
*/
|
|
359
|
-
function
|
|
422
|
+
function findCallerAgent() {
|
|
360
423
|
let pid = process.ppid;
|
|
361
424
|
while (pid > 1) {
|
|
362
425
|
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
@@ -366,9 +429,8 @@ function findCallerPid() {
|
|
|
366
429
|
const parts = result.stdout.trim().split(/\s+/);
|
|
367
430
|
const ppid = parseInt(parts[0], 10);
|
|
368
431
|
const cmd = parts.slice(1).join(" ");
|
|
369
|
-
if (cmd.includes("claude")
|
|
370
|
-
|
|
371
|
-
}
|
|
432
|
+
if (cmd.includes("claude")) return { pid, agent: "claude" };
|
|
433
|
+
if (cmd.includes("codex")) return { pid, agent: "codex" };
|
|
372
434
|
pid = ppid;
|
|
373
435
|
}
|
|
374
436
|
return null;
|
|
@@ -379,7 +441,9 @@ function findCallerPid() {
|
|
|
379
441
|
* @returns {{pid: string, command: string}[]}
|
|
380
442
|
*/
|
|
381
443
|
function findOrphanedProcesses() {
|
|
382
|
-
const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], {
|
|
444
|
+
const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], {
|
|
445
|
+
encoding: "utf-8",
|
|
446
|
+
});
|
|
383
447
|
|
|
384
448
|
if (result.status !== 0 || !result.stdout.trim()) {
|
|
385
449
|
return [];
|
|
@@ -433,6 +497,17 @@ async function readStdin() {
|
|
|
433
497
|
});
|
|
434
498
|
}
|
|
435
499
|
|
|
500
|
+
/**
|
|
501
|
+
* @param {string | null | undefined} value
|
|
502
|
+
* @returns {Promise<string | undefined>}
|
|
503
|
+
*/
|
|
504
|
+
async function readStdinIfNeeded(value) {
|
|
505
|
+
if (value && value !== "-") return value;
|
|
506
|
+
if (!hasStdinData()) return undefined;
|
|
507
|
+
const stdinText = await readStdin();
|
|
508
|
+
return stdinText || undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
436
511
|
// =============================================================================
|
|
437
512
|
// =============================================================================
|
|
438
513
|
// Helpers - CLI argument parsing
|
|
@@ -453,6 +528,7 @@ async function readStdin() {
|
|
|
453
528
|
* @property {boolean} all
|
|
454
529
|
* @property {boolean} orphans
|
|
455
530
|
* @property {boolean} force
|
|
531
|
+
* @property {boolean} stale
|
|
456
532
|
* @property {boolean} version
|
|
457
533
|
* @property {boolean} help
|
|
458
534
|
* @property {string} [tool]
|
|
@@ -461,6 +537,8 @@ async function readStdin() {
|
|
|
461
537
|
* @property {number} [tail]
|
|
462
538
|
* @property {number} [limit]
|
|
463
539
|
* @property {string} [branch]
|
|
540
|
+
* @property {string} [archangels]
|
|
541
|
+
* @property {string} [autoApprove]
|
|
464
542
|
*/
|
|
465
543
|
function parseCliArgs(args) {
|
|
466
544
|
const { values, positionals } = parseArgs({
|
|
@@ -476,15 +554,18 @@ function parseCliArgs(args) {
|
|
|
476
554
|
all: { type: "boolean", default: false },
|
|
477
555
|
orphans: { type: "boolean", default: false },
|
|
478
556
|
force: { type: "boolean", default: false },
|
|
557
|
+
stale: { type: "boolean", default: false },
|
|
479
558
|
version: { type: "boolean", short: "V", default: false },
|
|
480
559
|
help: { type: "boolean", short: "h", default: false },
|
|
481
560
|
// Value flags
|
|
482
561
|
tool: { type: "string" },
|
|
562
|
+
"auto-approve": { type: "string" },
|
|
483
563
|
session: { type: "string" },
|
|
484
564
|
timeout: { type: "string" },
|
|
485
565
|
tail: { type: "string" },
|
|
486
566
|
limit: { type: "string" },
|
|
487
567
|
branch: { type: "string" },
|
|
568
|
+
archangels: { type: "string" },
|
|
488
569
|
},
|
|
489
570
|
allowPositionals: true,
|
|
490
571
|
strict: false, // Don't error on unknown flags
|
|
@@ -501,6 +582,7 @@ function parseCliArgs(args) {
|
|
|
501
582
|
all: Boolean(values.all),
|
|
502
583
|
orphans: Boolean(values.orphans),
|
|
503
584
|
force: Boolean(values.force),
|
|
585
|
+
stale: Boolean(values.stale),
|
|
504
586
|
version: Boolean(values.version),
|
|
505
587
|
help: Boolean(values.help),
|
|
506
588
|
tool: /** @type {string | undefined} */ (values.tool),
|
|
@@ -509,6 +591,8 @@ function parseCliArgs(args) {
|
|
|
509
591
|
tail: values.tail !== undefined ? Number(values.tail) : undefined,
|
|
510
592
|
limit: values.limit !== undefined ? Number(values.limit) : undefined,
|
|
511
593
|
branch: /** @type {string | undefined} */ (values.branch),
|
|
594
|
+
archangels: /** @type {string | undefined} */ (values.archangels),
|
|
595
|
+
autoApprove: /** @type {string | undefined} */ (values["auto-approve"]),
|
|
512
596
|
},
|
|
513
597
|
positionals,
|
|
514
598
|
};
|
|
@@ -517,6 +601,10 @@ function parseCliArgs(args) {
|
|
|
517
601
|
// Helpers - session tracking
|
|
518
602
|
// =============================================================================
|
|
519
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
|
+
|
|
520
608
|
/**
|
|
521
609
|
* @param {string} session
|
|
522
610
|
* @returns {ParsedSession | null}
|
|
@@ -529,19 +617,27 @@ function parseSessionName(session) {
|
|
|
529
617
|
const rest = match[2];
|
|
530
618
|
|
|
531
619
|
// Archangel: {tool}-archangel-{name}-{uuid}
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
);
|
|
620
|
+
const archangelPattern = new RegExp(`^archangel-(.+)-(${UUID_PATTERN})$`, "i");
|
|
621
|
+
const archangelMatch = rest.match(archangelPattern);
|
|
535
622
|
if (archangelMatch) {
|
|
536
623
|
return { tool, archangelName: archangelMatch[1], uuid: archangelMatch[2] };
|
|
537
624
|
}
|
|
538
625
|
|
|
539
|
-
// Partner: {tool}-partner-{uuid}
|
|
540
|
-
const
|
|
541
|
-
|
|
626
|
+
// Partner: {tool}-partner-{uuid}[-p{hash}|-yolo]
|
|
627
|
+
const partnerPattern = new RegExp(
|
|
628
|
+
`^partner-(${UUID_PATTERN})(?:-p(${PERM_HASH_PATTERN})|-(yolo))?$`,
|
|
629
|
+
"i",
|
|
542
630
|
);
|
|
631
|
+
const partnerMatch = rest.match(partnerPattern);
|
|
543
632
|
if (partnerMatch) {
|
|
544
|
-
|
|
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;
|
|
545
641
|
}
|
|
546
642
|
|
|
547
643
|
// Anything else
|
|
@@ -550,10 +646,19 @@ function parseSessionName(session) {
|
|
|
550
646
|
|
|
551
647
|
/**
|
|
552
648
|
* @param {string} tool
|
|
649
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
553
650
|
* @returns {string}
|
|
554
651
|
*/
|
|
555
|
-
function generateSessionName(tool) {
|
|
556
|
-
|
|
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}`;
|
|
557
662
|
}
|
|
558
663
|
|
|
559
664
|
/**
|
|
@@ -1151,6 +1256,54 @@ function getArchangelSessionPattern(config) {
|
|
|
1151
1256
|
return `${config.tool}-archangel-${config.name}`;
|
|
1152
1257
|
}
|
|
1153
1258
|
|
|
1259
|
+
/**
|
|
1260
|
+
* @param {string} rfpId
|
|
1261
|
+
* @param {string} prompt
|
|
1262
|
+
*/
|
|
1263
|
+
function writeRfpRecord(rfpId, prompt) {
|
|
1264
|
+
ensureRfpDir();
|
|
1265
|
+
const p = path.join(RFP_DIR, `${rfpId}.md`);
|
|
1266
|
+
const block = [`### ${rfpId}`, "", prompt.trim(), ""].join("\n");
|
|
1267
|
+
writeFileSync(p, block, "utf-8");
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* @param {string} input
|
|
1272
|
+
* @returns {string}
|
|
1273
|
+
*/
|
|
1274
|
+
function resolveRfpId(input) {
|
|
1275
|
+
ensureRfpDir();
|
|
1276
|
+
if (!existsSync(RFP_DIR)) return input;
|
|
1277
|
+
const files = readdirSync(RFP_DIR).filter((f) => f.endsWith(".md"));
|
|
1278
|
+
const ids = files.map((f) => f.replace(/\.md$/, ""));
|
|
1279
|
+
const matches = ids.filter((id) => id.startsWith(input));
|
|
1280
|
+
if (matches.length === 1) return matches[0];
|
|
1281
|
+
if (matches.length > 1) {
|
|
1282
|
+
console.log("ERROR: ambiguous rfp id. Matches:");
|
|
1283
|
+
for (const m of matches) console.log(` ${m}`);
|
|
1284
|
+
process.exit(1);
|
|
1285
|
+
}
|
|
1286
|
+
return input;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* @param {ParentSession | null} parent
|
|
1291
|
+
* @returns {string}
|
|
1292
|
+
*/
|
|
1293
|
+
function generateRfpId(parent) {
|
|
1294
|
+
const now = new Date();
|
|
1295
|
+
const y = now.getFullYear();
|
|
1296
|
+
const mo = String(now.getMonth() + 1).padStart(2, "0");
|
|
1297
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
1298
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
1299
|
+
const mi = String(now.getMinutes()).padStart(2, "0");
|
|
1300
|
+
const s = String(now.getSeconds()).padStart(2, "0");
|
|
1301
|
+
const ts = `${y}-${mo}-${d}-${h}-${mi}-${s}`;
|
|
1302
|
+
const base = parent?.uuid ? parent.uuid.split("-")[0] : randomUUID().split("-")[0];
|
|
1303
|
+
const suffix = randomUUID().split("-")[0].slice(0, 4);
|
|
1304
|
+
return `rfp-${base}-${ts}-${suffix}`.toLowerCase();
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1154
1307
|
// =============================================================================
|
|
1155
1308
|
// Helpers - mailbox
|
|
1156
1309
|
// =============================================================================
|
|
@@ -1167,15 +1320,25 @@ function ensureMailboxDir() {
|
|
|
1167
1320
|
}
|
|
1168
1321
|
}
|
|
1169
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* @returns {void}
|
|
1325
|
+
*/
|
|
1326
|
+
function ensureRfpDir() {
|
|
1327
|
+
if (!existsSync(RFP_DIR)) {
|
|
1328
|
+
mkdirSync(RFP_DIR, { recursive: true });
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1170
1332
|
/**
|
|
1171
1333
|
* @param {MailboxPayload} payload
|
|
1334
|
+
* @param {string} [type]
|
|
1172
1335
|
* @returns {void}
|
|
1173
1336
|
*/
|
|
1174
|
-
function writeToMailbox(payload) {
|
|
1337
|
+
function writeToMailbox(payload, type = "observation") {
|
|
1175
1338
|
ensureMailboxDir();
|
|
1176
1339
|
const entry = {
|
|
1177
1340
|
timestamp: new Date().toISOString(),
|
|
1178
|
-
type
|
|
1341
|
+
type,
|
|
1179
1342
|
payload,
|
|
1180
1343
|
};
|
|
1181
1344
|
appendFileSync(MAILBOX_PATH, JSON.stringify(entry) + "\n");
|
|
@@ -1397,8 +1560,8 @@ function findCurrentClaudeSession() {
|
|
|
1397
1560
|
|
|
1398
1561
|
// We might be running from Claude but not inside tmux (e.g., VSCode, Cursor)
|
|
1399
1562
|
// Find Claude sessions in the same cwd and pick the most recently active one
|
|
1400
|
-
const
|
|
1401
|
-
if (!
|
|
1563
|
+
const caller = findCallerAgent();
|
|
1564
|
+
if (!caller) return null;
|
|
1402
1565
|
|
|
1403
1566
|
const cwd = process.cwd();
|
|
1404
1567
|
const sessions = tmuxListSessions();
|
|
@@ -1812,6 +1975,7 @@ const State = {
|
|
|
1812
1975
|
THINKING: "thinking",
|
|
1813
1976
|
CONFIRMING: "confirming",
|
|
1814
1977
|
RATE_LIMITED: "rate_limited",
|
|
1978
|
+
FEEDBACK_MODAL: "feedback_modal",
|
|
1815
1979
|
};
|
|
1816
1980
|
|
|
1817
1981
|
/**
|
|
@@ -1839,6 +2003,17 @@ function detectState(screen, config) {
|
|
|
1839
2003
|
return State.RATE_LIMITED;
|
|
1840
2004
|
}
|
|
1841
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
|
+
|
|
1842
2017
|
// Confirming - check before THINKING because "Running…" in tool output matches thinking patterns
|
|
1843
2018
|
const confirmPatterns = config.confirmPatterns || [];
|
|
1844
2019
|
for (const pattern of confirmPatterns) {
|
|
@@ -1980,12 +2155,18 @@ class Agent {
|
|
|
1980
2155
|
/**
|
|
1981
2156
|
* @param {boolean} [yolo]
|
|
1982
2157
|
* @param {string | null} [sessionName]
|
|
2158
|
+
* @param {string | null} [customAllowedTools]
|
|
1983
2159
|
* @returns {string}
|
|
1984
2160
|
*/
|
|
1985
|
-
getCommand(yolo, sessionName = null) {
|
|
2161
|
+
getCommand(yolo, sessionName = null, customAllowedTools = null) {
|
|
1986
2162
|
let base;
|
|
1987
2163
|
if (yolo) {
|
|
1988
2164
|
base = this.yoloCommand;
|
|
2165
|
+
} else if (customAllowedTools) {
|
|
2166
|
+
// Custom permissions from --auto-approve flag
|
|
2167
|
+
// Escape for shell: backslashes first, then double quotes
|
|
2168
|
+
const escaped = customAllowedTools.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2169
|
+
base = `${this.startCommand} --allowedTools "${escaped}"`;
|
|
1989
2170
|
} else if (this.safeAllowedTools) {
|
|
1990
2171
|
// Default: auto-approve safe read-only operations
|
|
1991
2172
|
base = `${this.startCommand} --allowedTools "${this.safeAllowedTools}"`;
|
|
@@ -2002,38 +2183,85 @@ class Agent {
|
|
|
2002
2183
|
return base;
|
|
2003
2184
|
}
|
|
2004
2185
|
|
|
2005
|
-
|
|
2186
|
+
/**
|
|
2187
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
2188
|
+
* @returns {string | null}
|
|
2189
|
+
*/
|
|
2190
|
+
getDefaultSession({ allowedTools = null, yolo = false } = {}) {
|
|
2006
2191
|
// Check env var for explicit session
|
|
2007
2192
|
if (this.envVar && process.env[this.envVar]) {
|
|
2008
|
-
return process.env[this.envVar];
|
|
2193
|
+
return process.env[this.envVar] ?? null;
|
|
2009
2194
|
}
|
|
2010
2195
|
|
|
2011
2196
|
const cwd = process.cwd();
|
|
2012
|
-
|
|
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;
|
|
2230
|
+
|
|
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;
|
|
2013
2243
|
|
|
2014
|
-
|
|
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
|
|
2015
2251
|
const current = tmuxCurrentSession();
|
|
2016
2252
|
if (current) {
|
|
2017
2253
|
const sessions = tmuxListSessions();
|
|
2018
|
-
const existing = sessions
|
|
2019
|
-
if (!childPattern.test(s)) return false;
|
|
2020
|
-
const sessionCwd = getTmuxSessionCwd(s);
|
|
2021
|
-
return sessionCwd === cwd;
|
|
2022
|
-
});
|
|
2254
|
+
const existing = findSessionInCwdOrParent(sessions);
|
|
2023
2255
|
if (existing) return existing;
|
|
2024
|
-
// 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
|
|
2025
2257
|
return null;
|
|
2026
2258
|
}
|
|
2027
2259
|
|
|
2028
|
-
// Walk up to find claude/codex ancestor and reuse its session
|
|
2029
|
-
const
|
|
2030
|
-
if (
|
|
2260
|
+
// Walk up to find claude/codex ancestor and reuse its session
|
|
2261
|
+
const caller = findCallerAgent();
|
|
2262
|
+
if (caller) {
|
|
2031
2263
|
const sessions = tmuxListSessions();
|
|
2032
|
-
const existing = sessions
|
|
2033
|
-
if (!childPattern.test(s)) return false;
|
|
2034
|
-
const sessionCwd = getTmuxSessionCwd(s);
|
|
2035
|
-
return sessionCwd === cwd;
|
|
2036
|
-
});
|
|
2264
|
+
const existing = findSessionInCwdOrParent(sessions);
|
|
2037
2265
|
if (existing) return existing;
|
|
2038
2266
|
}
|
|
2039
2267
|
|
|
@@ -2042,10 +2270,11 @@ class Agent {
|
|
|
2042
2270
|
}
|
|
2043
2271
|
|
|
2044
2272
|
/**
|
|
2273
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
2045
2274
|
* @returns {string}
|
|
2046
2275
|
*/
|
|
2047
|
-
generateSession() {
|
|
2048
|
-
return generateSessionName(this.name);
|
|
2276
|
+
generateSession(options = {}) {
|
|
2277
|
+
return generateSessionName(this.name, options);
|
|
2049
2278
|
}
|
|
2050
2279
|
|
|
2051
2280
|
/**
|
|
@@ -2371,8 +2600,12 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2371
2600
|
const initialScreen = tmuxCapture(session);
|
|
2372
2601
|
const initialState = agent.getState(initialScreen);
|
|
2373
2602
|
|
|
2374
|
-
//
|
|
2375
|
-
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
|
|
2376
2609
|
initialState === State.RATE_LIMITED ||
|
|
2377
2610
|
initialState === State.CONFIRMING ||
|
|
2378
2611
|
initialState === State.READY
|
|
@@ -2385,6 +2618,13 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2385
2618
|
const screen = tmuxCapture(session);
|
|
2386
2619
|
const state = agent.getState(screen);
|
|
2387
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
|
+
|
|
2388
2628
|
if (state === State.RATE_LIMITED || state === State.CONFIRMING || state === State.READY) {
|
|
2389
2629
|
return { state, screen };
|
|
2390
2630
|
}
|
|
@@ -2425,6 +2665,13 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
|
2425
2665
|
return { state, screen };
|
|
2426
2666
|
}
|
|
2427
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
|
+
|
|
2428
2675
|
if (screen !== lastScreen) {
|
|
2429
2676
|
lastScreen = screen;
|
|
2430
2677
|
stableAt = Date.now();
|
|
@@ -2534,6 +2781,7 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2534
2781
|
continue;
|
|
2535
2782
|
}
|
|
2536
2783
|
|
|
2784
|
+
// FEEDBACK_MODAL is handled by the underlying waitFn (pollForResponse)
|
|
2537
2785
|
debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
|
|
2538
2786
|
}
|
|
2539
2787
|
|
|
@@ -2545,12 +2793,13 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2545
2793
|
* @param {string | null | undefined} session
|
|
2546
2794
|
* @param {Object} [options]
|
|
2547
2795
|
* @param {boolean} [options.yolo]
|
|
2796
|
+
* @param {string | null} [options.allowedTools]
|
|
2548
2797
|
* @returns {Promise<string>}
|
|
2549
2798
|
*/
|
|
2550
|
-
async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
2799
|
+
async function cmdStart(agent, session, { yolo = false, allowedTools = null } = {}) {
|
|
2551
2800
|
// Generate session name if not provided
|
|
2552
2801
|
if (!session) {
|
|
2553
|
-
session = agent.generateSession();
|
|
2802
|
+
session = agent.generateSession({ allowedTools, yolo });
|
|
2554
2803
|
}
|
|
2555
2804
|
|
|
2556
2805
|
if (tmuxHasSession(session)) return session;
|
|
@@ -2562,7 +2811,7 @@ async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
|
2562
2811
|
process.exit(1);
|
|
2563
2812
|
}
|
|
2564
2813
|
|
|
2565
|
-
const command = agent.getCommand(yolo, session);
|
|
2814
|
+
const command = agent.getCommand(yolo, session, allowedTools);
|
|
2566
2815
|
tmuxNewSession(session, command);
|
|
2567
2816
|
|
|
2568
2817
|
const start = Date.now();
|
|
@@ -2575,6 +2824,18 @@ async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
|
2575
2824
|
continue;
|
|
2576
2825
|
}
|
|
2577
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
|
+
|
|
2578
2839
|
if (state === State.READY) return session;
|
|
2579
2840
|
|
|
2580
2841
|
await sleep(POLL_MS);
|
|
@@ -2622,6 +2883,7 @@ function cmdAgents() {
|
|
|
2622
2883
|
const isDefault =
|
|
2623
2884
|
(parsed.tool === "claude" && session === claudeDefault) ||
|
|
2624
2885
|
(parsed.tool === "codex" && session === codexDefault);
|
|
2886
|
+
const perms = getSessionPermissions(session);
|
|
2625
2887
|
|
|
2626
2888
|
// Get session metadata (Claude only)
|
|
2627
2889
|
const meta = getSessionMeta(session);
|
|
@@ -2632,6 +2894,7 @@ function cmdAgents() {
|
|
|
2632
2894
|
state: state || "unknown",
|
|
2633
2895
|
target: isDefault ? "*" : "",
|
|
2634
2896
|
type,
|
|
2897
|
+
mode: perms.mode,
|
|
2635
2898
|
plan: meta?.slug || "-",
|
|
2636
2899
|
branch: meta?.gitBranch || "-",
|
|
2637
2900
|
};
|
|
@@ -2643,14 +2906,15 @@ function cmdAgents() {
|
|
|
2643
2906
|
const maxState = Math.max(5, ...agents.map((a) => a.state.length));
|
|
2644
2907
|
const maxTarget = Math.max(6, ...agents.map((a) => a.target.length));
|
|
2645
2908
|
const maxType = Math.max(4, ...agents.map((a) => a.type.length));
|
|
2909
|
+
const maxMode = Math.max(4, ...agents.map((a) => a.mode.length));
|
|
2646
2910
|
const maxPlan = Math.max(4, ...agents.map((a) => a.plan.length));
|
|
2647
2911
|
|
|
2648
2912
|
console.log(
|
|
2649
|
-
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(maxTarget)} ${"TYPE".padEnd(maxType)} ${"PLAN".padEnd(maxPlan)} BRANCH`,
|
|
2913
|
+
`${"SESSION".padEnd(maxSession)} ${"TOOL".padEnd(maxTool)} ${"STATE".padEnd(maxState)} ${"TARGET".padEnd(maxTarget)} ${"TYPE".padEnd(maxType)} ${"MODE".padEnd(maxMode)} ${"PLAN".padEnd(maxPlan)} BRANCH`,
|
|
2650
2914
|
);
|
|
2651
2915
|
for (const a of agents) {
|
|
2652
2916
|
console.log(
|
|
2653
|
-
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.target.padEnd(maxTarget)} ${a.type.padEnd(maxType)} ${a.plan.padEnd(maxPlan)} ${a.branch}`,
|
|
2917
|
+
`${a.session.padEnd(maxSession)} ${a.tool.padEnd(maxTool)} ${a.state.padEnd(maxState)} ${a.target.padEnd(maxTarget)} ${a.type.padEnd(maxType)} ${a.mode.padEnd(maxMode)} ${a.plan.padEnd(maxPlan)} ${a.branch}`,
|
|
2654
2918
|
);
|
|
2655
2919
|
}
|
|
2656
2920
|
|
|
@@ -2707,11 +2971,29 @@ function startArchangel(config, parentSession = null) {
|
|
|
2707
2971
|
env,
|
|
2708
2972
|
});
|
|
2709
2973
|
child.unref();
|
|
2974
|
+
const watchingLabel = parentSession
|
|
2975
|
+
? parentSession.session || parentSession.uuid?.slice(0, 8)
|
|
2976
|
+
: null;
|
|
2710
2977
|
console.log(
|
|
2711
|
-
`Summoning: ${config.name} (pid ${child.pid})${
|
|
2978
|
+
`Summoning: ${config.name} (pid ${child.pid})${watchingLabel ? ` [watching: ${watchingLabel}]` : ""}`,
|
|
2712
2979
|
);
|
|
2713
2980
|
}
|
|
2714
2981
|
|
|
2982
|
+
/**
|
|
2983
|
+
* @param {string} pattern
|
|
2984
|
+
* @param {number} [timeoutMs]
|
|
2985
|
+
* @returns {Promise<string | undefined>}
|
|
2986
|
+
*/
|
|
2987
|
+
async function waitForArchangelSession(pattern, timeoutMs = ARCHANGEL_STARTUP_TIMEOUT_MS) {
|
|
2988
|
+
const start = Date.now();
|
|
2989
|
+
while (Date.now() - start < timeoutMs) {
|
|
2990
|
+
const session = findArchangelSession(pattern);
|
|
2991
|
+
if (session) return session;
|
|
2992
|
+
await sleep(200);
|
|
2993
|
+
}
|
|
2994
|
+
return undefined;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2715
2997
|
// =============================================================================
|
|
2716
2998
|
// Command: archangel (runs as the archangel process itself)
|
|
2717
2999
|
// =============================================================================
|
|
@@ -2959,28 +3241,18 @@ async function cmdArchangel(agentName) {
|
|
|
2959
3241
|
|
|
2960
3242
|
const cleanedResponse = agent.getResponse(sessionName, afterScreen) || "";
|
|
2961
3243
|
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
cleanedResponse.match(/^\+\d+ lines\]/) ||
|
|
2966
|
-
cleanedResponse.length < 20;
|
|
2967
|
-
|
|
2968
|
-
if (
|
|
2969
|
-
cleanedResponse &&
|
|
2970
|
-
!isGarbage &&
|
|
2971
|
-
!cleanedResponse.toLowerCase().includes("no issues found")
|
|
2972
|
-
) {
|
|
3244
|
+
const isSkippable = !cleanedResponse || cleanedResponse.trim() === "EMPTY_RESPONSE";
|
|
3245
|
+
|
|
3246
|
+
if (!isSkippable) {
|
|
2973
3247
|
writeToMailbox({
|
|
2974
3248
|
agent: /** @type {string} */ (agentName),
|
|
2975
3249
|
session: sessionName,
|
|
2976
3250
|
branch: getCurrentBranch(),
|
|
2977
3251
|
commit: getCurrentCommit(),
|
|
2978
3252
|
files,
|
|
2979
|
-
message: cleanedResponse
|
|
3253
|
+
message: cleanedResponse,
|
|
2980
3254
|
});
|
|
2981
3255
|
console.log(`[archangel:${agentName}] Wrote observation for ${files.length} file(s)`);
|
|
2982
|
-
} else if (isGarbage) {
|
|
2983
|
-
console.log(`[archangel:${agentName}] Skipped garbage response`);
|
|
2984
3256
|
}
|
|
2985
3257
|
} catch (err) {
|
|
2986
3258
|
console.error(`[archangel:${agentName}] Error:`, err instanceof Error ? err.message : err);
|
|
@@ -3172,6 +3444,7 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
|
3172
3444
|
import { dirname, join } from "node:path";
|
|
3173
3445
|
import { fileURLToPath } from "node:url";
|
|
3174
3446
|
import { createHash } from "node:crypto";
|
|
3447
|
+
import { execSync } from "node:child_process";
|
|
3175
3448
|
|
|
3176
3449
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3177
3450
|
const AI_DIR = join(__dirname, "..");
|
|
@@ -3179,6 +3452,15 @@ const DEBUG = process.env.AX_DEBUG === "1";
|
|
|
3179
3452
|
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
3180
3453
|
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
3181
3454
|
|
|
3455
|
+
function getTmuxSessionName() {
|
|
3456
|
+
if (!process.env.TMUX) return null;
|
|
3457
|
+
try {
|
|
3458
|
+
return execSync("tmux display-message -p '#S'", { encoding: "utf-8" }).trim();
|
|
3459
|
+
} catch {
|
|
3460
|
+
return null;
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3182
3464
|
// Read hook input from stdin
|
|
3183
3465
|
let hookInput = {};
|
|
3184
3466
|
try {
|
|
@@ -3193,8 +3475,9 @@ const hookEvent = hookInput.hook_event_name || "";
|
|
|
3193
3475
|
|
|
3194
3476
|
if (DEBUG) console.error("[hook] session:", sessionId, "event:", hookEvent);
|
|
3195
3477
|
|
|
3196
|
-
|
|
3197
|
-
if (
|
|
3478
|
+
const tmuxSession = getTmuxSessionName();
|
|
3479
|
+
if (DEBUG) console.error("[hook] tmux session:", tmuxSession);
|
|
3480
|
+
if (tmuxSession && (tmuxSession.includes("-archangel-") || tmuxSession.includes("-partner-"))) {
|
|
3198
3481
|
if (DEBUG) console.error("[hook] skipping non-parent session");
|
|
3199
3482
|
process.exit(0);
|
|
3200
3483
|
}
|
|
@@ -3385,7 +3668,7 @@ function cmdKill(session, { all = false, orphans = false, force = false } = {})
|
|
|
3385
3668
|
// If specific session provided, kill just that one
|
|
3386
3669
|
if (session) {
|
|
3387
3670
|
if (!tmuxHasSession(session)) {
|
|
3388
|
-
console.log("ERROR: session not found");
|
|
3671
|
+
console.log("ERROR: session not found. Run 'ax agents' to list sessions.");
|
|
3389
3672
|
process.exit(1);
|
|
3390
3673
|
}
|
|
3391
3674
|
tmuxKill(session);
|
|
@@ -3436,7 +3719,7 @@ function cmdAttach(session) {
|
|
|
3436
3719
|
// Resolve partial session name
|
|
3437
3720
|
const resolved = resolveSessionName(session);
|
|
3438
3721
|
if (!resolved || !tmuxHasSession(resolved)) {
|
|
3439
|
-
console.log("ERROR: session not found");
|
|
3722
|
+
console.log("ERROR: session not found. Run 'ax agents' to list sessions.");
|
|
3440
3723
|
process.exit(1);
|
|
3441
3724
|
}
|
|
3442
3725
|
|
|
@@ -3460,7 +3743,7 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
3460
3743
|
// Resolve partial session name
|
|
3461
3744
|
const resolved = resolveSessionName(sessionName);
|
|
3462
3745
|
if (!resolved) {
|
|
3463
|
-
console.log("ERROR: session not found");
|
|
3746
|
+
console.log("ERROR: session not found. Run 'ax agents' to list sessions.");
|
|
3464
3747
|
process.exit(1);
|
|
3465
3748
|
}
|
|
3466
3749
|
const parsed = parseSessionName(resolved);
|
|
@@ -3724,7 +4007,13 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3724
4007
|
console.log(`**Branch**: ${p.branch || "?"} @ ${p.commit || "?"}\n`);
|
|
3725
4008
|
}
|
|
3726
4009
|
|
|
3727
|
-
if (p.
|
|
4010
|
+
if (p.rfpId) {
|
|
4011
|
+
console.log(`**RFP**: ${p.rfpId}\n`);
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
if (entry.type === "proposal") {
|
|
4015
|
+
console.log(`**Proposal**: ${p.message || ""}\n`);
|
|
4016
|
+
} else if (p.message) {
|
|
3728
4017
|
console.log(`**Assistant**: ${p.message}\n`);
|
|
3729
4018
|
}
|
|
3730
4019
|
|
|
@@ -3738,13 +4027,246 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3738
4027
|
}
|
|
3739
4028
|
}
|
|
3740
4029
|
|
|
4030
|
+
/**
|
|
4031
|
+
* @param {string} rfpId
|
|
4032
|
+
* @param {string} archangel
|
|
4033
|
+
* @returns {string | null}
|
|
4034
|
+
*/
|
|
4035
|
+
function getProposalFromMailbox(rfpId, archangel) {
|
|
4036
|
+
if (!existsSync(MAILBOX_PATH)) return null;
|
|
4037
|
+
let result = null;
|
|
4038
|
+
try {
|
|
4039
|
+
const lines = readFileSync(MAILBOX_PATH, "utf-8").trim().split("\n").filter(Boolean);
|
|
4040
|
+
for (const line of lines) {
|
|
4041
|
+
try {
|
|
4042
|
+
const entry = JSON.parse(line);
|
|
4043
|
+
if (entry?.type !== "proposal") continue;
|
|
4044
|
+
const p = entry.payload || {};
|
|
4045
|
+
if (p.rfpId === rfpId && p.archangel === archangel) {
|
|
4046
|
+
result = p.message || "";
|
|
4047
|
+
}
|
|
4048
|
+
} catch {
|
|
4049
|
+
// Skip malformed lines
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
} catch (err) {
|
|
4053
|
+
debugError("getProposalFromMailbox", err);
|
|
4054
|
+
}
|
|
4055
|
+
return result;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
/**
|
|
4059
|
+
* @param {string} prompt
|
|
4060
|
+
* @param {{archangels?: string, fresh?: boolean, noWait?: boolean}} [options]
|
|
4061
|
+
*/
|
|
4062
|
+
async function cmdRfp(prompt, { archangels, fresh = false, noWait = false } = {}) {
|
|
4063
|
+
const configs = loadAgentConfigs();
|
|
4064
|
+
if (configs.length === 0) {
|
|
4065
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
4066
|
+
process.exit(1);
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
const requested = archangels
|
|
4070
|
+
? archangels
|
|
4071
|
+
.split(",")
|
|
4072
|
+
.map((s) => s.trim())
|
|
4073
|
+
.filter(Boolean)
|
|
4074
|
+
: configs.map((c) => c.name);
|
|
4075
|
+
|
|
4076
|
+
if (requested.length === 0) {
|
|
4077
|
+
console.log("ERROR: no archangels specified");
|
|
4078
|
+
process.exit(1);
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
const missing = requested.filter((name) => !configs.some((c) => c.name === name));
|
|
4082
|
+
if (missing.length > 0) {
|
|
4083
|
+
console.log(`ERROR: unknown archangel(s): ${missing.join(", ")}`);
|
|
4084
|
+
process.exit(1);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
const parent = findParentSession();
|
|
4088
|
+
const rfpId = generateRfpId(parent);
|
|
4089
|
+
|
|
4090
|
+
for (const name of requested) {
|
|
4091
|
+
const config = configs.find((c) => c.name === name);
|
|
4092
|
+
if (!config) continue;
|
|
4093
|
+
|
|
4094
|
+
const pattern = getArchangelSessionPattern(config);
|
|
4095
|
+
let session = findArchangelSession(pattern);
|
|
4096
|
+
if (!session) {
|
|
4097
|
+
startArchangel(config, parent);
|
|
4098
|
+
session = await waitForArchangelSession(pattern);
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
if (!session) {
|
|
4102
|
+
console.log(`ERROR: failed to start archangel '${name}'`);
|
|
4103
|
+
continue;
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
const { agent } = resolveAgent({ sessionName: session });
|
|
4107
|
+
|
|
4108
|
+
if (fresh) {
|
|
4109
|
+
tmuxSendLiteral(session, "/new");
|
|
4110
|
+
await sleep(50);
|
|
4111
|
+
tmuxSend(session, "Enter");
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
const ready = await waitUntilReady(agent, session, ARCHANGEL_STARTUP_TIMEOUT_MS);
|
|
4115
|
+
if (ready.state !== State.READY) {
|
|
4116
|
+
console.log(`[rfp] ${name} not ready (${ready.state}), skipping`);
|
|
4117
|
+
continue;
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
const rfpPrompt = `## RFP ${rfpId}\n\n${RFP_PREAMBLE}\n\n${prompt}\n\nReturn exactly one proposal.`;
|
|
4121
|
+
tmuxSendLiteral(session, rfpPrompt);
|
|
4122
|
+
await sleep(200);
|
|
4123
|
+
tmuxSend(session, "Enter");
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
writeRfpRecord(rfpId, prompt);
|
|
4127
|
+
const archangelList = requested.join(",");
|
|
4128
|
+
const base = rfpId.split("-")[1];
|
|
4129
|
+
const shortId = `rfp-${base}`;
|
|
4130
|
+
console.log(`rfp: ${rfpId} (${archangelList})`);
|
|
4131
|
+
if (noWait) {
|
|
4132
|
+
// Truncate prompt for display (first line, max 60 chars)
|
|
4133
|
+
const firstLine = prompt.split("\n")[0];
|
|
4134
|
+
const taskPreview = firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
|
|
4135
|
+
|
|
4136
|
+
let output = `Task: ${taskPreview}
|
|
4137
|
+
|
|
4138
|
+
Add this to your task list, then continue with your work.`;
|
|
4139
|
+
|
|
4140
|
+
// Show TaskCreate example if caller is Claude
|
|
4141
|
+
const caller = findCallerAgent();
|
|
4142
|
+
if (caller?.agent === "claude") {
|
|
4143
|
+
output += `
|
|
4144
|
+
|
|
4145
|
+
e.g.
|
|
4146
|
+
TaskCreate({ subject: "ax:${shortId} - ${taskPreview}" })
|
|
4147
|
+
`;
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
console.log(output);
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
/**
|
|
4155
|
+
* @param {string} rfpId
|
|
4156
|
+
* @param {{archangels?: string, timeoutMs?: number}} [options]
|
|
4157
|
+
*/
|
|
4158
|
+
async function cmdRfpWait(rfpId, { archangels, timeoutMs = ARCHANGEL_RESPONSE_TIMEOUT_MS } = {}) {
|
|
4159
|
+
const resolvedRfpId = resolveRfpId(rfpId);
|
|
4160
|
+
const configs = loadAgentConfigs();
|
|
4161
|
+
if (configs.length === 0) {
|
|
4162
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
4163
|
+
process.exit(1);
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
const requested = archangels
|
|
4167
|
+
? archangels
|
|
4168
|
+
.split(",")
|
|
4169
|
+
.map((s) => s.trim())
|
|
4170
|
+
.filter(Boolean)
|
|
4171
|
+
: configs.map((c) => c.name);
|
|
4172
|
+
|
|
4173
|
+
if (requested.length === 0) {
|
|
4174
|
+
console.log("ERROR: no archangels specified");
|
|
4175
|
+
process.exit(1);
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
const missing = requested.filter((name) => !configs.some((c) => c.name === name));
|
|
4179
|
+
if (missing.length > 0) {
|
|
4180
|
+
console.log(`ERROR: unknown archangel(s): ${missing.join(", ")}`);
|
|
4181
|
+
process.exit(1);
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
let wroteAny = false;
|
|
4185
|
+
let printedAny = false;
|
|
4186
|
+
|
|
4187
|
+
for (const name of requested) {
|
|
4188
|
+
const config = configs.find((c) => c.name === name);
|
|
4189
|
+
if (!config) continue;
|
|
4190
|
+
|
|
4191
|
+
const pattern = getArchangelSessionPattern(config);
|
|
4192
|
+
const session = findArchangelSession(pattern);
|
|
4193
|
+
if (!session) {
|
|
4194
|
+
console.log(`[rfp] ${name} session not found, skipping`);
|
|
4195
|
+
continue;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
const existing = getProposalFromMailbox(resolvedRfpId, name);
|
|
4199
|
+
if (existing !== null) {
|
|
4200
|
+
if (printedAny) console.log("");
|
|
4201
|
+
console.log(`[${name}]`);
|
|
4202
|
+
console.log(existing);
|
|
4203
|
+
wroteAny = true;
|
|
4204
|
+
printedAny = true;
|
|
4205
|
+
continue;
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
const { agent } = resolveAgent({ sessionName: session });
|
|
4209
|
+
let result;
|
|
4210
|
+
try {
|
|
4211
|
+
result = await waitUntilReady(agent, session, timeoutMs);
|
|
4212
|
+
} catch (err) {
|
|
4213
|
+
if (err instanceof TimeoutError) {
|
|
4214
|
+
console.log(`[rfp] ${name} timed out`);
|
|
4215
|
+
} else {
|
|
4216
|
+
console.log(`[rfp] ${name} error: ${err instanceof Error ? err.message : err}`);
|
|
4217
|
+
}
|
|
4218
|
+
continue;
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
if (result.state === State.RATE_LIMITED) {
|
|
4222
|
+
console.log(`[rfp] ${name} rate limited`);
|
|
4223
|
+
continue;
|
|
4224
|
+
}
|
|
4225
|
+
if (result.state === State.CONFIRMING) {
|
|
4226
|
+
console.log(`[rfp] ${name} awaiting confirmation`);
|
|
4227
|
+
continue;
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
const response = agent.getResponse(session, result.screen) || "";
|
|
4231
|
+
if (!response || response.trim() === "EMPTY_RESPONSE") {
|
|
4232
|
+
continue;
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
writeToMailbox(
|
|
4236
|
+
{
|
|
4237
|
+
agent: name,
|
|
4238
|
+
session,
|
|
4239
|
+
branch: getCurrentBranch(),
|
|
4240
|
+
commit: getCurrentCommit(),
|
|
4241
|
+
files: [],
|
|
4242
|
+
message: response,
|
|
4243
|
+
rfpId: resolvedRfpId,
|
|
4244
|
+
archangel: name,
|
|
4245
|
+
},
|
|
4246
|
+
"proposal",
|
|
4247
|
+
);
|
|
4248
|
+
if (printedAny) console.log("");
|
|
4249
|
+
console.log(`[${name}]`);
|
|
4250
|
+
console.log(response);
|
|
4251
|
+
wroteAny = true;
|
|
4252
|
+
printedAny = true;
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
if (!wroteAny) process.exit(1);
|
|
4256
|
+
}
|
|
4257
|
+
|
|
3741
4258
|
/**
|
|
3742
4259
|
* @param {Agent} agent
|
|
3743
4260
|
* @param {string | null | undefined} session
|
|
3744
4261
|
* @param {string} message
|
|
3745
|
-
* @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
|
|
4262
|
+
* @param {{noWait?: boolean, yolo?: boolean, allowedTools?: string | null, timeoutMs?: number}} [options]
|
|
3746
4263
|
*/
|
|
3747
|
-
async function cmdAsk(
|
|
4264
|
+
async function cmdAsk(
|
|
4265
|
+
agent,
|
|
4266
|
+
session,
|
|
4267
|
+
message,
|
|
4268
|
+
{ noWait = false, yolo = false, allowedTools = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
4269
|
+
) {
|
|
3748
4270
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3749
4271
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3750
4272
|
|
|
@@ -3758,7 +4280,7 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3758
4280
|
/** @type {string} */
|
|
3759
4281
|
const activeSession = sessionExists
|
|
3760
4282
|
? /** @type {string} */ (session)
|
|
3761
|
-
: await cmdStart(agent, session, { yolo });
|
|
4283
|
+
: await cmdStart(agent, session, { yolo, allowedTools });
|
|
3762
4284
|
|
|
3763
4285
|
tmuxSendLiteral(activeSession, message);
|
|
3764
4286
|
await sleep(50);
|
|
@@ -3767,12 +4289,26 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3767
4289
|
if (noWait) {
|
|
3768
4290
|
const parsed = parseSessionName(activeSession);
|
|
3769
4291
|
const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
|
|
3770
|
-
|
|
3771
|
-
|
|
4292
|
+
// Truncate message for display (first line, max 60 chars)
|
|
4293
|
+
const firstLine = message.split("\n")[0];
|
|
4294
|
+
const taskPreview = firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
|
|
4295
|
+
|
|
4296
|
+
let output = `Sent to: ${shortId}
|
|
4297
|
+
Task: ${taskPreview}
|
|
4298
|
+
|
|
4299
|
+
Add this to your task list, then continue with your work.`;
|
|
4300
|
+
|
|
4301
|
+
// Show TaskCreate example if caller is Claude
|
|
4302
|
+
const caller = findCallerAgent();
|
|
4303
|
+
if (caller?.agent === "claude") {
|
|
4304
|
+
output += `
|
|
3772
4305
|
|
|
3773
4306
|
e.g.
|
|
3774
|
-
|
|
3775
|
-
|
|
4307
|
+
TaskCreate({ subject: "ax:${shortId} - ${taskPreview}" })
|
|
4308
|
+
`;
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
console.log(output);
|
|
3776
4312
|
return;
|
|
3777
4313
|
}
|
|
3778
4314
|
|
|
@@ -3964,9 +4500,14 @@ async function cmdReview(
|
|
|
3964
4500
|
* @param {Agent} agent
|
|
3965
4501
|
* @param {string | null | undefined} session
|
|
3966
4502
|
* @param {number} [index]
|
|
3967
|
-
* @param {{wait?: boolean, timeoutMs?: number}} [options]
|
|
4503
|
+
* @param {{wait?: boolean, stale?: boolean, timeoutMs?: number}} [options]
|
|
3968
4504
|
*/
|
|
3969
|
-
async function cmdOutput(
|
|
4505
|
+
async function cmdOutput(
|
|
4506
|
+
agent,
|
|
4507
|
+
session,
|
|
4508
|
+
index = 0,
|
|
4509
|
+
{ wait = false, stale = false, timeoutMs } = {},
|
|
4510
|
+
) {
|
|
3970
4511
|
if (!session || !tmuxHasSession(session)) {
|
|
3971
4512
|
console.log("ERROR: no session");
|
|
3972
4513
|
process.exit(1);
|
|
@@ -3993,8 +4534,11 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3993
4534
|
}
|
|
3994
4535
|
|
|
3995
4536
|
if (state === State.THINKING) {
|
|
3996
|
-
|
|
3997
|
-
|
|
4537
|
+
if (!stale) {
|
|
4538
|
+
console.log("THINKING: Use --wait to block, or --stale for old response.");
|
|
4539
|
+
process.exit(1);
|
|
4540
|
+
}
|
|
4541
|
+
// --stale: fall through to show previous response
|
|
3998
4542
|
}
|
|
3999
4543
|
|
|
4000
4544
|
const output = agent.getResponse(session, screen, index);
|
|
@@ -4192,7 +4736,12 @@ function resolveAgent({ toolFlag, sessionName } = {}) {
|
|
|
4192
4736
|
if (invoked === "axclaude" || invoked === "claude") return { agent: ClaudeAgent };
|
|
4193
4737
|
if (invoked === "axcodex" || invoked === "codex") return { agent: CodexAgent };
|
|
4194
4738
|
|
|
4195
|
-
// 4.
|
|
4739
|
+
// 4. Infer from parent process (running from within claude/codex)
|
|
4740
|
+
const caller = findCallerAgent();
|
|
4741
|
+
if (caller?.agent === "claude") return { agent: ClaudeAgent };
|
|
4742
|
+
if (caller?.agent === "codex") return { agent: CodexAgent };
|
|
4743
|
+
|
|
4744
|
+
// 5. AX_DEFAULT_TOOL environment variable
|
|
4196
4745
|
const defaultTool = process.env.AX_DEFAULT_TOOL;
|
|
4197
4746
|
if (defaultTool === "claude") return { agent: ClaudeAgent };
|
|
4198
4747
|
if (defaultTool === "codex" || !defaultTool) return { agent: CodexAgent };
|
|
@@ -4208,72 +4757,64 @@ function resolveAgent({ toolFlag, sessionName } = {}) {
|
|
|
4208
4757
|
function printHelp(agent, cliName) {
|
|
4209
4758
|
const name = cliName;
|
|
4210
4759
|
const backendName = agent.displayName;
|
|
4211
|
-
const hasReview = !!agent.reviewOptions;
|
|
4212
4760
|
|
|
4213
4761
|
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
4214
4762
|
|
|
4215
4763
|
Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
4216
4764
|
|
|
4217
|
-
|
|
4218
|
-
|
|
4765
|
+
Messaging:
|
|
4766
|
+
<message> Send message to ${name}
|
|
4767
|
+
review [TYPE] Review code: pr, uncommitted, commit, custom
|
|
4768
|
+
|
|
4769
|
+
Sessions:
|
|
4770
|
+
compact Summarise session to shrink context size
|
|
4771
|
+
reset Start fresh conversation
|
|
4772
|
+
agents List all running agents
|
|
4219
4773
|
target Show default target session for current tool
|
|
4220
4774
|
attach [SESSION] Attach to agent session interactively
|
|
4221
|
-
|
|
4222
|
-
|
|
4775
|
+
kill Kill sessions (--all, --session=NAME, --orphans [--force])
|
|
4776
|
+
|
|
4777
|
+
Archangels:
|
|
4223
4778
|
summon [name] Summon archangels (all, or by name)
|
|
4224
4779
|
recall [name] Recall archangels (all, or by name)
|
|
4225
|
-
|
|
4226
|
-
|
|
4780
|
+
mailbox Archangel notes (filters: --branch=git, --all)
|
|
4781
|
+
rfp <prompt> Request proposals (--archangels=a,b)
|
|
4782
|
+
rfp wait <id> Wait for proposals (--archangels=a,b)
|
|
4783
|
+
|
|
4784
|
+
Recovery/State:
|
|
4785
|
+
status Exit code: ready=0 rate_limit=2 confirm=3 thinking=4
|
|
4227
4786
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
4228
|
-
debug Show raw screen output and detected state
|
|
4229
|
-
hasReview
|
|
4230
|
-
? `
|
|
4231
|
-
review [TYPE] Review code: pr, uncommitted, commit, custom`
|
|
4232
|
-
: ""
|
|
4233
|
-
}
|
|
4234
|
-
select N Select menu option N
|
|
4787
|
+
debug Show raw screen output and detected state
|
|
4235
4788
|
approve Approve pending action (send 'y')
|
|
4236
4789
|
reject Reject pending action (send 'n')
|
|
4790
|
+
select N Select menu option N
|
|
4237
4791
|
send KEYS Send key sequence (e.g. "1[Enter]", "[Escape]")
|
|
4238
|
-
|
|
4239
|
-
reset Start fresh conversation
|
|
4240
|
-
<message> Send message to ${name}
|
|
4792
|
+
log [SESSION] View conversation log (--tail=N, --follow, --reasoning)
|
|
4241
4793
|
|
|
4242
4794
|
Flags:
|
|
4243
4795
|
--tool=NAME Use specific agent (codex, claude)
|
|
4244
|
-
--session=
|
|
4796
|
+
--session=ID name | archangel | uuid-prefix | self
|
|
4797
|
+
--fresh Reset conversation before review
|
|
4798
|
+
--yolo Skip all confirmations (dangerous)
|
|
4799
|
+
--auto-approve=TOOLS Auto-approve specific tools (e.g. 'Bash("cargo *")')
|
|
4245
4800
|
--wait Wait for response (default for messages; required for approve/reject)
|
|
4246
4801
|
--no-wait Fire-and-forget: send message, print session ID, exit immediately
|
|
4247
4802
|
--timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
|
|
4248
|
-
--yolo Skip all confirmations (dangerous)
|
|
4249
|
-
--fresh Reset conversation before review
|
|
4250
|
-
--orphans Kill orphaned claude/codex processes (PPID=1)
|
|
4251
|
-
--force Use SIGKILL instead of SIGTERM (with --orphans)
|
|
4252
|
-
|
|
4253
|
-
Environment:
|
|
4254
|
-
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
4255
|
-
${agent.envVar} Override default session name
|
|
4256
|
-
AX_CLAUDE_CONFIG_DIR Override Claude config directory (default: ~/.claude)
|
|
4257
|
-
AX_CODEX_CONFIG_DIR Override Codex config directory (default: ~/.codex)
|
|
4258
|
-
AX_REVIEW_MODE=exec Bypass /review, send instructions directly (codex only)
|
|
4259
|
-
AX_DEBUG=1 Enable debug logging
|
|
4260
4803
|
|
|
4261
4804
|
Examples:
|
|
4262
4805
|
${name} "explain this codebase"
|
|
4263
|
-
${name} "review the error handling"
|
|
4264
|
-
${name} "FYI: auth was refactored" --no-wait
|
|
4806
|
+
${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
|
|
4807
|
+
${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
|
|
4808
|
+
${name} --auto-approve='Bash("cargo *")' "run tests" # Session with specific permissions
|
|
4265
4809
|
${name} review uncommitted --wait
|
|
4266
|
-
${name}
|
|
4267
|
-
${name} kill
|
|
4268
|
-
${name} kill --
|
|
4269
|
-
${name}
|
|
4270
|
-
${name}
|
|
4271
|
-
${name}
|
|
4272
|
-
${name}
|
|
4273
|
-
${name}
|
|
4274
|
-
${name} recall # Recall all archangels
|
|
4275
|
-
${name} recall reviewer # Recall one by name
|
|
4276
|
-
${name} agents # List all agents (shows TYPE=archangel)
|
|
4810
|
+
${name} kill # Kill agents in current project
|
|
4811
|
+
${name} kill --all # Kill all agents across all projects
|
|
4812
|
+
${name} kill --session=NAME # Kill specific session
|
|
4813
|
+
${name} summon # Summon all archangels from .ai/agents/*.md
|
|
4814
|
+
${name} summon reviewer # Summon by name (creates config if new)
|
|
4815
|
+
${name} recall # Recall all archangels
|
|
4816
|
+
${name} recall reviewer # Recall one by name
|
|
4817
|
+
${name} agents # List all agents (shows TYPE=archangel)
|
|
4277
4818
|
|
|
4278
4819
|
Note: Reviews and complex tasks may take several minutes.
|
|
4279
4820
|
Use Bash run_in_background for long operations (not --no-wait).`);
|
|
@@ -4300,7 +4841,8 @@ async function main() {
|
|
|
4300
4841
|
}
|
|
4301
4842
|
|
|
4302
4843
|
// Extract flags into local variables for convenience
|
|
4303
|
-
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force } =
|
|
4844
|
+
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force, stale, autoApprove } =
|
|
4845
|
+
flags;
|
|
4304
4846
|
|
|
4305
4847
|
// Session resolution (must happen before agent resolution so we can infer tool from session name)
|
|
4306
4848
|
let session = null;
|
|
@@ -4319,15 +4861,24 @@ async function main() {
|
|
|
4319
4861
|
}
|
|
4320
4862
|
|
|
4321
4863
|
// Agent resolution (considers --tool flag, session name, invocation, and env vars)
|
|
4322
|
-
const { agent, error: agentError } = resolveAgent({
|
|
4864
|
+
const { agent, error: agentError } = resolveAgent({
|
|
4865
|
+
toolFlag: flags.tool,
|
|
4866
|
+
sessionName: session,
|
|
4867
|
+
});
|
|
4323
4868
|
if (agentError) {
|
|
4324
4869
|
console.log(`ERROR: ${agentError}`);
|
|
4325
4870
|
process.exit(1);
|
|
4326
4871
|
}
|
|
4327
4872
|
|
|
4328
|
-
//
|
|
4873
|
+
// Validate --auto-approve is only used with Claude (Codex doesn't support --allowedTools)
|
|
4874
|
+
if (autoApprove && agent.name === "codex") {
|
|
4875
|
+
console.log("ERROR: --auto-approve is not supported by Codex. Use --yolo instead.");
|
|
4876
|
+
process.exit(1);
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4879
|
+
// If no explicit session, use agent's default (with permission filtering)
|
|
4329
4880
|
if (!session) {
|
|
4330
|
-
session = agent.getDefaultSession();
|
|
4881
|
+
session = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
4331
4882
|
}
|
|
4332
4883
|
|
|
4333
4884
|
// Timeout (convert seconds to milliseconds)
|
|
@@ -4355,7 +4906,7 @@ async function main() {
|
|
|
4355
4906
|
// Dispatch commands
|
|
4356
4907
|
if (cmd === "agents") return cmdAgents();
|
|
4357
4908
|
if (cmd === "target") {
|
|
4358
|
-
const defaultSession = agent.getDefaultSession();
|
|
4909
|
+
const defaultSession = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
4359
4910
|
if (defaultSession) {
|
|
4360
4911
|
console.log(defaultSession);
|
|
4361
4912
|
} else {
|
|
@@ -4371,20 +4922,39 @@ async function main() {
|
|
|
4371
4922
|
if (cmd === "attach") return cmdAttach(positionals[1] || session);
|
|
4372
4923
|
if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
|
|
4373
4924
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
4925
|
+
if (cmd === "rfp") {
|
|
4926
|
+
if (positionals[1] === "wait") {
|
|
4927
|
+
const rfpId = positionals[2];
|
|
4928
|
+
if (!rfpId) {
|
|
4929
|
+
console.log("ERROR: missing rfp id");
|
|
4930
|
+
process.exit(1);
|
|
4931
|
+
}
|
|
4932
|
+
return cmdRfpWait(rfpId, { archangels: flags.archangels, timeoutMs });
|
|
4933
|
+
}
|
|
4934
|
+
const rawPrompt = positionals.slice(1).join(" ");
|
|
4935
|
+
const prompt = await readStdinIfNeeded(rawPrompt);
|
|
4936
|
+
if (!prompt) {
|
|
4937
|
+
console.log("ERROR: missing prompt for rfp");
|
|
4938
|
+
process.exit(1);
|
|
4939
|
+
}
|
|
4940
|
+
return cmdRfp(prompt, { archangels: flags.archangels, fresh, noWait });
|
|
4941
|
+
}
|
|
4374
4942
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
4375
4943
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
4376
|
-
if (cmd === "review")
|
|
4377
|
-
|
|
4944
|
+
if (cmd === "review") {
|
|
4945
|
+
const customInstructions = await readStdinIfNeeded(positionals[2]);
|
|
4946
|
+
return cmdReview(agent, session, positionals[1], customInstructions ?? undefined, {
|
|
4378
4947
|
wait,
|
|
4379
4948
|
fresh,
|
|
4380
4949
|
timeoutMs,
|
|
4381
4950
|
});
|
|
4951
|
+
}
|
|
4382
4952
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
4383
4953
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
4384
4954
|
if (cmd === "output") {
|
|
4385
4955
|
const indexArg = positionals[1];
|
|
4386
4956
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
4387
|
-
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
4957
|
+
return cmdOutput(agent, session, index, { wait, stale, timeoutMs });
|
|
4388
4958
|
}
|
|
4389
4959
|
if (cmd === "send" && positionals.length > 1)
|
|
4390
4960
|
return cmdSend(session, positionals.slice(1).join(" "));
|
|
@@ -4394,18 +4964,17 @@ async function main() {
|
|
|
4394
4964
|
return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
|
|
4395
4965
|
|
|
4396
4966
|
// Default: send message
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
message = await readStdin();
|
|
4400
|
-
}
|
|
4967
|
+
const rawMessage = positionals.join(" ");
|
|
4968
|
+
let message = await readStdinIfNeeded(rawMessage);
|
|
4401
4969
|
|
|
4402
4970
|
if (!message || flags.help) {
|
|
4403
4971
|
printHelp(agent, cliName);
|
|
4404
4972
|
process.exit(0);
|
|
4405
4973
|
}
|
|
4974
|
+
const messageText = message;
|
|
4406
4975
|
|
|
4407
4976
|
// Detect "review ..." or "please review ..." and route to custom review mode
|
|
4408
|
-
const reviewMatch =
|
|
4977
|
+
const reviewMatch = messageText.match(/^(?:please )?review\s*(.*)/i);
|
|
4409
4978
|
if (reviewMatch && agent.reviewOptions) {
|
|
4410
4979
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
4411
4980
|
return cmdReview(agent, session, "custom", customInstructions, {
|
|
@@ -4415,7 +4984,12 @@ async function main() {
|
|
|
4415
4984
|
});
|
|
4416
4985
|
}
|
|
4417
4986
|
|
|
4418
|
-
return cmdAsk(agent, session,
|
|
4987
|
+
return cmdAsk(agent, session, messageText, {
|
|
4988
|
+
noWait,
|
|
4989
|
+
yolo,
|
|
4990
|
+
allowedTools: autoApprove,
|
|
4991
|
+
timeoutMs,
|
|
4992
|
+
});
|
|
4419
4993
|
}
|
|
4420
4994
|
|
|
4421
4995
|
// Run main() only when executed directly (not when imported for testing)
|
|
@@ -4455,4 +5029,6 @@ export {
|
|
|
4455
5029
|
extractThinking,
|
|
4456
5030
|
detectState,
|
|
4457
5031
|
State,
|
|
5032
|
+
normalizeAllowedTools,
|
|
5033
|
+
computePermissionHash,
|
|
4458
5034
|
};
|