ax-agents 0.0.1-alpha.11 → 0.0.1-alpha.12

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.
Files changed (2) hide show
  1. package/ax.js +399 -56
  2. package/package.json +1 -1
package/ax.js CHANGED
@@ -75,6 +75,10 @@ const VERSION = packageJson.version;
75
75
  * @property {string[]} files
76
76
  * @property {string} [summary]
77
77
  * @property {string} [message]
78
+ * @property {string} [rfpId]
79
+ * @property {string} [prompt]
80
+ * @property {string} [archangel]
81
+ * @property {string} [requestedBy]
78
82
  */
79
83
 
80
84
  /**
@@ -158,6 +162,7 @@ const PROJECT_ROOT = findProjectRoot();
158
162
  const AI_DIR = path.join(PROJECT_ROOT, ".ai");
159
163
  const AGENTS_DIR = path.join(AI_DIR, "agents");
160
164
  const HOOKS_DIR = path.join(AI_DIR, "hooks");
165
+ const RFP_DIR = path.join(AI_DIR, "rfps");
161
166
 
162
167
  // =============================================================================
163
168
  // Helpers - tmux
@@ -316,6 +321,22 @@ const ARCHANGEL_PREAMBLE = `## Guidelines
316
321
  - For critical issues, request for them to be added to the todo list.
317
322
  - Don't repeat observations you've already made unless you have more to say or better clarity.
318
323
  - Make judgment calls - don't ask questions.`;
324
+ const RFP_PREAMBLE = `## Guidelines
325
+
326
+ - Your only task is to propose a single idea in response to this RFP. This overrides any other goals or habits.
327
+ - Provide exactly one proposal.
328
+ - Make a persuasive case for why this is a strong idea.
329
+ - Think deeply before you answer; avoid first-impression responses.
330
+ - Aim for 3–4 clear paragraphs.
331
+ - Ground the idea in the actual context you were given; don’t ignore it.
332
+ - If you need context, read the existing project or conversation before proposing.
333
+ - Structure: (1) core insight/value, (2) who benefits & why now, (3) risks/tradeoffs (brief), (4) closing case.
334
+ - Focus on value: what improves, for whom, and why now.
335
+ - Do NOT review code or report bugs.
336
+ - Do NOT describe scope, implementation approach, or plan.
337
+ - You may briefly note tradeoffs, but they are not the focus.
338
+ - Prioritize clarity over brevity.
339
+ - If you have nothing to propose, respond with ONLY "EMPTY_RESPONSE".`;
319
340
 
320
341
  /**
321
342
  * @param {string} session
@@ -378,7 +399,9 @@ function findCallerPid() {
378
399
  * @returns {{pid: string, command: string}[]}
379
400
  */
380
401
  function findOrphanedProcesses() {
381
- const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], { encoding: "utf-8" });
402
+ const result = spawnSync("ps", ["-eo", "pid=,ppid=,args="], {
403
+ encoding: "utf-8",
404
+ });
382
405
 
383
406
  if (result.status !== 0 || !result.stdout.trim()) {
384
407
  return [];
@@ -432,6 +455,17 @@ async function readStdin() {
432
455
  });
433
456
  }
434
457
 
458
+ /**
459
+ * @param {string | null | undefined} value
460
+ * @returns {Promise<string | undefined>}
461
+ */
462
+ async function readStdinIfNeeded(value) {
463
+ if (value && value !== "-") return value;
464
+ if (!hasStdinData()) return undefined;
465
+ const stdinText = await readStdin();
466
+ return stdinText || undefined;
467
+ }
468
+
435
469
  // =============================================================================
436
470
  // =============================================================================
437
471
  // Helpers - CLI argument parsing
@@ -460,6 +494,7 @@ async function readStdin() {
460
494
  * @property {number} [tail]
461
495
  * @property {number} [limit]
462
496
  * @property {string} [branch]
497
+ * @property {string} [archangels]
463
498
  */
464
499
  function parseCliArgs(args) {
465
500
  const { values, positionals } = parseArgs({
@@ -484,6 +519,7 @@ function parseCliArgs(args) {
484
519
  tail: { type: "string" },
485
520
  limit: { type: "string" },
486
521
  branch: { type: "string" },
522
+ archangels: { type: "string" },
487
523
  },
488
524
  allowPositionals: true,
489
525
  strict: false, // Don't error on unknown flags
@@ -508,6 +544,7 @@ function parseCliArgs(args) {
508
544
  tail: values.tail !== undefined ? Number(values.tail) : undefined,
509
545
  limit: values.limit !== undefined ? Number(values.limit) : undefined,
510
546
  branch: /** @type {string | undefined} */ (values.branch),
547
+ archangels: /** @type {string | undefined} */ (values.archangels),
511
548
  },
512
549
  positionals,
513
550
  };
@@ -1150,6 +1187,54 @@ function getArchangelSessionPattern(config) {
1150
1187
  return `${config.tool}-archangel-${config.name}`;
1151
1188
  }
1152
1189
 
1190
+ /**
1191
+ * @param {string} rfpId
1192
+ * @param {string} prompt
1193
+ */
1194
+ function writeRfpRecord(rfpId, prompt) {
1195
+ ensureRfpDir();
1196
+ const p = path.join(RFP_DIR, `${rfpId}.md`);
1197
+ const block = [`### ${rfpId}`, "", prompt.trim(), ""].join("\n");
1198
+ writeFileSync(p, block, "utf-8");
1199
+ }
1200
+
1201
+ /**
1202
+ * @param {string} input
1203
+ * @returns {string}
1204
+ */
1205
+ function resolveRfpId(input) {
1206
+ ensureRfpDir();
1207
+ if (!existsSync(RFP_DIR)) return input;
1208
+ const files = readdirSync(RFP_DIR).filter((f) => f.endsWith(".md"));
1209
+ const ids = files.map((f) => f.replace(/\.md$/, ""));
1210
+ const matches = ids.filter((id) => id.startsWith(input));
1211
+ if (matches.length === 1) return matches[0];
1212
+ if (matches.length > 1) {
1213
+ console.log("ERROR: ambiguous rfp id. Matches:");
1214
+ for (const m of matches) console.log(` ${m}`);
1215
+ process.exit(1);
1216
+ }
1217
+ return input;
1218
+ }
1219
+
1220
+ /**
1221
+ * @param {ParentSession | null} parent
1222
+ * @returns {string}
1223
+ */
1224
+ function generateRfpId(parent) {
1225
+ const now = new Date();
1226
+ const y = now.getFullYear();
1227
+ const mo = String(now.getMonth() + 1).padStart(2, "0");
1228
+ const d = String(now.getDate()).padStart(2, "0");
1229
+ const h = String(now.getHours()).padStart(2, "0");
1230
+ const mi = String(now.getMinutes()).padStart(2, "0");
1231
+ const s = String(now.getSeconds()).padStart(2, "0");
1232
+ const ts = `${y}-${mo}-${d}-${h}-${mi}-${s}`;
1233
+ const base = parent?.uuid ? parent.uuid.split("-")[0] : randomUUID().split("-")[0];
1234
+ const suffix = randomUUID().split("-")[0].slice(0, 4);
1235
+ return `rfp-${base}-${ts}-${suffix}`.toLowerCase();
1236
+ }
1237
+
1153
1238
  // =============================================================================
1154
1239
  // Helpers - mailbox
1155
1240
  // =============================================================================
@@ -1166,15 +1251,25 @@ function ensureMailboxDir() {
1166
1251
  }
1167
1252
  }
1168
1253
 
1254
+ /**
1255
+ * @returns {void}
1256
+ */
1257
+ function ensureRfpDir() {
1258
+ if (!existsSync(RFP_DIR)) {
1259
+ mkdirSync(RFP_DIR, { recursive: true });
1260
+ }
1261
+ }
1262
+
1169
1263
  /**
1170
1264
  * @param {MailboxPayload} payload
1265
+ * @param {string} [type]
1171
1266
  * @returns {void}
1172
1267
  */
1173
- function writeToMailbox(payload) {
1268
+ function writeToMailbox(payload, type = "observation") {
1174
1269
  ensureMailboxDir();
1175
1270
  const entry = {
1176
1271
  timestamp: new Date().toISOString(),
1177
- type: "observation",
1272
+ type,
1178
1273
  payload,
1179
1274
  };
1180
1275
  appendFileSync(MAILBOX_PATH, JSON.stringify(entry) + "\n");
@@ -2711,6 +2806,21 @@ function startArchangel(config, parentSession = null) {
2711
2806
  );
2712
2807
  }
2713
2808
 
2809
+ /**
2810
+ * @param {string} pattern
2811
+ * @param {number} [timeoutMs]
2812
+ * @returns {Promise<string | undefined>}
2813
+ */
2814
+ async function waitForArchangelSession(pattern, timeoutMs = ARCHANGEL_STARTUP_TIMEOUT_MS) {
2815
+ const start = Date.now();
2816
+ while (Date.now() - start < timeoutMs) {
2817
+ const session = findArchangelSession(pattern);
2818
+ if (session) return session;
2819
+ await sleep(200);
2820
+ }
2821
+ return undefined;
2822
+ }
2823
+
2714
2824
  // =============================================================================
2715
2825
  // Command: archangel (runs as the archangel process itself)
2716
2826
  // =============================================================================
@@ -3713,7 +3823,13 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
3713
3823
  console.log(`**Branch**: ${p.branch || "?"} @ ${p.commit || "?"}\n`);
3714
3824
  }
3715
3825
 
3716
- if (p.message) {
3826
+ if (p.rfpId) {
3827
+ console.log(`**RFP**: ${p.rfpId}\n`);
3828
+ }
3829
+
3830
+ if (entry.type === "proposal") {
3831
+ console.log(`**Proposal**: ${p.message || ""}\n`);
3832
+ } else if (p.message) {
3717
3833
  console.log(`**Assistant**: ${p.message}\n`);
3718
3834
  }
3719
3835
 
@@ -3727,13 +3843,229 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
3727
3843
  }
3728
3844
  }
3729
3845
 
3846
+ /**
3847
+ * @param {string} rfpId
3848
+ * @param {string} archangel
3849
+ * @returns {string | null}
3850
+ */
3851
+ function getProposalFromMailbox(rfpId, archangel) {
3852
+ if (!existsSync(MAILBOX_PATH)) return null;
3853
+ let result = null;
3854
+ try {
3855
+ const lines = readFileSync(MAILBOX_PATH, "utf-8").trim().split("\n").filter(Boolean);
3856
+ for (const line of lines) {
3857
+ try {
3858
+ const entry = JSON.parse(line);
3859
+ if (entry?.type !== "proposal") continue;
3860
+ const p = entry.payload || {};
3861
+ if (p.rfpId === rfpId && p.archangel === archangel) {
3862
+ result = p.message || "";
3863
+ }
3864
+ } catch {
3865
+ // Skip malformed lines
3866
+ }
3867
+ }
3868
+ } catch (err) {
3869
+ debugError("getProposalFromMailbox", err);
3870
+ }
3871
+ return result;
3872
+ }
3873
+
3874
+ /**
3875
+ * @param {string} prompt
3876
+ * @param {{archangels?: string, fresh?: boolean, noWait?: boolean}} [options]
3877
+ */
3878
+ async function cmdRfp(prompt, { archangels, fresh = false, noWait = false } = {}) {
3879
+ const configs = loadAgentConfigs();
3880
+ if (configs.length === 0) {
3881
+ console.log(`No archangels found in ${AGENTS_DIR}/`);
3882
+ process.exit(1);
3883
+ }
3884
+
3885
+ const requested = archangels
3886
+ ? archangels
3887
+ .split(",")
3888
+ .map((s) => s.trim())
3889
+ .filter(Boolean)
3890
+ : configs.map((c) => c.name);
3891
+
3892
+ if (requested.length === 0) {
3893
+ console.log("ERROR: no archangels specified");
3894
+ process.exit(1);
3895
+ }
3896
+
3897
+ const missing = requested.filter((name) => !configs.some((c) => c.name === name));
3898
+ if (missing.length > 0) {
3899
+ console.log(`ERROR: unknown archangel(s): ${missing.join(", ")}`);
3900
+ process.exit(1);
3901
+ }
3902
+
3903
+ const parent = findParentSession();
3904
+ const rfpId = generateRfpId(parent);
3905
+
3906
+ for (const name of requested) {
3907
+ const config = configs.find((c) => c.name === name);
3908
+ if (!config) continue;
3909
+
3910
+ const pattern = getArchangelSessionPattern(config);
3911
+ let session = findArchangelSession(pattern);
3912
+ if (!session) {
3913
+ startArchangel(config, parent);
3914
+ session = await waitForArchangelSession(pattern);
3915
+ }
3916
+
3917
+ if (!session) {
3918
+ console.log(`ERROR: failed to start archangel '${name}'`);
3919
+ continue;
3920
+ }
3921
+
3922
+ const { agent } = resolveAgent({ sessionName: session });
3923
+
3924
+ if (fresh) {
3925
+ tmuxSendLiteral(session, "/new");
3926
+ await sleep(50);
3927
+ tmuxSend(session, "Enter");
3928
+ }
3929
+
3930
+ const ready = await waitUntilReady(agent, session, ARCHANGEL_STARTUP_TIMEOUT_MS);
3931
+ if (ready.state !== State.READY) {
3932
+ console.log(`[rfp] ${name} not ready (${ready.state}), skipping`);
3933
+ continue;
3934
+ }
3935
+
3936
+ const rfpPrompt = `## RFP ${rfpId}\n\n${RFP_PREAMBLE}\n\n${prompt}\n\nReturn exactly one proposal.`;
3937
+ tmuxSendLiteral(session, rfpPrompt);
3938
+ await sleep(200);
3939
+ tmuxSend(session, "Enter");
3940
+ }
3941
+
3942
+ writeRfpRecord(rfpId, prompt);
3943
+ const archangelList = requested.join(",");
3944
+ console.log(`rfp: ${rfpId} (${archangelList})`);
3945
+ if (noWait) {
3946
+ const cli = path.basename(process.argv[1], ".js");
3947
+ const base = rfpId.split("-")[1];
3948
+ const shortId = `rfp-${base}`;
3949
+ console.log(`e.g.\n ${cli} rfp wait ${shortId} --archangels=${archangelList}`);
3950
+ }
3951
+ }
3952
+
3953
+ /**
3954
+ * @param {string} rfpId
3955
+ * @param {{archangels?: string, timeoutMs?: number}} [options]
3956
+ */
3957
+ async function cmdRfpWait(rfpId, { archangels, timeoutMs = ARCHANGEL_RESPONSE_TIMEOUT_MS } = {}) {
3958
+ const resolvedRfpId = resolveRfpId(rfpId);
3959
+ const configs = loadAgentConfigs();
3960
+ if (configs.length === 0) {
3961
+ console.log(`No archangels found in ${AGENTS_DIR}/`);
3962
+ process.exit(1);
3963
+ }
3964
+
3965
+ const requested = archangels
3966
+ ? archangels
3967
+ .split(",")
3968
+ .map((s) => s.trim())
3969
+ .filter(Boolean)
3970
+ : configs.map((c) => c.name);
3971
+
3972
+ if (requested.length === 0) {
3973
+ console.log("ERROR: no archangels specified");
3974
+ process.exit(1);
3975
+ }
3976
+
3977
+ const missing = requested.filter((name) => !configs.some((c) => c.name === name));
3978
+ if (missing.length > 0) {
3979
+ console.log(`ERROR: unknown archangel(s): ${missing.join(", ")}`);
3980
+ process.exit(1);
3981
+ }
3982
+
3983
+ let wroteAny = false;
3984
+ let printedAny = false;
3985
+
3986
+ for (const name of requested) {
3987
+ const config = configs.find((c) => c.name === name);
3988
+ if (!config) continue;
3989
+
3990
+ const pattern = getArchangelSessionPattern(config);
3991
+ const session = findArchangelSession(pattern);
3992
+ if (!session) {
3993
+ console.log(`[rfp] ${name} session not found, skipping`);
3994
+ continue;
3995
+ }
3996
+
3997
+ const existing = getProposalFromMailbox(resolvedRfpId, name);
3998
+ if (existing !== null) {
3999
+ if (printedAny) console.log("");
4000
+ console.log(`[${name}]`);
4001
+ console.log(existing);
4002
+ wroteAny = true;
4003
+ printedAny = true;
4004
+ continue;
4005
+ }
4006
+
4007
+ const { agent } = resolveAgent({ sessionName: session });
4008
+ let result;
4009
+ try {
4010
+ result = await waitUntilReady(agent, session, timeoutMs);
4011
+ } catch (err) {
4012
+ if (err instanceof TimeoutError) {
4013
+ console.log(`[rfp] ${name} timed out`);
4014
+ } else {
4015
+ console.log(`[rfp] ${name} error: ${err instanceof Error ? err.message : err}`);
4016
+ }
4017
+ continue;
4018
+ }
4019
+
4020
+ if (result.state === State.RATE_LIMITED) {
4021
+ console.log(`[rfp] ${name} rate limited`);
4022
+ continue;
4023
+ }
4024
+ if (result.state === State.CONFIRMING) {
4025
+ console.log(`[rfp] ${name} awaiting confirmation`);
4026
+ continue;
4027
+ }
4028
+
4029
+ const response = agent.getResponse(session, result.screen) || "";
4030
+ if (!response || response.trim() === "EMPTY_RESPONSE") {
4031
+ continue;
4032
+ }
4033
+
4034
+ writeToMailbox(
4035
+ {
4036
+ agent: name,
4037
+ session,
4038
+ branch: getCurrentBranch(),
4039
+ commit: getCurrentCommit(),
4040
+ files: [],
4041
+ message: response,
4042
+ rfpId: resolvedRfpId,
4043
+ archangel: name,
4044
+ },
4045
+ "proposal",
4046
+ );
4047
+ if (printedAny) console.log("");
4048
+ console.log(`[${name}]`);
4049
+ console.log(response);
4050
+ wroteAny = true;
4051
+ printedAny = true;
4052
+ }
4053
+
4054
+ if (!wroteAny) process.exit(1);
4055
+ }
4056
+
3730
4057
  /**
3731
4058
  * @param {Agent} agent
3732
4059
  * @param {string | null | undefined} session
3733
4060
  * @param {string} message
3734
4061
  * @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
3735
4062
  */
3736
- async function cmdAsk(agent, session, message, { noWait = false, yolo = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
4063
+ async function cmdAsk(
4064
+ agent,
4065
+ session,
4066
+ message,
4067
+ { noWait = false, yolo = false, timeoutMs = DEFAULT_TIMEOUT_MS } = {},
4068
+ ) {
3737
4069
  const sessionExists = session != null && tmuxHasSession(session);
3738
4070
  const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
3739
4071
 
@@ -4197,72 +4529,62 @@ function resolveAgent({ toolFlag, sessionName } = {}) {
4197
4529
  function printHelp(agent, cliName) {
4198
4530
  const name = cliName;
4199
4531
  const backendName = agent.displayName;
4200
- const hasReview = !!agent.reviewOptions;
4201
4532
 
4202
4533
  console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
4203
4534
 
4204
4535
  Usage: ${name} [OPTIONS] <command|message> [ARGS...]
4205
4536
 
4206
- Commands:
4207
- agents List all running agents with state and log paths
4537
+ Messaging/State:
4538
+ <message> Send message to ${name}
4539
+ review [TYPE] Review code: pr, uncommitted, commit, custom
4540
+ status Exit code: ready=0 rate_limit=2 confirm=3 thinking=4
4541
+ output [-N] Show response (0=last, -1=prev, -2=older)
4542
+ compact Summarise session to shrink context size
4543
+ reset Start fresh conversation
4544
+
4545
+ Sessions:
4546
+ agents List all running agents
4208
4547
  target Show default target session for current tool
4209
4548
  attach [SESSION] Attach to agent session interactively
4210
- log SESSION View conversation log (--tail=N, --follow, --reasoning)
4211
- mailbox View archangel observations (--limit=N, --branch=X, --all)
4549
+ kill Kill sessions (--all, --session=NAME, --orphans [--force])
4550
+
4551
+ Archangels:
4212
4552
  summon [name] Summon archangels (all, or by name)
4213
4553
  recall [name] Recall archangels (all, or by name)
4214
- kill Kill sessions (--all, --session=NAME, --orphans [--force])
4215
- status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
4216
- 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
4554
+ mailbox Archangel notes (filters: --branch=git, --all)
4555
+ rfp <prompt> Request proposals (--archangels=a,b)
4556
+ rfp wait <id> Wait for proposals (--archangels=a,b)
4557
+
4558
+ Recovery:
4559
+ debug Show raw screen output and detected state
4224
4560
  approve Approve pending action (send 'y')
4225
4561
  reject Reject pending action (send 'n')
4562
+ select N Select menu option N
4226
4563
  send KEYS Send key sequence (e.g. "1[Enter]", "[Escape]")
4227
- compact Summarize conversation (when context is full)
4228
- reset Start fresh conversation
4229
- <message> Send message to ${name}
4564
+ log SESSION View conversation log (--tail=N, --follow, --reasoning)
4230
4565
 
4231
4566
  Flags:
4232
4567
  --tool=NAME Use specific agent (codex, claude)
4233
- --session=NAME Target session by name, archangel name, or UUID prefix (self = current)
4568
+ --session=ID name | archangel | uuid-prefix | self
4569
+ --fresh Reset conversation before review
4570
+ --yolo Skip all confirmations (dangerous)
4234
4571
  --wait Wait for response (default for messages; required for approve/reject)
4235
4572
  --no-wait Fire-and-forget: send message, print session ID, exit immediately
4236
4573
  --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
4574
 
4250
4575
  Examples:
4251
4576
  ${name} "explain this codebase"
4252
4577
  ${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
4253
4578
  ${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
4254
4579
  ${name} review uncommitted --wait
4255
- ${name} approve --wait
4256
- ${name} kill # Kill agents in current project
4257
- ${name} kill --all # Kill all agents across all projects
4258
- ${name} kill --session=NAME # Kill specific session
4259
- ${name} send "1[Enter]" # Recovery: select option 1 and press Enter
4260
- ${name} send "[Escape][Escape]" # Recovery: escape out of a dialog
4261
- ${name} summon # Summon all archangels from .ai/agents/*.md
4262
- ${name} summon reviewer # Summon by name (creates config if new)
4263
- ${name} recall # Recall all archangels
4264
- ${name} recall reviewer # Recall one by name
4265
- ${name} agents # List all agents (shows TYPE=archangel)
4580
+ ${name} kill # Kill agents in current project
4581
+ ${name} kill --all # Kill all agents across all projects
4582
+ ${name} kill --session=NAME # Kill specific session
4583
+ ${name} summon # Summon all archangels from .ai/agents/*.md
4584
+ ${name} summon reviewer # Summon by name (creates config if new)
4585
+ ${name} recall # Recall all archangels
4586
+ ${name} recall reviewer # Recall one by name
4587
+ ${name} agents # List all agents (shows TYPE=archangel)
4266
4588
 
4267
4589
  Note: Reviews and complex tasks may take several minutes.
4268
4590
  Use Bash run_in_background for long operations (not --no-wait).`);
@@ -4308,7 +4630,10 @@ async function main() {
4308
4630
  }
4309
4631
 
4310
4632
  // Agent resolution (considers --tool flag, session name, invocation, and env vars)
4311
- const { agent, error: agentError } = resolveAgent({ toolFlag: flags.tool, sessionName: session });
4633
+ const { agent, error: agentError } = resolveAgent({
4634
+ toolFlag: flags.tool,
4635
+ sessionName: session,
4636
+ });
4312
4637
  if (agentError) {
4313
4638
  console.log(`ERROR: ${agentError}`);
4314
4639
  process.exit(1);
@@ -4360,14 +4685,33 @@ async function main() {
4360
4685
  if (cmd === "attach") return cmdAttach(positionals[1] || session);
4361
4686
  if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
4362
4687
  if (cmd === "mailbox") return cmdMailbox({ limit, branch, all });
4688
+ if (cmd === "rfp") {
4689
+ if (positionals[1] === "wait") {
4690
+ const rfpId = positionals[2];
4691
+ if (!rfpId) {
4692
+ console.log("ERROR: missing rfp id");
4693
+ process.exit(1);
4694
+ }
4695
+ return cmdRfpWait(rfpId, { archangels: flags.archangels, timeoutMs });
4696
+ }
4697
+ const rawPrompt = positionals.slice(1).join(" ");
4698
+ const prompt = await readStdinIfNeeded(rawPrompt);
4699
+ if (!prompt) {
4700
+ console.log("ERROR: missing prompt for rfp");
4701
+ process.exit(1);
4702
+ }
4703
+ return cmdRfp(prompt, { archangels: flags.archangels, fresh, noWait });
4704
+ }
4363
4705
  if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
4364
4706
  if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
4365
- if (cmd === "review")
4366
- return cmdReview(agent, session, positionals[1], positionals[2], {
4707
+ if (cmd === "review") {
4708
+ const customInstructions = await readStdinIfNeeded(positionals[2]);
4709
+ return cmdReview(agent, session, positionals[1], customInstructions ?? undefined, {
4367
4710
  wait,
4368
4711
  fresh,
4369
4712
  timeoutMs,
4370
4713
  });
4714
+ }
4371
4715
  if (cmd === "status") return cmdStatus(agent, session);
4372
4716
  if (cmd === "debug") return cmdDebug(agent, session);
4373
4717
  if (cmd === "output") {
@@ -4383,18 +4727,17 @@ async function main() {
4383
4727
  return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
4384
4728
 
4385
4729
  // Default: send message
4386
- let message = positionals.join(" ");
4387
- if (!message && hasStdinData()) {
4388
- message = await readStdin();
4389
- }
4730
+ const rawMessage = positionals.join(" ");
4731
+ let message = await readStdinIfNeeded(rawMessage);
4390
4732
 
4391
4733
  if (!message || flags.help) {
4392
4734
  printHelp(agent, cliName);
4393
4735
  process.exit(0);
4394
4736
  }
4737
+ const messageText = message;
4395
4738
 
4396
4739
  // Detect "review ..." or "please review ..." and route to custom review mode
4397
- const reviewMatch = message.match(/^(?:please )?review\s*(.*)/i);
4740
+ const reviewMatch = messageText.match(/^(?:please )?review\s*(.*)/i);
4398
4741
  if (reviewMatch && agent.reviewOptions) {
4399
4742
  const customInstructions = reviewMatch[1].trim() || null;
4400
4743
  return cmdReview(agent, session, "custom", customInstructions, {
@@ -4404,7 +4747,7 @@ async function main() {
4404
4747
  });
4405
4748
  }
4406
4749
 
4407
- return cmdAsk(agent, session, message, { noWait, yolo, timeoutMs });
4750
+ return cmdAsk(agent, session, messageText, { noWait, yolo, timeoutMs });
4408
4751
  }
4409
4752
 
4410
4753
  // Run main() only when executed directly (not when imported for testing)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ax-agents",
3
- "version": "0.0.1-alpha.11",
3
+ "version": "0.0.1-alpha.12",
4
4
  "description": "A CLI for orchestrating AI coding agents via tmux",
5
5
  "bin": {
6
6
  "ax": "ax.js",