ax-agents 0.0.1-alpha.10 → 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 +403 -57
  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");
@@ -2703,11 +2798,29 @@ function startArchangel(config, parentSession = null) {
2703
2798
  env,
2704
2799
  });
2705
2800
  child.unref();
2801
+ const watchingLabel = parentSession
2802
+ ? parentSession.session || parentSession.uuid?.slice(0, 8)
2803
+ : null;
2706
2804
  console.log(
2707
- `Summoning: ${config.name} (pid ${child.pid})${parentSession ? ` [parent: ${parentSession.session}]` : ""}`,
2805
+ `Summoning: ${config.name} (pid ${child.pid})${watchingLabel ? ` [watching: ${watchingLabel}]` : ""}`,
2708
2806
  );
2709
2807
  }
2710
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
+
2711
2824
  // =============================================================================
2712
2825
  // Command: archangel (runs as the archangel process itself)
2713
2826
  // =============================================================================
@@ -3710,7 +3823,13 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
3710
3823
  console.log(`**Branch**: ${p.branch || "?"} @ ${p.commit || "?"}\n`);
3711
3824
  }
3712
3825
 
3713
- 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) {
3714
3833
  console.log(`**Assistant**: ${p.message}\n`);
3715
3834
  }
3716
3835
 
@@ -3724,13 +3843,229 @@ function cmdMailbox({ limit = 20, branch = null, all = false } = {}) {
3724
3843
  }
3725
3844
  }
3726
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
+
3727
4057
  /**
3728
4058
  * @param {Agent} agent
3729
4059
  * @param {string | null | undefined} session
3730
4060
  * @param {string} message
3731
4061
  * @param {{noWait?: boolean, yolo?: boolean, timeoutMs?: number}} [options]
3732
4062
  */
3733
- 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
+ ) {
3734
4069
  const sessionExists = session != null && tmuxHasSession(session);
3735
4070
  const nativeYolo = sessionExists && isYoloSession(/** @type {string} */ (session));
3736
4071
 
@@ -4194,72 +4529,62 @@ function resolveAgent({ toolFlag, sessionName } = {}) {
4194
4529
  function printHelp(agent, cliName) {
4195
4530
  const name = cliName;
4196
4531
  const backendName = agent.displayName;
4197
- const hasReview = !!agent.reviewOptions;
4198
4532
 
4199
4533
  console.log(`${name} v${VERSION} - agentic assistant CLI (${backendName})
4200
4534
 
4201
4535
  Usage: ${name} [OPTIONS] <command|message> [ARGS...]
4202
4536
 
4203
- Commands:
4204
- 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
4205
4547
  target Show default target session for current tool
4206
4548
  attach [SESSION] Attach to agent session interactively
4207
- log SESSION View conversation log (--tail=N, --follow, --reasoning)
4208
- mailbox View archangel observations (--limit=N, --branch=X, --all)
4549
+ kill Kill sessions (--all, --session=NAME, --orphans [--force])
4550
+
4551
+ Archangels:
4209
4552
  summon [name] Summon archangels (all, or by name)
4210
4553
  recall [name] Recall archangels (all, or by name)
4211
- kill Kill sessions (--all, --session=NAME, --orphans [--force])
4212
- status Check state (exit: 0=ready, 2=rate_limited, 3=confirming, 4=thinking)
4213
- output [-N] Show response (0=last, -1=prev, -2=older)
4214
- debug Show raw screen output and detected state${
4215
- hasReview
4216
- ? `
4217
- review [TYPE] Review code: pr, uncommitted, commit, custom`
4218
- : ""
4219
- }
4220
- 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
4221
4560
  approve Approve pending action (send 'y')
4222
4561
  reject Reject pending action (send 'n')
4562
+ select N Select menu option N
4223
4563
  send KEYS Send key sequence (e.g. "1[Enter]", "[Escape]")
4224
- compact Summarize conversation (when context is full)
4225
- reset Start fresh conversation
4226
- <message> Send message to ${name}
4564
+ log SESSION View conversation log (--tail=N, --follow, --reasoning)
4227
4565
 
4228
4566
  Flags:
4229
4567
  --tool=NAME Use specific agent (codex, claude)
4230
- --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)
4231
4571
  --wait Wait for response (default for messages; required for approve/reject)
4232
4572
  --no-wait Fire-and-forget: send message, print session ID, exit immediately
4233
4573
  --timeout=N Set timeout in seconds (default: ${DEFAULT_TIMEOUT_MS / 1000}, reviews: ${REVIEW_TIMEOUT_MS / 1000})
4234
- --yolo Skip all confirmations (dangerous)
4235
- --fresh Reset conversation before review
4236
- --orphans Kill orphaned claude/codex processes (PPID=1)
4237
- --force Use SIGKILL instead of SIGTERM (with --orphans)
4238
-
4239
- Environment:
4240
- AX_DEFAULT_TOOL Default agent when using 'ax' (claude or codex, default: codex)
4241
- ${agent.envVar} Override default session name
4242
- AX_CLAUDE_CONFIG_DIR Override Claude config directory (default: ~/.claude)
4243
- AX_CODEX_CONFIG_DIR Override Codex config directory (default: ~/.codex)
4244
- AX_REVIEW_MODE=exec Bypass /review, send instructions directly (codex only)
4245
- AX_DEBUG=1 Enable debug logging
4246
4574
 
4247
4575
  Examples:
4248
4576
  ${name} "explain this codebase"
4249
4577
  ${name} "review the error handling" # Auto custom review (${REVIEW_TIMEOUT_MS / 60000}min timeout)
4250
4578
  ${name} "FYI: auth was refactored" --no-wait # Send context to a working session (no response needed)
4251
4579
  ${name} review uncommitted --wait
4252
- ${name} approve --wait
4253
- ${name} kill # Kill agents in current project
4254
- ${name} kill --all # Kill all agents across all projects
4255
- ${name} kill --session=NAME # Kill specific session
4256
- ${name} send "1[Enter]" # Recovery: select option 1 and press Enter
4257
- ${name} send "[Escape][Escape]" # Recovery: escape out of a dialog
4258
- ${name} summon # Summon all archangels from .ai/agents/*.md
4259
- ${name} summon reviewer # Summon by name (creates config if new)
4260
- ${name} recall # Recall all archangels
4261
- ${name} recall reviewer # Recall one by name
4262
- ${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)
4263
4588
 
4264
4589
  Note: Reviews and complex tasks may take several minutes.
4265
4590
  Use Bash run_in_background for long operations (not --no-wait).`);
@@ -4305,7 +4630,10 @@ async function main() {
4305
4630
  }
4306
4631
 
4307
4632
  // Agent resolution (considers --tool flag, session name, invocation, and env vars)
4308
- const { agent, error: agentError } = resolveAgent({ toolFlag: flags.tool, sessionName: session });
4633
+ const { agent, error: agentError } = resolveAgent({
4634
+ toolFlag: flags.tool,
4635
+ sessionName: session,
4636
+ });
4309
4637
  if (agentError) {
4310
4638
  console.log(`ERROR: ${agentError}`);
4311
4639
  process.exit(1);
@@ -4357,14 +4685,33 @@ async function main() {
4357
4685
  if (cmd === "attach") return cmdAttach(positionals[1] || session);
4358
4686
  if (cmd === "log") return cmdLog(positionals[1] || session, { tail, reasoning, follow });
4359
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
+ }
4360
4705
  if (cmd === "approve") return cmdApprove(agent, session, { wait, timeoutMs });
4361
4706
  if (cmd === "reject") return cmdReject(agent, session, { wait, timeoutMs });
4362
- if (cmd === "review")
4363
- 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, {
4364
4710
  wait,
4365
4711
  fresh,
4366
4712
  timeoutMs,
4367
4713
  });
4714
+ }
4368
4715
  if (cmd === "status") return cmdStatus(agent, session);
4369
4716
  if (cmd === "debug") return cmdDebug(agent, session);
4370
4717
  if (cmd === "output") {
@@ -4380,18 +4727,17 @@ async function main() {
4380
4727
  return cmdSelect(agent, session, positionals[1], { wait, timeoutMs });
4381
4728
 
4382
4729
  // Default: send message
4383
- let message = positionals.join(" ");
4384
- if (!message && hasStdinData()) {
4385
- message = await readStdin();
4386
- }
4730
+ const rawMessage = positionals.join(" ");
4731
+ let message = await readStdinIfNeeded(rawMessage);
4387
4732
 
4388
4733
  if (!message || flags.help) {
4389
4734
  printHelp(agent, cliName);
4390
4735
  process.exit(0);
4391
4736
  }
4737
+ const messageText = message;
4392
4738
 
4393
4739
  // Detect "review ..." or "please review ..." and route to custom review mode
4394
- const reviewMatch = message.match(/^(?:please )?review\s*(.*)/i);
4740
+ const reviewMatch = messageText.match(/^(?:please )?review\s*(.*)/i);
4395
4741
  if (reviewMatch && agent.reviewOptions) {
4396
4742
  const customInstructions = reviewMatch[1].trim() || null;
4397
4743
  return cmdReview(agent, session, "custom", customInstructions, {
@@ -4401,7 +4747,7 @@ async function main() {
4401
4747
  });
4402
4748
  }
4403
4749
 
4404
- return cmdAsk(agent, session, message, { noWait, yolo, timeoutMs });
4750
+ return cmdAsk(agent, session, messageText, { noWait, yolo, timeoutMs });
4405
4751
  }
4406
4752
 
4407
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.10",
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",