@wrongstack/tools 0.287.0 → 0.291.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/_fetch-guard.d.ts.map +1 -1
  2. package/dist/_redact-command.d.ts.map +1 -1
  3. package/dist/_spawn-stream.d.ts.map +1 -1
  4. package/dist/audit.js +61 -14
  5. package/dist/audit.js.map +3 -3
  6. package/dist/auto-proceed-loop-guard.d.ts +128 -0
  7. package/dist/auto-proceed-loop-guard.d.ts.map +1 -0
  8. package/dist/auto-proceed-loop-guard.js +46 -0
  9. package/dist/auto-proceed-loop-guard.js.map +7 -0
  10. package/dist/bash.d.ts +1 -1
  11. package/dist/bash.d.ts.map +1 -1
  12. package/dist/bash.js +97 -19
  13. package/dist/bash.js.map +3 -3
  14. package/dist/builtin.d.ts +12 -10
  15. package/dist/builtin.d.ts.map +1 -1
  16. package/dist/builtin.js +1766 -864
  17. package/dist/builtin.js.map +3 -3
  18. package/dist/codebase-index/background-indexer.d.ts +1 -1
  19. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  20. package/dist/codebase-index/bm25.d.ts +1 -2
  21. package/dist/codebase-index/bm25.d.ts.map +1 -1
  22. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  23. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  24. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  25. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  26. package/dist/codebase-index/index-service.d.ts +11 -1
  27. package/dist/codebase-index/index-service.d.ts.map +1 -1
  28. package/dist/codebase-index/index.d.ts +2 -1
  29. package/dist/codebase-index/index.d.ts.map +1 -1
  30. package/dist/codebase-index/index.js +888 -254
  31. package/dist/codebase-index/index.js.map +3 -3
  32. package/dist/codebase-index/indexer.d.ts.map +1 -1
  33. package/dist/codebase-index/refs-extractor.d.ts +2 -17
  34. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  35. package/dist/codebase-index/schema.d.ts +43 -1
  36. package/dist/codebase-index/schema.d.ts.map +1 -1
  37. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  38. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  39. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  40. package/dist/codebase-index/worker.js +738 -212
  41. package/dist/codebase-index/worker.js.map +3 -3
  42. package/dist/codebase-index/writer.d.ts +60 -1
  43. package/dist/codebase-index/writer.d.ts.map +1 -1
  44. package/dist/e2e.js +477 -0
  45. package/dist/e2e.js.map +7 -0
  46. package/dist/exec.d.ts +1 -1
  47. package/dist/exec.d.ts.map +1 -1
  48. package/dist/exec.js +60 -5
  49. package/dist/exec.js.map +3 -3
  50. package/dist/fetch.js +2 -2
  51. package/dist/fetch.js.map +2 -2
  52. package/dist/format.js +61 -14
  53. package/dist/format.js.map +3 -3
  54. package/dist/glob.js +2 -2
  55. package/dist/glob.js.map +1 -1
  56. package/dist/grep.d.ts.map +1 -1
  57. package/dist/grep.js +2 -2
  58. package/dist/grep.js.map +2 -2
  59. package/dist/index.d.ts +3 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +2001 -873
  62. package/dist/index.js.map +3 -3
  63. package/dist/install.js +61 -14
  64. package/dist/install.js.map +3 -3
  65. package/dist/kanban.d.ts +21 -2
  66. package/dist/kanban.d.ts.map +1 -1
  67. package/dist/kanban.js +713 -572
  68. package/dist/kanban.js.map +3 -3
  69. package/dist/languages/detect.d.ts.map +1 -1
  70. package/dist/languages/index.js +61 -14
  71. package/dist/languages/index.js.map +3 -3
  72. package/dist/languages/types.d.ts +7 -0
  73. package/dist/languages/types.d.ts.map +1 -1
  74. package/dist/lint.js +61 -14
  75. package/dist/lint.js.map +3 -3
  76. package/dist/outdated.js +61 -14
  77. package/dist/outdated.js.map +3 -3
  78. package/dist/pack.js +1763 -861
  79. package/dist/pack.js.map +3 -3
  80. package/dist/plan.js +4 -0
  81. package/dist/plan.js.map +2 -2
  82. package/dist/process-registry.js +12 -5
  83. package/dist/process-registry.js.map +2 -2
  84. package/dist/read.d.ts.map +1 -1
  85. package/dist/read.js.map +2 -2
  86. package/dist/search.js +2 -2
  87. package/dist/search.js.map +2 -2
  88. package/dist/session-kanban.d.ts +9 -0
  89. package/dist/session-kanban.d.ts.map +1 -1
  90. package/dist/session-kanban.js +197 -3
  91. package/dist/session-kanban.js.map +2 -2
  92. package/dist/task.js +4 -0
  93. package/dist/task.js.map +2 -2
  94. package/dist/test.js +61 -14
  95. package/dist/test.js.map +3 -3
  96. package/dist/todo.js +4 -0
  97. package/dist/todo.js.map +2 -2
  98. package/dist/tree.js +2 -2
  99. package/dist/tree.js.map +1 -1
  100. package/dist/typecheck.js +61 -14
  101. package/dist/typecheck.js.map +3 -3
  102. package/package.json +13 -5
package/dist/index.js CHANGED
@@ -404,8 +404,10 @@ function redactCommand(cmd) {
404
404
  const flag = match.slice(0, match.indexOf(expectDefined(delim)) + 1);
405
405
  return `${flag}[REDACTED]`;
406
406
  }
407
- const flagEnd = match.match(/^--?[a-zA-Z][a-zA-Z0-9_-]*/)?.[0] ?? match;
408
- return `${flagEnd}=**redacted**`;
407
+ if (match.startsWith("--")) {
408
+ return match;
409
+ }
410
+ return `${match.slice(0, 2)}[REDACTED]`;
409
411
  });
410
412
  }
411
413
  return result;
@@ -417,9 +419,14 @@ var init_redact_command = __esm({
417
419
  SENSITIVE_FLAG_PATTERNS = [
418
420
  // --flag=value or --flag "value" (value captured up to next space or comma)
419
421
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
420
- // -f "value" style short flags
421
- /(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
422
- /(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
422
+ // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
423
+ // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
424
+ // NOTE: synced with @wrongstack/core observability/redact-command.ts.
425
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
426
+ // -p|-password|-a (redis auth) short flags: attached + separated + =value.
427
+ // Same token-start anchor; over-redaction is an accepted tradeoff for a
428
+ // redaction function. Synced with core copy.
429
+ /(?<![-\w])-(?:password|p|a)(?:[=\s]+)?[^\s,-]+/gi,
423
430
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
424
431
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
425
432
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
@@ -802,7 +809,12 @@ var init_process_registry = __esm({
802
809
 
803
810
  // src/_spawn-stream.ts
804
811
  import { spawn as spawn2 } from "node:child_process";
805
- import { buildChildEnv } from "@wrongstack/core";
812
+ import {
813
+ buildChildEnv,
814
+ emitProcessCompleted,
815
+ emitProcessOutput,
816
+ emitProcessStarted
817
+ } from "@wrongstack/core";
806
818
  async function* spawnStream(opts) {
807
819
  const max = opts.maxBytes ?? 2e5;
808
820
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -827,6 +839,19 @@ async function* spawnStream(opts) {
827
839
  });
828
840
  const registry = getProcessRegistry();
829
841
  const pid = child.pid;
842
+ const processStartedAt = Date.now();
843
+ let stdoutBytes = 0;
844
+ let stderrBytes = 0;
845
+ let telemetryCompleted = false;
846
+ emitProcessStarted({
847
+ ...pid !== void 0 ? { pid } : {},
848
+ parentPid: process.pid,
849
+ command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
850
+ args: redactCommand(opts.args.join(" ")).split(" ").filter(Boolean),
851
+ cwd: opts.cwd,
852
+ background: false,
853
+ startedAt: new Date(processStartedAt).toISOString()
854
+ });
830
855
  if (typeof pid === "number") {
831
856
  registry.register({
832
857
  pid,
@@ -855,6 +880,8 @@ async function* spawnStream(opts) {
855
880
  };
856
881
  const onOut = (c) => {
857
882
  const s = c.toString();
883
+ stdoutBytes += c.byteLength;
884
+ emitProcessOutput({ pid, stream: "stdout", chunk: c });
858
885
  if (stdout.length < max) stdout += s;
859
886
  spool.write(s);
860
887
  queue.push({ kind: "out", data: s });
@@ -867,6 +894,8 @@ async function* spawnStream(opts) {
867
894
  };
868
895
  const onErr = (c) => {
869
896
  const s = c.toString();
897
+ stderrBytes += c.byteLength;
898
+ emitProcessOutput({ pid, stream: "stderr", chunk: c });
870
899
  if (stderr.length < max) stderr += s;
871
900
  spool.write(s);
872
901
  queue.push({ kind: "err", data: s });
@@ -884,9 +913,25 @@ async function* spawnStream(opts) {
884
913
  queue.push({ kind: "error", data: e.message });
885
914
  wake();
886
915
  });
887
- child.on("close", (code) => {
916
+ const completeTelemetry = (code, signal, timedOut = false) => {
917
+ if (telemetryCompleted) return;
918
+ telemetryCompleted = true;
919
+ emitProcessCompleted({
920
+ ...pid !== void 0 ? { pid } : {},
921
+ exitCode: code,
922
+ ...signal ? { signal } : {},
923
+ durationMs: Date.now() - processStartedAt,
924
+ stdoutBytes,
925
+ stderrBytes,
926
+ timedOut,
927
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
928
+ });
929
+ };
930
+ child.on("close", (code, signal) => {
888
931
  if (typeof pid === "number") registry.unregister(pid);
889
- queue.push({ kind: "close", data: "", code: code ?? 0 });
932
+ const exitCode2 = code ?? (signal ? 1 : 0);
933
+ completeTelemetry(exitCode2, signal ?? void 0);
934
+ queue.push({ kind: "close", data: "", code: exitCode2, ...signal ? { signal } : {} });
890
935
  wake();
891
936
  });
892
937
  const onAbort = () => {
@@ -899,6 +944,7 @@ async function* spawnStream(opts) {
899
944
  }
900
945
  }
901
946
  queue.push({ kind: "close", data: "", code: 124 });
947
+ completeTelemetry(124, "SIGKILL", true);
902
948
  wake();
903
949
  };
904
950
  if (isWin) {
@@ -2506,13 +2552,14 @@ async function detectLanguageWorkspaces(options) {
2506
2552
  const target = options.target ? await canonicalInside(resolveFrom(cwd, options.target), projectRoot, "target") : void 0;
2507
2553
  const profiles = (options.profiles ?? languageProfileRegistry.list()).filter((profile) => !options.language || profile.id === options.language).slice().sort((a, b) => a.id.localeCompare(b.id));
2508
2554
  const limits = normalizeLimits(options.limits);
2555
+ const extraIgnores = new Set(options.ignoredDirectories ?? []);
2509
2556
  const state = {
2510
2557
  entries: 0,
2511
2558
  truncated: false,
2512
2559
  sourcePaths: /* @__PURE__ */ new Map(),
2513
2560
  candidates: /* @__PURE__ */ new Map()
2514
2561
  };
2515
- await scanDirectory(projectRoot, 0, profiles, limits, state, options.signal);
2562
+ await scanDirectory(projectRoot, 0, profiles, limits, state, extraIgnores, options.signal);
2516
2563
  addSourceFallbacks(projectRoot, profiles, state);
2517
2564
  if (target) addTargetEvidence(target, projectRoot, profiles, state);
2518
2565
  const workspaces = await Promise.all(
@@ -2526,7 +2573,7 @@ async function detectLanguageWorkspaces(options) {
2526
2573
  workspaces
2527
2574
  };
2528
2575
  }
2529
- async function scanDirectory(directory, depth, profiles, limits, state, signal) {
2576
+ async function scanDirectory(directory, depth, profiles, limits, state, extraIgnores, signal) {
2530
2577
  signal?.throwIfAborted();
2531
2578
  if (depth > limits.maxDepth || state.entries >= limits.maxEntries) {
2532
2579
  state.truncated = true;
@@ -2549,20 +2596,20 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
2549
2596
  const fullPath = path4.join(directory, entry.name);
2550
2597
  if (entry.isSymbolicLink()) continue;
2551
2598
  if (entry.isDirectory()) {
2552
- if (shouldIgnoreDirectory(entry.name, profiles)) continue;
2599
+ if (shouldIgnoreDirectory(entry.name, profiles, extraIgnores)) continue;
2553
2600
  if (depth >= limits.maxDepth) {
2554
2601
  state.truncated = true;
2555
2602
  continue;
2556
2603
  }
2557
- await scanDirectory(fullPath, depth + 1, profiles, limits, state, signal);
2604
+ await scanDirectory(fullPath, depth + 1, profiles, limits, state, extraIgnores, signal);
2558
2605
  continue;
2559
2606
  }
2560
2607
  if (!entry.isFile()) continue;
2561
2608
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2562
2609
  }
2563
2610
  }
2564
- function collectFileEvidence(directory, fullPath, basename9, profiles, state) {
2565
- const lower = basename9.toLowerCase();
2611
+ function collectFileEvidence(directory, fullPath, basename10, profiles, state) {
2612
+ const lower = basename10.toLowerCase();
2566
2613
  const extension = path4.extname(lower);
2567
2614
  for (const profile of profiles) {
2568
2615
  const detector = profile.detectors.find(
@@ -2573,7 +2620,7 @@ function collectFileEvidence(directory, fullPath, basename9, profiles, state) {
2573
2620
  candidate.evidence.push({
2574
2621
  kind: detector.kind,
2575
2622
  path: fullPath,
2576
- value: basename9,
2623
+ value: basename10,
2577
2624
  weight: detector.weight
2578
2625
  });
2579
2626
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2683,8 +2730,8 @@ function getCandidate(state, profile, root) {
2683
2730
  }
2684
2731
  return candidate;
2685
2732
  }
2686
- function shouldIgnoreDirectory(name, profiles) {
2687
- if (GLOBAL_IGNORES.has(name) || name.startsWith(".")) return true;
2733
+ function shouldIgnoreDirectory(name, profiles, extraIgnores) {
2734
+ if (GLOBAL_IGNORES.has(name) || extraIgnores.has(name) || name.startsWith(".")) return true;
2688
2735
  return profiles.some((profile) => profile.ignoredDirectories.includes(name));
2689
2736
  }
2690
2737
  function normalizeLimits(input) {
@@ -5210,6 +5257,11 @@ function parseAuditOutput(json2, exitCode) {
5210
5257
  // src/bash.ts
5211
5258
  import { spawn as spawn3 } from "node:child_process";
5212
5259
  import * as os4 from "node:os";
5260
+ import {
5261
+ emitProcessCompleted as emitProcessCompleted2,
5262
+ emitProcessOutput as emitProcessOutput2,
5263
+ emitProcessStarted as emitProcessStarted2
5264
+ } from "@wrongstack/core";
5213
5265
 
5214
5266
  // src/_env.ts
5215
5267
  import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core";
@@ -6085,6 +6137,32 @@ var bashTool = {
6085
6137
  }
6086
6138
  }
6087
6139
  const pid2 = child2.pid;
6140
+ let stdoutBytes2 = 0;
6141
+ let stderrBytes2 = 0;
6142
+ let telemetryCompleted2 = false;
6143
+ emitProcessStarted2({
6144
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
6145
+ parentPid: process.pid,
6146
+ command: redactCommand(`${shell} ${args.join(" ")}`),
6147
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
6148
+ cwd: ctx.projectRoot,
6149
+ background: true,
6150
+ startedAt: new Date(startedAt).toISOString()
6151
+ });
6152
+ const completeBackground = (exitCode, signal) => {
6153
+ if (telemetryCompleted2) return;
6154
+ telemetryCompleted2 = true;
6155
+ emitProcessCompleted2({
6156
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
6157
+ exitCode,
6158
+ ...signal ? { signal } : {},
6159
+ durationMs: Date.now() - startedAt,
6160
+ stdoutBytes: stdoutBytes2,
6161
+ stderrBytes: stderrBytes2,
6162
+ timedOut: false,
6163
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
6164
+ });
6165
+ };
6088
6166
  if (typeof pid2 === "number") {
6089
6167
  registry.register({
6090
6168
  pid: pid2,
@@ -6097,7 +6175,10 @@ var bashTool = {
6097
6175
  });
6098
6176
  child2.on("close", () => registry.unregister(pid2));
6099
6177
  }
6100
- const onBgData = (chunk) => {
6178
+ const onBgData = (chunk, stream) => {
6179
+ if (stream === "stdout") stdoutBytes2 += chunk.byteLength;
6180
+ else stderrBytes2 += chunk.byteLength;
6181
+ emitProcessOutput2({ pid: pid2, stream, chunk });
6101
6182
  if (truncated) return;
6102
6183
  const remain = MAX_OUTPUT - buf2.length;
6103
6184
  if (remain > 0) {
@@ -6105,24 +6186,28 @@ var bashTool = {
6105
6186
  }
6106
6187
  if (buf2.length >= MAX_OUTPUT) {
6107
6188
  truncated = true;
6108
- child2.stdout?.off("data", onBgData);
6109
- child2.stderr?.off("data", onBgData);
6189
+ child2.stdout?.off("data", onBgStdout);
6190
+ child2.stderr?.off("data", onBgStderr);
6110
6191
  }
6111
6192
  };
6112
- child2.stdout?.on("data", onBgData);
6113
- child2.stderr?.on("data", onBgData);
6193
+ const onBgStdout = (chunk) => onBgData(chunk, "stdout");
6194
+ const onBgStderr = (chunk) => onBgData(chunk, "stderr");
6195
+ child2.stdout?.on("data", onBgStdout);
6196
+ child2.stderr?.on("data", onBgStderr);
6114
6197
  const cleanupBackground = () => {
6115
- child2.stdout?.off("data", onBgData);
6116
- child2.stderr?.off("data", onBgData);
6198
+ child2.stdout?.off("data", onBgStdout);
6199
+ child2.stderr?.off("data", onBgStderr);
6117
6200
  };
6118
6201
  child2.on("error", () => {
6119
6202
  cleanupBackground();
6120
6203
  if (typeof pid2 === "number") registry.unregister(pid2);
6121
6204
  registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
6205
+ completeBackground(1);
6122
6206
  });
6123
- child2.on("close", () => {
6207
+ child2.on("close", (code, signal) => {
6124
6208
  cleanupBackground();
6125
6209
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
6210
+ completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
6126
6211
  });
6127
6212
  if (typeof pid2 === "number") child2.unref();
6128
6213
  yield {
@@ -6162,6 +6247,32 @@ var bashTool = {
6162
6247
  }
6163
6248
  }
6164
6249
  const pid = child.pid;
6250
+ let stdoutBytes = 0;
6251
+ let stderrBytes = 0;
6252
+ let telemetryCompleted = false;
6253
+ emitProcessStarted2({
6254
+ ...pid !== void 0 ? { pid } : {},
6255
+ parentPid: process.pid,
6256
+ command: redactCommand(`${shell} ${args.join(" ")}`),
6257
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
6258
+ cwd: ctx.projectRoot,
6259
+ background: false,
6260
+ startedAt: new Date(startedAt).toISOString()
6261
+ });
6262
+ const completeForeground = (exitCode, signal) => {
6263
+ if (telemetryCompleted) return;
6264
+ telemetryCompleted = true;
6265
+ emitProcessCompleted2({
6266
+ ...pid !== void 0 ? { pid } : {},
6267
+ exitCode,
6268
+ ...signal ? { signal } : {},
6269
+ durationMs: Date.now() - startedAt,
6270
+ stdoutBytes,
6271
+ stderrBytes,
6272
+ timedOut,
6273
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
6274
+ });
6275
+ };
6165
6276
  if (typeof pid === "number") {
6166
6277
  registry.register({
6167
6278
  pid,
@@ -6263,8 +6374,11 @@ var bashTool = {
6263
6374
  child.stderr?.resume();
6264
6375
  }
6265
6376
  };
6266
- const onData = (chunk) => {
6377
+ const onData = (chunk, stream) => {
6267
6378
  const text = chunk.toString();
6379
+ if (stream === "stdout") stdoutBytes += chunk.byteLength;
6380
+ else stderrBytes += chunk.byteLength;
6381
+ emitProcessOutput2({ pid, stream, chunk });
6268
6382
  if (buf.length < MAX_OUTPUT) {
6269
6383
  buf += text.slice(0, MAX_OUTPUT - buf.length);
6270
6384
  }
@@ -6273,17 +6387,21 @@ var bashTool = {
6273
6387
  push({ kind: "data", text });
6274
6388
  pauseIfFlooded();
6275
6389
  };
6276
- child.stdout?.on("data", onData);
6277
- child.stderr?.on("data", onData);
6390
+ const onStdoutData = (chunk) => onData(chunk, "stdout");
6391
+ const onStderrData = (chunk) => onData(chunk, "stderr");
6392
+ child.stdout?.on("data", onStdoutData);
6393
+ child.stderr?.on("data", onStderrData);
6278
6394
  child.on("error", (err) => {
6279
6395
  for (const t of timers) clearTimeout(t);
6280
6396
  registry.afterCall(Date.now() - startedAt, true);
6397
+ completeForeground(1);
6281
6398
  push({ kind: "error", err });
6282
6399
  });
6283
- child.on("close", (code) => {
6400
+ child.on("close", (code, signal) => {
6284
6401
  for (const t of timers) clearTimeout(t);
6285
6402
  if (typeof pid === "number") registry.unregister(pid);
6286
6403
  registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
6404
+ completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
6287
6405
  push({ kind: "end", code });
6288
6406
  });
6289
6407
  try {
@@ -6328,8 +6446,8 @@ ${hint}` : ""),
6328
6446
  for (const t of timers) clearTimeout(t);
6329
6447
  spool.finalize();
6330
6448
  if (isWin4) opts.signal.removeEventListener("abort", onAbort);
6331
- child.stdout?.off("data", onData);
6332
- child.stderr?.off("data", onData);
6449
+ child.stdout?.off("data", onStdoutData);
6450
+ child.stderr?.off("data", onStderrData);
6333
6451
  child.stdout?.destroy();
6334
6452
  child.stderr?.destroy();
6335
6453
  if (child.exitCode === null && !child.killed) {
@@ -7825,7 +7943,7 @@ import * as path13 from "node:path";
7825
7943
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
7826
7944
 
7827
7945
  // src/codebase-index/schema.ts
7828
- var SCHEMA_VERSION = 2;
7946
+ var SCHEMA_VERSION = 3;
7829
7947
 
7830
7948
  // src/codebase-index/lsp-kind.ts
7831
7949
  function lspKindToInternalKind(k) {
@@ -7868,7 +7986,7 @@ function tokenise(text) {
7868
7986
  return sanitised.toLowerCase().split(" ").filter(Boolean);
7869
7987
  }
7870
7988
  function splitName(name) {
7871
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
7989
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
7872
7990
  }
7873
7991
  function buildIndexableText(name, signature, docComment) {
7874
7992
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -7894,14 +8012,12 @@ function buildBm25Index(docs) {
7894
8012
  return new Bm25Index(documents, df, N, avgLen);
7895
8013
  }
7896
8014
  var Bm25Index = class {
7897
- constructor(documents, df, N, avgLen) {
8015
+ constructor(documents, _df, N, avgLen) {
7898
8016
  this.documents = documents;
7899
- this.df = df;
7900
8017
  this.N = N;
7901
8018
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
7902
8019
  }
7903
8020
  documents;
7904
- df;
7905
8021
  N;
7906
8022
  safeAvgLen;
7907
8023
  score(query, filter) {
@@ -7914,10 +8030,13 @@ var Bm25Index = class {
7914
8030
  for (const qTerm of qTokens) {
7915
8031
  let tf = 0;
7916
8032
  for (const t of doc.tokens) {
7917
- if (t === qTerm) tf++;
8033
+ if (t.startsWith(qTerm)) tf++;
7918
8034
  }
7919
8035
  if (tf === 0) continue;
7920
- const dfVal = this.df[qTerm] ?? 0;
8036
+ let dfVal = 0;
8037
+ for (const candidate of this.documents) {
8038
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
8039
+ }
7921
8040
  if (dfVal === 0) continue;
7922
8041
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
7923
8042
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -7950,6 +8069,29 @@ var Bm25Index = class {
7950
8069
 
7951
8070
  // src/codebase-index/writer.ts
7952
8071
  var DB_FILE = "index.db";
8072
+ function escapeLike(value) {
8073
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
8074
+ }
8075
+ function assignRefsToSymbols(refs, symbols) {
8076
+ if (refs.length === 0 || symbols.length === 0) return [];
8077
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
8078
+ const seen = /* @__PURE__ */ new Set();
8079
+ const assigned = [];
8080
+ for (const ref of refs) {
8081
+ let owner2;
8082
+ for (const symbol of ordered) {
8083
+ if (symbol.line > ref.line) break;
8084
+ owner2 = symbol;
8085
+ }
8086
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
8087
+ if (!owner2 || owner2.id <= 0) continue;
8088
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
8089
+ if (seen.has(key)) continue;
8090
+ seen.add(key);
8091
+ assigned.push({ ...ref, fromId: owner2.id });
8092
+ }
8093
+ return assigned;
8094
+ }
7953
8095
  function resolveIndexDir(projectRoot, override) {
7954
8096
  return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
7955
8097
  }
@@ -7957,6 +8099,53 @@ function codebaseIndexDirOverride(ctx) {
7957
8099
  const v = ctx.meta?.["codebaseIndexDir"];
7958
8100
  return typeof v === "string" ? v : void 0;
7959
8101
  }
8102
+ var StorePool = class {
8103
+ stores = /* @__PURE__ */ new Map();
8104
+ key(projectRoot, indexDir) {
8105
+ return `${projectRoot}\0${indexDir ?? ""}`;
8106
+ }
8107
+ /** Borrow a store. Creates it on first access for this key. */
8108
+ acquire(projectRoot, opts) {
8109
+ const k = this.key(projectRoot, opts?.indexDir);
8110
+ let store = this.stores.get(k);
8111
+ if (!store) {
8112
+ store = new IndexStore(projectRoot, { indexDir: opts?.indexDir });
8113
+ this.stores.set(k, store);
8114
+ }
8115
+ return store;
8116
+ }
8117
+ /** Return the store to the pool. The connection stays warm for subsequent
8118
+ * operations on the same (projectRoot, indexDir). */
8119
+ release(_store) {
8120
+ }
8121
+ /** Close every pooled connection and drain the pool. Call on shutdown. */
8122
+ closeAll() {
8123
+ for (const store of this.stores.values()) {
8124
+ try {
8125
+ store.close();
8126
+ } catch {
8127
+ }
8128
+ }
8129
+ this.stores.clear();
8130
+ }
8131
+ /** Remove one store from the pool. Used by tests that need isolation. */
8132
+ evict(projectRoot, indexDir) {
8133
+ const k = this.key(projectRoot, indexDir);
8134
+ const store = this.stores.get(k);
8135
+ if (store) {
8136
+ try {
8137
+ store.close();
8138
+ } catch {
8139
+ }
8140
+ this.stores.delete(k);
8141
+ }
8142
+ }
8143
+ /** True when the pool holds a connection for the given key. */
8144
+ has(projectRoot, indexDir) {
8145
+ return this.stores.has(this.key(projectRoot, indexDir));
8146
+ }
8147
+ };
8148
+ var indexStorePool = new StorePool();
7960
8149
  var warningSilenced = false;
7961
8150
  function silenceSqliteExperimentalWarning() {
7962
8151
  if (warningSilenced) return;
@@ -8003,7 +8192,7 @@ function sleepSync(ms) {
8003
8192
  } catch {
8004
8193
  }
8005
8194
  }
8006
- var IndexStore = class {
8195
+ var IndexStore = class _IndexStore {
8007
8196
  db;
8008
8197
  /** Absolute path to this project's index directory. */
8009
8198
  indexDir;
@@ -8055,10 +8244,7 @@ var IndexStore = class {
8055
8244
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
8056
8245
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
8057
8246
  }
8058
- const delay = Math.min(
8059
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
8060
- LOCK_RETRY_MAX_DELAY_MS
8061
- );
8247
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
8062
8248
  sleepSync(delay);
8063
8249
  }
8064
8250
  }
@@ -8140,8 +8326,24 @@ var IndexStore = class {
8140
8326
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
8141
8327
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
8142
8328
  try {
8143
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
8329
+ this.db.exec(
8330
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
8331
+ );
8144
8332
  this.ftsAvailable = true;
8333
+ const symbolCount = Number(
8334
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8335
+ );
8336
+ const ftsCount = Number(
8337
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8338
+ );
8339
+ if (symbolCount !== ftsCount) {
8340
+ this.db.exec("DELETE FROM symbols_fts");
8341
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
8342
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8343
+ for (const row of rows) {
8344
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
8345
+ }
8346
+ }
8145
8347
  } catch {
8146
8348
  this.ftsAvailable = false;
8147
8349
  }
@@ -8161,7 +8363,7 @@ var IndexStore = class {
8161
8363
  return this.runWithRetry(() => {
8162
8364
  this.db.exec("BEGIN IMMEDIATE");
8163
8365
  try {
8164
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8366
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8165
8367
  let nextId = (maxRows[0]?.m ?? 0) + 1;
8166
8368
  const stmt = this.db.prepare(
8167
8369
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -8199,7 +8401,9 @@ var IndexStore = class {
8199
8401
  deleteSymbolsForFile(file) {
8200
8402
  this.runWithRetry(() => {
8201
8403
  if (this.ftsAvailable) {
8202
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8404
+ this.stmt(
8405
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8406
+ ).run(file);
8203
8407
  }
8204
8408
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
8205
8409
  });
@@ -8211,9 +8415,21 @@ var IndexStore = class {
8211
8415
  */
8212
8416
  deleteFile(file) {
8213
8417
  this.runWithRetry(() => {
8214
- this.deleteRefsForFile(file);
8215
- this.deleteSymbolsForFile(file);
8216
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
8418
+ this.db.exec("BEGIN IMMEDIATE");
8419
+ try {
8420
+ if (this.ftsAvailable) {
8421
+ this.db.prepare(
8422
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8423
+ ).run(file);
8424
+ }
8425
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8426
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
8427
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
8428
+ this.db.exec("COMMIT");
8429
+ } catch (err) {
8430
+ this.db.exec("ROLLBACK");
8431
+ throw err;
8432
+ }
8217
8433
  });
8218
8434
  }
8219
8435
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -8236,14 +8452,22 @@ var IndexStore = class {
8236
8452
  ).all(file);
8237
8453
  if (!rows.length) return null;
8238
8454
  const r = expectDefined2(rows[0]);
8239
- return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
8455
+ return {
8456
+ file: r.file,
8457
+ lang: r.lang,
8458
+ mtimeMs: r.mtime_ms,
8459
+ symbolCount: r.symbol_count,
8460
+ lastIndexed: r.last_indexed
8461
+ };
8240
8462
  }
8241
8463
  getAllFileMetas() {
8242
- return this.db.prepare(
8243
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
8244
- ).all().map(
8245
- (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
8246
- );
8464
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8465
+ file: r.file,
8466
+ lang: r.lang,
8467
+ mtimeMs: r.mtime_ms,
8468
+ symbolCount: r.symbol_count,
8469
+ lastIndexed: r.last_indexed
8470
+ }));
8247
8471
  }
8248
8472
  // ─── Search ──────────────────────────────────────────────────────────────────
8249
8473
  search(query, filter) {
@@ -8267,8 +8491,8 @@ var IndexStore = class {
8267
8491
  values.push(filter.lang);
8268
8492
  }
8269
8493
  if (filter?.file) {
8270
- conditions.push("file LIKE ?");
8271
- values.push(`%${filter.file}%`);
8494
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
8495
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8272
8496
  }
8273
8497
  if (query.trim()) {
8274
8498
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -8306,9 +8530,11 @@ var IndexStore = class {
8306
8530
  * "users", camelCase-split text makes "complex" find "complexOperation").
8307
8531
  */
8308
8532
  searchRanked(query, filter, limit) {
8533
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
8534
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
8309
8535
  const tokens = tokenise(query);
8310
8536
  if (tokens.length === 0 || !this.ftsAvailable) {
8311
- return this.searchRankedFallback(query, filter, limit);
8537
+ return this.searchRankedFallback(query, filter, safeLimit);
8312
8538
  }
8313
8539
  let effectiveKind = filter?.kind;
8314
8540
  if (filter?.lspKind !== void 0) {
@@ -8328,11 +8554,13 @@ var IndexStore = class {
8328
8554
  values.push(filter.lang);
8329
8555
  }
8330
8556
  if (filter?.file) {
8331
- conditions.push("s.file LIKE ?");
8332
- values.push(`%${filter.file}%`);
8557
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
8558
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8333
8559
  }
8334
8560
  const where = conditions.join(" AND ");
8335
- const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
8561
+ const countRows = this.db.prepare(
8562
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8563
+ ).all(...values);
8336
8564
  const total = countRows[0] ? Number(countRows[0].n) : 0;
8337
8565
  if (total === 0) return { results: [], total: 0 };
8338
8566
  const rows = this.db.prepare(
@@ -8341,9 +8569,13 @@ var IndexStore = class {
8341
8569
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
8342
8570
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
8343
8571
  WHERE ${where}
8344
- ORDER BY bm25(symbols_fts)
8572
+ ORDER BY
8573
+ CASE WHEN lower(s.name) = lower(?) THEN 0
8574
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
8575
+ ELSE 2 END,
8576
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
8345
8577
  LIMIT ?`
8346
- ).all(...values, limit);
8578
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
8347
8579
  return {
8348
8580
  results: rows.map((r) => ({
8349
8581
  id: r.id,
@@ -8373,10 +8605,28 @@ var IndexStore = class {
8373
8605
  }
8374
8606
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8375
8607
  const bm25 = buildBm25Index(
8376
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
8608
+ candidates.map((c) => ({
8609
+ id: c.id,
8610
+ text: buildIndexableText(c.name, c.signature, c.docComment)
8611
+ }))
8377
8612
  );
8378
8613
  const scored = bm25.score(query, (id) => candidateById.has(id));
8379
- scored.sort((a, b) => b.score - a.score);
8614
+ const q = query.trim().toLowerCase();
8615
+ const rank = (id) => {
8616
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
8617
+ if (name === q) return 0;
8618
+ if (name.startsWith(q)) return 1;
8619
+ return 2;
8620
+ };
8621
+ scored.sort((a, b) => {
8622
+ const rankDiff = rank(a.id) - rank(b.id);
8623
+ if (rankDiff !== 0) return rankDiff;
8624
+ const scoreDiff = b.score - a.score;
8625
+ if (scoreDiff !== 0) return scoreDiff;
8626
+ const left = expectDefined2(candidateById.get(a.id));
8627
+ const right = expectDefined2(candidateById.get(b.id));
8628
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
8629
+ });
8380
8630
  const qTokens = tokenise(query);
8381
8631
  const results = scored.slice(0, limit).map(({ id, score }) => {
8382
8632
  const c = expectDefined2(candidateById.get(id));
@@ -8385,9 +8635,7 @@ var IndexStore = class {
8385
8635
  return { results, total: candidates.length };
8386
8636
  }
8387
8637
  getAllIndexable() {
8388
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
8389
- ({ id, text }) => ({ id, text })
8390
- );
8638
+ return this.stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8391
8639
  }
8392
8640
  /**
8393
8641
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -8397,28 +8645,22 @@ var IndexStore = class {
8397
8645
  * `symbols.id`). Ids may have gaps — that is fine.
8398
8646
  */
8399
8647
  getMaxSymbolId() {
8400
- const rows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8648
+ const rows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8401
8649
  return rows[0]?.m ?? 0;
8402
8650
  }
8403
8651
  // ─── Stats ───────────────────────────────────────────────────────────────────
8404
8652
  getStats() {
8405
8653
  const sizeBytes = this.sizeBytes();
8406
- const lastRows = this.db.prepare(
8407
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
8408
- ).all();
8654
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8409
8655
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8410
- const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
8656
+ const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
8411
8657
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
8412
- const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
8658
+ const fileRows = this.stmt("SELECT COUNT(*) FROM files").all();
8413
8659
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
8414
- const langRows = this.db.prepare(
8415
- "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
8416
- ).all();
8660
+ const langRows = this.stmt("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8417
8661
  const byLang = {};
8418
8662
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
8419
- const kindRows = this.db.prepare(
8420
- "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
8421
- ).all();
8663
+ const kindRows = this.stmt("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8422
8664
  const byKind = {};
8423
8665
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
8424
8666
  return {
@@ -8434,17 +8676,34 @@ var IndexStore = class {
8434
8676
  }
8435
8677
  setLastIndexed(ts2) {
8436
8678
  this.runWithRetry(() => {
8437
- this.db.prepare(
8438
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
8439
- ).run(String(ts2));
8679
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8680
+ });
8681
+ }
8682
+ getMetadata(key) {
8683
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
8684
+ return rows[0]?.value;
8685
+ }
8686
+ setMetadata(key, value) {
8687
+ this.runWithRetry(() => {
8688
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8440
8689
  });
8441
8690
  }
8442
8691
  clearAll() {
8443
8692
  this.runWithRetry(() => {
8444
- this.db.exec("DELETE FROM symbols");
8445
- this.db.exec("DELETE FROM files");
8446
- this.db.exec("DELETE FROM refs");
8447
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8693
+ this.db.exec("BEGIN IMMEDIATE");
8694
+ try {
8695
+ this.db.exec("DROP TABLE IF EXISTS refs");
8696
+ this.db.exec("DROP TABLE IF EXISTS symbols");
8697
+ this.db.exec("DROP TABLE IF EXISTS files");
8698
+ this.db.exec("DROP TABLE IF EXISTS metadata");
8699
+ if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
8700
+ this.db.exec("COMMIT");
8701
+ this.stmtCache.clear();
8702
+ this.initSchema();
8703
+ } catch (err) {
8704
+ this.db.exec("ROLLBACK");
8705
+ throw err;
8706
+ }
8448
8707
  });
8449
8708
  }
8450
8709
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -8525,7 +8784,7 @@ var IndexStore = class {
8525
8784
  ).run(...options.deleteForFiles);
8526
8785
  this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8527
8786
  }
8528
- const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
8787
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8529
8788
  let nextId = (maxRows[0]?.m ?? 0) + 1;
8530
8789
  const symStmt = this.db.prepare(
8531
8790
  `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
@@ -8535,6 +8794,7 @@ var IndexStore = class {
8535
8794
  const allInserted = [];
8536
8795
  const refsToInsert = [];
8537
8796
  for (const entry of entries) {
8797
+ const insertedForEntry = [];
8538
8798
  for (const s of entry.symbols) {
8539
8799
  const id = nextId++;
8540
8800
  symStmt.run(
@@ -8552,9 +8812,11 @@ var IndexStore = class {
8552
8812
  s.file
8553
8813
  );
8554
8814
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8555
- allInserted.push({ ...s, id });
8815
+ const inserted = { ...s, id };
8816
+ allInserted.push(inserted);
8817
+ insertedForEntry.push(inserted);
8556
8818
  }
8557
- for (const r of entry.refs) refsToInsert.push(r);
8819
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8558
8820
  }
8559
8821
  if (refsToInsert.length > 0) {
8560
8822
  const refStmt = this.db.prepare(
@@ -8592,9 +8854,7 @@ var IndexStore = class {
8592
8854
  */
8593
8855
  deleteRefsForFile(file) {
8594
8856
  this.runWithRetry(() => {
8595
- this.db.prepare(
8596
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
8597
- ).run(file);
8857
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8598
8858
  });
8599
8859
  }
8600
8860
  /**
@@ -8635,9 +8895,7 @@ var IndexStore = class {
8635
8895
  * Find all references FROM a given symbol (what does this symbol call/use?).
8636
8896
  */
8637
8897
  findRefsFrom(symbolId) {
8638
- return this.db.prepare(
8639
- "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
8640
- ).all(symbolId).map((r) => ({
8898
+ return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
8641
8899
  id: r.id,
8642
8900
  fromId: r.from_id,
8643
8901
  toName: r.to_name,
@@ -8654,6 +8912,356 @@ var IndexStore = class {
8654
8912
  return 0;
8655
8913
  }
8656
8914
  }
8915
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
8916
+ /**
8917
+ * Derive a monorepo package name from an absolute file path.
8918
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
8919
+ * Returns `undefined` for files outside any recognisable package structure.
8920
+ */
8921
+ static derivePackage(filePath) {
8922
+ const f = filePath.replace(/\\/g, "/");
8923
+ const pkgsIdx = f.indexOf("/packages/");
8924
+ if (pkgsIdx !== -1) {
8925
+ const rest = f.slice(pkgsIdx + "/packages/".length);
8926
+ const seg = rest.split("/")[0];
8927
+ return seg ? `@wrongstack/${seg}` : void 0;
8928
+ }
8929
+ const appsIdx = f.indexOf("/apps/");
8930
+ if (appsIdx !== -1) {
8931
+ const rest = f.slice(appsIdx + "/apps/".length);
8932
+ const seg = rest.split("/")[0];
8933
+ return seg ? `app:${seg}` : void 0;
8934
+ }
8935
+ return void 0;
8936
+ }
8937
+ static packageFromImport(moduleName) {
8938
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
8939
+ const parts = moduleName.split("/");
8940
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
8941
+ }
8942
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
8943
+ if (!moduleName.startsWith(".")) return void 0;
8944
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
8945
+ const absolute = path13.posix.normalize(
8946
+ path13.posix.join(path13.posix.dirname(normalizedFrom), moduleName)
8947
+ );
8948
+ const extension = path13.posix.extname(absolute);
8949
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
8950
+ const candidates = [
8951
+ absolute,
8952
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
8953
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(absolute, `index${ext}`)),
8954
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(base, `index${ext}`))
8955
+ ];
8956
+ const indexedByPortablePath = new Map(
8957
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
8958
+ );
8959
+ for (const candidate of candidates) {
8960
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
8961
+ if (indexed) return indexed;
8962
+ }
8963
+ return void 0;
8964
+ }
8965
+ /**
8966
+ * Package-level graph: each workspace package is a node; edges are derived
8967
+ * from cross-package symbol references (a symbol in package A references a
8968
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
8969
+ */
8970
+ getPackageGraph() {
8971
+ const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
8972
+ const pkgNodes = /* @__PURE__ */ new Map();
8973
+ const fileToPkg = /* @__PURE__ */ new Map();
8974
+ const symbolToPkg = /* @__PURE__ */ new Map();
8975
+ for (const s of symbols) {
8976
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
8977
+ fileToPkg.set(s.file, pkg);
8978
+ symbolToPkg.set(s.id, pkg);
8979
+ const node = pkgNodes.get(pkg);
8980
+ if (node) {
8981
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
8982
+ } else {
8983
+ pkgNodes.set(pkg, {
8984
+ id: `pkg:${pkg}`,
8985
+ label: pkg,
8986
+ kind: "package",
8987
+ package: pkg,
8988
+ symbolCount: 1,
8989
+ fileCount: 0
8990
+ });
8991
+ }
8992
+ }
8993
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
8994
+ for (const { file } of files) {
8995
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
8996
+ const node = pkgNodes.get(pkg);
8997
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
8998
+ }
8999
+ const refRows = this.db.prepare(
9000
+ `SELECT r.from_id, r.to_id, r.call_type
9001
+ FROM refs r
9002
+ WHERE r.to_id IS NOT NULL`
9003
+ ).all();
9004
+ const edgeMap = /* @__PURE__ */ new Map();
9005
+ for (const r of refRows) {
9006
+ if (r.call_type === "import") continue;
9007
+ const fromPkg = symbolToPkg.get(r.from_id);
9008
+ const toPkg = symbolToPkg.get(r.to_id);
9009
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
9010
+ const key = `${fromPkg}\0${toPkg}`;
9011
+ let e = edgeMap.get(key);
9012
+ if (!e) {
9013
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
9014
+ edgeMap.set(key, e);
9015
+ }
9016
+ e.weight++;
9017
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9018
+ }
9019
+ const importRows = this.db.prepare(
9020
+ `SELECT r.from_id, r.to_name
9021
+ FROM refs r
9022
+ WHERE r.call_type = 'import'`
9023
+ ).all();
9024
+ for (const r of importRows) {
9025
+ const fromPkg = symbolToPkg.get(r.from_id);
9026
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
9027
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
9028
+ const key = `${fromPkg}\0${toPkg}`;
9029
+ let edge = edgeMap.get(key);
9030
+ if (!edge) {
9031
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
9032
+ edgeMap.set(key, edge);
9033
+ }
9034
+ edge.weight++;
9035
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
9036
+ }
9037
+ const edges = [];
9038
+ for (const [key, e] of edgeMap) {
9039
+ const [source, target] = key.split("\0");
9040
+ let bestType = "call";
9041
+ let bestCount = 0;
9042
+ for (const [t, c] of e.types) {
9043
+ if (c > bestCount) {
9044
+ bestType = t;
9045
+ bestCount = c;
9046
+ }
9047
+ }
9048
+ edges.push({
9049
+ source: `pkg:${source}`,
9050
+ target: `pkg:${target}`,
9051
+ weight: e.weight,
9052
+ refType: bestType
9053
+ });
9054
+ }
9055
+ return { nodes: [...pkgNodes.values()], edges };
9056
+ }
9057
+ /**
9058
+ * File-level graph for a single package: each file is a node; edges are
9059
+ * derived from cross-file symbol references within the package.
9060
+ */
9061
+ getFileGraph(packageFilter) {
9062
+ const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
9063
+ const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
9064
+ const localFiles = new Set(pkgFilePaths);
9065
+ if (localFiles.size === 0) return { nodes: [], edges: [] };
9066
+ const filePlaceholders = [...localFiles].map(() => "?").join(",");
9067
+ const pkgSyms = this.db.prepare(
9068
+ `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
9069
+ ).all(...pkgFilePaths);
9070
+ const fileNodes = /* @__PURE__ */ new Map();
9071
+ const symToFile = /* @__PURE__ */ new Map();
9072
+ const fileStats = /* @__PURE__ */ new Map();
9073
+ for (const s of pkgSyms) {
9074
+ symToFile.set(s.id, s.file);
9075
+ const current = fileStats.get(s.file);
9076
+ fileStats.set(s.file, {
9077
+ count: (current?.count ?? 0) + 1,
9078
+ lang: current?.lang ?? s.lang
9079
+ });
9080
+ }
9081
+ const ensureFileNode = (file) => {
9082
+ if (fileNodes.has(file)) return;
9083
+ const stats = fileStats.get(file);
9084
+ fileNodes.set(file, {
9085
+ id: `file:${file}`,
9086
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
9087
+ kind: "file",
9088
+ package: _IndexStore.derivePackage(file) ?? "(root)",
9089
+ file,
9090
+ symbolCount: stats?.count ?? 0,
9091
+ lang: stats?.lang,
9092
+ external: !localFiles.has(file)
9093
+ });
9094
+ };
9095
+ for (const file of localFiles) {
9096
+ ensureFileNode(file);
9097
+ }
9098
+ const indexedFiles = new Set(allFiles.map((f) => f.file));
9099
+ const refRows = this.db.prepare(
9100
+ `SELECT r.from_id, r.to_id, r.call_type
9101
+ FROM refs r
9102
+ WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
9103
+ OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
9104
+ AND r.to_id IS NOT NULL`
9105
+ ).all(...pkgFilePaths, ...pkgFilePaths);
9106
+ const knownSymIds = new Set(pkgSyms.map((s) => s.id));
9107
+ const crossRefIds = /* @__PURE__ */ new Set();
9108
+ for (const r of refRows) {
9109
+ if (!knownSymIds.has(r.from_id)) crossRefIds.add(r.from_id);
9110
+ if (!knownSymIds.has(r.to_id)) crossRefIds.add(r.to_id);
9111
+ }
9112
+ if (crossRefIds.size > 0) {
9113
+ const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
9114
+ const extras = this.db.prepare(
9115
+ `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
9116
+ ).all(...crossRefIds);
9117
+ for (const x of extras) {
9118
+ symToFile.set(x.id, x.file);
9119
+ if (!fileStats.has(x.file)) {
9120
+ fileStats.set(x.file, { count: 0, lang: "ts" });
9121
+ }
9122
+ }
9123
+ }
9124
+ const edgeMap = /* @__PURE__ */ new Map();
9125
+ for (const r of refRows) {
9126
+ if (r.call_type === "import") continue;
9127
+ const fromFile = symToFile.get(r.from_id);
9128
+ const toFile = symToFile.get(r.to_id);
9129
+ if (!fromFile || !toFile || fromFile === toFile) continue;
9130
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
9131
+ ensureFileNode(fromFile);
9132
+ ensureFileNode(toFile);
9133
+ const key = `${fromFile}\0${toFile}`;
9134
+ let e = edgeMap.get(key);
9135
+ if (!e) {
9136
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
9137
+ edgeMap.set(key, e);
9138
+ }
9139
+ e.weight++;
9140
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9141
+ }
9142
+ const importRows = this.db.prepare(
9143
+ `SELECT r.from_id, r.to_name
9144
+ FROM refs r
9145
+ WHERE r.call_type = 'import'
9146
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
9147
+ ).all(...pkgFilePaths);
9148
+ for (const r of importRows) {
9149
+ const fromFile = symToFile.get(r.from_id);
9150
+ if (!fromFile || !localFiles.has(fromFile)) continue;
9151
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
9152
+ if (!toFile || fromFile === toFile) continue;
9153
+ ensureFileNode(fromFile);
9154
+ ensureFileNode(toFile);
9155
+ const key = `${fromFile}\0${toFile}`;
9156
+ let edge = edgeMap.get(key);
9157
+ if (!edge) {
9158
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
9159
+ edgeMap.set(key, edge);
9160
+ }
9161
+ edge.weight++;
9162
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
9163
+ }
9164
+ const edges = [];
9165
+ for (const [key, e] of edgeMap) {
9166
+ const [source, target] = key.split("\0");
9167
+ let bestType = "call";
9168
+ let bestCount = 0;
9169
+ for (const [t, c] of e.types) {
9170
+ if (c > bestCount) {
9171
+ bestType = t;
9172
+ bestCount = c;
9173
+ }
9174
+ }
9175
+ edges.push({
9176
+ source: `file:${source}`,
9177
+ target: `file:${target}`,
9178
+ weight: e.weight,
9179
+ refType: bestType
9180
+ });
9181
+ }
9182
+ return { nodes: [...fileNodes.values()], edges };
9183
+ }
9184
+ /**
9185
+ * Symbol-level graph for a single file: each symbol is a node; edges are
9186
+ * derived from intra-file and cross-file symbol references (who calls whom).
9187
+ */
9188
+ getSymbolGraph(fileFilter) {
9189
+ const syms = this.db.prepare(
9190
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
9191
+ ).all(fileFilter);
9192
+ if (syms.length === 0) return { nodes: [], edges: [] };
9193
+ const symById = new Map(syms.map((symbol) => [symbol.id, symbol]));
9194
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
9195
+ const toGraphNode = (s) => ({
9196
+ id: `sym:${s.id}`,
9197
+ label: s.name,
9198
+ kind: "symbol",
9199
+ symbolId: s.id,
9200
+ symbolKind: s.kind,
9201
+ file: s.file,
9202
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
9203
+ lang: s.lang,
9204
+ line: s.line,
9205
+ signature: s.signature,
9206
+ scope: s.scope,
9207
+ external: s.file !== fileFilter
9208
+ });
9209
+ const symIds = new Set(syms.map((s) => s.id));
9210
+ const refRows = this.db.prepare(
9211
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9212
+ FROM refs r
9213
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
9214
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
9215
+ ).all(fileFilter, fileFilter);
9216
+ const edgeMap = /* @__PURE__ */ new Map();
9217
+ for (const r of refRows) {
9218
+ if (r.to_id === null) continue;
9219
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
9220
+ relatedIds.add(r.from_id);
9221
+ relatedIds.add(r.to_id);
9222
+ const key = `${r.from_id}\0${r.to_id}`;
9223
+ let e = edgeMap.get(key);
9224
+ if (!e) {
9225
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
9226
+ edgeMap.set(key, e);
9227
+ }
9228
+ e.weight++;
9229
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9230
+ }
9231
+ const edges = [];
9232
+ for (const [key, e] of edgeMap) {
9233
+ const [fromId, toId] = key.split("\0").map(Number);
9234
+ let bestType = "call";
9235
+ let bestCount = 0;
9236
+ for (const [t, c] of e.types) {
9237
+ if (c > bestCount) {
9238
+ bestType = t;
9239
+ bestCount = c;
9240
+ }
9241
+ }
9242
+ edges.push({
9243
+ source: `sym:${fromId}`,
9244
+ target: `sym:${toId}`,
9245
+ weight: e.weight,
9246
+ refType: bestType
9247
+ });
9248
+ }
9249
+ const loadedIds = new Set(syms.map((s) => s.id));
9250
+ const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
9251
+ if (missingIds.length > 0) {
9252
+ const placeholders = missingIds.map(() => "?").join(",");
9253
+ const extras = this.db.prepare(
9254
+ `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
9255
+ ).all(...missingIds);
9256
+ for (const s of extras) symById.set(s.id, s);
9257
+ }
9258
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
9259
+ const aExternal = a.file === fileFilter ? 0 : 1;
9260
+ const bExternal = b.file === fileFilter ? 0 : 1;
9261
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
9262
+ }).map(toGraphNode);
9263
+ return { nodes, edges };
9264
+ }
8657
9265
  close() {
8658
9266
  this.stmtCache.clear();
8659
9267
  try {
@@ -8672,6 +9280,7 @@ import { Worker } from "node:worker_threads";
8672
9280
  import { expectDefined as expectDefined6 } from "@wrongstack/core";
8673
9281
  import * as fs14 from "node:fs/promises";
8674
9282
  import * as path19 from "node:path";
9283
+ import { availableParallelism } from "node:os";
8675
9284
  import { compileGlob as compileGlob2 } from "@wrongstack/core";
8676
9285
 
8677
9286
  // src/codebase-index/ts-parser.ts
@@ -8728,8 +9337,7 @@ function extToLang(ext) {
8728
9337
  return null;
8729
9338
  }
8730
9339
  }
8731
- function getSignature(node, sourceFile) {
8732
- const printer = ts.createPrinter({});
9340
+ function getSignature(printer, node, sourceFile) {
8733
9341
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
8734
9342
  return raw.replace(/\s+/g, " ").slice(0, 500);
8735
9343
  }
@@ -8748,20 +9356,14 @@ function getJsDoc(node, sourceFile) {
8748
9356
  }
8749
9357
  return "";
8750
9358
  }
8751
- function buildScope(node) {
8752
- const parts = [];
8753
- let current = node.parent;
8754
- while (current) {
8755
- if (ts.isClassDeclaration(current) || ts.isInterfaceDeclaration(current) || ts.isEnumDeclaration(current) || ts.isTypeAliasDeclaration(current)) {
8756
- parts.unshift(current.name?.text ?? "Anon");
8757
- } else if (ts.isMethodDeclaration(current) || ts.isGetAccessor(current) || ts.isSetAccessor(current) || ts.isPropertyDeclaration(current) || ts.isFunctionDeclaration(current)) {
8758
- if (current.name && ts.isIdentifier(current.name)) {
8759
- parts.unshift(current.name.text);
8760
- }
9359
+ function pushScopeName(node, parts) {
9360
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
9361
+ parts.push(node.name?.text ?? "Anon");
9362
+ } else if (ts.isMethodDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isFunctionDeclaration(node)) {
9363
+ if (node.name && ts.isIdentifier(node.name)) {
9364
+ parts.push(node.name.text);
8761
9365
  }
8762
- current = current.parent;
8763
9366
  }
8764
- return parts.join(".");
8765
9367
  }
8766
9368
  function parseSymbols(opts) {
8767
9369
  const { file, content, lang } = opts;
@@ -8772,41 +9374,39 @@ function parseSymbols(opts) {
8772
9374
  return { file, lang, symbols: [], mtimeMs: Date.now() };
8773
9375
  }
8774
9376
  const symbols = [];
8775
- function visit(node) {
9377
+ const refs = [];
9378
+ const printer = ts.createPrinter({});
9379
+ function visit(node, funcDepth, scopeParts) {
8776
9380
  const kind = kindOf(node);
8777
9381
  if (kind) {
8778
- const nameNode = node.name;
8779
- if (!nameNode || !ts.isIdentifier(nameNode)) return;
8780
- const name = nameNode.text;
8781
- const pos = nameNode.getStart(sourceFile);
8782
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
8783
- const scope = buildScope(node);
8784
- const signature = getSignature(node, sourceFile);
8785
- const docComment = getJsDoc(node, sourceFile);
8786
- const text = [name, signature, docComment].filter(Boolean).join(" | ");
8787
- symbols.push({
8788
- id: 0,
8789
- lang,
8790
- kind,
8791
- name,
8792
- file,
8793
- line: line + 1,
8794
- col: character,
8795
- signature,
8796
- docComment,
8797
- scope,
8798
- text
8799
- });
9382
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && funcDepth > 0) {
9383
+ } else {
9384
+ const nameNode = node.name;
9385
+ if (!nameNode || !ts.isIdentifier(nameNode)) {
9386
+ return;
9387
+ }
9388
+ const name = nameNode.text;
9389
+ const pos2 = nameNode.getStart(sourceFile);
9390
+ const { line: line2, character } = sourceFile.getLineAndCharacterOfPosition(pos2);
9391
+ const scope = scopeParts.join(".");
9392
+ const signature = getSignature(printer, node, sourceFile);
9393
+ const docComment = getJsDoc(node, sourceFile);
9394
+ const text = [name, signature, docComment].filter(Boolean).join(" | ");
9395
+ symbols.push({
9396
+ id: 0,
9397
+ lang,
9398
+ kind,
9399
+ name,
9400
+ file,
9401
+ line: line2 + 1,
9402
+ col: character,
9403
+ signature,
9404
+ docComment,
9405
+ scope,
9406
+ text
9407
+ });
9408
+ }
8800
9409
  }
8801
- ts.forEachChild(node, visit);
8802
- }
8803
- visit(sourceFile);
8804
- const refs = extractRefs(sourceFile);
8805
- return { file, lang, symbols, refs, mtimeMs: Date.now() };
8806
- }
8807
- function extractRefs(sourceFile) {
8808
- const refs = [];
8809
- function visit(node) {
8810
9410
  const pos = node.getStart(sourceFile);
8811
9411
  const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
8812
9412
  const lineNum = line + 1;
@@ -8831,10 +9431,14 @@ function extractRefs(sourceFile) {
8831
9431
  const moduleName = getModuleName(node);
8832
9432
  if (moduleName) refs.push({ fromId: 0, toName: moduleName, callType: "import", line: lineNum });
8833
9433
  }
8834
- ts.forEachChild(node, visit);
9434
+ const scopeIdx = scopeParts.length;
9435
+ pushScopeName(node, scopeParts);
9436
+ const childFuncDepth = ts.isFunctionLike(node) ? funcDepth + 1 : funcDepth;
9437
+ ts.forEachChild(node, (child) => visit(child, childFuncDepth, scopeParts));
9438
+ scopeParts.length = scopeIdx;
8835
9439
  }
8836
- visit(sourceFile);
8837
- return deduplicateRefs(refs);
9440
+ visit(sourceFile, 0, []);
9441
+ return { file, lang, symbols, refs: deduplicateRefs(refs), mtimeMs: Date.now() };
8838
9442
  }
8839
9443
  function getTypeName(name) {
8840
9444
  if (ts.isIdentifier(name)) return name.text;
@@ -9719,9 +10323,9 @@ function parseSymbols5(opts) {
9719
10323
  function regexParse2(opts) {
9720
10324
  const { file, content, lang } = opts;
9721
10325
  const symbols = [];
9722
- const basename9 = path17.basename(file).toLowerCase();
9723
- const isPackageJson = basename9 === "package.json";
9724
- const isTsconfig = basename9 === "tsconfig.json" || basename9 === "tsconfig.build.json";
10326
+ const basename10 = path17.basename(file).toLowerCase();
10327
+ const isPackageJson = basename10 === "package.json";
10328
+ const isTsconfig = basename10 === "tsconfig.json" || basename10 === "tsconfig.build.json";
9725
10329
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
9726
10330
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
9727
10331
  const lines = content.split("\n");
@@ -10116,16 +10720,14 @@ async function loadGitignoreMatcher(projectRoot) {
10116
10720
 
10117
10721
  // src/codebase-index/indexer.ts
10118
10722
  var YIELD_EVERY_N = 50;
10119
- var PARALLEL_BATCH = 20;
10723
+ var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
10120
10724
  function yieldEventLoop() {
10121
10725
  return new Promise((resolve15) => setImmediate(resolve15));
10122
10726
  }
10123
10727
  function throwIfAborted(signal) {
10124
10728
  if (!signal?.aborted) return;
10125
10729
  if (signal.reason instanceof Error) throw signal.reason;
10126
- throw new Error(
10127
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
10128
- );
10730
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
10129
10731
  }
10130
10732
  function isAbortError(err) {
10131
10733
  return err instanceof DOMException && err.name === "AbortError";
@@ -10141,8 +10743,20 @@ var DEFAULT_IGNORE = [
10141
10743
  "__snapshots__",
10142
10744
  ".nyc_output"
10143
10745
  ];
10746
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10747
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10748
+ function isWithinProject(projectRoot, file) {
10749
+ const rel = path19.relative(projectRoot, file);
10750
+ return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
10751
+ }
10752
+ function isMissingPathError(err) {
10753
+ const code = err?.code;
10754
+ return code === "ENOENT" || code === "ENOTDIR";
10755
+ }
10144
10756
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10145
10757
  const results = [];
10758
+ const errors = [];
10759
+ let complete = true;
10146
10760
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
10147
10761
  const globs = [
10148
10762
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -10166,7 +10780,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10166
10780
  let entries;
10167
10781
  try {
10168
10782
  entries = await fs14.readdir(dir, { withFileTypes: true });
10169
- } catch {
10783
+ } catch (err) {
10784
+ complete = false;
10785
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
10170
10786
  return;
10171
10787
  }
10172
10788
  dirCount++;
@@ -10178,8 +10794,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10178
10794
  if (isGitIgnored(rel, true)) continue;
10179
10795
  await walk2(full);
10180
10796
  } else if (e.isFile()) {
10181
- if (isGitIgnored(rel, false)) continue;
10182
- const ext = path19.extname(e.name);
10797
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
10798
+ const ext = path19.extname(e.name).toLowerCase();
10183
10799
  for (const { ext: extName, pat } of globs) {
10184
10800
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
10185
10801
  results.push(full);
@@ -10190,7 +10806,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10190
10806
  }
10191
10807
  };
10192
10808
  await walk2(projectRoot);
10193
- return results;
10809
+ return { files: results, complete, errors };
10194
10810
  }
10195
10811
  async function parseFile(file, content, lang) {
10196
10812
  switch (lang) {
@@ -10213,6 +10829,26 @@ async function parseFile(file, content, lang) {
10213
10829
  return { file, lang, symbols: [], mtimeMs: Date.now() };
10214
10830
  }
10215
10831
  }
10832
+ function assignRefsToSymbols2(refs, symbols) {
10833
+ if (refs.length === 0 || symbols.length === 0) return [];
10834
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10835
+ const seen = /* @__PURE__ */ new Set();
10836
+ const assigned = [];
10837
+ for (const ref of refs) {
10838
+ let owner2;
10839
+ for (const symbol of ordered) {
10840
+ if (symbol.line > ref.line) break;
10841
+ owner2 = symbol;
10842
+ }
10843
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10844
+ if (!owner2 || owner2.id <= 0) continue;
10845
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10846
+ if (seen.has(key)) continue;
10847
+ seen.add(key);
10848
+ assigned.push({ ...ref, fromId: owner2.id });
10849
+ }
10850
+ return assigned;
10851
+ }
10216
10852
  async function runIndexer(_ctx, opts) {
10217
10853
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
10218
10854
  try {
@@ -10225,7 +10861,9 @@ async function runIndexer(_ctx, opts) {
10225
10861
  }
10226
10862
  }
10227
10863
  async function runIndexerWithStore(store, opts) {
10228
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
10864
+ const { projectRoot, langs, ignore = [], signal } = opts;
10865
+ const relationGraphVersion = "2";
10866
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
10229
10867
  const startMs = Date.now();
10230
10868
  const errors = [];
10231
10869
  const langStats = {};
@@ -10234,10 +10872,18 @@ async function runIndexerWithStore(store, opts) {
10234
10872
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
10235
10873
  let files;
10236
10874
  let discoveredFiles = null;
10875
+ let discoveryComplete = true;
10237
10876
  if (opts.files && opts.files.length > 0) {
10238
- files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
10877
+ files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
10878
+ if (!isWithinProject(projectRoot, f)) return false;
10879
+ const rel = path19.relative(projectRoot, f).replace(/\\/g, "/");
10880
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path19.basename(f)) && !isGitIgnored(rel, false);
10881
+ });
10239
10882
  } else {
10240
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10883
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10884
+ files = discovery.files;
10885
+ errors.push(...discovery.errors);
10886
+ discoveryComplete = discovery.complete;
10241
10887
  discoveredFiles = new Set(files);
10242
10888
  }
10243
10889
  if (langs && langs.length > 0) {
@@ -10252,46 +10898,79 @@ async function runIndexerWithStore(store, opts) {
10252
10898
  if (!force) {
10253
10899
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
10254
10900
  }
10901
+ let filesSinceLastYield = 0;
10255
10902
  for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
10256
10903
  const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
10257
10904
  const batchFiles = files.slice(batchStart, batchEnd);
10258
10905
  opts.onProgress?.(batchEnd, files.length);
10259
- if (batchStart > 0 && batchStart % YIELD_EVERY_N === 0) {
10906
+ filesSinceLastYield += batchFiles.length;
10907
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
10908
+ filesSinceLastYield = 0;
10260
10909
  await yieldEventLoop();
10261
10910
  throwIfAborted(signal);
10262
10911
  }
10263
10912
  const statOpts = signal ? { signal } : {};
10264
10913
  const statReadParse = await Promise.allSettled(
10265
- batchFiles.map(async (file) => {
10266
- let stat18;
10267
- try {
10268
- stat18 = await fs14.stat(file, statOpts);
10269
- } catch (e) {
10270
- if (isAbortError(e)) throw e;
10271
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
10272
- }
10273
- if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
10274
- const lang = detectLang(file);
10275
- if (!lang) return { file, stat: stat18, lang: "", parsed: null };
10276
- const meta = existingMeta.get(file);
10277
- if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
10278
- return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
10279
- }
10280
- let content;
10281
- try {
10282
- content = await fs14.readFile(file, { encoding: "utf8", signal });
10283
- } catch (e) {
10284
- if (isAbortError(e)) throw e;
10285
- return { file, stat: stat18, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
10286
- }
10287
- let parsed;
10288
- try {
10289
- parsed = await parseFile(file, content, lang);
10290
- } catch (e) {
10291
- return { file, stat: stat18, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
10914
+ batchFiles.map(
10915
+ async (file) => {
10916
+ let stat18;
10917
+ try {
10918
+ stat18 = await fs14.stat(file, statOpts);
10919
+ } catch (e) {
10920
+ if (isAbortError(e)) throw e;
10921
+ return {
10922
+ file,
10923
+ stat: null,
10924
+ lang: "",
10925
+ parsed: null,
10926
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
10927
+ missing: isMissingPathError(e)
10928
+ };
10929
+ }
10930
+ if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
10931
+ const lang = detectLang(file);
10932
+ if (!lang) return { file, stat: stat18, lang: "", parsed: null };
10933
+ if (stat18.size > MAX_INDEX_FILE_BYTES) {
10934
+ return {
10935
+ file,
10936
+ stat: stat18,
10937
+ lang,
10938
+ parsed: null,
10939
+ error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
10940
+ };
10941
+ }
10942
+ const meta = existingMeta.get(file);
10943
+ if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
10944
+ return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
10945
+ }
10946
+ let content;
10947
+ try {
10948
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
10949
+ } catch (e) {
10950
+ if (isAbortError(e)) throw e;
10951
+ return {
10952
+ file,
10953
+ stat: stat18,
10954
+ lang,
10955
+ parsed: null,
10956
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
10957
+ };
10958
+ }
10959
+ let parsed;
10960
+ try {
10961
+ parsed = await parseFile(file, content, lang);
10962
+ } catch (e) {
10963
+ return {
10964
+ file,
10965
+ stat: stat18,
10966
+ lang,
10967
+ parsed: null,
10968
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
10969
+ };
10970
+ }
10971
+ return { file, stat: stat18, lang, parsed, content };
10292
10972
  }
10293
- return { file, stat: stat18, lang, parsed, content };
10294
- })
10973
+ )
10295
10974
  );
10296
10975
  const batchEntries = [];
10297
10976
  const deleteForFiles = [];
@@ -10306,8 +10985,8 @@ async function runIndexerWithStore(store, opts) {
10306
10985
  }
10307
10986
  const result = settled.value;
10308
10987
  if (result.error) {
10309
- if (result.stat) store.deleteFile(file);
10310
- if (result.error.includes("error:")) errors.push(result.error);
10988
+ if (result.missing) store.deleteFile(file);
10989
+ errors.push(`${file}: ${result.error}`);
10311
10990
  continue;
10312
10991
  }
10313
10992
  const { stat: stat18, lang, parsed } = result;
@@ -10331,6 +11010,8 @@ async function runIndexerWithStore(store, opts) {
10331
11010
  continue;
10332
11011
  }
10333
11012
  if (parsed.symbols.length === 0) {
11013
+ store.deleteRefsForFile(file);
11014
+ store.deleteSymbolsForFile(file);
10334
11015
  store.upsertFile({
10335
11016
  file,
10336
11017
  lang,
@@ -10341,15 +11022,11 @@ async function runIndexerWithStore(store, opts) {
10341
11022
  filesIndexed++;
10342
11023
  continue;
10343
11024
  }
10344
- const refs = [];
10345
- if (parsed.refs && parsed.refs.length > 0) {
10346
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
10347
- }
10348
11025
  batchEntries.push({
10349
11026
  file,
10350
11027
  lang,
10351
11028
  symbols: parsed.symbols,
10352
- refs,
11029
+ refs: parsed.refs ?? [],
10353
11030
  mtimeMs: Math.floor(stat18.mtimeMs),
10354
11031
  symbolCount: parsed.symbols.length
10355
11032
  });
@@ -10357,34 +11034,12 @@ async function runIndexerWithStore(store, opts) {
10357
11034
  }
10358
11035
  if (batchEntries.length > 0) {
10359
11036
  try {
10360
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
10361
- let cursor = 0;
11037
+ store.commitBatch(batchEntries, { deleteForFiles });
10362
11038
  for (const entry of batchEntries) {
10363
11039
  const count = entry.symbols.length;
10364
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
10365
- cursor += count;
10366
11040
  symbolsIndexed += count;
10367
11041
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
10368
11042
  filesIndexed++;
10369
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
10370
- const refsByLine = /* @__PURE__ */ new Map();
10371
- for (let i = 0; i < symbolsWithIds.length; i++) {
10372
- const sym = symbolsWithIds[i];
10373
- let arr = refsByLine.get(sym.line);
10374
- if (!arr) {
10375
- arr = [];
10376
- refsByLine.set(sym.line, arr);
10377
- }
10378
- arr.push(i);
10379
- }
10380
- for (const ref of entry.refs) {
10381
- const indices = refsByLine.get(ref.line);
10382
- if (indices && indices.length > 0) {
10383
- const idx = indices.shift();
10384
- ref.fromId = symbolsWithIds[idx].id;
10385
- }
10386
- }
10387
- }
10388
11043
  }
10389
11044
  } catch (err) {
10390
11045
  const message = err instanceof Error ? err.message : String(err);
@@ -10398,23 +11053,7 @@ async function runIndexerWithStore(store, opts) {
10398
11053
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
10399
11054
  filesIndexed++;
10400
11055
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
10401
- const refsByLine = /* @__PURE__ */ new Map();
10402
- for (const sym of symbolsWithIds) {
10403
- let arr = refsByLine.get(sym.line);
10404
- if (!arr) {
10405
- arr = [];
10406
- refsByLine.set(sym.line, arr);
10407
- }
10408
- arr.push(sym);
10409
- }
10410
- const fallbackBatch = [];
10411
- for (const ref of entry.refs) {
10412
- const syms = refsByLine.get(ref.line);
10413
- if (syms && syms.length > 0) {
10414
- const sym = syms.shift();
10415
- fallbackBatch.push({ ...ref, fromId: sym.id });
10416
- }
10417
- }
11056
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
10418
11057
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
10419
11058
  }
10420
11059
  store.upsertFile({
@@ -10425,19 +11064,23 @@ async function runIndexerWithStore(store, opts) {
10425
11064
  lastIndexed: Date.now()
10426
11065
  });
10427
11066
  } catch (innerErr) {
10428
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
11067
+ errors.push(
11068
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
11069
+ );
10429
11070
  }
10430
11071
  }
10431
11072
  }
10432
11073
  }
10433
11074
  }
10434
- if (discoveredFiles) {
11075
+ if (discoveredFiles && discoveryComplete) {
10435
11076
  for (const [file_] of existingMeta) {
10436
11077
  if (!discoveredFiles.has(file_)) {
10437
11078
  store.deleteFile(file_);
10438
11079
  }
10439
11080
  }
10440
11081
  }
11082
+ store.resolveRefs();
11083
+ store.setMetadata("relation_graph_version", relationGraphVersion);
10441
11084
  const durationMs = Date.now() - startMs;
10442
11085
  store.setLastIndexed(Date.now());
10443
11086
  return {
@@ -10473,7 +11116,7 @@ async function indexService(args, hooks = {}) {
10473
11116
  });
10474
11117
  }
10475
11118
  function searchService(args) {
10476
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11119
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
10477
11120
  try {
10478
11121
  return store.searchRanked(
10479
11122
  args.query,
@@ -10486,22 +11129,46 @@ function searchService(args) {
10486
11129
  args.limit
10487
11130
  );
10488
11131
  } finally {
10489
- store.close();
11132
+ indexStorePool.release(store);
10490
11133
  }
10491
11134
  }
10492
11135
  function statsService(args) {
10493
- const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11136
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
10494
11137
  try {
10495
11138
  return store.getStats();
10496
11139
  } finally {
10497
- store.close();
11140
+ indexStorePool.release(store);
11141
+ }
11142
+ }
11143
+ function packageGraphService(args) {
11144
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
11145
+ try {
11146
+ return store.getPackageGraph();
11147
+ } finally {
11148
+ indexStorePool.release(store);
11149
+ }
11150
+ }
11151
+ function fileGraphService(args) {
11152
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
11153
+ try {
11154
+ return store.getFileGraph(args.packageFilter);
11155
+ } finally {
11156
+ indexStorePool.release(store);
11157
+ }
11158
+ }
11159
+ function symbolGraphService(args) {
11160
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
11161
+ try {
11162
+ return store.getSymbolGraph(args.fileFilter);
11163
+ } finally {
11164
+ indexStorePool.release(store);
10498
11165
  }
10499
11166
  }
10500
11167
 
10501
11168
  // src/codebase-index/background-indexer.ts
10502
11169
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
10503
11170
  var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
10504
- var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
11171
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
10505
11172
  var _ready = false;
10506
11173
  var _indexing = false;
10507
11174
  var _currentFile = 0;
@@ -10579,14 +11246,22 @@ function ensureWorker() {
10579
11246
  if (!entry) return;
10580
11247
  pending.delete(msg.id);
10581
11248
  if (msg.ok) entry.resolve(msg.result);
10582
- else entry.reject(new Error(msg.error));
11249
+ else {
11250
+ const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
11251
+ if (msg.errorName && msg.errorName !== "Error") {
11252
+ error.name = msg.errorName;
11253
+ }
11254
+ entry.reject(error);
11255
+ }
10583
11256
  });
10584
11257
  w.on("error", (err) => {
11258
+ if (worker !== w) return;
10585
11259
  worker = null;
10586
11260
  failAllPending(err);
10587
11261
  });
10588
11262
  w.on("exit", () => {
10589
- if (worker === w) worker = null;
11263
+ if (worker !== w) return;
11264
+ worker = null;
10590
11265
  failAllPending(new Error("codebase-index worker exited"));
10591
11266
  });
10592
11267
  worker = w;
@@ -10603,14 +11278,28 @@ function terminateWorker(reason) {
10603
11278
  if (w) void w.terminate().catch(() => {
10604
11279
  });
10605
11280
  }
10606
- function shutdownCodebaseIndexHost() {
11281
+ async function shutdownCodebaseIndexHost() {
10607
11282
  cancelPendingReindexes();
10608
- terminateWorker(new Error("codebase-index host shut down"));
11283
+ indexStorePool.closeAll();
11284
+ const w = worker;
11285
+ worker = null;
11286
+ failAllPending(new Error("codebase-index host shut down"));
10609
11287
  workerUnavailable = false;
11288
+ if (w) {
11289
+ try {
11290
+ await w.terminate();
11291
+ } catch {
11292
+ }
11293
+ }
10610
11294
  }
10611
11295
  function callIndexOp(op, args, opts) {
10612
11296
  const w = ensureWorker();
10613
11297
  if (!w) return callInline(op, args, opts);
11298
+ if (opts.signal?.aborted) {
11299
+ return Promise.reject(
11300
+ opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
11301
+ );
11302
+ }
10614
11303
  return new Promise((resolve15, reject) => {
10615
11304
  const id = nextRpcId++;
10616
11305
  const timer = setTimeout(() => {
@@ -10625,8 +11314,7 @@ function callIndexOp(op, args, opts) {
10625
11314
  const onAbort = () => {
10626
11315
  w.postMessage({ type: "cancel", id });
10627
11316
  };
10628
- if (opts.signal?.aborted) onAbort();
10629
- else opts.signal?.addEventListener("abort", onAbort, { once: true });
11317
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
10630
11318
  const cleanup = () => {
10631
11319
  clearTimeout(timer);
10632
11320
  opts.signal?.removeEventListener("abort", onAbort);
@@ -10814,7 +11502,7 @@ var codebaseIndexTool = {
10814
11502
  category: "Project",
10815
11503
  icon: "index",
10816
11504
  description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
10817
- usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
11505
+ usageHint: "CREATE OR REFRESH THE SEARCH INDEX:\n\n- When `codebase-stats` or `codebase-search` reports no persisted index, call this without arguments, then retry the search.\n- Normal and first-time usage: call without arguments for an incremental build.\n- Use `force: true` only for a corrupt/stale index or an explicitly requested clean rebuild.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
10818
11506
  permission: "confirm",
10819
11507
  mutating: true,
10820
11508
  capabilities: ["fs.write.outside-project"],
@@ -10870,12 +11558,14 @@ var codebaseSearchTool = {
10870
11558
  name: "codebase-search",
10871
11559
  category: "Project",
10872
11560
  icon: "index",
10873
- description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Much more powerful and structured than raw `grep` for finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
10874
- usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
11561
+ 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).",
11562
+ usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
10875
11563
  permission: "auto",
10876
11564
  mutating: false,
10877
11565
  capabilities: ["fs.read"],
10878
- timeoutMs: 1e4,
11566
+ // The index host has its own 30s read watchdog. Leave enough headroom for
11567
+ // worker teardown and structured timeout reporting.
11568
+ timeoutMs: 35e3,
10879
11569
  inputSchema: {
10880
11570
  type: "object",
10881
11571
  properties: {
@@ -10885,11 +11575,34 @@ var codebaseSearchTool = {
10885
11575
  },
10886
11576
  kind: {
10887
11577
  type: "string",
10888
- description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
11578
+ enum: [
11579
+ "class",
11580
+ "interface",
11581
+ "enum",
11582
+ "type",
11583
+ "function",
11584
+ "method",
11585
+ "var",
11586
+ "const",
11587
+ "let",
11588
+ "property",
11589
+ "parameter",
11590
+ "namespace",
11591
+ "object",
11592
+ "literal",
11593
+ "schema",
11594
+ "struct",
11595
+ "trait",
11596
+ "impl",
11597
+ "static",
11598
+ "mod"
11599
+ ],
11600
+ description: "Filter by indexed symbol kind"
10889
11601
  },
10890
11602
  lang: {
10891
11603
  type: "string",
10892
- description: "Filter by language: ts, tsx, js, jsx"
11604
+ enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
11605
+ description: "Filter by indexed language"
10893
11606
  },
10894
11607
  lspKind: {
10895
11608
  type: "integer",
@@ -10914,20 +11627,12 @@ var codebaseSearchTool = {
10914
11627
  },
10915
11628
  async execute(input, ctx, execOpts) {
10916
11629
  const state = getIndexState();
10917
- if (!state.ready) {
10918
- return {
10919
- results: [],
10920
- total: 0,
10921
- query: input.query,
10922
- indexStatus: state.indexing ? `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.` : "Index not yet built. The codebase is being indexed at startup \u2014 search will be available shortly."
10923
- };
10924
- }
10925
- if (state.indexing) {
11630
+ if (state.indexing && !state.ready) {
10926
11631
  return {
10927
11632
  results: [],
10928
11633
  total: 0,
10929
11634
  query: input.query,
10930
- indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
11635
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
10931
11636
  };
10932
11637
  }
10933
11638
  if (state.lastError) {
@@ -10940,21 +11645,37 @@ var codebaseSearchTool = {
10940
11645
  indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
10941
11646
  };
10942
11647
  }
10943
- const limit = Math.min(input.limit ?? 20, 100);
11648
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
10944
11649
  const { results, total } = await searchCodebaseIndex(
10945
11650
  {
10946
11651
  projectRoot: ctx.projectRoot,
10947
11652
  indexDir: codebaseIndexDirOverride(ctx),
10948
11653
  query: input.query,
10949
- kind: input.kind,
10950
- lang: input.lang,
11654
+ kind: input.kind?.toLowerCase(),
11655
+ lang: input.lang?.toLowerCase(),
10951
11656
  file: input.file,
10952
11657
  lspKind: input.lspKind,
10953
11658
  limit
10954
11659
  },
10955
11660
  { signal: execOpts?.signal }
10956
11661
  );
10957
- return { results, total, query: input.query };
11662
+ let hasPersistedIndex = state.ready || total > 0;
11663
+ if (!hasPersistedIndex) {
11664
+ try {
11665
+ const stats = await codebaseIndexStats(
11666
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11667
+ { signal: execOpts?.signal }
11668
+ );
11669
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11670
+ } catch {
11671
+ }
11672
+ }
11673
+ return {
11674
+ results,
11675
+ total,
11676
+ query: input.query,
11677
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
11678
+ };
10958
11679
  }
10959
11680
  };
10960
11681
 
@@ -10963,12 +11684,15 @@ var codebaseStatsTool = {
10963
11684
  name: "codebase-stats",
10964
11685
  category: "Project",
10965
11686
  icon: "index",
10966
- description: "Return health and statistics about the current symbol index (total symbols, files, language/kind breakdown, size, last update). Useful to decide whether to re-index.",
10967
- usageHint: "CALL BEFORE HEAVY CODEBASE-SEARCH WORK:\n\n- Use to see if the index is up-to-date or needs a refresh.\n- No arguments required.\n- Helps avoid wasting tokens on searches against a stale index.\nLightweight and safe to call frequently.",
11687
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
11688
+ usageHint: "CHECK ONCE BEFORE BROAD CODE EXPLORATION:\n\n- Call before broad `tree`, `glob`, or `grep` exploration to see whether index-backed search is available.\n- If it reports no persisted data, run `codebase-index`, then use `codebase-search`.\n- No arguments required.\nLightweight and safe to call frequently.",
10968
11689
  permission: "auto",
10969
11690
  mutating: false,
10970
11691
  capabilities: ["fs.read"],
10971
- timeoutMs: 5e3,
11692
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
11693
+ // longer so the host can return a structured timeout instead of being
11694
+ // pre-empted by the generic tool executor.
11695
+ timeoutMs: 35e3,
10972
11696
  inputSchema: {
10973
11697
  type: "object",
10974
11698
  properties: {},
@@ -10976,7 +11700,8 @@ var codebaseStatsTool = {
10976
11700
  },
10977
11701
  async execute(_input, ctx, execOpts) {
10978
11702
  const idxState = getIndexState();
10979
- if (!idxState.ready) {
11703
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
11704
+ if (idxState.indexing) {
10980
11705
  return {
10981
11706
  totalSymbols: 0,
10982
11707
  totalFiles: 0,
@@ -10984,22 +11709,47 @@ var codebaseStatsTool = {
10984
11709
  byKind: {},
10985
11710
  lastIndexed: null,
10986
11711
  sizeBytes: 0,
10987
- indexPath: "",
11712
+ indexPath,
10988
11713
  version: SCHEMA_VERSION,
10989
- indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
11714
+ statsAvailable: false,
11715
+ indexing: {
11716
+ currentFile: idxState.currentFile,
11717
+ totalFiles: idxState.totalFiles
11718
+ },
11719
+ indexStatus: idxState.ready ? `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). The persisted snapshot remains available to codebase-search; retry stats after refresh.` : `Startup indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files). Do not start another index; retry stats or codebase-search after completion.`
10990
11720
  };
10991
11721
  }
10992
- const stats = await codebaseIndexStats(
10993
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
10994
- { signal: execOpts?.signal }
10995
- );
10996
- if (idxState.indexing) {
11722
+ let stats;
11723
+ try {
11724
+ stats = await codebaseIndexStats(
11725
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11726
+ { signal: execOpts?.signal }
11727
+ );
11728
+ } catch (err) {
11729
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
11730
+ throw err;
11731
+ }
10997
11732
  return {
10998
- ...stats,
10999
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
11000
- };
11733
+ totalSymbols: 0,
11734
+ totalFiles: 0,
11735
+ byLang: {},
11736
+ byKind: {},
11737
+ lastIndexed: null,
11738
+ sizeBytes: 0,
11739
+ indexPath,
11740
+ version: SCHEMA_VERSION,
11741
+ statsAvailable: false,
11742
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
11743
+ };
11001
11744
  }
11002
11745
  const circuit = idxState.circuit;
11746
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11747
+ let indexStatus;
11748
+ if (circuit.state === "open") {
11749
+ 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.");
11750
+ } else if (!hasPersistedIndex) {
11751
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
11752
+ }
11003
11753
  return {
11004
11754
  totalSymbols: stats.totalSymbols,
11005
11755
  totalFiles: stats.totalFiles,
@@ -11009,9 +11759,8 @@ var codebaseStatsTool = {
11009
11759
  sizeBytes: stats.sizeBytes,
11010
11760
  indexPath: stats.indexPath,
11011
11761
  version: stats.version,
11012
- ...circuit.state === "open" ? {
11013
- indexStatus: `Indexing is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. Stats reflect the last successful build.`
11014
- } : {}
11762
+ statsAvailable: true,
11763
+ ...indexStatus ? { indexStatus } : {}
11015
11764
  };
11016
11765
  }
11017
11766
  };
@@ -12530,6 +13279,11 @@ Compare this against your old_string and retry with the file's actual text.` : "
12530
13279
 
12531
13280
  // src/exec.ts
12532
13281
  import { spawn as spawn8 } from "node:child_process";
13282
+ import {
13283
+ emitProcessCompleted as emitProcessCompleted3,
13284
+ emitProcessOutput as emitProcessOutput3,
13285
+ emitProcessStarted as emitProcessStarted3
13286
+ } from "@wrongstack/core";
12533
13287
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
12534
13288
  init_output_spool();
12535
13289
  init_util();
@@ -13384,12 +14138,30 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13384
14138
  resolve15(result);
13385
14139
  };
13386
14140
  const startedAt = Date.now();
14141
+ let stdoutBytes = 0;
14142
+ let stderrBytes = 0;
14143
+ let telemetryCompleted = false;
14144
+ let timedOut = false;
13387
14145
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
13388
14146
  const resolved = resolveWin32Command(cmd);
13389
14147
  const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
13390
14148
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
13391
14149
  const spawnCmd = shim?.command ?? resolved;
13392
14150
  const spawnArgs = shim?.args ?? args;
14151
+ const emitCompletedOnce = (exitCode, pid2, signal2) => {
14152
+ if (telemetryCompleted) return;
14153
+ telemetryCompleted = true;
14154
+ emitProcessCompleted3({
14155
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
14156
+ exitCode,
14157
+ ...signal2 ? { signal: signal2 } : {},
14158
+ durationMs: Date.now() - startedAt,
14159
+ stdoutBytes,
14160
+ stderrBytes,
14161
+ timedOut,
14162
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
14163
+ });
14164
+ };
13393
14165
  let child;
13394
14166
  try {
13395
14167
  child = spawn8(spawnCmd, spawnArgs, {
@@ -13402,6 +14174,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13402
14174
  });
13403
14175
  } catch (err) {
13404
14176
  spool.finalize();
14177
+ emitProcessStarted3({
14178
+ parentPid: process.pid,
14179
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
14180
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
14181
+ cwd,
14182
+ background: false,
14183
+ startedAt: new Date(startedAt).toISOString()
14184
+ });
14185
+ emitCompletedOnce(1, void 0);
13405
14186
  finish({
13406
14187
  command: cmd,
13407
14188
  args,
@@ -13414,6 +14195,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13414
14195
  });
13415
14196
  return;
13416
14197
  }
14198
+ emitProcessStarted3({
14199
+ ...child.pid !== void 0 ? { pid: child.pid } : {},
14200
+ parentPid: process.pid,
14201
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
14202
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
14203
+ cwd,
14204
+ background: false,
14205
+ startedAt: new Date(startedAt).toISOString()
14206
+ });
13417
14207
  child.on("error", (err) => {
13418
14208
  const isAbort = err && err.code === "ABORT_ERR";
13419
14209
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
@@ -13422,6 +14212,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13422
14212
  if (typeof pid === "number") registry.unregister(pid);
13423
14213
  registry.afterCall(Date.now() - startedAt, true);
13424
14214
  spool.finalize();
14215
+ emitCompletedOnce(isAbort ? 124 : 1, child.pid, isAbort ? "ABORT" : void 0);
13425
14216
  finish({
13426
14217
  command: cmd,
13427
14218
  args,
@@ -13441,6 +14232,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13441
14232
  }
13442
14233
  const timer = setTimeout(() => {
13443
14234
  killed = true;
14235
+ timedOut = true;
13444
14236
  if (typeof pid === "number") registry.kill(pid);
13445
14237
  else child.kill("SIGTERM");
13446
14238
  }, timeout);
@@ -13455,11 +14247,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13455
14247
  }
13456
14248
  child.stdout?.on("data", (chunk) => {
13457
14249
  const text = chunk.toString();
14250
+ stdoutBytes += chunk.byteLength;
14251
+ emitProcessOutput3({ pid, stream: "stdout", chunk });
13458
14252
  if (stdout.length < MAX_OUTPUT2) stdout += text;
13459
14253
  spool.write(text);
13460
14254
  });
13461
14255
  child.stderr?.on("data", (chunk) => {
13462
14256
  const text = chunk.toString();
14257
+ stderrBytes += chunk.byteLength;
14258
+ emitProcessOutput3({ pid, stream: "stderr", chunk });
13463
14259
  if (stderr.length < MAX_OUTPUT2) stderr += text;
13464
14260
  spool.write(text);
13465
14261
  });
@@ -13469,6 +14265,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13469
14265
  if (typeof pid === "number") registry.unregister(pid);
13470
14266
  const durationMs = Date.now() - startedAt;
13471
14267
  const exitCode = killed ? 124 : code ?? 1;
14268
+ emitCompletedOnce(exitCode, pid);
13472
14269
  registry.afterCall(durationMs, exitCode !== 0);
13473
14270
  const spooled = spool.finalize();
13474
14271
  finish({
@@ -13544,7 +14341,7 @@ function guardedLookup(hostname4, options, callback) {
13544
14341
  var pinnedAgent;
13545
14342
  function getPinnedDispatcher() {
13546
14343
  if (!pinnedAgent) {
13547
- pinnedAgent = new Agent({ connect: { lookup: guardedLookup } });
14344
+ pinnedAgent = new Agent({ allowH2: false, connect: { lookup: guardedLookup } });
13548
14345
  }
13549
14346
  return pinnedAgent;
13550
14347
  }
@@ -13646,7 +14443,7 @@ async function assertNotPrivate(hostname4) {
13646
14443
  }
13647
14444
  }
13648
14445
  } catch (err) {
13649
- if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
14446
+ if (err instanceof ToolValidationError2) throw err;
13650
14447
  }
13651
14448
  }
13652
14449
  }
@@ -13994,7 +14791,7 @@ async function detectFixer(cwd) {
13994
14791
  init_util();
13995
14792
  import { spawn as spawn9 } from "node:child_process";
13996
14793
  import { statSync as statSync3 } from "node:fs";
13997
- import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
14794
+ import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
13998
14795
  import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
13999
14796
  var TIMEOUT_MS2 = 3e4;
14000
14797
  var MAX_OUTPUT3 = 1e5;
@@ -14139,7 +14936,7 @@ function validateWorktreeInput(input, projectRoot) {
14139
14936
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
14140
14937
  const root = resolve11(projectRoot);
14141
14938
  const abs = resolve11(root, input.worktreePath);
14142
- if (abs !== root && !abs.startsWith(root + sep4)) {
14939
+ if (abs !== root && !abs.startsWith(root + sep5)) {
14143
14940
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
14144
14941
  }
14145
14942
  }
@@ -14302,8 +15099,8 @@ var WALK_CONCURRENCY = 16;
14302
15099
  var globTool = {
14303
15100
  name: "glob",
14304
15101
  category: "Filesystem",
14305
- description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
14306
- usageHint: "RECOMMENDED FOR SCOPING SEARCHES:\n\n- Use early to get a list of files you actually care about.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
15102
+ description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
15103
+ usageHint: "PATH DISCOVERY AND SEARCH SCOPING:\n\n- When `codebase-search` is live, use it first for code concepts; use `glob` for filenames, path patterns, and non-indexed files.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
14307
15104
  selection: {
14308
15105
  doNotUseWhen: "you need to search inside file contents.",
14309
15106
  useInstead: ["grep"]
@@ -14491,8 +15288,8 @@ var NATIVE_MAX_FILE_BYTES = 1e6;
14491
15288
  var grepTool = {
14492
15289
  name: "grep",
14493
15290
  category: "Search",
14494
- description: "Search across files using a regular expression. This is one of the primary code search tools. Prefers ripgrep for speed and features when available.",
14495
- usageHint: 'POWERFUL CODE SEARCH TOOL:\n\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
15291
+ 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.",
15292
+ usageHint: 'EXACT-TEXT AND REGEX SEARCH:\n\n- When `codebase-search` is live, use it first for symbols and concepts; use `grep` for exact text, regexes, unsupported content, and concrete usage sites.\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
14496
15293
  selection: {
14497
15294
  doNotUseWhen: "you only need to locate files by name or path pattern.",
14498
15295
  useInstead: ["glob"]
@@ -15514,6 +16311,7 @@ function toYaml(data, indent = 0) {
15514
16311
  }
15515
16312
 
15516
16313
  // src/kanban.ts
16314
+ import { randomUUID } from "node:crypto";
15517
16315
  import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
15518
16316
  import {
15519
16317
  addCheckToTask,
@@ -15531,7 +16329,7 @@ import {
15531
16329
  duplicateBoard,
15532
16330
  exportBoardAsMarkdown,
15533
16331
  exportBoardToTaskGraph,
15534
- generateBoardFromDescription,
16332
+ createBoardFromText,
15535
16333
  getBoard as getBoard2,
15536
16334
  getKanbanOrchestrationSnapshot,
15537
16335
  getKanbanQueueHealth,
@@ -15553,7 +16351,9 @@ import {
15553
16351
  setTaskChain,
15554
16352
  splitTask,
15555
16353
  syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
16354
+ transitionTask,
15556
16355
  transferTaskToBoard,
16356
+ touchKanbanPresence as touchKanbanPresence2,
15557
16357
  updateBoard as updateBoard2,
15558
16358
  updateCheckOnTask,
15559
16359
  updateColumn,
@@ -15564,20 +16364,24 @@ import {
15564
16364
 
15565
16365
  // src/session-kanban.ts
15566
16366
  import { watch } from "node:fs";
15567
- import { basename as basename8, dirname as dirname13 } from "node:path";
16367
+ import { basename as basename9, dirname as dirname13 } from "node:path";
15568
16368
  import {
15569
16369
  deserializeTaskGraph,
16370
+ GlobalMailbox,
15570
16371
  loadPlan,
15571
16372
  loadTasks,
15572
16373
  mutatePlan,
15573
16374
  mutateTasks
15574
16375
  } from "@wrongstack/core";
16376
+ import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
15575
16377
  import {
15576
16378
  createBoard,
15577
16379
  getBoard,
16380
+ getKanbanDir,
15578
16381
  listBoards,
15579
16382
  removeBoard,
15580
16383
  syncBoardFromTaskGraph,
16384
+ touchKanbanPresence,
15581
16385
  updateBoard
15582
16386
  } from "@wrongstack/kanban";
15583
16387
  var SESSION_BOARD_TAG = "session-work";
@@ -15590,12 +16394,17 @@ var SESSION_KANBAN_COLUMNS = [
15590
16394
  ];
15591
16395
  var boardQueue = /* @__PURE__ */ new Map();
15592
16396
  var boardEnsures = /* @__PURE__ */ new Map();
16397
+ var pendingMirrors = /* @__PURE__ */ new Map();
16398
+ var activeMirrors = /* @__PURE__ */ new Set();
15593
16399
  var bindings = /* @__PURE__ */ new WeakMap();
15594
16400
  var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
15595
16401
  var activeSessionBoards = /* @__PURE__ */ new Map();
15596
16402
  function boardKey(projectRoot, sessionId) {
15597
16403
  return `${projectRoot}\0${sessionId}`;
15598
16404
  }
16405
+ function mirrorKey(projectRoot, sessionId, sourceSystem) {
16406
+ return `${boardKey(projectRoot, sessionId)}\0${sourceSystem}`;
16407
+ }
15599
16408
  function sessionTag(sessionId) {
15600
16409
  return `session:${sessionId}`;
15601
16410
  }
@@ -15731,6 +16540,43 @@ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
15731
16540
  return result?.board ?? null;
15732
16541
  });
15733
16542
  }
16543
+ function queueLatestMirror(projectRoot, sessionId, graph, sourceSystem) {
16544
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return;
16545
+ const key = mirrorKey(projectRoot, sessionId, sourceSystem);
16546
+ pendingMirrors.set(key, { projectRoot, sessionId, graph, sourceSystem });
16547
+ if (activeMirrors.has(key)) return;
16548
+ activeMirrors.add(key);
16549
+ void (async () => {
16550
+ try {
16551
+ for (; ; ) {
16552
+ const pending2 = pendingMirrors.get(key);
16553
+ if (!pending2) break;
16554
+ pendingMirrors.delete(key);
16555
+ try {
16556
+ await projectGraph(
16557
+ pending2.projectRoot,
16558
+ pending2.sessionId,
16559
+ pending2.graph,
16560
+ pending2.sourceSystem
16561
+ );
16562
+ } catch {
16563
+ }
16564
+ }
16565
+ } finally {
16566
+ activeMirrors.delete(key);
16567
+ const pending2 = pendingMirrors.get(key);
16568
+ if (pending2) {
16569
+ pendingMirrors.delete(key);
16570
+ queueLatestMirror(
16571
+ pending2.projectRoot,
16572
+ pending2.sessionId,
16573
+ pending2.graph,
16574
+ pending2.sourceSystem
16575
+ );
16576
+ }
16577
+ }
16578
+ })();
16579
+ }
15734
16580
  function todoListToSerializedGraph(todos, sessionId) {
15735
16581
  const nodes = todos.map((todo, index) => ({
15736
16582
  id: todo.id,
@@ -15844,14 +16690,65 @@ function fireAndForget(work) {
15844
16690
  void work.catch(() => {
15845
16691
  });
15846
16692
  }
16693
+ function broadcastTodoUpdate(context, todos) {
16694
+ const sessionId = context.session?.id ?? "";
16695
+ if (!context.agentId || !sessionId) return;
16696
+ const projectDir = resolveWstackPaths3({ projectRoot: context.projectRoot }).projectDir;
16697
+ const mailbox = new GlobalMailbox(projectDir);
16698
+ void mailbox.send({
16699
+ from: context.agentId,
16700
+ to: "*",
16701
+ type: "status",
16702
+ subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
16703
+ body: JSON.stringify({
16704
+ kind: "kanban.todos.updated",
16705
+ sessionId,
16706
+ revision: context.state.revision,
16707
+ todos
16708
+ }),
16709
+ priority: "normal",
16710
+ senderSessionId: sessionId,
16711
+ ttlMs: 6 * 60 * 60 * 1e3
16712
+ }).catch(() => {
16713
+ });
16714
+ }
16715
+ function notifyTodoUpdate(context, todos) {
16716
+ const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
16717
+ const text = `[KANBAN TODO UPDATE]
16718
+ Another Kanban agent reassessed the shared board. The canonical todo list is now:
16719
+ ${summary}
16720
+ Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
16721
+ const state = context.state;
16722
+ if (typeof state.appendBlockToLastUserMessage === "function") {
16723
+ if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
16724
+ }
16725
+ if (typeof state.appendMessage === "function") {
16726
+ state.appendMessage({ role: "user", content: [{ type: "text", text }] });
16727
+ }
16728
+ }
15847
16729
  function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
15848
- fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));
16730
+ queueLatestMirror(
16731
+ projectRoot,
16732
+ sessionId,
16733
+ todoListToSerializedGraph(todos, sessionId),
16734
+ "session-todo"
16735
+ );
15849
16736
  }
15850
16737
  function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
15851
- fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));
16738
+ queueLatestMirror(
16739
+ projectRoot,
16740
+ sessionId,
16741
+ taskFileToSerializedGraph(tasks, sessionId),
16742
+ "session-task"
16743
+ );
15852
16744
  }
15853
16745
  function mirrorSessionPlanToKanban(projectRoot, items, sessionId) {
15854
- fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));
16746
+ queueLatestMirror(
16747
+ projectRoot,
16748
+ sessionId,
16749
+ planFileToSerializedGraph(items, sessionId),
16750
+ "session-plan"
16751
+ );
15855
16752
  }
15856
16753
  function attachSessionKanbanMirror(context) {
15857
16754
  const existing = bindings.get(context);
@@ -15875,6 +16772,10 @@ function attachSessionKanbanMirror(context) {
15875
16772
  let watcher = null;
15876
16773
  let watchedDir = "";
15877
16774
  let timer = null;
16775
+ let boardWatcher = null;
16776
+ let watchedBoardId = "";
16777
+ let boardTimer = null;
16778
+ let presenceTimer = null;
15878
16779
  const sessionId = () => context.session?.id ?? "";
15879
16780
  const refreshFiles = async () => {
15880
16781
  const id = sessionId();
@@ -15890,6 +16791,48 @@ function attachSessionKanbanMirror(context) {
15890
16791
  if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
15891
16792
  }
15892
16793
  };
16794
+ const refreshBoard = async () => {
16795
+ if (!watchedBoardId) return;
16796
+ const board = await getBoard(context.projectRoot, watchedBoardId);
16797
+ if (board) applySessionKanbanBoardToTodos(context, board);
16798
+ };
16799
+ const configureBoardWatcher = async () => {
16800
+ const id = sessionId();
16801
+ const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
16802
+ if (!board || board.id === watchedBoardId) return;
16803
+ boardWatcher?.close();
16804
+ boardWatcher = null;
16805
+ watchedBoardId = board.id;
16806
+ try {
16807
+ const boardFileName = `${board.id}.json`;
16808
+ boardWatcher = watch(
16809
+ getKanbanDir(context.projectRoot),
16810
+ { persistent: false },
16811
+ (_event, filename) => {
16812
+ if (filename?.toString() !== boardFileName) return;
16813
+ if (boardTimer) clearTimeout(boardTimer);
16814
+ boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);
16815
+ }
16816
+ );
16817
+ const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
16818
+ sessionId: id,
16819
+ agentId: context.agentId,
16820
+ agentName: context.agentName
16821
+ });
16822
+ fireAndForget(touchPresence());
16823
+ if (presenceTimer) clearInterval(presenceTimer);
16824
+ presenceTimer = setInterval(() => fireAndForget(touchPresence()), 6e4);
16825
+ presenceTimer.unref?.();
16826
+ boardWatcher.on("error", () => {
16827
+ boardWatcher?.close();
16828
+ boardWatcher = null;
16829
+ watchedBoardId = "";
16830
+ });
16831
+ } catch {
16832
+ boardWatcher = null;
16833
+ watchedBoardId = "";
16834
+ }
16835
+ };
15893
16836
  const configureWatcher = () => {
15894
16837
  const planPath = context.meta["plan.path"];
15895
16838
  const taskPath = context.meta["task.path"];
@@ -15903,8 +16846,8 @@ function attachSessionKanbanMirror(context) {
15903
16846
  const name = filename?.toString();
15904
16847
  const currentPlanPath = context.meta["plan.path"];
15905
16848
  const currentTaskPath = context.meta["task.path"];
15906
- const planName = typeof currentPlanPath === "string" ? basename8(currentPlanPath) : "";
15907
- const taskName = typeof currentTaskPath === "string" ? basename8(currentTaskPath) : "";
16849
+ const planName = typeof currentPlanPath === "string" ? basename9(currentPlanPath) : "";
16850
+ const taskName = typeof currentTaskPath === "string" ? basename9(currentTaskPath) : "";
15908
16851
  if (name && name !== planName && name !== taskName) return;
15909
16852
  if (timer) clearTimeout(timer);
15910
16853
  timer = setTimeout(() => fireAndForget(refreshFiles()), 60);
@@ -15928,14 +16871,19 @@ function attachSessionKanbanMirror(context) {
15928
16871
  syncActiveSessionRegistration();
15929
16872
  configureWatcher();
15930
16873
  fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
16874
+ fireAndForget(configureBoardWatcher());
15931
16875
  fireAndForget(refreshFiles());
15932
16876
  }
15933
16877
  });
15934
16878
  configureWatcher();
16879
+ fireAndForget(configureBoardWatcher());
15935
16880
  const detach = () => {
15936
16881
  unsubscribe();
15937
16882
  if (timer) clearTimeout(timer);
16883
+ if (boardTimer) clearTimeout(boardTimer);
16884
+ if (presenceTimer) clearInterval(presenceTimer);
15938
16885
  watcher?.close();
16886
+ boardWatcher?.close();
15939
16887
  bindings.delete(context);
15940
16888
  if (attachedProjectRoot && registeredSessionId) {
15941
16889
  releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
@@ -15974,6 +16922,51 @@ function sourceStatus(task) {
15974
16922
  if (task.status === "failed") return "failed";
15975
16923
  return "pending";
15976
16924
  }
16925
+ function todoStatus(task) {
16926
+ const status = sourceStatus(task);
16927
+ if (status === "completed") return "completed";
16928
+ if (status === "in_progress" || status === "review") return "in_progress";
16929
+ return "pending";
16930
+ }
16931
+ function sessionTodoFromTask(task) {
16932
+ return {
16933
+ id: task.origin?.taskId ?? task.id,
16934
+ content: task.title,
16935
+ status: todoStatus(task),
16936
+ ...task.description ? { activeForm: task.description } : {}
16937
+ };
16938
+ }
16939
+ function sameTodos(left, right) {
16940
+ return left.length === right.length && left.every((todo, index) => {
16941
+ const candidate = right[index];
16942
+ return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask;
16943
+ });
16944
+ }
16945
+ function applySessionKanbanBoardToTodos(context, board) {
16946
+ const sessionId = context.session?.id ?? "";
16947
+ if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
16948
+ return [...context.todos];
16949
+ }
16950
+ const projectedTodos = board.tasks.filter(
16951
+ (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
16952
+ ).sort((left, right) => {
16953
+ const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;
16954
+ const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;
16955
+ return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);
16956
+ }).map(sessionTodoFromTask);
16957
+ const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
16958
+ const effectiveTodos = allCompleted ? [] : projectedTodos;
16959
+ if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
16960
+ suppressedTodoMirrors.add(context);
16961
+ try {
16962
+ context.state.replaceTodos(projectedTodos);
16963
+ } finally {
16964
+ suppressedTodoMirrors.delete(context);
16965
+ }
16966
+ notifyTodoUpdate(context, context.todos);
16967
+ broadcastTodoUpdate(context, context.todos);
16968
+ return [...context.todos];
16969
+ }
15977
16970
  async function applySessionKanbanTaskToSource(context, task, options = {}) {
15978
16971
  const originId = task.origin?.taskId;
15979
16972
  const graphId = task.origin?.graphId ?? "";
@@ -16037,8 +17030,8 @@ async function applySessionKanbanTaskToSource(context, task, options = {}) {
16037
17030
  var kanbanTool = {
16038
17031
  name: "kanban",
16039
17032
  category: "Project",
16040
- description: "Manage project-scoped multi-kanban boards stored under .wrongstack/kanbans. Supports board/task/column CRUD, ready-task queues, dependency chains, split/merge, assignment metadata, provider/model/fallback routing hints, goal metrics, success checks, notes, links, and run status updates.",
16041
- usageHint: "Use this for durable project kanban state. Agents should call snapshot/ready_tasks, then claim_task before working. Use set_chain for ordered work, split_task/merge_tasks when task scope changes, assign_task with provider/model/fallback hints before spawning, mark_assignment when starting or finishing, and release_task if the claim cannot be worked.",
17033
+ 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.",
17034
+ usageHint: "Use this for durable project kanban state. Reassess with get_board whenever evidence changes; agents may add, update, split, merge, reprioritize, or remove tasks so the board remains a live plan. Presence includes active/last-seen session and agent metadata. For managed boards, fully fill card details, use transition_task after every material step, attach truthful evidence, and never use update_task/move_task to bypass lifecycle guards. Worker completion enters Review; only passed acceptance criteria plus review evidence allow Done.",
16042
17035
  permission: "confirm",
16043
17036
  mutating: true,
16044
17037
  capabilities: ["fs.write"],
@@ -16075,6 +17068,7 @@ var kanbanTool = {
16075
17068
  "transfer_task",
16076
17069
  "get_task",
16077
17070
  "update_task",
17071
+ "transition_task",
16078
17072
  "move_task",
16079
17073
  "delete_task",
16080
17074
  "set_chain",
@@ -16105,6 +17099,7 @@ var kanbanTool = {
16105
17099
  targetColumnId: { type: "string" },
16106
17100
  title: { type: "string" },
16107
17101
  description: { type: "string" },
17102
+ dueDate: { type: "string" },
16108
17103
  tags: { type: "array", items: { type: "string" } },
16109
17104
  labels: { type: "array", items: { type: "string" } },
16110
17105
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
@@ -16151,6 +17146,18 @@ var kanbanTool = {
16151
17146
  type: "string",
16152
17147
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
16153
17148
  },
17149
+ lifecycleStage: {
17150
+ type: "string",
17151
+ enum: ["backlog", "todo", "running", "review", "done"]
17152
+ },
17153
+ transitionAction: { type: "string" },
17154
+ transitionComment: { type: "string" },
17155
+ attachmentUrl: { type: "string" },
17156
+ attachmentTitle: { type: "string" },
17157
+ attachmentType: {
17158
+ type: "string",
17159
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
17160
+ },
16154
17161
  releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
16155
17162
  releaseReason: { type: "string" },
16156
17163
  clearAssignee: { type: "boolean" },
@@ -16221,591 +17228,649 @@ var kanbanTool = {
16221
17228
  async execute(input, ctx) {
16222
17229
  const projectRoot = ctx.projectRoot;
16223
17230
  if (!projectRoot) return fail("No project root is available.");
17231
+ const withPresence = async (result) => {
17232
+ const boardId = result.board?.id ?? input.boardId;
17233
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
17234
+ try {
17235
+ const board = await touchKanbanPresence2(projectRoot, boardId, {
17236
+ sessionId: ctx.session.id,
17237
+ agentId: ctx.agentId,
17238
+ agentName: ctx.agentName,
17239
+ taskId: input.taskId ?? result.task?.id,
17240
+ runTaskId: input.runTaskId
17241
+ });
17242
+ return board ? { ...result, board } : result;
17243
+ } catch {
17244
+ return result;
17245
+ }
17246
+ };
16224
17247
  try {
16225
- switch (input.action) {
16226
- case "list_boards": {
16227
- const boards = await listBoards2(projectRoot);
16228
- return { ok: true, message: `${boards.length} board(s).`, boards };
16229
- }
16230
- case "get_board": {
16231
- const board = await requireBoard(projectRoot, input.boardId);
16232
- return board ? okBoard(board) : fail("Board not found.");
16233
- }
16234
- case "create_board": {
16235
- if (!input.title) return fail("create_board requires title.");
16236
- const board = await createBoard2(projectRoot, {
16237
- title: input.title,
16238
- ...input.description !== void 0 ? { description: input.description } : {},
16239
- ...input.tags !== void 0 ? { tags: input.tags } : {},
16240
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
16241
- });
16242
- return { ok: true, message: `Board created: ${board.title}`, board };
16243
- }
16244
- case "update_board": {
16245
- if (!input.boardId) return fail("update_board requires boardId.");
16246
- const board = await updateBoard2(projectRoot, input.boardId, {
16247
- ...input.title !== void 0 ? { title: input.title } : {},
16248
- ...input.description !== void 0 ? { description: input.description } : {},
16249
- ...input.tags !== void 0 ? { tags: input.tags } : {}
16250
- });
16251
- return board ? okBoard(board, "Board updated.") : fail("Board not found.");
16252
- }
16253
- case "duplicate_board": {
16254
- if (!input.boardId) return fail("duplicate_board requires boardId.");
16255
- const board = await duplicateBoard(projectRoot, input.boardId, {
16256
- ...input.title !== void 0 ? { title: input.title } : {},
16257
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16258
- ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
16259
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16260
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
16261
- });
16262
- return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
16263
- }
16264
- case "delete_board": {
16265
- if (!input.boardId) return fail("delete_board requires boardId.");
16266
- const removed = await removeBoard2(projectRoot, input.boardId);
16267
- return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
16268
- }
16269
- case "generate_board": {
16270
- if (!input.description) return fail("generate_board requires description.");
16271
- const boardInput = generateBoardFromDescription({
16272
- description: input.description,
16273
- ...input.title !== void 0 ? { title: input.title } : {},
16274
- ...input.context !== void 0 ? { context: input.context } : {},
16275
- ...input.columns !== void 0 ? { columns: input.columns } : {}
16276
- });
16277
- const board = await createBoard2(projectRoot, boardInput);
16278
- for (const taskInput2 of parseLinesIntoTasks(
16279
- input.description,
16280
- board.columns[0]?.id ?? "backlog"
16281
- )) {
16282
- await addTask(projectRoot, board.id, taskInput2);
17248
+ const result = await (async () => {
17249
+ switch (input.action) {
17250
+ case "list_boards": {
17251
+ const boards = await listBoards2(projectRoot);
17252
+ return { ok: true, message: `${boards.length} board(s).`, boards };
16283
17253
  }
16284
- return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
16285
- }
16286
- case "export_markdown": {
16287
- const board = await requireBoard(projectRoot, input.boardId);
16288
- if (!board) return fail("Board not found.");
16289
- return {
16290
- ok: true,
16291
- message: "Board exported.",
16292
- board,
16293
- markdown: exportBoardAsMarkdown(board)
16294
- };
16295
- }
16296
- case "export_task_graph": {
16297
- if (!input.boardId) return fail("export_task_graph requires boardId.");
16298
- const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
16299
- ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
16300
- ...input.specId !== void 0 ? { specId: input.specId } : {},
16301
- ...input.title !== void 0 ? { title: input.title } : {},
16302
- ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
16303
- ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
16304
- });
16305
- if (!exported) return fail("Board not found.");
16306
- return {
16307
- ok: true,
16308
- message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
16309
- board: exported.board,
16310
- taskGraph: serializeTaskGraph(exported.graph)
16311
- };
16312
- }
16313
- case "sync_task_graph": {
16314
- if (!input.boardId || !input.taskGraph) {
16315
- return fail("sync_task_graph requires boardId and taskGraph.");
17254
+ case "get_board": {
17255
+ const board = await requireBoard(projectRoot, input.boardId);
17256
+ return board ? okBoard(board) : fail("Board not found.");
16316
17257
  }
16317
- const graph = deserializeTaskGraph2(input.taskGraph);
16318
- const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
16319
- ...input.title !== void 0 ? { title: input.title } : {},
16320
- ...input.description !== void 0 ? { description: input.description } : {},
16321
- ...input.tags !== void 0 ? { tags: input.tags } : {},
16322
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16323
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16324
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16325
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16326
- ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
16327
- ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
16328
- });
16329
- return result ? {
16330
- ok: true,
16331
- message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
16332
- board: result.board
16333
- } : fail("Board not found.");
16334
- }
16335
- case "create_from_graph": {
16336
- if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
16337
- const graph = deserializeTaskGraph2(input.taskGraph);
16338
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16339
- ...input.title !== void 0 ? { title: input.title } : {},
16340
- ...input.description !== void 0 ? { description: input.description } : {},
16341
- ...input.tags !== void 0 ? { tags: input.tags } : {},
16342
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16343
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16344
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16345
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
16346
- });
16347
- return {
16348
- ok: true,
16349
- message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
16350
- board
16351
- };
16352
- }
16353
- case "import_session_tasks": {
16354
- const taskPath = ctx.meta?.["task.path"];
16355
- if (!taskPath) return fail("No session task file for this session.");
16356
- const file = await loadTasks2(taskPath);
16357
- if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
16358
- const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
16359
- const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
16360
- const tags = ["session", `session:${sessionId}`];
16361
- const existing = (await listBoards2(projectRoot)).find(
16362
- (b) => b.tags?.includes(`session:${sessionId}`)
16363
- );
16364
- if (existing) {
16365
- const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
17258
+ case "create_board": {
17259
+ if (!input.title) return fail("create_board requires title.");
17260
+ const board = await createBoard2(projectRoot, {
17261
+ title: input.title,
17262
+ ...input.description !== void 0 ? { description: input.description } : {},
17263
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17264
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
17265
+ });
17266
+ return { ok: true, message: `Board created: ${board.title}`, board };
17267
+ }
17268
+ case "update_board": {
17269
+ if (!input.boardId) return fail("update_board requires boardId.");
17270
+ const board = await updateBoard2(projectRoot, input.boardId, {
17271
+ ...input.title !== void 0 ? { title: input.title } : {},
17272
+ ...input.description !== void 0 ? { description: input.description } : {},
17273
+ ...input.tags !== void 0 ? { tags: input.tags } : {}
17274
+ });
17275
+ return board ? okBoard(board, "Board updated.") : fail("Board not found.");
17276
+ }
17277
+ case "duplicate_board": {
17278
+ if (!input.boardId) return fail("duplicate_board requires boardId.");
17279
+ const board = await duplicateBoard(projectRoot, input.boardId, {
17280
+ ...input.title !== void 0 ? { title: input.title } : {},
17281
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17282
+ ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
17283
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
17284
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
17285
+ });
17286
+ return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
17287
+ }
17288
+ case "delete_board": {
17289
+ if (!input.boardId) return fail("delete_board requires boardId.");
17290
+ const removed = await removeBoard2(projectRoot, input.boardId);
17291
+ return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
17292
+ }
17293
+ case "generate_board": {
17294
+ if (!input.description) return fail("generate_board requires description.");
17295
+ const boardInput = createBoardFromText({
17296
+ description: input.description,
17297
+ ...input.title !== void 0 ? { title: input.title } : {},
17298
+ ...input.context !== void 0 ? { context: input.context } : {},
17299
+ ...input.columns !== void 0 ? { columns: input.columns } : {}
17300
+ });
17301
+ const board = await createBoard2(projectRoot, boardInput);
17302
+ for (const taskInput2 of parseLinesIntoTasks(
17303
+ input.description,
17304
+ board.columns[0]?.id ?? "backlog"
17305
+ )) {
17306
+ await addTask(projectRoot, board.id, taskInput2);
17307
+ }
17308
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
17309
+ }
17310
+ case "export_markdown": {
17311
+ const board = await requireBoard(projectRoot, input.boardId);
17312
+ if (!board) return fail("Board not found.");
17313
+ return {
17314
+ ok: true,
17315
+ message: "Board exported.",
17316
+ board,
17317
+ markdown: exportBoardAsMarkdown(board)
17318
+ };
17319
+ }
17320
+ case "export_task_graph": {
17321
+ if (!input.boardId) return fail("export_task_graph requires boardId.");
17322
+ const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
17323
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
17324
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
17325
+ ...input.title !== void 0 ? { title: input.title } : {},
17326
+ ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
17327
+ ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
17328
+ });
17329
+ if (!exported) return fail("Board not found.");
17330
+ return {
17331
+ ok: true,
17332
+ message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
17333
+ board: exported.board,
17334
+ taskGraph: serializeTaskGraph(exported.graph)
17335
+ };
17336
+ }
17337
+ case "sync_task_graph": {
17338
+ if (!input.boardId || !input.taskGraph) {
17339
+ return fail("sync_task_graph requires boardId and taskGraph.");
17340
+ }
17341
+ const graph = deserializeTaskGraph2(input.taskGraph);
17342
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
17343
+ ...input.title !== void 0 ? { title: input.title } : {},
17344
+ ...input.description !== void 0 ? { description: input.description } : {},
17345
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17346
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17347
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
17348
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
17349
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
17350
+ ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
17351
+ ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
17352
+ });
17353
+ return result2 ? {
17354
+ ok: true,
17355
+ message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
17356
+ board: result2.board
17357
+ } : fail("Board not found.");
17358
+ }
17359
+ case "create_from_graph": {
17360
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
17361
+ const graph = deserializeTaskGraph2(input.taskGraph);
17362
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
17363
+ ...input.title !== void 0 ? { title: input.title } : {},
17364
+ ...input.description !== void 0 ? { description: input.description } : {},
17365
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17366
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17367
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
17368
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
17369
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
17370
+ });
17371
+ return {
17372
+ ok: true,
17373
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
17374
+ board
17375
+ };
17376
+ }
17377
+ case "import_session_tasks": {
17378
+ const taskPath = ctx.meta?.["task.path"];
17379
+ if (!taskPath) return fail("No session task file for this session.");
17380
+ const file = await loadTasks2(taskPath);
17381
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
17382
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
17383
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
17384
+ const tags = ["session", `session:${sessionId}`];
17385
+ const existing = (await listBoards2(projectRoot)).find(
17386
+ (b) => b.tags?.includes(`session:${sessionId}`)
17387
+ );
17388
+ if (existing) {
17389
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
17390
+ sourceSystem: "session",
17391
+ tags,
17392
+ archiveMissingTasks: true,
17393
+ includeCompletedTasks: true
17394
+ });
17395
+ return result2 ? {
17396
+ ok: true,
17397
+ message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
17398
+ board: result2.board
17399
+ } : fail("Session board vanished mid-sync.");
17400
+ }
17401
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
17402
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
16366
17403
  sourceSystem: "session",
16367
- tags,
16368
- archiveMissingTasks: true,
16369
- includeCompletedTasks: true
17404
+ tags
16370
17405
  });
16371
- return result ? {
17406
+ return {
16372
17407
  ok: true,
16373
- message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
16374
- board: result.board
16375
- } : fail("Session board vanished mid-sync.");
17408
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
17409
+ board
17410
+ };
16376
17411
  }
16377
- const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16378
- title: `Session tasks (${sessionId.slice(0, 8)})`,
16379
- sourceSystem: "session",
16380
- tags
16381
- });
16382
- return {
16383
- ok: true,
16384
- message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
16385
- board
16386
- };
16387
- }
16388
- case "search_tasks": {
16389
- const tasks = await searchKanban(projectRoot, {
16390
- query: input.query,
16391
- boardId: input.boardId,
16392
- assignedAgent: input.agentId,
16393
- status: input.status,
16394
- priority: input.priority,
16395
- label: input.labels?.[0],
16396
- chainId: input.chainId
16397
- });
16398
- return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
16399
- }
16400
- case "ready_tasks": {
16401
- const tasks = await listReadyTasks(projectRoot, {
16402
- query: input.query,
16403
- boardId: input.boardId,
16404
- assignedAgent: input.agentId,
16405
- priority: input.priority,
16406
- label: input.labels?.[0],
16407
- chainId: input.chainId,
16408
- limit: input.limit
16409
- });
16410
- return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
16411
- }
16412
- case "snapshot": {
16413
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
16414
- query: input.query,
16415
- boardId: input.boardId,
16416
- assignedAgent: input.agentId,
16417
- status: input.status,
16418
- priority: input.priority,
16419
- label: input.labels?.[0],
16420
- chainId: input.chainId
16421
- });
16422
- return {
16423
- ok: true,
16424
- message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
16425
- snapshot
16426
- };
16427
- }
16428
- case "add_column": {
16429
- if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
16430
- const result = await addColumn(projectRoot, input.boardId, {
16431
- title: input.title,
16432
- ...input.description !== void 0 ? { description: input.description } : {}
16433
- });
16434
- return result ? okBoard(result.board, "Column added.") : fail("Board not found.");
16435
- }
16436
- case "update_column": {
16437
- if (!input.boardId || !input.columnId)
16438
- return fail("update_column requires boardId and columnId.");
16439
- const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
16440
- ...input.title !== void 0 ? { title: input.title } : {},
16441
- ...input.description !== void 0 ? { description: input.description } : {},
16442
- ...input.order !== void 0 ? { order: input.order } : {}
16443
- });
16444
- return board ? okBoard(board, "Column updated.") : fail("Column not found.");
16445
- }
16446
- case "delete_column": {
16447
- if (!input.boardId || !input.columnId)
16448
- return fail("delete_column requires boardId and columnId.");
16449
- const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
16450
- moveTasksToColumnId: input.moveTasksToColumnId
16451
- });
16452
- return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
16453
- }
16454
- case "add_task": {
16455
- if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
16456
- const result = await addTask(projectRoot, input.boardId, taskInput(input));
16457
- return result ? okTask(result.board, result.task, "Task added.") : fail("Board not found.");
16458
- }
16459
- case "split_task": {
16460
- if (!input.boardId || !input.taskId || !input.childTitles?.length) {
16461
- return fail("split_task requires boardId, taskId, and childTitles.");
17412
+ case "search_tasks": {
17413
+ const tasks = await searchKanban(projectRoot, {
17414
+ query: input.query,
17415
+ boardId: input.boardId,
17416
+ assignedAgent: input.agentId,
17417
+ status: input.status,
17418
+ priority: input.priority,
17419
+ label: input.labels?.[0],
17420
+ chainId: input.chainId
17421
+ });
17422
+ return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
16462
17423
  }
16463
- const result = await splitTask(projectRoot, input.boardId, input.taskId, {
16464
- titles: input.childTitles,
16465
- ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
16466
- ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
16467
- ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
16468
- ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
16469
- ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
16470
- ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
16471
- ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
16472
- ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
16473
- });
16474
- return result ? {
16475
- ok: true,
16476
- message: `${result.children.length} child task(s) created.`,
16477
- board: result.board,
16478
- task: result.parent,
16479
- children: result.children
16480
- } : fail("Task not found.");
16481
- }
16482
- case "merge_tasks": {
16483
- if (!input.boardId || !input.taskIds?.length || !input.title) {
16484
- return fail("merge_tasks requires boardId, taskIds, and title.");
17424
+ case "ready_tasks": {
17425
+ const tasks = await listReadyTasks(projectRoot, {
17426
+ query: input.query,
17427
+ boardId: input.boardId,
17428
+ assignedAgent: input.agentId,
17429
+ priority: input.priority,
17430
+ label: input.labels?.[0],
17431
+ chainId: input.chainId,
17432
+ limit: input.limit
17433
+ });
17434
+ return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
16485
17435
  }
16486
- const result = await mergeTasks(projectRoot, input.boardId, {
16487
- taskIds: input.taskIds,
16488
- title: input.title,
16489
- ...input.description !== void 0 ? { description: input.description } : {},
16490
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16491
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16492
- ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
16493
- });
16494
- return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
16495
- }
16496
- case "copy_task": {
16497
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
16498
- return fail("copy_task requires boardId, taskId, and targetBoardId.");
17436
+ case "snapshot": {
17437
+ const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
17438
+ query: input.query,
17439
+ boardId: input.boardId,
17440
+ assignedAgent: input.agentId,
17441
+ status: input.status,
17442
+ priority: input.priority,
17443
+ label: input.labels?.[0],
17444
+ chainId: input.chainId
17445
+ });
17446
+ return {
17447
+ ok: true,
17448
+ message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
17449
+ snapshot
17450
+ };
16499
17451
  }
16500
- const result = await copyTaskToBoard(
16501
- projectRoot,
16502
- input.boardId,
16503
- input.taskId,
16504
- input.targetBoardId,
16505
- {
16506
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16507
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
16508
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16509
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17452
+ case "add_column": {
17453
+ if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
17454
+ const result2 = await addColumn(projectRoot, input.boardId, {
17455
+ title: input.title,
17456
+ ...input.description !== void 0 ? { description: input.description } : {}
17457
+ });
17458
+ return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
17459
+ }
17460
+ case "update_column": {
17461
+ if (!input.boardId || !input.columnId)
17462
+ return fail("update_column requires boardId and columnId.");
17463
+ const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
17464
+ ...input.title !== void 0 ? { title: input.title } : {},
17465
+ ...input.description !== void 0 ? { description: input.description } : {},
17466
+ ...input.order !== void 0 ? { order: input.order } : {}
17467
+ });
17468
+ return board ? okBoard(board, "Column updated.") : fail("Column not found.");
17469
+ }
17470
+ case "delete_column": {
17471
+ if (!input.boardId || !input.columnId)
17472
+ return fail("delete_column requires boardId and columnId.");
17473
+ const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
17474
+ moveTasksToColumnId: input.moveTasksToColumnId
17475
+ });
17476
+ return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
17477
+ }
17478
+ case "add_task": {
17479
+ if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
17480
+ const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
17481
+ return result2 ? okTask(result2.board, result2.task, "Task added.") : fail("Board not found.");
17482
+ }
17483
+ case "split_task": {
17484
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
17485
+ return fail("split_task requires boardId, taskId, and childTitles.");
16510
17486
  }
16511
- );
16512
- return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
16513
- }
16514
- case "transfer_task": {
16515
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
16516
- return fail("transfer_task requires boardId, taskId, and targetBoardId.");
17487
+ const result2 = await splitTask(projectRoot, input.boardId, input.taskId, {
17488
+ titles: input.childTitles,
17489
+ ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
17490
+ ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
17491
+ ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
17492
+ ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
17493
+ ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
17494
+ ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
17495
+ ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
17496
+ ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
17497
+ });
17498
+ return result2 ? {
17499
+ ok: true,
17500
+ message: `${result2.children.length} child task(s) created.`,
17501
+ board: result2.board,
17502
+ task: result2.parent,
17503
+ children: result2.children
17504
+ } : fail("Task not found.");
16517
17505
  }
16518
- const result = await transferTaskToBoard(
16519
- projectRoot,
16520
- input.boardId,
16521
- input.taskId,
16522
- input.targetBoardId,
16523
- {
17506
+ case "merge_tasks": {
17507
+ if (!input.boardId || !input.taskIds?.length || !input.title) {
17508
+ return fail("merge_tasks requires boardId, taskIds, and title.");
17509
+ }
17510
+ const result2 = await mergeTasks(projectRoot, input.boardId, {
17511
+ taskIds: input.taskIds,
17512
+ title: input.title,
17513
+ ...input.description !== void 0 ? { description: input.description } : {},
16524
17514
  ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16525
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
16526
17515
  ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16527
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17516
+ ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
17517
+ });
17518
+ return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
17519
+ }
17520
+ case "copy_task": {
17521
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
17522
+ return fail("copy_task requires boardId, taskId, and targetBoardId.");
16528
17523
  }
16529
- );
16530
- return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
16531
- }
16532
- case "get_task": {
16533
- if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
16534
- const task = await getTask(projectRoot, input.boardId, input.taskId);
16535
- return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
16536
- }
16537
- case "update_task": {
16538
- if (!input.boardId || !input.taskId)
16539
- return fail("update_task requires boardId and taskId.");
16540
- const board = await updateTask(
16541
- projectRoot,
16542
- input.boardId,
16543
- input.taskId,
16544
- taskPatch(input)
16545
- );
16546
- return board ? okBoard(board, "Task updated.") : fail("Task not found.");
16547
- }
16548
- case "move_task": {
16549
- if (!input.boardId || !input.taskId || !input.targetColumnId) {
16550
- return fail("move_task requires boardId, taskId, and targetColumnId.");
17524
+ const result2 = await copyTaskToBoard(
17525
+ projectRoot,
17526
+ input.boardId,
17527
+ input.taskId,
17528
+ input.targetBoardId,
17529
+ {
17530
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
17531
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
17532
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
17533
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17534
+ }
17535
+ );
17536
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
16551
17537
  }
16552
- const board = await moveTask(
16553
- projectRoot,
16554
- input.boardId,
16555
- input.taskId,
16556
- input.targetColumnId,
16557
- input.order
16558
- );
16559
- return board ? okBoard(board, "Task moved.") : fail("Move failed.");
16560
- }
16561
- case "delete_task": {
16562
- if (!input.boardId || !input.taskId)
16563
- return fail("delete_task requires boardId and taskId.");
16564
- const board = await removeTask(projectRoot, input.boardId, input.taskId);
16565
- return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
16566
- }
16567
- case "set_chain": {
16568
- if (!input.boardId || !input.taskIds?.length) {
16569
- return fail("set_chain requires boardId and taskIds.");
17538
+ case "transfer_task": {
17539
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
17540
+ return fail("transfer_task requires boardId, taskId, and targetBoardId.");
17541
+ }
17542
+ const result2 = await transferTaskToBoard(
17543
+ projectRoot,
17544
+ input.boardId,
17545
+ input.taskId,
17546
+ input.targetBoardId,
17547
+ {
17548
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
17549
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
17550
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
17551
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17552
+ }
17553
+ );
17554
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
16570
17555
  }
16571
- const result = await setTaskChain(projectRoot, input.boardId, {
16572
- taskIds: input.taskIds,
16573
- ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
16574
- ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
16575
- });
16576
- return result ? {
16577
- ok: true,
16578
- message: `Chain set: ${result.chainId}`,
16579
- board: result.board,
16580
- chain: result.tasks
16581
- } : fail("Board or task not found.");
16582
- }
16583
- case "get_chain": {
16584
- if (!input.boardId || !(input.taskId || input.chainId)) {
16585
- return fail("get_chain requires boardId and taskId or chainId.");
17556
+ case "get_task": {
17557
+ if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
17558
+ const task = await getTask(projectRoot, input.boardId, input.taskId);
17559
+ return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
16586
17560
  }
16587
- const result = await getTaskChain(
16588
- projectRoot,
16589
- input.boardId,
16590
- input.taskId ?? input.chainId ?? ""
16591
- );
16592
- return result ? {
16593
- ok: true,
16594
- message: `Chain loaded: ${result.chainId}`,
16595
- board: result.board,
16596
- chain: result.tasks
16597
- } : fail("Chain not found.");
16598
- }
16599
- case "claim_task": {
16600
- const result = await claimReadyTask(projectRoot, {
16601
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
16602
- ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
16603
- ...assignmentInput(input),
16604
- status: input.assignmentStatus ?? "queued"
16605
- });
16606
- return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
16607
- }
16608
- case "release_task": {
16609
- if (!input.boardId || !input.taskId) {
16610
- return fail("release_task requires boardId and taskId.");
17561
+ case "update_task": {
17562
+ if (!input.boardId || !input.taskId)
17563
+ return fail("update_task requires boardId and taskId.");
17564
+ const board = await updateTask(
17565
+ projectRoot,
17566
+ input.boardId,
17567
+ input.taskId,
17568
+ taskPatch(input)
17569
+ );
17570
+ return board ? okBoard(board, "Task updated.") : fail("Task not found.");
16611
17571
  }
16612
- const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
16613
- ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
16614
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
16615
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
16616
- });
16617
- return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
16618
- }
16619
- case "assign_task": {
16620
- if (!input.boardId || !input.taskId)
16621
- return fail("assign_task requires boardId and taskId.");
16622
- const board = await assignTask(
16623
- projectRoot,
16624
- input.boardId,
16625
- input.taskId,
16626
- assignmentInput(input)
16627
- );
16628
- return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
16629
- }
16630
- case "mark_assignment": {
16631
- if (!input.boardId || !input.taskId)
16632
- return fail("mark_assignment requires boardId and taskId.");
16633
- const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
16634
- const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
16635
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
16636
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
16637
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
16638
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
16639
- ...input.error !== void 0 ? { error: input.error } : {},
16640
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
16641
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
16642
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
16643
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
16644
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
16645
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
16646
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
16647
- });
16648
- return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
16649
- }
16650
- case "heartbeat_assignment": {
16651
- if (!input.boardId || !input.taskId) {
16652
- return fail("heartbeat_assignment requires boardId and taskId.");
17572
+ case "transition_task": {
17573
+ if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
17574
+ return fail(
17575
+ "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
17576
+ );
17577
+ }
17578
+ const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
17579
+ to: input.lifecycleStage,
17580
+ actor: input.author,
17581
+ comment: input.transitionComment,
17582
+ ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
17583
+ ...input.attachmentUrl !== void 0 ? {
17584
+ attachment: {
17585
+ url: input.attachmentUrl,
17586
+ type: input.attachmentType ?? "url",
17587
+ ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
17588
+ }
17589
+ } : {},
17590
+ patch: taskPatch(input)
17591
+ });
17592
+ return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
16653
17593
  }
16654
- const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
16655
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
16656
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
16657
- });
16658
- return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
16659
- }
16660
- case "recover_stale": {
16661
- if (!input.boardId) return fail("recover_stale requires boardId.");
16662
- const policyFields = [
16663
- input.recoveryPolicyFailOnCostCeiling !== void 0,
16664
- input.recoveryPolicyReleaseOnFailureKinds !== void 0,
16665
- input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
16666
- input.recoveryPolicyRetryPolicyOverride !== void 0
16667
- ].some(Boolean);
16668
- const result = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
16669
- ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
16670
- ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
16671
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
16672
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
16673
- ...policyFields ? {
16674
- policy: {
16675
- ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
16676
- ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
16677
- releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
16678
- } : {},
16679
- ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
16680
- releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
16681
- } : {},
16682
- ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
16683
- retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
16684
- } : {}
16685
- }
16686
- } : {}
16687
- });
16688
- return result ? {
16689
- ok: true,
16690
- message: `Recovered ${result.tasks.length} stale assignment(s).`,
16691
- board: result.board,
16692
- recoveredTasks: result.tasks
16693
- } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
16694
- }
16695
- case "events": {
16696
- if (!input.boardId) return fail("events requires boardId.");
16697
- const eventList = await listKanbanEvents(projectRoot, input.boardId);
16698
- return {
16699
- ok: true,
16700
- message: `${eventList.length} event(s).`,
16701
- events: eventList
16702
- };
16703
- }
16704
- case "queue_health": {
16705
- const health = await getKanbanQueueHealth(projectRoot, {
16706
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
16707
- });
16708
- return {
16709
- ok: true,
16710
- message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
16711
- queueHealth: health
16712
- };
16713
- }
16714
- case "add_dependency": {
16715
- if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
16716
- return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
17594
+ case "move_task": {
17595
+ if (!input.boardId || !input.taskId || !input.targetColumnId) {
17596
+ return fail("move_task requires boardId, taskId, and targetColumnId.");
17597
+ }
17598
+ const board = await moveTask(
17599
+ projectRoot,
17600
+ input.boardId,
17601
+ input.taskId,
17602
+ input.targetColumnId,
17603
+ input.order
17604
+ );
17605
+ return board ? okBoard(board, "Task moved.") : fail("Move failed.");
16717
17606
  }
16718
- const board = await addDependency(
16719
- projectRoot,
16720
- input.boardId,
16721
- input.taskId,
16722
- input.dependencyTaskId
16723
- );
16724
- return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
16725
- }
16726
- case "add_goal_metric": {
16727
- if (!input.boardId || !input.taskId || !input.metricName) {
16728
- return fail("add_goal_metric requires boardId, taskId, and metricName.");
17607
+ case "delete_task": {
17608
+ if (!input.boardId || !input.taskId)
17609
+ return fail("delete_task requires boardId and taskId.");
17610
+ const board = await removeTask(projectRoot, input.boardId, input.taskId);
17611
+ return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
16729
17612
  }
16730
- const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
16731
- name: input.metricName,
16732
- ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
16733
- ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
16734
- ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
16735
- ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
16736
- ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
16737
- });
16738
- return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
16739
- }
16740
- case "update_goal_metric": {
16741
- if (!input.boardId || !input.taskId || !input.metricId) {
16742
- return fail("update_goal_metric requires boardId, taskId, and metricId.");
17613
+ case "set_chain": {
17614
+ if (!input.boardId || !input.taskIds?.length) {
17615
+ return fail("set_chain requires boardId and taskIds.");
17616
+ }
17617
+ const result2 = await setTaskChain(projectRoot, input.boardId, {
17618
+ taskIds: input.taskIds,
17619
+ ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
17620
+ ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
17621
+ });
17622
+ return result2 ? {
17623
+ ok: true,
17624
+ message: `Chain set: ${result2.chainId}`,
17625
+ board: result2.board,
17626
+ chain: result2.tasks
17627
+ } : fail("Board or task not found.");
16743
17628
  }
16744
- const board = await updateGoalMetricOnTask(
16745
- projectRoot,
16746
- input.boardId,
16747
- input.taskId,
16748
- input.metricId,
16749
- {
16750
- ...input.metricName !== void 0 ? { name: input.metricName } : {},
17629
+ case "get_chain": {
17630
+ if (!input.boardId || !(input.taskId || input.chainId)) {
17631
+ return fail("get_chain requires boardId and taskId or chainId.");
17632
+ }
17633
+ const result2 = await getTaskChain(
17634
+ projectRoot,
17635
+ input.boardId,
17636
+ input.taskId ?? input.chainId ?? ""
17637
+ );
17638
+ return result2 ? {
17639
+ ok: true,
17640
+ message: `Chain loaded: ${result2.chainId}`,
17641
+ board: result2.board,
17642
+ chain: result2.tasks
17643
+ } : fail("Chain not found.");
17644
+ }
17645
+ case "claim_task": {
17646
+ const result2 = await claimReadyTask(projectRoot, {
17647
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
17648
+ ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
17649
+ ...assignmentInput(input),
17650
+ status: input.assignmentStatus ?? "queued"
17651
+ });
17652
+ return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
17653
+ }
17654
+ case "release_task": {
17655
+ if (!input.boardId || !input.taskId) {
17656
+ return fail("release_task requires boardId and taskId.");
17657
+ }
17658
+ const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
17659
+ ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
17660
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17661
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
17662
+ });
17663
+ return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
17664
+ }
17665
+ case "assign_task": {
17666
+ if (!input.boardId || !input.taskId)
17667
+ return fail("assign_task requires boardId and taskId.");
17668
+ const board = await assignTask(
17669
+ projectRoot,
17670
+ input.boardId,
17671
+ input.taskId,
17672
+ assignmentInput(input)
17673
+ );
17674
+ return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
17675
+ }
17676
+ case "mark_assignment": {
17677
+ if (!input.boardId || !input.taskId)
17678
+ return fail("mark_assignment requires boardId and taskId.");
17679
+ const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
17680
+ const board = await updateTaskAssignment(
17681
+ projectRoot,
17682
+ input.boardId,
17683
+ input.taskId,
17684
+ {
17685
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17686
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17687
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17688
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17689
+ ...input.error !== void 0 ? { error: input.error } : {},
17690
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17691
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17692
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17693
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17694
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17695
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17696
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17697
+ },
17698
+ // Ownership fence: when expectedLeaseId is supplied, the write is
17699
+ // applied only if the current assignment still holds this lease.
17700
+ // This prevents a recovered+reassigned stale worker's terminal
17701
+ // mark_assignment from overwriting the successor's state. The check
17702
+ // is atomic inside updateTaskAssignment's mutateBoard lock.
17703
+ input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17704
+ );
17705
+ return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
17706
+ }
17707
+ case "heartbeat_assignment": {
17708
+ if (!input.boardId || !input.taskId) {
17709
+ return fail("heartbeat_assignment requires boardId and taskId.");
17710
+ }
17711
+ const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
17712
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17713
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17714
+ // Ownership fence: when expectedLeaseId is supplied, the renewal
17715
+ // is applied only if the current assignment still holds this lease.
17716
+ // This prevents a recovered+reassigned stale worker's heartbeat
17717
+ // from renewing the successor's lease. The check is atomic inside
17718
+ // heartbeatTaskAssignment's mutateBoard lock.
17719
+ ...input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17720
+ });
17721
+ return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
17722
+ }
17723
+ case "recover_stale": {
17724
+ if (!input.boardId) return fail("recover_stale requires boardId.");
17725
+ const policyFields = [
17726
+ input.recoveryPolicyFailOnCostCeiling !== void 0,
17727
+ input.recoveryPolicyReleaseOnFailureKinds !== void 0,
17728
+ input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
17729
+ input.recoveryPolicyRetryPolicyOverride !== void 0
17730
+ ].some(Boolean);
17731
+ const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
17732
+ ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
17733
+ ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
17734
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17735
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
17736
+ ...policyFields ? {
17737
+ policy: {
17738
+ ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
17739
+ ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
17740
+ releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
17741
+ } : {},
17742
+ ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
17743
+ releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
17744
+ } : {},
17745
+ ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
17746
+ retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
17747
+ } : {}
17748
+ }
17749
+ } : {}
17750
+ });
17751
+ return result2 ? {
17752
+ ok: true,
17753
+ message: `Recovered ${result2.tasks.length} stale assignment(s).`,
17754
+ board: result2.board,
17755
+ recoveredTasks: result2.tasks
17756
+ } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
17757
+ }
17758
+ case "events": {
17759
+ if (!input.boardId) return fail("events requires boardId.");
17760
+ const eventList = await listKanbanEvents(projectRoot, input.boardId);
17761
+ return {
17762
+ ok: true,
17763
+ message: `${eventList.length} event(s).`,
17764
+ events: eventList
17765
+ };
17766
+ }
17767
+ case "queue_health": {
17768
+ const health = await getKanbanQueueHealth(projectRoot, {
17769
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
17770
+ });
17771
+ return {
17772
+ ok: true,
17773
+ message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
17774
+ queueHealth: health
17775
+ };
17776
+ }
17777
+ case "add_dependency": {
17778
+ if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
17779
+ return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
17780
+ }
17781
+ const board = await addDependency(
17782
+ projectRoot,
17783
+ input.boardId,
17784
+ input.taskId,
17785
+ input.dependencyTaskId
17786
+ );
17787
+ return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
17788
+ }
17789
+ case "add_goal_metric": {
17790
+ if (!input.boardId || !input.taskId || !input.metricName) {
17791
+ return fail("add_goal_metric requires boardId, taskId, and metricName.");
17792
+ }
17793
+ const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
17794
+ name: input.metricName,
16751
17795
  ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
16752
17796
  ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
16753
17797
  ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
16754
17798
  ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
16755
17799
  ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17800
+ });
17801
+ return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
17802
+ }
17803
+ case "update_goal_metric": {
17804
+ if (!input.boardId || !input.taskId || !input.metricId) {
17805
+ return fail("update_goal_metric requires boardId, taskId, and metricId.");
16756
17806
  }
16757
- );
16758
- return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
16759
- }
16760
- case "add_check": {
16761
- if (!input.boardId || !input.taskId || !input.checkDescription) {
16762
- return fail("add_check requires boardId, taskId, and checkDescription.");
17807
+ const board = await updateGoalMetricOnTask(
17808
+ projectRoot,
17809
+ input.boardId,
17810
+ input.taskId,
17811
+ input.metricId,
17812
+ {
17813
+ ...input.metricName !== void 0 ? { name: input.metricName } : {},
17814
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
17815
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17816
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17817
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17818
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17819
+ }
17820
+ );
17821
+ return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
16763
17822
  }
16764
- const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
16765
- description: input.checkDescription,
16766
- type: "manual",
16767
- status: input.checkStatus
16768
- });
16769
- return board ? okBoard(board, "Check added.") : fail("Task not found.");
16770
- }
16771
- case "update_check": {
16772
- if (!input.boardId || !input.taskId || !input.checkId) {
16773
- return fail("update_check requires boardId, taskId, and checkId.");
17823
+ case "add_check": {
17824
+ if (!input.boardId || !input.taskId || !input.checkDescription) {
17825
+ return fail("add_check requires boardId, taskId, and checkDescription.");
17826
+ }
17827
+ const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
17828
+ description: input.checkDescription,
17829
+ type: "manual",
17830
+ status: input.checkStatus
17831
+ });
17832
+ return board ? okBoard(board, "Check added.") : fail("Task not found.");
16774
17833
  }
16775
- const board = await updateCheckOnTask(
16776
- projectRoot,
16777
- input.boardId,
16778
- input.taskId,
16779
- input.checkId,
16780
- {
16781
- ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
16782
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17834
+ case "update_check": {
17835
+ if (!input.boardId || !input.taskId || !input.checkId) {
17836
+ return fail("update_check requires boardId, taskId, and checkId.");
16783
17837
  }
16784
- );
16785
- return board ? okBoard(board, "Check updated.") : fail("Check not found.");
16786
- }
16787
- case "add_note": {
16788
- if (!input.boardId || !input.taskId || !input.note)
16789
- return fail("add_note requires boardId, taskId, and note.");
16790
- const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
16791
- author: input.author ?? "agent",
16792
- content: input.note
16793
- });
16794
- return board ? okBoard(board, "Note added.") : fail("Task not found.");
16795
- }
16796
- case "add_link": {
16797
- if (!input.boardId || !input.taskId || !input.url)
16798
- return fail("add_link requires boardId, taskId, and url.");
16799
- const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
16800
- url: input.url,
16801
- type: input.linkType ?? "url",
16802
- ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
16803
- });
16804
- return board ? okBoard(board, "Link added.") : fail("Task not found.");
17838
+ const board = await updateCheckOnTask(
17839
+ projectRoot,
17840
+ input.boardId,
17841
+ input.taskId,
17842
+ input.checkId,
17843
+ {
17844
+ ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
17845
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17846
+ }
17847
+ );
17848
+ return board ? okBoard(board, "Check updated.") : fail("Check not found.");
17849
+ }
17850
+ case "add_note": {
17851
+ if (!input.boardId || !input.taskId || !input.note)
17852
+ return fail("add_note requires boardId, taskId, and note.");
17853
+ const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
17854
+ author: input.author ?? "agent",
17855
+ content: input.note
17856
+ });
17857
+ return board ? okBoard(board, "Note added.") : fail("Task not found.");
17858
+ }
17859
+ case "add_link": {
17860
+ if (!input.boardId || !input.taskId || !input.url)
17861
+ return fail("add_link requires boardId, taskId, and url.");
17862
+ const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
17863
+ url: input.url,
17864
+ type: input.linkType ?? "url",
17865
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17866
+ });
17867
+ return board ? okBoard(board, "Link added.") : fail("Task not found.");
17868
+ }
17869
+ default:
17870
+ return fail(`Unknown kanban action: ${input.action}`);
16805
17871
  }
16806
- default:
16807
- return fail(`Unknown kanban action: ${input.action}`);
16808
- }
17872
+ })();
17873
+ return withPresence(result);
16809
17874
  } catch (err) {
16810
17875
  return fail(err instanceof Error ? err.message : String(err));
16811
17876
  }
@@ -16829,6 +17894,7 @@ function taskInput(input) {
16829
17894
  title: input.title ?? "",
16830
17895
  columnId: input.columnId,
16831
17896
  description: input.description,
17897
+ dueDate: input.dueDate,
16832
17898
  priority: input.priority,
16833
17899
  ...input.taskType !== void 0 ? { type: input.taskType } : {},
16834
17900
  status: input.status,
@@ -16837,7 +17903,67 @@ function taskInput(input) {
16837
17903
  ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
16838
17904
  ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16839
17905
  ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
16840
- ...assignment ? { assignment } : {}
17906
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
17907
+ ...assignment ? { assignment } : {},
17908
+ ...input.order !== void 0 ? { order: input.order } : {},
17909
+ // Sprint 3 durable task-level fields
17910
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
17911
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
17912
+ // Relationships — chainId alone cannot construct a KanbanTaskChainRef,
17913
+ // so chain/previousTaskId/nextTaskId must be set via update_task.
17914
+ ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
17915
+ // Detail objects
17916
+ ...input.checkDescription !== void 0 ? {
17917
+ successCriteria: [
17918
+ {
17919
+ id: randomUUID(),
17920
+ description: input.checkDescription,
17921
+ type: "manual",
17922
+ status: input.checkStatus ?? "pending"
17923
+ }
17924
+ ]
17925
+ } : {},
17926
+ ...input.metricName !== void 0 ? {
17927
+ goalMetrics: [
17928
+ {
17929
+ id: randomUUID(),
17930
+ name: input.metricName,
17931
+ status: input.metricStatus ?? "pending",
17932
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17933
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17934
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17935
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17936
+ }
17937
+ ]
17938
+ } : {},
17939
+ ...input.url !== void 0 ? {
17940
+ links: [
17941
+ {
17942
+ url: input.url,
17943
+ type: input.linkType ?? "url",
17944
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17945
+ }
17946
+ ]
17947
+ } : {},
17948
+ ...input.note !== void 0 ? {
17949
+ notes: [
17950
+ {
17951
+ id: randomUUID(),
17952
+ author: input.author ?? "agent",
17953
+ content: input.note,
17954
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
17955
+ }
17956
+ ]
17957
+ } : {},
17958
+ // Origin tracking
17959
+ ...input.graphId !== void 0 ? {
17960
+ origin: {
17961
+ system: input.sourceSystem ?? "kanban-tool",
17962
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
17963
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
17964
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
17965
+ }
17966
+ } : {}
16841
17967
  };
16842
17968
  }
16843
17969
  function mergedDependsOn(input) {
@@ -16851,6 +17977,7 @@ function taskPatch(input) {
16851
17977
  return {
16852
17978
  title: input.title,
16853
17979
  description: input.description,
17980
+ dueDate: input.dueDate,
16854
17981
  columnId: input.columnId,
16855
17982
  order: input.order,
16856
17983
  priority: input.priority,
@@ -17612,7 +18739,7 @@ import {
17612
18739
  mutateTasks as mutateTasks2,
17613
18740
  formatTaskList
17614
18741
  } from "@wrongstack/core";
17615
- import { randomUUID } from "node:crypto";
18742
+ import { randomUUID as randomUUID2 } from "node:crypto";
17616
18743
  var planTool = {
17617
18744
  name: "plan",
17618
18745
  category: "Session",
@@ -17838,7 +18965,7 @@ var planTool = {
17838
18965
  try {
17839
18966
  const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
17840
18967
  f.tasks.push({
17841
- id: `task_${randomUUID()}`,
18968
+ id: `task_${randomUUID2()}`,
17842
18969
  title: taskifyMeta.title,
17843
18970
  description: taskifyMeta.details || void 0,
17844
18971
  type: "feature",
@@ -18980,7 +20107,7 @@ import {
18980
20107
  mutatePlan as mutatePlan3,
18981
20108
  formatPlan as formatPlan2
18982
20109
  } from "@wrongstack/core";
18983
- import { randomUUID as randomUUID2 } from "node:crypto";
20110
+ import { randomUUID as randomUUID3 } from "node:crypto";
18984
20111
  function findTaskIndex(tasks, query) {
18985
20112
  const asNum = Number.parseInt(query, 10);
18986
20113
  if (!Number.isNaN(asNum)) {
@@ -19168,7 +20295,7 @@ var taskTool = {
19168
20295
  }
19169
20296
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
19170
20297
  const newTask = {
19171
- id: `task_${Date.now()}_${randomUUID2().slice(0, 8)}`,
20298
+ id: `task_${Date.now()}_${randomUUID3().slice(0, 8)}`,
19172
20299
  title: t.title,
19173
20300
  description: t.description,
19174
20301
  type: t.type || "feature",
@@ -19229,7 +20356,7 @@ var taskTool = {
19229
20356
  });
19230
20357
  if (match.description) {
19231
20358
  todos.push({
19232
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
20359
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
19233
20360
  content: match.description.slice(0, 200),
19234
20361
  status: "pending",
19235
20362
  promotedFromTask: match.id
@@ -19238,7 +20365,7 @@ var taskTool = {
19238
20365
  if (input.subtasks && input.subtasks.length > 0) {
19239
20366
  for (const st of input.subtasks) {
19240
20367
  todos.push({
19241
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
20368
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
19242
20369
  content: st,
19243
20370
  status: "pending",
19244
20371
  promotedFromTask: match.id
@@ -19944,8 +21071,8 @@ var DEFAULT_IGNORE5 = [
19944
21071
  var treeTool = {
19945
21072
  name: "tree",
19946
21073
  category: "Filesystem",
19947
- description: "Display a directory tree of the project (or a subpath). This is the recommended way to explore the high-level structure of a codebase before reading specific files.",
19948
- usageHint: "BEST PRACTICE FOR INITIAL EXPLORATION:\n\n- Call early when working with an unfamiliar project or module.\n- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\n- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\nOutput is truncated for very large trees.",
21074
+ 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.",
21075
+ usageHint: "DIRECTORY-LAYOUT EXPLORATION:\n\n- When `codebase-search` is live, use it first for symbols and concepts; use `tree` when the directory hierarchy itself matters.\n- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\n- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\nOutput is truncated for very large trees.",
19949
21076
  permission: "auto",
19950
21077
  mutating: false,
19951
21078
  capabilities: ["fs.read"],
@@ -20348,15 +21475,15 @@ var OPTIONAL_TOOLS = [
20348
21475
  toolUseTool,
20349
21476
  batchToolUseTool,
20350
21477
  toolHelpTool,
20351
- codebaseIndexTool,
20352
- codebaseSearchTool,
20353
- codebaseStatsTool,
20354
21478
  setWorkingDirTool
20355
21479
  ];
20356
21480
  var TIER1_TOOLS = [
20357
21481
  readTool,
20358
21482
  writeTool,
20359
21483
  editTool,
21484
+ codebaseStatsTool,
21485
+ codebaseSearchTool,
21486
+ codebaseIndexTool,
20360
21487
  bashTool,
20361
21488
  grepTool,
20362
21489
  globTool,
@@ -20395,9 +21522,6 @@ var TIER3_TOOLS = [
20395
21522
  toolUseTool,
20396
21523
  batchToolUseTool,
20397
21524
  toolHelpTool,
20398
- codebaseIndexTool,
20399
- codebaseSearchTool,
20400
- codebaseStatsTool,
20401
21525
  setWorkingDirTool
20402
21526
  ];
20403
21527
  var builtinTools = [
@@ -20406,6 +21530,9 @@ var builtinTools = [
20406
21530
  readTool,
20407
21531
  writeTool,
20408
21532
  editTool,
21533
+ codebaseStatsTool,
21534
+ codebaseSearchTool,
21535
+ codebaseIndexTool,
20409
21536
  replaceTool,
20410
21537
  globTool,
20411
21538
  grepTool,
@@ -20440,9 +21567,6 @@ var builtinTools = [
20440
21567
  toolUseTool,
20441
21568
  batchToolUseTool,
20442
21569
  toolHelpTool,
20443
- codebaseIndexTool,
20444
- codebaseSearchTool,
20445
- codebaseStatsTool,
20446
21570
  setWorkingDirTool
20447
21571
  ];
20448
21572
 
@@ -21680,6 +22804,7 @@ export {
21680
22804
  TOOL_ICON_CONFIG,
21681
22805
  TOOL_ICON_MAP,
21682
22806
  _resetProcessRegistry,
22807
+ applySessionKanbanBoardToTodos,
21683
22808
  applySessionKanbanTaskToSource,
21684
22809
  assertBrowserUrlAllowed,
21685
22810
  attachSessionKanbanMirror,
@@ -21731,6 +22856,7 @@ export {
21731
22856
  executeLanguagePlan,
21732
22857
  executePackagePlan,
21733
22858
  fetchTool,
22859
+ fileGraphService,
21734
22860
  forgetTool,
21735
22861
  formatGlobalStatus,
21736
22862
  formatInstanceList,
@@ -21770,6 +22896,7 @@ export {
21770
22896
  normalizeShell,
21771
22897
  onIndexStateChange,
21772
22898
  outdatedTool,
22899
+ packageGraphService,
21773
22900
  parseLanguageDiagnostics,
21774
22901
  parsePackageReports,
21775
22902
  parsePrivateOriginAllowlist,
@@ -21800,6 +22927,7 @@ export {
21800
22927
  shutdownCodebaseIndexHost,
21801
22928
  startProcessGuardian,
21802
22929
  stopProcessGuardian,
22930
+ symbolGraphService,
21803
22931
  testTool,
21804
22932
  todoTool,
21805
22933
  toolHelpTool,