reasonix 0.4.24 → 0.4.26

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
@@ -7,7 +7,7 @@ import {
7
7
  memoryEnabled,
8
8
  readProjectMemory,
9
9
  sanitizeMemoryName
10
- } from "./chunk-K6MR4SWS.js";
10
+ } from "./chunk-2BYEKJHX.js";
11
11
 
12
12
  // src/cli/index.ts
13
13
  import { Command } from "commander";
@@ -1893,6 +1893,10 @@ var CacheFirstLoop = class {
1893
1893
  usage = resp.usage;
1894
1894
  }
1895
1895
  } catch (err) {
1896
+ if (signal.aborted) {
1897
+ yield { turn: this._turn, role: "done", content: "" };
1898
+ return;
1899
+ }
1896
1900
  yield {
1897
1901
  turn: this._turn,
1898
1902
  role: "error",
@@ -2212,6 +2216,74 @@ import { promises as fs } from "fs";
2212
2216
  import * as pathMod from "path";
2213
2217
  var DEFAULT_MAX_READ_BYTES = 2 * 1024 * 1024;
2214
2218
  var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
2219
+ var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([
2220
+ "node_modules",
2221
+ ".git",
2222
+ ".hg",
2223
+ ".svn",
2224
+ "dist",
2225
+ "build",
2226
+ "out",
2227
+ ".next",
2228
+ ".nuxt",
2229
+ "target",
2230
+ // Rust / Java
2231
+ ".venv",
2232
+ "venv",
2233
+ "__pycache__",
2234
+ ".pytest_cache",
2235
+ ".mypy_cache",
2236
+ ".cache",
2237
+ "coverage"
2238
+ ]);
2239
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2240
+ ".png",
2241
+ ".jpg",
2242
+ ".jpeg",
2243
+ ".gif",
2244
+ ".bmp",
2245
+ ".ico",
2246
+ ".webp",
2247
+ ".tiff",
2248
+ ".pdf",
2249
+ ".zip",
2250
+ ".tar",
2251
+ ".gz",
2252
+ ".bz2",
2253
+ ".xz",
2254
+ ".7z",
2255
+ ".rar",
2256
+ ".exe",
2257
+ ".dll",
2258
+ ".so",
2259
+ ".dylib",
2260
+ ".bin",
2261
+ ".class",
2262
+ ".jar",
2263
+ ".war",
2264
+ ".o",
2265
+ ".obj",
2266
+ ".lib",
2267
+ ".a",
2268
+ ".woff",
2269
+ ".woff2",
2270
+ ".ttf",
2271
+ ".otf",
2272
+ ".eot",
2273
+ ".mp3",
2274
+ ".mp4",
2275
+ ".mov",
2276
+ ".avi",
2277
+ ".webm",
2278
+ ".wasm",
2279
+ ".pyc",
2280
+ ".pyo"
2281
+ ]);
2282
+ function isLikelyBinaryByName(name) {
2283
+ const dot = name.lastIndexOf(".");
2284
+ if (dot < 0) return false;
2285
+ return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
2286
+ }
2215
2287
  function registerFilesystemTools(registry, opts) {
2216
2288
  const rootDir = pathMod.resolve(opts.rootDir);
2217
2289
  const allowWriting = opts.allowWriting !== false;
@@ -2221,7 +2293,12 @@ function registerFilesystemTools(registry, opts) {
2221
2293
  if (typeof raw !== "string" || raw.length === 0) {
2222
2294
  throw new Error("path must be a non-empty string");
2223
2295
  }
2224
- const resolved = pathMod.resolve(rootDir, raw);
2296
+ let normalized = raw;
2297
+ while (normalized.startsWith("/") || normalized.startsWith("\\")) {
2298
+ normalized = normalized.slice(1);
2299
+ }
2300
+ if (normalized.length === 0) normalized = ".";
2301
+ const resolved = pathMod.resolve(rootDir, normalized);
2225
2302
  const normRoot = pathMod.resolve(rootDir);
2226
2303
  const rel = pathMod.relative(normRoot, resolved);
2227
2304
  if (rel.startsWith("..") || pathMod.isAbsolute(rel)) {
@@ -2387,6 +2464,114 @@ function registerFilesystemTools(registry, opts) {
2387
2464
  return matches.length === 0 ? "(no matches)" : matches.join("\n");
2388
2465
  }
2389
2466
  });
2467
+ registry.register({
2468
+ name: "search_content",
2469
+ 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.",
2470
+ readOnly: true,
2471
+ parameters: {
2472
+ type: "object",
2473
+ properties: {
2474
+ pattern: {
2475
+ type: "string",
2476
+ description: "Substring (or regex) to search file contents for."
2477
+ },
2478
+ path: {
2479
+ type: "string",
2480
+ description: "Directory to start the search at (default: sandbox root)."
2481
+ },
2482
+ glob: {
2483
+ type: "string",
2484
+ description: "Optional file-name suffix or substring filter. Examples: '.ts' (only TypeScript), 'test' (any file with 'test' in the name). Reduces noise when you know the file shape."
2485
+ },
2486
+ case_sensitive: {
2487
+ type: "boolean",
2488
+ description: "When true, match case exactly. Default false (case-insensitive)."
2489
+ },
2490
+ include_deps: {
2491
+ type: "boolean",
2492
+ description: "When true, also search inside node_modules / .git / dist / build / etc. Off by default \u2014 most exploration questions are about the user's own code."
2493
+ }
2494
+ },
2495
+ required: ["pattern"]
2496
+ },
2497
+ fn: async (args) => {
2498
+ const startAbs = safePath(args.path ?? ".");
2499
+ const caseSensitive = args.case_sensitive === true;
2500
+ const includeDeps = args.include_deps === true;
2501
+ const nameFilter = typeof args.glob === "string" ? args.glob.toLowerCase() : null;
2502
+ let re = null;
2503
+ try {
2504
+ re = new RegExp(args.pattern, caseSensitive ? "" : "i");
2505
+ } catch {
2506
+ re = null;
2507
+ }
2508
+ const needle = caseSensitive ? args.pattern : args.pattern.toLowerCase();
2509
+ const matches = [];
2510
+ let totalBytes = 0;
2511
+ let scanned = 0;
2512
+ let truncated = false;
2513
+ const walk2 = async (dir) => {
2514
+ if (truncated) return;
2515
+ let entries;
2516
+ try {
2517
+ entries = await fs.readdir(dir, { withFileTypes: true });
2518
+ } catch {
2519
+ return;
2520
+ }
2521
+ for (const e of entries) {
2522
+ if (truncated) return;
2523
+ if (e.isDirectory()) {
2524
+ if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
2525
+ await walk2(pathMod.join(dir, e.name));
2526
+ continue;
2527
+ }
2528
+ if (!e.isFile()) continue;
2529
+ if (nameFilter && !e.name.toLowerCase().includes(nameFilter)) continue;
2530
+ if (isLikelyBinaryByName(e.name)) continue;
2531
+ const full = pathMod.join(dir, e.name);
2532
+ let stat;
2533
+ try {
2534
+ stat = await fs.stat(full);
2535
+ } catch {
2536
+ continue;
2537
+ }
2538
+ if (stat.size > 2 * 1024 * 1024) continue;
2539
+ let raw;
2540
+ try {
2541
+ raw = await fs.readFile(full);
2542
+ } catch {
2543
+ continue;
2544
+ }
2545
+ const firstNul = raw.indexOf(0);
2546
+ if (firstNul !== -1 && firstNul < 8 * 1024) continue;
2547
+ const text = raw.toString("utf8");
2548
+ const rel = pathMod.relative(rootDir, full);
2549
+ const lines = text.split(/\r?\n/);
2550
+ for (let li = 0; li < lines.length; li++) {
2551
+ const line = lines[li];
2552
+ const lineForCheck = caseSensitive ? line : line.toLowerCase();
2553
+ const hit = re ? re.test(line) : lineForCheck.includes(needle);
2554
+ if (!hit) continue;
2555
+ const display = line.length > 200 ? `${line.slice(0, 200)}\u2026` : line;
2556
+ const out = `${rel}:${li + 1}: ${display}`;
2557
+ if (totalBytes + out.length + 1 > maxListBytes) {
2558
+ matches.push(`[\u2026 truncated at ${maxListBytes} bytes \u2014 refine pattern or path \u2026]`);
2559
+ truncated = true;
2560
+ return;
2561
+ }
2562
+ matches.push(out);
2563
+ totalBytes += out.length + 1;
2564
+ }
2565
+ scanned++;
2566
+ }
2567
+ };
2568
+ await walk2(startAbs);
2569
+ if (matches.length === 0) {
2570
+ return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
2571
+ }
2572
+ return matches.join("\n");
2573
+ }
2574
+ });
2390
2575
  registry.register({
2391
2576
  name: "get_file_info",
2392
2577
  description: "Stat a path under the sandbox root. Returns type (file|directory|symlink), size in bytes, mtime in ISO-8601.",
@@ -2713,6 +2898,127 @@ function registerPlanTool(registry, opts = {}) {
2713
2898
  return registry;
2714
2899
  }
2715
2900
 
2901
+ // src/tools/subagent.ts
2902
+ var DEFAULT_MAX_RESULT_CHARS2 = 8e3;
2903
+ var DEFAULT_MAX_ITERS = 16;
2904
+ var DEFAULT_SUBAGENT_MODEL = "deepseek-chat";
2905
+ var SUBAGENT_TOOL_NAME = "spawn_subagent";
2906
+ var NEVER_INHERITED_TOOLS = /* @__PURE__ */ new Set([SUBAGENT_TOOL_NAME, "submit_plan"]);
2907
+ async function spawnSubagent(opts) {
2908
+ const model = opts.model ?? DEFAULT_SUBAGENT_MODEL;
2909
+ const maxToolIters = opts.maxToolIters ?? DEFAULT_MAX_ITERS;
2910
+ const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
2911
+ const sink = opts.sink;
2912
+ const startedAt = Date.now();
2913
+ const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
2914
+ sink?.current?.({
2915
+ kind: "start",
2916
+ task: taskPreview,
2917
+ iter: 0,
2918
+ elapsedMs: 0
2919
+ });
2920
+ const childTools = forkRegistryExcluding(opts.parentRegistry, NEVER_INHERITED_TOOLS);
2921
+ const childPrefix = new ImmutablePrefix({
2922
+ system: opts.system,
2923
+ toolSpecs: childTools.specs()
2924
+ });
2925
+ const childLoop = new CacheFirstLoop({
2926
+ client: opts.client,
2927
+ prefix: childPrefix,
2928
+ tools: childTools,
2929
+ model,
2930
+ maxToolIters,
2931
+ hooks: [],
2932
+ stream: false
2933
+ });
2934
+ const onParentAbort = () => childLoop.abort();
2935
+ opts.parentSignal?.addEventListener("abort", onParentAbort, { once: true });
2936
+ let final = "";
2937
+ let errorMessage;
2938
+ let toolIter = 0;
2939
+ try {
2940
+ for await (const ev of childLoop.step(opts.task)) {
2941
+ if (ev.role === "tool") {
2942
+ toolIter++;
2943
+ sink?.current?.({
2944
+ kind: "progress",
2945
+ task: taskPreview,
2946
+ iter: toolIter,
2947
+ elapsedMs: Date.now() - startedAt
2948
+ });
2949
+ }
2950
+ if (ev.role === "assistant_final") {
2951
+ final = ev.content ?? "";
2952
+ }
2953
+ if (ev.role === "error") {
2954
+ errorMessage = ev.error ?? "subagent error";
2955
+ }
2956
+ }
2957
+ } catch (err) {
2958
+ errorMessage = err.message;
2959
+ } finally {
2960
+ opts.parentSignal?.removeEventListener("abort", onParentAbort);
2961
+ }
2962
+ if (!errorMessage && !final) {
2963
+ errorMessage = opts.parentSignal?.aborted ? "subagent aborted before producing an answer" : "subagent ended without producing an answer";
2964
+ }
2965
+ const elapsedMs = Date.now() - startedAt;
2966
+ const turns = childLoop.stats.turns.length;
2967
+ const costUsd2 = childLoop.stats.totalCost;
2968
+ const truncated = final.length > maxResultChars ? `${final.slice(0, maxResultChars)}
2969
+
2970
+ [\u2026truncated ${final.length - maxResultChars} chars; ask the subagent for a tighter summary if you need more.]` : final;
2971
+ sink?.current?.({
2972
+ kind: "end",
2973
+ task: taskPreview,
2974
+ iter: toolIter,
2975
+ elapsedMs,
2976
+ summary: errorMessage ? void 0 : truncated.slice(0, 120),
2977
+ error: errorMessage,
2978
+ turns
2979
+ });
2980
+ return {
2981
+ success: !errorMessage,
2982
+ output: errorMessage ? "" : truncated,
2983
+ error: errorMessage,
2984
+ turns,
2985
+ toolIters: toolIter,
2986
+ elapsedMs,
2987
+ costUsd: costUsd2
2988
+ };
2989
+ }
2990
+ function formatSubagentResult(r) {
2991
+ if (!r.success) {
2992
+ return JSON.stringify({
2993
+ success: false,
2994
+ error: r.error ?? "unknown subagent error",
2995
+ turns: r.turns,
2996
+ tool_iters: r.toolIters,
2997
+ elapsed_ms: r.elapsedMs
2998
+ });
2999
+ }
3000
+ return JSON.stringify({
3001
+ success: true,
3002
+ output: r.output,
3003
+ turns: r.turns,
3004
+ tool_iters: r.toolIters,
3005
+ elapsed_ms: r.elapsedMs,
3006
+ cost_usd: r.costUsd
3007
+ });
3008
+ }
3009
+ function forkRegistryExcluding(parent, exclude) {
3010
+ const child = new ToolRegistry();
3011
+ for (const spec of parent.specs()) {
3012
+ const name = spec.function.name;
3013
+ if (exclude.has(name)) continue;
3014
+ const def = parent.get(name);
3015
+ if (!def) continue;
3016
+ child.register(def);
3017
+ }
3018
+ if (parent.planMode) child.setPlanMode(true);
3019
+ return child;
3020
+ }
3021
+
2716
3022
  // src/tools/shell.ts
2717
3023
  import { spawn as spawn2 } from "child_process";
2718
3024
  import { existsSync as existsSync3, statSync as statSync2 } from "fs";
@@ -2910,7 +3216,7 @@ function prepareSpawn(argv, opts = {}) {
2910
3216
  const cmdline = [resolved, ...tail].map(quoteForCmdExe).join(" ");
2911
3217
  return {
2912
3218
  bin: "cmd.exe",
2913
- args: ["/d", "/s", "/c", cmdline],
3219
+ args: ["/d", "/s", "/c", withUtf8Codepage(cmdline)],
2914
3220
  // windowsVerbatimArguments prevents Node from re-quoting the /c
2915
3221
  // payload — we've already composed an exact cmd.exe command
2916
3222
  // line. Without this Node wraps our already-quoted string in
@@ -2922,12 +3228,36 @@ function prepareSpawn(argv, opts = {}) {
2922
3228
  const cmdline = [head, ...tail].map(quoteForCmdExe).join(" ");
2923
3229
  return {
2924
3230
  bin: "cmd.exe",
2925
- args: ["/d", "/s", "/c", cmdline],
3231
+ args: ["/d", "/s", "/c", withUtf8Codepage(cmdline)],
2926
3232
  spawnOverrides: { windowsVerbatimArguments: true }
2927
3233
  };
2928
3234
  }
3235
+ if (isPowerShellExe(resolved)) {
3236
+ const patched = injectPowerShellUtf8(tail);
3237
+ if (patched) {
3238
+ return { bin: resolved, args: patched, spawnOverrides: {} };
3239
+ }
3240
+ }
2929
3241
  return { bin: resolved, args: [...tail], spawnOverrides: {} };
2930
3242
  }
3243
+ function isPowerShellExe(resolved) {
3244
+ return /(?:^|[\\/])(?:powershell|pwsh)(?:\.exe)?$/i.test(resolved);
3245
+ }
3246
+ function injectPowerShellUtf8(args) {
3247
+ const prelude = "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;$OutputEncoding=[System.Text.Encoding]::UTF8;";
3248
+ for (let i = 0; i < args.length; i++) {
3249
+ const a = args[i] ?? "";
3250
+ if (/^-(?:Command|c)$/i.test(a) && i + 1 < args.length) {
3251
+ const out = [...args];
3252
+ out[i + 1] = `${prelude}${args[i + 1] ?? ""}`;
3253
+ return out;
3254
+ }
3255
+ }
3256
+ return null;
3257
+ }
3258
+ function withUtf8Codepage(cmdline) {
3259
+ return `chcp 65001 >nul & ${cmdline}`;
3260
+ }
2931
3261
  function isBareWindowsName(s) {
2932
3262
  if (!s) return false;
2933
3263
  if (s.includes("/") || s.includes("\\")) return false;
@@ -4713,23 +5043,32 @@ import { existsSync as existsSync8, statSync as statSync4 } from "fs";
4713
5043
  import { render } from "ink";
4714
5044
  import React15, { useState as useState7 } from "react";
4715
5045
 
5046
+ // src/cli/ui/App.tsx
5047
+ import { Box as Box11, Static, Text as Text11, useApp, useInput as useInput4 } from "ink";
5048
+ import React12, { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState5 } from "react";
5049
+
4716
5050
  // src/tools/skills.ts
4717
5051
  function registerSkillTools(registry, opts = {}) {
4718
- const store = new SkillStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
5052
+ const store = new SkillStore({
5053
+ homeDir: opts.homeDir,
5054
+ projectRoot: opts.projectRoot,
5055
+ disableBuiltins: opts.disableBuiltins
5056
+ });
5057
+ const subagentRunner = opts.subagentRunner;
4719
5058
  registry.register({
4720
5059
  name: "run_skill",
4721
- description: "Load the full body of a user-defined skill into this conversation. Call when the pinned Skills index (in the system prompt) lists a skill whose description matches what's being asked. Returns the skill's markdown instructions \u2014 read them and continue the loop, calling whatever filesystem / shell / web tools the skill's prose requires. Skills are user content; follow their instructions, but keep Reasonix's own safety rules (no destructive ops without confirmation, etc.).",
5060
+ description: "Invoke a playbook from the Skills index pinned in the system prompt. Each entry is a self-contained instruction block. Skills marked with \u{1F9EC} in the index 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 \u{1F9EC} subagent skills, supply 'arguments' describing the concrete task \u2014 they'll be the only context the subagent has.",
4722
5061
  readOnly: true,
4723
5062
  parameters: {
4724
5063
  type: "object",
4725
5064
  properties: {
4726
5065
  name: {
4727
5066
  type: "string",
4728
- description: "Skill identifier as it appears in the pinned Skills index (e.g. 'review', 'security-review'). Case-sensitive."
5067
+ description: "Skill identifier as it appears in the pinned Skills index (e.g. 'explore', 'review', 'security-review'). Case-sensitive."
4729
5068
  },
4730
5069
  arguments: {
4731
5070
  type: "string",
4732
- description: "Optional free-form arguments the caller wants the skill to act on. Forwarded verbatim as an 'Arguments:' line appended to the skill body; the skill's own instructions decide how to consume them."
5071
+ description: "Free-form arguments the skill should act on. For inline skills: appended to the body as an 'Arguments:' line; the skill's own instructions decide how to consume them. For \u{1F9EC} subagent skills: REQUIRED \u2014 becomes the entire task description the subagent receives, since it has no other context."
4733
5072
  }
4734
5073
  },
4735
5074
  required: ["name"]
@@ -4748,6 +5087,19 @@ function registerSkillTools(registry, opts = {}) {
4748
5087
  });
4749
5088
  }
4750
5089
  const rawArgs = typeof args.arguments === "string" ? args.arguments.trim() : "";
5090
+ if (skill.runAs === "subagent") {
5091
+ if (!subagentRunner) {
5092
+ return JSON.stringify({
5093
+ error: `run_skill: skill ${JSON.stringify(name)} is marked runAs=subagent but no subagent runner is configured for this session. Skill authors who need isolation should run inside reasonix code (or a library setup that passes subagentRunner to registerSkillTools).`
5094
+ });
5095
+ }
5096
+ if (!rawArgs) {
5097
+ return JSON.stringify({
5098
+ error: `run_skill: skill ${JSON.stringify(name)} is a subagent and requires 'arguments' \u2014 the subagent has no other context, so describe the concrete task in the arguments field.`
5099
+ });
5100
+ }
5101
+ return subagentRunner(skill, rawArgs);
5102
+ }
4751
5103
  const header2 = [
4752
5104
  `# Skill: ${skill.name}`,
4753
5105
  skill.description ? `> ${skill.description}` : "",
@@ -4764,10 +5116,6 @@ ${skill.body}${argsBlock}`;
4764
5116
  return registry;
4765
5117
  }
4766
5118
 
4767
- // src/cli/ui/App.tsx
4768
- import { Box as Box11, Static, Text as Text11, useApp, useInput as useInput4 } from "ink";
4769
- import React12, { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState5 } from "react";
4770
-
4771
5119
  // src/cli/ui/EventLog.tsx
4772
5120
  import { Box as Box3, Text as Text3 } from "ink";
4773
5121
  import React4 from "react";
@@ -4840,7 +5188,7 @@ function stripMath(s) {
4840
5188
  ).replace(/\\sqrt\s*\{([^{}]+)\}/g, (_m, g) => `\u221A(${g.trim()})`).replace(/\\boxed\s*\{([^{}]+)\}/g, (_m, g) => `\u3010${g.trim()}\u3011`).replace(/\\text\s*\{([^{}]+)\}/g, (_m, g) => g.trim()).replace(/\\overline\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0304`).replace(/\\hat\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0302`).replace(/\\vec\s*\{([^{}]+)\}/g, (_m, g) => `\u2192${g.trim()}`).replace(/\\cdot/g, "\xB7").replace(/\\times/g, "\xD7").replace(/\\div/g, "\xF7").replace(/\\pm/g, "\xB1").replace(/\\mp/g, "\u2213").replace(/\\leq/g, "\u2264").replace(/\\geq/g, "\u2265").replace(/\\neq/g, "\u2260").replace(/\\approx/g, "\u2248").replace(/\\in\b/g, "\u2208").replace(/\\notin\b/g, "\u2209").replace(/\\infty/g, "\u221E").replace(/\\sum\b/g, "\u03A3").replace(/\\prod\b/g, "\u03A0").replace(/\\int\b/g, "\u222B").replace(/\\alpha/g, "\u03B1").replace(/\\beta/g, "\u03B2").replace(/\\gamma/g, "\u03B3").replace(/\\delta/g, "\u03B4").replace(/\\theta/g, "\u03B8").replace(/\\lambda/g, "\u03BB").replace(/\\mu/g, "\u03BC").replace(/\\pi/g, "\u03C0").replace(/\\sigma/g, "\u03C3").replace(/\\phi/g, "\u03C6").replace(/\\omega/g, "\u03C9").replace(/\\implies\b/g, "\u21D2").replace(/\\iff\b/g, "\u21D4").replace(/\\to\b/g, "\u2192").replace(/\\rightarrow/g, "\u2192").replace(/\\Rightarrow/g, "\u21D2").replace(/\\leftarrow/g, "\u2190").replace(/\\Leftarrow/g, "\u21D0").replace(/\\ldots/g, "\u2026").replace(/\\cdots/g, "\u22EF").replace(/\\quad/g, " ").replace(/\\qquad/g, " ").replace(/\\,/g, " ").replace(/\\;/g, " ").replace(/\\!/g, "").replace(/\\\\/g, "\n").replace(/\^\{([\w+-]+)\}/g, (_m, g) => toSuperscript(g)).replace(/\^([0-9+\-n])/g, (_m, g) => toSuperscript(g)).replace(/_\{([\w+-]+)\}/g, (_m, g) => toSubscript(g)).replace(/_([0-9+\-])/g, (_m, g) => toSubscript(g)).replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g, "($1)/($2)").replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}/g, "$1").replace(/\\[a-zA-Z]+/g, "").replace(/[ \t]{2,}/g, " ");
4841
5189
  }
4842
5190
  var INLINE_RE = /(\*\*([^*\n]+?)\*\*|```([^\n]+?)```|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
4843
- function InlineMd({ text }) {
5191
+ function InlineMd({ text, padTo }) {
4844
5192
  const parts = [];
4845
5193
  let last = 0;
4846
5194
  let idx = 0;
@@ -4872,8 +5220,20 @@ function InlineMd({ text }) {
4872
5220
  if (last < text.length) {
4873
5221
  parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last)));
4874
5222
  }
5223
+ if (padTo !== void 0) {
5224
+ const seen = visibleWidth(text);
5225
+ if (seen < padTo) {
5226
+ parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `pad${idx++}` }, " ".repeat(padTo - seen)));
5227
+ }
5228
+ }
4875
5229
  return /* @__PURE__ */ React2.createElement(Text2, null, parts);
4876
5230
  }
5231
+ function stripInlineMarkup(s) {
5232
+ return s.replace(/\*\*([^*\n]+?)\*\*/g, "$1").replace(/```([^\n]+?)```/g, (_m, c) => c.replace(/^(\w+)\s+/, "")).replace(/`([^`\n]+?)`/g, "$1").replace(/(?<![*\w])\*([^*\n]+?)\*(?!\w)/g, "$1");
5233
+ }
5234
+ function visibleWidth(s) {
5235
+ return displayWidth(stripInlineMarkup(s));
5236
+ }
4877
5237
  function parseBlocks(raw) {
4878
5238
  const lines = raw.split(/\r?\n/);
4879
5239
  const out = [];
@@ -4980,17 +5340,47 @@ function parseBlocks(raw) {
4980
5340
  out.push({ kind: "heading", level: hm[1].length, text: hm[2].trim() });
4981
5341
  continue;
4982
5342
  }
4983
- if (line.includes("|")) {
5343
+ if (/^\s*┌─+┐\s*$/.test(line)) {
5344
+ let j = i + 1;
5345
+ const bodyLines = [];
5346
+ while (j < lines.length && !/^\s*└─+┘\s*$/.test(lines[j])) {
5347
+ const inner = lines[j];
5348
+ const m = inner.match(/^\s*│\s?(.*?)\s?│\s*$/);
5349
+ bodyLines.push(m ? m[1] ?? "" : inner);
5350
+ j++;
5351
+ }
5352
+ if (j < lines.length) {
5353
+ flushPara();
5354
+ flushList();
5355
+ out.push({ kind: "code", lang: "", text: bodyLines.join("\n") });
5356
+ i = j;
5357
+ continue;
5358
+ }
5359
+ }
5360
+ if (line.includes("|") || line.includes("\u2502")) {
4984
5361
  const next = (lines[i + 1] ?? "").trim();
4985
- if (/^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(next)) {
5362
+ const isGfmSep = /^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(next);
5363
+ const isBoxSep = /^[│─┼┬┴┌┐└┘├┤\s]+$/.test(next) && /─{2,}/.test(next);
5364
+ if (isGfmSep || isBoxSep) {
4986
5365
  flushPara();
4987
5366
  flushList();
4988
5367
  const header2 = splitTableRow(line);
5368
+ const colCount = header2.length;
4989
5369
  const rows = [];
4990
5370
  let j = i + 2;
4991
5371
  while (j < lines.length) {
4992
5372
  const r = lines[j].replace(/\s+$/g, "");
4993
- if (r.trim() === "" || !r.includes("|")) break;
5373
+ if (r.trim() === "") break;
5374
+ if (!r.includes("|") && !r.includes("\u2502")) {
5375
+ const prev = rows[rows.length - 1];
5376
+ if (prev && prev.length === colCount) {
5377
+ const lastIdx = prev.length - 1;
5378
+ prev[lastIdx] = `${prev[lastIdx] ?? ""} ${r.trim()}`;
5379
+ j++;
5380
+ continue;
5381
+ }
5382
+ break;
5383
+ }
4994
5384
  rows.push(splitTableRow(r));
4995
5385
  j++;
4996
5386
  }
@@ -5049,7 +5439,7 @@ function BlockView({ block }) {
5049
5439
  }
5050
5440
  function splitTableRow(line) {
5051
5441
  const SENTINEL = "\0";
5052
- const masked = line.replace(/\\\|/g, SENTINEL);
5442
+ const masked = line.replace(/\\\|/g, SENTINEL).replace(/│/g, "|");
5053
5443
  const trimmed = masked.trim().replace(/^\||\|$/g, "");
5054
5444
  return trimmed.split("|").map((c) => c.trim().replace(new RegExp(SENTINEL, "g"), "|"));
5055
5445
  }
@@ -5057,24 +5447,19 @@ function TableBlockRow({ block }) {
5057
5447
  const colCount = Math.max(block.header.length, ...block.rows.map((r) => r.length));
5058
5448
  const widths = [];
5059
5449
  for (let c = 0; c < colCount; c++) {
5060
- const cellLengths = [displayWidth(block.header[c] ?? "")];
5061
- for (const r of block.rows) cellLengths.push(displayWidth(r[c] ?? ""));
5450
+ const cellLengths = [visibleWidth(block.header[c] ?? "")];
5451
+ for (const r of block.rows) cellLengths.push(visibleWidth(r[c] ?? ""));
5062
5452
  widths.push(Math.min(40, Math.max(3, ...cellLengths)));
5063
5453
  }
5064
- const pad3 = (s, w) => {
5065
- const dw = displayWidth(s);
5066
- if (dw >= w) return s;
5067
- return s + " ".repeat(w - dw);
5068
- };
5069
5454
  const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
5070
5455
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Box2, null, block.header.map((cell, ci) => (
5071
5456
  // biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
5072
- /* @__PURE__ */ React2.createElement(Text2, { key: `h-${ci}`, bold: true, color: "cyan" }, pad3(cell, widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
5457
+ /* @__PURE__ */ React2.createElement(Text2, { key: `h-${ci}`, bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: cell, padTo: widths[ci] ?? 3 }), ci < colCount - 1 ? " \u2502 " : "")
5073
5458
  ))), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, separator), block.rows.map((row2, ri) => (
5074
5459
  // biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
5075
5460
  /* @__PURE__ */ React2.createElement(Box2, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
5076
5461
  // biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
5077
- /* @__PURE__ */ React2.createElement(Text2, { key: `c-${ri}-${ci}` }, pad3(row2[ci] ?? "", widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
5462
+ /* @__PURE__ */ React2.createElement(Text2, { key: `c-${ri}-${ci}` }, /* @__PURE__ */ React2.createElement(InlineMd, { text: row2[ci] ?? "", padTo: widths[ci] ?? 3 }), ci < colCount - 1 ? " \u2502 " : "")
5078
5463
  )))
5079
5464
  )));
5080
5465
  }
@@ -6710,6 +7095,7 @@ function App({
6710
7095
  const abortedThisTurn = useRef2(false);
6711
7096
  const [ongoingTool, setOngoingTool] = useState5(null);
6712
7097
  const [toolProgress, setToolProgress] = useState5(null);
7098
+ const [subagentActivity, setSubagentActivity] = useState5(null);
6713
7099
  const [statusLine, setStatusLine] = useState5(null);
6714
7100
  const [balance, setBalance] = useState5(null);
6715
7101
  const [latestVersion, setLatestVersion] = useState5(null);
@@ -6766,9 +7152,30 @@ function App({
6766
7152
  });
6767
7153
  }, [slashMatches]);
6768
7154
  const loopRef = useRef2(null);
7155
+ const subagentSinkRef = useRef2({ current: null });
6769
7156
  const loop = useMemo(() => {
6770
7157
  if (loopRef.current) return loopRef.current;
6771
7158
  const client = new DeepSeekClient();
7159
+ if (tools && !tools.has("run_skill")) {
7160
+ registerSkillTools(tools, {
7161
+ projectRoot: codeMode?.rootDir,
7162
+ subagentRunner: async (skill, task) => {
7163
+ const result = await spawnSubagent({
7164
+ client,
7165
+ parentRegistry: tools,
7166
+ // Skill body is the subagent's persona/playbook; the user-
7167
+ // supplied task is what to actually do inside it.
7168
+ system: skill.body,
7169
+ task,
7170
+ // Per-skill model override (frontmatter `model: ...`),
7171
+ // else falls through to spawnSubagent's default.
7172
+ model: skill.model,
7173
+ sink: subagentSinkRef.current
7174
+ });
7175
+ return formatSubagentResult(result);
7176
+ }
7177
+ });
7178
+ }
6772
7179
  const prefix = new ImmutablePrefix({
6773
7180
  system,
6774
7181
  toolSpecs: tools?.specs()
@@ -6786,7 +7193,7 @@ function App({
6786
7193
  });
6787
7194
  loopRef.current = l;
6788
7195
  return l;
6789
- }, [model, system, harvest2, branch, session, tools]);
7196
+ }, [model, system, harvest2, branch, session, tools, codeMode]);
6790
7197
  useEffect2(() => {
6791
7198
  loop.hooks = hookList;
6792
7199
  }, [loop, hookList]);
@@ -6826,6 +7233,40 @@ function App({
6826
7233
  if (progressSink.current) progressSink.current = null;
6827
7234
  };
6828
7235
  }, [progressSink]);
7236
+ useEffect2(() => {
7237
+ subagentSinkRef.current.current = (ev) => {
7238
+ if (ev.kind === "start") {
7239
+ setSubagentActivity({
7240
+ task: ev.task,
7241
+ iter: ev.iter ?? 0,
7242
+ elapsedMs: ev.elapsedMs ?? 0
7243
+ });
7244
+ return;
7245
+ }
7246
+ if (ev.kind === "progress") {
7247
+ setSubagentActivity({
7248
+ task: ev.task,
7249
+ iter: ev.iter ?? 0,
7250
+ elapsedMs: ev.elapsedMs ?? 0
7251
+ });
7252
+ return;
7253
+ }
7254
+ setSubagentActivity(null);
7255
+ const seconds = ((ev.elapsedMs ?? 0) / 1e3).toFixed(1);
7256
+ const summary2 = ev.error ? `\u{1F9EC} subagent "${ev.task}" failed after ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \u2014 ${ev.error}` : `\u{1F9EC} subagent "${ev.task}" done in ${seconds}s \xB7 ${ev.iter ?? 0} tool call(s) \xB7 ${ev.turns ?? 0} turn(s)`;
7257
+ setHistorical((prev) => [
7258
+ ...prev,
7259
+ {
7260
+ id: `subagent-end-${Date.now()}`,
7261
+ role: "info",
7262
+ text: summary2
7263
+ }
7264
+ ]);
7265
+ };
7266
+ return () => {
7267
+ subagentSinkRef.current.current = null;
7268
+ };
7269
+ }, []);
6829
7270
  const sessionBannerShown = useRef2(false);
6830
7271
  useEffect2(() => {
6831
7272
  if (sessionBannerShown.current) return;
@@ -7436,7 +7877,7 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
7436
7877
  balance,
7437
7878
  updateAvailable
7438
7879
  }
7439
- ), /* @__PURE__ */ React12.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React12.createElement(EventRow, { key: item.id, event: item })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && streaming ? /* @__PURE__ */ React12.createElement(Box11, { marginY: 1 }, /* @__PURE__ */ React12.createElement(EventRow, { event: streaming })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && ongoingTool ? /* @__PURE__ */ React12.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !ongoingTool && statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React12.createElement(
7880
+ ), /* @__PURE__ */ React12.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React12.createElement(EventRow, { key: item.id, event: item })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && streaming ? /* @__PURE__ */ React12.createElement(Box11, { marginY: 1 }, /* @__PURE__ */ React12.createElement(EventRow, { event: streaming })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && ongoingTool ? /* @__PURE__ */ React12.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && subagentActivity ? /* @__PURE__ */ React12.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !ongoingTool && statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React12.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React12.createElement(
7440
7881
  PlanRefineInput,
7441
7882
  {
7442
7883
  mode: stagedInput.mode,
@@ -7466,6 +7907,13 @@ function StatusRow({ text }) {
7466
7907
  const elapsed = useElapsedSeconds();
7467
7908
  return /* @__PURE__ */ React12.createElement(Box11, { marginY: 1 }, /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, ` ${text}`), /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, ` ${elapsed}s`));
7468
7909
  }
7910
+ function SubagentRow({
7911
+ activity
7912
+ }) {
7913
+ const tick = useTick();
7914
+ const seconds = (activity.elapsedMs / 1e3).toFixed(1);
7915
+ return /* @__PURE__ */ React12.createElement(Box11, { paddingLeft: 2 }, /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, SPINNER_FRAMES[tick % SPINNER_FRAMES.length]), /* @__PURE__ */ React12.createElement(Text11, { color: "magenta" }, ` \u{1F9EC} subagent: ${activity.task}`), /* @__PURE__ */ React12.createElement(Text11, { dimColor: true }, ` \xB7 iter ${activity.iter} \xB7 ${seconds}s`));
7916
+ }
7469
7917
  function OngoingToolRow({
7470
7918
  tool,
7471
7919
  progress
@@ -7784,7 +8232,6 @@ async function chatCommand(opts) {
7784
8232
  if (!opts.seedTools) {
7785
8233
  if (!tools) tools = new ToolRegistry();
7786
8234
  registerMemoryTools(tools, {});
7787
- registerSkillTools(tools);
7788
8235
  }
7789
8236
  let sessionPreview;
7790
8237
  if (opts.session && !opts.forceResume && !opts.forceNew) {
@@ -7824,7 +8271,7 @@ async function chatCommand(opts) {
7824
8271
  // src/cli/commands/code.tsx
7825
8272
  import { basename, resolve as resolve5 } from "path";
7826
8273
  async function codeCommand(opts = {}) {
7827
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-VDN5U3YE.js");
8274
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-6DMLWG2H.js");
7828
8275
  const rootDir = resolve5(opts.dir ?? process.cwd());
7829
8276
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
7830
8277
  const tools = new ToolRegistry();
@@ -7840,7 +8287,6 @@ async function codeCommand(opts = {}) {
7840
8287
  });
7841
8288
  registerPlanTool(tools);
7842
8289
  registerMemoryTools(tools, { projectRoot: rootDir });
7843
- registerSkillTools(tools, { projectRoot: rootDir });
7844
8290
  process.stderr.write(
7845
8291
  `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
7846
8292
  `