reasonix 0.4.24 → 0.4.27

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-ANMDY236.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;
@@ -4709,27 +5039,36 @@ function formatLogSize(path = defaultUsageLogPath()) {
4709
5039
  }
4710
5040
 
4711
5041
  // src/cli/commands/chat.tsx
4712
- import { existsSync as existsSync8, statSync as statSync4 } from "fs";
5042
+ import { existsSync as existsSync8, statSync as statSync5 } 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";
@@ -4789,6 +5137,8 @@ function PlanStateBlock({ planState }) {
4789
5137
  }
4790
5138
 
4791
5139
  // src/cli/ui/markdown.tsx
5140
+ import { readFileSync as readFileSync9, statSync as statSync4 } from "fs";
5141
+ import { isAbsolute as isAbsolute3, join as join7 } from "path";
4792
5142
  import { Box as Box2, Text as Text2 } from "ink";
4793
5143
  import React2 from "react";
4794
5144
  var SUPERSCRIPT = {
@@ -4839,8 +5189,75 @@ function stripMath(s) {
4839
5189
  (_m, n, k) => `C(${n.trim()},${k.trim()})`
4840
5190
  ).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
5191
  }
4842
- var INLINE_RE = /(\*\*([^*\n]+?)\*\*|```([^\n]+?)```|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
4843
- function InlineMd({ text }) {
5192
+ function isExternalUrl(url) {
5193
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith("mailto:") || url.startsWith("//");
5194
+ }
5195
+ function parseCitationUrl(url) {
5196
+ const trimmed = url.trim();
5197
+ if (!trimmed) return null;
5198
+ let m = trimmed.match(/^(.+?)#L(\d+)(?:-L?(\d+))?$/);
5199
+ if (m) {
5200
+ return {
5201
+ path: m[1] ?? "",
5202
+ startLine: Number(m[2]),
5203
+ endLine: m[3] ? Number(m[3]) : void 0
5204
+ };
5205
+ }
5206
+ m = trimmed.match(/^(.+?):(\d+)(?:-(\d+))?$/);
5207
+ if (m) {
5208
+ return {
5209
+ path: m[1] ?? "",
5210
+ startLine: Number(m[2]),
5211
+ endLine: m[3] ? Number(m[3]) : void 0
5212
+ };
5213
+ }
5214
+ return { path: trimmed };
5215
+ }
5216
+ function validateCitation(url, projectRoot) {
5217
+ const parts = parseCitationUrl(url);
5218
+ if (!parts || !parts.path) return { ok: false, reason: "empty path" };
5219
+ const fullPath = isAbsolute3(parts.path) ? parts.path : join7(projectRoot, parts.path);
5220
+ let stat;
5221
+ try {
5222
+ stat = statSync4(fullPath);
5223
+ } catch {
5224
+ return { ok: false, reason: "file not found" };
5225
+ }
5226
+ if (!stat.isFile()) return { ok: false, reason: "not a file" };
5227
+ if (parts.startLine === void 0) return { ok: true };
5228
+ let lineCount;
5229
+ try {
5230
+ lineCount = readFileSync9(fullPath, "utf8").split("\n").length;
5231
+ } catch {
5232
+ return { ok: false, reason: "unreadable" };
5233
+ }
5234
+ if (parts.startLine < 1 || parts.startLine > lineCount) {
5235
+ return { ok: false, reason: `line ${parts.startLine} > ${lineCount}` };
5236
+ }
5237
+ if (parts.endLine !== void 0) {
5238
+ if (parts.endLine < parts.startLine || parts.endLine > lineCount) {
5239
+ return { ok: false, reason: `range end ${parts.endLine} invalid` };
5240
+ }
5241
+ }
5242
+ return { ok: true };
5243
+ }
5244
+ function collectCitations(text, projectRoot) {
5245
+ const map = /* @__PURE__ */ new Map();
5246
+ const re = /\[([^\]\n]+)\]\(([^)\n]+)\)/g;
5247
+ for (const m of text.matchAll(re)) {
5248
+ const url = m[2] ?? "";
5249
+ if (!url || isExternalUrl(url)) continue;
5250
+ if (map.has(url)) continue;
5251
+ map.set(url, validateCitation(url, projectRoot));
5252
+ }
5253
+ return map;
5254
+ }
5255
+ var INLINE_RE = /(\[([^\]\n]+)\]\(([^)\n]+)\)|\*\*([^*\n]+?)\*\*|```([^\n]+?)```|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
5256
+ function InlineMd({
5257
+ text,
5258
+ padTo,
5259
+ citations
5260
+ }) {
4844
5261
  const parts = [];
4845
5262
  let last = 0;
4846
5263
  let idx = 0;
@@ -4849,22 +5266,41 @@ function InlineMd({ text }) {
4849
5266
  if (start > last) {
4850
5267
  parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last, start)));
4851
5268
  }
4852
- if (m[2] !== void 0) {
5269
+ if (m[2] !== void 0 && m[3] !== void 0) {
5270
+ const linkText = m[2];
5271
+ const url = m[3];
5272
+ if (isExternalUrl(url)) {
5273
+ parts.push(
5274
+ /* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "blue", underline: true }, linkText)
5275
+ );
5276
+ } else {
5277
+ const status = citations?.get(url);
5278
+ if (status && !status.ok) {
5279
+ parts.push(
5280
+ /* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "red", strikethrough: true }, `${linkText} \u274C`)
5281
+ );
5282
+ } else {
5283
+ parts.push(
5284
+ /* @__PURE__ */ React2.createElement(Text2, { key: `l${idx++}`, color: "cyan", underline: true }, linkText)
5285
+ );
5286
+ }
5287
+ }
5288
+ } else if (m[4] !== void 0) {
4853
5289
  parts.push(
4854
- /* @__PURE__ */ React2.createElement(Text2, { key: `b${idx++}`, bold: true }, m[2])
5290
+ /* @__PURE__ */ React2.createElement(Text2, { key: `b${idx++}`, bold: true }, m[4])
4855
5291
  );
4856
- } else if (m[3] !== void 0) {
4857
- const stripped = m[3].replace(/^(\w+)\s+/, "");
5292
+ } else if (m[5] !== void 0) {
5293
+ const stripped = m[5].replace(/^(\w+)\s+/, "");
4858
5294
  parts.push(
4859
5295
  /* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, stripped)
4860
5296
  );
4861
- } else if (m[4] !== void 0) {
5297
+ } else if (m[6] !== void 0) {
4862
5298
  parts.push(
4863
- /* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, m[4])
5299
+ /* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, m[6])
4864
5300
  );
4865
- } else if (m[5] !== void 0) {
5301
+ } else if (m[7] !== void 0) {
4866
5302
  parts.push(
4867
- /* @__PURE__ */ React2.createElement(Text2, { key: `i${idx++}`, italic: true }, m[5])
5303
+ /* @__PURE__ */ React2.createElement(Text2, { key: `i${idx++}`, italic: true }, m[7])
4868
5304
  );
4869
5305
  }
4870
5306
  last = start + m[0].length;
@@ -4872,8 +5308,20 @@ function InlineMd({ text }) {
4872
5308
  if (last < text.length) {
4873
5309
  parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last)));
4874
5310
  }
5311
+ if (padTo !== void 0) {
5312
+ const seen = visibleWidth(text);
5313
+ if (seen < padTo) {
5314
+ parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `pad${idx++}` }, " ".repeat(padTo - seen)));
5315
+ }
5316
+ }
4875
5317
  return /* @__PURE__ */ React2.createElement(Text2, null, parts);
4876
5318
  }
5319
+ function stripInlineMarkup(s) {
5320
+ return s.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, "$1").replace(/\*\*([^*\n]+?)\*\*/g, "$1").replace(/```([^\n]+?)```/g, (_m, c) => c.replace(/^(\w+)\s+/, "")).replace(/`([^`\n]+?)`/g, "$1").replace(/(?<![*\w])\*([^*\n]+?)\*(?!\w)/g, "$1");
5321
+ }
5322
+ function visibleWidth(s) {
5323
+ return displayWidth(stripInlineMarkup(s));
5324
+ }
4877
5325
  function parseBlocks(raw) {
4878
5326
  const lines = raw.split(/\r?\n/);
4879
5327
  const out = [];
@@ -4980,17 +5428,47 @@ function parseBlocks(raw) {
4980
5428
  out.push({ kind: "heading", level: hm[1].length, text: hm[2].trim() });
4981
5429
  continue;
4982
5430
  }
4983
- if (line.includes("|")) {
5431
+ if (/^\s*┌─+┐\s*$/.test(line)) {
5432
+ let j = i + 1;
5433
+ const bodyLines = [];
5434
+ while (j < lines.length && !/^\s*└─+┘\s*$/.test(lines[j])) {
5435
+ const inner = lines[j];
5436
+ const m = inner.match(/^\s*│\s?(.*?)\s?│\s*$/);
5437
+ bodyLines.push(m ? m[1] ?? "" : inner);
5438
+ j++;
5439
+ }
5440
+ if (j < lines.length) {
5441
+ flushPara();
5442
+ flushList();
5443
+ out.push({ kind: "code", lang: "", text: bodyLines.join("\n") });
5444
+ i = j;
5445
+ continue;
5446
+ }
5447
+ }
5448
+ if (line.includes("|") || line.includes("\u2502")) {
4984
5449
  const next = (lines[i + 1] ?? "").trim();
4985
- if (/^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(next)) {
5450
+ const isGfmSep = /^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(next);
5451
+ const isBoxSep = /^[│─┼┬┴┌┐└┘├┤\s]+$/.test(next) && /─{2,}/.test(next);
5452
+ if (isGfmSep || isBoxSep) {
4986
5453
  flushPara();
4987
5454
  flushList();
4988
5455
  const header2 = splitTableRow(line);
5456
+ const colCount = header2.length;
4989
5457
  const rows = [];
4990
5458
  let j = i + 2;
4991
5459
  while (j < lines.length) {
4992
5460
  const r = lines[j].replace(/\s+$/g, "");
4993
- if (r.trim() === "" || !r.includes("|")) break;
5461
+ if (r.trim() === "") break;
5462
+ if (!r.includes("|") && !r.includes("\u2502")) {
5463
+ const prev = rows[rows.length - 1];
5464
+ if (prev && prev.length === colCount) {
5465
+ const lastIdx = prev.length - 1;
5466
+ prev[lastIdx] = `${prev[lastIdx] ?? ""} ${r.trim()}`;
5467
+ j++;
5468
+ continue;
5469
+ }
5470
+ break;
5471
+ }
4994
5472
  rows.push(splitTableRow(r));
4995
5473
  j++;
4996
5474
  }
@@ -5029,52 +5507,47 @@ function parseBlocks(raw) {
5029
5507
  flushList();
5030
5508
  return out;
5031
5509
  }
5032
- function BlockView({ block }) {
5510
+ function BlockView({ block, citations }) {
5033
5511
  switch (block.kind) {
5034
5512
  case "heading":
5035
- return /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text }));
5513
+ return /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text, citations }));
5036
5514
  case "paragraph":
5037
- return /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text });
5515
+ return /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text, citations });
5038
5516
  case "bullet":
5039
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React2.createElement(Box2, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React2.createElement(InlineMd, { text: item }))));
5517
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React2.createElement(Box2, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React2.createElement(InlineMd, { text: item, citations }))));
5040
5518
  case "code":
5041
5519
  return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
5042
5520
  case "edit-block":
5043
5521
  return /* @__PURE__ */ React2.createElement(EditBlockRow, { block });
5044
5522
  case "table":
5045
- return /* @__PURE__ */ React2.createElement(TableBlockRow, { block });
5523
+ return /* @__PURE__ */ React2.createElement(TableBlockRow, { block, citations });
5046
5524
  case "hr":
5047
5525
  return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5048
5526
  }
5049
5527
  }
5050
5528
  function splitTableRow(line) {
5051
5529
  const SENTINEL = "\0";
5052
- const masked = line.replace(/\\\|/g, SENTINEL);
5530
+ const masked = line.replace(/\\\|/g, SENTINEL).replace(/│/g, "|");
5053
5531
  const trimmed = masked.trim().replace(/^\||\|$/g, "");
5054
5532
  return trimmed.split("|").map((c) => c.trim().replace(new RegExp(SENTINEL, "g"), "|"));
5055
5533
  }
5056
- function TableBlockRow({ block }) {
5534
+ function TableBlockRow({ block, citations }) {
5057
5535
  const colCount = Math.max(block.header.length, ...block.rows.map((r) => r.length));
5058
5536
  const widths = [];
5059
5537
  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] ?? ""));
5538
+ const cellLengths = [visibleWidth(block.header[c] ?? "")];
5539
+ for (const r of block.rows) cellLengths.push(visibleWidth(r[c] ?? ""));
5062
5540
  widths.push(Math.min(40, Math.max(3, ...cellLengths)));
5063
5541
  }
5064
- const pad3 = (s, w) => {
5065
- const dw = displayWidth(s);
5066
- if (dw >= w) return s;
5067
- return s + " ".repeat(w - dw);
5068
- };
5069
5542
  const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
5070
5543
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Box2, null, block.header.map((cell, ci) => (
5071
5544
  // 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 " : "")
5545
+ /* @__PURE__ */ React2.createElement(Text2, { key: `h-${ci}`, bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: cell, padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
5073
5546
  ))), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, separator), block.rows.map((row2, ri) => (
5074
5547
  // biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
5075
5548
  /* @__PURE__ */ React2.createElement(Box2, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
5076
5549
  // 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 " : "")
5550
+ /* @__PURE__ */ React2.createElement(Text2, { key: `c-${ri}-${ci}` }, /* @__PURE__ */ React2.createElement(InlineMd, { text: row2[ci] ?? "", padTo: widths[ci] ?? 3, citations }), ci < colCount - 1 ? " \u2502 " : "")
5078
5551
  )))
5079
5552
  )));
5080
5553
  }
@@ -5096,10 +5569,22 @@ function EditBlockRow({ block }) {
5096
5569
  const replaceLines = block.replace.split("\n");
5097
5570
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, block.filename), isNewFile ? /* @__PURE__ */ React2.createElement(Text2, { color: "green", bold: true }, " (new file)") : null), isNewFile ? null : /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, searchLines.map((line, i) => /* @__PURE__ */ React2.createElement(Text2, { key: `s-${i}-${line.length}`, color: "red" }, `- ${line}`))), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: isNewFile ? 1 : 0 }, replaceLines.map((line, i) => /* @__PURE__ */ React2.createElement(Text2, { key: `r-${i}-${line.length}`, color: "green" }, `+ ${line}`))));
5098
5571
  }
5099
- function Markdown({ text }) {
5572
+ function Markdown({ text, projectRoot }) {
5100
5573
  const cleaned = stripMath(text);
5574
+ const root = projectRoot ?? process.cwd();
5575
+ const citations = React2.useMemo(() => collectCitations(cleaned, root), [cleaned, root]);
5101
5576
  const blocks = React2.useMemo(() => parseBlocks(cleaned), [cleaned]);
5102
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React2.createElement(BlockView, { key: `${i}-${b.kind}`, block: b })));
5577
+ const broken = [];
5578
+ for (const [url, status] of citations) {
5579
+ if (!status.ok) broken.push({ url, reason: status.reason });
5580
+ }
5581
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React2.createElement(BlockView, { key: `${i}-${b.kind}`, block: b, citations })), broken.length > 0 ? /* @__PURE__ */ React2.createElement(BrokenCitationsBlock, { items: broken }) : null);
5582
+ }
5583
+ function BrokenCitationsBlock({ items }) {
5584
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, `\u26A0 ${items.length} broken citation${items.length > 1 ? "s" : ""} \u2014 the model referenced paths or lines that don't exist`), items.map((b, i) => (
5585
+ // biome-ignore lint/suspicious/noArrayIndexKey: list is derived from a Map iteration order, stable per render
5586
+ /* @__PURE__ */ React2.createElement(Text2, { key: `bc-${i}`, color: "red" }, ` \u274C ${b.url} \u2192 ${b.reason}`)
5587
+ )));
5103
5588
  }
5104
5589
 
5105
5590
  // src/cli/ui/ticker.tsx
@@ -5125,13 +5610,16 @@ function useElapsedSeconds() {
5125
5610
  }
5126
5611
 
5127
5612
  // src/cli/ui/EventLog.tsx
5128
- var EventRow = React4.memo(function EventRow2({ event }) {
5613
+ var EventRow = React4.memo(function EventRow2({
5614
+ event,
5615
+ projectRoot
5616
+ }) {
5129
5617
  if (event.role === "user") {
5130
5618
  return /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React4.createElement(Text3, null, event.text));
5131
5619
  }
5132
5620
  if (event.role === "assistant") {
5133
5621
  if (event.streaming) return /* @__PURE__ */ React4.createElement(StreamingAssistant, { event });
5134
- return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React4.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React4.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React4.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React4.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React4.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta" }, event.repair) : null);
5622
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React4.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React4.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React4.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React4.createElement(Markdown, { text: event.text, projectRoot }) : /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React4.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React4.createElement(Text3, { color: "magenta" }, event.repair) : null);
5135
5623
  }
5136
5624
  if (event.role === "tool") {
5137
5625
  const isError = event.text.startsWith("ERROR:");
@@ -5347,14 +5835,14 @@ function findNextEnabled(items, from, step) {
5347
5835
 
5348
5836
  // src/cli/ui/PlanConfirm.tsx
5349
5837
  var DEFAULT_MAX_RENDERED = 2400;
5350
- function PlanConfirm({ plan, onChoose, maxRenderedChars }) {
5838
+ function PlanConfirm({ plan, onChoose, maxRenderedChars, projectRoot }) {
5351
5839
  const cap = maxRenderedChars ?? DEFAULT_MAX_RENDERED;
5352
5840
  const tooLong = plan.length > cap;
5353
5841
  const visible = tooLong ? `${plan.slice(0, cap)}
5354
5842
 
5355
5843
  \u2026 (${plan.length - cap} chars truncated \u2014 use /tool to view the full proposal)` : plan;
5356
5844
  const hasOpenQuestions = /^#{1,6}\s*(open[-\s]?questions?|risks?|unknowns?|assumptions?|unclear)/im.test(plan) || /^#{1,6}\s*(待确认|开放问题|风险|未知|假设|不确定)/im.test(plan);
5357
- return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Markdown, { text: visible })), hasOpenQuestions ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(
5845
+ return /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React6.createElement(Box5, null, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "\u25B8 plan submitted \u2014 awaiting your review")), /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Markdown, { text: visible, projectRoot })), hasOpenQuestions ? /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "yellow" }, "\u25B2 the plan has open questions or flagged risks \u2014 pick", " ", /* @__PURE__ */ React6.createElement(Text5, { bold: true }, "Refine / answer questions"), " to write concrete answers before the model moves on.")) : null, /* @__PURE__ */ React6.createElement(Box5, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(
5358
5846
  SingleSelect,
5359
5847
  {
5360
5848
  initialValue: hasOpenQuestions ? "refine" : "approve",
@@ -5721,7 +6209,7 @@ function formatTokens(n) {
5721
6209
  import { spawnSync } from "child_process";
5722
6210
 
5723
6211
  // src/cli/commands/stats.ts
5724
- import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
6212
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
5725
6213
  function statsCommand(opts) {
5726
6214
  if (opts.transcript) {
5727
6215
  transcriptSummary(opts.transcript);
@@ -5734,7 +6222,7 @@ function transcriptSummary(path) {
5734
6222
  console.error(`no such transcript: ${path}`);
5735
6223
  process.exit(1);
5736
6224
  }
5737
- const lines = readFileSync9(path, "utf8").split(/\r?\n/).filter(Boolean);
6225
+ const lines = readFileSync10(path, "utf8").split(/\r?\n/).filter(Boolean);
5738
6226
  let assistantTurns = 0;
5739
6227
  let toolCalls = 0;
5740
6228
  let lastTurn = 0;
@@ -6710,6 +7198,7 @@ function App({
6710
7198
  const abortedThisTurn = useRef2(false);
6711
7199
  const [ongoingTool, setOngoingTool] = useState5(null);
6712
7200
  const [toolProgress, setToolProgress] = useState5(null);
7201
+ const [subagentActivity, setSubagentActivity] = useState5(null);
6713
7202
  const [statusLine, setStatusLine] = useState5(null);
6714
7203
  const [balance, setBalance] = useState5(null);
6715
7204
  const [latestVersion, setLatestVersion] = useState5(null);
@@ -6766,9 +7255,30 @@ function App({
6766
7255
  });
6767
7256
  }, [slashMatches]);
6768
7257
  const loopRef = useRef2(null);
7258
+ const subagentSinkRef = useRef2({ current: null });
6769
7259
  const loop = useMemo(() => {
6770
7260
  if (loopRef.current) return loopRef.current;
6771
7261
  const client = new DeepSeekClient();
7262
+ if (tools && !tools.has("run_skill")) {
7263
+ registerSkillTools(tools, {
7264
+ projectRoot: codeMode?.rootDir,
7265
+ subagentRunner: async (skill, task) => {
7266
+ const result = await spawnSubagent({
7267
+ client,
7268
+ parentRegistry: tools,
7269
+ // Skill body is the subagent's persona/playbook; the user-
7270
+ // supplied task is what to actually do inside it.
7271
+ system: skill.body,
7272
+ task,
7273
+ // Per-skill model override (frontmatter `model: ...`),
7274
+ // else falls through to spawnSubagent's default.
7275
+ model: skill.model,
7276
+ sink: subagentSinkRef.current
7277
+ });
7278
+ return formatSubagentResult(result);
7279
+ }
7280
+ });
7281
+ }
6772
7282
  const prefix = new ImmutablePrefix({
6773
7283
  system,
6774
7284
  toolSpecs: tools?.specs()
@@ -6786,7 +7296,7 @@ function App({
6786
7296
  });
6787
7297
  loopRef.current = l;
6788
7298
  return l;
6789
- }, [model, system, harvest2, branch, session, tools]);
7299
+ }, [model, system, harvest2, branch, session, tools, codeMode]);
6790
7300
  useEffect2(() => {
6791
7301
  loop.hooks = hookList;
6792
7302
  }, [loop, hookList]);
@@ -6826,6 +7336,40 @@ function App({
6826
7336
  if (progressSink.current) progressSink.current = null;
6827
7337
  };
6828
7338
  }, [progressSink]);
7339
+ useEffect2(() => {
7340
+ subagentSinkRef.current.current = (ev) => {
7341
+ if (ev.kind === "start") {
7342
+ setSubagentActivity({
7343
+ task: ev.task,
7344
+ iter: ev.iter ?? 0,
7345
+ elapsedMs: ev.elapsedMs ?? 0
7346
+ });
7347
+ return;
7348
+ }
7349
+ if (ev.kind === "progress") {
7350
+ setSubagentActivity({
7351
+ task: ev.task,
7352
+ iter: ev.iter ?? 0,
7353
+ elapsedMs: ev.elapsedMs ?? 0
7354
+ });
7355
+ return;
7356
+ }
7357
+ setSubagentActivity(null);
7358
+ const seconds = ((ev.elapsedMs ?? 0) / 1e3).toFixed(1);
7359
+ 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)`;
7360
+ setHistorical((prev) => [
7361
+ ...prev,
7362
+ {
7363
+ id: `subagent-end-${Date.now()}`,
7364
+ role: "info",
7365
+ text: summary2
7366
+ }
7367
+ ]);
7368
+ };
7369
+ return () => {
7370
+ subagentSinkRef.current.current = null;
7371
+ };
7372
+ }, []);
6829
7373
  const sessionBannerShown = useRef2(false);
6830
7374
  useEffect2(() => {
6831
7375
  if (sessionBannerShown.current) return;
@@ -7436,14 +7980,14 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
7436
7980
  balance,
7437
7981
  updateAvailable
7438
7982
  }
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(
7983
+ ), /* @__PURE__ */ React12.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React12.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && streaming ? /* @__PURE__ */ React12.createElement(Box11, { marginY: 1 }, /* @__PURE__ */ React12.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : 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
7984
  PlanRefineInput,
7441
7985
  {
7442
7986
  mode: stagedInput.mode,
7443
7987
  onSubmit: handleStagedInputSubmit,
7444
7988
  onCancel: handleStagedInputCancel
7445
7989
  }
7446
- ) : pendingPlan ? /* @__PURE__ */ React12.createElement(PlanConfirm, { plan: pendingPlan, onChoose: handlePlanConfirm }) : pendingShell ? /* @__PURE__ */ React12.createElement(
7990
+ ) : pendingPlan ? /* @__PURE__ */ React12.createElement(PlanConfirm, { plan: pendingPlan, onChoose: handlePlanConfirm, projectRoot: hookCwd }) : pendingShell ? /* @__PURE__ */ React12.createElement(
7447
7991
  ShellConfirm,
7448
7992
  {
7449
7993
  command: pendingShell,
@@ -7466,6 +8010,13 @@ function StatusRow({ text }) {
7466
8010
  const elapsed = useElapsedSeconds();
7467
8011
  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
8012
  }
8013
+ function SubagentRow({
8014
+ activity
8015
+ }) {
8016
+ const tick = useTick();
8017
+ const seconds = (activity.elapsedMs / 1e3).toFixed(1);
8018
+ 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`));
8019
+ }
7469
8020
  function OngoingToolRow({
7470
8021
  tool,
7471
8022
  progress
@@ -7784,14 +8335,13 @@ async function chatCommand(opts) {
7784
8335
  if (!opts.seedTools) {
7785
8336
  if (!tools) tools = new ToolRegistry();
7786
8337
  registerMemoryTools(tools, {});
7787
- registerSkillTools(tools);
7788
8338
  }
7789
8339
  let sessionPreview;
7790
8340
  if (opts.session && !opts.forceResume && !opts.forceNew) {
7791
8341
  const prior = loadSessionMessages(opts.session);
7792
8342
  if (prior.length > 0) {
7793
8343
  const p = sessionPath(opts.session);
7794
- const mtime = existsSync8(p) ? statSync4(p).mtime : /* @__PURE__ */ new Date();
8344
+ const mtime = existsSync8(p) ? statSync5(p).mtime : /* @__PURE__ */ new Date();
7795
8345
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
7796
8346
  }
7797
8347
  } else if (opts.session && opts.forceNew) {
@@ -7824,7 +8374,7 @@ async function chatCommand(opts) {
7824
8374
  // src/cli/commands/code.tsx
7825
8375
  import { basename, resolve as resolve5 } from "path";
7826
8376
  async function codeCommand(opts = {}) {
7827
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-VDN5U3YE.js");
8377
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-75XLIUTO.js");
7828
8378
  const rootDir = resolve5(opts.dir ?? process.cwd());
7829
8379
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
7830
8380
  const tools = new ToolRegistry();
@@ -7840,7 +8390,6 @@ async function codeCommand(opts = {}) {
7840
8390
  });
7841
8391
  registerPlanTool(tools);
7842
8392
  registerMemoryTools(tools, { projectRoot: rootDir });
7843
- registerSkillTools(tools, { projectRoot: rootDir });
7844
8393
  process.stderr.write(
7845
8394
  `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
7846
8395
  `
@@ -8933,7 +9482,19 @@ function resolveSession(flag, configSession) {
8933
9482
  }
8934
9483
 
8935
9484
  // src/cli/index.ts
8936
- var DEFAULT_SYSTEM = "You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.";
9485
+ var DEFAULT_SYSTEM = `You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.
9486
+
9487
+ # Cite or shut up \u2014 non-negotiable
9488
+
9489
+ Every factual claim about a codebase must be backed by evidence. Reasonix VALIDATES your citations \u2014 broken paths render in **red strikethrough with \u274C** in front of the user.
9490
+
9491
+ **Positive claims** \u2014 append a markdown link:
9492
+ - \u2705 \`The MCP client supports listResources [listResources](src/mcp/client.ts:142).\`
9493
+ - \u274C \`The MCP client supports listResources.\` \u2190 unverifiable, do not write.
9494
+
9495
+ **Negative claims** ("X is missing", "Y isn't implemented", "lacks Z") are the #1 hallucination shape. STOP before writing them. If you have a search tool, call it first; if the search returns nothing, cite the search itself as evidence (\`No matches for "foo" in src/\`). If you have no tool, qualify hard: "I haven't verified \u2014 this is a guess."
9496
+
9497
+ Asserting absence without checking is how evaluative answers go wrong. Treat the urge to write "missing" as a red flag in your own reasoning.`;
8937
9498
  var program = new Command();
8938
9499
  program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION);
8939
9500
  program.action(async () => {