ax-agents 0.0.1-alpha.8 → 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 +788 -187
- 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) {
|
|
@@ -1852,7 +2027,22 @@ function detectState(screen, config) {
|
|
|
1852
2027
|
}
|
|
1853
2028
|
}
|
|
1854
2029
|
|
|
1855
|
-
//
|
|
2030
|
+
// Ready - check BEFORE thinking to avoid false positives from timing messages like "✻ Worked for 45s"
|
|
2031
|
+
// If the prompt symbol is visible, the agent is ready regardless of spinner characters in timing messages
|
|
2032
|
+
if (lastLines.includes(config.promptSymbol)) {
|
|
2033
|
+
// Check if any line has the prompt followed by pasted content indicator
|
|
2034
|
+
// "[Pasted text" indicates user has pasted content and Claude is still processing
|
|
2035
|
+
const linesArray = lastLines.split("\n");
|
|
2036
|
+
const promptWithPaste = linesArray.some(
|
|
2037
|
+
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text"),
|
|
2038
|
+
);
|
|
2039
|
+
if (!promptWithPaste) {
|
|
2040
|
+
return State.READY;
|
|
2041
|
+
}
|
|
2042
|
+
// If prompt has pasted content, Claude is still processing - not ready yet
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// Thinking - spinners (check last lines only)
|
|
1856
2046
|
const spinners = config.spinners || [];
|
|
1857
2047
|
if (spinners.some((s) => lastLines.includes(s))) {
|
|
1858
2048
|
return State.THINKING;
|
|
@@ -1877,20 +2067,6 @@ function detectState(screen, config) {
|
|
|
1877
2067
|
}
|
|
1878
2068
|
}
|
|
1879
2069
|
|
|
1880
|
-
// Ready - only if prompt symbol is visible AND not followed by pasted content
|
|
1881
|
-
// "[Pasted text" indicates user has pasted content and Claude is still processing
|
|
1882
|
-
if (lastLines.includes(config.promptSymbol)) {
|
|
1883
|
-
// Check if any line has the prompt followed by pasted content indicator
|
|
1884
|
-
const linesArray = lastLines.split("\n");
|
|
1885
|
-
const promptWithPaste = linesArray.some(
|
|
1886
|
-
(l) => l.includes(config.promptSymbol) && l.includes("[Pasted text"),
|
|
1887
|
-
);
|
|
1888
|
-
if (!promptWithPaste) {
|
|
1889
|
-
return State.READY;
|
|
1890
|
-
}
|
|
1891
|
-
// If prompt has pasted content, Claude is still processing - not ready yet
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
2070
|
return State.STARTING;
|
|
1895
2071
|
}
|
|
1896
2072
|
|
|
@@ -1979,12 +2155,18 @@ class Agent {
|
|
|
1979
2155
|
/**
|
|
1980
2156
|
* @param {boolean} [yolo]
|
|
1981
2157
|
* @param {string | null} [sessionName]
|
|
2158
|
+
* @param {string | null} [customAllowedTools]
|
|
1982
2159
|
* @returns {string}
|
|
1983
2160
|
*/
|
|
1984
|
-
getCommand(yolo, sessionName = null) {
|
|
2161
|
+
getCommand(yolo, sessionName = null, customAllowedTools = null) {
|
|
1985
2162
|
let base;
|
|
1986
2163
|
if (yolo) {
|
|
1987
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}"`;
|
|
1988
2170
|
} else if (this.safeAllowedTools) {
|
|
1989
2171
|
// Default: auto-approve safe read-only operations
|
|
1990
2172
|
base = `${this.startCommand} --allowedTools "${this.safeAllowedTools}"`;
|
|
@@ -2001,38 +2183,85 @@ class Agent {
|
|
|
2001
2183
|
return base;
|
|
2002
2184
|
}
|
|
2003
2185
|
|
|
2004
|
-
|
|
2186
|
+
/**
|
|
2187
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
2188
|
+
* @returns {string | null}
|
|
2189
|
+
*/
|
|
2190
|
+
getDefaultSession({ allowedTools = null, yolo = false } = {}) {
|
|
2005
2191
|
// Check env var for explicit session
|
|
2006
2192
|
if (this.envVar && process.env[this.envVar]) {
|
|
2007
|
-
return process.env[this.envVar];
|
|
2193
|
+
return process.env[this.envVar] ?? null;
|
|
2008
2194
|
}
|
|
2009
2195
|
|
|
2010
2196
|
const cwd = process.cwd();
|
|
2011
|
-
|
|
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;
|
|
2012
2240
|
|
|
2013
|
-
|
|
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
|
|
2014
2251
|
const current = tmuxCurrentSession();
|
|
2015
2252
|
if (current) {
|
|
2016
2253
|
const sessions = tmuxListSessions();
|
|
2017
|
-
const existing = sessions
|
|
2018
|
-
if (!childPattern.test(s)) return false;
|
|
2019
|
-
const sessionCwd = getTmuxSessionCwd(s);
|
|
2020
|
-
return sessionCwd === cwd;
|
|
2021
|
-
});
|
|
2254
|
+
const existing = findSessionInCwdOrParent(sessions);
|
|
2022
2255
|
if (existing) return existing;
|
|
2023
|
-
// 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
|
|
2024
2257
|
return null;
|
|
2025
2258
|
}
|
|
2026
2259
|
|
|
2027
|
-
// Walk up to find claude/codex ancestor and reuse its session
|
|
2028
|
-
const
|
|
2029
|
-
if (
|
|
2260
|
+
// Walk up to find claude/codex ancestor and reuse its session
|
|
2261
|
+
const caller = findCallerAgent();
|
|
2262
|
+
if (caller) {
|
|
2030
2263
|
const sessions = tmuxListSessions();
|
|
2031
|
-
const existing = sessions
|
|
2032
|
-
if (!childPattern.test(s)) return false;
|
|
2033
|
-
const sessionCwd = getTmuxSessionCwd(s);
|
|
2034
|
-
return sessionCwd === cwd;
|
|
2035
|
-
});
|
|
2264
|
+
const existing = findSessionInCwdOrParent(sessions);
|
|
2036
2265
|
if (existing) return existing;
|
|
2037
2266
|
}
|
|
2038
2267
|
|
|
@@ -2041,10 +2270,11 @@ class Agent {
|
|
|
2041
2270
|
}
|
|
2042
2271
|
|
|
2043
2272
|
/**
|
|
2273
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
2044
2274
|
* @returns {string}
|
|
2045
2275
|
*/
|
|
2046
|
-
generateSession() {
|
|
2047
|
-
return generateSessionName(this.name);
|
|
2276
|
+
generateSession(options = {}) {
|
|
2277
|
+
return generateSessionName(this.name, options);
|
|
2048
2278
|
}
|
|
2049
2279
|
|
|
2050
2280
|
/**
|
|
@@ -2370,8 +2600,12 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2370
2600
|
const initialScreen = tmuxCapture(session);
|
|
2371
2601
|
const initialState = agent.getState(initialScreen);
|
|
2372
2602
|
|
|
2373
|
-
//
|
|
2374
|
-
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
|
|
2375
2609
|
initialState === State.RATE_LIMITED ||
|
|
2376
2610
|
initialState === State.CONFIRMING ||
|
|
2377
2611
|
initialState === State.READY
|
|
@@ -2384,6 +2618,13 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2384
2618
|
const screen = tmuxCapture(session);
|
|
2385
2619
|
const state = agent.getState(screen);
|
|
2386
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
|
+
|
|
2387
2628
|
if (state === State.RATE_LIMITED || state === State.CONFIRMING || state === State.READY) {
|
|
2388
2629
|
return { state, screen };
|
|
2389
2630
|
}
|
|
@@ -2424,6 +2665,13 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
|
2424
2665
|
return { state, screen };
|
|
2425
2666
|
}
|
|
2426
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
|
+
|
|
2427
2675
|
if (screen !== lastScreen) {
|
|
2428
2676
|
lastScreen = screen;
|
|
2429
2677
|
stableAt = Date.now();
|
|
@@ -2533,6 +2781,7 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2533
2781
|
continue;
|
|
2534
2782
|
}
|
|
2535
2783
|
|
|
2784
|
+
// FEEDBACK_MODAL is handled by the underlying waitFn (pollForResponse)
|
|
2536
2785
|
debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
|
|
2537
2786
|
}
|
|
2538
2787
|
|
|
@@ -2544,12 +2793,13 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2544
2793
|
* @param {string | null | undefined} session
|
|
2545
2794
|
* @param {Object} [options]
|
|
2546
2795
|
* @param {boolean} [options.yolo]
|
|
2796
|
+
* @param {string | null} [options.allowedTools]
|
|
2547
2797
|
* @returns {Promise<string>}
|
|
2548
2798
|
*/
|
|
2549
|
-
async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
2799
|
+
async function cmdStart(agent, session, { yolo = false, allowedTools = null } = {}) {
|
|
2550
2800
|
// Generate session name if not provided
|
|
2551
2801
|
if (!session) {
|
|
2552
|
-
session = agent.generateSession();
|
|
2802
|
+
session = agent.generateSession({ allowedTools, yolo });
|
|
2553
2803
|
}
|
|
2554
2804
|
|
|
2555
2805
|
if (tmuxHasSession(session)) return session;
|
|
@@ -2561,7 +2811,7 @@ async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
|
2561
2811
|
process.exit(1);
|
|
2562
2812
|
}
|
|
2563
2813
|
|
|
2564
|
-
const command = agent.getCommand(yolo, session);
|
|
2814
|
+
const command = agent.getCommand(yolo, session, allowedTools);
|
|
2565
2815
|
tmuxNewSession(session, command);
|
|
2566
2816
|
|
|
2567
2817
|
const start = Date.now();
|
|
@@ -2574,6 +2824,18 @@ async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
|
2574
2824
|
continue;
|
|
2575
2825
|
}
|
|
2576
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
|
+
|
|
2577
2839
|
if (state === State.READY) return session;
|
|
2578
2840
|
|
|
2579
2841
|
await sleep(POLL_MS);
|
|
@@ -2621,6 +2883,7 @@ function cmdAgents() {
|
|
|
2621
2883
|
const isDefault =
|
|
2622
2884
|
(parsed.tool === "claude" && session === claudeDefault) ||
|
|
2623
2885
|
(parsed.tool === "codex" && session === codexDefault);
|
|
2886
|
+
const perms = getSessionPermissions(session);
|
|
2624
2887
|
|
|
2625
2888
|
// Get session metadata (Claude only)
|
|
2626
2889
|
const meta = getSessionMeta(session);
|
|
@@ -2631,6 +2894,7 @@ function cmdAgents() {
|
|
|
2631
2894
|
state: state || "unknown",
|
|
2632
2895
|
target: isDefault ? "*" : "",
|
|
2633
2896
|
type,
|
|
2897
|
+
mode: perms.mode,
|
|
2634
2898
|
plan: meta?.slug || "-",
|
|
2635
2899
|
branch: meta?.gitBranch || "-",
|
|
2636
2900
|
};
|
|
@@ -2642,14 +2906,15 @@ function cmdAgents() {
|
|
|
2642
2906
|
const maxState = Math.max(5, ...agents.map((a) => a.state.length));
|
|
2643
2907
|
const maxTarget = Math.max(6, ...agents.map((a) => a.target.length));
|
|
2644
2908
|
const maxType = Math.max(4, ...agents.map((a) => a.type.length));
|
|
2909
|
+
const maxMode = Math.max(4, ...agents.map((a) => a.mode.length));
|
|
2645
2910
|
const maxPlan = Math.max(4, ...agents.map((a) => a.plan.length));
|
|
2646
2911
|
|
|
2647
2912
|
console.log(
|
|
2648
|
-
`${"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`,
|
|
2649
2914
|
);
|
|
2650
2915
|
for (const a of agents) {
|
|
2651
2916
|
console.log(
|
|
2652
|
-
`${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}`,
|
|
2653
2918
|
);
|
|
2654
2919
|
}
|
|
2655
2920
|
|
|
@@ -2706,11 +2971,29 @@ function startArchangel(config, parentSession = null) {
|
|
|
2706
2971
|
env,
|
|
2707
2972
|
});
|
|
2708
2973
|
child.unref();
|
|
2974
|
+
const watchingLabel = parentSession
|
|
2975
|
+
? parentSession.session || parentSession.uuid?.slice(0, 8)
|
|
2976
|
+
: null;
|
|
2709
2977
|
console.log(
|
|
2710
|
-
`Summoning: ${config.name} (pid ${child.pid})${
|
|
2978
|
+
`Summoning: ${config.name} (pid ${child.pid})${watchingLabel ? ` [watching: ${watchingLabel}]` : ""}`,
|
|
2711
2979
|
);
|
|
2712
2980
|
}
|
|
2713
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
|
+
|
|
2714
2997
|
// =============================================================================
|
|
2715
2998
|
// Command: archangel (runs as the archangel process itself)
|
|
2716
2999
|
// =============================================================================
|
|
@@ -2958,28 +3241,18 @@ async function cmdArchangel(agentName) {
|
|
|
2958
3241
|
|
|
2959
3242
|
const cleanedResponse = agent.getResponse(sessionName, afterScreen) || "";
|
|
2960
3243
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
cleanedResponse.match(/^\+\d+ lines\]/) ||
|
|
2965
|
-
cleanedResponse.length < 20;
|
|
2966
|
-
|
|
2967
|
-
if (
|
|
2968
|
-
cleanedResponse &&
|
|
2969
|
-
!isGarbage &&
|
|
2970
|
-
!cleanedResponse.toLowerCase().includes("no issues found")
|
|
2971
|
-
) {
|
|
3244
|
+
const isSkippable = !cleanedResponse || cleanedResponse.trim() === "EMPTY_RESPONSE";
|
|
3245
|
+
|
|
3246
|
+
if (!isSkippable) {
|
|
2972
3247
|
writeToMailbox({
|
|
2973
3248
|
agent: /** @type {string} */ (agentName),
|
|
2974
3249
|
session: sessionName,
|
|
2975
3250
|
branch: getCurrentBranch(),
|
|
2976
3251
|
commit: getCurrentCommit(),
|
|
2977
3252
|
files,
|
|
2978
|
-
message: cleanedResponse
|
|
3253
|
+
message: cleanedResponse,
|
|
2979
3254
|
});
|
|
2980
3255
|
console.log(`[archangel:${agentName}] Wrote observation for ${files.length} file(s)`);
|
|
2981
|
-
} else if (isGarbage) {
|
|
2982
|
-
console.log(`[archangel:${agentName}] Skipped garbage response`);
|
|
2983
3256
|
}
|
|
2984
3257
|
} catch (err) {
|
|
2985
3258
|
console.error(`[archangel:${agentName}] Error:`, err instanceof Error ? err.message : err);
|
|
@@ -3171,6 +3444,7 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
|
3171
3444
|
import { dirname, join } from "node:path";
|
|
3172
3445
|
import { fileURLToPath } from "node:url";
|
|
3173
3446
|
import { createHash } from "node:crypto";
|
|
3447
|
+
import { execSync } from "node:child_process";
|
|
3174
3448
|
|
|
3175
3449
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3176
3450
|
const AI_DIR = join(__dirname, "..");
|
|
@@ -3178,6 +3452,15 @@ const DEBUG = process.env.AX_DEBUG === "1";
|
|
|
3178
3452
|
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
3179
3453
|
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
3180
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
|
+
|
|
3181
3464
|
// Read hook input from stdin
|
|
3182
3465
|
let hookInput = {};
|
|
3183
3466
|
try {
|
|
@@ -3192,8 +3475,9 @@ const hookEvent = hookInput.hook_event_name || "";
|
|
|
3192
3475
|
|
|
3193
3476
|
if (DEBUG) console.error("[hook] session:", sessionId, "event:", hookEvent);
|
|
3194
3477
|
|
|
3195
|
-
|
|
3196
|
-
if (
|
|
3478
|
+
const tmuxSession = getTmuxSessionName();
|
|
3479
|
+
if (DEBUG) console.error("[hook] tmux session:", tmuxSession);
|
|
3480
|
+
if (tmuxSession && (tmuxSession.includes("-archangel-") || tmuxSession.includes("-partner-"))) {
|
|
3197
3481
|
if (DEBUG) console.error("[hook] skipping non-parent session");
|
|
3198
3482
|
process.exit(0);
|
|
3199
3483
|
}
|
|
@@ -3384,7 +3668,7 @@ function cmdKill(session, { all = false, orphans = false, force = false } = {})
|
|
|
3384
3668
|
// If specific session provided, kill just that one
|
|
3385
3669
|
if (session) {
|
|
3386
3670
|
if (!tmuxHasSession(session)) {
|
|
3387
|
-
console.log("ERROR: session not found");
|
|
3671
|
+
console.log("ERROR: session not found. Run 'ax agents' to list sessions.");
|
|
3388
3672
|
process.exit(1);
|
|
3389
3673
|
}
|
|
3390
3674
|
tmuxKill(session);
|
|
@@ -3435,7 +3719,7 @@ function cmdAttach(session) {
|
|
|
3435
3719
|
// Resolve partial session name
|
|
3436
3720
|
const resolved = resolveSessionName(session);
|
|
3437
3721
|
if (!resolved || !tmuxHasSession(resolved)) {
|
|
3438
|
-
console.log("ERROR: session not found");
|
|
3722
|
+
console.log("ERROR: session not found. Run 'ax agents' to list sessions.");
|
|
3439
3723
|
process.exit(1);
|
|
3440
3724
|
}
|
|
3441
3725
|
|
|
@@ -3459,7 +3743,7 @@ function cmdLog(sessionName, { tail = 50, reasoning = false, follow = false } =
|
|
|
3459
3743
|
// Resolve partial session name
|
|
3460
3744
|
const resolved = resolveSessionName(sessionName);
|
|
3461
3745
|
if (!resolved) {
|
|
3462
|
-
console.log("ERROR: session not found");
|
|
3746
|
+
console.log("ERROR: session not found. Run 'ax agents' to list sessions.");
|
|
3463
3747
|
process.exit(1);
|
|
3464
3748
|
}
|
|
3465
3749
|
const parsed = parseSessionName(resolved);
|
|
@@ -3723,7 +4007,13 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3723
4007
|
console.log(`**Branch**: ${p.branch || "?"} @ ${p.commit || "?"}\n`);
|
|
3724
4008
|
}
|
|
3725
4009
|
|
|
3726
|
-
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) {
|
|
3727
4017
|
console.log(`**Assistant**: ${p.message}\n`);
|
|
3728
4018
|
}
|
|
3729
4019
|
|
|
@@ -3737,13 +4027,246 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3737
4027
|
}
|
|
3738
4028
|
}
|
|
3739
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
|
+
|
|
3740
4258
|
/**
|
|
3741
4259
|
* @param {Agent} agent
|
|
3742
4260
|
* @param {string | null | undefined} session
|
|
3743
4261
|
* @param {string} message
|
|
3744
|
-
* @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
|
|
4262
|
+
* @param {{noWait?: boolean, yolo?: boolean, allowedTools?: string | null, timeoutMs?: number}} [options]
|
|
3745
4263
|
*/
|
|
3746
|
-
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
|
+
) {
|
|
3747
4270
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3748
4271
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3749
4272
|
|
|
@@ -3757,7 +4280,7 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3757
4280
|
/** @type {string} */
|
|
3758
4281
|
const activeSession = sessionExists
|
|
3759
4282
|
? /** @type {string} */ (session)
|
|
3760
|
-
: await cmdStart(agent, session, { yolo });
|
|
4283
|
+
: await cmdStart(agent, session, { yolo, allowedTools });
|
|
3761
4284
|
|
|
3762
4285
|
tmuxSendLiteral(activeSession, message);
|
|
3763
4286
|
await sleep(50);
|
|
@@ -3766,12 +4289,26 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3766
4289
|
if (noWait) {
|
|
3767
4290
|
const parsed = parseSessionName(activeSession);
|
|
3768
4291
|
const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
|
|
3769
|
-
|
|
3770
|
-
|
|
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 += `
|
|
3771
4305
|
|
|
3772
4306
|
e.g.
|
|
3773
|
-
|
|
3774
|
-
|
|
4307
|
+
TaskCreate({ subject: "ax:${shortId} - ${taskPreview}" })
|
|
4308
|
+
`;
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
console.log(output);
|
|
3775
4312
|
return;
|
|
3776
4313
|
}
|
|
3777
4314
|
|
|
@@ -3963,9 +4500,14 @@ async function cmdReview(
|
|
|
3963
4500
|
* @param {Agent} agent
|
|
3964
4501
|
* @param {string | null | undefined} session
|
|
3965
4502
|
* @param {number} [index]
|
|
3966
|
-
* @param {{wait?: boolean, timeoutMs?: number}} [options]
|
|
4503
|
+
* @param {{wait?: boolean, stale?: boolean, timeoutMs?: number}} [options]
|
|
3967
4504
|
*/
|
|
3968
|
-
async function cmdOutput(
|
|
4505
|
+
async function cmdOutput(
|
|
4506
|
+
agent,
|
|
4507
|
+
session,
|
|
4508
|
+
index = 0,
|
|
4509
|
+
{ wait = false, stale = false, timeoutMs } = {},
|
|
4510
|
+
) {
|
|
3969
4511
|
if (!session || !tmuxHasSession(session)) {
|
|
3970
4512
|
console.log("ERROR: no session");
|
|
3971
4513
|
process.exit(1);
|
|
@@ -3992,8 +4534,11 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3992
4534
|
}
|
|
3993
4535
|
|
|
3994
4536
|
if (state === State.THINKING) {
|
|
3995
|
-
|
|
3996
|
-
|
|
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
|
|
3997
4542
|
}
|
|
3998
4543
|
|
|
3999
4544
|
const output = agent.getResponse(session, screen, index);
|
|
@@ -4161,20 +4706,48 @@ async function cmdSelect(agent, session, n, { wait = false, timeoutMs } = {}) {
|
|
|
4161
4706
|
// =============================================================================
|
|
4162
4707
|
|
|
4163
4708
|
/**
|
|
4164
|
-
*
|
|
4709
|
+
* Resolve the agent to use based on (in priority order):
|
|
4710
|
+
* 1. Explicit --tool flag
|
|
4711
|
+
* 2. Session name (e.g., "claude-archangel-..." → ClaudeAgent)
|
|
4712
|
+
* 3. CLI invocation name (axclaude, axcodex)
|
|
4713
|
+
* 4. AX_DEFAULT_TOOL environment variable
|
|
4714
|
+
* 5. Default to CodexAgent
|
|
4715
|
+
*
|
|
4716
|
+
* @param {{toolFlag?: string, sessionName?: string | null}} options
|
|
4717
|
+
* @returns {{agent: Agent, error?: string}}
|
|
4165
4718
|
*/
|
|
4166
|
-
function
|
|
4719
|
+
function resolveAgent({ toolFlag, sessionName } = {}) {
|
|
4720
|
+
// 1. Explicit --tool flag takes highest priority
|
|
4721
|
+
if (toolFlag) {
|
|
4722
|
+
if (toolFlag === "claude") return { agent: ClaudeAgent };
|
|
4723
|
+
if (toolFlag === "codex") return { agent: CodexAgent };
|
|
4724
|
+
return { agent: CodexAgent, error: `unknown tool '${toolFlag}'` };
|
|
4725
|
+
}
|
|
4726
|
+
|
|
4727
|
+
// 2. Infer from session name (e.g., "claude-archangel-..." or "codex-partner-...")
|
|
4728
|
+
if (sessionName) {
|
|
4729
|
+
const parsed = parseSessionName(sessionName);
|
|
4730
|
+
if (parsed?.tool === "claude") return { agent: ClaudeAgent };
|
|
4731
|
+
if (parsed?.tool === "codex") return { agent: CodexAgent };
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
// 3. CLI invocation name
|
|
4167
4735
|
const invoked = path.basename(process.argv[1], ".js");
|
|
4168
|
-
if (invoked === "axclaude" || invoked === "claude") return ClaudeAgent;
|
|
4169
|
-
if (invoked === "axcodex" || invoked === "codex") return CodexAgent;
|
|
4736
|
+
if (invoked === "axclaude" || invoked === "claude") return { agent: ClaudeAgent };
|
|
4737
|
+
if (invoked === "axcodex" || invoked === "codex") return { agent: CodexAgent };
|
|
4738
|
+
|
|
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 };
|
|
4170
4743
|
|
|
4171
|
-
//
|
|
4744
|
+
// 5. AX_DEFAULT_TOOL environment variable
|
|
4172
4745
|
const defaultTool = process.env.AX_DEFAULT_TOOL;
|
|
4173
|
-
if (defaultTool === "claude") return ClaudeAgent;
|
|
4174
|
-
if (defaultTool === "codex" || !defaultTool) return CodexAgent;
|
|
4746
|
+
if (defaultTool === "claude") return { agent: ClaudeAgent };
|
|
4747
|
+
if (defaultTool === "codex" || !defaultTool) return { agent: CodexAgent };
|
|
4175
4748
|
|
|
4176
4749
|
console.error(`WARNING: invalid AX_DEFAULT_TOOL="${defaultTool}", using codex`);
|
|
4177
|
-
return CodexAgent;
|
|
4750
|
+
return { agent: CodexAgent };
|
|
4178
4751
|
}
|
|
4179
4752
|
|
|
4180
4753
|
/**
|
|
@@ -4184,72 +4757,64 @@ function getAgentFromInvocation() {
|
|
|
4184
4757
|
function printHelp(agent, cliName) {
|
|
4185
4758
|
const name = cliName;
|
|
4186
4759
|
const backendName = agent.displayName;
|
|
4187
|
-
const hasReview = !!agent.reviewOptions;
|
|
4188
4760
|
|
|
4189
4761
|
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
4190
4762
|
|
|
4191
4763
|
Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
4192
4764
|
|
|
4193
|
-
|
|
4194
|
-
|
|
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
|
|
4195
4773
|
target Show default target session for current tool
|
|
4196
4774
|
attach [SESSION] Attach to agent session interactively
|
|
4197
|
-
|
|
4198
|
-
|
|
4775
|
+
kill Kill sessions (--all, --session=NAME, --orphans [--force])
|
|
4776
|
+
|
|
4777
|
+
Archangels:
|
|
4199
4778
|
summon [name] Summon archangels (all, or by name)
|
|
4200
4779
|
recall [name] Recall archangels (all, or by name)
|
|
4201
|
-
|
|
4202
|
-
|
|
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
|
|
4203
4786
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
4204
|
-
debug Show raw screen output and detected state
|
|
4205
|
-
hasReview
|
|
4206
|
-
? `
|
|
4207
|
-
review [TYPE] Review code: pr, uncommitted, commit, custom`
|
|
4208
|
-
: ""
|
|
4209
|
-
}
|
|
4210
|
-
select N Select menu option N
|
|
4787
|
+
debug Show raw screen output and detected state
|
|
4211
4788
|
approve Approve pending action (send 'y')
|
|
4212
4789
|
reject Reject pending action (send 'n')
|
|
4790
|
+
select N Select menu option N
|
|
4213
4791
|
send KEYS Send key sequence (e.g. "1[Enter]", "[Escape]")
|
|
4214
|
-
|
|
4215
|
-
reset Start fresh conversation
|
|
4216
|
-
<message> Send message to ${name}
|
|
4792
|
+
log [SESSION] View conversation log (--tail=N, --follow, --reasoning)
|
|
4217
4793
|
|
|
4218
4794
|
Flags:
|
|
4219
4795
|
--tool=NAME Use specific agent (codex, claude)
|
|
4220
|
-
--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 *")')
|
|
4221
4800
|
--wait Wait for response (default for messages; required for approve/reject)
|
|
4222
4801
|
--no-wait Fire-and-forget: send message, print session ID, exit immediately
|
|
4223
4802
|
--timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
|
|
4224
|
-
--yolo Skip all confirmations (dangerous)
|
|
4225
|
-
--fresh Reset conversation before review
|
|
4226
|
-
--orphans Kill orphaned claude/codex processes (PPID=1)
|
|
4227
|
-
--force Use SIGKILL instead of SIGTERM (with --orphans)
|
|
4228
|
-
|
|
4229
|
-
Environment:
|
|
4230
|
-
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
4231
|
-
${agent.envVar} Override default session name
|
|
4232
|
-
AX_CLAUDE_CONFIG_DIR Override Claude config directory (default: ~/.claude)
|
|
4233
|
-
AX_CODEX_CONFIG_DIR Override Codex config directory (default: ~/.codex)
|
|
4234
|
-
AX_REVIEW_MODE=exec Bypass /review, send instructions directly (codex only)
|
|
4235
|
-
AX_DEBUG=1 Enable debug logging
|
|
4236
4803
|
|
|
4237
4804
|
Examples:
|
|
4238
4805
|
${name} "explain this codebase"
|
|
4239
|
-
${name} "review the error handling"
|
|
4240
|
-
${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
|
|
4241
4809
|
${name} review uncommitted --wait
|
|
4242
|
-
${name}
|
|
4243
|
-
${name} kill
|
|
4244
|
-
${name} kill --
|
|
4245
|
-
${name}
|
|
4246
|
-
${name}
|
|
4247
|
-
${name}
|
|
4248
|
-
${name}
|
|
4249
|
-
${name}
|
|
4250
|
-
${name} recall # Recall all archangels
|
|
4251
|
-
${name} recall reviewer # Recall one by name
|
|
4252
|
-
${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)
|
|
4253
4818
|
|
|
4254
4819
|
Note: Reviews and complex tasks may take several minutes.
|
|
4255
4820
|
Use Bash run_in_background for long operations (not --no-wait).`);
|
|
@@ -4276,21 +4841,11 @@ async function main() {
|
|
|
4276
4841
|
}
|
|
4277
4842
|
|
|
4278
4843
|
// Extract flags into local variables for convenience
|
|
4279
|
-
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force } =
|
|
4280
|
-
|
|
4281
|
-
// Agent selection
|
|
4282
|
-
let agent = getAgentFromInvocation();
|
|
4283
|
-
if (flags.tool) {
|
|
4284
|
-
if (flags.tool === "claude") agent = ClaudeAgent;
|
|
4285
|
-
else if (flags.tool === "codex") agent = CodexAgent;
|
|
4286
|
-
else {
|
|
4287
|
-
console.log(`ERROR: unknown tool '${flags.tool}'`);
|
|
4288
|
-
process.exit(1);
|
|
4289
|
-
}
|
|
4290
|
-
}
|
|
4844
|
+
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force, stale, autoApprove } =
|
|
4845
|
+
flags;
|
|
4291
4846
|
|
|
4292
|
-
// Session resolution
|
|
4293
|
-
let session =
|
|
4847
|
+
// Session resolution (must happen before agent resolution so we can infer tool from session name)
|
|
4848
|
+
let session = null;
|
|
4294
4849
|
if (flags.session) {
|
|
4295
4850
|
if (flags.session === "self") {
|
|
4296
4851
|
const current = tmuxCurrentSession();
|
|
@@ -4305,6 +4860,27 @@ async function main() {
|
|
|
4305
4860
|
}
|
|
4306
4861
|
}
|
|
4307
4862
|
|
|
4863
|
+
// Agent resolution (considers --tool flag, session name, invocation, and env vars)
|
|
4864
|
+
const { agent, error: agentError } = resolveAgent({
|
|
4865
|
+
toolFlag: flags.tool,
|
|
4866
|
+
sessionName: session,
|
|
4867
|
+
});
|
|
4868
|
+
if (agentError) {
|
|
4869
|
+
console.log(`ERROR: ${agentError}`);
|
|
4870
|
+
process.exit(1);
|
|
4871
|
+
}
|
|
4872
|
+
|
|
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)
|
|
4880
|
+
if (!session) {
|
|
4881
|
+
session = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
4882
|
+
}
|
|
4883
|
+
|
|
4308
4884
|
// Timeout (convert seconds to milliseconds)
|
|
4309
4885
|
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
4310
4886
|
if (flags.timeout !== undefined) {
|
|
@@ -4330,7 +4906,7 @@ async function main() {
|
|
|
4330
4906
|
// Dispatch commands
|
|
4331
4907
|
if (cmd === "agents") return cmdAgents();
|
|
4332
4908
|
if (cmd === "target") {
|
|
4333
|
-
const defaultSession = agent.getDefaultSession();
|
|
4909
|
+
const defaultSession = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
4334
4910
|
if (defaultSession) {
|
|
4335
4911
|
console.log(defaultSession);
|
|
4336
4912
|
} else {
|
|
@@ -4346,20 +4922,39 @@ async function main() {
|
|
|
4346
4922
|
if (cmd === "attach") return cmdAttach(positionals[1] || session);
|
|
4347
4923
|
if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
|
|
4348
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
|
+
}
|
|
4349
4942
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
4350
4943
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
4351
|
-
if (cmd === "review")
|
|
4352
|
-
|
|
4944
|
+
if (cmd === "review") {
|
|
4945
|
+
const customInstructions = await readStdinIfNeeded(positionals[2]);
|
|
4946
|
+
return cmdReview(agent, session, positionals[1], customInstructions ?? undefined, {
|
|
4353
4947
|
wait,
|
|
4354
4948
|
fresh,
|
|
4355
4949
|
timeoutMs,
|
|
4356
4950
|
});
|
|
4951
|
+
}
|
|
4357
4952
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
4358
4953
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
4359
4954
|
if (cmd === "output") {
|
|
4360
4955
|
const indexArg = positionals[1];
|
|
4361
4956
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
4362
|
-
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
4957
|
+
return cmdOutput(agent, session, index, { wait, stale, timeoutMs });
|
|
4363
4958
|
}
|
|
4364
4959
|
if (cmd === "send" && positionals.length > 1)
|
|
4365
4960
|
return cmdSend(session, positionals.slice(1).join(" "));
|
|
@@ -4369,18 +4964,17 @@ async function main() {
|
|
|
4369
4964
|
return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
|
|
4370
4965
|
|
|
4371
4966
|
// Default: send message
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
message = await readStdin();
|
|
4375
|
-
}
|
|
4967
|
+
const rawMessage = positionals.join(" ");
|
|
4968
|
+
let message = await readStdinIfNeeded(rawMessage);
|
|
4376
4969
|
|
|
4377
4970
|
if (!message || flags.help) {
|
|
4378
4971
|
printHelp(agent, cliName);
|
|
4379
4972
|
process.exit(0);
|
|
4380
4973
|
}
|
|
4974
|
+
const messageText = message;
|
|
4381
4975
|
|
|
4382
4976
|
// Detect "review ..." or "please review ..." and route to custom review mode
|
|
4383
|
-
const reviewMatch =
|
|
4977
|
+
const reviewMatch = messageText.match(/^(?:please )?review\s*(.*)/i);
|
|
4384
4978
|
if (reviewMatch && agent.reviewOptions) {
|
|
4385
4979
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
4386
4980
|
return cmdReview(agent, session, "custom", customInstructions, {
|
|
@@ -4390,7 +4984,12 @@ async function main() {
|
|
|
4390
4984
|
});
|
|
4391
4985
|
}
|
|
4392
4986
|
|
|
4393
|
-
return cmdAsk(agent, session,
|
|
4987
|
+
return cmdAsk(agent, session, messageText, {
|
|
4988
|
+
noWait,
|
|
4989
|
+
yolo,
|
|
4990
|
+
allowedTools: autoApprove,
|
|
4991
|
+
timeoutMs,
|
|
4992
|
+
});
|
|
4394
4993
|
}
|
|
4395
4994
|
|
|
4396
4995
|
// Run main() only when executed directly (not when imported for testing)
|
|
@@ -4430,4 +5029,6 @@ export {
|
|
|
4430
5029
|
extractThinking,
|
|
4431
5030
|
detectState,
|
|
4432
5031
|
State,
|
|
5032
|
+
normalizeAllowedTools,
|
|
5033
|
+
computePermissionHash,
|
|
4433
5034
|
};
|