@wrongstack/tools 0.264.0 → 0.267.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.
Files changed (90) 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 +796 -340
  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.d.ts +1 -0
  23. package/dist/edit.js +33 -22
  24. package/dist/edit.js.map +1 -1
  25. package/dist/exec.js +140 -3
  26. package/dist/exec.js.map +1 -1
  27. package/dist/fetch.js +1 -0
  28. package/dist/fetch.js.map +1 -1
  29. package/dist/format.js +153 -11
  30. package/dist/format.js.map +1 -1
  31. package/dist/git.d.ts +7 -0
  32. package/dist/git.js +20 -2
  33. package/dist/git.js.map +1 -1
  34. package/dist/glob.js +14 -7
  35. package/dist/glob.js.map +1 -1
  36. package/dist/grep.js +14 -7
  37. package/dist/grep.js.map +1 -1
  38. package/dist/index.d.ts +55 -3
  39. package/dist/index.js +957 -341
  40. package/dist/index.js.map +1 -1
  41. package/dist/install.js +153 -11
  42. package/dist/install.js.map +1 -1
  43. package/dist/json.js +1 -0
  44. package/dist/json.js.map +1 -1
  45. package/dist/lint.js +153 -11
  46. package/dist/lint.js.map +1 -1
  47. package/dist/logs.js +14 -7
  48. package/dist/logs.js.map +1 -1
  49. package/dist/memory.js +1 -0
  50. package/dist/memory.js.map +1 -1
  51. package/dist/mode.js +1 -0
  52. package/dist/mode.js.map +1 -1
  53. package/dist/outdated.js +21 -10
  54. package/dist/outdated.js.map +1 -1
  55. package/dist/pack.js +765 -339
  56. package/dist/pack.js.map +1 -1
  57. package/dist/patch.js +14 -7
  58. package/dist/patch.js.map +1 -1
  59. package/dist/process-registry.d.ts +56 -2
  60. package/dist/process-registry.js +138 -3
  61. package/dist/process-registry.js.map +1 -1
  62. package/dist/read.d.ts +3 -0
  63. package/dist/read.js +124 -22
  64. package/dist/read.js.map +1 -1
  65. package/dist/replace.js +14 -7
  66. package/dist/replace.js.map +1 -1
  67. package/dist/scaffold.js +14 -7
  68. package/dist/scaffold.js.map +1 -1
  69. package/dist/search.js +1 -0
  70. package/dist/search.js.map +1 -1
  71. package/dist/test.js +153 -11
  72. package/dist/test.js.map +1 -1
  73. package/dist/todo.js +1 -0
  74. package/dist/todo.js.map +1 -1
  75. package/dist/tool-help.js +1 -0
  76. package/dist/tool-help.js.map +1 -1
  77. package/dist/tool-icons.d.ts +20 -0
  78. package/dist/tool-icons.js +130 -0
  79. package/dist/tool-icons.js.map +1 -0
  80. package/dist/tool-search.js +1 -0
  81. package/dist/tool-search.js.map +1 -1
  82. package/dist/tool-use.js +1 -0
  83. package/dist/tool-use.js.map +1 -1
  84. package/dist/tree.js +14 -7
  85. package/dist/tree.js.map +1 -1
  86. package/dist/typecheck.js +153 -11
  87. package/dist/typecheck.js.map +1 -1
  88. package/dist/write.js +21 -15
  89. package/dist/write.js.map +1 -1
  90. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as fs4 from 'node:fs/promises';
2
+ import { toErrorMessage } from '@wrongstack/core/utils';
2
3
  import * as path from 'node:path';
3
4
  import { resolve, sep, dirname, join } from 'node:path';
4
5
  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';
6
- import { toErrorMessage } from '@wrongstack/core/utils';
6
+ 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, assessCommitSafety, recordPackageAction, detectPackageEcosystem, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
7
7
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
8
8
  import * as os from 'node:os';
9
9
  import * as fs7 from 'node:fs';
@@ -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) {
@@ -164,6 +168,8 @@ function normalizeCommandOutput(raw, opts = {}) {
164
168
  text = text.replace(/\n{3,}/g, "\n\n");
165
169
  return truncateHeadTail(text, opts.maxBytes ?? COMMAND_OUTPUT_MAX_BYTES);
166
170
  }
171
+
172
+ // src/read.ts
167
173
  var MAX_BYTES = 5 * 1024 * 1024;
168
174
  var readTool = {
169
175
  name: "read",
@@ -173,6 +179,7 @@ var readTool = {
173
179
  permission: "auto",
174
180
  mutating: false,
175
181
  capabilities: ["fs.read"],
182
+ icon: "file",
176
183
  maxOutputBytes: 262144,
177
184
  timeoutMs: 5e3,
178
185
  inputSchema: {
@@ -189,6 +196,11 @@ var readTool = {
189
196
  limit: {
190
197
  type: "integer",
191
198
  description: "Maximum number of lines to return (default is 2000)."
199
+ },
200
+ mode: {
201
+ type: "string",
202
+ enum: ["content", "summary"],
203
+ description: "Return full line-numbered content (default) or a compact file summary with imports/exports/symbols."
192
204
  }
193
205
  },
194
206
  required: ["path"]
@@ -202,14 +214,27 @@ var readTool = {
202
214
  } catch (err) {
203
215
  const code = err.code;
204
216
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
205
- throw new Error(
206
- `read: failed to stat "${input.path}": ${toErrorMessage(err)}`
207
- );
217
+ throw new Error(`read: failed to stat "${input.path}": ${toErrorMessage(err)}`);
208
218
  }
209
219
  if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
210
220
  if (stat11.size > MAX_BYTES) {
211
221
  throw new Error(`read: file too large (${stat11.size} bytes, limit ${MAX_BYTES})`);
212
222
  }
223
+ const offset = Math.max(1, input.offset ?? 1);
224
+ const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
225
+ const prior = getReadRangeRecord(ctx, absPath);
226
+ const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
227
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat11.mtimeMs, offset, requestedEnd)) {
228
+ ctx.recordRead(absPath, stat11.mtimeMs);
229
+ return {
230
+ text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat11.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
231
+ total_lines: prior.totalLines,
232
+ encoding: "utf8",
233
+ truncated: requestedEnd < prior.totalLines,
234
+ cached: true,
235
+ note: "Repeated read suppressed to save tokens."
236
+ };
237
+ }
213
238
  const buf = await fs4.readFile(absPath);
214
239
  if (isBinaryBuffer(buf)) {
215
240
  throw new Error(`read: "${input.path}" appears to be binary`);
@@ -217,17 +242,38 @@ var readTool = {
217
242
  const text = buf.toString("utf8");
218
243
  const allLines = text.split(/\r\n|\r|\n/);
219
244
  const total = allLines.length;
220
- const offset = Math.max(1, input.offset ?? 1);
221
- const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
245
+ if (input.mode === "summary") {
246
+ ctx.recordRead(absPath, stat11.mtimeMs);
247
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, Math.min(total, 200));
248
+ return {
249
+ text: summarizeFile(input.path, stat11.size, allLines),
250
+ total_lines: total,
251
+ encoding: "utf8",
252
+ truncated: total > 200,
253
+ note: "Summary mode returned compact structure instead of full file content."
254
+ };
255
+ }
222
256
  if (limit === 0) {
223
257
  ctx.recordRead(absPath, stat11.mtimeMs);
258
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, 1, 0);
224
259
  return { text: "", total_lines: total, encoding: "utf8", truncated: total > 0 };
225
260
  }
261
+ if (offset > total) {
262
+ ctx.recordRead(absPath, stat11.mtimeMs);
263
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, total + 1, total + 1);
264
+ return {
265
+ text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
266
+ total_lines: total,
267
+ encoding: "utf8",
268
+ truncated: false
269
+ };
270
+ }
226
271
  const slice = allLines.slice(offset - 1, offset - 1 + limit);
227
272
  const truncated = offset - 1 + slice.length < total;
228
273
  const width = String(offset + slice.length - 1).length;
229
274
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
230
275
  ctx.recordRead(absPath, stat11.mtimeMs);
276
+ rememberReadRange(ctx, absPath, stat11.mtimeMs, total, offset, offset + slice.length - 1);
231
277
  return {
232
278
  text: numbered,
233
279
  total_lines: total,
@@ -236,6 +282,62 @@ var readTool = {
236
282
  };
237
283
  }
238
284
  };
285
+ var READ_RANGES_META_KEY = "tools.read.ranges.v1";
286
+ function getReadRanges(ctx) {
287
+ const existing = ctx.meta[READ_RANGES_META_KEY];
288
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
289
+ return existing;
290
+ }
291
+ const next = {};
292
+ ctx.meta[READ_RANGES_META_KEY] = next;
293
+ return next;
294
+ }
295
+ function getReadRangeRecord(ctx, absPath) {
296
+ return getReadRanges(ctx)[absPath];
297
+ }
298
+ function rememberReadRange(ctx, absPath, mtimeMs, totalLines, start, end) {
299
+ if (end < start) return;
300
+ const ranges = getReadRanges(ctx);
301
+ const prior = ranges[absPath];
302
+ const nextRanges = prior && Math.abs(prior.mtimeMs - mtimeMs) <= 1 ? prior.ranges.slice() : [];
303
+ nextRanges.push({ start, end });
304
+ ranges[absPath] = {
305
+ mtimeMs,
306
+ totalLines,
307
+ ranges: mergeRanges(nextRanges)
308
+ };
309
+ }
310
+ function coversRange(record, mtimeMs, start, end) {
311
+ if (Math.abs(record.mtimeMs - mtimeMs) > 1) return false;
312
+ return record.ranges.some((range) => range.start <= start && range.end >= end);
313
+ }
314
+ function mergeRanges(ranges) {
315
+ const sorted = ranges.slice().sort((a, b) => a.start - b.start);
316
+ const merged = [];
317
+ for (const range of sorted) {
318
+ const last = merged[merged.length - 1];
319
+ if (!last || range.start > last.end + 1) {
320
+ merged.push({ ...range });
321
+ continue;
322
+ }
323
+ last.end = Math.max(last.end, range.end);
324
+ }
325
+ return merged;
326
+ }
327
+ function summarizeFile(filePath, bytes, lines) {
328
+ const interesting = lines.map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(
329
+ ({ line }) => /^(import\s|export\s|class\s|interface\s|type\s|function\s|const\s+\w+\s*=|let\s+\w+\s*=|var\s+\w+\s*=|def\s+|async\s+function\s)/.test(
330
+ line
331
+ )
332
+ ).slice(0, 80).map(({ line, number }) => `${number}: ${line}`);
333
+ return [
334
+ `summary: ${filePath}`,
335
+ `bytes=${bytes}`,
336
+ `total_lines=${lines.length}`,
337
+ interesting.length > 0 ? `symbols/imports:
338
+ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
339
+ ].join("\n");
340
+ }
239
341
  var writeTool = {
240
342
  name: "write",
241
343
  category: "Filesystem",
@@ -245,6 +347,7 @@ var writeTool = {
245
347
  mutating: true,
246
348
  timeoutMs: 5e3,
247
349
  capabilities: ["fs.write"],
350
+ icon: "file",
248
351
  inputSchema: {
249
352
  type: "object",
250
353
  properties: {
@@ -303,11 +406,12 @@ var writeTool = {
303
406
  var editTool = {
304
407
  name: "edit",
305
408
  category: "Filesystem",
306
- description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It requires that you have previously called `read` on the file in the current session. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
307
- usageHint: "MANDATORY WORKFLOW:\n1. Call `read` on the target file first (in the same conversation).\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nThis tool is much safer than `write` for existing files because it works against the last-read version.",
409
+ description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
410
+ usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.",
308
411
  permission: "confirm",
309
412
  mutating: true,
310
413
  capabilities: ["fs.write"],
414
+ icon: "edit",
311
415
  timeoutMs: 5e3,
312
416
  inputSchema: {
313
417
  type: "object",
@@ -332,9 +436,7 @@ var editTool = {
332
436
  throw err;
333
437
  });
334
438
  if (!stat11.isFile()) throw new Error(`edit: "${input.path}" is not a regular file`);
335
- if (!ctx.hasRead(absPath)) {
336
- throw new Error(`edit: file "${input.path}" was not read in this session. Read it first.`);
337
- }
439
+ const autoRead = !ctx.hasRead(absPath);
338
440
  const original = await fs4.readFile(absPath, "utf8");
339
441
  const updated = await fs4.stat(absPath);
340
442
  const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
@@ -342,15 +444,21 @@ var editTool = {
342
444
  if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
343
445
  throw new Error(`edit: file "${input.path}" was modified externally. Re-read it first.`);
344
446
  }
447
+ if (autoRead && updated.mtimeMs > stat11.mtimeMs + mtimeTolerance) {
448
+ throw new Error(`edit: file "${input.path}" changed while being auto-read. Retry the edit.`);
449
+ }
450
+ const autoReadNote = autoRead ? `No prior read was recorded for "${input.path}"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.` : void 0;
345
451
  const style = detectNewlineStyle(original);
346
452
  const fileLf = normalizeToLf(original);
347
453
  const oldLf = normalizeToLf(input.old_string);
348
454
  const newLf = normalizeToLf(input.new_string);
349
455
  if (oldLf === newLf) {
456
+ if (autoRead) ctx.recordRead(absPath, updated.mtimeMs);
350
457
  return {
351
458
  path: absPath,
352
459
  replacements: 0,
353
- diff: "(no-op: old and new are identical)"
460
+ diff: "(no-op: old and new are identical)",
461
+ note: autoReadNote
354
462
  };
355
463
  }
356
464
  let count = 0;
@@ -391,7 +499,8 @@ var editTool = {
391
499
  return {
392
500
  path: absPath,
393
501
  replacements: input.replace_all ? count : 1,
394
- diff
502
+ diff,
503
+ note: autoReadNote
395
504
  };
396
505
  }
397
506
  };
@@ -475,6 +584,7 @@ var replaceTool = {
475
584
  permission: "confirm",
476
585
  mutating: true,
477
586
  capabilities: ["fs.write"],
587
+ icon: "edit",
478
588
  timeoutMs: 3e4,
479
589
  inputSchema: {
480
590
  type: "object",
@@ -676,6 +786,7 @@ var globTool = {
676
786
  permission: "auto",
677
787
  mutating: false,
678
788
  capabilities: ["fs.read"],
789
+ icon: "folder",
679
790
  maxOutputBytes: 65536,
680
791
  timeoutMs: 5e3,
681
792
  inputSchema: {
@@ -761,6 +872,7 @@ var grepTool = {
761
872
  permission: "auto",
762
873
  mutating: false,
763
874
  capabilities: ["fs.read"],
875
+ icon: "search",
764
876
  maxOutputBytes: 131072,
765
877
  timeoutMs: 1e4,
766
878
  inputSchema: {
@@ -1162,6 +1274,23 @@ var CircuitBreaker = class {
1162
1274
  lastSlowAt = null;
1163
1275
  /** Timestamp when the breaker was opened (for cooldown calculation). */
1164
1276
  openedAt = null;
1277
+ /**
1278
+ * Master enable flag. When false the breaker is bypassed: `beforeCall`
1279
+ * always returns true and `afterCall` records nothing. The class itself
1280
+ * defaults to enabled (so the standalone unit tests exercise tripping); the
1281
+ * ProcessRegistry flips this off until the user opts in via `/settings`.
1282
+ */
1283
+ enabled = true;
1284
+ /**
1285
+ * Fired (best-effort) when the breaker transitions into the `open` state.
1286
+ * The registry uses this to arm its auto kill/reset countdown.
1287
+ */
1288
+ onTrip;
1289
+ /**
1290
+ * Fired (best-effort) when the breaker returns to `closed` after having been
1291
+ * open/half-open. The registry uses this to cancel a pending kill/reset.
1292
+ */
1293
+ onReset;
1165
1294
  constructor(config = {}) {
1166
1295
  this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
1167
1296
  this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
@@ -1170,12 +1299,22 @@ var CircuitBreaker = class {
1170
1299
  this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
1171
1300
  this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
1172
1301
  }
1302
+ /** Toggle the master enable. Disabling resets to a clean `closed` state. */
1303
+ setEnabled(enabled) {
1304
+ if (this.enabled === enabled) return;
1305
+ this.enabled = enabled;
1306
+ if (!enabled) this._reset();
1307
+ }
1308
+ get isEnabled() {
1309
+ return this.enabled;
1310
+ }
1173
1311
  /**
1174
1312
  * Returns true if the circuit allows a new call to proceed.
1175
1313
  * When false, callers should abort the tool call and return a
1176
1314
  * circuit-breaker error instead of spawning a process.
1177
1315
  */
1178
1316
  get canProceed() {
1317
+ if (!this.enabled) return true;
1179
1318
  this._checkStateTransition();
1180
1319
  return this.state !== "open";
1181
1320
  }
@@ -1211,7 +1350,7 @@ var CircuitBreaker = class {
1211
1350
  * not affect breaker state.
1212
1351
  */
1213
1352
  beforeCall(bypass = false) {
1214
- if (bypass) return true;
1353
+ if (bypass || !this.enabled) return true;
1215
1354
  this._checkStateTransition();
1216
1355
  if (this.state === "open") return false;
1217
1356
  return true;
@@ -1226,7 +1365,7 @@ var CircuitBreaker = class {
1226
1365
  * Use for background/fire-and-forget processes.
1227
1366
  */
1228
1367
  afterCall(durationMs, failed, bypass = false) {
1229
- if (bypass) return;
1368
+ if (bypass || !this.enabled) return;
1230
1369
  const now = Date.now();
1231
1370
  if (this.state === "half-open") {
1232
1371
  if (failed) {
@@ -1272,12 +1411,23 @@ var CircuitBreaker = class {
1272
1411
  if (this.state === "open") return;
1273
1412
  this.state = "open";
1274
1413
  this.openedAt = Date.now();
1414
+ try {
1415
+ this.onTrip?.();
1416
+ } catch {
1417
+ }
1275
1418
  }
1276
1419
  _reset() {
1420
+ const wasRecovering = this.state !== "closed";
1277
1421
  this.state = "closed";
1278
1422
  this.consecutiveFailures = 0;
1279
1423
  this.window = [];
1280
1424
  this.openedAt = null;
1425
+ if (wasRecovering) {
1426
+ try {
1427
+ this.onReset?.();
1428
+ } catch {
1429
+ }
1430
+ }
1281
1431
  }
1282
1432
  /** Transition from open → half-open when cooldown elapses. */
1283
1433
  _checkStateTransition() {
@@ -1339,8 +1489,21 @@ function killWin32Tree(pid) {
1339
1489
  var ProcessRegistryImpl = class {
1340
1490
  processes = /* @__PURE__ */ new Map();
1341
1491
  breaker;
1492
+ /**
1493
+ * Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,
1494
+ * a countdown is armed; on expiry all tracked processes are killed and the
1495
+ * breaker is reset to closed (forced recovery). Zero means manual recovery
1496
+ * only (`/kill reset`).
1497
+ */
1498
+ autoKillResetMs = 0;
1499
+ autoKillTimer = null;
1500
+ autoKillArmedAt = null;
1501
+ breakerCountdownListeners = [];
1342
1502
  constructor(breakerConfig) {
1343
1503
  this.breaker = new CircuitBreaker(breakerConfig);
1504
+ this.breaker.onTrip = () => this._armAutoKillReset();
1505
+ this.breaker.onReset = () => this._cancelAutoKillReset();
1506
+ this.breaker.setEnabled(false);
1344
1507
  }
1345
1508
  register(info) {
1346
1509
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
@@ -1416,6 +1579,90 @@ var ProcessRegistryImpl = class {
1416
1579
  forceBreakerReset() {
1417
1580
  this.breaker.forceReset();
1418
1581
  }
1582
+ /**
1583
+ * Configure circuit-breaker protection at runtime. Called from `/settings`
1584
+ * (instant, all modes) and on TUI mount (applies persisted config).
1585
+ *
1586
+ * - `enabled` toggles whether the breaker gates `bash`/`exec`.
1587
+ * - `autoKillResetMs` arms the auto kill/reset countdown when the breaker
1588
+ * trips (0 = manual recovery only).
1589
+ *
1590
+ * Re-applies cleanly on every call: cancels a pending countdown when the
1591
+ * timeout is cleared or protection disabled, and re-arms if the breaker is
1592
+ * currently open under the new settings.
1593
+ */
1594
+ setBreakerConfig(cfg) {
1595
+ if (cfg.enabled !== void 0) this.breaker.setEnabled(cfg.enabled);
1596
+ if (cfg.autoKillResetMs !== void 0) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);
1597
+ if (this.autoKillResetMs <= 0) {
1598
+ this._cancelAutoKillReset();
1599
+ return;
1600
+ }
1601
+ if (this.breaker.isEnabled && this.breaker.snapshot().state === "open") {
1602
+ this._armAutoKillReset();
1603
+ }
1604
+ }
1605
+ /**
1606
+ * Live countdown to the next auto kill/reset, or null when nothing is armed.
1607
+ * The TUI polls this on a 1s tick while armed so the statusline decrements.
1608
+ */
1609
+ getBreakerCountdown() {
1610
+ if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;
1611
+ const elapsed = Date.now() - this.autoKillArmedAt;
1612
+ return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };
1613
+ }
1614
+ /**
1615
+ * Subscribe to countdown arm/cancel events. Returns an unsubscribe function.
1616
+ * Use {@link getBreakerCountdown} for the live ticking value between events.
1617
+ */
1618
+ onBreakerCountdownChange(listener) {
1619
+ this.breakerCountdownListeners.push(listener);
1620
+ return () => {
1621
+ this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);
1622
+ };
1623
+ }
1624
+ _emitBreakerCountdown() {
1625
+ const snap = this.getBreakerCountdown();
1626
+ for (const l of this.breakerCountdownListeners) {
1627
+ try {
1628
+ l(snap);
1629
+ } catch {
1630
+ }
1631
+ }
1632
+ }
1633
+ /**
1634
+ * Arm the auto kill/reset countdown. Idempotent: re-arming resets the window
1635
+ * (a fresh trip after a failed half-open probe restarts the clock). No-op
1636
+ * when protection is off or no timeout is configured.
1637
+ */
1638
+ _armAutoKillReset() {
1639
+ if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;
1640
+ this._clearAutoKillTimer();
1641
+ this.autoKillArmedAt = Date.now();
1642
+ this.autoKillTimer = setTimeout(() => {
1643
+ this.autoKillTimer = null;
1644
+ this.autoKillArmedAt = null;
1645
+ this.killAll({ force: false });
1646
+ this.breaker.forceReset();
1647
+ this._emitBreakerCountdown();
1648
+ }, this.autoKillResetMs);
1649
+ this.autoKillTimer.unref?.();
1650
+ this._emitBreakerCountdown();
1651
+ }
1652
+ _cancelAutoKillReset() {
1653
+ const wasArmed = this.autoKillArmedAt !== null;
1654
+ this._clearAutoKillTimer();
1655
+ if (wasArmed) {
1656
+ this.autoKillArmedAt = null;
1657
+ this._emitBreakerCountdown();
1658
+ }
1659
+ }
1660
+ _clearAutoKillTimer() {
1661
+ if (this.autoKillTimer !== null) {
1662
+ clearTimeout(this.autoKillTimer);
1663
+ this.autoKillTimer = null;
1664
+ }
1665
+ }
1419
1666
  /** Kill a single process by PID.
1420
1667
  *
1421
1668
  * On POSIX: sends SIGTERM to the *process group* (-pid) so that
@@ -1540,6 +1787,7 @@ var bashTool = {
1540
1787
  permission: "confirm",
1541
1788
  mutating: true,
1542
1789
  riskTier: "destructive",
1790
+ icon: "terminal",
1543
1791
  // Trust rules match on the literal `command` string. Without subjectKey
1544
1792
  // the policy heuristic would have done the same here, but declaring it
1545
1793
  // explicitly removes the implicit cross-tool aliasing.
@@ -2002,6 +2250,7 @@ var execTool = {
2002
2250
  riskTier: "standard",
2003
2251
  timeoutMs: DEFAULT_TIMEOUT_MS2,
2004
2252
  capabilities: ["shell.restricted"],
2253
+ icon: "terminal",
2005
2254
  inputSchema: {
2006
2255
  type: "object",
2007
2256
  properties: {
@@ -2101,7 +2350,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
2101
2350
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
2102
2351
  const resolved = resolveWin32Command(cmd);
2103
2352
  const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
2104
- const child = spawn(resolved, args, {
2353
+ const spawnCmd = needsShell ? cmd : resolved;
2354
+ const child = spawn(spawnCmd, args, {
2105
2355
  cwd,
2106
2356
  env: buildChildEnv(sessionId),
2107
2357
  stdio: ["ignore", "pipe", "pipe"],
@@ -2288,6 +2538,7 @@ var fetchTool = {
2288
2538
  permission: "confirm",
2289
2539
  mutating: false,
2290
2540
  capabilities: ["net.outbound"],
2541
+ icon: "web",
2291
2542
  // Trust rules for fetch match on the literal URL — declare it explicitly
2292
2543
  // so a user can trust `https://api.example.com/*` without accidentally
2293
2544
  // matching that pattern on any other tool that happens to have a `url`
@@ -2438,6 +2689,7 @@ var searchTool = {
2438
2689
  permission: "confirm",
2439
2690
  mutating: false,
2440
2691
  capabilities: ["net.outbound"],
2692
+ icon: "search",
2441
2693
  timeoutMs: TIMEOUT_MS2,
2442
2694
  inputSchema: {
2443
2695
  type: "object",
@@ -2653,6 +2905,7 @@ var todoTool = {
2653
2905
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
2654
2906
  timeoutMs: 1e3,
2655
2907
  capabilities: ["session.todo"],
2908
+ icon: "todo",
2656
2909
  inputSchema: {
2657
2910
  type: "object",
2658
2911
  properties: {
@@ -2753,11 +3006,12 @@ var todoTool = {
2753
3006
  var planTool = {
2754
3007
  name: "plan",
2755
3008
  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.',
3009
+ 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.',
3010
+ 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
3011
  permission: "confirm",
2759
3012
  mutating: true,
2760
3013
  capabilities: ["fs.write"],
3014
+ icon: "plan",
2761
3015
  timeoutMs: 2e3,
2762
3016
  inputSchema: {
2763
3017
  type: "object",
@@ -2797,12 +3051,26 @@ var planTool = {
2797
3051
  template: {
2798
3052
  type: "string",
2799
3053
  description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
3054
+ },
3055
+ scope: {
3056
+ type: "string",
3057
+ enum: ["session", "project"],
3058
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
2800
3059
  }
2801
3060
  },
2802
3061
  required: ["action"]
2803
3062
  },
2804
3063
  async execute(input, ctx) {
2805
- const planPath = ctx.meta["plan.path"];
3064
+ const sessionPlanPath = ctx.meta["plan.path"];
3065
+ let planPath;
3066
+ if (input.scope === "project") {
3067
+ if (typeof sessionPlanPath === "string") {
3068
+ const lastSep = Math.max(sessionPlanPath.lastIndexOf("/"), sessionPlanPath.lastIndexOf("\\"));
3069
+ planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
3070
+ }
3071
+ } else {
3072
+ planPath = sessionPlanPath;
3073
+ }
2806
3074
  if (typeof planPath !== "string" || !planPath) {
2807
3075
  return {
2808
3076
  ok: false,
@@ -2816,148 +3084,169 @@ var planTool = {
2816
3084
  let early = null;
2817
3085
  const taskifyMeta = { title: "", details: "" };
2818
3086
  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;
3087
+ let plan;
3088
+ try {
3089
+ plan = await mutatePlan(planPath, sessionId, async (p) => {
3090
+ switch (input.action) {
3091
+ case "show":
3092
+ break;
3093
+ case "add": {
3094
+ const title = input.title?.trim();
3095
+ if (!title) {
3096
+ early = mkResult(p, false, "add requires `title`.");
3097
+ return p;
3098
+ }
3099
+ const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
3100
+ return updated;
2870
3101
  }
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;
3102
+ case "start":
3103
+ case "done": {
3104
+ if (!input.target) {
3105
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
3106
+ return p;
3107
+ }
3108
+ const next = setPlanItemStatus(
3109
+ p,
3110
+ input.target,
3111
+ input.action === "start" ? "in_progress" : "done"
3112
+ );
3113
+ if (next === p) {
3114
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3115
+ return p;
3116
+ }
3117
+ return next;
2885
3118
  }
2886
- const template = getPlanTemplate(templateName);
2887
- if (!template) {
2888
- early = mkResult(p, false, `Unknown template "${templateName}".`);
2889
- return p;
3119
+ case "remove": {
3120
+ if (!input.target) {
3121
+ early = mkResult(p, false, "remove requires `target` (id|index|substring).");
3122
+ return p;
3123
+ }
3124
+ const next = removePlanItem(p, input.target);
3125
+ if (next === p) {
3126
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3127
+ return p;
3128
+ }
3129
+ return next;
2890
3130
  }
2891
- let updated = p;
2892
- for (const item of template.items) {
2893
- ({ plan: updated } = addPlanItem(updated, item.title, item.details));
3131
+ case "promote": {
3132
+ if (!input.target) {
3133
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
3134
+ return p;
3135
+ }
3136
+ const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
3137
+ if (!derived) {
3138
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3139
+ return p;
3140
+ }
3141
+ ctx.state.replaceTodos(derived.todos);
3142
+ early = mkResult(
3143
+ derived.plan,
3144
+ true,
3145
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
3146
+ derived.todos
3147
+ );
3148
+ return derived.plan;
2894
3149
  }
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;
3150
+ case "template_use": {
3151
+ const templateName = input.template?.trim();
3152
+ if (!templateName) {
3153
+ early = mkResult(p, false, "template_use requires `template` name.");
3154
+ return p;
3155
+ }
3156
+ const template = getPlanTemplate(templateName);
3157
+ if (!template) {
3158
+ early = mkResult(p, false, `Unknown template "${templateName}".`);
3159
+ return p;
3160
+ }
3161
+ let updated = p;
3162
+ for (const item of template.items) {
3163
+ ({ plan: updated } = addPlanItem(updated, item.title, item.details));
3164
+ }
3165
+ early = mkResult(
3166
+ updated,
3167
+ true,
3168
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
3169
+ );
3170
+ return updated;
2908
3171
  }
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));
3172
+ case "clear":
3173
+ return clearPlan(p);
3174
+ case "taskify": {
3175
+ if (!input.target) {
3176
+ early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
3177
+ return p;
3178
+ }
3179
+ let itemIdx = -1;
3180
+ const asNum = Number.parseInt(input.target, 10);
3181
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
3182
+ itemIdx = asNum - 1;
3183
+ } else {
3184
+ itemIdx = p.items.findIndex((it) => it.id === input.target);
3185
+ if (itemIdx === -1) {
3186
+ const lower = input.target.toLowerCase();
3187
+ itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
3188
+ }
2918
3189
  }
3190
+ if (itemIdx === -1 || !p.items[itemIdx]) {
3191
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
3192
+ return p;
3193
+ }
3194
+ const item = p.items[itemIdx];
3195
+ taskifyMeta.title = item.title;
3196
+ taskifyMeta.details = item.details ?? "";
3197
+ didTaskify = true;
3198
+ break;
2919
3199
  }
2920
- if (itemIdx === -1 || !p.items[itemIdx]) {
2921
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
3200
+ default:
3201
+ early = mkResult(p, false, `Unknown action "${input.action}".`);
2922
3202
  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
3203
  }
2930
- default:
2931
- early = mkResult(p, false, `Unknown action "${input.action}".`);
2932
- return p;
2933
- }
2934
- return p;
2935
- });
3204
+ return p;
3205
+ });
3206
+ } catch (err) {
3207
+ return {
3208
+ ok: false,
3209
+ message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
3210
+ plan: "",
3211
+ count: 0,
3212
+ open: 0
3213
+ };
3214
+ }
2936
3215
  if (early) return early;
2937
3216
  if (didTaskify) {
2938
- const taskPath = ctx.meta["task.path"];
2939
- if (typeof taskPath !== "string" || !taskPath) {
3217
+ const taskPathRaw = ctx.meta["task.path"];
3218
+ if (typeof taskPathRaw !== "string" || !taskPathRaw) {
2940
3219
  return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
2941
3220
  }
2942
- const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
3221
+ let taskPath = taskPathRaw;
3222
+ if (input.scope === "project") {
3223
+ const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
3224
+ taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
3225
+ }
2943
3226
  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.
3227
+ try {
3228
+ const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
3229
+ f.tasks.push({
3230
+ id: `task_${randomUUID()}`,
3231
+ title: taskifyMeta.title,
3232
+ description: taskifyMeta.details || void 0,
3233
+ type: "feature",
3234
+ priority: "medium",
3235
+ status: "pending",
3236
+ createdAt: now,
3237
+ updatedAt: now
3238
+ });
3239
+ return f;
3240
+ });
3241
+ return mkResult(
3242
+ plan,
3243
+ true,
3244
+ `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
2959
3245
  ${formatTaskList(taskFile.tasks)}`
2960
- );
3246
+ );
3247
+ } catch (err) {
3248
+ return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
3249
+ }
2961
3250
  }
2962
3251
  return mkResult(plan, true, `Plan ${input.action} ok.`);
2963
3252
  }
@@ -2980,8 +3269,9 @@ var gitTool = {
2980
3269
  name: "git",
2981
3270
  category: "Git",
2982
3271
  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
- 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.",
3272
+ 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.\n- For `commit` in a possibly-shared working tree, pass an explicit `files` list scoped to what YOU changed. A bare commit (no `files`) includes ALL staged changes and may capture another agent's half-done work. Heed the `warning` field on the result.\nNever pass raw git flags through `args` for dangerous operations \u2014 use the structured fields.",
2984
3273
  permission: "confirm",
3274
+ icon: "git",
2985
3275
  // Conservative: any of these may mutate. The non-mutating commands
2986
3276
  // (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
2987
3277
  // and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
@@ -3068,6 +3358,22 @@ var gitTool = {
3068
3358
  };
3069
3359
  }
3070
3360
  const args = buildArgs(input);
3361
+ let safetyWarning;
3362
+ if (input.command === "commit") {
3363
+ try {
3364
+ const report = await assessCommitSafety({
3365
+ cwd: ctx.cwd,
3366
+ projectRoot: ctx.projectRoot,
3367
+ sessionId: ctx.session?.id,
3368
+ signal: opts.signal
3369
+ });
3370
+ if (report.warning) {
3371
+ const scopeNote = input.files ? "" : "\nNote: this commit has no explicit `files` list, so it will include ALL staged changes. Pass `files` to scope the commit to only what you changed.";
3372
+ safetyWarning = report.warning + scopeNote;
3373
+ }
3374
+ } catch {
3375
+ }
3376
+ }
3071
3377
  let stagedDiff;
3072
3378
  if (input.command === "commit" && !input.dry_run) {
3073
3379
  try {
@@ -3081,6 +3387,7 @@ var gitTool = {
3081
3387
  }
3082
3388
  const result = await runGit(args, gitDir, opts.signal);
3083
3389
  if (stagedDiff !== void 0) result.diff = stagedDiff;
3390
+ if (safetyWarning !== void 0) result.warning = safetyWarning;
3084
3391
  return result;
3085
3392
  }
3086
3393
  };
@@ -3240,6 +3547,7 @@ var patchTool = {
3240
3547
  permission: "confirm",
3241
3548
  mutating: true,
3242
3549
  capabilities: ["fs.write"],
3550
+ icon: "edit",
3243
3551
  timeoutMs: 3e4,
3244
3552
  inputSchema: {
3245
3553
  type: "object",
@@ -3353,6 +3661,7 @@ var jsonTool = {
3353
3661
  mutating: false,
3354
3662
  timeoutMs: 5e3,
3355
3663
  capabilities: ["fs.read"],
3664
+ icon: "json",
3356
3665
  inputSchema: {
3357
3666
  type: "object",
3358
3667
  properties: {
@@ -3475,6 +3784,7 @@ var diffTool = {
3475
3784
  permission: "auto",
3476
3785
  mutating: false,
3477
3786
  capabilities: ["fs.read"],
3787
+ icon: "diff",
3478
3788
  timeoutMs: 1e4,
3479
3789
  inputSchema: {
3480
3790
  type: "object",
@@ -3633,6 +3943,7 @@ var treeTool = {
3633
3943
  permission: "auto",
3634
3944
  mutating: false,
3635
3945
  capabilities: ["fs.read"],
3946
+ icon: "tree",
3636
3947
  timeoutMs: 15e3,
3637
3948
  inputSchema: {
3638
3949
  type: "object",
@@ -3799,8 +4110,9 @@ async function* spawnStream(opts) {
3799
4110
  let pending2 = "";
3800
4111
  let error;
3801
4112
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
3802
- const cmd = resolveWin32Command(opts.cmd);
3803
- const needsShell = isWin2 && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
4113
+ const resolved = resolveWin32Command(opts.cmd);
4114
+ const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
4115
+ const cmd = needsShell ? opts.cmd : resolved;
3804
4116
  const child = spawn(cmd, opts.args, {
3805
4117
  cwd: opts.cwd,
3806
4118
  env: buildChildEnv(),
@@ -3958,6 +4270,7 @@ var lintTool = {
3958
4270
  mutating: false,
3959
4271
  timeoutMs: 6e4,
3960
4272
  capabilities: ["shell.restricted"],
4273
+ icon: "code",
3961
4274
  inputSchema: {
3962
4275
  type: "object",
3963
4276
  properties: {
@@ -4052,6 +4365,7 @@ var formatTool = {
4052
4365
  permission: "confirm",
4053
4366
  mutating: true,
4054
4367
  capabilities: ["fs.write", "shell.exec"],
4368
+ icon: "code",
4055
4369
  timeoutMs: 6e4,
4056
4370
  inputSchema: {
4057
4371
  type: "object",
@@ -4153,6 +4467,7 @@ var typecheckTool = {
4153
4467
  mutating: false,
4154
4468
  timeoutMs: 12e4,
4155
4469
  capabilities: ["shell.restricted"],
4470
+ icon: "code",
4156
4471
  inputSchema: {
4157
4472
  type: "object",
4158
4473
  properties: {
@@ -4239,6 +4554,7 @@ var testTool = {
4239
4554
  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
4555
  permission: "confirm",
4241
4556
  mutating: false,
4557
+ icon: "test",
4242
4558
  timeoutMs: 12e4,
4243
4559
  capabilities: ["shell.restricted"],
4244
4560
  inputSchema: {
@@ -4396,6 +4712,7 @@ var installTool = {
4396
4712
  permission: "confirm",
4397
4713
  mutating: true,
4398
4714
  riskTier: "standard",
4715
+ icon: "package",
4399
4716
  timeoutMs: 12e4,
4400
4717
  capabilities: ["package.install", "shell.restricted"],
4401
4718
  inputSchema: {
@@ -4535,6 +4852,7 @@ var auditTool = {
4535
4852
  permission: "confirm",
4536
4853
  mutating: false,
4537
4854
  capabilities: ["shell.restricted"],
4855
+ icon: "package",
4538
4856
  timeoutMs: 6e4,
4539
4857
  inputSchema: {
4540
4858
  type: "object",
@@ -4630,6 +4948,7 @@ var outdatedTool = {
4630
4948
  description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
4631
4949
  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
4950
  permission: "confirm",
4951
+ icon: "package",
4633
4952
  // Network side-effecting (registry HTTP). Pairs with `mutating: true`
4634
4953
  // so the H7 invariant test (`no auto-permission tool declares
4635
4954
  // mutating: true`) passes — a tool claiming `'auto'` must be purely
@@ -4639,12 +4958,15 @@ var outdatedTool = {
4639
4958
  // fixed four sibling tools (mcp_control, shellcheck, shellcheck_scan,
4640
4959
  // web_search) but missed this one; applying the same contract here.
4641
4960
  mutating: true,
4642
- // Capability is just "network" — the tool only hits the package
4961
+ // Capability is outbound network — the tool only hits the package
4643
4962
  // registry over HTTP, never touches the filesystem or runs shell.
4963
+ // Use the canonical `net.outbound` capability (not the non-existent
4964
+ // `network` string) so the subagent allowlist recognises it and
4965
+ // permits read-only registry lookups under a director.
4644
4966
  // The H7 invariant test requires this array to be non-empty for
4645
4967
  // any mutating:true tool (meta-tools whitelisted). See
4646
4968
  // tests/permission-mutating-invariant.test.ts:92.
4647
- capabilities: ["network"],
4969
+ capabilities: ["net.outbound"],
4648
4970
  timeoutMs: 6e4,
4649
4971
  inputSchema: {
4650
4972
  type: "object",
@@ -4681,7 +5003,8 @@ function runOutdated(manager, args, cwd, signal) {
4681
5003
  const MAX = 1e5;
4682
5004
  const resolved = resolveWin32Command(manager);
4683
5005
  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 } : {} });
5006
+ const spawnCmd = needsShell ? manager : resolved;
5007
+ const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
4685
5008
  child.stdout?.on("data", (c) => {
4686
5009
  if (stdout.length < MAX) stdout += c.toString();
4687
5010
  });
@@ -4746,6 +5069,7 @@ var logsTool = {
4746
5069
  mutating: false,
4747
5070
  timeoutMs: 3e4,
4748
5071
  capabilities: ["shell.restricted"],
5072
+ icon: "logs",
4749
5073
  inputSchema: {
4750
5074
  type: "object",
4751
5075
  properties: {
@@ -4950,6 +5274,7 @@ var documentTool = {
4950
5274
  mutating: false,
4951
5275
  timeoutMs: 3e4,
4952
5276
  capabilities: ["fs.read"],
5277
+ icon: "document",
4953
5278
  inputSchema: {
4954
5279
  type: "object",
4955
5280
  properties: {
@@ -5185,6 +5510,7 @@ var scaffoldTool = {
5185
5510
  permission: "confirm",
5186
5511
  mutating: true,
5187
5512
  capabilities: ["fs.write.outside-project", "fs.write"],
5513
+ icon: "scaffold",
5188
5514
  timeoutMs: 3e4,
5189
5515
  inputSchema: {
5190
5516
  type: "object",
@@ -5280,6 +5606,7 @@ var toolSearchTool = {
5280
5606
  mutating: false,
5281
5607
  timeoutMs: 1e3,
5282
5608
  capabilities: ["tool.meta"],
5609
+ icon: "meta",
5283
5610
  inputSchema: {
5284
5611
  type: "object",
5285
5612
  properties: {
@@ -5359,6 +5686,7 @@ var toolUseTool = {
5359
5686
  mutating: true,
5360
5687
  timeoutMs: 6e4,
5361
5688
  capabilities: ["tool.mutate.any"],
5689
+ icon: "meta",
5362
5690
  inputSchema: {
5363
5691
  type: "object",
5364
5692
  properties: {
@@ -5429,6 +5757,7 @@ var batchToolUseTool = {
5429
5757
  mutating: true,
5430
5758
  timeoutMs: 12e4,
5431
5759
  capabilities: ["tool.mutate.any"],
5760
+ icon: "meta",
5432
5761
  inputSchema: {
5433
5762
  type: "object",
5434
5763
  properties: {
@@ -5534,6 +5863,7 @@ var toolHelpTool = {
5534
5863
  mutating: false,
5535
5864
  timeoutMs: 5e3,
5536
5865
  capabilities: ["tool.meta"],
5866
+ icon: "meta",
5537
5867
  inputSchema: {
5538
5868
  type: "object",
5539
5869
  properties: {
@@ -5658,6 +5988,7 @@ function rememberTool(memory) {
5658
5988
  mutating: true,
5659
5989
  timeoutMs: 2e3,
5660
5990
  capabilities: ["memory.write"],
5991
+ icon: "settings",
5661
5992
  inputSchema: {
5662
5993
  type: "object",
5663
5994
  properties: {
@@ -5832,6 +6163,7 @@ function createModeTool(modeStore) {
5832
6163
  mutating: true,
5833
6164
  timeoutMs: 5e3,
5834
6165
  capabilities: ["session.mode"],
6166
+ icon: "settings",
5835
6167
  inputSchema: {
5836
6168
  type: "object",
5837
6169
  properties: {
@@ -6588,39 +6920,57 @@ var IndexStore = class {
6588
6920
  }
6589
6921
  });
6590
6922
  }
6923
+ /**
6924
+ * Bulk-insert refs for many source symbols in a single transaction.
6925
+ *
6926
+ * Unlike {@link insertRefs} this does NOT delete per source id — the caller
6927
+ * (the indexer) has already cleared stale refs for the file via
6928
+ * {@link deleteRefsForFile}, so the per-source DELETE would be redundant work
6929
+ * repeated once per symbol. One transaction for the whole file instead of one
6930
+ * per symbol turns an O(symbols) transaction count into O(1).
6931
+ *
6932
+ * Each ref's own {@link Ref.fromId} is used; pass an empty array to no-op.
6933
+ */
6934
+ insertRefsBatch(refs) {
6935
+ if (refs.length === 0) return;
6936
+ this.runWithRetry(() => {
6937
+ const stmt = this.db.prepare(
6938
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
6939
+ VALUES (?, ?, ?, ?, ?)`
6940
+ );
6941
+ for (const ref of refs) {
6942
+ stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
6943
+ }
6944
+ });
6945
+ }
6591
6946
  /**
6592
6947
  * Delete all refs whose source symbols are in a given file.
6593
6948
  * Used when re-indexing a file to clear stale refs.
6594
6949
  */
6595
6950
  deleteRefsForFile(file) {
6596
6951
  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));
6952
+ this.db.prepare(
6953
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
6954
+ ).run(file);
6603
6955
  });
6604
6956
  }
6605
6957
  /**
6606
6958
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
6607
6959
  * Call this after all symbols have been inserted to fill in cross-references.
6960
+ *
6961
+ * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
6962
+ * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
6963
+ * that found a target — matching the previous per-row loop's return value.
6608
6964
  */
6609
6965
  resolveRefs() {
6610
6966
  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;
6967
+ const result = this.db.prepare(
6968
+ `UPDATE refs SET to_id = (
6969
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
6970
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
6971
+ AND to_name IN (SELECT name FROM symbols)`
6972
+ ).run();
6973
+ return result.changes ?? 0;
6624
6974
  });
6625
6975
  }
6626
6976
  /**
@@ -8095,13 +8445,26 @@ async function runIndexerWithStore(store, opts) {
8095
8445
  symbolsIndexed += count;
8096
8446
  langStats[lang] = (langStats[lang] ?? 0) + count;
8097
8447
  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);
8448
+ const refsByLine = /* @__PURE__ */ new Map();
8449
+ for (const r of parsed.refs) {
8450
+ let arr = refsByLine.get(r.line);
8451
+ if (!arr) {
8452
+ arr = [];
8453
+ refsByLine.set(r.line, arr);
8104
8454
  }
8455
+ arr.push(r);
8456
+ }
8457
+ const batch = [];
8458
+ for (const sym of symbolsWithIds) {
8459
+ const symRefs = refsByLine.get(sym.line);
8460
+ if (symRefs) {
8461
+ for (const r of symRefs) {
8462
+ batch.push({ ...r, fromId: sym.id });
8463
+ }
8464
+ }
8465
+ }
8466
+ if (batch.length > 0) {
8467
+ store.insertRefsBatch(batch);
8105
8468
  }
8106
8469
  }
8107
8470
  store.upsertFile({
@@ -8494,6 +8857,7 @@ async function codebaseIndexStats(args, opts = {}) {
8494
8857
  var codebaseIndexTool = {
8495
8858
  name: "codebase-index",
8496
8859
  category: "Project",
8860
+ icon: "index",
8497
8861
  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
8862
  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
8863
  permission: "confirm",
@@ -8550,6 +8914,7 @@ var codebaseIndexTool = {
8550
8914
  var codebaseSearchTool = {
8551
8915
  name: "codebase-search",
8552
8916
  category: "Project",
8917
+ icon: "index",
8553
8918
  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
8919
  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
8920
  permission: "auto",
@@ -8638,6 +9003,7 @@ var codebaseSearchTool = {
8638
9003
  var codebaseStatsTool = {
8639
9004
  name: "codebase-stats",
8640
9005
  category: "Project",
9006
+ icon: "index",
8641
9007
  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
9008
  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
9009
  permission: "auto",
@@ -8698,6 +9064,7 @@ var setWorkingDirTool = {
8698
9064
  permission: "confirm",
8699
9065
  mutating: true,
8700
9066
  capabilities: ["fs.read"],
9067
+ icon: "settings",
8701
9068
  timeoutMs: 5e3,
8702
9069
  inputSchema: {
8703
9070
  type: "object",
@@ -8758,11 +9125,12 @@ function findTaskIndex(tasks, query2) {
8758
9125
  var taskTool = {
8759
9126
  name: "task",
8760
9127
  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',
9128
+ 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.',
9129
+ 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
9130
  permission: "confirm",
8764
9131
  mutating: true,
8765
9132
  capabilities: ["fs.write"],
9133
+ icon: "task",
8766
9134
  timeoutMs: 2e3,
8767
9135
  inputSchema: {
8768
9136
  type: "object",
@@ -8828,12 +9196,26 @@ var taskTool = {
8828
9196
  type: "array",
8829
9197
  items: { type: "string" },
8830
9198
  description: "Optional subtask titles for action=promote. Each becomes a pending todo."
9199
+ },
9200
+ scope: {
9201
+ type: "string",
9202
+ enum: ["session", "project"],
9203
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
8831
9204
  }
8832
9205
  },
8833
9206
  required: ["action"]
8834
9207
  },
8835
9208
  async execute(input, ctx) {
8836
- const taskPath = ctx.meta["task.path"];
9209
+ const sessionTaskPath = ctx.meta["task.path"];
9210
+ let taskPath;
9211
+ if (input.scope === "project") {
9212
+ if (typeof sessionTaskPath === "string") {
9213
+ const lastSep = Math.max(sessionTaskPath.lastIndexOf("/"), sessionTaskPath.lastIndexOf("\\"));
9214
+ taskPath = lastSep >= 0 ? sessionTaskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
9215
+ }
9216
+ } else {
9217
+ taskPath = sessionTaskPath;
9218
+ }
8837
9219
  if (typeof taskPath !== "string" || !taskPath) {
8838
9220
  return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
8839
9221
  }
@@ -8843,23 +9225,66 @@ var taskTool = {
8843
9225
  const planifyMeta = { title: "", details: "" };
8844
9226
  let didPlanify = false;
8845
9227
  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;
9228
+ let file;
9229
+ try {
9230
+ file = await mutateTasks(taskPath, sessionId, async (f) => {
9231
+ switch (input.action) {
9232
+ case "show":
9233
+ break;
9234
+ case "replace": {
9235
+ if (!Array.isArray(input.tasks)) {
9236
+ early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
9237
+ return f;
9238
+ }
9239
+ const newIds = new Set(input.tasks.map((t) => t.id));
9240
+ if (newIds.size !== input.tasks.length) {
9241
+ const seen = /* @__PURE__ */ new Set();
9242
+ const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => seen.has(id) ? true : (seen.add(id), false)))];
9243
+ early = {
9244
+ ok: false,
9245
+ message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
9246
+ count: 0,
9247
+ completed: 0,
9248
+ inProgress: 0
9249
+ };
9250
+ return f;
9251
+ }
9252
+ for (const t of input.tasks) {
9253
+ if (t.dependsOn && t.dependsOn.length > 0) {
9254
+ const missing = t.dependsOn.filter((d) => !newIds.has(d));
9255
+ if (missing.length > 0) {
9256
+ early = {
9257
+ ok: false,
9258
+ message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
9259
+ count: 0,
9260
+ completed: 0,
9261
+ inProgress: 0
9262
+ };
9263
+ return f;
9264
+ }
9265
+ }
9266
+ }
9267
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9268
+ f.tasks = input.tasks.map((t) => ({
9269
+ ...t,
9270
+ createdAt: t.createdAt || now,
9271
+ updatedAt: now
9272
+ }));
9273
+ break;
8854
9274
  }
8855
- const newIds = new Set(input.tasks.map((t) => t.id));
8856
- for (const t of input.tasks) {
9275
+ case "add": {
9276
+ const t = input.task;
9277
+ if (!t || !t.title) {
9278
+ early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
9279
+ return f;
9280
+ }
8857
9281
  if (t.dependsOn && t.dependsOn.length > 0) {
8858
- const missing = t.dependsOn.filter((d) => !newIds.has(d));
9282
+ const existingIds = new Set(f.tasks.map((e) => e.id));
9283
+ const missing = t.dependsOn.filter((d) => !existingIds.has(d));
8859
9284
  if (missing.length > 0) {
8860
9285
  early = {
8861
9286
  ok: false,
8862
- message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
9287
+ message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
8863
9288
  count: 0,
8864
9289
  completed: 0,
8865
9290
  inProgress: 0
@@ -8867,165 +9292,170 @@ var taskTool = {
8867
9292
  return f;
8868
9293
  }
8869
9294
  }
9295
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9296
+ const newTask = {
9297
+ id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
9298
+ title: t.title,
9299
+ description: t.description,
9300
+ type: t.type || "feature",
9301
+ priority: t.priority || "medium",
9302
+ status: t.status || "pending",
9303
+ dependsOn: t.dependsOn,
9304
+ assignee: t.assignee,
9305
+ estimateHours: t.estimateHours,
9306
+ tags: t.tags,
9307
+ createdAt: now,
9308
+ updatedAt: now
9309
+ };
9310
+ f.tasks.push(newTask);
9311
+ break;
8870
9312
  }
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
- };
9313
+ case "status": {
9314
+ if (!input.id || !input.status) {
9315
+ early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
8896
9316
  return f;
8897
9317
  }
9318
+ const task = f.tasks.find((t) => t.id === input.id);
9319
+ if (!task) {
9320
+ early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
9321
+ return f;
9322
+ }
9323
+ task.status = input.status;
9324
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9325
+ break;
8898
9326
  }
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) {
9327
+ case "promote": {
9328
+ const target = input.target?.trim();
9329
+ if (!target) {
9330
+ early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
9331
+ return f;
9332
+ }
9333
+ const idx = findTaskIndex(f.tasks, target);
9334
+ if (idx === -1) {
9335
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9336
+ return f;
9337
+ }
9338
+ const match = f.tasks[idx];
9339
+ if (!match) {
9340
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9341
+ return f;
9342
+ }
9343
+ if (match.status !== "completed" && match.status !== "failed") {
9344
+ match.status = "in_progress";
9345
+ match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9346
+ }
9347
+ const todos = [];
9348
+ const ts2 = Date.now();
8961
9349
  todos.push({
8962
- id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
8963
- content: match.description.slice(0, 200),
8964
- status: "pending",
9350
+ id: `todo_${ts2}_task`,
9351
+ content: match.title,
9352
+ status: "in_progress",
9353
+ activeForm: match.title,
8965
9354
  promotedFromTask: match.id
8966
9355
  });
8967
- }
8968
- if (input.subtasks && input.subtasks.length > 0) {
8969
- for (const st of input.subtasks) {
9356
+ if (match.description) {
8970
9357
  todos.push({
8971
9358
  id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
8972
- content: st,
9359
+ content: match.description.slice(0, 200),
8973
9360
  status: "pending",
8974
9361
  promotedFromTask: match.id
8975
9362
  });
8976
9363
  }
9364
+ if (input.subtasks && input.subtasks.length > 0) {
9365
+ for (const st of input.subtasks) {
9366
+ todos.push({
9367
+ id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
9368
+ content: st,
9369
+ status: "pending",
9370
+ promotedFromTask: match.id
9371
+ });
9372
+ }
9373
+ }
9374
+ todosToReplace = todos;
9375
+ promoteMeta.count = todos.length;
9376
+ promoteMeta.title = match.title;
9377
+ break;
8977
9378
  }
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;
9379
+ case "planify": {
9380
+ const target = input.target?.trim();
9381
+ if (!target) {
9382
+ early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
9383
+ return f;
9384
+ }
9385
+ const idx = findTaskIndex(f.tasks, target);
9386
+ if (idx === -1) {
9387
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9388
+ return f;
9389
+ }
9390
+ const match = f.tasks[idx];
9391
+ if (!match) {
9392
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9393
+ return f;
9394
+ }
9395
+ planifyMeta.title = match.title;
9396
+ planifyMeta.details = match.description ?? "";
9397
+ didPlanify = true;
9398
+ break;
8993
9399
  }
8994
- const match = f.tasks[idx];
8995
- if (!match) {
8996
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
9400
+ default:
9401
+ early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
8997
9402
  return f;
8998
- }
8999
- planifyMeta.title = match.title;
9000
- planifyMeta.details = match.description ?? "";
9001
- didPlanify = true;
9002
- break;
9003
9403
  }
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
- });
9404
+ return f;
9405
+ });
9406
+ } catch (err) {
9407
+ return {
9408
+ ok: false,
9409
+ message: `Task change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
9410
+ count: 0,
9411
+ completed: 0,
9412
+ inProgress: 0
9413
+ };
9414
+ }
9010
9415
  if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
9011
9416
  if (early) return early;
9012
9417
  if (didPlanify) {
9013
9418
  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);
9419
+ const planPathRaw = ctx.meta["plan.path"];
9420
+ const prog = computeTaskItemProgress(file.tasks);
9421
+ if (typeof planPathRaw === "string" && planPathRaw) {
9422
+ let planPath = planPathRaw;
9423
+ if (input.scope === "project") {
9424
+ const lastSep = Math.max(planPath.lastIndexOf("/"), planPath.lastIndexOf("\\"));
9425
+ planPath = lastSep >= 0 ? planPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
9426
+ }
9427
+ let formatted = "";
9428
+ try {
9429
+ await mutatePlan(planPath, sessionId, (pf) => {
9430
+ const { plan: updated } = addPlanItem(pf, title, details || void 0);
9431
+ formatted = formatPlan(updated);
9432
+ return updated;
9433
+ });
9434
+ } catch (err) {
9435
+ return {
9436
+ ok: false,
9437
+ message: `planify: plan not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
9438
+ count: file.tasks.length,
9439
+ completed: prog.completed,
9440
+ inProgress: prog.inProgress
9441
+ };
9442
+ }
9019
9443
  return {
9020
9444
  ok: true,
9021
9445
  message: `planify ok \u2014 added "${title}" to plan.
9022
- ${formatPlan(updated)}`,
9446
+ ${formatted}`,
9023
9447
  count: file.tasks.length,
9024
- completed: computeTaskItemProgress(file.tasks).completed,
9025
- inProgress: computeTaskItemProgress(file.tasks).inProgress
9448
+ completed: prog.completed,
9449
+ inProgress: prog.inProgress
9026
9450
  };
9027
9451
  }
9028
- return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
9452
+ return {
9453
+ ok: false,
9454
+ message: "Plan storage path not configured \u2014 cannot planify.",
9455
+ count: file.tasks.length,
9456
+ completed: prog.completed,
9457
+ inProgress: prog.inProgress
9458
+ };
9029
9459
  }
9030
9460
  const p = computeTaskItemProgress(file.tasks);
9031
9461
  const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
@@ -9069,6 +9499,36 @@ var TIER1_TOOLS = [
9069
9499
  jsonTool,
9070
9500
  searchTool
9071
9501
  ];
9502
+ var TIER2_TOOLS = [
9503
+ replaceTool,
9504
+ execTool,
9505
+ fetchTool,
9506
+ gitTool,
9507
+ treeTool,
9508
+ lintTool,
9509
+ formatTool,
9510
+ typecheckTool,
9511
+ testTool,
9512
+ todoTool,
9513
+ planTool,
9514
+ taskTool,
9515
+ installTool,
9516
+ auditTool
9517
+ ];
9518
+ var TIER3_TOOLS = [
9519
+ outdatedTool,
9520
+ logsTool,
9521
+ documentTool,
9522
+ scaffoldTool,
9523
+ toolSearchTool,
9524
+ toolUseTool,
9525
+ batchToolUseTool,
9526
+ toolHelpTool,
9527
+ codebaseIndexTool,
9528
+ codebaseSearchTool,
9529
+ codebaseStatsTool,
9530
+ setWorkingDirTool
9531
+ ];
9072
9532
  var builtinTools = [
9073
9533
  readTool,
9074
9534
  writeTool,
@@ -9115,6 +9575,162 @@ var builtinToolsPack = {
9115
9575
  tools: builtinTools
9116
9576
  };
9117
9577
 
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 };
9578
+ // src/tool-icon-map.ts
9579
+ var TOOL_ICON_MAP = {
9580
+ // File operations
9581
+ read: "file",
9582
+ write: "file",
9583
+ create: "file",
9584
+ // File modification
9585
+ edit: "edit",
9586
+ patch: "edit",
9587
+ replace: "edit",
9588
+ // Content search
9589
+ grep: "search",
9590
+ search: "search",
9591
+ // File discovery
9592
+ glob: "folder",
9593
+ // Shell/command execution
9594
+ bash: "terminal",
9595
+ exec: "terminal",
9596
+ run: "terminal",
9597
+ command: "terminal",
9598
+ shell: "terminal",
9599
+ // Network
9600
+ fetch: "web",
9601
+ curl: "web",
9602
+ http: "web",
9603
+ request: "web",
9604
+ // Version control
9605
+ git: "git",
9606
+ // Directory structure
9607
+ tree: "tree",
9608
+ ls: "tree",
9609
+ list: "tree",
9610
+ // Code quality
9611
+ lint: "code",
9612
+ format: "code",
9613
+ typecheck: "code",
9614
+ // Testing
9615
+ test: "test",
9616
+ tests: "test",
9617
+ // Package management
9618
+ install: "package",
9619
+ uninstall: "package",
9620
+ audit: "package",
9621
+ outdated: "package",
9622
+ npm: "package",
9623
+ pnpm: "package",
9624
+ yarn: "package",
9625
+ // Documentation
9626
+ document: "document",
9627
+ doc: "document",
9628
+ jsdoc: "document",
9629
+ // Project scaffolding
9630
+ scaffold: "scaffold",
9631
+ generate: "scaffold",
9632
+ template: "scaffold",
9633
+ // Task management
9634
+ todo: "todo",
9635
+ todos: "todo",
9636
+ // Planning
9637
+ plan: "plan",
9638
+ planning: "plan",
9639
+ // Structured tasks
9640
+ task: "task",
9641
+ tasks: "task",
9642
+ // Meta/tools
9643
+ "tool-use": "meta",
9644
+ "batch-tool-use": "meta",
9645
+ "tool-search": "meta",
9646
+ "tool-help": "meta",
9647
+ tool_use: "meta",
9648
+ batch_tool_use: "meta",
9649
+ tool_search: "meta",
9650
+ tool_help: "meta",
9651
+ // Code indexing
9652
+ "codebase-index": "index",
9653
+ "codebase-search": "index",
9654
+ "codebase-stats": "index",
9655
+ "codebase_index": "index",
9656
+ "codebase_search": "index",
9657
+ "codebase_stats": "index",
9658
+ // Data
9659
+ json: "json",
9660
+ parse: "json",
9661
+ query: "json",
9662
+ // Comparison
9663
+ diff: "diff",
9664
+ compare: "diff",
9665
+ // Logs
9666
+ logs: "logs",
9667
+ log: "logs",
9668
+ // Configuration
9669
+ "set-working-dir": "settings",
9670
+ set_working_dir: "settings",
9671
+ cwd: "settings",
9672
+ cd: "settings",
9673
+ // AI/Agent
9674
+ think: "brain",
9675
+ reason: "brain",
9676
+ analyze: "brain",
9677
+ reasoning: "brain"
9678
+ };
9679
+ function getToolIcon(toolName) {
9680
+ return TOOL_ICON_MAP[toolName.toLowerCase()] ?? "fallback";
9681
+ }
9682
+ var TOOL_ICON_CONFIG = {
9683
+ file: { icon: "file", color: "#6366f1" },
9684
+ // indigo
9685
+ edit: { icon: "edit", color: "#f59e0b" },
9686
+ // amber
9687
+ search: { icon: "search", color: "#10b981" },
9688
+ // emerald
9689
+ folder: { icon: "folder", color: "#8b5cf6" },
9690
+ // violet
9691
+ terminal: { icon: "terminal", color: "#fb923c" },
9692
+ // orange
9693
+ web: { icon: "web", color: "#06b6d4" },
9694
+ // cyan
9695
+ git: { icon: "git", color: "#f97316" },
9696
+ // orange
9697
+ tree: { icon: "tree", color: "#22c55e" },
9698
+ // green
9699
+ code: { icon: "code", color: "#3b82f6" },
9700
+ // blue
9701
+ test: { icon: "test", color: "#84cc16" },
9702
+ // lime
9703
+ package: { icon: "package", color: "#ec4899" },
9704
+ // pink
9705
+ document: { icon: "document", color: "#14b8a6" },
9706
+ // teal
9707
+ scaffold: { icon: "scaffold", color: "#f43f5e" },
9708
+ // rose
9709
+ todo: { icon: "todo", color: "#a855f7" },
9710
+ // purple
9711
+ plan: { icon: "plan", color: "#7c3aed" },
9712
+ // violet-dark
9713
+ task: { icon: "task", color: "#db2777" },
9714
+ // pink-dark
9715
+ meta: { icon: "meta", color: "#6b7280" },
9716
+ // gray
9717
+ index: { icon: "index", color: "#0ea5e9" },
9718
+ // sky
9719
+ json: { icon: "json", color: "#fbbf24" },
9720
+ // yellow
9721
+ diff: { icon: "diff", color: "#a3e635" },
9722
+ // lime-light
9723
+ logs: { icon: "logs", color: "#78716c" },
9724
+ // stone
9725
+ settings: { icon: "settings", color: "#64748b" },
9726
+ // slate
9727
+ brain: { icon: "brain", color: "#d946ef" },
9728
+ // fuchsia
9729
+ fallback: { icon: "fallback", color: "#9ca3af" }
9730
+ // gray-light
9731
+ };
9732
+ var FALLBACK_ICON = "fallback";
9733
+
9734
+ 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
9735
  //# sourceMappingURL=index.js.map
9120
9736
  //# sourceMappingURL=index.js.map