reasonix 0.27.3 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  memoryEnabled,
13
13
  readProjectMemory,
14
14
  sanitizeMemoryName
15
- } from "./chunk-R2L5YEEF.js";
15
+ } from "./chunk-COFBA5FV.js";
16
16
 
17
17
  // src/cli/index.ts
18
18
  import { Command } from "commander";
@@ -2742,6 +2742,10 @@ var ToolRegistry = class {
2742
2742
  wasFlattened(name) {
2743
2743
  return Boolean(this._tools.get(name)?.flatSchema);
2744
2744
  }
2745
+ /** Unknown / unannotated tools default to false — third-party MCP tools must opt in. */
2746
+ isParallelSafe(name) {
2747
+ return this._tools.get(name)?.parallelSafe === true;
2748
+ }
2745
2749
  specs() {
2746
2750
  return [...this._tools.values()].map((t3) => ({
2747
2751
  type: "function",
@@ -4433,6 +4437,48 @@ var CacheFirstLoop = class {
4433
4437
  this._escalateThisTurn = true;
4434
4438
  return true;
4435
4439
  }
4440
+ async runOneToolCall(call, signal) {
4441
+ const name = call.function?.name ?? "";
4442
+ const args = call.function?.arguments ?? "{}";
4443
+ const parsedArgs = safeParseToolArgs(args);
4444
+ const preReport = await runHooks({
4445
+ hooks: this.hooks,
4446
+ payload: {
4447
+ event: "PreToolUse",
4448
+ cwd: this.hookCwd,
4449
+ toolName: name,
4450
+ toolArgs: parsedArgs
4451
+ }
4452
+ });
4453
+ const preWarnings = [...hookWarnings(preReport.outcomes, this._turn)];
4454
+ if (preReport.blocked) {
4455
+ const blocking = preReport.outcomes[preReport.outcomes.length - 1];
4456
+ const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
4457
+ return {
4458
+ preWarnings,
4459
+ postWarnings: [],
4460
+ result: `[hook block] ${blocking?.hook.command ?? "<unknown>"}
4461
+ ${reason}`
4462
+ };
4463
+ }
4464
+ const result = await this.tools.dispatch(name, args, {
4465
+ signal,
4466
+ maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
4467
+ confirmationGate: this.confirmationGate
4468
+ });
4469
+ const postReport = await runHooks({
4470
+ hooks: this.hooks,
4471
+ payload: {
4472
+ event: "PostToolUse",
4473
+ cwd: this.hookCwd,
4474
+ toolName: name,
4475
+ toolArgs: parsedArgs,
4476
+ toolResult: result
4477
+ }
4478
+ });
4479
+ const postWarnings = [...hookWarnings(postReport.outcomes, this._turn)];
4480
+ return { preWarnings, postWarnings, result };
4481
+ }
4436
4482
  buildMessages(pendingUser) {
4437
4483
  const healed = healLoadedMessages(this.log.toMessages(), DEFAULT_MAX_RESULT_CHARS);
4438
4484
  const msgs = [...this.prefix.toMessages(), ...healed.messages];
@@ -4924,71 +4970,69 @@ var CacheFirstLoop = class {
4924
4970
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "context-guard" });
4925
4971
  return;
4926
4972
  }
4927
- for (const call of repairedCalls) {
4928
- const name = call.function?.name ?? "";
4929
- const args = call.function?.arguments ?? "{}";
4930
- yield {
4931
- turn: this._turn,
4932
- role: "tool_start",
4933
- content: "",
4934
- toolName: name,
4935
- toolArgs: args
4936
- };
4937
- const parsedArgs = safeParseToolArgs(args);
4938
- const preReport = await runHooks({
4939
- hooks: this.hooks,
4940
- payload: {
4941
- event: "PreToolUse",
4942
- cwd: this.hookCwd,
4943
- toolName: name,
4944
- toolArgs: parsedArgs
4973
+ const dispatchSerial = (process.env.REASONIX_TOOL_DISPATCH ?? "auto").toLowerCase() === "serial";
4974
+ const parallelMaxParsed = Number.parseInt(process.env.REASONIX_PARALLEL_MAX ?? "", 10);
4975
+ const parallelMax = Number.isFinite(parallelMaxParsed) && parallelMaxParsed >= 1 ? Math.min(parallelMaxParsed, 16) : 3;
4976
+ let callIdx = 0;
4977
+ while (callIdx < repairedCalls.length) {
4978
+ const chunk = [];
4979
+ if (!dispatchSerial) {
4980
+ while (callIdx < repairedCalls.length && chunk.length < parallelMax && this.tools.isParallelSafe(repairedCalls[callIdx]?.function?.name ?? "")) {
4981
+ chunk.push(repairedCalls[callIdx++]);
4945
4982
  }
4946
- });
4947
- for (const w of hookWarnings(preReport.outcomes, this._turn)) yield w;
4948
- let result;
4949
- if (preReport.blocked) {
4950
- const blocking = preReport.outcomes[preReport.outcomes.length - 1];
4951
- const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
4952
- result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
4953
- ${reason}`;
4954
- } else {
4955
- result = await this.tools.dispatch(name, args, {
4956
- signal,
4957
- maxResultTokens: DEFAULT_MAX_RESULT_TOKENS,
4958
- confirmationGate: this.confirmationGate
4959
- });
4960
- const postReport = await runHooks({
4961
- hooks: this.hooks,
4962
- payload: {
4963
- event: "PostToolUse",
4964
- cwd: this.hookCwd,
4965
- toolName: name,
4966
- toolArgs: parsedArgs,
4967
- toolResult: result
4968
- }
4969
- });
4970
- for (const w of hookWarnings(postReport.outcomes, this._turn)) yield w;
4971
4983
  }
4972
- this.appendAndPersist({
4973
- role: "tool",
4974
- tool_call_id: call.id ?? "",
4975
- name,
4976
- content: result
4977
- });
4978
- if (this.noteToolFailureSignal(result)) {
4984
+ if (chunk.length === 0) {
4985
+ chunk.push(repairedCalls[callIdx++]);
4986
+ }
4987
+ for (const call of chunk) {
4979
4988
  yield {
4980
4989
  turn: this._turn,
4981
- role: "warning",
4982
- content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailures.formatBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
4990
+ role: "tool_start",
4991
+ content: "",
4992
+ toolName: call.function?.name ?? "",
4993
+ toolArgs: call.function?.arguments ?? "{}"
4994
+ };
4995
+ }
4996
+ const settled = await Promise.allSettled(chunk.map((c) => this.runOneToolCall(c, signal)));
4997
+ for (let k = 0; k < chunk.length; k++) {
4998
+ const call = chunk[k];
4999
+ const name = call.function?.name ?? "";
5000
+ const args = call.function?.arguments ?? "{}";
5001
+ const s = settled[k];
5002
+ let result;
5003
+ let preWarnings = [];
5004
+ let postWarnings = [];
5005
+ if (s.status === "fulfilled") {
5006
+ preWarnings = s.value.preWarnings;
5007
+ postWarnings = s.value.postWarnings;
5008
+ result = s.value.result;
5009
+ } else {
5010
+ const err = s.reason instanceof Error ? s.reason : new Error(String(s.reason));
5011
+ result = JSON.stringify({ error: `${err.name}: ${err.message}` });
5012
+ }
5013
+ for (const w of preWarnings) yield w;
5014
+ for (const w of postWarnings) yield w;
5015
+ this.appendAndPersist({
5016
+ role: "tool",
5017
+ tool_call_id: call.id ?? "",
5018
+ name,
5019
+ content: result
5020
+ });
5021
+ if (this.noteToolFailureSignal(result)) {
5022
+ yield {
5023
+ turn: this._turn,
5024
+ role: "warning",
5025
+ content: `\u21E7 auto-escalating to ${ESCALATION_MODEL} for the rest of this turn \u2014 flash hit ${this._turnFailures.formatBreakdown()}. Next turn falls back to ${this.model} unless /pro is armed.`
5026
+ };
5027
+ }
5028
+ yield {
5029
+ turn: this._turn,
5030
+ role: "tool",
5031
+ content: result,
5032
+ toolName: name,
5033
+ toolArgs: args
4983
5034
  };
4984
5035
  }
4985
- yield {
4986
- turn: this._turn,
4987
- role: "tool",
4988
- content: result,
4989
- toolName: name,
4990
- toolArgs: args
4991
- };
4992
5036
  }
4993
5037
  }
4994
5038
  yield* forceSummaryAfterIterLimit(this.summaryContext(), { reason: "budget" });
@@ -5633,6 +5677,7 @@ function registerFilesystemTools(registry, opts) {
5633
5677
  };
5634
5678
  registry.register({
5635
5679
  name: "read_file",
5680
+ parallelSafe: true,
5636
5681
  description: `Read a file under the sandbox root. To save context, PREFER to scope the read instead of pulling the whole file:
5637
5682
  - head: N \u2192 first N lines (imports, public API, small configs)
5638
5683
  - tail: N \u2192 last N lines (recently-added code, log tails)
@@ -5716,6 +5761,7 @@ ${slice.join("\n")}`;
5716
5761
  });
5717
5762
  registry.register({
5718
5763
  name: "list_directory",
5764
+ parallelSafe: true,
5719
5765
  description: "List entries in a directory under the sandbox root. Returns one line per entry, marking directories with a trailing slash. Not recursive \u2014 use directory_tree for that.",
5720
5766
  readOnly: true,
5721
5767
  parameters: {
@@ -5736,6 +5782,7 @@ ${slice.join("\n")}`;
5736
5782
  });
5737
5783
  registry.register({
5738
5784
  name: "directory_tree",
5785
+ parallelSafe: true,
5739
5786
  description: `Recursively list entries in a directory. Shows indented tree structure with directories marked '/'. Budget-aware by default:
5740
5787
  - maxDepth defaults to 2 (root + one level). A depth-4 tree on a real repo blew ~5K tokens in one call. If you truly need deeper, pass maxDepth:N explicitly.
5741
5788
  - Skips ${[...SKIP_DIR_NAMES].sort().join(", ")} unless include_deps:true. Traversing into node_modules / .git / dist is almost always token-waste.
@@ -5814,6 +5861,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5814
5861
  });
5815
5862
  registry.register({
5816
5863
  name: "search_files",
5864
+ parallelSafe: true,
5817
5865
  description: "Find files whose NAME matches a substring or regex. Case-insensitive. Walks the directory recursively under the sandbox root. Returns one path per line. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) by default.",
5818
5866
  readOnly: true,
5819
5867
  parameters: {
@@ -5839,6 +5887,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5839
5887
  });
5840
5888
  registry.register({
5841
5889
  name: "search_content",
5890
+ parallelSafe: true,
5842
5891
  description: "Recursively grep file CONTENTS for a substring or regex. This is the right tool for 'find all places that call X', 'where is Y referenced', 'what files contain Z'. Different from search_files (which matches FILE NAMES). Returns one match per line in 'path:line: text' format. Skips dependency / VCS / build directories (node_modules, .git, dist, build, .next, target, .venv) and binary files by default.",
5843
5892
  readOnly: true,
5844
5893
  parameters: {
@@ -5881,6 +5930,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
5881
5930
  });
5882
5931
  registry.register({
5883
5932
  name: "get_file_info",
5933
+ parallelSafe: true,
5884
5934
  description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
5885
5935
  readOnly: true,
5886
5936
  parameters: {
@@ -6061,6 +6111,7 @@ function registerMemoryTools(registry, opts = {}) {
6061
6111
  name: "recall_memory",
6062
6112
  description: "Read the full body of a memory file when its MEMORY.md one-liner (already in the system prompt) isn't enough detail. Most of the time the index suffices \u2014 only call this when the user's question genuinely requires the full context.",
6063
6113
  readOnly: true,
6114
+ parallelSafe: true,
6064
6115
  parameters: {
6065
6116
  type: "object",
6066
6117
  properties: {
@@ -6372,7 +6423,52 @@ function registerPlanTool(registry, opts = {}) {
6372
6423
  return registry;
6373
6424
  }
6374
6425
 
6426
+ // src/tools/subagent-types.ts
6427
+ var EXPLORE_SYSTEM = `You are an exploration subagent. Wide-net read-only investigation; return one distilled answer.
6428
+
6429
+ How to operate:
6430
+ - Read-only tools only (read_file, search_files, search_content, directory_tree, list_directory, get_file_info).
6431
+ - For "find all places that call / reference / use X" \u2014 use search_content (content grep), NOT search_files (which only matches names).
6432
+ - Cast a wide net first to map the territory, then read the 3-10 most relevant files in full. Stop as soon as you can answer.
6433
+ - The parent does not see your tool calls \u2014 over-exploration is pure waste.
6434
+
6435
+ Final answer:
6436
+ - One paragraph or short bullets; lead with the conclusion.
6437
+ - Cite file:line ranges when they back the claim.
6438
+ - No follow-up offers, no "let me know if you need more" \u2014 the parent will ask again.
6439
+
6440
+ ${NEGATIVE_CLAIM_RULE}
6441
+
6442
+ ${TUI_FORMATTING_RULES}`;
6443
+ var VERIFY_SYSTEM = `You are a verify subagent. Narrow check \u2014 return YES / NO / INCONCLUSIVE with evidence. Do not expand scope.
6444
+
6445
+ How to operate:
6446
+ - Read only what's needed to verify the specific claim. No exploration past the claim.
6447
+ - Use search_content / read_file to confirm the exact behavior, type, or call site in question.
6448
+ - Cap at 6-8 tool calls. If you can't verify in that, return INCONCLUSIVE plus what's missing.
6449
+
6450
+ Final answer:
6451
+ - Lead with VERIFIED / NOT VERIFIED / INCONCLUSIVE.
6452
+ - Cite file:line for the evidence.
6453
+ - One paragraph or a few bullets. No follow-up offers.
6454
+
6455
+ ${NEGATIVE_CLAIM_RULE}
6456
+
6457
+ ${TUI_FORMATTING_RULES}`;
6458
+ var TYPES = {
6459
+ explore: { system: EXPLORE_SYSTEM, maxToolIters: 20 },
6460
+ verify: { system: VERIFY_SYSTEM, maxToolIters: 8 }
6461
+ };
6462
+ var SUBAGENT_TYPE_NAMES = Object.freeze(
6463
+ Object.keys(TYPES)
6464
+ );
6465
+
6375
6466
  // src/tools/subagent.ts
6467
+ var runIdCounter = 0;
6468
+ function nextRunId() {
6469
+ runIdCounter++;
6470
+ return `sub-${runIdCounter.toString(36)}`;
6471
+ }
6376
6472
  var DEFAULT_SUBAGENT_SYSTEM = `You are a Reasonix subagent. The parent agent spawned you to handle one focused subtask, then return.
6377
6473
 
6378
6474
  Rules:
@@ -6399,16 +6495,53 @@ async function spawnSubagent(opts) {
6399
6495
  const sink = opts.sink;
6400
6496
  const skillName = opts.skillName;
6401
6497
  const startedAt = Date.now();
6498
+ const runId = nextRunId();
6402
6499
  const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
6403
6500
  sink?.current?.({
6404
6501
  kind: "start",
6502
+ runId,
6405
6503
  task: taskPreview,
6406
6504
  skillName,
6407
6505
  model: model2,
6408
6506
  iter: 0,
6409
6507
  elapsedMs: 0
6410
6508
  });
6411
- const childTools = forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
6509
+ if (opts.allowedTools) {
6510
+ const missing = opts.allowedTools.filter((n) => !opts.parentRegistry.has(n));
6511
+ if (missing.length > 0) {
6512
+ const errorMessage2 = `subagent allow-list names tool(s) not registered in the parent: ${missing.join(", ")}. Fix the skill's \`allowed-tools\` frontmatter or check spelling.`;
6513
+ sink?.current?.({
6514
+ kind: "end",
6515
+ runId,
6516
+ task: taskPreview,
6517
+ skillName,
6518
+ model: model2,
6519
+ iter: 0,
6520
+ elapsedMs: Date.now() - startedAt,
6521
+ error: errorMessage2,
6522
+ turns: 0,
6523
+ costUsd: 0,
6524
+ usage: new Usage()
6525
+ });
6526
+ return {
6527
+ success: false,
6528
+ output: "",
6529
+ error: errorMessage2,
6530
+ turns: 0,
6531
+ toolIters: 0,
6532
+ elapsedMs: Date.now() - startedAt,
6533
+ costUsd: 0,
6534
+ model: model2,
6535
+ skillName,
6536
+ usage: new Usage()
6537
+ };
6538
+ }
6539
+ }
6540
+ const childTools = opts.allowedTools ? forkRegistryWithAllowList(
6541
+ opts.parentRegistry,
6542
+ new Set(opts.allowedTools),
6543
+ NEVER_INHERITED_TOOLS
6544
+ ) : forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
6412
6545
  const childPrefix = new ImmutablePrefix({
6413
6546
  system: opts.system,
6414
6547
  toolSpecs: childTools.specs()
@@ -6441,12 +6574,13 @@ async function spawnSubagent(opts) {
6441
6574
  let summarisingEmitted = false;
6442
6575
  try {
6443
6576
  for await (const ev of childLoop.step(opts.task)) {
6444
- sink?.current?.({ kind: "inner", task: taskPreview, skillName, model: model2, inner: ev });
6577
+ sink?.current?.({ kind: "inner", runId, task: taskPreview, skillName, model: model2, inner: ev });
6445
6578
  if (ev.role === "tool") {
6446
6579
  toolIter++;
6447
6580
  summarisingEmitted = false;
6448
6581
  sink?.current?.({
6449
6582
  kind: "progress",
6583
+ runId,
6450
6584
  task: taskPreview,
6451
6585
  skillName,
6452
6586
  model: model2,
@@ -6458,6 +6592,7 @@ async function spawnSubagent(opts) {
6458
6592
  summarisingEmitted = true;
6459
6593
  sink?.current?.({
6460
6594
  kind: "phase",
6595
+ runId,
6461
6596
  task: taskPreview,
6462
6597
  skillName,
6463
6598
  model: model2,
@@ -6494,6 +6629,7 @@ async function spawnSubagent(opts) {
6494
6629
  [\u2026truncated ${final.length - maxResultChars} chars; ask the subagent for a tighter summary if you need more.]` : final;
6495
6630
  sink?.current?.({
6496
6631
  kind: "end",
6632
+ runId,
6497
6633
  task: taskPreview,
6498
6634
  skillName,
6499
6635
  model: model2,
@@ -6560,6 +6696,19 @@ function forkRegistryExcluding(parent, exclude) {
6560
6696
  if (parent.planMode) child.setPlanMode(true);
6561
6697
  return child;
6562
6698
  }
6699
+ function forkRegistryWithAllowList(parent, allow, alsoExclude) {
6700
+ const child = new ToolRegistry();
6701
+ for (const spec of parent.specs()) {
6702
+ const name = spec.function.name;
6703
+ if (!allow.has(name)) continue;
6704
+ if (alsoExclude.has(name)) continue;
6705
+ const def2 = parent.get(name);
6706
+ if (!def2) continue;
6707
+ child.register(def2);
6708
+ }
6709
+ if (parent.planMode) child.setPlanMode(true);
6710
+ return child;
6711
+ }
6563
6712
 
6564
6713
  // src/tools/shell.ts
6565
6714
  import * as pathMod7 from "path";
@@ -7831,6 +7980,7 @@ function registerShellTools(registry, opts) {
7831
7980
  name: "job_output",
7832
7981
  description: "Read the latest output of a background job started with `run_background`. By default returns the tail of the buffer (last 80 lines). Pass `since` (the `byteLength` from a previous call) to stream only new content incrementally. Tells you whether the job is still running, so you can stop polling when it's done.",
7833
7982
  readOnly: true,
7983
+ parallelSafe: true,
7834
7984
  parameters: {
7835
7985
  type: "object",
7836
7986
  properties: {
@@ -7875,6 +8025,7 @@ function registerShellTools(registry, opts) {
7875
8025
  name: "list_jobs",
7876
8026
  description: "List every background job started this session \u2014 running and exited \u2014 with id, command, pid, status. Use when you've lost track of which job_id corresponds to which process, or to see what's still alive.",
7877
8027
  readOnly: true,
8028
+ parallelSafe: true,
7878
8029
  parameters: { type: "object", properties: {} },
7879
8030
  fn: async () => {
7880
8031
  const all = jobs2.list();
@@ -8132,6 +8283,7 @@ function registerWebTools(registry, opts = {}) {
8132
8283
  name: "web_search",
8133
8284
  description: "Search the public web. Returns ranked results with title, url, and snippet. Call this when the answer's correctness depends on current state \u2014 anything that changes over time (events, prices, releases, status of a thing in the real world). Composing such answers from training memory invents stale numbers; search first, then ground the answer in the results. For evergreen / definitional questions you don't need this.",
8134
8285
  readOnly: true,
8286
+ parallelSafe: true,
8135
8287
  parameters: {
8136
8288
  type: "object",
8137
8289
  properties: {
@@ -8155,6 +8307,7 @@ function registerWebTools(registry, opts = {}) {
8155
8307
  name: "web_fetch",
8156
8308
  description: "Download a URL and return its visible text content (HTML pages get scripts/styles/nav stripped). Truncated at the tool-result cap. Use after web_search when a snippet isn't enough.",
8157
8309
  readOnly: true,
8310
+ parallelSafe: true,
8158
8311
  parameters: {
8159
8312
  type: "object",
8160
8313
  properties: {
@@ -15452,6 +15605,7 @@ async function registerSemanticSearchTool(registry, opts) {
15452
15605
  name: "semantic_search",
15453
15606
  description: "FIRST CHOICE for descriptive queries. Use this BEFORE search_content (grep) when the user describes WHAT code does ('where do we handle X', 'which file owns Y', 'how does Z work', 'find the logic that \u2026'). Returns ranked snippets ordered by semantic relevance \u2014 finds the right file even when your description shares no words with the code. Falls back to search_content / search_files only for: exact identifiers, regex patterns, or counting occurrences of a known token. If your first instinct is grep on a paraphrased question, you are wrong \u2014 try semantic_search first.",
15454
15607
  readOnly: true,
15608
+ parallelSafe: true,
15455
15609
  parameters: {
15456
15610
  type: "object",
15457
15611
  properties: {
@@ -16830,6 +16984,7 @@ function registerSkillTools(registry, opts = {}) {
16830
16984
  name: "run_skill",
16831
16985
  description: "Invoke a playbook from the Skills index pinned in the system prompt. Each entry is a self-contained instruction block. Pass `name` as the BARE skill identifier (e.g. 'explore'), NOT the `[\u{1F9EC} subagent]` tag that appears after it in the index. Entries tagged `[\u{1F9EC} subagent]` spawn an isolated subagent \u2014 only the final distilled answer comes back, the model's tool calls + reasoning during the run never enter your context. Plain skills are inlined: the body becomes a tool result you read and follow. For subagent skills, supply 'arguments' describing the concrete task \u2014 they'll be the only context the subagent has.",
16832
16986
  readOnly: true,
16987
+ parallelSafe: true,
16833
16988
  parameters: {
16834
16989
  type: "object",
16835
16990
  properties: {
@@ -24998,9 +25153,7 @@ function subagentPhaseLabel(phase, iter, elapsedMs) {
24998
25153
  if (iter === 0) return "thinking\u2026";
24999
25154
  return "working through tools\u2026";
25000
25155
  }
25001
- function SubagentRow({
25002
- activity
25003
- }) {
25156
+ function SubagentRow({ activity }) {
25004
25157
  useTick();
25005
25158
  const seconds = (activity.elapsedMs / 1e3).toFixed(1);
25006
25159
  const phase = subagentPhaseLabel(activity.phase, activity.iter, activity.elapsedMs);
@@ -25021,6 +25174,46 @@ function SubagentRow({
25021
25174
  }
25022
25175
  ), /* @__PURE__ */ React58.createElement(Text2, { color: FG.faint }, "task ", /* @__PURE__ */ React58.createElement(Text2, { color: FG.sub }, activity.task)), /* @__PURE__ */ React58.createElement(Text2, { color: FG.faint }, "last ", last ? /* @__PURE__ */ React58.createElement(React58.Fragment, null, /* @__PURE__ */ React58.createElement(Text2, { color: last.color }, `${last.glyph} `), /* @__PURE__ */ React58.createElement(Text2, { color: FG.body }, last.label), last.meta ? /* @__PURE__ */ React58.createElement(Text2, { color: FG.faint }, ` ${last.meta}`) : null) : /* @__PURE__ */ React58.createElement(Text2, { color: FG.faint }, "queued\u2026")), /* @__PURE__ */ React58.createElement(Text2, { color: TONE.brand }, "\u25B6 ", phase));
25023
25176
  }
25177
+ function SubagentLiveStack({
25178
+ activities,
25179
+ max = 3
25180
+ }) {
25181
+ const tick = useTick();
25182
+ if (activities.length === 0) return null;
25183
+ if (activities.length === 1) return /* @__PURE__ */ React58.createElement(SubagentRow, { activity: activities[0] });
25184
+ const visible = activities.slice(0, max);
25185
+ const overflow = activities.length - visible.length;
25186
+ const summarising = activities.filter((a) => a.phase === "summarising").length;
25187
+ const metaParts = [`${activities.length} running`];
25188
+ if (summarising > 0) metaParts.push(`${summarising} summarising`);
25189
+ return /* @__PURE__ */ React58.createElement(Card, { tone: CARD.subagent.color }, /* @__PURE__ */ React58.createElement(
25190
+ CardHeader,
25191
+ {
25192
+ glyph: "\u232C",
25193
+ tone: CARD.subagent.color,
25194
+ title: "subagents",
25195
+ titleColor: PILL_SECTION.plan.fg,
25196
+ titleBg: PILL_SECTION.plan.bg,
25197
+ subtitle: metaParts.join(" \xB7 "),
25198
+ right: /* @__PURE__ */ React58.createElement(Spinner, { kind: "braille", color: CARD.subagent.color })
25199
+ }
25200
+ ), visible.map((a, i) => /* @__PURE__ */ React58.createElement(CompactSubagentLine, { key: a.runId, activity: a, tick, index: i })), overflow > 0 ? /* @__PURE__ */ React58.createElement(Text2, { color: FG.faint }, ` +${overflow} more running\u2026`) : null);
25201
+ }
25202
+ function CompactSubagentLine({
25203
+ activity,
25204
+ tick,
25205
+ index
25206
+ }) {
25207
+ const summarising = activity.phase === "summarising";
25208
+ const spinnerFrame = SPINNER_FRAMES[(tick + index) % SPINNER_FRAMES.length] ?? "\xB7";
25209
+ const glyph = summarising ? "\u25B6" : spinnerFrame;
25210
+ const glyphColor = summarising ? TONE.brand : CARD.subagent.color;
25211
+ const seconds = (activity.elapsedMs / 1e3).toFixed(1).padStart(5);
25212
+ const title = activity.skillName ?? truncate3(activity.task, 28);
25213
+ const titlePadded = title.padEnd(28);
25214
+ const last = activity.lastInner;
25215
+ return /* @__PURE__ */ React58.createElement(Box2, { flexDirection: "row" }, /* @__PURE__ */ React58.createElement(Text2, { color: glyphColor, bold: true }, ` ${glyph} `), /* @__PURE__ */ React58.createElement(Text2, { color: FG.body }, titlePadded), /* @__PURE__ */ React58.createElement(Text2, { color: FG.faint }, ` iter ${String(activity.iter).padStart(2)} \xB7 ${seconds}s \xB7 `), last ? /* @__PURE__ */ React58.createElement(React58.Fragment, null, /* @__PURE__ */ React58.createElement(Text2, { color: last.color }, `${last.glyph} `), /* @__PURE__ */ React58.createElement(Text2, { color: FG.body }, truncate3(last.label, 18)), last.meta ? /* @__PURE__ */ React58.createElement(Text2, { color: FG.faint }, ` ${last.meta}`) : null) : /* @__PURE__ */ React58.createElement(Text2, { color: FG.faint }, "queued\u2026"));
25216
+ }
25024
25217
  function truncate3(text, max) {
25025
25218
  return text.length > max ? `${text.slice(0, max)}\u2026` : text;
25026
25219
  }
@@ -28873,7 +29066,7 @@ function useSubagent({
28873
29066
  log,
28874
29067
  getWalletCurrency
28875
29068
  }) {
28876
- const [activity, setActivity] = useState17(null);
29069
+ const [activities, setActivities] = useState17([]);
28877
29070
  const sinkRef = useRef8({ current: null });
28878
29071
  const getWalletCurrencyRef = useRef8(getWalletCurrency);
28879
29072
  useEffect13(() => {
@@ -28882,24 +29075,11 @@ function useSubagent({
28882
29075
  useEffect13(() => {
28883
29076
  sinkRef.current.current = (ev) => {
28884
29077
  if (ev.kind === "start") {
28885
- setActivity({
28886
- task: ev.task,
28887
- iter: ev.iter ?? 0,
28888
- elapsedMs: ev.elapsedMs ?? 0,
28889
- skillName: ev.skillName,
28890
- model: ev.model,
28891
- phase: "exploring",
28892
- lastInner: null
28893
- });
28894
- return;
28895
- }
28896
- if (ev.kind === "progress") {
28897
- setActivity(
28898
- (prev) => prev ? {
28899
- ...prev,
28900
- iter: ev.iter ?? prev.iter,
28901
- elapsedMs: ev.elapsedMs ?? prev.elapsedMs
28902
- } : {
29078
+ setActivities((prev) => {
29079
+ if (prev.some((a) => a.runId === ev.runId)) return prev;
29080
+ const next = {
29081
+ runId: ev.runId,
29082
+ startedAt: Date.now() - (ev.elapsedMs ?? 0),
28903
29083
  task: ev.task,
28904
29084
  iter: ev.iter ?? 0,
28905
29085
  elapsedMs: ev.elapsedMs ?? 0,
@@ -28907,45 +29087,59 @@ function useSubagent({
28907
29087
  model: ev.model,
28908
29088
  phase: "exploring",
28909
29089
  lastInner: null
28910
- }
28911
- );
28912
- return;
28913
- }
28914
- if (ev.kind === "phase") {
28915
- setActivity((prev) => prev ? { ...prev, phase: ev.phase } : prev);
29090
+ };
29091
+ return [...prev, next];
29092
+ });
28916
29093
  return;
28917
29094
  }
28918
- if (ev.kind === "inner" && ev.inner) {
28919
- const summary2 = summariseInner(ev.inner);
28920
- if (!summary2) return;
28921
- setActivity((prev) => prev ? { ...prev, lastInner: summary2 } : prev);
29095
+ if (ev.kind === "end") {
29096
+ setActivities((prev) => prev.filter((a) => a.runId !== ev.runId));
29097
+ const seconds = ((ev.elapsedMs ?? 0) / 1e3).toFixed(1);
29098
+ const costTail = ev.costUsd !== void 0 && ev.costUsd > 0 ? ` \xB7 ${formatCost(ev.costUsd, getWalletCurrencyRef.current?.())}` : "";
29099
+ const summary = ev.error ? `\u232C subagent "${ev.task}" failed after ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \u2014 ${ev.error}` : `\u232C subagent "${ev.task}" done in ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \xB7 ${ev.turns ?? 0} turn(s)${costTail}`;
29100
+ log.pushInfo(summary);
29101
+ if (!ev.error && ev.usage && ev.model) {
29102
+ appendUsage({
29103
+ session: session ?? null,
29104
+ model: ev.model,
29105
+ usage: ev.usage,
29106
+ kind: "subagent",
29107
+ subagent: {
29108
+ skillName: ev.skillName,
29109
+ taskPreview: ev.task.slice(0, 60),
29110
+ toolIters: ev.iter ?? 0,
29111
+ durationMs: ev.elapsedMs ?? 0
29112
+ }
29113
+ });
29114
+ }
28922
29115
  return;
28923
29116
  }
28924
- setActivity(null);
28925
- const seconds = ((ev.elapsedMs ?? 0) / 1e3).toFixed(1);
28926
- const costTail = ev.costUsd !== void 0 && ev.costUsd > 0 ? ` \xB7 ${formatCost(ev.costUsd, getWalletCurrencyRef.current?.())}` : "";
28927
- const summary = ev.error ? `\u232C subagent "${ev.task}" failed after ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \u2014 ${ev.error}` : `\u232C subagent "${ev.task}" done in ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \xB7 ${ev.turns ?? 0} turn(s)${costTail}`;
28928
- log.pushInfo(summary);
28929
- if (!ev.error && ev.usage && ev.model) {
28930
- appendUsage({
28931
- session: session ?? null,
28932
- model: ev.model,
28933
- usage: ev.usage,
28934
- kind: "subagent",
28935
- subagent: {
28936
- skillName: ev.skillName,
28937
- taskPreview: ev.task.slice(0, 60),
28938
- toolIters: ev.iter ?? 0,
28939
- durationMs: ev.elapsedMs ?? 0
29117
+ setActivities(
29118
+ (prev) => prev.map((a) => {
29119
+ if (a.runId !== ev.runId) return a;
29120
+ if (ev.kind === "progress") {
29121
+ return {
29122
+ ...a,
29123
+ iter: ev.iter ?? a.iter,
29124
+ elapsedMs: ev.elapsedMs ?? a.elapsedMs
29125
+ };
28940
29126
  }
28941
- });
28942
- }
29127
+ if (ev.kind === "phase") {
29128
+ return { ...a, phase: ev.phase ?? a.phase };
29129
+ }
29130
+ if (ev.kind === "inner" && ev.inner) {
29131
+ const summary = summariseInner(ev.inner);
29132
+ return summary ? { ...a, lastInner: summary } : a;
29133
+ }
29134
+ return a;
29135
+ })
29136
+ );
28943
29137
  };
28944
29138
  return () => {
28945
29139
  sinkRef.current.current = null;
28946
29140
  };
28947
29141
  }, [session, log]);
28948
- return { activity, sinkRef };
29142
+ return { activities, sinkRef };
28949
29143
  }
28950
29144
 
28951
29145
  // src/cli/ui/App.tsx
@@ -29043,7 +29237,7 @@ function AppInner({
29043
29237
  };
29044
29238
  }, [stdout4]);
29045
29239
  const walletCurrencyRef = useRef9(void 0);
29046
- const { activity: subagentActivity, sinkRef: subagentSinkRef } = useSubagent({
29240
+ const { activities: subagentActivities, sinkRef: subagentSinkRef } = useSubagent({
29047
29241
  session,
29048
29242
  log,
29049
29243
  getWalletCurrency: () => walletCurrencyRef.current
@@ -29195,6 +29389,7 @@ function AppInner({
29195
29389
  // Per-skill model override (frontmatter `model: ...`),
29196
29390
  // else falls through to spawnSubagent's default.
29197
29391
  model: skill2.model,
29392
+ allowedTools: skill2.allowedTools,
29198
29393
  sink: subagentSinkRef.current,
29199
29394
  // Stamped onto every event so the TUI sink + usage log can
29200
29395
  // attribute the run to a skill without extra bookkeeping.
@@ -30812,7 +31007,7 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
30812
31007
  dashboardUrl,
30813
31008
  languageVersion
30814
31009
  }
30815
- ) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && ongoingTool ? /* @__PURE__ */ React65.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && subagentActivity ? /* @__PURE__ */ React65.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && !ongoingTool && statusLine ? /* @__PURE__ */ React65.createElement(ThinkingRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && !pendingChoice && !stagedChoiceCustom && !pendingRevision && !stagedCheckpointRevise && !pendingCheckpoint ? /* @__PURE__ */ React65.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && busy && !isStreaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React65.createElement(ThinkingRow, { text: "processing\u2026" }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview ? /* @__PURE__ */ React65.createElement(PlanLiveRow, null) : null, /* @__PURE__ */ React65.createElement(ToastRail, null)), stagedInput ? /* @__PURE__ */ React65.createElement(
31010
+ ) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && ongoingTool ? /* @__PURE__ */ React65.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && subagentActivities.length > 0 ? /* @__PURE__ */ React65.createElement(SubagentLiveStack, { activities: subagentActivities, max: 3 }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && !ongoingTool && statusLine ? /* @__PURE__ */ React65.createElement(ThinkingRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && !pendingChoice && !stagedChoiceCustom && !pendingRevision && !stagedCheckpointRevise && !pendingCheckpoint ? /* @__PURE__ */ React65.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview && busy && !isStreaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React65.createElement(ThinkingRow, { text: "processing\u2026" }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !pendingReviseEditor && !pendingSessionsPicker && !pendingMcpHub && !stagedInput && !pendingEditReview ? /* @__PURE__ */ React65.createElement(PlanLiveRow, null) : null, /* @__PURE__ */ React65.createElement(ToastRail, null)), stagedInput ? /* @__PURE__ */ React65.createElement(
30816
31011
  PlanRefineInput,
30817
31012
  {
30818
31013
  mode: stagedInput.mode,
@@ -31416,7 +31611,7 @@ async function chatCommand(opts) {
31416
31611
  import { readFileSync as readFileSync24 } from "fs";
31417
31612
  import { basename as basename2, resolve as resolve10 } from "path";
31418
31613
  async function codeCommand(opts = {}) {
31419
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-YUL7CYKY.js");
31614
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-VF7B6BWR.js");
31420
31615
  const rootDir = resolve10(opts.dir ?? process.cwd());
31421
31616
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
31422
31617
  const tools = new ToolRegistry();