@wrongstack/tools 0.287.0 → 0.289.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 (93) hide show
  1. package/dist/_fetch-guard.d.ts.map +1 -1
  2. package/dist/_redact-command.d.ts.map +1 -1
  3. package/dist/_spawn-stream.d.ts.map +1 -1
  4. package/dist/audit.js +61 -14
  5. package/dist/audit.js.map +3 -3
  6. package/dist/bash.d.ts +1 -1
  7. package/dist/bash.d.ts.map +1 -1
  8. package/dist/bash.js +97 -19
  9. package/dist/bash.js.map +3 -3
  10. package/dist/builtin.d.ts +12 -10
  11. package/dist/builtin.d.ts.map +1 -1
  12. package/dist/builtin.js +1620 -803
  13. package/dist/builtin.js.map +3 -3
  14. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  15. package/dist/codebase-index/bm25.d.ts +1 -2
  16. package/dist/codebase-index/bm25.d.ts.map +1 -1
  17. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  18. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  19. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  20. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  21. package/dist/codebase-index/index-service.d.ts +11 -1
  22. package/dist/codebase-index/index-service.d.ts.map +1 -1
  23. package/dist/codebase-index/index.d.ts +2 -1
  24. package/dist/codebase-index/index.d.ts.map +1 -1
  25. package/dist/codebase-index/index.js +749 -192
  26. package/dist/codebase-index/index.js.map +3 -3
  27. package/dist/codebase-index/indexer.d.ts.map +1 -1
  28. package/dist/codebase-index/schema.d.ts +43 -1
  29. package/dist/codebase-index/schema.d.ts.map +1 -1
  30. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  31. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  32. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  33. package/dist/codebase-index/worker.js +610 -152
  34. package/dist/codebase-index/worker.js.map +3 -3
  35. package/dist/codebase-index/writer.d.ts +27 -1
  36. package/dist/codebase-index/writer.d.ts.map +1 -1
  37. package/dist/e2e.js +477 -0
  38. package/dist/e2e.js.map +7 -0
  39. package/dist/exec.d.ts +1 -1
  40. package/dist/exec.d.ts.map +1 -1
  41. package/dist/exec.js +60 -5
  42. package/dist/exec.js.map +3 -3
  43. package/dist/fetch.js +2 -2
  44. package/dist/fetch.js.map +2 -2
  45. package/dist/format.js +61 -14
  46. package/dist/format.js.map +3 -3
  47. package/dist/glob.js +2 -2
  48. package/dist/glob.js.map +1 -1
  49. package/dist/grep.d.ts.map +1 -1
  50. package/dist/grep.js +2 -2
  51. package/dist/grep.js.map +2 -2
  52. package/dist/index.d.ts +3 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1786 -809
  55. package/dist/index.js.map +3 -3
  56. package/dist/install.js +61 -14
  57. package/dist/install.js.map +3 -3
  58. package/dist/kanban.d.ts +11 -2
  59. package/dist/kanban.d.ts.map +1 -1
  60. package/dist/kanban.js +695 -571
  61. package/dist/kanban.js.map +3 -3
  62. package/dist/languages/detect.d.ts.map +1 -1
  63. package/dist/languages/index.js +61 -14
  64. package/dist/languages/index.js.map +3 -3
  65. package/dist/languages/types.d.ts +7 -0
  66. package/dist/languages/types.d.ts.map +1 -1
  67. package/dist/lint.js +61 -14
  68. package/dist/lint.js.map +3 -3
  69. package/dist/outdated.js +61 -14
  70. package/dist/outdated.js.map +3 -3
  71. package/dist/pack.js +1617 -800
  72. package/dist/pack.js.map +3 -3
  73. package/dist/plan.js +4 -0
  74. package/dist/plan.js.map +2 -2
  75. package/dist/process-registry.js +12 -5
  76. package/dist/process-registry.js.map +2 -2
  77. package/dist/search.js +2 -2
  78. package/dist/search.js.map +2 -2
  79. package/dist/session-kanban.d.ts +9 -0
  80. package/dist/session-kanban.d.ts.map +1 -1
  81. package/dist/session-kanban.js +137 -0
  82. package/dist/session-kanban.js.map +2 -2
  83. package/dist/task.js +4 -0
  84. package/dist/task.js.map +2 -2
  85. package/dist/test.js +61 -14
  86. package/dist/test.js.map +3 -3
  87. package/dist/todo.js +4 -0
  88. package/dist/todo.js.map +2 -2
  89. package/dist/tree.js +2 -2
  90. package/dist/tree.js.map +1 -1
  91. package/dist/typecheck.js +61 -14
  92. package/dist/typecheck.js.map +3 -3
  93. package/package.json +7 -3
package/dist/index.js CHANGED
@@ -404,8 +404,10 @@ function redactCommand(cmd) {
404
404
  const flag = match.slice(0, match.indexOf(expectDefined(delim)) + 1);
405
405
  return `${flag}[REDACTED]`;
406
406
  }
407
- const flagEnd = match.match(/^--?[a-zA-Z][a-zA-Z0-9_-]*/)?.[0] ?? match;
408
- return `${flagEnd}=**redacted**`;
407
+ if (match.startsWith("--")) {
408
+ return match;
409
+ }
410
+ return `${match.slice(0, 2)}[REDACTED]`;
409
411
  });
410
412
  }
411
413
  return result;
@@ -417,9 +419,14 @@ var init_redact_command = __esm({
417
419
  SENSITIVE_FLAG_PATTERNS = [
418
420
  // --flag=value or --flag "value" (value captured up to next space or comma)
419
421
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
420
- // -f "value" style short flags
421
- /(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
422
- /(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
422
+ // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
423
+ // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
424
+ // NOTE: synced with @wrongstack/core observability/redact-command.ts.
425
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
426
+ // -p|-password|-a (redis auth) short flags: attached + separated + =value.
427
+ // Same token-start anchor; over-redaction is an accepted tradeoff for a
428
+ // redaction function. Synced with core copy.
429
+ /(?<![-\w])-(?:password|p|a)(?:[=\s]+)?[^\s,-]+/gi,
423
430
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
424
431
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
425
432
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
@@ -802,7 +809,12 @@ var init_process_registry = __esm({
802
809
 
803
810
  // src/_spawn-stream.ts
804
811
  import { spawn as spawn2 } from "node:child_process";
805
- import { buildChildEnv } from "@wrongstack/core";
812
+ import {
813
+ buildChildEnv,
814
+ emitProcessCompleted,
815
+ emitProcessOutput,
816
+ emitProcessStarted
817
+ } from "@wrongstack/core";
806
818
  async function* spawnStream(opts) {
807
819
  const max = opts.maxBytes ?? 2e5;
808
820
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -827,6 +839,19 @@ async function* spawnStream(opts) {
827
839
  });
828
840
  const registry = getProcessRegistry();
829
841
  const pid = child.pid;
842
+ const processStartedAt = Date.now();
843
+ let stdoutBytes = 0;
844
+ let stderrBytes = 0;
845
+ let telemetryCompleted = false;
846
+ emitProcessStarted({
847
+ ...pid !== void 0 ? { pid } : {},
848
+ parentPid: process.pid,
849
+ command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
850
+ args: redactCommand(opts.args.join(" ")).split(" ").filter(Boolean),
851
+ cwd: opts.cwd,
852
+ background: false,
853
+ startedAt: new Date(processStartedAt).toISOString()
854
+ });
830
855
  if (typeof pid === "number") {
831
856
  registry.register({
832
857
  pid,
@@ -855,6 +880,8 @@ async function* spawnStream(opts) {
855
880
  };
856
881
  const onOut = (c) => {
857
882
  const s = c.toString();
883
+ stdoutBytes += c.byteLength;
884
+ emitProcessOutput({ pid, stream: "stdout", chunk: c });
858
885
  if (stdout.length < max) stdout += s;
859
886
  spool.write(s);
860
887
  queue.push({ kind: "out", data: s });
@@ -867,6 +894,8 @@ async function* spawnStream(opts) {
867
894
  };
868
895
  const onErr = (c) => {
869
896
  const s = c.toString();
897
+ stderrBytes += c.byteLength;
898
+ emitProcessOutput({ pid, stream: "stderr", chunk: c });
870
899
  if (stderr.length < max) stderr += s;
871
900
  spool.write(s);
872
901
  queue.push({ kind: "err", data: s });
@@ -884,9 +913,25 @@ async function* spawnStream(opts) {
884
913
  queue.push({ kind: "error", data: e.message });
885
914
  wake();
886
915
  });
887
- child.on("close", (code) => {
916
+ const completeTelemetry = (code, signal, timedOut = false) => {
917
+ if (telemetryCompleted) return;
918
+ telemetryCompleted = true;
919
+ emitProcessCompleted({
920
+ ...pid !== void 0 ? { pid } : {},
921
+ exitCode: code,
922
+ ...signal ? { signal } : {},
923
+ durationMs: Date.now() - processStartedAt,
924
+ stdoutBytes,
925
+ stderrBytes,
926
+ timedOut,
927
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
928
+ });
929
+ };
930
+ child.on("close", (code, signal) => {
888
931
  if (typeof pid === "number") registry.unregister(pid);
889
- queue.push({ kind: "close", data: "", code: code ?? 0 });
932
+ const exitCode2 = code ?? (signal ? 1 : 0);
933
+ completeTelemetry(exitCode2, signal ?? void 0);
934
+ queue.push({ kind: "close", data: "", code: exitCode2, ...signal ? { signal } : {} });
890
935
  wake();
891
936
  });
892
937
  const onAbort = () => {
@@ -899,6 +944,7 @@ async function* spawnStream(opts) {
899
944
  }
900
945
  }
901
946
  queue.push({ kind: "close", data: "", code: 124 });
947
+ completeTelemetry(124, "SIGKILL", true);
902
948
  wake();
903
949
  };
904
950
  if (isWin) {
@@ -2506,13 +2552,14 @@ async function detectLanguageWorkspaces(options) {
2506
2552
  const target = options.target ? await canonicalInside(resolveFrom(cwd, options.target), projectRoot, "target") : void 0;
2507
2553
  const profiles = (options.profiles ?? languageProfileRegistry.list()).filter((profile) => !options.language || profile.id === options.language).slice().sort((a, b) => a.id.localeCompare(b.id));
2508
2554
  const limits = normalizeLimits(options.limits);
2555
+ const extraIgnores = new Set(options.ignoredDirectories ?? []);
2509
2556
  const state = {
2510
2557
  entries: 0,
2511
2558
  truncated: false,
2512
2559
  sourcePaths: /* @__PURE__ */ new Map(),
2513
2560
  candidates: /* @__PURE__ */ new Map()
2514
2561
  };
2515
- await scanDirectory(projectRoot, 0, profiles, limits, state, options.signal);
2562
+ await scanDirectory(projectRoot, 0, profiles, limits, state, extraIgnores, options.signal);
2516
2563
  addSourceFallbacks(projectRoot, profiles, state);
2517
2564
  if (target) addTargetEvidence(target, projectRoot, profiles, state);
2518
2565
  const workspaces = await Promise.all(
@@ -2526,7 +2573,7 @@ async function detectLanguageWorkspaces(options) {
2526
2573
  workspaces
2527
2574
  };
2528
2575
  }
2529
- async function scanDirectory(directory, depth, profiles, limits, state, signal) {
2576
+ async function scanDirectory(directory, depth, profiles, limits, state, extraIgnores, signal) {
2530
2577
  signal?.throwIfAborted();
2531
2578
  if (depth > limits.maxDepth || state.entries >= limits.maxEntries) {
2532
2579
  state.truncated = true;
@@ -2549,20 +2596,20 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
2549
2596
  const fullPath = path4.join(directory, entry.name);
2550
2597
  if (entry.isSymbolicLink()) continue;
2551
2598
  if (entry.isDirectory()) {
2552
- if (shouldIgnoreDirectory(entry.name, profiles)) continue;
2599
+ if (shouldIgnoreDirectory(entry.name, profiles, extraIgnores)) continue;
2553
2600
  if (depth >= limits.maxDepth) {
2554
2601
  state.truncated = true;
2555
2602
  continue;
2556
2603
  }
2557
- await scanDirectory(fullPath, depth + 1, profiles, limits, state, signal);
2604
+ await scanDirectory(fullPath, depth + 1, profiles, limits, state, extraIgnores, signal);
2558
2605
  continue;
2559
2606
  }
2560
2607
  if (!entry.isFile()) continue;
2561
2608
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2562
2609
  }
2563
2610
  }
2564
- function collectFileEvidence(directory, fullPath, basename9, profiles, state) {
2565
- const lower = basename9.toLowerCase();
2611
+ function collectFileEvidence(directory, fullPath, basename10, profiles, state) {
2612
+ const lower = basename10.toLowerCase();
2566
2613
  const extension = path4.extname(lower);
2567
2614
  for (const profile of profiles) {
2568
2615
  const detector = profile.detectors.find(
@@ -2573,7 +2620,7 @@ function collectFileEvidence(directory, fullPath, basename9, profiles, state) {
2573
2620
  candidate.evidence.push({
2574
2621
  kind: detector.kind,
2575
2622
  path: fullPath,
2576
- value: basename9,
2623
+ value: basename10,
2577
2624
  weight: detector.weight
2578
2625
  });
2579
2626
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2683,8 +2730,8 @@ function getCandidate(state, profile, root) {
2683
2730
  }
2684
2731
  return candidate;
2685
2732
  }
2686
- function shouldIgnoreDirectory(name, profiles) {
2687
- if (GLOBAL_IGNORES.has(name) || name.startsWith(".")) return true;
2733
+ function shouldIgnoreDirectory(name, profiles, extraIgnores) {
2734
+ if (GLOBAL_IGNORES.has(name) || extraIgnores.has(name) || name.startsWith(".")) return true;
2688
2735
  return profiles.some((profile) => profile.ignoredDirectories.includes(name));
2689
2736
  }
2690
2737
  function normalizeLimits(input) {
@@ -5210,6 +5257,11 @@ function parseAuditOutput(json2, exitCode) {
5210
5257
  // src/bash.ts
5211
5258
  import { spawn as spawn3 } from "node:child_process";
5212
5259
  import * as os4 from "node:os";
5260
+ import {
5261
+ emitProcessCompleted as emitProcessCompleted2,
5262
+ emitProcessOutput as emitProcessOutput2,
5263
+ emitProcessStarted as emitProcessStarted2
5264
+ } from "@wrongstack/core";
5213
5265
 
5214
5266
  // src/_env.ts
5215
5267
  import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core";
@@ -6085,6 +6137,32 @@ var bashTool = {
6085
6137
  }
6086
6138
  }
6087
6139
  const pid2 = child2.pid;
6140
+ let stdoutBytes2 = 0;
6141
+ let stderrBytes2 = 0;
6142
+ let telemetryCompleted2 = false;
6143
+ emitProcessStarted2({
6144
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
6145
+ parentPid: process.pid,
6146
+ command: redactCommand(`${shell} ${args.join(" ")}`),
6147
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
6148
+ cwd: ctx.projectRoot,
6149
+ background: true,
6150
+ startedAt: new Date(startedAt).toISOString()
6151
+ });
6152
+ const completeBackground = (exitCode, signal) => {
6153
+ if (telemetryCompleted2) return;
6154
+ telemetryCompleted2 = true;
6155
+ emitProcessCompleted2({
6156
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
6157
+ exitCode,
6158
+ ...signal ? { signal } : {},
6159
+ durationMs: Date.now() - startedAt,
6160
+ stdoutBytes: stdoutBytes2,
6161
+ stderrBytes: stderrBytes2,
6162
+ timedOut: false,
6163
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
6164
+ });
6165
+ };
6088
6166
  if (typeof pid2 === "number") {
6089
6167
  registry.register({
6090
6168
  pid: pid2,
@@ -6097,7 +6175,10 @@ var bashTool = {
6097
6175
  });
6098
6176
  child2.on("close", () => registry.unregister(pid2));
6099
6177
  }
6100
- const onBgData = (chunk) => {
6178
+ const onBgData = (chunk, stream) => {
6179
+ if (stream === "stdout") stdoutBytes2 += chunk.byteLength;
6180
+ else stderrBytes2 += chunk.byteLength;
6181
+ emitProcessOutput2({ pid: pid2, stream, chunk });
6101
6182
  if (truncated) return;
6102
6183
  const remain = MAX_OUTPUT - buf2.length;
6103
6184
  if (remain > 0) {
@@ -6105,24 +6186,28 @@ var bashTool = {
6105
6186
  }
6106
6187
  if (buf2.length >= MAX_OUTPUT) {
6107
6188
  truncated = true;
6108
- child2.stdout?.off("data", onBgData);
6109
- child2.stderr?.off("data", onBgData);
6189
+ child2.stdout?.off("data", onBgStdout);
6190
+ child2.stderr?.off("data", onBgStderr);
6110
6191
  }
6111
6192
  };
6112
- child2.stdout?.on("data", onBgData);
6113
- child2.stderr?.on("data", onBgData);
6193
+ const onBgStdout = (chunk) => onBgData(chunk, "stdout");
6194
+ const onBgStderr = (chunk) => onBgData(chunk, "stderr");
6195
+ child2.stdout?.on("data", onBgStdout);
6196
+ child2.stderr?.on("data", onBgStderr);
6114
6197
  const cleanupBackground = () => {
6115
- child2.stdout?.off("data", onBgData);
6116
- child2.stderr?.off("data", onBgData);
6198
+ child2.stdout?.off("data", onBgStdout);
6199
+ child2.stderr?.off("data", onBgStderr);
6117
6200
  };
6118
6201
  child2.on("error", () => {
6119
6202
  cleanupBackground();
6120
6203
  if (typeof pid2 === "number") registry.unregister(pid2);
6121
6204
  registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
6205
+ completeBackground(1);
6122
6206
  });
6123
- child2.on("close", () => {
6207
+ child2.on("close", (code, signal) => {
6124
6208
  cleanupBackground();
6125
6209
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
6210
+ completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
6126
6211
  });
6127
6212
  if (typeof pid2 === "number") child2.unref();
6128
6213
  yield {
@@ -6162,6 +6247,32 @@ var bashTool = {
6162
6247
  }
6163
6248
  }
6164
6249
  const pid = child.pid;
6250
+ let stdoutBytes = 0;
6251
+ let stderrBytes = 0;
6252
+ let telemetryCompleted = false;
6253
+ emitProcessStarted2({
6254
+ ...pid !== void 0 ? { pid } : {},
6255
+ parentPid: process.pid,
6256
+ command: redactCommand(`${shell} ${args.join(" ")}`),
6257
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
6258
+ cwd: ctx.projectRoot,
6259
+ background: false,
6260
+ startedAt: new Date(startedAt).toISOString()
6261
+ });
6262
+ const completeForeground = (exitCode, signal) => {
6263
+ if (telemetryCompleted) return;
6264
+ telemetryCompleted = true;
6265
+ emitProcessCompleted2({
6266
+ ...pid !== void 0 ? { pid } : {},
6267
+ exitCode,
6268
+ ...signal ? { signal } : {},
6269
+ durationMs: Date.now() - startedAt,
6270
+ stdoutBytes,
6271
+ stderrBytes,
6272
+ timedOut,
6273
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
6274
+ });
6275
+ };
6165
6276
  if (typeof pid === "number") {
6166
6277
  registry.register({
6167
6278
  pid,
@@ -6263,8 +6374,11 @@ var bashTool = {
6263
6374
  child.stderr?.resume();
6264
6375
  }
6265
6376
  };
6266
- const onData = (chunk) => {
6377
+ const onData = (chunk, stream) => {
6267
6378
  const text = chunk.toString();
6379
+ if (stream === "stdout") stdoutBytes += chunk.byteLength;
6380
+ else stderrBytes += chunk.byteLength;
6381
+ emitProcessOutput2({ pid, stream, chunk });
6268
6382
  if (buf.length < MAX_OUTPUT) {
6269
6383
  buf += text.slice(0, MAX_OUTPUT - buf.length);
6270
6384
  }
@@ -6273,17 +6387,21 @@ var bashTool = {
6273
6387
  push({ kind: "data", text });
6274
6388
  pauseIfFlooded();
6275
6389
  };
6276
- child.stdout?.on("data", onData);
6277
- child.stderr?.on("data", onData);
6390
+ const onStdoutData = (chunk) => onData(chunk, "stdout");
6391
+ const onStderrData = (chunk) => onData(chunk, "stderr");
6392
+ child.stdout?.on("data", onStdoutData);
6393
+ child.stderr?.on("data", onStderrData);
6278
6394
  child.on("error", (err) => {
6279
6395
  for (const t of timers) clearTimeout(t);
6280
6396
  registry.afterCall(Date.now() - startedAt, true);
6397
+ completeForeground(1);
6281
6398
  push({ kind: "error", err });
6282
6399
  });
6283
- child.on("close", (code) => {
6400
+ child.on("close", (code, signal) => {
6284
6401
  for (const t of timers) clearTimeout(t);
6285
6402
  if (typeof pid === "number") registry.unregister(pid);
6286
6403
  registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
6404
+ completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
6287
6405
  push({ kind: "end", code });
6288
6406
  });
6289
6407
  try {
@@ -6328,8 +6446,8 @@ ${hint}` : ""),
6328
6446
  for (const t of timers) clearTimeout(t);
6329
6447
  spool.finalize();
6330
6448
  if (isWin4) opts.signal.removeEventListener("abort", onAbort);
6331
- child.stdout?.off("data", onData);
6332
- child.stderr?.off("data", onData);
6449
+ child.stdout?.off("data", onStdoutData);
6450
+ child.stderr?.off("data", onStderrData);
6333
6451
  child.stdout?.destroy();
6334
6452
  child.stderr?.destroy();
6335
6453
  if (child.exitCode === null && !child.killed) {
@@ -7825,7 +7943,7 @@ import * as path13 from "node:path";
7825
7943
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
7826
7944
 
7827
7945
  // src/codebase-index/schema.ts
7828
- var SCHEMA_VERSION = 2;
7946
+ var SCHEMA_VERSION = 3;
7829
7947
 
7830
7948
  // src/codebase-index/lsp-kind.ts
7831
7949
  function lspKindToInternalKind(k) {
@@ -7868,7 +7986,7 @@ function tokenise(text) {
7868
7986
  return sanitised.toLowerCase().split(" ").filter(Boolean);
7869
7987
  }
7870
7988
  function splitName(name) {
7871
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
7989
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
7872
7990
  }
7873
7991
  function buildIndexableText(name, signature, docComment) {
7874
7992
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -7894,14 +8012,12 @@ function buildBm25Index(docs) {
7894
8012
  return new Bm25Index(documents, df, N, avgLen);
7895
8013
  }
7896
8014
  var Bm25Index = class {
7897
- constructor(documents, df, N, avgLen) {
8015
+ constructor(documents, _df, N, avgLen) {
7898
8016
  this.documents = documents;
7899
- this.df = df;
7900
8017
  this.N = N;
7901
8018
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
7902
8019
  }
7903
8020
  documents;
7904
- df;
7905
8021
  N;
7906
8022
  safeAvgLen;
7907
8023
  score(query, filter) {
@@ -7914,10 +8030,13 @@ var Bm25Index = class {
7914
8030
  for (const qTerm of qTokens) {
7915
8031
  let tf = 0;
7916
8032
  for (const t of doc.tokens) {
7917
- if (t === qTerm) tf++;
8033
+ if (t.startsWith(qTerm)) tf++;
7918
8034
  }
7919
8035
  if (tf === 0) continue;
7920
- const dfVal = this.df[qTerm] ?? 0;
8036
+ let dfVal = 0;
8037
+ for (const candidate of this.documents) {
8038
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
8039
+ }
7921
8040
  if (dfVal === 0) continue;
7922
8041
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
7923
8042
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -7950,6 +8069,29 @@ var Bm25Index = class {
7950
8069
 
7951
8070
  // src/codebase-index/writer.ts
7952
8071
  var DB_FILE = "index.db";
8072
+ function escapeLike(value) {
8073
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
8074
+ }
8075
+ function assignRefsToSymbols(refs, symbols) {
8076
+ if (refs.length === 0 || symbols.length === 0) return [];
8077
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
8078
+ const seen = /* @__PURE__ */ new Set();
8079
+ const assigned = [];
8080
+ for (const ref of refs) {
8081
+ let owner2;
8082
+ for (const symbol of ordered) {
8083
+ if (symbol.line > ref.line) break;
8084
+ owner2 = symbol;
8085
+ }
8086
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
8087
+ if (!owner2 || owner2.id <= 0) continue;
8088
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
8089
+ if (seen.has(key)) continue;
8090
+ seen.add(key);
8091
+ assigned.push({ ...ref, fromId: owner2.id });
8092
+ }
8093
+ return assigned;
8094
+ }
7953
8095
  function resolveIndexDir(projectRoot, override) {
7954
8096
  return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
7955
8097
  }
@@ -8003,7 +8145,7 @@ function sleepSync(ms) {
8003
8145
  } catch {
8004
8146
  }
8005
8147
  }
8006
- var IndexStore = class {
8148
+ var IndexStore = class _IndexStore {
8007
8149
  db;
8008
8150
  /** Absolute path to this project's index directory. */
8009
8151
  indexDir;
@@ -8055,10 +8197,7 @@ var IndexStore = class {
8055
8197
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
8056
8198
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
8057
8199
  }
8058
- const delay = Math.min(
8059
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
8060
- LOCK_RETRY_MAX_DELAY_MS
8061
- );
8200
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
8062
8201
  sleepSync(delay);
8063
8202
  }
8064
8203
  }
@@ -8140,8 +8279,24 @@ var IndexStore = class {
8140
8279
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
8141
8280
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
8142
8281
  try {
8143
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
8282
+ this.db.exec(
8283
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
8284
+ );
8144
8285
  this.ftsAvailable = true;
8286
+ const symbolCount = Number(
8287
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8288
+ );
8289
+ const ftsCount = Number(
8290
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8291
+ );
8292
+ if (symbolCount !== ftsCount) {
8293
+ this.db.exec("DELETE FROM symbols_fts");
8294
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
8295
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8296
+ for (const row of rows) {
8297
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
8298
+ }
8299
+ }
8145
8300
  } catch {
8146
8301
  this.ftsAvailable = false;
8147
8302
  }
@@ -8199,7 +8354,9 @@ var IndexStore = class {
8199
8354
  deleteSymbolsForFile(file) {
8200
8355
  this.runWithRetry(() => {
8201
8356
  if (this.ftsAvailable) {
8202
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8357
+ this.stmt(
8358
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8359
+ ).run(file);
8203
8360
  }
8204
8361
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
8205
8362
  });
@@ -8211,9 +8368,21 @@ var IndexStore = class {
8211
8368
  */
8212
8369
  deleteFile(file) {
8213
8370
  this.runWithRetry(() => {
8214
- this.deleteRefsForFile(file);
8215
- this.deleteSymbolsForFile(file);
8216
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
8371
+ this.db.exec("BEGIN IMMEDIATE");
8372
+ try {
8373
+ if (this.ftsAvailable) {
8374
+ this.db.prepare(
8375
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8376
+ ).run(file);
8377
+ }
8378
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8379
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
8380
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
8381
+ this.db.exec("COMMIT");
8382
+ } catch (err) {
8383
+ this.db.exec("ROLLBACK");
8384
+ throw err;
8385
+ }
8217
8386
  });
8218
8387
  }
8219
8388
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -8236,14 +8405,22 @@ var IndexStore = class {
8236
8405
  ).all(file);
8237
8406
  if (!rows.length) return null;
8238
8407
  const r = expectDefined2(rows[0]);
8239
- return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
8408
+ return {
8409
+ file: r.file,
8410
+ lang: r.lang,
8411
+ mtimeMs: r.mtime_ms,
8412
+ symbolCount: r.symbol_count,
8413
+ lastIndexed: r.last_indexed
8414
+ };
8240
8415
  }
8241
8416
  getAllFileMetas() {
8242
- return this.db.prepare(
8243
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
8244
- ).all().map(
8245
- (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
8246
- );
8417
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8418
+ file: r.file,
8419
+ lang: r.lang,
8420
+ mtimeMs: r.mtime_ms,
8421
+ symbolCount: r.symbol_count,
8422
+ lastIndexed: r.last_indexed
8423
+ }));
8247
8424
  }
8248
8425
  // ─── Search ──────────────────────────────────────────────────────────────────
8249
8426
  search(query, filter) {
@@ -8267,8 +8444,8 @@ var IndexStore = class {
8267
8444
  values.push(filter.lang);
8268
8445
  }
8269
8446
  if (filter?.file) {
8270
- conditions.push("file LIKE ?");
8271
- values.push(`%${filter.file}%`);
8447
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
8448
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8272
8449
  }
8273
8450
  if (query.trim()) {
8274
8451
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -8306,9 +8483,11 @@ var IndexStore = class {
8306
8483
  * "users", camelCase-split text makes "complex" find "complexOperation").
8307
8484
  */
8308
8485
  searchRanked(query, filter, limit) {
8486
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
8487
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
8309
8488
  const tokens = tokenise(query);
8310
8489
  if (tokens.length === 0 || !this.ftsAvailable) {
8311
- return this.searchRankedFallback(query, filter, limit);
8490
+ return this.searchRankedFallback(query, filter, safeLimit);
8312
8491
  }
8313
8492
  let effectiveKind = filter?.kind;
8314
8493
  if (filter?.lspKind !== void 0) {
@@ -8328,11 +8507,13 @@ var IndexStore = class {
8328
8507
  values.push(filter.lang);
8329
8508
  }
8330
8509
  if (filter?.file) {
8331
- conditions.push("s.file LIKE ?");
8332
- values.push(`%${filter.file}%`);
8510
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
8511
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8333
8512
  }
8334
8513
  const where = conditions.join(" AND ");
8335
- const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
8514
+ const countRows = this.db.prepare(
8515
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8516
+ ).all(...values);
8336
8517
  const total = countRows[0] ? Number(countRows[0].n) : 0;
8337
8518
  if (total === 0) return { results: [], total: 0 };
8338
8519
  const rows = this.db.prepare(
@@ -8341,9 +8522,13 @@ var IndexStore = class {
8341
8522
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
8342
8523
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
8343
8524
  WHERE ${where}
8344
- ORDER BY bm25(symbols_fts)
8525
+ ORDER BY
8526
+ CASE WHEN lower(s.name) = lower(?) THEN 0
8527
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
8528
+ ELSE 2 END,
8529
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
8345
8530
  LIMIT ?`
8346
- ).all(...values, limit);
8531
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
8347
8532
  return {
8348
8533
  results: rows.map((r) => ({
8349
8534
  id: r.id,
@@ -8373,10 +8558,28 @@ var IndexStore = class {
8373
8558
  }
8374
8559
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8375
8560
  const bm25 = buildBm25Index(
8376
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
8561
+ candidates.map((c) => ({
8562
+ id: c.id,
8563
+ text: buildIndexableText(c.name, c.signature, c.docComment)
8564
+ }))
8377
8565
  );
8378
8566
  const scored = bm25.score(query, (id) => candidateById.has(id));
8379
- scored.sort((a, b) => b.score - a.score);
8567
+ const q = query.trim().toLowerCase();
8568
+ const rank = (id) => {
8569
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
8570
+ if (name === q) return 0;
8571
+ if (name.startsWith(q)) return 1;
8572
+ return 2;
8573
+ };
8574
+ scored.sort((a, b) => {
8575
+ const rankDiff = rank(a.id) - rank(b.id);
8576
+ if (rankDiff !== 0) return rankDiff;
8577
+ const scoreDiff = b.score - a.score;
8578
+ if (scoreDiff !== 0) return scoreDiff;
8579
+ const left = expectDefined2(candidateById.get(a.id));
8580
+ const right = expectDefined2(candidateById.get(b.id));
8581
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
8582
+ });
8380
8583
  const qTokens = tokenise(query);
8381
8584
  const results = scored.slice(0, limit).map(({ id, score }) => {
8382
8585
  const c = expectDefined2(candidateById.get(id));
@@ -8385,9 +8588,7 @@ var IndexStore = class {
8385
8588
  return { results, total: candidates.length };
8386
8589
  }
8387
8590
  getAllIndexable() {
8388
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
8389
- ({ id, text }) => ({ id, text })
8390
- );
8591
+ return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8391
8592
  }
8392
8593
  /**
8393
8594
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -8403,22 +8604,16 @@ var IndexStore = class {
8403
8604
  // ─── Stats ───────────────────────────────────────────────────────────────────
8404
8605
  getStats() {
8405
8606
  const sizeBytes = this.sizeBytes();
8406
- const lastRows = this.db.prepare(
8407
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
8408
- ).all();
8607
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8409
8608
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8410
8609
  const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
8411
8610
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
8412
8611
  const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
8413
8612
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
8414
- const langRows = this.db.prepare(
8415
- "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
8416
- ).all();
8613
+ const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8417
8614
  const byLang = {};
8418
8615
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
8419
- const kindRows = this.db.prepare(
8420
- "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
8421
- ).all();
8616
+ const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8422
8617
  const byKind = {};
8423
8618
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
8424
8619
  return {
@@ -8434,17 +8629,31 @@ var IndexStore = class {
8434
8629
  }
8435
8630
  setLastIndexed(ts2) {
8436
8631
  this.runWithRetry(() => {
8437
- this.db.prepare(
8438
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
8439
- ).run(String(ts2));
8632
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8633
+ });
8634
+ }
8635
+ getMetadata(key) {
8636
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
8637
+ return rows[0]?.value;
8638
+ }
8639
+ setMetadata(key, value) {
8640
+ this.runWithRetry(() => {
8641
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8440
8642
  });
8441
8643
  }
8442
8644
  clearAll() {
8443
8645
  this.runWithRetry(() => {
8444
- this.db.exec("DELETE FROM symbols");
8445
- this.db.exec("DELETE FROM files");
8446
- this.db.exec("DELETE FROM refs");
8447
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8646
+ this.db.exec("BEGIN IMMEDIATE");
8647
+ try {
8648
+ this.db.exec("DELETE FROM refs");
8649
+ this.db.exec("DELETE FROM symbols");
8650
+ this.db.exec("DELETE FROM files");
8651
+ if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8652
+ this.db.exec("COMMIT");
8653
+ } catch (err) {
8654
+ this.db.exec("ROLLBACK");
8655
+ throw err;
8656
+ }
8448
8657
  });
8449
8658
  }
8450
8659
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -8535,6 +8744,7 @@ var IndexStore = class {
8535
8744
  const allInserted = [];
8536
8745
  const refsToInsert = [];
8537
8746
  for (const entry of entries) {
8747
+ const insertedForEntry = [];
8538
8748
  for (const s of entry.symbols) {
8539
8749
  const id = nextId++;
8540
8750
  symStmt.run(
@@ -8552,9 +8762,11 @@ var IndexStore = class {
8552
8762
  s.file
8553
8763
  );
8554
8764
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8555
- allInserted.push({ ...s, id });
8765
+ const inserted = { ...s, id };
8766
+ allInserted.push(inserted);
8767
+ insertedForEntry.push(inserted);
8556
8768
  }
8557
- for (const r of entry.refs) refsToInsert.push(r);
8769
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8558
8770
  }
8559
8771
  if (refsToInsert.length > 0) {
8560
8772
  const refStmt = this.db.prepare(
@@ -8592,9 +8804,7 @@ var IndexStore = class {
8592
8804
  */
8593
8805
  deleteRefsForFile(file) {
8594
8806
  this.runWithRetry(() => {
8595
- this.db.prepare(
8596
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
8597
- ).run(file);
8807
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8598
8808
  });
8599
8809
  }
8600
8810
  /**
@@ -8635,9 +8845,7 @@ var IndexStore = class {
8635
8845
  * Find all references FROM a given symbol (what does this symbol call/use?).
8636
8846
  */
8637
8847
  findRefsFrom(symbolId) {
8638
- return this.db.prepare(
8639
- "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
8640
- ).all(symbolId).map((r) => ({
8848
+ return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
8641
8849
  id: r.id,
8642
8850
  fromId: r.from_id,
8643
8851
  toName: r.to_name,
@@ -8654,6 +8862,325 @@ var IndexStore = class {
8654
8862
  return 0;
8655
8863
  }
8656
8864
  }
8865
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
8866
+ /**
8867
+ * Derive a monorepo package name from an absolute file path.
8868
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
8869
+ * Returns `undefined` for files outside any recognisable package structure.
8870
+ */
8871
+ static derivePackage(filePath) {
8872
+ const f = filePath.replace(/\\/g, "/");
8873
+ const pkgsIdx = f.indexOf("/packages/");
8874
+ if (pkgsIdx !== -1) {
8875
+ const rest = f.slice(pkgsIdx + "/packages/".length);
8876
+ const seg = rest.split("/")[0];
8877
+ return seg ? `@wrongstack/${seg}` : void 0;
8878
+ }
8879
+ const appsIdx = f.indexOf("/apps/");
8880
+ if (appsIdx !== -1) {
8881
+ const rest = f.slice(appsIdx + "/apps/".length);
8882
+ const seg = rest.split("/")[0];
8883
+ return seg ? `app:${seg}` : void 0;
8884
+ }
8885
+ return void 0;
8886
+ }
8887
+ static packageFromImport(moduleName) {
8888
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
8889
+ const parts = moduleName.split("/");
8890
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
8891
+ }
8892
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
8893
+ if (!moduleName.startsWith(".")) return void 0;
8894
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
8895
+ const absolute = path13.posix.normalize(
8896
+ path13.posix.join(path13.posix.dirname(normalizedFrom), moduleName)
8897
+ );
8898
+ const extension = path13.posix.extname(absolute);
8899
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
8900
+ const candidates = [
8901
+ absolute,
8902
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
8903
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(absolute, `index${ext}`)),
8904
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(base, `index${ext}`))
8905
+ ];
8906
+ const indexedByPortablePath = new Map(
8907
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
8908
+ );
8909
+ for (const candidate of candidates) {
8910
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
8911
+ if (indexed) return indexed;
8912
+ }
8913
+ return void 0;
8914
+ }
8915
+ /**
8916
+ * Package-level graph: each workspace package is a node; edges are derived
8917
+ * from cross-package symbol references (a symbol in package A references a
8918
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
8919
+ */
8920
+ getPackageGraph() {
8921
+ const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8922
+ const pkgNodes = /* @__PURE__ */ new Map();
8923
+ const fileToPkg = /* @__PURE__ */ new Map();
8924
+ const symbolToPkg = /* @__PURE__ */ new Map();
8925
+ for (const s of symbols) {
8926
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
8927
+ fileToPkg.set(s.file, pkg);
8928
+ symbolToPkg.set(s.id, pkg);
8929
+ const node = pkgNodes.get(pkg);
8930
+ if (node) {
8931
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
8932
+ } else {
8933
+ pkgNodes.set(pkg, {
8934
+ id: `pkg:${pkg}`,
8935
+ label: pkg,
8936
+ kind: "package",
8937
+ package: pkg,
8938
+ symbolCount: 1,
8939
+ fileCount: 0
8940
+ });
8941
+ }
8942
+ }
8943
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
8944
+ for (const { file } of files) {
8945
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
8946
+ const node = pkgNodes.get(pkg);
8947
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
8948
+ }
8949
+ const refRows = this.db.prepare(
8950
+ `SELECT r.from_id, r.to_id, r.call_type
8951
+ FROM refs r
8952
+ WHERE r.to_id IS NOT NULL`
8953
+ ).all();
8954
+ const edgeMap = /* @__PURE__ */ new Map();
8955
+ for (const r of refRows) {
8956
+ if (r.call_type === "import") continue;
8957
+ const fromPkg = symbolToPkg.get(r.from_id);
8958
+ const toPkg = symbolToPkg.get(r.to_id);
8959
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
8960
+ const key = `${fromPkg}\0${toPkg}`;
8961
+ let e = edgeMap.get(key);
8962
+ if (!e) {
8963
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8964
+ edgeMap.set(key, e);
8965
+ }
8966
+ e.weight++;
8967
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8968
+ }
8969
+ const importRows = this.db.prepare(
8970
+ `SELECT r.from_id, r.to_name
8971
+ FROM refs r
8972
+ WHERE r.call_type = 'import'`
8973
+ ).all();
8974
+ for (const r of importRows) {
8975
+ const fromPkg = symbolToPkg.get(r.from_id);
8976
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
8977
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
8978
+ const key = `${fromPkg}\0${toPkg}`;
8979
+ let edge = edgeMap.get(key);
8980
+ if (!edge) {
8981
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8982
+ edgeMap.set(key, edge);
8983
+ }
8984
+ edge.weight++;
8985
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
8986
+ }
8987
+ const edges = [];
8988
+ for (const [key, e] of edgeMap) {
8989
+ const [source, target] = key.split("\0");
8990
+ let bestType = "call";
8991
+ let bestCount = 0;
8992
+ for (const [t, c] of e.types) {
8993
+ if (c > bestCount) {
8994
+ bestType = t;
8995
+ bestCount = c;
8996
+ }
8997
+ }
8998
+ edges.push({
8999
+ source: `pkg:${source}`,
9000
+ target: `pkg:${target}`,
9001
+ weight: e.weight,
9002
+ refType: bestType
9003
+ });
9004
+ }
9005
+ return { nodes: [...pkgNodes.values()], edges };
9006
+ }
9007
+ /**
9008
+ * File-level graph for a single package: each file is a node; edges are
9009
+ * derived from cross-file symbol references within the package.
9010
+ */
9011
+ getFileGraph(packageFilter) {
9012
+ const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
9013
+ const pkgSyms = allSymbols.filter(
9014
+ (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
9015
+ );
9016
+ if (pkgSyms.length === 0) return { nodes: [], edges: [] };
9017
+ const fileNodes = /* @__PURE__ */ new Map();
9018
+ const symToFile = /* @__PURE__ */ new Map();
9019
+ const fileStats = /* @__PURE__ */ new Map();
9020
+ for (const s of allSymbols) {
9021
+ symToFile.set(s.id, s.file);
9022
+ const current = fileStats.get(s.file);
9023
+ fileStats.set(s.file, {
9024
+ count: (current?.count ?? 0) + 1,
9025
+ lang: current?.lang ?? s.lang
9026
+ });
9027
+ }
9028
+ const localFiles = new Set(pkgSyms.map((s) => s.file));
9029
+ const indexedFiles = new Set(allSymbols.map((s) => s.file));
9030
+ const ensureFileNode = (file) => {
9031
+ if (fileNodes.has(file)) return;
9032
+ const stats = fileStats.get(file);
9033
+ fileNodes.set(file, {
9034
+ id: `file:${file}`,
9035
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
9036
+ kind: "file",
9037
+ package: _IndexStore.derivePackage(file) ?? "(root)",
9038
+ file,
9039
+ symbolCount: stats?.count ?? 0,
9040
+ lang: stats?.lang,
9041
+ external: !localFiles.has(file)
9042
+ });
9043
+ };
9044
+ for (const file of localFiles) {
9045
+ ensureFileNode(file);
9046
+ }
9047
+ const refRows = this.db.prepare(
9048
+ `SELECT r.from_id, r.to_id, r.call_type
9049
+ FROM refs r
9050
+ WHERE r.to_id IS NOT NULL`
9051
+ ).all();
9052
+ const edgeMap = /* @__PURE__ */ new Map();
9053
+ for (const r of refRows) {
9054
+ if (r.call_type === "import") continue;
9055
+ const fromFile = symToFile.get(r.from_id);
9056
+ const toFile = symToFile.get(r.to_id);
9057
+ if (!fromFile || !toFile || fromFile === toFile) continue;
9058
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
9059
+ ensureFileNode(fromFile);
9060
+ ensureFileNode(toFile);
9061
+ const key = `${fromFile}\0${toFile}`;
9062
+ let e = edgeMap.get(key);
9063
+ if (!e) {
9064
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
9065
+ edgeMap.set(key, e);
9066
+ }
9067
+ e.weight++;
9068
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9069
+ }
9070
+ const importRows = this.db.prepare(
9071
+ `SELECT r.from_id, r.to_name
9072
+ FROM refs r
9073
+ WHERE r.call_type = 'import'`
9074
+ ).all();
9075
+ for (const r of importRows) {
9076
+ const fromFile = symToFile.get(r.from_id);
9077
+ if (!fromFile || !localFiles.has(fromFile)) continue;
9078
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
9079
+ if (!toFile || fromFile === toFile) continue;
9080
+ ensureFileNode(fromFile);
9081
+ ensureFileNode(toFile);
9082
+ const key = `${fromFile}\0${toFile}`;
9083
+ let edge = edgeMap.get(key);
9084
+ if (!edge) {
9085
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
9086
+ edgeMap.set(key, edge);
9087
+ }
9088
+ edge.weight++;
9089
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
9090
+ }
9091
+ const edges = [];
9092
+ for (const [key, e] of edgeMap) {
9093
+ const [source, target] = key.split("\0");
9094
+ let bestType = "call";
9095
+ let bestCount = 0;
9096
+ for (const [t, c] of e.types) {
9097
+ if (c > bestCount) {
9098
+ bestType = t;
9099
+ bestCount = c;
9100
+ }
9101
+ }
9102
+ edges.push({
9103
+ source: `file:${source}`,
9104
+ target: `file:${target}`,
9105
+ weight: e.weight,
9106
+ refType: bestType
9107
+ });
9108
+ }
9109
+ return { nodes: [...fileNodes.values()], edges };
9110
+ }
9111
+ /**
9112
+ * Symbol-level graph for a single file: each symbol is a node; edges are
9113
+ * derived from intra-file and cross-file symbol references (who calls whom).
9114
+ */
9115
+ getSymbolGraph(fileFilter) {
9116
+ const allSymbols = this.db.prepare(
9117
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
9118
+ ).all();
9119
+ const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
9120
+ if (syms.length === 0) return { nodes: [], edges: [] };
9121
+ const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
9122
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
9123
+ const toGraphNode = (s) => ({
9124
+ id: `sym:${s.id}`,
9125
+ label: s.name,
9126
+ kind: "symbol",
9127
+ symbolId: s.id,
9128
+ symbolKind: s.kind,
9129
+ file: s.file,
9130
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
9131
+ lang: s.lang,
9132
+ line: s.line,
9133
+ signature: s.signature,
9134
+ scope: s.scope,
9135
+ external: s.file !== fileFilter
9136
+ });
9137
+ const symIds = new Set(syms.map((s) => s.id));
9138
+ const refRows = this.db.prepare(
9139
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9140
+ FROM refs r
9141
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
9142
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
9143
+ ).all(fileFilter, fileFilter);
9144
+ const edgeMap = /* @__PURE__ */ new Map();
9145
+ for (const r of refRows) {
9146
+ if (r.to_id === null) continue;
9147
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
9148
+ relatedIds.add(r.from_id);
9149
+ relatedIds.add(r.to_id);
9150
+ const key = `${r.from_id}\0${r.to_id}`;
9151
+ let e = edgeMap.get(key);
9152
+ if (!e) {
9153
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
9154
+ edgeMap.set(key, e);
9155
+ }
9156
+ e.weight++;
9157
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9158
+ }
9159
+ const edges = [];
9160
+ for (const [key, e] of edgeMap) {
9161
+ const [fromId, toId] = key.split("\0").map(Number);
9162
+ let bestType = "call";
9163
+ let bestCount = 0;
9164
+ for (const [t, c] of e.types) {
9165
+ if (c > bestCount) {
9166
+ bestType = t;
9167
+ bestCount = c;
9168
+ }
9169
+ }
9170
+ edges.push({
9171
+ source: `sym:${fromId}`,
9172
+ target: `sym:${toId}`,
9173
+ weight: e.weight,
9174
+ refType: bestType
9175
+ });
9176
+ }
9177
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
9178
+ const aExternal = a.file === fileFilter ? 0 : 1;
9179
+ const bExternal = b.file === fileFilter ? 0 : 1;
9180
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
9181
+ }).map(toGraphNode);
9182
+ return { nodes, edges };
9183
+ }
8657
9184
  close() {
8658
9185
  this.stmtCache.clear();
8659
9186
  try {
@@ -8748,6 +9275,14 @@ function getJsDoc(node, sourceFile) {
8748
9275
  }
8749
9276
  return "";
8750
9277
  }
9278
+ function hasFunctionLikeAncestor(node) {
9279
+ let current = node.parent;
9280
+ while (current) {
9281
+ if (ts.isFunctionLike(current)) return true;
9282
+ current = current.parent;
9283
+ }
9284
+ return false;
9285
+ }
8751
9286
  function buildScope(node) {
8752
9287
  const parts = [];
8753
9288
  let current = node.parent;
@@ -8775,6 +9310,10 @@ function parseSymbols(opts) {
8775
9310
  function visit(node) {
8776
9311
  const kind = kindOf(node);
8777
9312
  if (kind) {
9313
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
9314
+ ts.forEachChild(node, visit);
9315
+ return;
9316
+ }
8778
9317
  const nameNode = node.name;
8779
9318
  if (!nameNode || !ts.isIdentifier(nameNode)) return;
8780
9319
  const name = nameNode.text;
@@ -9719,9 +10258,9 @@ function parseSymbols5(opts) {
9719
10258
  function regexParse2(opts) {
9720
10259
  const { file, content, lang } = opts;
9721
10260
  const symbols = [];
9722
- const basename9 = path17.basename(file).toLowerCase();
9723
- const isPackageJson = basename9 === "package.json";
9724
- const isTsconfig = basename9 === "tsconfig.json" || basename9 === "tsconfig.build.json";
10261
+ const basename10 = path17.basename(file).toLowerCase();
10262
+ const isPackageJson = basename10 === "package.json";
10263
+ const isTsconfig = basename10 === "tsconfig.json" || basename10 === "tsconfig.build.json";
9725
10264
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
9726
10265
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
9727
10266
  const lines = content.split("\n");
@@ -10123,9 +10662,7 @@ function yieldEventLoop() {
10123
10662
  function throwIfAborted(signal) {
10124
10663
  if (!signal?.aborted) return;
10125
10664
  if (signal.reason instanceof Error) throw signal.reason;
10126
- throw new Error(
10127
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
10128
- );
10665
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
10129
10666
  }
10130
10667
  function isAbortError(err) {
10131
10668
  return err instanceof DOMException && err.name === "AbortError";
@@ -10141,8 +10678,20 @@ var DEFAULT_IGNORE = [
10141
10678
  "__snapshots__",
10142
10679
  ".nyc_output"
10143
10680
  ];
10681
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10682
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10683
+ function isWithinProject(projectRoot, file) {
10684
+ const rel = path19.relative(projectRoot, file);
10685
+ return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
10686
+ }
10687
+ function isMissingPathError(err) {
10688
+ const code = err?.code;
10689
+ return code === "ENOENT" || code === "ENOTDIR";
10690
+ }
10144
10691
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10145
10692
  const results = [];
10693
+ const errors = [];
10694
+ let complete = true;
10146
10695
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
10147
10696
  const globs = [
10148
10697
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -10166,7 +10715,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10166
10715
  let entries;
10167
10716
  try {
10168
10717
  entries = await fs14.readdir(dir, { withFileTypes: true });
10169
- } catch {
10718
+ } catch (err) {
10719
+ complete = false;
10720
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
10170
10721
  return;
10171
10722
  }
10172
10723
  dirCount++;
@@ -10178,8 +10729,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10178
10729
  if (isGitIgnored(rel, true)) continue;
10179
10730
  await walk2(full);
10180
10731
  } else if (e.isFile()) {
10181
- if (isGitIgnored(rel, false)) continue;
10182
- const ext = path19.extname(e.name);
10732
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
10733
+ const ext = path19.extname(e.name).toLowerCase();
10183
10734
  for (const { ext: extName, pat } of globs) {
10184
10735
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
10185
10736
  results.push(full);
@@ -10190,7 +10741,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10190
10741
  }
10191
10742
  };
10192
10743
  await walk2(projectRoot);
10193
- return results;
10744
+ return { files: results, complete, errors };
10194
10745
  }
10195
10746
  async function parseFile(file, content, lang) {
10196
10747
  switch (lang) {
@@ -10213,6 +10764,26 @@ async function parseFile(file, content, lang) {
10213
10764
  return { file, lang, symbols: [], mtimeMs: Date.now() };
10214
10765
  }
10215
10766
  }
10767
+ function assignRefsToSymbols2(refs, symbols) {
10768
+ if (refs.length === 0 || symbols.length === 0) return [];
10769
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10770
+ const seen = /* @__PURE__ */ new Set();
10771
+ const assigned = [];
10772
+ for (const ref of refs) {
10773
+ let owner2;
10774
+ for (const symbol of ordered) {
10775
+ if (symbol.line > ref.line) break;
10776
+ owner2 = symbol;
10777
+ }
10778
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10779
+ if (!owner2 || owner2.id <= 0) continue;
10780
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10781
+ if (seen.has(key)) continue;
10782
+ seen.add(key);
10783
+ assigned.push({ ...ref, fromId: owner2.id });
10784
+ }
10785
+ return assigned;
10786
+ }
10216
10787
  async function runIndexer(_ctx, opts) {
10217
10788
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
10218
10789
  try {
@@ -10225,7 +10796,9 @@ async function runIndexer(_ctx, opts) {
10225
10796
  }
10226
10797
  }
10227
10798
  async function runIndexerWithStore(store, opts) {
10228
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
10799
+ const { projectRoot, langs, ignore = [], signal } = opts;
10800
+ const relationGraphVersion = "2";
10801
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
10229
10802
  const startMs = Date.now();
10230
10803
  const errors = [];
10231
10804
  const langStats = {};
@@ -10234,10 +10807,18 @@ async function runIndexerWithStore(store, opts) {
10234
10807
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
10235
10808
  let files;
10236
10809
  let discoveredFiles = null;
10810
+ let discoveryComplete = true;
10237
10811
  if (opts.files && opts.files.length > 0) {
10238
- files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
10812
+ files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
10813
+ if (!isWithinProject(projectRoot, f)) return false;
10814
+ const rel = path19.relative(projectRoot, f).replace(/\\/g, "/");
10815
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path19.basename(f)) && !isGitIgnored(rel, false);
10816
+ });
10239
10817
  } else {
10240
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10818
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10819
+ files = discovery.files;
10820
+ errors.push(...discovery.errors);
10821
+ discoveryComplete = discovery.complete;
10241
10822
  discoveredFiles = new Set(files);
10242
10823
  }
10243
10824
  if (langs && langs.length > 0) {
@@ -10262,36 +10843,66 @@ async function runIndexerWithStore(store, opts) {
10262
10843
  }
10263
10844
  const statOpts = signal ? { signal } : {};
10264
10845
  const statReadParse = await Promise.allSettled(
10265
- batchFiles.map(async (file) => {
10266
- let stat18;
10267
- try {
10268
- stat18 = await fs14.stat(file, statOpts);
10269
- } catch (e) {
10270
- if (isAbortError(e)) throw e;
10271
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
10272
- }
10273
- if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
10274
- const lang = detectLang(file);
10275
- if (!lang) return { file, stat: stat18, lang: "", parsed: null };
10276
- const meta = existingMeta.get(file);
10277
- if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
10278
- return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
10279
- }
10280
- let content;
10281
- try {
10282
- content = await fs14.readFile(file, { encoding: "utf8", signal });
10283
- } catch (e) {
10284
- if (isAbortError(e)) throw e;
10285
- return { file, stat: stat18, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
10286
- }
10287
- let parsed;
10288
- try {
10289
- parsed = await parseFile(file, content, lang);
10290
- } catch (e) {
10291
- return { file, stat: stat18, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
10846
+ batchFiles.map(
10847
+ async (file) => {
10848
+ let stat18;
10849
+ try {
10850
+ stat18 = await fs14.stat(file, statOpts);
10851
+ } catch (e) {
10852
+ if (isAbortError(e)) throw e;
10853
+ return {
10854
+ file,
10855
+ stat: null,
10856
+ lang: "",
10857
+ parsed: null,
10858
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
10859
+ missing: isMissingPathError(e)
10860
+ };
10861
+ }
10862
+ if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
10863
+ const lang = detectLang(file);
10864
+ if (!lang) return { file, stat: stat18, lang: "", parsed: null };
10865
+ if (stat18.size > MAX_INDEX_FILE_BYTES) {
10866
+ return {
10867
+ file,
10868
+ stat: stat18,
10869
+ lang,
10870
+ parsed: null,
10871
+ error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
10872
+ };
10873
+ }
10874
+ const meta = existingMeta.get(file);
10875
+ if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
10876
+ return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
10877
+ }
10878
+ let content;
10879
+ try {
10880
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
10881
+ } catch (e) {
10882
+ if (isAbortError(e)) throw e;
10883
+ return {
10884
+ file,
10885
+ stat: stat18,
10886
+ lang,
10887
+ parsed: null,
10888
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
10889
+ };
10890
+ }
10891
+ let parsed;
10892
+ try {
10893
+ parsed = await parseFile(file, content, lang);
10894
+ } catch (e) {
10895
+ return {
10896
+ file,
10897
+ stat: stat18,
10898
+ lang,
10899
+ parsed: null,
10900
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
10901
+ };
10902
+ }
10903
+ return { file, stat: stat18, lang, parsed, content };
10292
10904
  }
10293
- return { file, stat: stat18, lang, parsed, content };
10294
- })
10905
+ )
10295
10906
  );
10296
10907
  const batchEntries = [];
10297
10908
  const deleteForFiles = [];
@@ -10306,8 +10917,8 @@ async function runIndexerWithStore(store, opts) {
10306
10917
  }
10307
10918
  const result = settled.value;
10308
10919
  if (result.error) {
10309
- if (result.stat) store.deleteFile(file);
10310
- if (result.error.includes("error:")) errors.push(result.error);
10920
+ if (result.missing) store.deleteFile(file);
10921
+ errors.push(`${file}: ${result.error}`);
10311
10922
  continue;
10312
10923
  }
10313
10924
  const { stat: stat18, lang, parsed } = result;
@@ -10331,6 +10942,8 @@ async function runIndexerWithStore(store, opts) {
10331
10942
  continue;
10332
10943
  }
10333
10944
  if (parsed.symbols.length === 0) {
10945
+ store.deleteRefsForFile(file);
10946
+ store.deleteSymbolsForFile(file);
10334
10947
  store.upsertFile({
10335
10948
  file,
10336
10949
  lang,
@@ -10341,15 +10954,11 @@ async function runIndexerWithStore(store, opts) {
10341
10954
  filesIndexed++;
10342
10955
  continue;
10343
10956
  }
10344
- const refs = [];
10345
- if (parsed.refs && parsed.refs.length > 0) {
10346
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
10347
- }
10348
10957
  batchEntries.push({
10349
10958
  file,
10350
10959
  lang,
10351
10960
  symbols: parsed.symbols,
10352
- refs,
10961
+ refs: parsed.refs ?? [],
10353
10962
  mtimeMs: Math.floor(stat18.mtimeMs),
10354
10963
  symbolCount: parsed.symbols.length
10355
10964
  });
@@ -10357,34 +10966,12 @@ async function runIndexerWithStore(store, opts) {
10357
10966
  }
10358
10967
  if (batchEntries.length > 0) {
10359
10968
  try {
10360
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
10361
- let cursor = 0;
10969
+ store.commitBatch(batchEntries, { deleteForFiles });
10362
10970
  for (const entry of batchEntries) {
10363
10971
  const count = entry.symbols.length;
10364
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
10365
- cursor += count;
10366
10972
  symbolsIndexed += count;
10367
10973
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
10368
10974
  filesIndexed++;
10369
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
10370
- const refsByLine = /* @__PURE__ */ new Map();
10371
- for (let i = 0; i < symbolsWithIds.length; i++) {
10372
- const sym = symbolsWithIds[i];
10373
- let arr = refsByLine.get(sym.line);
10374
- if (!arr) {
10375
- arr = [];
10376
- refsByLine.set(sym.line, arr);
10377
- }
10378
- arr.push(i);
10379
- }
10380
- for (const ref of entry.refs) {
10381
- const indices = refsByLine.get(ref.line);
10382
- if (indices && indices.length > 0) {
10383
- const idx = indices.shift();
10384
- ref.fromId = symbolsWithIds[idx].id;
10385
- }
10386
- }
10387
- }
10388
10975
  }
10389
10976
  } catch (err) {
10390
10977
  const message = err instanceof Error ? err.message : String(err);
@@ -10398,23 +10985,7 @@ async function runIndexerWithStore(store, opts) {
10398
10985
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
10399
10986
  filesIndexed++;
10400
10987
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
10401
- const refsByLine = /* @__PURE__ */ new Map();
10402
- for (const sym of symbolsWithIds) {
10403
- let arr = refsByLine.get(sym.line);
10404
- if (!arr) {
10405
- arr = [];
10406
- refsByLine.set(sym.line, arr);
10407
- }
10408
- arr.push(sym);
10409
- }
10410
- const fallbackBatch = [];
10411
- for (const ref of entry.refs) {
10412
- const syms = refsByLine.get(ref.line);
10413
- if (syms && syms.length > 0) {
10414
- const sym = syms.shift();
10415
- fallbackBatch.push({ ...ref, fromId: sym.id });
10416
- }
10417
- }
10988
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
10418
10989
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
10419
10990
  }
10420
10991
  store.upsertFile({
@@ -10425,19 +10996,23 @@ async function runIndexerWithStore(store, opts) {
10425
10996
  lastIndexed: Date.now()
10426
10997
  });
10427
10998
  } catch (innerErr) {
10428
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
10999
+ errors.push(
11000
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
11001
+ );
10429
11002
  }
10430
11003
  }
10431
11004
  }
10432
11005
  }
10433
11006
  }
10434
- if (discoveredFiles) {
11007
+ if (discoveredFiles && discoveryComplete) {
10435
11008
  for (const [file_] of existingMeta) {
10436
11009
  if (!discoveredFiles.has(file_)) {
10437
11010
  store.deleteFile(file_);
10438
11011
  }
10439
11012
  }
10440
11013
  }
11014
+ store.resolveRefs();
11015
+ store.setMetadata("relation_graph_version", relationGraphVersion);
10441
11016
  const durationMs = Date.now() - startMs;
10442
11017
  store.setLastIndexed(Date.now());
10443
11018
  return {
@@ -10497,11 +11072,35 @@ function statsService(args) {
10497
11072
  store.close();
10498
11073
  }
10499
11074
  }
11075
+ function packageGraphService(args) {
11076
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11077
+ try {
11078
+ return store.getPackageGraph();
11079
+ } finally {
11080
+ store.close();
11081
+ }
11082
+ }
11083
+ function fileGraphService(args) {
11084
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11085
+ try {
11086
+ return store.getFileGraph(args.packageFilter);
11087
+ } finally {
11088
+ store.close();
11089
+ }
11090
+ }
11091
+ function symbolGraphService(args) {
11092
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11093
+ try {
11094
+ return store.getSymbolGraph(args.fileFilter);
11095
+ } finally {
11096
+ store.close();
11097
+ }
11098
+ }
10500
11099
 
10501
11100
  // src/codebase-index/background-indexer.ts
10502
11101
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
10503
11102
  var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
10504
- var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
11103
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
10505
11104
  var _ready = false;
10506
11105
  var _indexing = false;
10507
11106
  var _currentFile = 0;
@@ -10579,14 +11178,22 @@ function ensureWorker() {
10579
11178
  if (!entry) return;
10580
11179
  pending.delete(msg.id);
10581
11180
  if (msg.ok) entry.resolve(msg.result);
10582
- else entry.reject(new Error(msg.error));
11181
+ else {
11182
+ const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
11183
+ if (msg.errorName && msg.errorName !== "Error") {
11184
+ error.name = msg.errorName;
11185
+ }
11186
+ entry.reject(error);
11187
+ }
10583
11188
  });
10584
11189
  w.on("error", (err) => {
11190
+ if (worker !== w) return;
10585
11191
  worker = null;
10586
11192
  failAllPending(err);
10587
11193
  });
10588
11194
  w.on("exit", () => {
10589
- if (worker === w) worker = null;
11195
+ if (worker !== w) return;
11196
+ worker = null;
10590
11197
  failAllPending(new Error("codebase-index worker exited"));
10591
11198
  });
10592
11199
  worker = w;
@@ -10611,6 +11218,11 @@ function shutdownCodebaseIndexHost() {
10611
11218
  function callIndexOp(op, args, opts) {
10612
11219
  const w = ensureWorker();
10613
11220
  if (!w) return callInline(op, args, opts);
11221
+ if (opts.signal?.aborted) {
11222
+ return Promise.reject(
11223
+ opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
11224
+ );
11225
+ }
10614
11226
  return new Promise((resolve15, reject) => {
10615
11227
  const id = nextRpcId++;
10616
11228
  const timer = setTimeout(() => {
@@ -10625,8 +11237,7 @@ function callIndexOp(op, args, opts) {
10625
11237
  const onAbort = () => {
10626
11238
  w.postMessage({ type: "cancel", id });
10627
11239
  };
10628
- if (opts.signal?.aborted) onAbort();
10629
- else opts.signal?.addEventListener("abort", onAbort, { once: true });
11240
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
10630
11241
  const cleanup = () => {
10631
11242
  clearTimeout(timer);
10632
11243
  opts.signal?.removeEventListener("abort", onAbort);
@@ -10814,7 +11425,7 @@ var codebaseIndexTool = {
10814
11425
  category: "Project",
10815
11426
  icon: "index",
10816
11427
  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.",
10817
- 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.",
11428
+ usageHint: "CREATE OR REFRESH THE SEARCH INDEX:\n\n- When `codebase-stats` or `codebase-search` reports no persisted index, call this without arguments, then retry the search.\n- Normal and first-time usage: call without arguments for an incremental build.\n- Use `force: true` only for a corrupt/stale index or an explicitly requested clean rebuild.\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.",
10818
11429
  permission: "confirm",
10819
11430
  mutating: true,
10820
11431
  capabilities: ["fs.write.outside-project"],
@@ -10870,12 +11481,14 @@ var codebaseSearchTool = {
10870
11481
  name: "codebase-search",
10871
11482
  category: "Project",
10872
11483
  icon: "index",
10873
- description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Much more powerful and structured than raw `grep` for finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
10874
- 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.",
11484
+ description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
11485
+ usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\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.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
10875
11486
  permission: "auto",
10876
11487
  mutating: false,
10877
11488
  capabilities: ["fs.read"],
10878
- timeoutMs: 1e4,
11489
+ // The index host has its own 30s read watchdog. Leave enough headroom for
11490
+ // worker teardown and structured timeout reporting.
11491
+ timeoutMs: 35e3,
10879
11492
  inputSchema: {
10880
11493
  type: "object",
10881
11494
  properties: {
@@ -10885,11 +11498,34 @@ var codebaseSearchTool = {
10885
11498
  },
10886
11499
  kind: {
10887
11500
  type: "string",
10888
- description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
11501
+ enum: [
11502
+ "class",
11503
+ "interface",
11504
+ "enum",
11505
+ "type",
11506
+ "function",
11507
+ "method",
11508
+ "var",
11509
+ "const",
11510
+ "let",
11511
+ "property",
11512
+ "parameter",
11513
+ "namespace",
11514
+ "object",
11515
+ "literal",
11516
+ "schema",
11517
+ "struct",
11518
+ "trait",
11519
+ "impl",
11520
+ "static",
11521
+ "mod"
11522
+ ],
11523
+ description: "Filter by indexed symbol kind"
10889
11524
  },
10890
11525
  lang: {
10891
11526
  type: "string",
10892
- description: "Filter by language: ts, tsx, js, jsx"
11527
+ enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
11528
+ description: "Filter by indexed language"
10893
11529
  },
10894
11530
  lspKind: {
10895
11531
  type: "integer",
@@ -10914,20 +11550,12 @@ var codebaseSearchTool = {
10914
11550
  },
10915
11551
  async execute(input, ctx, execOpts) {
10916
11552
  const state = getIndexState();
10917
- if (!state.ready) {
10918
- return {
10919
- results: [],
10920
- total: 0,
10921
- query: input.query,
10922
- indexStatus: state.indexing ? `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.` : "Index not yet built. The codebase is being indexed at startup \u2014 search will be available shortly."
10923
- };
10924
- }
10925
- if (state.indexing) {
11553
+ if (state.indexing && !state.ready) {
10926
11554
  return {
10927
11555
  results: [],
10928
11556
  total: 0,
10929
11557
  query: input.query,
10930
- indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
11558
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
10931
11559
  };
10932
11560
  }
10933
11561
  if (state.lastError) {
@@ -10940,21 +11568,37 @@ var codebaseSearchTool = {
10940
11568
  indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
10941
11569
  };
10942
11570
  }
10943
- const limit = Math.min(input.limit ?? 20, 100);
11571
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
10944
11572
  const { results, total } = await searchCodebaseIndex(
10945
11573
  {
10946
11574
  projectRoot: ctx.projectRoot,
10947
11575
  indexDir: codebaseIndexDirOverride(ctx),
10948
11576
  query: input.query,
10949
- kind: input.kind,
10950
- lang: input.lang,
11577
+ kind: input.kind?.toLowerCase(),
11578
+ lang: input.lang?.toLowerCase(),
10951
11579
  file: input.file,
10952
11580
  lspKind: input.lspKind,
10953
11581
  limit
10954
11582
  },
10955
11583
  { signal: execOpts?.signal }
10956
11584
  );
10957
- return { results, total, query: input.query };
11585
+ let hasPersistedIndex = state.ready || total > 0;
11586
+ if (!hasPersistedIndex) {
11587
+ try {
11588
+ const stats = await codebaseIndexStats(
11589
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11590
+ { signal: execOpts?.signal }
11591
+ );
11592
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11593
+ } catch {
11594
+ }
11595
+ }
11596
+ return {
11597
+ results,
11598
+ total,
11599
+ query: input.query,
11600
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
11601
+ };
10958
11602
  }
10959
11603
  };
10960
11604
 
@@ -10963,12 +11607,15 @@ var codebaseStatsTool = {
10963
11607
  name: "codebase-stats",
10964
11608
  category: "Project",
10965
11609
  icon: "index",
10966
- 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.",
10967
- 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.",
11610
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
11611
+ usageHint: "CHECK ONCE BEFORE BROAD CODE EXPLORATION:\n\n- Call before broad `tree`, `glob`, or `grep` exploration to see whether index-backed search is available.\n- If it reports no persisted data, run `codebase-index`, then use `codebase-search`.\n- No arguments required.\nLightweight and safe to call frequently.",
10968
11612
  permission: "auto",
10969
11613
  mutating: false,
10970
11614
  capabilities: ["fs.read"],
10971
- timeoutMs: 5e3,
11615
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
11616
+ // longer so the host can return a structured timeout instead of being
11617
+ // pre-empted by the generic tool executor.
11618
+ timeoutMs: 35e3,
10972
11619
  inputSchema: {
10973
11620
  type: "object",
10974
11621
  properties: {},
@@ -10976,7 +11623,8 @@ var codebaseStatsTool = {
10976
11623
  },
10977
11624
  async execute(_input, ctx, execOpts) {
10978
11625
  const idxState = getIndexState();
10979
- if (!idxState.ready) {
11626
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
11627
+ if (idxState.indexing) {
10980
11628
  return {
10981
11629
  totalSymbols: 0,
10982
11630
  totalFiles: 0,
@@ -10984,22 +11632,47 @@ var codebaseStatsTool = {
10984
11632
  byKind: {},
10985
11633
  lastIndexed: null,
10986
11634
  sizeBytes: 0,
10987
- indexPath: "",
11635
+ indexPath,
10988
11636
  version: SCHEMA_VERSION,
10989
- indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
11637
+ statsAvailable: false,
11638
+ indexing: {
11639
+ currentFile: idxState.currentFile,
11640
+ totalFiles: idxState.totalFiles
11641
+ },
11642
+ indexStatus: idxState.ready ? `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). The persisted snapshot remains available to codebase-search; retry stats after refresh.` : `Startup indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files). Do not start another index; retry stats or codebase-search after completion.`
10990
11643
  };
10991
11644
  }
10992
- const stats = await codebaseIndexStats(
10993
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
10994
- { signal: execOpts?.signal }
10995
- );
10996
- if (idxState.indexing) {
11645
+ let stats;
11646
+ try {
11647
+ stats = await codebaseIndexStats(
11648
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11649
+ { signal: execOpts?.signal }
11650
+ );
11651
+ } catch (err) {
11652
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
11653
+ throw err;
11654
+ }
10997
11655
  return {
10998
- ...stats,
10999
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
11656
+ totalSymbols: 0,
11657
+ totalFiles: 0,
11658
+ byLang: {},
11659
+ byKind: {},
11660
+ lastIndexed: null,
11661
+ sizeBytes: 0,
11662
+ indexPath,
11663
+ version: SCHEMA_VERSION,
11664
+ statsAvailable: false,
11665
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
11000
11666
  };
11001
11667
  }
11002
11668
  const circuit = idxState.circuit;
11669
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11670
+ let indexStatus;
11671
+ if (circuit.state === "open") {
11672
+ indexStatus = `${hasPersistedIndex ? "Indexing" : "No persisted index data found, and indexing"} is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. ` + (hasPersistedIndex ? "Stats reflect the last successful build." : "Build the index after recovery.");
11673
+ } else if (!hasPersistedIndex) {
11674
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
11675
+ }
11003
11676
  return {
11004
11677
  totalSymbols: stats.totalSymbols,
11005
11678
  totalFiles: stats.totalFiles,
@@ -11009,9 +11682,8 @@ var codebaseStatsTool = {
11009
11682
  sizeBytes: stats.sizeBytes,
11010
11683
  indexPath: stats.indexPath,
11011
11684
  version: stats.version,
11012
- ...circuit.state === "open" ? {
11013
- indexStatus: `Indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. Stats reflect the last successful build.`
11014
- } : {}
11685
+ statsAvailable: true,
11686
+ ...indexStatus ? { indexStatus } : {}
11015
11687
  };
11016
11688
  }
11017
11689
  };
@@ -12530,6 +13202,11 @@ Compare this against your old_string and retry with the file's actual text.` : "
12530
13202
 
12531
13203
  // src/exec.ts
12532
13204
  import { spawn as spawn8 } from "node:child_process";
13205
+ import {
13206
+ emitProcessCompleted as emitProcessCompleted3,
13207
+ emitProcessOutput as emitProcessOutput3,
13208
+ emitProcessStarted as emitProcessStarted3
13209
+ } from "@wrongstack/core";
12533
13210
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
12534
13211
  init_output_spool();
12535
13212
  init_util();
@@ -13384,12 +14061,30 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13384
14061
  resolve15(result);
13385
14062
  };
13386
14063
  const startedAt = Date.now();
14064
+ let stdoutBytes = 0;
14065
+ let stderrBytes = 0;
14066
+ let telemetryCompleted = false;
14067
+ let timedOut = false;
13387
14068
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
13388
14069
  const resolved = resolveWin32Command(cmd);
13389
14070
  const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
13390
14071
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
13391
14072
  const spawnCmd = shim?.command ?? resolved;
13392
14073
  const spawnArgs = shim?.args ?? args;
14074
+ const emitCompletedOnce = (exitCode, pid2, signal2) => {
14075
+ if (telemetryCompleted) return;
14076
+ telemetryCompleted = true;
14077
+ emitProcessCompleted3({
14078
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
14079
+ exitCode,
14080
+ ...signal2 ? { signal: signal2 } : {},
14081
+ durationMs: Date.now() - startedAt,
14082
+ stdoutBytes,
14083
+ stderrBytes,
14084
+ timedOut,
14085
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
14086
+ });
14087
+ };
13393
14088
  let child;
13394
14089
  try {
13395
14090
  child = spawn8(spawnCmd, spawnArgs, {
@@ -13402,6 +14097,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13402
14097
  });
13403
14098
  } catch (err) {
13404
14099
  spool.finalize();
14100
+ emitProcessStarted3({
14101
+ parentPid: process.pid,
14102
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
14103
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
14104
+ cwd,
14105
+ background: false,
14106
+ startedAt: new Date(startedAt).toISOString()
14107
+ });
14108
+ emitCompletedOnce(1, void 0);
13405
14109
  finish({
13406
14110
  command: cmd,
13407
14111
  args,
@@ -13414,6 +14118,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13414
14118
  });
13415
14119
  return;
13416
14120
  }
14121
+ emitProcessStarted3({
14122
+ ...child.pid !== void 0 ? { pid: child.pid } : {},
14123
+ parentPid: process.pid,
14124
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
14125
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
14126
+ cwd,
14127
+ background: false,
14128
+ startedAt: new Date(startedAt).toISOString()
14129
+ });
13417
14130
  child.on("error", (err) => {
13418
14131
  const isAbort = err && err.code === "ABORT_ERR";
13419
14132
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
@@ -13422,6 +14135,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13422
14135
  if (typeof pid === "number") registry.unregister(pid);
13423
14136
  registry.afterCall(Date.now() - startedAt, true);
13424
14137
  spool.finalize();
14138
+ emitCompletedOnce(isAbort ? 124 : 1, child.pid, isAbort ? "ABORT" : void 0);
13425
14139
  finish({
13426
14140
  command: cmd,
13427
14141
  args,
@@ -13441,6 +14155,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13441
14155
  }
13442
14156
  const timer = setTimeout(() => {
13443
14157
  killed = true;
14158
+ timedOut = true;
13444
14159
  if (typeof pid === "number") registry.kill(pid);
13445
14160
  else child.kill("SIGTERM");
13446
14161
  }, timeout);
@@ -13455,11 +14170,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13455
14170
  }
13456
14171
  child.stdout?.on("data", (chunk) => {
13457
14172
  const text = chunk.toString();
14173
+ stdoutBytes += chunk.byteLength;
14174
+ emitProcessOutput3({ pid, stream: "stdout", chunk });
13458
14175
  if (stdout.length < MAX_OUTPUT2) stdout += text;
13459
14176
  spool.write(text);
13460
14177
  });
13461
14178
  child.stderr?.on("data", (chunk) => {
13462
14179
  const text = chunk.toString();
14180
+ stderrBytes += chunk.byteLength;
14181
+ emitProcessOutput3({ pid, stream: "stderr", chunk });
13463
14182
  if (stderr.length < MAX_OUTPUT2) stderr += text;
13464
14183
  spool.write(text);
13465
14184
  });
@@ -13469,6 +14188,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13469
14188
  if (typeof pid === "number") registry.unregister(pid);
13470
14189
  const durationMs = Date.now() - startedAt;
13471
14190
  const exitCode = killed ? 124 : code ?? 1;
14191
+ emitCompletedOnce(exitCode, pid);
13472
14192
  registry.afterCall(durationMs, exitCode !== 0);
13473
14193
  const spooled = spool.finalize();
13474
14194
  finish({
@@ -13544,7 +14264,7 @@ function guardedLookup(hostname4, options, callback) {
13544
14264
  var pinnedAgent;
13545
14265
  function getPinnedDispatcher() {
13546
14266
  if (!pinnedAgent) {
13547
- pinnedAgent = new Agent({ connect: { lookup: guardedLookup } });
14267
+ pinnedAgent = new Agent({ allowH2: false, connect: { lookup: guardedLookup } });
13548
14268
  }
13549
14269
  return pinnedAgent;
13550
14270
  }
@@ -13646,7 +14366,7 @@ async function assertNotPrivate(hostname4) {
13646
14366
  }
13647
14367
  }
13648
14368
  } catch (err) {
13649
- if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
14369
+ if (err instanceof ToolValidationError2) throw err;
13650
14370
  }
13651
14371
  }
13652
14372
  }
@@ -13994,7 +14714,7 @@ async function detectFixer(cwd) {
13994
14714
  init_util();
13995
14715
  import { spawn as spawn9 } from "node:child_process";
13996
14716
  import { statSync as statSync3 } from "node:fs";
13997
- import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
14717
+ import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
13998
14718
  import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
13999
14719
  var TIMEOUT_MS2 = 3e4;
14000
14720
  var MAX_OUTPUT3 = 1e5;
@@ -14139,7 +14859,7 @@ function validateWorktreeInput(input, projectRoot) {
14139
14859
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
14140
14860
  const root = resolve11(projectRoot);
14141
14861
  const abs = resolve11(root, input.worktreePath);
14142
- if (abs !== root && !abs.startsWith(root + sep4)) {
14862
+ if (abs !== root && !abs.startsWith(root + sep5)) {
14143
14863
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
14144
14864
  }
14145
14865
  }
@@ -14302,8 +15022,8 @@ var WALK_CONCURRENCY = 16;
14302
15022
  var globTool = {
14303
15023
  name: "glob",
14304
15024
  category: "Filesystem",
14305
- description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
14306
- usageHint: "RECOMMENDED FOR SCOPING SEARCHES:\n\n- Use early to get a list of files you actually care about.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
15025
+ description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
15026
+ usageHint: "PATH DISCOVERY AND SEARCH SCOPING:\n\n- When `codebase-search` is live, use it first for code concepts; use `glob` for filenames, path patterns, and non-indexed files.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
14307
15027
  selection: {
14308
15028
  doNotUseWhen: "you need to search inside file contents.",
14309
15029
  useInstead: ["grep"]
@@ -14491,8 +15211,8 @@ var NATIVE_MAX_FILE_BYTES = 1e6;
14491
15211
  var grepTool = {
14492
15212
  name: "grep",
14493
15213
  category: "Search",
14494
- description: "Search across files using a regular expression. This is one of the primary code search tools. Prefers ripgrep for speed and features when available.",
14495
- usageHint: 'POWERFUL CODE SEARCH TOOL:\n\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
15214
+ description: "Search exact file contents using a regular expression. Use index-backed `codebase-search` first for symbol or concept discovery when it is live. Prefers ripgrep for speed and features when available.",
15215
+ usageHint: 'EXACT-TEXT AND REGEX SEARCH:\n\n- When `codebase-search` is live, use it first for symbols and concepts; use `grep` for exact text, regexes, unsupported content, and concrete usage sites.\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
14496
15216
  selection: {
14497
15217
  doNotUseWhen: "you only need to locate files by name or path pattern.",
14498
15218
  useInstead: ["glob"]
@@ -15514,6 +16234,7 @@ function toYaml(data, indent = 0) {
15514
16234
  }
15515
16235
 
15516
16236
  // src/kanban.ts
16237
+ import { randomUUID } from "node:crypto";
15517
16238
  import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
15518
16239
  import {
15519
16240
  addCheckToTask,
@@ -15553,7 +16274,9 @@ import {
15553
16274
  setTaskChain,
15554
16275
  splitTask,
15555
16276
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
16277
+ transitionTask,
15556
16278
  transferTaskToBoard,
16279
+ touchKanbanPresence as touchKanbanPresence2,
15557
16280
  updateBoard as updateBoard2,
15558
16281
  updateCheckOnTask,
15559
16282
  updateColumn,
@@ -15564,20 +16287,24 @@ import {
15564
16287
 
15565
16288
  // src/session-kanban.ts
15566
16289
  import { watch } from "node:fs";
15567
- import { basename as basename8, dirname as dirname13 } from "node:path";
16290
+ import { basename as basename9, dirname as dirname13 } from "node:path";
15568
16291
  import {
15569
16292
  deserializeTaskGraph,
16293
+ GlobalMailbox,
15570
16294
  loadPlan,
15571
16295
  loadTasks,
15572
16296
  mutatePlan,
15573
16297
  mutateTasks
15574
16298
  } from "@wrongstack/core";
16299
+ import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
15575
16300
  import {
15576
16301
  createBoard,
15577
16302
  getBoard,
16303
+ getKanbanDir,
15578
16304
  listBoards,
15579
16305
  removeBoard,
15580
16306
  syncBoardFromTaskGraph,
16307
+ touchKanbanPresence,
15581
16308
  updateBoard
15582
16309
  } from "@wrongstack/kanban";
15583
16310
  var SESSION_BOARD_TAG = "session-work";
@@ -15844,6 +16571,42 @@ function fireAndForget(work) {
15844
16571
  void work.catch(() => {
15845
16572
  });
15846
16573
  }
16574
+ function broadcastTodoUpdate(context, todos) {
16575
+ const sessionId = context.session?.id ?? "";
16576
+ if (!context.agentId || !sessionId) return;
16577
+ const projectDir = resolveWstackPaths3({ projectRoot: context.projectRoot }).projectDir;
16578
+ const mailbox = new GlobalMailbox(projectDir);
16579
+ void mailbox.send({
16580
+ from: context.agentId,
16581
+ to: "*",
16582
+ type: "status",
16583
+ subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
16584
+ body: JSON.stringify({
16585
+ kind: "kanban.todos.updated",
16586
+ sessionId,
16587
+ revision: context.state.revision,
16588
+ todos
16589
+ }),
16590
+ priority: "normal",
16591
+ senderSessionId: sessionId,
16592
+ ttlMs: 6 * 60 * 60 * 1e3
16593
+ }).catch(() => {
16594
+ });
16595
+ }
16596
+ function notifyTodoUpdate(context, todos) {
16597
+ const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
16598
+ const text = `[KANBAN TODO UPDATE]
16599
+ Another Kanban agent reassessed the shared board. The canonical todo list is now:
16600
+ ${summary}
16601
+ Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
16602
+ const state = context.state;
16603
+ if (typeof state.appendBlockToLastUserMessage === "function") {
16604
+ if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
16605
+ }
16606
+ if (typeof state.appendMessage === "function") {
16607
+ state.appendMessage({ role: "user", content: [{ type: "text", text }] });
16608
+ }
16609
+ }
15847
16610
  function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
15848
16611
  fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));
15849
16612
  }
@@ -15875,6 +16638,10 @@ function attachSessionKanbanMirror(context) {
15875
16638
  let watcher = null;
15876
16639
  let watchedDir = "";
15877
16640
  let timer = null;
16641
+ let boardWatcher = null;
16642
+ let watchedBoardId = "";
16643
+ let boardTimer = null;
16644
+ let presenceTimer = null;
15878
16645
  const sessionId = () => context.session?.id ?? "";
15879
16646
  const refreshFiles = async () => {
15880
16647
  const id = sessionId();
@@ -15890,9 +16657,51 @@ function attachSessionKanbanMirror(context) {
15890
16657
  if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
15891
16658
  }
15892
16659
  };
15893
- const configureWatcher = () => {
15894
- const planPath = context.meta["plan.path"];
15895
- const taskPath = context.meta["task.path"];
16660
+ const refreshBoard = async () => {
16661
+ if (!watchedBoardId) return;
16662
+ const board = await getBoard(context.projectRoot, watchedBoardId);
16663
+ if (board) applySessionKanbanBoardToTodos(context, board);
16664
+ };
16665
+ const configureBoardWatcher = async () => {
16666
+ const id = sessionId();
16667
+ const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
16668
+ if (!board || board.id === watchedBoardId) return;
16669
+ boardWatcher?.close();
16670
+ boardWatcher = null;
16671
+ watchedBoardId = board.id;
16672
+ try {
16673
+ const boardFileName = `${board.id}.json`;
16674
+ boardWatcher = watch(
16675
+ getKanbanDir(context.projectRoot),
16676
+ { persistent: false },
16677
+ (_event, filename) => {
16678
+ if (filename?.toString() !== boardFileName) return;
16679
+ if (boardTimer) clearTimeout(boardTimer);
16680
+ boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);
16681
+ }
16682
+ );
16683
+ const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
16684
+ sessionId: id,
16685
+ agentId: context.agentId,
16686
+ agentName: context.agentName
16687
+ });
16688
+ fireAndForget(touchPresence());
16689
+ if (presenceTimer) clearInterval(presenceTimer);
16690
+ presenceTimer = setInterval(() => fireAndForget(touchPresence()), 6e4);
16691
+ presenceTimer.unref?.();
16692
+ boardWatcher.on("error", () => {
16693
+ boardWatcher?.close();
16694
+ boardWatcher = null;
16695
+ watchedBoardId = "";
16696
+ });
16697
+ } catch {
16698
+ boardWatcher = null;
16699
+ watchedBoardId = "";
16700
+ }
16701
+ };
16702
+ const configureWatcher = () => {
16703
+ const planPath = context.meta["plan.path"];
16704
+ const taskPath = context.meta["task.path"];
15896
16705
  const candidate = typeof planPath === "string" && planPath ? dirname13(planPath) : typeof taskPath === "string" && taskPath ? dirname13(taskPath) : "";
15897
16706
  if (!candidate || candidate === watchedDir) return;
15898
16707
  watcher?.close();
@@ -15903,8 +16712,8 @@ function attachSessionKanbanMirror(context) {
15903
16712
  const name = filename?.toString();
15904
16713
  const currentPlanPath = context.meta["plan.path"];
15905
16714
  const currentTaskPath = context.meta["task.path"];
15906
- const planName = typeof currentPlanPath === "string" ? basename8(currentPlanPath) : "";
15907
- const taskName = typeof currentTaskPath === "string" ? basename8(currentTaskPath) : "";
16715
+ const planName = typeof currentPlanPath === "string" ? basename9(currentPlanPath) : "";
16716
+ const taskName = typeof currentTaskPath === "string" ? basename9(currentTaskPath) : "";
15908
16717
  if (name && name !== planName && name !== taskName) return;
15909
16718
  if (timer) clearTimeout(timer);
15910
16719
  timer = setTimeout(() => fireAndForget(refreshFiles()), 60);
@@ -15928,14 +16737,19 @@ function attachSessionKanbanMirror(context) {
15928
16737
  syncActiveSessionRegistration();
15929
16738
  configureWatcher();
15930
16739
  fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
16740
+ fireAndForget(configureBoardWatcher());
15931
16741
  fireAndForget(refreshFiles());
15932
16742
  }
15933
16743
  });
15934
16744
  configureWatcher();
16745
+ fireAndForget(configureBoardWatcher());
15935
16746
  const detach = () => {
15936
16747
  unsubscribe();
15937
16748
  if (timer) clearTimeout(timer);
16749
+ if (boardTimer) clearTimeout(boardTimer);
16750
+ if (presenceTimer) clearInterval(presenceTimer);
15938
16751
  watcher?.close();
16752
+ boardWatcher?.close();
15939
16753
  bindings.delete(context);
15940
16754
  if (attachedProjectRoot && registeredSessionId) {
15941
16755
  releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
@@ -15974,6 +16788,51 @@ function sourceStatus(task) {
15974
16788
  if (task.status === "failed") return "failed";
15975
16789
  return "pending";
15976
16790
  }
16791
+ function todoStatus(task) {
16792
+ const status = sourceStatus(task);
16793
+ if (status === "completed") return "completed";
16794
+ if (status === "in_progress" || status === "review") return "in_progress";
16795
+ return "pending";
16796
+ }
16797
+ function sessionTodoFromTask(task) {
16798
+ return {
16799
+ id: task.origin?.taskId ?? task.id,
16800
+ content: task.title,
16801
+ status: todoStatus(task),
16802
+ ...task.description ? { activeForm: task.description } : {}
16803
+ };
16804
+ }
16805
+ function sameTodos(left, right) {
16806
+ return left.length === right.length && left.every((todo, index) => {
16807
+ const candidate = right[index];
16808
+ return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask;
16809
+ });
16810
+ }
16811
+ function applySessionKanbanBoardToTodos(context, board) {
16812
+ const sessionId = context.session?.id ?? "";
16813
+ if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
16814
+ return [...context.todos];
16815
+ }
16816
+ const projectedTodos = board.tasks.filter(
16817
+ (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
16818
+ ).sort((left, right) => {
16819
+ const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;
16820
+ const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;
16821
+ return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);
16822
+ }).map(sessionTodoFromTask);
16823
+ const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
16824
+ const effectiveTodos = allCompleted ? [] : projectedTodos;
16825
+ if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
16826
+ suppressedTodoMirrors.add(context);
16827
+ try {
16828
+ context.state.replaceTodos(projectedTodos);
16829
+ } finally {
16830
+ suppressedTodoMirrors.delete(context);
16831
+ }
16832
+ notifyTodoUpdate(context, context.todos);
16833
+ broadcastTodoUpdate(context, context.todos);
16834
+ return [...context.todos];
16835
+ }
15977
16836
  async function applySessionKanbanTaskToSource(context, task, options = {}) {
15978
16837
  const originId = task.origin?.taskId;
15979
16838
  const graphId = task.origin?.graphId ?? "";
@@ -16037,8 +16896,8 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
16037
16896
  var kanbanTool = {
16038
16897
  name: "kanban",
16039
16898
  category: "Project",
16040
- description: "Manage project-scoped multi-kanban boards stored under .wrongstack/kanbans. Supports board/task/column CRUD, ready-task queues, dependency chains, split/merge, assignment metadata, provider/model/fallback routing hints, goal metrics, success checks, notes, links, and run status updates.",
16041
- usageHint: "Use this for durable project kanban state. Agents should call snapshot/ready_tasks, then claim_task before working. Use set_chain for ordered work, split_task/merge_tasks when task scope changes, assign_task with provider/model/fallback hints before spawning, mark_assignment when starting or finishing, and release_task if the claim cannot be worked.",
16899
+ description: "Manage and audit project-scoped multi-kanban boards stored under .wrongstack/kanbans. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Successful board access records live agent/session presence.",
16900
+ usageHint: "Use this for durable project kanban state. Reassess with get_board whenever evidence changes; agents may add, update, split, merge, reprioritize, or remove tasks so the board remains a live plan. Presence includes active/last-seen session and agent metadata. For managed boards, fully fill card details, use transition_task after every material step, attach truthful evidence, and never use update_task/move_task to bypass lifecycle guards. Worker completion enters Review; only passed acceptance criteria plus review evidence allow Done.",
16042
16901
  permission: "confirm",
16043
16902
  mutating: true,
16044
16903
  capabilities: ["fs.write"],
@@ -16075,6 +16934,7 @@ var kanbanTool = {
16075
16934
  "transfer_task",
16076
16935
  "get_task",
16077
16936
  "update_task",
16937
+ "transition_task",
16078
16938
  "move_task",
16079
16939
  "delete_task",
16080
16940
  "set_chain",
@@ -16105,6 +16965,7 @@ var kanbanTool = {
16105
16965
  targetColumnId: { type: "string" },
16106
16966
  title: { type: "string" },
16107
16967
  description: { type: "string" },
16968
+ dueDate: { type: "string" },
16108
16969
  tags: { type: "array", items: { type: "string" } },
16109
16970
  labels: { type: "array", items: { type: "string" } },
16110
16971
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
@@ -16151,6 +17012,18 @@ var kanbanTool = {
16151
17012
  type: "string",
16152
17013
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
16153
17014
  },
17015
+ lifecycleStage: {
17016
+ type: "string",
17017
+ enum: ["backlog", "todo", "running", "review", "done"]
17018
+ },
17019
+ transitionAction: { type: "string" },
17020
+ transitionComment: { type: "string" },
17021
+ attachmentUrl: { type: "string" },
17022
+ attachmentTitle: { type: "string" },
17023
+ attachmentType: {
17024
+ type: "string",
17025
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
17026
+ },
16154
17027
  releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
16155
17028
  releaseReason: { type: "string" },
16156
17029
  clearAssignee: { type: "boolean" },
@@ -16221,591 +17094,632 @@ var kanbanTool = {
16221
17094
  async execute(input, ctx) {
16222
17095
  const projectRoot = ctx.projectRoot;
16223
17096
  if (!projectRoot) return fail("No project root is available.");
17097
+ const withPresence = async (result) => {
17098
+ const boardId = result.board?.id ?? input.boardId;
17099
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
17100
+ try {
17101
+ const board = await touchKanbanPresence2(projectRoot, boardId, {
17102
+ sessionId: ctx.session.id,
17103
+ agentId: ctx.agentId,
17104
+ agentName: ctx.agentName,
17105
+ taskId: input.taskId ?? result.task?.id,
17106
+ runTaskId: input.runTaskId
17107
+ });
17108
+ return board ? { ...result, board } : result;
17109
+ } catch {
17110
+ return result;
17111
+ }
17112
+ };
16224
17113
  try {
16225
- switch (input.action) {
16226
- case "list_boards": {
16227
- const boards = await listBoards2(projectRoot);
16228
- return { ok: true, message: `${boards.length} board(s).`, boards };
16229
- }
16230
- case "get_board": {
16231
- const board = await requireBoard(projectRoot, input.boardId);
16232
- return board ? okBoard(board) : fail("Board not found.");
16233
- }
16234
- case "create_board": {
16235
- if (!input.title) return fail("create_board requires title.");
16236
- const board = await createBoard2(projectRoot, {
16237
- title: input.title,
16238
- ...input.description !== void 0 ? { description: input.description } : {},
16239
- ...input.tags !== void 0 ? { tags: input.tags } : {},
16240
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
16241
- });
16242
- return { ok: true, message: `Board created: ${board.title}`, board };
16243
- }
16244
- case "update_board": {
16245
- if (!input.boardId) return fail("update_board requires boardId.");
16246
- const board = await updateBoard2(projectRoot, input.boardId, {
16247
- ...input.title !== void 0 ? { title: input.title } : {},
16248
- ...input.description !== void 0 ? { description: input.description } : {},
16249
- ...input.tags !== void 0 ? { tags: input.tags } : {}
16250
- });
16251
- return board ? okBoard(board, "Board updated.") : fail("Board not found.");
16252
- }
16253
- case "duplicate_board": {
16254
- if (!input.boardId) return fail("duplicate_board requires boardId.");
16255
- const board = await duplicateBoard(projectRoot, input.boardId, {
16256
- ...input.title !== void 0 ? { title: input.title } : {},
16257
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16258
- ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
16259
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16260
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
16261
- });
16262
- return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
16263
- }
16264
- case "delete_board": {
16265
- if (!input.boardId) return fail("delete_board requires boardId.");
16266
- const removed = await removeBoard2(projectRoot, input.boardId);
16267
- return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
16268
- }
16269
- case "generate_board": {
16270
- if (!input.description) return fail("generate_board requires description.");
16271
- const boardInput = generateBoardFromDescription({
16272
- description: input.description,
16273
- ...input.title !== void 0 ? { title: input.title } : {},
16274
- ...input.context !== void 0 ? { context: input.context } : {},
16275
- ...input.columns !== void 0 ? { columns: input.columns } : {}
16276
- });
16277
- const board = await createBoard2(projectRoot, boardInput);
16278
- for (const taskInput2 of parseLinesIntoTasks(
16279
- input.description,
16280
- board.columns[0]?.id ?? "backlog"
16281
- )) {
16282
- await addTask(projectRoot, board.id, taskInput2);
17114
+ const result = await (async () => {
17115
+ switch (input.action) {
17116
+ case "list_boards": {
17117
+ const boards = await listBoards2(projectRoot);
17118
+ return { ok: true, message: `${boards.length} board(s).`, boards };
16283
17119
  }
16284
- return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
16285
- }
16286
- case "export_markdown": {
16287
- const board = await requireBoard(projectRoot, input.boardId);
16288
- if (!board) return fail("Board not found.");
16289
- return {
16290
- ok: true,
16291
- message: "Board exported.",
16292
- board,
16293
- markdown: exportBoardAsMarkdown(board)
16294
- };
16295
- }
16296
- case "export_task_graph": {
16297
- if (!input.boardId) return fail("export_task_graph requires boardId.");
16298
- const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
16299
- ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
16300
- ...input.specId !== void 0 ? { specId: input.specId } : {},
16301
- ...input.title !== void 0 ? { title: input.title } : {},
16302
- ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
16303
- ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
16304
- });
16305
- if (!exported) return fail("Board not found.");
16306
- return {
16307
- ok: true,
16308
- message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
16309
- board: exported.board,
16310
- taskGraph: serializeTaskGraph(exported.graph)
16311
- };
16312
- }
16313
- case "sync_task_graph": {
16314
- if (!input.boardId || !input.taskGraph) {
16315
- return fail("sync_task_graph requires boardId and taskGraph.");
17120
+ case "get_board": {
17121
+ const board = await requireBoard(projectRoot, input.boardId);
17122
+ return board ? okBoard(board) : fail("Board not found.");
16316
17123
  }
16317
- const graph = deserializeTaskGraph2(input.taskGraph);
16318
- const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
16319
- ...input.title !== void 0 ? { title: input.title } : {},
16320
- ...input.description !== void 0 ? { description: input.description } : {},
16321
- ...input.tags !== void 0 ? { tags: input.tags } : {},
16322
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16323
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16324
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16325
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16326
- ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
16327
- ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
16328
- });
16329
- return result ? {
16330
- ok: true,
16331
- message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
16332
- board: result.board
16333
- } : fail("Board not found.");
16334
- }
16335
- case "create_from_graph": {
16336
- if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
16337
- const graph = deserializeTaskGraph2(input.taskGraph);
16338
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16339
- ...input.title !== void 0 ? { title: input.title } : {},
16340
- ...input.description !== void 0 ? { description: input.description } : {},
16341
- ...input.tags !== void 0 ? { tags: input.tags } : {},
16342
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16343
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16344
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16345
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
16346
- });
16347
- return {
16348
- ok: true,
16349
- message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
16350
- board
16351
- };
16352
- }
16353
- case "import_session_tasks": {
16354
- const taskPath = ctx.meta?.["task.path"];
16355
- if (!taskPath) return fail("No session task file for this session.");
16356
- const file = await loadTasks2(taskPath);
16357
- if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
16358
- const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
16359
- const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
16360
- const tags = ["session", `session:${sessionId}`];
16361
- const existing = (await listBoards2(projectRoot)).find(
16362
- (b) => b.tags?.includes(`session:${sessionId}`)
16363
- );
16364
- if (existing) {
16365
- const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
17124
+ case "create_board": {
17125
+ if (!input.title) return fail("create_board requires title.");
17126
+ const board = await createBoard2(projectRoot, {
17127
+ title: input.title,
17128
+ ...input.description !== void 0 ? { description: input.description } : {},
17129
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17130
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
17131
+ });
17132
+ return { ok: true, message: `Board created: ${board.title}`, board };
17133
+ }
17134
+ case "update_board": {
17135
+ if (!input.boardId) return fail("update_board requires boardId.");
17136
+ const board = await updateBoard2(projectRoot, input.boardId, {
17137
+ ...input.title !== void 0 ? { title: input.title } : {},
17138
+ ...input.description !== void 0 ? { description: input.description } : {},
17139
+ ...input.tags !== void 0 ? { tags: input.tags } : {}
17140
+ });
17141
+ return board ? okBoard(board, "Board updated.") : fail("Board not found.");
17142
+ }
17143
+ case "duplicate_board": {
17144
+ if (!input.boardId) return fail("duplicate_board requires boardId.");
17145
+ const board = await duplicateBoard(projectRoot, input.boardId, {
17146
+ ...input.title !== void 0 ? { title: input.title } : {},
17147
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17148
+ ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
17149
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
17150
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
17151
+ });
17152
+ return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
17153
+ }
17154
+ case "delete_board": {
17155
+ if (!input.boardId) return fail("delete_board requires boardId.");
17156
+ const removed = await removeBoard2(projectRoot, input.boardId);
17157
+ return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
17158
+ }
17159
+ case "generate_board": {
17160
+ if (!input.description) return fail("generate_board requires description.");
17161
+ const boardInput = generateBoardFromDescription({
17162
+ description: input.description,
17163
+ ...input.title !== void 0 ? { title: input.title } : {},
17164
+ ...input.context !== void 0 ? { context: input.context } : {},
17165
+ ...input.columns !== void 0 ? { columns: input.columns } : {}
17166
+ });
17167
+ const board = await createBoard2(projectRoot, boardInput);
17168
+ for (const taskInput2 of parseLinesIntoTasks(
17169
+ input.description,
17170
+ board.columns[0]?.id ?? "backlog"
17171
+ )) {
17172
+ await addTask(projectRoot, board.id, taskInput2);
17173
+ }
17174
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
17175
+ }
17176
+ case "export_markdown": {
17177
+ const board = await requireBoard(projectRoot, input.boardId);
17178
+ if (!board) return fail("Board not found.");
17179
+ return {
17180
+ ok: true,
17181
+ message: "Board exported.",
17182
+ board,
17183
+ markdown: exportBoardAsMarkdown(board)
17184
+ };
17185
+ }
17186
+ case "export_task_graph": {
17187
+ if (!input.boardId) return fail("export_task_graph requires boardId.");
17188
+ const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
17189
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
17190
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
17191
+ ...input.title !== void 0 ? { title: input.title } : {},
17192
+ ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
17193
+ ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
17194
+ });
17195
+ if (!exported) return fail("Board not found.");
17196
+ return {
17197
+ ok: true,
17198
+ message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
17199
+ board: exported.board,
17200
+ taskGraph: serializeTaskGraph(exported.graph)
17201
+ };
17202
+ }
17203
+ case "sync_task_graph": {
17204
+ if (!input.boardId || !input.taskGraph) {
17205
+ return fail("sync_task_graph requires boardId and taskGraph.");
17206
+ }
17207
+ const graph = deserializeTaskGraph2(input.taskGraph);
17208
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
17209
+ ...input.title !== void 0 ? { title: input.title } : {},
17210
+ ...input.description !== void 0 ? { description: input.description } : {},
17211
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17212
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17213
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
17214
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
17215
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
17216
+ ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
17217
+ ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
17218
+ });
17219
+ return result2 ? {
17220
+ ok: true,
17221
+ message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
17222
+ board: result2.board
17223
+ } : fail("Board not found.");
17224
+ }
17225
+ case "create_from_graph": {
17226
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
17227
+ const graph = deserializeTaskGraph2(input.taskGraph);
17228
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
17229
+ ...input.title !== void 0 ? { title: input.title } : {},
17230
+ ...input.description !== void 0 ? { description: input.description } : {},
17231
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17232
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17233
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
17234
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
17235
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
17236
+ });
17237
+ return {
17238
+ ok: true,
17239
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
17240
+ board
17241
+ };
17242
+ }
17243
+ case "import_session_tasks": {
17244
+ const taskPath = ctx.meta?.["task.path"];
17245
+ if (!taskPath) return fail("No session task file for this session.");
17246
+ const file = await loadTasks2(taskPath);
17247
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
17248
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
17249
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
17250
+ const tags = ["session", `session:${sessionId}`];
17251
+ const existing = (await listBoards2(projectRoot)).find(
17252
+ (b) => b.tags?.includes(`session:${sessionId}`)
17253
+ );
17254
+ if (existing) {
17255
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
17256
+ sourceSystem: "session",
17257
+ tags,
17258
+ archiveMissingTasks: true,
17259
+ includeCompletedTasks: true
17260
+ });
17261
+ return result2 ? {
17262
+ ok: true,
17263
+ message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
17264
+ board: result2.board
17265
+ } : fail("Session board vanished mid-sync.");
17266
+ }
17267
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
17268
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
16366
17269
  sourceSystem: "session",
16367
- tags,
16368
- archiveMissingTasks: true,
16369
- includeCompletedTasks: true
17270
+ tags
16370
17271
  });
16371
- return result ? {
17272
+ return {
16372
17273
  ok: true,
16373
- message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
16374
- board: result.board
16375
- } : fail("Session board vanished mid-sync.");
17274
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
17275
+ board
17276
+ };
16376
17277
  }
16377
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16378
- title: `Session tasks (${sessionId.slice(0, 8)})`,
16379
- sourceSystem: "session",
16380
- tags
16381
- });
16382
- return {
16383
- ok: true,
16384
- message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
16385
- board
16386
- };
16387
- }
16388
- case "search_tasks": {
16389
- const tasks = await searchKanban(projectRoot, {
16390
- query: input.query,
16391
- boardId: input.boardId,
16392
- assignedAgent: input.agentId,
16393
- status: input.status,
16394
- priority: input.priority,
16395
- label: input.labels?.[0],
16396
- chainId: input.chainId
16397
- });
16398
- return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
16399
- }
16400
- case "ready_tasks": {
16401
- const tasks = await listReadyTasks(projectRoot, {
16402
- query: input.query,
16403
- boardId: input.boardId,
16404
- assignedAgent: input.agentId,
16405
- priority: input.priority,
16406
- label: input.labels?.[0],
16407
- chainId: input.chainId,
16408
- limit: input.limit
16409
- });
16410
- return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
16411
- }
16412
- case "snapshot": {
16413
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
16414
- query: input.query,
16415
- boardId: input.boardId,
16416
- assignedAgent: input.agentId,
16417
- status: input.status,
16418
- priority: input.priority,
16419
- label: input.labels?.[0],
16420
- chainId: input.chainId
16421
- });
16422
- return {
16423
- ok: true,
16424
- message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
16425
- snapshot
16426
- };
16427
- }
16428
- case "add_column": {
16429
- if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
16430
- const result = await addColumn(projectRoot, input.boardId, {
16431
- title: input.title,
16432
- ...input.description !== void 0 ? { description: input.description } : {}
16433
- });
16434
- return result ? okBoard(result.board, "Column added.") : fail("Board not found.");
16435
- }
16436
- case "update_column": {
16437
- if (!input.boardId || !input.columnId)
16438
- return fail("update_column requires boardId and columnId.");
16439
- const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
16440
- ...input.title !== void 0 ? { title: input.title } : {},
16441
- ...input.description !== void 0 ? { description: input.description } : {},
16442
- ...input.order !== void 0 ? { order: input.order } : {}
16443
- });
16444
- return board ? okBoard(board, "Column updated.") : fail("Column not found.");
16445
- }
16446
- case "delete_column": {
16447
- if (!input.boardId || !input.columnId)
16448
- return fail("delete_column requires boardId and columnId.");
16449
- const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
16450
- moveTasksToColumnId: input.moveTasksToColumnId
16451
- });
16452
- return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
16453
- }
16454
- case "add_task": {
16455
- if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
16456
- const result = await addTask(projectRoot, input.boardId, taskInput(input));
16457
- return result ? okTask(result.board, result.task, "Task added.") : fail("Board not found.");
16458
- }
16459
- case "split_task": {
16460
- if (!input.boardId || !input.taskId || !input.childTitles?.length) {
16461
- return fail("split_task requires boardId, taskId, and childTitles.");
17278
+ case "search_tasks": {
17279
+ const tasks = await searchKanban(projectRoot, {
17280
+ query: input.query,
17281
+ boardId: input.boardId,
17282
+ assignedAgent: input.agentId,
17283
+ status: input.status,
17284
+ priority: input.priority,
17285
+ label: input.labels?.[0],
17286
+ chainId: input.chainId
17287
+ });
17288
+ return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
16462
17289
  }
16463
- const result = await splitTask(projectRoot, input.boardId, input.taskId, {
16464
- titles: input.childTitles,
16465
- ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
16466
- ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
16467
- ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
16468
- ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
16469
- ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
16470
- ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
16471
- ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
16472
- ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
16473
- });
16474
- return result ? {
16475
- ok: true,
16476
- message: `${result.children.length} child task(s) created.`,
16477
- board: result.board,
16478
- task: result.parent,
16479
- children: result.children
16480
- } : fail("Task not found.");
16481
- }
16482
- case "merge_tasks": {
16483
- if (!input.boardId || !input.taskIds?.length || !input.title) {
16484
- return fail("merge_tasks requires boardId, taskIds, and title.");
17290
+ case "ready_tasks": {
17291
+ const tasks = await listReadyTasks(projectRoot, {
17292
+ query: input.query,
17293
+ boardId: input.boardId,
17294
+ assignedAgent: input.agentId,
17295
+ priority: input.priority,
17296
+ label: input.labels?.[0],
17297
+ chainId: input.chainId,
17298
+ limit: input.limit
17299
+ });
17300
+ return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
16485
17301
  }
16486
- const result = await mergeTasks(projectRoot, input.boardId, {
16487
- taskIds: input.taskIds,
16488
- title: input.title,
16489
- ...input.description !== void 0 ? { description: input.description } : {},
16490
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16491
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16492
- ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
16493
- });
16494
- return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
16495
- }
16496
- case "copy_task": {
16497
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
16498
- return fail("copy_task requires boardId, taskId, and targetBoardId.");
17302
+ case "snapshot": {
17303
+ const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
17304
+ query: input.query,
17305
+ boardId: input.boardId,
17306
+ assignedAgent: input.agentId,
17307
+ status: input.status,
17308
+ priority: input.priority,
17309
+ label: input.labels?.[0],
17310
+ chainId: input.chainId
17311
+ });
17312
+ return {
17313
+ ok: true,
17314
+ message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
17315
+ snapshot
17316
+ };
16499
17317
  }
16500
- const result = await copyTaskToBoard(
16501
- projectRoot,
16502
- input.boardId,
16503
- input.taskId,
16504
- input.targetBoardId,
16505
- {
16506
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16507
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
16508
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16509
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17318
+ case "add_column": {
17319
+ if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
17320
+ const result2 = await addColumn(projectRoot, input.boardId, {
17321
+ title: input.title,
17322
+ ...input.description !== void 0 ? { description: input.description } : {}
17323
+ });
17324
+ return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
17325
+ }
17326
+ case "update_column": {
17327
+ if (!input.boardId || !input.columnId)
17328
+ return fail("update_column requires boardId and columnId.");
17329
+ const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
17330
+ ...input.title !== void 0 ? { title: input.title } : {},
17331
+ ...input.description !== void 0 ? { description: input.description } : {},
17332
+ ...input.order !== void 0 ? { order: input.order } : {}
17333
+ });
17334
+ return board ? okBoard(board, "Column updated.") : fail("Column not found.");
17335
+ }
17336
+ case "delete_column": {
17337
+ if (!input.boardId || !input.columnId)
17338
+ return fail("delete_column requires boardId and columnId.");
17339
+ const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
17340
+ moveTasksToColumnId: input.moveTasksToColumnId
17341
+ });
17342
+ return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
17343
+ }
17344
+ case "add_task": {
17345
+ if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
17346
+ const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
17347
+ return result2 ? okTask(result2.board, result2.task, "Task added.") : fail("Board not found.");
17348
+ }
17349
+ case "split_task": {
17350
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
17351
+ return fail("split_task requires boardId, taskId, and childTitles.");
16510
17352
  }
16511
- );
16512
- return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
16513
- }
16514
- case "transfer_task": {
16515
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
16516
- return fail("transfer_task requires boardId, taskId, and targetBoardId.");
17353
+ const result2 = await splitTask(projectRoot, input.boardId, input.taskId, {
17354
+ titles: input.childTitles,
17355
+ ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
17356
+ ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
17357
+ ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
17358
+ ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
17359
+ ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
17360
+ ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
17361
+ ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
17362
+ ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
17363
+ });
17364
+ return result2 ? {
17365
+ ok: true,
17366
+ message: `${result2.children.length} child task(s) created.`,
17367
+ board: result2.board,
17368
+ task: result2.parent,
17369
+ children: result2.children
17370
+ } : fail("Task not found.");
16517
17371
  }
16518
- const result = await transferTaskToBoard(
16519
- projectRoot,
16520
- input.boardId,
16521
- input.taskId,
16522
- input.targetBoardId,
16523
- {
17372
+ case "merge_tasks": {
17373
+ if (!input.boardId || !input.taskIds?.length || !input.title) {
17374
+ return fail("merge_tasks requires boardId, taskIds, and title.");
17375
+ }
17376
+ const result2 = await mergeTasks(projectRoot, input.boardId, {
17377
+ taskIds: input.taskIds,
17378
+ title: input.title,
17379
+ ...input.description !== void 0 ? { description: input.description } : {},
16524
17380
  ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16525
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
16526
17381
  ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16527
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17382
+ ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
17383
+ });
17384
+ return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
17385
+ }
17386
+ case "copy_task": {
17387
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
17388
+ return fail("copy_task requires boardId, taskId, and targetBoardId.");
16528
17389
  }
16529
- );
16530
- return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
16531
- }
16532
- case "get_task": {
16533
- if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
16534
- const task = await getTask(projectRoot, input.boardId, input.taskId);
16535
- return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
16536
- }
16537
- case "update_task": {
16538
- if (!input.boardId || !input.taskId)
16539
- return fail("update_task requires boardId and taskId.");
16540
- const board = await updateTask(
16541
- projectRoot,
16542
- input.boardId,
16543
- input.taskId,
16544
- taskPatch(input)
16545
- );
16546
- return board ? okBoard(board, "Task updated.") : fail("Task not found.");
16547
- }
16548
- case "move_task": {
16549
- if (!input.boardId || !input.taskId || !input.targetColumnId) {
16550
- return fail("move_task requires boardId, taskId, and targetColumnId.");
17390
+ const result2 = await copyTaskToBoard(
17391
+ projectRoot,
17392
+ input.boardId,
17393
+ input.taskId,
17394
+ input.targetBoardId,
17395
+ {
17396
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
17397
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
17398
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
17399
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17400
+ }
17401
+ );
17402
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
16551
17403
  }
16552
- const board = await moveTask(
16553
- projectRoot,
16554
- input.boardId,
16555
- input.taskId,
16556
- input.targetColumnId,
16557
- input.order
16558
- );
16559
- return board ? okBoard(board, "Task moved.") : fail("Move failed.");
16560
- }
16561
- case "delete_task": {
16562
- if (!input.boardId || !input.taskId)
16563
- return fail("delete_task requires boardId and taskId.");
16564
- const board = await removeTask(projectRoot, input.boardId, input.taskId);
16565
- return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
16566
- }
16567
- case "set_chain": {
16568
- if (!input.boardId || !input.taskIds?.length) {
16569
- return fail("set_chain requires boardId and taskIds.");
17404
+ case "transfer_task": {
17405
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
17406
+ return fail("transfer_task requires boardId, taskId, and targetBoardId.");
17407
+ }
17408
+ const result2 = await transferTaskToBoard(
17409
+ projectRoot,
17410
+ input.boardId,
17411
+ input.taskId,
17412
+ input.targetBoardId,
17413
+ {
17414
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
17415
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
17416
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
17417
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17418
+ }
17419
+ );
17420
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
16570
17421
  }
16571
- const result = await setTaskChain(projectRoot, input.boardId, {
16572
- taskIds: input.taskIds,
16573
- ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
16574
- ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
16575
- });
16576
- return result ? {
16577
- ok: true,
16578
- message: `Chain set: ${result.chainId}`,
16579
- board: result.board,
16580
- chain: result.tasks
16581
- } : fail("Board or task not found.");
16582
- }
16583
- case "get_chain": {
16584
- if (!input.boardId || !(input.taskId || input.chainId)) {
16585
- return fail("get_chain requires boardId and taskId or chainId.");
17422
+ case "get_task": {
17423
+ if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
17424
+ const task = await getTask(projectRoot, input.boardId, input.taskId);
17425
+ return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
16586
17426
  }
16587
- const result = await getTaskChain(
16588
- projectRoot,
16589
- input.boardId,
16590
- input.taskId ?? input.chainId ?? ""
16591
- );
16592
- return result ? {
16593
- ok: true,
16594
- message: `Chain loaded: ${result.chainId}`,
16595
- board: result.board,
16596
- chain: result.tasks
16597
- } : fail("Chain not found.");
16598
- }
16599
- case "claim_task": {
16600
- const result = await claimReadyTask(projectRoot, {
16601
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
16602
- ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
16603
- ...assignmentInput(input),
16604
- status: input.assignmentStatus ?? "queued"
16605
- });
16606
- return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
16607
- }
16608
- case "release_task": {
16609
- if (!input.boardId || !input.taskId) {
16610
- return fail("release_task requires boardId and taskId.");
17427
+ case "update_task": {
17428
+ if (!input.boardId || !input.taskId)
17429
+ return fail("update_task requires boardId and taskId.");
17430
+ const board = await updateTask(
17431
+ projectRoot,
17432
+ input.boardId,
17433
+ input.taskId,
17434
+ taskPatch(input)
17435
+ );
17436
+ return board ? okBoard(board, "Task updated.") : fail("Task not found.");
16611
17437
  }
16612
- const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
16613
- ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
16614
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
16615
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
16616
- });
16617
- return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
16618
- }
16619
- case "assign_task": {
16620
- if (!input.boardId || !input.taskId)
16621
- return fail("assign_task requires boardId and taskId.");
16622
- const board = await assignTask(
16623
- projectRoot,
16624
- input.boardId,
16625
- input.taskId,
16626
- assignmentInput(input)
16627
- );
16628
- return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
16629
- }
16630
- case "mark_assignment": {
16631
- if (!input.boardId || !input.taskId)
16632
- return fail("mark_assignment requires boardId and taskId.");
16633
- const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
16634
- const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
16635
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
16636
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
16637
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
16638
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
16639
- ...input.error !== void 0 ? { error: input.error } : {},
16640
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
16641
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
16642
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
16643
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
16644
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
16645
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
16646
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
16647
- });
16648
- return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
16649
- }
16650
- case "heartbeat_assignment": {
16651
- if (!input.boardId || !input.taskId) {
16652
- return fail("heartbeat_assignment requires boardId and taskId.");
17438
+ case "transition_task": {
17439
+ if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
17440
+ return fail(
17441
+ "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
17442
+ );
17443
+ }
17444
+ const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
17445
+ to: input.lifecycleStage,
17446
+ actor: input.author,
17447
+ comment: input.transitionComment,
17448
+ ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
17449
+ ...input.attachmentUrl !== void 0 ? {
17450
+ attachment: {
17451
+ url: input.attachmentUrl,
17452
+ type: input.attachmentType ?? "url",
17453
+ ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
17454
+ }
17455
+ } : {},
17456
+ patch: taskPatch(input)
17457
+ });
17458
+ return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
16653
17459
  }
16654
- const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
16655
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
16656
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
16657
- });
16658
- return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
16659
- }
16660
- case "recover_stale": {
16661
- if (!input.boardId) return fail("recover_stale requires boardId.");
16662
- const policyFields = [
16663
- input.recoveryPolicyFailOnCostCeiling !== void 0,
16664
- input.recoveryPolicyReleaseOnFailureKinds !== void 0,
16665
- input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
16666
- input.recoveryPolicyRetryPolicyOverride !== void 0
16667
- ].some(Boolean);
16668
- const result = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
16669
- ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
16670
- ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
16671
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
16672
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
16673
- ...policyFields ? {
16674
- policy: {
16675
- ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
16676
- ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
16677
- releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
16678
- } : {},
16679
- ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
16680
- releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
16681
- } : {},
16682
- ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
16683
- retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
16684
- } : {}
16685
- }
16686
- } : {}
16687
- });
16688
- return result ? {
16689
- ok: true,
16690
- message: `Recovered ${result.tasks.length} stale assignment(s).`,
16691
- board: result.board,
16692
- recoveredTasks: result.tasks
16693
- } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
16694
- }
16695
- case "events": {
16696
- if (!input.boardId) return fail("events requires boardId.");
16697
- const eventList = await listKanbanEvents(projectRoot, input.boardId);
16698
- return {
16699
- ok: true,
16700
- message: `${eventList.length} event(s).`,
16701
- events: eventList
16702
- };
16703
- }
16704
- case "queue_health": {
16705
- const health = await getKanbanQueueHealth(projectRoot, {
16706
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
16707
- });
16708
- return {
16709
- ok: true,
16710
- message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
16711
- queueHealth: health
16712
- };
16713
- }
16714
- case "add_dependency": {
16715
- if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
16716
- return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
17460
+ case "move_task": {
17461
+ if (!input.boardId || !input.taskId || !input.targetColumnId) {
17462
+ return fail("move_task requires boardId, taskId, and targetColumnId.");
17463
+ }
17464
+ const board = await moveTask(
17465
+ projectRoot,
17466
+ input.boardId,
17467
+ input.taskId,
17468
+ input.targetColumnId,
17469
+ input.order
17470
+ );
17471
+ return board ? okBoard(board, "Task moved.") : fail("Move failed.");
16717
17472
  }
16718
- const board = await addDependency(
16719
- projectRoot,
16720
- input.boardId,
16721
- input.taskId,
16722
- input.dependencyTaskId
16723
- );
16724
- return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
16725
- }
16726
- case "add_goal_metric": {
16727
- if (!input.boardId || !input.taskId || !input.metricName) {
16728
- return fail("add_goal_metric requires boardId, taskId, and metricName.");
17473
+ case "delete_task": {
17474
+ if (!input.boardId || !input.taskId)
17475
+ return fail("delete_task requires boardId and taskId.");
17476
+ const board = await removeTask(projectRoot, input.boardId, input.taskId);
17477
+ return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
16729
17478
  }
16730
- const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
16731
- name: input.metricName,
16732
- ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
16733
- ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
16734
- ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
16735
- ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
16736
- ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
16737
- });
16738
- return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
16739
- }
16740
- case "update_goal_metric": {
16741
- if (!input.boardId || !input.taskId || !input.metricId) {
16742
- return fail("update_goal_metric requires boardId, taskId, and metricId.");
17479
+ case "set_chain": {
17480
+ if (!input.boardId || !input.taskIds?.length) {
17481
+ return fail("set_chain requires boardId and taskIds.");
17482
+ }
17483
+ const result2 = await setTaskChain(projectRoot, input.boardId, {
17484
+ taskIds: input.taskIds,
17485
+ ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
17486
+ ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
17487
+ });
17488
+ return result2 ? {
17489
+ ok: true,
17490
+ message: `Chain set: ${result2.chainId}`,
17491
+ board: result2.board,
17492
+ chain: result2.tasks
17493
+ } : fail("Board or task not found.");
16743
17494
  }
16744
- const board = await updateGoalMetricOnTask(
16745
- projectRoot,
16746
- input.boardId,
16747
- input.taskId,
16748
- input.metricId,
16749
- {
16750
- ...input.metricName !== void 0 ? { name: input.metricName } : {},
17495
+ case "get_chain": {
17496
+ if (!input.boardId || !(input.taskId || input.chainId)) {
17497
+ return fail("get_chain requires boardId and taskId or chainId.");
17498
+ }
17499
+ const result2 = await getTaskChain(
17500
+ projectRoot,
17501
+ input.boardId,
17502
+ input.taskId ?? input.chainId ?? ""
17503
+ );
17504
+ return result2 ? {
17505
+ ok: true,
17506
+ message: `Chain loaded: ${result2.chainId}`,
17507
+ board: result2.board,
17508
+ chain: result2.tasks
17509
+ } : fail("Chain not found.");
17510
+ }
17511
+ case "claim_task": {
17512
+ const result2 = await claimReadyTask(projectRoot, {
17513
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
17514
+ ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
17515
+ ...assignmentInput(input),
17516
+ status: input.assignmentStatus ?? "queued"
17517
+ });
17518
+ return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
17519
+ }
17520
+ case "release_task": {
17521
+ if (!input.boardId || !input.taskId) {
17522
+ return fail("release_task requires boardId and taskId.");
17523
+ }
17524
+ const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
17525
+ ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
17526
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17527
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
17528
+ });
17529
+ return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
17530
+ }
17531
+ case "assign_task": {
17532
+ if (!input.boardId || !input.taskId)
17533
+ return fail("assign_task requires boardId and taskId.");
17534
+ const board = await assignTask(
17535
+ projectRoot,
17536
+ input.boardId,
17537
+ input.taskId,
17538
+ assignmentInput(input)
17539
+ );
17540
+ return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
17541
+ }
17542
+ case "mark_assignment": {
17543
+ if (!input.boardId || !input.taskId)
17544
+ return fail("mark_assignment requires boardId and taskId.");
17545
+ const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
17546
+ const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
17547
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17548
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17549
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17550
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17551
+ ...input.error !== void 0 ? { error: input.error } : {},
17552
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17553
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17554
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17555
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17556
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17557
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17558
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17559
+ });
17560
+ return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
17561
+ }
17562
+ case "heartbeat_assignment": {
17563
+ if (!input.boardId || !input.taskId) {
17564
+ return fail("heartbeat_assignment requires boardId and taskId.");
17565
+ }
17566
+ const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
17567
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17568
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
17569
+ });
17570
+ return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
17571
+ }
17572
+ case "recover_stale": {
17573
+ if (!input.boardId) return fail("recover_stale requires boardId.");
17574
+ const policyFields = [
17575
+ input.recoveryPolicyFailOnCostCeiling !== void 0,
17576
+ input.recoveryPolicyReleaseOnFailureKinds !== void 0,
17577
+ input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
17578
+ input.recoveryPolicyRetryPolicyOverride !== void 0
17579
+ ].some(Boolean);
17580
+ const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
17581
+ ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
17582
+ ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
17583
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17584
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
17585
+ ...policyFields ? {
17586
+ policy: {
17587
+ ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
17588
+ ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
17589
+ releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
17590
+ } : {},
17591
+ ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
17592
+ releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
17593
+ } : {},
17594
+ ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
17595
+ retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
17596
+ } : {}
17597
+ }
17598
+ } : {}
17599
+ });
17600
+ return result2 ? {
17601
+ ok: true,
17602
+ message: `Recovered ${result2.tasks.length} stale assignment(s).`,
17603
+ board: result2.board,
17604
+ recoveredTasks: result2.tasks
17605
+ } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
17606
+ }
17607
+ case "events": {
17608
+ if (!input.boardId) return fail("events requires boardId.");
17609
+ const eventList = await listKanbanEvents(projectRoot, input.boardId);
17610
+ return {
17611
+ ok: true,
17612
+ message: `${eventList.length} event(s).`,
17613
+ events: eventList
17614
+ };
17615
+ }
17616
+ case "queue_health": {
17617
+ const health = await getKanbanQueueHealth(projectRoot, {
17618
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
17619
+ });
17620
+ return {
17621
+ ok: true,
17622
+ message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
17623
+ queueHealth: health
17624
+ };
17625
+ }
17626
+ case "add_dependency": {
17627
+ if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
17628
+ return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
17629
+ }
17630
+ const board = await addDependency(
17631
+ projectRoot,
17632
+ input.boardId,
17633
+ input.taskId,
17634
+ input.dependencyTaskId
17635
+ );
17636
+ return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
17637
+ }
17638
+ case "add_goal_metric": {
17639
+ if (!input.boardId || !input.taskId || !input.metricName) {
17640
+ return fail("add_goal_metric requires boardId, taskId, and metricName.");
17641
+ }
17642
+ const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
17643
+ name: input.metricName,
16751
17644
  ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
16752
17645
  ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
16753
17646
  ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
16754
17647
  ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
16755
17648
  ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17649
+ });
17650
+ return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
17651
+ }
17652
+ case "update_goal_metric": {
17653
+ if (!input.boardId || !input.taskId || !input.metricId) {
17654
+ return fail("update_goal_metric requires boardId, taskId, and metricId.");
16756
17655
  }
16757
- );
16758
- return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
16759
- }
16760
- case "add_check": {
16761
- if (!input.boardId || !input.taskId || !input.checkDescription) {
16762
- return fail("add_check requires boardId, taskId, and checkDescription.");
17656
+ const board = await updateGoalMetricOnTask(
17657
+ projectRoot,
17658
+ input.boardId,
17659
+ input.taskId,
17660
+ input.metricId,
17661
+ {
17662
+ ...input.metricName !== void 0 ? { name: input.metricName } : {},
17663
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
17664
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17665
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17666
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17667
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17668
+ }
17669
+ );
17670
+ return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
16763
17671
  }
16764
- const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
16765
- description: input.checkDescription,
16766
- type: "manual",
16767
- status: input.checkStatus
16768
- });
16769
- return board ? okBoard(board, "Check added.") : fail("Task not found.");
16770
- }
16771
- case "update_check": {
16772
- if (!input.boardId || !input.taskId || !input.checkId) {
16773
- return fail("update_check requires boardId, taskId, and checkId.");
17672
+ case "add_check": {
17673
+ if (!input.boardId || !input.taskId || !input.checkDescription) {
17674
+ return fail("add_check requires boardId, taskId, and checkDescription.");
17675
+ }
17676
+ const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
17677
+ description: input.checkDescription,
17678
+ type: "manual",
17679
+ status: input.checkStatus
17680
+ });
17681
+ return board ? okBoard(board, "Check added.") : fail("Task not found.");
16774
17682
  }
16775
- const board = await updateCheckOnTask(
16776
- projectRoot,
16777
- input.boardId,
16778
- input.taskId,
16779
- input.checkId,
16780
- {
16781
- ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
16782
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17683
+ case "update_check": {
17684
+ if (!input.boardId || !input.taskId || !input.checkId) {
17685
+ return fail("update_check requires boardId, taskId, and checkId.");
16783
17686
  }
16784
- );
16785
- return board ? okBoard(board, "Check updated.") : fail("Check not found.");
16786
- }
16787
- case "add_note": {
16788
- if (!input.boardId || !input.taskId || !input.note)
16789
- return fail("add_note requires boardId, taskId, and note.");
16790
- const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
16791
- author: input.author ?? "agent",
16792
- content: input.note
16793
- });
16794
- return board ? okBoard(board, "Note added.") : fail("Task not found.");
16795
- }
16796
- case "add_link": {
16797
- if (!input.boardId || !input.taskId || !input.url)
16798
- return fail("add_link requires boardId, taskId, and url.");
16799
- const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
16800
- url: input.url,
16801
- type: input.linkType ?? "url",
16802
- ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
16803
- });
16804
- return board ? okBoard(board, "Link added.") : fail("Task not found.");
17687
+ const board = await updateCheckOnTask(
17688
+ projectRoot,
17689
+ input.boardId,
17690
+ input.taskId,
17691
+ input.checkId,
17692
+ {
17693
+ ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
17694
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17695
+ }
17696
+ );
17697
+ return board ? okBoard(board, "Check updated.") : fail("Check not found.");
17698
+ }
17699
+ case "add_note": {
17700
+ if (!input.boardId || !input.taskId || !input.note)
17701
+ return fail("add_note requires boardId, taskId, and note.");
17702
+ const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
17703
+ author: input.author ?? "agent",
17704
+ content: input.note
17705
+ });
17706
+ return board ? okBoard(board, "Note added.") : fail("Task not found.");
17707
+ }
17708
+ case "add_link": {
17709
+ if (!input.boardId || !input.taskId || !input.url)
17710
+ return fail("add_link requires boardId, taskId, and url.");
17711
+ const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
17712
+ url: input.url,
17713
+ type: input.linkType ?? "url",
17714
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17715
+ });
17716
+ return board ? okBoard(board, "Link added.") : fail("Task not found.");
17717
+ }
17718
+ default:
17719
+ return fail(`Unknown kanban action: ${input.action}`);
16805
17720
  }
16806
- default:
16807
- return fail(`Unknown kanban action: ${input.action}`);
16808
- }
17721
+ })();
17722
+ return withPresence(result);
16809
17723
  } catch (err) {
16810
17724
  return fail(err instanceof Error ? err.message : String(err));
16811
17725
  }
@@ -16829,6 +17743,7 @@ function taskInput(input) {
16829
17743
  title: input.title ?? "",
16830
17744
  columnId: input.columnId,
16831
17745
  description: input.description,
17746
+ dueDate: input.dueDate,
16832
17747
  priority: input.priority,
16833
17748
  ...input.taskType !== void 0 ? { type: input.taskType } : {},
16834
17749
  status: input.status,
@@ -16837,7 +17752,67 @@ function taskInput(input) {
16837
17752
  ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
16838
17753
  ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16839
17754
  ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
16840
- ...assignment ? { assignment } : {}
17755
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
17756
+ ...assignment ? { assignment } : {},
17757
+ ...input.order !== void 0 ? { order: input.order } : {},
17758
+ // Sprint 3 durable task-level fields
17759
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
17760
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
17761
+ // Relationships — chainId alone cannot construct a KanbanTaskChainRef,
17762
+ // so chain/previousTaskId/nextTaskId must be set via update_task.
17763
+ ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
17764
+ // Detail objects
17765
+ ...input.checkDescription !== void 0 ? {
17766
+ successCriteria: [
17767
+ {
17768
+ id: randomUUID(),
17769
+ description: input.checkDescription,
17770
+ type: "manual",
17771
+ status: input.checkStatus ?? "pending"
17772
+ }
17773
+ ]
17774
+ } : {},
17775
+ ...input.metricName !== void 0 ? {
17776
+ goalMetrics: [
17777
+ {
17778
+ id: randomUUID(),
17779
+ name: input.metricName,
17780
+ status: input.metricStatus ?? "pending",
17781
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17782
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17783
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17784
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17785
+ }
17786
+ ]
17787
+ } : {},
17788
+ ...input.url !== void 0 ? {
17789
+ links: [
17790
+ {
17791
+ url: input.url,
17792
+ type: input.linkType ?? "url",
17793
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17794
+ }
17795
+ ]
17796
+ } : {},
17797
+ ...input.note !== void 0 ? {
17798
+ notes: [
17799
+ {
17800
+ id: randomUUID(),
17801
+ author: input.author ?? "agent",
17802
+ content: input.note,
17803
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
17804
+ }
17805
+ ]
17806
+ } : {},
17807
+ // Origin tracking
17808
+ ...input.graphId !== void 0 ? {
17809
+ origin: {
17810
+ system: input.sourceSystem ?? "kanban-tool",
17811
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
17812
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
17813
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
17814
+ }
17815
+ } : {}
16841
17816
  };
16842
17817
  }
16843
17818
  function mergedDependsOn(input) {
@@ -16851,6 +17826,7 @@ function taskPatch(input) {
16851
17826
  return {
16852
17827
  title: input.title,
16853
17828
  description: input.description,
17829
+ dueDate: input.dueDate,
16854
17830
  columnId: input.columnId,
16855
17831
  order: input.order,
16856
17832
  priority: input.priority,
@@ -17612,7 +18588,7 @@ import {
17612
18588
  mutateTasks as mutateTasks2,
17613
18589
  formatTaskList
17614
18590
  } from "@wrongstack/core";
17615
- import { randomUUID } from "node:crypto";
18591
+ import { randomUUID as randomUUID2 } from "node:crypto";
17616
18592
  var planTool = {
17617
18593
  name: "plan",
17618
18594
  category: "Session",
@@ -17838,7 +18814,7 @@ var planTool = {
17838
18814
  try {
17839
18815
  const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
17840
18816
  f.tasks.push({
17841
- id: `task_${randomUUID()}`,
18817
+ id: `task_${randomUUID2()}`,
17842
18818
  title: taskifyMeta.title,
17843
18819
  description: taskifyMeta.details || void 0,
17844
18820
  type: "feature",
@@ -18980,7 +19956,7 @@ import {
18980
19956
  mutatePlan as mutatePlan3,
18981
19957
  formatPlan as formatPlan2
18982
19958
  } from "@wrongstack/core";
18983
- import { randomUUID as randomUUID2 } from "node:crypto";
19959
+ import { randomUUID as randomUUID3 } from "node:crypto";
18984
19960
  function findTaskIndex(tasks, query) {
18985
19961
  const asNum = Number.parseInt(query, 10);
18986
19962
  if (!Number.isNaN(asNum)) {
@@ -19168,7 +20144,7 @@ var taskTool = {
19168
20144
  }
19169
20145
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
19170
20146
  const newTask = {
19171
- id: `task_${Date.now()}_${randomUUID2().slice(0, 8)}`,
20147
+ id: `task_${Date.now()}_${randomUUID3().slice(0, 8)}`,
19172
20148
  title: t.title,
19173
20149
  description: t.description,
19174
20150
  type: t.type || "feature",
@@ -19229,7 +20205,7 @@ var taskTool = {
19229
20205
  });
19230
20206
  if (match.description) {
19231
20207
  todos.push({
19232
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
20208
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
19233
20209
  content: match.description.slice(0, 200),
19234
20210
  status: "pending",
19235
20211
  promotedFromTask: match.id
@@ -19238,7 +20214,7 @@ var taskTool = {
19238
20214
  if (input.subtasks && input.subtasks.length > 0) {
19239
20215
  for (const st of input.subtasks) {
19240
20216
  todos.push({
19241
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
20217
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
19242
20218
  content: st,
19243
20219
  status: "pending",
19244
20220
  promotedFromTask: match.id
@@ -19944,8 +20920,8 @@ var DEFAULT_IGNORE5 = [
19944
20920
  var treeTool = {
19945
20921
  name: "tree",
19946
20922
  category: "Filesystem",
19947
- description: "Display a directory tree of the project (or a subpath). This is the recommended way to explore the high-level structure of a codebase before reading specific files.",
19948
- usageHint: "BEST PRACTICE FOR INITIAL EXPLORATION:\n\n- Call early when working with an unfamiliar project or module.\n- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\n- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\nOutput is truncated for very large trees.",
20923
+ description: "Display a project or subpath directory tree. Use it for structural layout; use index-backed `codebase-search` first for code understanding when it is live.",
20924
+ usageHint: "DIRECTORY-LAYOUT EXPLORATION:\n\n- When `codebase-search` is live, use it first for symbols and concepts; use `tree` when the directory hierarchy itself matters.\n- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\n- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\nOutput is truncated for very large trees.",
19949
20925
  permission: "auto",
19950
20926
  mutating: false,
19951
20927
  capabilities: ["fs.read"],
@@ -20348,15 +21324,15 @@ var OPTIONAL_TOOLS = [
20348
21324
  toolUseTool,
20349
21325
  batchToolUseTool,
20350
21326
  toolHelpTool,
20351
- codebaseIndexTool,
20352
- codebaseSearchTool,
20353
- codebaseStatsTool,
20354
21327
  setWorkingDirTool
20355
21328
  ];
20356
21329
  var TIER1_TOOLS = [
20357
21330
  readTool,
20358
21331
  writeTool,
20359
21332
  editTool,
21333
+ codebaseStatsTool,
21334
+ codebaseSearchTool,
21335
+ codebaseIndexTool,
20360
21336
  bashTool,
20361
21337
  grepTool,
20362
21338
  globTool,
@@ -20395,9 +21371,6 @@ var TIER3_TOOLS = [
20395
21371
  toolUseTool,
20396
21372
  batchToolUseTool,
20397
21373
  toolHelpTool,
20398
- codebaseIndexTool,
20399
- codebaseSearchTool,
20400
- codebaseStatsTool,
20401
21374
  setWorkingDirTool
20402
21375
  ];
20403
21376
  var builtinTools = [
@@ -20406,6 +21379,9 @@ var builtinTools = [
20406
21379
  readTool,
20407
21380
  writeTool,
20408
21381
  editTool,
21382
+ codebaseStatsTool,
21383
+ codebaseSearchTool,
21384
+ codebaseIndexTool,
20409
21385
  replaceTool,
20410
21386
  globTool,
20411
21387
  grepTool,
@@ -20440,9 +21416,6 @@ var builtinTools = [
20440
21416
  toolUseTool,
20441
21417
  batchToolUseTool,
20442
21418
  toolHelpTool,
20443
- codebaseIndexTool,
20444
- codebaseSearchTool,
20445
- codebaseStatsTool,
20446
21419
  setWorkingDirTool
20447
21420
  ];
20448
21421
 
@@ -21680,6 +22653,7 @@ export {
21680
22653
  TOOL_ICON_CONFIG,
21681
22654
  TOOL_ICON_MAP,
21682
22655
  _resetProcessRegistry,
22656
+ applySessionKanbanBoardToTodos,
21683
22657
  applySessionKanbanTaskToSource,
21684
22658
  assertBrowserUrlAllowed,
21685
22659
  attachSessionKanbanMirror,
@@ -21731,6 +22705,7 @@ export {
21731
22705
  executeLanguagePlan,
21732
22706
  executePackagePlan,
21733
22707
  fetchTool,
22708
+ fileGraphService,
21734
22709
  forgetTool,
21735
22710
  formatGlobalStatus,
21736
22711
  formatInstanceList,
@@ -21770,6 +22745,7 @@ export {
21770
22745
  normalizeShell,
21771
22746
  onIndexStateChange,
21772
22747
  outdatedTool,
22748
+ packageGraphService,
21773
22749
  parseLanguageDiagnostics,
21774
22750
  parsePackageReports,
21775
22751
  parsePrivateOriginAllowlist,
@@ -21800,6 +22776,7 @@ export {
21800
22776
  shutdownCodebaseIndexHost,
21801
22777
  startProcessGuardian,
21802
22778
  stopProcessGuardian,
22779
+ symbolGraphService,
21803
22780
  testTool,
21804
22781
  todoTool,
21805
22782
  toolHelpTool,