@wrongstack/tools 0.264.0 → 0.265.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/audit.js +154 -11
  2. package/dist/audit.js.map +1 -1
  3. package/dist/bash.js +138 -2
  4. package/dist/bash.js.map +1 -1
  5. package/dist/batch-tool-use.js +1 -0
  6. package/dist/batch-tool-use.js.map +1 -1
  7. package/dist/builtin.d.ts +20 -1
  8. package/dist/builtin.js +661 -325
  9. package/dist/builtin.js.map +1 -1
  10. package/dist/circuit-breaker.d.ts +20 -0
  11. package/dist/circuit-breaker.js +40 -2
  12. package/dist/circuit-breaker.js.map +1 -1
  13. package/dist/codebase-index/index.d.ts +16 -0
  14. package/dist/codebase-index/index.js +59 -25
  15. package/dist/codebase-index/index.js.map +1 -1
  16. package/dist/codebase-index/worker.js +56 -25
  17. package/dist/codebase-index/worker.js.map +1 -1
  18. package/dist/diff.js +14 -7
  19. package/dist/diff.js.map +1 -1
  20. package/dist/document.js +14 -8
  21. package/dist/document.js.map +1 -1
  22. package/dist/edit.js +21 -15
  23. package/dist/edit.js.map +1 -1
  24. package/dist/exec.js +140 -3
  25. package/dist/exec.js.map +1 -1
  26. package/dist/fetch.js +1 -0
  27. package/dist/fetch.js.map +1 -1
  28. package/dist/format.js +153 -11
  29. package/dist/format.js.map +1 -1
  30. package/dist/git.js +1 -0
  31. package/dist/git.js.map +1 -1
  32. package/dist/glob.js +14 -7
  33. package/dist/glob.js.map +1 -1
  34. package/dist/grep.js +14 -7
  35. package/dist/grep.js.map +1 -1
  36. package/dist/index.d.ts +55 -3
  37. package/dist/index.js +819 -325
  38. package/dist/index.js.map +1 -1
  39. package/dist/install.js +153 -11
  40. package/dist/install.js.map +1 -1
  41. package/dist/json.js +1 -0
  42. package/dist/json.js.map +1 -1
  43. package/dist/lint.js +153 -11
  44. package/dist/lint.js.map +1 -1
  45. package/dist/logs.js +14 -7
  46. package/dist/logs.js.map +1 -1
  47. package/dist/memory.js +1 -0
  48. package/dist/memory.js.map +1 -1
  49. package/dist/mode.js +1 -0
  50. package/dist/mode.js.map +1 -1
  51. package/dist/outdated.js +16 -8
  52. package/dist/outdated.js.map +1 -1
  53. package/dist/pack.js +630 -324
  54. package/dist/pack.js.map +1 -1
  55. package/dist/patch.js +14 -7
  56. package/dist/patch.js.map +1 -1
  57. package/dist/process-registry.d.ts +56 -2
  58. package/dist/process-registry.js +138 -3
  59. package/dist/process-registry.js.map +1 -1
  60. package/dist/read.js +21 -16
  61. package/dist/read.js.map +1 -1
  62. package/dist/replace.js +14 -7
  63. package/dist/replace.js.map +1 -1
  64. package/dist/scaffold.js +14 -7
  65. package/dist/scaffold.js.map +1 -1
  66. package/dist/search.js +1 -0
  67. package/dist/search.js.map +1 -1
  68. package/dist/test.js +153 -11
  69. package/dist/test.js.map +1 -1
  70. package/dist/todo.js +1 -0
  71. package/dist/todo.js.map +1 -1
  72. package/dist/tool-help.js +1 -0
  73. package/dist/tool-help.js.map +1 -1
  74. package/dist/tool-icons.d.ts +20 -0
  75. package/dist/tool-icons.js +130 -0
  76. package/dist/tool-icons.js.map +1 -0
  77. package/dist/tool-search.js +1 -0
  78. package/dist/tool-search.js.map +1 -1
  79. package/dist/tool-use.js +1 -0
  80. package/dist/tool-use.js.map +1 -1
  81. package/dist/tree.js +14 -7
  82. package/dist/tree.js.map +1 -1
  83. package/dist/typecheck.js +153 -11
  84. package/dist/typecheck.js.map +1 -1
  85. package/dist/write.js +21 -15
  86. package/dist/write.js.map +1 -1
  87. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import * as fs4 from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { resolve, sep, dirname, join } from 'node:path';
4
4
  import * as Core from '@wrongstack/core';
5
- import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, emptyTaskFile, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, mutateTasks, emptyPlan, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
5
+ import { atomicWrite, unifiedDiff, detectNewlineStyle, normalizeToLf, toStyle, compileGlob, expectDefined, buildChildEnv, isPrivateIPv4, isPrivateIPv6, loadPlan, setPlanItemStatus, savePlan, loadTasks, saveTasks, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, mutateTasks, formatTaskList, formatPlan, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
6
6
  import { toErrorMessage } from '@wrongstack/core/utils';
7
7
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
8
8
  import * as os from 'node:os';
@@ -36,22 +36,29 @@ async function detectPackageManager(cwd) {
36
36
  function resolvePath(input, ctx) {
37
37
  return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.workingDir ?? ctx.cwd, input);
38
38
  }
39
+ function allowedRoots(ctx) {
40
+ return [path.resolve(ctx.projectRoot), path.resolve(Core.wstackGlobalRoot())];
41
+ }
42
+ function isInsideAny(target, roots) {
43
+ return roots.some((root) => {
44
+ const rel = path.relative(root, target);
45
+ return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
46
+ });
47
+ }
39
48
  function ensureInsideRoot(absPath, ctx) {
40
- if (ctx.allowOutsideProjectRoot) return path.resolve(absPath);
41
- const root = path.resolve(ctx.projectRoot);
42
49
  const target = path.resolve(absPath);
43
- const rel = path.relative(root, target);
44
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
45
- throw new Error(`Path "${absPath}" is outside project root "${root}"`);
46
- }
47
- return target;
50
+ if (ctx.allowOutsideProjectRoot) return target;
51
+ if (isInsideAny(target, allowedRoots(ctx))) return target;
52
+ throw new Error(`Path "${absPath}" is outside project root "${path.resolve(ctx.projectRoot)}"`);
48
53
  }
49
54
  function safeResolve(input, ctx) {
50
55
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
51
56
  }
52
57
  async function assertRealInsideRoot(absPath, ctx) {
53
58
  if (ctx.allowOutsideProjectRoot) return;
54
- const realRoot = await fs4.realpath(ctx.projectRoot).catch(() => path.resolve(ctx.projectRoot));
59
+ const realRoots = await Promise.all(
60
+ allowedRoots(ctx).map((r) => fs4.realpath(r).catch(() => path.resolve(r)))
61
+ );
55
62
  let probe = absPath;
56
63
  for (; ; ) {
57
64
  let real;
@@ -66,13 +73,10 @@ async function assertRealInsideRoot(absPath, ctx) {
66
73
  }
67
74
  throw err;
68
75
  }
69
- const rel = path.relative(realRoot, real);
70
- if (rel.startsWith("..") || path.isAbsolute(rel)) {
71
- throw new Error(
72
- `Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
73
- );
74
- }
75
- return;
76
+ if (isInsideAny(real, realRoots)) return;
77
+ throw new Error(
78
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
79
+ );
76
80
  }
77
81
  }
78
82
  async function safeResolveReal(input, ctx) {
@@ -173,6 +177,7 @@ var readTool = {
173
177
  permission: "auto",
174
178
  mutating: false,
175
179
  capabilities: ["fs.read"],
180
+ icon: "file",
176
181
  maxOutputBytes: 262144,
177
182
  timeoutMs: 5e3,
178
183
  inputSchema: {
@@ -245,6 +250,7 @@ var writeTool = {
245
250
  mutating: true,
246
251
  timeoutMs: 5e3,
247
252
  capabilities: ["fs.write"],
253
+ icon: "file",
248
254
  inputSchema: {
249
255
  type: "object",
250
256
  properties: {
@@ -308,6 +314,7 @@ var editTool = {
308
314
  permission: "confirm",
309
315
  mutating: true,
310
316
  capabilities: ["fs.write"],
317
+ icon: "edit",
311
318
  timeoutMs: 5e3,
312
319
  inputSchema: {
313
320
  type: "object",
@@ -475,6 +482,7 @@ var replaceTool = {
475
482
  permission: "confirm",
476
483
  mutating: true,
477
484
  capabilities: ["fs.write"],
485
+ icon: "edit",
478
486
  timeoutMs: 3e4,
479
487
  inputSchema: {
480
488
  type: "object",
@@ -676,6 +684,7 @@ var globTool = {
676
684
  permission: "auto",
677
685
  mutating: false,
678
686
  capabilities: ["fs.read"],
687
+ icon: "folder",
679
688
  maxOutputBytes: 65536,
680
689
  timeoutMs: 5e3,
681
690
  inputSchema: {
@@ -761,6 +770,7 @@ var grepTool = {
761
770
  permission: "auto",
762
771
  mutating: false,
763
772
  capabilities: ["fs.read"],
773
+ icon: "search",
764
774
  maxOutputBytes: 131072,
765
775
  timeoutMs: 1e4,
766
776
  inputSchema: {
@@ -1162,6 +1172,23 @@ var CircuitBreaker = class {
1162
1172
  lastSlowAt = null;
1163
1173
  /** Timestamp when the breaker was opened (for cooldown calculation). */
1164
1174
  openedAt = null;
1175
+ /**
1176
+ * Master enable flag. When false the breaker is bypassed: `beforeCall`
1177
+ * always returns true and `afterCall` records nothing. The class itself
1178
+ * defaults to enabled (so the standalone unit tests exercise tripping); the
1179
+ * ProcessRegistry flips this off until the user opts in via `/settings`.
1180
+ */
1181
+ enabled = true;
1182
+ /**
1183
+ * Fired (best-effort) when the breaker transitions into the `open` state.
1184
+ * The registry uses this to arm its auto kill/reset countdown.
1185
+ */
1186
+ onTrip;
1187
+ /**
1188
+ * Fired (best-effort) when the breaker returns to `closed` after having been
1189
+ * open/half-open. The registry uses this to cancel a pending kill/reset.
1190
+ */
1191
+ onReset;
1165
1192
  constructor(config = {}) {
1166
1193
  this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
1167
1194
  this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
@@ -1170,12 +1197,22 @@ var CircuitBreaker = class {
1170
1197
  this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
1171
1198
  this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
1172
1199
  }
1200
+ /** Toggle the master enable. Disabling resets to a clean `closed` state. */
1201
+ setEnabled(enabled) {
1202
+ if (this.enabled === enabled) return;
1203
+ this.enabled = enabled;
1204
+ if (!enabled) this._reset();
1205
+ }
1206
+ get isEnabled() {
1207
+ return this.enabled;
1208
+ }
1173
1209
  /**
1174
1210
  * Returns true if the circuit allows a new call to proceed.
1175
1211
  * When false, callers should abort the tool call and return a
1176
1212
  * circuit-breaker error instead of spawning a process.
1177
1213
  */
1178
1214
  get canProceed() {
1215
+ if (!this.enabled) return true;
1179
1216
  this._checkStateTransition();
1180
1217
  return this.state !== "open";
1181
1218
  }
@@ -1211,7 +1248,7 @@ var CircuitBreaker = class {
1211
1248
  * not affect breaker state.
1212
1249
  */
1213
1250
  beforeCall(bypass = false) {
1214
- if (bypass) return true;
1251
+ if (bypass || !this.enabled) return true;
1215
1252
  this._checkStateTransition();
1216
1253
  if (this.state === "open") return false;
1217
1254
  return true;
@@ -1226,7 +1263,7 @@ var CircuitBreaker = class {
1226
1263
  * Use for background/fire-and-forget processes.
1227
1264
  */
1228
1265
  afterCall(durationMs, failed, bypass = false) {
1229
- if (bypass) return;
1266
+ if (bypass || !this.enabled) return;
1230
1267
  const now = Date.now();
1231
1268
  if (this.state === "half-open") {
1232
1269
  if (failed) {
@@ -1272,12 +1309,23 @@ var CircuitBreaker = class {
1272
1309
  if (this.state === "open") return;
1273
1310
  this.state = "open";
1274
1311
  this.openedAt = Date.now();
1312
+ try {
1313
+ this.onTrip?.();
1314
+ } catch {
1315
+ }
1275
1316
  }
1276
1317
  _reset() {
1318
+ const wasRecovering = this.state !== "closed";
1277
1319
  this.state = "closed";
1278
1320
  this.consecutiveFailures = 0;
1279
1321
  this.window = [];
1280
1322
  this.openedAt = null;
1323
+ if (wasRecovering) {
1324
+ try {
1325
+ this.onReset?.();
1326
+ } catch {
1327
+ }
1328
+ }
1281
1329
  }
1282
1330
  /** Transition from open → half-open when cooldown elapses. */
1283
1331
  _checkStateTransition() {
@@ -1339,8 +1387,21 @@ function killWin32Tree(pid) {
1339
1387
  var ProcessRegistryImpl = class {
1340
1388
  processes = /* @__PURE__ */ new Map();
1341
1389
  breaker;
1390
+ /**
1391
+ * Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,
1392
+ * a countdown is armed; on expiry all tracked processes are killed and the
1393
+ * breaker is reset to closed (forced recovery). Zero means manual recovery
1394
+ * only (`/kill reset`).
1395
+ */
1396
+ autoKillResetMs = 0;
1397
+ autoKillTimer = null;
1398
+ autoKillArmedAt = null;
1399
+ breakerCountdownListeners = [];
1342
1400
  constructor(breakerConfig) {
1343
1401
  this.breaker = new CircuitBreaker(breakerConfig);
1402
+ this.breaker.onTrip = () => this._armAutoKillReset();
1403
+ this.breaker.onReset = () => this._cancelAutoKillReset();
1404
+ this.breaker.setEnabled(false);
1344
1405
  }
1345
1406
  register(info) {
1346
1407
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
@@ -1416,6 +1477,90 @@ var ProcessRegistryImpl = class {
1416
1477
  forceBreakerReset() {
1417
1478
  this.breaker.forceReset();
1418
1479
  }
1480
+ /**
1481
+ * Configure circuit-breaker protection at runtime. Called from `/settings`
1482
+ * (instant, all modes) and on TUI mount (applies persisted config).
1483
+ *
1484
+ * - `enabled` toggles whether the breaker gates `bash`/`exec`.
1485
+ * - `autoKillResetMs` arms the auto kill/reset countdown when the breaker
1486
+ * trips (0 = manual recovery only).
1487
+ *
1488
+ * Re-applies cleanly on every call: cancels a pending countdown when the
1489
+ * timeout is cleared or protection disabled, and re-arms if the breaker is
1490
+ * currently open under the new settings.
1491
+ */
1492
+ setBreakerConfig(cfg) {
1493
+ if (cfg.enabled !== void 0) this.breaker.setEnabled(cfg.enabled);
1494
+ if (cfg.autoKillResetMs !== void 0) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);
1495
+ if (this.autoKillResetMs <= 0) {
1496
+ this._cancelAutoKillReset();
1497
+ return;
1498
+ }
1499
+ if (this.breaker.isEnabled && this.breaker.snapshot().state === "open") {
1500
+ this._armAutoKillReset();
1501
+ }
1502
+ }
1503
+ /**
1504
+ * Live countdown to the next auto kill/reset, or null when nothing is armed.
1505
+ * The TUI polls this on a 1s tick while armed so the statusline decrements.
1506
+ */
1507
+ getBreakerCountdown() {
1508
+ if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;
1509
+ const elapsed = Date.now() - this.autoKillArmedAt;
1510
+ return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };
1511
+ }
1512
+ /**
1513
+ * Subscribe to countdown arm/cancel events. Returns an unsubscribe function.
1514
+ * Use {@link getBreakerCountdown} for the live ticking value between events.
1515
+ */
1516
+ onBreakerCountdownChange(listener) {
1517
+ this.breakerCountdownListeners.push(listener);
1518
+ return () => {
1519
+ this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);
1520
+ };
1521
+ }
1522
+ _emitBreakerCountdown() {
1523
+ const snap = this.getBreakerCountdown();
1524
+ for (const l of this.breakerCountdownListeners) {
1525
+ try {
1526
+ l(snap);
1527
+ } catch {
1528
+ }
1529
+ }
1530
+ }
1531
+ /**
1532
+ * Arm the auto kill/reset countdown. Idempotent: re-arming resets the window
1533
+ * (a fresh trip after a failed half-open probe restarts the clock). No-op
1534
+ * when protection is off or no timeout is configured.
1535
+ */
1536
+ _armAutoKillReset() {
1537
+ if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;
1538
+ this._clearAutoKillTimer();
1539
+ this.autoKillArmedAt = Date.now();
1540
+ this.autoKillTimer = setTimeout(() => {
1541
+ this.autoKillTimer = null;
1542
+ this.autoKillArmedAt = null;
1543
+ this.killAll({ force: false });
1544
+ this.breaker.forceReset();
1545
+ this._emitBreakerCountdown();
1546
+ }, this.autoKillResetMs);
1547
+ this.autoKillTimer.unref?.();
1548
+ this._emitBreakerCountdown();
1549
+ }
1550
+ _cancelAutoKillReset() {
1551
+ const wasArmed = this.autoKillArmedAt !== null;
1552
+ this._clearAutoKillTimer();
1553
+ if (wasArmed) {
1554
+ this.autoKillArmedAt = null;
1555
+ this._emitBreakerCountdown();
1556
+ }
1557
+ }
1558
+ _clearAutoKillTimer() {
1559
+ if (this.autoKillTimer !== null) {
1560
+ clearTimeout(this.autoKillTimer);
1561
+ this.autoKillTimer = null;
1562
+ }
1563
+ }
1419
1564
  /** Kill a single process by PID.
1420
1565
  *
1421
1566
  * On POSIX: sends SIGTERM to the *process group* (-pid) so that
@@ -1540,6 +1685,7 @@ var bashTool = {
1540
1685
  permission: "confirm",
1541
1686
  mutating: true,
1542
1687
  riskTier: "destructive",
1688
+ icon: "terminal",
1543
1689
  // Trust rules match on the literal `command` string. Without subjectKey
1544
1690
  // the policy heuristic would have done the same here, but declaring it
1545
1691
  // explicitly removes the implicit cross-tool aliasing.
@@ -2002,6 +2148,7 @@ var execTool = {
2002
2148
  riskTier: "standard",
2003
2149
  timeoutMs: DEFAULT_TIMEOUT_MS2,
2004
2150
  capabilities: ["shell.restricted"],
2151
+ icon: "terminal",
2005
2152
  inputSchema: {
2006
2153
  type: "object",
2007
2154
  properties: {
@@ -2101,7 +2248,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2101
2248
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
2102
2249
  const resolved = resolveWin32Command(cmd);
2103
2250
  const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
2104
- const child = spawn(resolved, args, {
2251
+ const spawnCmd = needsShell ? cmd : resolved;
2252
+ const child = spawn(spawnCmd, args, {
2105
2253
  cwd,
2106
2254
  env: buildChildEnv(sessionId),
2107
2255
  stdio: ["ignore", "pipe", "pipe"],
@@ -2288,6 +2436,7 @@ var fetchTool = {
2288
2436
  permission: "confirm",
2289
2437
  mutating: false,
2290
2438
  capabilities: ["net.outbound"],
2439
+ icon: "web",
2291
2440
  // Trust rules for fetch match on the literal URL — declare it explicitly
2292
2441
  // so a user can trust `https://api.example.com/*` without accidentally
2293
2442
  // matching that pattern on any other tool that happens to have a `url`
@@ -2438,6 +2587,7 @@ var searchTool = {
2438
2587
  permission: "confirm",
2439
2588
  mutating: false,
2440
2589
  capabilities: ["net.outbound"],
2590
+ icon: "search",
2441
2591
  timeoutMs: TIMEOUT_MS2,
2442
2592
  inputSchema: {
2443
2593
  type: "object",
@@ -2653,6 +2803,7 @@ var todoTool = {
2653
2803
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
2654
2804
  timeoutMs: 1e3,
2655
2805
  capabilities: ["session.todo"],
2806
+ icon: "todo",
2656
2807
  inputSchema: {
2657
2808
  type: "object",
2658
2809
  properties: {
@@ -2753,11 +2904,12 @@ var todoTool = {
2753
2904
  var planTool = {
2754
2905
  name: "plan",
2755
2906
  category: "Session",
2756
- description: "Manage a persistent strategic plan for the current session. Unlike todos, plans are meant for higher-level, multi-phase approaches and survive across conversation resumptions. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute.",
2757
- usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns or even multiple sessions.',
2907
+ description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
2908
+ usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
2758
2909
  permission: "confirm",
2759
2910
  mutating: true,
2760
2911
  capabilities: ["fs.write"],
2912
+ icon: "plan",
2761
2913
  timeoutMs: 2e3,
2762
2914
  inputSchema: {
2763
2915
  type: "object",
@@ -2797,12 +2949,26 @@ var planTool = {
2797
2949
  template: {
2798
2950
  type: "string",
2799
2951
  description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
2952
+ },
2953
+ scope: {
2954
+ type: "string",
2955
+ enum: ["session", "project"],
2956
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
2800
2957
  }
2801
2958
  },
2802
2959
  required: ["action"]
2803
2960
  },
2804
2961
  async execute(input, ctx) {
2805
- const planPath = ctx.meta["plan.path"];
2962
+ const sessionPlanPath = ctx.meta["plan.path"];
2963
+ let planPath;
2964
+ if (input.scope === "project") {
2965
+ if (typeof sessionPlanPath === "string") {
2966
+ const lastSep = Math.max(sessionPlanPath.lastIndexOf("/"), sessionPlanPath.lastIndexOf("\\"));
2967
+ planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
2968
+ }
2969
+ } else {
2970
+ planPath = sessionPlanPath;
2971
+ }
2806
2972
  if (typeof planPath !== "string" || !planPath) {
2807
2973
  return {
2808
2974
  ok: false,
@@ -2816,148 +2982,169 @@ var planTool = {
2816
2982
  let early = null;
2817
2983
  const taskifyMeta = { title: "", details: "" };
2818
2984
  let didTaskify = false;
2819
- const plan = await mutatePlan(planPath, sessionId, async (p) => {
2820
- switch (input.action) {
2821
- case "show":
2822
- break;
2823
- case "add": {
2824
- const title = input.title?.trim();
2825
- if (!title) {
2826
- early = mkResult(p, false, "add requires `title`.");
2827
- return p;
2828
- }
2829
- const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
2830
- return updated;
2831
- }
2832
- case "start":
2833
- case "done": {
2834
- if (!input.target) {
2835
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
2836
- return p;
2837
- }
2838
- const next = setPlanItemStatus(
2839
- p,
2840
- input.target,
2841
- input.action === "start" ? "in_progress" : "done"
2842
- );
2843
- if (next === p) {
2844
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
2845
- return p;
2846
- }
2847
- return next;
2848
- }
2849
- case "remove": {
2850
- if (!input.target) {
2851
- early = mkResult(p, false, "remove requires `target` (id|index|substring).");
2852
- return p;
2853
- }
2854
- const next = removePlanItem(p, input.target);
2855
- if (next === p) {
2856
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
2857
- return p;
2858
- }
2859
- return next;
2860
- }
2861
- case "promote": {
2862
- if (!input.target) {
2863
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
2864
- return p;
2865
- }
2866
- const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
2867
- if (!derived) {
2868
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
2869
- return p;
2985
+ let plan;
2986
+ try {
2987
+ plan = await mutatePlan(planPath, sessionId, async (p) => {
2988
+ switch (input.action) {
2989
+ case "show":
2990
+ break;
2991
+ case "add": {
2992
+ const title = input.title?.trim();
2993
+ if (!title) {
2994
+ early = mkResult(p, false, "add requires `title`.");
2995
+ return p;
2996
+ }
2997
+ const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
2998
+ return updated;
2870
2999
  }
2871
- ctx.state.replaceTodos(derived.todos);
2872
- early = mkResult(
2873
- derived.plan,
2874
- true,
2875
- `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
2876
- derived.todos
2877
- );
2878
- return derived.plan;
2879
- }
2880
- case "template_use": {
2881
- const templateName = input.template?.trim();
2882
- if (!templateName) {
2883
- early = mkResult(p, false, "template_use requires `template` name.");
2884
- return p;
3000
+ case "start":
3001
+ case "done": {
3002
+ if (!input.target) {
3003
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
3004
+ return p;
3005
+ }
3006
+ const next = setPlanItemStatus(
3007
+ p,
3008
+ input.target,
3009
+ input.action === "start" ? "in_progress" : "done"
3010
+ );
3011
+ if (next === p) {
3012
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3013
+ return p;
3014
+ }
3015
+ return next;
2885
3016
  }
2886
- const template = getPlanTemplate(templateName);
2887
- if (!template) {
2888
- early = mkResult(p, false, `Unknown template "${templateName}".`);
2889
- return p;
3017
+ case "remove": {
3018
+ if (!input.target) {
3019
+ early = mkResult(p, false, "remove requires `target` (id|index|substring).");
3020
+ return p;
3021
+ }
3022
+ const next = removePlanItem(p, input.target);
3023
+ if (next === p) {
3024
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3025
+ return p;
3026
+ }
3027
+ return next;
2890
3028
  }
2891
- let updated = p;
2892
- for (const item of template.items) {
2893
- ({ plan: updated } = addPlanItem(updated, item.title, item.details));
3029
+ case "promote": {
3030
+ if (!input.target) {
3031
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
3032
+ return p;
3033
+ }
3034
+ const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
3035
+ if (!derived) {
3036
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3037
+ return p;
3038
+ }
3039
+ ctx.state.replaceTodos(derived.todos);
3040
+ early = mkResult(
3041
+ derived.plan,
3042
+ true,
3043
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
3044
+ derived.todos
3045
+ );
3046
+ return derived.plan;
2894
3047
  }
2895
- early = mkResult(
2896
- updated,
2897
- true,
2898
- `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
2899
- );
2900
- return updated;
2901
- }
2902
- case "clear":
2903
- return clearPlan(p);
2904
- case "taskify": {
2905
- if (!input.target) {
2906
- early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
2907
- return p;
3048
+ case "template_use": {
3049
+ const templateName = input.template?.trim();
3050
+ if (!templateName) {
3051
+ early = mkResult(p, false, "template_use requires `template` name.");
3052
+ return p;
3053
+ }
3054
+ const template = getPlanTemplate(templateName);
3055
+ if (!template) {
3056
+ early = mkResult(p, false, `Unknown template "${templateName}".`);
3057
+ return p;
3058
+ }
3059
+ let updated = p;
3060
+ for (const item of template.items) {
3061
+ ({ plan: updated } = addPlanItem(updated, item.title, item.details));
3062
+ }
3063
+ early = mkResult(
3064
+ updated,
3065
+ true,
3066
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
3067
+ );
3068
+ return updated;
2908
3069
  }
2909
- let itemIdx = -1;
2910
- const asNum = Number.parseInt(input.target, 10);
2911
- if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
2912
- itemIdx = asNum - 1;
2913
- } else {
2914
- itemIdx = p.items.findIndex((it) => it.id === input.target);
2915
- if (itemIdx === -1) {
2916
- const lower = input.target.toLowerCase();
2917
- itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
3070
+ case "clear":
3071
+ return clearPlan(p);
3072
+ case "taskify": {
3073
+ if (!input.target) {
3074
+ early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
3075
+ return p;
2918
3076
  }
3077
+ let itemIdx = -1;
3078
+ const asNum = Number.parseInt(input.target, 10);
3079
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
3080
+ itemIdx = asNum - 1;
3081
+ } else {
3082
+ itemIdx = p.items.findIndex((it) => it.id === input.target);
3083
+ if (itemIdx === -1) {
3084
+ const lower = input.target.toLowerCase();
3085
+ itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
3086
+ }
3087
+ }
3088
+ if (itemIdx === -1 || !p.items[itemIdx]) {
3089
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3090
+ return p;
3091
+ }
3092
+ const item = p.items[itemIdx];
3093
+ taskifyMeta.title = item.title;
3094
+ taskifyMeta.details = item.details ?? "";
3095
+ didTaskify = true;
3096
+ break;
2919
3097
  }
2920
- if (itemIdx === -1 || !p.items[itemIdx]) {
2921
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
3098
+ default:
3099
+ early = mkResult(p, false, `Unknown action "${input.action}".`);
2922
3100
  return p;
2923
- }
2924
- const item = p.items[itemIdx];
2925
- taskifyMeta.title = item.title;
2926
- taskifyMeta.details = item.details ?? "";
2927
- didTaskify = true;
2928
- break;
2929
3101
  }
2930
- default:
2931
- early = mkResult(p, false, `Unknown action "${input.action}".`);
2932
- return p;
2933
- }
2934
- return p;
2935
- });
3102
+ return p;
3103
+ });
3104
+ } catch (err) {
3105
+ return {
3106
+ ok: false,
3107
+ message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
3108
+ plan: "",
3109
+ count: 0,
3110
+ open: 0
3111
+ };
3112
+ }
2936
3113
  if (early) return early;
2937
3114
  if (didTaskify) {
2938
- const taskPath = ctx.meta["task.path"];
2939
- if (typeof taskPath !== "string" || !taskPath) {
3115
+ const taskPathRaw = ctx.meta["task.path"];
3116
+ if (typeof taskPathRaw !== "string" || !taskPathRaw) {
2940
3117
  return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
2941
3118
  }
2942
- const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
3119
+ let taskPath = taskPathRaw;
3120
+ if (input.scope === "project") {
3121
+ const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
3122
+ taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
3123
+ }
2943
3124
  const now = (/* @__PURE__ */ new Date()).toISOString();
2944
- taskFile.tasks.push({
2945
- id: `task_${randomUUID()}`,
2946
- title: taskifyMeta.title,
2947
- description: taskifyMeta.details || void 0,
2948
- type: "feature",
2949
- priority: "medium",
2950
- status: "pending",
2951
- createdAt: now,
2952
- updatedAt: now
2953
- });
2954
- await saveTasks(taskPath, taskFile);
2955
- return mkResult(
2956
- plan,
2957
- true,
2958
- `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
3125
+ try {
3126
+ const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
3127
+ f.tasks.push({
3128
+ id: `task_${randomUUID()}`,
3129
+ title: taskifyMeta.title,
3130
+ description: taskifyMeta.details || void 0,
3131
+ type: "feature",
3132
+ priority: "medium",
3133
+ status: "pending",
3134
+ createdAt: now,
3135
+ updatedAt: now
3136
+ });
3137
+ return f;
3138
+ });
3139
+ return mkResult(
3140
+ plan,
3141
+ true,
3142
+ `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
2959
3143
  ${formatTaskList(taskFile.tasks)}`
2960
- );
3144
+ );
3145
+ } catch (err) {
3146
+ return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
3147
+ }
2961
3148
  }
2962
3149
  return mkResult(plan, true, `Plan ${input.action} ok.`);
2963
3150
  }
@@ -2982,6 +3169,7 @@ var gitTool = {
2982
3169
  description: "Safe wrapper around common git operations. Supports status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset, worktree, etc. This is the preferred way to interact with git instead of using the raw `bash` or `exec` tools.",
2983
3170
  usageHint: "ALWAYS prefer this tool over raw shell git commands.\n\nKey fields:\n- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\n- Use `message` only for commit operations.\n- Use `files` array for operations that take paths (status, diff, add, etc.).\n- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\nNever pass raw git flags through `args` for dangerous operations \u2014 use the structured fields.",
2984
3171
  permission: "confirm",
3172
+ icon: "git",
2985
3173
  // Conservative: any of these may mutate. The non-mutating commands
2986
3174
  // (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
2987
3175
  // and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
@@ -3240,6 +3428,7 @@ var patchTool = {
3240
3428
  permission: "confirm",
3241
3429
  mutating: true,
3242
3430
  capabilities: ["fs.write"],
3431
+ icon: "edit",
3243
3432
  timeoutMs: 3e4,
3244
3433
  inputSchema: {
3245
3434
  type: "object",
@@ -3353,6 +3542,7 @@ var jsonTool = {
3353
3542
  mutating: false,
3354
3543
  timeoutMs: 5e3,
3355
3544
  capabilities: ["fs.read"],
3545
+ icon: "json",
3356
3546
  inputSchema: {
3357
3547
  type: "object",
3358
3548
  properties: {
@@ -3475,6 +3665,7 @@ var diffTool = {
3475
3665
  permission: "auto",
3476
3666
  mutating: false,
3477
3667
  capabilities: ["fs.read"],
3668
+ icon: "diff",
3478
3669
  timeoutMs: 1e4,
3479
3670
  inputSchema: {
3480
3671
  type: "object",
@@ -3633,6 +3824,7 @@ var treeTool = {
3633
3824
  permission: "auto",
3634
3825
  mutating: false,
3635
3826
  capabilities: ["fs.read"],
3827
+ icon: "tree",
3636
3828
  timeoutMs: 15e3,
3637
3829
  inputSchema: {
3638
3830
  type: "object",
@@ -3799,8 +3991,9 @@ async function* spawnStream(opts) {
3799
3991
  let pending2 = "";
3800
3992
  let error;
3801
3993
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
3802
- const cmd = resolveWin32Command(opts.cmd);
3803
- const needsShell = isWin2 && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
3994
+ const resolved = resolveWin32Command(opts.cmd);
3995
+ const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
3996
+ const cmd = needsShell ? opts.cmd : resolved;
3804
3997
  const child = spawn(cmd, opts.args, {
3805
3998
  cwd: opts.cwd,
3806
3999
  env: buildChildEnv(),
@@ -3958,6 +4151,7 @@ var lintTool = {
3958
4151
  mutating: false,
3959
4152
  timeoutMs: 6e4,
3960
4153
  capabilities: ["shell.restricted"],
4154
+ icon: "code",
3961
4155
  inputSchema: {
3962
4156
  type: "object",
3963
4157
  properties: {
@@ -4052,6 +4246,7 @@ var formatTool = {
4052
4246
  permission: "confirm",
4053
4247
  mutating: true,
4054
4248
  capabilities: ["fs.write", "shell.exec"],
4249
+ icon: "code",
4055
4250
  timeoutMs: 6e4,
4056
4251
  inputSchema: {
4057
4252
  type: "object",
@@ -4153,6 +4348,7 @@ var typecheckTool = {
4153
4348
  mutating: false,
4154
4349
  timeoutMs: 12e4,
4155
4350
  capabilities: ["shell.restricted"],
4351
+ icon: "code",
4156
4352
  inputSchema: {
4157
4353
  type: "object",
4158
4354
  properties: {
@@ -4239,6 +4435,7 @@ var testTool = {
4239
4435
  usageHint: "ESSENTIAL BEFORE CONSIDERING WORK DONE:\n\n- Use `files` or `grep` to run only relevant tests during development.\n- `coverage: true` is useful when working on critical paths.\nRun tests frequently. A clean test run is usually required before the task can be considered complete.",
4240
4436
  permission: "confirm",
4241
4437
  mutating: false,
4438
+ icon: "test",
4242
4439
  timeoutMs: 12e4,
4243
4440
  capabilities: ["shell.restricted"],
4244
4441
  inputSchema: {
@@ -4396,6 +4593,7 @@ var installTool = {
4396
4593
  permission: "confirm",
4397
4594
  mutating: true,
4398
4595
  riskTier: "standard",
4596
+ icon: "package",
4399
4597
  timeoutMs: 12e4,
4400
4598
  capabilities: ["package.install", "shell.restricted"],
4401
4599
  inputSchema: {
@@ -4535,6 +4733,7 @@ var auditTool = {
4535
4733
  permission: "confirm",
4536
4734
  mutating: false,
4537
4735
  capabilities: ["shell.restricted"],
4736
+ icon: "package",
4538
4737
  timeoutMs: 6e4,
4539
4738
  inputSchema: {
4540
4739
  type: "object",
@@ -4630,6 +4829,7 @@ var outdatedTool = {
4630
4829
  description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
4631
4830
  usageHint: "MAINTENANCE & SECURITY TOOL:\n\n- Run periodically or before dependency-related work.\n- Helps surface packages that may need updates for security or features.\n- Hits the package registry over HTTP, so it is NOT purely local \u2014 flagged as mutating for the confirmation gate.\nUse the output to decide on upgrades. Prefer this over manual shell commands for dependency hygiene.",
4632
4831
  permission: "confirm",
4832
+ icon: "package",
4633
4833
  // Network side-effecting (registry HTTP). Pairs with `mutating: true`
4634
4834
  // so the H7 invariant test (`no auto-permission tool declares
4635
4835
  // mutating: true`) passes — a tool claiming `'auto'` must be purely
@@ -4681,7 +4881,8 @@ function runOutdated(manager, args, cwd, signal) {
4681
4881
  const MAX = 1e5;
4682
4882
  const resolved = resolveWin32Command(manager);
4683
4883
  const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
4684
- const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
4884
+ const spawnCmd = needsShell ? manager : resolved;
4885
+ const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
4685
4886
  child.stdout?.on("data", (c) => {
4686
4887
  if (stdout.length < MAX) stdout += c.toString();
4687
4888
  });
@@ -4746,6 +4947,7 @@ var logsTool = {
4746
4947
  mutating: false,
4747
4948
  timeoutMs: 3e4,
4748
4949
  capabilities: ["shell.restricted"],
4950
+ icon: "logs",
4749
4951
  inputSchema: {
4750
4952
  type: "object",
4751
4953
  properties: {
@@ -4950,6 +5152,7 @@ var documentTool = {
4950
5152
  mutating: false,
4951
5153
  timeoutMs: 3e4,
4952
5154
  capabilities: ["fs.read"],
5155
+ icon: "document",
4953
5156
  inputSchema: {
4954
5157
  type: "object",
4955
5158
  properties: {
@@ -5185,6 +5388,7 @@ var scaffoldTool = {
5185
5388
  permission: "confirm",
5186
5389
  mutating: true,
5187
5390
  capabilities: ["fs.write.outside-project", "fs.write"],
5391
+ icon: "scaffold",
5188
5392
  timeoutMs: 3e4,
5189
5393
  inputSchema: {
5190
5394
  type: "object",
@@ -5280,6 +5484,7 @@ var toolSearchTool = {
5280
5484
  mutating: false,
5281
5485
  timeoutMs: 1e3,
5282
5486
  capabilities: ["tool.meta"],
5487
+ icon: "meta",
5283
5488
  inputSchema: {
5284
5489
  type: "object",
5285
5490
  properties: {
@@ -5359,6 +5564,7 @@ var toolUseTool = {
5359
5564
  mutating: true,
5360
5565
  timeoutMs: 6e4,
5361
5566
  capabilities: ["tool.mutate.any"],
5567
+ icon: "meta",
5362
5568
  inputSchema: {
5363
5569
  type: "object",
5364
5570
  properties: {
@@ -5429,6 +5635,7 @@ var batchToolUseTool = {
5429
5635
  mutating: true,
5430
5636
  timeoutMs: 12e4,
5431
5637
  capabilities: ["tool.mutate.any"],
5638
+ icon: "meta",
5432
5639
  inputSchema: {
5433
5640
  type: "object",
5434
5641
  properties: {
@@ -5534,6 +5741,7 @@ var toolHelpTool = {
5534
5741
  mutating: false,
5535
5742
  timeoutMs: 5e3,
5536
5743
  capabilities: ["tool.meta"],
5744
+ icon: "meta",
5537
5745
  inputSchema: {
5538
5746
  type: "object",
5539
5747
  properties: {
@@ -5658,6 +5866,7 @@ function rememberTool(memory) {
5658
5866
  mutating: true,
5659
5867
  timeoutMs: 2e3,
5660
5868
  capabilities: ["memory.write"],
5869
+ icon: "settings",
5661
5870
  inputSchema: {
5662
5871
  type: "object",
5663
5872
  properties: {
@@ -5832,6 +6041,7 @@ function createModeTool(modeStore) {
5832
6041
  mutating: true,
5833
6042
  timeoutMs: 5e3,
5834
6043
  capabilities: ["session.mode"],
6044
+ icon: "settings",
5835
6045
  inputSchema: {
5836
6046
  type: "object",
5837
6047
  properties: {
@@ -6588,39 +6798,57 @@ var IndexStore = class {
6588
6798
  }
6589
6799
  });
6590
6800
  }
6801
+ /**
6802
+ * Bulk-insert refs for many source symbols in a single transaction.
6803
+ *
6804
+ * Unlike {@link insertRefs} this does NOT delete per source id — the caller
6805
+ * (the indexer) has already cleared stale refs for the file via
6806
+ * {@link deleteRefsForFile}, so the per-source DELETE would be redundant work
6807
+ * repeated once per symbol. One transaction for the whole file instead of one
6808
+ * per symbol turns an O(symbols) transaction count into O(1).
6809
+ *
6810
+ * Each ref's own {@link Ref.fromId} is used; pass an empty array to no-op.
6811
+ */
6812
+ insertRefsBatch(refs) {
6813
+ if (refs.length === 0) return;
6814
+ this.runWithRetry(() => {
6815
+ const stmt = this.db.prepare(
6816
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
6817
+ VALUES (?, ?, ?, ?, ?)`
6818
+ );
6819
+ for (const ref of refs) {
6820
+ stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
6821
+ }
6822
+ });
6823
+ }
6591
6824
  /**
6592
6825
  * Delete all refs whose source symbols are in a given file.
6593
6826
  * Used when re-indexing a file to clear stale refs.
6594
6827
  */
6595
6828
  deleteRefsForFile(file) {
6596
6829
  this.runWithRetry(() => {
6597
- const ids = this.db.prepare(
6598
- "SELECT id FROM symbols WHERE file = ?"
6599
- ).all(file);
6600
- if (!ids.length) return;
6601
- const placeholders = ids.map(() => "?").join(",");
6602
- this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
6830
+ this.db.prepare(
6831
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
6832
+ ).run(file);
6603
6833
  });
6604
6834
  }
6605
6835
  /**
6606
6836
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
6607
6837
  * Call this after all symbols have been inserted to fill in cross-references.
6838
+ *
6839
+ * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
6840
+ * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
6841
+ * that found a target — matching the previous per-row loop's return value.
6608
6842
  */
6609
6843
  resolveRefs() {
6610
6844
  return this.runWithRetry(() => {
6611
- const unresolved = this.db.prepare(
6612
- "SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
6613
- ).all();
6614
- let resolved = 0;
6615
- for (const row of unresolved) {
6616
- const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
6617
- const first = target[0];
6618
- if (first) {
6619
- this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
6620
- resolved++;
6621
- }
6622
- }
6623
- return resolved;
6845
+ const result = this.db.prepare(
6846
+ `UPDATE refs SET to_id = (
6847
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
6848
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
6849
+ AND to_name IN (SELECT name FROM symbols)`
6850
+ ).run();
6851
+ return result.changes ?? 0;
6624
6852
  });
6625
6853
  }
6626
6854
  /**
@@ -8095,13 +8323,26 @@ async function runIndexerWithStore(store, opts) {
8095
8323
  symbolsIndexed += count;
8096
8324
  langStats[lang] = (langStats[lang] ?? 0) + count;
8097
8325
  if (parsed.refs && parsed.refs.length > 0) {
8098
- for (let i = 0; i < symbolsWithIds.length; i++) {
8099
- const sym = expectDefined(symbolsWithIds[i]);
8100
- const symRefs = parsed.refs.filter((r) => r.line === sym.line);
8101
- if (symRefs.length > 0) {
8102
- const refsWithFromId = symRefs.map((r) => ({ ...r, fromId: sym.id }));
8103
- store.insertRefs(sym.id, refsWithFromId);
8326
+ const refsByLine = /* @__PURE__ */ new Map();
8327
+ for (const r of parsed.refs) {
8328
+ let arr = refsByLine.get(r.line);
8329
+ if (!arr) {
8330
+ arr = [];
8331
+ refsByLine.set(r.line, arr);
8104
8332
  }
8333
+ arr.push(r);
8334
+ }
8335
+ const batch = [];
8336
+ for (const sym of symbolsWithIds) {
8337
+ const symRefs = refsByLine.get(sym.line);
8338
+ if (symRefs) {
8339
+ for (const r of symRefs) {
8340
+ batch.push({ ...r, fromId: sym.id });
8341
+ }
8342
+ }
8343
+ }
8344
+ if (batch.length > 0) {
8345
+ store.insertRefsBatch(batch);
8105
8346
  }
8106
8347
  }
8107
8348
  store.upsertFile({
@@ -8494,6 +8735,7 @@ async function codebaseIndexStats(args, opts = {}) {
8494
8735
  var codebaseIndexTool = {
8495
8736
  name: "codebase-index",
8496
8737
  category: "Project",
8738
+ icon: "index",
8497
8739
  description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
8498
8740
  usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
8499
8741
  permission: "confirm",
@@ -8550,6 +8792,7 @@ var codebaseIndexTool = {
8550
8792
  var codebaseSearchTool = {
8551
8793
  name: "codebase-search",
8552
8794
  category: "Project",
8795
+ icon: "index",
8553
8796
  description: "Semantic/keyword search over the indexed codebase symbols (functions, classes, interfaces, etc.). Uses BM25 ranking. Much more powerful and structured than raw `grep` for finding code by name or concept.",
8554
8797
  usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
8555
8798
  permission: "auto",
@@ -8638,6 +8881,7 @@ var codebaseSearchTool = {
8638
8881
  var codebaseStatsTool = {
8639
8882
  name: "codebase-stats",
8640
8883
  category: "Project",
8884
+ icon: "index",
8641
8885
  description: "Return health and statistics about the current symbol index (total symbols, files, language/kind breakdown, size, last update). Useful to decide whether to re-index.",
8642
8886
  usageHint: "CALL BEFORE HEAVY CODEBASE-SEARCH WORK:\n\n- Use to see if the index is up-to-date or needs a refresh.\n- No arguments required.\n- Helps avoid wasting tokens on searches against a stale index.\nLightweight and safe to call frequently.",
8643
8887
  permission: "auto",
@@ -8698,6 +8942,7 @@ var setWorkingDirTool = {
8698
8942
  permission: "confirm",
8699
8943
  mutating: true,
8700
8944
  capabilities: ["fs.read"],
8945
+ icon: "settings",
8701
8946
  timeoutMs: 5e3,
8702
8947
  inputSchema: {
8703
8948
  type: "object",
@@ -8758,11 +9003,12 @@ function findTaskIndex(tasks, query2) {
8758
9003
  var taskTool = {
8759
9004
  name: "task",
8760
9005
  category: "Session",
8761
- description: "Manage structured work items with dependencies, types, and priorities. Use this for complex, multi-step work where tasks have ordering constraints. Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), dependencies between items, priority ranking, and agent assignment. The task list persists across session resumes.",
8762
- usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n- `action: "promote"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\n- `action: "planify"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate',
9006
+ description: 'Manage session-persistent structured work items with dependencies, types, and priorities. Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), dependencies between items, priority ranking, and agent assignment. Tasks are written to disk and survive session resumes. By default they are isolated to this session; use `scope: "project"` to store tasks in a shared project-level file visible to all sessions.',
9007
+ usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n- `action: "promote"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\n- `action: "planify"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate\n- `scope`: "session" (default, isolated) or "project" (shared across sessions)',
8763
9008
  permission: "confirm",
8764
9009
  mutating: true,
8765
9010
  capabilities: ["fs.write"],
9011
+ icon: "task",
8766
9012
  timeoutMs: 2e3,
8767
9013
  inputSchema: {
8768
9014
  type: "object",
@@ -8828,12 +9074,26 @@ var taskTool = {
8828
9074
  type: "array",
8829
9075
  items: { type: "string" },
8830
9076
  description: "Optional subtask titles for action=promote. Each becomes a pending todo."
9077
+ },
9078
+ scope: {
9079
+ type: "string",
9080
+ enum: ["session", "project"],
9081
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
8831
9082
  }
8832
9083
  },
8833
9084
  required: ["action"]
8834
9085
  },
8835
9086
  async execute(input, ctx) {
8836
- const taskPath = ctx.meta["task.path"];
9087
+ const sessionTaskPath = ctx.meta["task.path"];
9088
+ let taskPath;
9089
+ if (input.scope === "project") {
9090
+ if (typeof sessionTaskPath === "string") {
9091
+ const lastSep = Math.max(sessionTaskPath.lastIndexOf("/"), sessionTaskPath.lastIndexOf("\\"));
9092
+ taskPath = lastSep >= 0 ? sessionTaskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
9093
+ }
9094
+ } else {
9095
+ taskPath = sessionTaskPath;
9096
+ }
8837
9097
  if (typeof taskPath !== "string" || !taskPath) {
8838
9098
  return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
8839
9099
  }
@@ -8843,23 +9103,66 @@ var taskTool = {
8843
9103
  const planifyMeta = { title: "", details: "" };
8844
9104
  let didPlanify = false;
8845
9105
  let todosToReplace = null;
8846
- const file = await mutateTasks(taskPath, sessionId, async (f) => {
8847
- switch (input.action) {
8848
- case "show":
8849
- break;
8850
- case "replace": {
8851
- if (!Array.isArray(input.tasks)) {
8852
- early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
8853
- return f;
9106
+ let file;
9107
+ try {
9108
+ file = await mutateTasks(taskPath, sessionId, async (f) => {
9109
+ switch (input.action) {
9110
+ case "show":
9111
+ break;
9112
+ case "replace": {
9113
+ if (!Array.isArray(input.tasks)) {
9114
+ early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
9115
+ return f;
9116
+ }
9117
+ const newIds = new Set(input.tasks.map((t) => t.id));
9118
+ if (newIds.size !== input.tasks.length) {
9119
+ const seen = /* @__PURE__ */ new Set();
9120
+ const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => seen.has(id) ? true : (seen.add(id), false)))];
9121
+ early = {
9122
+ ok: false,
9123
+ message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
9124
+ count: 0,
9125
+ completed: 0,
9126
+ inProgress: 0
9127
+ };
9128
+ return f;
9129
+ }
9130
+ for (const t of input.tasks) {
9131
+ if (t.dependsOn && t.dependsOn.length > 0) {
9132
+ const missing = t.dependsOn.filter((d) => !newIds.has(d));
9133
+ if (missing.length > 0) {
9134
+ early = {
9135
+ ok: false,
9136
+ message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
9137
+ count: 0,
9138
+ completed: 0,
9139
+ inProgress: 0
9140
+ };
9141
+ return f;
9142
+ }
9143
+ }
9144
+ }
9145
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9146
+ f.tasks = input.tasks.map((t) => ({
9147
+ ...t,
9148
+ createdAt: t.createdAt || now,
9149
+ updatedAt: now
9150
+ }));
9151
+ break;
8854
9152
  }
8855
- const newIds = new Set(input.tasks.map((t) => t.id));
8856
- for (const t of input.tasks) {
9153
+ case "add": {
9154
+ const t = input.task;
9155
+ if (!t || !t.title) {
9156
+ early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
9157
+ return f;
9158
+ }
8857
9159
  if (t.dependsOn && t.dependsOn.length > 0) {
8858
- const missing = t.dependsOn.filter((d) => !newIds.has(d));
9160
+ const existingIds = new Set(f.tasks.map((e) => e.id));
9161
+ const missing = t.dependsOn.filter((d) => !existingIds.has(d));
8859
9162
  if (missing.length > 0) {
8860
9163
  early = {
8861
9164
  ok: false,
8862
- message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
9165
+ message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
8863
9166
  count: 0,
8864
9167
  completed: 0,
8865
9168
  inProgress: 0
@@ -8867,165 +9170,170 @@ var taskTool = {
8867
9170
  return f;
8868
9171
  }
8869
9172
  }
9173
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9174
+ const newTask = {
9175
+ id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
9176
+ title: t.title,
9177
+ description: t.description,
9178
+ type: t.type || "feature",
9179
+ priority: t.priority || "medium",
9180
+ status: t.status || "pending",
9181
+ dependsOn: t.dependsOn,
9182
+ assignee: t.assignee,
9183
+ estimateHours: t.estimateHours,
9184
+ tags: t.tags,
9185
+ createdAt: now,
9186
+ updatedAt: now
9187
+ };
9188
+ f.tasks.push(newTask);
9189
+ break;
8870
9190
  }
8871
- const now = (/* @__PURE__ */ new Date()).toISOString();
8872
- f.tasks = input.tasks.map((t) => ({
8873
- ...t,
8874
- createdAt: t.createdAt || now,
8875
- updatedAt: now
8876
- }));
8877
- break;
8878
- }
8879
- case "add": {
8880
- const t = input.task;
8881
- if (!t || !t.title) {
8882
- early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
8883
- return f;
8884
- }
8885
- if (t.dependsOn && t.dependsOn.length > 0) {
8886
- const existingIds = new Set(f.tasks.map((e) => e.id));
8887
- const missing = t.dependsOn.filter((d) => !existingIds.has(d));
8888
- if (missing.length > 0) {
8889
- early = {
8890
- ok: false,
8891
- message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
8892
- count: 0,
8893
- completed: 0,
8894
- inProgress: 0
8895
- };
9191
+ case "status": {
9192
+ if (!input.id || !input.status) {
9193
+ early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
8896
9194
  return f;
8897
9195
  }
9196
+ const task = f.tasks.find((t) => t.id === input.id);
9197
+ if (!task) {
9198
+ early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
9199
+ return f;
9200
+ }
9201
+ task.status = input.status;
9202
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9203
+ break;
8898
9204
  }
8899
- const now = (/* @__PURE__ */ new Date()).toISOString();
8900
- const newTask = {
8901
- id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
8902
- title: t.title,
8903
- description: t.description,
8904
- type: t.type || "feature",
8905
- priority: t.priority || "medium",
8906
- status: t.status || "pending",
8907
- dependsOn: t.dependsOn,
8908
- assignee: t.assignee,
8909
- estimateHours: t.estimateHours,
8910
- tags: t.tags,
8911
- createdAt: now,
8912
- updatedAt: now
8913
- };
8914
- f.tasks.push(newTask);
8915
- break;
8916
- }
8917
- case "status": {
8918
- if (!input.id || !input.status) {
8919
- early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
8920
- return f;
8921
- }
8922
- const task = f.tasks.find((t) => t.id === input.id);
8923
- if (!task) {
8924
- early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
8925
- return f;
8926
- }
8927
- task.status = input.status;
8928
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8929
- break;
8930
- }
8931
- case "promote": {
8932
- const target = input.target?.trim();
8933
- if (!target) {
8934
- early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
8935
- return f;
8936
- }
8937
- const idx = findTaskIndex(f.tasks, target);
8938
- if (idx === -1) {
8939
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8940
- return f;
8941
- }
8942
- const match = f.tasks[idx];
8943
- if (!match) {
8944
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8945
- return f;
8946
- }
8947
- if (match.status !== "completed" && match.status !== "failed") {
8948
- match.status = "in_progress";
8949
- match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8950
- }
8951
- const todos = [];
8952
- const ts2 = Date.now();
8953
- todos.push({
8954
- id: `todo_${ts2}_task`,
8955
- content: match.title,
8956
- status: "in_progress",
8957
- activeForm: match.title,
8958
- promotedFromTask: match.id
8959
- });
8960
- if (match.description) {
9205
+ case "promote": {
9206
+ const target = input.target?.trim();
9207
+ if (!target) {
9208
+ early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
9209
+ return f;
9210
+ }
9211
+ const idx = findTaskIndex(f.tasks, target);
9212
+ if (idx === -1) {
9213
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9214
+ return f;
9215
+ }
9216
+ const match = f.tasks[idx];
9217
+ if (!match) {
9218
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9219
+ return f;
9220
+ }
9221
+ if (match.status !== "completed" && match.status !== "failed") {
9222
+ match.status = "in_progress";
9223
+ match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9224
+ }
9225
+ const todos = [];
9226
+ const ts2 = Date.now();
8961
9227
  todos.push({
8962
- id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
8963
- content: match.description.slice(0, 200),
8964
- status: "pending",
9228
+ id: `todo_${ts2}_task`,
9229
+ content: match.title,
9230
+ status: "in_progress",
9231
+ activeForm: match.title,
8965
9232
  promotedFromTask: match.id
8966
9233
  });
8967
- }
8968
- if (input.subtasks && input.subtasks.length > 0) {
8969
- for (const st of input.subtasks) {
9234
+ if (match.description) {
8970
9235
  todos.push({
8971
9236
  id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
8972
- content: st,
9237
+ content: match.description.slice(0, 200),
8973
9238
  status: "pending",
8974
9239
  promotedFromTask: match.id
8975
9240
  });
8976
9241
  }
9242
+ if (input.subtasks && input.subtasks.length > 0) {
9243
+ for (const st of input.subtasks) {
9244
+ todos.push({
9245
+ id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
9246
+ content: st,
9247
+ status: "pending",
9248
+ promotedFromTask: match.id
9249
+ });
9250
+ }
9251
+ }
9252
+ todosToReplace = todos;
9253
+ promoteMeta.count = todos.length;
9254
+ promoteMeta.title = match.title;
9255
+ break;
8977
9256
  }
8978
- todosToReplace = todos;
8979
- promoteMeta.count = todos.length;
8980
- promoteMeta.title = match.title;
8981
- break;
8982
- }
8983
- case "planify": {
8984
- const target = input.target?.trim();
8985
- if (!target) {
8986
- early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
8987
- return f;
8988
- }
8989
- const idx = findTaskIndex(f.tasks, target);
8990
- if (idx === -1) {
8991
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8992
- return f;
9257
+ case "planify": {
9258
+ const target = input.target?.trim();
9259
+ if (!target) {
9260
+ early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
9261
+ return f;
9262
+ }
9263
+ const idx = findTaskIndex(f.tasks, target);
9264
+ if (idx === -1) {
9265
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9266
+ return f;
9267
+ }
9268
+ const match = f.tasks[idx];
9269
+ if (!match) {
9270
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9271
+ return f;
9272
+ }
9273
+ planifyMeta.title = match.title;
9274
+ planifyMeta.details = match.description ?? "";
9275
+ didPlanify = true;
9276
+ break;
8993
9277
  }
8994
- const match = f.tasks[idx];
8995
- if (!match) {
8996
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9278
+ default:
9279
+ early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
8997
9280
  return f;
8998
- }
8999
- planifyMeta.title = match.title;
9000
- planifyMeta.details = match.description ?? "";
9001
- didPlanify = true;
9002
- break;
9003
9281
  }
9004
- default:
9005
- early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
9006
- return f;
9007
- }
9008
- return f;
9009
- });
9282
+ return f;
9283
+ });
9284
+ } catch (err) {
9285
+ return {
9286
+ ok: false,
9287
+ message: `Task change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
9288
+ count: 0,
9289
+ completed: 0,
9290
+ inProgress: 0
9291
+ };
9292
+ }
9010
9293
  if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
9011
9294
  if (early) return early;
9012
9295
  if (didPlanify) {
9013
9296
  const { title, details } = planifyMeta;
9014
- const planPath = ctx.meta["plan.path"];
9015
- if (typeof planPath === "string" && planPath) {
9016
- const planCfg = await loadPlan(planPath) ?? emptyPlan(sessionId);
9017
- const { plan: updated } = addPlanItem(planCfg, title, details || void 0);
9018
- await savePlan(planPath, updated);
9297
+ const planPathRaw = ctx.meta["plan.path"];
9298
+ const prog = computeTaskItemProgress(file.tasks);
9299
+ if (typeof planPathRaw === "string" && planPathRaw) {
9300
+ let planPath = planPathRaw;
9301
+ if (input.scope === "project") {
9302
+ const lastSep = Math.max(planPath.lastIndexOf("/"), planPath.lastIndexOf("\\"));
9303
+ planPath = lastSep >= 0 ? planPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
9304
+ }
9305
+ let formatted = "";
9306
+ try {
9307
+ await mutatePlan(planPath, sessionId, (pf) => {
9308
+ const { plan: updated } = addPlanItem(pf, title, details || void 0);
9309
+ formatted = formatPlan(updated);
9310
+ return updated;
9311
+ });
9312
+ } catch (err) {
9313
+ return {
9314
+ ok: false,
9315
+ message: `planify: plan not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
9316
+ count: file.tasks.length,
9317
+ completed: prog.completed,
9318
+ inProgress: prog.inProgress
9319
+ };
9320
+ }
9019
9321
  return {
9020
9322
  ok: true,
9021
9323
  message: `planify ok \u2014 added "${title}" to plan.
9022
- ${formatPlan(updated)}`,
9324
+ ${formatted}`,
9023
9325
  count: file.tasks.length,
9024
- completed: computeTaskItemProgress(file.tasks).completed,
9025
- inProgress: computeTaskItemProgress(file.tasks).inProgress
9326
+ completed: prog.completed,
9327
+ inProgress: prog.inProgress
9026
9328
  };
9027
9329
  }
9028
- return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
9330
+ return {
9331
+ ok: false,
9332
+ message: "Plan storage path not configured \u2014 cannot planify.",
9333
+ count: file.tasks.length,
9334
+ completed: prog.completed,
9335
+ inProgress: prog.inProgress
9336
+ };
9029
9337
  }
9030
9338
  const p = computeTaskItemProgress(file.tasks);
9031
9339
  const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
@@ -9069,6 +9377,36 @@ var TIER1_TOOLS = [
9069
9377
  jsonTool,
9070
9378
  searchTool
9071
9379
  ];
9380
+ var TIER2_TOOLS = [
9381
+ replaceTool,
9382
+ execTool,
9383
+ fetchTool,
9384
+ gitTool,
9385
+ treeTool,
9386
+ lintTool,
9387
+ formatTool,
9388
+ typecheckTool,
9389
+ testTool,
9390
+ todoTool,
9391
+ planTool,
9392
+ taskTool,
9393
+ installTool,
9394
+ auditTool
9395
+ ];
9396
+ var TIER3_TOOLS = [
9397
+ outdatedTool,
9398
+ logsTool,
9399
+ documentTool,
9400
+ scaffoldTool,
9401
+ toolSearchTool,
9402
+ toolUseTool,
9403
+ batchToolUseTool,
9404
+ toolHelpTool,
9405
+ codebaseIndexTool,
9406
+ codebaseSearchTool,
9407
+ codebaseStatsTool,
9408
+ setWorkingDirTool
9409
+ ];
9072
9410
  var builtinTools = [
9073
9411
  readTool,
9074
9412
  writeTool,
@@ -9115,6 +9453,162 @@ var builtinToolsPack = {
9115
9453
  tools: builtinTools
9116
9454
  };
9117
9455
 
9118
- export { CircuitBreaker, CircuitOpenError, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
9456
+ // src/tool-icon-map.ts
9457
+ var TOOL_ICON_MAP = {
9458
+ // File operations
9459
+ read: "file",
9460
+ write: "file",
9461
+ create: "file",
9462
+ // File modification
9463
+ edit: "edit",
9464
+ patch: "edit",
9465
+ replace: "edit",
9466
+ // Content search
9467
+ grep: "search",
9468
+ search: "search",
9469
+ // File discovery
9470
+ glob: "folder",
9471
+ // Shell/command execution
9472
+ bash: "terminal",
9473
+ exec: "terminal",
9474
+ run: "terminal",
9475
+ command: "terminal",
9476
+ shell: "terminal",
9477
+ // Network
9478
+ fetch: "web",
9479
+ curl: "web",
9480
+ http: "web",
9481
+ request: "web",
9482
+ // Version control
9483
+ git: "git",
9484
+ // Directory structure
9485
+ tree: "tree",
9486
+ ls: "tree",
9487
+ list: "tree",
9488
+ // Code quality
9489
+ lint: "code",
9490
+ format: "code",
9491
+ typecheck: "code",
9492
+ // Testing
9493
+ test: "test",
9494
+ tests: "test",
9495
+ // Package management
9496
+ install: "package",
9497
+ uninstall: "package",
9498
+ audit: "package",
9499
+ outdated: "package",
9500
+ npm: "package",
9501
+ pnpm: "package",
9502
+ yarn: "package",
9503
+ // Documentation
9504
+ document: "document",
9505
+ doc: "document",
9506
+ jsdoc: "document",
9507
+ // Project scaffolding
9508
+ scaffold: "scaffold",
9509
+ generate: "scaffold",
9510
+ template: "scaffold",
9511
+ // Task management
9512
+ todo: "todo",
9513
+ todos: "todo",
9514
+ // Planning
9515
+ plan: "plan",
9516
+ planning: "plan",
9517
+ // Structured tasks
9518
+ task: "task",
9519
+ tasks: "task",
9520
+ // Meta/tools
9521
+ "tool-use": "meta",
9522
+ "batch-tool-use": "meta",
9523
+ "tool-search": "meta",
9524
+ "tool-help": "meta",
9525
+ tool_use: "meta",
9526
+ batch_tool_use: "meta",
9527
+ tool_search: "meta",
9528
+ tool_help: "meta",
9529
+ // Code indexing
9530
+ "codebase-index": "index",
9531
+ "codebase-search": "index",
9532
+ "codebase-stats": "index",
9533
+ "codebase_index": "index",
9534
+ "codebase_search": "index",
9535
+ "codebase_stats": "index",
9536
+ // Data
9537
+ json: "json",
9538
+ parse: "json",
9539
+ query: "json",
9540
+ // Comparison
9541
+ diff: "diff",
9542
+ compare: "diff",
9543
+ // Logs
9544
+ logs: "logs",
9545
+ log: "logs",
9546
+ // Configuration
9547
+ "set-working-dir": "settings",
9548
+ set_working_dir: "settings",
9549
+ cwd: "settings",
9550
+ cd: "settings",
9551
+ // AI/Agent
9552
+ think: "brain",
9553
+ reason: "brain",
9554
+ analyze: "brain",
9555
+ reasoning: "brain"
9556
+ };
9557
+ function getToolIcon(toolName) {
9558
+ return TOOL_ICON_MAP[toolName.toLowerCase()] ?? "fallback";
9559
+ }
9560
+ var TOOL_ICON_CONFIG = {
9561
+ file: { icon: "file", color: "#6366f1" },
9562
+ // indigo
9563
+ edit: { icon: "edit", color: "#f59e0b" },
9564
+ // amber
9565
+ search: { icon: "search", color: "#10b981" },
9566
+ // emerald
9567
+ folder: { icon: "folder", color: "#8b5cf6" },
9568
+ // violet
9569
+ terminal: { icon: "terminal", color: "#ef4444" },
9570
+ // red
9571
+ web: { icon: "web", color: "#06b6d4" },
9572
+ // cyan
9573
+ git: { icon: "git", color: "#f97316" },
9574
+ // orange
9575
+ tree: { icon: "tree", color: "#22c55e" },
9576
+ // green
9577
+ code: { icon: "code", color: "#3b82f6" },
9578
+ // blue
9579
+ test: { icon: "test", color: "#84cc16" },
9580
+ // lime
9581
+ package: { icon: "package", color: "#ec4899" },
9582
+ // pink
9583
+ document: { icon: "document", color: "#14b8a6" },
9584
+ // teal
9585
+ scaffold: { icon: "scaffold", color: "#f43f5e" },
9586
+ // rose
9587
+ todo: { icon: "todo", color: "#a855f7" },
9588
+ // purple
9589
+ plan: { icon: "plan", color: "#7c3aed" },
9590
+ // violet-dark
9591
+ task: { icon: "task", color: "#db2777" },
9592
+ // pink-dark
9593
+ meta: { icon: "meta", color: "#6b7280" },
9594
+ // gray
9595
+ index: { icon: "index", color: "#0ea5e9" },
9596
+ // sky
9597
+ json: { icon: "json", color: "#fbbf24" },
9598
+ // yellow
9599
+ diff: { icon: "diff", color: "#a3e635" },
9600
+ // lime-light
9601
+ logs: { icon: "logs", color: "#78716c" },
9602
+ // stone
9603
+ settings: { icon: "settings", color: "#64748b" },
9604
+ // slate
9605
+ brain: { icon: "brain", color: "#d946ef" },
9606
+ // fuchsia
9607
+ fallback: { icon: "fallback", color: "#9ca3af" }
9608
+ // gray-light
9609
+ };
9610
+ var FALLBACK_ICON = "fallback";
9611
+
9612
+ export { CircuitBreaker, CircuitOpenError, FALLBACK_ICON, IndexCircuitBreaker, IndexTimeoutError, OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, TOOL_ICON_CONFIG, TOOL_ICON_MAP, _resetProcessRegistry, auditTool, bashTool, batchToolUseTool, builtinTools, builtinToolsPack, cancelPendingReindexes, codebaseIndexStats, codebaseIndexTool, codebaseSearchTool, codebaseStatsTool, createModeTool, diffTool, documentTool, editTool, enqueueReindex, execTool, fetchTool, forgetTool, formatTool, getIndexState, getProcessRegistry, getToolIcon, gitTool, globTool, grepTool, indexCircuitBreaker, installTool, isIndexReady, isIndexableFile, isIndexing, jsonTool, lintTool, logsTool, onIndexStateChange, outdatedTool, patchTool, planTool, readTool, relatedMemoryTool, rememberTool, replaceTool, resetIndexCircuitBreaker, runStartupIndex, scaffoldTool, searchCodebaseIndex, searchMemoryTool, searchTool, shutdownCodebaseIndexHost, testTool, todoTool, toolHelpTool, toolSearchTool, toolUseTool, treeTool, typecheckTool, writeTool };
9119
9613
  //# sourceMappingURL=index.js.map
9120
9614
  //# sourceMappingURL=index.js.map