ax-agents 0.0.1-alpha.11 → 0.0.1-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ax.js +711 -133
- 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
|
// =============================================================================
|
|
@@ -316,6 +364,22 @@ const ARCHANGEL_PREAMBLE = `## Guidelines
|
|
|
316
364
|
- For critical issues, request for them to be added to the todo list.
|
|
317
365
|
- Don't repeat observations you've already made unless you have more to say or better clarity.
|
|
318
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".`;
|
|
319
383
|
|
|
320
384
|
/**
|
|
321
385
|
* @param {string} session
|
|
@@ -353,9 +417,9 @@ async function waitFor(session, predicate, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
|
353
417
|
// =============================================================================
|
|
354
418
|
|
|
355
419
|
/**
|
|
356
|
-
* @returns {number | null}
|
|
420
|
+
* @returns {{pid: number, agent: 'claude' | 'codex'} | null}
|
|
357
421
|
*/
|
|
358
|
-
function
|
|
422
|
+
function findCallerAgent() {
|
|
359
423
|
let pid = process.ppid;
|
|
360
424
|
while (pid > 1) {
|
|
361
425
|
const result = spawnSync("ps", ["-p", pid.toString(), "-o", "ppid=,comm="], {
|
|
@@ -365,9 +429,8 @@ function findCallerPid() {
|
|
|
365
429
|
const parts = result.stdout.trim().split(/\s+/);
|
|
366
430
|
const ppid = parseInt(parts[0], 10);
|
|
367
431
|
const cmd = parts.slice(1).join(" ");
|
|
368
|
-
if (cmd.includes("claude")
|
|
369
|
-
|
|
370
|
-
}
|
|
432
|
+
if (cmd.includes("claude")) return { pid, agent: "claude" };
|
|
433
|
+
if (cmd.includes("codex")) return { pid, agent: "codex" };
|
|
371
434
|
pid = ppid;
|
|
372
435
|
}
|
|
373
436
|
return null;
|
|
@@ -378,7 +441,9 @@ function findCallerPid() {
|
|
|
378
441
|
* @returns {{pid: string, command: string}[]}
|
|
379
442
|
*/
|
|
380
443
|
function findOrphanedProcesses() {
|
|
381
|
-
const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], {
|
|
444
|
+
const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], {
|
|
445
|
+
encoding: "utf-8",
|
|
446
|
+
});
|
|
382
447
|
|
|
383
448
|
if (result.status !== 0 || !result.stdout.trim()) {
|
|
384
449
|
return [];
|
|
@@ -432,6 +497,17 @@ async function readStdin() {
|
|
|
432
497
|
});
|
|
433
498
|
}
|
|
434
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
|
+
|
|
435
511
|
// =============================================================================
|
|
436
512
|
// =============================================================================
|
|
437
513
|
// Helpers - CLI argument parsing
|
|
@@ -452,6 +528,7 @@ async function readStdin() {
|
|
|
452
528
|
* @property {boolean} all
|
|
453
529
|
* @property {boolean} orphans
|
|
454
530
|
* @property {boolean} force
|
|
531
|
+
* @property {boolean} stale
|
|
455
532
|
* @property {boolean} version
|
|
456
533
|
* @property {boolean} help
|
|
457
534
|
* @property {string} [tool]
|
|
@@ -460,6 +537,8 @@ async function readStdin() {
|
|
|
460
537
|
* @property {number} [tail]
|
|
461
538
|
* @property {number} [limit]
|
|
462
539
|
* @property {string} [branch]
|
|
540
|
+
* @property {string} [archangels]
|
|
541
|
+
* @property {string} [autoApprove]
|
|
463
542
|
*/
|
|
464
543
|
function parseCliArgs(args) {
|
|
465
544
|
const { values, positionals } = parseArgs({
|
|
@@ -475,15 +554,18 @@ function parseCliArgs(args) {
|
|
|
475
554
|
all: { type: "boolean", default: false },
|
|
476
555
|
orphans: { type: "boolean", default: false },
|
|
477
556
|
force: { type: "boolean", default: false },
|
|
557
|
+
stale: { type: "boolean", default: false },
|
|
478
558
|
version: { type: "boolean", short: "V", default: false },
|
|
479
559
|
help: { type: "boolean", short: "h", default: false },
|
|
480
560
|
// Value flags
|
|
481
561
|
tool: { type: "string" },
|
|
562
|
+
"auto-approve": { type: "string" },
|
|
482
563
|
session: { type: "string" },
|
|
483
564
|
timeout: { type: "string" },
|
|
484
565
|
tail: { type: "string" },
|
|
485
566
|
limit: { type: "string" },
|
|
486
567
|
branch: { type: "string" },
|
|
568
|
+
archangels: { type: "string" },
|
|
487
569
|
},
|
|
488
570
|
allowPositionals: true,
|
|
489
571
|
strict: false, // Don't error on unknown flags
|
|
@@ -500,6 +582,7 @@ function parseCliArgs(args) {
|
|
|
500
582
|
all: Boolean(values.all),
|
|
501
583
|
orphans: Boolean(values.orphans),
|
|
502
584
|
force: Boolean(values.force),
|
|
585
|
+
stale: Boolean(values.stale),
|
|
503
586
|
version: Boolean(values.version),
|
|
504
587
|
help: Boolean(values.help),
|
|
505
588
|
tool: /** @type {string | undefined} */ (values.tool),
|
|
@@ -508,6 +591,8 @@ function parseCliArgs(args) {
|
|
|
508
591
|
tail: values.tail !== undefined ? Number(values.tail) : undefined,
|
|
509
592
|
limit: values.limit !== undefined ? Number(values.limit) : undefined,
|
|
510
593
|
branch: /** @type {string | undefined} */ (values.branch),
|
|
594
|
+
archangels: /** @type {string | undefined} */ (values.archangels),
|
|
595
|
+
autoApprove: /** @type {string | undefined} */ (values["auto-approve"]),
|
|
511
596
|
},
|
|
512
597
|
positionals,
|
|
513
598
|
};
|
|
@@ -516,6 +601,10 @@ function parseCliArgs(args) {
|
|
|
516
601
|
// Helpers - session tracking
|
|
517
602
|
// =============================================================================
|
|
518
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
|
+
|
|
519
608
|
/**
|
|
520
609
|
* @param {string} session
|
|
521
610
|
* @returns {ParsedSession | null}
|
|
@@ -528,19 +617,27 @@ function parseSessionName(session) {
|
|
|
528
617
|
const rest = match[2];
|
|
529
618
|
|
|
530
619
|
// Archangel: {tool}-archangel-{name}-{uuid}
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
);
|
|
620
|
+
const archangelPattern = new RegExp(`^archangel-(.+)-(${UUID_PATTERN})$`, "i");
|
|
621
|
+
const archangelMatch = rest.match(archangelPattern);
|
|
534
622
|
if (archangelMatch) {
|
|
535
623
|
return { tool, archangelName: archangelMatch[1], uuid: archangelMatch[2] };
|
|
536
624
|
}
|
|
537
625
|
|
|
538
|
-
// Partner: {tool}-partner-{uuid}
|
|
539
|
-
const
|
|
540
|
-
|
|
626
|
+
// Partner: {tool}-partner-{uuid}[-p{hash}|-yolo]
|
|
627
|
+
const partnerPattern = new RegExp(
|
|
628
|
+
`^partner-(${UUID_PATTERN})(?:-p(${PERM_HASH_PATTERN})|-(yolo))?$`,
|
|
629
|
+
"i",
|
|
541
630
|
);
|
|
631
|
+
const partnerMatch = rest.match(partnerPattern);
|
|
542
632
|
if (partnerMatch) {
|
|
543
|
-
|
|
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;
|
|
544
641
|
}
|
|
545
642
|
|
|
546
643
|
// Anything else
|
|
@@ -549,10 +646,19 @@ function parseSessionName(session) {
|
|
|
549
646
|
|
|
550
647
|
/**
|
|
551
648
|
* @param {string} tool
|
|
649
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
552
650
|
* @returns {string}
|
|
553
651
|
*/
|
|
554
|
-
function generateSessionName(tool) {
|
|
555
|
-
|
|
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}`;
|
|
556
662
|
}
|
|
557
663
|
|
|
558
664
|
/**
|
|
@@ -1150,6 +1256,54 @@ function getArchangelSessionPattern(config) {
|
|
|
1150
1256
|
return `${config.tool}-archangel-${config.name}`;
|
|
1151
1257
|
}
|
|
1152
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
|
+
|
|
1153
1307
|
// =============================================================================
|
|
1154
1308
|
// Helpers - mailbox
|
|
1155
1309
|
// =============================================================================
|
|
@@ -1166,15 +1320,25 @@ function ensureMailboxDir() {
|
|
|
1166
1320
|
}
|
|
1167
1321
|
}
|
|
1168
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* @returns {void}
|
|
1325
|
+
*/
|
|
1326
|
+
function ensureRfpDir() {
|
|
1327
|
+
if (!existsSync(RFP_DIR)) {
|
|
1328
|
+
mkdirSync(RFP_DIR, { recursive: true });
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1169
1332
|
/**
|
|
1170
1333
|
* @param {MailboxPayload} payload
|
|
1334
|
+
* @param {string} [type]
|
|
1171
1335
|
* @returns {void}
|
|
1172
1336
|
*/
|
|
1173
|
-
function writeToMailbox(payload) {
|
|
1337
|
+
function writeToMailbox(payload, type = "observation") {
|
|
1174
1338
|
ensureMailboxDir();
|
|
1175
1339
|
const entry = {
|
|
1176
1340
|
timestamp: new Date().toISOString(),
|
|
1177
|
-
type
|
|
1341
|
+
type,
|
|
1178
1342
|
payload,
|
|
1179
1343
|
};
|
|
1180
1344
|
appendFileSync(MAILBOX_PATH, JSON.stringify(entry) + "\n");
|
|
@@ -1396,8 +1560,8 @@ function findCurrentClaudeSession() {
|
|
|
1396
1560
|
|
|
1397
1561
|
// We might be running from Claude but not inside tmux (e.g., VSCode, Cursor)
|
|
1398
1562
|
// Find Claude sessions in the same cwd and pick the most recently active one
|
|
1399
|
-
const
|
|
1400
|
-
if (!
|
|
1563
|
+
const caller = findCallerAgent();
|
|
1564
|
+
if (!caller) return null;
|
|
1401
1565
|
|
|
1402
1566
|
const cwd = process.cwd();
|
|
1403
1567
|
const sessions = tmuxListSessions();
|
|
@@ -1811,6 +1975,7 @@ const State = {
|
|
|
1811
1975
|
THINKING: "thinking",
|
|
1812
1976
|
CONFIRMING: "confirming",
|
|
1813
1977
|
RATE_LIMITED: "rate_limited",
|
|
1978
|
+
FEEDBACK_MODAL: "feedback_modal",
|
|
1814
1979
|
};
|
|
1815
1980
|
|
|
1816
1981
|
/**
|
|
@@ -1838,6 +2003,17 @@ function detectState(screen, config) {
|
|
|
1838
2003
|
return State.RATE_LIMITED;
|
|
1839
2004
|
}
|
|
1840
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
|
+
|
|
1841
2017
|
// Confirming - check before THINKING because "Running…" in tool output matches thinking patterns
|
|
1842
2018
|
const confirmPatterns = config.confirmPatterns || [];
|
|
1843
2019
|
for (const pattern of confirmPatterns) {
|
|
@@ -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 quotes for shell since tmux runs the command through a shell
|
|
2168
|
+
const escaped = customAllowedTools.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}"`;
|
|
@@ -1993,43 +2175,93 @@ class Agent {
|
|
|
1993
2175
|
}
|
|
1994
2176
|
// Some agents support session ID flags for deterministic session tracking
|
|
1995
2177
|
if (this.sessionIdFlag && sessionName) {
|
|
1996
|
-
|
|
2178
|
+
const parsed = parseSessionName(sessionName);
|
|
2179
|
+
if (parsed?.uuid) {
|
|
2180
|
+
return `${base} ${this.sessionIdFlag} ${parsed.uuid}`;
|
|
2181
|
+
}
|
|
1997
2182
|
}
|
|
1998
2183
|
return base;
|
|
1999
2184
|
}
|
|
2000
2185
|
|
|
2001
|
-
|
|
2186
|
+
/**
|
|
2187
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
2188
|
+
* @returns {string | null}
|
|
2189
|
+
*/
|
|
2190
|
+
getDefaultSession({ allowedTools = null, yolo = false } = {}) {
|
|
2002
2191
|
// Check env var for explicit session
|
|
2003
2192
|
if (this.envVar && process.env[this.envVar]) {
|
|
2004
|
-
return process.env[this.envVar];
|
|
2193
|
+
return process.env[this.envVar] ?? null;
|
|
2005
2194
|
}
|
|
2006
2195
|
|
|
2007
2196
|
const cwd = process.cwd();
|
|
2008
|
-
|
|
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();
|
|
2009
2236
|
|
|
2010
|
-
|
|
2237
|
+
while (searchDir !== homeDir && searchDir !== "/") {
|
|
2238
|
+
const existing = matchingSessions.find((s) => sessionCwds.get(s) === searchDir);
|
|
2239
|
+
if (existing) return existing;
|
|
2240
|
+
|
|
2241
|
+
// Stop at git root (don't leak across projects)
|
|
2242
|
+
if (existsSync(path.join(searchDir, ".git"))) break;
|
|
2243
|
+
|
|
2244
|
+
searchDir = path.dirname(searchDir);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
return null;
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
// If inside tmux, look for existing agent session in cwd or parent
|
|
2011
2251
|
const current = tmuxCurrentSession();
|
|
2012
2252
|
if (current) {
|
|
2013
2253
|
const sessions = tmuxListSessions();
|
|
2014
|
-
const existing = sessions
|
|
2015
|
-
if (!childPattern.test(s)) return false;
|
|
2016
|
-
const sessionCwd = getTmuxSessionCwd(s);
|
|
2017
|
-
return sessionCwd === cwd;
|
|
2018
|
-
});
|
|
2254
|
+
const existing = findSessionInCwdOrParent(sessions);
|
|
2019
2255
|
if (existing) return existing;
|
|
2020
|
-
// 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
|
|
2021
2257
|
return null;
|
|
2022
2258
|
}
|
|
2023
2259
|
|
|
2024
|
-
// Walk up to find claude/codex ancestor and reuse its session
|
|
2025
|
-
const
|
|
2026
|
-
if (
|
|
2260
|
+
// Walk up to find claude/codex ancestor and reuse its session
|
|
2261
|
+
const caller = findCallerAgent();
|
|
2262
|
+
if (caller) {
|
|
2027
2263
|
const sessions = tmuxListSessions();
|
|
2028
|
-
const existing = sessions
|
|
2029
|
-
if (!childPattern.test(s)) return false;
|
|
2030
|
-
const sessionCwd = getTmuxSessionCwd(s);
|
|
2031
|
-
return sessionCwd === cwd;
|
|
2032
|
-
});
|
|
2264
|
+
const existing = findSessionInCwdOrParent(sessions);
|
|
2033
2265
|
if (existing) return existing;
|
|
2034
2266
|
}
|
|
2035
2267
|
|
|
@@ -2038,10 +2270,11 @@ class Agent {
|
|
|
2038
2270
|
}
|
|
2039
2271
|
|
|
2040
2272
|
/**
|
|
2273
|
+
* @param {{allowedTools?: string | null, yolo?: boolean}} [options]
|
|
2041
2274
|
* @returns {string}
|
|
2042
2275
|
*/
|
|
2043
|
-
generateSession() {
|
|
2044
|
-
return generateSessionName(this.name);
|
|
2276
|
+
generateSession(options = {}) {
|
|
2277
|
+
return generateSessionName(this.name, options);
|
|
2045
2278
|
}
|
|
2046
2279
|
|
|
2047
2280
|
/**
|
|
@@ -2367,8 +2600,12 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2367
2600
|
const initialScreen = tmuxCapture(session);
|
|
2368
2601
|
const initialState = agent.getState(initialScreen);
|
|
2369
2602
|
|
|
2370
|
-
//
|
|
2371
|
-
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
|
|
2372
2609
|
initialState === State.RATE_LIMITED ||
|
|
2373
2610
|
initialState === State.CONFIRMING ||
|
|
2374
2611
|
initialState === State.READY
|
|
@@ -2381,6 +2618,13 @@ async function waitUntilReady(agent, session, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
2381
2618
|
const screen = tmuxCapture(session);
|
|
2382
2619
|
const state = agent.getState(screen);
|
|
2383
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
|
+
|
|
2384
2628
|
if (state === State.RATE_LIMITED || state === State.CONFIRMING || state === State.READY) {
|
|
2385
2629
|
return { state, screen };
|
|
2386
2630
|
}
|
|
@@ -2421,6 +2665,13 @@ async function pollForResponse(agent, session, timeoutMs, hooks = {}) {
|
|
|
2421
2665
|
return { state, screen };
|
|
2422
2666
|
}
|
|
2423
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
|
+
|
|
2424
2675
|
if (screen !== lastScreen) {
|
|
2425
2676
|
lastScreen = screen;
|
|
2426
2677
|
stableAt = Date.now();
|
|
@@ -2530,6 +2781,7 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2530
2781
|
continue;
|
|
2531
2782
|
}
|
|
2532
2783
|
|
|
2784
|
+
// FEEDBACK_MODAL is handled by the underlying waitFn (pollForResponse)
|
|
2533
2785
|
debugError("autoApproveLoop", new Error(`unexpected state: ${state}`));
|
|
2534
2786
|
}
|
|
2535
2787
|
|
|
@@ -2541,12 +2793,13 @@ async function autoApproveLoop(agent, session, timeoutMs, waitFn) {
|
|
|
2541
2793
|
* @param {string | null | undefined} session
|
|
2542
2794
|
* @param {Object} [options]
|
|
2543
2795
|
* @param {boolean} [options.yolo]
|
|
2796
|
+
* @param {string | null} [options.allowedTools]
|
|
2544
2797
|
* @returns {Promise<string>}
|
|
2545
2798
|
*/
|
|
2546
|
-
async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
2799
|
+
async function cmdStart(agent, session, { yolo = false, allowedTools = null } = {}) {
|
|
2547
2800
|
// Generate session name if not provided
|
|
2548
2801
|
if (!session) {
|
|
2549
|
-
session = agent.generateSession();
|
|
2802
|
+
session = agent.generateSession({ allowedTools, yolo });
|
|
2550
2803
|
}
|
|
2551
2804
|
|
|
2552
2805
|
if (tmuxHasSession(session)) return session;
|
|
@@ -2558,7 +2811,7 @@ async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
|
2558
2811
|
process.exit(1);
|
|
2559
2812
|
}
|
|
2560
2813
|
|
|
2561
|
-
const command = agent.getCommand(yolo, session);
|
|
2814
|
+
const command = agent.getCommand(yolo, session, allowedTools);
|
|
2562
2815
|
tmuxNewSession(session, command);
|
|
2563
2816
|
|
|
2564
2817
|
const start = Date.now();
|
|
@@ -2571,6 +2824,18 @@ async function cmdStart(agent, session, { yolo = false } = {}) {
|
|
|
2571
2824
|
continue;
|
|
2572
2825
|
}
|
|
2573
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
|
+
|
|
2574
2839
|
if (state === State.READY) return session;
|
|
2575
2840
|
|
|
2576
2841
|
await sleep(POLL_MS);
|
|
@@ -2711,6 +2976,21 @@ function startArchangel(config, parentSession = null) {
|
|
|
2711
2976
|
);
|
|
2712
2977
|
}
|
|
2713
2978
|
|
|
2979
|
+
/**
|
|
2980
|
+
* @param {string} pattern
|
|
2981
|
+
* @param {number} [timeoutMs]
|
|
2982
|
+
* @returns {Promise<string | undefined>}
|
|
2983
|
+
*/
|
|
2984
|
+
async function waitForArchangelSession(pattern, timeoutMs = ARCHANGEL_STARTUP_TIMEOUT_MS) {
|
|
2985
|
+
const start = Date.now();
|
|
2986
|
+
while (Date.now() - start < timeoutMs) {
|
|
2987
|
+
const session = findArchangelSession(pattern);
|
|
2988
|
+
if (session) return session;
|
|
2989
|
+
await sleep(200);
|
|
2990
|
+
}
|
|
2991
|
+
return undefined;
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2714
2994
|
// =============================================================================
|
|
2715
2995
|
// Command: archangel (runs as the archangel process itself)
|
|
2716
2996
|
// =============================================================================
|
|
@@ -3161,6 +3441,7 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
|
3161
3441
|
import { dirname, join } from "node:path";
|
|
3162
3442
|
import { fileURLToPath } from "node:url";
|
|
3163
3443
|
import { createHash } from "node:crypto";
|
|
3444
|
+
import { execSync } from "node:child_process";
|
|
3164
3445
|
|
|
3165
3446
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
3166
3447
|
const AI_DIR = join(__dirname, "..");
|
|
@@ -3168,6 +3449,15 @@ const DEBUG = process.env.AX_DEBUG === "1";
|
|
|
3168
3449
|
const MAILBOX = join(AI_DIR, "mailbox.jsonl");
|
|
3169
3450
|
const MAX_AGE_MS = 60 * 60 * 1000;
|
|
3170
3451
|
|
|
3452
|
+
function getTmuxSessionName() {
|
|
3453
|
+
if (!process.env.TMUX) return null;
|
|
3454
|
+
try {
|
|
3455
|
+
return execSync("tmux display-message -p '#S'", { encoding: "utf-8" }).trim();
|
|
3456
|
+
} catch {
|
|
3457
|
+
return null;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3171
3461
|
// Read hook input from stdin
|
|
3172
3462
|
let hookInput = {};
|
|
3173
3463
|
try {
|
|
@@ -3182,8 +3472,9 @@ const hookEvent = hookInput.hook_event_name || "";
|
|
|
3182
3472
|
|
|
3183
3473
|
if (DEBUG) console.error("[hook] session:", sessionId, "event:", hookEvent);
|
|
3184
3474
|
|
|
3185
|
-
|
|
3186
|
-
if (
|
|
3475
|
+
const tmuxSession = getTmuxSessionName();
|
|
3476
|
+
if (DEBUG) console.error("[hook] tmux session:", tmuxSession);
|
|
3477
|
+
if (tmuxSession && (tmuxSession.includes("-archangel-") || tmuxSession.includes("-partner-"))) {
|
|
3187
3478
|
if (DEBUG) console.error("[hook] skipping non-parent session");
|
|
3188
3479
|
process.exit(0);
|
|
3189
3480
|
}
|
|
@@ -3713,7 +4004,13 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3713
4004
|
console.log(`**Branch**: ${p.branch || "?"} @ ${p.commit || "?"}\n`);
|
|
3714
4005
|
}
|
|
3715
4006
|
|
|
3716
|
-
if (p.
|
|
4007
|
+
if (p.rfpId) {
|
|
4008
|
+
console.log(`**RFP**: ${p.rfpId}\n`);
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
if (entry.type === "proposal") {
|
|
4012
|
+
console.log(`**Proposal**: ${p.message || ""}\n`);
|
|
4013
|
+
} else if (p.message) {
|
|
3717
4014
|
console.log(`**Assistant**: ${p.message}\n`);
|
|
3718
4015
|
}
|
|
3719
4016
|
|
|
@@ -3727,13 +4024,246 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
|
|
|
3727
4024
|
}
|
|
3728
4025
|
}
|
|
3729
4026
|
|
|
4027
|
+
/**
|
|
4028
|
+
* @param {string} rfpId
|
|
4029
|
+
* @param {string} archangel
|
|
4030
|
+
* @returns {string | null}
|
|
4031
|
+
*/
|
|
4032
|
+
function getProposalFromMailbox(rfpId, archangel) {
|
|
4033
|
+
if (!existsSync(MAILBOX_PATH)) return null;
|
|
4034
|
+
let result = null;
|
|
4035
|
+
try {
|
|
4036
|
+
const lines = readFileSync(MAILBOX_PATH, "utf-8").trim().split("\n").filter(Boolean);
|
|
4037
|
+
for (const line of lines) {
|
|
4038
|
+
try {
|
|
4039
|
+
const entry = JSON.parse(line);
|
|
4040
|
+
if (entry?.type !== "proposal") continue;
|
|
4041
|
+
const p = entry.payload || {};
|
|
4042
|
+
if (p.rfpId === rfpId && p.archangel === archangel) {
|
|
4043
|
+
result = p.message || "";
|
|
4044
|
+
}
|
|
4045
|
+
} catch {
|
|
4046
|
+
// Skip malformed lines
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
} catch (err) {
|
|
4050
|
+
debugError("getProposalFromMailbox", err);
|
|
4051
|
+
}
|
|
4052
|
+
return result;
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
/**
|
|
4056
|
+
* @param {string} prompt
|
|
4057
|
+
* @param {{archangels?: string, fresh?: boolean, noWait?: boolean}} [options]
|
|
4058
|
+
*/
|
|
4059
|
+
async function cmdRfp(prompt, { archangels, fresh = false, noWait = false } = {}) {
|
|
4060
|
+
const configs = loadAgentConfigs();
|
|
4061
|
+
if (configs.length === 0) {
|
|
4062
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
4063
|
+
process.exit(1);
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
const requested = archangels
|
|
4067
|
+
? archangels
|
|
4068
|
+
.split(",")
|
|
4069
|
+
.map((s) => s.trim())
|
|
4070
|
+
.filter(Boolean)
|
|
4071
|
+
: configs.map((c) => c.name);
|
|
4072
|
+
|
|
4073
|
+
if (requested.length === 0) {
|
|
4074
|
+
console.log("ERROR: no archangels specified");
|
|
4075
|
+
process.exit(1);
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
const missing = requested.filter((name) => !configs.some((c) => c.name === name));
|
|
4079
|
+
if (missing.length > 0) {
|
|
4080
|
+
console.log(`ERROR: unknown archangel(s): ${missing.join(", ")}`);
|
|
4081
|
+
process.exit(1);
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
const parent = findParentSession();
|
|
4085
|
+
const rfpId = generateRfpId(parent);
|
|
4086
|
+
|
|
4087
|
+
for (const name of requested) {
|
|
4088
|
+
const config = configs.find((c) => c.name === name);
|
|
4089
|
+
if (!config) continue;
|
|
4090
|
+
|
|
4091
|
+
const pattern = getArchangelSessionPattern(config);
|
|
4092
|
+
let session = findArchangelSession(pattern);
|
|
4093
|
+
if (!session) {
|
|
4094
|
+
startArchangel(config, parent);
|
|
4095
|
+
session = await waitForArchangelSession(pattern);
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
if (!session) {
|
|
4099
|
+
console.log(`ERROR: failed to start archangel '${name}'`);
|
|
4100
|
+
continue;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
const { agent } = resolveAgent({ sessionName: session });
|
|
4104
|
+
|
|
4105
|
+
if (fresh) {
|
|
4106
|
+
tmuxSendLiteral(session, "/new");
|
|
4107
|
+
await sleep(50);
|
|
4108
|
+
tmuxSend(session, "Enter");
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
const ready = await waitUntilReady(agent, session, ARCHANGEL_STARTUP_TIMEOUT_MS);
|
|
4112
|
+
if (ready.state !== State.READY) {
|
|
4113
|
+
console.log(`[rfp] ${name} not ready (${ready.state}), skipping`);
|
|
4114
|
+
continue;
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
const rfpPrompt = `## RFP ${rfpId}\n\n${RFP_PREAMBLE}\n\n${prompt}\n\nReturn exactly one proposal.`;
|
|
4118
|
+
tmuxSendLiteral(session, rfpPrompt);
|
|
4119
|
+
await sleep(200);
|
|
4120
|
+
tmuxSend(session, "Enter");
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
writeRfpRecord(rfpId, prompt);
|
|
4124
|
+
const archangelList = requested.join(",");
|
|
4125
|
+
const base = rfpId.split("-")[1];
|
|
4126
|
+
const shortId = `rfp-${base}`;
|
|
4127
|
+
console.log(`rfp: ${rfpId} (${archangelList})`);
|
|
4128
|
+
if (noWait) {
|
|
4129
|
+
// Truncate prompt for display (first line, max 60 chars)
|
|
4130
|
+
const firstLine = prompt.split("\n")[0];
|
|
4131
|
+
const taskPreview = firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
|
|
4132
|
+
|
|
4133
|
+
let output = `Task: ${taskPreview}
|
|
4134
|
+
|
|
4135
|
+
Add this to your task list, then continue with your work.`;
|
|
4136
|
+
|
|
4137
|
+
// Show TaskCreate example if caller is Claude
|
|
4138
|
+
const caller = findCallerAgent();
|
|
4139
|
+
if (caller?.agent === "claude") {
|
|
4140
|
+
output += `
|
|
4141
|
+
|
|
4142
|
+
e.g.
|
|
4143
|
+
TaskCreate({ subject: "ax:${shortId} - ${taskPreview}" })
|
|
4144
|
+
`;
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
console.log(output);
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
/**
|
|
4152
|
+
* @param {string} rfpId
|
|
4153
|
+
* @param {{archangels?: string, timeoutMs?: number}} [options]
|
|
4154
|
+
*/
|
|
4155
|
+
async function cmdRfpWait(rfpId, { archangels, timeoutMs = ARCHANGEL_RESPONSE_TIMEOUT_MS } = {}) {
|
|
4156
|
+
const resolvedRfpId = resolveRfpId(rfpId);
|
|
4157
|
+
const configs = loadAgentConfigs();
|
|
4158
|
+
if (configs.length === 0) {
|
|
4159
|
+
console.log(`No archangels found in ${AGENTS_DIR}/`);
|
|
4160
|
+
process.exit(1);
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
const requested = archangels
|
|
4164
|
+
? archangels
|
|
4165
|
+
.split(",")
|
|
4166
|
+
.map((s) => s.trim())
|
|
4167
|
+
.filter(Boolean)
|
|
4168
|
+
: configs.map((c) => c.name);
|
|
4169
|
+
|
|
4170
|
+
if (requested.length === 0) {
|
|
4171
|
+
console.log("ERROR: no archangels specified");
|
|
4172
|
+
process.exit(1);
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
const missing = requested.filter((name) => !configs.some((c) => c.name === name));
|
|
4176
|
+
if (missing.length > 0) {
|
|
4177
|
+
console.log(`ERROR: unknown archangel(s): ${missing.join(", ")}`);
|
|
4178
|
+
process.exit(1);
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
let wroteAny = false;
|
|
4182
|
+
let printedAny = false;
|
|
4183
|
+
|
|
4184
|
+
for (const name of requested) {
|
|
4185
|
+
const config = configs.find((c) => c.name === name);
|
|
4186
|
+
if (!config) continue;
|
|
4187
|
+
|
|
4188
|
+
const pattern = getArchangelSessionPattern(config);
|
|
4189
|
+
const session = findArchangelSession(pattern);
|
|
4190
|
+
if (!session) {
|
|
4191
|
+
console.log(`[rfp] ${name} session not found, skipping`);
|
|
4192
|
+
continue;
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
const existing = getProposalFromMailbox(resolvedRfpId, name);
|
|
4196
|
+
if (existing !== null) {
|
|
4197
|
+
if (printedAny) console.log("");
|
|
4198
|
+
console.log(`[${name}]`);
|
|
4199
|
+
console.log(existing);
|
|
4200
|
+
wroteAny = true;
|
|
4201
|
+
printedAny = true;
|
|
4202
|
+
continue;
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
const { agent } = resolveAgent({ sessionName: session });
|
|
4206
|
+
let result;
|
|
4207
|
+
try {
|
|
4208
|
+
result = await waitUntilReady(agent, session, timeoutMs);
|
|
4209
|
+
} catch (err) {
|
|
4210
|
+
if (err instanceof TimeoutError) {
|
|
4211
|
+
console.log(`[rfp] ${name} timed out`);
|
|
4212
|
+
} else {
|
|
4213
|
+
console.log(`[rfp] ${name} error: ${err instanceof Error ? err.message : err}`);
|
|
4214
|
+
}
|
|
4215
|
+
continue;
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
if (result.state === State.RATE_LIMITED) {
|
|
4219
|
+
console.log(`[rfp] ${name} rate limited`);
|
|
4220
|
+
continue;
|
|
4221
|
+
}
|
|
4222
|
+
if (result.state === State.CONFIRMING) {
|
|
4223
|
+
console.log(`[rfp] ${name} awaiting confirmation`);
|
|
4224
|
+
continue;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
const response = agent.getResponse(session, result.screen) || "";
|
|
4228
|
+
if (!response || response.trim() === "EMPTY_RESPONSE") {
|
|
4229
|
+
continue;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
writeToMailbox(
|
|
4233
|
+
{
|
|
4234
|
+
agent: name,
|
|
4235
|
+
session,
|
|
4236
|
+
branch: getCurrentBranch(),
|
|
4237
|
+
commit: getCurrentCommit(),
|
|
4238
|
+
files: [],
|
|
4239
|
+
message: response,
|
|
4240
|
+
rfpId: resolvedRfpId,
|
|
4241
|
+
archangel: name,
|
|
4242
|
+
},
|
|
4243
|
+
"proposal",
|
|
4244
|
+
);
|
|
4245
|
+
if (printedAny) console.log("");
|
|
4246
|
+
console.log(`[${name}]`);
|
|
4247
|
+
console.log(response);
|
|
4248
|
+
wroteAny = true;
|
|
4249
|
+
printedAny = true;
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
if (!wroteAny) process.exit(1);
|
|
4253
|
+
}
|
|
4254
|
+
|
|
3730
4255
|
/**
|
|
3731
4256
|
* @param {Agent} agent
|
|
3732
4257
|
* @param {string | null | undefined} session
|
|
3733
4258
|
* @param {string} message
|
|
3734
|
-
* @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
|
|
4259
|
+
* @param {{noWait?: boolean, yolo?: boolean, allowedTools?: string | null, timeoutMs?: number}} [options]
|
|
3735
4260
|
*/
|
|
3736
|
-
async function cmdAsk(
|
|
4261
|
+
async function cmdAsk(
|
|
4262
|
+
agent,
|
|
4263
|
+
session,
|
|
4264
|
+
message,
|
|
4265
|
+
{ noWait = false, yolo = false, allowedTools = null, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
|
|
4266
|
+
) {
|
|
3737
4267
|
const sessionExists = session != null && tmuxHasSession(session);
|
|
3738
4268
|
const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
|
|
3739
4269
|
|
|
@@ -3747,7 +4277,7 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3747
4277
|
/** @type {string} */
|
|
3748
4278
|
const activeSession = sessionExists
|
|
3749
4279
|
? /** @type {string} */ (session)
|
|
3750
|
-
: await cmdStart(agent, session, { yolo });
|
|
4280
|
+
: await cmdStart(agent, session, { yolo, allowedTools });
|
|
3751
4281
|
|
|
3752
4282
|
tmuxSendLiteral(activeSession, message);
|
|
3753
4283
|
await sleep(50);
|
|
@@ -3756,12 +4286,26 @@ async function cmdAsk(agent, session, message, { noWait = false, yolo = false, t
|
|
|
3756
4286
|
if (noWait) {
|
|
3757
4287
|
const parsed = parseSessionName(activeSession);
|
|
3758
4288
|
const shortId = parsed?.uuid?.slice(0, 8) || activeSession;
|
|
3759
|
-
|
|
3760
|
-
|
|
4289
|
+
// Truncate message for display (first line, max 60 chars)
|
|
4290
|
+
const firstLine = message.split("\n")[0];
|
|
4291
|
+
const taskPreview = firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
|
|
4292
|
+
|
|
4293
|
+
let output = `Sent to: ${shortId}
|
|
4294
|
+
Task: ${taskPreview}
|
|
4295
|
+
|
|
4296
|
+
Add this to your task list, then continue with your work.`;
|
|
4297
|
+
|
|
4298
|
+
// Show TaskCreate example if caller is Claude
|
|
4299
|
+
const caller = findCallerAgent();
|
|
4300
|
+
if (caller?.agent === "claude") {
|
|
4301
|
+
output += `
|
|
3761
4302
|
|
|
3762
4303
|
e.g.
|
|
3763
|
-
|
|
3764
|
-
|
|
4304
|
+
TaskCreate({ subject: "ax:${shortId} - ${taskPreview}" })
|
|
4305
|
+
`;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
console.log(output);
|
|
3765
4309
|
return;
|
|
3766
4310
|
}
|
|
3767
4311
|
|
|
@@ -3953,9 +4497,14 @@ async function cmdReview(
|
|
|
3953
4497
|
* @param {Agent} agent
|
|
3954
4498
|
* @param {string | null | undefined} session
|
|
3955
4499
|
* @param {number} [index]
|
|
3956
|
-
* @param {{wait?: boolean, timeoutMs?: number}} [options]
|
|
4500
|
+
* @param {{wait?: boolean, stale?: boolean, timeoutMs?: number}} [options]
|
|
3957
4501
|
*/
|
|
3958
|
-
async function cmdOutput(
|
|
4502
|
+
async function cmdOutput(
|
|
4503
|
+
agent,
|
|
4504
|
+
session,
|
|
4505
|
+
index = 0,
|
|
4506
|
+
{ wait = false, stale = false, timeoutMs } = {},
|
|
4507
|
+
) {
|
|
3959
4508
|
if (!session || !tmuxHasSession(session)) {
|
|
3960
4509
|
console.log("ERROR: no session");
|
|
3961
4510
|
process.exit(1);
|
|
@@ -3982,8 +4531,11 @@ async function cmdOutput(agent, session, index = 0, { wait = false, timeoutMs }
|
|
|
3982
4531
|
}
|
|
3983
4532
|
|
|
3984
4533
|
if (state === State.THINKING) {
|
|
3985
|
-
|
|
3986
|
-
|
|
4534
|
+
if (!stale) {
|
|
4535
|
+
console.log("THINKING: Use --wait to block, or --stale for old response.");
|
|
4536
|
+
process.exit(1);
|
|
4537
|
+
}
|
|
4538
|
+
// --stale: fall through to show previous response
|
|
3987
4539
|
}
|
|
3988
4540
|
|
|
3989
4541
|
const output = agent.getResponse(session, screen, index);
|
|
@@ -4181,7 +4733,12 @@ function resolveAgent({ toolFlag, sessionName } = {}) {
|
|
|
4181
4733
|
if (invoked === "axclaude" || invoked === "claude") return { agent: ClaudeAgent };
|
|
4182
4734
|
if (invoked === "axcodex" || invoked === "codex") return { agent: CodexAgent };
|
|
4183
4735
|
|
|
4184
|
-
// 4.
|
|
4736
|
+
// 4. Infer from parent process (running from within claude/codex)
|
|
4737
|
+
const caller = findCallerAgent();
|
|
4738
|
+
if (caller?.agent === "claude") return { agent: ClaudeAgent };
|
|
4739
|
+
if (caller?.agent === "codex") return { agent: CodexAgent };
|
|
4740
|
+
|
|
4741
|
+
// 5. AX_DEFAULT_TOOL environment variable
|
|
4185
4742
|
const defaultTool = process.env.AX_DEFAULT_TOOL;
|
|
4186
4743
|
if (defaultTool === "claude") return { agent: ClaudeAgent };
|
|
4187
4744
|
if (defaultTool === "codex" || !defaultTool) return { agent: CodexAgent };
|
|
@@ -4197,72 +4754,64 @@ function resolveAgent({ toolFlag, sessionName } = {}) {
|
|
|
4197
4754
|
function printHelp(agent, cliName) {
|
|
4198
4755
|
const name = cliName;
|
|
4199
4756
|
const backendName = agent.displayName;
|
|
4200
|
-
const hasReview = !!agent.reviewOptions;
|
|
4201
4757
|
|
|
4202
4758
|
console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
|
|
4203
4759
|
|
|
4204
4760
|
Usage: ${name} [OPTIONS] <command|message> [ARGS...]
|
|
4205
4761
|
|
|
4206
|
-
|
|
4207
|
-
|
|
4762
|
+
Messaging:
|
|
4763
|
+
<message> Send message to ${name}
|
|
4764
|
+
review [TYPE] Review code: pr, uncommitted, commit, custom
|
|
4765
|
+
|
|
4766
|
+
Sessions:
|
|
4767
|
+
compact Summarise session to shrink context size
|
|
4768
|
+
reset Start fresh conversation
|
|
4769
|
+
agents List all running agents
|
|
4208
4770
|
target Show default target session for current tool
|
|
4209
4771
|
attach [SESSION] Attach to agent session interactively
|
|
4210
|
-
|
|
4211
|
-
|
|
4772
|
+
kill Kill sessions (--all, --session=NAME, --orphans [--force])
|
|
4773
|
+
|
|
4774
|
+
Archangels:
|
|
4212
4775
|
summon [name] Summon archangels (all, or by name)
|
|
4213
4776
|
recall [name] Recall archangels (all, or by name)
|
|
4214
|
-
|
|
4215
|
-
|
|
4777
|
+
mailbox Archangel notes (filters: --branch=git, --all)
|
|
4778
|
+
rfp <prompt> Request proposals (--archangels=a,b)
|
|
4779
|
+
rfp wait <id> Wait for proposals (--archangels=a,b)
|
|
4780
|
+
|
|
4781
|
+
Recovery/State:
|
|
4782
|
+
status Exit code: ready=0 rate_limit=2 confirm=3 thinking=4
|
|
4216
4783
|
output [-N] Show response (0=last, -1=prev, -2=older)
|
|
4217
|
-
debug Show raw screen output and detected state
|
|
4218
|
-
hasReview
|
|
4219
|
-
? `
|
|
4220
|
-
review [TYPE] Review code: pr, uncommitted, commit, custom`
|
|
4221
|
-
: ""
|
|
4222
|
-
}
|
|
4223
|
-
select N Select menu option N
|
|
4784
|
+
debug Show raw screen output and detected state
|
|
4224
4785
|
approve Approve pending action (send 'y')
|
|
4225
4786
|
reject Reject pending action (send 'n')
|
|
4787
|
+
select N Select menu option N
|
|
4226
4788
|
send KEYS Send key sequence (e.g. "1[Enter]", "[Escape]")
|
|
4227
|
-
|
|
4228
|
-
reset Start fresh conversation
|
|
4229
|
-
<message> Send message to ${name}
|
|
4789
|
+
log [SESSION] View conversation log (--tail=N, --follow, --reasoning)
|
|
4230
4790
|
|
|
4231
4791
|
Flags:
|
|
4232
4792
|
--tool=NAME Use specific agent (codex, claude)
|
|
4233
|
-
--session=
|
|
4793
|
+
--session=ID name | archangel | uuid-prefix | self
|
|
4794
|
+
--fresh Reset conversation before review
|
|
4795
|
+
--yolo Skip all confirmations (dangerous)
|
|
4796
|
+
--auto-approve=TOOLS Auto-approve specific tools (e.g. 'Bash("cargo *")')
|
|
4234
4797
|
--wait Wait for response (default for messages; required for approve/reject)
|
|
4235
4798
|
--no-wait Fire-and-forget: send message, print session ID, exit immediately
|
|
4236
4799
|
--timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
|
|
4237
|
-
--yolo Skip all confirmations (dangerous)
|
|
4238
|
-
--fresh Reset conversation before review
|
|
4239
|
-
--orphans Kill orphaned claude/codex processes (PPID=1)
|
|
4240
|
-
--force Use SIGKILL instead of SIGTERM (with --orphans)
|
|
4241
|
-
|
|
4242
|
-
Environment:
|
|
4243
|
-
AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
|
|
4244
|
-
${agent.envVar} Override default session name
|
|
4245
|
-
AX_CLAUDE_CONFIG_DIR Override Claude config directory (default: ~/.claude)
|
|
4246
|
-
AX_CODEX_CONFIG_DIR Override Codex config directory (default: ~/.codex)
|
|
4247
|
-
AX_REVIEW_MODE=exec Bypass /review, send instructions directly (codex only)
|
|
4248
|
-
AX_DEBUG=1 Enable debug logging
|
|
4249
4800
|
|
|
4250
4801
|
Examples:
|
|
4251
4802
|
${name} "explain this codebase"
|
|
4252
|
-
${name} "review the error handling"
|
|
4253
|
-
${name} "FYI: auth was refactored" --no-wait
|
|
4803
|
+
${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
|
|
4804
|
+
${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
|
|
4805
|
+
${name} --auto-approve='Bash("cargo *")' "run tests" # Session with specific permissions
|
|
4254
4806
|
${name} review uncommitted --wait
|
|
4255
|
-
${name}
|
|
4256
|
-
${name} kill
|
|
4257
|
-
${name} kill --
|
|
4258
|
-
${name}
|
|
4259
|
-
${name}
|
|
4260
|
-
${name}
|
|
4261
|
-
${name}
|
|
4262
|
-
${name}
|
|
4263
|
-
${name} recall # Recall all archangels
|
|
4264
|
-
${name} recall reviewer # Recall one by name
|
|
4265
|
-
${name} agents # List all agents (shows TYPE=archangel)
|
|
4807
|
+
${name} kill # Kill agents in current project
|
|
4808
|
+
${name} kill --all # Kill all agents across all projects
|
|
4809
|
+
${name} kill --session=NAME # Kill specific session
|
|
4810
|
+
${name} summon # Summon all archangels from .ai/agents/*.md
|
|
4811
|
+
${name} summon reviewer # Summon by name (creates config if new)
|
|
4812
|
+
${name} recall # Recall all archangels
|
|
4813
|
+
${name} recall reviewer # Recall one by name
|
|
4814
|
+
${name} agents # List all agents (shows TYPE=archangel)
|
|
4266
4815
|
|
|
4267
4816
|
Note: Reviews and complex tasks may take several minutes.
|
|
4268
4817
|
Use Bash run_in_background for long operations (not --no-wait).`);
|
|
@@ -4289,7 +4838,8 @@ async function main() {
|
|
|
4289
4838
|
}
|
|
4290
4839
|
|
|
4291
4840
|
// Extract flags into local variables for convenience
|
|
4292
|
-
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force } =
|
|
4841
|
+
const { wait, noWait, yolo, fresh, reasoning, follow, all, orphans, force, stale, autoApprove } =
|
|
4842
|
+
flags;
|
|
4293
4843
|
|
|
4294
4844
|
// Session resolution (must happen before agent resolution so we can infer tool from session name)
|
|
4295
4845
|
let session = null;
|
|
@@ -4308,15 +4858,18 @@ async function main() {
|
|
|
4308
4858
|
}
|
|
4309
4859
|
|
|
4310
4860
|
// Agent resolution (considers --tool flag, session name, invocation, and env vars)
|
|
4311
|
-
const { agent, error: agentError } = resolveAgent({
|
|
4861
|
+
const { agent, error: agentError } = resolveAgent({
|
|
4862
|
+
toolFlag: flags.tool,
|
|
4863
|
+
sessionName: session,
|
|
4864
|
+
});
|
|
4312
4865
|
if (agentError) {
|
|
4313
4866
|
console.log(`ERROR: ${agentError}`);
|
|
4314
4867
|
process.exit(1);
|
|
4315
4868
|
}
|
|
4316
4869
|
|
|
4317
|
-
// If no explicit session, use agent's default
|
|
4870
|
+
// If no explicit session, use agent's default (with permission filtering)
|
|
4318
4871
|
if (!session) {
|
|
4319
|
-
session = agent.getDefaultSession();
|
|
4872
|
+
session = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
4320
4873
|
}
|
|
4321
4874
|
|
|
4322
4875
|
// Timeout (convert seconds to milliseconds)
|
|
@@ -4344,7 +4897,7 @@ async function main() {
|
|
|
4344
4897
|
// Dispatch commands
|
|
4345
4898
|
if (cmd === "agents") return cmdAgents();
|
|
4346
4899
|
if (cmd === "target") {
|
|
4347
|
-
const defaultSession = agent.getDefaultSession();
|
|
4900
|
+
const defaultSession = agent.getDefaultSession({ allowedTools: autoApprove, yolo });
|
|
4348
4901
|
if (defaultSession) {
|
|
4349
4902
|
console.log(defaultSession);
|
|
4350
4903
|
} else {
|
|
@@ -4360,20 +4913,39 @@ async function main() {
|
|
|
4360
4913
|
if (cmd === "attach") return cmdAttach(positionals[1] || session);
|
|
4361
4914
|
if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
|
|
4362
4915
|
if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
|
|
4916
|
+
if (cmd === "rfp") {
|
|
4917
|
+
if (positionals[1] === "wait") {
|
|
4918
|
+
const rfpId = positionals[2];
|
|
4919
|
+
if (!rfpId) {
|
|
4920
|
+
console.log("ERROR: missing rfp id");
|
|
4921
|
+
process.exit(1);
|
|
4922
|
+
}
|
|
4923
|
+
return cmdRfpWait(rfpId, { archangels: flags.archangels, timeoutMs });
|
|
4924
|
+
}
|
|
4925
|
+
const rawPrompt = positionals.slice(1).join(" ");
|
|
4926
|
+
const prompt = await readStdinIfNeeded(rawPrompt);
|
|
4927
|
+
if (!prompt) {
|
|
4928
|
+
console.log("ERROR: missing prompt for rfp");
|
|
4929
|
+
process.exit(1);
|
|
4930
|
+
}
|
|
4931
|
+
return cmdRfp(prompt, { archangels: flags.archangels, fresh, noWait });
|
|
4932
|
+
}
|
|
4363
4933
|
if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
|
|
4364
4934
|
if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
|
|
4365
|
-
if (cmd === "review")
|
|
4366
|
-
|
|
4935
|
+
if (cmd === "review") {
|
|
4936
|
+
const customInstructions = await readStdinIfNeeded(positionals[2]);
|
|
4937
|
+
return cmdReview(agent, session, positionals[1], customInstructions ?? undefined, {
|
|
4367
4938
|
wait,
|
|
4368
4939
|
fresh,
|
|
4369
4940
|
timeoutMs,
|
|
4370
4941
|
});
|
|
4942
|
+
}
|
|
4371
4943
|
if (cmd === "status") return cmdStatus(agent, session);
|
|
4372
4944
|
if (cmd === "debug") return cmdDebug(agent, session);
|
|
4373
4945
|
if (cmd === "output") {
|
|
4374
4946
|
const indexArg = positionals[1];
|
|
4375
4947
|
const index = indexArg?.startsWith("-") ? parseInt(indexArg, 10) : 0;
|
|
4376
|
-
return cmdOutput(agent, session, index, { wait, timeoutMs });
|
|
4948
|
+
return cmdOutput(agent, session, index, { wait, stale, timeoutMs });
|
|
4377
4949
|
}
|
|
4378
4950
|
if (cmd === "send" && positionals.length > 1)
|
|
4379
4951
|
return cmdSend(session, positionals.slice(1).join(" "));
|
|
@@ -4383,18 +4955,17 @@ async function main() {
|
|
|
4383
4955
|
return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
|
|
4384
4956
|
|
|
4385
4957
|
// Default: send message
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
message = await readStdin();
|
|
4389
|
-
}
|
|
4958
|
+
const rawMessage = positionals.join(" ");
|
|
4959
|
+
let message = await readStdinIfNeeded(rawMessage);
|
|
4390
4960
|
|
|
4391
4961
|
if (!message || flags.help) {
|
|
4392
4962
|
printHelp(agent, cliName);
|
|
4393
4963
|
process.exit(0);
|
|
4394
4964
|
}
|
|
4965
|
+
const messageText = message;
|
|
4395
4966
|
|
|
4396
4967
|
// Detect "review ..." or "please review ..." and route to custom review mode
|
|
4397
|
-
const reviewMatch =
|
|
4968
|
+
const reviewMatch = messageText.match(/^(?:please )?review\s*(.*)/i);
|
|
4398
4969
|
if (reviewMatch && agent.reviewOptions) {
|
|
4399
4970
|
const customInstructions = reviewMatch[1].trim() || null;
|
|
4400
4971
|
return cmdReview(agent, session, "custom", customInstructions, {
|
|
@@ -4404,7 +4975,12 @@ async function main() {
|
|
|
4404
4975
|
});
|
|
4405
4976
|
}
|
|
4406
4977
|
|
|
4407
|
-
return cmdAsk(agent, session,
|
|
4978
|
+
return cmdAsk(agent, session, messageText, {
|
|
4979
|
+
noWait,
|
|
4980
|
+
yolo,
|
|
4981
|
+
allowedTools: autoApprove,
|
|
4982
|
+
timeoutMs,
|
|
4983
|
+
});
|
|
4408
4984
|
}
|
|
4409
4985
|
|
|
4410
4986
|
// Run main() only when executed directly (not when imported for testing)
|
|
@@ -4444,4 +5020,6 @@ export {
|
|
|
4444
5020
|
extractThinking,
|
|
4445
5021
|
detectState,
|
|
4446
5022
|
State,
|
|
5023
|
+
normalizeAllowedTools,
|
|
5024
|
+
computePermissionHash,
|
|
4447
5025
|
};
|