@wrongstack/tools 0.287.0 → 0.289.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/_fetch-guard.d.ts.map +1 -1
  2. package/dist/_redact-command.d.ts.map +1 -1
  3. package/dist/_spawn-stream.d.ts.map +1 -1
  4. package/dist/audit.js +61 -14
  5. package/dist/audit.js.map +3 -3
  6. package/dist/bash.d.ts +1 -1
  7. package/dist/bash.d.ts.map +1 -1
  8. package/dist/bash.js +97 -19
  9. package/dist/bash.js.map +3 -3
  10. package/dist/builtin.d.ts +12 -10
  11. package/dist/builtin.d.ts.map +1 -1
  12. package/dist/builtin.js +1620 -803
  13. package/dist/builtin.js.map +3 -3
  14. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  15. package/dist/codebase-index/bm25.d.ts +1 -2
  16. package/dist/codebase-index/bm25.d.ts.map +1 -1
  17. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  18. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  19. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  20. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  21. package/dist/codebase-index/index-service.d.ts +11 -1
  22. package/dist/codebase-index/index-service.d.ts.map +1 -1
  23. package/dist/codebase-index/index.d.ts +2 -1
  24. package/dist/codebase-index/index.d.ts.map +1 -1
  25. package/dist/codebase-index/index.js +749 -192
  26. package/dist/codebase-index/index.js.map +3 -3
  27. package/dist/codebase-index/indexer.d.ts.map +1 -1
  28. package/dist/codebase-index/schema.d.ts +43 -1
  29. package/dist/codebase-index/schema.d.ts.map +1 -1
  30. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  31. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  32. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  33. package/dist/codebase-index/worker.js +610 -152
  34. package/dist/codebase-index/worker.js.map +3 -3
  35. package/dist/codebase-index/writer.d.ts +27 -1
  36. package/dist/codebase-index/writer.d.ts.map +1 -1
  37. package/dist/e2e.js +477 -0
  38. package/dist/e2e.js.map +7 -0
  39. package/dist/exec.d.ts +1 -1
  40. package/dist/exec.d.ts.map +1 -1
  41. package/dist/exec.js +60 -5
  42. package/dist/exec.js.map +3 -3
  43. package/dist/fetch.js +2 -2
  44. package/dist/fetch.js.map +2 -2
  45. package/dist/format.js +61 -14
  46. package/dist/format.js.map +3 -3
  47. package/dist/glob.js +2 -2
  48. package/dist/glob.js.map +1 -1
  49. package/dist/grep.d.ts.map +1 -1
  50. package/dist/grep.js +2 -2
  51. package/dist/grep.js.map +2 -2
  52. package/dist/index.d.ts +3 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1786 -809
  55. package/dist/index.js.map +3 -3
  56. package/dist/install.js +61 -14
  57. package/dist/install.js.map +3 -3
  58. package/dist/kanban.d.ts +11 -2
  59. package/dist/kanban.d.ts.map +1 -1
  60. package/dist/kanban.js +695 -571
  61. package/dist/kanban.js.map +3 -3
  62. package/dist/languages/detect.d.ts.map +1 -1
  63. package/dist/languages/index.js +61 -14
  64. package/dist/languages/index.js.map +3 -3
  65. package/dist/languages/types.d.ts +7 -0
  66. package/dist/languages/types.d.ts.map +1 -1
  67. package/dist/lint.js +61 -14
  68. package/dist/lint.js.map +3 -3
  69. package/dist/outdated.js +61 -14
  70. package/dist/outdated.js.map +3 -3
  71. package/dist/pack.js +1617 -800
  72. package/dist/pack.js.map +3 -3
  73. package/dist/plan.js +4 -0
  74. package/dist/plan.js.map +2 -2
  75. package/dist/process-registry.js +12 -5
  76. package/dist/process-registry.js.map +2 -2
  77. package/dist/search.js +2 -2
  78. package/dist/search.js.map +2 -2
  79. package/dist/session-kanban.d.ts +9 -0
  80. package/dist/session-kanban.d.ts.map +1 -1
  81. package/dist/session-kanban.js +137 -0
  82. package/dist/session-kanban.js.map +2 -2
  83. package/dist/task.js +4 -0
  84. package/dist/task.js.map +2 -2
  85. package/dist/test.js +61 -14
  86. package/dist/test.js.map +3 -3
  87. package/dist/todo.js +4 -0
  88. package/dist/todo.js.map +2 -2
  89. package/dist/tree.js +2 -2
  90. package/dist/tree.js.map +1 -1
  91. package/dist/typecheck.js +61 -14
  92. package/dist/typecheck.js.map +3 -3
  93. package/package.json +7 -3
package/dist/builtin.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
  }
@@ -7678,7 +7820,7 @@ function sleepSync(ms) {
7678
7820
  } catch {
7679
7821
  }
7680
7822
  }
7681
- var IndexStore = class {
7823
+ var IndexStore = class _IndexStore {
7682
7824
  db;
7683
7825
  /** Absolute path to this project's index directory. */
7684
7826
  indexDir;
@@ -7730,10 +7872,7 @@ var IndexStore = class {
7730
7872
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
7731
7873
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
7732
7874
  }
7733
- const delay = Math.min(
7734
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
7735
- LOCK_RETRY_MAX_DELAY_MS
7736
- );
7875
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
7737
7876
  sleepSync(delay);
7738
7877
  }
7739
7878
  }
@@ -7815,8 +7954,24 @@ var IndexStore = class {
7815
7954
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
7816
7955
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
7817
7956
  try {
7818
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
7957
+ this.db.exec(
7958
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
7959
+ );
7819
7960
  this.ftsAvailable = true;
7961
+ const symbolCount = Number(
7962
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
7963
+ );
7964
+ const ftsCount = Number(
7965
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
7966
+ );
7967
+ if (symbolCount !== ftsCount) {
7968
+ this.db.exec("DELETE FROM symbols_fts");
7969
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
7970
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
7971
+ for (const row of rows) {
7972
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
7973
+ }
7974
+ }
7820
7975
  } catch {
7821
7976
  this.ftsAvailable = false;
7822
7977
  }
@@ -7874,7 +8029,9 @@ var IndexStore = class {
7874
8029
  deleteSymbolsForFile(file) {
7875
8030
  this.runWithRetry(() => {
7876
8031
  if (this.ftsAvailable) {
7877
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8032
+ this.stmt(
8033
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8034
+ ).run(file);
7878
8035
  }
7879
8036
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7880
8037
  });
@@ -7886,9 +8043,21 @@ var IndexStore = class {
7886
8043
  */
7887
8044
  deleteFile(file) {
7888
8045
  this.runWithRetry(() => {
7889
- this.deleteRefsForFile(file);
7890
- this.deleteSymbolsForFile(file);
7891
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
8046
+ this.db.exec("BEGIN IMMEDIATE");
8047
+ try {
8048
+ if (this.ftsAvailable) {
8049
+ this.db.prepare(
8050
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8051
+ ).run(file);
8052
+ }
8053
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8054
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
8055
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
8056
+ this.db.exec("COMMIT");
8057
+ } catch (err) {
8058
+ this.db.exec("ROLLBACK");
8059
+ throw err;
8060
+ }
7892
8061
  });
7893
8062
  }
7894
8063
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -7911,14 +8080,22 @@ var IndexStore = class {
7911
8080
  ).all(file);
7912
8081
  if (!rows.length) return null;
7913
8082
  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 };
8083
+ return {
8084
+ file: r.file,
8085
+ lang: r.lang,
8086
+ mtimeMs: r.mtime_ms,
8087
+ symbolCount: r.symbol_count,
8088
+ lastIndexed: r.last_indexed
8089
+ };
7915
8090
  }
7916
8091
  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
- );
8092
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8093
+ file: r.file,
8094
+ lang: r.lang,
8095
+ mtimeMs: r.mtime_ms,
8096
+ symbolCount: r.symbol_count,
8097
+ lastIndexed: r.last_indexed
8098
+ }));
7922
8099
  }
7923
8100
  // ─── Search ──────────────────────────────────────────────────────────────────
7924
8101
  search(query, filter) {
@@ -7942,8 +8119,8 @@ var IndexStore = class {
7942
8119
  values.push(filter.lang);
7943
8120
  }
7944
8121
  if (filter?.file) {
7945
- conditions.push("file LIKE ?");
7946
- values.push(`%${filter.file}%`);
8122
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
8123
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
7947
8124
  }
7948
8125
  if (query.trim()) {
7949
8126
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -7981,9 +8158,11 @@ var IndexStore = class {
7981
8158
  * "users", camelCase-split text makes "complex" find "complexOperation").
7982
8159
  */
7983
8160
  searchRanked(query, filter, limit) {
8161
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
8162
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
7984
8163
  const tokens = tokenise(query);
7985
8164
  if (tokens.length === 0 || !this.ftsAvailable) {
7986
- return this.searchRankedFallback(query, filter, limit);
8165
+ return this.searchRankedFallback(query, filter, safeLimit);
7987
8166
  }
7988
8167
  let effectiveKind = filter?.kind;
7989
8168
  if (filter?.lspKind !== void 0) {
@@ -8003,11 +8182,13 @@ var IndexStore = class {
8003
8182
  values.push(filter.lang);
8004
8183
  }
8005
8184
  if (filter?.file) {
8006
- conditions.push("s.file LIKE ?");
8007
- values.push(`%${filter.file}%`);
8185
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
8186
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8008
8187
  }
8009
8188
  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);
8189
+ const countRows = this.db.prepare(
8190
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8191
+ ).all(...values);
8011
8192
  const total = countRows[0] ? Number(countRows[0].n) : 0;
8012
8193
  if (total === 0) return { results: [], total: 0 };
8013
8194
  const rows = this.db.prepare(
@@ -8016,9 +8197,13 @@ var IndexStore = class {
8016
8197
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
8017
8198
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
8018
8199
  WHERE ${where}
8019
- ORDER BY bm25(symbols_fts)
8200
+ ORDER BY
8201
+ CASE WHEN lower(s.name) = lower(?) THEN 0
8202
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
8203
+ ELSE 2 END,
8204
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
8020
8205
  LIMIT ?`
8021
- ).all(...values, limit);
8206
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
8022
8207
  return {
8023
8208
  results: rows.map((r) => ({
8024
8209
  id: r.id,
@@ -8048,10 +8233,28 @@ var IndexStore = class {
8048
8233
  }
8049
8234
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8050
8235
  const bm25 = buildBm25Index(
8051
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
8236
+ candidates.map((c) => ({
8237
+ id: c.id,
8238
+ text: buildIndexableText(c.name, c.signature, c.docComment)
8239
+ }))
8052
8240
  );
8053
8241
  const scored = bm25.score(query, (id) => candidateById.has(id));
8054
- scored.sort((a, b) => b.score - a.score);
8242
+ const q = query.trim().toLowerCase();
8243
+ const rank = (id) => {
8244
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
8245
+ if (name === q) return 0;
8246
+ if (name.startsWith(q)) return 1;
8247
+ return 2;
8248
+ };
8249
+ scored.sort((a, b) => {
8250
+ const rankDiff = rank(a.id) - rank(b.id);
8251
+ if (rankDiff !== 0) return rankDiff;
8252
+ const scoreDiff = b.score - a.score;
8253
+ if (scoreDiff !== 0) return scoreDiff;
8254
+ const left = expectDefined2(candidateById.get(a.id));
8255
+ const right = expectDefined2(candidateById.get(b.id));
8256
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
8257
+ });
8055
8258
  const qTokens = tokenise(query);
8056
8259
  const results = scored.slice(0, limit).map(({ id, score }) => {
8057
8260
  const c = expectDefined2(candidateById.get(id));
@@ -8060,9 +8263,7 @@ var IndexStore = class {
8060
8263
  return { results, total: candidates.length };
8061
8264
  }
8062
8265
  getAllIndexable() {
8063
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
8064
- ({ id, text }) => ({ id, text })
8065
- );
8266
+ return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8066
8267
  }
8067
8268
  /**
8068
8269
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -8078,22 +8279,16 @@ var IndexStore = class {
8078
8279
  // ─── Stats ───────────────────────────────────────────────────────────────────
8079
8280
  getStats() {
8080
8281
  const sizeBytes = this.sizeBytes();
8081
- const lastRows = this.db.prepare(
8082
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
8083
- ).all();
8282
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8084
8283
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8085
8284
  const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
8086
8285
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
8087
8286
  const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
8088
8287
  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();
8288
+ const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8092
8289
  const byLang = {};
8093
8290
  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();
8291
+ const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8097
8292
  const byKind = {};
8098
8293
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
8099
8294
  return {
@@ -8109,17 +8304,31 @@ var IndexStore = class {
8109
8304
  }
8110
8305
  setLastIndexed(ts2) {
8111
8306
  this.runWithRetry(() => {
8112
- this.db.prepare(
8113
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
8114
- ).run(String(ts2));
8307
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8308
+ });
8309
+ }
8310
+ getMetadata(key) {
8311
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
8312
+ return rows[0]?.value;
8313
+ }
8314
+ setMetadata(key, value) {
8315
+ this.runWithRetry(() => {
8316
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8115
8317
  });
8116
8318
  }
8117
8319
  clearAll() {
8118
8320
  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");
8321
+ this.db.exec("BEGIN IMMEDIATE");
8322
+ try {
8323
+ this.db.exec("DELETE FROM refs");
8324
+ this.db.exec("DELETE FROM symbols");
8325
+ this.db.exec("DELETE FROM files");
8326
+ if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8327
+ this.db.exec("COMMIT");
8328
+ } catch (err) {
8329
+ this.db.exec("ROLLBACK");
8330
+ throw err;
8331
+ }
8123
8332
  });
8124
8333
  }
8125
8334
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -8210,6 +8419,7 @@ var IndexStore = class {
8210
8419
  const allInserted = [];
8211
8420
  const refsToInsert = [];
8212
8421
  for (const entry of entries) {
8422
+ const insertedForEntry = [];
8213
8423
  for (const s of entry.symbols) {
8214
8424
  const id = nextId++;
8215
8425
  symStmt.run(
@@ -8227,9 +8437,11 @@ var IndexStore = class {
8227
8437
  s.file
8228
8438
  );
8229
8439
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8230
- allInserted.push({ ...s, id });
8440
+ const inserted = { ...s, id };
8441
+ allInserted.push(inserted);
8442
+ insertedForEntry.push(inserted);
8231
8443
  }
8232
- for (const r of entry.refs) refsToInsert.push(r);
8444
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8233
8445
  }
8234
8446
  if (refsToInsert.length > 0) {
8235
8447
  const refStmt = this.db.prepare(
@@ -8267,9 +8479,7 @@ var IndexStore = class {
8267
8479
  */
8268
8480
  deleteRefsForFile(file) {
8269
8481
  this.runWithRetry(() => {
8270
- this.db.prepare(
8271
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
8272
- ).run(file);
8482
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8273
8483
  });
8274
8484
  }
8275
8485
  /**
@@ -8310,9 +8520,7 @@ var IndexStore = class {
8310
8520
  * Find all references FROM a given symbol (what does this symbol call/use?).
8311
8521
  */
8312
8522
  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) => ({
8523
+ 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
8524
  id: r.id,
8317
8525
  fromId: r.from_id,
8318
8526
  toName: r.to_name,
@@ -8329,6 +8537,325 @@ var IndexStore = class {
8329
8537
  return 0;
8330
8538
  }
8331
8539
  }
8540
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
8541
+ /**
8542
+ * Derive a monorepo package name from an absolute file path.
8543
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
8544
+ * Returns `undefined` for files outside any recognisable package structure.
8545
+ */
8546
+ static derivePackage(filePath) {
8547
+ const f = filePath.replace(/\\/g, "/");
8548
+ const pkgsIdx = f.indexOf("/packages/");
8549
+ if (pkgsIdx !== -1) {
8550
+ const rest = f.slice(pkgsIdx + "/packages/".length);
8551
+ const seg = rest.split("/")[0];
8552
+ return seg ? `@wrongstack/${seg}` : void 0;
8553
+ }
8554
+ const appsIdx = f.indexOf("/apps/");
8555
+ if (appsIdx !== -1) {
8556
+ const rest = f.slice(appsIdx + "/apps/".length);
8557
+ const seg = rest.split("/")[0];
8558
+ return seg ? `app:${seg}` : void 0;
8559
+ }
8560
+ return void 0;
8561
+ }
8562
+ static packageFromImport(moduleName) {
8563
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
8564
+ const parts = moduleName.split("/");
8565
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
8566
+ }
8567
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
8568
+ if (!moduleName.startsWith(".")) return void 0;
8569
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
8570
+ const absolute = path13.posix.normalize(
8571
+ path13.posix.join(path13.posix.dirname(normalizedFrom), moduleName)
8572
+ );
8573
+ const extension = path13.posix.extname(absolute);
8574
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
8575
+ const candidates = [
8576
+ absolute,
8577
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
8578
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(absolute, `index${ext}`)),
8579
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(base, `index${ext}`))
8580
+ ];
8581
+ const indexedByPortablePath = new Map(
8582
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
8583
+ );
8584
+ for (const candidate of candidates) {
8585
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
8586
+ if (indexed) return indexed;
8587
+ }
8588
+ return void 0;
8589
+ }
8590
+ /**
8591
+ * Package-level graph: each workspace package is a node; edges are derived
8592
+ * from cross-package symbol references (a symbol in package A references a
8593
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
8594
+ */
8595
+ getPackageGraph() {
8596
+ const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8597
+ const pkgNodes = /* @__PURE__ */ new Map();
8598
+ const fileToPkg = /* @__PURE__ */ new Map();
8599
+ const symbolToPkg = /* @__PURE__ */ new Map();
8600
+ for (const s of symbols) {
8601
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
8602
+ fileToPkg.set(s.file, pkg);
8603
+ symbolToPkg.set(s.id, pkg);
8604
+ const node = pkgNodes.get(pkg);
8605
+ if (node) {
8606
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
8607
+ } else {
8608
+ pkgNodes.set(pkg, {
8609
+ id: `pkg:${pkg}`,
8610
+ label: pkg,
8611
+ kind: "package",
8612
+ package: pkg,
8613
+ symbolCount: 1,
8614
+ fileCount: 0
8615
+ });
8616
+ }
8617
+ }
8618
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
8619
+ for (const { file } of files) {
8620
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
8621
+ const node = pkgNodes.get(pkg);
8622
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
8623
+ }
8624
+ const refRows = this.db.prepare(
8625
+ `SELECT r.from_id, r.to_id, r.call_type
8626
+ FROM refs r
8627
+ WHERE r.to_id IS NOT NULL`
8628
+ ).all();
8629
+ const edgeMap = /* @__PURE__ */ new Map();
8630
+ for (const r of refRows) {
8631
+ if (r.call_type === "import") continue;
8632
+ const fromPkg = symbolToPkg.get(r.from_id);
8633
+ const toPkg = symbolToPkg.get(r.to_id);
8634
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
8635
+ const key = `${fromPkg}\0${toPkg}`;
8636
+ let e = edgeMap.get(key);
8637
+ if (!e) {
8638
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8639
+ edgeMap.set(key, e);
8640
+ }
8641
+ e.weight++;
8642
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8643
+ }
8644
+ const importRows = this.db.prepare(
8645
+ `SELECT r.from_id, r.to_name
8646
+ FROM refs r
8647
+ WHERE r.call_type = 'import'`
8648
+ ).all();
8649
+ for (const r of importRows) {
8650
+ const fromPkg = symbolToPkg.get(r.from_id);
8651
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
8652
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
8653
+ const key = `${fromPkg}\0${toPkg}`;
8654
+ let edge = edgeMap.get(key);
8655
+ if (!edge) {
8656
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8657
+ edgeMap.set(key, edge);
8658
+ }
8659
+ edge.weight++;
8660
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
8661
+ }
8662
+ const edges = [];
8663
+ for (const [key, e] of edgeMap) {
8664
+ const [source, target] = key.split("\0");
8665
+ let bestType = "call";
8666
+ let bestCount = 0;
8667
+ for (const [t, c] of e.types) {
8668
+ if (c > bestCount) {
8669
+ bestType = t;
8670
+ bestCount = c;
8671
+ }
8672
+ }
8673
+ edges.push({
8674
+ source: `pkg:${source}`,
8675
+ target: `pkg:${target}`,
8676
+ weight: e.weight,
8677
+ refType: bestType
8678
+ });
8679
+ }
8680
+ return { nodes: [...pkgNodes.values()], edges };
8681
+ }
8682
+ /**
8683
+ * File-level graph for a single package: each file is a node; edges are
8684
+ * derived from cross-file symbol references within the package.
8685
+ */
8686
+ getFileGraph(packageFilter) {
8687
+ const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8688
+ const pkgSyms = allSymbols.filter(
8689
+ (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
8690
+ );
8691
+ if (pkgSyms.length === 0) return { nodes: [], edges: [] };
8692
+ const fileNodes = /* @__PURE__ */ new Map();
8693
+ const symToFile = /* @__PURE__ */ new Map();
8694
+ const fileStats = /* @__PURE__ */ new Map();
8695
+ for (const s of allSymbols) {
8696
+ symToFile.set(s.id, s.file);
8697
+ const current = fileStats.get(s.file);
8698
+ fileStats.set(s.file, {
8699
+ count: (current?.count ?? 0) + 1,
8700
+ lang: current?.lang ?? s.lang
8701
+ });
8702
+ }
8703
+ const localFiles = new Set(pkgSyms.map((s) => s.file));
8704
+ const indexedFiles = new Set(allSymbols.map((s) => s.file));
8705
+ const ensureFileNode = (file) => {
8706
+ if (fileNodes.has(file)) return;
8707
+ const stats = fileStats.get(file);
8708
+ fileNodes.set(file, {
8709
+ id: `file:${file}`,
8710
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
8711
+ kind: "file",
8712
+ package: _IndexStore.derivePackage(file) ?? "(root)",
8713
+ file,
8714
+ symbolCount: stats?.count ?? 0,
8715
+ lang: stats?.lang,
8716
+ external: !localFiles.has(file)
8717
+ });
8718
+ };
8719
+ for (const file of localFiles) {
8720
+ ensureFileNode(file);
8721
+ }
8722
+ const refRows = this.db.prepare(
8723
+ `SELECT r.from_id, r.to_id, r.call_type
8724
+ FROM refs r
8725
+ WHERE r.to_id IS NOT NULL`
8726
+ ).all();
8727
+ const edgeMap = /* @__PURE__ */ new Map();
8728
+ for (const r of refRows) {
8729
+ if (r.call_type === "import") continue;
8730
+ const fromFile = symToFile.get(r.from_id);
8731
+ const toFile = symToFile.get(r.to_id);
8732
+ if (!fromFile || !toFile || fromFile === toFile) continue;
8733
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
8734
+ ensureFileNode(fromFile);
8735
+ ensureFileNode(toFile);
8736
+ const key = `${fromFile}\0${toFile}`;
8737
+ let e = edgeMap.get(key);
8738
+ if (!e) {
8739
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8740
+ edgeMap.set(key, e);
8741
+ }
8742
+ e.weight++;
8743
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8744
+ }
8745
+ const importRows = this.db.prepare(
8746
+ `SELECT r.from_id, r.to_name
8747
+ FROM refs r
8748
+ WHERE r.call_type = 'import'`
8749
+ ).all();
8750
+ for (const r of importRows) {
8751
+ const fromFile = symToFile.get(r.from_id);
8752
+ if (!fromFile || !localFiles.has(fromFile)) continue;
8753
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
8754
+ if (!toFile || fromFile === toFile) continue;
8755
+ ensureFileNode(fromFile);
8756
+ ensureFileNode(toFile);
8757
+ const key = `${fromFile}\0${toFile}`;
8758
+ let edge = edgeMap.get(key);
8759
+ if (!edge) {
8760
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8761
+ edgeMap.set(key, edge);
8762
+ }
8763
+ edge.weight++;
8764
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
8765
+ }
8766
+ const edges = [];
8767
+ for (const [key, e] of edgeMap) {
8768
+ const [source, target] = key.split("\0");
8769
+ let bestType = "call";
8770
+ let bestCount = 0;
8771
+ for (const [t, c] of e.types) {
8772
+ if (c > bestCount) {
8773
+ bestType = t;
8774
+ bestCount = c;
8775
+ }
8776
+ }
8777
+ edges.push({
8778
+ source: `file:${source}`,
8779
+ target: `file:${target}`,
8780
+ weight: e.weight,
8781
+ refType: bestType
8782
+ });
8783
+ }
8784
+ return { nodes: [...fileNodes.values()], edges };
8785
+ }
8786
+ /**
8787
+ * Symbol-level graph for a single file: each symbol is a node; edges are
8788
+ * derived from intra-file and cross-file symbol references (who calls whom).
8789
+ */
8790
+ getSymbolGraph(fileFilter) {
8791
+ const allSymbols = this.db.prepare(
8792
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
8793
+ ).all();
8794
+ const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
8795
+ if (syms.length === 0) return { nodes: [], edges: [] };
8796
+ const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
8797
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
8798
+ const toGraphNode = (s) => ({
8799
+ id: `sym:${s.id}`,
8800
+ label: s.name,
8801
+ kind: "symbol",
8802
+ symbolId: s.id,
8803
+ symbolKind: s.kind,
8804
+ file: s.file,
8805
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
8806
+ lang: s.lang,
8807
+ line: s.line,
8808
+ signature: s.signature,
8809
+ scope: s.scope,
8810
+ external: s.file !== fileFilter
8811
+ });
8812
+ const symIds = new Set(syms.map((s) => s.id));
8813
+ const refRows = this.db.prepare(
8814
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
8815
+ FROM refs r
8816
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
8817
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
8818
+ ).all(fileFilter, fileFilter);
8819
+ const edgeMap = /* @__PURE__ */ new Map();
8820
+ for (const r of refRows) {
8821
+ if (r.to_id === null) continue;
8822
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
8823
+ relatedIds.add(r.from_id);
8824
+ relatedIds.add(r.to_id);
8825
+ const key = `${r.from_id}\0${r.to_id}`;
8826
+ let e = edgeMap.get(key);
8827
+ if (!e) {
8828
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8829
+ edgeMap.set(key, e);
8830
+ }
8831
+ e.weight++;
8832
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8833
+ }
8834
+ const edges = [];
8835
+ for (const [key, e] of edgeMap) {
8836
+ const [fromId, toId] = key.split("\0").map(Number);
8837
+ let bestType = "call";
8838
+ let bestCount = 0;
8839
+ for (const [t, c] of e.types) {
8840
+ if (c > bestCount) {
8841
+ bestType = t;
8842
+ bestCount = c;
8843
+ }
8844
+ }
8845
+ edges.push({
8846
+ source: `sym:${fromId}`,
8847
+ target: `sym:${toId}`,
8848
+ weight: e.weight,
8849
+ refType: bestType
8850
+ });
8851
+ }
8852
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
8853
+ const aExternal = a.file === fileFilter ? 0 : 1;
8854
+ const bExternal = b.file === fileFilter ? 0 : 1;
8855
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
8856
+ }).map(toGraphNode);
8857
+ return { nodes, edges };
8858
+ }
8332
8859
  close() {
8333
8860
  this.stmtCache.clear();
8334
8861
  try {
@@ -8423,6 +8950,14 @@ function getJsDoc(node, sourceFile) {
8423
8950
  }
8424
8951
  return "";
8425
8952
  }
8953
+ function hasFunctionLikeAncestor(node) {
8954
+ let current = node.parent;
8955
+ while (current) {
8956
+ if (ts.isFunctionLike(current)) return true;
8957
+ current = current.parent;
8958
+ }
8959
+ return false;
8960
+ }
8426
8961
  function buildScope(node) {
8427
8962
  const parts = [];
8428
8963
  let current = node.parent;
@@ -8450,6 +8985,10 @@ function parseSymbols(opts) {
8450
8985
  function visit(node) {
8451
8986
  const kind = kindOf(node);
8452
8987
  if (kind) {
8988
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
8989
+ ts.forEachChild(node, visit);
8990
+ return;
8991
+ }
8453
8992
  const nameNode = node.name;
8454
8993
  if (!nameNode || !ts.isIdentifier(nameNode)) return;
8455
8994
  const name = nameNode.text;
@@ -9394,9 +9933,9 @@ function parseSymbols5(opts) {
9394
9933
  function regexParse2(opts) {
9395
9934
  const { file, content, lang } = opts;
9396
9935
  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";
9936
+ const basename9 = path17.basename(file).toLowerCase();
9937
+ const isPackageJson = basename9 === "package.json";
9938
+ const isTsconfig = basename9 === "tsconfig.json" || basename9 === "tsconfig.build.json";
9400
9939
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
9401
9940
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
9402
9941
  const lines = content.split("\n");
@@ -9798,9 +10337,7 @@ function yieldEventLoop() {
9798
10337
  function throwIfAborted(signal) {
9799
10338
  if (!signal?.aborted) return;
9800
10339
  if (signal.reason instanceof Error) throw signal.reason;
9801
- throw new Error(
9802
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
9803
- );
10340
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
9804
10341
  }
9805
10342
  function isAbortError(err) {
9806
10343
  return err instanceof DOMException && err.name === "AbortError";
@@ -9816,8 +10353,20 @@ var DEFAULT_IGNORE = [
9816
10353
  "__snapshots__",
9817
10354
  ".nyc_output"
9818
10355
  ];
10356
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10357
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10358
+ function isWithinProject(projectRoot, file) {
10359
+ const rel = path19.relative(projectRoot, file);
10360
+ return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
10361
+ }
10362
+ function isMissingPathError(err) {
10363
+ const code = err?.code;
10364
+ return code === "ENOENT" || code === "ENOTDIR";
10365
+ }
9819
10366
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9820
10367
  const results = [];
10368
+ const errors = [];
10369
+ let complete = true;
9821
10370
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
9822
10371
  const globs = [
9823
10372
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -9841,7 +10390,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9841
10390
  let entries;
9842
10391
  try {
9843
10392
  entries = await fs14.readdir(dir, { withFileTypes: true });
9844
- } catch {
10393
+ } catch (err) {
10394
+ complete = false;
10395
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
9845
10396
  return;
9846
10397
  }
9847
10398
  dirCount++;
@@ -9853,8 +10404,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9853
10404
  if (isGitIgnored(rel, true)) continue;
9854
10405
  await walk(full);
9855
10406
  } else if (e.isFile()) {
9856
- if (isGitIgnored(rel, false)) continue;
9857
- const ext = path19.extname(e.name);
10407
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
10408
+ const ext = path19.extname(e.name).toLowerCase();
9858
10409
  for (const { ext: extName, pat } of globs) {
9859
10410
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
9860
10411
  results.push(full);
@@ -9865,7 +10416,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9865
10416
  }
9866
10417
  };
9867
10418
  await walk(projectRoot);
9868
- return results;
10419
+ return { files: results, complete, errors };
9869
10420
  }
9870
10421
  async function parseFile(file, content, lang) {
9871
10422
  switch (lang) {
@@ -9888,6 +10439,26 @@ async function parseFile(file, content, lang) {
9888
10439
  return { file, lang, symbols: [], mtimeMs: Date.now() };
9889
10440
  }
9890
10441
  }
10442
+ function assignRefsToSymbols2(refs, symbols) {
10443
+ if (refs.length === 0 || symbols.length === 0) return [];
10444
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10445
+ const seen = /* @__PURE__ */ new Set();
10446
+ const assigned = [];
10447
+ for (const ref of refs) {
10448
+ let owner2;
10449
+ for (const symbol of ordered) {
10450
+ if (symbol.line > ref.line) break;
10451
+ owner2 = symbol;
10452
+ }
10453
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10454
+ if (!owner2 || owner2.id <= 0) continue;
10455
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10456
+ if (seen.has(key)) continue;
10457
+ seen.add(key);
10458
+ assigned.push({ ...ref, fromId: owner2.id });
10459
+ }
10460
+ return assigned;
10461
+ }
9891
10462
  async function runIndexer(_ctx, opts) {
9892
10463
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
9893
10464
  try {
@@ -9900,7 +10471,9 @@ async function runIndexer(_ctx, opts) {
9900
10471
  }
9901
10472
  }
9902
10473
  async function runIndexerWithStore(store, opts) {
9903
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
10474
+ const { projectRoot, langs, ignore = [], signal } = opts;
10475
+ const relationGraphVersion = "2";
10476
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
9904
10477
  const startMs = Date.now();
9905
10478
  const errors = [];
9906
10479
  const langStats = {};
@@ -9909,10 +10482,18 @@ async function runIndexerWithStore(store, opts) {
9909
10482
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
9910
10483
  let files;
9911
10484
  let discoveredFiles = null;
10485
+ let discoveryComplete = true;
9912
10486
  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));
10487
+ files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
10488
+ if (!isWithinProject(projectRoot, f)) return false;
10489
+ const rel = path19.relative(projectRoot, f).replace(/\\/g, "/");
10490
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path19.basename(f)) && !isGitIgnored(rel, false);
10491
+ });
9914
10492
  } else {
9915
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10493
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10494
+ files = discovery.files;
10495
+ errors.push(...discovery.errors);
10496
+ discoveryComplete = discovery.complete;
9916
10497
  discoveredFiles = new Set(files);
9917
10498
  }
9918
10499
  if (langs && langs.length > 0) {
@@ -9937,36 +10518,66 @@ async function runIndexerWithStore(store, opts) {
9937
10518
  }
9938
10519
  const statOpts = signal ? { signal } : {};
9939
10520
  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)}` };
10521
+ batchFiles.map(
10522
+ async (file) => {
10523
+ let stat17;
10524
+ try {
10525
+ stat17 = await fs14.stat(file, statOpts);
10526
+ } catch (e) {
10527
+ if (isAbortError(e)) throw e;
10528
+ return {
10529
+ file,
10530
+ stat: null,
10531
+ lang: "",
10532
+ parsed: null,
10533
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
10534
+ missing: isMissingPathError(e)
10535
+ };
10536
+ }
10537
+ if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
10538
+ const lang = detectLang(file);
10539
+ if (!lang) return { file, stat: stat17, lang: "", parsed: null };
10540
+ if (stat17.size > MAX_INDEX_FILE_BYTES) {
10541
+ return {
10542
+ file,
10543
+ stat: stat17,
10544
+ lang,
10545
+ parsed: null,
10546
+ error: `file too large (${stat17.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
10547
+ };
10548
+ }
10549
+ const meta = existingMeta.get(file);
10550
+ if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
10551
+ return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
10552
+ }
10553
+ let content;
10554
+ try {
10555
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
10556
+ } catch (e) {
10557
+ if (isAbortError(e)) throw e;
10558
+ return {
10559
+ file,
10560
+ stat: stat17,
10561
+ lang,
10562
+ parsed: null,
10563
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
10564
+ };
10565
+ }
10566
+ let parsed;
10567
+ try {
10568
+ parsed = await parseFile(file, content, lang);
10569
+ } catch (e) {
10570
+ return {
10571
+ file,
10572
+ stat: stat17,
10573
+ lang,
10574
+ parsed: null,
10575
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
10576
+ };
10577
+ }
10578
+ return { file, stat: stat17, lang, parsed, content };
9967
10579
  }
9968
- return { file, stat: stat17, lang, parsed, content };
9969
- })
10580
+ )
9970
10581
  );
9971
10582
  const batchEntries = [];
9972
10583
  const deleteForFiles = [];
@@ -9981,8 +10592,8 @@ async function runIndexerWithStore(store, opts) {
9981
10592
  }
9982
10593
  const result = settled.value;
9983
10594
  if (result.error) {
9984
- if (result.stat) store.deleteFile(file);
9985
- if (result.error.includes("error:")) errors.push(result.error);
10595
+ if (result.missing) store.deleteFile(file);
10596
+ errors.push(`${file}: ${result.error}`);
9986
10597
  continue;
9987
10598
  }
9988
10599
  const { stat: stat17, lang, parsed } = result;
@@ -10006,6 +10617,8 @@ async function runIndexerWithStore(store, opts) {
10006
10617
  continue;
10007
10618
  }
10008
10619
  if (parsed.symbols.length === 0) {
10620
+ store.deleteRefsForFile(file);
10621
+ store.deleteSymbolsForFile(file);
10009
10622
  store.upsertFile({
10010
10623
  file,
10011
10624
  lang,
@@ -10016,15 +10629,11 @@ async function runIndexerWithStore(store, opts) {
10016
10629
  filesIndexed++;
10017
10630
  continue;
10018
10631
  }
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
10632
  batchEntries.push({
10024
10633
  file,
10025
10634
  lang,
10026
10635
  symbols: parsed.symbols,
10027
- refs,
10636
+ refs: parsed.refs ?? [],
10028
10637
  mtimeMs: Math.floor(stat17.mtimeMs),
10029
10638
  symbolCount: parsed.symbols.length
10030
10639
  });
@@ -10032,34 +10641,12 @@ async function runIndexerWithStore(store, opts) {
10032
10641
  }
10033
10642
  if (batchEntries.length > 0) {
10034
10643
  try {
10035
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
10036
- let cursor = 0;
10644
+ store.commitBatch(batchEntries, { deleteForFiles });
10037
10645
  for (const entry of batchEntries) {
10038
10646
  const count = entry.symbols.length;
10039
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
10040
- cursor += count;
10041
10647
  symbolsIndexed += count;
10042
10648
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
10043
10649
  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
10650
  }
10064
10651
  } catch (err) {
10065
10652
  const message = err instanceof Error ? err.message : String(err);
@@ -10073,23 +10660,7 @@ async function runIndexerWithStore(store, opts) {
10073
10660
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
10074
10661
  filesIndexed++;
10075
10662
  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
- }
10663
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
10093
10664
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
10094
10665
  }
10095
10666
  store.upsertFile({
@@ -10100,19 +10671,23 @@ async function runIndexerWithStore(store, opts) {
10100
10671
  lastIndexed: Date.now()
10101
10672
  });
10102
10673
  } catch (innerErr) {
10103
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
10674
+ errors.push(
10675
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
10676
+ );
10104
10677
  }
10105
10678
  }
10106
10679
  }
10107
10680
  }
10108
10681
  }
10109
- if (discoveredFiles) {
10682
+ if (discoveredFiles && discoveryComplete) {
10110
10683
  for (const [file_] of existingMeta) {
10111
10684
  if (!discoveredFiles.has(file_)) {
10112
10685
  store.deleteFile(file_);
10113
10686
  }
10114
10687
  }
10115
10688
  }
10689
+ store.resolveRefs();
10690
+ store.setMetadata("relation_graph_version", relationGraphVersion);
10116
10691
  const durationMs = Date.now() - startMs;
10117
10692
  store.setLastIndexed(Date.now());
10118
10693
  return {
@@ -10175,7 +10750,7 @@ function statsService(args) {
10175
10750
 
10176
10751
  // src/codebase-index/background-indexer.ts
10177
10752
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
10178
- var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
10753
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
10179
10754
  var _ready = false;
10180
10755
  var _indexing = false;
10181
10756
  var _currentFile = 0;
@@ -10244,14 +10819,22 @@ function ensureWorker() {
10244
10819
  if (!entry) return;
10245
10820
  pending.delete(msg.id);
10246
10821
  if (msg.ok) entry.resolve(msg.result);
10247
- else entry.reject(new Error(msg.error));
10822
+ else {
10823
+ const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
10824
+ if (msg.errorName && msg.errorName !== "Error") {
10825
+ error.name = msg.errorName;
10826
+ }
10827
+ entry.reject(error);
10828
+ }
10248
10829
  });
10249
10830
  w.on("error", (err) => {
10831
+ if (worker !== w) return;
10250
10832
  worker = null;
10251
10833
  failAllPending(err);
10252
10834
  });
10253
10835
  w.on("exit", () => {
10254
- if (worker === w) worker = null;
10836
+ if (worker !== w) return;
10837
+ worker = null;
10255
10838
  failAllPending(new Error("codebase-index worker exited"));
10256
10839
  });
10257
10840
  worker = w;
@@ -10271,6 +10854,11 @@ function terminateWorker(reason) {
10271
10854
  function callIndexOp(op, args, opts) {
10272
10855
  const w = ensureWorker();
10273
10856
  if (!w) return callInline(op, args, opts);
10857
+ if (opts.signal?.aborted) {
10858
+ return Promise.reject(
10859
+ opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
10860
+ );
10861
+ }
10274
10862
  return new Promise((resolve14, reject) => {
10275
10863
  const id = nextRpcId++;
10276
10864
  const timer = setTimeout(() => {
@@ -10285,8 +10873,7 @@ function callIndexOp(op, args, opts) {
10285
10873
  const onAbort = () => {
10286
10874
  w.postMessage({ type: "cancel", id });
10287
10875
  };
10288
- if (opts.signal?.aborted) onAbort();
10289
- else opts.signal?.addEventListener("abort", onAbort, { once: true });
10876
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
10290
10877
  const cleanup = () => {
10291
10878
  clearTimeout(timer);
10292
10879
  opts.signal?.removeEventListener("abort", onAbort);
@@ -10430,7 +11017,7 @@ var codebaseIndexTool = {
10430
11017
  category: "Project",
10431
11018
  icon: "index",
10432
11019
  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.",
11020
+ 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
11021
  permission: "confirm",
10435
11022
  mutating: true,
10436
11023
  capabilities: ["fs.write.outside-project"],
@@ -10486,12 +11073,14 @@ var codebaseSearchTool = {
10486
11073
  name: "codebase-search",
10487
11074
  category: "Project",
10488
11075
  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.",
11076
+ 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).",
11077
+ 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
11078
  permission: "auto",
10492
11079
  mutating: false,
10493
11080
  capabilities: ["fs.read"],
10494
- timeoutMs: 1e4,
11081
+ // The index host has its own 30s read watchdog. Leave enough headroom for
11082
+ // worker teardown and structured timeout reporting.
11083
+ timeoutMs: 35e3,
10495
11084
  inputSchema: {
10496
11085
  type: "object",
10497
11086
  properties: {
@@ -10501,11 +11090,34 @@ var codebaseSearchTool = {
10501
11090
  },
10502
11091
  kind: {
10503
11092
  type: "string",
10504
- description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
11093
+ enum: [
11094
+ "class",
11095
+ "interface",
11096
+ "enum",
11097
+ "type",
11098
+ "function",
11099
+ "method",
11100
+ "var",
11101
+ "const",
11102
+ "let",
11103
+ "property",
11104
+ "parameter",
11105
+ "namespace",
11106
+ "object",
11107
+ "literal",
11108
+ "schema",
11109
+ "struct",
11110
+ "trait",
11111
+ "impl",
11112
+ "static",
11113
+ "mod"
11114
+ ],
11115
+ description: "Filter by indexed symbol kind"
10505
11116
  },
10506
11117
  lang: {
10507
11118
  type: "string",
10508
- description: "Filter by language: ts, tsx, js, jsx"
11119
+ enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
11120
+ description: "Filter by indexed language"
10509
11121
  },
10510
11122
  lspKind: {
10511
11123
  type: "integer",
@@ -10530,20 +11142,12 @@ var codebaseSearchTool = {
10530
11142
  },
10531
11143
  async execute(input, ctx, execOpts) {
10532
11144
  const state = getIndexState();
10533
- if (!state.ready) {
11145
+ if (state.indexing && !state.ready) {
10534
11146
  return {
10535
11147
  results: [],
10536
11148
  total: 0,
10537
11149
  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) {
10542
- return {
10543
- results: [],
10544
- total: 0,
10545
- query: input.query,
10546
- indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
11150
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
10547
11151
  };
10548
11152
  }
10549
11153
  if (state.lastError) {
@@ -10556,21 +11160,37 @@ var codebaseSearchTool = {
10556
11160
  indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
10557
11161
  };
10558
11162
  }
10559
- const limit = Math.min(input.limit ?? 20, 100);
11163
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
10560
11164
  const { results, total } = await searchCodebaseIndex(
10561
11165
  {
10562
11166
  projectRoot: ctx.projectRoot,
10563
11167
  indexDir: codebaseIndexDirOverride(ctx),
10564
11168
  query: input.query,
10565
- kind: input.kind,
10566
- lang: input.lang,
11169
+ kind: input.kind?.toLowerCase(),
11170
+ lang: input.lang?.toLowerCase(),
10567
11171
  file: input.file,
10568
11172
  lspKind: input.lspKind,
10569
11173
  limit
10570
11174
  },
10571
11175
  { signal: execOpts?.signal }
10572
11176
  );
10573
- return { results, total, query: input.query };
11177
+ let hasPersistedIndex = state.ready || total > 0;
11178
+ if (!hasPersistedIndex) {
11179
+ try {
11180
+ const stats = await codebaseIndexStats(
11181
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11182
+ { signal: execOpts?.signal }
11183
+ );
11184
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11185
+ } catch {
11186
+ }
11187
+ }
11188
+ return {
11189
+ results,
11190
+ total,
11191
+ query: input.query,
11192
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
11193
+ };
10574
11194
  }
10575
11195
  };
10576
11196
 
@@ -10579,12 +11199,15 @@ var codebaseStatsTool = {
10579
11199
  name: "codebase-stats",
10580
11200
  category: "Project",
10581
11201
  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.",
11202
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
11203
+ 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
11204
  permission: "auto",
10585
11205
  mutating: false,
10586
11206
  capabilities: ["fs.read"],
10587
- timeoutMs: 5e3,
11207
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
11208
+ // longer so the host can return a structured timeout instead of being
11209
+ // pre-empted by the generic tool executor.
11210
+ timeoutMs: 35e3,
10588
11211
  inputSchema: {
10589
11212
  type: "object",
10590
11213
  properties: {},
@@ -10592,7 +11215,8 @@ var codebaseStatsTool = {
10592
11215
  },
10593
11216
  async execute(_input, ctx, execOpts) {
10594
11217
  const idxState = getIndexState();
10595
- if (!idxState.ready) {
11218
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
11219
+ if (idxState.indexing) {
10596
11220
  return {
10597
11221
  totalSymbols: 0,
10598
11222
  totalFiles: 0,
@@ -10600,22 +11224,47 @@ var codebaseStatsTool = {
10600
11224
  byKind: {},
10601
11225
  lastIndexed: null,
10602
11226
  sizeBytes: 0,
10603
- indexPath: "",
11227
+ indexPath,
10604
11228
  version: SCHEMA_VERSION,
10605
- indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
11229
+ statsAvailable: false,
11230
+ indexing: {
11231
+ currentFile: idxState.currentFile,
11232
+ totalFiles: idxState.totalFiles
11233
+ },
11234
+ 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
11235
  };
10607
11236
  }
10608
- const stats = await codebaseIndexStats(
10609
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
10610
- { signal: execOpts?.signal }
10611
- );
10612
- if (idxState.indexing) {
11237
+ let stats;
11238
+ try {
11239
+ stats = await codebaseIndexStats(
11240
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11241
+ { signal: execOpts?.signal }
11242
+ );
11243
+ } catch (err) {
11244
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
11245
+ throw err;
11246
+ }
10613
11247
  return {
10614
- ...stats,
10615
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
11248
+ totalSymbols: 0,
11249
+ totalFiles: 0,
11250
+ byLang: {},
11251
+ byKind: {},
11252
+ lastIndexed: null,
11253
+ sizeBytes: 0,
11254
+ indexPath,
11255
+ version: SCHEMA_VERSION,
11256
+ statsAvailable: false,
11257
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
10616
11258
  };
10617
11259
  }
10618
11260
  const circuit = idxState.circuit;
11261
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11262
+ let indexStatus;
11263
+ if (circuit.state === "open") {
11264
+ 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.");
11265
+ } else if (!hasPersistedIndex) {
11266
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
11267
+ }
10619
11268
  return {
10620
11269
  totalSymbols: stats.totalSymbols,
10621
11270
  totalFiles: stats.totalFiles,
@@ -10625,9 +11274,8 @@ var codebaseStatsTool = {
10625
11274
  sizeBytes: stats.sizeBytes,
10626
11275
  indexPath: stats.indexPath,
10627
11276
  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
- } : {}
11277
+ statsAvailable: true,
11278
+ ...indexStatus ? { indexStatus } : {}
10631
11279
  };
10632
11280
  }
10633
11281
  };
@@ -12146,6 +12794,11 @@ Compare this against your old_string and retry with the file's actual text.` : "
12146
12794
 
12147
12795
  // src/exec.ts
12148
12796
  import { spawn as spawn8 } from "node:child_process";
12797
+ import {
12798
+ emitProcessCompleted as emitProcessCompleted3,
12799
+ emitProcessOutput as emitProcessOutput3,
12800
+ emitProcessStarted as emitProcessStarted3
12801
+ } from "@wrongstack/core";
12149
12802
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
12150
12803
  init_output_spool();
12151
12804
  init_util();
@@ -13254,12 +13907,30 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13254
13907
  resolve14(result);
13255
13908
  };
13256
13909
  const startedAt = Date.now();
13910
+ let stdoutBytes = 0;
13911
+ let stderrBytes = 0;
13912
+ let telemetryCompleted = false;
13913
+ let timedOut = false;
13257
13914
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
13258
13915
  const resolved = resolveWin32Command(cmd);
13259
13916
  const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
13260
13917
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
13261
13918
  const spawnCmd = shim?.command ?? resolved;
13262
13919
  const spawnArgs = shim?.args ?? args;
13920
+ const emitCompletedOnce = (exitCode, pid2, signal2) => {
13921
+ if (telemetryCompleted) return;
13922
+ telemetryCompleted = true;
13923
+ emitProcessCompleted3({
13924
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
13925
+ exitCode,
13926
+ ...signal2 ? { signal: signal2 } : {},
13927
+ durationMs: Date.now() - startedAt,
13928
+ stdoutBytes,
13929
+ stderrBytes,
13930
+ timedOut,
13931
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
13932
+ });
13933
+ };
13263
13934
  let child;
13264
13935
  try {
13265
13936
  child = spawn8(spawnCmd, spawnArgs, {
@@ -13272,6 +13943,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13272
13943
  });
13273
13944
  } catch (err) {
13274
13945
  spool.finalize();
13946
+ emitProcessStarted3({
13947
+ parentPid: process.pid,
13948
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
13949
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
13950
+ cwd,
13951
+ background: false,
13952
+ startedAt: new Date(startedAt).toISOString()
13953
+ });
13954
+ emitCompletedOnce(1, void 0);
13275
13955
  finish({
13276
13956
  command: cmd,
13277
13957
  args,
@@ -13284,6 +13964,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13284
13964
  });
13285
13965
  return;
13286
13966
  }
13967
+ emitProcessStarted3({
13968
+ ...child.pid !== void 0 ? { pid: child.pid } : {},
13969
+ parentPid: process.pid,
13970
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
13971
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
13972
+ cwd,
13973
+ background: false,
13974
+ startedAt: new Date(startedAt).toISOString()
13975
+ });
13287
13976
  child.on("error", (err) => {
13288
13977
  const isAbort = err && err.code === "ABORT_ERR";
13289
13978
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
@@ -13292,6 +13981,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13292
13981
  if (typeof pid === "number") registry.unregister(pid);
13293
13982
  registry.afterCall(Date.now() - startedAt, true);
13294
13983
  spool.finalize();
13984
+ emitCompletedOnce(isAbort ? 124 : 1, child.pid, isAbort ? "ABORT" : void 0);
13295
13985
  finish({
13296
13986
  command: cmd,
13297
13987
  args,
@@ -13311,6 +14001,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13311
14001
  }
13312
14002
  const timer = setTimeout(() => {
13313
14003
  killed = true;
14004
+ timedOut = true;
13314
14005
  if (typeof pid === "number") registry.kill(pid);
13315
14006
  else child.kill("SIGTERM");
13316
14007
  }, timeout);
@@ -13325,11 +14016,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13325
14016
  }
13326
14017
  child.stdout?.on("data", (chunk) => {
13327
14018
  const text = chunk.toString();
14019
+ stdoutBytes += chunk.byteLength;
14020
+ emitProcessOutput3({ pid, stream: "stdout", chunk });
13328
14021
  if (stdout.length < MAX_OUTPUT2) stdout += text;
13329
14022
  spool.write(text);
13330
14023
  });
13331
14024
  child.stderr?.on("data", (chunk) => {
13332
14025
  const text = chunk.toString();
14026
+ stderrBytes += chunk.byteLength;
14027
+ emitProcessOutput3({ pid, stream: "stderr", chunk });
13333
14028
  if (stderr.length < MAX_OUTPUT2) stderr += text;
13334
14029
  spool.write(text);
13335
14030
  });
@@ -13339,6 +14034,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13339
14034
  if (typeof pid === "number") registry.unregister(pid);
13340
14035
  const durationMs = Date.now() - startedAt;
13341
14036
  const exitCode = killed ? 124 : code ?? 1;
14037
+ emitCompletedOnce(exitCode, pid);
13342
14038
  registry.afterCall(durationMs, exitCode !== 0);
13343
14039
  const spooled = spool.finalize();
13344
14040
  finish({
@@ -13414,7 +14110,7 @@ function guardedLookup(hostname2, options, callback) {
13414
14110
  var pinnedAgent;
13415
14111
  function getPinnedDispatcher() {
13416
14112
  if (!pinnedAgent) {
13417
- pinnedAgent = new Agent({ connect: { lookup: guardedLookup } });
14113
+ pinnedAgent = new Agent({ allowH2: false, connect: { lookup: guardedLookup } });
13418
14114
  }
13419
14115
  return pinnedAgent;
13420
14116
  }
@@ -13516,7 +14212,7 @@ async function assertNotPrivate(hostname2) {
13516
14212
  }
13517
14213
  }
13518
14214
  } catch (err) {
13519
- if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
14215
+ if (err instanceof ToolValidationError2) throw err;
13520
14216
  }
13521
14217
  }
13522
14218
  }
@@ -13864,7 +14560,7 @@ async function detectFixer(cwd) {
13864
14560
  init_util();
13865
14561
  import { spawn as spawn9 } from "node:child_process";
13866
14562
  import { statSync as statSync3 } from "node:fs";
13867
- import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
14563
+ import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
13868
14564
  import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
13869
14565
  var TIMEOUT_MS2 = 3e4;
13870
14566
  var MAX_OUTPUT3 = 1e5;
@@ -14009,7 +14705,7 @@ function validateWorktreeInput(input, projectRoot) {
14009
14705
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
14010
14706
  const root = resolve11(projectRoot);
14011
14707
  const abs = resolve11(root, input.worktreePath);
14012
- if (abs !== root && !abs.startsWith(root + sep4)) {
14708
+ if (abs !== root && !abs.startsWith(root + sep5)) {
14013
14709
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
14014
14710
  }
14015
14711
  }
@@ -14172,8 +14868,8 @@ var WALK_CONCURRENCY = 16;
14172
14868
  var globTool = {
14173
14869
  name: "glob",
14174
14870
  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.",
14871
+ description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
14872
+ 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
14873
  selection: {
14178
14874
  doNotUseWhen: "you need to search inside file contents.",
14179
14875
  useInstead: ["grep"]
@@ -14361,8 +15057,8 @@ var NATIVE_MAX_FILE_BYTES = 1e6;
14361
15057
  var grepTool = {
14362
15058
  name: "grep",
14363
15059
  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.',
15060
+ 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.",
15061
+ 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
15062
  selection: {
14367
15063
  doNotUseWhen: "you only need to locate files by name or path pattern.",
14368
15064
  useInstead: ["glob"]
@@ -15384,6 +16080,7 @@ function toYaml(data, indent = 0) {
15384
16080
  }
15385
16081
 
15386
16082
  // src/kanban.ts
16083
+ import { randomUUID } from "node:crypto";
15387
16084
  import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
15388
16085
  import {
15389
16086
  addCheckToTask,
@@ -15423,7 +16120,9 @@ import {
15423
16120
  setTaskChain,
15424
16121
  splitTask,
15425
16122
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
16123
+ transitionTask,
15426
16124
  transferTaskToBoard,
16125
+ touchKanbanPresence as touchKanbanPresence2,
15427
16126
  updateBoard as updateBoard2,
15428
16127
  updateCheckOnTask,
15429
16128
  updateColumn,
@@ -15435,17 +16134,21 @@ import {
15435
16134
  // src/session-kanban.ts
15436
16135
  import {
15437
16136
  deserializeTaskGraph,
16137
+ GlobalMailbox,
15438
16138
  loadPlan,
15439
16139
  loadTasks,
15440
16140
  mutatePlan,
15441
16141
  mutateTasks
15442
16142
  } from "@wrongstack/core";
16143
+ import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
15443
16144
  import {
15444
16145
  createBoard,
15445
16146
  getBoard,
16147
+ getKanbanDir,
15446
16148
  listBoards,
15447
16149
  removeBoard,
15448
16150
  syncBoardFromTaskGraph,
16151
+ touchKanbanPresence,
15449
16152
  updateBoard
15450
16153
  } from "@wrongstack/kanban";
15451
16154
  var SESSION_BOARD_TAG = "session-work";
@@ -15657,8 +16360,8 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
15657
16360
  var kanbanTool = {
15658
16361
  name: "kanban",
15659
16362
  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.",
16363
+ 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.",
16364
+ 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
16365
  permission: "confirm",
15663
16366
  mutating: true,
15664
16367
  capabilities: ["fs.write"],
@@ -15695,6 +16398,7 @@ var kanbanTool = {
15695
16398
  "transfer_task",
15696
16399
  "get_task",
15697
16400
  "update_task",
16401
+ "transition_task",
15698
16402
  "move_task",
15699
16403
  "delete_task",
15700
16404
  "set_chain",
@@ -15725,6 +16429,7 @@ var kanbanTool = {
15725
16429
  targetColumnId: { type: "string" },
15726
16430
  title: { type: "string" },
15727
16431
  description: { type: "string" },
16432
+ dueDate: { type: "string" },
15728
16433
  tags: { type: "array", items: { type: "string" } },
15729
16434
  labels: { type: "array", items: { type: "string" } },
15730
16435
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
@@ -15771,6 +16476,18 @@ var kanbanTool = {
15771
16476
  type: "string",
15772
16477
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
15773
16478
  },
16479
+ lifecycleStage: {
16480
+ type: "string",
16481
+ enum: ["backlog", "todo", "running", "review", "done"]
16482
+ },
16483
+ transitionAction: { type: "string" },
16484
+ transitionComment: { type: "string" },
16485
+ attachmentUrl: { type: "string" },
16486
+ attachmentTitle: { type: "string" },
16487
+ attachmentType: {
16488
+ type: "string",
16489
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
16490
+ },
15774
16491
  releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
15775
16492
  releaseReason: { type: "string" },
15776
16493
  clearAssignee: { type: "boolean" },
@@ -15841,591 +16558,632 @@ var kanbanTool = {
15841
16558
  async execute(input, ctx) {
15842
16559
  const projectRoot = ctx.projectRoot;
15843
16560
  if (!projectRoot) return fail("No project root is available.");
16561
+ const withPresence = async (result) => {
16562
+ const boardId = result.board?.id ?? input.boardId;
16563
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
16564
+ try {
16565
+ const board = await touchKanbanPresence2(projectRoot, boardId, {
16566
+ sessionId: ctx.session.id,
16567
+ agentId: ctx.agentId,
16568
+ agentName: ctx.agentName,
16569
+ taskId: input.taskId ?? result.task?.id,
16570
+ runTaskId: input.runTaskId
16571
+ });
16572
+ return board ? { ...result, board } : result;
16573
+ } catch {
16574
+ return result;
16575
+ }
16576
+ };
15844
16577
  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);
16578
+ const result = await (async () => {
16579
+ switch (input.action) {
16580
+ case "list_boards": {
16581
+ const boards = await listBoards2(projectRoot);
16582
+ return { ok: true, message: `${boards.length} board(s).`, boards };
15903
16583
  }
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.");
16584
+ case "get_board": {
16585
+ const board = await requireBoard(projectRoot, input.boardId);
16586
+ return board ? okBoard(board) : fail("Board not found.");
15936
16587
  }
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, {
16588
+ case "create_board": {
16589
+ if (!input.title) return fail("create_board requires title.");
16590
+ const board = await createBoard2(projectRoot, {
16591
+ title: input.title,
16592
+ ...input.description !== void 0 ? { description: input.description } : {},
16593
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16594
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
16595
+ });
16596
+ return { ok: true, message: `Board created: ${board.title}`, board };
16597
+ }
16598
+ case "update_board": {
16599
+ if (!input.boardId) return fail("update_board requires boardId.");
16600
+ const board = await updateBoard2(projectRoot, input.boardId, {
16601
+ ...input.title !== void 0 ? { title: input.title } : {},
16602
+ ...input.description !== void 0 ? { description: input.description } : {},
16603
+ ...input.tags !== void 0 ? { tags: input.tags } : {}
16604
+ });
16605
+ return board ? okBoard(board, "Board updated.") : fail("Board not found.");
16606
+ }
16607
+ case "duplicate_board": {
16608
+ if (!input.boardId) return fail("duplicate_board requires boardId.");
16609
+ const board = await duplicateBoard(projectRoot, input.boardId, {
16610
+ ...input.title !== void 0 ? { title: input.title } : {},
16611
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16612
+ ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
16613
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16614
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
16615
+ });
16616
+ return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
16617
+ }
16618
+ case "delete_board": {
16619
+ if (!input.boardId) return fail("delete_board requires boardId.");
16620
+ const removed = await removeBoard2(projectRoot, input.boardId);
16621
+ return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
16622
+ }
16623
+ case "generate_board": {
16624
+ if (!input.description) return fail("generate_board requires description.");
16625
+ const boardInput = generateBoardFromDescription({
16626
+ description: input.description,
16627
+ ...input.title !== void 0 ? { title: input.title } : {},
16628
+ ...input.context !== void 0 ? { context: input.context } : {},
16629
+ ...input.columns !== void 0 ? { columns: input.columns } : {}
16630
+ });
16631
+ const board = await createBoard2(projectRoot, boardInput);
16632
+ for (const taskInput2 of parseLinesIntoTasks(
16633
+ input.description,
16634
+ board.columns[0]?.id ?? "backlog"
16635
+ )) {
16636
+ await addTask(projectRoot, board.id, taskInput2);
16637
+ }
16638
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
16639
+ }
16640
+ case "export_markdown": {
16641
+ const board = await requireBoard(projectRoot, input.boardId);
16642
+ if (!board) return fail("Board not found.");
16643
+ return {
16644
+ ok: true,
16645
+ message: "Board exported.",
16646
+ board,
16647
+ markdown: exportBoardAsMarkdown(board)
16648
+ };
16649
+ }
16650
+ case "export_task_graph": {
16651
+ if (!input.boardId) return fail("export_task_graph requires boardId.");
16652
+ const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
16653
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
16654
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
16655
+ ...input.title !== void 0 ? { title: input.title } : {},
16656
+ ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
16657
+ ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
16658
+ });
16659
+ if (!exported) return fail("Board not found.");
16660
+ return {
16661
+ ok: true,
16662
+ message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
16663
+ board: exported.board,
16664
+ taskGraph: serializeTaskGraph(exported.graph)
16665
+ };
16666
+ }
16667
+ case "sync_task_graph": {
16668
+ if (!input.boardId || !input.taskGraph) {
16669
+ return fail("sync_task_graph requires boardId and taskGraph.");
16670
+ }
16671
+ const graph = deserializeTaskGraph2(input.taskGraph);
16672
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
16673
+ ...input.title !== void 0 ? { title: input.title } : {},
16674
+ ...input.description !== void 0 ? { description: input.description } : {},
16675
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16676
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16677
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16678
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16679
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16680
+ ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
16681
+ ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
16682
+ });
16683
+ return result2 ? {
16684
+ ok: true,
16685
+ message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
16686
+ board: result2.board
16687
+ } : fail("Board not found.");
16688
+ }
16689
+ case "create_from_graph": {
16690
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
16691
+ const graph = deserializeTaskGraph2(input.taskGraph);
16692
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16693
+ ...input.title !== void 0 ? { title: input.title } : {},
16694
+ ...input.description !== void 0 ? { description: input.description } : {},
16695
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16696
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16697
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16698
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16699
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
16700
+ });
16701
+ return {
16702
+ ok: true,
16703
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
16704
+ board
16705
+ };
16706
+ }
16707
+ case "import_session_tasks": {
16708
+ const taskPath = ctx.meta?.["task.path"];
16709
+ if (!taskPath) return fail("No session task file for this session.");
16710
+ const file = await loadTasks2(taskPath);
16711
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
16712
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
16713
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
16714
+ const tags = ["session", `session:${sessionId}`];
16715
+ const existing = (await listBoards2(projectRoot)).find(
16716
+ (b) => b.tags?.includes(`session:${sessionId}`)
16717
+ );
16718
+ if (existing) {
16719
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
16720
+ sourceSystem: "session",
16721
+ tags,
16722
+ archiveMissingTasks: true,
16723
+ includeCompletedTasks: true
16724
+ });
16725
+ return result2 ? {
16726
+ ok: true,
16727
+ message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
16728
+ board: result2.board
16729
+ } : fail("Session board vanished mid-sync.");
16730
+ }
16731
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16732
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
15986
16733
  sourceSystem: "session",
15987
- tags,
15988
- archiveMissingTasks: true,
15989
- includeCompletedTasks: true
16734
+ tags
15990
16735
  });
15991
- return result ? {
16736
+ return {
15992
16737
  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.");
16738
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
16739
+ board
16740
+ };
15996
16741
  }
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.");
16742
+ case "search_tasks": {
16743
+ const tasks = await searchKanban(projectRoot, {
16744
+ query: input.query,
16745
+ boardId: input.boardId,
16746
+ assignedAgent: input.agentId,
16747
+ status: input.status,
16748
+ priority: input.priority,
16749
+ label: input.labels?.[0],
16750
+ chainId: input.chainId
16751
+ });
16752
+ return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
16082
16753
  }
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.");
16754
+ case "ready_tasks": {
16755
+ const tasks = await listReadyTasks(projectRoot, {
16756
+ query: input.query,
16757
+ boardId: input.boardId,
16758
+ assignedAgent: input.agentId,
16759
+ priority: input.priority,
16760
+ label: input.labels?.[0],
16761
+ chainId: input.chainId,
16762
+ limit: input.limit
16763
+ });
16764
+ return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
16105
16765
  }
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.");
16766
+ case "snapshot": {
16767
+ const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
16768
+ query: input.query,
16769
+ boardId: input.boardId,
16770
+ assignedAgent: input.agentId,
16771
+ status: input.status,
16772
+ priority: input.priority,
16773
+ label: input.labels?.[0],
16774
+ chainId: input.chainId
16775
+ });
16776
+ return {
16777
+ ok: true,
16778
+ message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
16779
+ snapshot
16780
+ };
16119
16781
  }
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 } : {}
16782
+ case "add_column": {
16783
+ if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
16784
+ const result2 = await addColumn(projectRoot, input.boardId, {
16785
+ title: input.title,
16786
+ ...input.description !== void 0 ? { description: input.description } : {}
16787
+ });
16788
+ return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
16789
+ }
16790
+ case "update_column": {
16791
+ if (!input.boardId || !input.columnId)
16792
+ return fail("update_column requires boardId and columnId.");
16793
+ const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
16794
+ ...input.title !== void 0 ? { title: input.title } : {},
16795
+ ...input.description !== void 0 ? { description: input.description } : {},
16796
+ ...input.order !== void 0 ? { order: input.order } : {}
16797
+ });
16798
+ return board ? okBoard(board, "Column updated.") : fail("Column not found.");
16799
+ }
16800
+ case "delete_column": {
16801
+ if (!input.boardId || !input.columnId)
16802
+ return fail("delete_column requires boardId and columnId.");
16803
+ const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
16804
+ moveTasksToColumnId: input.moveTasksToColumnId
16805
+ });
16806
+ return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
16807
+ }
16808
+ case "add_task": {
16809
+ if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
16810
+ const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
16811
+ return result2 ? okTask(result2.board, result2.task, "Task added.") : fail("Board not found.");
16812
+ }
16813
+ case "split_task": {
16814
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
16815
+ return fail("split_task requires boardId, taskId, and childTitles.");
16130
16816
  }
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.");
16817
+ const result2 = await splitTask(projectRoot, input.boardId, input.taskId, {
16818
+ titles: input.childTitles,
16819
+ ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
16820
+ ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
16821
+ ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
16822
+ ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
16823
+ ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
16824
+ ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
16825
+ ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
16826
+ ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
16827
+ });
16828
+ return result2 ? {
16829
+ ok: true,
16830
+ message: `${result2.children.length} child task(s) created.`,
16831
+ board: result2.board,
16832
+ task: result2.parent,
16833
+ children: result2.children
16834
+ } : fail("Task not found.");
16137
16835
  }
16138
- const result = await transferTaskToBoard(
16139
- projectRoot,
16140
- input.boardId,
16141
- input.taskId,
16142
- input.targetBoardId,
16143
- {
16836
+ case "merge_tasks": {
16837
+ if (!input.boardId || !input.taskIds?.length || !input.title) {
16838
+ return fail("merge_tasks requires boardId, taskIds, and title.");
16839
+ }
16840
+ const result2 = await mergeTasks(projectRoot, input.boardId, {
16841
+ taskIds: input.taskIds,
16842
+ title: input.title,
16843
+ ...input.description !== void 0 ? { description: input.description } : {},
16144
16844
  ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16145
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
16146
16845
  ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16147
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16846
+ ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
16847
+ });
16848
+ return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
16849
+ }
16850
+ case "copy_task": {
16851
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
16852
+ return fail("copy_task requires boardId, taskId, and targetBoardId.");
16148
16853
  }
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.");
16854
+ const result2 = await copyTaskToBoard(
16855
+ projectRoot,
16856
+ input.boardId,
16857
+ input.taskId,
16858
+ input.targetBoardId,
16859
+ {
16860
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16861
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
16862
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16863
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16864
+ }
16865
+ );
16866
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
16171
16867
  }
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.");
16868
+ case "transfer_task": {
16869
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
16870
+ return fail("transfer_task requires boardId, taskId, and targetBoardId.");
16871
+ }
16872
+ const result2 = await transferTaskToBoard(
16873
+ projectRoot,
16874
+ input.boardId,
16875
+ input.taskId,
16876
+ input.targetBoardId,
16877
+ {
16878
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16879
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
16880
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16881
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16882
+ }
16883
+ );
16884
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
16190
16885
  }
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.");
16886
+ case "get_task": {
16887
+ if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
16888
+ const task = await getTask(projectRoot, input.boardId, input.taskId);
16889
+ return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
16206
16890
  }
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.");
16891
+ case "update_task": {
16892
+ if (!input.boardId || !input.taskId)
16893
+ return fail("update_task requires boardId and taskId.");
16894
+ const board = await updateTask(
16895
+ projectRoot,
16896
+ input.boardId,
16897
+ input.taskId,
16898
+ taskPatch(input)
16899
+ );
16900
+ return board ? okBoard(board, "Task updated.") : fail("Task not found.");
16231
16901
  }
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.");
16902
+ case "transition_task": {
16903
+ if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
16904
+ return fail(
16905
+ "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
16906
+ );
16907
+ }
16908
+ const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
16909
+ to: input.lifecycleStage,
16910
+ actor: input.author,
16911
+ comment: input.transitionComment,
16912
+ ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
16913
+ ...input.attachmentUrl !== void 0 ? {
16914
+ attachment: {
16915
+ url: input.attachmentUrl,
16916
+ type: input.attachmentType ?? "url",
16917
+ ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
16918
+ }
16919
+ } : {},
16920
+ patch: taskPatch(input)
16921
+ });
16922
+ return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
16273
16923
  }
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.");
16924
+ case "move_task": {
16925
+ if (!input.boardId || !input.taskId || !input.targetColumnId) {
16926
+ return fail("move_task requires boardId, taskId, and targetColumnId.");
16927
+ }
16928
+ const board = await moveTask(
16929
+ projectRoot,
16930
+ input.boardId,
16931
+ input.taskId,
16932
+ input.targetColumnId,
16933
+ input.order
16934
+ );
16935
+ return board ? okBoard(board, "Task moved.") : fail("Move failed.");
16337
16936
  }
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.");
16937
+ case "delete_task": {
16938
+ if (!input.boardId || !input.taskId)
16939
+ return fail("delete_task requires boardId and taskId.");
16940
+ const board = await removeTask(projectRoot, input.boardId, input.taskId);
16941
+ return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
16349
16942
  }
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.");
16943
+ case "set_chain": {
16944
+ if (!input.boardId || !input.taskIds?.length) {
16945
+ return fail("set_chain requires boardId and taskIds.");
16946
+ }
16947
+ const result2 = await setTaskChain(projectRoot, input.boardId, {
16948
+ taskIds: input.taskIds,
16949
+ ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
16950
+ ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
16951
+ });
16952
+ return result2 ? {
16953
+ ok: true,
16954
+ message: `Chain set: ${result2.chainId}`,
16955
+ board: result2.board,
16956
+ chain: result2.tasks
16957
+ } : fail("Board or task not found.");
16363
16958
  }
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 } : {},
16959
+ case "get_chain": {
16960
+ if (!input.boardId || !(input.taskId || input.chainId)) {
16961
+ return fail("get_chain requires boardId and taskId or chainId.");
16962
+ }
16963
+ const result2 = await getTaskChain(
16964
+ projectRoot,
16965
+ input.boardId,
16966
+ input.taskId ?? input.chainId ?? ""
16967
+ );
16968
+ return result2 ? {
16969
+ ok: true,
16970
+ message: `Chain loaded: ${result2.chainId}`,
16971
+ board: result2.board,
16972
+ chain: result2.tasks
16973
+ } : fail("Chain not found.");
16974
+ }
16975
+ case "claim_task": {
16976
+ const result2 = await claimReadyTask(projectRoot, {
16977
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
16978
+ ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
16979
+ ...assignmentInput(input),
16980
+ status: input.assignmentStatus ?? "queued"
16981
+ });
16982
+ return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
16983
+ }
16984
+ case "release_task": {
16985
+ if (!input.boardId || !input.taskId) {
16986
+ return fail("release_task requires boardId and taskId.");
16987
+ }
16988
+ const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
16989
+ ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
16990
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
16991
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
16992
+ });
16993
+ return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
16994
+ }
16995
+ case "assign_task": {
16996
+ if (!input.boardId || !input.taskId)
16997
+ return fail("assign_task requires boardId and taskId.");
16998
+ const board = await assignTask(
16999
+ projectRoot,
17000
+ input.boardId,
17001
+ input.taskId,
17002
+ assignmentInput(input)
17003
+ );
17004
+ return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
17005
+ }
17006
+ case "mark_assignment": {
17007
+ if (!input.boardId || !input.taskId)
17008
+ return fail("mark_assignment requires boardId and taskId.");
17009
+ const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
17010
+ const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
17011
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17012
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17013
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17014
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17015
+ ...input.error !== void 0 ? { error: input.error } : {},
17016
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17017
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17018
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17019
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17020
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17021
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17022
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17023
+ });
17024
+ return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
17025
+ }
17026
+ case "heartbeat_assignment": {
17027
+ if (!input.boardId || !input.taskId) {
17028
+ return fail("heartbeat_assignment requires boardId and taskId.");
17029
+ }
17030
+ const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
17031
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17032
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
17033
+ });
17034
+ return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
17035
+ }
17036
+ case "recover_stale": {
17037
+ if (!input.boardId) return fail("recover_stale requires boardId.");
17038
+ const policyFields = [
17039
+ input.recoveryPolicyFailOnCostCeiling !== void 0,
17040
+ input.recoveryPolicyReleaseOnFailureKinds !== void 0,
17041
+ input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
17042
+ input.recoveryPolicyRetryPolicyOverride !== void 0
17043
+ ].some(Boolean);
17044
+ const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
17045
+ ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
17046
+ ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
17047
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17048
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
17049
+ ...policyFields ? {
17050
+ policy: {
17051
+ ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
17052
+ ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
17053
+ releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
17054
+ } : {},
17055
+ ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
17056
+ releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
17057
+ } : {},
17058
+ ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
17059
+ retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
17060
+ } : {}
17061
+ }
17062
+ } : {}
17063
+ });
17064
+ return result2 ? {
17065
+ ok: true,
17066
+ message: `Recovered ${result2.tasks.length} stale assignment(s).`,
17067
+ board: result2.board,
17068
+ recoveredTasks: result2.tasks
17069
+ } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
17070
+ }
17071
+ case "events": {
17072
+ if (!input.boardId) return fail("events requires boardId.");
17073
+ const eventList = await listKanbanEvents(projectRoot, input.boardId);
17074
+ return {
17075
+ ok: true,
17076
+ message: `${eventList.length} event(s).`,
17077
+ events: eventList
17078
+ };
17079
+ }
17080
+ case "queue_health": {
17081
+ const health = await getKanbanQueueHealth(projectRoot, {
17082
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
17083
+ });
17084
+ return {
17085
+ ok: true,
17086
+ message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
17087
+ queueHealth: health
17088
+ };
17089
+ }
17090
+ case "add_dependency": {
17091
+ if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
17092
+ return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
17093
+ }
17094
+ const board = await addDependency(
17095
+ projectRoot,
17096
+ input.boardId,
17097
+ input.taskId,
17098
+ input.dependencyTaskId
17099
+ );
17100
+ return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
17101
+ }
17102
+ case "add_goal_metric": {
17103
+ if (!input.boardId || !input.taskId || !input.metricName) {
17104
+ return fail("add_goal_metric requires boardId, taskId, and metricName.");
17105
+ }
17106
+ const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
17107
+ name: input.metricName,
16371
17108
  ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
16372
17109
  ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
16373
17110
  ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
16374
17111
  ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
16375
17112
  ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17113
+ });
17114
+ return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
17115
+ }
17116
+ case "update_goal_metric": {
17117
+ if (!input.boardId || !input.taskId || !input.metricId) {
17118
+ return fail("update_goal_metric requires boardId, taskId, and metricId.");
16376
17119
  }
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.");
17120
+ const board = await updateGoalMetricOnTask(
17121
+ projectRoot,
17122
+ input.boardId,
17123
+ input.taskId,
17124
+ input.metricId,
17125
+ {
17126
+ ...input.metricName !== void 0 ? { name: input.metricName } : {},
17127
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
17128
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17129
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17130
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17131
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17132
+ }
17133
+ );
17134
+ return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
16383
17135
  }
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.");
17136
+ case "add_check": {
17137
+ if (!input.boardId || !input.taskId || !input.checkDescription) {
17138
+ return fail("add_check requires boardId, taskId, and checkDescription.");
17139
+ }
17140
+ const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
17141
+ description: input.checkDescription,
17142
+ type: "manual",
17143
+ status: input.checkStatus
17144
+ });
17145
+ return board ? okBoard(board, "Check added.") : fail("Task not found.");
16394
17146
  }
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 } : {}
17147
+ case "update_check": {
17148
+ if (!input.boardId || !input.taskId || !input.checkId) {
17149
+ return fail("update_check requires boardId, taskId, and checkId.");
16403
17150
  }
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.");
17151
+ const board = await updateCheckOnTask(
17152
+ projectRoot,
17153
+ input.boardId,
17154
+ input.taskId,
17155
+ input.checkId,
17156
+ {
17157
+ ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
17158
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17159
+ }
17160
+ );
17161
+ return board ? okBoard(board, "Check updated.") : fail("Check not found.");
17162
+ }
17163
+ case "add_note": {
17164
+ if (!input.boardId || !input.taskId || !input.note)
17165
+ return fail("add_note requires boardId, taskId, and note.");
17166
+ const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
17167
+ author: input.author ?? "agent",
17168
+ content: input.note
17169
+ });
17170
+ return board ? okBoard(board, "Note added.") : fail("Task not found.");
17171
+ }
17172
+ case "add_link": {
17173
+ if (!input.boardId || !input.taskId || !input.url)
17174
+ return fail("add_link requires boardId, taskId, and url.");
17175
+ const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
17176
+ url: input.url,
17177
+ type: input.linkType ?? "url",
17178
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17179
+ });
17180
+ return board ? okBoard(board, "Link added.") : fail("Task not found.");
17181
+ }
17182
+ default:
17183
+ return fail(`Unknown kanban action: ${input.action}`);
16425
17184
  }
16426
- default:
16427
- return fail(`Unknown kanban action: ${input.action}`);
16428
- }
17185
+ })();
17186
+ return withPresence(result);
16429
17187
  } catch (err) {
16430
17188
  return fail(err instanceof Error ? err.message : String(err));
16431
17189
  }
@@ -16449,6 +17207,7 @@ function taskInput(input) {
16449
17207
  title: input.title ?? "",
16450
17208
  columnId: input.columnId,
16451
17209
  description: input.description,
17210
+ dueDate: input.dueDate,
16452
17211
  priority: input.priority,
16453
17212
  ...input.taskType !== void 0 ? { type: input.taskType } : {},
16454
17213
  status: input.status,
@@ -16457,7 +17216,67 @@ function taskInput(input) {
16457
17216
  ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
16458
17217
  ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16459
17218
  ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
16460
- ...assignment ? { assignment } : {}
17219
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
17220
+ ...assignment ? { assignment } : {},
17221
+ ...input.order !== void 0 ? { order: input.order } : {},
17222
+ // Sprint 3 durable task-level fields
17223
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
17224
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
17225
+ // Relationships — chainId alone cannot construct a KanbanTaskChainRef,
17226
+ // so chain/previousTaskId/nextTaskId must be set via update_task.
17227
+ ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
17228
+ // Detail objects
17229
+ ...input.checkDescription !== void 0 ? {
17230
+ successCriteria: [
17231
+ {
17232
+ id: randomUUID(),
17233
+ description: input.checkDescription,
17234
+ type: "manual",
17235
+ status: input.checkStatus ?? "pending"
17236
+ }
17237
+ ]
17238
+ } : {},
17239
+ ...input.metricName !== void 0 ? {
17240
+ goalMetrics: [
17241
+ {
17242
+ id: randomUUID(),
17243
+ name: input.metricName,
17244
+ status: input.metricStatus ?? "pending",
17245
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17246
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17247
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17248
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17249
+ }
17250
+ ]
17251
+ } : {},
17252
+ ...input.url !== void 0 ? {
17253
+ links: [
17254
+ {
17255
+ url: input.url,
17256
+ type: input.linkType ?? "url",
17257
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17258
+ }
17259
+ ]
17260
+ } : {},
17261
+ ...input.note !== void 0 ? {
17262
+ notes: [
17263
+ {
17264
+ id: randomUUID(),
17265
+ author: input.author ?? "agent",
17266
+ content: input.note,
17267
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
17268
+ }
17269
+ ]
17270
+ } : {},
17271
+ // Origin tracking
17272
+ ...input.graphId !== void 0 ? {
17273
+ origin: {
17274
+ system: input.sourceSystem ?? "kanban-tool",
17275
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
17276
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
17277
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
17278
+ }
17279
+ } : {}
16461
17280
  };
16462
17281
  }
16463
17282
  function mergedDependsOn(input) {
@@ -16471,6 +17290,7 @@ function taskPatch(input) {
16471
17290
  return {
16472
17291
  title: input.title,
16473
17292
  description: input.description,
17293
+ dueDate: input.dueDate,
16474
17294
  columnId: input.columnId,
16475
17295
  order: input.order,
16476
17296
  priority: input.priority,
@@ -17232,7 +18052,7 @@ import {
17232
18052
  mutateTasks as mutateTasks2,
17233
18053
  formatTaskList
17234
18054
  } from "@wrongstack/core";
17235
- import { randomUUID } from "node:crypto";
18055
+ import { randomUUID as randomUUID2 } from "node:crypto";
17236
18056
  var planTool = {
17237
18057
  name: "plan",
17238
18058
  category: "Session",
@@ -17458,7 +18278,7 @@ var planTool = {
17458
18278
  try {
17459
18279
  const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
17460
18280
  f.tasks.push({
17461
- id: `task_${randomUUID()}`,
18281
+ id: `task_${randomUUID2()}`,
17462
18282
  title: taskifyMeta.title,
17463
18283
  description: taskifyMeta.details || void 0,
17464
18284
  type: "feature",
@@ -18600,7 +19420,7 @@ import {
18600
19420
  mutatePlan as mutatePlan3,
18601
19421
  formatPlan as formatPlan2
18602
19422
  } from "@wrongstack/core";
18603
- import { randomUUID as randomUUID2 } from "node:crypto";
19423
+ import { randomUUID as randomUUID3 } from "node:crypto";
18604
19424
  function findTaskIndex(tasks, query) {
18605
19425
  const asNum = Number.parseInt(query, 10);
18606
19426
  if (!Number.isNaN(asNum)) {
@@ -18788,7 +19608,7 @@ var taskTool = {
18788
19608
  }
18789
19609
  const now = (/* @__PURE__ */ new Date()).toISOString();
18790
19610
  const newTask = {
18791
- id: `task_${Date.now()}_${randomUUID2().slice(0, 8)}`,
19611
+ id: `task_${Date.now()}_${randomUUID3().slice(0, 8)}`,
18792
19612
  title: t.title,
18793
19613
  description: t.description,
18794
19614
  type: t.type || "feature",
@@ -18849,7 +19669,7 @@ var taskTool = {
18849
19669
  });
18850
19670
  if (match.description) {
18851
19671
  todos.push({
18852
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
19672
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
18853
19673
  content: match.description.slice(0, 200),
18854
19674
  status: "pending",
18855
19675
  promotedFromTask: match.id
@@ -18858,7 +19678,7 @@ var taskTool = {
18858
19678
  if (input.subtasks && input.subtasks.length > 0) {
18859
19679
  for (const st of input.subtasks) {
18860
19680
  todos.push({
18861
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
19681
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
18862
19682
  content: st,
18863
19683
  status: "pending",
18864
19684
  promotedFromTask: match.id
@@ -19564,8 +20384,8 @@ var DEFAULT_IGNORE5 = [
19564
20384
  var treeTool = {
19565
20385
  name: "tree",
19566
20386
  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.",
20387
+ 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.",
20388
+ 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
20389
  permission: "auto",
19570
20390
  mutating: false,
19571
20391
  capabilities: ["fs.read"],
@@ -19968,15 +20788,15 @@ var OPTIONAL_TOOLS = [
19968
20788
  toolUseTool,
19969
20789
  batchToolUseTool,
19970
20790
  toolHelpTool,
19971
- codebaseIndexTool,
19972
- codebaseSearchTool,
19973
- codebaseStatsTool,
19974
20791
  setWorkingDirTool
19975
20792
  ];
19976
20793
  var TIER1_TOOLS = [
19977
20794
  readTool,
19978
20795
  writeTool,
19979
20796
  editTool,
20797
+ codebaseStatsTool,
20798
+ codebaseSearchTool,
20799
+ codebaseIndexTool,
19980
20800
  bashTool,
19981
20801
  grepTool,
19982
20802
  globTool,
@@ -20015,9 +20835,6 @@ var TIER3_TOOLS = [
20015
20835
  toolUseTool,
20016
20836
  batchToolUseTool,
20017
20837
  toolHelpTool,
20018
- codebaseIndexTool,
20019
- codebaseSearchTool,
20020
- codebaseStatsTool,
20021
20838
  setWorkingDirTool
20022
20839
  ];
20023
20840
  var builtinTools = [
@@ -20026,6 +20843,9 @@ var builtinTools = [
20026
20843
  readTool,
20027
20844
  writeTool,
20028
20845
  editTool,
20846
+ codebaseStatsTool,
20847
+ codebaseSearchTool,
20848
+ codebaseIndexTool,
20029
20849
  replaceTool,
20030
20850
  globTool,
20031
20851
  grepTool,
@@ -20060,9 +20880,6 @@ var builtinTools = [
20060
20880
  toolUseTool,
20061
20881
  batchToolUseTool,
20062
20882
  toolHelpTool,
20063
- codebaseIndexTool,
20064
- codebaseSearchTool,
20065
- codebaseStatsTool,
20066
20883
  setWorkingDirTool
20067
20884
  ];
20068
20885
  export {