reasonix 0.8.0 → 0.11.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
@@ -10,7 +10,7 @@ import {
10
10
  memoryEnabled,
11
11
  readProjectMemory,
12
12
  sanitizeMemoryName
13
- } from "./chunk-DVBNMXA6.js";
13
+ } from "./chunk-GXABXQMU.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";
@@ -22,80 +22,80 @@ import { dirname, join } from "path";
22
22
  function defaultConfigPath() {
23
23
  return join(homedir(), ".reasonix", "config.json");
24
24
  }
25
- function readConfig(path = defaultConfigPath()) {
25
+ function readConfig(path5 = defaultConfigPath()) {
26
26
  try {
27
- const raw = readFileSync(path, "utf8");
27
+ const raw = readFileSync(path5, "utf8");
28
28
  const parsed = JSON.parse(raw);
29
29
  if (parsed && typeof parsed === "object") return parsed;
30
30
  } catch {
31
31
  }
32
32
  return {};
33
33
  }
34
- function writeConfig(cfg, path = defaultConfigPath()) {
35
- mkdirSync(dirname(path), { recursive: true });
36
- writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
34
+ function writeConfig(cfg, path5 = defaultConfigPath()) {
35
+ mkdirSync(dirname(path5), { recursive: true });
36
+ writeFileSync(path5, JSON.stringify(cfg, null, 2), "utf8");
37
37
  try {
38
- chmodSync(path, 384);
38
+ chmodSync(path5, 384);
39
39
  } catch {
40
40
  }
41
41
  }
42
- function loadApiKey(path = defaultConfigPath()) {
42
+ function loadApiKey(path5 = defaultConfigPath()) {
43
43
  if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
44
- return readConfig(path).apiKey;
44
+ return readConfig(path5).apiKey;
45
45
  }
46
- function searchEnabled(path = defaultConfigPath()) {
46
+ function searchEnabled(path5 = defaultConfigPath()) {
47
47
  const env = process.env.REASONIX_SEARCH;
48
48
  if (env === "off" || env === "false" || env === "0") return false;
49
- const cfg = readConfig(path).search;
49
+ const cfg = readConfig(path5).search;
50
50
  if (cfg === false) return false;
51
51
  return true;
52
52
  }
53
- function saveApiKey(key, path = defaultConfigPath()) {
54
- const cfg = readConfig(path);
53
+ function saveApiKey(key, path5 = defaultConfigPath()) {
54
+ const cfg = readConfig(path5);
55
55
  cfg.apiKey = key.trim();
56
- writeConfig(cfg, path);
56
+ writeConfig(cfg, path5);
57
57
  }
58
- function loadProjectShellAllowed(rootDir, path = defaultConfigPath()) {
59
- const cfg = readConfig(path);
58
+ function loadProjectShellAllowed(rootDir, path5 = defaultConfigPath()) {
59
+ const cfg = readConfig(path5);
60
60
  return cfg.projects?.[rootDir]?.shellAllowed ?? [];
61
61
  }
62
- function addProjectShellAllowed(rootDir, prefix, path = defaultConfigPath()) {
62
+ function addProjectShellAllowed(rootDir, prefix, path5 = defaultConfigPath()) {
63
63
  const trimmed = prefix.trim();
64
64
  if (!trimmed) return;
65
- const cfg = readConfig(path);
65
+ const cfg = readConfig(path5);
66
66
  if (!cfg.projects) cfg.projects = {};
67
67
  if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
68
68
  const existing = cfg.projects[rootDir].shellAllowed ?? [];
69
69
  if (existing.includes(trimmed)) return;
70
70
  cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
71
- writeConfig(cfg, path);
71
+ writeConfig(cfg, path5);
72
72
  }
73
- function loadEditMode(path = defaultConfigPath()) {
74
- const v = readConfig(path).editMode;
73
+ function loadEditMode(path5 = defaultConfigPath()) {
74
+ const v = readConfig(path5).editMode;
75
75
  return v === "auto" ? "auto" : "review";
76
76
  }
77
- function saveEditMode(mode2, path = defaultConfigPath()) {
78
- const cfg = readConfig(path);
77
+ function saveEditMode(mode2, path5 = defaultConfigPath()) {
78
+ const cfg = readConfig(path5);
79
79
  cfg.editMode = mode2;
80
- writeConfig(cfg, path);
80
+ writeConfig(cfg, path5);
81
81
  }
82
- function editModeHintShown(path = defaultConfigPath()) {
83
- return readConfig(path).editModeHintShown === true;
82
+ function editModeHintShown(path5 = defaultConfigPath()) {
83
+ return readConfig(path5).editModeHintShown === true;
84
84
  }
85
- function loadReasoningEffort(path = defaultConfigPath()) {
86
- const v = readConfig(path).reasoningEffort;
85
+ function loadReasoningEffort(path5 = defaultConfigPath()) {
86
+ const v = readConfig(path5).reasoningEffort;
87
87
  return v === "high" ? "high" : "max";
88
88
  }
89
- function saveReasoningEffort(effort2, path = defaultConfigPath()) {
90
- const cfg = readConfig(path);
89
+ function saveReasoningEffort(effort2, path5 = defaultConfigPath()) {
90
+ const cfg = readConfig(path5);
91
91
  cfg.reasoningEffort = effort2;
92
- writeConfig(cfg, path);
92
+ writeConfig(cfg, path5);
93
93
  }
94
- function markEditModeHintShown(path = defaultConfigPath()) {
95
- const cfg = readConfig(path);
94
+ function markEditModeHintShown(path5 = defaultConfigPath()) {
95
+ const cfg = readConfig(path5);
96
96
  if (cfg.editModeHintShown === true) return;
97
97
  cfg.editModeHintShown = true;
98
- writeConfig(cfg, path);
98
+ writeConfig(cfg, path5);
99
99
  }
100
100
  function isPlausibleKey(key) {
101
101
  const trimmed = key.trim();
@@ -156,8 +156,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
156
156
  }
157
157
  function sleep(ms, signal) {
158
158
  if (ms <= 0) return Promise.resolve();
159
- return new Promise((resolve8, reject) => {
160
- const timer = setTimeout(resolve8, ms);
159
+ return new Promise((resolve9, reject) => {
160
+ const timer = setTimeout(resolve9, ms);
161
161
  if (signal) {
162
162
  const onAbort = () => {
163
163
  clearTimeout(timer);
@@ -597,10 +597,10 @@ function globalSettingsPath(homeDirOverride) {
597
597
  function projectSettingsPath(projectRoot) {
598
598
  return join2(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
599
599
  }
600
- function readSettingsFile(path) {
601
- if (!existsSync(path)) return null;
600
+ function readSettingsFile(path5) {
601
+ if (!existsSync(path5)) return null;
602
602
  try {
603
- const raw = readFileSync2(path, "utf8");
603
+ const raw = readFileSync2(path5, "utf8");
604
604
  const parsed = JSON.parse(raw);
605
605
  if (parsed && typeof parsed === "object") return parsed;
606
606
  } catch {
@@ -642,13 +642,13 @@ function matchesTool(hook, toolName) {
642
642
  }
643
643
  }
644
644
  function defaultSpawner(input) {
645
- return new Promise((resolve8) => {
645
+ return new Promise((resolve9) => {
646
646
  const child = spawn(input.command, {
647
647
  cwd: input.cwd,
648
648
  shell: true,
649
649
  stdio: ["pipe", "pipe", "pipe"]
650
650
  });
651
- let stdout2 = "";
651
+ let stdout3 = "";
652
652
  let stderr = "";
653
653
  let timedOut = false;
654
654
  const timer = setTimeout(() => {
@@ -662,16 +662,16 @@ function defaultSpawner(input) {
662
662
  }, 500);
663
663
  }, input.timeoutMs);
664
664
  child.stdout.on("data", (chunk) => {
665
- stdout2 += chunk.toString("utf8");
665
+ stdout3 += chunk.toString("utf8");
666
666
  });
667
667
  child.stderr.on("data", (chunk) => {
668
668
  stderr += chunk.toString("utf8");
669
669
  });
670
670
  child.once("error", (err) => {
671
671
  clearTimeout(timer);
672
- resolve8({
672
+ resolve9({
673
673
  exitCode: null,
674
- stdout: stdout2,
674
+ stdout: stdout3,
675
675
  stderr,
676
676
  timedOut: false,
677
677
  spawnError: err
@@ -679,9 +679,9 @@ function defaultSpawner(input) {
679
679
  });
680
680
  child.once("close", (code) => {
681
681
  clearTimeout(timer);
682
- resolve8({
682
+ resolve9({
683
683
  exitCode: code,
684
- stdout: stdout2.trim(),
684
+ stdout: stdout3.trim(),
685
685
  stderr: stderr.trim(),
686
686
  timedOut
687
687
  });
@@ -715,13 +715,13 @@ async function runHooks(opts) {
715
715
  const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));
716
716
  const outcomes = [];
717
717
  let blocked = false;
718
- const stdin3 = `${JSON.stringify(opts.payload)}
718
+ const stdin4 = `${JSON.stringify(opts.payload)}
719
719
  `;
720
720
  for (const hook of matching) {
721
721
  const start = Date.now();
722
722
  const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];
723
723
  const cwd = hook.cwd ?? opts.payload.cwd;
724
- const raw = await spawner({ command: hook.command, cwd, stdin: stdin3, timeoutMs });
724
+ const raw = await spawner({ command: hook.command, cwd, stdin: stdin4, timeoutMs });
725
725
  const decision = decideOutcome(event, raw);
726
726
  outcomes.push({
727
727
  hook,
@@ -804,10 +804,10 @@ function loadTokenizer() {
804
804
  }
805
805
  const addedMap = /* @__PURE__ */ new Map();
806
806
  const addedContents = [];
807
- for (const t of data.added_tokens) {
808
- if (!t.special) {
809
- addedMap.set(t.content, t.id);
810
- addedContents.push(t.content);
807
+ for (const t2 of data.added_tokens) {
808
+ if (!t2.special) {
809
+ addedMap.set(t2.content, t2.id);
810
+ addedContents.push(t2.content);
811
811
  }
812
812
  }
813
813
  addedContents.sort((a, b) => b.length - a.length);
@@ -874,29 +874,29 @@ function bpeEncode(piece, mergeRank) {
874
874
  }
875
875
  function encode(text) {
876
876
  if (!text) return [];
877
- const t = loadTokenizer();
877
+ const t2 = loadTokenizer();
878
878
  const ids = [];
879
879
  const process2 = (segment) => {
880
880
  if (!segment) return;
881
881
  let chunks = [segment];
882
- for (const re of t.splitRegexes) chunks = applySplit(chunks, re);
882
+ for (const re of t2.splitRegexes) chunks = applySplit(chunks, re);
883
883
  for (const chunk of chunks) {
884
884
  if (!chunk) continue;
885
- const byteLevel = byteLevelEncode(chunk, t.byteToChar);
886
- const pieces = bpeEncode(byteLevel, t.mergeRank);
885
+ const byteLevel = byteLevelEncode(chunk, t2.byteToChar);
886
+ const pieces = bpeEncode(byteLevel, t2.mergeRank);
887
887
  for (const p of pieces) {
888
- const id = t.vocab[p];
888
+ const id = t2.vocab[p];
889
889
  if (id !== void 0) ids.push(id);
890
890
  }
891
891
  }
892
892
  };
893
- if (t.addedPattern) {
894
- t.addedPattern.lastIndex = 0;
893
+ if (t2.addedPattern) {
894
+ t2.addedPattern.lastIndex = 0;
895
895
  let last = 0;
896
- for (const m of text.matchAll(t.addedPattern)) {
896
+ for (const m of text.matchAll(t2.addedPattern)) {
897
897
  const idx = m.index ?? 0;
898
898
  if (idx > last) process2(text.slice(last, idx));
899
- const id = t.addedMap.get(m[0]);
899
+ const id = t2.addedMap.get(m[0]);
900
900
  if (id !== void 0) ids.push(id);
901
901
  last = idx + m[0].length;
902
902
  }
@@ -987,14 +987,14 @@ function collect(prefix, schema, out, required, isRootRequired) {
987
987
  out[prefix] = schema;
988
988
  if (isRootRequired) required.push(prefix);
989
989
  }
990
- function setByPath(target, path, value) {
990
+ function setByPath(target, path5, value) {
991
991
  let cur = target;
992
- for (let i = 0; i < path.length - 1; i++) {
993
- const key = path[i];
992
+ for (let i = 0; i < path5.length - 1; i++) {
993
+ const key = path5[i];
994
994
  if (typeof cur[key] !== "object" || cur[key] === null) cur[key] = {};
995
995
  cur = cur[key];
996
996
  }
997
- cur[path[path.length - 1]] = value;
997
+ cur[path5[path5.length - 1]] = value;
998
998
  }
999
999
 
1000
1000
  // src/tools.ts
@@ -1060,12 +1060,12 @@ var ToolRegistry = class {
1060
1060
  return Boolean(this._tools.get(name)?.flatSchema);
1061
1061
  }
1062
1062
  specs() {
1063
- return [...this._tools.values()].map((t) => ({
1063
+ return [...this._tools.values()].map((t2) => ({
1064
1064
  type: "function",
1065
1065
  function: {
1066
- name: t.name,
1067
- description: t.description ?? "",
1068
- parameters: t.flatSchema ?? t.parameters ?? { type: "object", properties: {} }
1066
+ name: t2.name,
1067
+ description: t2.description ?? "",
1068
+ parameters: t2.flatSchema ?? t2.parameters ?? { type: "object", properties: {} }
1069
1069
  }
1070
1070
  }));
1071
1071
  }
@@ -1277,7 +1277,7 @@ var ImmutablePrefix = class {
1277
1277
  return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
1278
1278
  }
1279
1279
  tools() {
1280
- return this.toolSpecs.map((t) => structuredClone(t));
1280
+ return this.toolSpecs.map((t2) => structuredClone(t2));
1281
1281
  }
1282
1282
  get fingerprint() {
1283
1283
  const blob = JSON.stringify({
@@ -1660,10 +1660,10 @@ function sanitizeName(name) {
1660
1660
  return cleaned || "default";
1661
1661
  }
1662
1662
  function loadSessionMessages(name) {
1663
- const path = sessionPath(name);
1664
- if (!existsSync3(path)) return [];
1663
+ const path5 = sessionPath(name);
1664
+ if (!existsSync3(path5)) return [];
1665
1665
  try {
1666
- const raw = readFileSync4(path, "utf8");
1666
+ const raw = readFileSync4(path5, "utf8");
1667
1667
  const out = [];
1668
1668
  for (const line of raw.split(/\r?\n/)) {
1669
1669
  const trimmed = line.trim();
@@ -1680,12 +1680,12 @@ function loadSessionMessages(name) {
1680
1680
  }
1681
1681
  }
1682
1682
  function appendSessionMessage(name, message) {
1683
- const path = sessionPath(name);
1684
- mkdirSync2(dirname3(path), { recursive: true });
1685
- appendFileSync(path, `${JSON.stringify(message)}
1683
+ const path5 = sessionPath(name);
1684
+ mkdirSync2(dirname3(path5), { recursive: true });
1685
+ appendFileSync(path5, `${JSON.stringify(message)}
1686
1686
  `, "utf8");
1687
1687
  try {
1688
- chmodSync2(path, 384);
1688
+ chmodSync2(path5, 384);
1689
1689
  } catch {
1690
1690
  }
1691
1691
  }
@@ -1695,21 +1695,21 @@ function listSessions() {
1695
1695
  try {
1696
1696
  const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1697
1697
  return files.map((file) => {
1698
- const path = join4(dir, file);
1699
- const stat = statSync(path);
1698
+ const path5 = join4(dir, file);
1699
+ const stat = statSync(path5);
1700
1700
  const name = file.replace(/\.jsonl$/, "");
1701
- const messageCount = countLines(path);
1702
- return { name, path, size: stat.size, messageCount, mtime: stat.mtime };
1701
+ const messageCount = countLines(path5);
1702
+ return { name, path: path5, size: stat.size, messageCount, mtime: stat.mtime };
1703
1703
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1704
1704
  } catch {
1705
1705
  return [];
1706
1706
  }
1707
1707
  }
1708
1708
  function deleteSession(name) {
1709
- const path = sessionPath(name);
1709
+ const path5 = sessionPath(name);
1710
1710
  try {
1711
- unlinkSync(path);
1712
- const sidecar = path.replace(/\.jsonl$/, ".pending.json");
1711
+ unlinkSync(path5);
1712
+ const sidecar = path5.replace(/\.jsonl$/, ".pending.json");
1713
1713
  try {
1714
1714
  unlinkSync(sidecar);
1715
1715
  } catch {
@@ -1720,19 +1720,19 @@ function deleteSession(name) {
1720
1720
  }
1721
1721
  }
1722
1722
  function rewriteSession(name, messages) {
1723
- const path = sessionPath(name);
1724
- mkdirSync2(dirname3(path), { recursive: true });
1723
+ const path5 = sessionPath(name);
1724
+ mkdirSync2(dirname3(path5), { recursive: true });
1725
1725
  const body = messages.map((m) => JSON.stringify(m)).join("\n");
1726
- writeFileSync2(path, body ? `${body}
1726
+ writeFileSync2(path5, body ? `${body}
1727
1727
  ` : "", "utf8");
1728
1728
  try {
1729
- chmodSync2(path, 384);
1729
+ chmodSync2(path5, 384);
1730
1730
  } catch {
1731
1731
  }
1732
1732
  }
1733
- function countLines(path) {
1733
+ function countLines(path5) {
1734
1734
  try {
1735
- const raw = readFileSync4(path, "utf8");
1735
+ const raw = readFileSync4(path5, "utf8");
1736
1736
  return raw.split(/\r?\n/).filter((l) => l.trim()).length;
1737
1737
  } catch {
1738
1738
  return 0;
@@ -1788,27 +1788,27 @@ var SessionStats = class {
1788
1788
  return stats2;
1789
1789
  }
1790
1790
  get totalCost() {
1791
- return this.turns.reduce((sum, t) => sum + t.cost, 0);
1791
+ return this.turns.reduce((sum, t2) => sum + t2.cost, 0);
1792
1792
  }
1793
1793
  get totalClaudeEquivalent() {
1794
- return this.turns.reduce((sum, t) => sum + claudeEquivalentCost(t.usage), 0);
1794
+ return this.turns.reduce((sum, t2) => sum + claudeEquivalentCost(t2.usage), 0);
1795
1795
  }
1796
1796
  get savingsVsClaude() {
1797
1797
  const c = this.totalClaudeEquivalent;
1798
1798
  return c > 0 ? 1 - this.totalCost / c : 0;
1799
1799
  }
1800
1800
  get totalInputCost() {
1801
- return this.turns.reduce((sum, t) => sum + inputCostUsd(t.model, t.usage), 0);
1801
+ return this.turns.reduce((sum, t2) => sum + inputCostUsd(t2.model, t2.usage), 0);
1802
1802
  }
1803
1803
  get totalOutputCost() {
1804
- return this.turns.reduce((sum, t) => sum + outputCostUsd(t.model, t.usage), 0);
1804
+ return this.turns.reduce((sum, t2) => sum + outputCostUsd(t2.model, t2.usage), 0);
1805
1805
  }
1806
1806
  get aggregateCacheHitRatio() {
1807
1807
  let hit = 0;
1808
1808
  let miss = 0;
1809
- for (const t of this.turns) {
1810
- hit += t.usage.promptCacheHitTokens;
1811
- miss += t.usage.promptCacheMissTokens;
1809
+ for (const t2 of this.turns) {
1810
+ hit += t2.usage.promptCacheHitTokens;
1811
+ miss += t2.usage.promptCacheMissTokens;
1812
1812
  }
1813
1813
  const denom = hit + miss;
1814
1814
  return denom > 0 ? hit / denom : 0;
@@ -2198,13 +2198,13 @@ var CacheFirstLoop = class {
2198
2198
  * with `<`.
2199
2199
  */
2200
2200
  looksLikePartialEscalationMarker(buf) {
2201
- const t = buf.trimStart();
2202
- if (t.length === 0) return true;
2203
- if (t.length <= NEEDS_PRO_MARKER_PREFIX.length) {
2204
- return NEEDS_PRO_MARKER_PREFIX.startsWith(t);
2201
+ const t2 = buf.trimStart();
2202
+ if (t2.length === 0) return true;
2203
+ if (t2.length <= NEEDS_PRO_MARKER_PREFIX.length) {
2204
+ return NEEDS_PRO_MARKER_PREFIX.startsWith(t2);
2205
2205
  }
2206
- if (!t.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
2207
- const rest = t.slice(NEEDS_PRO_MARKER_PREFIX.length);
2206
+ if (!t2.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
2207
+ const rest = t2.slice(NEEDS_PRO_MARKER_PREFIX.length);
2208
2208
  if (rest[0] !== ">" && rest[0] !== ":") return false;
2209
2209
  return true;
2210
2210
  }
@@ -2312,7 +2312,9 @@ var CacheFirstLoop = class {
2312
2312
  this._proArmedForNextTurn = false;
2313
2313
  armedConsumed = true;
2314
2314
  }
2315
+ const carryAbort = this._turnAbort.signal.aborted;
2315
2316
  this._turnAbort = new AbortController();
2317
+ if (carryAbort) this._turnAbort.abort();
2316
2318
  const signal = this._turnAbort.signal;
2317
2319
  if (armedConsumed) {
2318
2320
  yield {
@@ -2435,8 +2437,8 @@ var CacheFirstLoop = class {
2435
2437
  }
2436
2438
  );
2437
2439
  for (let k = 0; k < budget; k++) {
2438
- const sample = queue.shift() ?? await new Promise((resolve8) => {
2439
- waiter = resolve8;
2440
+ const sample = queue.shift() ?? await new Promise((resolve9) => {
2441
+ waiter = resolve9;
2440
2442
  });
2441
2443
  yield {
2442
2444
  turn: this._turn,
@@ -3174,7 +3176,7 @@ function listFilesWithStatsSync(root, opts = {}) {
3174
3176
  const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
3175
3177
  const rootAbs = resolve(root);
3176
3178
  const out = [];
3177
- const walk2 = (dirAbs, dirRel) => {
3179
+ const walk3 = (dirAbs, dirRel) => {
3178
3180
  if (out.length >= maxResults) return;
3179
3181
  let entries;
3180
3182
  try {
@@ -3188,7 +3190,7 @@ function listFilesWithStatsSync(root, opts = {}) {
3188
3190
  const relPath = dirRel ? `${dirRel}/${ent.name}` : ent.name;
3189
3191
  if (ent.isDirectory()) {
3190
3192
  if (ent.name.startsWith(".") || ignore.has(ent.name)) continue;
3191
- walk2(join5(dirAbs, ent.name), relPath);
3193
+ walk3(join5(dirAbs, ent.name), relPath);
3192
3194
  } else if (ent.isFile()) {
3193
3195
  let mtimeMs = 0;
3194
3196
  try {
@@ -3199,7 +3201,7 @@ function listFilesWithStatsSync(root, opts = {}) {
3199
3201
  }
3200
3202
  }
3201
3203
  };
3202
- walk2(rootAbs, "");
3204
+ walk3(rootAbs, "");
3203
3205
  return out;
3204
3206
  }
3205
3207
  var AT_PICKER_PREFIX = /(?:^|\s)@([a-zA-Z0-9_./\\-]*)$/;
@@ -3259,7 +3261,7 @@ function rankPickerCandidates(files, query, limitOrOpts) {
3259
3261
  var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
3260
3262
  function expandAtMentions(text, rootDir, opts = {}) {
3261
3263
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
3262
- const fs2 = opts.fs ?? defaultFs;
3264
+ const fs6 = opts.fs ?? defaultFs;
3263
3265
  const root = resolve(rootDir);
3264
3266
  const seen = /* @__PURE__ */ new Map();
3265
3267
  const expansions = [];
@@ -3269,7 +3271,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
3269
3271
  if (!cleaned) continue;
3270
3272
  const token = `@${cleaned}`;
3271
3273
  if (seen.has(token)) continue;
3272
- const expansion = resolveMention(cleaned, root, maxBytes, fs2);
3274
+ const expansion = resolveMention(cleaned, root, maxBytes, fs6);
3273
3275
  seen.set(token, expansion);
3274
3276
  expansions.push(expansion);
3275
3277
  }
@@ -3277,7 +3279,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
3277
3279
  const blocks = [];
3278
3280
  for (const ex of expansions) {
3279
3281
  if (ex.ok) {
3280
- const content = readSafe(root, ex.path, fs2);
3282
+ const content = readSafe(root, ex.path, fs6);
3281
3283
  blocks.push(`<file path="${ex.path}">
3282
3284
  ${content}
3283
3285
  </file>`);
@@ -3291,7 +3293,7 @@ ${content}
3291
3293
  ${blocks.join("\n\n")}`;
3292
3294
  return { text: augmented, expansions };
3293
3295
  }
3294
- function resolveMention(rawPath, root, maxBytes, fs2) {
3296
+ function resolveMention(rawPath, root, maxBytes, fs6) {
3295
3297
  if (isAbsolute(rawPath)) {
3296
3298
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
3297
3299
  }
@@ -3300,22 +3302,22 @@ function resolveMention(rawPath, root, maxBytes, fs2) {
3300
3302
  if (rel.startsWith("..") || isAbsolute(rel)) {
3301
3303
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
3302
3304
  }
3303
- if (!fs2.exists(resolved)) {
3305
+ if (!fs6.exists(resolved)) {
3304
3306
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "missing" };
3305
3307
  }
3306
- if (!fs2.isFile(resolved)) {
3308
+ if (!fs6.isFile(resolved)) {
3307
3309
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
3308
3310
  }
3309
- const size = fs2.size(resolved);
3311
+ const size = fs6.size(resolved);
3310
3312
  if (size > maxBytes) {
3311
3313
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "too-large", bytes: size };
3312
3314
  }
3313
3315
  return { token: `@${rawPath}`, path: rawPath, ok: true, bytes: size };
3314
3316
  }
3315
- function readSafe(root, rawPath, fs2) {
3317
+ function readSafe(root, rawPath, fs6) {
3316
3318
  const resolved = resolve(root, rawPath);
3317
3319
  try {
3318
- return fs2.read(resolved);
3320
+ return fs6.read(resolved);
3319
3321
  } catch {
3320
3322
  return "(read failed)";
3321
3323
  }
@@ -3338,6 +3340,108 @@ var defaultFs = {
3338
3340
  },
3339
3341
  read: (p) => readFileSync5(p, "utf8")
3340
3342
  };
3343
+ var AT_URL_PATTERN = /(?<=^|\s)@(https?:\/\/\S+)/g;
3344
+ var DEFAULT_AT_URL_MAX_CHARS = 32e3;
3345
+ async function expandAtUrls(text, opts = {}) {
3346
+ const maxChars = opts.maxChars ?? DEFAULT_AT_URL_MAX_CHARS;
3347
+ const fetcher = opts.fetcher;
3348
+ if (!fetcher) {
3349
+ throw new Error("expandAtUrls: fetcher option is required (wire src/tools/web.ts:webFetch)");
3350
+ }
3351
+ const seen = /* @__PURE__ */ new Map();
3352
+ const bodies = /* @__PURE__ */ new Map();
3353
+ const order = [];
3354
+ for (const match of text.matchAll(AT_URL_PATTERN)) {
3355
+ const rawUrl = match[1] ?? "";
3356
+ const url = stripUrlTail(rawUrl);
3357
+ if (!url) continue;
3358
+ if (seen.has(url)) continue;
3359
+ const cached2 = opts.cache?.get(url);
3360
+ if (cached2) {
3361
+ seen.set(url, cached2);
3362
+ if (cached2.body) bodies.set(url, cached2.body);
3363
+ order.push(url);
3364
+ continue;
3365
+ }
3366
+ let expansion;
3367
+ let body = "";
3368
+ try {
3369
+ const page = await fetcher(url, {
3370
+ maxChars,
3371
+ timeoutMs: opts.timeoutMs,
3372
+ signal: opts.signal
3373
+ });
3374
+ body = page.text;
3375
+ expansion = {
3376
+ token: `@${url}`,
3377
+ url,
3378
+ ok: true,
3379
+ title: page.title,
3380
+ chars: body.length,
3381
+ truncated: page.truncated
3382
+ };
3383
+ } catch (err) {
3384
+ const message = err.message ?? String(err);
3385
+ let skip = "fetch-error";
3386
+ if (/aborted|timeout/i.test(message)) skip = "timeout";
3387
+ else if (/40\d|forbidden|access denied|captcha/i.test(message)) skip = "blocked";
3388
+ expansion = {
3389
+ token: `@${url}`,
3390
+ url,
3391
+ ok: false,
3392
+ skip,
3393
+ error: message
3394
+ };
3395
+ }
3396
+ seen.set(url, expansion);
3397
+ if (body) bodies.set(url, body);
3398
+ if (opts.cache) opts.cache.set(url, { ...expansion, body });
3399
+ order.push(url);
3400
+ }
3401
+ if (seen.size === 0) return { text, expansions: [] };
3402
+ const expansions = order.map((u) => seen.get(u)).filter(Boolean);
3403
+ const blocks = [];
3404
+ for (const ex of expansions) {
3405
+ if (ex.ok) {
3406
+ const titleAttr = ex.title ? ` title="${escapeAttr(ex.title)}"` : "";
3407
+ const truncTag = ex.truncated ? ' truncated="true"' : "";
3408
+ const body = bodies.get(ex.url) ?? "";
3409
+ blocks.push(`<url href="${ex.url}"${titleAttr}${truncTag}>
3410
+ ${body}
3411
+ </url>`);
3412
+ } else {
3413
+ const reasonAttr = ex.skip ?? "fetch-error";
3414
+ blocks.push(`<url href="${ex.url}" skipped="${reasonAttr}" />`);
3415
+ }
3416
+ }
3417
+ const augmented = `${text}
3418
+
3419
+ [Referenced URLs]
3420
+ ${blocks.join("\n\n")}`;
3421
+ return { text: augmented, expansions };
3422
+ }
3423
+ function stripUrlTail(raw) {
3424
+ let s = raw;
3425
+ while (s.length > 0) {
3426
+ const last = s[s.length - 1];
3427
+ if (".,;:!?".includes(last)) {
3428
+ s = s.slice(0, -1);
3429
+ continue;
3430
+ }
3431
+ if (")]}>".includes(last)) {
3432
+ const open = { ")": "(", "]": "[", "}": "{", ">": "<" }[last];
3433
+ if (!s.includes(open)) {
3434
+ s = s.slice(0, -1);
3435
+ continue;
3436
+ }
3437
+ }
3438
+ break;
3439
+ }
3440
+ return s;
3441
+ }
3442
+ function escapeAttr(s) {
3443
+ return s.replace(/"/g, "&quot;").replace(/[\r\n]+/g, " ").trim();
3444
+ }
3341
3445
 
3342
3446
  // src/tools/filesystem.ts
3343
3447
  import { promises as fs } from "fs";
@@ -3411,9 +3515,9 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3411
3515
  ".pyo"
3412
3516
  ]);
3413
3517
  function isLikelyBinaryByName(name) {
3414
- const dot = name.lastIndexOf(".");
3415
- if (dot < 0) return false;
3416
- return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
3518
+ const dot2 = name.lastIndexOf(".");
3519
+ if (dot2 < 0) return false;
3520
+ return BINARY_EXTENSIONS.has(name.slice(dot2).toLowerCase());
3417
3521
  }
3418
3522
  function registerFilesystemTools(registry, opts) {
3419
3523
  const rootDir = pathMod.resolve(opts.rootDir);
@@ -3564,7 +3668,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3564
3668
  let totalBytes = 0;
3565
3669
  let truncated = false;
3566
3670
  const PER_DIR_CHILD_CAP = 50;
3567
- const walk2 = async (dir, depth) => {
3671
+ const walk3 = async (dir, depth) => {
3568
3672
  if (truncated) return;
3569
3673
  if (depth > maxDepth) return;
3570
3674
  let entries;
@@ -3604,11 +3708,11 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3604
3708
  lines.push(line);
3605
3709
  emitted++;
3606
3710
  if (e.isDirectory() && !skip) {
3607
- await walk2(pathMod.join(dir, e.name), depth + 1);
3711
+ await walk3(pathMod.join(dir, e.name), depth + 1);
3608
3712
  }
3609
3713
  }
3610
3714
  };
3611
- await walk2(startAbs, 0);
3715
+ await walk3(startAbs, 0);
3612
3716
  return lines.join("\n") || "(empty tree)";
3613
3717
  }
3614
3718
  });
@@ -3638,7 +3742,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3638
3742
  }
3639
3743
  const matches = [];
3640
3744
  let totalBytes = 0;
3641
- const walk2 = async (dir) => {
3745
+ const walk3 = async (dir) => {
3642
3746
  let entries;
3643
3747
  try {
3644
3748
  entries = await fs.readdir(dir, { withFileTypes: true });
@@ -3658,10 +3762,10 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3658
3762
  matches.push(rel);
3659
3763
  totalBytes += rel.length + 1;
3660
3764
  }
3661
- if (e.isDirectory()) await walk2(full);
3765
+ if (e.isDirectory()) await walk3(full);
3662
3766
  }
3663
3767
  };
3664
- await walk2(startAbs);
3768
+ await walk3(startAbs);
3665
3769
  return matches.length === 0 ? "(no matches)" : matches.join("\n");
3666
3770
  }
3667
3771
  });
@@ -3711,7 +3815,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3711
3815
  let totalBytes = 0;
3712
3816
  let scanned = 0;
3713
3817
  let truncated = false;
3714
- const walk2 = async (dir) => {
3818
+ const walk3 = async (dir) => {
3715
3819
  if (truncated) return;
3716
3820
  let entries;
3717
3821
  try {
@@ -3723,7 +3827,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3723
3827
  if (truncated) return;
3724
3828
  if (e.isDirectory()) {
3725
3829
  if (!includeDeps && SKIP_DIR_NAMES.has(e.name)) continue;
3726
- await walk2(pathMod.join(dir, e.name));
3830
+ await walk3(pathMod.join(dir, e.name));
3727
3831
  continue;
3728
3832
  }
3729
3833
  if (!e.isFile()) continue;
@@ -3766,7 +3870,7 @@ Prefer \`list_directory\` for a single-level view, \`search_files\` to find spec
3766
3870
  scanned++;
3767
3871
  }
3768
3872
  };
3769
- await walk2(startAbs);
3873
+ await walk3(startAbs);
3770
3874
  if (matches.length === 0) {
3771
3875
  return scanned === 0 ? "(no files scanned \u2014 path empty or all files filtered out)" : `(no matches across ${scanned} file${scanned === 1 ? "" : "s"})`;
3772
3876
  }
@@ -3975,7 +4079,7 @@ function registerMemoryTools(registry, opts = {}) {
3975
4079
  });
3976
4080
  }
3977
4081
  try {
3978
- const path = store.write({
4082
+ const path5 = store.write({
3979
4083
  name: args.name,
3980
4084
  type: args.type,
3981
4085
  scope: args.scope,
@@ -3988,7 +4092,7 @@ function registerMemoryTools(registry, opts = {}) {
3988
4092
  "",
3989
4093
  "TREAT THIS AS ESTABLISHED FACT for the rest of this session.",
3990
4094
  "The user just told you \u2014 don't re-explore the filesystem to re-derive it.",
3991
- `(Saved to ${path}; pins into the system prompt on next /new or launch.)`
4095
+ `(Saved to ${path5}; pins into the system prompt on next /new or launch.)`
3992
4096
  ].join("\n");
3993
4097
  } catch (err) {
3994
4098
  return JSON.stringify({ error: `remember failed: ${err.message}` });
@@ -4466,7 +4570,11 @@ async function spawnSubagent(opts) {
4466
4570
  stream: false
4467
4571
  });
4468
4572
  const onParentAbort = () => childLoop.abort();
4469
- opts.parentSignal?.addEventListener("abort", onParentAbort, { once: true });
4573
+ if (opts.parentSignal?.aborted) {
4574
+ childLoop.abort();
4575
+ } else {
4576
+ opts.parentSignal?.addEventListener("abort", onParentAbort, { once: true });
4577
+ }
4470
4578
  let final = "";
4471
4579
  let errorMessage;
4472
4580
  let toolIter = 0;
@@ -4484,7 +4592,11 @@ async function spawnSubagent(opts) {
4484
4592
  });
4485
4593
  }
4486
4594
  if (ev.role === "assistant_final") {
4487
- final = ev.content ?? "";
4595
+ if (ev.forcedSummary) {
4596
+ errorMessage = ev.content?.trim() || "subagent ended without producing an answer";
4597
+ } else {
4598
+ final = ev.content ?? "";
4599
+ }
4488
4600
  }
4489
4601
  if (ev.role === "error") {
4490
4602
  errorMessage = ev.error ?? "subagent error";
@@ -4531,14 +4643,14 @@ async function spawnSubagent(opts) {
4531
4643
  usage
4532
4644
  };
4533
4645
  }
4534
- function aggregateChildUsage(loop) {
4646
+ function aggregateChildUsage(loop2) {
4535
4647
  const agg = new Usage();
4536
- for (const t of loop.stats.turns) {
4537
- agg.promptTokens += t.usage.promptTokens;
4538
- agg.completionTokens += t.usage.completionTokens;
4539
- agg.totalTokens += t.usage.totalTokens;
4540
- agg.promptCacheHitTokens += t.usage.promptCacheHitTokens;
4541
- agg.promptCacheMissTokens += t.usage.promptCacheMissTokens;
4648
+ for (const t2 of loop2.stats.turns) {
4649
+ agg.promptTokens += t2.usage.promptTokens;
4650
+ agg.completionTokens += t2.usage.completionTokens;
4651
+ agg.totalTokens += t2.usage.totalTokens;
4652
+ agg.promptCacheHitTokens += t2.usage.promptCacheHitTokens;
4653
+ agg.promptCacheMissTokens += t2.usage.promptCacheMissTokens;
4542
4654
  }
4543
4655
  return agg;
4544
4656
  }
@@ -5056,7 +5168,7 @@ async function runCommand(cmd, opts) {
5056
5168
  };
5057
5169
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
5058
5170
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
5059
- return await new Promise((resolve8, reject) => {
5171
+ return await new Promise((resolve9, reject) => {
5060
5172
  let child;
5061
5173
  try {
5062
5174
  child = spawn3(bin, args, effectiveSpawnOpts);
@@ -5089,7 +5201,7 @@ async function runCommand(cmd, opts) {
5089
5201
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
5090
5202
 
5091
5203
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
5092
- resolve8({ exitCode: code, output, timedOut });
5204
+ resolve9({ exitCode: code, output, timedOut });
5093
5205
  });
5094
5206
  });
5095
5207
  }
@@ -5204,7 +5316,7 @@ function registerShellTools(registry, opts) {
5204
5316
  const snapshot2 = opts.extraAllowed ?? [];
5205
5317
  return () => snapshot2;
5206
5318
  })();
5207
- const allowAll = opts.allowAll ?? false;
5319
+ const isAllowAll = typeof opts.allowAll === "function" ? opts.allowAll : () => opts.allowAll === true;
5208
5320
  registry.register({
5209
5321
  name: "run_command",
5210
5322
  description: "Run a shell command in the project root and return its combined stdout+stderr.\n\nConstraints (read these before the first call):\n\u2022 ONE process per call, NO shell expansion. `&&`, `||`, `|`, `;`, `>`, `<`, `2>&1` are all rejected up-front \u2014 split into separate calls and combine results in reasoning. Example: instead of `grep foo *.ts | wc -l`, use `grep -c foo *.ts`; instead of `cd sub && npm test`, use `npm test --prefix sub` (or whatever --cwd flag the binary accepts).\n\u2022 `cd` DOES NOT PERSIST between calls \u2014 each call spawns a fresh process rooted at the project. If a tool needs a subdirectory, pass it via the tool's own flag (`npm --prefix`, `cargo -C`, `git -C`, `pytest tests/\u2026`), NOT via a preceding `cd`.\n\u2022 Avoid commands with unbounded output (`netstat -ano`, `find /`, etc.) \u2014 they waste tokens. Filter at source: `netstat -ano -p TCP`, `find src -name '*.ts'`, `grep -c`, `wc -l`.\n\nCommon read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
@@ -5213,7 +5325,7 @@ function registerShellTools(registry, opts) {
5213
5325
  // during planning. Anything that would otherwise trigger a
5214
5326
  // confirmation prompt is treated as "not read-only" and bounced.
5215
5327
  readOnlyCheck: (args) => {
5216
- if (allowAll) return true;
5328
+ if (isAllowAll()) return true;
5217
5329
  const cmd = typeof args?.command === "string" ? args.command.trim() : "";
5218
5330
  if (!cmd) return false;
5219
5331
  return isAllowed(cmd, getExtraAllowed());
@@ -5235,7 +5347,7 @@ function registerShellTools(registry, opts) {
5235
5347
  fn: async (args, ctx) => {
5236
5348
  const cmd = args.command.trim();
5237
5349
  if (!cmd) throw new Error("run_command: empty command");
5238
- if (!allowAll && !isAllowed(cmd, getExtraAllowed())) {
5350
+ if (!isAllowAll() && !isAllowed(cmd, getExtraAllowed())) {
5239
5351
  throw new NeedsConfirmationError(cmd);
5240
5352
  }
5241
5353
  const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
@@ -5268,7 +5380,7 @@ function registerShellTools(registry, opts) {
5268
5380
  fn: async (args, ctx) => {
5269
5381
  const cmd = args.command.trim();
5270
5382
  if (!cmd) throw new Error("run_background: empty command");
5271
- if (!allowAll && !isAllowed(cmd, getExtraAllowed())) {
5383
+ if (!isAllowAll() && !isAllowed(cmd, getExtraAllowed())) {
5272
5384
  throw new NeedsConfirmationError(cmd);
5273
5385
  }
5274
5386
  const result = await jobs2.start(cmd, {
@@ -5562,10 +5674,10 @@ ${i + 1}. ${r.title}`);
5562
5674
  // src/env.ts
5563
5675
  import { readFileSync as readFileSync6 } from "fs";
5564
5676
  import { resolve as resolve5 } from "path";
5565
- function loadDotenv(path = ".env") {
5677
+ function loadDotenv(path5 = ".env") {
5566
5678
  let raw;
5567
5679
  try {
5568
- raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
5680
+ raw = readFileSync6(resolve5(process.cwd(), path5), "utf8");
5569
5681
  } catch {
5570
5682
  return;
5571
5683
  }
@@ -5629,13 +5741,13 @@ function writeMeta(stream, meta) {
5629
5741
  stream.write(`${JSON.stringify(line)}
5630
5742
  `);
5631
5743
  }
5632
- function openTranscriptFile(path, meta) {
5633
- const stream = createWriteStream(path, { flags: "a" });
5744
+ function openTranscriptFile(path5, meta) {
5745
+ const stream = createWriteStream(path5, { flags: "a" });
5634
5746
  writeMeta(stream, meta);
5635
5747
  return stream;
5636
5748
  }
5637
- function readTranscript(path) {
5638
- const raw = readFileSync7(path, "utf8");
5749
+ function readTranscript(path5) {
5750
+ const raw = readFileSync7(path5, "utf8");
5639
5751
  return parseTranscript(raw);
5640
5752
  }
5641
5753
  function isPlanStateEmptyShape(s) {
@@ -5684,8 +5796,8 @@ function computeCumulativeStats(pages, upToIdx) {
5684
5796
  }
5685
5797
  return computeReplayStats(flat);
5686
5798
  }
5687
- function replayFromFile(path) {
5688
- const parsed = readTranscript(path);
5799
+ function replayFromFile(path5) {
5800
+ const parsed = readTranscript(path5);
5689
5801
  return { parsed, stats: computeReplayStats(parsed.records) };
5690
5802
  }
5691
5803
  function computeReplayStats(records) {
@@ -5742,15 +5854,15 @@ function computeReplayStats(records) {
5742
5854
  };
5743
5855
  }
5744
5856
  function summarizeTurns(turns) {
5745
- const totalCost = turns.reduce((s, t) => s + t.cost, 0);
5746
- const totalInput = turns.reduce((s, t) => s + inputCostUsd(t.model, t.usage), 0);
5747
- const totalOutput = turns.reduce((s, t) => s + outputCostUsd(t.model, t.usage), 0);
5748
- const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);
5857
+ const totalCost = turns.reduce((s, t2) => s + t2.cost, 0);
5858
+ const totalInput = turns.reduce((s, t2) => s + inputCostUsd(t2.model, t2.usage), 0);
5859
+ const totalOutput = turns.reduce((s, t2) => s + outputCostUsd(t2.model, t2.usage), 0);
5860
+ const totalClaude = turns.reduce((s, t2) => s + claudeEquivalentCost(t2.usage), 0);
5749
5861
  let hit = 0;
5750
5862
  let miss = 0;
5751
- for (const t of turns) {
5752
- hit += t.usage.promptCacheHitTokens;
5753
- miss += t.usage.promptCacheMissTokens;
5863
+ for (const t2 of turns) {
5864
+ hit += t2.usage.promptCacheHitTokens;
5865
+ miss += t2.usage.promptCacheMissTokens;
5754
5866
  }
5755
5867
  const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
5756
5868
  const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
@@ -5827,8 +5939,8 @@ function diffTranscripts(a, b) {
5827
5939
  return { a: aSide, b: bSide, pairs, firstDivergenceTurn };
5828
5940
  }
5829
5941
  function classifyDivergence(a, b, aTools, bTools) {
5830
- const aNames = aTools.map((t) => t.tool ?? "").sort();
5831
- const bNames = bTools.map((t) => t.tool ?? "").sort();
5942
+ const aNames = aTools.map((t2) => t2.tool ?? "").sort();
5943
+ const bNames = bTools.map((t2) => t2.tool ?? "").sort();
5832
5944
  if (aNames.join(",") !== bNames.join(",")) {
5833
5945
  return `tool calls differ: A=[${aNames.join(",") || "\u2014"}] B=[${bNames.join(",") || "\u2014"}]`;
5834
5946
  }
@@ -5858,7 +5970,7 @@ function tokenOverlap(a, b) {
5858
5970
  const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
5859
5971
  if (ta.size === 0 && tb.size === 0) return 1;
5860
5972
  let shared = 0;
5861
- for (const t of ta) if (tb.has(t)) shared++;
5973
+ for (const t2 of ta) if (tb.has(t2)) shared++;
5862
5974
  return 2 * shared / (ta.size + tb.size);
5863
5975
  }
5864
5976
  function levenshtein(a, b) {
@@ -6051,8 +6163,8 @@ function renderMarkdown(report) {
6051
6163
  out.push(`| turn | kind | ${a.label} tool calls | ${b.label} tool calls | note |`);
6052
6164
  out.push("|---:|:---:|---|---|---|");
6053
6165
  for (const p of report.pairs) {
6054
- const aTools = p.aTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
6055
- const bTools = p.bTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
6166
+ const aTools = p.aTools.map((t2) => t2.tool).filter(Boolean).join(", ") || "\u2014";
6167
+ const bTools = p.bTools.map((t2) => t2.tool).filter(Boolean).join(", ") || "\u2014";
6056
6168
  out.push(`| ${p.turn} | ${p.kind} | ${aTools} | ${bTools} | ${p.divergenceNote ?? ""} |`);
6057
6169
  }
6058
6170
  out.push("");
@@ -6279,7 +6391,7 @@ var McpClient = class {
6279
6391
  const id = this.nextId++;
6280
6392
  const frame = { jsonrpc: "2.0", id, method, params };
6281
6393
  let abortHandler = null;
6282
- const promise = new Promise((resolve8, reject) => {
6394
+ const promise = new Promise((resolve9, reject) => {
6283
6395
  const timeout = setTimeout(() => {
6284
6396
  this.pending.delete(id);
6285
6397
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -6288,7 +6400,7 @@ var McpClient = class {
6288
6400
  );
6289
6401
  }, this.requestTimeoutMs);
6290
6402
  this.pending.set(id, {
6291
- resolve: resolve8,
6403
+ resolve: resolve9,
6292
6404
  reject,
6293
6405
  timeout
6294
6406
  });
@@ -6411,12 +6523,12 @@ var StdioTransport = class {
6411
6523
  }
6412
6524
  async send(message) {
6413
6525
  if (this.closed) throw new Error("MCP transport is closed");
6414
- return new Promise((resolve8, reject) => {
6526
+ return new Promise((resolve9, reject) => {
6415
6527
  const line = `${JSON.stringify(message)}
6416
6528
  `;
6417
6529
  this.child.stdin.write(line, "utf8", (err) => {
6418
6530
  if (err) reject(err);
6419
- else resolve8();
6531
+ else resolve9();
6420
6532
  });
6421
6533
  });
6422
6534
  }
@@ -6427,8 +6539,8 @@ var StdioTransport = class {
6427
6539
  continue;
6428
6540
  }
6429
6541
  if (this.closed) return;
6430
- const next = await new Promise((resolve8) => {
6431
- this.waiters.push(resolve8);
6542
+ const next = await new Promise((resolve9) => {
6543
+ this.waiters.push(resolve9);
6432
6544
  });
6433
6545
  if (next === null) return;
6434
6546
  yield next;
@@ -6494,8 +6606,8 @@ var SseTransport = class {
6494
6606
  constructor(opts) {
6495
6607
  this.url = opts.url;
6496
6608
  this.headers = opts.headers ?? {};
6497
- this.endpointReady = new Promise((resolve8, reject) => {
6498
- this.resolveEndpoint = resolve8;
6609
+ this.endpointReady = new Promise((resolve9, reject) => {
6610
+ this.resolveEndpoint = resolve9;
6499
6611
  this.rejectEndpoint = reject;
6500
6612
  });
6501
6613
  this.endpointReady.catch(() => void 0);
@@ -6522,8 +6634,8 @@ var SseTransport = class {
6522
6634
  continue;
6523
6635
  }
6524
6636
  if (this.closed) return;
6525
- const next = await new Promise((resolve8) => {
6526
- this.waiters.push(resolve8);
6637
+ const next = await new Promise((resolve9) => {
6638
+ this.waiters.push(resolve9);
6527
6639
  });
6528
6640
  if (next === null) return;
6529
6641
  yield next;
@@ -6791,8 +6903,8 @@ function applyEditBlock(block, rootDir) {
6791
6903
  function applyEditBlocks(blocks, rootDir) {
6792
6904
  return blocks.map((b) => applyEditBlock(b, rootDir));
6793
6905
  }
6794
- function toWholeFileEditBlock(path, content, rootDir) {
6795
- const abs = resolve6(rootDir, path);
6906
+ function toWholeFileEditBlock(path5, content, rootDir) {
6907
+ const abs = resolve6(rootDir, path5);
6796
6908
  let search = "";
6797
6909
  if (existsSync6(abs)) {
6798
6910
  try {
@@ -6801,7 +6913,7 @@ function toWholeFileEditBlock(path, content, rootDir) {
6801
6913
  search = "";
6802
6914
  }
6803
6915
  }
6804
- return { path, search, replace: content, offset: 0 };
6916
+ return { path: path5, search, replace: content, offset: 0 };
6805
6917
  }
6806
6918
  function snapshotBeforeEdits(blocks, rootDir) {
6807
6919
  const absRoot = resolve6(rootDir);
@@ -6980,20 +7092,20 @@ function appendUsage(input) {
6980
7092
  };
6981
7093
  if (input.kind === "subagent") record.kind = "subagent";
6982
7094
  if (input.subagent) record.subagent = input.subagent;
6983
- const path = input.path ?? defaultUsageLogPath();
7095
+ const path5 = input.path ?? defaultUsageLogPath();
6984
7096
  try {
6985
- mkdirSync5(dirname7(path), { recursive: true });
6986
- appendFileSync2(path, `${JSON.stringify(record)}
7097
+ mkdirSync5(dirname7(path5), { recursive: true });
7098
+ appendFileSync2(path5, `${JSON.stringify(record)}
6987
7099
  `, "utf8");
6988
7100
  } catch {
6989
7101
  }
6990
7102
  return record;
6991
7103
  }
6992
- function readUsageLog(path = defaultUsageLogPath()) {
6993
- if (!existsSync8(path)) return [];
7104
+ function readUsageLog(path5 = defaultUsageLogPath()) {
7105
+ if (!existsSync8(path5)) return [];
6994
7106
  let raw;
6995
7107
  try {
6996
- raw = readFileSync10(path, "utf8");
7108
+ raw = readFileSync10(path5, "utf8");
6997
7109
  } catch {
6998
7110
  return [];
6999
7111
  }
@@ -7097,10 +7209,10 @@ function aggregateUsage(records, opts = {}) {
7097
7209
  subagents
7098
7210
  };
7099
7211
  }
7100
- function formatLogSize(path = defaultUsageLogPath()) {
7101
- if (!existsSync8(path)) return "";
7212
+ function formatLogSize(path5 = defaultUsageLogPath()) {
7213
+ if (!existsSync8(path5)) return "";
7102
7214
  try {
7103
- const s = statSync4(path);
7215
+ const s = statSync4(path5);
7104
7216
  const bytes = s.size;
7105
7217
  if (bytes < 1024) return `${bytes} B`;
7106
7218
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -7116,7 +7228,7 @@ import { render } from "ink";
7116
7228
  import React26, { useState as useState12 } from "react";
7117
7229
 
7118
7230
  // src/cli/ui/App.tsx
7119
- import { Box as Box21, Static, useApp, useStdout as useStdout8 } from "ink";
7231
+ import { Box as Box21, Static, Text as Text19, useApp, useStdout as useStdout8 } from "ink";
7120
7232
  import React23, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState10 } from "react";
7121
7233
 
7122
7234
  // src/code/pending-edits.ts
@@ -7127,24 +7239,24 @@ function pendingEditsPath(sessionName) {
7127
7239
  }
7128
7240
  function savePendingEdits(sessionName, blocks) {
7129
7241
  if (!sessionName) return;
7130
- const path = pendingEditsPath(sessionName);
7242
+ const path5 = pendingEditsPath(sessionName);
7131
7243
  try {
7132
7244
  if (blocks.length === 0) {
7133
- if (existsSync9(path)) unlinkSync3(path);
7245
+ if (existsSync9(path5)) unlinkSync3(path5);
7134
7246
  return;
7135
7247
  }
7136
- mkdirSync6(dirname8(path), { recursive: true });
7137
- writeFileSync5(path, JSON.stringify(blocks, null, 2), "utf8");
7248
+ mkdirSync6(dirname8(path5), { recursive: true });
7249
+ writeFileSync5(path5, JSON.stringify(blocks, null, 2), "utf8");
7138
7250
  } catch {
7139
7251
  }
7140
7252
  }
7141
7253
  function loadPendingEdits(sessionName) {
7142
7254
  if (!sessionName) return null;
7143
- const path = pendingEditsPath(sessionName);
7144
- if (!existsSync9(path)) return null;
7255
+ const path5 = pendingEditsPath(sessionName);
7256
+ if (!existsSync9(path5)) return null;
7145
7257
  let raw;
7146
7258
  try {
7147
- raw = readFileSync11(path, "utf8");
7259
+ raw = readFileSync11(path5, "utf8");
7148
7260
  } catch {
7149
7261
  return null;
7150
7262
  }
@@ -7164,9 +7276,9 @@ function loadPendingEdits(sessionName) {
7164
7276
  }
7165
7277
  function clearPendingEdits(sessionName) {
7166
7278
  if (!sessionName) return;
7167
- const path = pendingEditsPath(sessionName);
7279
+ const path5 = pendingEditsPath(sessionName);
7168
7280
  try {
7169
- if (existsSync9(path)) unlinkSync3(path);
7281
+ if (existsSync9(path5)) unlinkSync3(path5);
7170
7282
  } catch {
7171
7283
  }
7172
7284
  }
@@ -7187,10 +7299,10 @@ function planStatePath(sessionName) {
7187
7299
  return join10(sessionsDir(), `${sanitizeName(sessionName)}.plan.json`);
7188
7300
  }
7189
7301
  function loadPlanState(sessionName) {
7190
- const path = planStatePath(sessionName);
7191
- if (!existsSync10(path)) return null;
7302
+ const path5 = planStatePath(sessionName);
7303
+ if (!existsSync10(path5)) return null;
7192
7304
  try {
7193
- const raw = readFileSync12(path, "utf8");
7305
+ const raw = readFileSync12(path5, "utf8");
7194
7306
  const parsed = JSON.parse(raw);
7195
7307
  if (!parsed || typeof parsed !== "object") return null;
7196
7308
  if (parsed.version !== 1) return null;
@@ -7226,9 +7338,9 @@ function loadPlanState(sessionName) {
7226
7338
  }
7227
7339
  }
7228
7340
  function savePlanState(sessionName, steps, completedStepIds, extras) {
7229
- const path = planStatePath(sessionName);
7341
+ const path5 = planStatePath(sessionName);
7230
7342
  try {
7231
- mkdirSync7(dirname9(path), { recursive: true });
7343
+ mkdirSync7(dirname9(path5), { recursive: true });
7232
7344
  const state = {
7233
7345
  version: 1,
7234
7346
  steps,
@@ -7237,7 +7349,7 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
7237
7349
  };
7238
7350
  if (extras?.body) state.body = extras.body;
7239
7351
  if (extras?.summary) state.summary = extras.summary;
7240
- writeFileSync6(path, `${JSON.stringify(state, null, 2)}
7352
+ writeFileSync6(path5, `${JSON.stringify(state, null, 2)}
7241
7353
  `, "utf8");
7242
7354
  } catch (err) {
7243
7355
  process.stderr.write(
@@ -7247,9 +7359,9 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
7247
7359
  }
7248
7360
  }
7249
7361
  function clearPlanState(sessionName) {
7250
- const path = planStatePath(sessionName);
7362
+ const path5 = planStatePath(sessionName);
7251
7363
  try {
7252
- if (existsSync10(path)) unlinkSync4(path);
7364
+ if (existsSync10(path5)) unlinkSync4(path5);
7253
7365
  } catch {
7254
7366
  }
7255
7367
  }
@@ -7317,9 +7429,9 @@ function listPlanArchives(sessionName) {
7317
7429
  return summaries;
7318
7430
  }
7319
7431
  function relativeTime(updatedAt, now = Date.now()) {
7320
- const t = Date.parse(updatedAt);
7321
- if (Number.isNaN(t)) return updatedAt;
7322
- const diffMs = Math.max(0, now - t);
7432
+ const t2 = Date.parse(updatedAt);
7433
+ if (Number.isNaN(t2)) return updatedAt;
7434
+ const diffMs = Math.max(0, now - t2);
7323
7435
  const sec = Math.floor(diffMs / 1e3);
7324
7436
  if (sec < 60) return `${sec}s ago`;
7325
7437
  const min = Math.floor(sec / 60);
@@ -7364,7 +7476,7 @@ function registerSkillTools(registry, opts = {}) {
7364
7476
  }
7365
7477
  const stripped = raw.replace(/\[[^\]]*\]/g, " ").trim();
7366
7478
  const tokens = stripped.split(/\s+/).filter(Boolean);
7367
- const name = tokens.find((t) => /^[a-zA-Z0-9]/.test(t)) ?? "";
7479
+ const name = tokens.find((t2) => /^[a-zA-Z0-9]/.test(t2)) ?? "";
7368
7480
  if (!name) {
7369
7481
  return JSON.stringify({
7370
7482
  error: "run_skill requires a 'name' argument",
@@ -7427,12 +7539,12 @@ function AtMentionSuggestions({
7427
7539
  const shown = matches.slice(windowStart, windowStart + MAX);
7428
7540
  const hiddenAbove = windowStart;
7429
7541
  const hiddenBelow = total - windowStart - shown.length;
7430
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingX: 1, marginTop: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((path, i) => /* @__PURE__ */ React.createElement(FileRow, { key: path, path, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick \xB7 file content inlined on send"));
7542
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingX: 1, marginTop: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((path5, i) => /* @__PURE__ */ React.createElement(FileRow, { key: path5, path: path5, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick \xB7 file content inlined on send"));
7431
7543
  }
7432
- function FileRow({ path, isSelected }) {
7433
- const slash = path.lastIndexOf("/");
7434
- const dir = slash >= 0 ? `${path.slice(0, slash)}/` : "";
7435
- const base = slash >= 0 ? path.slice(slash + 1) : path;
7544
+ function FileRow({ path: path5, isSelected }) {
7545
+ const slash = path5.lastIndexOf("/");
7546
+ const dir = slash >= 0 ? `${path5.slice(0, slash)}/` : "";
7547
+ const base = slash >= 0 ? path5.slice(slash + 1) : path5;
7436
7548
  if (isSelected) {
7437
7549
  return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { backgroundColor: "#67e8f9", color: "black", bold: true }, ` \u25B8 ${base}${dir ? ` ${dir}` : ""} `));
7438
7550
  }
@@ -7453,8 +7565,8 @@ function ModalCard({
7453
7565
  icon,
7454
7566
  children
7455
7567
  }) {
7456
- const { stdout: stdout2 } = useStdout();
7457
- const cols = stdout2?.columns ?? 80;
7568
+ const { stdout: stdout3 } = useStdout();
7569
+ const cols = stdout3?.columns ?? 80;
7458
7570
  const ruleWidth = Math.min(80, Math.max(28, cols - 4));
7459
7571
  const titleText = icon ? ` ${icon} ${title} ` : ` ${title} `;
7460
7572
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color: accent }, "\u2594".repeat(ruleWidth))), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { backgroundColor: accent, color: "black", bold: true }, titleText), subtitle ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, ` ${subtitle}`) : null), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1, flexDirection: "column" }, children), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: accent, dimColor: true }, "\u2581".repeat(ruleWidth))));
@@ -8017,8 +8129,8 @@ function capLines(lines, maxLines, indent) {
8017
8129
  var MODAL_OVERHEAD_ROWS = 18;
8018
8130
  var MIN_DIFF_ROWS = 8;
8019
8131
  function EditConfirm({ block, onChoose }) {
8020
- const { stdout: stdout2 } = useStdout2();
8021
- const rows = stdout2?.rows ?? 40;
8132
+ const { stdout: stdout3 } = useStdout2();
8133
+ const rows = stdout3?.rows ?? 40;
8022
8134
  const budget = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
8023
8135
  const allLines = useMemo(
8024
8136
  () => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
@@ -9008,10 +9120,10 @@ function gradientCells(width, glyph = GLYPH.block) {
9008
9120
  if (width <= 0) return cells;
9009
9121
  const last = GRADIENT.length - 1;
9010
9122
  for (let i = 0; i < width; i++) {
9011
- const t = width === 1 ? 0 : i * last / (width - 1);
9012
- const lo = Math.floor(t);
9123
+ const t2 = width === 1 ? 0 : i * last / (width - 1);
9124
+ const lo = Math.floor(t2);
9013
9125
  const hi = Math.min(last, lo + 1);
9014
- const color = t - lo < 0.5 ? GRADIENT[lo] : GRADIENT[hi];
9126
+ const color = t2 - lo < 0.5 ? GRADIENT[lo] : GRADIENT[hi];
9015
9127
  cells.push({ ch: glyph, color });
9016
9128
  }
9017
9129
  return cells;
@@ -9025,7 +9137,7 @@ function TickerProvider({ children, disabled }) {
9025
9137
  const [tick, setTick] = useState3(0);
9026
9138
  useEffect2(() => {
9027
9139
  if (disabled) return;
9028
- const id = setInterval(() => setTick((t) => t + 1), TICK_MS);
9140
+ const id = setInterval(() => setTick((t2) => t2 + 1), TICK_MS);
9029
9141
  return () => clearInterval(id);
9030
9142
  }, [disabled]);
9031
9143
  return /* @__PURE__ */ React10.createElement(TickContext.Provider, { value: tick }, children);
@@ -9314,8 +9426,8 @@ var EventRow = React11.memo(function EventRow2({
9314
9426
  return /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text8, null, event.text));
9315
9427
  });
9316
9428
  function TurnSeparator() {
9317
- const { stdout: stdout2 } = useStdout3();
9318
- const cols = stdout2?.columns ?? 80;
9429
+ const { stdout: stdout3 } = useStdout3();
9430
+ const cols = stdout3?.columns ?? 80;
9319
9431
  const width = Math.max(16, cols - 2);
9320
9432
  const sideWidth = Math.max(2, Math.floor((width - 5) / 2));
9321
9433
  const leftCells = gradientCells(sideWidth, "\u2500");
@@ -9346,10 +9458,10 @@ function EditFileDiff({ text }) {
9346
9458
  function BranchBlock({ branch: branch2 }) {
9347
9459
  return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text8, { backgroundColor: "#93c5fd", color: "black", bold: true }, ` \u2387 BRANCH \xD7${branch2.budget} `), /* @__PURE__ */ React11.createElement(Text8, null, " "), /* @__PURE__ */ React11.createElement(Text8, { color: "#93c5fd" }, "picked "), /* @__PURE__ */ React11.createElement(Text8, { color: "#93c5fd", bold: true }, "#", branch2.chosenIndex)), /* @__PURE__ */ React11.createElement(Box9, { paddingLeft: 2, marginTop: 1 }, branch2.uncertainties.map((u, i) => {
9348
9460
  const chosen = i === branch2.chosenIndex;
9349
- const t = (branch2.temperatures[i] ?? 0).toFixed(1);
9461
+ const t2 = (branch2.temperatures[i] ?? 0).toFixed(1);
9350
9462
  return (
9351
9463
  // biome-ignore lint/suspicious/noArrayIndexKey: branch index is positional and stable
9352
- /* @__PURE__ */ React11.createElement(Text8, { key: `b-${i}` }, /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#475569", bold: chosen }, chosen ? "\u25B8 " : " "), /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#94a3b8", bold: chosen }, `#${i}`), /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, ` T=${t} u=${u} `))
9464
+ /* @__PURE__ */ React11.createElement(Text8, { key: `b-${i}` }, /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#475569", bold: chosen }, chosen ? "\u25B8 " : " "), /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#94a3b8", bold: chosen }, `#${i}`), /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, ` T=${t2} u=${u} `))
9353
9465
  );
9354
9466
  })));
9355
9467
  }
@@ -9467,15 +9579,14 @@ function ModeStatusBar({
9467
9579
  if (planMode) {
9468
9580
  return /* @__PURE__ */ React12.createElement(ModeBarFrame, null, /* @__PURE__ */ React12.createElement(ModePill, { label: "PLAN MODE", bg: "red", flash }), /* @__PURE__ */ React12.createElement(Text9, { dimColor: true }, " writes gated \xB7 /plan off to leave"), jobsTag);
9469
9581
  }
9470
- const isAuto = editMode === "auto";
9471
- const label = isAuto ? "AUTO" : "REVIEW";
9472
- const bg = isAuto ? "magenta" : "cyan";
9473
- const mid = isAuto ? "edits land now \xB7 u to undo" : pendingCount > 0 ? `${pendingCount} queued \xB7 y apply \xB7 n discard` : "edits queued \xB7 y apply \xB7 n discard";
9582
+ const label = editMode === "yolo" ? "YOLO" : editMode === "auto" ? "AUTO" : "REVIEW";
9583
+ const bg = editMode === "yolo" ? "red" : editMode === "auto" ? "magenta" : "cyan";
9584
+ const mid = editMode === "yolo" ? "edits + shell auto \xB7 /undo to roll back" : editMode === "auto" ? "edits land now \xB7 u to undo" : pendingCount > 0 ? `${pendingCount} queued \xB7 y apply \xB7 n discard` : "edits queued \xB7 y apply \xB7 n discard";
9474
9585
  return /* @__PURE__ */ React12.createElement(ModeBarFrame, null, /* @__PURE__ */ React12.createElement(ModePill, { label, bg, flash }), /* @__PURE__ */ React12.createElement(Text9, { dimColor: true }, ` ${mid} \xB7 Shift+Tab to flip`), jobsTag);
9475
9586
  }
9476
9587
  function ModeBarFrame({ children }) {
9477
- const { stdout: stdout2 } = useStdout4();
9478
- const cols = stdout2?.columns ?? 80;
9588
+ const { stdout: stdout3 } = useStdout4();
9589
+ const cols = stdout3?.columns ?? 80;
9479
9590
  const ruleWidth = Math.max(20, cols - 2);
9480
9591
  return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Box10, { paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text9, { color: "#475569", dimColor: true }, "\u254C".repeat(ruleWidth))), /* @__PURE__ */ React12.createElement(Box10, { paddingX: 1 }, children));
9481
9592
  }
@@ -9534,26 +9645,26 @@ function summarizeToolArgs(name, args) {
9534
9645
  return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
9535
9646
  }
9536
9647
  const hasSuffix = (s) => name === s || name.endsWith(`_${s}`);
9537
- const path = typeof parsed.path === "string" ? parsed.path : void 0;
9648
+ const path5 = typeof parsed.path === "string" ? parsed.path : void 0;
9538
9649
  if (hasSuffix("read_file")) {
9539
9650
  const head = typeof parsed.head === "number" ? `, head=${parsed.head}` : "";
9540
9651
  const tail = typeof parsed.tail === "number" ? `, tail=${parsed.tail}` : "";
9541
- return `path: ${path ?? "?"}${head}${tail}`;
9652
+ return `path: ${path5 ?? "?"}${head}${tail}`;
9542
9653
  }
9543
9654
  if (hasSuffix("write_file")) {
9544
9655
  const content = typeof parsed.content === "string" ? parsed.content : "";
9545
- return `path: ${path ?? "?"} (${content.length} chars)`;
9656
+ return `path: ${path5 ?? "?"} (${content.length} chars)`;
9546
9657
  }
9547
9658
  if (hasSuffix("edit_file")) {
9548
9659
  const edits = Array.isArray(parsed.edits) ? parsed.edits.length : 0;
9549
- return `path: ${path ?? "?"} (${edits} edit${edits === 1 ? "" : "s"})`;
9660
+ return `path: ${path5 ?? "?"} (${edits} edit${edits === 1 ? "" : "s"})`;
9550
9661
  }
9551
9662
  if (hasSuffix("list_directory") || hasSuffix("directory_tree")) {
9552
- return `path: ${path ?? "?"}`;
9663
+ return `path: ${path5 ?? "?"}`;
9553
9664
  }
9554
9665
  if (hasSuffix("search_files")) {
9555
9666
  const pattern = typeof parsed.pattern === "string" ? parsed.pattern : "?";
9556
- return `path: ${path ?? "?"} \xB7 pattern: ${pattern}`;
9667
+ return `path: ${path5 ?? "?"} \xB7 pattern: ${pattern}`;
9557
9668
  }
9558
9669
  if (hasSuffix("move_file")) {
9559
9670
  const src = typeof parsed.source === "string" ? parsed.source : "?";
@@ -9561,7 +9672,7 @@ function summarizeToolArgs(name, args) {
9561
9672
  return `${src} \u2192 ${dst}`;
9562
9673
  }
9563
9674
  if (hasSuffix("get_file_info")) {
9564
- return `path: ${path ?? "?"}`;
9675
+ return `path: ${path5 ?? "?"}`;
9565
9676
  }
9566
9677
  return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
9567
9678
  }
@@ -10276,8 +10387,8 @@ function PromptInput({
10276
10387
  if (action.historyHandoff === "prev") onHistoryPrev?.();
10277
10388
  if (action.historyHandoff === "next") onHistoryNext?.();
10278
10389
  }, !disabled);
10279
- const { stdout: stdout2 } = useStdout5();
10280
- const cols = stdout2?.columns ?? 80;
10390
+ const { stdout: stdout3 } = useStdout5();
10391
+ const cols = stdout3?.columns ?? 80;
10281
10392
  const narrow = cols <= 90;
10282
10393
  const promptBody = narrow ? "\u203A " : "you \u203A ";
10283
10394
  const promptPrefix = BAR + promptBody;
@@ -10648,8 +10759,8 @@ function StatsPanel({
10648
10759
  const branchOn = (branchBudget ?? 1) > 1;
10649
10760
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model2] ?? DEFAULT_CONTEXT_TOKENS;
10650
10761
  const ctxRatio = summary.lastPromptTokens / ctxMax;
10651
- const { stdout: stdout2 } = useStdout6();
10652
- const columns = stdout2?.columns ?? 80;
10762
+ const { stdout: stdout3 } = useStdout6();
10763
+ const columns = stdout3?.columns ?? 80;
10653
10764
  const narrow = columns < NARROW_BREAKPOINT;
10654
10765
  const coldStart = summary.turns <= COLD_START_TURNS;
10655
10766
  return /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column", paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React21.createElement(
@@ -10815,8 +10926,8 @@ function formatTokens(n) {
10815
10926
  import { Box as Box20, Text as Text18, useStdout as useStdout7 } from "ink";
10816
10927
  import React22 from "react";
10817
10928
  function WelcomeBanner({ inCodeMode }) {
10818
- const { stdout: stdout2 } = useStdout7();
10819
- const cols = stdout2?.columns ?? 80;
10929
+ const { stdout: stdout3 } = useStdout7();
10930
+ const cols = stdout3?.columns ?? 80;
10820
10931
  const ruleWidth = Math.min(60, Math.max(28, cols - 4));
10821
10932
  return /* @__PURE__ */ React22.createElement(Box20, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth }), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.brand }, "\u25C8 welcome"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " \xB7 type a message to start")), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.primary }, "quick start")), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/help", desc: "every command + keyboard shortcut" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/skill", desc: "invoke a stored playbook" }), inCodeMode ? /* @__PURE__ */ React22.createElement(React22.Fragment, null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "@path", desc: "inline a file in your message" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "!cmd", desc: "run a shell command, output goes to context" })) : null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "/exit", desc: "quit (Ctrl+C also works)" }), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { dimColor: true, italic: true }, "tip:"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " Ctrl+J inserts a newline \xB7 trailing \\ also continues")), /* @__PURE__ */ React22.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth, thin: true })));
10822
10933
  }
@@ -10877,7 +10988,7 @@ function parseEditIndices(raw, max) {
10877
10988
  if (!trimmed) return { ok: [] };
10878
10989
  if (max <= 0) return { error: "no pending edits to address" };
10879
10990
  const seen = /* @__PURE__ */ new Set();
10880
- const tokens = trimmed.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
10991
+ const tokens = trimmed.split(",").map((t2) => t2.trim()).filter((t2) => t2.length > 0);
10881
10992
  if (tokens.length === 0) return { ok: [] };
10882
10993
  for (const tok of tokens) {
10883
10994
  const range = tok.match(/^(\d+)-(\d+)$/);
@@ -10927,13 +11038,21 @@ function describeRepair(repair) {
10927
11038
  }
10928
11039
 
10929
11040
  // src/cli/ui/hash-memory.ts
10930
- import { appendFileSync as appendFileSync3, existsSync as existsSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
10931
- import { join as join12 } from "path";
10932
- var NEW_FILE_HEADER = `# Reasonix project memory
11041
+ import { appendFileSync as appendFileSync3, existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
11042
+ import { homedir as homedir6 } from "os";
11043
+ import { dirname as dirname10, join as join12 } from "path";
11044
+ var PROJECT_HEADER = `# Reasonix project memory
10933
11045
 
10934
11046
  Notes the user pinned via the \`#\` prompt prefix. The whole file is
10935
11047
  loaded into the immutable system prefix every session \u2014 keep it terse.
10936
11048
 
11049
+ `;
11050
+ var GLOBAL_HEADER = `# Reasonix global memory
11051
+
11052
+ Cross-project notes the user pinned via the \`#g\` prompt prefix. Loaded
11053
+ into every Reasonix session's prefix regardless of working directory.
11054
+ Private to this machine \u2014 not committed anywhere.
11055
+
10937
11056
  `;
10938
11057
  function detectHashMemory(text) {
10939
11058
  if (text.startsWith("\\#")) {
@@ -10941,28 +11060,105 @@ function detectHashMemory(text) {
10941
11060
  }
10942
11061
  if (!text.startsWith("#")) return null;
10943
11062
  if (text.startsWith("##")) return null;
11063
+ if (/^#g\s*$/.test(text)) return null;
11064
+ const globalMatch = /^#g\s+(.+)$/s.exec(text);
11065
+ if (globalMatch) {
11066
+ const body2 = globalMatch[1].trim();
11067
+ if (!body2) return null;
11068
+ return { kind: "memory-global", note: body2 };
11069
+ }
10944
11070
  const body = text.slice(1).trim();
10945
11071
  if (!body) return null;
10946
11072
  return { kind: "memory", note: body };
10947
11073
  }
10948
11074
  function appendProjectMemory(rootDir, note) {
10949
- const path = join12(rootDir, PROJECT_MEMORY_FILE);
11075
+ return appendBulletToFile(join12(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
11076
+ }
11077
+ var GLOBAL_MEMORY_DIR = ".reasonix";
11078
+ var GLOBAL_MEMORY_FILE = "REASONIX.md";
11079
+ function globalMemoryPath(homeDir = homedir6()) {
11080
+ return join12(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
11081
+ }
11082
+ function appendGlobalMemory(note, homeDir) {
11083
+ return appendBulletToFile(globalMemoryPath(homeDir), note, GLOBAL_HEADER);
11084
+ }
11085
+ function appendBulletToFile(path5, note, newFileHeader) {
10950
11086
  const trimmed = note.trim();
10951
11087
  if (!trimmed) throw new Error("note body cannot be empty");
10952
11088
  const bullet = `- ${trimmed}
10953
11089
  `;
10954
- if (!existsSync11(path)) {
10955
- writeFileSync7(path, `${NEW_FILE_HEADER}${bullet}`, "utf8");
10956
- return { path, created: true };
11090
+ if (!existsSync11(path5)) {
11091
+ mkdirSync8(dirname10(path5), { recursive: true });
11092
+ writeFileSync7(path5, `${newFileHeader}${bullet}`, "utf8");
11093
+ return { path: path5, created: true };
10957
11094
  }
10958
11095
  let prefix = "";
10959
11096
  try {
10960
- const existing = readFileSync14(path, "utf8");
11097
+ const existing = readFileSync14(path5, "utf8");
10961
11098
  if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n";
10962
11099
  } catch {
10963
11100
  }
10964
- appendFileSync3(path, `${prefix}${bullet}`, "utf8");
10965
- return { path, created: false };
11101
+ appendFileSync3(path5, `${prefix}${bullet}`, "utf8");
11102
+ return { path: path5, created: false };
11103
+ }
11104
+
11105
+ // src/cli/ui/loop.ts
11106
+ var MIN_LOOP_INTERVAL_MS = 5e3;
11107
+ var MAX_LOOP_INTERVAL_MS = 6 * 60 * 6e4;
11108
+ function parseLoopInterval(raw) {
11109
+ const s = raw.trim().toLowerCase();
11110
+ if (!s) return null;
11111
+ const m = /^([0-9]+(?:\.[0-9]+)?)(s|sec|secs|m|min|mins|h|hr|hrs)?$/.exec(s);
11112
+ if (!m) return null;
11113
+ const n = Number.parseFloat(m[1] ?? "");
11114
+ if (!Number.isFinite(n) || n <= 0) return null;
11115
+ const unit = m[2] ?? "s";
11116
+ let ms;
11117
+ if (unit === "s" || unit === "sec" || unit === "secs") ms = Math.round(n * 1e3);
11118
+ else if (unit === "m" || unit === "min" || unit === "mins") ms = Math.round(n * 6e4);
11119
+ else if (unit === "h" || unit === "hr" || unit === "hrs") ms = Math.round(n * 60 * 6e4);
11120
+ else return null;
11121
+ if (ms < MIN_LOOP_INTERVAL_MS) return null;
11122
+ if (ms > MAX_LOOP_INTERVAL_MS) return null;
11123
+ return { ms };
11124
+ }
11125
+ function parseLoopCommand(args) {
11126
+ if (args.length === 0) return { kind: "status" };
11127
+ const first = (args[0] ?? "").toLowerCase();
11128
+ if (args.length === 1 && (first === "stop" || first === "off" || first === "cancel")) {
11129
+ return { kind: "stop" };
11130
+ }
11131
+ const interval = parseLoopInterval(args[0] ?? "");
11132
+ if (!interval) {
11133
+ return {
11134
+ kind: "error",
11135
+ message: "usage: /loop <interval> <prompt> (interval = 5s..6h, e.g. 30s, 5m, 1h)\n /loop stop (cancel an active loop)\n /loop (show active-loop status)"
11136
+ };
11137
+ }
11138
+ const prompt = args.slice(1).join(" ").trim();
11139
+ if (!prompt) {
11140
+ return {
11141
+ kind: "error",
11142
+ message: `usage: /loop ${args[0]} <prompt> \u2014 interval is fine but the prompt is missing.`
11143
+ };
11144
+ }
11145
+ return { kind: "start", intervalMs: interval.ms, prompt };
11146
+ }
11147
+ function formatLoopStatus(prompt, nextFireMs, iter) {
11148
+ const preview = prompt.length > 36 ? `${prompt.slice(0, 33)}\u2026` : prompt;
11149
+ const when = nextFireMs <= 0 ? "firing now" : `next in ${formatDuration2(nextFireMs)}`;
11150
+ return `loop: \`${preview}\` \xB7 ${when} \xB7 iter ${iter}`;
11151
+ }
11152
+ function formatDuration2(ms) {
11153
+ if (ms < 1e3) return `${ms}ms`;
11154
+ const totalSec = Math.round(ms / 1e3);
11155
+ if (totalSec < 60) return `${totalSec}s`;
11156
+ const m = Math.floor(totalSec / 60);
11157
+ const s = totalSec % 60;
11158
+ if (m < 60) return s === 0 ? `${m}m` : `${m}m${s}s`;
11159
+ const h = Math.floor(m / 60);
11160
+ const mm = m % 60;
11161
+ return mm === 0 ? `${h}h` : `${h}h${mm}m`;
10966
11162
  }
10967
11163
 
10968
11164
  // src/cli/ui/mcp-browse.ts
@@ -11268,8 +11464,17 @@ var SLASH_COMMANDS = [
11268
11464
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
11269
11465
  { cmd: "forget", summary: "delete the current session from disk" },
11270
11466
  { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
11467
+ {
11468
+ cmd: "semantic",
11469
+ summary: "show semantic_search status \u2014 built? Ollama installed? how to enable"
11470
+ },
11271
11471
  { cmd: "clear", summary: "clear visible scrollback only (log/context kept)" },
11272
11472
  { cmd: "new", summary: "start a fresh conversation (clear context + scrollback)" },
11473
+ {
11474
+ cmd: "loop",
11475
+ argsHint: "<5s..6h> <prompt> \xB7 stop \xB7 (no args = status)",
11476
+ summary: "auto-resubmit <prompt> every <interval> until you type something / Esc / /loop stop"
11477
+ },
11273
11478
  { cmd: "exit", summary: "quit the TUI" },
11274
11479
  // Code-mode only
11275
11480
  {
@@ -11284,6 +11489,11 @@ var SLASH_COMMANDS = [
11284
11489
  summary: "drop pending edit blocks without writing (no arg \u2192 all; indices \u2192 that subset)",
11285
11490
  contextual: "code"
11286
11491
  },
11492
+ {
11493
+ cmd: "walk",
11494
+ summary: "step through pending edits one block at a time (git-add-p style: y/n per block, a apply rest, A flip AUTO)",
11495
+ contextual: "code"
11496
+ },
11287
11497
  { cmd: "undo", summary: "roll back the last applied edit batch", contextual: "code" },
11288
11498
  {
11289
11499
  cmd: "history",
@@ -11316,10 +11526,10 @@ var SLASH_COMMANDS = [
11316
11526
  },
11317
11527
  {
11318
11528
  cmd: "mode",
11319
- argsHint: "[review|auto]",
11320
- summary: "edit-gate: review (queue for /apply) or auto (apply+undo banner). Shift+Tab cycles.",
11529
+ argsHint: "[review|auto|yolo]",
11530
+ summary: "edit-gate: review (queue) \xB7 auto (apply+undo) \xB7 yolo (apply+auto-shell). Shift+Tab cycles.",
11321
11531
  contextual: "code",
11322
- argCompleter: ["review", "auto"]
11532
+ argCompleter: ["review", "auto", "yolo"]
11323
11533
  },
11324
11534
  { cmd: "jobs", summary: "list background jobs started by run_background", contextual: "code" },
11325
11535
  {
@@ -11380,12 +11590,12 @@ function statsCommand(opts) {
11380
11590
  }
11381
11591
  dashboard(opts);
11382
11592
  }
11383
- function transcriptSummary(path) {
11384
- if (!existsSync12(path)) {
11385
- console.error(`no such transcript: ${path}`);
11593
+ function transcriptSummary(path5) {
11594
+ if (!existsSync12(path5)) {
11595
+ console.error(`no such transcript: ${path5}`);
11386
11596
  process.exit(1);
11387
11597
  }
11388
- const lines = readFileSync15(path, "utf8").split(/\r?\n/).filter(Boolean);
11598
+ const lines = readFileSync15(path5, "utf8").split(/\r?\n/).filter(Boolean);
11389
11599
  let assistantTurns = 0;
11390
11600
  let toolCalls = 0;
11391
11601
  let lastTurn = 0;
@@ -11398,25 +11608,25 @@ function transcriptSummary(path) {
11398
11608
  } catch {
11399
11609
  }
11400
11610
  }
11401
- console.log(`transcript: ${path}`);
11611
+ console.log(`transcript: ${path5}`);
11402
11612
  console.log(`assistant turns: ${assistantTurns}`);
11403
11613
  console.log(`tool invocations: ${toolCalls}`);
11404
11614
  console.log(`last turn index: ${lastTurn}`);
11405
11615
  }
11406
11616
  function dashboard(opts) {
11407
- const path = opts.logPath ?? defaultUsageLogPath();
11408
- const records = readUsageLog(path);
11617
+ const path5 = opts.logPath ?? defaultUsageLogPath();
11618
+ const records = readUsageLog(path5);
11409
11619
  if (records.length === 0) {
11410
11620
  console.log("no usage data yet.");
11411
11621
  console.log("");
11412
- console.log(` ${path}`);
11622
+ console.log(` ${path5}`);
11413
11623
  console.log("");
11414
11624
  console.log("run `reasonix chat`, `reasonix code`, or `reasonix run <task>` \u2014 every turn");
11415
11625
  console.log("appends one line to the log and `reasonix stats` will roll it up.");
11416
11626
  return;
11417
11627
  }
11418
11628
  const agg = aggregateUsage(records, { now: opts.now });
11419
- console.log(renderDashboard(agg, path));
11629
+ console.log(renderDashboard(agg, path5));
11420
11630
  }
11421
11631
  function renderDashboard(agg, logPath) {
11422
11632
  const lines = [];
@@ -11500,7 +11710,7 @@ function pad(s, width, align = "left") {
11500
11710
  }
11501
11711
 
11502
11712
  // src/cli/ui/slash/handlers/admin.ts
11503
- var hooks = (args, loop, ctx) => {
11713
+ var hooks = (args, loop2, ctx) => {
11504
11714
  const sub = (args[0] ?? "").toLowerCase();
11505
11715
  if (sub === "reload") {
11506
11716
  if (!ctx.reloadHooks) {
@@ -11516,7 +11726,7 @@ var hooks = (args, loop, ctx) => {
11516
11726
  info: "usage: /hooks list active hooks\n /hooks reload re-read settings.json files"
11517
11727
  };
11518
11728
  }
11519
- const all = loop.hooks;
11729
+ const all = loop2.hooks;
11520
11730
  const projPath = ctx.codeRoot ? projectSettingsPath(ctx.codeRoot) : void 0;
11521
11731
  const globPath = globalSettingsPath();
11522
11732
  if (all.length === 0) {
@@ -11588,14 +11798,14 @@ var update = (_args, _loop, ctx) => {
11588
11798
  return { info: lines.join("\n") };
11589
11799
  };
11590
11800
  var stats = () => {
11591
- const path = defaultUsageLogPath();
11592
- const records = readUsageLog(path);
11801
+ const path5 = defaultUsageLogPath();
11802
+ const records = readUsageLog(path5);
11593
11803
  if (records.length === 0) {
11594
11804
  return {
11595
11805
  info: [
11596
11806
  "no usage data yet.",
11597
11807
  "",
11598
- ` ${path}`,
11808
+ ` ${path5}`,
11599
11809
  "",
11600
11810
  "every turn you run here appends one record \u2014 this session's turns",
11601
11811
  "will show up in the dashboard once you send a message."
@@ -11603,7 +11813,7 @@ var stats = () => {
11603
11813
  };
11604
11814
  }
11605
11815
  const agg = aggregateUsage(records);
11606
- return { info: renderDashboard(agg, path) };
11816
+ return { info: renderDashboard(agg, path5) };
11607
11817
  };
11608
11818
  var handlers = {
11609
11819
  hook: hooks,
@@ -11618,8 +11828,8 @@ var clear = () => ({
11618
11828
  clear: true,
11619
11829
  info: "\u25B8 terminal cleared (viewport + scrollback). Context (message log) is intact \u2014 next turn still sees everything. Use /new to start fresh, or /forget to delete the session entirely."
11620
11830
  });
11621
- var resetLog = (_args, loop) => {
11622
- const { dropped } = loop.clearLog();
11831
+ var resetLog = (_args, loop2) => {
11832
+ const { dropped } = loop2.clearLog();
11623
11833
  return {
11624
11834
  clear: true,
11625
11835
  info: `\u25B8 new conversation \u2014 dropped ${dropped} message(s) from context. Same session, fresh slate.`
@@ -11647,9 +11857,13 @@ var keys = () => ({
11647
11857
  " /<name> slash command; Tab/Enter picks from the suggestion list",
11648
11858
  " @<path> inline a file under [Referenced files] (code mode).",
11649
11859
  " Trailing `@\u2026` opens a file picker; \u2191/\u2193 navigate, Tab/Enter pick.",
11860
+ " @https://... fetch the URL, strip HTML, inline under [Referenced URLs].",
11861
+ " Cached per session \u2014 same URL twice fetches once.",
11650
11862
  " !<cmd> run <cmd> as shell in the sandbox root; output goes into context",
11651
11863
  " so the model sees it next turn. No allowlist gate.",
11652
- " #<note> append <note> to REASONIX.md so it pins into every future session.",
11864
+ " #<note> append <note> to <project>/REASONIX.md (committable, team-shared).",
11865
+ " #g <note> append <note> to ~/.reasonix/REASONIX.md (global, never committed).",
11866
+ " Both pin into the immutable prefix every future session.",
11653
11867
  " Use `\\#literal` if you actually want a `#` heading sent to the model.",
11654
11868
  "",
11655
11869
  "Pickers (slash + @-mention):",
@@ -11691,13 +11905,14 @@ var help = () => ({
11691
11905
  " /retry truncate & resend your last message (fresh sample from the model)",
11692
11906
  " /apply [N|1,3|1-4] (code mode) commit pending edit blocks (no arg \u2192 all; index \u2192 subset)",
11693
11907
  " /discard [N|1,3|1-4] (code mode) drop pending edits (no arg \u2192 all; index \u2192 subset)",
11908
+ " /walk (code mode) step through pending edits one block at a time (y/n per block, a apply rest, A flip AUTO)",
11694
11909
  " /undo (code mode) roll back the latest non-undone edit batch",
11695
11910
  " /history (code mode) list every edit batch this session",
11696
11911
  " /show [id] (code mode) dump a stored edit diff (newest when id omitted)",
11697
11912
  ' /commit "msg" (code mode) git add -A && git commit -m "msg"',
11698
11913
  " /plan [on|off] (code mode) toggle read-only plan mode; writes gated behind submit_plan + your approval",
11699
11914
  " /apply-plan (code mode) force-approve pending/in-text plan (fallback)",
11700
- " /mode [review|auto] (code mode) edit-gate: queue edits for /apply or apply instantly (Shift+Tab cycles, u undoes within 5s)",
11915
+ " /mode [review|auto|yolo] (code mode) review = queue \xB7 auto = apply+undo banner \xB7 yolo = apply+auto-shell. Shift+Tab cycles all three.",
11701
11916
  " /jobs (code mode) list background processes (run_background) \u2014 running and exited",
11702
11917
  " /kill <id> (code mode) stop a background job by id (SIGTERM \u2192 SIGKILL)",
11703
11918
  " /logs <id> [lines] (code mode) tail a background job's output (default 80 lines)",
@@ -11705,6 +11920,7 @@ var help = () => ({
11705
11920
  " /forget delete the current session from disk",
11706
11921
  " /new start fresh: drop all context + clear scrollback",
11707
11922
  " /clear clear displayed scrollback only (context kept \u2014 model still sees it)",
11923
+ " /loop <interval> <prompt> auto-resubmit <prompt> every <interval> (5s..6h). /loop stop \xB7 type anything to cancel.",
11708
11924
  " /exit quit",
11709
11925
  "",
11710
11926
  "Shell shortcut:",
@@ -11714,15 +11930,22 @@ var help = () => ({
11714
11930
  " Example: !git status !ls src/ !npm test",
11715
11931
  "",
11716
11932
  "Quick memory:",
11717
- " #<note> append <note> to REASONIX.md (pinned into every",
11718
- " future session's prefix). Faster than /memory for",
11719
- " one-liners. Example: #always use pnpm not npm",
11933
+ " #<note> append <note> to <project>/REASONIX.md (committable).",
11934
+ " Example: #findByEmail must be case-insensitive",
11935
+ " #g <note> append <note> to ~/.reasonix/REASONIX.md (global, never committed).",
11936
+ " Example: #g always run pnpm not npm",
11937
+ " Both pin into every future session's prefix. Faster than /memory.",
11720
11938
  " Use `\\#text` to send a literal `#text` to the model.",
11721
11939
  "",
11722
11940
  "File references (code mode):",
11723
11941
  " @path/to/file inline file content under [Referenced files] on send.",
11724
11942
  " Type `@` to open the picker (\u2191\u2193 navigate, Tab/Enter pick).",
11725
11943
  "",
11944
+ "URL references:",
11945
+ " @https://example.com fetch the URL, strip HTML, inline under [Referenced URLs].",
11946
+ " Same URL twice in one session fetches once (in-mem cache).",
11947
+ " Trailing sentence punctuation (./,/)) is stripped automatically.",
11948
+ "",
11726
11949
  "Presets (branch + harvest are NEVER auto-enabled \u2014 opt-in only):",
11727
11950
  " fast v4-flash \xB7 effort=high cheapest \xB7 quick Q&A, one-line edits",
11728
11951
  " smart v4-flash \xB7 effort=max \u2190 default \xB7 day-to-day coding",
@@ -11742,8 +11965,8 @@ var help = () => ({
11742
11965
  var setup = () => ({
11743
11966
  info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
11744
11967
  });
11745
- var retry = (_args, loop) => {
11746
- const prev = loop.retryLastUser();
11968
+ var retry = (_args, loop2) => {
11969
+ const prev = loop2.retryLastUser();
11747
11970
  if (!prev) {
11748
11971
  return {
11749
11972
  info: "nothing to retry \u2014 no prior user message in this session's log."
@@ -11755,6 +11978,37 @@ var retry = (_args, loop) => {
11755
11978
  resubmit: prev
11756
11979
  };
11757
11980
  };
11981
+ var loop = (args, _loop, ctx) => {
11982
+ if (!ctx.startLoop || !ctx.stopLoop || !ctx.getLoopStatus) {
11983
+ return {
11984
+ info: "/loop is only available in the interactive TUI (not in run/replay)."
11985
+ };
11986
+ }
11987
+ const cmd = parseLoopCommand(args);
11988
+ if (cmd.kind === "error") return { info: cmd.message };
11989
+ if (cmd.kind === "stop") {
11990
+ const wasActive = ctx.getLoopStatus() !== null;
11991
+ ctx.stopLoop();
11992
+ return {
11993
+ info: wasActive ? "\u25B8 loop stopped." : "no active loop to stop."
11994
+ };
11995
+ }
11996
+ if (cmd.kind === "status") {
11997
+ const status2 = ctx.getLoopStatus();
11998
+ if (!status2) {
11999
+ return {
12000
+ info: "no active loop. Start one with `/loop <interval> <prompt>` (e.g. /loop 30s npm test).\nCancels on: /loop stop \xB7 Esc \xB7 /clear \xB7 /new \xB7 any user-typed prompt."
12001
+ };
12002
+ }
12003
+ return { info: `\u25B8 ${formatLoopStatus(status2.prompt, status2.nextFireMs, status2.iter)}` };
12004
+ }
12005
+ ctx.startLoop(cmd.intervalMs, cmd.prompt);
12006
+ return {
12007
+ info: `\u25B8 loop started \u2014 re-submitting "${cmd.prompt}" every ${formatDuration2(
12008
+ cmd.intervalMs
12009
+ )}. Type anything (or /loop stop) to cancel.`
12010
+ };
12011
+ };
11758
12012
  var handlers2 = {
11759
12013
  exit,
11760
12014
  quit: exit,
@@ -11765,7 +12019,8 @@ var handlers2 = {
11765
12019
  help,
11766
12020
  "?": help,
11767
12021
  setup,
11768
- retry
12022
+ retry,
12023
+ loop
11769
12024
  };
11770
12025
 
11771
12026
  // src/cli/ui/slash/helpers.ts
@@ -11860,8 +12115,8 @@ ${gitTail(commit2)}` };
11860
12115
  }
11861
12116
  function gitTail(res) {
11862
12117
  const stderr = res.stderr ?? "";
11863
- const stdout2 = res.stdout ?? "";
11864
- const body = stderr.trim() || stdout2.trim();
12118
+ const stdout3 = res.stdout ?? "";
12119
+ const body = stderr.trim() || stdout3.trim();
11865
12120
  if (body) return body;
11866
12121
  if (res.error) return res.error.message;
11867
12122
  return "(no output from git)";
@@ -11961,15 +12216,17 @@ var mode = (args, _loop, ctx) => {
11961
12216
  let target;
11962
12217
  if (raw === "review") target = "review";
11963
12218
  else if (raw === "auto") target = "auto";
12219
+ else if (raw === "yolo") target = "yolo";
11964
12220
  else if (raw === "") {
11965
- target = current === "auto" ? "review" : "auto";
12221
+ target = current === "review" ? "auto" : current === "auto" ? "yolo" : "review";
11966
12222
  } else {
11967
- return { info: "usage: /mode <review|auto> (Shift+Tab also cycles)" };
12223
+ return {
12224
+ info: "usage: /mode <review|auto|yolo> (Shift+Tab also cycles)"
12225
+ };
11968
12226
  }
11969
12227
  ctx.setEditMode(target);
11970
- return {
11971
- info: target === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo, or /undo later" : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)"
11972
- };
12228
+ const banner = target === "yolo" ? "\u25B8 edit mode: YOLO \u2014 edits AND shell commands auto-run with no prompt. /undo still rolls back edits. Use carefully." : target === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo, or /undo later. Shell commands still ask." : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)";
12229
+ return { info: banner };
11973
12230
  };
11974
12231
  var commit = (args, _loop, ctx) => {
11975
12232
  if (!ctx.codeRoot) {
@@ -11986,6 +12243,14 @@ var commit = (args, _loop, ctx) => {
11986
12243
  }
11987
12244
  return runGitCommit(ctx.codeRoot, message);
11988
12245
  };
12246
+ var walk2 = (_args, _loop, ctx) => {
12247
+ if (!ctx.startWalkthrough) {
12248
+ return {
12249
+ info: "/walk is only available inside `reasonix code`."
12250
+ };
12251
+ }
12252
+ return { info: ctx.startWalkthrough() };
12253
+ };
11989
12254
  var handlers3 = {
11990
12255
  undo,
11991
12256
  history,
@@ -11996,7 +12261,8 @@ var handlers3 = {
11996
12261
  "apply-plan": applyPlan,
11997
12262
  applyplan: applyPlan,
11998
12263
  mode,
11999
- commit
12264
+ commit,
12265
+ walk: walk2
12000
12266
  };
12001
12267
 
12002
12268
  // src/cli/ui/slash/handlers/jobs.ts
@@ -12061,10 +12327,10 @@ var handlers4 = {
12061
12327
  };
12062
12328
 
12063
12329
  // src/cli/ui/slash/handlers/mcp.ts
12064
- var mcp = (_args, loop, ctx) => {
12330
+ var mcp = (_args, loop2, ctx) => {
12065
12331
  const servers = ctx.mcpServers ?? [];
12066
12332
  const specs = ctx.mcpSpecs ?? [];
12067
- const toolSpecs = loop.prefix.toolSpecs ?? [];
12333
+ const toolSpecs = loop2.prefix.toolSpecs ?? [];
12068
12334
  if (servers.length === 0 && specs.length === 0 && toolSpecs.length === 0) {
12069
12335
  return {
12070
12336
  info: 'no MCP servers attached. Run `reasonix setup` to pick some, or launch with --mcp "<spec>". `reasonix mcp list` shows the catalog.'
@@ -12109,7 +12375,7 @@ var mcp = (_args, loop, ctx) => {
12109
12375
  }
12110
12376
  if (toolSpecs.length > 0) {
12111
12377
  lines.push(`Tools in registry (${toolSpecs.length}):`);
12112
- for (const t of toolSpecs) lines.push(` \xB7 ${t.function.name}`);
12378
+ for (const t2 of toolSpecs) lines.push(` \xB7 ${t2.function.name}`);
12113
12379
  }
12114
12380
  lines.push("");
12115
12381
  lines.push("To change this set, exit and run `reasonix setup`.");
@@ -12253,14 +12519,14 @@ var memory = (args, _loop, ctx) => {
12253
12519
  var handlers6 = { memory };
12254
12520
 
12255
12521
  // src/cli/ui/slash/handlers/model.ts
12256
- var model = (args, loop, ctx) => {
12522
+ var model = (args, loop2, ctx) => {
12257
12523
  const id = args[0];
12258
12524
  const known = ctx.models ?? null;
12259
12525
  if (!id) {
12260
12526
  const hint = known && known.length > 0 ? known.join(" | ") : "try deepseek-v4-flash or deepseek-v4-pro \u2014 run /models to fetch the live list";
12261
12527
  return { info: `usage: /model <id> (${hint})` };
12262
12528
  }
12263
- loop.configure({ model: id });
12529
+ loop2.configure({ model: id });
12264
12530
  if (known && known.length > 0 && !known.includes(id)) {
12265
12531
  return {
12266
12532
  info: `model \u2192 ${id} (\u26A0 not in the fetched catalog: ${known.join(", ")}. If this is wrong the next call will 400 \u2014 run /models to refresh.)`
@@ -12268,7 +12534,7 @@ var model = (args, loop, ctx) => {
12268
12534
  }
12269
12535
  return { info: `model \u2192 ${id}` };
12270
12536
  };
12271
- var models = (_args, loop, ctx) => {
12537
+ var models = (_args, loop2, ctx) => {
12272
12538
  const list = ctx.models ?? null;
12273
12539
  if (list === null) {
12274
12540
  ctx.refreshModels?.();
@@ -12281,7 +12547,7 @@ var models = (_args, loop, ctx) => {
12281
12547
  info: "DeepSeek /models returned an empty list. Try /models again, or check your account status at api-docs.deepseek.com."
12282
12548
  };
12283
12549
  }
12284
- const current = loop.model;
12550
+ const current = loop2.model;
12285
12551
  const lines = list.map((id) => id === current ? `\u25B8 ${id} (current)` : ` ${id}`);
12286
12552
  return {
12287
12553
  info: [
@@ -12293,18 +12559,18 @@ var models = (_args, loop, ctx) => {
12293
12559
  ].join("\n")
12294
12560
  };
12295
12561
  };
12296
- var harvest2 = (args, loop) => {
12562
+ var harvest2 = (args, loop2) => {
12297
12563
  const arg = (args[0] ?? "").toLowerCase();
12298
- const on = arg === "" ? !loop.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
12299
- loop.configure({ harvest: on });
12300
- if (loop.harvestEnabled) {
12564
+ const on = arg === "" ? !loop2.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
12565
+ loop2.configure({ harvest: on });
12566
+ if (loop2.harvestEnabled) {
12301
12567
  return {
12302
12568
  info: "harvest \u2192 on (Pillar-2 plan-state extraction \xB7 +1 cheap flash call per turn \xB7 opt-in only; no preset turns it on)"
12303
12569
  };
12304
12570
  }
12305
12571
  return { info: "harvest \u2192 off" };
12306
12572
  };
12307
- var preset = (args, loop) => {
12573
+ var preset = (args, loop2) => {
12308
12574
  const name = (args[0] ?? "").toLowerCase();
12309
12575
  const applyAndPersist = (effort2) => {
12310
12576
  try {
@@ -12313,7 +12579,7 @@ var preset = (args, loop) => {
12313
12579
  }
12314
12580
  };
12315
12581
  if (name === "fast" || name === "default") {
12316
- loop.configure({
12582
+ loop2.configure({
12317
12583
  model: "deepseek-v4-flash",
12318
12584
  reasoningEffort: "high",
12319
12585
  harvest: false,
@@ -12323,7 +12589,7 @@ var preset = (args, loop) => {
12323
12589
  return { info: "preset \u2192 fast (v4-flash \xB7 effort=high \xB7 cheapest)" };
12324
12590
  }
12325
12591
  if (name === "smart") {
12326
- loop.configure({
12592
+ loop2.configure({
12327
12593
  model: "deepseek-v4-flash",
12328
12594
  reasoningEffort: "max",
12329
12595
  harvest: false,
@@ -12333,7 +12599,7 @@ var preset = (args, loop) => {
12333
12599
  return { info: "preset \u2192 smart (v4-flash \xB7 effort=max \xB7 default \xB7 ~1.5\xD7 fast)" };
12334
12600
  }
12335
12601
  if (name === "max" || name === "best") {
12336
- loop.configure({
12602
+ loop2.configure({
12337
12603
  model: "deepseek-v4-pro",
12338
12604
  reasoningEffort: "max",
12339
12605
  harvest: false,
@@ -12346,10 +12612,10 @@ var preset = (args, loop) => {
12346
12612
  }
12347
12613
  return { info: "usage: /preset <fast|smart|max>" };
12348
12614
  };
12349
- var branch = (args, loop) => {
12615
+ var branch = (args, loop2) => {
12350
12616
  const raw = (args[0] ?? "").toLowerCase();
12351
12617
  if (raw === "" || raw === "off" || raw === "0" || raw === "1") {
12352
- loop.configure({ branch: 1 });
12618
+ loop2.configure({ branch: 1 });
12353
12619
  return { info: "branch \u2192 off" };
12354
12620
  }
12355
12621
  const n = Number.parseInt(raw, 10);
@@ -12359,36 +12625,36 @@ var branch = (args, loop) => {
12359
12625
  if (n > 8) {
12360
12626
  return { info: "branch budget capped at 8 to prevent runaway cost" };
12361
12627
  }
12362
- loop.configure({ branch: n });
12628
+ loop2.configure({ branch: n });
12363
12629
  return {
12364
12630
  info: `branch \u2192 ${n} (runs ${n} parallel samples per turn \xB7 ${n}\xD7 per-turn cost \xB7 streaming disabled \xB7 manual only, no preset enables branching)`
12365
12631
  };
12366
12632
  };
12367
- var effort = (args, loop) => {
12633
+ var effort = (args, loop2) => {
12368
12634
  const raw = (args[0] ?? "").toLowerCase();
12369
12635
  if (raw === "") {
12370
12636
  return {
12371
- info: `reasoning_effort \u2192 ${loop.reasoningEffort} (use /effort high for cheaper/faster, /effort max for the agent-class default \xB7 persisted across relaunches)`
12637
+ info: `reasoning_effort \u2192 ${loop2.reasoningEffort} (use /effort high for cheaper/faster, /effort max for the agent-class default \xB7 persisted across relaunches)`
12372
12638
  };
12373
12639
  }
12374
12640
  if (raw !== "high" && raw !== "max") {
12375
12641
  return { info: "usage: /effort <high|max>" };
12376
12642
  }
12377
- loop.configure({ reasoningEffort: raw });
12643
+ loop2.configure({ reasoningEffort: raw });
12378
12644
  try {
12379
12645
  saveReasoningEffort(raw);
12380
12646
  } catch {
12381
12647
  }
12382
12648
  return { info: `reasoning_effort \u2192 ${raw} (persisted)` };
12383
12649
  };
12384
- var pro = (args, loop, ctx) => {
12650
+ var pro = (args, loop2, ctx) => {
12385
12651
  const arg = (args[0] ?? "").toLowerCase();
12386
12652
  if (arg === "off" || arg === "cancel" || arg === "disarm") {
12387
- if (!loop.proArmed) {
12653
+ if (!loop2.proArmed) {
12388
12654
  return { info: "nothing armed \u2014 /pro with no args will arm pro for your next turn" };
12389
12655
  }
12390
12656
  if (ctx.disarmPro) ctx.disarmPro();
12391
- else loop.disarmPro();
12657
+ else loop2.disarmPro();
12392
12658
  return { info: "\u25B8 /pro disarmed \u2014 next turn falls back to the current preset" };
12393
12659
  }
12394
12660
  if (arg && arg !== "on" && arg !== "arm") {
@@ -12397,7 +12663,7 @@ var pro = (args, loop, ctx) => {
12397
12663
  };
12398
12664
  }
12399
12665
  if (ctx.armPro) ctx.armPro();
12400
- else loop.armProForNextTurn();
12666
+ else loop2.armProForNextTurn();
12401
12667
  return {
12402
12668
  info: `\u25B8 /pro armed \u2014 your NEXT message runs on ${ESCALATION_MODEL_ID} regardless of preset. Auto-disarms after one turn. Use /preset max for a persistent switch.`
12403
12669
  };
@@ -12414,8 +12680,8 @@ var handlers7 = {
12414
12680
  };
12415
12681
 
12416
12682
  // src/cli/ui/slash/handlers/observability.ts
12417
- var think = (_args, loop) => {
12418
- const raw = loop.scratch.reasoning;
12683
+ var think = (_args, loop2) => {
12684
+ const raw = loop2.scratch.reasoning;
12419
12685
  if (!raw || !raw.trim()) {
12420
12686
  return {
12421
12687
  info: "no reasoning cached. `/think` shows the full thinking-mode thought for the most recent turn \u2014 only thinking-mode models (deepseek-v4-flash / -v4-pro / -reasoner) produce it, and only once the turn completes."
@@ -12457,10 +12723,10 @@ var tool = (args, _loop, ctx) => {
12457
12723
  ${entry.text}`
12458
12724
  };
12459
12725
  };
12460
- var context = (_args, loop) => {
12461
- const systemTokens = countTokens(loop.prefix.system);
12462
- const toolsTokens = countTokens(JSON.stringify(loop.prefix.toolSpecs));
12463
- const entries = loop.log.toMessages();
12726
+ var context = (_args, loop2) => {
12727
+ const systemTokens = countTokens(loop2.prefix.system);
12728
+ const toolsTokens = countTokens(JSON.stringify(loop2.prefix.toolSpecs));
12729
+ const entries = loop2.log.toMessages();
12464
12730
  let userTokens = 0;
12465
12731
  let assistantTokens = 0;
12466
12732
  let toolResultTokens = 0;
@@ -12485,7 +12751,7 @@ var context = (_args, loop) => {
12485
12751
  }
12486
12752
  const logTokens = userTokens + assistantTokens + toolResultTokens + toolCallTokens;
12487
12753
  const total = systemTokens + toolsTokens + logTokens;
12488
- const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
12754
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop2.model] ?? DEFAULT_CONTEXT_TOKENS;
12489
12755
  const pct2 = (n) => total > 0 ? `${Math.round(n / total * 100)}%`.padStart(4) : " 0%";
12490
12756
  const row2 = (label, n, note = "") => ` ${label.padEnd(20)}${compactNum(n).padStart(8)} tokens ${pct2(n)}${note ? ` ${note}` : ""}`;
12491
12757
  const lines = [
@@ -12494,7 +12760,7 @@ var context = (_args, loop) => {
12494
12760
  )}% of window)`,
12495
12761
  "",
12496
12762
  row2("system prompt", systemTokens),
12497
- row2("tool specs", toolsTokens, `(${loop.prefix.toolSpecs.length} tools)`),
12763
+ row2("tool specs", toolsTokens, `(${loop2.prefix.toolSpecs.length} tools)`),
12498
12764
  row2("log (all turns)", logTokens, `(${entries.length} messages)`),
12499
12765
  ` user ${compactNum(userTokens).padStart(8)} tokens`,
12500
12766
  ` assistant ${compactNum(assistantTokens).padStart(8)} tokens`,
@@ -12505,9 +12771,9 @@ var context = (_args, loop) => {
12505
12771
  const top = [...toolBreakdown].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
12506
12772
  lines.push("");
12507
12773
  lines.push(`Top tool results by cost (of ${toolBreakdown.length} total):`);
12508
- for (const t of top) {
12774
+ for (const t2 of top) {
12509
12775
  lines.push(
12510
- ` turn ${String(t.turn).padStart(3)} ${t.name.padEnd(22)} ${compactNum(t.tokens).padStart(8)} tokens`
12776
+ ` turn ${String(t2.turn).padStart(3)} ${t2.name.padEnd(22)} ${compactNum(t2.tokens).padStart(8)} tokens`
12511
12777
  );
12512
12778
  }
12513
12779
  }
@@ -12517,23 +12783,23 @@ var context = (_args, loop) => {
12517
12783
  );
12518
12784
  return { info: lines.join("\n") };
12519
12785
  };
12520
- var status = (_args, loop, ctx) => {
12521
- const branchBudget = loop.branchOptions.budget ?? 1;
12522
- const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop.model] ?? DEFAULT_CONTEXT_TOKENS;
12523
- const lastPromptTokens = loop.stats.summary().lastPromptTokens;
12786
+ var status = (_args, loop2, ctx) => {
12787
+ const branchBudget = loop2.branchOptions.budget ?? 1;
12788
+ const ctxMax = DEEPSEEK_CONTEXT_TOKENS[loop2.model] ?? DEFAULT_CONTEXT_TOKENS;
12789
+ const lastPromptTokens = loop2.stats.summary().lastPromptTokens;
12524
12790
  const ctxPct = ctxMax > 0 ? Math.round(lastPromptTokens / ctxMax * 100) : 0;
12525
12791
  const ctxLine = lastPromptTokens > 0 ? ` ctx ${compactNum(lastPromptTokens)}/${compactNum(ctxMax)} (${ctxPct}%)` : " ctx no turns yet";
12526
12792
  const pending = ctx.pendingEditCount ?? 0;
12527
- const sessionLine = loop.sessionName ? ` session "${loop.sessionName}" \xB7 ${loop.log.length} messages in log (resumed ${loop.resumedMessageCount})` : " session (ephemeral \u2014 no persistence)";
12793
+ const sessionLine = loop2.sessionName ? ` session "${loop2.sessionName}" \xB7 ${loop2.log.length} messages in log (resumed ${loop2.resumedMessageCount})` : " session (ephemeral \u2014 no persistence)";
12528
12794
  const mcpCount = ctx.mcpSpecs?.length ?? 0;
12529
- const toolCount = loop.prefix.toolSpecs.length;
12795
+ const toolCount = loop2.prefix.toolSpecs.length;
12530
12796
  const mcpLine = ` mcp ${mcpCount} server(s), ${toolCount} tool(s) in registry`;
12531
12797
  const pendingLine = pending > 0 ? ` edits ${pending} pending (/apply to commit, /discard to drop)` : "";
12532
12798
  const planLine = ctx.planMode ? " plan ON \u2014 writes gated (submit_plan + approval)" : "";
12533
- const modeLine = ctx.editMode === "auto" ? " mode AUTO \u2014 edits apply immediately (u to undo within 5s \xB7 Shift+Tab to flip)" : ctx.editMode === "review" ? " mode review \u2014 edits queue for /apply or y (Shift+Tab to flip)" : "";
12799
+ const modeLine = ctx.editMode === "yolo" ? " mode YOLO \u2014 edits + shell auto-run with no prompt (/undo still rolls back \xB7 Shift+Tab to flip)" : ctx.editMode === "auto" ? " mode AUTO \u2014 edits apply immediately (u to undo within 5s \xB7 Shift+Tab to flip)" : ctx.editMode === "review" ? " mode review \u2014 edits queue for /apply or y (Shift+Tab to flip)" : "";
12534
12800
  const lines = [
12535
- ` model ${loop.model}`,
12536
- ` flags harvest=${loop.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop.stream ? "on" : "off"} \xB7 effort=${loop.reasoningEffort}`,
12801
+ ` model ${loop2.model}`,
12802
+ ` flags harvest=${loop2.harvestEnabled ? "on" : "off"} \xB7 branch=${branchBudget > 1 ? branchBudget : "off"} \xB7 stream=${loop2.stream ? "on" : "off"} \xB7 effort=${loop2.reasoningEffort}`,
12537
12803
  ctxLine,
12538
12804
  mcpLine,
12539
12805
  sessionLine
@@ -12543,10 +12809,10 @@ var status = (_args, loop, ctx) => {
12543
12809
  if (modeLine) lines.push(modeLine);
12544
12810
  return { info: lines.join("\n") };
12545
12811
  };
12546
- var compact = (args, loop) => {
12812
+ var compact = (args, loop2) => {
12547
12813
  const tight = Number.parseInt(args[0] ?? "", 10);
12548
12814
  const cap = Number.isFinite(tight) && tight >= 100 ? tight : 4e3;
12549
- const { healedCount, tokensSaved, charsSaved } = loop.compact(cap);
12815
+ const { healedCount, tokensSaved, charsSaved } = loop2.compact(cap);
12550
12816
  if (healedCount === 0) {
12551
12817
  return {
12552
12818
  info: `\u25B8 nothing to compact \u2014 no tool result or tool-call args in history exceed ${cap.toLocaleString()} tokens.`
@@ -12567,8 +12833,8 @@ var handlers8 = {
12567
12833
 
12568
12834
  // src/cli/ui/slash/handlers/plans.ts
12569
12835
  import { basename } from "path";
12570
- var plans = (_args, loop) => {
12571
- const sessionName = loop.sessionName;
12836
+ var plans = (_args, loop2) => {
12837
+ const sessionName = loop2.sessionName;
12572
12838
  if (!sessionName) {
12573
12839
  return {
12574
12840
  info: "no session attached \u2014 `/plans` is per-session. Run `reasonix code` in a project to get a session."
@@ -12609,8 +12875,8 @@ var plans = (_args, loop) => {
12609
12875
  }
12610
12876
  return { info: lines.join("\n") };
12611
12877
  };
12612
- var replay = (args, loop) => {
12613
- const sessionName = loop.sessionName;
12878
+ var replay = (args, loop2) => {
12879
+ const sessionName = loop2.sessionName;
12614
12880
  if (!sessionName) {
12615
12881
  return {
12616
12882
  info: "no session attached \u2014 `/replay` is per-session. Run `reasonix code` in a project to get a session."
@@ -12649,8 +12915,380 @@ var handlers9 = {
12649
12915
  replay
12650
12916
  };
12651
12917
 
12918
+ // src/cli/ui/slash/handlers/semantic.ts
12919
+ import { promises as fs2 } from "fs";
12920
+ import path from "path";
12921
+
12922
+ // src/index/semantic/embedding.ts
12923
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
12924
+ var DEFAULT_EMBED_MODEL = "nomic-embed-text";
12925
+ var DEFAULT_TIMEOUT_MS = 3e4;
12926
+ var EmbeddingError = class extends Error {
12927
+ constructor(message, cause) {
12928
+ super(message);
12929
+ this.cause = cause;
12930
+ this.name = "EmbeddingError";
12931
+ }
12932
+ cause;
12933
+ };
12934
+ async function embed(text, opts = {}) {
12935
+ const baseUrl = opts.baseUrl ?? process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
12936
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? DEFAULT_EMBED_MODEL;
12937
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12938
+ const controller = new AbortController();
12939
+ const onCallerAbort = () => controller.abort(opts.signal?.reason);
12940
+ if (opts.signal) {
12941
+ if (opts.signal.aborted) controller.abort(opts.signal.reason);
12942
+ else opts.signal.addEventListener("abort", onCallerAbort, { once: true });
12943
+ }
12944
+ const timer = setTimeout(() => controller.abort(new Error("embedding timeout")), timeoutMs);
12945
+ let res;
12946
+ try {
12947
+ res = await fetch(`${baseUrl}/api/embeddings`, {
12948
+ method: "POST",
12949
+ headers: { "content-type": "application/json" },
12950
+ body: JSON.stringify({ model: model2, prompt: text }),
12951
+ signal: controller.signal
12952
+ });
12953
+ } catch (err) {
12954
+ clearTimeout(timer);
12955
+ if (opts.signal) opts.signal.removeEventListener("abort", onCallerAbort);
12956
+ const msg = err instanceof Error ? err.message : String(err);
12957
+ if (/ECONNREFUSED|connect ECONNREFUSED|fetch failed/i.test(msg)) {
12958
+ throw new EmbeddingError(
12959
+ `Cannot reach Ollama at ${baseUrl}. Install from https://ollama.com, then run \`ollama pull ${model2}\` and \`ollama serve\`. Override the URL via OLLAMA_URL.`,
12960
+ err
12961
+ );
12962
+ }
12963
+ throw new EmbeddingError(`embedding request failed: ${msg}`, err);
12964
+ } finally {
12965
+ clearTimeout(timer);
12966
+ if (opts.signal) opts.signal.removeEventListener("abort", onCallerAbort);
12967
+ }
12968
+ if (!res.ok) {
12969
+ const body = await res.text().catch(() => "");
12970
+ if (res.status === 404 && /model.*not found/i.test(body)) {
12971
+ throw new EmbeddingError(
12972
+ `Embedding model "${model2}" not pulled. Run \`ollama pull ${model2}\` once, then retry.`
12973
+ );
12974
+ }
12975
+ throw new EmbeddingError(`Ollama returned ${res.status}: ${body.slice(0, 200)}`);
12976
+ }
12977
+ const json = await res.json();
12978
+ if (!json.embedding || !Array.isArray(json.embedding)) {
12979
+ throw new EmbeddingError(`Ollama response missing 'embedding' array`);
12980
+ }
12981
+ const out = new Float32Array(json.embedding.length);
12982
+ for (let i = 0; i < json.embedding.length; i++) {
12983
+ const v = json.embedding[i];
12984
+ if (typeof v !== "number" || !Number.isFinite(v)) {
12985
+ throw new EmbeddingError(`embedding[${i}] is not a finite number`);
12986
+ }
12987
+ out[i] = v;
12988
+ }
12989
+ return out;
12990
+ }
12991
+ async function embedAll(texts, opts = {}) {
12992
+ const out = new Array(texts.length).fill(null);
12993
+ for (let i = 0; i < texts.length; i++) {
12994
+ if (opts.signal?.aborted) {
12995
+ throw new EmbeddingError("embedding aborted");
12996
+ }
12997
+ const text = texts[i];
12998
+ if (text === void 0) continue;
12999
+ try {
13000
+ out[i] = await embed(text, opts);
13001
+ } catch (err) {
13002
+ opts.onError?.(i, err);
13003
+ }
13004
+ opts.onProgress?.(i + 1, texts.length);
13005
+ }
13006
+ return out;
13007
+ }
13008
+ async function probeOllama(opts = {}) {
13009
+ const baseUrl = opts.baseUrl ?? process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
13010
+ try {
13011
+ const res = await fetch(`${baseUrl}/api/tags`, { signal: opts.signal });
13012
+ if (!res.ok) return { ok: false, error: `Ollama returned ${res.status}` };
13013
+ const json = await res.json();
13014
+ const models2 = (json.models ?? []).map((m) => m.name).filter((n) => typeof n === "string");
13015
+ return { ok: true, models: models2 };
13016
+ } catch (err) {
13017
+ const msg = err instanceof Error ? err.message : String(err);
13018
+ return { ok: false, error: msg };
13019
+ }
13020
+ }
13021
+
13022
+ // src/index/semantic/i18n.ts
13023
+ var cachedLocale = null;
13024
+ function detectLocale() {
13025
+ if (cachedLocale) return cachedLocale;
13026
+ const override = (process.env.REASONIX_LANG ?? "").toLowerCase();
13027
+ if (override === "zh" || override === "en") {
13028
+ cachedLocale = override;
13029
+ return cachedLocale;
13030
+ }
13031
+ const env = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? "";
13032
+ if (/^zh[-_]/i.test(env)) {
13033
+ cachedLocale = "zh";
13034
+ return "zh";
13035
+ }
13036
+ try {
13037
+ const sys = new Intl.DateTimeFormat().resolvedOptions().locale ?? "";
13038
+ if (/^zh[-_]/i.test(sys)) {
13039
+ cachedLocale = "zh";
13040
+ return "zh";
13041
+ }
13042
+ } catch {
13043
+ }
13044
+ cachedLocale = "en";
13045
+ return "en";
13046
+ }
13047
+ function t(key, vars = {}) {
13048
+ const loc = detectLocale();
13049
+ const dict = loc === "zh" ? ZH : EN;
13050
+ const tpl = dict[key] ?? EN[key];
13051
+ return tpl.replace(/\{(\w+)\}/g, (_m, name) => {
13052
+ const v = vars[name];
13053
+ return v === void 0 ? `{${name}}` : String(v);
13054
+ });
13055
+ }
13056
+ var EN = {
13057
+ // ── preflight ─────────────────────────────────────────────────────
13058
+ ollamaNotFound: "\u2717 `ollama` not found on PATH.\n Install from https://ollama.com (one-time, ~150 MB), then retry.\n",
13059
+ daemonNotReachableHint: "\u2717 Ollama daemon not reachable. Run `ollama serve` and retry, or pass --yes to start it automatically.\n",
13060
+ daemonStartConfirm: "Ollama daemon isn't running. Start `ollama serve` now?",
13061
+ daemonAbortStart: "\u2717 aborted \u2014 start `ollama serve` yourself and retry.\n",
13062
+ daemonStarting: "\u25B8 starting `ollama serve`\u2026\n",
13063
+ daemonStartTimeout: "\u2717 daemon didn't come up within 15s. Try `ollama serve` in a separate terminal and retry.\n",
13064
+ daemonReady: "\u2713 daemon up{pid}\n",
13065
+ modelNotPulledHint: '\u2717 embedding model "{model}" not pulled. Run `ollama pull {model}` and retry, or pass --yes to pull it automatically.\n',
13066
+ modelPullConfirm: `Embedding model "{model}" isn't pulled yet. Pull it now? (~274 MB for nomic-embed-text)`,
13067
+ modelAbortPull: "\u2717 aborted \u2014 pull the model yourself and retry.\n",
13068
+ modelPulling: "\u25B8 pulling {model}\u2026\n",
13069
+ modelPullFailed: "\u2717 `ollama pull {model}` failed (exit {code}).\n",
13070
+ modelPulled: "\u2713 {model} pulled\n",
13071
+ // ── progress ─────────────────────────────────────────────────────
13072
+ // The TTY-mode progress writer paints `<spinner> <status> <elapsed>s`
13073
+ // every 120ms. The status itself comes from one of these keys based
13074
+ // on the current phase. {files}, {done}, {total}, {pct} are
13075
+ // substituted by the writer.
13076
+ progressStarting: "starting\u2026",
13077
+ progressScan: "scanning project \xB7 {files} files",
13078
+ progressEmbed: "embedding {done}/{total} chunks \xB7 {pct}%",
13079
+ progressEmbedHeartbeat: " {done}/{total}\n",
13080
+ progressScanLine: "scanning files\u2026\n",
13081
+ progressEmbedLine: "embedding {total} chunks across {files} files\u2026\n",
13082
+ // Final result line after a successful build.
13083
+ indexSuccess: "\u2713 indexed {scanned} files ({changed} changed, {added} new chunks, {removed} stale removed) in {seconds}s\n",
13084
+ indexSuccessWithSkips: "\u2713 indexed {scanned} files ({changed} changed, {added} new chunks, {removed} stale removed, {skipped} skipped due to embed errors) in {seconds}s\n",
13085
+ indexNothingToDo: " (nothing to do \u2014 re-run with --rebuild to force a full rebuild)\n",
13086
+ indexFailed: "\u2717 index failed: {msg}\n",
13087
+ // ── /semantic slash ──────────────────────────────────────────────
13088
+ slashHeader: "semantic_search status",
13089
+ slashEnabled: "\u2713 enabled \u2014 index built, tool registered.",
13090
+ slashEnabledDetail: " index size: {chunks} chunks across {files} files",
13091
+ slashEnabledHowto: " the model will call semantic_search automatically when it fits.",
13092
+ slashIndexMissing: "\u2717 no index built yet for this project.",
13093
+ slashHowToBuild: " to enable, exit Reasonix and run in your shell:\n reasonix index",
13094
+ slashOllamaMissing: " prerequisite: install Ollama from https://ollama.com",
13095
+ slashDaemonDown: " Ollama is installed but the daemon isn't running. start it with: ollama serve",
13096
+ slashIndexInfo: " what semantic_search does: cross-language code understanding via local embeddings.\n better than grep when you describe WHAT something does, not WHICH token to find."
13097
+ };
13098
+ var ZH = {
13099
+ ollamaNotFound: "\u2717 \u672A\u627E\u5230 `ollama`\u3002\n \u8BF7\u8BBF\u95EE https://ollama.com \u5B89\u88C5\uFF08\u4E00\u6B21\u6027\uFF0C\u7EA6 150 MB\uFF09\uFF0C\u7136\u540E\u91CD\u8BD5\u3002\n",
13100
+ daemonNotReachableHint: "\u2717 Ollama \u5B88\u62A4\u8FDB\u7A0B\u672A\u542F\u52A8\u3002\u8BF7\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\uFF0C\u6216\u52A0 --yes \u8BA9\u6211\u81EA\u52A8\u542F\u52A8\u3002\n",
13101
+ daemonStartConfirm: "Ollama \u5B88\u62A4\u8FDB\u7A0B\u672A\u8FD0\u884C\u3002\u73B0\u5728\u542F\u52A8 `ollama serve` \u5417\uFF1F",
13102
+ daemonAbortStart: "\u2717 \u5DF2\u53D6\u6D88\u2014\u2014\u8BF7\u81EA\u884C\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\u3002\n",
13103
+ daemonStarting: "\u25B8 \u6B63\u5728\u542F\u52A8 `ollama serve`\u2026\n",
13104
+ daemonStartTimeout: "\u2717 15 \u79D2\u5185\u5B88\u62A4\u8FDB\u7A0B\u672A\u5C31\u7EEA\u3002\u8BF7\u5728\u53E6\u4E00\u4E2A\u7EC8\u7AEF\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\u3002\n",
13105
+ daemonReady: "\u2713 \u5B88\u62A4\u8FDB\u7A0B\u5DF2\u542F\u52A8{pid}\n",
13106
+ modelNotPulledHint: '\u2717 \u5D4C\u5165\u6A21\u578B "{model}" \u672A\u4E0B\u8F7D\u3002\u8BF7\u8FD0\u884C `ollama pull {model}` \u540E\u91CD\u8BD5\uFF0C\u6216\u52A0 --yes \u8BA9\u6211\u81EA\u52A8\u4E0B\u8F7D\u3002\n',
13107
+ modelPullConfirm: '\u5D4C\u5165\u6A21\u578B "{model}" \u8FD8\u672A\u4E0B\u8F7D\u3002\u73B0\u5728\u4E0B\u8F7D\u5417\uFF1F\uFF08nomic-embed-text \u7EA6 274 MB\uFF09',
13108
+ modelAbortPull: "\u2717 \u5DF2\u53D6\u6D88\u2014\u2014\u8BF7\u81EA\u884C\u4E0B\u8F7D\u6A21\u578B\u540E\u91CD\u8BD5\u3002\n",
13109
+ modelPulling: "\u25B8 \u6B63\u5728\u4E0B\u8F7D {model}\u2026\n",
13110
+ modelPullFailed: "\u2717 `ollama pull {model}` \u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\n",
13111
+ modelPulled: "\u2713 {model} \u4E0B\u8F7D\u5B8C\u6210\n",
13112
+ progressStarting: "\u6B63\u5728\u542F\u52A8\u2026",
13113
+ progressScan: "\u626B\u63CF\u9879\u76EE \xB7 \u5DF2\u626B\u63CF {files} \u4E2A\u6587\u4EF6",
13114
+ progressEmbed: "\u6B63\u5728\u5411\u91CF\u5316 {done}/{total} \u4E2A\u7247\u6BB5 \xB7 {pct}%",
13115
+ progressEmbedHeartbeat: " {done}/{total}\n",
13116
+ progressScanLine: "\u6B63\u5728\u626B\u63CF\u6587\u4EF6\u2026\n",
13117
+ progressEmbedLine: "\u6B63\u5728\u5411\u91CF\u5316 {total} \u4E2A\u7247\u6BB5\uFF08\u6D89\u53CA {files} \u4E2A\u6587\u4EF6\uFF09\u2026\n",
13118
+ indexSuccess: "\u2713 \u5DF2\u5EFA\u7ACB\u7D22\u5F15\uFF1A\u626B\u63CF {scanned} \u4E2A\u6587\u4EF6\uFF08{changed} \u4E2A\u6709\u53D8\u5316\uFF0C\u65B0\u589E {added} \u4E2A\u7247\u6BB5\uFF0C\u79FB\u9664 {removed} \u4E2A\u8FC7\u671F\uFF09\uFF1B\u8017\u65F6 {seconds}s\n",
13119
+ indexSuccessWithSkips: "\u2713 \u5DF2\u5EFA\u7ACB\u7D22\u5F15\uFF1A\u626B\u63CF {scanned} \u4E2A\u6587\u4EF6\uFF08{changed} \u4E2A\u6709\u53D8\u5316\uFF0C\u65B0\u589E {added} \u4E2A\u7247\u6BB5\uFF0C\u79FB\u9664 {removed} \u4E2A\u8FC7\u671F\uFF0C\u8DF3\u8FC7 {skipped} \u4E2A\u5D4C\u5165\u5931\u8D25\u7684\u7247\u6BB5\uFF09\uFF1B\u8017\u65F6 {seconds}s\n",
13120
+ indexNothingToDo: " \uFF08\u6CA1\u6709\u53D8\u5316\u2014\u2014\u52A0 --rebuild \u5F3A\u5236\u91CD\u5EFA\uFF09\n",
13121
+ indexFailed: "\u2717 \u5EFA\u7ACB\u7D22\u5F15\u5931\u8D25\uFF1A{msg}\n",
13122
+ slashHeader: "semantic_search \u72B6\u6001",
13123
+ slashEnabled: "\u2713 \u5DF2\u542F\u7528\u2014\u2014\u7D22\u5F15\u5DF2\u5EFA\u597D\uFF0C\u5DE5\u5177\u5DF2\u6CE8\u518C\u3002",
13124
+ slashEnabledDetail: " \u7D22\u5F15\u89C4\u6A21\uFF1A{chunks} \u4E2A\u7247\u6BB5\uFF0C{files} \u4E2A\u6587\u4EF6",
13125
+ slashEnabledHowto: " \u6A21\u578B\u5728\u5408\u9002\u7684\u65F6\u5019\u4F1A\u81EA\u52A8\u8C03\u7528 semantic_search\u3002",
13126
+ slashIndexMissing: "\u2717 \u5F53\u524D\u9879\u76EE\u8FD8\u6CA1\u6709\u7D22\u5F15\u3002",
13127
+ slashHowToBuild: " \u542F\u7528\u65B9\u5F0F\uFF1A\u9000\u51FA Reasonix\uFF0C\u5728\u7EC8\u7AEF\u8FD0\u884C\uFF1A\n reasonix index",
13128
+ slashOllamaMissing: " \u524D\u7F6E\u4F9D\u8D56\uFF1A\u4ECE https://ollama.com \u5B89\u88C5 Ollama",
13129
+ slashDaemonDown: " \u5DF2\u88C5 Ollama \u4F46\u5B88\u62A4\u8FDB\u7A0B\u672A\u542F\u52A8\uFF0C\u8BF7\u8FD0\u884C\uFF1Aollama serve",
13130
+ slashIndexInfo: ' semantic_search \u7528\u672C\u5730 embedding \u505A\u8DE8\u8BED\u8A00\u4EE3\u7801\u7406\u89E3\u3002\n \u5F53\u4F60\u63CF\u8FF0"\u505A\u4EC0\u4E48"\u800C\u4E0D\u662F\u5177\u4F53 token \u65F6\uFF0C\u6BD4 grep \u66F4\u597D\u3002'
13131
+ };
13132
+
13133
+ // src/index/semantic/ollama-launcher.ts
13134
+ import { spawn as spawn5, spawnSync as spawnSync2 } from "child_process";
13135
+ import { setTimeout as sleep2 } from "timers/promises";
13136
+ function findOllamaBinary() {
13137
+ const cmd = process.platform === "win32" ? "where" : "which";
13138
+ const out = spawnSync2(cmd, ["ollama"], { encoding: "utf8" });
13139
+ if (out.status !== 0) return null;
13140
+ const first = out.stdout.split(/\r?\n/).find((l) => l.trim().length > 0);
13141
+ return first ? first.trim() : null;
13142
+ }
13143
+ async function checkOllamaStatus(modelName, baseUrl) {
13144
+ const binary = findOllamaBinary();
13145
+ const probe = await probeOllama({ baseUrl });
13146
+ const installedModels = probe.ok ? probe.models : [];
13147
+ const wanted = modelName.includes(":") ? modelName : `${modelName}:latest`;
13148
+ const modelPulled = installedModels.some((m) => m === modelName || m === wanted);
13149
+ return {
13150
+ binaryFound: binary !== null,
13151
+ daemonRunning: probe.ok,
13152
+ modelPulled,
13153
+ modelName,
13154
+ installedModels
13155
+ };
13156
+ }
13157
+ async function startOllamaDaemon(opts = {}) {
13158
+ const timeoutMs = opts.timeoutMs ?? 15e3;
13159
+ const child = spawn5("ollama", ["serve"], {
13160
+ detached: true,
13161
+ stdio: "ignore",
13162
+ windowsHide: true
13163
+ });
13164
+ child.unref();
13165
+ const pid = child.pid ?? null;
13166
+ const start = Date.now();
13167
+ while (Date.now() - start < timeoutMs) {
13168
+ if (opts.signal?.aborted) return { ready: false, pid };
13169
+ const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });
13170
+ if (probe.ok) return { ready: true, pid };
13171
+ await sleep2(500);
13172
+ }
13173
+ return { ready: false, pid };
13174
+ }
13175
+ async function pullOllamaModel(modelName, opts = {}) {
13176
+ return new Promise((resolve9) => {
13177
+ const child = spawn5("ollama", ["pull", modelName], {
13178
+ stdio: ["ignore", "pipe", "pipe"],
13179
+ windowsHide: true
13180
+ });
13181
+ if (opts.signal) {
13182
+ const onAbort = () => child.kill();
13183
+ opts.signal.addEventListener("abort", onAbort, { once: true });
13184
+ child.once("exit", () => opts.signal?.removeEventListener("abort", onAbort));
13185
+ }
13186
+ streamLines(child.stdout, (l) => opts.onLine?.(l, "stdout"));
13187
+ streamLines(child.stderr, (l) => opts.onLine?.(l, "stderr"));
13188
+ child.once("exit", (code) => resolve9(code ?? -1));
13189
+ child.once("error", () => resolve9(-1));
13190
+ });
13191
+ }
13192
+ function streamLines(stream, cb) {
13193
+ if (!stream) return;
13194
+ let buf = "";
13195
+ stream.setEncoding("utf8");
13196
+ stream.on("data", (chunk) => {
13197
+ buf += chunk;
13198
+ let nl = buf.indexOf("\n");
13199
+ while (nl !== -1) {
13200
+ const line = buf.slice(0, nl).replace(/\r$/, "");
13201
+ buf = buf.slice(nl + 1);
13202
+ if (line.length > 0) cb(line);
13203
+ nl = buf.indexOf("\n");
13204
+ }
13205
+ });
13206
+ stream.on("end", () => {
13207
+ if (buf.length > 0) cb(buf.replace(/\r$/, ""));
13208
+ });
13209
+ }
13210
+
13211
+ // src/cli/ui/slash/handlers/semantic.ts
13212
+ var semantic = (_args, _loop, ctx) => {
13213
+ const root = ctx.codeRoot;
13214
+ if (!root) {
13215
+ return {
13216
+ info: "/semantic is only available inside `reasonix code` (needs a project root)."
13217
+ };
13218
+ }
13219
+ void (async () => {
13220
+ const status2 = await renderSemanticStatus(root);
13221
+ ctx.postInfo?.(status2);
13222
+ })();
13223
+ return { info: "\u25B8 checking semantic_search status\u2026" };
13224
+ };
13225
+ async function renderSemanticStatus(rootDir) {
13226
+ const lines = [t("slashHeader"), ""];
13227
+ const indexExists2 = await indexFileExists(rootDir);
13228
+ if (indexExists2) {
13229
+ const meta = await readIndexMeta(rootDir);
13230
+ lines.push(t("slashEnabled"));
13231
+ if (meta) {
13232
+ lines.push(
13233
+ t("slashEnabledDetail", {
13234
+ chunks: meta.chunks,
13235
+ files: meta.files
13236
+ })
13237
+ );
13238
+ }
13239
+ lines.push(t("slashEnabledHowto"));
13240
+ return lines.join("\n");
13241
+ }
13242
+ lines.push(t("slashIndexMissing"));
13243
+ lines.push(t("slashIndexInfo"));
13244
+ lines.push("");
13245
+ if (findOllamaBinary() === null) {
13246
+ lines.push(t("slashOllamaMissing"));
13247
+ } else {
13248
+ const probe = await probeOllama();
13249
+ if (!probe.ok) lines.push(t("slashDaemonDown"));
13250
+ }
13251
+ lines.push(t("slashHowToBuild"));
13252
+ return lines.join("\n");
13253
+ }
13254
+ async function indexFileExists(rootDir) {
13255
+ try {
13256
+ await fs2.access(path.join(rootDir, ".reasonix", "semantic", "index.meta.json"));
13257
+ return true;
13258
+ } catch {
13259
+ return false;
13260
+ }
13261
+ }
13262
+ async function readIndexMeta(rootDir) {
13263
+ const dataPath = path.join(rootDir, ".reasonix", "semantic", "index.jsonl");
13264
+ try {
13265
+ const stat = await fs2.stat(dataPath);
13266
+ if (stat.size > 10 * 1024 * 1024) {
13267
+ return { chunks: Math.round(stat.size / 500), files: 0 };
13268
+ }
13269
+ const raw = await fs2.readFile(dataPath, "utf8");
13270
+ const seenPaths = /* @__PURE__ */ new Set();
13271
+ let chunks = 0;
13272
+ for (const line of raw.split("\n")) {
13273
+ if (line.length === 0) continue;
13274
+ chunks++;
13275
+ try {
13276
+ const parsed = JSON.parse(line);
13277
+ if (parsed.p) seenPaths.add(parsed.p);
13278
+ } catch {
13279
+ }
13280
+ }
13281
+ return { chunks, files: seenPaths.size };
13282
+ } catch {
13283
+ return null;
13284
+ }
13285
+ }
13286
+ var handlers10 = {
13287
+ semantic
13288
+ };
13289
+
12652
13290
  // src/cli/ui/slash/handlers/sessions.ts
12653
- var sessions = (_args, loop) => {
13291
+ var sessions = (_args, loop2) => {
12654
13292
  const items = listSessions();
12655
13293
  if (items.length === 0) {
12656
13294
  return {
@@ -12661,7 +13299,7 @@ var sessions = (_args, loop) => {
12661
13299
  for (const s of items) {
12662
13300
  const sizeKb = (s.size / 1024).toFixed(1);
12663
13301
  const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
12664
- const marker = s.name === loop.sessionName ? "\u25B8" : " ";
13302
+ const marker = s.name === loop2.sessionName ? "\u25B8" : " ";
12665
13303
  lines.push(
12666
13304
  ` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}`
12667
13305
  );
@@ -12670,17 +13308,17 @@ var sessions = (_args, loop) => {
12670
13308
  lines.push("Resume with: reasonix chat --session <name>");
12671
13309
  return { info: lines.join("\n") };
12672
13310
  };
12673
- var forget = (_args, loop) => {
12674
- if (!loop.sessionName) {
13311
+ var forget = (_args, loop2) => {
13312
+ if (!loop2.sessionName) {
12675
13313
  return { info: "not in a session \u2014 nothing to forget" };
12676
13314
  }
12677
- const name = loop.sessionName;
13315
+ const name = loop2.sessionName;
12678
13316
  const ok = deleteSession(name);
12679
13317
  return {
12680
13318
  info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
12681
13319
  };
12682
13320
  };
12683
- var handlers10 = {
13321
+ var handlers11 = {
12684
13322
  sessions,
12685
13323
  forget
12686
13324
  };
@@ -12756,7 +13394,7 @@ ${found.body}${argsLine}`;
12756
13394
  resubmit: payload
12757
13395
  };
12758
13396
  };
12759
- var handlers11 = {
13397
+ var handlers12 = {
12760
13398
  skill,
12761
13399
  skills: skill
12762
13400
  };
@@ -12773,11 +13411,12 @@ var HANDLERS = {
12773
13411
  ...handlers8,
12774
13412
  ...handlers9,
12775
13413
  ...handlers10,
12776
- ...handlers11
13414
+ ...handlers11,
13415
+ ...handlers12
12777
13416
  };
12778
- function handleSlash(cmd, args, loop, ctx = {}) {
13417
+ function handleSlash(cmd, args, loop2, ctx = {}) {
12779
13418
  const h = HANDLERS[cmd];
12780
- if (h) return h(args, loop, ctx);
13419
+ if (h) return h(args, loop2, ctx);
12781
13420
  return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
12782
13421
  }
12783
13422
 
@@ -13087,16 +13726,16 @@ function useEditHistory(codeMode) {
13087
13726
  const status2 = entryStatus(entry);
13088
13727
  const header2 = `\u25B8 edit #${entry.id} \xB7 ${when} \xB7 ${entry.source} \xB7 ${status2} \xB7 ${files.length} file(s)`;
13089
13728
  const countLines3 = (s) => s.length === 0 ? 0 : (s.match(/\n/g)?.length ?? 0) + 1;
13090
- const fileLines = files.map((path) => {
13091
- const fileBlocks = entry.blocks.filter((b) => b.path === path);
13729
+ const fileLines = files.map((path5) => {
13730
+ const fileBlocks = entry.blocks.filter((b) => b.path === path5);
13092
13731
  let removed = 0;
13093
13732
  let added = 0;
13094
13733
  for (const b of fileBlocks) {
13095
13734
  removed += countLines3(b.search);
13096
13735
  added += countLines3(b.replace);
13097
13736
  }
13098
- const state = entry.undoneFiles.has(path) ? "UNDONE" : "applied";
13099
- return ` ${state.padEnd(7)} -${String(removed).padStart(3)}/+${String(added).padStart(3)} ${path} (${fileBlocks.length} block${fileBlocks.length === 1 ? "" : "s"})`;
13737
+ const state = entry.undoneFiles.has(path5) ? "UNDONE" : "applied";
13738
+ return ` ${state.padEnd(7)} -${String(removed).padStart(3)}/+${String(added).padStart(3)} ${path5} (${fileBlocks.length} block${fileBlocks.length === 1 ? "" : "s"})`;
13100
13739
  });
13101
13740
  return [
13102
13741
  header2,
@@ -13129,14 +13768,14 @@ function useEditHistory(codeMode) {
13129
13768
 
13130
13769
  // src/cli/ui/useSessionInfo.ts
13131
13770
  import { useCallback as useCallback3, useEffect as useEffect4, useState as useState8 } from "react";
13132
- function useSessionInfo(loop) {
13771
+ function useSessionInfo(loop2) {
13133
13772
  const [balance, setBalance] = useState8(null);
13134
13773
  const [models2, setModels] = useState8(null);
13135
13774
  const [latestVersion, setLatestVersion] = useState8(null);
13136
13775
  useEffect4(() => {
13137
13776
  let cancelled = false;
13138
13777
  void (async () => {
13139
- const bal = await loop.client.getBalance().catch(() => null);
13778
+ const bal = await loop2.client.getBalance().catch(() => null);
13140
13779
  if (cancelled || !bal || !bal.balance_infos.length) return;
13141
13780
  const primary = bal.balance_infos[0];
13142
13781
  setBalance({ currency: primary.currency, total: Number(primary.total_balance) });
@@ -13144,18 +13783,18 @@ function useSessionInfo(loop) {
13144
13783
  return () => {
13145
13784
  cancelled = true;
13146
13785
  };
13147
- }, [loop]);
13786
+ }, [loop2]);
13148
13787
  useEffect4(() => {
13149
13788
  let cancelled = false;
13150
13789
  void (async () => {
13151
- const list = await loop.client.listModels().catch(() => null);
13790
+ const list = await loop2.client.listModels().catch(() => null);
13152
13791
  if (cancelled || !list) return;
13153
13792
  setModels(list.data.map((m) => m.id));
13154
13793
  })();
13155
13794
  return () => {
13156
13795
  cancelled = true;
13157
13796
  };
13158
- }, [loop]);
13797
+ }, [loop2]);
13159
13798
  useEffect4(() => {
13160
13799
  let cancelled = false;
13161
13800
  void (async () => {
@@ -13170,19 +13809,19 @@ function useSessionInfo(loop) {
13170
13809
  const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
13171
13810
  const refreshBalance = useCallback3(() => {
13172
13811
  void (async () => {
13173
- const bal = await loop.client.getBalance().catch(() => null);
13812
+ const bal = await loop2.client.getBalance().catch(() => null);
13174
13813
  if (bal?.balance_infos.length) {
13175
13814
  const p = bal.balance_infos[0];
13176
13815
  setBalance({ currency: p.currency, total: Number(p.total_balance) });
13177
13816
  }
13178
13817
  })();
13179
- }, [loop]);
13818
+ }, [loop2]);
13180
13819
  const refreshModels = useCallback3(() => {
13181
13820
  void (async () => {
13182
- const list = await loop.client.listModels().catch(() => null);
13821
+ const list = await loop2.client.listModels().catch(() => null);
13183
13822
  if (list) setModels(list.data.map((m) => m.id));
13184
13823
  })();
13185
- }, [loop]);
13824
+ }, [loop2]);
13186
13825
  const refreshLatestVersion = useCallback3(() => {
13187
13826
  void (async () => {
13188
13827
  const fresh = await getLatestVersion({ force: true });
@@ -13260,6 +13899,17 @@ function useSubagent({ session, setHistorical }) {
13260
13899
  // src/cli/ui/App.tsx
13261
13900
  var FLUSH_INTERVAL_MS = 100;
13262
13901
  var PLAIN_UI = process.env.REASONIX_UI === "plain";
13902
+ function LoopStatusRow({
13903
+ loop: loop2
13904
+ }) {
13905
+ const [, setTick] = React23.useState(0);
13906
+ React23.useEffect(() => {
13907
+ const id = setInterval(() => setTick((t2) => t2 + 1), 1e3);
13908
+ return () => clearInterval(id);
13909
+ }, []);
13910
+ const nextFireMs = Math.max(0, loop2.nextFireAt - Date.now());
13911
+ return /* @__PURE__ */ React23.createElement(Box21, null, /* @__PURE__ */ React23.createElement(Text19, { color: "cyan" }, `\u25B8 ${formatLoopStatus(loop2.prompt, nextFireMs, loop2.iter)} \xB7 /loop stop or type to cancel`));
13912
+ }
13263
13913
  function App({
13264
13914
  model: model2,
13265
13915
  system,
@@ -13279,21 +13929,24 @@ function App({
13279
13929
  const [input, setInput] = useState10("");
13280
13930
  const [busy, setBusy] = useState10(false);
13281
13931
  const abortedThisTurn = useRef6(false);
13932
+ useEffect6(() => {
13933
+ busyRef.current = busy;
13934
+ }, [busy]);
13282
13935
  const [ongoingTool, setOngoingTool] = useState10(null);
13283
13936
  const [toolProgress, setToolProgress] = useState10(null);
13284
- const { stdout: stdout2 } = useStdout8();
13937
+ const { stdout: stdout3 } = useStdout8();
13285
13938
  useEffect6(() => {
13286
- if (!stdout2 || !stdout2.isTTY) return;
13287
- stdout2.write("\x1B[?2004h");
13288
- stdout2.write("\x1B[>4;2m");
13939
+ if (!stdout3 || !stdout3.isTTY) return;
13940
+ stdout3.write("\x1B[?2004h");
13941
+ stdout3.write("\x1B[>4;2m");
13289
13942
  return () => {
13290
- stdout2.write("\x1B[?2004l");
13291
- stdout2.write("\x1B[>4m");
13943
+ stdout3.write("\x1B[?2004l");
13944
+ stdout3.write("\x1B[>4m");
13292
13945
  };
13293
- }, [stdout2]);
13946
+ }, [stdout3]);
13294
13947
  const [isResizing, setIsResizing] = useState10(false);
13295
13948
  useEffect6(() => {
13296
- if (!stdout2 || !stdout2.isTTY) return;
13949
+ if (!stdout3 || !stdout3.isTTY) return;
13297
13950
  let timer = null;
13298
13951
  const onResize = () => {
13299
13952
  setIsResizing(true);
@@ -13303,12 +13956,12 @@ function App({
13303
13956
  timer = null;
13304
13957
  }, 400);
13305
13958
  };
13306
- stdout2.on("resize", onResize);
13959
+ stdout3.on("resize", onResize);
13307
13960
  return () => {
13308
- stdout2.off("resize", onResize);
13961
+ stdout3.off("resize", onResize);
13309
13962
  if (timer) clearTimeout(timer);
13310
13963
  };
13311
- }, [stdout2]);
13964
+ }, [stdout3]);
13312
13965
  const { activity: subagentActivity, sinkRef: subagentSinkRef } = useSubagent({
13313
13966
  session,
13314
13967
  setHistorical
@@ -13332,6 +13985,7 @@ function App({
13332
13985
  const [pendingCount, setPendingCount] = useState10(0);
13333
13986
  const syncPendingCount = useCallback4(() => {
13334
13987
  setPendingCount(pendingEdits.current.length);
13988
+ setPendingTick((t2) => t2 + 1);
13335
13989
  }, []);
13336
13990
  const [editMode, setEditMode] = useState10(() => codeMode ? loadEditMode() : "review");
13337
13991
  const editModeRef = useRef6(editMode);
@@ -13340,6 +13994,8 @@ function App({
13340
13994
  if (codeMode) saveEditMode(editMode);
13341
13995
  }, [editMode, codeMode]);
13342
13996
  const [pendingEditReview, setPendingEditReview] = useState10(null);
13997
+ const [walkthroughActive, setWalkthroughActive] = useState10(false);
13998
+ const [pendingTick, setPendingTick] = useState10(0);
13343
13999
  const editReviewResolveRef = useRef6(null);
13344
14000
  const turnEditPolicyRef = useRef6("ask");
13345
14001
  const [modeFlash, setModeFlash] = useState10(false);
@@ -13370,6 +14026,16 @@ function App({
13370
14026
  const promptHistory = useRef6([]);
13371
14027
  const historyCursor = useRef6(-1);
13372
14028
  const assistantIterCounter = useRef6(0);
14029
+ const atUrlCache = useRef6(/* @__PURE__ */ new Map());
14030
+ const [activeLoop, setActiveLoop] = useState10(null);
14031
+ const loopTimerRef = useRef6(null);
14032
+ const handleSubmitRef = useRef6(null);
14033
+ const busyRef = useRef6(false);
14034
+ const activeLoopRef = useRef6(activeLoop);
14035
+ const loopFiringRef = useRef6(false);
14036
+ useEffect6(() => {
14037
+ activeLoopRef.current = activeLoop;
14038
+ }, [activeLoop]);
13373
14039
  const toolHistoryRef = useRef6([]);
13374
14040
  const planStepsRef = useRef6(null);
13375
14041
  const completedStepIdsRef = useRef6(/* @__PURE__ */ new Set());
@@ -13414,7 +14080,7 @@ function App({
13414
14080
  };
13415
14081
  }, []);
13416
14082
  const loopRef = useRef6(null);
13417
- const loop = useMemo3(() => {
14083
+ const loop2 = useMemo3(() => {
13418
14084
  if (loopRef.current) return loopRef.current;
13419
14085
  const client = new DeepSeekClient();
13420
14086
  if (tools && !tools.has("run_skill")) {
@@ -13463,8 +14129,8 @@ function App({
13463
14129
  return l;
13464
14130
  }, [model2, system, harvest3, branch2, session, tools, codeMode]);
13465
14131
  useEffect6(() => {
13466
- loop.hooks = hookList;
13467
- }, [loop, hookList]);
14132
+ loop2.hooks = hookList;
14133
+ }, [loop2, hookList]);
13468
14134
  const {
13469
14135
  balance,
13470
14136
  models: models2,
@@ -13473,7 +14139,7 @@ function App({
13473
14139
  refreshBalance,
13474
14140
  refreshModels,
13475
14141
  refreshLatestVersion
13476
- } = useSessionInfo(loop);
14142
+ } = useSessionInfo(loop2);
13477
14143
  const {
13478
14144
  slashMatches,
13479
14145
  slashSelected,
@@ -13516,13 +14182,13 @@ function App({
13516
14182
  text: "\u25B8 ephemeral chat (no session persistence) \u2014 drop --no-session to enable"
13517
14183
  }
13518
14184
  ]);
13519
- } else if (loop.resumedMessageCount > 0) {
14185
+ } else if (loop2.resumedMessageCount > 0) {
13520
14186
  setHistorical((prev) => [
13521
14187
  ...prev,
13522
14188
  {
13523
14189
  id: `sys-resume-${Date.now()}`,
13524
14190
  role: "info",
13525
- text: `\u25B8 resumed session "${session}" with ${loop.resumedMessageCount} prior messages \xB7 /forget to start over \xB7 /sessions to list`
14191
+ text: `\u25B8 resumed session "${session}" with ${loop2.resumedMessageCount} prior messages \xB7 /forget to start over \xB7 /sessions to list`
13526
14192
  }
13527
14193
  ]);
13528
14194
  } else {
@@ -13585,7 +14251,7 @@ function App({
13585
14251
  ]);
13586
14252
  markEditModeHintShown();
13587
14253
  }
13588
- }, [session, loop, codeMode, syncPendingCount]);
14254
+ }, [session, loop2, codeMode, syncPendingCount]);
13589
14255
  const quitProcess = useCallback4(() => {
13590
14256
  transcriptRef.current?.end();
13591
14257
  process.exit(0);
@@ -13609,31 +14275,46 @@ function App({
13609
14275
  if (key.escape && busy) {
13610
14276
  if (abortedThisTurn.current) return;
13611
14277
  abortedThisTurn.current = true;
13612
- const resolve8 = editReviewResolveRef.current;
13613
- if (resolve8) {
14278
+ const resolve9 = editReviewResolveRef.current;
14279
+ if (resolve9) {
13614
14280
  editReviewResolveRef.current = null;
13615
14281
  setPendingEditReview(null);
13616
- resolve8("reject");
14282
+ resolve9("reject");
13617
14283
  }
13618
- loop.abort();
14284
+ if (activeLoopRef.current) stopLoop();
14285
+ loop2.abort();
14286
+ return;
14287
+ }
14288
+ if (key.escape && !busy && activeLoopRef.current) {
14289
+ stopLoop();
14290
+ return;
14291
+ }
14292
+ if (key.escape && walkthroughActive) {
14293
+ setWalkthroughActive(false);
14294
+ const remaining = pendingEdits.current.length;
14295
+ setHistorical((prev) => [
14296
+ ...prev,
14297
+ {
14298
+ id: `walk-esc-${Date.now()}`,
14299
+ role: "info",
14300
+ text: remaining > 0 ? `\u25B8 walk cancelled \u2014 ${remaining} block(s) still pending.` : "\u25B8 walk cancelled."
14301
+ }
14302
+ ]);
13619
14303
  return;
13620
14304
  }
13621
- if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
14305
+ if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
13622
14306
  setEditMode((m) => {
13623
- const next = m === "auto" ? "review" : "auto";
14307
+ const next = m === "review" ? "auto" : m === "auto" ? "yolo" : "review";
14308
+ const message = next === "yolo" ? "\u25B8 edit mode: YOLO \u2014 edits AND shell commands auto-run. /undo still rolls back edits. Use carefully." : next === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo. Shell commands still ask." : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)";
13624
14309
  setHistorical((prev) => [
13625
14310
  ...prev,
13626
- {
13627
- id: `mode-${Date.now()}`,
13628
- role: "info",
13629
- text: next === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo" : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)"
13630
- }
14311
+ { id: `mode-${Date.now()}`, role: "info", text: message }
13631
14312
  ]);
13632
14313
  return next;
13633
14314
  });
13634
14315
  return;
13635
14316
  }
13636
- if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision && // Fire when EITHER the banner is up OR there's any non-undone
14317
+ if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision && // Fire when EITHER the banner is up OR there's any non-undone
13637
14318
  // history entry — the keybind is useful long after the 5-second
13638
14319
  // banner expires, which users rightly want.
13639
14320
  (undoBanner || hasUndoable())) {
@@ -13734,7 +14415,7 @@ function App({
13734
14415
  }
13735
14416
  return formatEditResults(results);
13736
14417
  };
13737
- if (editModeRef.current === "auto") return applyNow();
14418
+ if (editModeRef.current === "auto" || editModeRef.current === "yolo") return applyNow();
13738
14419
  if (turnEditPolicyRef.current === "apply-all") return applyNow();
13739
14420
  const choice = await new Promise((resolveChoice) => {
13740
14421
  editReviewResolveRef.current = resolveChoice;
@@ -13827,7 +14508,7 @@ function App({
13827
14508
  },
13828
14509
  [session, syncPendingCount]
13829
14510
  );
13830
- const prefixHash = loop.prefix.fingerprint;
14511
+ const prefixHash = loop2.prefix.fingerprint;
13831
14512
  const writeTranscript = useCallback4(
13832
14513
  (ev) => {
13833
14514
  const stream = transcriptRef.current;
@@ -13846,10 +14527,98 @@ function App({
13846
14527
  const clearPendingPlan = useCallback4(() => {
13847
14528
  setPendingPlan(null);
13848
14529
  }, []);
14530
+ const stopLoop = useCallback4(() => {
14531
+ if (loopTimerRef.current) {
14532
+ clearTimeout(loopTimerRef.current);
14533
+ loopTimerRef.current = null;
14534
+ }
14535
+ setActiveLoop((cur) => {
14536
+ if (!cur) return cur;
14537
+ setHistorical((prev) => [
14538
+ ...prev,
14539
+ {
14540
+ id: `loop-stop-${Date.now()}`,
14541
+ role: "info",
14542
+ text: `\u25B8 loop stopped (after ${cur.iter} iter${cur.iter === 1 ? "" : "s"}).`
14543
+ }
14544
+ ]);
14545
+ return null;
14546
+ });
14547
+ }, []);
14548
+ const startLoop = useCallback4((intervalMs, prompt) => {
14549
+ if (loopTimerRef.current) {
14550
+ clearTimeout(loopTimerRef.current);
14551
+ loopTimerRef.current = null;
14552
+ }
14553
+ setActiveLoop({
14554
+ prompt,
14555
+ intervalMs,
14556
+ nextFireAt: Date.now() + intervalMs,
14557
+ iter: 0
14558
+ });
14559
+ }, []);
14560
+ const startWalkthrough = useCallback4(() => {
14561
+ if (!codeMode) {
14562
+ return "/walk is only available inside `reasonix code`.";
14563
+ }
14564
+ if (pendingEdits.current.length === 0) {
14565
+ return "nothing pending \u2014 nothing to walk through.";
14566
+ }
14567
+ setWalkthroughActive(true);
14568
+ return `\u25B8 walking ${pendingEdits.current.length} edit block(s) \u2014 y apply \xB7 n reject \xB7 a apply rest \xB7 A flip to AUTO \xB7 Esc cancels (keeps remaining queued).`;
14569
+ }, [codeMode]);
14570
+ const handleWalkChoice = useCallback4(
14571
+ (choice) => {
14572
+ if (choice === "apply") {
14573
+ const out = codeApply([1]);
14574
+ setHistorical((prev) => [...prev, { id: `walk-${Date.now()}`, role: "info", text: out }]);
14575
+ } else if (choice === "reject") {
14576
+ const out = codeDiscard([1]);
14577
+ setHistorical((prev) => [...prev, { id: `walk-${Date.now()}`, role: "info", text: out }]);
14578
+ } else if (choice === "apply-rest-of-turn") {
14579
+ const out = codeApply();
14580
+ setHistorical((prev) => [...prev, { id: `walk-${Date.now()}`, role: "info", text: out }]);
14581
+ setWalkthroughActive(false);
14582
+ return;
14583
+ } else if (choice === "flip-to-auto") {
14584
+ setEditMode("auto");
14585
+ saveEditMode("auto");
14586
+ const out = codeApply([1]);
14587
+ setHistorical((prev) => [
14588
+ ...prev,
14589
+ { id: `walk-${Date.now()}`, role: "info", text: out },
14590
+ {
14591
+ id: `walk-flip-${Date.now()}`,
14592
+ role: "info",
14593
+ text: "\u25B8 flipped to AUTO mode \u2014 future edits will apply immediately. Walk exited."
14594
+ }
14595
+ ]);
14596
+ setWalkthroughActive(false);
14597
+ return;
14598
+ }
14599
+ if (pendingEdits.current.length === 0) setWalkthroughActive(false);
14600
+ },
14601
+ [codeApply, codeDiscard]
14602
+ );
14603
+ const getLoopStatus = useCallback4(() => {
14604
+ const cur = activeLoopRef.current;
14605
+ if (!cur) return null;
14606
+ return {
14607
+ prompt: cur.prompt,
14608
+ intervalMs: cur.intervalMs,
14609
+ iter: cur.iter,
14610
+ nextFireMs: Math.max(0, cur.nextFireAt - Date.now())
14611
+ };
14612
+ }, []);
13849
14613
  const handleSubmit = useCallback4(
13850
14614
  async (raw) => {
13851
14615
  let text = raw.trim();
13852
- if (!text || busy) return;
14616
+ if (!text) return;
14617
+ if (activeLoopRef.current && !loopFiringRef.current) {
14618
+ stopLoop();
14619
+ }
14620
+ loopFiringRef.current = false;
14621
+ if (busy) return;
13853
14622
  if (atMatches && atMatches.length > 0 && atPicker) {
13854
14623
  const sel = atMatches[atSelected] ?? atMatches[0];
13855
14624
  if (sel) {
@@ -13882,18 +14651,20 @@ function App({
13882
14651
  return;
13883
14652
  }
13884
14653
  const hashParse = detectHashMemory(text);
13885
- if (hashParse?.kind === "memory") {
14654
+ if (hashParse?.kind === "memory" || hashParse?.kind === "memory-global") {
14655
+ const isGlobal = hashParse.kind === "memory-global";
13886
14656
  const memRoot = codeMode?.rootDir ?? process.cwd();
13887
14657
  promptHistory.current.push(text);
13888
14658
  try {
13889
- const result = appendProjectMemory(memRoot, hashParse.note);
14659
+ const result = isGlobal ? appendGlobalMemory(hashParse.note) : appendProjectMemory(memRoot, hashParse.note);
13890
14660
  const verb = result.created ? "created" : "appended to";
14661
+ const scopeTag = isGlobal ? "global" : "project";
13891
14662
  setHistorical((prev) => [
13892
14663
  ...prev,
13893
14664
  {
13894
14665
  id: `hash-${Date.now()}`,
13895
14666
  role: "info",
13896
- text: `\u25B8 noted \u2014 ${verb} ${result.path}`
14667
+ text: `\u25B8 noted (${scopeTag}) \u2014 ${verb} ${result.path}`
13897
14668
  }
13898
14669
  ]);
13899
14670
  } catch (err) {
@@ -13936,7 +14707,7 @@ function App({
13936
14707
  ...prev,
13937
14708
  { id: `bang-o-${Date.now()}`, role: "info", text: formatted }
13938
14709
  ]);
13939
- loop.appendAndPersist({
14710
+ loop2.appendAndPersist({
13940
14711
  role: "user",
13941
14712
  content: formatBangUserMessage(bangCmd, formatted)
13942
14713
  });
@@ -13968,7 +14739,7 @@ function App({
13968
14739
  }
13969
14740
  const slash = parseSlash(text);
13970
14741
  if (slash) {
13971
- const result = handleSlash(slash.cmd, slash.args, loop, {
14742
+ const result = handleSlash(slash.cmd, slash.args, loop2, {
13972
14743
  mcpSpecs,
13973
14744
  mcpServers,
13974
14745
  codeUndo: codeMode ? codeUndo : void 0,
@@ -13986,13 +14757,17 @@ function App({
13986
14757
  editMode: codeMode ? editMode : void 0,
13987
14758
  setEditMode: codeMode ? setEditMode : void 0,
13988
14759
  armPro: () => {
13989
- loop.armProForNextTurn();
14760
+ loop2.armProForNextTurn();
13990
14761
  setProArmed(true);
13991
14762
  },
13992
14763
  disarmPro: () => {
13993
- loop.disarmPro();
14764
+ loop2.disarmPro();
13994
14765
  setProArmed(false);
13995
14766
  },
14767
+ startLoop,
14768
+ stopLoop,
14769
+ getLoopStatus,
14770
+ startWalkthrough: codeMode ? startWalkthrough : void 0,
13996
14771
  jobs: codeMode?.jobs,
13997
14772
  postInfo: (text2) => setHistorical((prev) => [
13998
14773
  ...prev,
@@ -14009,12 +14784,13 @@ function App({
14009
14784
  refreshModels
14010
14785
  });
14011
14786
  if (result.exit) {
14787
+ if (activeLoopRef.current) stopLoop();
14012
14788
  transcriptRef.current?.end();
14013
14789
  exit2();
14014
14790
  return;
14015
14791
  }
14016
14792
  if (result.clear && result.info) {
14017
- stdout2?.write("\x1B[2J\x1B[3J\x1B[H");
14793
+ stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
14018
14794
  setHistorical([
14019
14795
  {
14020
14796
  id: `sys-${Date.now()}`,
@@ -14027,16 +14803,18 @@ function App({
14027
14803
  clearPendingEdits(session ?? null);
14028
14804
  syncPendingCount();
14029
14805
  }
14806
+ if (activeLoopRef.current) stopLoop();
14030
14807
  return;
14031
14808
  }
14032
14809
  if (result.clear) {
14033
- stdout2?.write("\x1B[2J\x1B[3J\x1B[H");
14810
+ stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
14034
14811
  setHistorical([]);
14035
14812
  if (codeMode) {
14036
14813
  pendingEdits.current = [];
14037
14814
  clearPendingEdits(session ?? null);
14038
14815
  syncPendingCount();
14039
14816
  }
14817
+ if (activeLoopRef.current) stopLoop();
14040
14818
  return;
14041
14819
  }
14042
14820
  if (result.info) {
@@ -14165,8 +14943,47 @@ function App({
14165
14943
  }
14166
14944
  }
14167
14945
  }
14946
+ if (/(?:^|\s)@https?:\/\//.test(text)) {
14947
+ try {
14948
+ const urlExpanded = await expandAtUrls(modelInput, {
14949
+ fetcher: webFetch,
14950
+ cache: atUrlCache.current
14951
+ });
14952
+ if (urlExpanded.expansions.length > 0) {
14953
+ modelInput = urlExpanded.text;
14954
+ const inlined = urlExpanded.expansions.filter((ex) => ex.ok).map((ex) => {
14955
+ const tag = ex.title ? `${ex.title} (${ex.url})` : ex.url;
14956
+ const trunc = ex.truncated ? " \xB7 truncated" : "";
14957
+ return `${tag} \xB7 ${(ex.chars ?? 0).toLocaleString()} chars${trunc}`;
14958
+ });
14959
+ const skipped = urlExpanded.expansions.filter((ex) => !ex.ok).map((ex) => `${ex.url} (${ex.skip ?? "fetch-error"})`);
14960
+ const parts = [];
14961
+ if (inlined.length > 0) parts.push(`inlined ${inlined.join("; ")}`);
14962
+ if (skipped.length > 0) parts.push(`skipped ${skipped.join("; ")}`);
14963
+ if (parts.length > 0) {
14964
+ setHistorical((prev) => [
14965
+ ...prev,
14966
+ {
14967
+ id: `aturl-${Date.now()}`,
14968
+ role: "info",
14969
+ text: `\u25B8 @url: ${parts.join("; ")}`
14970
+ }
14971
+ ]);
14972
+ }
14973
+ }
14974
+ } catch (err) {
14975
+ setHistorical((prev) => [
14976
+ ...prev,
14977
+ {
14978
+ id: `aturl-e-${Date.now()}`,
14979
+ role: "warning",
14980
+ text: `@url expansion failed: ${err.message}`
14981
+ }
14982
+ ]);
14983
+ }
14984
+ }
14168
14985
  try {
14169
- for await (const ev of loop.step(modelInput)) {
14986
+ for await (const ev of loop2.step(modelInput)) {
14170
14987
  writeTranscript(ev);
14171
14988
  if (ev.role !== "status") {
14172
14989
  setStatusLine((cur) => cur ? null : cur);
@@ -14206,7 +15023,7 @@ function App({
14206
15023
  flush();
14207
15024
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
14208
15025
  setStreaming(null);
14209
- setSummary(loop.stats.summary());
15026
+ setSummary(loop2.stats.summary());
14210
15027
  if (ev.stats?.usage) {
14211
15028
  appendUsage({
14212
15029
  session: session ?? null,
@@ -14240,7 +15057,7 @@ function App({
14240
15057
  if (codeMode && finalText && !ev.forcedSummary) {
14241
15058
  const blocks = parseEditBlocks(finalText);
14242
15059
  if (blocks.length > 0) {
14243
- if (editModeRef.current === "auto") {
15060
+ if (editModeRef.current === "auto" || editModeRef.current === "yolo") {
14244
15061
  const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
14245
15062
  const results = applyEditBlocks(blocks, codeMode.rootDir);
14246
15063
  const good = results.some(
@@ -14452,7 +15269,7 @@ function App({
14452
15269
  event: "Stop",
14453
15270
  cwd: hookCwd,
14454
15271
  lastAssistantText: streamRef.text,
14455
- turn: loop.stats.summary().turns
15272
+ turn: loop2.stats.summary().turns
14456
15273
  }
14457
15274
  });
14458
15275
  for (const o of stopReport.outcomes) {
@@ -14473,7 +15290,7 @@ function App({
14473
15290
  setOngoingTool(null);
14474
15291
  setToolProgress(null);
14475
15292
  setStatusLine(null);
14476
- setSummary(loop.stats.summary());
15293
+ setSummary(loop2.stats.summary());
14477
15294
  setBusy(false);
14478
15295
  setTurnOnPro(false);
14479
15296
  refreshBalance();
@@ -14491,7 +15308,7 @@ function App({
14491
15308
  exit2,
14492
15309
  hookCwd,
14493
15310
  hookList,
14494
- loop,
15311
+ loop2,
14495
15312
  latestVersion,
14496
15313
  mcpSpecs,
14497
15314
  mcpServers,
@@ -14520,15 +15337,57 @@ function App({
14520
15337
  refreshModels,
14521
15338
  proArmed,
14522
15339
  persistPlanState,
14523
- stdout2
15340
+ stdout3,
15341
+ stopLoop,
15342
+ startLoop,
15343
+ getLoopStatus,
15344
+ startWalkthrough
14524
15345
  ]
14525
15346
  );
14526
- const handleShellConfirm = useCallback4(
14527
- async (choice) => {
14528
- const pending = pendingShell;
14529
- if (!pending || !codeMode) return;
14530
- const { command: cmd, kind } = pending;
14531
- setPendingShell(null);
15347
+ useEffect6(() => {
15348
+ handleSubmitRef.current = handleSubmit;
15349
+ }, [handleSubmit]);
15350
+ useEffect6(() => {
15351
+ if (!activeLoop) return;
15352
+ const delay = Math.max(0, activeLoop.nextFireAt - Date.now());
15353
+ const timer = setTimeout(async () => {
15354
+ loopTimerRef.current = null;
15355
+ if (busyRef.current) {
15356
+ setActiveLoop((cur2) => cur2 ? { ...cur2, nextFireAt: Date.now() + 1e3 } : cur2);
15357
+ return;
15358
+ }
15359
+ const cur = activeLoopRef.current;
15360
+ if (!cur) return;
15361
+ const nextIter = cur.iter + 1;
15362
+ setActiveLoop(
15363
+ (c) => c ? { ...c, iter: nextIter, nextFireAt: Date.now() + cur.intervalMs } : c
15364
+ );
15365
+ setHistorical((prev) => [
15366
+ ...prev,
15367
+ {
15368
+ id: `loop-fire-${Date.now()}`,
15369
+ role: "info",
15370
+ text: `\u25B8 /loop iter ${nextIter} \u2192 ${cur.prompt}`
15371
+ }
15372
+ ]);
15373
+ loopFiringRef.current = true;
15374
+ try {
15375
+ await handleSubmitRef.current?.(cur.prompt);
15376
+ } catch {
15377
+ stopLoop();
15378
+ } finally {
15379
+ loopFiringRef.current = false;
15380
+ }
15381
+ }, delay);
15382
+ loopTimerRef.current = timer;
15383
+ return () => clearTimeout(timer);
15384
+ }, [activeLoop, stopLoop]);
15385
+ const handleShellConfirm = useCallback4(
15386
+ async (choice) => {
15387
+ const pending = pendingShell;
15388
+ if (!pending || !codeMode) return;
15389
+ const { command: cmd, kind } = pending;
15390
+ setPendingShell(null);
14532
15391
  let synthetic;
14533
15392
  if (choice === "deny") {
14534
15393
  setHistorical((prev) => [
@@ -14612,13 +15471,13 @@ ${body}`;
14612
15471
  }
14613
15472
  }
14614
15473
  if (busy) {
14615
- loop.abort();
15474
+ loop2.abort();
14616
15475
  setQueuedSubmit(synthetic);
14617
15476
  } else {
14618
15477
  await handleSubmit(synthetic);
14619
15478
  }
14620
15479
  },
14621
- [pendingShell, codeMode, handleSubmit, busy, loop]
15480
+ [pendingShell, codeMode, handleSubmit, busy, loop2]
14622
15481
  );
14623
15482
  useEffect6(() => {
14624
15483
  if (!busy && queuedSubmit !== null) {
@@ -14656,13 +15515,13 @@ ${body}`;
14656
15515
  { id: `plan-${choice}-${Date.now()}`, role: "info", text: marker }
14657
15516
  ]);
14658
15517
  if (busy) {
14659
- loop.abort();
15518
+ loop2.abort();
14660
15519
  setQueuedSubmit(synthetic);
14661
15520
  } else {
14662
15521
  await handleSubmit(synthetic);
14663
15522
  }
14664
15523
  },
14665
- [pendingPlan, togglePlanMode, busy, loop, handleSubmit, persistPlanState]
15524
+ [pendingPlan, togglePlanMode, busy, loop2, handleSubmit, persistPlanState]
14666
15525
  );
14667
15526
  const handlePlanConfirmRef = useRef6(handlePlanConfirm);
14668
15527
  useEffect6(() => {
@@ -14713,13 +15572,13 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
14713
15572
  { id: `plan-${staged.mode}-${Date.now()}`, role: "info", text: marker }
14714
15573
  ]);
14715
15574
  if (busy) {
14716
- loop.abort();
15575
+ loop2.abort();
14717
15576
  setQueuedSubmit(synthetic);
14718
15577
  } else {
14719
15578
  await handleSubmit(synthetic);
14720
15579
  }
14721
15580
  },
14722
- [stagedInput, togglePlanMode, busy, loop, handleSubmit]
15581
+ [stagedInput, togglePlanMode, busy, loop2, handleSubmit]
14723
15582
  );
14724
15583
  const handleStagedInputCancel = useCallback4(() => {
14725
15584
  if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
@@ -14748,13 +15607,13 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
14748
15607
  { id: `cp-${choice}-${Date.now()}`, role: "info", text: marker }
14749
15608
  ]);
14750
15609
  if (busy) {
14751
- loop.abort();
15610
+ loop2.abort();
14752
15611
  setQueuedSubmit(synthetic);
14753
15612
  } else {
14754
15613
  await handleSubmit(synthetic);
14755
15614
  }
14756
15615
  },
14757
- [pendingCheckpoint, busy, loop, handleSubmit]
15616
+ [pendingCheckpoint, busy, loop2, handleSubmit]
14758
15617
  );
14759
15618
  const handleCheckpointConfirmRef = useRef6(handleCheckpointConfirm);
14760
15619
  useEffect6(() => {
@@ -14782,13 +15641,13 @@ If the feedback only tweaks how you execute (extra constraints, style preference
14782
15641
  { id: `cp-revise-${Date.now()}`, role: "info", text: marker }
14783
15642
  ]);
14784
15643
  if (busy) {
14785
- loop.abort();
15644
+ loop2.abort();
14786
15645
  setQueuedSubmit(synthetic);
14787
15646
  } else {
14788
15647
  await handleSubmit(synthetic);
14789
15648
  }
14790
15649
  },
14791
- [stagedCheckpointRevise, busy, loop, handleSubmit]
15650
+ [stagedCheckpointRevise, busy, loop2, handleSubmit]
14792
15651
  );
14793
15652
  const handleCheckpointReviseCancel = useCallback4(() => {
14794
15653
  const snap = stagedCheckpointRevise;
@@ -14811,7 +15670,7 @@ If the feedback only tweaks how you execute (extra constraints, style preference
14811
15670
  { id: `choice-cancel-${Date.now()}`, role: "info", text: "\u25B8 choice cancelled" }
14812
15671
  ]);
14813
15672
  if (busy) {
14814
- loop.abort();
15673
+ loop2.abort();
14815
15674
  setQueuedSubmit(synthetic2);
14816
15675
  } else {
14817
15676
  await handleSubmit(synthetic2);
@@ -14826,13 +15685,13 @@ If the feedback only tweaks how you execute (extra constraints, style preference
14826
15685
  { id: `choice-pick-${Date.now()}`, role: "info", text: `\u25B8 chose ${label}` }
14827
15686
  ]);
14828
15687
  if (busy) {
14829
- loop.abort();
15688
+ loop2.abort();
14830
15689
  setQueuedSubmit(synthetic);
14831
15690
  } else {
14832
15691
  await handleSubmit(synthetic);
14833
15692
  }
14834
15693
  },
14835
- [pendingChoice, busy, loop, handleSubmit]
15694
+ [pendingChoice, busy, loop2, handleSubmit]
14836
15695
  );
14837
15696
  const handleChoiceConfirmRef = useRef6(handleChoiceConfirm);
14838
15697
  useEffect6(() => {
@@ -14857,13 +15716,13 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
14857
15716
  { id: `choice-custom-${Date.now()}`, role: "info", text: marker }
14858
15717
  ]);
14859
15718
  if (busy) {
14860
- loop.abort();
15719
+ loop2.abort();
14861
15720
  setQueuedSubmit(synthetic);
14862
15721
  } else {
14863
15722
  await handleSubmit(synthetic);
14864
15723
  }
14865
15724
  },
14866
- [busy, loop, handleSubmit]
15725
+ [busy, loop2, handleSubmit]
14867
15726
  );
14868
15727
  const handleChoiceCustomCancel = useCallback4(() => {
14869
15728
  const snap = stagedChoiceCustom;
@@ -14882,7 +15741,7 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
14882
15741
  { id: `revise-reject-${Date.now()}`, role: "info", text: "\u25B8 revision rejected" }
14883
15742
  ]);
14884
15743
  if (busy) {
14885
- loop.abort();
15744
+ loop2.abort();
14886
15745
  setQueuedSubmit(synthetic2);
14887
15746
  } else {
14888
15747
  await handleSubmit(synthetic2);
@@ -14917,13 +15776,13 @@ ${snap.remainingSteps.map((s, i) => ` ${i + 1}. ${s.id} \xB7 ${s.title} \u2014
14917
15776
 
14918
15777
  Continue executing from the next pending step. Call mark_step_complete after each one as before.`;
14919
15778
  if (busy) {
14920
- loop.abort();
15779
+ loop2.abort();
14921
15780
  setQueuedSubmit(synthetic);
14922
15781
  } else {
14923
15782
  await handleSubmit(synthetic);
14924
15783
  }
14925
15784
  },
14926
- [pendingRevision, busy, loop, handleSubmit, persistPlanState]
15785
+ [pendingRevision, busy, loop2, handleSubmit, persistPlanState]
14927
15786
  );
14928
15787
  const handleReviseConfirmRef = useRef6(handleReviseConfirm);
14929
15788
  useEffect6(() => {
@@ -14936,17 +15795,17 @@ Continue executing from the next pending step. Call mark_step_complete after eac
14936
15795
  return /* @__PURE__ */ React23.createElement(React23.Fragment, null, /* @__PURE__ */ React23.createElement(
14937
15796
  TickerProvider,
14938
15797
  {
14939
- disabled: PLAIN_UI || isResizing || !!pendingPlan || !!pendingShell || !!pendingEditReview || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
15798
+ disabled: PLAIN_UI || isResizing || !!pendingPlan || !!pendingShell || !!pendingEditReview || walkthroughActive || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
14940
15799
  },
14941
15800
  /* @__PURE__ */ React23.createElement(Box21, { flexDirection: "column" }, /* @__PURE__ */ React23.createElement(
14942
15801
  StatsPanel,
14943
15802
  {
14944
15803
  summary,
14945
- model: loop.model,
15804
+ model: loop2.model,
14946
15805
  prefixHash,
14947
- harvestOn: loop.harvestEnabled,
14948
- branchBudget: loop.branchOptions.budget,
14949
- reasoningEffort: loop.reasoningEffort,
15806
+ harvestOn: loop2.harvestEnabled,
15807
+ branchBudget: loop2.branchOptions.budget,
15808
+ reasoningEffort: loop2.reasoningEffort,
14950
15809
  planMode,
14951
15810
  editMode: codeMode ? editMode : void 0,
14952
15811
  balance,
@@ -15028,13 +15887,20 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15028
15887
  {
15029
15888
  block: pendingEditReview,
15030
15889
  onChoose: (choice) => {
15031
- const resolve8 = editReviewResolveRef.current;
15032
- if (resolve8) {
15890
+ const resolve9 = editReviewResolveRef.current;
15891
+ if (resolve9) {
15033
15892
  editReviewResolveRef.current = null;
15034
- resolve8(choice);
15893
+ resolve9(choice);
15035
15894
  }
15036
15895
  }
15037
15896
  }
15897
+ ) : walkthroughActive && pendingEdits.current.length > 0 ? /* @__PURE__ */ React23.createElement(
15898
+ EditConfirm,
15899
+ {
15900
+ key: `walk-${pendingTick}`,
15901
+ block: pendingEdits.current[0],
15902
+ onChoose: handleWalkChoice
15903
+ }
15038
15904
  ) : /* @__PURE__ */ React23.createElement(React23.Fragment, null, codeMode ? /* @__PURE__ */ React23.createElement(
15039
15905
  ModeStatusBar,
15040
15906
  {
@@ -15045,7 +15911,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15045
15911
  undoArmed: !!undoBanner || hasUndoable(),
15046
15912
  jobs: codeMode.jobs
15047
15913
  }
15048
- ) : null, /* @__PURE__ */ React23.createElement(
15914
+ ) : null, activeLoop ? /* @__PURE__ */ React23.createElement(LoopStatusRow, { loop: activeLoop }) : null, /* @__PURE__ */ React23.createElement(
15049
15915
  PromptInput,
15050
15916
  {
15051
15917
  value: input,
@@ -15076,7 +15942,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15076
15942
  }
15077
15943
 
15078
15944
  // src/cli/ui/SessionPicker.tsx
15079
- import { Box as Box22, Text as Text19 } from "ink";
15945
+ import { Box as Box22, Text as Text20 } from "ink";
15080
15946
  import React24 from "react";
15081
15947
  function SessionPicker({
15082
15948
  sessionName,
@@ -15084,7 +15950,7 @@ function SessionPicker({
15084
15950
  lastActive,
15085
15951
  onChoose
15086
15952
  }) {
15087
- return /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React24.createElement(Box22, { marginBottom: 1 }, /* @__PURE__ */ React24.createElement(Text19, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React24.createElement(Text19, { dimColor: true }, ` \xB7 last active ${relativeTime2(lastActive)}`)), /* @__PURE__ */ React24.createElement(
15953
+ return /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React24.createElement(Box22, { marginBottom: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, ` \xB7 last active ${relativeTime2(lastActive)}`)), /* @__PURE__ */ React24.createElement(
15088
15954
  SingleSelect,
15089
15955
  {
15090
15956
  initialValue: "new",
@@ -15107,7 +15973,7 @@ function SessionPicker({
15107
15973
  ],
15108
15974
  onSubmit: (v) => onChoose(v)
15109
15975
  }
15110
- ), /* @__PURE__ */ React24.createElement(Box22, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text19, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] select")));
15976
+ ), /* @__PURE__ */ React24.createElement(Box22, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] select")));
15111
15977
  }
15112
15978
  function relativeTime2(date) {
15113
15979
  const ms = Date.now() - date.getTime();
@@ -15123,7 +15989,7 @@ function relativeTime2(date) {
15123
15989
  }
15124
15990
 
15125
15991
  // src/cli/ui/Setup.tsx
15126
- import { Box as Box23, Text as Text20, useApp as useApp2 } from "ink";
15992
+ import { Box as Box23, Text as Text21, useApp as useApp2 } from "ink";
15127
15993
  import TextInput from "ink-text-input";
15128
15994
  import React25, { useState as useState11 } from "react";
15129
15995
  function Setup({ onReady }) {
@@ -15149,7 +16015,7 @@ function Setup({ onReady }) {
15149
16015
  }
15150
16016
  onReady(trimmed);
15151
16017
  };
15152
- return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Text20, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React25.createElement(
16018
+ return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Text21, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React25.createElement(
15153
16019
  TextInput,
15154
16020
  {
15155
16021
  value,
@@ -15158,7 +16024,7 @@ function Setup({ onReady }) {
15158
16024
  mask: "\u2022",
15159
16025
  placeholder: "sk-..."
15160
16026
  }
15161
- )), error ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { color: "red" }, error)) : value ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text20, { dimColor: true }, "(Type /exit to abort.)")));
16027
+ )), error ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { color: "red" }, error)) : value ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "(Type /exit to abort.)")));
15162
16028
  }
15163
16029
 
15164
16030
  // src/cli/commands/chat.tsx
@@ -15331,8 +16197,653 @@ async function chatCommand(opts) {
15331
16197
 
15332
16198
  // src/cli/commands/code.tsx
15333
16199
  import { basename as basename2, resolve as resolve7 } from "path";
16200
+
16201
+ // src/index/semantic/builder.ts
16202
+ import { promises as fs5 } from "fs";
16203
+ import path4 from "path";
16204
+
16205
+ // src/index/semantic/chunker.ts
16206
+ import { promises as fs3 } from "fs";
16207
+ import path2 from "path";
16208
+ var DEFAULT_MAX_CHUNK_CHARS = 4e3;
16209
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
16210
+ "node_modules",
16211
+ ".git",
16212
+ "dist",
16213
+ "build",
16214
+ "out",
16215
+ ".next",
16216
+ ".nuxt",
16217
+ "target",
16218
+ ".venv",
16219
+ "venv",
16220
+ "__pycache__",
16221
+ ".pytest_cache",
16222
+ ".mypy_cache",
16223
+ ".cache",
16224
+ "coverage",
16225
+ ".turbo",
16226
+ ".vercel",
16227
+ ".reasonix"
16228
+ ]);
16229
+ var SKIP_FILES = /* @__PURE__ */ new Set([
16230
+ "package-lock.json",
16231
+ "yarn.lock",
16232
+ "pnpm-lock.yaml",
16233
+ "Cargo.lock",
16234
+ "poetry.lock",
16235
+ "Pipfile.lock",
16236
+ "go.sum",
16237
+ ".DS_Store"
16238
+ ]);
16239
+ var BINARY_EXTS = /* @__PURE__ */ new Set([
16240
+ // Images
16241
+ ".png",
16242
+ ".jpg",
16243
+ ".jpeg",
16244
+ ".gif",
16245
+ ".webp",
16246
+ ".bmp",
16247
+ ".ico",
16248
+ ".tiff",
16249
+ // Fonts
16250
+ ".woff",
16251
+ ".woff2",
16252
+ ".ttf",
16253
+ ".otf",
16254
+ ".eot",
16255
+ // Archives / binaries
16256
+ ".zip",
16257
+ ".tar",
16258
+ ".gz",
16259
+ ".rar",
16260
+ ".7z",
16261
+ ".exe",
16262
+ ".dll",
16263
+ ".so",
16264
+ ".dylib",
16265
+ ".class",
16266
+ ".jar",
16267
+ ".wasm",
16268
+ ".o",
16269
+ ".a",
16270
+ // Media
16271
+ ".mp3",
16272
+ ".mp4",
16273
+ ".wav",
16274
+ ".ogg",
16275
+ ".webm",
16276
+ ".mov",
16277
+ // Other
16278
+ ".pdf",
16279
+ ".sqlite",
16280
+ ".db"
16281
+ ]);
16282
+ function chunkText(text, filePath, windowLines, overlap, maxChunkChars = DEFAULT_MAX_CHUNK_CHARS) {
16283
+ const lines = text.split(/\r?\n/);
16284
+ if (lines.length === 0 || lines.length === 1 && lines[0] === "") return [];
16285
+ const stride = Math.max(1, windowLines - overlap);
16286
+ const chunks = [];
16287
+ for (let start = 0; start < lines.length; start += stride) {
16288
+ const end = Math.min(lines.length, start + windowLines);
16289
+ const slice = lines.slice(start, end).join("\n").trim();
16290
+ if (slice.length === 0) {
16291
+ if (end >= lines.length) break;
16292
+ continue;
16293
+ }
16294
+ const window = {
16295
+ path: filePath,
16296
+ startLine: start + 1,
16297
+ endLine: end,
16298
+ text: slice
16299
+ };
16300
+ for (const sub of safeSplit(window, maxChunkChars)) chunks.push(sub);
16301
+ if (end >= lines.length) break;
16302
+ }
16303
+ return chunks;
16304
+ }
16305
+ function safeSplit(chunk, maxChars) {
16306
+ if (chunk.text.length <= maxChars) return [chunk];
16307
+ const lines = chunk.text.split("\n");
16308
+ const out = [];
16309
+ let bufLines = [];
16310
+ let bufStart = chunk.startLine;
16311
+ let bufLen = 0;
16312
+ const flush = (untilLineNo) => {
16313
+ if (bufLines.length === 0) return;
16314
+ out.push({
16315
+ path: chunk.path,
16316
+ startLine: bufStart,
16317
+ endLine: untilLineNo,
16318
+ text: bufLines.join("\n")
16319
+ });
16320
+ bufLines = [];
16321
+ bufLen = 0;
16322
+ };
16323
+ for (let i = 0; i < lines.length; i++) {
16324
+ const line = lines[i] ?? "";
16325
+ const lineLen = line.length + 1;
16326
+ if (lineLen > maxChars) {
16327
+ flush(chunk.startLine + i - 1);
16328
+ out.push({
16329
+ path: chunk.path,
16330
+ startLine: chunk.startLine + i,
16331
+ endLine: chunk.startLine + i,
16332
+ text: line.slice(0, maxChars)
16333
+ });
16334
+ bufStart = chunk.startLine + i + 1;
16335
+ continue;
16336
+ }
16337
+ if (bufLen + lineLen > maxChars && bufLines.length > 0) {
16338
+ flush(chunk.startLine + i - 1);
16339
+ bufStart = chunk.startLine + i;
16340
+ }
16341
+ bufLines.push(line);
16342
+ bufLen += lineLen;
16343
+ }
16344
+ flush(chunk.endLine);
16345
+ return out;
16346
+ }
16347
+ async function* walkChunks(root, opts = {}) {
16348
+ const windowLines = opts.windowLines ?? 60;
16349
+ const overlap = Math.min(opts.overlap ?? 12, Math.max(0, windowLines - 1));
16350
+ const maxFileBytes = opts.maxFileBytes ?? 256 * 1024;
16351
+ const maxChunkChars = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS;
16352
+ const stack = [root];
16353
+ while (stack.length > 0) {
16354
+ const dir = stack.pop();
16355
+ if (!dir) break;
16356
+ let entries;
16357
+ try {
16358
+ entries = await fs3.readdir(dir, { withFileTypes: true });
16359
+ } catch {
16360
+ continue;
16361
+ }
16362
+ for (const entry of entries) {
16363
+ const name = entry.name;
16364
+ if (entry.isDirectory()) {
16365
+ if (SKIP_DIRS.has(name) || name.startsWith(".")) {
16366
+ if (SKIP_DIRS.has(name) || name === ".git") continue;
16367
+ }
16368
+ stack.push(path2.join(dir, name));
16369
+ continue;
16370
+ }
16371
+ if (!entry.isFile()) continue;
16372
+ if (SKIP_FILES.has(name)) continue;
16373
+ const ext = path2.extname(name).toLowerCase();
16374
+ if (BINARY_EXTS.has(ext)) continue;
16375
+ const abs = path2.join(dir, name);
16376
+ let stat;
16377
+ try {
16378
+ stat = await fs3.stat(abs);
16379
+ } catch {
16380
+ continue;
16381
+ }
16382
+ if (stat.size > maxFileBytes) continue;
16383
+ let text;
16384
+ try {
16385
+ text = await fs3.readFile(abs, "utf8");
16386
+ } catch {
16387
+ continue;
16388
+ }
16389
+ if (text.indexOf("\0") !== -1) continue;
16390
+ const rel = path2.relative(root, abs).split(path2.sep).join("/");
16391
+ for (const chunk of chunkText(text, rel, windowLines, overlap, maxChunkChars)) {
16392
+ yield chunk;
16393
+ }
16394
+ }
16395
+ }
16396
+ }
16397
+
16398
+ // src/index/semantic/store.ts
16399
+ import { promises as fs4 } from "fs";
16400
+ import path3 from "path";
16401
+ var STORE_VERSION = 1;
16402
+ var META_FILE = "index.meta.json";
16403
+ var DATA_FILE = "index.jsonl";
16404
+ var SemanticStore = class {
16405
+ constructor(indexDir, model2) {
16406
+ this.indexDir = indexDir;
16407
+ this.model = model2;
16408
+ }
16409
+ indexDir;
16410
+ model;
16411
+ entries = [];
16412
+ byPath = /* @__PURE__ */ new Map();
16413
+ dim = 0;
16414
+ /** True when no entries are loaded — the index doesn't exist or is empty. */
16415
+ get empty() {
16416
+ return this.entries.length === 0;
16417
+ }
16418
+ /** Total number of indexed chunks. */
16419
+ get size() {
16420
+ return this.entries.length;
16421
+ }
16422
+ /** Read-only view, mostly for tests. */
16423
+ get all() {
16424
+ return this.entries;
16425
+ }
16426
+ /** Last-known mtime per indexed file (ms epoch) for incremental rebuilds. */
16427
+ fileMtimes() {
16428
+ const out = /* @__PURE__ */ new Map();
16429
+ for (const [p, group] of this.byPath) {
16430
+ const first = group[0];
16431
+ if (first) out.set(p, first.mtimeMs);
16432
+ }
16433
+ return out;
16434
+ }
16435
+ /** Append entries to in-memory state and to disk. Re-indexes the
16436
+ * `byPath` map. Caller is responsible for L2-normalizing each
16437
+ * embedding before calling — the search hot path assumes unit vectors. */
16438
+ async add(entries) {
16439
+ if (entries.length === 0) return;
16440
+ if (this.dim === 0) this.dim = entries[0].embedding.length;
16441
+ const lines = [];
16442
+ for (const e of entries) {
16443
+ if (e.embedding.length !== this.dim) {
16444
+ throw new Error(
16445
+ `embedding dim mismatch: expected ${this.dim}, got ${e.embedding.length} for ${e.path}:${e.startLine}`
16446
+ );
16447
+ }
16448
+ this.entries.push(e);
16449
+ const list = this.byPath.get(e.path);
16450
+ if (list) list.push(e);
16451
+ else this.byPath.set(e.path, [e]);
16452
+ lines.push(serializeEntry(e));
16453
+ }
16454
+ await fs4.mkdir(this.indexDir, { recursive: true });
16455
+ await fs4.appendFile(path3.join(this.indexDir, DATA_FILE), `${lines.join("\n")}
16456
+ `, "utf8");
16457
+ await this.writeMeta();
16458
+ }
16459
+ /**
16460
+ * Drop every entry whose `path` is in `paths`. Used by incremental
16461
+ * rebuild: when a file's mtime changes, the existing entries for
16462
+ * it are evicted before re-chunking + re-embedding. Implementation
16463
+ * rewrites the JSONL — append-only is fine for adds, but deletes
16464
+ * need a compaction pass.
16465
+ */
16466
+ async remove(paths) {
16467
+ if (paths.length === 0) return 0;
16468
+ const drop = new Set(paths);
16469
+ const before = this.entries.length;
16470
+ this.entries = this.entries.filter((e) => !drop.has(e.path));
16471
+ for (const p of paths) this.byPath.delete(p);
16472
+ const removed = before - this.entries.length;
16473
+ if (removed > 0) await this.flush();
16474
+ return removed;
16475
+ }
16476
+ /**
16477
+ * Top-K cosine search. `query` MUST already be L2-normalized — the
16478
+ * caller embeds + normalizes once per query. Filtering hits by
16479
+ * minimum score (`minScore`) is optional; tune via UI to suppress
16480
+ * weakly relevant snippets that distract the model.
16481
+ */
16482
+ search(query, topK = 8, minScore = 0) {
16483
+ if (this.entries.length === 0) return [];
16484
+ if (query.length !== this.dim && this.dim !== 0) {
16485
+ throw new Error(`query dim ${query.length} \u2260 index dim ${this.dim}`);
16486
+ }
16487
+ const heap = [];
16488
+ for (const entry of this.entries) {
16489
+ const score = dot(query, entry.embedding);
16490
+ if (score < minScore) continue;
16491
+ if (heap.length < topK) {
16492
+ heap.push({ entry, score });
16493
+ if (heap.length === topK) heap.sort((a, b) => a.score - b.score);
16494
+ } else if (score > heap[0].score) {
16495
+ heap[0] = { entry, score };
16496
+ for (let i = 0; i < heap.length - 1; i++) {
16497
+ if (heap[i].score > heap[i + 1].score) {
16498
+ const tmp = heap[i];
16499
+ heap[i] = heap[i + 1];
16500
+ heap[i + 1] = tmp;
16501
+ }
16502
+ }
16503
+ }
16504
+ }
16505
+ return heap.sort((a, b) => b.score - a.score);
16506
+ }
16507
+ /**
16508
+ * Rewrite the JSONL on disk with the current in-memory state. Used
16509
+ * after `remove` and from `flush`. We write to a temp file and
16510
+ * rename so a Ctrl+C mid-write never leaves the index half-empty.
16511
+ */
16512
+ async flush() {
16513
+ await fs4.mkdir(this.indexDir, { recursive: true });
16514
+ const tmp = path3.join(this.indexDir, `${DATA_FILE}.tmp`);
16515
+ const final = path3.join(this.indexDir, DATA_FILE);
16516
+ const lines = this.entries.map(serializeEntry).join("\n");
16517
+ await fs4.writeFile(tmp, lines.length > 0 ? `${lines}
16518
+ ` : "", "utf8");
16519
+ await fs4.rename(tmp, final);
16520
+ await this.writeMeta();
16521
+ }
16522
+ async writeMeta() {
16523
+ const meta = {
16524
+ version: STORE_VERSION,
16525
+ model: this.model,
16526
+ dim: this.dim,
16527
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16528
+ };
16529
+ await fs4.writeFile(
16530
+ path3.join(this.indexDir, META_FILE),
16531
+ `${JSON.stringify(meta, null, 2)}
16532
+ `,
16533
+ "utf8"
16534
+ );
16535
+ }
16536
+ /** Drop everything from disk + memory. Used by `--rebuild`. */
16537
+ async wipe() {
16538
+ this.entries = [];
16539
+ this.byPath.clear();
16540
+ this.dim = 0;
16541
+ await fs4.rm(path3.join(this.indexDir, DATA_FILE), { force: true });
16542
+ await fs4.rm(path3.join(this.indexDir, META_FILE), { force: true });
16543
+ }
16544
+ };
16545
+ async function openStore(indexDir, model2) {
16546
+ const store = new SemanticStore(indexDir, model2);
16547
+ const dataPath = path3.join(indexDir, DATA_FILE);
16548
+ const metaPath = path3.join(indexDir, META_FILE);
16549
+ let meta = null;
16550
+ try {
16551
+ const raw2 = await fs4.readFile(metaPath, "utf8");
16552
+ meta = JSON.parse(raw2);
16553
+ } catch {
16554
+ }
16555
+ if (meta) {
16556
+ if (meta.version !== STORE_VERSION) {
16557
+ throw new Error(
16558
+ `Index format version ${meta.version} does not match current ${STORE_VERSION}. Run \`reasonix index --rebuild\`.`
16559
+ );
16560
+ }
16561
+ if (meta.model !== model2) {
16562
+ throw new Error(
16563
+ `Index was built with model "${meta.model}" but current is "${model2}". Run \`reasonix index --rebuild\`.`
16564
+ );
16565
+ }
16566
+ }
16567
+ let raw;
16568
+ try {
16569
+ raw = await fs4.readFile(dataPath, "utf8");
16570
+ } catch {
16571
+ return store;
16572
+ }
16573
+ for (const line of raw.split("\n")) {
16574
+ if (line.length === 0) continue;
16575
+ try {
16576
+ const entry = deserializeEntry(line);
16577
+ store.dim = entry.embedding.length;
16578
+ store.entries.push(entry);
16579
+ const map = store.byPath;
16580
+ const list = map.get(entry.path);
16581
+ if (list) list.push(entry);
16582
+ else map.set(entry.path, [entry]);
16583
+ } catch {
16584
+ }
16585
+ }
16586
+ return store;
16587
+ }
16588
+ function normalize(v) {
16589
+ let sum = 0;
16590
+ for (let i = 0; i < v.length; i++) sum += v[i] * v[i];
16591
+ const inv = sum > 0 ? 1 / Math.sqrt(sum) : 0;
16592
+ for (let i = 0; i < v.length; i++) v[i] = v[i] * inv;
16593
+ return v;
16594
+ }
16595
+ function dot(a, b) {
16596
+ let s = 0;
16597
+ for (let i = 0; i < a.length; i++) s += a[i] * b[i];
16598
+ return s;
16599
+ }
16600
+ function serializeEntry(e) {
16601
+ const buf = Buffer.from(e.embedding.buffer, e.embedding.byteOffset, e.embedding.byteLength);
16602
+ return JSON.stringify({
16603
+ p: e.path,
16604
+ s: e.startLine,
16605
+ e: e.endLine,
16606
+ m: e.mtimeMs,
16607
+ t: e.text,
16608
+ v: buf.toString("base64")
16609
+ });
16610
+ }
16611
+ function deserializeEntry(line) {
16612
+ const parsed = JSON.parse(line);
16613
+ const bytes = Buffer.from(parsed.v, "base64");
16614
+ const f32 = new Float32Array(
16615
+ bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
16616
+ );
16617
+ return {
16618
+ path: parsed.p,
16619
+ startLine: parsed.s,
16620
+ endLine: parsed.e,
16621
+ mtimeMs: parsed.m,
16622
+ text: parsed.t,
16623
+ embedding: f32
16624
+ };
16625
+ }
16626
+
16627
+ // src/index/semantic/builder.ts
16628
+ var INDEX_DIR_NAME = path4.join(".reasonix", "semantic");
16629
+ async function buildIndex(root, opts = {}) {
16630
+ const t0 = Date.now();
16631
+ const indexDir = path4.join(root, INDEX_DIR_NAME);
16632
+ const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });
16633
+ if (!probe.ok) {
16634
+ throw new Error(
16635
+ `Ollama is not reachable: ${probe.error}. Install from https://ollama.com, then \`ollama serve\` and \`ollama pull ${opts.model ?? "nomic-embed-text"}\`.`
16636
+ );
16637
+ }
16638
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
16639
+ const store = await openStore(indexDir, model2);
16640
+ if (opts.rebuild) await store.wipe();
16641
+ const lastMtimes = store.fileMtimes();
16642
+ const seenPaths = /* @__PURE__ */ new Set();
16643
+ const fileChunks = /* @__PURE__ */ new Map();
16644
+ let filesScanned = 0;
16645
+ let filesSkipped = 0;
16646
+ for await (const chunk of walkChunks(root, {
16647
+ windowLines: opts.windowLines,
16648
+ overlap: opts.overlap,
16649
+ maxFileBytes: opts.maxFileBytes
16650
+ })) {
16651
+ seenPaths.add(chunk.path);
16652
+ let bucket = fileChunks.get(chunk.path);
16653
+ if (!bucket) {
16654
+ filesScanned++;
16655
+ const abs = path4.join(root, chunk.path);
16656
+ let mtimeMs = 0;
16657
+ try {
16658
+ const stat = await fs5.stat(abs);
16659
+ mtimeMs = stat.mtimeMs;
16660
+ } catch {
16661
+ continue;
16662
+ }
16663
+ const last = lastMtimes.get(chunk.path);
16664
+ if (last !== void 0 && last === mtimeMs && !opts.rebuild) {
16665
+ filesSkipped++;
16666
+ continue;
16667
+ }
16668
+ bucket = { chunks: [], mtimeMs };
16669
+ fileChunks.set(chunk.path, bucket);
16670
+ }
16671
+ bucket.chunks.push(chunk);
16672
+ opts.onProgress?.({ phase: "scan", filesScanned });
16673
+ }
16674
+ const deletedPaths = [];
16675
+ for (const oldPath of lastMtimes.keys()) {
16676
+ if (!seenPaths.has(oldPath)) deletedPaths.push(oldPath);
16677
+ }
16678
+ const replacePaths = [...fileChunks.keys()].filter((p) => lastMtimes.has(p));
16679
+ const removed = await store.remove([...deletedPaths, ...replacePaths]);
16680
+ let chunksAdded = 0;
16681
+ let chunksSkipped = 0;
16682
+ const filesChanged = fileChunks.size;
16683
+ let chunksTotal = 0;
16684
+ for (const { chunks } of fileChunks.values()) chunksTotal += chunks.length;
16685
+ let chunksDone = 0;
16686
+ for (const [, bucket] of fileChunks) {
16687
+ if (bucket.chunks.length === 0) continue;
16688
+ const texts = bucket.chunks.map((c) => c.text);
16689
+ const vectors = await embedAll(texts, {
16690
+ ...opts,
16691
+ onProgress: (done, total) => {
16692
+ opts.onProgress?.({
16693
+ phase: "embed",
16694
+ filesScanned,
16695
+ filesChanged,
16696
+ chunksTotal,
16697
+ chunksDone: chunksDone + done
16698
+ });
16699
+ if (done === total) chunksDone += total;
16700
+ },
16701
+ onError: (idx, err) => {
16702
+ chunksSkipped++;
16703
+ const c = bucket.chunks[idx];
16704
+ const where = c ? `${c.path}:${c.startLine}-${c.endLine}` : `chunk #${idx}`;
16705
+ const msg = err instanceof Error ? err.message : String(err);
16706
+ process.stderr.write(`
16707
+ ! skipped ${where}: ${msg}
16708
+ `);
16709
+ }
16710
+ });
16711
+ const entries = [];
16712
+ for (let i = 0; i < bucket.chunks.length; i++) {
16713
+ const vec = vectors[i];
16714
+ if (!vec) continue;
16715
+ const c = bucket.chunks[i];
16716
+ if (!c) continue;
16717
+ normalize(vec);
16718
+ entries.push({
16719
+ path: c.path,
16720
+ startLine: c.startLine,
16721
+ endLine: c.endLine,
16722
+ text: c.text,
16723
+ embedding: vec,
16724
+ mtimeMs: bucket.mtimeMs
16725
+ });
16726
+ }
16727
+ if (entries.length > 0) await store.add(entries);
16728
+ chunksAdded += entries.length;
16729
+ }
16730
+ opts.onProgress?.({
16731
+ phase: "done",
16732
+ filesScanned,
16733
+ filesSkipped,
16734
+ filesChanged,
16735
+ chunksTotal,
16736
+ chunksDone
16737
+ });
16738
+ return {
16739
+ filesScanned,
16740
+ filesChanged,
16741
+ chunksAdded,
16742
+ chunksRemoved: removed,
16743
+ chunksSkipped,
16744
+ durationMs: Date.now() - t0
16745
+ };
16746
+ }
16747
+ async function querySemantic(root, query, opts = {}) {
16748
+ const indexDir = path4.join(root, INDEX_DIR_NAME);
16749
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
16750
+ const store = await openStore(indexDir, model2);
16751
+ if (store.empty) return null;
16752
+ const qvec = await embed(query, opts);
16753
+ normalize(qvec);
16754
+ return store.search(qvec, opts.topK ?? 8, opts.minScore ?? 0.3);
16755
+ }
16756
+ async function indexExists(root) {
16757
+ const meta = path4.join(root, INDEX_DIR_NAME, "index.meta.json");
16758
+ try {
16759
+ await fs5.access(meta);
16760
+ return true;
16761
+ } catch {
16762
+ return false;
16763
+ }
16764
+ }
16765
+
16766
+ // src/index/semantic/tool.ts
16767
+ async function registerSemanticSearchTool(registry, opts) {
16768
+ if (!await indexExists(opts.root)) return false;
16769
+ const defaultTopK = opts.defaultTopK ?? 8;
16770
+ const defaultMinScore = opts.defaultMinScore ?? 0.3;
16771
+ registry.register({
16772
+ name: "semantic_search",
16773
+ 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.",
16774
+ readOnly: true,
16775
+ parameters: {
16776
+ type: "object",
16777
+ properties: {
16778
+ query: {
16779
+ type: "string",
16780
+ description: "Natural-language description, phrased as a question or noun phrase: 'where do we validate the session cookie?' / 'retry backoff logic' / 'code that prevents user changes from immediately landing on disk'. Do NOT pass exact identifiers \u2014 those are search_content's job."
16781
+ },
16782
+ topK: {
16783
+ type: "integer",
16784
+ description: `Number of snippets to return (1..16). Default ${defaultTopK}.`
16785
+ },
16786
+ minScore: {
16787
+ type: "number",
16788
+ description: `Drop snippets with cosine score below this (0..1). Default ${defaultMinScore}. Raise for stricter matches; lower if the index is small.`
16789
+ }
16790
+ },
16791
+ required: ["query"]
16792
+ },
16793
+ fn: async (args, ctx) => {
16794
+ const hits = await querySemantic(opts.root, args.query, {
16795
+ topK: args.topK ?? defaultTopK,
16796
+ minScore: args.minScore ?? defaultMinScore,
16797
+ baseUrl: opts.baseUrl,
16798
+ model: opts.model,
16799
+ signal: ctx?.signal
16800
+ });
16801
+ if (hits === null) {
16802
+ return "No semantic index found for this project. Run `reasonix index` to build one.";
16803
+ }
16804
+ if (hits.length === 0) {
16805
+ return `query: ${args.query}
16806
+
16807
+ no matches above the score threshold (${args.minScore ?? defaultMinScore}).`;
16808
+ }
16809
+ return formatHits(args.query, hits);
16810
+ }
16811
+ });
16812
+ return true;
16813
+ }
16814
+ function formatHits(query, hits) {
16815
+ const lines = [`query: ${query}`, `
16816
+ results (${hits.length}):`];
16817
+ hits.forEach((h, i) => {
16818
+ const { entry, score } = h;
16819
+ lines.push(
16820
+ `
16821
+ ${i + 1}. ${entry.path}:${entry.startLine}-${entry.endLine} (score ${score.toFixed(3)})`
16822
+ );
16823
+ const preview = entry.text.split("\n").slice(0, 8).join("\n");
16824
+ lines.push(indentBlock(preview, " "));
16825
+ if (entry.text.split("\n").length > 8) {
16826
+ lines.push(
16827
+ ` \u2026(${entry.text.split("\n").length - 8} more lines \u2014 read_file ${entry.path}:${entry.startLine} for the full chunk)`
16828
+ );
16829
+ }
16830
+ });
16831
+ return lines.join("\n");
16832
+ }
16833
+ function indentBlock(text, prefix) {
16834
+ return text.split("\n").map((l) => prefix + l).join("\n");
16835
+ }
16836
+ async function bootstrapSemanticSearchInCodeMode(registry, rootDir, opts = {}) {
16837
+ if (await indexExists(rootDir)) {
16838
+ await registerSemanticSearchTool(registry, { ...opts, root: rootDir });
16839
+ return { enabled: true };
16840
+ }
16841
+ return { enabled: false };
16842
+ }
16843
+
16844
+ // src/cli/commands/code.tsx
15334
16845
  async function codeCommand(opts = {}) {
15335
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-POARCKKR.js");
16846
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-FMYQ7IDW.js");
15336
16847
  const rootDir = resolve7(opts.dir ?? process.cwd());
15337
16848
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
15338
16849
  const tools = new ToolRegistry();
@@ -15346,13 +16857,18 @@ async function codeCommand(opts = {}) {
15346
16857
  // via ShellConfirm mid-session takes effect on the next shell call
15347
16858
  // instead of waiting for `/new` or a relaunch.
15348
16859
  extraAllowed: () => loadProjectShellAllowed(rootDir),
16860
+ // `yolo` edit-mode disables shell confirmations entirely. Re-read
16861
+ // from config on each dispatch so /mode yolo (or Shift+Tab cycling
16862
+ // through to it) flips the gate live without forcing a relaunch.
16863
+ allowAll: () => loadEditMode() === "yolo",
15349
16864
  jobs: jobs2
15350
16865
  });
15351
16866
  registerPlanTool(tools);
15352
16867
  registerChoiceTool(tools);
15353
16868
  registerMemoryTools(tools, { projectRoot: rootDir });
16869
+ const semantic2 = await bootstrapSemanticSearchInCodeMode(tools, rootDir);
15354
16870
  process.stderr.write(
15355
- `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
16871
+ `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)${semantic2.enabled ? " \xB7 semantic_search on" : ""}
15356
16872
  `
15357
16873
  );
15358
16874
  process.once("exit", () => {
@@ -15361,7 +16877,7 @@ async function codeCommand(opts = {}) {
15361
16877
  await chatCommand({
15362
16878
  model: opts.model ?? "deepseek-v4-flash",
15363
16879
  harvest: opts.harvest ?? false,
15364
- system: codeSystemPrompt2(rootDir),
16880
+ system: codeSystemPrompt2(rootDir, { hasSemanticSearch: semantic2.enabled }),
15365
16881
  transcript: opts.transcript,
15366
16882
  session,
15367
16883
  seedTools: tools,
@@ -15378,32 +16894,32 @@ import { render as render2 } from "ink";
15378
16894
  import React29 from "react";
15379
16895
 
15380
16896
  // src/cli/ui/DiffApp.tsx
15381
- import { Box as Box25, Static as Static2, Text as Text22, useApp as useApp3, useInput } from "ink";
16897
+ import { Box as Box25, Static as Static2, Text as Text23, useApp as useApp3, useInput } from "ink";
15382
16898
  import React28, { useState as useState13 } from "react";
15383
16899
 
15384
16900
  // src/cli/ui/RecordView.tsx
15385
- import { Box as Box24, Text as Text21 } from "ink";
16901
+ import { Box as Box24, Text as Text22 } from "ink";
15386
16902
  import React27 from "react";
15387
16903
  function RecordView({ rec, compact: compact2 = false }) {
15388
16904
  const toolArgsMax = compact2 ? 120 : 200;
15389
16905
  const toolContentMax = compact2 ? 200 : 400;
15390
16906
  if (rec.role === "user") {
15391
16907
  const content = rec.content.includes("\n") ? rec.content.split("\n").join("\n ") : rec.content;
15392
- return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text21, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React27.createElement(Text21, null, content));
16908
+ return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React27.createElement(Text22, null, content));
15393
16909
  }
15394
16910
  if (rec.role === "assistant_final") {
15395
- return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text21, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React27.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React27.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React27.createElement(Text21, null, rec.content) : /* @__PURE__ */ React27.createElement(Text21, { dimColor: true, italic: true }, "(tool-call response only)"));
16911
+ return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text22, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React27.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React27.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React27.createElement(Text22, null, rec.content) : /* @__PURE__ */ React27.createElement(Text22, { dimColor: true, italic: true }, "(tool-call response only)"));
15396
16912
  }
15397
16913
  if (rec.role === "tool") {
15398
- return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text21, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, " args: ", truncate2(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, " \u2192 ", truncate2(rec.content, toolContentMax)));
16914
+ return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " args: ", truncate2(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " \u2192 ", truncate2(rec.content, toolContentMax)));
15399
16915
  }
15400
16916
  if (rec.role === "error") {
15401
- return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text21, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React27.createElement(Text21, { color: "red" }, rec.error ?? rec.content));
16917
+ return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React27.createElement(Text22, { color: "red" }, rec.error ?? rec.content));
15402
16918
  }
15403
16919
  if (rec.role === "done" || rec.role === "assistant_delta") {
15404
16920
  return null;
15405
16921
  }
15406
- return /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, "[", rec.role, "] ", rec.content));
16922
+ return /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, "[", rec.role, "] ", rec.content));
15407
16923
  }
15408
16924
  function CacheBadge({ usage }) {
15409
16925
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -15412,7 +16928,7 @@ function CacheBadge({ usage }) {
15412
16928
  if (total === 0) return null;
15413
16929
  const pct2 = hit / total * 100;
15414
16930
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
15415
- return /* @__PURE__ */ React27.createElement(Text21, null, /* @__PURE__ */ React27.createElement(Text21, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React27.createElement(Text21, { color }, pct2.toFixed(1), "%"));
16931
+ return /* @__PURE__ */ React27.createElement(Text22, null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React27.createElement(Text22, { color }, pct2.toFixed(1), "%"));
15416
16932
  }
15417
16933
  function truncate2(s, max) {
15418
16934
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -15446,7 +16962,7 @@ function DiffApp({ report }) {
15446
16962
  }
15447
16963
  });
15448
16964
  const pair = report.pairs[idx];
15449
- return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column" }, /* @__PURE__ */ React28.createElement(DiffHeader, { report }), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text22, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React28.createElement(Text22, null, pair ? /* @__PURE__ */ React28.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React28.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React28.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React28.createElement(Text22, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React28.createElement(Text22, null, pair.divergenceNote)) : null, /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "j"), "/", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "k"), "/", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "N"), "/", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "g"), "/", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React28.createElement(Text22, { bold: true }, "q"), " ", "quit")));
16965
+ return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column" }, /* @__PURE__ */ React28.createElement(DiffHeader, { report }), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text23, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React28.createElement(Text23, null, pair ? /* @__PURE__ */ React28.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React28.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React28.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React28.createElement(Text23, null, pair.divergenceNote)) : null, /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "j"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "k"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "N"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "g"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "q"), " ", "quit")));
15450
16966
  }
15451
16967
  function DiffHeader({ report }) {
15452
16968
  const a = report.a;
@@ -15464,7 +16980,7 @@ function DiffHeader({ report }) {
15464
16980
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
15465
16981
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
15466
16982
  }
15467
- return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React28.createElement(Box25, { justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text22, null, /* @__PURE__ */ React28.createElement(Text22, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React28.createElement(Text22, { color: "blue" }, a.label), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, " vs B="), /* @__PURE__ */ React28.createElement(Text22, { color: "magenta" }, b.label)), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React28.createElement(Text22, null, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, "cache "), /* @__PURE__ */ React28.createElement(Text22, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text22, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text22, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React28.createElement(Text22, null, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, "cost "), /* @__PURE__ */ React28.createElement(Text22, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text22, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text22, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React28.createElement(Text22, null, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true }, "model calls "), /* @__PURE__ */ React28.createElement(Text22, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true, italic: true }, prefixLine)) : null);
16983
+ return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React28.createElement(Box25, { justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React28.createElement(Text23, { color: "blue" }, a.label), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " vs B="), /* @__PURE__ */ React28.createElement(Text23, { color: "magenta" }, b.label)), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "cache "), /* @__PURE__ */ React28.createElement(Text23, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text23, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text23, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "cost "), /* @__PURE__ */ React28.createElement(Text23, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text23, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text23, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "model calls "), /* @__PURE__ */ React28.createElement(Text23, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true, italic: true }, prefixLine)) : null);
15468
16984
  }
15469
16985
  function Pane({
15470
16986
  label,
@@ -15480,21 +16996,21 @@ function Pane({
15480
16996
  borderStyle: "single",
15481
16997
  borderColor: headerColor
15482
16998
  },
15483
- /* @__PURE__ */ React28.createElement(Text22, { color: headerColor, bold: true }, label),
15484
- records.length === 0 ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text22, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React28.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React28.createElement(RecordView, { key, rec, compact: true }))
16999
+ /* @__PURE__ */ React28.createElement(Text23, { color: headerColor, bold: true }, label),
17000
+ records.length === 0 ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React28.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React28.createElement(RecordView, { key, rec, compact: true }))
15485
17001
  );
15486
17002
  }
15487
17003
  function KindBadge({ kind }) {
15488
17004
  if (kind === "match") {
15489
- return /* @__PURE__ */ React28.createElement(Text22, { color: "green" }, "\u2713 match");
17005
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "green" }, "\u2713 match");
15490
17006
  }
15491
17007
  if (kind === "diverge") {
15492
- return /* @__PURE__ */ React28.createElement(Text22, { color: "yellow" }, "\u2605 diverge");
17008
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "\u2605 diverge");
15493
17009
  }
15494
17010
  if (kind === "only_in_a") {
15495
- return /* @__PURE__ */ React28.createElement(Text22, { color: "blue" }, "\u2190 only in A");
17011
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "blue" }, "\u2190 only in A");
15496
17012
  }
15497
- return /* @__PURE__ */ React28.createElement(Text22, { color: "magenta" }, "\u2192 only in B");
17013
+ return /* @__PURE__ */ React28.createElement(Text23, { color: "magenta" }, "\u2192 only in B");
15498
17014
  }
15499
17015
  function paneRecords(pair, side) {
15500
17016
  if (!pair) return [];
@@ -15535,6 +17051,200 @@ markdown report written to ${opts.mdPath}`);
15535
17051
  console.log(renderSummaryTable(report));
15536
17052
  }
15537
17053
 
17054
+ // src/cli/commands/index.ts
17055
+ import { resolve as resolve8 } from "path";
17056
+
17057
+ // src/index/semantic/preflight.ts
17058
+ import { stdin as stdin2, stdout } from "process";
17059
+ import { createInterface } from "readline/promises";
17060
+ async function ollamaPreflight(opts) {
17061
+ const log = opts.log ?? ((line) => process.stderr.write(line));
17062
+ const status2 = await checkOllamaStatus(opts.model, opts.baseUrl);
17063
+ if (!status2.binaryFound) {
17064
+ log(t("ollamaNotFound"));
17065
+ return false;
17066
+ }
17067
+ if (!status2.daemonRunning) {
17068
+ if (!opts.interactive && !opts.yesToAll) {
17069
+ log(t("daemonNotReachableHint"));
17070
+ return false;
17071
+ }
17072
+ const ok = opts.yesToAll || await confirm(t("daemonStartConfirm"), true);
17073
+ if (!ok) {
17074
+ log(t("daemonAbortStart"));
17075
+ return false;
17076
+ }
17077
+ log(t("daemonStarting"));
17078
+ const started = await startOllamaDaemon({ baseUrl: opts.baseUrl, timeoutMs: 15e3 });
17079
+ if (!started.ready) {
17080
+ log(t("daemonStartTimeout"));
17081
+ return false;
17082
+ }
17083
+ log(t("daemonReady", { pid: started.pid ? ` (pid ${started.pid})` : "" }));
17084
+ }
17085
+ const after = status2.daemonRunning ? status2 : await checkOllamaStatus(opts.model, opts.baseUrl);
17086
+ if (!after.modelPulled) {
17087
+ if (!opts.interactive && !opts.yesToAll) {
17088
+ log(t("modelNotPulledHint", { model: opts.model }));
17089
+ return false;
17090
+ }
17091
+ const ok = opts.yesToAll || await confirm(t("modelPullConfirm", { model: opts.model }), true);
17092
+ if (!ok) {
17093
+ log(t("modelAbortPull"));
17094
+ return false;
17095
+ }
17096
+ log(t("modelPulling", { model: opts.model }));
17097
+ const ESC = String.fromCharCode(27);
17098
+ const ANSI_CSI = new RegExp(`${ESC}\\[[0-9;]*[A-Za-z]`, "g");
17099
+ const code = await pullOllamaModel(opts.model, {
17100
+ onLine: (line) => {
17101
+ const cleaned = line.replace(ANSI_CSI, "").trim();
17102
+ if (cleaned.length === 0) return;
17103
+ log(` ${cleaned}
17104
+ `);
17105
+ }
17106
+ });
17107
+ if (code !== 0) {
17108
+ log(t("modelPullFailed", { model: opts.model, code }));
17109
+ return false;
17110
+ }
17111
+ log(t("modelPulled", { model: opts.model }));
17112
+ }
17113
+ return true;
17114
+ }
17115
+ async function confirm(question, defaultYes) {
17116
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
17117
+ const rl = createInterface({ input: stdin2, output: stdout });
17118
+ try {
17119
+ const raw = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
17120
+ if (raw === "") return defaultYes;
17121
+ return raw === "y" || raw === "yes";
17122
+ } finally {
17123
+ rl.close();
17124
+ }
17125
+ }
17126
+
17127
+ // src/cli/commands/index.ts
17128
+ async function indexCommand(opts = {}) {
17129
+ const root = resolve8(opts.dir ?? process.cwd());
17130
+ const tty = process.stderr.isTTY === true && process.stdin.isTTY === true;
17131
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
17132
+ const preflightOk = await ollamaPreflight({
17133
+ model: model2,
17134
+ baseUrl: opts.ollamaUrl,
17135
+ interactive: tty && !opts.yes,
17136
+ yesToAll: opts.yes ?? false
17137
+ });
17138
+ if (!preflightOk) process.exit(1);
17139
+ const writer = makeProgressWriter(tty);
17140
+ const t0 = Date.now();
17141
+ let result;
17142
+ try {
17143
+ result = await buildIndex(root, {
17144
+ rebuild: opts.rebuild,
17145
+ model: model2,
17146
+ baseUrl: opts.ollamaUrl,
17147
+ onProgress: (p) => writer.update(p)
17148
+ });
17149
+ } catch (err) {
17150
+ writer.clear();
17151
+ const msg = err instanceof Error ? err.message : String(err);
17152
+ process.stderr.write(t("indexFailed", { msg }));
17153
+ process.exit(1);
17154
+ }
17155
+ writer.clear();
17156
+ const seconds = ((Date.now() - t0) / 1e3).toFixed(1);
17157
+ const successKey = result.chunksSkipped > 0 ? "indexSuccessWithSkips" : "indexSuccess";
17158
+ process.stderr.write(
17159
+ t(successKey, {
17160
+ scanned: result.filesScanned,
17161
+ changed: result.filesChanged,
17162
+ added: result.chunksAdded,
17163
+ removed: result.chunksRemoved,
17164
+ skipped: result.chunksSkipped,
17165
+ seconds
17166
+ })
17167
+ );
17168
+ if (result.filesChanged === 0 && !opts.rebuild) {
17169
+ process.stderr.write(t("indexNothingToDo"));
17170
+ }
17171
+ }
17172
+ var SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17173
+ var SPINNER_INTERVAL_MS = 120;
17174
+ function makeProgressWriter(tty) {
17175
+ if (!tty) return makeNonTtyWriter();
17176
+ return makeTtyWriter();
17177
+ }
17178
+ function makeNonTtyWriter() {
17179
+ let lastPhase = null;
17180
+ let lastChunks = 0;
17181
+ return {
17182
+ update(p) {
17183
+ if (p.phase !== lastPhase) {
17184
+ lastPhase = p.phase;
17185
+ if (p.phase === "scan") {
17186
+ process.stderr.write(t("progressScanLine"));
17187
+ } else if (p.phase === "embed") {
17188
+ process.stderr.write(
17189
+ t("progressEmbedLine", {
17190
+ total: p.chunksTotal ?? 0,
17191
+ files: p.filesChanged ?? 0
17192
+ })
17193
+ );
17194
+ }
17195
+ }
17196
+ if (p.phase === "embed" && p.chunksDone !== void 0 && p.chunksDone - lastChunks >= 50) {
17197
+ lastChunks = p.chunksDone;
17198
+ process.stderr.write(
17199
+ t("progressEmbedHeartbeat", {
17200
+ done: p.chunksDone,
17201
+ total: p.chunksTotal ?? "?"
17202
+ })
17203
+ );
17204
+ }
17205
+ },
17206
+ clear() {
17207
+ }
17208
+ };
17209
+ }
17210
+ function makeTtyWriter() {
17211
+ let status2 = t("progressStarting");
17212
+ let lastLineLen = 0;
17213
+ let frameIdx = 0;
17214
+ const startTs = Date.now();
17215
+ const repaint = () => {
17216
+ const frame = SPINNER_FRAMES2[frameIdx % SPINNER_FRAMES2.length];
17217
+ frameIdx++;
17218
+ const elapsed = ((Date.now() - startTs) / 1e3).toFixed(1);
17219
+ const line = `${frame} ${status2} ${elapsed}s`;
17220
+ const padded = line + " ".repeat(Math.max(0, lastLineLen - line.length));
17221
+ process.stderr.write(`\r${padded}`);
17222
+ lastLineLen = line.length;
17223
+ };
17224
+ repaint();
17225
+ const interval = setInterval(repaint, SPINNER_INTERVAL_MS);
17226
+ return {
17227
+ update(p) {
17228
+ if (p.phase === "scan") {
17229
+ status2 = t("progressScan", { files: p.filesScanned ?? 0 });
17230
+ } else if (p.phase === "embed") {
17231
+ const done = p.chunksDone ?? 0;
17232
+ const total = p.chunksTotal ?? 0;
17233
+ const pct2 = total > 0 ? (done / total * 100).toFixed(0) : "0";
17234
+ status2 = t("progressEmbed", { done, total, pct: pct2 });
17235
+ }
17236
+ repaint();
17237
+ },
17238
+ clear() {
17239
+ clearInterval(interval);
17240
+ if (lastLineLen > 0) {
17241
+ process.stderr.write(`\r${" ".repeat(lastLineLen)}\r`);
17242
+ lastLineLen = 0;
17243
+ }
17244
+ }
17245
+ };
17246
+ }
17247
+
15538
17248
  // src/cli/commands/mcp-inspect.ts
15539
17249
  async function mcpInspectCommand(opts) {
15540
17250
  const spec = parseMcpSpec(opts.spec);
@@ -15581,9 +17291,9 @@ function formatSection(title, section, render5) {
15581
17291
  for (const item of section.items) lines.push(` ${render5(item)}`);
15582
17292
  return lines.join("\n");
15583
17293
  }
15584
- function toolLine(t) {
15585
- const desc = t.description ? ` \u2014 ${oneLine(t.description, 80)}` : "";
15586
- return `\xB7 ${t.name}${desc}`;
17294
+ function toolLine(t2) {
17295
+ const desc = t2.description ? ` \u2014 ${oneLine(t2.description, 80)}` : "";
17296
+ return `\xB7 ${t2.name}${desc}`;
15587
17297
  }
15588
17298
  function resourceLine(r) {
15589
17299
  const mime = r.mimeType ? ` [${r.mimeType}]` : "";
@@ -15669,7 +17379,7 @@ import { render as render3 } from "ink";
15669
17379
  import React31 from "react";
15670
17380
 
15671
17381
  // src/cli/ui/ReplayApp.tsx
15672
- import { Box as Box26, Static as Static3, Text as Text23, useApp as useApp4, useInput as useInput2 } from "ink";
17382
+ import { Box as Box26, Static as Static3, Text as Text24, useApp as useApp4, useInput as useInput2 } from "ink";
15673
17383
  import React30, { useMemo as useMemo4, useState as useState14 } from "react";
15674
17384
  function ReplayApp({ meta, pages }) {
15675
17385
  const { exit: exit2 } = useApp4();
@@ -15717,7 +17427,7 @@ function ReplayApp({ meta, pages }) {
15717
17427
  model: cumStats.models[0] ?? meta?.model ?? "?",
15718
17428
  prefixHash
15719
17429
  }
15720
- ), /* @__PURE__ */ React30.createElement(Box26, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React30.createElement(Box26, { justifyContent: "space-between" }, /* @__PURE__ */ React30.createElement(Text23, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React30.createElement(Text23, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React30.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React30.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React30.createElement(Text23, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React30.createElement(Box26, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React30.createElement(Text23, { dimColor: true }, /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "j"), "/", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "k"), "/", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React30.createElement(Text23, { bold: true }, "q"), " quit")));
17430
+ ), /* @__PURE__ */ React30.createElement(Box26, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React30.createElement(Box26, { justifyContent: "space-between" }, /* @__PURE__ */ React30.createElement(Text24, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React30.createElement(Text24, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React30.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React30.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React30.createElement(Text24, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React30.createElement(Box26, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React30.createElement(Text24, { dimColor: true }, /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "j"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "k"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "q"), " quit")));
15721
17431
  }
15722
17432
 
15723
17433
  // src/cli/commands/replay.ts
@@ -15823,12 +17533,12 @@ function oneLine2(s, max = 200) {
15823
17533
  }
15824
17534
 
15825
17535
  // src/cli/commands/run.ts
15826
- import { stdin as stdin2, stdout } from "process";
15827
- import { createInterface } from "readline/promises";
17536
+ import { stdin as stdin3, stdout as stdout2 } from "process";
17537
+ import { createInterface as createInterface2 } from "readline/promises";
15828
17538
  async function ensureApiKey() {
15829
17539
  const existing = loadApiKey();
15830
17540
  if (existing) return existing;
15831
- if (!stdin2.isTTY) {
17541
+ if (!stdin3.isTTY) {
15832
17542
  process.stderr.write(
15833
17543
  "DEEPSEEK_API_KEY is not set and stdin is not a TTY (cannot prompt).\nSet the env var, or run `reasonix chat` once interactively to save a key.\n"
15834
17544
  );
@@ -15837,7 +17547,7 @@ async function ensureApiKey() {
15837
17547
  process.stdout.write(
15838
17548
  "DeepSeek API key not configured.\nGet one at https://platform.deepseek.com/api_keys\n"
15839
17549
  );
15840
- const rl = createInterface({ input: stdin2, output: stdout });
17550
+ const rl = createInterface2({ input: stdin3, output: stdout2 });
15841
17551
  try {
15842
17552
  while (true) {
15843
17553
  const answer = (await rl.question("API key \u203A ")).trim();
@@ -15896,7 +17606,7 @@ async function runCommand2(opts) {
15896
17606
  system: opts.system,
15897
17607
  toolSpecs: tools?.specs()
15898
17608
  });
15899
- const loop = new CacheFirstLoop({
17609
+ const loop2 = new CacheFirstLoop({
15900
17610
  client,
15901
17611
  prefix,
15902
17612
  tools,
@@ -15921,7 +17631,7 @@ async function runCommand2(opts) {
15921
17631
  });
15922
17632
  }
15923
17633
  try {
15924
- for await (const ev of loop.step(opts.task)) {
17634
+ for await (const ev of loop2.step(opts.task)) {
15925
17635
  if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
15926
17636
  if (ev.role === "tool") process.stdout.write(`
15927
17637
  [tool ${ev.toolName}] ${ev.content}
@@ -15940,7 +17650,7 @@ async function runCommand2(opts) {
15940
17650
  } finally {
15941
17651
  transcriptStream?.end();
15942
17652
  }
15943
- const s = loop.stats.summary();
17653
+ const s = loop2.stats.summary();
15944
17654
  process.stdout.write(
15945
17655
  `
15946
17656
  \u2014 turns:${s.turns} cache:${(s.cacheHitRatio * 100).toFixed(1)}% cost:$${s.totalCostUsd.toFixed(6)} save-vs-claude:${s.savingsVsClaudePct.toFixed(1)}%
@@ -15988,14 +17698,14 @@ function listAll() {
15988
17698
  console.log("Resume: reasonix chat --session <name>");
15989
17699
  }
15990
17700
  function inspectSession(name, verbose) {
15991
- const path = sessionPath(name);
17701
+ const path5 = sessionPath(name);
15992
17702
  const messages = loadSessionMessages(name);
15993
17703
  if (messages.length === 0) {
15994
17704
  console.error(`no session named "${name}" (or it's empty).`);
15995
- console.error(`looked at: ${path}`);
17705
+ console.error(`looked at: ${path5}`);
15996
17706
  process.exit(1);
15997
17707
  }
15998
- console.log(`[session] ${name} ${messages.length} messages ${path}`);
17708
+ console.log(`[session] ${name} ${messages.length} messages ${path5}`);
15999
17709
  console.log("");
16000
17710
  let turnIndex = 0;
16001
17711
  for (const msg of messages) {
@@ -16037,7 +17747,7 @@ import { render as render4 } from "ink";
16037
17747
  import React33 from "react";
16038
17748
 
16039
17749
  // src/cli/ui/Wizard.tsx
16040
- import { Box as Box27, Text as Text24, useApp as useApp5, useInput as useInput3 } from "ink";
17750
+ import { Box as Box27, Text as Text25, useApp as useApp5, useInput as useInput3 } from "ink";
16041
17751
  import TextInput2 from "ink-text-input";
16042
17752
  import React32, { useState as useState15 } from "react";
16043
17753
 
@@ -16111,7 +17821,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16111
17821
  setStep("mcp");
16112
17822
  }
16113
17823
  }
16114
- ), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] confirm \xB7 [Esc] cancel")));
17824
+ ), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] confirm \xB7 [Esc] cancel")));
16115
17825
  }
16116
17826
  if (step === "mcp") {
16117
17827
  return /* @__PURE__ */ React32.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(
@@ -16165,8 +17875,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16165
17875
  }
16166
17876
  ), specs.map((spec, i) => (
16167
17877
  // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
16168
- /* @__PURE__ */ React32.createElement(Box27, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "\xB7 ", spec))
16169
- )), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { color: "red" }, error)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "[Enter] save \xB7 [Esc] cancel"))), /* @__PURE__ */ React32.createElement(
17878
+ /* @__PURE__ */ React32.createElement(Box27, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "\xB7 ", spec))
17879
+ )), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[Enter] save \xB7 [Esc] cancel"))), /* @__PURE__ */ React32.createElement(
16170
17880
  ReviewConfirm,
16171
17881
  {
16172
17882
  onConfirm: () => {
@@ -16192,7 +17902,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16192
17902
  }
16193
17903
  ));
16194
17904
  }
16195
- return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "[Enter] to exit")), /* @__PURE__ */ React32.createElement(ExitOnEnter, { onExit: exit2 }));
17905
+ return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[Enter] to exit")), /* @__PURE__ */ React32.createElement(ExitOnEnter, { onExit: exit2 }));
16196
17906
  }
16197
17907
  function ApiKeyStep({
16198
17908
  onSubmit,
@@ -16200,7 +17910,7 @@ function ApiKeyStep({
16200
17910
  onError
16201
17911
  }) {
16202
17912
  const [value, setValue] = useState15("");
16203
- return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React32.createElement(
17913
+ return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React32.createElement(
16204
17914
  TextInput2,
16205
17915
  {
16206
17916
  value,
@@ -16217,7 +17927,7 @@ function ApiKeyStep({
16217
17927
  mask: "\u2022",
16218
17928
  placeholder: "sk-..."
16219
17929
  }
16220
- )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { color: "red" }, error)) : value ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "preview: ", redactKey(value))) : null);
17930
+ )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : value ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "preview: ", redactKey(value))) : null);
16221
17931
  }
16222
17932
  function McpArgsStep({
16223
17933
  entry,
@@ -16226,7 +17936,7 @@ function McpArgsStep({
16226
17936
  onError
16227
17937
  }) {
16228
17938
  const [value, setValue] = useState15("");
16229
- return /* @__PURE__ */ React32.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column" }, /* @__PURE__ */ React32.createElement(Text24, null, entry.summary), entry.note ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, null, "Required parameter: "), /* @__PURE__ */ React32.createElement(Text24, { bold: true }, entry.userArgs)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React32.createElement(
17939
+ return /* @__PURE__ */ React32.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column" }, /* @__PURE__ */ React32.createElement(Text25, null, entry.summary), entry.note ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Required parameter: "), /* @__PURE__ */ React32.createElement(Text25, { bold: true }, entry.userArgs)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React32.createElement(
16230
17940
  TextInput2,
16231
17941
  {
16232
17942
  value,
@@ -16242,7 +17952,7 @@ function McpArgsStep({
16242
17952
  },
16243
17953
  placeholder: placeholderFor(entry)
16244
17954
  }
16245
- )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text24, { color: "red" }, error)) : null));
17955
+ )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : null));
16246
17956
  }
16247
17957
  function ReviewConfirm({ onConfirm }) {
16248
17958
  useInput3((_i, key) => {
@@ -16262,10 +17972,10 @@ function StepFrame({
16262
17972
  total,
16263
17973
  children
16264
17974
  }) {
16265
- return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text24, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React32.createElement(Text24, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1, flexDirection: "column" }, children));
17975
+ return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1, flexDirection: "column" }, children));
16266
17976
  }
16267
17977
  function SummaryLine({ label, value }) {
16268
- return /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text24, null, label.padEnd(12)), /* @__PURE__ */ React32.createElement(Text24, { bold: true }, value));
17978
+ return /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text25, null, label.padEnd(12)), /* @__PURE__ */ React32.createElement(Text25, { bold: true }, value));
16269
17979
  }
16270
17980
  function presetItems() {
16271
17981
  return ["fast", "smart", "max"].map((name) => ({
@@ -16339,7 +18049,7 @@ async function setupCommand(_opts = {}) {
16339
18049
  }
16340
18050
 
16341
18051
  // src/cli/commands/update.ts
16342
- import { spawn as spawn5 } from "child_process";
18052
+ import { spawn as spawn6 } from "child_process";
16343
18053
  function planUpdate(input) {
16344
18054
  const diff = compareVersions(input.current, input.latest);
16345
18055
  if (diff > 0) {
@@ -16369,13 +18079,13 @@ function planUpdate(input) {
16369
18079
  };
16370
18080
  }
16371
18081
  function defaultSpawn(argv) {
16372
- return new Promise((resolve8, reject) => {
16373
- const child = spawn5(argv[0], argv.slice(1), {
18082
+ return new Promise((resolve9, reject) => {
18083
+ const child = spawn6(argv[0], argv.slice(1), {
16374
18084
  stdio: "inherit",
16375
18085
  shell: process.platform === "win32"
16376
18086
  });
16377
18087
  child.once("error", reject);
16378
- child.once("exit", (code) => resolve8(code ?? 1));
18088
+ child.once("exit", (code) => resolve9(code ?? 1));
16379
18089
  });
16380
18090
  }
16381
18091
  async function updateCommand(opts = {}) {
@@ -16671,6 +18381,16 @@ program.command("update").description(
16671
18381
  ).option("--dry-run", "Print the plan without executing the install").action(async (opts) => {
16672
18382
  await updateCommand({ dryRun: !!opts.dryRun });
16673
18383
  });
18384
+ program.command("index").description(
18385
+ "Build (or incrementally refresh) a local semantic search index for the project so `reasonix code` can answer 'where do we\u2026' questions by meaning, not just by token. Uses Ollama as the embedding backend; missing daemon / model is offered to the user with a confirm prompt."
18386
+ ).option("--rebuild", "Wipe and rebuild from scratch").option("--model <name>", "Embedding model (default: nomic-embed-text)").option("--dir <path>", "Project root to index (default: cwd)").option("--ollama-url <url>", "Override Ollama base URL (default: http://localhost:11434)").option(
18387
+ "-y, --yes",
18388
+ "Skip preflight prompts \u2014 auto-start the daemon and pull the model if missing (use in scripts)"
18389
+ ).action(
18390
+ async (opts) => {
18391
+ await indexCommand(opts);
18392
+ }
18393
+ );
16674
18394
  program.parseAsync(process.argv).catch((err) => {
16675
18395
  console.error(err);
16676
18396
  process.exit(1);