@wrongstack/tools 0.260.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 (88) hide show
  1. package/dist/audit.js +154 -10
  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 +674 -333
  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 +62 -27
  15. package/dist/codebase-index/index.js.map +1 -1
  16. package/dist/codebase-index/worker.js +59 -27
  17. package/dist/codebase-index/worker.js.map +1 -1
  18. package/dist/diff.js +14 -6
  19. package/dist/diff.js.map +1 -1
  20. package/dist/document.js +18 -11
  21. package/dist/document.js.map +1 -1
  22. package/dist/edit.js +22 -14
  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.d.ts +19 -1
  27. package/dist/fetch.js +2 -1
  28. package/dist/fetch.js.map +1 -1
  29. package/dist/format.js +153 -10
  30. package/dist/format.js.map +1 -1
  31. package/dist/git.js +1 -0
  32. package/dist/git.js.map +1 -1
  33. package/dist/glob.js +14 -6
  34. package/dist/glob.js.map +1 -1
  35. package/dist/grep.js +14 -6
  36. package/dist/grep.js.map +1 -1
  37. package/dist/index.d.ts +55 -3
  38. package/dist/index.js +833 -336
  39. package/dist/index.js.map +1 -1
  40. package/dist/install.js +154 -10
  41. package/dist/install.js.map +1 -1
  42. package/dist/json.js +2 -0
  43. package/dist/json.js.map +1 -1
  44. package/dist/lint.js +153 -10
  45. package/dist/lint.js.map +1 -1
  46. package/dist/logs.js +14 -6
  47. package/dist/logs.js.map +1 -1
  48. package/dist/memory.js +1 -0
  49. package/dist/memory.js.map +1 -1
  50. package/dist/mode.js +1 -0
  51. package/dist/mode.js.map +1 -1
  52. package/dist/outdated.js +16 -7
  53. package/dist/outdated.js.map +1 -1
  54. package/dist/pack.js +643 -332
  55. package/dist/pack.js.map +1 -1
  56. package/dist/patch.js +14 -6
  57. package/dist/patch.js.map +1 -1
  58. package/dist/process-registry.d.ts +56 -2
  59. package/dist/process-registry.js +138 -3
  60. package/dist/process-registry.js.map +1 -1
  61. package/dist/read.js +24 -18
  62. package/dist/read.js.map +1 -1
  63. package/dist/replace.js +14 -6
  64. package/dist/replace.js.map +1 -1
  65. package/dist/scaffold.js +14 -6
  66. package/dist/scaffold.js.map +1 -1
  67. package/dist/search.js +3 -3
  68. package/dist/search.js.map +1 -1
  69. package/dist/test.js +153 -10
  70. package/dist/test.js.map +1 -1
  71. package/dist/todo.js +1 -0
  72. package/dist/todo.js.map +1 -1
  73. package/dist/tool-help.js +1 -0
  74. package/dist/tool-help.js.map +1 -1
  75. package/dist/tool-icons.d.ts +20 -0
  76. package/dist/tool-icons.js +130 -0
  77. package/dist/tool-icons.js.map +1 -0
  78. package/dist/tool-search.js +1 -0
  79. package/dist/tool-search.js.map +1 -1
  80. package/dist/tool-use.js +1 -0
  81. package/dist/tool-use.js.map +1 -1
  82. package/dist/tree.js +14 -6
  83. package/dist/tree.js.map +1 -1
  84. package/dist/typecheck.js +153 -10
  85. package/dist/typecheck.js.map +1 -1
  86. package/dist/write.js +22 -14
  87. package/dist/write.js.map +1 -1
  88. package/package.json +6 -2
package/dist/builtin.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { spawn, execFileSync, spawnSync } from 'node:child_process';
2
2
  import * as Core from '@wrongstack/core';
3
- import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, loadTasks, emptyTaskFile, saveTasks, formatTaskList, formatPlan, mutateTasks, loadPlan, emptyPlan, savePlan, computeTaskItemProgress, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
3
+ import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
4
4
  import * as fs from 'node:fs';
5
5
  import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
6
6
  import * as fs14 from 'node:fs/promises';
7
7
  import * as path3 from 'node:path';
8
8
  import { resolve, sep, dirname, join } from 'node:path';
9
9
  import * as os from 'node:os';
10
+ import { toErrorMessage } from '@wrongstack/core/utils';
10
11
  import { createRequire } from 'node:module';
11
12
  import { fileURLToPath } from 'node:url';
12
13
  import { Worker } from 'node:worker_threads';
@@ -141,6 +142,23 @@ var CircuitBreaker = class {
141
142
  lastSlowAt = null;
142
143
  /** Timestamp when the breaker was opened (for cooldown calculation). */
143
144
  openedAt = null;
145
+ /**
146
+ * Master enable flag. When false the breaker is bypassed: `beforeCall`
147
+ * always returns true and `afterCall` records nothing. The class itself
148
+ * defaults to enabled (so the standalone unit tests exercise tripping); the
149
+ * ProcessRegistry flips this off until the user opts in via `/settings`.
150
+ */
151
+ enabled = true;
152
+ /**
153
+ * Fired (best-effort) when the breaker transitions into the `open` state.
154
+ * The registry uses this to arm its auto kill/reset countdown.
155
+ */
156
+ onTrip;
157
+ /**
158
+ * Fired (best-effort) when the breaker returns to `closed` after having been
159
+ * open/half-open. The registry uses this to cancel a pending kill/reset.
160
+ */
161
+ onReset;
144
162
  constructor(config = {}) {
145
163
  this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
146
164
  this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
@@ -149,12 +167,22 @@ var CircuitBreaker = class {
149
167
  this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
150
168
  this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
151
169
  }
170
+ /** Toggle the master enable. Disabling resets to a clean `closed` state. */
171
+ setEnabled(enabled) {
172
+ if (this.enabled === enabled) return;
173
+ this.enabled = enabled;
174
+ if (!enabled) this._reset();
175
+ }
176
+ get isEnabled() {
177
+ return this.enabled;
178
+ }
152
179
  /**
153
180
  * Returns true if the circuit allows a new call to proceed.
154
181
  * When false, callers should abort the tool call and return a
155
182
  * circuit-breaker error instead of spawning a process.
156
183
  */
157
184
  get canProceed() {
185
+ if (!this.enabled) return true;
158
186
  this._checkStateTransition();
159
187
  return this.state !== "open";
160
188
  }
@@ -190,7 +218,7 @@ var CircuitBreaker = class {
190
218
  * not affect breaker state.
191
219
  */
192
220
  beforeCall(bypass = false) {
193
- if (bypass) return true;
221
+ if (bypass || !this.enabled) return true;
194
222
  this._checkStateTransition();
195
223
  if (this.state === "open") return false;
196
224
  return true;
@@ -205,7 +233,7 @@ var CircuitBreaker = class {
205
233
  * Use for background/fire-and-forget processes.
206
234
  */
207
235
  afterCall(durationMs, failed, bypass = false) {
208
- if (bypass) return;
236
+ if (bypass || !this.enabled) return;
209
237
  const now = Date.now();
210
238
  if (this.state === "half-open") {
211
239
  if (failed) {
@@ -251,12 +279,23 @@ var CircuitBreaker = class {
251
279
  if (this.state === "open") return;
252
280
  this.state = "open";
253
281
  this.openedAt = Date.now();
282
+ try {
283
+ this.onTrip?.();
284
+ } catch {
285
+ }
254
286
  }
255
287
  _reset() {
288
+ const wasRecovering = this.state !== "closed";
256
289
  this.state = "closed";
257
290
  this.consecutiveFailures = 0;
258
291
  this.window = [];
259
292
  this.openedAt = null;
293
+ if (wasRecovering) {
294
+ try {
295
+ this.onReset?.();
296
+ } catch {
297
+ }
298
+ }
260
299
  }
261
300
  /** Transition from open → half-open when cooldown elapses. */
262
301
  _checkStateTransition() {
@@ -318,8 +357,21 @@ function killWin32Tree(pid) {
318
357
  var ProcessRegistryImpl = class {
319
358
  processes = /* @__PURE__ */ new Map();
320
359
  breaker;
360
+ /**
361
+ * Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,
362
+ * a countdown is armed; on expiry all tracked processes are killed and the
363
+ * breaker is reset to closed (forced recovery). Zero means manual recovery
364
+ * only (`/kill reset`).
365
+ */
366
+ autoKillResetMs = 0;
367
+ autoKillTimer = null;
368
+ autoKillArmedAt = null;
369
+ breakerCountdownListeners = [];
321
370
  constructor(breakerConfig) {
322
371
  this.breaker = new CircuitBreaker(breakerConfig);
372
+ this.breaker.onTrip = () => this._armAutoKillReset();
373
+ this.breaker.onReset = () => this._cancelAutoKillReset();
374
+ this.breaker.setEnabled(false);
323
375
  }
324
376
  register(info) {
325
377
  this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
@@ -395,6 +447,90 @@ var ProcessRegistryImpl = class {
395
447
  forceBreakerReset() {
396
448
  this.breaker.forceReset();
397
449
  }
450
+ /**
451
+ * Configure circuit-breaker protection at runtime. Called from `/settings`
452
+ * (instant, all modes) and on TUI mount (applies persisted config).
453
+ *
454
+ * - `enabled` toggles whether the breaker gates `bash`/`exec`.
455
+ * - `autoKillResetMs` arms the auto kill/reset countdown when the breaker
456
+ * trips (0 = manual recovery only).
457
+ *
458
+ * Re-applies cleanly on every call: cancels a pending countdown when the
459
+ * timeout is cleared or protection disabled, and re-arms if the breaker is
460
+ * currently open under the new settings.
461
+ */
462
+ setBreakerConfig(cfg) {
463
+ if (cfg.enabled !== void 0) this.breaker.setEnabled(cfg.enabled);
464
+ if (cfg.autoKillResetMs !== void 0) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);
465
+ if (this.autoKillResetMs <= 0) {
466
+ this._cancelAutoKillReset();
467
+ return;
468
+ }
469
+ if (this.breaker.isEnabled && this.breaker.snapshot().state === "open") {
470
+ this._armAutoKillReset();
471
+ }
472
+ }
473
+ /**
474
+ * Live countdown to the next auto kill/reset, or null when nothing is armed.
475
+ * The TUI polls this on a 1s tick while armed so the statusline decrements.
476
+ */
477
+ getBreakerCountdown() {
478
+ if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;
479
+ const elapsed = Date.now() - this.autoKillArmedAt;
480
+ return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };
481
+ }
482
+ /**
483
+ * Subscribe to countdown arm/cancel events. Returns an unsubscribe function.
484
+ * Use {@link getBreakerCountdown} for the live ticking value between events.
485
+ */
486
+ onBreakerCountdownChange(listener) {
487
+ this.breakerCountdownListeners.push(listener);
488
+ return () => {
489
+ this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);
490
+ };
491
+ }
492
+ _emitBreakerCountdown() {
493
+ const snap = this.getBreakerCountdown();
494
+ for (const l of this.breakerCountdownListeners) {
495
+ try {
496
+ l(snap);
497
+ } catch {
498
+ }
499
+ }
500
+ }
501
+ /**
502
+ * Arm the auto kill/reset countdown. Idempotent: re-arming resets the window
503
+ * (a fresh trip after a failed half-open probe restarts the clock). No-op
504
+ * when protection is off or no timeout is configured.
505
+ */
506
+ _armAutoKillReset() {
507
+ if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;
508
+ this._clearAutoKillTimer();
509
+ this.autoKillArmedAt = Date.now();
510
+ this.autoKillTimer = setTimeout(() => {
511
+ this.autoKillTimer = null;
512
+ this.autoKillArmedAt = null;
513
+ this.killAll({ force: false });
514
+ this.breaker.forceReset();
515
+ this._emitBreakerCountdown();
516
+ }, this.autoKillResetMs);
517
+ this.autoKillTimer.unref?.();
518
+ this._emitBreakerCountdown();
519
+ }
520
+ _cancelAutoKillReset() {
521
+ const wasArmed = this.autoKillArmedAt !== null;
522
+ this._clearAutoKillTimer();
523
+ if (wasArmed) {
524
+ this.autoKillArmedAt = null;
525
+ this._emitBreakerCountdown();
526
+ }
527
+ }
528
+ _clearAutoKillTimer() {
529
+ if (this.autoKillTimer !== null) {
530
+ clearTimeout(this.autoKillTimer);
531
+ this.autoKillTimer = null;
532
+ }
533
+ }
398
534
  /** Kill a single process by PID.
399
535
  *
400
536
  * On POSIX: sends SIGTERM to the *process group* (-pid) so that
@@ -533,8 +669,9 @@ async function* spawnStream(opts) {
533
669
  let pending2 = "";
534
670
  let error;
535
671
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
536
- const cmd = resolveWin32Command(opts.cmd);
537
- const needsShell = isWin && (cmd.endsWith(".cmd") || cmd.endsWith(".bat"));
672
+ const resolved = resolveWin32Command(opts.cmd);
673
+ const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
674
+ const cmd = needsShell ? opts.cmd : resolved;
538
675
  const child = spawn(cmd, opts.args, {
539
676
  cwd: opts.cwd,
540
677
  env: buildChildEnv(),
@@ -698,20 +835,29 @@ async function detectPackageManager(cwd) {
698
835
  function resolvePath(input, ctx) {
699
836
  return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
700
837
  }
838
+ function allowedRoots(ctx) {
839
+ return [path3.resolve(ctx.projectRoot), path3.resolve(Core.wstackGlobalRoot())];
840
+ }
841
+ function isInsideAny(target, roots) {
842
+ return roots.some((root) => {
843
+ const rel = path3.relative(root, target);
844
+ return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
845
+ });
846
+ }
701
847
  function ensureInsideRoot(absPath, ctx) {
702
- const root = path3.resolve(ctx.projectRoot);
703
848
  const target = path3.resolve(absPath);
704
- const rel = path3.relative(root, target);
705
- if (rel.startsWith("..") || path3.isAbsolute(rel)) {
706
- throw new Error(`Path "${absPath}" is outside project root "${root}"`);
707
- }
708
- return target;
849
+ if (ctx.allowOutsideProjectRoot) return target;
850
+ if (isInsideAny(target, allowedRoots(ctx))) return target;
851
+ throw new Error(`Path "${absPath}" is outside project root "${path3.resolve(ctx.projectRoot)}"`);
709
852
  }
710
853
  function safeResolve(input, ctx) {
711
854
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
712
855
  }
713
856
  async function assertRealInsideRoot(absPath, ctx) {
714
- const realRoot = await fs14.realpath(ctx.projectRoot).catch(() => path3.resolve(ctx.projectRoot));
857
+ if (ctx.allowOutsideProjectRoot) return;
858
+ const realRoots = await Promise.all(
859
+ allowedRoots(ctx).map((r) => fs14.realpath(r).catch(() => path3.resolve(r)))
860
+ );
715
861
  let probe = absPath;
716
862
  for (; ; ) {
717
863
  let real;
@@ -726,13 +872,10 @@ async function assertRealInsideRoot(absPath, ctx) {
726
872
  }
727
873
  throw err;
728
874
  }
729
- const rel = path3.relative(realRoot, real);
730
- if (rel.startsWith("..") || path3.isAbsolute(rel)) {
731
- throw new Error(
732
- `Path "${absPath}" resolves through a symlink outside project root "${realRoot}"`
733
- );
734
- }
735
- return;
875
+ if (isInsideAny(real, realRoots)) return;
876
+ throw new Error(
877
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
878
+ );
736
879
  }
737
880
  }
738
881
  async function safeResolveReal(input, ctx) {
@@ -834,6 +977,7 @@ var auditTool = {
834
977
  permission: "confirm",
835
978
  mutating: false,
836
979
  capabilities: ["shell.restricted"],
980
+ icon: "package",
837
981
  timeoutMs: 6e4,
838
982
  inputSchema: {
839
983
  type: "object",
@@ -938,6 +1082,7 @@ var bashTool = {
938
1082
  permission: "confirm",
939
1083
  mutating: true,
940
1084
  riskTier: "destructive",
1085
+ icon: "terminal",
941
1086
  // Trust rules match on the literal `command` string. Without subjectKey
942
1087
  // the policy heuristic would have done the same here, but declaring it
943
1088
  // explicitly removes the implicit cross-tool aliasing.
@@ -1278,6 +1423,7 @@ var batchToolUseTool = {
1278
1423
  mutating: true,
1279
1424
  timeoutMs: 12e4,
1280
1425
  capabilities: ["tool.mutate.any"],
1426
+ icon: "meta",
1281
1427
  inputSchema: {
1282
1428
  type: "object",
1283
1429
  properties: {
@@ -1607,7 +1753,7 @@ function loadDatabaseSync() {
1607
1753
  DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
1608
1754
  } catch (err) {
1609
1755
  throw new Error(
1610
- `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${err instanceof Error ? err.message : String(err)}`
1756
+ `The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
1611
1757
  );
1612
1758
  }
1613
1759
  return DatabaseSyncCtor;
@@ -2053,39 +2199,57 @@ var IndexStore = class {
2053
2199
  }
2054
2200
  });
2055
2201
  }
2202
+ /**
2203
+ * Bulk-insert refs for many source symbols in a single transaction.
2204
+ *
2205
+ * Unlike {@link insertRefs} this does NOT delete per source id — the caller
2206
+ * (the indexer) has already cleared stale refs for the file via
2207
+ * {@link deleteRefsForFile}, so the per-source DELETE would be redundant work
2208
+ * repeated once per symbol. One transaction for the whole file instead of one
2209
+ * per symbol turns an O(symbols) transaction count into O(1).
2210
+ *
2211
+ * Each ref's own {@link Ref.fromId} is used; pass an empty array to no-op.
2212
+ */
2213
+ insertRefsBatch(refs) {
2214
+ if (refs.length === 0) return;
2215
+ this.runWithRetry(() => {
2216
+ const stmt = this.db.prepare(
2217
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
2218
+ VALUES (?, ?, ?, ?, ?)`
2219
+ );
2220
+ for (const ref of refs) {
2221
+ stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
2222
+ }
2223
+ });
2224
+ }
2056
2225
  /**
2057
2226
  * Delete all refs whose source symbols are in a given file.
2058
2227
  * Used when re-indexing a file to clear stale refs.
2059
2228
  */
2060
2229
  deleteRefsForFile(file) {
2061
2230
  this.runWithRetry(() => {
2062
- const ids = this.db.prepare(
2063
- "SELECT id FROM symbols WHERE file = ?"
2064
- ).all(file);
2065
- if (!ids.length) return;
2066
- const placeholders = ids.map(() => "?").join(",");
2067
- this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
2231
+ this.db.prepare(
2232
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
2233
+ ).run(file);
2068
2234
  });
2069
2235
  }
2070
2236
  /**
2071
2237
  * Resolve `to_name` → `to_id` for all refs that have a name but no id.
2072
2238
  * Call this after all symbols have been inserted to fill in cross-references.
2239
+ *
2240
+ * Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
2241
+ * the UPDATE to refs that will actually resolve, so `.changes` counts only refs
2242
+ * that found a target — matching the previous per-row loop's return value.
2073
2243
  */
2074
2244
  resolveRefs() {
2075
2245
  return this.runWithRetry(() => {
2076
- const unresolved = this.db.prepare(
2077
- "SELECT id, to_name FROM refs WHERE to_id IS NULL AND to_name IS NOT NULL"
2078
- ).all();
2079
- let resolved = 0;
2080
- for (const row of unresolved) {
2081
- const target = this.db.prepare("SELECT id FROM symbols WHERE name = ? LIMIT 1").all(row.to_name);
2082
- const first = target[0];
2083
- if (first) {
2084
- this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
2085
- resolved++;
2086
- }
2087
- }
2088
- return resolved;
2246
+ const result = this.db.prepare(
2247
+ `UPDATE refs SET to_id = (
2248
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
2249
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
2250
+ AND to_name IN (SELECT name FROM symbols)`
2251
+ ).run();
2252
+ return result.changes ?? 0;
2089
2253
  });
2090
2254
  }
2091
2255
  /**
@@ -2192,7 +2356,7 @@ function getSignature(node, sourceFile) {
2192
2356
  }
2193
2357
  function getJsDoc(node, sourceFile) {
2194
2358
  const fullText = sourceFile.getFullText();
2195
- const nodePos = node.getFullWidth();
2359
+ const nodePos = node.getFullStart();
2196
2360
  const comments = ts.getLeadingCommentRanges(fullText, nodePos);
2197
2361
  if (!comments) return "";
2198
2362
  for (const range of comments) {
@@ -3560,14 +3724,27 @@ async function runIndexerWithStore(store, opts) {
3560
3724
  symbolsIndexed += count;
3561
3725
  langStats[lang] = (langStats[lang] ?? 0) + count;
3562
3726
  if (parsed.refs && parsed.refs.length > 0) {
3563
- for (let i = 0; i < symbolsWithIds.length; i++) {
3564
- const sym = expectDefined(symbolsWithIds[i]);
3565
- const symRefs = parsed.refs.filter((r) => r.line === sym.line);
3566
- if (symRefs.length > 0) {
3567
- const refsWithFromId = symRefs.map((r) => ({ ...r, fromId: sym.id }));
3568
- store.insertRefs(sym.id, refsWithFromId);
3727
+ const refsByLine = /* @__PURE__ */ new Map();
3728
+ for (const r of parsed.refs) {
3729
+ let arr = refsByLine.get(r.line);
3730
+ if (!arr) {
3731
+ arr = [];
3732
+ refsByLine.set(r.line, arr);
3733
+ }
3734
+ arr.push(r);
3735
+ }
3736
+ const batch = [];
3737
+ for (const sym of symbolsWithIds) {
3738
+ const symRefs = refsByLine.get(sym.line);
3739
+ if (symRefs) {
3740
+ for (const r of symRefs) {
3741
+ batch.push({ ...r, fromId: sym.id });
3742
+ }
3569
3743
  }
3570
3744
  }
3745
+ if (batch.length > 0) {
3746
+ store.insertRefsBatch(batch);
3747
+ }
3571
3748
  }
3572
3749
  store.upsertFile({
3573
3750
  file,
@@ -3900,6 +4077,7 @@ async function codebaseIndexStats(args, opts = {}) {
3900
4077
  var codebaseIndexTool = {
3901
4078
  name: "codebase-index",
3902
4079
  category: "Project",
4080
+ icon: "index",
3903
4081
  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.",
3904
4082
  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.",
3905
4083
  permission: "confirm",
@@ -3956,6 +4134,7 @@ var codebaseIndexTool = {
3956
4134
  var codebaseSearchTool = {
3957
4135
  name: "codebase-search",
3958
4136
  category: "Project",
4137
+ icon: "index",
3959
4138
  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.",
3960
4139
  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.",
3961
4140
  permission: "auto",
@@ -4044,6 +4223,7 @@ var codebaseSearchTool = {
4044
4223
  var codebaseStatsTool = {
4045
4224
  name: "codebase-stats",
4046
4225
  category: "Project",
4226
+ icon: "index",
4047
4227
  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.",
4048
4228
  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.",
4049
4229
  permission: "auto",
@@ -4104,6 +4284,7 @@ var diffTool = {
4104
4284
  permission: "auto",
4105
4285
  mutating: false,
4106
4286
  capabilities: ["fs.read"],
4287
+ icon: "diff",
4107
4288
  timeoutMs: 1e4,
4108
4289
  inputSchema: {
4109
4290
  type: "object",
@@ -4250,6 +4431,7 @@ var documentTool = {
4250
4431
  mutating: false,
4251
4432
  timeoutMs: 3e4,
4252
4433
  capabilities: ["fs.read"],
4434
+ icon: "document",
4253
4435
  inputSchema: {
4254
4436
  type: "object",
4255
4437
  properties: {
@@ -4328,10 +4510,10 @@ async function resolveFiles(filesInput, cwd) {
4328
4510
  }
4329
4511
  function processFile(content, absPath, _style, _overwrite, target) {
4330
4512
  const results = [];
4331
- const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/;
4332
- const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/;
4333
- const classRegex = /class\s+(\w+)/;
4334
- const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/;
4513
+ const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
4514
+ const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
4515
+ const classRegex = /class\s+(\w+)/g;
4516
+ const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/g;
4335
4517
  const allMatches = [];
4336
4518
  if (target === "all" || target === "function") {
4337
4519
  for (const m of content.matchAll(functionRegex)) {
@@ -4394,6 +4576,7 @@ var editTool = {
4394
4576
  permission: "confirm",
4395
4577
  mutating: true,
4396
4578
  capabilities: ["fs.write"],
4579
+ icon: "edit",
4397
4580
  timeoutMs: 5e3,
4398
4581
  inputSchema: {
4399
4582
  type: "object",
@@ -4616,6 +4799,7 @@ var execTool = {
4616
4799
  riskTier: "standard",
4617
4800
  timeoutMs: DEFAULT_TIMEOUT_MS2,
4618
4801
  capabilities: ["shell.restricted"],
4802
+ icon: "terminal",
4619
4803
  inputSchema: {
4620
4804
  type: "object",
4621
4805
  properties: {
@@ -4715,7 +4899,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
4715
4899
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
4716
4900
  const resolved = resolveWin32Command(cmd);
4717
4901
  const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
4718
- const child = spawn(resolved, args, {
4902
+ const spawnCmd = needsShell ? cmd : resolved;
4903
+ const child = spawn(spawnCmd, args, {
4719
4904
  cwd,
4720
4905
  env: buildChildEnv(sessionId),
4721
4906
  stdio: ["ignore", "pipe", "pipe"],
@@ -4902,6 +5087,7 @@ var fetchTool = {
4902
5087
  permission: "confirm",
4903
5088
  mutating: false,
4904
5089
  capabilities: ["net.outbound"],
5090
+ icon: "web",
4905
5091
  // Trust rules for fetch match on the literal URL — declare it explicitly
4906
5092
  // so a user can trust `https://api.example.com/*` without accidentally
4907
5093
  // matching that pattern on any other tool that happens to have a `url`
@@ -5051,6 +5237,7 @@ var formatTool = {
5051
5237
  permission: "confirm",
5052
5238
  mutating: true,
5053
5239
  capabilities: ["fs.write", "shell.exec"],
5240
+ icon: "code",
5054
5241
  timeoutMs: 6e4,
5055
5242
  inputSchema: {
5056
5243
  type: "object",
@@ -5151,6 +5338,7 @@ var gitTool = {
5151
5338
  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.",
5152
5339
  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.",
5153
5340
  permission: "confirm",
5341
+ icon: "git",
5154
5342
  // Conservative: any of these may mutate. The non-mutating commands
5155
5343
  // (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
5156
5344
  // and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
@@ -5410,6 +5598,7 @@ var globTool = {
5410
5598
  permission: "auto",
5411
5599
  mutating: false,
5412
5600
  capabilities: ["fs.read"],
5601
+ icon: "folder",
5413
5602
  maxOutputBytes: 65536,
5414
5603
  timeoutMs: 5e3,
5415
5604
  inputSchema: {
@@ -5542,6 +5731,7 @@ var grepTool = {
5542
5731
  permission: "auto",
5543
5732
  mutating: false,
5544
5733
  capabilities: ["fs.read"],
5734
+ icon: "search",
5545
5735
  maxOutputBytes: 131072,
5546
5736
  timeoutMs: 1e4,
5547
5737
  inputSchema: {
@@ -5828,6 +6018,7 @@ var installTool = {
5828
6018
  permission: "confirm",
5829
6019
  mutating: true,
5830
6020
  riskTier: "standard",
6021
+ icon: "package",
5831
6022
  timeoutMs: 12e4,
5832
6023
  capabilities: ["package.install", "shell.restricted"],
5833
6024
  inputSchema: {
@@ -5952,6 +6143,7 @@ function resolveManifestPath(cwd, pkgManager) {
5952
6143
  case "yarn":
5953
6144
  case "npm":
5954
6145
  return join(cwd, "package.json");
6146
+ /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
5955
6147
  default:
5956
6148
  return join(cwd, "package.json");
5957
6149
  }
@@ -5965,6 +6157,7 @@ var jsonTool = {
5965
6157
  mutating: false,
5966
6158
  timeoutMs: 5e3,
5967
6159
  capabilities: ["fs.read"],
6160
+ icon: "json",
5968
6161
  inputSchema: {
5969
6162
  type: "object",
5970
6163
  properties: {
@@ -6007,6 +6200,7 @@ var jsonTool = {
6007
6200
  data: null,
6008
6201
  formatted: "",
6009
6202
  type: "unknown",
6203
+ /* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
6010
6204
  error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
6011
6205
  };
6012
6206
  }
@@ -6089,6 +6283,7 @@ var lintTool = {
6089
6283
  mutating: false,
6090
6284
  timeoutMs: 6e4,
6091
6285
  capabilities: ["shell.restricted"],
6286
+ icon: "code",
6092
6287
  inputSchema: {
6093
6288
  type: "object",
6094
6289
  properties: {
@@ -6182,6 +6377,7 @@ var logsTool = {
6182
6377
  mutating: false,
6183
6378
  timeoutMs: 3e4,
6184
6379
  capabilities: ["shell.restricted"],
6380
+ icon: "logs",
6185
6381
  inputSchema: {
6186
6382
  type: "object",
6187
6383
  properties: {
@@ -6383,6 +6579,7 @@ var outdatedTool = {
6383
6579
  description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
6384
6580
  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.",
6385
6581
  permission: "confirm",
6582
+ icon: "package",
6386
6583
  // Network side-effecting (registry HTTP). Pairs with `mutating: true`
6387
6584
  // so the H7 invariant test (`no auto-permission tool declares
6388
6585
  // mutating: true`) passes — a tool claiming `'auto'` must be purely
@@ -6434,7 +6631,8 @@ function runOutdated(manager, args, cwd, signal) {
6434
6631
  const MAX = 1e5;
6435
6632
  const resolved = resolveWin32Command(manager);
6436
6633
  const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
6437
- const child = spawn(resolved, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
6634
+ const spawnCmd = needsShell ? manager : resolved;
6635
+ const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
6438
6636
  child.stdout?.on("data", (c) => {
6439
6637
  if (stdout.length < MAX) stdout += c.toString();
6440
6638
  });
@@ -6498,6 +6696,7 @@ var patchTool = {
6498
6696
  permission: "confirm",
6499
6697
  mutating: true,
6500
6698
  capabilities: ["fs.write"],
6699
+ icon: "edit",
6501
6700
  timeoutMs: 3e4,
6502
6701
  inputSchema: {
6503
6702
  type: "object",
@@ -6605,11 +6804,12 @@ function extractPatchedFiles(output) {
6605
6804
  var planTool = {
6606
6805
  name: "plan",
6607
6806
  category: "Session",
6608
- 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.",
6609
- 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.',
6807
+ 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.',
6808
+ 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.',
6610
6809
  permission: "confirm",
6611
6810
  mutating: true,
6612
6811
  capabilities: ["fs.write"],
6812
+ icon: "plan",
6613
6813
  timeoutMs: 2e3,
6614
6814
  inputSchema: {
6615
6815
  type: "object",
@@ -6649,12 +6849,26 @@ var planTool = {
6649
6849
  template: {
6650
6850
  type: "string",
6651
6851
  description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
6852
+ },
6853
+ scope: {
6854
+ type: "string",
6855
+ enum: ["session", "project"],
6856
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
6652
6857
  }
6653
6858
  },
6654
6859
  required: ["action"]
6655
6860
  },
6656
6861
  async execute(input, ctx) {
6657
- const planPath = ctx.meta["plan.path"];
6862
+ const sessionPlanPath = ctx.meta["plan.path"];
6863
+ let planPath;
6864
+ if (input.scope === "project") {
6865
+ if (typeof sessionPlanPath === "string") {
6866
+ const lastSep = Math.max(sessionPlanPath.lastIndexOf("/"), sessionPlanPath.lastIndexOf("\\"));
6867
+ planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
6868
+ }
6869
+ } else {
6870
+ planPath = sessionPlanPath;
6871
+ }
6658
6872
  if (typeof planPath !== "string" || !planPath) {
6659
6873
  return {
6660
6874
  ok: false,
@@ -6668,148 +6882,169 @@ var planTool = {
6668
6882
  let early = null;
6669
6883
  const taskifyMeta = { title: "", details: "" };
6670
6884
  let didTaskify = false;
6671
- const plan = await mutatePlan(planPath, sessionId, async (p) => {
6672
- switch (input.action) {
6673
- case "show":
6674
- break;
6675
- case "add": {
6676
- const title = input.title?.trim();
6677
- if (!title) {
6678
- early = mkResult(p, false, "add requires `title`.");
6679
- return p;
6680
- }
6681
- const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
6682
- return updated;
6683
- }
6684
- case "start":
6685
- case "done": {
6686
- if (!input.target) {
6687
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
6688
- return p;
6689
- }
6690
- const next = setPlanItemStatus(
6691
- p,
6692
- input.target,
6693
- input.action === "start" ? "in_progress" : "done"
6694
- );
6695
- if (next === p) {
6696
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
6697
- return p;
6698
- }
6699
- return next;
6700
- }
6701
- case "remove": {
6702
- if (!input.target) {
6703
- early = mkResult(p, false, "remove requires `target` (id|index|substring).");
6704
- return p;
6705
- }
6706
- const next = removePlanItem(p, input.target);
6707
- if (next === p) {
6708
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
6709
- return p;
6710
- }
6711
- return next;
6712
- }
6713
- case "promote": {
6714
- if (!input.target) {
6715
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
6716
- return p;
6717
- }
6718
- const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
6719
- if (!derived) {
6720
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
6721
- return p;
6885
+ let plan;
6886
+ try {
6887
+ plan = await mutatePlan(planPath, sessionId, async (p) => {
6888
+ switch (input.action) {
6889
+ case "show":
6890
+ break;
6891
+ case "add": {
6892
+ const title = input.title?.trim();
6893
+ if (!title) {
6894
+ early = mkResult(p, false, "add requires `title`.");
6895
+ return p;
6896
+ }
6897
+ const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
6898
+ return updated;
6722
6899
  }
6723
- ctx.state.replaceTodos(derived.todos);
6724
- early = mkResult(
6725
- derived.plan,
6726
- true,
6727
- `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
6728
- derived.todos
6729
- );
6730
- return derived.plan;
6731
- }
6732
- case "template_use": {
6733
- const templateName = input.template?.trim();
6734
- if (!templateName) {
6735
- early = mkResult(p, false, "template_use requires `template` name.");
6736
- return p;
6900
+ case "start":
6901
+ case "done": {
6902
+ if (!input.target) {
6903
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
6904
+ return p;
6905
+ }
6906
+ const next = setPlanItemStatus(
6907
+ p,
6908
+ input.target,
6909
+ input.action === "start" ? "in_progress" : "done"
6910
+ );
6911
+ if (next === p) {
6912
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
6913
+ return p;
6914
+ }
6915
+ return next;
6737
6916
  }
6738
- const template = getPlanTemplate(templateName);
6739
- if (!template) {
6740
- early = mkResult(p, false, `Unknown template "${templateName}".`);
6741
- return p;
6917
+ case "remove": {
6918
+ if (!input.target) {
6919
+ early = mkResult(p, false, "remove requires `target` (id|index|substring).");
6920
+ return p;
6921
+ }
6922
+ const next = removePlanItem(p, input.target);
6923
+ if (next === p) {
6924
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
6925
+ return p;
6926
+ }
6927
+ return next;
6742
6928
  }
6743
- let updated = p;
6744
- for (const item of template.items) {
6745
- ({ plan: updated } = addPlanItem(updated, item.title, item.details));
6929
+ case "promote": {
6930
+ if (!input.target) {
6931
+ early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
6932
+ return p;
6933
+ }
6934
+ const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
6935
+ if (!derived) {
6936
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
6937
+ return p;
6938
+ }
6939
+ ctx.state.replaceTodos(derived.todos);
6940
+ early = mkResult(
6941
+ derived.plan,
6942
+ true,
6943
+ `${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
6944
+ derived.todos
6945
+ );
6946
+ return derived.plan;
6746
6947
  }
6747
- early = mkResult(
6748
- updated,
6749
- true,
6750
- `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
6751
- );
6752
- return updated;
6753
- }
6754
- case "clear":
6755
- return clearPlan(p);
6756
- case "taskify": {
6757
- if (!input.target) {
6758
- early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
6759
- return p;
6948
+ case "template_use": {
6949
+ const templateName = input.template?.trim();
6950
+ if (!templateName) {
6951
+ early = mkResult(p, false, "template_use requires `template` name.");
6952
+ return p;
6953
+ }
6954
+ const template = getPlanTemplate(templateName);
6955
+ if (!template) {
6956
+ early = mkResult(p, false, `Unknown template "${templateName}".`);
6957
+ return p;
6958
+ }
6959
+ let updated = p;
6960
+ for (const item of template.items) {
6961
+ ({ plan: updated } = addPlanItem(updated, item.title, item.details));
6962
+ }
6963
+ early = mkResult(
6964
+ updated,
6965
+ true,
6966
+ `Applied template "${template.name}" \u2014 ${template.items.length} items added.`
6967
+ );
6968
+ return updated;
6760
6969
  }
6761
- let itemIdx = -1;
6762
- const asNum = Number.parseInt(input.target, 10);
6763
- if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
6764
- itemIdx = asNum - 1;
6765
- } else {
6766
- itemIdx = p.items.findIndex((it) => it.id === input.target);
6767
- if (itemIdx === -1) {
6768
- const lower = input.target.toLowerCase();
6769
- itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
6970
+ case "clear":
6971
+ return clearPlan(p);
6972
+ case "taskify": {
6973
+ if (!input.target) {
6974
+ early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
6975
+ return p;
6770
6976
  }
6977
+ let itemIdx = -1;
6978
+ const asNum = Number.parseInt(input.target, 10);
6979
+ if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
6980
+ itemIdx = asNum - 1;
6981
+ } else {
6982
+ itemIdx = p.items.findIndex((it) => it.id === input.target);
6983
+ if (itemIdx === -1) {
6984
+ const lower = input.target.toLowerCase();
6985
+ itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
6986
+ }
6987
+ }
6988
+ if (itemIdx === -1 || !p.items[itemIdx]) {
6989
+ early = mkResult(p, false, `No plan item matched "${input.target}".`);
6990
+ return p;
6991
+ }
6992
+ const item = p.items[itemIdx];
6993
+ taskifyMeta.title = item.title;
6994
+ taskifyMeta.details = item.details ?? "";
6995
+ didTaskify = true;
6996
+ break;
6771
6997
  }
6772
- if (itemIdx === -1 || !p.items[itemIdx]) {
6773
- early = mkResult(p, false, `No plan item matched "${input.target}".`);
6998
+ default:
6999
+ early = mkResult(p, false, `Unknown action "${input.action}".`);
6774
7000
  return p;
6775
- }
6776
- const item = p.items[itemIdx];
6777
- taskifyMeta.title = item.title;
6778
- taskifyMeta.details = item.details ?? "";
6779
- didTaskify = true;
6780
- break;
6781
7001
  }
6782
- default:
6783
- early = mkResult(p, false, `Unknown action "${input.action}".`);
6784
- return p;
6785
- }
6786
- return p;
6787
- });
7002
+ return p;
7003
+ });
7004
+ } catch (err) {
7005
+ return {
7006
+ ok: false,
7007
+ message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
7008
+ plan: "",
7009
+ count: 0,
7010
+ open: 0
7011
+ };
7012
+ }
6788
7013
  if (early) return early;
6789
7014
  if (didTaskify) {
6790
- const taskPath = ctx.meta["task.path"];
6791
- if (typeof taskPath !== "string" || !taskPath) {
7015
+ const taskPathRaw = ctx.meta["task.path"];
7016
+ if (typeof taskPathRaw !== "string" || !taskPathRaw) {
6792
7017
  return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
6793
7018
  }
6794
- const taskFile = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
7019
+ let taskPath = taskPathRaw;
7020
+ if (input.scope === "project") {
7021
+ const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
7022
+ taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
7023
+ }
6795
7024
  const now = (/* @__PURE__ */ new Date()).toISOString();
6796
- taskFile.tasks.push({
6797
- id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
6798
- title: taskifyMeta.title,
6799
- description: taskifyMeta.details || void 0,
6800
- type: "feature",
6801
- priority: "medium",
6802
- status: "pending",
6803
- createdAt: now,
6804
- updatedAt: now
6805
- });
6806
- await saveTasks(taskPath, taskFile);
6807
- return mkResult(
6808
- plan,
6809
- true,
6810
- `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
7025
+ try {
7026
+ const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
7027
+ f.tasks.push({
7028
+ id: `task_${randomUUID()}`,
7029
+ title: taskifyMeta.title,
7030
+ description: taskifyMeta.details || void 0,
7031
+ type: "feature",
7032
+ priority: "medium",
7033
+ status: "pending",
7034
+ createdAt: now,
7035
+ updatedAt: now
7036
+ });
7037
+ return f;
7038
+ });
7039
+ return mkResult(
7040
+ plan,
7041
+ true,
7042
+ `taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
6811
7043
  ${formatTaskList(taskFile.tasks)}`
6812
- );
7044
+ );
7045
+ } catch (err) {
7046
+ return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
7047
+ }
6813
7048
  }
6814
7049
  return mkResult(plan, true, `Plan ${input.action} ok.`);
6815
7050
  }
@@ -6835,6 +7070,7 @@ var readTool = {
6835
7070
  permission: "auto",
6836
7071
  mutating: false,
6837
7072
  capabilities: ["fs.read"],
7073
+ icon: "file",
6838
7074
  maxOutputBytes: 262144,
6839
7075
  timeoutMs: 5e3,
6840
7076
  inputSchema: {
@@ -6865,7 +7101,7 @@ var readTool = {
6865
7101
  const code = err.code;
6866
7102
  if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
6867
7103
  throw new Error(
6868
- `read: failed to stat "${input.path}": ${err instanceof Error ? err.message : String(err)}`
7104
+ `read: failed to stat "${input.path}": ${toErrorMessage(err)}`
6869
7105
  );
6870
7106
  }
6871
7107
  if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
@@ -6907,6 +7143,7 @@ var replaceTool = {
6907
7143
  permission: "confirm",
6908
7144
  mutating: true,
6909
7145
  capabilities: ["fs.write"],
7146
+ icon: "edit",
6910
7147
  timeoutMs: 3e4,
6911
7148
  inputSchema: {
6912
7149
  type: "object",
@@ -7198,6 +7435,7 @@ var scaffoldTool = {
7198
7435
  permission: "confirm",
7199
7436
  mutating: true,
7200
7437
  capabilities: ["fs.write.outside-project", "fs.write"],
7438
+ icon: "scaffold",
7201
7439
  timeoutMs: 3e4,
7202
7440
  inputSchema: {
7203
7441
  type: "object",
@@ -7293,6 +7531,7 @@ var searchTool = {
7293
7531
  permission: "confirm",
7294
7532
  mutating: false,
7295
7533
  capabilities: ["net.outbound"],
7534
+ icon: "search",
7296
7535
  timeoutMs: TIMEOUT_MS3,
7297
7536
  inputSchema: {
7298
7537
  type: "object",
@@ -7367,7 +7606,7 @@ async function duckduckgoSearch(query2, num, signal) {
7367
7606
  truncated: results.length >= num
7368
7607
  };
7369
7608
  } catch (err) {
7370
- console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: err instanceof Error ? err.message : String(err) }));
7609
+ console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage(err) }));
7371
7610
  return {
7372
7611
  query: query2,
7373
7612
  results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
@@ -7506,6 +7745,7 @@ var setWorkingDirTool = {
7506
7745
  permission: "confirm",
7507
7746
  mutating: true,
7508
7747
  capabilities: ["fs.read"],
7748
+ icon: "settings",
7509
7749
  timeoutMs: 5e3,
7510
7750
  inputSchema: {
7511
7751
  type: "object",
@@ -7530,7 +7770,7 @@ var setWorkingDirTool = {
7530
7770
  } catch (err) {
7531
7771
  return {
7532
7772
  current: ctx.workingDir,
7533
- error: err instanceof Error ? err.message : String(err)
7773
+ error: toErrorMessage(err)
7534
7774
  };
7535
7775
  }
7536
7776
  try {
@@ -7566,11 +7806,12 @@ function findTaskIndex(tasks, query2) {
7566
7806
  var taskTool = {
7567
7807
  name: "task",
7568
7808
  category: "Session",
7569
- 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.",
7570
- 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',
7809
+ 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.',
7810
+ 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)',
7571
7811
  permission: "confirm",
7572
7812
  mutating: true,
7573
7813
  capabilities: ["fs.write"],
7814
+ icon: "task",
7574
7815
  timeoutMs: 2e3,
7575
7816
  inputSchema: {
7576
7817
  type: "object",
@@ -7636,12 +7877,26 @@ var taskTool = {
7636
7877
  type: "array",
7637
7878
  items: { type: "string" },
7638
7879
  description: "Optional subtask titles for action=promote. Each becomes a pending todo."
7880
+ },
7881
+ scope: {
7882
+ type: "string",
7883
+ enum: ["session", "project"],
7884
+ description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
7639
7885
  }
7640
7886
  },
7641
7887
  required: ["action"]
7642
7888
  },
7643
7889
  async execute(input, ctx) {
7644
- const taskPath = ctx.meta["task.path"];
7890
+ const sessionTaskPath = ctx.meta["task.path"];
7891
+ let taskPath;
7892
+ if (input.scope === "project") {
7893
+ if (typeof sessionTaskPath === "string") {
7894
+ const lastSep = Math.max(sessionTaskPath.lastIndexOf("/"), sessionTaskPath.lastIndexOf("\\"));
7895
+ taskPath = lastSep >= 0 ? sessionTaskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
7896
+ }
7897
+ } else {
7898
+ taskPath = sessionTaskPath;
7899
+ }
7645
7900
  if (typeof taskPath !== "string" || !taskPath) {
7646
7901
  return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
7647
7902
  }
@@ -7651,23 +7906,66 @@ var taskTool = {
7651
7906
  const planifyMeta = { title: "", details: "" };
7652
7907
  let didPlanify = false;
7653
7908
  let todosToReplace = null;
7654
- const file = await mutateTasks(taskPath, sessionId, async (f) => {
7655
- switch (input.action) {
7656
- case "show":
7657
- break;
7658
- case "replace": {
7659
- if (!Array.isArray(input.tasks)) {
7660
- early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
7661
- return f;
7909
+ let file;
7910
+ try {
7911
+ file = await mutateTasks(taskPath, sessionId, async (f) => {
7912
+ switch (input.action) {
7913
+ case "show":
7914
+ break;
7915
+ case "replace": {
7916
+ if (!Array.isArray(input.tasks)) {
7917
+ early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
7918
+ return f;
7919
+ }
7920
+ const newIds = new Set(input.tasks.map((t) => t.id));
7921
+ if (newIds.size !== input.tasks.length) {
7922
+ const seen = /* @__PURE__ */ new Set();
7923
+ const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => seen.has(id) ? true : (seen.add(id), false)))];
7924
+ early = {
7925
+ ok: false,
7926
+ message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
7927
+ count: 0,
7928
+ completed: 0,
7929
+ inProgress: 0
7930
+ };
7931
+ return f;
7932
+ }
7933
+ for (const t of input.tasks) {
7934
+ if (t.dependsOn && t.dependsOn.length > 0) {
7935
+ const missing = t.dependsOn.filter((d) => !newIds.has(d));
7936
+ if (missing.length > 0) {
7937
+ early = {
7938
+ ok: false,
7939
+ message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
7940
+ count: 0,
7941
+ completed: 0,
7942
+ inProgress: 0
7943
+ };
7944
+ return f;
7945
+ }
7946
+ }
7947
+ }
7948
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7949
+ f.tasks = input.tasks.map((t) => ({
7950
+ ...t,
7951
+ createdAt: t.createdAt || now,
7952
+ updatedAt: now
7953
+ }));
7954
+ break;
7662
7955
  }
7663
- const newIds = new Set(input.tasks.map((t) => t.id));
7664
- for (const t of input.tasks) {
7956
+ case "add": {
7957
+ const t = input.task;
7958
+ if (!t || !t.title) {
7959
+ early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
7960
+ return f;
7961
+ }
7665
7962
  if (t.dependsOn && t.dependsOn.length > 0) {
7666
- const missing = t.dependsOn.filter((d) => !newIds.has(d));
7963
+ const existingIds = new Set(f.tasks.map((e) => e.id));
7964
+ const missing = t.dependsOn.filter((d) => !existingIds.has(d));
7667
7965
  if (missing.length > 0) {
7668
7966
  early = {
7669
7967
  ok: false,
7670
- message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
7968
+ message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
7671
7969
  count: 0,
7672
7970
  completed: 0,
7673
7971
  inProgress: 0
@@ -7675,165 +7973,170 @@ var taskTool = {
7675
7973
  return f;
7676
7974
  }
7677
7975
  }
7976
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7977
+ const newTask = {
7978
+ id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
7979
+ title: t.title,
7980
+ description: t.description,
7981
+ type: t.type || "feature",
7982
+ priority: t.priority || "medium",
7983
+ status: t.status || "pending",
7984
+ dependsOn: t.dependsOn,
7985
+ assignee: t.assignee,
7986
+ estimateHours: t.estimateHours,
7987
+ tags: t.tags,
7988
+ createdAt: now,
7989
+ updatedAt: now
7990
+ };
7991
+ f.tasks.push(newTask);
7992
+ break;
7678
7993
  }
7679
- const now = (/* @__PURE__ */ new Date()).toISOString();
7680
- f.tasks = input.tasks.map((t) => ({
7681
- ...t,
7682
- createdAt: t.createdAt || now,
7683
- updatedAt: now
7684
- }));
7685
- break;
7686
- }
7687
- case "add": {
7688
- const t = input.task;
7689
- if (!t || !t.title) {
7690
- early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
7691
- return f;
7692
- }
7693
- if (t.dependsOn && t.dependsOn.length > 0) {
7694
- const existingIds = new Set(f.tasks.map((e) => e.id));
7695
- const missing = t.dependsOn.filter((d) => !existingIds.has(d));
7696
- if (missing.length > 0) {
7697
- early = {
7698
- ok: false,
7699
- message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
7700
- count: 0,
7701
- completed: 0,
7702
- inProgress: 0
7703
- };
7994
+ case "status": {
7995
+ if (!input.id || !input.status) {
7996
+ early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
7704
7997
  return f;
7705
7998
  }
7999
+ const task = f.tasks.find((t) => t.id === input.id);
8000
+ if (!task) {
8001
+ early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
8002
+ return f;
8003
+ }
8004
+ task.status = input.status;
8005
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8006
+ break;
7706
8007
  }
7707
- const now = (/* @__PURE__ */ new Date()).toISOString();
7708
- const newTask = {
7709
- id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
7710
- title: t.title,
7711
- description: t.description,
7712
- type: t.type || "feature",
7713
- priority: t.priority || "medium",
7714
- status: t.status || "pending",
7715
- dependsOn: t.dependsOn,
7716
- assignee: t.assignee,
7717
- estimateHours: t.estimateHours,
7718
- tags: t.tags,
7719
- createdAt: now,
7720
- updatedAt: now
7721
- };
7722
- f.tasks.push(newTask);
7723
- break;
7724
- }
7725
- case "status": {
7726
- if (!input.id || !input.status) {
7727
- early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
7728
- return f;
7729
- }
7730
- const task = f.tasks.find((t) => t.id === input.id);
7731
- if (!task) {
7732
- early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
7733
- return f;
7734
- }
7735
- task.status = input.status;
7736
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7737
- break;
7738
- }
7739
- case "promote": {
7740
- const target = input.target?.trim();
7741
- if (!target) {
7742
- early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
7743
- return f;
7744
- }
7745
- const idx = findTaskIndex(f.tasks, target);
7746
- if (idx === -1) {
7747
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
7748
- return f;
7749
- }
7750
- const match = f.tasks[idx];
7751
- if (!match) {
7752
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
7753
- return f;
7754
- }
7755
- if (match.status !== "completed" && match.status !== "failed") {
7756
- match.status = "in_progress";
7757
- match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7758
- }
7759
- const todos = [];
7760
- const ts2 = Date.now();
7761
- todos.push({
7762
- id: `todo_${ts2}_task`,
7763
- content: match.title,
7764
- status: "in_progress",
7765
- activeForm: match.title,
7766
- promotedFromTask: match.id
7767
- });
7768
- if (match.description) {
8008
+ case "promote": {
8009
+ const target = input.target?.trim();
8010
+ if (!target) {
8011
+ early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
8012
+ return f;
8013
+ }
8014
+ const idx = findTaskIndex(f.tasks, target);
8015
+ if (idx === -1) {
8016
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8017
+ return f;
8018
+ }
8019
+ const match = f.tasks[idx];
8020
+ if (!match) {
8021
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8022
+ return f;
8023
+ }
8024
+ if (match.status !== "completed" && match.status !== "failed") {
8025
+ match.status = "in_progress";
8026
+ match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8027
+ }
8028
+ const todos = [];
8029
+ const ts2 = Date.now();
7769
8030
  todos.push({
7770
- id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
7771
- content: match.description.slice(0, 200),
7772
- status: "pending",
8031
+ id: `todo_${ts2}_task`,
8032
+ content: match.title,
8033
+ status: "in_progress",
8034
+ activeForm: match.title,
7773
8035
  promotedFromTask: match.id
7774
8036
  });
7775
- }
7776
- if (input.subtasks && input.subtasks.length > 0) {
7777
- for (const st of input.subtasks) {
8037
+ if (match.description) {
7778
8038
  todos.push({
7779
8039
  id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
7780
- content: st,
8040
+ content: match.description.slice(0, 200),
7781
8041
  status: "pending",
7782
8042
  promotedFromTask: match.id
7783
8043
  });
7784
8044
  }
8045
+ if (input.subtasks && input.subtasks.length > 0) {
8046
+ for (const st of input.subtasks) {
8047
+ todos.push({
8048
+ id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
8049
+ content: st,
8050
+ status: "pending",
8051
+ promotedFromTask: match.id
8052
+ });
8053
+ }
8054
+ }
8055
+ todosToReplace = todos;
8056
+ promoteMeta.count = todos.length;
8057
+ promoteMeta.title = match.title;
8058
+ break;
7785
8059
  }
7786
- todosToReplace = todos;
7787
- promoteMeta.count = todos.length;
7788
- promoteMeta.title = match.title;
7789
- break;
7790
- }
7791
- case "planify": {
7792
- const target = input.target?.trim();
7793
- if (!target) {
7794
- early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
7795
- return f;
7796
- }
7797
- const idx = findTaskIndex(f.tasks, target);
7798
- if (idx === -1) {
7799
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
7800
- return f;
8060
+ case "planify": {
8061
+ const target = input.target?.trim();
8062
+ if (!target) {
8063
+ early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
8064
+ return f;
8065
+ }
8066
+ const idx = findTaskIndex(f.tasks, target);
8067
+ if (idx === -1) {
8068
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8069
+ return f;
8070
+ }
8071
+ const match = f.tasks[idx];
8072
+ if (!match) {
8073
+ early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8074
+ return f;
8075
+ }
8076
+ planifyMeta.title = match.title;
8077
+ planifyMeta.details = match.description ?? "";
8078
+ didPlanify = true;
8079
+ break;
7801
8080
  }
7802
- const match = f.tasks[idx];
7803
- if (!match) {
7804
- early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
8081
+ default:
8082
+ early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
7805
8083
  return f;
7806
- }
7807
- planifyMeta.title = match.title;
7808
- planifyMeta.details = match.description ?? "";
7809
- didPlanify = true;
7810
- break;
7811
8084
  }
7812
- default:
7813
- early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
7814
- return f;
7815
- }
7816
- return f;
7817
- });
8085
+ return f;
8086
+ });
8087
+ } catch (err) {
8088
+ return {
8089
+ ok: false,
8090
+ message: `Task change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
8091
+ count: 0,
8092
+ completed: 0,
8093
+ inProgress: 0
8094
+ };
8095
+ }
7818
8096
  if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
7819
8097
  if (early) return early;
7820
8098
  if (didPlanify) {
7821
8099
  const { title, details } = planifyMeta;
7822
- const planPath = ctx.meta["plan.path"];
7823
- if (typeof planPath === "string" && planPath) {
7824
- const planCfg = await loadPlan(planPath) ?? emptyPlan(sessionId);
7825
- const { plan: updated } = addPlanItem(planCfg, title, details || void 0);
7826
- await savePlan(planPath, updated);
8100
+ const planPathRaw = ctx.meta["plan.path"];
8101
+ const prog = computeTaskItemProgress(file.tasks);
8102
+ if (typeof planPathRaw === "string" && planPathRaw) {
8103
+ let planPath = planPathRaw;
8104
+ if (input.scope === "project") {
8105
+ const lastSep = Math.max(planPath.lastIndexOf("/"), planPath.lastIndexOf("\\"));
8106
+ planPath = lastSep >= 0 ? planPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
8107
+ }
8108
+ let formatted = "";
8109
+ try {
8110
+ await mutatePlan(planPath, sessionId, (pf) => {
8111
+ const { plan: updated } = addPlanItem(pf, title, details || void 0);
8112
+ formatted = formatPlan(updated);
8113
+ return updated;
8114
+ });
8115
+ } catch (err) {
8116
+ return {
8117
+ ok: false,
8118
+ message: `planify: plan not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
8119
+ count: file.tasks.length,
8120
+ completed: prog.completed,
8121
+ inProgress: prog.inProgress
8122
+ };
8123
+ }
7827
8124
  return {
7828
8125
  ok: true,
7829
8126
  message: `planify ok \u2014 added "${title}" to plan.
7830
- ${formatPlan(updated)}`,
8127
+ ${formatted}`,
7831
8128
  count: file.tasks.length,
7832
- completed: computeTaskItemProgress(file.tasks).completed,
7833
- inProgress: computeTaskItemProgress(file.tasks).inProgress
8129
+ completed: prog.completed,
8130
+ inProgress: prog.inProgress
7834
8131
  };
7835
8132
  }
7836
- return { ok: false, message: "Plan storage path not configured \u2014 cannot planify.", count: 0, completed: 0, inProgress: 0 };
8133
+ return {
8134
+ ok: false,
8135
+ message: "Plan storage path not configured \u2014 cannot planify.",
8136
+ count: file.tasks.length,
8137
+ completed: prog.completed,
8138
+ inProgress: prog.inProgress
8139
+ };
7837
8140
  }
7838
8141
  const p = computeTaskItemProgress(file.tasks);
7839
8142
  const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
@@ -7854,6 +8157,7 @@ var testTool = {
7854
8157
  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.",
7855
8158
  permission: "confirm",
7856
8159
  mutating: false,
8160
+ icon: "test",
7857
8161
  timeoutMs: 12e4,
7858
8162
  capabilities: ["shell.restricted"],
7859
8163
  inputSchema: {
@@ -8013,6 +8317,7 @@ var todoTool = {
8013
8317
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
8014
8318
  timeoutMs: 1e3,
8015
8319
  capabilities: ["session.todo"],
8320
+ icon: "todo",
8016
8321
  inputSchema: {
8017
8322
  type: "object",
8018
8323
  properties: {
@@ -8121,6 +8426,7 @@ var toolHelpTool = {
8121
8426
  mutating: false,
8122
8427
  timeoutMs: 5e3,
8123
8428
  capabilities: ["tool.meta"],
8429
+ icon: "meta",
8124
8430
  inputSchema: {
8125
8431
  type: "object",
8126
8432
  properties: {
@@ -8244,6 +8550,7 @@ var toolSearchTool = {
8244
8550
  mutating: false,
8245
8551
  timeoutMs: 1e3,
8246
8552
  capabilities: ["tool.meta"],
8553
+ icon: "meta",
8247
8554
  inputSchema: {
8248
8555
  type: "object",
8249
8556
  properties: {
@@ -8323,6 +8630,7 @@ var toolUseTool = {
8323
8630
  mutating: true,
8324
8631
  timeoutMs: 6e4,
8325
8632
  capabilities: ["tool.mutate.any"],
8633
+ icon: "meta",
8326
8634
  inputSchema: {
8327
8635
  type: "object",
8328
8636
  properties: {
@@ -8403,6 +8711,7 @@ var treeTool = {
8403
8711
  permission: "auto",
8404
8712
  mutating: false,
8405
8713
  capabilities: ["fs.read"],
8714
+ icon: "tree",
8406
8715
  timeoutMs: 15e3,
8407
8716
  inputSchema: {
8408
8717
  type: "object",
@@ -8568,6 +8877,7 @@ var typecheckTool = {
8568
8877
  mutating: false,
8569
8878
  timeoutMs: 12e4,
8570
8879
  capabilities: ["shell.restricted"],
8880
+ icon: "code",
8571
8881
  inputSchema: {
8572
8882
  type: "object",
8573
8883
  properties: {
@@ -8656,6 +8966,7 @@ var writeTool = {
8656
8966
  mutating: true,
8657
8967
  timeoutMs: 5e3,
8658
8968
  capabilities: ["fs.write"],
8969
+ icon: "file",
8659
8970
  inputSchema: {
8660
8971
  type: "object",
8661
8972
  properties: {
@@ -8741,6 +9052,36 @@ var TIER1_TOOLS = [
8741
9052
  jsonTool,
8742
9053
  searchTool
8743
9054
  ];
9055
+ var TIER2_TOOLS = [
9056
+ replaceTool,
9057
+ execTool,
9058
+ fetchTool,
9059
+ gitTool,
9060
+ treeTool,
9061
+ lintTool,
9062
+ formatTool,
9063
+ typecheckTool,
9064
+ testTool,
9065
+ todoTool,
9066
+ planTool,
9067
+ taskTool,
9068
+ installTool,
9069
+ auditTool
9070
+ ];
9071
+ var TIER3_TOOLS = [
9072
+ outdatedTool,
9073
+ logsTool,
9074
+ documentTool,
9075
+ scaffoldTool,
9076
+ toolSearchTool,
9077
+ toolUseTool,
9078
+ batchToolUseTool,
9079
+ toolHelpTool,
9080
+ codebaseIndexTool,
9081
+ codebaseSearchTool,
9082
+ codebaseStatsTool,
9083
+ setWorkingDirTool
9084
+ ];
8744
9085
  var builtinTools = [
8745
9086
  readTool,
8746
9087
  writeTool,
@@ -8780,6 +9121,6 @@ var builtinTools = [
8780
9121
  setWorkingDirTool
8781
9122
  ];
8782
9123
 
8783
- export { OPTIONAL_TOOLS, TIER1_TOOLS, builtinTools };
9124
+ export { OPTIONAL_TOOLS, TIER1_TOOLS, TIER2_TOOLS, TIER3_TOOLS, builtinTools };
8784
9125
  //# sourceMappingURL=builtin.js.map
8785
9126
  //# sourceMappingURL=builtin.js.map