@wrongstack/tools 0.287.0 → 0.291.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 (102) 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/auto-proceed-loop-guard.d.ts +128 -0
  7. package/dist/auto-proceed-loop-guard.d.ts.map +1 -0
  8. package/dist/auto-proceed-loop-guard.js +46 -0
  9. package/dist/auto-proceed-loop-guard.js.map +7 -0
  10. package/dist/bash.d.ts +1 -1
  11. package/dist/bash.d.ts.map +1 -1
  12. package/dist/bash.js +97 -19
  13. package/dist/bash.js.map +3 -3
  14. package/dist/builtin.d.ts +12 -10
  15. package/dist/builtin.d.ts.map +1 -1
  16. package/dist/builtin.js +1766 -864
  17. package/dist/builtin.js.map +3 -3
  18. package/dist/codebase-index/background-indexer.d.ts +1 -1
  19. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  20. package/dist/codebase-index/bm25.d.ts +1 -2
  21. package/dist/codebase-index/bm25.d.ts.map +1 -1
  22. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  23. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  24. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  25. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  26. package/dist/codebase-index/index-service.d.ts +11 -1
  27. package/dist/codebase-index/index-service.d.ts.map +1 -1
  28. package/dist/codebase-index/index.d.ts +2 -1
  29. package/dist/codebase-index/index.d.ts.map +1 -1
  30. package/dist/codebase-index/index.js +888 -254
  31. package/dist/codebase-index/index.js.map +3 -3
  32. package/dist/codebase-index/indexer.d.ts.map +1 -1
  33. package/dist/codebase-index/refs-extractor.d.ts +2 -17
  34. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  35. package/dist/codebase-index/schema.d.ts +43 -1
  36. package/dist/codebase-index/schema.d.ts.map +1 -1
  37. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  38. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  39. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  40. package/dist/codebase-index/worker.js +738 -212
  41. package/dist/codebase-index/worker.js.map +3 -3
  42. package/dist/codebase-index/writer.d.ts +60 -1
  43. package/dist/codebase-index/writer.d.ts.map +1 -1
  44. package/dist/e2e.js +477 -0
  45. package/dist/e2e.js.map +7 -0
  46. package/dist/exec.d.ts +1 -1
  47. package/dist/exec.d.ts.map +1 -1
  48. package/dist/exec.js +60 -5
  49. package/dist/exec.js.map +3 -3
  50. package/dist/fetch.js +2 -2
  51. package/dist/fetch.js.map +2 -2
  52. package/dist/format.js +61 -14
  53. package/dist/format.js.map +3 -3
  54. package/dist/glob.js +2 -2
  55. package/dist/glob.js.map +1 -1
  56. package/dist/grep.d.ts.map +1 -1
  57. package/dist/grep.js +2 -2
  58. package/dist/grep.js.map +2 -2
  59. package/dist/index.d.ts +3 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2001 -873
  62. package/dist/index.js.map +3 -3
  63. package/dist/install.js +61 -14
  64. package/dist/install.js.map +3 -3
  65. package/dist/kanban.d.ts +21 -2
  66. package/dist/kanban.d.ts.map +1 -1
  67. package/dist/kanban.js +713 -572
  68. package/dist/kanban.js.map +3 -3
  69. package/dist/languages/detect.d.ts.map +1 -1
  70. package/dist/languages/index.js +61 -14
  71. package/dist/languages/index.js.map +3 -3
  72. package/dist/languages/types.d.ts +7 -0
  73. package/dist/languages/types.d.ts.map +1 -1
  74. package/dist/lint.js +61 -14
  75. package/dist/lint.js.map +3 -3
  76. package/dist/outdated.js +61 -14
  77. package/dist/outdated.js.map +3 -3
  78. package/dist/pack.js +1763 -861
  79. package/dist/pack.js.map +3 -3
  80. package/dist/plan.js +4 -0
  81. package/dist/plan.js.map +2 -2
  82. package/dist/process-registry.js +12 -5
  83. package/dist/process-registry.js.map +2 -2
  84. package/dist/read.d.ts.map +1 -1
  85. package/dist/read.js.map +2 -2
  86. package/dist/search.js +2 -2
  87. package/dist/search.js.map +2 -2
  88. package/dist/session-kanban.d.ts +9 -0
  89. package/dist/session-kanban.d.ts.map +1 -1
  90. package/dist/session-kanban.js +197 -3
  91. package/dist/session-kanban.js.map +2 -2
  92. package/dist/task.js +4 -0
  93. package/dist/task.js.map +2 -2
  94. package/dist/test.js +61 -14
  95. package/dist/test.js.map +3 -3
  96. package/dist/todo.js +4 -0
  97. package/dist/todo.js.map +2 -2
  98. package/dist/tree.js +2 -2
  99. package/dist/tree.js.map +1 -1
  100. package/dist/typecheck.js +61 -14
  101. package/dist/typecheck.js.map +3 -3
  102. package/package.json +13 -5
package/dist/pack.js CHANGED
@@ -337,8 +337,10 @@ function redactCommand(cmd) {
337
337
  const flag = match.slice(0, match.indexOf(expectDefined(delim)) + 1);
338
338
  return `${flag}[REDACTED]`;
339
339
  }
340
- const flagEnd = match.match(/^--?[a-zA-Z][a-zA-Z0-9_-]*/)?.[0] ?? match;
341
- return `${flagEnd}=**redacted**`;
340
+ if (match.startsWith("--")) {
341
+ return match;
342
+ }
343
+ return `${match.slice(0, 2)}[REDACTED]`;
342
344
  });
343
345
  }
344
346
  return result;
@@ -350,9 +352,14 @@ var init_redact_command = __esm({
350
352
  SENSITIVE_FLAG_PATTERNS = [
351
353
  // --flag=value or --flag "value" (value captured up to next space or comma)
352
354
  /--(?: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,
353
- // -f "value" style short flags
354
- /(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
355
- /(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
355
+ // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
356
+ // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
357
+ // NOTE: synced with @wrongstack/core observability/redact-command.ts.
358
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
359
+ // -p|-password|-a (redis auth) short flags: attached + separated + =value.
360
+ // Same token-start anchor; over-redaction is an accepted tradeoff for a
361
+ // redaction function. Synced with core copy.
362
+ /(?<![-\w])-(?:password|p|a)(?:[=\s]+)?[^\s,-]+/gi,
356
363
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
357
364
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
358
365
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
@@ -799,7 +806,12 @@ var init_win32_resolve = __esm({
799
806
 
800
807
  // src/_spawn-stream.ts
801
808
  import { spawn as spawn2 } from "node:child_process";
802
- import { buildChildEnv } from "@wrongstack/core";
809
+ import {
810
+ buildChildEnv,
811
+ emitProcessCompleted,
812
+ emitProcessOutput,
813
+ emitProcessStarted
814
+ } from "@wrongstack/core";
803
815
  async function* spawnStream(opts) {
804
816
  const max = opts.maxBytes ?? 2e5;
805
817
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -824,6 +836,19 @@ async function* spawnStream(opts) {
824
836
  });
825
837
  const registry = getProcessRegistry();
826
838
  const pid = child.pid;
839
+ const processStartedAt = Date.now();
840
+ let stdoutBytes = 0;
841
+ let stderrBytes = 0;
842
+ let telemetryCompleted = false;
843
+ emitProcessStarted({
844
+ ...pid !== void 0 ? { pid } : {},
845
+ parentPid: process.pid,
846
+ command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
847
+ args: redactCommand(opts.args.join(" ")).split(" ").filter(Boolean),
848
+ cwd: opts.cwd,
849
+ background: false,
850
+ startedAt: new Date(processStartedAt).toISOString()
851
+ });
827
852
  if (typeof pid === "number") {
828
853
  registry.register({
829
854
  pid,
@@ -852,6 +877,8 @@ async function* spawnStream(opts) {
852
877
  };
853
878
  const onOut = (c) => {
854
879
  const s = c.toString();
880
+ stdoutBytes += c.byteLength;
881
+ emitProcessOutput({ pid, stream: "stdout", chunk: c });
855
882
  if (stdout.length < max) stdout += s;
856
883
  spool.write(s);
857
884
  queue.push({ kind: "out", data: s });
@@ -864,6 +891,8 @@ async function* spawnStream(opts) {
864
891
  };
865
892
  const onErr = (c) => {
866
893
  const s = c.toString();
894
+ stderrBytes += c.byteLength;
895
+ emitProcessOutput({ pid, stream: "stderr", chunk: c });
867
896
  if (stderr.length < max) stderr += s;
868
897
  spool.write(s);
869
898
  queue.push({ kind: "err", data: s });
@@ -881,9 +910,25 @@ async function* spawnStream(opts) {
881
910
  queue.push({ kind: "error", data: e.message });
882
911
  wake();
883
912
  });
884
- child.on("close", (code) => {
913
+ const completeTelemetry = (code, signal, timedOut = false) => {
914
+ if (telemetryCompleted) return;
915
+ telemetryCompleted = true;
916
+ emitProcessCompleted({
917
+ ...pid !== void 0 ? { pid } : {},
918
+ exitCode: code,
919
+ ...signal ? { signal } : {},
920
+ durationMs: Date.now() - processStartedAt,
921
+ stdoutBytes,
922
+ stderrBytes,
923
+ timedOut,
924
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
925
+ });
926
+ };
927
+ child.on("close", (code, signal) => {
885
928
  if (typeof pid === "number") registry.unregister(pid);
886
- queue.push({ kind: "close", data: "", code: code ?? 0 });
929
+ const exitCode2 = code ?? (signal ? 1 : 0);
930
+ completeTelemetry(exitCode2, signal ?? void 0);
931
+ queue.push({ kind: "close", data: "", code: exitCode2, ...signal ? { signal } : {} });
887
932
  wake();
888
933
  });
889
934
  const onAbort = () => {
@@ -896,6 +941,7 @@ async function* spawnStream(opts) {
896
941
  }
897
942
  }
898
943
  queue.push({ kind: "close", data: "", code: 124 });
944
+ completeTelemetry(124, "SIGKILL", true);
899
945
  wake();
900
946
  };
901
947
  if (isWin) {
@@ -2503,13 +2549,14 @@ async function detectLanguageWorkspaces(options) {
2503
2549
  const target = options.target ? await canonicalInside(resolveFrom(cwd, options.target), projectRoot, "target") : void 0;
2504
2550
  const profiles = (options.profiles ?? languageProfileRegistry.list()).filter((profile) => !options.language || profile.id === options.language).slice().sort((a, b) => a.id.localeCompare(b.id));
2505
2551
  const limits = normalizeLimits(options.limits);
2552
+ const extraIgnores = new Set(options.ignoredDirectories ?? []);
2506
2553
  const state = {
2507
2554
  entries: 0,
2508
2555
  truncated: false,
2509
2556
  sourcePaths: /* @__PURE__ */ new Map(),
2510
2557
  candidates: /* @__PURE__ */ new Map()
2511
2558
  };
2512
- await scanDirectory(projectRoot, 0, profiles, limits, state, options.signal);
2559
+ await scanDirectory(projectRoot, 0, profiles, limits, state, extraIgnores, options.signal);
2513
2560
  addSourceFallbacks(projectRoot, profiles, state);
2514
2561
  if (target) addTargetEvidence(target, projectRoot, profiles, state);
2515
2562
  const workspaces = await Promise.all(
@@ -2523,7 +2570,7 @@ async function detectLanguageWorkspaces(options) {
2523
2570
  workspaces
2524
2571
  };
2525
2572
  }
2526
- async function scanDirectory(directory, depth, profiles, limits, state, signal) {
2573
+ async function scanDirectory(directory, depth, profiles, limits, state, extraIgnores, signal) {
2527
2574
  signal?.throwIfAborted();
2528
2575
  if (depth > limits.maxDepth || state.entries >= limits.maxEntries) {
2529
2576
  state.truncated = true;
@@ -2546,20 +2593,20 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
2546
2593
  const fullPath = path4.join(directory, entry.name);
2547
2594
  if (entry.isSymbolicLink()) continue;
2548
2595
  if (entry.isDirectory()) {
2549
- if (shouldIgnoreDirectory(entry.name, profiles)) continue;
2596
+ if (shouldIgnoreDirectory(entry.name, profiles, extraIgnores)) continue;
2550
2597
  if (depth >= limits.maxDepth) {
2551
2598
  state.truncated = true;
2552
2599
  continue;
2553
2600
  }
2554
- await scanDirectory(fullPath, depth + 1, profiles, limits, state, signal);
2601
+ await scanDirectory(fullPath, depth + 1, profiles, limits, state, extraIgnores, signal);
2555
2602
  continue;
2556
2603
  }
2557
2604
  if (!entry.isFile()) continue;
2558
2605
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2559
2606
  }
2560
2607
  }
2561
- function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2562
- const lower = basename8.toLowerCase();
2608
+ function collectFileEvidence(directory, fullPath, basename9, profiles, state) {
2609
+ const lower = basename9.toLowerCase();
2563
2610
  const extension = path4.extname(lower);
2564
2611
  for (const profile of profiles) {
2565
2612
  const detector = profile.detectors.find(
@@ -2570,7 +2617,7 @@ function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2570
2617
  candidate.evidence.push({
2571
2618
  kind: detector.kind,
2572
2619
  path: fullPath,
2573
- value: basename8,
2620
+ value: basename9,
2574
2621
  weight: detector.weight
2575
2622
  });
2576
2623
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2680,8 +2727,8 @@ function getCandidate(state, profile, root) {
2680
2727
  }
2681
2728
  return candidate;
2682
2729
  }
2683
- function shouldIgnoreDirectory(name, profiles) {
2684
- if (GLOBAL_IGNORES.has(name) || name.startsWith(".")) return true;
2730
+ function shouldIgnoreDirectory(name, profiles, extraIgnores) {
2731
+ if (GLOBAL_IGNORES.has(name) || extraIgnores.has(name) || name.startsWith(".")) return true;
2685
2732
  return profiles.some((profile) => profile.ignoredDirectories.includes(name));
2686
2733
  }
2687
2734
  function normalizeLimits(input) {
@@ -4897,6 +4944,11 @@ function parseAuditOutput(json2, exitCode) {
4897
4944
  // src/bash.ts
4898
4945
  import { spawn as spawn3 } from "node:child_process";
4899
4946
  import * as os4 from "node:os";
4947
+ import {
4948
+ emitProcessCompleted as emitProcessCompleted2,
4949
+ emitProcessOutput as emitProcessOutput2,
4950
+ emitProcessStarted as emitProcessStarted2
4951
+ } from "@wrongstack/core";
4900
4952
 
4901
4953
  // src/_env.ts
4902
4954
  import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core";
@@ -5766,6 +5818,32 @@ var bashTool = {
5766
5818
  }
5767
5819
  }
5768
5820
  const pid2 = child2.pid;
5821
+ let stdoutBytes2 = 0;
5822
+ let stderrBytes2 = 0;
5823
+ let telemetryCompleted2 = false;
5824
+ emitProcessStarted2({
5825
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
5826
+ parentPid: process.pid,
5827
+ command: redactCommand(`${shell} ${args.join(" ")}`),
5828
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
5829
+ cwd: ctx.projectRoot,
5830
+ background: true,
5831
+ startedAt: new Date(startedAt).toISOString()
5832
+ });
5833
+ const completeBackground = (exitCode, signal) => {
5834
+ if (telemetryCompleted2) return;
5835
+ telemetryCompleted2 = true;
5836
+ emitProcessCompleted2({
5837
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
5838
+ exitCode,
5839
+ ...signal ? { signal } : {},
5840
+ durationMs: Date.now() - startedAt,
5841
+ stdoutBytes: stdoutBytes2,
5842
+ stderrBytes: stderrBytes2,
5843
+ timedOut: false,
5844
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
5845
+ });
5846
+ };
5769
5847
  if (typeof pid2 === "number") {
5770
5848
  registry.register({
5771
5849
  pid: pid2,
@@ -5778,7 +5856,10 @@ var bashTool = {
5778
5856
  });
5779
5857
  child2.on("close", () => registry.unregister(pid2));
5780
5858
  }
5781
- const onBgData = (chunk) => {
5859
+ const onBgData = (chunk, stream) => {
5860
+ if (stream === "stdout") stdoutBytes2 += chunk.byteLength;
5861
+ else stderrBytes2 += chunk.byteLength;
5862
+ emitProcessOutput2({ pid: pid2, stream, chunk });
5782
5863
  if (truncated) return;
5783
5864
  const remain = MAX_OUTPUT - buf2.length;
5784
5865
  if (remain > 0) {
@@ -5786,24 +5867,28 @@ var bashTool = {
5786
5867
  }
5787
5868
  if (buf2.length >= MAX_OUTPUT) {
5788
5869
  truncated = true;
5789
- child2.stdout?.off("data", onBgData);
5790
- child2.stderr?.off("data", onBgData);
5870
+ child2.stdout?.off("data", onBgStdout);
5871
+ child2.stderr?.off("data", onBgStderr);
5791
5872
  }
5792
5873
  };
5793
- child2.stdout?.on("data", onBgData);
5794
- child2.stderr?.on("data", onBgData);
5874
+ const onBgStdout = (chunk) => onBgData(chunk, "stdout");
5875
+ const onBgStderr = (chunk) => onBgData(chunk, "stderr");
5876
+ child2.stdout?.on("data", onBgStdout);
5877
+ child2.stderr?.on("data", onBgStderr);
5795
5878
  const cleanupBackground = () => {
5796
- child2.stdout?.off("data", onBgData);
5797
- child2.stderr?.off("data", onBgData);
5879
+ child2.stdout?.off("data", onBgStdout);
5880
+ child2.stderr?.off("data", onBgStderr);
5798
5881
  };
5799
5882
  child2.on("error", () => {
5800
5883
  cleanupBackground();
5801
5884
  if (typeof pid2 === "number") registry.unregister(pid2);
5802
5885
  registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
5886
+ completeBackground(1);
5803
5887
  });
5804
- child2.on("close", () => {
5888
+ child2.on("close", (code, signal) => {
5805
5889
  cleanupBackground();
5806
5890
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
5891
+ completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
5807
5892
  });
5808
5893
  if (typeof pid2 === "number") child2.unref();
5809
5894
  yield {
@@ -5843,6 +5928,32 @@ var bashTool = {
5843
5928
  }
5844
5929
  }
5845
5930
  const pid = child.pid;
5931
+ let stdoutBytes = 0;
5932
+ let stderrBytes = 0;
5933
+ let telemetryCompleted = false;
5934
+ emitProcessStarted2({
5935
+ ...pid !== void 0 ? { pid } : {},
5936
+ parentPid: process.pid,
5937
+ command: redactCommand(`${shell} ${args.join(" ")}`),
5938
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
5939
+ cwd: ctx.projectRoot,
5940
+ background: false,
5941
+ startedAt: new Date(startedAt).toISOString()
5942
+ });
5943
+ const completeForeground = (exitCode, signal) => {
5944
+ if (telemetryCompleted) return;
5945
+ telemetryCompleted = true;
5946
+ emitProcessCompleted2({
5947
+ ...pid !== void 0 ? { pid } : {},
5948
+ exitCode,
5949
+ ...signal ? { signal } : {},
5950
+ durationMs: Date.now() - startedAt,
5951
+ stdoutBytes,
5952
+ stderrBytes,
5953
+ timedOut,
5954
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
5955
+ });
5956
+ };
5846
5957
  if (typeof pid === "number") {
5847
5958
  registry.register({
5848
5959
  pid,
@@ -5944,8 +6055,11 @@ var bashTool = {
5944
6055
  child.stderr?.resume();
5945
6056
  }
5946
6057
  };
5947
- const onData = (chunk) => {
6058
+ const onData = (chunk, stream) => {
5948
6059
  const text = chunk.toString();
6060
+ if (stream === "stdout") stdoutBytes += chunk.byteLength;
6061
+ else stderrBytes += chunk.byteLength;
6062
+ emitProcessOutput2({ pid, stream, chunk });
5949
6063
  if (buf.length < MAX_OUTPUT) {
5950
6064
  buf += text.slice(0, MAX_OUTPUT - buf.length);
5951
6065
  }
@@ -5954,17 +6068,21 @@ var bashTool = {
5954
6068
  push({ kind: "data", text });
5955
6069
  pauseIfFlooded();
5956
6070
  };
5957
- child.stdout?.on("data", onData);
5958
- child.stderr?.on("data", onData);
6071
+ const onStdoutData = (chunk) => onData(chunk, "stdout");
6072
+ const onStderrData = (chunk) => onData(chunk, "stderr");
6073
+ child.stdout?.on("data", onStdoutData);
6074
+ child.stderr?.on("data", onStderrData);
5959
6075
  child.on("error", (err) => {
5960
6076
  for (const t of timers) clearTimeout(t);
5961
6077
  registry.afterCall(Date.now() - startedAt, true);
6078
+ completeForeground(1);
5962
6079
  push({ kind: "error", err });
5963
6080
  });
5964
- child.on("close", (code) => {
6081
+ child.on("close", (code, signal) => {
5965
6082
  for (const t of timers) clearTimeout(t);
5966
6083
  if (typeof pid === "number") registry.unregister(pid);
5967
6084
  registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
6085
+ completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
5968
6086
  push({ kind: "end", code });
5969
6087
  });
5970
6088
  try {
@@ -6009,8 +6127,8 @@ ${hint}` : ""),
6009
6127
  for (const t of timers) clearTimeout(t);
6010
6128
  spool.finalize();
6011
6129
  if (isWin4) opts.signal.removeEventListener("abort", onAbort);
6012
- child.stdout?.off("data", onData);
6013
- child.stderr?.off("data", onData);
6130
+ child.stdout?.off("data", onStdoutData);
6131
+ child.stderr?.off("data", onStderrData);
6014
6132
  child.stdout?.destroy();
6015
6133
  child.stderr?.destroy();
6016
6134
  if (child.exitCode === null && !child.killed) {
@@ -7500,7 +7618,7 @@ import * as path13 from "node:path";
7500
7618
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
7501
7619
 
7502
7620
  // src/codebase-index/schema.ts
7503
- var SCHEMA_VERSION = 2;
7621
+ var SCHEMA_VERSION = 3;
7504
7622
 
7505
7623
  // src/codebase-index/lsp-kind.ts
7506
7624
  function lspKindToInternalKind(k) {
@@ -7543,7 +7661,7 @@ function tokenise(text) {
7543
7661
  return sanitised.toLowerCase().split(" ").filter(Boolean);
7544
7662
  }
7545
7663
  function splitName(name) {
7546
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
7664
+ 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();
7547
7665
  }
7548
7666
  function buildIndexableText(name, signature, docComment) {
7549
7667
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -7569,14 +7687,12 @@ function buildBm25Index(docs) {
7569
7687
  return new Bm25Index(documents, df, N, avgLen);
7570
7688
  }
7571
7689
  var Bm25Index = class {
7572
- constructor(documents, df, N, avgLen) {
7690
+ constructor(documents, _df, N, avgLen) {
7573
7691
  this.documents = documents;
7574
- this.df = df;
7575
7692
  this.N = N;
7576
7693
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
7577
7694
  }
7578
7695
  documents;
7579
- df;
7580
7696
  N;
7581
7697
  safeAvgLen;
7582
7698
  score(query, filter) {
@@ -7589,10 +7705,13 @@ var Bm25Index = class {
7589
7705
  for (const qTerm of qTokens) {
7590
7706
  let tf = 0;
7591
7707
  for (const t of doc.tokens) {
7592
- if (t === qTerm) tf++;
7708
+ if (t.startsWith(qTerm)) tf++;
7593
7709
  }
7594
7710
  if (tf === 0) continue;
7595
- const dfVal = this.df[qTerm] ?? 0;
7711
+ let dfVal = 0;
7712
+ for (const candidate of this.documents) {
7713
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
7714
+ }
7596
7715
  if (dfVal === 0) continue;
7597
7716
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
7598
7717
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -7625,6 +7744,29 @@ var Bm25Index = class {
7625
7744
 
7626
7745
  // src/codebase-index/writer.ts
7627
7746
  var DB_FILE = "index.db";
7747
+ function escapeLike(value) {
7748
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
7749
+ }
7750
+ function assignRefsToSymbols(refs, symbols) {
7751
+ if (refs.length === 0 || symbols.length === 0) return [];
7752
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
7753
+ const seen = /* @__PURE__ */ new Set();
7754
+ const assigned = [];
7755
+ for (const ref of refs) {
7756
+ let owner2;
7757
+ for (const symbol of ordered) {
7758
+ if (symbol.line > ref.line) break;
7759
+ owner2 = symbol;
7760
+ }
7761
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
7762
+ if (!owner2 || owner2.id <= 0) continue;
7763
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
7764
+ if (seen.has(key)) continue;
7765
+ seen.add(key);
7766
+ assigned.push({ ...ref, fromId: owner2.id });
7767
+ }
7768
+ return assigned;
7769
+ }
7628
7770
  function resolveIndexDir(projectRoot, override) {
7629
7771
  return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
7630
7772
  }
@@ -7632,6 +7774,53 @@ function codebaseIndexDirOverride(ctx) {
7632
7774
  const v = ctx.meta?.["codebaseIndexDir"];
7633
7775
  return typeof v === "string" ? v : void 0;
7634
7776
  }
7777
+ var StorePool = class {
7778
+ stores = /* @__PURE__ */ new Map();
7779
+ key(projectRoot, indexDir) {
7780
+ return `${projectRoot}\0${indexDir ?? ""}`;
7781
+ }
7782
+ /** Borrow a store. Creates it on first access for this key. */
7783
+ acquire(projectRoot, opts) {
7784
+ const k = this.key(projectRoot, opts?.indexDir);
7785
+ let store = this.stores.get(k);
7786
+ if (!store) {
7787
+ store = new IndexStore(projectRoot, { indexDir: opts?.indexDir });
7788
+ this.stores.set(k, store);
7789
+ }
7790
+ return store;
7791
+ }
7792
+ /** Return the store to the pool. The connection stays warm for subsequent
7793
+ * operations on the same (projectRoot, indexDir). */
7794
+ release(_store) {
7795
+ }
7796
+ /** Close every pooled connection and drain the pool. Call on shutdown. */
7797
+ closeAll() {
7798
+ for (const store of this.stores.values()) {
7799
+ try {
7800
+ store.close();
7801
+ } catch {
7802
+ }
7803
+ }
7804
+ this.stores.clear();
7805
+ }
7806
+ /** Remove one store from the pool. Used by tests that need isolation. */
7807
+ evict(projectRoot, indexDir) {
7808
+ const k = this.key(projectRoot, indexDir);
7809
+ const store = this.stores.get(k);
7810
+ if (store) {
7811
+ try {
7812
+ store.close();
7813
+ } catch {
7814
+ }
7815
+ this.stores.delete(k);
7816
+ }
7817
+ }
7818
+ /** True when the pool holds a connection for the given key. */
7819
+ has(projectRoot, indexDir) {
7820
+ return this.stores.has(this.key(projectRoot, indexDir));
7821
+ }
7822
+ };
7823
+ var indexStorePool = new StorePool();
7635
7824
  var warningSilenced = false;
7636
7825
  function silenceSqliteExperimentalWarning() {
7637
7826
  if (warningSilenced) return;
@@ -7678,7 +7867,7 @@ function sleepSync(ms) {
7678
7867
  } catch {
7679
7868
  }
7680
7869
  }
7681
- var IndexStore = class {
7870
+ var IndexStore = class _IndexStore {
7682
7871
  db;
7683
7872
  /** Absolute path to this project's index directory. */
7684
7873
  indexDir;
@@ -7730,10 +7919,7 @@ var IndexStore = class {
7730
7919
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
7731
7920
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
7732
7921
  }
7733
- const delay = Math.min(
7734
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
7735
- LOCK_RETRY_MAX_DELAY_MS
7736
- );
7922
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
7737
7923
  sleepSync(delay);
7738
7924
  }
7739
7925
  }
@@ -7815,8 +8001,24 @@ var IndexStore = class {
7815
8001
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
7816
8002
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
7817
8003
  try {
7818
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
8004
+ this.db.exec(
8005
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
8006
+ );
7819
8007
  this.ftsAvailable = true;
8008
+ const symbolCount = Number(
8009
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8010
+ );
8011
+ const ftsCount = Number(
8012
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8013
+ );
8014
+ if (symbolCount !== ftsCount) {
8015
+ this.db.exec("DELETE FROM symbols_fts");
8016
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
8017
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8018
+ for (const row of rows) {
8019
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
8020
+ }
8021
+ }
7820
8022
  } catch {
7821
8023
  this.ftsAvailable = false;
7822
8024
  }
@@ -7836,7 +8038,7 @@ var IndexStore = class {
7836
8038
  return this.runWithRetry(() => {
7837
8039
  this.db.exec("BEGIN IMMEDIATE");
7838
8040
  try {
7839
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8041
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
7840
8042
  let nextId = (maxRows[0]?.m ?? 0) + 1;
7841
8043
  const stmt = this.db.prepare(
7842
8044
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -7874,7 +8076,9 @@ var IndexStore = class {
7874
8076
  deleteSymbolsForFile(file) {
7875
8077
  this.runWithRetry(() => {
7876
8078
  if (this.ftsAvailable) {
7877
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8079
+ this.stmt(
8080
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8081
+ ).run(file);
7878
8082
  }
7879
8083
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7880
8084
  });
@@ -7886,9 +8090,21 @@ var IndexStore = class {
7886
8090
  */
7887
8091
  deleteFile(file) {
7888
8092
  this.runWithRetry(() => {
7889
- this.deleteRefsForFile(file);
7890
- this.deleteSymbolsForFile(file);
7891
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
8093
+ this.db.exec("BEGIN IMMEDIATE");
8094
+ try {
8095
+ if (this.ftsAvailable) {
8096
+ this.db.prepare(
8097
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8098
+ ).run(file);
8099
+ }
8100
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8101
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
8102
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
8103
+ this.db.exec("COMMIT");
8104
+ } catch (err) {
8105
+ this.db.exec("ROLLBACK");
8106
+ throw err;
8107
+ }
7892
8108
  });
7893
8109
  }
7894
8110
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -7911,14 +8127,22 @@ var IndexStore = class {
7911
8127
  ).all(file);
7912
8128
  if (!rows.length) return null;
7913
8129
  const r = expectDefined2(rows[0]);
7914
- return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
8130
+ return {
8131
+ file: r.file,
8132
+ lang: r.lang,
8133
+ mtimeMs: r.mtime_ms,
8134
+ symbolCount: r.symbol_count,
8135
+ lastIndexed: r.last_indexed
8136
+ };
7915
8137
  }
7916
8138
  getAllFileMetas() {
7917
- return this.db.prepare(
7918
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
7919
- ).all().map(
7920
- (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
7921
- );
8139
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8140
+ file: r.file,
8141
+ lang: r.lang,
8142
+ mtimeMs: r.mtime_ms,
8143
+ symbolCount: r.symbol_count,
8144
+ lastIndexed: r.last_indexed
8145
+ }));
7922
8146
  }
7923
8147
  // ─── Search ──────────────────────────────────────────────────────────────────
7924
8148
  search(query, filter) {
@@ -7942,8 +8166,8 @@ var IndexStore = class {
7942
8166
  values.push(filter.lang);
7943
8167
  }
7944
8168
  if (filter?.file) {
7945
- conditions.push("file LIKE ?");
7946
- values.push(`%${filter.file}%`);
8169
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
8170
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
7947
8171
  }
7948
8172
  if (query.trim()) {
7949
8173
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -7981,9 +8205,11 @@ var IndexStore = class {
7981
8205
  * "users", camelCase-split text makes "complex" find "complexOperation").
7982
8206
  */
7983
8207
  searchRanked(query, filter, limit) {
8208
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
8209
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
7984
8210
  const tokens = tokenise(query);
7985
8211
  if (tokens.length === 0 || !this.ftsAvailable) {
7986
- return this.searchRankedFallback(query, filter, limit);
8212
+ return this.searchRankedFallback(query, filter, safeLimit);
7987
8213
  }
7988
8214
  let effectiveKind = filter?.kind;
7989
8215
  if (filter?.lspKind !== void 0) {
@@ -8003,11 +8229,13 @@ var IndexStore = class {
8003
8229
  values.push(filter.lang);
8004
8230
  }
8005
8231
  if (filter?.file) {
8006
- conditions.push("s.file LIKE ?");
8007
- values.push(`%${filter.file}%`);
8232
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
8233
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8008
8234
  }
8009
8235
  const where = conditions.join(" AND ");
8010
- 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);
8236
+ const countRows = this.db.prepare(
8237
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8238
+ ).all(...values);
8011
8239
  const total = countRows[0] ? Number(countRows[0].n) : 0;
8012
8240
  if (total === 0) return { results: [], total: 0 };
8013
8241
  const rows = this.db.prepare(
@@ -8016,9 +8244,13 @@ var IndexStore = class {
8016
8244
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
8017
8245
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
8018
8246
  WHERE ${where}
8019
- ORDER BY bm25(symbols_fts)
8247
+ ORDER BY
8248
+ CASE WHEN lower(s.name) = lower(?) THEN 0
8249
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
8250
+ ELSE 2 END,
8251
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
8020
8252
  LIMIT ?`
8021
- ).all(...values, limit);
8253
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
8022
8254
  return {
8023
8255
  results: rows.map((r) => ({
8024
8256
  id: r.id,
@@ -8048,10 +8280,28 @@ var IndexStore = class {
8048
8280
  }
8049
8281
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8050
8282
  const bm25 = buildBm25Index(
8051
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
8283
+ candidates.map((c) => ({
8284
+ id: c.id,
8285
+ text: buildIndexableText(c.name, c.signature, c.docComment)
8286
+ }))
8052
8287
  );
8053
8288
  const scored = bm25.score(query, (id) => candidateById.has(id));
8054
- scored.sort((a, b) => b.score - a.score);
8289
+ const q = query.trim().toLowerCase();
8290
+ const rank = (id) => {
8291
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
8292
+ if (name === q) return 0;
8293
+ if (name.startsWith(q)) return 1;
8294
+ return 2;
8295
+ };
8296
+ scored.sort((a, b) => {
8297
+ const rankDiff = rank(a.id) - rank(b.id);
8298
+ if (rankDiff !== 0) return rankDiff;
8299
+ const scoreDiff = b.score - a.score;
8300
+ if (scoreDiff !== 0) return scoreDiff;
8301
+ const left = expectDefined2(candidateById.get(a.id));
8302
+ const right = expectDefined2(candidateById.get(b.id));
8303
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
8304
+ });
8055
8305
  const qTokens = tokenise(query);
8056
8306
  const results = scored.slice(0, limit).map(({ id, score }) => {
8057
8307
  const c = expectDefined2(candidateById.get(id));
@@ -8060,9 +8310,7 @@ var IndexStore = class {
8060
8310
  return { results, total: candidates.length };
8061
8311
  }
8062
8312
  getAllIndexable() {
8063
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
8064
- ({ id, text }) => ({ id, text })
8065
- );
8313
+ return this.stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8066
8314
  }
8067
8315
  /**
8068
8316
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -8072,28 +8320,22 @@ var IndexStore = class {
8072
8320
  * `symbols.id`). Ids may have gaps — that is fine.
8073
8321
  */
8074
8322
  getMaxSymbolId() {
8075
- const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8323
+ const rows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8076
8324
  return rows[0]?.m ?? 0;
8077
8325
  }
8078
8326
  // ─── Stats ───────────────────────────────────────────────────────────────────
8079
8327
  getStats() {
8080
8328
  const sizeBytes = this.sizeBytes();
8081
- const lastRows = this.db.prepare(
8082
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
8083
- ).all();
8329
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8084
8330
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8085
- const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
8331
+ const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
8086
8332
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
8087
- const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
8333
+ const fileRows = this.stmt("SELECT COUNT(*) FROM files").all();
8088
8334
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
8089
- const langRows = this.db.prepare(
8090
- "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
8091
- ).all();
8335
+ const langRows = this.stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8092
8336
  const byLang = {};
8093
8337
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
8094
- const kindRows = this.db.prepare(
8095
- "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
8096
- ).all();
8338
+ const kindRows = this.stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8097
8339
  const byKind = {};
8098
8340
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
8099
8341
  return {
@@ -8109,17 +8351,34 @@ var IndexStore = class {
8109
8351
  }
8110
8352
  setLastIndexed(ts2) {
8111
8353
  this.runWithRetry(() => {
8112
- this.db.prepare(
8113
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
8114
- ).run(String(ts2));
8354
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8355
+ });
8356
+ }
8357
+ getMetadata(key) {
8358
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
8359
+ return rows[0]?.value;
8360
+ }
8361
+ setMetadata(key, value) {
8362
+ this.runWithRetry(() => {
8363
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8115
8364
  });
8116
8365
  }
8117
8366
  clearAll() {
8118
8367
  this.runWithRetry(() => {
8119
- this.db.exec("DELETE FROM symbols");
8120
- this.db.exec("DELETE FROM files");
8121
- this.db.exec("DELETE FROM refs");
8122
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8368
+ this.db.exec("BEGIN IMMEDIATE");
8369
+ try {
8370
+ this.db.exec("DROP TABLE IF EXISTS refs");
8371
+ this.db.exec("DROP TABLE IF EXISTS symbols");
8372
+ this.db.exec("DROP TABLE IF EXISTS files");
8373
+ this.db.exec("DROP TABLE IF EXISTS metadata");
8374
+ if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
8375
+ this.db.exec("COMMIT");
8376
+ this.stmtCache.clear();
8377
+ this.initSchema();
8378
+ } catch (err) {
8379
+ this.db.exec("ROLLBACK");
8380
+ throw err;
8381
+ }
8123
8382
  });
8124
8383
  }
8125
8384
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -8200,7 +8459,7 @@ var IndexStore = class {
8200
8459
  ).run(...options.deleteForFiles);
8201
8460
  this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8202
8461
  }
8203
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8462
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8204
8463
  let nextId = (maxRows[0]?.m ?? 0) + 1;
8205
8464
  const symStmt = this.db.prepare(
8206
8465
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -8210,6 +8469,7 @@ var IndexStore = class {
8210
8469
  const allInserted = [];
8211
8470
  const refsToInsert = [];
8212
8471
  for (const entry of entries) {
8472
+ const insertedForEntry = [];
8213
8473
  for (const s of entry.symbols) {
8214
8474
  const id = nextId++;
8215
8475
  symStmt.run(
@@ -8227,9 +8487,11 @@ var IndexStore = class {
8227
8487
  s.file
8228
8488
  );
8229
8489
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8230
- allInserted.push({ ...s, id });
8490
+ const inserted = { ...s, id };
8491
+ allInserted.push(inserted);
8492
+ insertedForEntry.push(inserted);
8231
8493
  }
8232
- for (const r of entry.refs) refsToInsert.push(r);
8494
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8233
8495
  }
8234
8496
  if (refsToInsert.length > 0) {
8235
8497
  const refStmt = this.db.prepare(
@@ -8267,9 +8529,7 @@ var IndexStore = class {
8267
8529
  */
8268
8530
  deleteRefsForFile(file) {
8269
8531
  this.runWithRetry(() => {
8270
- this.db.prepare(
8271
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
8272
- ).run(file);
8532
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8273
8533
  });
8274
8534
  }
8275
8535
  /**
@@ -8310,9 +8570,7 @@ var IndexStore = class {
8310
8570
  * Find all references FROM a given symbol (what does this symbol call/use?).
8311
8571
  */
8312
8572
  findRefsFrom(symbolId) {
8313
- return this.db.prepare(
8314
- "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
8315
- ).all(symbolId).map((r) => ({
8573
+ return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
8316
8574
  id: r.id,
8317
8575
  fromId: r.from_id,
8318
8576
  toName: r.to_name,
@@ -8329,6 +8587,356 @@ var IndexStore = class {
8329
8587
  return 0;
8330
8588
  }
8331
8589
  }
8590
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
8591
+ /**
8592
+ * Derive a monorepo package name from an absolute file path.
8593
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
8594
+ * Returns `undefined` for files outside any recognisable package structure.
8595
+ */
8596
+ static derivePackage(filePath) {
8597
+ const f = filePath.replace(/\\/g, "/");
8598
+ const pkgsIdx = f.indexOf("/packages/");
8599
+ if (pkgsIdx !== -1) {
8600
+ const rest = f.slice(pkgsIdx + "/packages/".length);
8601
+ const seg = rest.split("/")[0];
8602
+ return seg ? `@wrongstack/${seg}` : void 0;
8603
+ }
8604
+ const appsIdx = f.indexOf("/apps/");
8605
+ if (appsIdx !== -1) {
8606
+ const rest = f.slice(appsIdx + "/apps/".length);
8607
+ const seg = rest.split("/")[0];
8608
+ return seg ? `app:${seg}` : void 0;
8609
+ }
8610
+ return void 0;
8611
+ }
8612
+ static packageFromImport(moduleName) {
8613
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
8614
+ const parts = moduleName.split("/");
8615
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
8616
+ }
8617
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
8618
+ if (!moduleName.startsWith(".")) return void 0;
8619
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
8620
+ const absolute = path13.posix.normalize(
8621
+ path13.posix.join(path13.posix.dirname(normalizedFrom), moduleName)
8622
+ );
8623
+ const extension = path13.posix.extname(absolute);
8624
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
8625
+ const candidates = [
8626
+ absolute,
8627
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
8628
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(absolute, `index${ext}`)),
8629
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(base, `index${ext}`))
8630
+ ];
8631
+ const indexedByPortablePath = new Map(
8632
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
8633
+ );
8634
+ for (const candidate of candidates) {
8635
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
8636
+ if (indexed) return indexed;
8637
+ }
8638
+ return void 0;
8639
+ }
8640
+ /**
8641
+ * Package-level graph: each workspace package is a node; edges are derived
8642
+ * from cross-package symbol references (a symbol in package A references a
8643
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
8644
+ */
8645
+ getPackageGraph() {
8646
+ const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
8647
+ const pkgNodes = /* @__PURE__ */ new Map();
8648
+ const fileToPkg = /* @__PURE__ */ new Map();
8649
+ const symbolToPkg = /* @__PURE__ */ new Map();
8650
+ for (const s of symbols) {
8651
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
8652
+ fileToPkg.set(s.file, pkg);
8653
+ symbolToPkg.set(s.id, pkg);
8654
+ const node = pkgNodes.get(pkg);
8655
+ if (node) {
8656
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
8657
+ } else {
8658
+ pkgNodes.set(pkg, {
8659
+ id: `pkg:${pkg}`,
8660
+ label: pkg,
8661
+ kind: "package",
8662
+ package: pkg,
8663
+ symbolCount: 1,
8664
+ fileCount: 0
8665
+ });
8666
+ }
8667
+ }
8668
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
8669
+ for (const { file } of files) {
8670
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
8671
+ const node = pkgNodes.get(pkg);
8672
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
8673
+ }
8674
+ const refRows = this.db.prepare(
8675
+ `SELECT r.from_id, r.to_id, r.call_type
8676
+ FROM refs r
8677
+ WHERE r.to_id IS NOT NULL`
8678
+ ).all();
8679
+ const edgeMap = /* @__PURE__ */ new Map();
8680
+ for (const r of refRows) {
8681
+ if (r.call_type === "import") continue;
8682
+ const fromPkg = symbolToPkg.get(r.from_id);
8683
+ const toPkg = symbolToPkg.get(r.to_id);
8684
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
8685
+ const key = `${fromPkg}\0${toPkg}`;
8686
+ let e = edgeMap.get(key);
8687
+ if (!e) {
8688
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8689
+ edgeMap.set(key, e);
8690
+ }
8691
+ e.weight++;
8692
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8693
+ }
8694
+ const importRows = this.db.prepare(
8695
+ `SELECT r.from_id, r.to_name
8696
+ FROM refs r
8697
+ WHERE r.call_type = 'import'`
8698
+ ).all();
8699
+ for (const r of importRows) {
8700
+ const fromPkg = symbolToPkg.get(r.from_id);
8701
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
8702
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
8703
+ const key = `${fromPkg}\0${toPkg}`;
8704
+ let edge = edgeMap.get(key);
8705
+ if (!edge) {
8706
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8707
+ edgeMap.set(key, edge);
8708
+ }
8709
+ edge.weight++;
8710
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
8711
+ }
8712
+ const edges = [];
8713
+ for (const [key, e] of edgeMap) {
8714
+ const [source, target] = key.split("\0");
8715
+ let bestType = "call";
8716
+ let bestCount = 0;
8717
+ for (const [t, c] of e.types) {
8718
+ if (c > bestCount) {
8719
+ bestType = t;
8720
+ bestCount = c;
8721
+ }
8722
+ }
8723
+ edges.push({
8724
+ source: `pkg:${source}`,
8725
+ target: `pkg:${target}`,
8726
+ weight: e.weight,
8727
+ refType: bestType
8728
+ });
8729
+ }
8730
+ return { nodes: [...pkgNodes.values()], edges };
8731
+ }
8732
+ /**
8733
+ * File-level graph for a single package: each file is a node; edges are
8734
+ * derived from cross-file symbol references within the package.
8735
+ */
8736
+ getFileGraph(packageFilter) {
8737
+ const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
8738
+ const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
8739
+ const localFiles = new Set(pkgFilePaths);
8740
+ if (localFiles.size === 0) return { nodes: [], edges: [] };
8741
+ const filePlaceholders = [...localFiles].map(() => "?").join(",");
8742
+ const pkgSyms = this.db.prepare(
8743
+ `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
8744
+ ).all(...pkgFilePaths);
8745
+ const fileNodes = /* @__PURE__ */ new Map();
8746
+ const symToFile = /* @__PURE__ */ new Map();
8747
+ const fileStats = /* @__PURE__ */ new Map();
8748
+ for (const s of pkgSyms) {
8749
+ symToFile.set(s.id, s.file);
8750
+ const current = fileStats.get(s.file);
8751
+ fileStats.set(s.file, {
8752
+ count: (current?.count ?? 0) + 1,
8753
+ lang: current?.lang ?? s.lang
8754
+ });
8755
+ }
8756
+ const ensureFileNode = (file) => {
8757
+ if (fileNodes.has(file)) return;
8758
+ const stats = fileStats.get(file);
8759
+ fileNodes.set(file, {
8760
+ id: `file:${file}`,
8761
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
8762
+ kind: "file",
8763
+ package: _IndexStore.derivePackage(file) ?? "(root)",
8764
+ file,
8765
+ symbolCount: stats?.count ?? 0,
8766
+ lang: stats?.lang,
8767
+ external: !localFiles.has(file)
8768
+ });
8769
+ };
8770
+ for (const file of localFiles) {
8771
+ ensureFileNode(file);
8772
+ }
8773
+ const indexedFiles = new Set(allFiles.map((f) => f.file));
8774
+ const refRows = this.db.prepare(
8775
+ `SELECT r.from_id, r.to_id, r.call_type
8776
+ FROM refs r
8777
+ WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
8778
+ OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
8779
+ AND r.to_id IS NOT NULL`
8780
+ ).all(...pkgFilePaths, ...pkgFilePaths);
8781
+ const knownSymIds = new Set(pkgSyms.map((s) => s.id));
8782
+ const crossRefIds = /* @__PURE__ */ new Set();
8783
+ for (const r of refRows) {
8784
+ if (!knownSymIds.has(r.from_id)) crossRefIds.add(r.from_id);
8785
+ if (!knownSymIds.has(r.to_id)) crossRefIds.add(r.to_id);
8786
+ }
8787
+ if (crossRefIds.size > 0) {
8788
+ const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
8789
+ const extras = this.db.prepare(
8790
+ `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
8791
+ ).all(...crossRefIds);
8792
+ for (const x of extras) {
8793
+ symToFile.set(x.id, x.file);
8794
+ if (!fileStats.has(x.file)) {
8795
+ fileStats.set(x.file, { count: 0, lang: "ts" });
8796
+ }
8797
+ }
8798
+ }
8799
+ const edgeMap = /* @__PURE__ */ new Map();
8800
+ for (const r of refRows) {
8801
+ if (r.call_type === "import") continue;
8802
+ const fromFile = symToFile.get(r.from_id);
8803
+ const toFile = symToFile.get(r.to_id);
8804
+ if (!fromFile || !toFile || fromFile === toFile) continue;
8805
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
8806
+ ensureFileNode(fromFile);
8807
+ ensureFileNode(toFile);
8808
+ const key = `${fromFile}\0${toFile}`;
8809
+ let e = edgeMap.get(key);
8810
+ if (!e) {
8811
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8812
+ edgeMap.set(key, e);
8813
+ }
8814
+ e.weight++;
8815
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8816
+ }
8817
+ const importRows = this.db.prepare(
8818
+ `SELECT r.from_id, r.to_name
8819
+ FROM refs r
8820
+ WHERE r.call_type = 'import'
8821
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
8822
+ ).all(...pkgFilePaths);
8823
+ for (const r of importRows) {
8824
+ const fromFile = symToFile.get(r.from_id);
8825
+ if (!fromFile || !localFiles.has(fromFile)) continue;
8826
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
8827
+ if (!toFile || fromFile === toFile) continue;
8828
+ ensureFileNode(fromFile);
8829
+ ensureFileNode(toFile);
8830
+ const key = `${fromFile}\0${toFile}`;
8831
+ let edge = edgeMap.get(key);
8832
+ if (!edge) {
8833
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8834
+ edgeMap.set(key, edge);
8835
+ }
8836
+ edge.weight++;
8837
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
8838
+ }
8839
+ const edges = [];
8840
+ for (const [key, e] of edgeMap) {
8841
+ const [source, target] = key.split("\0");
8842
+ let bestType = "call";
8843
+ let bestCount = 0;
8844
+ for (const [t, c] of e.types) {
8845
+ if (c > bestCount) {
8846
+ bestType = t;
8847
+ bestCount = c;
8848
+ }
8849
+ }
8850
+ edges.push({
8851
+ source: `file:${source}`,
8852
+ target: `file:${target}`,
8853
+ weight: e.weight,
8854
+ refType: bestType
8855
+ });
8856
+ }
8857
+ return { nodes: [...fileNodes.values()], edges };
8858
+ }
8859
+ /**
8860
+ * Symbol-level graph for a single file: each symbol is a node; edges are
8861
+ * derived from intra-file and cross-file symbol references (who calls whom).
8862
+ */
8863
+ getSymbolGraph(fileFilter) {
8864
+ const syms = this.db.prepare(
8865
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
8866
+ ).all(fileFilter);
8867
+ if (syms.length === 0) return { nodes: [], edges: [] };
8868
+ const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
8869
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
8870
+ const toGraphNode = (s) => ({
8871
+ id: `sym:${s.id}`,
8872
+ label: s.name,
8873
+ kind: "symbol",
8874
+ symbolId: s.id,
8875
+ symbolKind: s.kind,
8876
+ file: s.file,
8877
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
8878
+ lang: s.lang,
8879
+ line: s.line,
8880
+ signature: s.signature,
8881
+ scope: s.scope,
8882
+ external: s.file !== fileFilter
8883
+ });
8884
+ const symIds = new Set(syms.map((s) => s.id));
8885
+ const refRows = this.db.prepare(
8886
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
8887
+ FROM refs r
8888
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
8889
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
8890
+ ).all(fileFilter, fileFilter);
8891
+ const edgeMap = /* @__PURE__ */ new Map();
8892
+ for (const r of refRows) {
8893
+ if (r.to_id === null) continue;
8894
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
8895
+ relatedIds.add(r.from_id);
8896
+ relatedIds.add(r.to_id);
8897
+ const key = `${r.from_id}\0${r.to_id}`;
8898
+ let e = edgeMap.get(key);
8899
+ if (!e) {
8900
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8901
+ edgeMap.set(key, e);
8902
+ }
8903
+ e.weight++;
8904
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8905
+ }
8906
+ const edges = [];
8907
+ for (const [key, e] of edgeMap) {
8908
+ const [fromId, toId] = key.split("\0").map(Number);
8909
+ let bestType = "call";
8910
+ let bestCount = 0;
8911
+ for (const [t, c] of e.types) {
8912
+ if (c > bestCount) {
8913
+ bestType = t;
8914
+ bestCount = c;
8915
+ }
8916
+ }
8917
+ edges.push({
8918
+ source: `sym:${fromId}`,
8919
+ target: `sym:${toId}`,
8920
+ weight: e.weight,
8921
+ refType: bestType
8922
+ });
8923
+ }
8924
+ const loadedIds = new Set(syms.map((s) => s.id));
8925
+ const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
8926
+ if (missingIds.length > 0) {
8927
+ const placeholders = missingIds.map(() => "?").join(",");
8928
+ const extras = this.db.prepare(
8929
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
8930
+ ).all(...missingIds);
8931
+ for (const s of extras) symById.set(s.id, s);
8932
+ }
8933
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
8934
+ const aExternal = a.file === fileFilter ? 0 : 1;
8935
+ const bExternal = b.file === fileFilter ? 0 : 1;
8936
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
8937
+ }).map(toGraphNode);
8938
+ return { nodes, edges };
8939
+ }
8332
8940
  close() {
8333
8941
  this.stmtCache.clear();
8334
8942
  try {
@@ -8347,6 +8955,7 @@ import { Worker } from "node:worker_threads";
8347
8955
  import { expectDefined as expectDefined6 } from "@wrongstack/core";
8348
8956
  import * as fs14 from "node:fs/promises";
8349
8957
  import * as path19 from "node:path";
8958
+ import { availableParallelism } from "node:os";
8350
8959
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
8351
8960
 
8352
8961
  // src/codebase-index/ts-parser.ts
@@ -8403,8 +9012,7 @@ function extToLang(ext) {
8403
9012
  return null;
8404
9013
  }
8405
9014
  }
8406
- function getSignature(node, sourceFile) {
8407
- const printer = ts.createPrinter({});
9015
+ function getSignature(printer, node, sourceFile) {
8408
9016
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
8409
9017
  return raw.replace(/\s+/g, " ").slice(0, 500);
8410
9018
  }
@@ -8423,20 +9031,14 @@ function getJsDoc(node, sourceFile) {
8423
9031
  }
8424
9032
  return "";
8425
9033
  }
8426
- function buildScope(node) {
8427
- const parts = [];
8428
- let current = node.parent;
8429
- while (current) {
8430
- if (ts.isClassDeclaration(current) || ts.isInterfaceDeclaration(current) || ts.isEnumDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
8431
- parts.unshift(current.name?.text ?? "Anon");
8432
- } else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
8433
- if (current.name && ts.isIdentifier(current.name)) {
8434
- parts.unshift(current.name.text);
8435
- }
9034
+ function pushScopeName(node, parts) {
9035
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
9036
+ parts.push(node.name?.text ?? "Anon");
9037
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
9038
+ if (node.name && ts.isIdentifier(node.name)) {
9039
+ parts.push(node.name.text);
8436
9040
  }
8437
- current = current.parent;
8438
9041
  }
8439
- return parts.join(".");
8440
9042
  }
8441
9043
  function parseSymbols(opts) {
8442
9044
  const { file, content, lang } = opts;
@@ -8447,41 +9049,39 @@ function parseSymbols(opts) {
8447
9049
  return { file, lang, symbols: [], mtimeMs: Date.now() };
8448
9050
  }
8449
9051
  const symbols = [];
8450
- function visit(node) {
9052
+ const refs = [];
9053
+ const printer = ts.createPrinter({});
9054
+ function visit(node, funcDepth, scopeParts) {
8451
9055
  const kind = kindOf(node);
8452
9056
  if (kind) {
8453
- const nameNode = node.name;
8454
- if (!nameNode || !ts.isIdentifier(nameNode)) return;
8455
- const name = nameNode.text;
8456
- const pos = nameNode.getStart(sourceFile);
8457
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
8458
- const scope = buildScope(node);
8459
- const signature = getSignature(node, sourceFile);
8460
- const docComment = getJsDoc(node, sourceFile);
8461
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
8462
- symbols.push({
8463
- id: 0,
8464
- lang,
8465
- kind,
8466
- name,
8467
- file,
8468
- line: line + 1,
8469
- col: character,
8470
- signature,
8471
- docComment,
8472
- scope,
8473
- text
8474
- });
9057
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
9058
+ } else {
9059
+ const nameNode = node.name;
9060
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
9061
+ return;
9062
+ }
9063
+ const name = nameNode.text;
9064
+ const pos2 = nameNode.getStart(sourceFile);
9065
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
9066
+ const scope = scopeParts.join(".");
9067
+ const signature = getSignature(printer, node, sourceFile);
9068
+ const docComment = getJsDoc(node, sourceFile);
9069
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
9070
+ symbols.push({
9071
+ id: 0,
9072
+ lang,
9073
+ kind,
9074
+ name,
9075
+ file,
9076
+ line: line2 + 1,
9077
+ col: character,
9078
+ signature,
9079
+ docComment,
9080
+ scope,
9081
+ text
9082
+ });
9083
+ }
8475
9084
  }
8476
- ts.forEachChild(node, visit);
8477
- }
8478
- visit(sourceFile);
8479
- const refs = extractRefs(sourceFile);
8480
- return { file, lang, symbols, refs, mtimeMs: Date.now() };
8481
- }
8482
- function extractRefs(sourceFile) {
8483
- const refs = [];
8484
- function visit(node) {
8485
9085
  const pos = node.getStart(sourceFile);
8486
9086
  const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
8487
9087
  const lineNum = line + 1;
@@ -8506,10 +9106,14 @@ function extractRefs(sourceFile) {
8506
9106
  const moduleName = getModuleName(node);
8507
9107
  if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
8508
9108
  }
8509
- ts.forEachChild(node, visit);
9109
+ const scopeIdx = scopeParts.length;
9110
+ pushScopeName(node, scopeParts);
9111
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
9112
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
9113
+ scopeParts.length = scopeIdx;
8510
9114
  }
8511
- visit(sourceFile);
8512
- return deduplicateRefs(refs);
9115
+ visit(sourceFile, 0, []);
9116
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
8513
9117
  }
8514
9118
  function getTypeName(name) {
8515
9119
  if (ts.isIdentifier(name)) return name.text;
@@ -9394,9 +9998,9 @@ function parseSymbols5(opts) {
9394
9998
  function regexParse2(opts) {
9395
9999
  const { file, content, lang } = opts;
9396
10000
  const symbols = [];
9397
- const basename8 = path17.basename(file).toLowerCase();
9398
- const isPackageJson = basename8 === "package.json";
9399
- const isTsconfig = basename8 === "tsconfig.json" || basename8 === "tsconfig.build.json";
10001
+ const basename9 = path17.basename(file).toLowerCase();
10002
+ const isPackageJson = basename9 === "package.json";
10003
+ const isTsconfig = basename9 === "tsconfig.json" || basename9 === "tsconfig.build.json";
9400
10004
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
9401
10005
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
9402
10006
  const lines = content.split("\n");
@@ -9791,16 +10395,14 @@ async function loadGitignoreMatcher(projectRoot) {
9791
10395
 
9792
10396
  // src/codebase-index/indexer.ts
9793
10397
  var YIELD_EVERY_N = 50;
9794
- var PARALLEL_BATCH = 20;
10398
+ var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
9795
10399
  function yieldEventLoop() {
9796
10400
  return new Promise((resolve14) => setImmediate(resolve14));
9797
10401
  }
9798
10402
  function throwIfAborted(signal) {
9799
10403
  if (!signal?.aborted) return;
9800
10404
  if (signal.reason instanceof Error) throw signal.reason;
9801
- throw new Error(
9802
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
9803
- );
10405
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
9804
10406
  }
9805
10407
  function isAbortError(err) {
9806
10408
  return err instanceof DOMException && err.name === "AbortError";
@@ -9816,8 +10418,20 @@ var DEFAULT_IGNORE = [
9816
10418
  "__snapshots__",
9817
10419
  ".nyc_output"
9818
10420
  ];
10421
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10422
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10423
+ function isWithinProject(projectRoot, file) {
10424
+ const rel = path19.relative(projectRoot, file);
10425
+ return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
10426
+ }
10427
+ function isMissingPathError(err) {
10428
+ const code = err?.code;
10429
+ return code === "ENOENT" || code === "ENOTDIR";
10430
+ }
9819
10431
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9820
10432
  const results = [];
10433
+ const errors = [];
10434
+ let complete = true;
9821
10435
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
9822
10436
  const globs = [
9823
10437
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -9841,7 +10455,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9841
10455
  let entries;
9842
10456
  try {
9843
10457
  entries = await fs14.readdir(dir, { withFileTypes: true });
9844
- } catch {
10458
+ } catch (err) {
10459
+ complete = false;
10460
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
9845
10461
  return;
9846
10462
  }
9847
10463
  dirCount++;
@@ -9853,8 +10469,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9853
10469
  if (isGitIgnored(rel, true)) continue;
9854
10470
  await walk(full);
9855
10471
  } else if (e.isFile()) {
9856
- if (isGitIgnored(rel, false)) continue;
9857
- const ext = path19.extname(e.name);
10472
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
10473
+ const ext = path19.extname(e.name).toLowerCase();
9858
10474
  for (const { ext: extName, pat } of globs) {
9859
10475
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
9860
10476
  results.push(full);
@@ -9865,7 +10481,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9865
10481
  }
9866
10482
  };
9867
10483
  await walk(projectRoot);
9868
- return results;
10484
+ return { files: results, complete, errors };
9869
10485
  }
9870
10486
  async function parseFile(file, content, lang) {
9871
10487
  switch (lang) {
@@ -9888,6 +10504,26 @@ async function parseFile(file, content, lang) {
9888
10504
  return { file, lang, symbols: [], mtimeMs: Date.now() };
9889
10505
  }
9890
10506
  }
10507
+ function assignRefsToSymbols2(refs, symbols) {
10508
+ if (refs.length === 0 || symbols.length === 0) return [];
10509
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10510
+ const seen = /* @__PURE__ */ new Set();
10511
+ const assigned = [];
10512
+ for (const ref of refs) {
10513
+ let owner2;
10514
+ for (const symbol of ordered) {
10515
+ if (symbol.line > ref.line) break;
10516
+ owner2 = symbol;
10517
+ }
10518
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10519
+ if (!owner2 || owner2.id <= 0) continue;
10520
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10521
+ if (seen.has(key)) continue;
10522
+ seen.add(key);
10523
+ assigned.push({ ...ref, fromId: owner2.id });
10524
+ }
10525
+ return assigned;
10526
+ }
9891
10527
  async function runIndexer(_ctx, opts) {
9892
10528
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
9893
10529
  try {
@@ -9900,7 +10536,9 @@ async function runIndexer(_ctx, opts) {
9900
10536
  }
9901
10537
  }
9902
10538
  async function runIndexerWithStore(store, opts) {
9903
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
10539
+ const { projectRoot, langs, ignore = [], signal } = opts;
10540
+ const relationGraphVersion = "2";
10541
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
9904
10542
  const startMs = Date.now();
9905
10543
  const errors = [];
9906
10544
  const langStats = {};
@@ -9909,10 +10547,18 @@ async function runIndexerWithStore(store, opts) {
9909
10547
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
9910
10548
  let files;
9911
10549
  let discoveredFiles = null;
10550
+ let discoveryComplete = true;
9912
10551
  if (opts.files && opts.files.length > 0) {
9913
- files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
10552
+ files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
10553
+ if (!isWithinProject(projectRoot, f)) return false;
10554
+ const rel = path19.relative(projectRoot, f).replace(/\\/g, "/");
10555
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path19.basename(f)) && !isGitIgnored(rel, false);
10556
+ });
9914
10557
  } else {
9915
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10558
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10559
+ files = discovery.files;
10560
+ errors.push(...discovery.errors);
10561
+ discoveryComplete = discovery.complete;
9916
10562
  discoveredFiles = new Set(files);
9917
10563
  }
9918
10564
  if (langs && langs.length > 0) {
@@ -9927,46 +10573,79 @@ async function runIndexerWithStore(store, opts) {
9927
10573
  if (!force) {
9928
10574
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
9929
10575
  }
10576
+ let filesSinceLastYield = 0;
9930
10577
  for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
9931
10578
  const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
9932
10579
  const batchFiles = files.slice(batchStart, batchEnd);
9933
10580
  opts.onProgress?.(batchEnd, files.length);
9934
- if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
10581
+ filesSinceLastYield += batchFiles.length;
10582
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
10583
+ filesSinceLastYield = 0;
9935
10584
  await yieldEventLoop();
9936
10585
  throwIfAborted(signal);
9937
10586
  }
9938
10587
  const statOpts = signal ? { signal } : {};
9939
10588
  const statReadParse = await Promise.allSettled(
9940
- batchFiles.map(async (file) => {
9941
- let stat17;
9942
- try {
9943
- stat17 = await fs14.stat(file, statOpts);
9944
- } catch (e) {
9945
- if (isAbortError(e)) throw e;
9946
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
9947
- }
9948
- if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
9949
- const lang = detectLang(file);
9950
- if (!lang) return { file, stat: stat17, lang: "", parsed: null };
9951
- const meta = existingMeta.get(file);
9952
- if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
9953
- return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
9954
- }
9955
- let content;
9956
- try {
9957
- content = await fs14.readFile(file, { encoding: "utf8", signal });
9958
- } catch (e) {
9959
- if (isAbortError(e)) throw e;
9960
- return { file, stat: stat17, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
9961
- }
9962
- let parsed;
9963
- try {
9964
- parsed = await parseFile(file, content, lang);
9965
- } catch (e) {
9966
- return { file, stat: stat17, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
10589
+ batchFiles.map(
10590
+ async (file) => {
10591
+ let stat17;
10592
+ try {
10593
+ stat17 = await fs14.stat(file, statOpts);
10594
+ } catch (e) {
10595
+ if (isAbortError(e)) throw e;
10596
+ return {
10597
+ file,
10598
+ stat: null,
10599
+ lang: "",
10600
+ parsed: null,
10601
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
10602
+ missing: isMissingPathError(e)
10603
+ };
10604
+ }
10605
+ if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
10606
+ const lang = detectLang(file);
10607
+ if (!lang) return { file, stat: stat17, lang: "", parsed: null };
10608
+ if (stat17.size > MAX_INDEX_FILE_BYTES) {
10609
+ return {
10610
+ file,
10611
+ stat: stat17,
10612
+ lang,
10613
+ parsed: null,
10614
+ error: `file too large (${stat17.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
10615
+ };
10616
+ }
10617
+ const meta = existingMeta.get(file);
10618
+ if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
10619
+ return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
10620
+ }
10621
+ let content;
10622
+ try {
10623
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
10624
+ } catch (e) {
10625
+ if (isAbortError(e)) throw e;
10626
+ return {
10627
+ file,
10628
+ stat: stat17,
10629
+ lang,
10630
+ parsed: null,
10631
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
10632
+ };
10633
+ }
10634
+ let parsed;
10635
+ try {
10636
+ parsed = await parseFile(file, content, lang);
10637
+ } catch (e) {
10638
+ return {
10639
+ file,
10640
+ stat: stat17,
10641
+ lang,
10642
+ parsed: null,
10643
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
10644
+ };
10645
+ }
10646
+ return { file, stat: stat17, lang, parsed, content };
9967
10647
  }
9968
- return { file, stat: stat17, lang, parsed, content };
9969
- })
10648
+ )
9970
10649
  );
9971
10650
  const batchEntries = [];
9972
10651
  const deleteForFiles = [];
@@ -9981,8 +10660,8 @@ async function runIndexerWithStore(store, opts) {
9981
10660
  }
9982
10661
  const result = settled.value;
9983
10662
  if (result.error) {
9984
- if (result.stat) store.deleteFile(file);
9985
- if (result.error.includes("error:")) errors.push(result.error);
10663
+ if (result.missing) store.deleteFile(file);
10664
+ errors.push(`${file}: ${result.error}`);
9986
10665
  continue;
9987
10666
  }
9988
10667
  const { stat: stat17, lang, parsed } = result;
@@ -10006,6 +10685,8 @@ async function runIndexerWithStore(store, opts) {
10006
10685
  continue;
10007
10686
  }
10008
10687
  if (parsed.symbols.length === 0) {
10688
+ store.deleteRefsForFile(file);
10689
+ store.deleteSymbolsForFile(file);
10009
10690
  store.upsertFile({
10010
10691
  file,
10011
10692
  lang,
@@ -10016,15 +10697,11 @@ async function runIndexerWithStore(store, opts) {
10016
10697
  filesIndexed++;
10017
10698
  continue;
10018
10699
  }
10019
- const refs = [];
10020
- if (parsed.refs && parsed.refs.length > 0) {
10021
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
10022
- }
10023
10700
  batchEntries.push({
10024
10701
  file,
10025
10702
  lang,
10026
10703
  symbols: parsed.symbols,
10027
- refs,
10704
+ refs: parsed.refs ?? [],
10028
10705
  mtimeMs: Math.floor(stat17.mtimeMs),
10029
10706
  symbolCount: parsed.symbols.length
10030
10707
  });
@@ -10032,34 +10709,12 @@ async function runIndexerWithStore(store, opts) {
10032
10709
  }
10033
10710
  if (batchEntries.length > 0) {
10034
10711
  try {
10035
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
10036
- let cursor = 0;
10712
+ store.commitBatch(batchEntries, { deleteForFiles });
10037
10713
  for (const entry of batchEntries) {
10038
10714
  const count = entry.symbols.length;
10039
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
10040
- cursor += count;
10041
10715
  symbolsIndexed += count;
10042
10716
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
10043
10717
  filesIndexed++;
10044
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
10045
- const refsByLine = /* @__PURE__ */ new Map();
10046
- for (let i = 0; i < symbolsWithIds.length; i++) {
10047
- const sym = symbolsWithIds[i];
10048
- let arr = refsByLine.get(sym.line);
10049
- if (!arr) {
10050
- arr = [];
10051
- refsByLine.set(sym.line, arr);
10052
- }
10053
- arr.push(i);
10054
- }
10055
- for (const ref of entry.refs) {
10056
- const indices = refsByLine.get(ref.line);
10057
- if (indices && indices.length > 0) {
10058
- const idx = indices.shift();
10059
- ref.fromId = symbolsWithIds[idx].id;
10060
- }
10061
- }
10062
- }
10063
10718
  }
10064
10719
  } catch (err) {
10065
10720
  const message = err instanceof Error ? err.message : String(err);
@@ -10073,23 +10728,7 @@ async function runIndexerWithStore(store, opts) {
10073
10728
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
10074
10729
  filesIndexed++;
10075
10730
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
10076
- const refsByLine = /* @__PURE__ */ new Map();
10077
- for (const sym of symbolsWithIds) {
10078
- let arr = refsByLine.get(sym.line);
10079
- if (!arr) {
10080
- arr = [];
10081
- refsByLine.set(sym.line, arr);
10082
- }
10083
- arr.push(sym);
10084
- }
10085
- const fallbackBatch = [];
10086
- for (const ref of entry.refs) {
10087
- const syms = refsByLine.get(ref.line);
10088
- if (syms && syms.length > 0) {
10089
- const sym = syms.shift();
10090
- fallbackBatch.push({ ...ref, fromId: sym.id });
10091
- }
10092
- }
10731
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
10093
10732
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
10094
10733
  }
10095
10734
  store.upsertFile({
@@ -10100,19 +10739,23 @@ async function runIndexerWithStore(store, opts) {
10100
10739
  lastIndexed: Date.now()
10101
10740
  });
10102
10741
  } catch (innerErr) {
10103
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
10742
+ errors.push(
10743
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
10744
+ );
10104
10745
  }
10105
10746
  }
10106
10747
  }
10107
10748
  }
10108
10749
  }
10109
- if (discoveredFiles) {
10750
+ if (discoveredFiles && discoveryComplete) {
10110
10751
  for (const [file_] of existingMeta) {
10111
10752
  if (!discoveredFiles.has(file_)) {
10112
10753
  store.deleteFile(file_);
10113
10754
  }
10114
10755
  }
10115
10756
  }
10757
+ store.resolveRefs();
10758
+ store.setMetadata("relation_graph_version", relationGraphVersion);
10116
10759
  const durationMs = Date.now() - startMs;
10117
10760
  store.setLastIndexed(Date.now());
10118
10761
  return {
@@ -10148,7 +10791,7 @@ async function indexService(args, hooks = {}) {
10148
10791
  });
10149
10792
  }
10150
10793
  function searchService(args) {
10151
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
10794
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
10152
10795
  try {
10153
10796
  return store.searchRanked(
10154
10797
  args.query,
@@ -10161,21 +10804,21 @@ function searchService(args) {
10161
10804
  args.limit
10162
10805
  );
10163
10806
  } finally {
10164
- store.close();
10807
+ indexStorePool.release(store);
10165
10808
  }
10166
10809
  }
10167
10810
  function statsService(args) {
10168
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
10811
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
10169
10812
  try {
10170
10813
  return store.getStats();
10171
10814
  } finally {
10172
- store.close();
10815
+ indexStorePool.release(store);
10173
10816
  }
10174
10817
  }
10175
10818
 
10176
10819
  // src/codebase-index/background-indexer.ts
10177
10820
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
10178
- var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
10821
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
10179
10822
  var _ready = false;
10180
10823
  var _indexing = false;
10181
10824
  var _currentFile = 0;
@@ -10244,14 +10887,22 @@ function ensureWorker() {
10244
10887
  if (!entry) return;
10245
10888
  pending.delete(msg.id);
10246
10889
  if (msg.ok) entry.resolve(msg.result);
10247
- else entry.reject(new Error(msg.error));
10890
+ else {
10891
+ const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
10892
+ if (msg.errorName && msg.errorName !== "Error") {
10893
+ error.name = msg.errorName;
10894
+ }
10895
+ entry.reject(error);
10896
+ }
10248
10897
  });
10249
10898
  w.on("error", (err) => {
10899
+ if (worker !== w) return;
10250
10900
  worker = null;
10251
10901
  failAllPending(err);
10252
10902
  });
10253
10903
  w.on("exit", () => {
10254
- if (worker === w) worker = null;
10904
+ if (worker !== w) return;
10905
+ worker = null;
10255
10906
  failAllPending(new Error("codebase-index worker exited"));
10256
10907
  });
10257
10908
  worker = w;
@@ -10271,6 +10922,11 @@ function terminateWorker(reason) {
10271
10922
  function callIndexOp(op, args, opts) {
10272
10923
  const w = ensureWorker();
10273
10924
  if (!w) return callInline(op, args, opts);
10925
+ if (opts.signal?.aborted) {
10926
+ return Promise.reject(
10927
+ opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
10928
+ );
10929
+ }
10274
10930
  return new Promise((resolve14, reject) => {
10275
10931
  const id = nextRpcId++;
10276
10932
  const timer = setTimeout(() => {
@@ -10285,8 +10941,7 @@ function callIndexOp(op, args, opts) {
10285
10941
  const onAbort = () => {
10286
10942
  w.postMessage({ type: "cancel", id });
10287
10943
  };
10288
- if (opts.signal?.aborted) onAbort();
10289
- else opts.signal?.addEventListener("abort", onAbort, { once: true });
10944
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
10290
10945
  const cleanup = () => {
10291
10946
  clearTimeout(timer);
10292
10947
  opts.signal?.removeEventListener("abort", onAbort);
@@ -10430,7 +11085,7 @@ var codebaseIndexTool = {
10430
11085
  category: "Project",
10431
11086
  icon: "index",
10432
11087
  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.",
10433
- 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.",
11088
+ 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.",
10434
11089
  permission: "confirm",
10435
11090
  mutating: true,
10436
11091
  capabilities: ["fs.write.outside-project"],
@@ -10486,12 +11141,14 @@ var codebaseSearchTool = {
10486
11141
  name: "codebase-search",
10487
11142
  category: "Project",
10488
11143
  icon: "index",
10489
- 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).",
10490
- 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.",
11144
+ 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).",
11145
+ 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.",
10491
11146
  permission: "auto",
10492
11147
  mutating: false,
10493
11148
  capabilities: ["fs.read"],
10494
- timeoutMs: 1e4,
11149
+ // The index host has its own 30s read watchdog. Leave enough headroom for
11150
+ // worker teardown and structured timeout reporting.
11151
+ timeoutMs: 35e3,
10495
11152
  inputSchema: {
10496
11153
  type: "object",
10497
11154
  properties: {
@@ -10501,11 +11158,34 @@ var codebaseSearchTool = {
10501
11158
  },
10502
11159
  kind: {
10503
11160
  type: "string",
10504
- description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
11161
+ enum: [
11162
+ "class",
11163
+ "interface",
11164
+ "enum",
11165
+ "type",
11166
+ "function",
11167
+ "method",
11168
+ "var",
11169
+ "const",
11170
+ "let",
11171
+ "property",
11172
+ "parameter",
11173
+ "namespace",
11174
+ "object",
11175
+ "literal",
11176
+ "schema",
11177
+ "struct",
11178
+ "trait",
11179
+ "impl",
11180
+ "static",
11181
+ "mod"
11182
+ ],
11183
+ description: "Filter by indexed symbol kind"
10505
11184
  },
10506
11185
  lang: {
10507
11186
  type: "string",
10508
- description: "Filter by language: ts, tsx, js, jsx"
11187
+ enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
11188
+ description: "Filter by indexed language"
10509
11189
  },
10510
11190
  lspKind: {
10511
11191
  type: "integer",
@@ -10530,20 +11210,12 @@ var codebaseSearchTool = {
10530
11210
  },
10531
11211
  async execute(input, ctx, execOpts) {
10532
11212
  const state = getIndexState();
10533
- if (!state.ready) {
10534
- return {
10535
- results: [],
10536
- total: 0,
10537
- query: input.query,
10538
- 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."
10539
- };
10540
- }
10541
- if (state.indexing) {
11213
+ if (state.indexing && !state.ready) {
10542
11214
  return {
10543
11215
  results: [],
10544
11216
  total: 0,
10545
11217
  query: input.query,
10546
- indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
11218
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
10547
11219
  };
10548
11220
  }
10549
11221
  if (state.lastError) {
@@ -10556,21 +11228,37 @@ var codebaseSearchTool = {
10556
11228
  indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
10557
11229
  };
10558
11230
  }
10559
- const limit = Math.min(input.limit ?? 20, 100);
11231
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
10560
11232
  const { results, total } = await searchCodebaseIndex(
10561
11233
  {
10562
11234
  projectRoot: ctx.projectRoot,
10563
11235
  indexDir: codebaseIndexDirOverride(ctx),
10564
11236
  query: input.query,
10565
- kind: input.kind,
10566
- lang: input.lang,
11237
+ kind: input.kind?.toLowerCase(),
11238
+ lang: input.lang?.toLowerCase(),
10567
11239
  file: input.file,
10568
11240
  lspKind: input.lspKind,
10569
11241
  limit
10570
11242
  },
10571
11243
  { signal: execOpts?.signal }
10572
11244
  );
10573
- return { results, total, query: input.query };
11245
+ let hasPersistedIndex = state.ready || total > 0;
11246
+ if (!hasPersistedIndex) {
11247
+ try {
11248
+ const stats = await codebaseIndexStats(
11249
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11250
+ { signal: execOpts?.signal }
11251
+ );
11252
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11253
+ } catch {
11254
+ }
11255
+ }
11256
+ return {
11257
+ results,
11258
+ total,
11259
+ query: input.query,
11260
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
11261
+ };
10574
11262
  }
10575
11263
  };
10576
11264
 
@@ -10579,12 +11267,15 @@ var codebaseStatsTool = {
10579
11267
  name: "codebase-stats",
10580
11268
  category: "Project",
10581
11269
  icon: "index",
10582
- 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.",
10583
- 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.",
11270
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
11271
+ 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.",
10584
11272
  permission: "auto",
10585
11273
  mutating: false,
10586
11274
  capabilities: ["fs.read"],
10587
- timeoutMs: 5e3,
11275
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
11276
+ // longer so the host can return a structured timeout instead of being
11277
+ // pre-empted by the generic tool executor.
11278
+ timeoutMs: 35e3,
10588
11279
  inputSchema: {
10589
11280
  type: "object",
10590
11281
  properties: {},
@@ -10592,7 +11283,8 @@ var codebaseStatsTool = {
10592
11283
  },
10593
11284
  async execute(_input, ctx, execOpts) {
10594
11285
  const idxState = getIndexState();
10595
- if (!idxState.ready) {
11286
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
11287
+ if (idxState.indexing) {
10596
11288
  return {
10597
11289
  totalSymbols: 0,
10598
11290
  totalFiles: 0,
@@ -10600,22 +11292,47 @@ var codebaseStatsTool = {
10600
11292
  byKind: {},
10601
11293
  lastIndexed: null,
10602
11294
  sizeBytes: 0,
10603
- indexPath: "",
11295
+ indexPath,
10604
11296
  version: SCHEMA_VERSION,
10605
- indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
11297
+ statsAvailable: false,
11298
+ indexing: {
11299
+ currentFile: idxState.currentFile,
11300
+ totalFiles: idxState.totalFiles
11301
+ },
11302
+ 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.`
10606
11303
  };
10607
11304
  }
10608
- const stats = await codebaseIndexStats(
10609
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
10610
- { signal: execOpts?.signal }
10611
- );
10612
- if (idxState.indexing) {
11305
+ let stats;
11306
+ try {
11307
+ stats = await codebaseIndexStats(
11308
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11309
+ { signal: execOpts?.signal }
11310
+ );
11311
+ } catch (err) {
11312
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
11313
+ throw err;
11314
+ }
10613
11315
  return {
10614
- ...stats,
10615
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
11316
+ totalSymbols: 0,
11317
+ totalFiles: 0,
11318
+ byLang: {},
11319
+ byKind: {},
11320
+ lastIndexed: null,
11321
+ sizeBytes: 0,
11322
+ indexPath,
11323
+ version: SCHEMA_VERSION,
11324
+ statsAvailable: false,
11325
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
10616
11326
  };
10617
11327
  }
10618
11328
  const circuit = idxState.circuit;
11329
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11330
+ let indexStatus;
11331
+ if (circuit.state === "open") {
11332
+ 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.");
11333
+ } else if (!hasPersistedIndex) {
11334
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
11335
+ }
10619
11336
  return {
10620
11337
  totalSymbols: stats.totalSymbols,
10621
11338
  totalFiles: stats.totalFiles,
@@ -10625,9 +11342,8 @@ var codebaseStatsTool = {
10625
11342
  sizeBytes: stats.sizeBytes,
10626
11343
  indexPath: stats.indexPath,
10627
11344
  version: stats.version,
10628
- ...circuit.state === "open" ? {
10629
- 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.`
10630
- } : {}
11345
+ statsAvailable: true,
11346
+ ...indexStatus ? { indexStatus } : {}
10631
11347
  };
10632
11348
  }
10633
11349
  };
@@ -12146,6 +12862,11 @@ Compare this against your old_string and retry with the file's actual text.` : "
12146
12862
 
12147
12863
  // src/exec.ts
12148
12864
  import { spawn as spawn8 } from "node:child_process";
12865
+ import {
12866
+ emitProcessCompleted as emitProcessCompleted3,
12867
+ emitProcessOutput as emitProcessOutput3,
12868
+ emitProcessStarted as emitProcessStarted3
12869
+ } from "@wrongstack/core";
12149
12870
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
12150
12871
  init_output_spool();
12151
12872
  init_util();
@@ -13254,12 +13975,30 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13254
13975
  resolve14(result);
13255
13976
  };
13256
13977
  const startedAt = Date.now();
13978
+ let stdoutBytes = 0;
13979
+ let stderrBytes = 0;
13980
+ let telemetryCompleted = false;
13981
+ let timedOut = false;
13257
13982
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
13258
13983
  const resolved = resolveWin32Command(cmd);
13259
13984
  const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
13260
13985
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
13261
13986
  const spawnCmd = shim?.command ?? resolved;
13262
13987
  const spawnArgs = shim?.args ?? args;
13988
+ const emitCompletedOnce = (exitCode, pid2, signal2) => {
13989
+ if (telemetryCompleted) return;
13990
+ telemetryCompleted = true;
13991
+ emitProcessCompleted3({
13992
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
13993
+ exitCode,
13994
+ ...signal2 ? { signal: signal2 } : {},
13995
+ durationMs: Date.now() - startedAt,
13996
+ stdoutBytes,
13997
+ stderrBytes,
13998
+ timedOut,
13999
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
14000
+ });
14001
+ };
13263
14002
  let child;
13264
14003
  try {
13265
14004
  child = spawn8(spawnCmd, spawnArgs, {
@@ -13272,6 +14011,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13272
14011
  });
13273
14012
  } catch (err) {
13274
14013
  spool.finalize();
14014
+ emitProcessStarted3({
14015
+ parentPid: process.pid,
14016
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
14017
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
14018
+ cwd,
14019
+ background: false,
14020
+ startedAt: new Date(startedAt).toISOString()
14021
+ });
14022
+ emitCompletedOnce(1, void 0);
13275
14023
  finish({
13276
14024
  command: cmd,
13277
14025
  args,
@@ -13284,6 +14032,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13284
14032
  });
13285
14033
  return;
13286
14034
  }
14035
+ emitProcessStarted3({
14036
+ ...child.pid !== void 0 ? { pid: child.pid } : {},
14037
+ parentPid: process.pid,
14038
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
14039
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
14040
+ cwd,
14041
+ background: false,
14042
+ startedAt: new Date(startedAt).toISOString()
14043
+ });
13287
14044
  child.on("error", (err) => {
13288
14045
  const isAbort = err && err.code === "ABORT_ERR";
13289
14046
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
@@ -13292,6 +14049,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13292
14049
  if (typeof pid === "number") registry.unregister(pid);
13293
14050
  registry.afterCall(Date.now() - startedAt, true);
13294
14051
  spool.finalize();
14052
+ emitCompletedOnce(isAbort ? 124 : 1, child.pid, isAbort ? "ABORT" : void 0);
13295
14053
  finish({
13296
14054
  command: cmd,
13297
14055
  args,
@@ -13311,6 +14069,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13311
14069
  }
13312
14070
  const timer = setTimeout(() => {
13313
14071
  killed = true;
14072
+ timedOut = true;
13314
14073
  if (typeof pid === "number") registry.kill(pid);
13315
14074
  else child.kill("SIGTERM");
13316
14075
  }, timeout);
@@ -13325,11 +14084,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13325
14084
  }
13326
14085
  child.stdout?.on("data", (chunk) => {
13327
14086
  const text = chunk.toString();
14087
+ stdoutBytes += chunk.byteLength;
14088
+ emitProcessOutput3({ pid, stream: "stdout", chunk });
13328
14089
  if (stdout.length < MAX_OUTPUT2) stdout += text;
13329
14090
  spool.write(text);
13330
14091
  });
13331
14092
  child.stderr?.on("data", (chunk) => {
13332
14093
  const text = chunk.toString();
14094
+ stderrBytes += chunk.byteLength;
14095
+ emitProcessOutput3({ pid, stream: "stderr", chunk });
13333
14096
  if (stderr.length < MAX_OUTPUT2) stderr += text;
13334
14097
  spool.write(text);
13335
14098
  });
@@ -13339,6 +14102,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13339
14102
  if (typeof pid === "number") registry.unregister(pid);
13340
14103
  const durationMs = Date.now() - startedAt;
13341
14104
  const exitCode = killed ? 124 : code ?? 1;
14105
+ emitCompletedOnce(exitCode, pid);
13342
14106
  registry.afterCall(durationMs, exitCode !== 0);
13343
14107
  const spooled = spool.finalize();
13344
14108
  finish({
@@ -13414,7 +14178,7 @@ function guardedLookup(hostname2, options, callback) {
13414
14178
  var pinnedAgent;
13415
14179
  function getPinnedDispatcher() {
13416
14180
  if (!pinnedAgent) {
13417
- pinnedAgent = new Agent({ connect: { lookup: guardedLookup } });
14181
+ pinnedAgent = new Agent({ allowH2: false, connect: { lookup: guardedLookup } });
13418
14182
  }
13419
14183
  return pinnedAgent;
13420
14184
  }
@@ -13516,7 +14280,7 @@ async function assertNotPrivate(hostname2) {
13516
14280
  }
13517
14281
  }
13518
14282
  } catch (err) {
13519
- if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
14283
+ if (err instanceof ToolValidationError2) throw err;
13520
14284
  }
13521
14285
  }
13522
14286
  }
@@ -13864,7 +14628,7 @@ async function detectFixer(cwd) {
13864
14628
  init_util();
13865
14629
  import { spawn as spawn9 } from "node:child_process";
13866
14630
  import { statSync as statSync3 } from "node:fs";
13867
- import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
14631
+ import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
13868
14632
  import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
13869
14633
  var TIMEOUT_MS2 = 3e4;
13870
14634
  var MAX_OUTPUT3 = 1e5;
@@ -14009,7 +14773,7 @@ function validateWorktreeInput(input, projectRoot) {
14009
14773
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
14010
14774
  const root = resolve11(projectRoot);
14011
14775
  const abs = resolve11(root, input.worktreePath);
14012
- if (abs !== root && !abs.startsWith(root + sep4)) {
14776
+ if (abs !== root && !abs.startsWith(root + sep5)) {
14013
14777
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
14014
14778
  }
14015
14779
  }
@@ -14172,8 +14936,8 @@ var WALK_CONCURRENCY = 16;
14172
14936
  var globTool = {
14173
14937
  name: "glob",
14174
14938
  category: "Filesystem",
14175
- description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
14176
- 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.",
14939
+ description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
14940
+ 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.",
14177
14941
  selection: {
14178
14942
  doNotUseWhen: "you need to search inside file contents.",
14179
14943
  useInstead: ["grep"]
@@ -14361,8 +15125,8 @@ var NATIVE_MAX_FILE_BYTES = 1e6;
14361
15125
  var grepTool = {
14362
15126
  name: "grep",
14363
15127
  category: "Search",
14364
- 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.",
14365
- 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.',
15128
+ 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.",
15129
+ 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.',
14366
15130
  selection: {
14367
15131
  doNotUseWhen: "you only need to locate files by name or path pattern.",
14368
15132
  useInstead: ["glob"]
@@ -15384,6 +16148,7 @@ function toYaml(data, indent = 0) {
15384
16148
  }
15385
16149
 
15386
16150
  // src/kanban.ts
16151
+ import { randomUUID } from "node:crypto";
15387
16152
  import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
15388
16153
  import {
15389
16154
  addCheckToTask,
@@ -15401,7 +16166,7 @@ import {
15401
16166
  duplicateBoard,
15402
16167
  exportBoardAsMarkdown,
15403
16168
  exportBoardToTaskGraph,
15404
- generateBoardFromDescription,
16169
+ createBoardFromText,
15405
16170
  getBoard as getBoard2,
15406
16171
  getKanbanOrchestrationSnapshot,
15407
16172
  getKanbanQueueHealth,
@@ -15423,7 +16188,9 @@ import {
15423
16188
  setTaskChain,
15424
16189
  splitTask,
15425
16190
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
16191
+ transitionTask,
15426
16192
  transferTaskToBoard,
16193
+ touchKanbanPresence as touchKanbanPresence2,
15427
16194
  updateBoard as updateBoard2,
15428
16195
  updateCheckOnTask,
15429
16196
  updateColumn,
@@ -15435,17 +16202,21 @@ import {
15435
16202
  // src/session-kanban.ts
15436
16203
  import {
15437
16204
  deserializeTaskGraph,
16205
+ GlobalMailbox,
15438
16206
  loadPlan,
15439
16207
  loadTasks,
15440
16208
  mutatePlan,
15441
16209
  mutateTasks
15442
16210
  } from "@wrongstack/core";
16211
+ import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
15443
16212
  import {
15444
16213
  createBoard,
15445
16214
  getBoard,
16215
+ getKanbanDir,
15446
16216
  listBoards,
15447
16217
  removeBoard,
15448
16218
  syncBoardFromTaskGraph,
16219
+ touchKanbanPresence,
15449
16220
  updateBoard
15450
16221
  } from "@wrongstack/kanban";
15451
16222
  var SESSION_BOARD_TAG = "session-work";
@@ -15657,8 +16428,8 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
15657
16428
  var kanbanTool = {
15658
16429
  name: "kanban",
15659
16430
  category: "Project",
15660
- 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.",
15661
- 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.",
16431
+ 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.",
16432
+ 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.",
15662
16433
  permission: "confirm",
15663
16434
  mutating: true,
15664
16435
  capabilities: ["fs.write"],
@@ -15695,6 +16466,7 @@ var kanbanTool = {
15695
16466
  "transfer_task",
15696
16467
  "get_task",
15697
16468
  "update_task",
16469
+ "transition_task",
15698
16470
  "move_task",
15699
16471
  "delete_task",
15700
16472
  "set_chain",
@@ -15725,6 +16497,7 @@ var kanbanTool = {
15725
16497
  targetColumnId: { type: "string" },
15726
16498
  title: { type: "string" },
15727
16499
  description: { type: "string" },
16500
+ dueDate: { type: "string" },
15728
16501
  tags: { type: "array", items: { type: "string" } },
15729
16502
  labels: { type: "array", items: { type: "string" } },
15730
16503
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
@@ -15771,6 +16544,18 @@ var kanbanTool = {
15771
16544
  type: "string",
15772
16545
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
15773
16546
  },
16547
+ lifecycleStage: {
16548
+ type: "string",
16549
+ enum: ["backlog", "todo", "running", "review", "done"]
16550
+ },
16551
+ transitionAction: { type: "string" },
16552
+ transitionComment: { type: "string" },
16553
+ attachmentUrl: { type: "string" },
16554
+ attachmentTitle: { type: "string" },
16555
+ attachmentType: {
16556
+ type: "string",
16557
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
16558
+ },
15774
16559
  releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
15775
16560
  releaseReason: { type: "string" },
15776
16561
  clearAssignee: { type: "boolean" },
@@ -15841,591 +16626,649 @@ var kanbanTool = {
15841
16626
  async execute(input, ctx) {
15842
16627
  const projectRoot = ctx.projectRoot;
15843
16628
  if (!projectRoot) return fail("No project root is available.");
16629
+ const withPresence = async (result) => {
16630
+ const boardId = result.board?.id ?? input.boardId;
16631
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
16632
+ try {
16633
+ const board = await touchKanbanPresence2(projectRoot, boardId, {
16634
+ sessionId: ctx.session.id,
16635
+ agentId: ctx.agentId,
16636
+ agentName: ctx.agentName,
16637
+ taskId: input.taskId ?? result.task?.id,
16638
+ runTaskId: input.runTaskId
16639
+ });
16640
+ return board ? { ...result, board } : result;
16641
+ } catch {
16642
+ return result;
16643
+ }
16644
+ };
15844
16645
  try {
15845
- switch (input.action) {
15846
- case "list_boards": {
15847
- const boards = await listBoards2(projectRoot);
15848
- return { ok: true, message: `${boards.length} board(s).`, boards };
15849
- }
15850
- case "get_board": {
15851
- const board = await requireBoard(projectRoot, input.boardId);
15852
- return board ? okBoard(board) : fail("Board not found.");
15853
- }
15854
- case "create_board": {
15855
- if (!input.title) return fail("create_board requires title.");
15856
- const board = await createBoard2(projectRoot, {
15857
- title: input.title,
15858
- ...input.description !== void 0 ? { description: input.description } : {},
15859
- ...input.tags !== void 0 ? { tags: input.tags } : {},
15860
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
15861
- });
15862
- return { ok: true, message: `Board created: ${board.title}`, board };
15863
- }
15864
- case "update_board": {
15865
- if (!input.boardId) return fail("update_board requires boardId.");
15866
- const board = await updateBoard2(projectRoot, input.boardId, {
15867
- ...input.title !== void 0 ? { title: input.title } : {},
15868
- ...input.description !== void 0 ? { description: input.description } : {},
15869
- ...input.tags !== void 0 ? { tags: input.tags } : {}
15870
- });
15871
- return board ? okBoard(board, "Board updated.") : fail("Board not found.");
15872
- }
15873
- case "duplicate_board": {
15874
- if (!input.boardId) return fail("duplicate_board requires boardId.");
15875
- const board = await duplicateBoard(projectRoot, input.boardId, {
15876
- ...input.title !== void 0 ? { title: input.title } : {},
15877
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
15878
- ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
15879
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
15880
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
15881
- });
15882
- return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
15883
- }
15884
- case "delete_board": {
15885
- if (!input.boardId) return fail("delete_board requires boardId.");
15886
- const removed = await removeBoard2(projectRoot, input.boardId);
15887
- return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
15888
- }
15889
- case "generate_board": {
15890
- if (!input.description) return fail("generate_board requires description.");
15891
- const boardInput = generateBoardFromDescription({
15892
- description: input.description,
15893
- ...input.title !== void 0 ? { title: input.title } : {},
15894
- ...input.context !== void 0 ? { context: input.context } : {},
15895
- ...input.columns !== void 0 ? { columns: input.columns } : {}
15896
- });
15897
- const board = await createBoard2(projectRoot, boardInput);
15898
- for (const taskInput2 of parseLinesIntoTasks(
15899
- input.description,
15900
- board.columns[0]?.id ?? "backlog"
15901
- )) {
15902
- await addTask(projectRoot, board.id, taskInput2);
16646
+ const result = await (async () => {
16647
+ switch (input.action) {
16648
+ case "list_boards": {
16649
+ const boards = await listBoards2(projectRoot);
16650
+ return { ok: true, message: `${boards.length} board(s).`, boards };
15903
16651
  }
15904
- return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
15905
- }
15906
- case "export_markdown": {
15907
- const board = await requireBoard(projectRoot, input.boardId);
15908
- if (!board) return fail("Board not found.");
15909
- return {
15910
- ok: true,
15911
- message: "Board exported.",
15912
- board,
15913
- markdown: exportBoardAsMarkdown(board)
15914
- };
15915
- }
15916
- case "export_task_graph": {
15917
- if (!input.boardId) return fail("export_task_graph requires boardId.");
15918
- const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
15919
- ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
15920
- ...input.specId !== void 0 ? { specId: input.specId } : {},
15921
- ...input.title !== void 0 ? { title: input.title } : {},
15922
- ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
15923
- ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
15924
- });
15925
- if (!exported) return fail("Board not found.");
15926
- return {
15927
- ok: true,
15928
- message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
15929
- board: exported.board,
15930
- taskGraph: serializeTaskGraph(exported.graph)
15931
- };
15932
- }
15933
- case "sync_task_graph": {
15934
- if (!input.boardId || !input.taskGraph) {
15935
- return fail("sync_task_graph requires boardId and taskGraph.");
16652
+ case "get_board": {
16653
+ const board = await requireBoard(projectRoot, input.boardId);
16654
+ return board ? okBoard(board) : fail("Board not found.");
15936
16655
  }
15937
- const graph = deserializeTaskGraph2(input.taskGraph);
15938
- const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
15939
- ...input.title !== void 0 ? { title: input.title } : {},
15940
- ...input.description !== void 0 ? { description: input.description } : {},
15941
- ...input.tags !== void 0 ? { tags: input.tags } : {},
15942
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
15943
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
15944
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
15945
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
15946
- ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
15947
- ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
15948
- });
15949
- return result ? {
15950
- ok: true,
15951
- message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
15952
- board: result.board
15953
- } : fail("Board not found.");
15954
- }
15955
- case "create_from_graph": {
15956
- if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
15957
- const graph = deserializeTaskGraph2(input.taskGraph);
15958
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
15959
- ...input.title !== void 0 ? { title: input.title } : {},
15960
- ...input.description !== void 0 ? { description: input.description } : {},
15961
- ...input.tags !== void 0 ? { tags: input.tags } : {},
15962
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
15963
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
15964
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
15965
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
15966
- });
15967
- return {
15968
- ok: true,
15969
- message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
15970
- board
15971
- };
15972
- }
15973
- case "import_session_tasks": {
15974
- const taskPath = ctx.meta?.["task.path"];
15975
- if (!taskPath) return fail("No session task file for this session.");
15976
- const file = await loadTasks2(taskPath);
15977
- if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
15978
- const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
15979
- const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
15980
- const tags = ["session", `session:${sessionId}`];
15981
- const existing = (await listBoards2(projectRoot)).find(
15982
- (b) => b.tags?.includes(`session:${sessionId}`)
15983
- );
15984
- if (existing) {
15985
- const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
16656
+ case "create_board": {
16657
+ if (!input.title) return fail("create_board requires title.");
16658
+ const board = await createBoard2(projectRoot, {
16659
+ title: input.title,
16660
+ ...input.description !== void 0 ? { description: input.description } : {},
16661
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16662
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
16663
+ });
16664
+ return { ok: true, message: `Board created: ${board.title}`, board };
16665
+ }
16666
+ case "update_board": {
16667
+ if (!input.boardId) return fail("update_board requires boardId.");
16668
+ const board = await updateBoard2(projectRoot, input.boardId, {
16669
+ ...input.title !== void 0 ? { title: input.title } : {},
16670
+ ...input.description !== void 0 ? { description: input.description } : {},
16671
+ ...input.tags !== void 0 ? { tags: input.tags } : {}
16672
+ });
16673
+ return board ? okBoard(board, "Board updated.") : fail("Board not found.");
16674
+ }
16675
+ case "duplicate_board": {
16676
+ if (!input.boardId) return fail("duplicate_board requires boardId.");
16677
+ const board = await duplicateBoard(projectRoot, input.boardId, {
16678
+ ...input.title !== void 0 ? { title: input.title } : {},
16679
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16680
+ ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
16681
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16682
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
16683
+ });
16684
+ return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
16685
+ }
16686
+ case "delete_board": {
16687
+ if (!input.boardId) return fail("delete_board requires boardId.");
16688
+ const removed = await removeBoard2(projectRoot, input.boardId);
16689
+ return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
16690
+ }
16691
+ case "generate_board": {
16692
+ if (!input.description) return fail("generate_board requires description.");
16693
+ const boardInput = createBoardFromText({
16694
+ description: input.description,
16695
+ ...input.title !== void 0 ? { title: input.title } : {},
16696
+ ...input.context !== void 0 ? { context: input.context } : {},
16697
+ ...input.columns !== void 0 ? { columns: input.columns } : {}
16698
+ });
16699
+ const board = await createBoard2(projectRoot, boardInput);
16700
+ for (const taskInput2 of parseLinesIntoTasks(
16701
+ input.description,
16702
+ board.columns[0]?.id ?? "backlog"
16703
+ )) {
16704
+ await addTask(projectRoot, board.id, taskInput2);
16705
+ }
16706
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
16707
+ }
16708
+ case "export_markdown": {
16709
+ const board = await requireBoard(projectRoot, input.boardId);
16710
+ if (!board) return fail("Board not found.");
16711
+ return {
16712
+ ok: true,
16713
+ message: "Board exported.",
16714
+ board,
16715
+ markdown: exportBoardAsMarkdown(board)
16716
+ };
16717
+ }
16718
+ case "export_task_graph": {
16719
+ if (!input.boardId) return fail("export_task_graph requires boardId.");
16720
+ const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
16721
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
16722
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
16723
+ ...input.title !== void 0 ? { title: input.title } : {},
16724
+ ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
16725
+ ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
16726
+ });
16727
+ if (!exported) return fail("Board not found.");
16728
+ return {
16729
+ ok: true,
16730
+ message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
16731
+ board: exported.board,
16732
+ taskGraph: serializeTaskGraph(exported.graph)
16733
+ };
16734
+ }
16735
+ case "sync_task_graph": {
16736
+ if (!input.boardId || !input.taskGraph) {
16737
+ return fail("sync_task_graph requires boardId and taskGraph.");
16738
+ }
16739
+ const graph = deserializeTaskGraph2(input.taskGraph);
16740
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
16741
+ ...input.title !== void 0 ? { title: input.title } : {},
16742
+ ...input.description !== void 0 ? { description: input.description } : {},
16743
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16744
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16745
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16746
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16747
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16748
+ ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
16749
+ ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
16750
+ });
16751
+ return result2 ? {
16752
+ ok: true,
16753
+ message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
16754
+ board: result2.board
16755
+ } : fail("Board not found.");
16756
+ }
16757
+ case "create_from_graph": {
16758
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
16759
+ const graph = deserializeTaskGraph2(input.taskGraph);
16760
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16761
+ ...input.title !== void 0 ? { title: input.title } : {},
16762
+ ...input.description !== void 0 ? { description: input.description } : {},
16763
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16764
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16765
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16766
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16767
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
16768
+ });
16769
+ return {
16770
+ ok: true,
16771
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
16772
+ board
16773
+ };
16774
+ }
16775
+ case "import_session_tasks": {
16776
+ const taskPath = ctx.meta?.["task.path"];
16777
+ if (!taskPath) return fail("No session task file for this session.");
16778
+ const file = await loadTasks2(taskPath);
16779
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
16780
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
16781
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
16782
+ const tags = ["session", `session:${sessionId}`];
16783
+ const existing = (await listBoards2(projectRoot)).find(
16784
+ (b) => b.tags?.includes(`session:${sessionId}`)
16785
+ );
16786
+ if (existing) {
16787
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
16788
+ sourceSystem: "session",
16789
+ tags,
16790
+ archiveMissingTasks: true,
16791
+ includeCompletedTasks: true
16792
+ });
16793
+ return result2 ? {
16794
+ ok: true,
16795
+ message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
16796
+ board: result2.board
16797
+ } : fail("Session board vanished mid-sync.");
16798
+ }
16799
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16800
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
15986
16801
  sourceSystem: "session",
15987
- tags,
15988
- archiveMissingTasks: true,
15989
- includeCompletedTasks: true
16802
+ tags
15990
16803
  });
15991
- return result ? {
16804
+ return {
15992
16805
  ok: true,
15993
- message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
15994
- board: result.board
15995
- } : fail("Session board vanished mid-sync.");
16806
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
16807
+ board
16808
+ };
15996
16809
  }
15997
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
15998
- title: `Session tasks (${sessionId.slice(0, 8)})`,
15999
- sourceSystem: "session",
16000
- tags
16001
- });
16002
- return {
16003
- ok: true,
16004
- message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
16005
- board
16006
- };
16007
- }
16008
- case "search_tasks": {
16009
- const tasks = await searchKanban(projectRoot, {
16010
- query: input.query,
16011
- boardId: input.boardId,
16012
- assignedAgent: input.agentId,
16013
- status: input.status,
16014
- priority: input.priority,
16015
- label: input.labels?.[0],
16016
- chainId: input.chainId
16017
- });
16018
- return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
16019
- }
16020
- case "ready_tasks": {
16021
- const tasks = await listReadyTasks(projectRoot, {
16022
- query: input.query,
16023
- boardId: input.boardId,
16024
- assignedAgent: input.agentId,
16025
- priority: input.priority,
16026
- label: input.labels?.[0],
16027
- chainId: input.chainId,
16028
- limit: input.limit
16029
- });
16030
- return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
16031
- }
16032
- case "snapshot": {
16033
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
16034
- query: input.query,
16035
- boardId: input.boardId,
16036
- assignedAgent: input.agentId,
16037
- status: input.status,
16038
- priority: input.priority,
16039
- label: input.labels?.[0],
16040
- chainId: input.chainId
16041
- });
16042
- return {
16043
- ok: true,
16044
- message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
16045
- snapshot
16046
- };
16047
- }
16048
- case "add_column": {
16049
- if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
16050
- const result = await addColumn(projectRoot, input.boardId, {
16051
- title: input.title,
16052
- ...input.description !== void 0 ? { description: input.description } : {}
16053
- });
16054
- return result ? okBoard(result.board, "Column added.") : fail("Board not found.");
16055
- }
16056
- case "update_column": {
16057
- if (!input.boardId || !input.columnId)
16058
- return fail("update_column requires boardId and columnId.");
16059
- const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
16060
- ...input.title !== void 0 ? { title: input.title } : {},
16061
- ...input.description !== void 0 ? { description: input.description } : {},
16062
- ...input.order !== void 0 ? { order: input.order } : {}
16063
- });
16064
- return board ? okBoard(board, "Column updated.") : fail("Column not found.");
16065
- }
16066
- case "delete_column": {
16067
- if (!input.boardId || !input.columnId)
16068
- return fail("delete_column requires boardId and columnId.");
16069
- const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
16070
- moveTasksToColumnId: input.moveTasksToColumnId
16071
- });
16072
- return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
16073
- }
16074
- case "add_task": {
16075
- if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
16076
- const result = await addTask(projectRoot, input.boardId, taskInput(input));
16077
- return result ? okTask(result.board, result.task, "Task added.") : fail("Board not found.");
16078
- }
16079
- case "split_task": {
16080
- if (!input.boardId || !input.taskId || !input.childTitles?.length) {
16081
- return fail("split_task requires boardId, taskId, and childTitles.");
16810
+ case "search_tasks": {
16811
+ const tasks = await searchKanban(projectRoot, {
16812
+ query: input.query,
16813
+ boardId: input.boardId,
16814
+ assignedAgent: input.agentId,
16815
+ status: input.status,
16816
+ priority: input.priority,
16817
+ label: input.labels?.[0],
16818
+ chainId: input.chainId
16819
+ });
16820
+ return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
16082
16821
  }
16083
- const result = await splitTask(projectRoot, input.boardId, input.taskId, {
16084
- titles: input.childTitles,
16085
- ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
16086
- ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
16087
- ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
16088
- ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
16089
- ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
16090
- ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
16091
- ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
16092
- ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
16093
- });
16094
- return result ? {
16095
- ok: true,
16096
- message: `${result.children.length} child task(s) created.`,
16097
- board: result.board,
16098
- task: result.parent,
16099
- children: result.children
16100
- } : fail("Task not found.");
16101
- }
16102
- case "merge_tasks": {
16103
- if (!input.boardId || !input.taskIds?.length || !input.title) {
16104
- return fail("merge_tasks requires boardId, taskIds, and title.");
16822
+ case "ready_tasks": {
16823
+ const tasks = await listReadyTasks(projectRoot, {
16824
+ query: input.query,
16825
+ boardId: input.boardId,
16826
+ assignedAgent: input.agentId,
16827
+ priority: input.priority,
16828
+ label: input.labels?.[0],
16829
+ chainId: input.chainId,
16830
+ limit: input.limit
16831
+ });
16832
+ return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
16105
16833
  }
16106
- const result = await mergeTasks(projectRoot, input.boardId, {
16107
- taskIds: input.taskIds,
16108
- title: input.title,
16109
- ...input.description !== void 0 ? { description: input.description } : {},
16110
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16111
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16112
- ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
16113
- });
16114
- return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
16115
- }
16116
- case "copy_task": {
16117
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
16118
- return fail("copy_task requires boardId, taskId, and targetBoardId.");
16834
+ case "snapshot": {
16835
+ const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
16836
+ query: input.query,
16837
+ boardId: input.boardId,
16838
+ assignedAgent: input.agentId,
16839
+ status: input.status,
16840
+ priority: input.priority,
16841
+ label: input.labels?.[0],
16842
+ chainId: input.chainId
16843
+ });
16844
+ return {
16845
+ ok: true,
16846
+ message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
16847
+ snapshot
16848
+ };
16119
16849
  }
16120
- const result = await copyTaskToBoard(
16121
- projectRoot,
16122
- input.boardId,
16123
- input.taskId,
16124
- input.targetBoardId,
16125
- {
16126
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16127
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
16128
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16129
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16850
+ case "add_column": {
16851
+ if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
16852
+ const result2 = await addColumn(projectRoot, input.boardId, {
16853
+ title: input.title,
16854
+ ...input.description !== void 0 ? { description: input.description } : {}
16855
+ });
16856
+ return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
16857
+ }
16858
+ case "update_column": {
16859
+ if (!input.boardId || !input.columnId)
16860
+ return fail("update_column requires boardId and columnId.");
16861
+ const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
16862
+ ...input.title !== void 0 ? { title: input.title } : {},
16863
+ ...input.description !== void 0 ? { description: input.description } : {},
16864
+ ...input.order !== void 0 ? { order: input.order } : {}
16865
+ });
16866
+ return board ? okBoard(board, "Column updated.") : fail("Column not found.");
16867
+ }
16868
+ case "delete_column": {
16869
+ if (!input.boardId || !input.columnId)
16870
+ return fail("delete_column requires boardId and columnId.");
16871
+ const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
16872
+ moveTasksToColumnId: input.moveTasksToColumnId
16873
+ });
16874
+ return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
16875
+ }
16876
+ case "add_task": {
16877
+ if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
16878
+ const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
16879
+ return result2 ? okTask(result2.board, result2.task, "Task added.") : fail("Board not found.");
16880
+ }
16881
+ case "split_task": {
16882
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
16883
+ return fail("split_task requires boardId, taskId, and childTitles.");
16130
16884
  }
16131
- );
16132
- return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
16133
- }
16134
- case "transfer_task": {
16135
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
16136
- return fail("transfer_task requires boardId, taskId, and targetBoardId.");
16885
+ const result2 = await splitTask(projectRoot, input.boardId, input.taskId, {
16886
+ titles: input.childTitles,
16887
+ ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
16888
+ ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
16889
+ ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
16890
+ ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
16891
+ ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
16892
+ ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
16893
+ ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
16894
+ ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
16895
+ });
16896
+ return result2 ? {
16897
+ ok: true,
16898
+ message: `${result2.children.length} child task(s) created.`,
16899
+ board: result2.board,
16900
+ task: result2.parent,
16901
+ children: result2.children
16902
+ } : fail("Task not found.");
16137
16903
  }
16138
- const result = await transferTaskToBoard(
16139
- projectRoot,
16140
- input.boardId,
16141
- input.taskId,
16142
- input.targetBoardId,
16143
- {
16904
+ case "merge_tasks": {
16905
+ if (!input.boardId || !input.taskIds?.length || !input.title) {
16906
+ return fail("merge_tasks requires boardId, taskIds, and title.");
16907
+ }
16908
+ const result2 = await mergeTasks(projectRoot, input.boardId, {
16909
+ taskIds: input.taskIds,
16910
+ title: input.title,
16911
+ ...input.description !== void 0 ? { description: input.description } : {},
16144
16912
  ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16145
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
16146
16913
  ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16147
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16914
+ ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
16915
+ });
16916
+ return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
16917
+ }
16918
+ case "copy_task": {
16919
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
16920
+ return fail("copy_task requires boardId, taskId, and targetBoardId.");
16148
16921
  }
16149
- );
16150
- return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
16151
- }
16152
- case "get_task": {
16153
- if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
16154
- const task = await getTask(projectRoot, input.boardId, input.taskId);
16155
- return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
16156
- }
16157
- case "update_task": {
16158
- if (!input.boardId || !input.taskId)
16159
- return fail("update_task requires boardId and taskId.");
16160
- const board = await updateTask(
16161
- projectRoot,
16162
- input.boardId,
16163
- input.taskId,
16164
- taskPatch(input)
16165
- );
16166
- return board ? okBoard(board, "Task updated.") : fail("Task not found.");
16167
- }
16168
- case "move_task": {
16169
- if (!input.boardId || !input.taskId || !input.targetColumnId) {
16170
- return fail("move_task requires boardId, taskId, and targetColumnId.");
16922
+ const result2 = await copyTaskToBoard(
16923
+ projectRoot,
16924
+ input.boardId,
16925
+ input.taskId,
16926
+ input.targetBoardId,
16927
+ {
16928
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16929
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
16930
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16931
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16932
+ }
16933
+ );
16934
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
16171
16935
  }
16172
- const board = await moveTask(
16173
- projectRoot,
16174
- input.boardId,
16175
- input.taskId,
16176
- input.targetColumnId,
16177
- input.order
16178
- );
16179
- return board ? okBoard(board, "Task moved.") : fail("Move failed.");
16180
- }
16181
- case "delete_task": {
16182
- if (!input.boardId || !input.taskId)
16183
- return fail("delete_task requires boardId and taskId.");
16184
- const board = await removeTask(projectRoot, input.boardId, input.taskId);
16185
- return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
16186
- }
16187
- case "set_chain": {
16188
- if (!input.boardId || !input.taskIds?.length) {
16189
- return fail("set_chain requires boardId and taskIds.");
16936
+ case "transfer_task": {
16937
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
16938
+ return fail("transfer_task requires boardId, taskId, and targetBoardId.");
16939
+ }
16940
+ const result2 = await transferTaskToBoard(
16941
+ projectRoot,
16942
+ input.boardId,
16943
+ input.taskId,
16944
+ input.targetBoardId,
16945
+ {
16946
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16947
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
16948
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16949
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16950
+ }
16951
+ );
16952
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
16190
16953
  }
16191
- const result = await setTaskChain(projectRoot, input.boardId, {
16192
- taskIds: input.taskIds,
16193
- ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
16194
- ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
16195
- });
16196
- return result ? {
16197
- ok: true,
16198
- message: `Chain set: ${result.chainId}`,
16199
- board: result.board,
16200
- chain: result.tasks
16201
- } : fail("Board or task not found.");
16202
- }
16203
- case "get_chain": {
16204
- if (!input.boardId || !(input.taskId || input.chainId)) {
16205
- return fail("get_chain requires boardId and taskId or chainId.");
16954
+ case "get_task": {
16955
+ if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
16956
+ const task = await getTask(projectRoot, input.boardId, input.taskId);
16957
+ return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
16206
16958
  }
16207
- const result = await getTaskChain(
16208
- projectRoot,
16209
- input.boardId,
16210
- input.taskId ?? input.chainId ?? ""
16211
- );
16212
- return result ? {
16213
- ok: true,
16214
- message: `Chain loaded: ${result.chainId}`,
16215
- board: result.board,
16216
- chain: result.tasks
16217
- } : fail("Chain not found.");
16218
- }
16219
- case "claim_task": {
16220
- const result = await claimReadyTask(projectRoot, {
16221
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
16222
- ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
16223
- ...assignmentInput(input),
16224
- status: input.assignmentStatus ?? "queued"
16225
- });
16226
- return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
16227
- }
16228
- case "release_task": {
16229
- if (!input.boardId || !input.taskId) {
16230
- return fail("release_task requires boardId and taskId.");
16959
+ case "update_task": {
16960
+ if (!input.boardId || !input.taskId)
16961
+ return fail("update_task requires boardId and taskId.");
16962
+ const board = await updateTask(
16963
+ projectRoot,
16964
+ input.boardId,
16965
+ input.taskId,
16966
+ taskPatch(input)
16967
+ );
16968
+ return board ? okBoard(board, "Task updated.") : fail("Task not found.");
16231
16969
  }
16232
- const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
16233
- ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
16234
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
16235
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
16236
- });
16237
- return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
16238
- }
16239
- case "assign_task": {
16240
- if (!input.boardId || !input.taskId)
16241
- return fail("assign_task requires boardId and taskId.");
16242
- const board = await assignTask(
16243
- projectRoot,
16244
- input.boardId,
16245
- input.taskId,
16246
- assignmentInput(input)
16247
- );
16248
- return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
16249
- }
16250
- case "mark_assignment": {
16251
- if (!input.boardId || !input.taskId)
16252
- return fail("mark_assignment requires boardId and taskId.");
16253
- const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
16254
- const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
16255
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
16256
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
16257
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
16258
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
16259
- ...input.error !== void 0 ? { error: input.error } : {},
16260
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
16261
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
16262
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
16263
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
16264
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
16265
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
16266
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
16267
- });
16268
- return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
16269
- }
16270
- case "heartbeat_assignment": {
16271
- if (!input.boardId || !input.taskId) {
16272
- return fail("heartbeat_assignment requires boardId and taskId.");
16970
+ case "transition_task": {
16971
+ if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
16972
+ return fail(
16973
+ "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
16974
+ );
16975
+ }
16976
+ const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
16977
+ to: input.lifecycleStage,
16978
+ actor: input.author,
16979
+ comment: input.transitionComment,
16980
+ ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
16981
+ ...input.attachmentUrl !== void 0 ? {
16982
+ attachment: {
16983
+ url: input.attachmentUrl,
16984
+ type: input.attachmentType ?? "url",
16985
+ ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
16986
+ }
16987
+ } : {},
16988
+ patch: taskPatch(input)
16989
+ });
16990
+ return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
16273
16991
  }
16274
- const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
16275
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
16276
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
16277
- });
16278
- return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
16279
- }
16280
- case "recover_stale": {
16281
- if (!input.boardId) return fail("recover_stale requires boardId.");
16282
- const policyFields = [
16283
- input.recoveryPolicyFailOnCostCeiling !== void 0,
16284
- input.recoveryPolicyReleaseOnFailureKinds !== void 0,
16285
- input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
16286
- input.recoveryPolicyRetryPolicyOverride !== void 0
16287
- ].some(Boolean);
16288
- const result = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
16289
- ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
16290
- ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
16291
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
16292
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
16293
- ...policyFields ? {
16294
- policy: {
16295
- ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
16296
- ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
16297
- releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
16298
- } : {},
16299
- ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
16300
- releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
16301
- } : {},
16302
- ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
16303
- retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
16304
- } : {}
16305
- }
16306
- } : {}
16307
- });
16308
- return result ? {
16309
- ok: true,
16310
- message: `Recovered ${result.tasks.length} stale assignment(s).`,
16311
- board: result.board,
16312
- recoveredTasks: result.tasks
16313
- } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
16314
- }
16315
- case "events": {
16316
- if (!input.boardId) return fail("events requires boardId.");
16317
- const eventList = await listKanbanEvents(projectRoot, input.boardId);
16318
- return {
16319
- ok: true,
16320
- message: `${eventList.length} event(s).`,
16321
- events: eventList
16322
- };
16323
- }
16324
- case "queue_health": {
16325
- const health = await getKanbanQueueHealth(projectRoot, {
16326
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
16327
- });
16328
- return {
16329
- ok: true,
16330
- message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
16331
- queueHealth: health
16332
- };
16333
- }
16334
- case "add_dependency": {
16335
- if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
16336
- return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
16992
+ case "move_task": {
16993
+ if (!input.boardId || !input.taskId || !input.targetColumnId) {
16994
+ return fail("move_task requires boardId, taskId, and targetColumnId.");
16995
+ }
16996
+ const board = await moveTask(
16997
+ projectRoot,
16998
+ input.boardId,
16999
+ input.taskId,
17000
+ input.targetColumnId,
17001
+ input.order
17002
+ );
17003
+ return board ? okBoard(board, "Task moved.") : fail("Move failed.");
16337
17004
  }
16338
- const board = await addDependency(
16339
- projectRoot,
16340
- input.boardId,
16341
- input.taskId,
16342
- input.dependencyTaskId
16343
- );
16344
- return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
16345
- }
16346
- case "add_goal_metric": {
16347
- if (!input.boardId || !input.taskId || !input.metricName) {
16348
- return fail("add_goal_metric requires boardId, taskId, and metricName.");
17005
+ case "delete_task": {
17006
+ if (!input.boardId || !input.taskId)
17007
+ return fail("delete_task requires boardId and taskId.");
17008
+ const board = await removeTask(projectRoot, input.boardId, input.taskId);
17009
+ return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
16349
17010
  }
16350
- const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
16351
- name: input.metricName,
16352
- ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
16353
- ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
16354
- ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
16355
- ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
16356
- ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
16357
- });
16358
- return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
16359
- }
16360
- case "update_goal_metric": {
16361
- if (!input.boardId || !input.taskId || !input.metricId) {
16362
- return fail("update_goal_metric requires boardId, taskId, and metricId.");
17011
+ case "set_chain": {
17012
+ if (!input.boardId || !input.taskIds?.length) {
17013
+ return fail("set_chain requires boardId and taskIds.");
17014
+ }
17015
+ const result2 = await setTaskChain(projectRoot, input.boardId, {
17016
+ taskIds: input.taskIds,
17017
+ ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
17018
+ ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
17019
+ });
17020
+ return result2 ? {
17021
+ ok: true,
17022
+ message: `Chain set: ${result2.chainId}`,
17023
+ board: result2.board,
17024
+ chain: result2.tasks
17025
+ } : fail("Board or task not found.");
16363
17026
  }
16364
- const board = await updateGoalMetricOnTask(
16365
- projectRoot,
16366
- input.boardId,
16367
- input.taskId,
16368
- input.metricId,
16369
- {
16370
- ...input.metricName !== void 0 ? { name: input.metricName } : {},
17027
+ case "get_chain": {
17028
+ if (!input.boardId || !(input.taskId || input.chainId)) {
17029
+ return fail("get_chain requires boardId and taskId or chainId.");
17030
+ }
17031
+ const result2 = await getTaskChain(
17032
+ projectRoot,
17033
+ input.boardId,
17034
+ input.taskId ?? input.chainId ?? ""
17035
+ );
17036
+ return result2 ? {
17037
+ ok: true,
17038
+ message: `Chain loaded: ${result2.chainId}`,
17039
+ board: result2.board,
17040
+ chain: result2.tasks
17041
+ } : fail("Chain not found.");
17042
+ }
17043
+ case "claim_task": {
17044
+ const result2 = await claimReadyTask(projectRoot, {
17045
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
17046
+ ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
17047
+ ...assignmentInput(input),
17048
+ status: input.assignmentStatus ?? "queued"
17049
+ });
17050
+ return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
17051
+ }
17052
+ case "release_task": {
17053
+ if (!input.boardId || !input.taskId) {
17054
+ return fail("release_task requires boardId and taskId.");
17055
+ }
17056
+ const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
17057
+ ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
17058
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17059
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
17060
+ });
17061
+ return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
17062
+ }
17063
+ case "assign_task": {
17064
+ if (!input.boardId || !input.taskId)
17065
+ return fail("assign_task requires boardId and taskId.");
17066
+ const board = await assignTask(
17067
+ projectRoot,
17068
+ input.boardId,
17069
+ input.taskId,
17070
+ assignmentInput(input)
17071
+ );
17072
+ return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
17073
+ }
17074
+ case "mark_assignment": {
17075
+ if (!input.boardId || !input.taskId)
17076
+ return fail("mark_assignment requires boardId and taskId.");
17077
+ const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
17078
+ const board = await updateTaskAssignment(
17079
+ projectRoot,
17080
+ input.boardId,
17081
+ input.taskId,
17082
+ {
17083
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17084
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17085
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17086
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17087
+ ...input.error !== void 0 ? { error: input.error } : {},
17088
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17089
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17090
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17091
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17092
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17093
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17094
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17095
+ },
17096
+ // Ownership fence: when expectedLeaseId is supplied, the write is
17097
+ // applied only if the current assignment still holds this lease.
17098
+ // This prevents a recovered+reassigned stale worker's terminal
17099
+ // mark_assignment from overwriting the successor's state. The check
17100
+ // is atomic inside updateTaskAssignment's mutateBoard lock.
17101
+ input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17102
+ );
17103
+ return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
17104
+ }
17105
+ case "heartbeat_assignment": {
17106
+ if (!input.boardId || !input.taskId) {
17107
+ return fail("heartbeat_assignment requires boardId and taskId.");
17108
+ }
17109
+ const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
17110
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17111
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17112
+ // Ownership fence: when expectedLeaseId is supplied, the renewal
17113
+ // is applied only if the current assignment still holds this lease.
17114
+ // This prevents a recovered+reassigned stale worker's heartbeat
17115
+ // from renewing the successor's lease. The check is atomic inside
17116
+ // heartbeatTaskAssignment's mutateBoard lock.
17117
+ ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17118
+ });
17119
+ return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
17120
+ }
17121
+ case "recover_stale": {
17122
+ if (!input.boardId) return fail("recover_stale requires boardId.");
17123
+ const policyFields = [
17124
+ input.recoveryPolicyFailOnCostCeiling !== void 0,
17125
+ input.recoveryPolicyReleaseOnFailureKinds !== void 0,
17126
+ input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
17127
+ input.recoveryPolicyRetryPolicyOverride !== void 0
17128
+ ].some(Boolean);
17129
+ const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
17130
+ ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
17131
+ ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
17132
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17133
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
17134
+ ...policyFields ? {
17135
+ policy: {
17136
+ ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
17137
+ ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
17138
+ releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
17139
+ } : {},
17140
+ ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
17141
+ releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
17142
+ } : {},
17143
+ ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
17144
+ retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
17145
+ } : {}
17146
+ }
17147
+ } : {}
17148
+ });
17149
+ return result2 ? {
17150
+ ok: true,
17151
+ message: `Recovered ${result2.tasks.length} stale assignment(s).`,
17152
+ board: result2.board,
17153
+ recoveredTasks: result2.tasks
17154
+ } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
17155
+ }
17156
+ case "events": {
17157
+ if (!input.boardId) return fail("events requires boardId.");
17158
+ const eventList = await listKanbanEvents(projectRoot, input.boardId);
17159
+ return {
17160
+ ok: true,
17161
+ message: `${eventList.length} event(s).`,
17162
+ events: eventList
17163
+ };
17164
+ }
17165
+ case "queue_health": {
17166
+ const health = await getKanbanQueueHealth(projectRoot, {
17167
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
17168
+ });
17169
+ return {
17170
+ ok: true,
17171
+ message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
17172
+ queueHealth: health
17173
+ };
17174
+ }
17175
+ case "add_dependency": {
17176
+ if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
17177
+ return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
17178
+ }
17179
+ const board = await addDependency(
17180
+ projectRoot,
17181
+ input.boardId,
17182
+ input.taskId,
17183
+ input.dependencyTaskId
17184
+ );
17185
+ return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
17186
+ }
17187
+ case "add_goal_metric": {
17188
+ if (!input.boardId || !input.taskId || !input.metricName) {
17189
+ return fail("add_goal_metric requires boardId, taskId, and metricName.");
17190
+ }
17191
+ const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
17192
+ name: input.metricName,
16371
17193
  ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
16372
17194
  ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
16373
17195
  ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
16374
17196
  ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
16375
17197
  ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17198
+ });
17199
+ return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
17200
+ }
17201
+ case "update_goal_metric": {
17202
+ if (!input.boardId || !input.taskId || !input.metricId) {
17203
+ return fail("update_goal_metric requires boardId, taskId, and metricId.");
16376
17204
  }
16377
- );
16378
- return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
16379
- }
16380
- case "add_check": {
16381
- if (!input.boardId || !input.taskId || !input.checkDescription) {
16382
- return fail("add_check requires boardId, taskId, and checkDescription.");
17205
+ const board = await updateGoalMetricOnTask(
17206
+ projectRoot,
17207
+ input.boardId,
17208
+ input.taskId,
17209
+ input.metricId,
17210
+ {
17211
+ ...input.metricName !== void 0 ? { name: input.metricName } : {},
17212
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
17213
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17214
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17215
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17216
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17217
+ }
17218
+ );
17219
+ return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
16383
17220
  }
16384
- const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
16385
- description: input.checkDescription,
16386
- type: "manual",
16387
- status: input.checkStatus
16388
- });
16389
- return board ? okBoard(board, "Check added.") : fail("Task not found.");
16390
- }
16391
- case "update_check": {
16392
- if (!input.boardId || !input.taskId || !input.checkId) {
16393
- return fail("update_check requires boardId, taskId, and checkId.");
17221
+ case "add_check": {
17222
+ if (!input.boardId || !input.taskId || !input.checkDescription) {
17223
+ return fail("add_check requires boardId, taskId, and checkDescription.");
17224
+ }
17225
+ const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
17226
+ description: input.checkDescription,
17227
+ type: "manual",
17228
+ status: input.checkStatus
17229
+ });
17230
+ return board ? okBoard(board, "Check added.") : fail("Task not found.");
16394
17231
  }
16395
- const board = await updateCheckOnTask(
16396
- projectRoot,
16397
- input.boardId,
16398
- input.taskId,
16399
- input.checkId,
16400
- {
16401
- ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
16402
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17232
+ case "update_check": {
17233
+ if (!input.boardId || !input.taskId || !input.checkId) {
17234
+ return fail("update_check requires boardId, taskId, and checkId.");
16403
17235
  }
16404
- );
16405
- return board ? okBoard(board, "Check updated.") : fail("Check not found.");
16406
- }
16407
- case "add_note": {
16408
- if (!input.boardId || !input.taskId || !input.note)
16409
- return fail("add_note requires boardId, taskId, and note.");
16410
- const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
16411
- author: input.author ?? "agent",
16412
- content: input.note
16413
- });
16414
- return board ? okBoard(board, "Note added.") : fail("Task not found.");
16415
- }
16416
- case "add_link": {
16417
- if (!input.boardId || !input.taskId || !input.url)
16418
- return fail("add_link requires boardId, taskId, and url.");
16419
- const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
16420
- url: input.url,
16421
- type: input.linkType ?? "url",
16422
- ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
16423
- });
16424
- return board ? okBoard(board, "Link added.") : fail("Task not found.");
17236
+ const board = await updateCheckOnTask(
17237
+ projectRoot,
17238
+ input.boardId,
17239
+ input.taskId,
17240
+ input.checkId,
17241
+ {
17242
+ ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
17243
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17244
+ }
17245
+ );
17246
+ return board ? okBoard(board, "Check updated.") : fail("Check not found.");
17247
+ }
17248
+ case "add_note": {
17249
+ if (!input.boardId || !input.taskId || !input.note)
17250
+ return fail("add_note requires boardId, taskId, and note.");
17251
+ const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
17252
+ author: input.author ?? "agent",
17253
+ content: input.note
17254
+ });
17255
+ return board ? okBoard(board, "Note added.") : fail("Task not found.");
17256
+ }
17257
+ case "add_link": {
17258
+ if (!input.boardId || !input.taskId || !input.url)
17259
+ return fail("add_link requires boardId, taskId, and url.");
17260
+ const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
17261
+ url: input.url,
17262
+ type: input.linkType ?? "url",
17263
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17264
+ });
17265
+ return board ? okBoard(board, "Link added.") : fail("Task not found.");
17266
+ }
17267
+ default:
17268
+ return fail(`Unknown kanban action: ${input.action}`);
16425
17269
  }
16426
- default:
16427
- return fail(`Unknown kanban action: ${input.action}`);
16428
- }
17270
+ })();
17271
+ return withPresence(result);
16429
17272
  } catch (err) {
16430
17273
  return fail(err instanceof Error ? err.message : String(err));
16431
17274
  }
@@ -16449,6 +17292,7 @@ function taskInput(input) {
16449
17292
  title: input.title ?? "",
16450
17293
  columnId: input.columnId,
16451
17294
  description: input.description,
17295
+ dueDate: input.dueDate,
16452
17296
  priority: input.priority,
16453
17297
  ...input.taskType !== void 0 ? { type: input.taskType } : {},
16454
17298
  status: input.status,
@@ -16457,7 +17301,67 @@ function taskInput(input) {
16457
17301
  ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
16458
17302
  ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16459
17303
  ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
16460
- ...assignment ? { assignment } : {}
17304
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
17305
+ ...assignment ? { assignment } : {},
17306
+ ...input.order !== void 0 ? { order: input.order } : {},
17307
+ // Sprint 3 durable task-level fields
17308
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
17309
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
17310
+ // Relationships — chainId alone cannot construct a KanbanTaskChainRef,
17311
+ // so chain/previousTaskId/nextTaskId must be set via update_task.
17312
+ ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
17313
+ // Detail objects
17314
+ ...input.checkDescription !== void 0 ? {
17315
+ successCriteria: [
17316
+ {
17317
+ id: randomUUID(),
17318
+ description: input.checkDescription,
17319
+ type: "manual",
17320
+ status: input.checkStatus ?? "pending"
17321
+ }
17322
+ ]
17323
+ } : {},
17324
+ ...input.metricName !== void 0 ? {
17325
+ goalMetrics: [
17326
+ {
17327
+ id: randomUUID(),
17328
+ name: input.metricName,
17329
+ status: input.metricStatus ?? "pending",
17330
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17331
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17332
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17333
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17334
+ }
17335
+ ]
17336
+ } : {},
17337
+ ...input.url !== void 0 ? {
17338
+ links: [
17339
+ {
17340
+ url: input.url,
17341
+ type: input.linkType ?? "url",
17342
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17343
+ }
17344
+ ]
17345
+ } : {},
17346
+ ...input.note !== void 0 ? {
17347
+ notes: [
17348
+ {
17349
+ id: randomUUID(),
17350
+ author: input.author ?? "agent",
17351
+ content: input.note,
17352
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
17353
+ }
17354
+ ]
17355
+ } : {},
17356
+ // Origin tracking
17357
+ ...input.graphId !== void 0 ? {
17358
+ origin: {
17359
+ system: input.sourceSystem ?? "kanban-tool",
17360
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
17361
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
17362
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
17363
+ }
17364
+ } : {}
16461
17365
  };
16462
17366
  }
16463
17367
  function mergedDependsOn(input) {
@@ -16471,6 +17375,7 @@ function taskPatch(input) {
16471
17375
  return {
16472
17376
  title: input.title,
16473
17377
  description: input.description,
17378
+ dueDate: input.dueDate,
16474
17379
  columnId: input.columnId,
16475
17380
  order: input.order,
16476
17381
  priority: input.priority,
@@ -17232,7 +18137,7 @@ import {
17232
18137
  mutateTasks as mutateTasks2,
17233
18138
  formatTaskList
17234
18139
  } from "@wrongstack/core";
17235
- import { randomUUID } from "node:crypto";
18140
+ import { randomUUID as randomUUID2 } from "node:crypto";
17236
18141
  var planTool = {
17237
18142
  name: "plan",
17238
18143
  category: "Session",
@@ -17458,7 +18363,7 @@ var planTool = {
17458
18363
  try {
17459
18364
  const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
17460
18365
  f.tasks.push({
17461
- id: `task_${randomUUID()}`,
18366
+ id: `task_${randomUUID2()}`,
17462
18367
  title: taskifyMeta.title,
17463
18368
  description: taskifyMeta.details || void 0,
17464
18369
  type: "feature",
@@ -18600,7 +19505,7 @@ import {
18600
19505
  mutatePlan as mutatePlan3,
18601
19506
  formatPlan as formatPlan2
18602
19507
  } from "@wrongstack/core";
18603
- import { randomUUID as randomUUID2 } from "node:crypto";
19508
+ import { randomUUID as randomUUID3 } from "node:crypto";
18604
19509
  function findTaskIndex(tasks, query) {
18605
19510
  const asNum = Number.parseInt(query, 10);
18606
19511
  if (!Number.isNaN(asNum)) {
@@ -18788,7 +19693,7 @@ var taskTool = {
18788
19693
  }
18789
19694
  const now = (/* @__PURE__ */ new Date()).toISOString();
18790
19695
  const newTask = {
18791
- id: `task_${Date.now()}_${randomUUID2().slice(0, 8)}`,
19696
+ id: `task_${Date.now()}_${randomUUID3().slice(0, 8)}`,
18792
19697
  title: t.title,
18793
19698
  description: t.description,
18794
19699
  type: t.type || "feature",
@@ -18849,7 +19754,7 @@ var taskTool = {
18849
19754
  });
18850
19755
  if (match.description) {
18851
19756
  todos.push({
18852
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
19757
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
18853
19758
  content: match.description.slice(0, 200),
18854
19759
  status: "pending",
18855
19760
  promotedFromTask: match.id
@@ -18858,7 +19763,7 @@ var taskTool = {
18858
19763
  if (input.subtasks && input.subtasks.length > 0) {
18859
19764
  for (const st of input.subtasks) {
18860
19765
  todos.push({
18861
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
19766
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
18862
19767
  content: st,
18863
19768
  status: "pending",
18864
19769
  promotedFromTask: match.id
@@ -19564,8 +20469,8 @@ var DEFAULT_IGNORE5 = [
19564
20469
  var treeTool = {
19565
20470
  name: "tree",
19566
20471
  category: "Filesystem",
19567
- 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.",
19568
- 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.",
20472
+ 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.",
20473
+ 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.",
19569
20474
  permission: "auto",
19570
20475
  mutating: false,
19571
20476
  capabilities: ["fs.read"],
@@ -19968,9 +20873,6 @@ var OPTIONAL_TOOLS = [
19968
20873
  toolUseTool,
19969
20874
  batchToolUseTool,
19970
20875
  toolHelpTool,
19971
- codebaseIndexTool,
19972
- codebaseSearchTool,
19973
- codebaseStatsTool,
19974
20876
  setWorkingDirTool
19975
20877
  ];
19976
20878
  var builtinTools = [
@@ -19979,6 +20881,9 @@ var builtinTools = [
19979
20881
  readTool,
19980
20882
  writeTool,
19981
20883
  editTool,
20884
+ codebaseStatsTool,
20885
+ codebaseSearchTool,
20886
+ codebaseIndexTool,
19982
20887
  replaceTool,
19983
20888
  globTool,
19984
20889
  grepTool,
@@ -20013,9 +20918,6 @@ var builtinTools = [
20013
20918
  toolUseTool,
20014
20919
  batchToolUseTool,
20015
20920
  toolHelpTool,
20016
- codebaseIndexTool,
20017
- codebaseSearchTool,
20018
- codebaseStatsTool,
20019
20921
  setWorkingDirTool
20020
20922
  ];
20021
20923