@wrongstack/tools 0.286.0 → 0.289.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +2 -2
  2. package/dist/_edit-match.d.ts.map +1 -1
  3. package/dist/_fetch-guard.d.ts.map +1 -1
  4. package/dist/_redact-command.d.ts.map +1 -1
  5. package/dist/_spawn-stream.d.ts.map +1 -1
  6. package/dist/audit.js +61 -14
  7. package/dist/audit.js.map +3 -3
  8. package/dist/bash.d.ts +1 -1
  9. package/dist/bash.d.ts.map +1 -1
  10. package/dist/bash.js +97 -19
  11. package/dist/bash.js.map +3 -3
  12. package/dist/batch-tool-use.d.ts +1 -1
  13. package/dist/batch-tool-use.d.ts.map +1 -1
  14. package/dist/batch-tool-use.js +26 -8
  15. package/dist/batch-tool-use.js.map +2 -2
  16. package/dist/browser/index.js +1 -1
  17. package/dist/browser/index.js.map +1 -1
  18. package/dist/browser/types.d.ts +1 -1
  19. package/dist/browser/types.d.ts.map +1 -1
  20. package/dist/builtin.d.ts +12 -10
  21. package/dist/builtin.d.ts.map +1 -1
  22. package/dist/builtin.js +2107 -864
  23. package/dist/builtin.js.map +4 -4
  24. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  25. package/dist/codebase-index/bm25.d.ts +1 -2
  26. package/dist/codebase-index/bm25.d.ts.map +1 -1
  27. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  28. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  29. package/dist/codebase-index/codebase-stats-tool.d.ts +7 -0
  30. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  31. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  32. package/dist/codebase-index/index-service.d.ts +11 -1
  33. package/dist/codebase-index/index-service.d.ts.map +1 -1
  34. package/dist/codebase-index/index.d.ts +2 -1
  35. package/dist/codebase-index/index.d.ts.map +1 -1
  36. package/dist/codebase-index/index.js +934 -283
  37. package/dist/codebase-index/index.js.map +4 -4
  38. package/dist/codebase-index/indexer.d.ts.map +1 -1
  39. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  40. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  41. package/dist/codebase-index/schema.d.ts +43 -1
  42. package/dist/codebase-index/schema.d.ts.map +1 -1
  43. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  44. package/dist/codebase-index/worker-protocol.d.ts +1 -0
  45. package/dist/codebase-index/worker-protocol.d.ts.map +1 -1
  46. package/dist/codebase-index/worker.js +793 -241
  47. package/dist/codebase-index/worker.js.map +4 -4
  48. package/dist/codebase-index/writer.d.ts +27 -1
  49. package/dist/codebase-index/writer.d.ts.map +1 -1
  50. package/dist/e2e.js +477 -0
  51. package/dist/e2e.js.map +7 -0
  52. package/dist/edit.d.ts.map +1 -1
  53. package/dist/edit.js +36 -12
  54. package/dist/edit.js.map +3 -3
  55. package/dist/exec.d.ts +1 -1
  56. package/dist/exec.d.ts.map +1 -1
  57. package/dist/exec.js +60 -5
  58. package/dist/exec.js.map +3 -3
  59. package/dist/fetch.js +2 -2
  60. package/dist/fetch.js.map +2 -2
  61. package/dist/format.js +61 -14
  62. package/dist/format.js.map +3 -3
  63. package/dist/glob.js +2 -2
  64. package/dist/glob.js.map +1 -1
  65. package/dist/grep.d.ts.map +1 -1
  66. package/dist/grep.js +2 -2
  67. package/dist/grep.js.map +2 -2
  68. package/dist/index.d.ts +3 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +2535 -865
  71. package/dist/index.js.map +4 -4
  72. package/dist/install.js +61 -14
  73. package/dist/install.js.map +3 -3
  74. package/dist/kanban.d.ts +16 -2
  75. package/dist/kanban.d.ts.map +1 -1
  76. package/dist/kanban.js +795 -532
  77. package/dist/kanban.js.map +4 -4
  78. package/dist/languages/detect.d.ts.map +1 -1
  79. package/dist/languages/index.js +61 -14
  80. package/dist/languages/index.js.map +3 -3
  81. package/dist/languages/types.d.ts +7 -0
  82. package/dist/languages/types.d.ts.map +1 -1
  83. package/dist/lint.js +61 -14
  84. package/dist/lint.js.map +3 -3
  85. package/dist/next-steps.d.ts +6 -2
  86. package/dist/next-steps.d.ts.map +1 -1
  87. package/dist/next-steps.js +3 -3
  88. package/dist/next-steps.js.map +2 -2
  89. package/dist/outdated.js +61 -14
  90. package/dist/outdated.js.map +3 -3
  91. package/dist/pack.js +2104 -861
  92. package/dist/pack.js.map +4 -4
  93. package/dist/plan.d.ts.map +1 -1
  94. package/dist/plan.js +158 -4
  95. package/dist/plan.js.map +4 -4
  96. package/dist/process-registry.js +12 -5
  97. package/dist/process-registry.js.map +2 -2
  98. package/dist/search.js +2 -2
  99. package/dist/search.js.map +2 -2
  100. package/dist/session-kanban.d.ts +47 -0
  101. package/dist/session-kanban.d.ts.map +1 -0
  102. package/dist/session-kanban.js +626 -0
  103. package/dist/session-kanban.js.map +7 -0
  104. package/dist/task.d.ts.map +1 -1
  105. package/dist/task.js +167 -4
  106. package/dist/task.js.map +4 -4
  107. package/dist/test.js +61 -14
  108. package/dist/test.js.map +3 -3
  109. package/dist/todo.d.ts.map +1 -1
  110. package/dist/todo.js +159 -7
  111. package/dist/todo.js.map +4 -4
  112. package/dist/tool-icon-map.d.ts.map +1 -1
  113. package/dist/tool-icons.d.ts.map +1 -1
  114. package/dist/tool-icons.js +7 -1
  115. package/dist/tool-icons.js.map +2 -2
  116. package/dist/tree.js +2 -2
  117. package/dist/tree.js.map +1 -1
  118. package/dist/typecheck.js +61 -14
  119. package/dist/typecheck.js.map +3 -3
  120. package/package.json +11 -3
package/dist/pack.js CHANGED
@@ -337,8 +337,10 @@ function redactCommand(cmd) {
337
337
  const flag = match.slice(0, match.indexOf(expectDefined(delim)) + 1);
338
338
  return `${flag}[REDACTED]`;
339
339
  }
340
- const flagEnd = match.match(/^--?[a-zA-Z][a-zA-Z0-9_-]*/)?.[0] ?? match;
341
- return `${flagEnd}=**redacted**`;
340
+ if (match.startsWith("--")) {
341
+ return match;
342
+ }
343
+ return `${match.slice(0, 2)}[REDACTED]`;
342
344
  });
343
345
  }
344
346
  return result;
@@ -350,9 +352,14 @@ var init_redact_command = __esm({
350
352
  SENSITIVE_FLAG_PATTERNS = [
351
353
  // --flag=value or --flag "value" (value captured up to next space or comma)
352
354
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
353
- // -f "value" style short flags
354
- /(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
355
- /(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
355
+ // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
356
+ // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
357
+ // NOTE: synced with @wrongstack/core observability/redact-command.ts.
358
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
359
+ // -p|-password|-a (redis auth) short flags: attached + separated + =value.
360
+ // Same token-start anchor; over-redaction is an accepted tradeoff for a
361
+ // redaction function. Synced with core copy.
362
+ /(?<![-\w])-(?:password|p|a)(?:[=\s]+)?[^\s,-]+/gi,
356
363
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
357
364
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
358
365
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
@@ -799,7 +806,12 @@ var init_win32_resolve = __esm({
799
806
 
800
807
  // src/_spawn-stream.ts
801
808
  import { spawn as spawn2 } from "node:child_process";
802
- import { buildChildEnv } from "@wrongstack/core";
809
+ import {
810
+ buildChildEnv,
811
+ emitProcessCompleted,
812
+ emitProcessOutput,
813
+ emitProcessStarted
814
+ } from "@wrongstack/core";
803
815
  async function* spawnStream(opts) {
804
816
  const max = opts.maxBytes ?? 2e5;
805
817
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -824,6 +836,19 @@ async function* spawnStream(opts) {
824
836
  });
825
837
  const registry = getProcessRegistry();
826
838
  const pid = child.pid;
839
+ const processStartedAt = Date.now();
840
+ let stdoutBytes = 0;
841
+ let stderrBytes = 0;
842
+ let telemetryCompleted = false;
843
+ emitProcessStarted({
844
+ ...pid !== void 0 ? { pid } : {},
845
+ parentPid: process.pid,
846
+ command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
847
+ args: redactCommand(opts.args.join(" ")).split(" ").filter(Boolean),
848
+ cwd: opts.cwd,
849
+ background: false,
850
+ startedAt: new Date(processStartedAt).toISOString()
851
+ });
827
852
  if (typeof pid === "number") {
828
853
  registry.register({
829
854
  pid,
@@ -852,6 +877,8 @@ async function* spawnStream(opts) {
852
877
  };
853
878
  const onOut = (c) => {
854
879
  const s = c.toString();
880
+ stdoutBytes += c.byteLength;
881
+ emitProcessOutput({ pid, stream: "stdout", chunk: c });
855
882
  if (stdout.length < max) stdout += s;
856
883
  spool.write(s);
857
884
  queue.push({ kind: "out", data: s });
@@ -864,6 +891,8 @@ async function* spawnStream(opts) {
864
891
  };
865
892
  const onErr = (c) => {
866
893
  const s = c.toString();
894
+ stderrBytes += c.byteLength;
895
+ emitProcessOutput({ pid, stream: "stderr", chunk: c });
867
896
  if (stderr.length < max) stderr += s;
868
897
  spool.write(s);
869
898
  queue.push({ kind: "err", data: s });
@@ -881,9 +910,25 @@ async function* spawnStream(opts) {
881
910
  queue.push({ kind: "error", data: e.message });
882
911
  wake();
883
912
  });
884
- child.on("close", (code) => {
913
+ const completeTelemetry = (code, signal, timedOut = false) => {
914
+ if (telemetryCompleted) return;
915
+ telemetryCompleted = true;
916
+ emitProcessCompleted({
917
+ ...pid !== void 0 ? { pid } : {},
918
+ exitCode: code,
919
+ ...signal ? { signal } : {},
920
+ durationMs: Date.now() - processStartedAt,
921
+ stdoutBytes,
922
+ stderrBytes,
923
+ timedOut,
924
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
925
+ });
926
+ };
927
+ child.on("close", (code, signal) => {
885
928
  if (typeof pid === "number") registry.unregister(pid);
886
- queue.push({ kind: "close", data: "", code: code ?? 0 });
929
+ const exitCode2 = code ?? (signal ? 1 : 0);
930
+ completeTelemetry(exitCode2, signal ?? void 0);
931
+ queue.push({ kind: "close", data: "", code: exitCode2, ...signal ? { signal } : {} });
887
932
  wake();
888
933
  });
889
934
  const onAbort = () => {
@@ -896,6 +941,7 @@ async function* spawnStream(opts) {
896
941
  }
897
942
  }
898
943
  queue.push({ kind: "close", data: "", code: 124 });
944
+ completeTelemetry(124, "SIGKILL", true);
899
945
  wake();
900
946
  };
901
947
  if (isWin) {
@@ -2503,13 +2549,14 @@ async function detectLanguageWorkspaces(options) {
2503
2549
  const target = options.target ? await canonicalInside(resolveFrom(cwd, options.target), projectRoot, "target") : void 0;
2504
2550
  const profiles = (options.profiles ?? languageProfileRegistry.list()).filter((profile) => !options.language || profile.id === options.language).slice().sort((a, b) => a.id.localeCompare(b.id));
2505
2551
  const limits = normalizeLimits(options.limits);
2552
+ const extraIgnores = new Set(options.ignoredDirectories ?? []);
2506
2553
  const state = {
2507
2554
  entries: 0,
2508
2555
  truncated: false,
2509
2556
  sourcePaths: /* @__PURE__ */ new Map(),
2510
2557
  candidates: /* @__PURE__ */ new Map()
2511
2558
  };
2512
- await scanDirectory(projectRoot, 0, profiles, limits, state, options.signal);
2559
+ await scanDirectory(projectRoot, 0, profiles, limits, state, extraIgnores, options.signal);
2513
2560
  addSourceFallbacks(projectRoot, profiles, state);
2514
2561
  if (target) addTargetEvidence(target, projectRoot, profiles, state);
2515
2562
  const workspaces = await Promise.all(
@@ -2523,7 +2570,7 @@ async function detectLanguageWorkspaces(options) {
2523
2570
  workspaces
2524
2571
  };
2525
2572
  }
2526
- async function scanDirectory(directory, depth, profiles, limits, state, signal) {
2573
+ async function scanDirectory(directory, depth, profiles, limits, state, extraIgnores, signal) {
2527
2574
  signal?.throwIfAborted();
2528
2575
  if (depth > limits.maxDepth || state.entries >= limits.maxEntries) {
2529
2576
  state.truncated = true;
@@ -2546,20 +2593,20 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
2546
2593
  const fullPath = path4.join(directory, entry.name);
2547
2594
  if (entry.isSymbolicLink()) continue;
2548
2595
  if (entry.isDirectory()) {
2549
- if (shouldIgnoreDirectory(entry.name, profiles)) continue;
2596
+ if (shouldIgnoreDirectory(entry.name, profiles, extraIgnores)) continue;
2550
2597
  if (depth >= limits.maxDepth) {
2551
2598
  state.truncated = true;
2552
2599
  continue;
2553
2600
  }
2554
- await scanDirectory(fullPath, depth + 1, profiles, limits, state, signal);
2601
+ await scanDirectory(fullPath, depth + 1, profiles, limits, state, extraIgnores, signal);
2555
2602
  continue;
2556
2603
  }
2557
2604
  if (!entry.isFile()) continue;
2558
2605
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2559
2606
  }
2560
2607
  }
2561
- function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2562
- const lower = basename8.toLowerCase();
2608
+ function collectFileEvidence(directory, fullPath, basename9, profiles, state) {
2609
+ const lower = basename9.toLowerCase();
2563
2610
  const extension = path4.extname(lower);
2564
2611
  for (const profile of profiles) {
2565
2612
  const detector = profile.detectors.find(
@@ -2570,7 +2617,7 @@ function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2570
2617
  candidate.evidence.push({
2571
2618
  kind: detector.kind,
2572
2619
  path: fullPath,
2573
- value: basename8,
2620
+ value: basename9,
2574
2621
  weight: detector.weight
2575
2622
  });
2576
2623
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2680,8 +2727,8 @@ function getCandidate(state, profile, root) {
2680
2727
  }
2681
2728
  return candidate;
2682
2729
  }
2683
- function shouldIgnoreDirectory(name, profiles) {
2684
- if (GLOBAL_IGNORES.has(name) || name.startsWith(".")) return true;
2730
+ function shouldIgnoreDirectory(name, profiles, extraIgnores) {
2731
+ if (GLOBAL_IGNORES.has(name) || extraIgnores.has(name) || name.startsWith(".")) return true;
2685
2732
  return profiles.some((profile) => profile.ignoredDirectories.includes(name));
2686
2733
  }
2687
2734
  function normalizeLimits(input) {
@@ -4897,6 +4944,11 @@ function parseAuditOutput(json2, exitCode) {
4897
4944
  // src/bash.ts
4898
4945
  import { spawn as spawn3 } from "node:child_process";
4899
4946
  import * as os4 from "node:os";
4947
+ import {
4948
+ emitProcessCompleted as emitProcessCompleted2,
4949
+ emitProcessOutput as emitProcessOutput2,
4950
+ emitProcessStarted as emitProcessStarted2
4951
+ } from "@wrongstack/core";
4900
4952
 
4901
4953
  // src/_env.ts
4902
4954
  import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core";
@@ -5766,6 +5818,32 @@ var bashTool = {
5766
5818
  }
5767
5819
  }
5768
5820
  const pid2 = child2.pid;
5821
+ let stdoutBytes2 = 0;
5822
+ let stderrBytes2 = 0;
5823
+ let telemetryCompleted2 = false;
5824
+ emitProcessStarted2({
5825
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
5826
+ parentPid: process.pid,
5827
+ command: redactCommand(`${shell} ${args.join(" ")}`),
5828
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
5829
+ cwd: ctx.projectRoot,
5830
+ background: true,
5831
+ startedAt: new Date(startedAt).toISOString()
5832
+ });
5833
+ const completeBackground = (exitCode, signal) => {
5834
+ if (telemetryCompleted2) return;
5835
+ telemetryCompleted2 = true;
5836
+ emitProcessCompleted2({
5837
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
5838
+ exitCode,
5839
+ ...signal ? { signal } : {},
5840
+ durationMs: Date.now() - startedAt,
5841
+ stdoutBytes: stdoutBytes2,
5842
+ stderrBytes: stderrBytes2,
5843
+ timedOut: false,
5844
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
5845
+ });
5846
+ };
5769
5847
  if (typeof pid2 === "number") {
5770
5848
  registry.register({
5771
5849
  pid: pid2,
@@ -5778,7 +5856,10 @@ var bashTool = {
5778
5856
  });
5779
5857
  child2.on("close", () => registry.unregister(pid2));
5780
5858
  }
5781
- const onBgData = (chunk) => {
5859
+ const onBgData = (chunk, stream) => {
5860
+ if (stream === "stdout") stdoutBytes2 += chunk.byteLength;
5861
+ else stderrBytes2 += chunk.byteLength;
5862
+ emitProcessOutput2({ pid: pid2, stream, chunk });
5782
5863
  if (truncated) return;
5783
5864
  const remain = MAX_OUTPUT - buf2.length;
5784
5865
  if (remain > 0) {
@@ -5786,24 +5867,28 @@ var bashTool = {
5786
5867
  }
5787
5868
  if (buf2.length >= MAX_OUTPUT) {
5788
5869
  truncated = true;
5789
- child2.stdout?.off("data", onBgData);
5790
- child2.stderr?.off("data", onBgData);
5870
+ child2.stdout?.off("data", onBgStdout);
5871
+ child2.stderr?.off("data", onBgStderr);
5791
5872
  }
5792
5873
  };
5793
- child2.stdout?.on("data", onBgData);
5794
- child2.stderr?.on("data", onBgData);
5874
+ const onBgStdout = (chunk) => onBgData(chunk, "stdout");
5875
+ const onBgStderr = (chunk) => onBgData(chunk, "stderr");
5876
+ child2.stdout?.on("data", onBgStdout);
5877
+ child2.stderr?.on("data", onBgStderr);
5795
5878
  const cleanupBackground = () => {
5796
- child2.stdout?.off("data", onBgData);
5797
- child2.stderr?.off("data", onBgData);
5879
+ child2.stdout?.off("data", onBgStdout);
5880
+ child2.stderr?.off("data", onBgStderr);
5798
5881
  };
5799
5882
  child2.on("error", () => {
5800
5883
  cleanupBackground();
5801
5884
  if (typeof pid2 === "number") registry.unregister(pid2);
5802
5885
  registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
5886
+ completeBackground(1);
5803
5887
  });
5804
- child2.on("close", () => {
5888
+ child2.on("close", (code, signal) => {
5805
5889
  cleanupBackground();
5806
5890
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
5891
+ completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
5807
5892
  });
5808
5893
  if (typeof pid2 === "number") child2.unref();
5809
5894
  yield {
@@ -5843,6 +5928,32 @@ var bashTool = {
5843
5928
  }
5844
5929
  }
5845
5930
  const pid = child.pid;
5931
+ let stdoutBytes = 0;
5932
+ let stderrBytes = 0;
5933
+ let telemetryCompleted = false;
5934
+ emitProcessStarted2({
5935
+ ...pid !== void 0 ? { pid } : {},
5936
+ parentPid: process.pid,
5937
+ command: redactCommand(`${shell} ${args.join(" ")}`),
5938
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
5939
+ cwd: ctx.projectRoot,
5940
+ background: false,
5941
+ startedAt: new Date(startedAt).toISOString()
5942
+ });
5943
+ const completeForeground = (exitCode, signal) => {
5944
+ if (telemetryCompleted) return;
5945
+ telemetryCompleted = true;
5946
+ emitProcessCompleted2({
5947
+ ...pid !== void 0 ? { pid } : {},
5948
+ exitCode,
5949
+ ...signal ? { signal } : {},
5950
+ durationMs: Date.now() - startedAt,
5951
+ stdoutBytes,
5952
+ stderrBytes,
5953
+ timedOut,
5954
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
5955
+ });
5956
+ };
5846
5957
  if (typeof pid === "number") {
5847
5958
  registry.register({
5848
5959
  pid,
@@ -5944,8 +6055,11 @@ var bashTool = {
5944
6055
  child.stderr?.resume();
5945
6056
  }
5946
6057
  };
5947
- const onData = (chunk) => {
6058
+ const onData = (chunk, stream) => {
5948
6059
  const text = chunk.toString();
6060
+ if (stream === "stdout") stdoutBytes += chunk.byteLength;
6061
+ else stderrBytes += chunk.byteLength;
6062
+ emitProcessOutput2({ pid, stream, chunk });
5949
6063
  if (buf.length < MAX_OUTPUT) {
5950
6064
  buf += text.slice(0, MAX_OUTPUT - buf.length);
5951
6065
  }
@@ -5954,17 +6068,21 @@ var bashTool = {
5954
6068
  push({ kind: "data", text });
5955
6069
  pauseIfFlooded();
5956
6070
  };
5957
- child.stdout?.on("data", onData);
5958
- child.stderr?.on("data", onData);
6071
+ const onStdoutData = (chunk) => onData(chunk, "stdout");
6072
+ const onStderrData = (chunk) => onData(chunk, "stderr");
6073
+ child.stdout?.on("data", onStdoutData);
6074
+ child.stderr?.on("data", onStderrData);
5959
6075
  child.on("error", (err) => {
5960
6076
  for (const t of timers) clearTimeout(t);
5961
6077
  registry.afterCall(Date.now() - startedAt, true);
6078
+ completeForeground(1);
5962
6079
  push({ kind: "error", err });
5963
6080
  });
5964
- child.on("close", (code) => {
6081
+ child.on("close", (code, signal) => {
5965
6082
  for (const t of timers) clearTimeout(t);
5966
6083
  if (typeof pid === "number") registry.unregister(pid);
5967
6084
  registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
6085
+ completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
5968
6086
  push({ kind: "end", code });
5969
6087
  });
5970
6088
  try {
@@ -6009,8 +6127,8 @@ ${hint}` : ""),
6009
6127
  for (const t of timers) clearTimeout(t);
6010
6128
  spool.finalize();
6011
6129
  if (isWin4) opts.signal.removeEventListener("abort", onAbort);
6012
- child.stdout?.off("data", onData);
6013
- child.stderr?.off("data", onData);
6130
+ child.stdout?.off("data", onStdoutData);
6131
+ child.stderr?.off("data", onStderrData);
6014
6132
  child.stdout?.destroy();
6015
6133
  child.stderr?.destroy();
6016
6134
  if (child.exitCode === null && !child.killed) {
@@ -6054,6 +6172,9 @@ ${hint}` : ""),
6054
6172
  };
6055
6173
 
6056
6174
  // src/batch-tool-use.ts
6175
+ import {
6176
+ GOVERNED_TOOL_EXECUTOR_META_KEY
6177
+ } from "@wrongstack/core";
6057
6178
  var batchToolUseTool = {
6058
6179
  name: "batch_tool_use",
6059
6180
  category: "Meta",
@@ -6090,7 +6211,7 @@ var batchToolUseTool = {
6090
6211
  },
6091
6212
  required: ["calls"]
6092
6213
  },
6093
- async execute(input, ctx, opts) {
6214
+ async execute(input, ctx, _opts) {
6094
6215
  if (!input?.calls || input.calls.length === 0) {
6095
6216
  return {
6096
6217
  results: [],
@@ -6100,18 +6221,33 @@ var batchToolUseTool = {
6100
6221
  stop_on_error: false
6101
6222
  };
6102
6223
  }
6224
+ const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
6225
+ if (typeof governedExecute !== "function") {
6226
+ return {
6227
+ results: input.calls.map((call) => ({
6228
+ tool: call.tool,
6229
+ success: false,
6230
+ error: "governed nested execution is unavailable; call the tool directly",
6231
+ executionMs: 0
6232
+ })),
6233
+ total: input.calls.length,
6234
+ succeeded: 0,
6235
+ failed: input.calls.length,
6236
+ stop_on_error: input.stop_on_error ?? false
6237
+ };
6238
+ }
6103
6239
  const results = [];
6104
6240
  let succeeded = 0;
6105
6241
  let failed = 0;
6106
6242
  if (input.parallel !== false) {
6107
- const promises = input.calls.map(async (call) => executeSingle(call, ctx, opts));
6243
+ const promises = input.calls.map(async (call) => executeSingle(call, ctx, governedExecute));
6108
6244
  const allResults = await Promise.all(promises);
6109
6245
  results.push(...allResults);
6110
6246
  succeeded = allResults.filter((r) => r.success).length;
6111
6247
  failed = allResults.filter((r) => !r.success).length;
6112
6248
  } else {
6113
6249
  for (const call of input.calls) {
6114
- const result = await executeSingle(call, ctx, opts ?? { signal: void 0 });
6250
+ const result = await executeSingle(call, ctx, governedExecute);
6115
6251
  results.push(result);
6116
6252
  if (result.success) {
6117
6253
  succeeded++;
@@ -6130,9 +6266,9 @@ var batchToolUseTool = {
6130
6266
  };
6131
6267
  }
6132
6268
  };
6133
- async function executeSingle(call, ctx, opts) {
6269
+ async function executeSingle(call, ctx, governedExecute) {
6134
6270
  const start = Date.now();
6135
- const tool = ctx.tools.find((t) => t.name === call.tool);
6271
+ const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
6136
6272
  if (!tool) {
6137
6273
  return {
6138
6274
  tool: call.tool,
@@ -6142,11 +6278,11 @@ async function executeSingle(call, ctx, opts) {
6142
6278
  };
6143
6279
  }
6144
6280
  try {
6145
- const result = await tool.execute(call.input, ctx, opts);
6281
+ const result = await governedExecute(call.tool, call.input);
6146
6282
  return {
6147
6283
  tool: call.tool,
6148
- success: true,
6149
- result,
6284
+ success: result.success,
6285
+ ...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
6150
6286
  executionMs: Date.now() - start
6151
6287
  };
6152
6288
  } catch (e) {
@@ -6556,7 +6692,7 @@ var BrowserSessionManager = class {
6556
6692
  constructor(options, launcher = defaultLauncher) {
6557
6693
  this.launcher = launcher;
6558
6694
  this.artifacts = new BrowserArtifactStore(options.artifactRoot);
6559
- this.allowPrivateHosts = options.allowPrivateHosts ?? false;
6695
+ this.allowPrivateHosts = options.allowPrivateHosts ?? true;
6560
6696
  this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
6561
6697
  this.networkProxy = new BrowserNetworkGuardProxy({
6562
6698
  allowPrivateHosts: this.allowPrivateHosts,
@@ -7482,7 +7618,7 @@ import * as path13 from "node:path";
7482
7618
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
7483
7619
 
7484
7620
  // src/codebase-index/schema.ts
7485
- var SCHEMA_VERSION = 2;
7621
+ var SCHEMA_VERSION = 3;
7486
7622
 
7487
7623
  // src/codebase-index/lsp-kind.ts
7488
7624
  function lspKindToInternalKind(k) {
@@ -7525,7 +7661,7 @@ function tokenise(text) {
7525
7661
  return sanitised.toLowerCase().split(" ").filter(Boolean);
7526
7662
  }
7527
7663
  function splitName(name) {
7528
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
7664
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
7529
7665
  }
7530
7666
  function buildIndexableText(name, signature, docComment) {
7531
7667
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -7551,14 +7687,12 @@ function buildBm25Index(docs) {
7551
7687
  return new Bm25Index(documents, df, N, avgLen);
7552
7688
  }
7553
7689
  var Bm25Index = class {
7554
- constructor(documents, df, N, avgLen) {
7690
+ constructor(documents, _df, N, avgLen) {
7555
7691
  this.documents = documents;
7556
- this.df = df;
7557
7692
  this.N = N;
7558
7693
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
7559
7694
  }
7560
7695
  documents;
7561
- df;
7562
7696
  N;
7563
7697
  safeAvgLen;
7564
7698
  score(query, filter) {
@@ -7571,10 +7705,13 @@ var Bm25Index = class {
7571
7705
  for (const qTerm of qTokens) {
7572
7706
  let tf = 0;
7573
7707
  for (const t of doc.tokens) {
7574
- if (t === qTerm) tf++;
7708
+ if (t.startsWith(qTerm)) tf++;
7575
7709
  }
7576
7710
  if (tf === 0) continue;
7577
- const dfVal = this.df[qTerm] ?? 0;
7711
+ let dfVal = 0;
7712
+ for (const candidate of this.documents) {
7713
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
7714
+ }
7578
7715
  if (dfVal === 0) continue;
7579
7716
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
7580
7717
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -7607,6 +7744,29 @@ var Bm25Index = class {
7607
7744
 
7608
7745
  // src/codebase-index/writer.ts
7609
7746
  var DB_FILE = "index.db";
7747
+ function escapeLike(value) {
7748
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
7749
+ }
7750
+ function assignRefsToSymbols(refs, symbols) {
7751
+ if (refs.length === 0 || symbols.length === 0) return [];
7752
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
7753
+ const seen = /* @__PURE__ */ new Set();
7754
+ const assigned = [];
7755
+ for (const ref of refs) {
7756
+ let owner2;
7757
+ for (const symbol of ordered) {
7758
+ if (symbol.line > ref.line) break;
7759
+ owner2 = symbol;
7760
+ }
7761
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
7762
+ if (!owner2 || owner2.id <= 0) continue;
7763
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
7764
+ if (seen.has(key)) continue;
7765
+ seen.add(key);
7766
+ assigned.push({ ...ref, fromId: owner2.id });
7767
+ }
7768
+ return assigned;
7769
+ }
7610
7770
  function resolveIndexDir(projectRoot, override) {
7611
7771
  return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
7612
7772
  }
@@ -7660,7 +7820,7 @@ function sleepSync(ms) {
7660
7820
  } catch {
7661
7821
  }
7662
7822
  }
7663
- var IndexStore = class {
7823
+ var IndexStore = class _IndexStore {
7664
7824
  db;
7665
7825
  /** Absolute path to this project's index directory. */
7666
7826
  indexDir;
@@ -7712,10 +7872,7 @@ var IndexStore = class {
7712
7872
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
7713
7873
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
7714
7874
  }
7715
- const delay = Math.min(
7716
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
7717
- LOCK_RETRY_MAX_DELAY_MS
7718
- );
7875
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
7719
7876
  sleepSync(delay);
7720
7877
  }
7721
7878
  }
@@ -7797,8 +7954,24 @@ var IndexStore = class {
7797
7954
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
7798
7955
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
7799
7956
  try {
7800
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
7957
+ this.db.exec(
7958
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
7959
+ );
7801
7960
  this.ftsAvailable = true;
7961
+ const symbolCount = Number(
7962
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
7963
+ );
7964
+ const ftsCount = Number(
7965
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
7966
+ );
7967
+ if (symbolCount !== ftsCount) {
7968
+ this.db.exec("DELETE FROM symbols_fts");
7969
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
7970
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
7971
+ for (const row of rows) {
7972
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
7973
+ }
7974
+ }
7802
7975
  } catch {
7803
7976
  this.ftsAvailable = false;
7804
7977
  }
@@ -7856,7 +8029,9 @@ var IndexStore = class {
7856
8029
  deleteSymbolsForFile(file) {
7857
8030
  this.runWithRetry(() => {
7858
8031
  if (this.ftsAvailable) {
7859
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8032
+ this.stmt(
8033
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8034
+ ).run(file);
7860
8035
  }
7861
8036
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
7862
8037
  });
@@ -7868,9 +8043,21 @@ var IndexStore = class {
7868
8043
  */
7869
8044
  deleteFile(file) {
7870
8045
  this.runWithRetry(() => {
7871
- this.deleteRefsForFile(file);
7872
- this.deleteSymbolsForFile(file);
7873
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
8046
+ this.db.exec("BEGIN IMMEDIATE");
8047
+ try {
8048
+ if (this.ftsAvailable) {
8049
+ this.db.prepare(
8050
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8051
+ ).run(file);
8052
+ }
8053
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8054
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
8055
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
8056
+ this.db.exec("COMMIT");
8057
+ } catch (err) {
8058
+ this.db.exec("ROLLBACK");
8059
+ throw err;
8060
+ }
7874
8061
  });
7875
8062
  }
7876
8063
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -7893,14 +8080,22 @@ var IndexStore = class {
7893
8080
  ).all(file);
7894
8081
  if (!rows.length) return null;
7895
8082
  const r = expectDefined2(rows[0]);
7896
- return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
8083
+ return {
8084
+ file: r.file,
8085
+ lang: r.lang,
8086
+ mtimeMs: r.mtime_ms,
8087
+ symbolCount: r.symbol_count,
8088
+ lastIndexed: r.last_indexed
8089
+ };
7897
8090
  }
7898
8091
  getAllFileMetas() {
7899
- return this.db.prepare(
7900
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
7901
- ).all().map(
7902
- (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
7903
- );
8092
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8093
+ file: r.file,
8094
+ lang: r.lang,
8095
+ mtimeMs: r.mtime_ms,
8096
+ symbolCount: r.symbol_count,
8097
+ lastIndexed: r.last_indexed
8098
+ }));
7904
8099
  }
7905
8100
  // ─── Search ──────────────────────────────────────────────────────────────────
7906
8101
  search(query, filter) {
@@ -7924,8 +8119,8 @@ var IndexStore = class {
7924
8119
  values.push(filter.lang);
7925
8120
  }
7926
8121
  if (filter?.file) {
7927
- conditions.push("file LIKE ?");
7928
- values.push(`%${filter.file}%`);
8122
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
8123
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
7929
8124
  }
7930
8125
  if (query.trim()) {
7931
8126
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -7963,9 +8158,11 @@ var IndexStore = class {
7963
8158
  * "users", camelCase-split text makes "complex" find "complexOperation").
7964
8159
  */
7965
8160
  searchRanked(query, filter, limit) {
8161
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
8162
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
7966
8163
  const tokens = tokenise(query);
7967
8164
  if (tokens.length === 0 || !this.ftsAvailable) {
7968
- return this.searchRankedFallback(query, filter, limit);
8165
+ return this.searchRankedFallback(query, filter, safeLimit);
7969
8166
  }
7970
8167
  let effectiveKind = filter?.kind;
7971
8168
  if (filter?.lspKind !== void 0) {
@@ -7985,11 +8182,13 @@ var IndexStore = class {
7985
8182
  values.push(filter.lang);
7986
8183
  }
7987
8184
  if (filter?.file) {
7988
- conditions.push("s.file LIKE ?");
7989
- values.push(`%${filter.file}%`);
8185
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
8186
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
7990
8187
  }
7991
8188
  const where = conditions.join(" AND ");
7992
- const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
8189
+ const countRows = this.db.prepare(
8190
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8191
+ ).all(...values);
7993
8192
  const total = countRows[0] ? Number(countRows[0].n) : 0;
7994
8193
  if (total === 0) return { results: [], total: 0 };
7995
8194
  const rows = this.db.prepare(
@@ -7998,9 +8197,13 @@ var IndexStore = class {
7998
8197
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
7999
8198
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
8000
8199
  WHERE ${where}
8001
- ORDER BY bm25(symbols_fts)
8200
+ ORDER BY
8201
+ CASE WHEN lower(s.name) = lower(?) THEN 0
8202
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
8203
+ ELSE 2 END,
8204
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
8002
8205
  LIMIT ?`
8003
- ).all(...values, limit);
8206
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
8004
8207
  return {
8005
8208
  results: rows.map((r) => ({
8006
8209
  id: r.id,
@@ -8030,10 +8233,28 @@ var IndexStore = class {
8030
8233
  }
8031
8234
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8032
8235
  const bm25 = buildBm25Index(
8033
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
8236
+ candidates.map((c) => ({
8237
+ id: c.id,
8238
+ text: buildIndexableText(c.name, c.signature, c.docComment)
8239
+ }))
8034
8240
  );
8035
8241
  const scored = bm25.score(query, (id) => candidateById.has(id));
8036
- scored.sort((a, b) => b.score - a.score);
8242
+ const q = query.trim().toLowerCase();
8243
+ const rank = (id) => {
8244
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
8245
+ if (name === q) return 0;
8246
+ if (name.startsWith(q)) return 1;
8247
+ return 2;
8248
+ };
8249
+ scored.sort((a, b) => {
8250
+ const rankDiff = rank(a.id) - rank(b.id);
8251
+ if (rankDiff !== 0) return rankDiff;
8252
+ const scoreDiff = b.score - a.score;
8253
+ if (scoreDiff !== 0) return scoreDiff;
8254
+ const left = expectDefined2(candidateById.get(a.id));
8255
+ const right = expectDefined2(candidateById.get(b.id));
8256
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
8257
+ });
8037
8258
  const qTokens = tokenise(query);
8038
8259
  const results = scored.slice(0, limit).map(({ id, score }) => {
8039
8260
  const c = expectDefined2(candidateById.get(id));
@@ -8042,9 +8263,7 @@ var IndexStore = class {
8042
8263
  return { results, total: candidates.length };
8043
8264
  }
8044
8265
  getAllIndexable() {
8045
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
8046
- ({ id, text }) => ({ id, text })
8047
- );
8266
+ return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8048
8267
  }
8049
8268
  /**
8050
8269
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -8060,22 +8279,16 @@ var IndexStore = class {
8060
8279
  // ─── Stats ───────────────────────────────────────────────────────────────────
8061
8280
  getStats() {
8062
8281
  const sizeBytes = this.sizeBytes();
8063
- const lastRows = this.db.prepare(
8064
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
8065
- ).all();
8282
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8066
8283
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8067
8284
  const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
8068
8285
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
8069
8286
  const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
8070
8287
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
8071
- const langRows = this.db.prepare(
8072
- "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
8073
- ).all();
8288
+ const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8074
8289
  const byLang = {};
8075
8290
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
8076
- const kindRows = this.db.prepare(
8077
- "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
8078
- ).all();
8291
+ const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8079
8292
  const byKind = {};
8080
8293
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
8081
8294
  return {
@@ -8091,17 +8304,31 @@ var IndexStore = class {
8091
8304
  }
8092
8305
  setLastIndexed(ts2) {
8093
8306
  this.runWithRetry(() => {
8094
- this.db.prepare(
8095
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
8096
- ).run(String(ts2));
8307
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8308
+ });
8309
+ }
8310
+ getMetadata(key) {
8311
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
8312
+ return rows[0]?.value;
8313
+ }
8314
+ setMetadata(key, value) {
8315
+ this.runWithRetry(() => {
8316
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8097
8317
  });
8098
8318
  }
8099
8319
  clearAll() {
8100
8320
  this.runWithRetry(() => {
8101
- this.db.exec("DELETE FROM symbols");
8102
- this.db.exec("DELETE FROM files");
8103
- this.db.exec("DELETE FROM refs");
8104
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8321
+ this.db.exec("BEGIN IMMEDIATE");
8322
+ try {
8323
+ this.db.exec("DELETE FROM refs");
8324
+ this.db.exec("DELETE FROM symbols");
8325
+ this.db.exec("DELETE FROM files");
8326
+ if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8327
+ this.db.exec("COMMIT");
8328
+ } catch (err) {
8329
+ this.db.exec("ROLLBACK");
8330
+ throw err;
8331
+ }
8105
8332
  });
8106
8333
  }
8107
8334
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -8192,6 +8419,7 @@ var IndexStore = class {
8192
8419
  const allInserted = [];
8193
8420
  const refsToInsert = [];
8194
8421
  for (const entry of entries) {
8422
+ const insertedForEntry = [];
8195
8423
  for (const s of entry.symbols) {
8196
8424
  const id = nextId++;
8197
8425
  symStmt.run(
@@ -8209,9 +8437,11 @@ var IndexStore = class {
8209
8437
  s.file
8210
8438
  );
8211
8439
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8212
- allInserted.push({ ...s, id });
8440
+ const inserted = { ...s, id };
8441
+ allInserted.push(inserted);
8442
+ insertedForEntry.push(inserted);
8213
8443
  }
8214
- for (const r of entry.refs) refsToInsert.push(r);
8444
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8215
8445
  }
8216
8446
  if (refsToInsert.length > 0) {
8217
8447
  const refStmt = this.db.prepare(
@@ -8249,9 +8479,7 @@ var IndexStore = class {
8249
8479
  */
8250
8480
  deleteRefsForFile(file) {
8251
8481
  this.runWithRetry(() => {
8252
- this.db.prepare(
8253
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
8254
- ).run(file);
8482
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8255
8483
  });
8256
8484
  }
8257
8485
  /**
@@ -8292,9 +8520,7 @@ var IndexStore = class {
8292
8520
  * Find all references FROM a given symbol (what does this symbol call/use?).
8293
8521
  */
8294
8522
  findRefsFrom(symbolId) {
8295
- return this.db.prepare(
8296
- "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
8297
- ).all(symbolId).map((r) => ({
8523
+ return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
8298
8524
  id: r.id,
8299
8525
  fromId: r.from_id,
8300
8526
  toName: r.to_name,
@@ -8311,6 +8537,325 @@ var IndexStore = class {
8311
8537
  return 0;
8312
8538
  }
8313
8539
  }
8540
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
8541
+ /**
8542
+ * Derive a monorepo package name from an absolute file path.
8543
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
8544
+ * Returns `undefined` for files outside any recognisable package structure.
8545
+ */
8546
+ static derivePackage(filePath) {
8547
+ const f = filePath.replace(/\\/g, "/");
8548
+ const pkgsIdx = f.indexOf("/packages/");
8549
+ if (pkgsIdx !== -1) {
8550
+ const rest = f.slice(pkgsIdx + "/packages/".length);
8551
+ const seg = rest.split("/")[0];
8552
+ return seg ? `@wrongstack/${seg}` : void 0;
8553
+ }
8554
+ const appsIdx = f.indexOf("/apps/");
8555
+ if (appsIdx !== -1) {
8556
+ const rest = f.slice(appsIdx + "/apps/".length);
8557
+ const seg = rest.split("/")[0];
8558
+ return seg ? `app:${seg}` : void 0;
8559
+ }
8560
+ return void 0;
8561
+ }
8562
+ static packageFromImport(moduleName) {
8563
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
8564
+ const parts = moduleName.split("/");
8565
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
8566
+ }
8567
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
8568
+ if (!moduleName.startsWith(".")) return void 0;
8569
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
8570
+ const absolute = path13.posix.normalize(
8571
+ path13.posix.join(path13.posix.dirname(normalizedFrom), moduleName)
8572
+ );
8573
+ const extension = path13.posix.extname(absolute);
8574
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
8575
+ const candidates = [
8576
+ absolute,
8577
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
8578
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(absolute, `index${ext}`)),
8579
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(base, `index${ext}`))
8580
+ ];
8581
+ const indexedByPortablePath = new Map(
8582
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
8583
+ );
8584
+ for (const candidate of candidates) {
8585
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
8586
+ if (indexed) return indexed;
8587
+ }
8588
+ return void 0;
8589
+ }
8590
+ /**
8591
+ * Package-level graph: each workspace package is a node; edges are derived
8592
+ * from cross-package symbol references (a symbol in package A references a
8593
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
8594
+ */
8595
+ getPackageGraph() {
8596
+ const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8597
+ const pkgNodes = /* @__PURE__ */ new Map();
8598
+ const fileToPkg = /* @__PURE__ */ new Map();
8599
+ const symbolToPkg = /* @__PURE__ */ new Map();
8600
+ for (const s of symbols) {
8601
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
8602
+ fileToPkg.set(s.file, pkg);
8603
+ symbolToPkg.set(s.id, pkg);
8604
+ const node = pkgNodes.get(pkg);
8605
+ if (node) {
8606
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
8607
+ } else {
8608
+ pkgNodes.set(pkg, {
8609
+ id: `pkg:${pkg}`,
8610
+ label: pkg,
8611
+ kind: "package",
8612
+ package: pkg,
8613
+ symbolCount: 1,
8614
+ fileCount: 0
8615
+ });
8616
+ }
8617
+ }
8618
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
8619
+ for (const { file } of files) {
8620
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
8621
+ const node = pkgNodes.get(pkg);
8622
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
8623
+ }
8624
+ const refRows = this.db.prepare(
8625
+ `SELECT r.from_id, r.to_id, r.call_type
8626
+ FROM refs r
8627
+ WHERE r.to_id IS NOT NULL`
8628
+ ).all();
8629
+ const edgeMap = /* @__PURE__ */ new Map();
8630
+ for (const r of refRows) {
8631
+ if (r.call_type === "import") continue;
8632
+ const fromPkg = symbolToPkg.get(r.from_id);
8633
+ const toPkg = symbolToPkg.get(r.to_id);
8634
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
8635
+ const key = `${fromPkg}\0${toPkg}`;
8636
+ let e = edgeMap.get(key);
8637
+ if (!e) {
8638
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8639
+ edgeMap.set(key, e);
8640
+ }
8641
+ e.weight++;
8642
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8643
+ }
8644
+ const importRows = this.db.prepare(
8645
+ `SELECT r.from_id, r.to_name
8646
+ FROM refs r
8647
+ WHERE r.call_type = 'import'`
8648
+ ).all();
8649
+ for (const r of importRows) {
8650
+ const fromPkg = symbolToPkg.get(r.from_id);
8651
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
8652
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
8653
+ const key = `${fromPkg}\0${toPkg}`;
8654
+ let edge = edgeMap.get(key);
8655
+ if (!edge) {
8656
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8657
+ edgeMap.set(key, edge);
8658
+ }
8659
+ edge.weight++;
8660
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
8661
+ }
8662
+ const edges = [];
8663
+ for (const [key, e] of edgeMap) {
8664
+ const [source, target] = key.split("\0");
8665
+ let bestType = "call";
8666
+ let bestCount = 0;
8667
+ for (const [t, c] of e.types) {
8668
+ if (c > bestCount) {
8669
+ bestType = t;
8670
+ bestCount = c;
8671
+ }
8672
+ }
8673
+ edges.push({
8674
+ source: `pkg:${source}`,
8675
+ target: `pkg:${target}`,
8676
+ weight: e.weight,
8677
+ refType: bestType
8678
+ });
8679
+ }
8680
+ return { nodes: [...pkgNodes.values()], edges };
8681
+ }
8682
+ /**
8683
+ * File-level graph for a single package: each file is a node; edges are
8684
+ * derived from cross-file symbol references within the package.
8685
+ */
8686
+ getFileGraph(packageFilter) {
8687
+ const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8688
+ const pkgSyms = allSymbols.filter(
8689
+ (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
8690
+ );
8691
+ if (pkgSyms.length === 0) return { nodes: [], edges: [] };
8692
+ const fileNodes = /* @__PURE__ */ new Map();
8693
+ const symToFile = /* @__PURE__ */ new Map();
8694
+ const fileStats = /* @__PURE__ */ new Map();
8695
+ for (const s of allSymbols) {
8696
+ symToFile.set(s.id, s.file);
8697
+ const current = fileStats.get(s.file);
8698
+ fileStats.set(s.file, {
8699
+ count: (current?.count ?? 0) + 1,
8700
+ lang: current?.lang ?? s.lang
8701
+ });
8702
+ }
8703
+ const localFiles = new Set(pkgSyms.map((s) => s.file));
8704
+ const indexedFiles = new Set(allSymbols.map((s) => s.file));
8705
+ const ensureFileNode = (file) => {
8706
+ if (fileNodes.has(file)) return;
8707
+ const stats = fileStats.get(file);
8708
+ fileNodes.set(file, {
8709
+ id: `file:${file}`,
8710
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
8711
+ kind: "file",
8712
+ package: _IndexStore.derivePackage(file) ?? "(root)",
8713
+ file,
8714
+ symbolCount: stats?.count ?? 0,
8715
+ lang: stats?.lang,
8716
+ external: !localFiles.has(file)
8717
+ });
8718
+ };
8719
+ for (const file of localFiles) {
8720
+ ensureFileNode(file);
8721
+ }
8722
+ const refRows = this.db.prepare(
8723
+ `SELECT r.from_id, r.to_id, r.call_type
8724
+ FROM refs r
8725
+ WHERE r.to_id IS NOT NULL`
8726
+ ).all();
8727
+ const edgeMap = /* @__PURE__ */ new Map();
8728
+ for (const r of refRows) {
8729
+ if (r.call_type === "import") continue;
8730
+ const fromFile = symToFile.get(r.from_id);
8731
+ const toFile = symToFile.get(r.to_id);
8732
+ if (!fromFile || !toFile || fromFile === toFile) continue;
8733
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
8734
+ ensureFileNode(fromFile);
8735
+ ensureFileNode(toFile);
8736
+ const key = `${fromFile}\0${toFile}`;
8737
+ let e = edgeMap.get(key);
8738
+ if (!e) {
8739
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8740
+ edgeMap.set(key, e);
8741
+ }
8742
+ e.weight++;
8743
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8744
+ }
8745
+ const importRows = this.db.prepare(
8746
+ `SELECT r.from_id, r.to_name
8747
+ FROM refs r
8748
+ WHERE r.call_type = 'import'`
8749
+ ).all();
8750
+ for (const r of importRows) {
8751
+ const fromFile = symToFile.get(r.from_id);
8752
+ if (!fromFile || !localFiles.has(fromFile)) continue;
8753
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
8754
+ if (!toFile || fromFile === toFile) continue;
8755
+ ensureFileNode(fromFile);
8756
+ ensureFileNode(toFile);
8757
+ const key = `${fromFile}\0${toFile}`;
8758
+ let edge = edgeMap.get(key);
8759
+ if (!edge) {
8760
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8761
+ edgeMap.set(key, edge);
8762
+ }
8763
+ edge.weight++;
8764
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
8765
+ }
8766
+ const edges = [];
8767
+ for (const [key, e] of edgeMap) {
8768
+ const [source, target] = key.split("\0");
8769
+ let bestType = "call";
8770
+ let bestCount = 0;
8771
+ for (const [t, c] of e.types) {
8772
+ if (c > bestCount) {
8773
+ bestType = t;
8774
+ bestCount = c;
8775
+ }
8776
+ }
8777
+ edges.push({
8778
+ source: `file:${source}`,
8779
+ target: `file:${target}`,
8780
+ weight: e.weight,
8781
+ refType: bestType
8782
+ });
8783
+ }
8784
+ return { nodes: [...fileNodes.values()], edges };
8785
+ }
8786
+ /**
8787
+ * Symbol-level graph for a single file: each symbol is a node; edges are
8788
+ * derived from intra-file and cross-file symbol references (who calls whom).
8789
+ */
8790
+ getSymbolGraph(fileFilter) {
8791
+ const allSymbols = this.db.prepare(
8792
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
8793
+ ).all();
8794
+ const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
8795
+ if (syms.length === 0) return { nodes: [], edges: [] };
8796
+ const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
8797
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
8798
+ const toGraphNode = (s) => ({
8799
+ id: `sym:${s.id}`,
8800
+ label: s.name,
8801
+ kind: "symbol",
8802
+ symbolId: s.id,
8803
+ symbolKind: s.kind,
8804
+ file: s.file,
8805
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
8806
+ lang: s.lang,
8807
+ line: s.line,
8808
+ signature: s.signature,
8809
+ scope: s.scope,
8810
+ external: s.file !== fileFilter
8811
+ });
8812
+ const symIds = new Set(syms.map((s) => s.id));
8813
+ const refRows = this.db.prepare(
8814
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
8815
+ FROM refs r
8816
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
8817
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
8818
+ ).all(fileFilter, fileFilter);
8819
+ const edgeMap = /* @__PURE__ */ new Map();
8820
+ for (const r of refRows) {
8821
+ if (r.to_id === null) continue;
8822
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
8823
+ relatedIds.add(r.from_id);
8824
+ relatedIds.add(r.to_id);
8825
+ const key = `${r.from_id}\0${r.to_id}`;
8826
+ let e = edgeMap.get(key);
8827
+ if (!e) {
8828
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8829
+ edgeMap.set(key, e);
8830
+ }
8831
+ e.weight++;
8832
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8833
+ }
8834
+ const edges = [];
8835
+ for (const [key, e] of edgeMap) {
8836
+ const [fromId, toId] = key.split("\0").map(Number);
8837
+ let bestType = "call";
8838
+ let bestCount = 0;
8839
+ for (const [t, c] of e.types) {
8840
+ if (c > bestCount) {
8841
+ bestType = t;
8842
+ bestCount = c;
8843
+ }
8844
+ }
8845
+ edges.push({
8846
+ source: `sym:${fromId}`,
8847
+ target: `sym:${toId}`,
8848
+ weight: e.weight,
8849
+ refType: bestType
8850
+ });
8851
+ }
8852
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
8853
+ const aExternal = a.file === fileFilter ? 0 : 1;
8854
+ const bExternal = b.file === fileFilter ? 0 : 1;
8855
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
8856
+ }).map(toGraphNode);
8857
+ return { nodes, edges };
8858
+ }
8314
8859
  close() {
8315
8860
  this.stmtCache.clear();
8316
8861
  try {
@@ -8405,6 +8950,14 @@ function getJsDoc(node, sourceFile) {
8405
8950
  }
8406
8951
  return "";
8407
8952
  }
8953
+ function hasFunctionLikeAncestor(node) {
8954
+ let current = node.parent;
8955
+ while (current) {
8956
+ if (ts.isFunctionLike(current)) return true;
8957
+ current = current.parent;
8958
+ }
8959
+ return false;
8960
+ }
8408
8961
  function buildScope(node) {
8409
8962
  const parts = [];
8410
8963
  let current = node.parent;
@@ -8432,6 +8985,10 @@ function parseSymbols(opts) {
8432
8985
  function visit(node) {
8433
8986
  const kind = kindOf(node);
8434
8987
  if (kind) {
8988
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
8989
+ ts.forEachChild(node, visit);
8990
+ return;
8991
+ }
8435
8992
  const nameNode = node.name;
8436
8993
  if (!nameNode || !ts.isIdentifier(nameNode)) return;
8437
8994
  const name = nameNode.text;
@@ -8519,6 +9076,7 @@ function detectLang(file) {
8519
9076
  }
8520
9077
 
8521
9078
  // src/codebase-index/go-parser.ts
9079
+ init_win32_resolve();
8522
9080
  import { spawn as spawn4 } from "node:child_process";
8523
9081
  import * as os5 from "node:os";
8524
9082
  import * as path14 from "node:path";
@@ -8827,27 +9385,42 @@ async function syncGoParse(filePath, content, lang) {
8827
9385
  try {
8828
9386
  const scriptPath = path14.join(tmpDir, "parse.go");
8829
9387
  await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
8830
- const proc = spawn4("go", ["run", scriptPath], {
8831
- stdio: ["pipe", "pipe", "pipe"],
8832
- windowsHide: true
8833
- });
8834
- let stdout = "";
8835
- proc.stdout?.on("data", (chunk) => {
8836
- stdout += chunk.toString();
8837
- });
8838
- proc.stdin?.write(content);
8839
- proc.stdin?.end();
8840
- const { code } = await Promise.race([
8841
- new Promise((resolve14) => {
8842
- proc.on("close", (c) => resolve14({ code: c }));
8843
- }),
8844
- new Promise(
8845
- (_, reject) => setTimeout(() => {
9388
+ const goBinary = resolveWin32Command("go");
9389
+ const goResult = await new Promise(
9390
+ (resolve14, reject) => {
9391
+ let settled = false;
9392
+ const proc = spawn4(goBinary, ["run", scriptPath], {
9393
+ stdio: ["pipe", "pipe", "pipe"],
9394
+ windowsHide: true
9395
+ });
9396
+ proc.on("error", (err) => {
9397
+ if (settled) return;
9398
+ settled = true;
9399
+ reject(err);
9400
+ });
9401
+ let stdout2 = "";
9402
+ proc.stdout?.on("data", (chunk) => {
9403
+ stdout2 += chunk.toString();
9404
+ });
9405
+ proc.stderr?.resume();
9406
+ proc.stdin?.write(content);
9407
+ proc.stdin?.end();
9408
+ const timer = setTimeout(() => {
9409
+ if (settled) return;
9410
+ settled = true;
8846
9411
  proc.kill("SIGKILL");
8847
9412
  reject(new Error("timeout"));
8848
- }, 15e3)
8849
- )
8850
- ]).catch(() => ({ code: -1 }));
9413
+ }, 15e3);
9414
+ timer.unref?.();
9415
+ proc.on("close", (code2) => {
9416
+ if (settled) return;
9417
+ settled = true;
9418
+ clearTimeout(timer);
9419
+ resolve14({ code: code2, stdout: stdout2 });
9420
+ });
9421
+ }
9422
+ );
9423
+ const { code, stdout } = goResult;
8851
9424
  if (code !== 0 || !stdout.trim()) {
8852
9425
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
8853
9426
  }
@@ -8874,7 +9447,8 @@ async function syncGoParse(filePath, content, lang) {
8874
9447
  }
8875
9448
 
8876
9449
  // src/codebase-index/py-parser.ts
8877
- import { spawn as spawn5 } from "node:child_process";
9450
+ init_win32_resolve();
9451
+ import { spawn as spawn5, spawnSync } from "node:child_process";
8878
9452
  import * as fs11 from "node:fs/promises";
8879
9453
  import * as os6 from "node:os";
8880
9454
  import * as path15 from "node:path";
@@ -9089,36 +9663,74 @@ visitor.visit(tree)
9089
9663
 
9090
9664
  print(json.dumps([s.to_dict() for s in syms]))
9091
9665
  `;
9092
- var _cachedScriptPath = null;
9093
- async function syncPyParse(filePath, content, lang) {
9094
- try {
9095
- if (!_cachedScriptPath) {
9096
- const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
9097
- await fs11.mkdir(tmpDir, { recursive: true });
9098
- _cachedScriptPath = path15.join(tmpDir, "parse.py");
9099
- await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9100
- }
9101
- const proc = spawn5("python", [_cachedScriptPath, filePath], {
9666
+ function resolvePython() {
9667
+ const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
9668
+ for (const name of candidates) {
9669
+ const resolved = resolveWin32Command(name);
9670
+ const result = spawnSync(resolved, ["--version"], {
9671
+ stdio: "pipe",
9672
+ timeout: 5e3
9673
+ });
9674
+ if (result.error) continue;
9675
+ if (result.status !== 0) continue;
9676
+ return resolved;
9677
+ }
9678
+ return null;
9679
+ }
9680
+ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
9681
+ return new Promise((resolve14, reject) => {
9682
+ let settled = false;
9683
+ const proc = spawn5(pyBinary, [scriptPath, filePath], {
9102
9684
  stdio: ["pipe", "pipe", "pipe"],
9103
9685
  windowsHide: true
9104
9686
  });
9687
+ proc.on("error", (err) => {
9688
+ if (settled) return;
9689
+ settled = true;
9690
+ reject(err);
9691
+ });
9105
9692
  proc.stdin?.write(content);
9106
9693
  proc.stdin?.end();
9107
9694
  let stdout = "";
9108
9695
  proc.stdout?.on("data", (chunk) => {
9109
9696
  stdout += chunk.toString();
9110
9697
  });
9111
- const { code } = await Promise.race([
9112
- new Promise((resolve14) => {
9113
- proc.on("close", (c) => resolve14({ code: c }));
9114
- }),
9115
- new Promise(
9116
- (_, reject) => setTimeout(() => {
9117
- proc.kill("SIGKILL");
9118
- reject(new Error("timeout"));
9119
- }, 15e3)
9120
- )
9121
- ]).catch(() => ({ code: -1 }));
9698
+ proc.stderr?.resume();
9699
+ const timer = setTimeout(() => {
9700
+ if (settled) return;
9701
+ settled = true;
9702
+ proc.kill("SIGKILL");
9703
+ reject(new Error("timeout"));
9704
+ }, 15e3);
9705
+ timer.unref?.();
9706
+ proc.on("close", (code) => {
9707
+ if (settled) return;
9708
+ settled = true;
9709
+ clearTimeout(timer);
9710
+ resolve14({ code, stdout });
9711
+ });
9712
+ });
9713
+ }
9714
+ var _cachedScriptPath = null;
9715
+ var _cachedPyBinary = null;
9716
+ async function syncPyParse(filePath, content, lang) {
9717
+ try {
9718
+ if (!_cachedScriptPath) {
9719
+ const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
9720
+ await fs11.mkdir(tmpDir, { recursive: true });
9721
+ _cachedScriptPath = path15.join(tmpDir, "parse.py");
9722
+ await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9723
+ }
9724
+ if (!_cachedPyBinary) {
9725
+ _cachedPyBinary = resolvePython();
9726
+ if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9727
+ }
9728
+ const { code, stdout } = await spawnPyParser(
9729
+ _cachedPyBinary,
9730
+ _cachedScriptPath,
9731
+ filePath,
9732
+ content
9733
+ );
9122
9734
  if (code !== 0 || !stdout.trim()) {
9123
9735
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9124
9736
  }
@@ -9143,6 +9755,7 @@ async function syncPyParse(filePath, content, lang) {
9143
9755
  }
9144
9756
 
9145
9757
  // src/codebase-index/rs-parser.ts
9758
+ init_win32_resolve();
9146
9759
  import { expectDefined as expectDefined3 } from "@wrongstack/core";
9147
9760
  import { execFileSync, spawn as spawn6 } from "node:child_process";
9148
9761
  import * as fs12 from "node:fs/promises";
@@ -9187,30 +9800,45 @@ async function tryNativeParse(file, content) {
9187
9800
  const crateDir = path16.join(toolsDir, "syn-parser");
9188
9801
  const tmpFile = path16.join(crateDir, "src", "input.rs");
9189
9802
  await fs12.writeFile(tmpFile, content, "utf8");
9190
- const proc = spawn6(
9191
- "cargo",
9192
- ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
9193
- {
9194
- cwd: process.cwd(),
9195
- stdio: ["pipe", "pipe", "pipe"],
9196
- windowsHide: true
9197
- }
9198
- );
9199
- let stdout = "";
9200
- proc.stdout?.on("data", (chunk) => {
9201
- stdout += chunk.toString();
9202
- });
9203
- const { code } = await Promise.race([
9204
- new Promise((resolve14) => {
9205
- proc.on("close", (c) => resolve14({ code: c }));
9206
- }),
9207
- new Promise(
9208
- (_, reject) => setTimeout(() => {
9803
+ const cargoBinary = resolveWin32Command("cargo");
9804
+ const result = await new Promise(
9805
+ (resolve14, reject) => {
9806
+ let settled = false;
9807
+ const proc = spawn6(
9808
+ cargoBinary,
9809
+ ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
9810
+ {
9811
+ cwd: process.cwd(),
9812
+ stdio: ["pipe", "pipe", "pipe"],
9813
+ windowsHide: true
9814
+ }
9815
+ );
9816
+ proc.on("error", (err) => {
9817
+ if (settled) return;
9818
+ settled = true;
9819
+ reject(err);
9820
+ });
9821
+ let stdout2 = "";
9822
+ proc.stdout?.on("data", (chunk) => {
9823
+ stdout2 += chunk.toString();
9824
+ });
9825
+ proc.stderr?.resume();
9826
+ const timer = setTimeout(() => {
9827
+ if (settled) return;
9828
+ settled = true;
9209
9829
  proc.kill("SIGKILL");
9210
9830
  reject(new Error("timeout"));
9211
- }, 15e3)
9212
- )
9213
- ]).catch(() => ({ code: -1 }));
9831
+ }, 15e3);
9832
+ timer.unref?.();
9833
+ proc.on("close", (c) => {
9834
+ if (settled) return;
9835
+ settled = true;
9836
+ clearTimeout(timer);
9837
+ resolve14({ code: c, stdout: stdout2 });
9838
+ });
9839
+ }
9840
+ );
9841
+ const { code, stdout } = result;
9214
9842
  if (code === 0 && stdout.trim()) {
9215
9843
  const symbols = JSON.parse(stdout.trim());
9216
9844
  return {
@@ -9305,9 +9933,9 @@ function parseSymbols5(opts) {
9305
9933
  function regexParse2(opts) {
9306
9934
  const { file, content, lang } = opts;
9307
9935
  const symbols = [];
9308
- const basename8 = path17.basename(file).toLowerCase();
9309
- const isPackageJson = basename8 === "package.json";
9310
- const isTsconfig = basename8 === "tsconfig.json" || basename8 === "tsconfig.build.json";
9936
+ const basename9 = path17.basename(file).toLowerCase();
9937
+ const isPackageJson = basename9 === "package.json";
9938
+ const isTsconfig = basename9 === "tsconfig.json" || basename9 === "tsconfig.build.json";
9311
9939
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
9312
9940
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
9313
9941
  const lines = content.split("\n");
@@ -9709,9 +10337,7 @@ function yieldEventLoop() {
9709
10337
  function throwIfAborted(signal) {
9710
10338
  if (!signal?.aborted) return;
9711
10339
  if (signal.reason instanceof Error) throw signal.reason;
9712
- throw new Error(
9713
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
9714
- );
10340
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
9715
10341
  }
9716
10342
  function isAbortError(err) {
9717
10343
  return err instanceof DOMException && err.name === "AbortError";
@@ -9727,8 +10353,20 @@ var DEFAULT_IGNORE = [
9727
10353
  "__snapshots__",
9728
10354
  ".nyc_output"
9729
10355
  ];
10356
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10357
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10358
+ function isWithinProject(projectRoot, file) {
10359
+ const rel = path19.relative(projectRoot, file);
10360
+ return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
10361
+ }
10362
+ function isMissingPathError(err) {
10363
+ const code = err?.code;
10364
+ return code === "ENOENT" || code === "ENOTDIR";
10365
+ }
9730
10366
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9731
10367
  const results = [];
10368
+ const errors = [];
10369
+ let complete = true;
9732
10370
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
9733
10371
  const globs = [
9734
10372
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -9752,7 +10390,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9752
10390
  let entries;
9753
10391
  try {
9754
10392
  entries = await fs14.readdir(dir, { withFileTypes: true });
9755
- } catch {
10393
+ } catch (err) {
10394
+ complete = false;
10395
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
9756
10396
  return;
9757
10397
  }
9758
10398
  dirCount++;
@@ -9764,8 +10404,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9764
10404
  if (isGitIgnored(rel, true)) continue;
9765
10405
  await walk(full);
9766
10406
  } else if (e.isFile()) {
9767
- if (isGitIgnored(rel, false)) continue;
9768
- const ext = path19.extname(e.name);
10407
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
10408
+ const ext = path19.extname(e.name).toLowerCase();
9769
10409
  for (const { ext: extName, pat } of globs) {
9770
10410
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
9771
10411
  results.push(full);
@@ -9776,7 +10416,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
9776
10416
  }
9777
10417
  };
9778
10418
  await walk(projectRoot);
9779
- return results;
10419
+ return { files: results, complete, errors };
9780
10420
  }
9781
10421
  async function parseFile(file, content, lang) {
9782
10422
  switch (lang) {
@@ -9799,6 +10439,26 @@ async function parseFile(file, content, lang) {
9799
10439
  return { file, lang, symbols: [], mtimeMs: Date.now() };
9800
10440
  }
9801
10441
  }
10442
+ function assignRefsToSymbols2(refs, symbols) {
10443
+ if (refs.length === 0 || symbols.length === 0) return [];
10444
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10445
+ const seen = /* @__PURE__ */ new Set();
10446
+ const assigned = [];
10447
+ for (const ref of refs) {
10448
+ let owner2;
10449
+ for (const symbol of ordered) {
10450
+ if (symbol.line > ref.line) break;
10451
+ owner2 = symbol;
10452
+ }
10453
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10454
+ if (!owner2 || owner2.id <= 0) continue;
10455
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10456
+ if (seen.has(key)) continue;
10457
+ seen.add(key);
10458
+ assigned.push({ ...ref, fromId: owner2.id });
10459
+ }
10460
+ return assigned;
10461
+ }
9802
10462
  async function runIndexer(_ctx, opts) {
9803
10463
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
9804
10464
  try {
@@ -9811,7 +10471,9 @@ async function runIndexer(_ctx, opts) {
9811
10471
  }
9812
10472
  }
9813
10473
  async function runIndexerWithStore(store, opts) {
9814
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
10474
+ const { projectRoot, langs, ignore = [], signal } = opts;
10475
+ const relationGraphVersion = "2";
10476
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
9815
10477
  const startMs = Date.now();
9816
10478
  const errors = [];
9817
10479
  const langStats = {};
@@ -9820,10 +10482,18 @@ async function runIndexerWithStore(store, opts) {
9820
10482
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
9821
10483
  let files;
9822
10484
  let discoveredFiles = null;
10485
+ let discoveryComplete = true;
9823
10486
  if (opts.files && opts.files.length > 0) {
9824
- files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
10487
+ files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
10488
+ if (!isWithinProject(projectRoot, f)) return false;
10489
+ const rel = path19.relative(projectRoot, f).replace(/\\/g, "/");
10490
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path19.basename(f)) && !isGitIgnored(rel, false);
10491
+ });
9825
10492
  } else {
9826
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10493
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10494
+ files = discovery.files;
10495
+ errors.push(...discovery.errors);
10496
+ discoveryComplete = discovery.complete;
9827
10497
  discoveredFiles = new Set(files);
9828
10498
  }
9829
10499
  if (langs && langs.length > 0) {
@@ -9848,36 +10518,66 @@ async function runIndexerWithStore(store, opts) {
9848
10518
  }
9849
10519
  const statOpts = signal ? { signal } : {};
9850
10520
  const statReadParse = await Promise.allSettled(
9851
- batchFiles.map(async (file) => {
9852
- let stat17;
9853
- try {
9854
- stat17 = await fs14.stat(file, statOpts);
9855
- } catch (e) {
9856
- if (isAbortError(e)) throw e;
9857
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
9858
- }
9859
- if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
9860
- const lang = detectLang(file);
9861
- if (!lang) return { file, stat: stat17, lang: "", parsed: null };
9862
- const meta = existingMeta.get(file);
9863
- if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
9864
- return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
9865
- }
9866
- let content;
9867
- try {
9868
- content = await fs14.readFile(file, { encoding: "utf8", signal });
9869
- } catch (e) {
9870
- if (isAbortError(e)) throw e;
9871
- return { file, stat: stat17, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
9872
- }
9873
- let parsed;
9874
- try {
9875
- parsed = await parseFile(file, content, lang);
9876
- } catch (e) {
9877
- return { file, stat: stat17, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
10521
+ batchFiles.map(
10522
+ async (file) => {
10523
+ let stat17;
10524
+ try {
10525
+ stat17 = await fs14.stat(file, statOpts);
10526
+ } catch (e) {
10527
+ if (isAbortError(e)) throw e;
10528
+ return {
10529
+ file,
10530
+ stat: null,
10531
+ lang: "",
10532
+ parsed: null,
10533
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
10534
+ missing: isMissingPathError(e)
10535
+ };
10536
+ }
10537
+ if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
10538
+ const lang = detectLang(file);
10539
+ if (!lang) return { file, stat: stat17, lang: "", parsed: null };
10540
+ if (stat17.size > MAX_INDEX_FILE_BYTES) {
10541
+ return {
10542
+ file,
10543
+ stat: stat17,
10544
+ lang,
10545
+ parsed: null,
10546
+ error: `file too large (${stat17.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
10547
+ };
10548
+ }
10549
+ const meta = existingMeta.get(file);
10550
+ if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
10551
+ return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
10552
+ }
10553
+ let content;
10554
+ try {
10555
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
10556
+ } catch (e) {
10557
+ if (isAbortError(e)) throw e;
10558
+ return {
10559
+ file,
10560
+ stat: stat17,
10561
+ lang,
10562
+ parsed: null,
10563
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
10564
+ };
10565
+ }
10566
+ let parsed;
10567
+ try {
10568
+ parsed = await parseFile(file, content, lang);
10569
+ } catch (e) {
10570
+ return {
10571
+ file,
10572
+ stat: stat17,
10573
+ lang,
10574
+ parsed: null,
10575
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
10576
+ };
10577
+ }
10578
+ return { file, stat: stat17, lang, parsed, content };
9878
10579
  }
9879
- return { file, stat: stat17, lang, parsed, content };
9880
- })
10580
+ )
9881
10581
  );
9882
10582
  const batchEntries = [];
9883
10583
  const deleteForFiles = [];
@@ -9892,8 +10592,8 @@ async function runIndexerWithStore(store, opts) {
9892
10592
  }
9893
10593
  const result = settled.value;
9894
10594
  if (result.error) {
9895
- if (result.stat) store.deleteFile(file);
9896
- if (result.error.includes("error:")) errors.push(result.error);
10595
+ if (result.missing) store.deleteFile(file);
10596
+ errors.push(`${file}: ${result.error}`);
9897
10597
  continue;
9898
10598
  }
9899
10599
  const { stat: stat17, lang, parsed } = result;
@@ -9917,6 +10617,8 @@ async function runIndexerWithStore(store, opts) {
9917
10617
  continue;
9918
10618
  }
9919
10619
  if (parsed.symbols.length === 0) {
10620
+ store.deleteRefsForFile(file);
10621
+ store.deleteSymbolsForFile(file);
9920
10622
  store.upsertFile({
9921
10623
  file,
9922
10624
  lang,
@@ -9927,15 +10629,11 @@ async function runIndexerWithStore(store, opts) {
9927
10629
  filesIndexed++;
9928
10630
  continue;
9929
10631
  }
9930
- const refs = [];
9931
- if (parsed.refs && parsed.refs.length > 0) {
9932
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
9933
- }
9934
10632
  batchEntries.push({
9935
10633
  file,
9936
10634
  lang,
9937
10635
  symbols: parsed.symbols,
9938
- refs,
10636
+ refs: parsed.refs ?? [],
9939
10637
  mtimeMs: Math.floor(stat17.mtimeMs),
9940
10638
  symbolCount: parsed.symbols.length
9941
10639
  });
@@ -9943,34 +10641,12 @@ async function runIndexerWithStore(store, opts) {
9943
10641
  }
9944
10642
  if (batchEntries.length > 0) {
9945
10643
  try {
9946
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
9947
- let cursor = 0;
10644
+ store.commitBatch(batchEntries, { deleteForFiles });
9948
10645
  for (const entry of batchEntries) {
9949
10646
  const count = entry.symbols.length;
9950
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
9951
- cursor += count;
9952
10647
  symbolsIndexed += count;
9953
10648
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
9954
10649
  filesIndexed++;
9955
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
9956
- const refsByLine = /* @__PURE__ */ new Map();
9957
- for (let i = 0; i < symbolsWithIds.length; i++) {
9958
- const sym = symbolsWithIds[i];
9959
- let arr = refsByLine.get(sym.line);
9960
- if (!arr) {
9961
- arr = [];
9962
- refsByLine.set(sym.line, arr);
9963
- }
9964
- arr.push(i);
9965
- }
9966
- for (const ref of entry.refs) {
9967
- const indices = refsByLine.get(ref.line);
9968
- if (indices && indices.length > 0) {
9969
- const idx = indices.shift();
9970
- ref.fromId = symbolsWithIds[idx].id;
9971
- }
9972
- }
9973
- }
9974
10650
  }
9975
10651
  } catch (err) {
9976
10652
  const message = err instanceof Error ? err.message : String(err);
@@ -9984,23 +10660,7 @@ async function runIndexerWithStore(store, opts) {
9984
10660
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
9985
10661
  filesIndexed++;
9986
10662
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
9987
- const refsByLine = /* @__PURE__ */ new Map();
9988
- for (const sym of symbolsWithIds) {
9989
- let arr = refsByLine.get(sym.line);
9990
- if (!arr) {
9991
- arr = [];
9992
- refsByLine.set(sym.line, arr);
9993
- }
9994
- arr.push(sym);
9995
- }
9996
- const fallbackBatch = [];
9997
- for (const ref of entry.refs) {
9998
- const syms = refsByLine.get(ref.line);
9999
- if (syms && syms.length > 0) {
10000
- const sym = syms.shift();
10001
- fallbackBatch.push({ ...ref, fromId: sym.id });
10002
- }
10003
- }
10663
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
10004
10664
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
10005
10665
  }
10006
10666
  store.upsertFile({
@@ -10011,19 +10671,23 @@ async function runIndexerWithStore(store, opts) {
10011
10671
  lastIndexed: Date.now()
10012
10672
  });
10013
10673
  } catch (innerErr) {
10014
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
10674
+ errors.push(
10675
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
10676
+ );
10015
10677
  }
10016
10678
  }
10017
10679
  }
10018
10680
  }
10019
10681
  }
10020
- if (discoveredFiles) {
10682
+ if (discoveredFiles && discoveryComplete) {
10021
10683
  for (const [file_] of existingMeta) {
10022
10684
  if (!discoveredFiles.has(file_)) {
10023
10685
  store.deleteFile(file_);
10024
10686
  }
10025
10687
  }
10026
10688
  }
10689
+ store.resolveRefs();
10690
+ store.setMetadata("relation_graph_version", relationGraphVersion);
10027
10691
  const durationMs = Date.now() - startMs;
10028
10692
  store.setLastIndexed(Date.now());
10029
10693
  return {
@@ -10086,7 +10750,7 @@ function statsService(args) {
10086
10750
 
10087
10751
  // src/codebase-index/background-indexer.ts
10088
10752
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
10089
- var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
10753
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
10090
10754
  var _ready = false;
10091
10755
  var _indexing = false;
10092
10756
  var _currentFile = 0;
@@ -10155,14 +10819,22 @@ function ensureWorker() {
10155
10819
  if (!entry) return;
10156
10820
  pending.delete(msg.id);
10157
10821
  if (msg.ok) entry.resolve(msg.result);
10158
- else entry.reject(new Error(msg.error));
10822
+ else {
10823
+ const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
10824
+ if (msg.errorName && msg.errorName !== "Error") {
10825
+ error.name = msg.errorName;
10826
+ }
10827
+ entry.reject(error);
10828
+ }
10159
10829
  });
10160
10830
  w.on("error", (err) => {
10831
+ if (worker !== w) return;
10161
10832
  worker = null;
10162
10833
  failAllPending(err);
10163
10834
  });
10164
10835
  w.on("exit", () => {
10165
- if (worker === w) worker = null;
10836
+ if (worker !== w) return;
10837
+ worker = null;
10166
10838
  failAllPending(new Error("codebase-index worker exited"));
10167
10839
  });
10168
10840
  worker = w;
@@ -10182,6 +10854,11 @@ function terminateWorker(reason) {
10182
10854
  function callIndexOp(op, args, opts) {
10183
10855
  const w = ensureWorker();
10184
10856
  if (!w) return callInline(op, args, opts);
10857
+ if (opts.signal?.aborted) {
10858
+ return Promise.reject(
10859
+ opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
10860
+ );
10861
+ }
10185
10862
  return new Promise((resolve14, reject) => {
10186
10863
  const id = nextRpcId++;
10187
10864
  const timer = setTimeout(() => {
@@ -10196,8 +10873,7 @@ function callIndexOp(op, args, opts) {
10196
10873
  const onAbort = () => {
10197
10874
  w.postMessage({ type: "cancel", id });
10198
10875
  };
10199
- if (opts.signal?.aborted) onAbort();
10200
- else opts.signal?.addEventListener("abort", onAbort, { once: true });
10876
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
10201
10877
  const cleanup = () => {
10202
10878
  clearTimeout(timer);
10203
10879
  opts.signal?.removeEventListener("abort", onAbort);
@@ -10341,7 +11017,7 @@ var codebaseIndexTool = {
10341
11017
  category: "Project",
10342
11018
  icon: "index",
10343
11019
  description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
10344
- usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
11020
+ usageHint: "CREATE OR REFRESH THE SEARCH INDEX:\n\n- When `codebase-stats` or `codebase-search` reports no persisted index, call this without arguments, then retry the search.\n- Normal and first-time usage: call without arguments for an incremental build.\n- Use `force: true` only for a corrupt/stale index or an explicitly requested clean rebuild.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
10345
11021
  permission: "confirm",
10346
11022
  mutating: true,
10347
11023
  capabilities: ["fs.write.outside-project"],
@@ -10397,12 +11073,14 @@ var codebaseSearchTool = {
10397
11073
  name: "codebase-search",
10398
11074
  category: "Project",
10399
11075
  icon: "index",
10400
- 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).",
10401
- usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
11076
+ description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
11077
+ usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
10402
11078
  permission: "auto",
10403
11079
  mutating: false,
10404
11080
  capabilities: ["fs.read"],
10405
- timeoutMs: 1e4,
11081
+ // The index host has its own 30s read watchdog. Leave enough headroom for
11082
+ // worker teardown and structured timeout reporting.
11083
+ timeoutMs: 35e3,
10406
11084
  inputSchema: {
10407
11085
  type: "object",
10408
11086
  properties: {
@@ -10412,11 +11090,34 @@ var codebaseSearchTool = {
10412
11090
  },
10413
11091
  kind: {
10414
11092
  type: "string",
10415
- description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
11093
+ enum: [
11094
+ "class",
11095
+ "interface",
11096
+ "enum",
11097
+ "type",
11098
+ "function",
11099
+ "method",
11100
+ "var",
11101
+ "const",
11102
+ "let",
11103
+ "property",
11104
+ "parameter",
11105
+ "namespace",
11106
+ "object",
11107
+ "literal",
11108
+ "schema",
11109
+ "struct",
11110
+ "trait",
11111
+ "impl",
11112
+ "static",
11113
+ "mod"
11114
+ ],
11115
+ description: "Filter by indexed symbol kind"
10416
11116
  },
10417
11117
  lang: {
10418
11118
  type: "string",
10419
- description: "Filter by language: ts, tsx, js, jsx"
11119
+ enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
11120
+ description: "Filter by indexed language"
10420
11121
  },
10421
11122
  lspKind: {
10422
11123
  type: "integer",
@@ -10441,20 +11142,12 @@ var codebaseSearchTool = {
10441
11142
  },
10442
11143
  async execute(input, ctx, execOpts) {
10443
11144
  const state = getIndexState();
10444
- if (!state.ready) {
10445
- return {
10446
- results: [],
10447
- total: 0,
10448
- query: input.query,
10449
- 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."
10450
- };
10451
- }
10452
- if (state.indexing) {
11145
+ if (state.indexing && !state.ready) {
10453
11146
  return {
10454
11147
  results: [],
10455
11148
  total: 0,
10456
11149
  query: input.query,
10457
- indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
11150
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
10458
11151
  };
10459
11152
  }
10460
11153
  if (state.lastError) {
@@ -10467,21 +11160,37 @@ var codebaseSearchTool = {
10467
11160
  indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
10468
11161
  };
10469
11162
  }
10470
- const limit = Math.min(input.limit ?? 20, 100);
11163
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
10471
11164
  const { results, total } = await searchCodebaseIndex(
10472
11165
  {
10473
11166
  projectRoot: ctx.projectRoot,
10474
11167
  indexDir: codebaseIndexDirOverride(ctx),
10475
11168
  query: input.query,
10476
- kind: input.kind,
10477
- lang: input.lang,
11169
+ kind: input.kind?.toLowerCase(),
11170
+ lang: input.lang?.toLowerCase(),
10478
11171
  file: input.file,
10479
11172
  lspKind: input.lspKind,
10480
11173
  limit
10481
11174
  },
10482
11175
  { signal: execOpts?.signal }
10483
11176
  );
10484
- return { results, total, query: input.query };
11177
+ let hasPersistedIndex = state.ready || total > 0;
11178
+ if (!hasPersistedIndex) {
11179
+ try {
11180
+ const stats = await codebaseIndexStats(
11181
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11182
+ { signal: execOpts?.signal }
11183
+ );
11184
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11185
+ } catch {
11186
+ }
11187
+ }
11188
+ return {
11189
+ results,
11190
+ total,
11191
+ query: input.query,
11192
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
11193
+ };
10485
11194
  }
10486
11195
  };
10487
11196
 
@@ -10490,12 +11199,15 @@ var codebaseStatsTool = {
10490
11199
  name: "codebase-stats",
10491
11200
  category: "Project",
10492
11201
  icon: "index",
10493
- 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.",
10494
- usageHint: "CALL BEFORE HEAVY CODEBASE-SEARCH WORK:\n\n- Use to see if the index is up-to-date or needs a refresh.\n- No arguments required.\n- Helps avoid wasting tokens on searches against a stale index.\nLightweight and safe to call frequently.",
11202
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
11203
+ usageHint: "CHECK ONCE BEFORE BROAD CODE EXPLORATION:\n\n- Call before broad `tree`, `glob`, or `grep` exploration to see whether index-backed search is available.\n- If it reports no persisted data, run `codebase-index`, then use `codebase-search`.\n- No arguments required.\nLightweight and safe to call frequently.",
10495
11204
  permission: "auto",
10496
11205
  mutating: false,
10497
11206
  capabilities: ["fs.read"],
10498
- timeoutMs: 5e3,
11207
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
11208
+ // longer so the host can return a structured timeout instead of being
11209
+ // pre-empted by the generic tool executor.
11210
+ timeoutMs: 35e3,
10499
11211
  inputSchema: {
10500
11212
  type: "object",
10501
11213
  properties: {},
@@ -10503,7 +11215,8 @@ var codebaseStatsTool = {
10503
11215
  },
10504
11216
  async execute(_input, ctx, execOpts) {
10505
11217
  const idxState = getIndexState();
10506
- if (!idxState.ready) {
11218
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
11219
+ if (idxState.indexing) {
10507
11220
  return {
10508
11221
  totalSymbols: 0,
10509
11222
  totalFiles: 0,
@@ -10511,22 +11224,47 @@ var codebaseStatsTool = {
10511
11224
  byKind: {},
10512
11225
  lastIndexed: null,
10513
11226
  sizeBytes: 0,
10514
- indexPath: "",
11227
+ indexPath,
10515
11228
  version: SCHEMA_VERSION,
10516
- indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
11229
+ statsAvailable: false,
11230
+ indexing: {
11231
+ currentFile: idxState.currentFile,
11232
+ totalFiles: idxState.totalFiles
11233
+ },
11234
+ indexStatus: idxState.ready ? `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). The persisted snapshot remains available to codebase-search; retry stats after refresh.` : `Startup indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files). Do not start another index; retry stats or codebase-search after completion.`
10517
11235
  };
10518
11236
  }
10519
- const stats = await codebaseIndexStats(
10520
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
10521
- { signal: execOpts?.signal }
10522
- );
10523
- if (idxState.indexing) {
11237
+ let stats;
11238
+ try {
11239
+ stats = await codebaseIndexStats(
11240
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11241
+ { signal: execOpts?.signal }
11242
+ );
11243
+ } catch (err) {
11244
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
11245
+ throw err;
11246
+ }
10524
11247
  return {
10525
- ...stats,
10526
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
11248
+ totalSymbols: 0,
11249
+ totalFiles: 0,
11250
+ byLang: {},
11251
+ byKind: {},
11252
+ lastIndexed: null,
11253
+ sizeBytes: 0,
11254
+ indexPath,
11255
+ version: SCHEMA_VERSION,
11256
+ statsAvailable: false,
11257
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
10527
11258
  };
10528
11259
  }
10529
11260
  const circuit = idxState.circuit;
11261
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11262
+ let indexStatus;
11263
+ if (circuit.state === "open") {
11264
+ indexStatus = `${hasPersistedIndex ? "Indexing" : "No persisted index data found, and indexing"} is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. ` + (hasPersistedIndex ? "Stats reflect the last successful build." : "Build the index after recovery.");
11265
+ } else if (!hasPersistedIndex) {
11266
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
11267
+ }
10530
11268
  return {
10531
11269
  totalSymbols: stats.totalSymbols,
10532
11270
  totalFiles: stats.totalFiles,
@@ -10536,9 +11274,8 @@ var codebaseStatsTool = {
10536
11274
  sizeBytes: stats.sizeBytes,
10537
11275
  indexPath: stats.indexPath,
10538
11276
  version: stats.version,
10539
- ...circuit.state === "open" ? {
10540
- 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.`
10541
- } : {}
11277
+ statsAvailable: true,
11278
+ ...indexStatus ? { indexStatus } : {}
10542
11279
  };
10543
11280
  }
10544
11281
  };
@@ -11570,25 +12307,46 @@ function findLadderMatches(fileLf, oldLf) {
11570
12307
  const needleLines = oldLf.split("\n");
11571
12308
  if (needleLines.length > fileLines.length) return void 0;
11572
12309
  const offsets = lineOffsets(fileLines);
12310
+ const fileTrimEnd = fileLines.map((l) => l.trimEnd());
12311
+ const needleTrimEnd = needleLines.map((l) => l.trimEnd());
11573
12312
  const trailing = windowScan(
11574
- fileLines,
11575
- needleLines,
12313
+ fileTrimEnd,
12314
+ needleTrimEnd,
11576
12315
  offsets,
11577
- (a, b) => a.trimEnd() === b.trimEnd()
12316
+ fileLines,
12317
+ (a, b) => a === b
11578
12318
  );
11579
12319
  if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
11580
- const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
12320
+ const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);
11581
12321
  if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
11582
- const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
12322
+ const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());
12323
+ const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());
12324
+ const normalized = windowScan(
12325
+ fileTrimmed,
12326
+ needleTrimmed,
12327
+ offsets,
12328
+ fileLines,
12329
+ (a, b) => a === b
12330
+ );
11583
12331
  if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
11584
12332
  return fuzzyScan(fileLines, needleLines, offsets);
11585
12333
  }
11586
12334
  function lineAt(text, pos) {
12335
+ if (pos < 512) {
12336
+ let line2 = 1;
12337
+ for (let i = 0; i < pos; i++) {
12338
+ if (text.charCodeAt(i) === 10) line2++;
12339
+ }
12340
+ return line2;
12341
+ }
11587
12342
  let line = 1;
11588
- for (let i = 0; i < pos; i++) {
11589
- if (text.charCodeAt(i) === 10) line++;
12343
+ let search = 0;
12344
+ while (true) {
12345
+ const idx = text.indexOf("\n", search);
12346
+ if (idx === -1 || idx >= pos) return line;
12347
+ search = idx + 1;
12348
+ line++;
11590
12349
  }
11591
- return line;
11592
12350
  }
11593
12351
  function lineOffsets(lines) {
11594
12352
  const out = new Array(lines.length);
@@ -11607,19 +12365,19 @@ function windowToMatch(fileLines, offsets, start, windowLen) {
11607
12365
  startLine: start + 1
11608
12366
  };
11609
12367
  }
11610
- function windowScan(fileLines, needleLines, offsets, eq) {
12368
+ function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
11611
12369
  const n = needleLines.length;
11612
12370
  const out = [];
11613
- for (let i = 0; i + n <= fileLines.length; i++) {
12371
+ for (let i = 0; i + n <= comparisonLines.length; i++) {
11614
12372
  let all = true;
11615
12373
  for (let j = 0; j < n; j++) {
11616
- if (!eq(fileLines[i + j], needleLines[j])) {
12374
+ if (!eq(comparisonLines[i + j], needleLines[j])) {
11617
12375
  all = false;
11618
12376
  break;
11619
12377
  }
11620
12378
  }
11621
12379
  if (all) {
11622
- out.push(windowToMatch(fileLines, offsets, i, n));
12380
+ out.push(windowToMatch(originalLines, offsets, i, n));
11623
12381
  i += n - 1;
11624
12382
  }
11625
12383
  }
@@ -11941,8 +12699,10 @@ var editTool = {
11941
12699
  note: autoReadNote
11942
12700
  };
11943
12701
  }
12702
+ opts?.signal?.throwIfAborted();
11944
12703
  const ladder = findLadderMatches(fileLf, oldLf);
11945
12704
  if (!ladder) {
12705
+ opts?.signal?.throwIfAborted();
11946
12706
  const hint = nearestMatchHint(fileLf, oldLf);
11947
12707
  throw new ToolValidationError({
11948
12708
  message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
@@ -12010,6 +12770,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
12010
12770
  before: original,
12011
12771
  after: newFile
12012
12772
  });
12773
+ opts?.signal?.throwIfAborted();
12013
12774
  const diff = unifiedDiff(original, newFile, {
12014
12775
  fromFile: input.path,
12015
12776
  toFile: input.path
@@ -12033,6 +12794,11 @@ Compare this against your old_string and retry with the file's actual text.` : "
12033
12794
 
12034
12795
  // src/exec.ts
12035
12796
  import { spawn as spawn8 } from "node:child_process";
12797
+ import {
12798
+ emitProcessCompleted as emitProcessCompleted3,
12799
+ emitProcessOutput as emitProcessOutput3,
12800
+ emitProcessStarted as emitProcessStarted3
12801
+ } from "@wrongstack/core";
12036
12802
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
12037
12803
  init_output_spool();
12038
12804
  init_util();
@@ -13141,12 +13907,30 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13141
13907
  resolve14(result);
13142
13908
  };
13143
13909
  const startedAt = Date.now();
13910
+ let stdoutBytes = 0;
13911
+ let stderrBytes = 0;
13912
+ let telemetryCompleted = false;
13913
+ let timedOut = false;
13144
13914
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
13145
13915
  const resolved = resolveWin32Command(cmd);
13146
13916
  const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
13147
13917
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
13148
13918
  const spawnCmd = shim?.command ?? resolved;
13149
13919
  const spawnArgs = shim?.args ?? args;
13920
+ const emitCompletedOnce = (exitCode, pid2, signal2) => {
13921
+ if (telemetryCompleted) return;
13922
+ telemetryCompleted = true;
13923
+ emitProcessCompleted3({
13924
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
13925
+ exitCode,
13926
+ ...signal2 ? { signal: signal2 } : {},
13927
+ durationMs: Date.now() - startedAt,
13928
+ stdoutBytes,
13929
+ stderrBytes,
13930
+ timedOut,
13931
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
13932
+ });
13933
+ };
13150
13934
  let child;
13151
13935
  try {
13152
13936
  child = spawn8(spawnCmd, spawnArgs, {
@@ -13159,6 +13943,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13159
13943
  });
13160
13944
  } catch (err) {
13161
13945
  spool.finalize();
13946
+ emitProcessStarted3({
13947
+ parentPid: process.pid,
13948
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
13949
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
13950
+ cwd,
13951
+ background: false,
13952
+ startedAt: new Date(startedAt).toISOString()
13953
+ });
13954
+ emitCompletedOnce(1, void 0);
13162
13955
  finish({
13163
13956
  command: cmd,
13164
13957
  args,
@@ -13171,6 +13964,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13171
13964
  });
13172
13965
  return;
13173
13966
  }
13967
+ emitProcessStarted3({
13968
+ ...child.pid !== void 0 ? { pid: child.pid } : {},
13969
+ parentPid: process.pid,
13970
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
13971
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
13972
+ cwd,
13973
+ background: false,
13974
+ startedAt: new Date(startedAt).toISOString()
13975
+ });
13174
13976
  child.on("error", (err) => {
13175
13977
  const isAbort = err && err.code === "ABORT_ERR";
13176
13978
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
@@ -13179,6 +13981,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13179
13981
  if (typeof pid === "number") registry.unregister(pid);
13180
13982
  registry.afterCall(Date.now() - startedAt, true);
13181
13983
  spool.finalize();
13984
+ emitCompletedOnce(isAbort ? 124 : 1, child.pid, isAbort ? "ABORT" : void 0);
13182
13985
  finish({
13183
13986
  command: cmd,
13184
13987
  args,
@@ -13198,6 +14001,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13198
14001
  }
13199
14002
  const timer = setTimeout(() => {
13200
14003
  killed = true;
14004
+ timedOut = true;
13201
14005
  if (typeof pid === "number") registry.kill(pid);
13202
14006
  else child.kill("SIGTERM");
13203
14007
  }, timeout);
@@ -13212,11 +14016,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13212
14016
  }
13213
14017
  child.stdout?.on("data", (chunk) => {
13214
14018
  const text = chunk.toString();
14019
+ stdoutBytes += chunk.byteLength;
14020
+ emitProcessOutput3({ pid, stream: "stdout", chunk });
13215
14021
  if (stdout.length < MAX_OUTPUT2) stdout += text;
13216
14022
  spool.write(text);
13217
14023
  });
13218
14024
  child.stderr?.on("data", (chunk) => {
13219
14025
  const text = chunk.toString();
14026
+ stderrBytes += chunk.byteLength;
14027
+ emitProcessOutput3({ pid, stream: "stderr", chunk });
13220
14028
  if (stderr.length < MAX_OUTPUT2) stderr += text;
13221
14029
  spool.write(text);
13222
14030
  });
@@ -13226,6 +14034,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13226
14034
  if (typeof pid === "number") registry.unregister(pid);
13227
14035
  const durationMs = Date.now() - startedAt;
13228
14036
  const exitCode = killed ? 124 : code ?? 1;
14037
+ emitCompletedOnce(exitCode, pid);
13229
14038
  registry.afterCall(durationMs, exitCode !== 0);
13230
14039
  const spooled = spool.finalize();
13231
14040
  finish({
@@ -13301,7 +14110,7 @@ function guardedLookup(hostname2, options, callback) {
13301
14110
  var pinnedAgent;
13302
14111
  function getPinnedDispatcher() {
13303
14112
  if (!pinnedAgent) {
13304
- pinnedAgent = new Agent({ connect: { lookup: guardedLookup } });
14113
+ pinnedAgent = new Agent({ allowH2: false, connect: { lookup: guardedLookup } });
13305
14114
  }
13306
14115
  return pinnedAgent;
13307
14116
  }
@@ -13403,7 +14212,7 @@ async function assertNotPrivate(hostname2) {
13403
14212
  }
13404
14213
  }
13405
14214
  } catch (err) {
13406
- if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
14215
+ if (err instanceof ToolValidationError2) throw err;
13407
14216
  }
13408
14217
  }
13409
14218
  }
@@ -13751,7 +14560,7 @@ async function detectFixer(cwd) {
13751
14560
  init_util();
13752
14561
  import { spawn as spawn9 } from "node:child_process";
13753
14562
  import { statSync as statSync3 } from "node:fs";
13754
- import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
14563
+ import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
13755
14564
  import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
13756
14565
  var TIMEOUT_MS2 = 3e4;
13757
14566
  var MAX_OUTPUT3 = 1e5;
@@ -13896,7 +14705,7 @@ function validateWorktreeInput(input, projectRoot) {
13896
14705
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
13897
14706
  const root = resolve11(projectRoot);
13898
14707
  const abs = resolve11(root, input.worktreePath);
13899
- if (abs !== root && !abs.startsWith(root + sep4)) {
14708
+ if (abs !== root && !abs.startsWith(root + sep5)) {
13900
14709
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
13901
14710
  }
13902
14711
  }
@@ -14059,8 +14868,8 @@ var WALK_CONCURRENCY = 16;
14059
14868
  var globTool = {
14060
14869
  name: "glob",
14061
14870
  category: "Filesystem",
14062
- description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
14063
- usageHint: "RECOMMENDED FOR SCOPING SEARCHES:\n\n- Use early to get a list of files you actually care about.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
14871
+ description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
14872
+ usageHint: "PATH DISCOVERY AND SEARCH SCOPING:\n\n- When `codebase-search` is live, use it first for code concepts; use `glob` for filenames, path patterns, and non-indexed files.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
14064
14873
  selection: {
14065
14874
  doNotUseWhen: "you need to search inside file contents.",
14066
14875
  useInstead: ["grep"]
@@ -14248,8 +15057,8 @@ var NATIVE_MAX_FILE_BYTES = 1e6;
14248
15057
  var grepTool = {
14249
15058
  name: "grep",
14250
15059
  category: "Search",
14251
- 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.",
14252
- usageHint: 'POWERFUL CODE SEARCH TOOL:\n\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
15060
+ description: "Search exact file contents using a regular expression. Use index-backed `codebase-search` first for symbol or concept discovery when it is live. Prefers ripgrep for speed and features when available.",
15061
+ usageHint: 'EXACT-TEXT AND REGEX SEARCH:\n\n- When `codebase-search` is live, use it first for symbols and concepts; use `grep` for exact text, regexes, unsupported content, and concrete usage sites.\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
14253
15062
  selection: {
14254
15063
  doNotUseWhen: "you only need to locate files by name or path pattern.",
14255
15064
  useInstead: ["glob"]
@@ -15271,7 +16080,8 @@ function toYaml(data, indent = 0) {
15271
16080
  }
15272
16081
 
15273
16082
  // src/kanban.ts
15274
- import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core";
16083
+ import { randomUUID } from "node:crypto";
16084
+ import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
15275
16085
  import {
15276
16086
  addCheckToTask,
15277
16087
  addColumn,
@@ -15283,18 +16093,19 @@ import {
15283
16093
  assignTask,
15284
16094
  claimReadyTask,
15285
16095
  copyTaskToBoard,
15286
- createBoard,
16096
+ createBoard as createBoard2,
16097
+ createBoardFromTaskGraph,
15287
16098
  duplicateBoard,
15288
16099
  exportBoardAsMarkdown,
15289
16100
  exportBoardToTaskGraph,
15290
16101
  generateBoardFromDescription,
15291
- getBoard,
16102
+ getBoard as getBoard2,
15292
16103
  getKanbanOrchestrationSnapshot,
15293
16104
  getKanbanQueueHealth,
15294
16105
  getTask,
15295
16106
  getTaskChain,
15296
16107
  heartbeatTaskAssignment,
15297
- listBoards,
16108
+ listBoards as listBoards2,
15298
16109
  listKanbanEvents,
15299
16110
  listReadyTasks,
15300
16111
  mergeTasks,
@@ -15302,26 +16113,255 @@ import {
15302
16113
  parseLinesIntoTasks,
15303
16114
  recoverStaleTaskAssignments,
15304
16115
  releaseTaskClaim,
15305
- removeBoard,
16116
+ removeBoard as removeBoard2,
15306
16117
  removeColumn,
15307
16118
  removeTask,
15308
16119
  searchKanban,
15309
16120
  setTaskChain,
15310
16121
  splitTask,
15311
- syncBoardFromTaskGraph,
16122
+ syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
16123
+ transitionTask,
15312
16124
  transferTaskToBoard,
15313
- updateBoard,
16125
+ touchKanbanPresence as touchKanbanPresence2,
16126
+ updateBoard as updateBoard2,
15314
16127
  updateCheckOnTask,
15315
16128
  updateColumn,
15316
16129
  updateGoalMetricOnTask,
15317
16130
  updateTask,
15318
16131
  updateTaskAssignment
15319
16132
  } from "@wrongstack/kanban";
16133
+
16134
+ // src/session-kanban.ts
16135
+ import {
16136
+ deserializeTaskGraph,
16137
+ GlobalMailbox,
16138
+ loadPlan,
16139
+ loadTasks,
16140
+ mutatePlan,
16141
+ mutateTasks
16142
+ } from "@wrongstack/core";
16143
+ import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
16144
+ import {
16145
+ createBoard,
16146
+ getBoard,
16147
+ getKanbanDir,
16148
+ listBoards,
16149
+ removeBoard,
16150
+ syncBoardFromTaskGraph,
16151
+ touchKanbanPresence,
16152
+ updateBoard
16153
+ } from "@wrongstack/kanban";
16154
+ var SESSION_BOARD_TAG = "session-work";
16155
+ var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
16156
+ var SESSION_KANBAN_COLUMNS = [
16157
+ { id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
16158
+ { id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
16159
+ { id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
16160
+ { id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
16161
+ ];
16162
+ var boardQueue = /* @__PURE__ */ new Map();
16163
+ var boardEnsures = /* @__PURE__ */ new Map();
16164
+ function boardKey(projectRoot, sessionId) {
16165
+ return `${projectRoot}\0${sessionId}`;
16166
+ }
16167
+ function sessionTag(sessionId) {
16168
+ return `session:${sessionId}`;
16169
+ }
16170
+ function sessionBoardTitle(sessionId) {
16171
+ const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
16172
+ return `Session ${leaf.slice(0, 12)}`;
16173
+ }
16174
+ function sessionBoardTags(sessionId) {
16175
+ return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
16176
+ }
16177
+ function sameColumns(columns) {
16178
+ return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
16179
+ }
16180
+ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
16181
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
16182
+ const key = boardKey(projectRoot, sessionId);
16183
+ const inFlight = boardEnsures.get(key);
16184
+ if (inFlight) return inFlight;
16185
+ const promise = (async () => {
16186
+ const summary = (await listBoards(projectRoot)).find(
16187
+ (board2) => board2.tags?.includes(sessionTag(sessionId))
16188
+ );
16189
+ let board = summary ? await getBoard(projectRoot, summary.id) : null;
16190
+ if (!board) {
16191
+ return createBoard(projectRoot, {
16192
+ title: sessionBoardTitle(sessionId),
16193
+ description: "Live session work: todos, tasks, and plan items.",
16194
+ tags: sessionBoardTags(sessionId),
16195
+ columns: SESSION_KANBAN_COLUMNS,
16196
+ generatedBy: `session-kanban:${sessionId}`
16197
+ });
16198
+ }
16199
+ if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
16200
+ board = await updateBoard(projectRoot, board.id, {
16201
+ title: sessionBoardTitle(sessionId),
16202
+ description: "Live session work: todos, tasks, and plan items.",
16203
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
16204
+ columns: SESSION_KANBAN_COLUMNS
16205
+ }) ?? board;
16206
+ }
16207
+ return board;
16208
+ })();
16209
+ boardEnsures.set(key, promise);
16210
+ try {
16211
+ return await promise;
16212
+ } finally {
16213
+ boardEnsures.delete(key);
16214
+ }
16215
+ }
16216
+ function enqueueBoardWork(projectRoot, sessionId, work) {
16217
+ const key = boardKey(projectRoot, sessionId);
16218
+ const previous = boardQueue.get(key) ?? Promise.resolve();
16219
+ const result = previous.catch(() => void 0).then(work);
16220
+ const tail = result.then(
16221
+ () => void 0,
16222
+ () => void 0
16223
+ );
16224
+ boardQueue.set(key, tail);
16225
+ void tail.then(() => {
16226
+ if (boardQueue.get(key) === tail) boardQueue.delete(key);
16227
+ });
16228
+ return result;
16229
+ }
16230
+ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
16231
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
16232
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
16233
+ const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
16234
+ if (!board) return null;
16235
+ const result = await syncBoardFromTaskGraph(
16236
+ projectRoot,
16237
+ board.id,
16238
+ deserializeTaskGraph(graph),
16239
+ {
16240
+ sourceSystem,
16241
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
16242
+ archiveMissingTasks: true,
16243
+ includeCompletedTasks: true
16244
+ }
16245
+ );
16246
+ return result?.board ?? null;
16247
+ });
16248
+ }
16249
+ function todoListToSerializedGraph(todos, sessionId) {
16250
+ const nodes = todos.map((todo, index) => ({
16251
+ id: todo.id,
16252
+ title: todo.content,
16253
+ description: todo.activeForm ?? "",
16254
+ type: "chore",
16255
+ priority: "medium",
16256
+ status: todo.status,
16257
+ createdAt: index,
16258
+ updatedAt: index
16259
+ }));
16260
+ return {
16261
+ id: `todo:${sessionId}`,
16262
+ specId: `todo:${sessionId}`,
16263
+ title: "Session todos",
16264
+ nodes,
16265
+ edges: [],
16266
+ rootNodes: nodes.map((node) => node.id),
16267
+ createdAt: 0,
16268
+ updatedAt: 0
16269
+ };
16270
+ }
16271
+ function taskFileToSerializedGraph(tasks, sessionId) {
16272
+ const ids = new Set(tasks.map((task) => task.id));
16273
+ const nodes = tasks.map((task, index) => ({
16274
+ id: task.id,
16275
+ title: task.title,
16276
+ description: task.description ?? "",
16277
+ type: task.type,
16278
+ priority: task.priority,
16279
+ status: task.status,
16280
+ ...task.assignee ? { assignee: task.assignee } : {},
16281
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
16282
+ createdAt: index,
16283
+ updatedAt: index
16284
+ }));
16285
+ const edges = tasks.flatMap(
16286
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
16287
+ id: `${dependency}->${task.id}`,
16288
+ from: dependency,
16289
+ to: task.id,
16290
+ type: "depends_on"
16291
+ }))
16292
+ );
16293
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
16294
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
16295
+ return {
16296
+ // Keep the historical graph id so existing mirrored task cards are reused.
16297
+ id: `session:${sessionId}`,
16298
+ specId: `session:${sessionId}`,
16299
+ title: "Session tasks",
16300
+ nodes,
16301
+ edges,
16302
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
16303
+ createdAt: 0,
16304
+ updatedAt: 0
16305
+ };
16306
+ }
16307
+ var PLAN_STATUS_TO_TASK = {
16308
+ open: "pending",
16309
+ in_progress: "in_progress",
16310
+ done: "completed"
16311
+ };
16312
+ function planFileToSerializedGraph(items, sessionId) {
16313
+ const nodes = items.map((item, index) => ({
16314
+ id: item.id,
16315
+ title: item.title,
16316
+ description: item.details ?? "",
16317
+ type: "chore",
16318
+ priority: "medium",
16319
+ status: PLAN_STATUS_TO_TASK[item.status],
16320
+ createdAt: index,
16321
+ updatedAt: index
16322
+ }));
16323
+ return {
16324
+ id: `plan:${sessionId}`,
16325
+ specId: `plan:${sessionId}`,
16326
+ title: "Session plan",
16327
+ nodes,
16328
+ edges: [],
16329
+ rootNodes: nodes.map((node) => node.id),
16330
+ createdAt: 0,
16331
+ updatedAt: 0
16332
+ };
16333
+ }
16334
+ function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
16335
+ return projectGraph(
16336
+ projectRoot,
16337
+ sessionId,
16338
+ todoListToSerializedGraph(todos, sessionId),
16339
+ "session-todo"
16340
+ );
16341
+ }
16342
+ function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
16343
+ return projectGraph(
16344
+ projectRoot,
16345
+ sessionId,
16346
+ taskFileToSerializedGraph(tasks, sessionId),
16347
+ "session-task"
16348
+ );
16349
+ }
16350
+ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
16351
+ return projectGraph(
16352
+ projectRoot,
16353
+ sessionId,
16354
+ planFileToSerializedGraph(items, sessionId),
16355
+ "session-plan"
16356
+ );
16357
+ }
16358
+
16359
+ // src/kanban.ts
15320
16360
  var kanbanTool = {
15321
16361
  name: "kanban",
15322
16362
  category: "Project",
15323
- 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.",
15324
- usageHint: "Use this for durable project kanban state. Agents should call snapshot/ready_tasks, then claim_task before working. Use set_chain for ordered work, split_task/merge_tasks when task scope changes, assign_task with provider/model/fallback hints before spawning, mark_assignment when starting or finishing, and release_task if the claim cannot be worked.",
16363
+ description: "Manage and audit project-scoped multi-kanban boards stored under .wrongstack/kanbans. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Successful board access records live agent/session presence.",
16364
+ usageHint: "Use this for durable project kanban state. Reassess with get_board whenever evidence changes; agents may add, update, split, merge, reprioritize, or remove tasks so the board remains a live plan. Presence includes active/last-seen session and agent metadata. For managed boards, fully fill card details, use transition_task after every material step, attach truthful evidence, and never use update_task/move_task to bypass lifecycle guards. Worker completion enters Review; only passed acceptance criteria plus review evidence allow Done.",
15325
16365
  permission: "confirm",
15326
16366
  mutating: true,
15327
16367
  capabilities: ["fs.write"],
@@ -15343,6 +16383,8 @@ var kanbanTool = {
15343
16383
  "export_markdown",
15344
16384
  "export_task_graph",
15345
16385
  "sync_task_graph",
16386
+ "create_from_graph",
16387
+ "import_session_tasks",
15346
16388
  "search_tasks",
15347
16389
  "ready_tasks",
15348
16390
  "snapshot",
@@ -15356,6 +16398,7 @@ var kanbanTool = {
15356
16398
  "transfer_task",
15357
16399
  "get_task",
15358
16400
  "update_task",
16401
+ "transition_task",
15359
16402
  "move_task",
15360
16403
  "delete_task",
15361
16404
  "set_chain",
@@ -15386,9 +16429,14 @@ var kanbanTool = {
15386
16429
  targetColumnId: { type: "string" },
15387
16430
  title: { type: "string" },
15388
16431
  description: { type: "string" },
16432
+ dueDate: { type: "string" },
15389
16433
  tags: { type: "array", items: { type: "string" } },
15390
16434
  labels: { type: "array", items: { type: "string" } },
15391
16435
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
16436
+ taskType: {
16437
+ type: "string",
16438
+ enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
16439
+ },
15392
16440
  status: {
15393
16441
  type: "string",
15394
16442
  enum: [
@@ -15428,11 +16476,36 @@ var kanbanTool = {
15428
16476
  type: "string",
15429
16477
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
15430
16478
  },
16479
+ lifecycleStage: {
16480
+ type: "string",
16481
+ enum: ["backlog", "todo", "running", "review", "done"]
16482
+ },
16483
+ transitionAction: { type: "string" },
16484
+ transitionComment: { type: "string" },
16485
+ attachmentUrl: { type: "string" },
16486
+ attachmentTitle: { type: "string" },
16487
+ attachmentType: {
16488
+ type: "string",
16489
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
16490
+ },
15431
16491
  releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
15432
16492
  releaseReason: { type: "string" },
15433
16493
  clearAssignee: { type: "boolean" },
15434
- recoveryMode: { type: "string", enum: ["release", "retry", "fail"] },
16494
+ recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
15435
16495
  recoveryNow: { type: "string" },
16496
+ recoveryPolicyFailOnCostCeiling: { type: "boolean" },
16497
+ recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
16498
+ recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
16499
+ recoveryPolicyRetryPolicyOverride: {
16500
+ type: "string",
16501
+ enum: ["off", "incremental", "exponential"]
16502
+ },
16503
+ assignee: { type: "string" },
16504
+ costCeilingUsd: { type: "number" },
16505
+ retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
16506
+ lastFailureKind: { type: "string" },
16507
+ dependsOn: { type: "array", items: { type: "string" } },
16508
+ estimatedHours: { type: "number" },
15436
16509
  taskGraph: { type: "object" },
15437
16510
  graphId: { type: "string" },
15438
16511
  specId: { type: "string" },
@@ -15485,538 +16558,632 @@ var kanbanTool = {
15485
16558
  async execute(input, ctx) {
15486
16559
  const projectRoot = ctx.projectRoot;
15487
16560
  if (!projectRoot) return fail("No project root is available.");
16561
+ const withPresence = async (result) => {
16562
+ const boardId = result.board?.id ?? input.boardId;
16563
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
16564
+ try {
16565
+ const board = await touchKanbanPresence2(projectRoot, boardId, {
16566
+ sessionId: ctx.session.id,
16567
+ agentId: ctx.agentId,
16568
+ agentName: ctx.agentName,
16569
+ taskId: input.taskId ?? result.task?.id,
16570
+ runTaskId: input.runTaskId
16571
+ });
16572
+ return board ? { ...result, board } : result;
16573
+ } catch {
16574
+ return result;
16575
+ }
16576
+ };
15488
16577
  try {
15489
- switch (input.action) {
15490
- case "list_boards": {
15491
- const boards = await listBoards(projectRoot);
15492
- return { ok: true, message: `${boards.length} board(s).`, boards };
15493
- }
15494
- case "get_board": {
15495
- const board = await requireBoard(projectRoot, input.boardId);
15496
- return board ? okBoard(board) : fail("Board not found.");
15497
- }
15498
- case "create_board": {
15499
- if (!input.title) return fail("create_board requires title.");
15500
- const board = await createBoard(projectRoot, {
15501
- title: input.title,
15502
- ...input.description !== void 0 ? { description: input.description } : {},
15503
- ...input.tags !== void 0 ? { tags: input.tags } : {},
15504
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
15505
- });
15506
- return { ok: true, message: `Board created: ${board.title}`, board };
15507
- }
15508
- case "update_board": {
15509
- if (!input.boardId) return fail("update_board requires boardId.");
15510
- const board = await updateBoard(projectRoot, input.boardId, {
15511
- ...input.title !== void 0 ? { title: input.title } : {},
15512
- ...input.description !== void 0 ? { description: input.description } : {},
15513
- ...input.tags !== void 0 ? { tags: input.tags } : {}
15514
- });
15515
- return board ? okBoard(board, "Board updated.") : fail("Board not found.");
15516
- }
15517
- case "duplicate_board": {
15518
- if (!input.boardId) return fail("duplicate_board requires boardId.");
15519
- const board = await duplicateBoard(projectRoot, input.boardId, {
15520
- ...input.title !== void 0 ? { title: input.title } : {},
15521
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
15522
- ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
15523
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
15524
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
15525
- });
15526
- return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
15527
- }
15528
- case "delete_board": {
15529
- if (!input.boardId) return fail("delete_board requires boardId.");
15530
- const removed = await removeBoard(projectRoot, input.boardId);
15531
- return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
15532
- }
15533
- case "generate_board": {
15534
- if (!input.description) return fail("generate_board requires description.");
15535
- const boardInput = generateBoardFromDescription({
15536
- description: input.description,
15537
- ...input.title !== void 0 ? { title: input.title } : {},
15538
- ...input.context !== void 0 ? { context: input.context } : {},
15539
- ...input.columns !== void 0 ? { columns: input.columns } : {}
15540
- });
15541
- const board = await createBoard(projectRoot, boardInput);
15542
- for (const taskInput2 of parseLinesIntoTasks(
15543
- input.description,
15544
- board.columns[0]?.id ?? "backlog"
15545
- )) {
15546
- await addTask(projectRoot, board.id, taskInput2);
16578
+ const result = await (async () => {
16579
+ switch (input.action) {
16580
+ case "list_boards": {
16581
+ const boards = await listBoards2(projectRoot);
16582
+ return { ok: true, message: `${boards.length} board(s).`, boards };
15547
16583
  }
15548
- return okBoard(await getBoard(projectRoot, board.id) ?? board, "Board generated.");
15549
- }
15550
- case "export_markdown": {
15551
- const board = await requireBoard(projectRoot, input.boardId);
15552
- if (!board) return fail("Board not found.");
15553
- return {
15554
- ok: true,
15555
- message: "Board exported.",
15556
- board,
15557
- markdown: exportBoardAsMarkdown(board)
15558
- };
15559
- }
15560
- case "export_task_graph": {
15561
- if (!input.boardId) return fail("export_task_graph requires boardId.");
15562
- const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
15563
- ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
15564
- ...input.specId !== void 0 ? { specId: input.specId } : {},
15565
- ...input.title !== void 0 ? { title: input.title } : {},
15566
- ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
15567
- ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
15568
- });
15569
- if (!exported) return fail("Board not found.");
15570
- return {
15571
- ok: true,
15572
- message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
15573
- board: exported.board,
15574
- taskGraph: serializeTaskGraph(exported.graph)
15575
- };
15576
- }
15577
- case "sync_task_graph": {
15578
- if (!input.boardId || !input.taskGraph) {
15579
- return fail("sync_task_graph requires boardId and taskGraph.");
16584
+ case "get_board": {
16585
+ const board = await requireBoard(projectRoot, input.boardId);
16586
+ return board ? okBoard(board) : fail("Board not found.");
15580
16587
  }
15581
- const graph = deserializeTaskGraph(input.taskGraph);
15582
- const result = await syncBoardFromTaskGraph(projectRoot, input.boardId, graph, {
15583
- ...input.title !== void 0 ? { title: input.title } : {},
15584
- ...input.description !== void 0 ? { description: input.description } : {},
15585
- ...input.tags !== void 0 ? { tags: input.tags } : {},
15586
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
15587
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
15588
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
15589
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
15590
- ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
15591
- ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
15592
- });
15593
- return result ? {
15594
- ok: true,
15595
- message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
15596
- board: result.board
15597
- } : fail("Board not found.");
15598
- }
15599
- case "search_tasks": {
15600
- const tasks = await searchKanban(projectRoot, {
15601
- query: input.query,
15602
- boardId: input.boardId,
15603
- assignedAgent: input.agentId,
15604
- status: input.status,
15605
- priority: input.priority,
15606
- label: input.labels?.[0],
15607
- chainId: input.chainId
15608
- });
15609
- return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
15610
- }
15611
- case "ready_tasks": {
15612
- const tasks = await listReadyTasks(projectRoot, {
15613
- query: input.query,
15614
- boardId: input.boardId,
15615
- assignedAgent: input.agentId,
15616
- priority: input.priority,
15617
- label: input.labels?.[0],
15618
- chainId: input.chainId,
15619
- limit: input.limit
15620
- });
15621
- return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
15622
- }
15623
- case "snapshot": {
15624
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
15625
- query: input.query,
15626
- boardId: input.boardId,
15627
- assignedAgent: input.agentId,
15628
- status: input.status,
15629
- priority: input.priority,
15630
- label: input.labels?.[0],
15631
- chainId: input.chainId
15632
- });
15633
- return {
15634
- ok: true,
15635
- message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
15636
- snapshot
15637
- };
15638
- }
15639
- case "add_column": {
15640
- if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
15641
- const result = await addColumn(projectRoot, input.boardId, {
15642
- title: input.title,
15643
- ...input.description !== void 0 ? { description: input.description } : {}
15644
- });
15645
- return result ? okBoard(result.board, "Column added.") : fail("Board not found.");
15646
- }
15647
- case "update_column": {
15648
- if (!input.boardId || !input.columnId)
15649
- return fail("update_column requires boardId and columnId.");
15650
- const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
15651
- ...input.title !== void 0 ? { title: input.title } : {},
15652
- ...input.description !== void 0 ? { description: input.description } : {},
15653
- ...input.order !== void 0 ? { order: input.order } : {}
15654
- });
15655
- return board ? okBoard(board, "Column updated.") : fail("Column not found.");
15656
- }
15657
- case "delete_column": {
15658
- if (!input.boardId || !input.columnId)
15659
- return fail("delete_column requires boardId and columnId.");
15660
- const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
15661
- moveTasksToColumnId: input.moveTasksToColumnId
15662
- });
15663
- return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
15664
- }
15665
- case "add_task": {
15666
- if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
15667
- const result = await addTask(projectRoot, input.boardId, taskInput(input));
15668
- return result ? okTask(result.board, result.task, "Task added.") : fail("Board not found.");
15669
- }
15670
- case "split_task": {
15671
- if (!input.boardId || !input.taskId || !input.childTitles?.length) {
15672
- return fail("split_task requires boardId, taskId, and childTitles.");
16588
+ case "create_board": {
16589
+ if (!input.title) return fail("create_board requires title.");
16590
+ const board = await createBoard2(projectRoot, {
16591
+ title: input.title,
16592
+ ...input.description !== void 0 ? { description: input.description } : {},
16593
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16594
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
16595
+ });
16596
+ return { ok: true, message: `Board created: ${board.title}`, board };
15673
16597
  }
15674
- const result = await splitTask(projectRoot, input.boardId, input.taskId, {
15675
- titles: input.childTitles,
15676
- ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
15677
- ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
15678
- ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
15679
- ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
15680
- ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
15681
- ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
15682
- ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
15683
- ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
15684
- });
15685
- return result ? {
15686
- ok: true,
15687
- message: `${result.children.length} child task(s) created.`,
15688
- board: result.board,
15689
- task: result.parent,
15690
- children: result.children
15691
- } : fail("Task not found.");
15692
- }
15693
- case "merge_tasks": {
15694
- if (!input.boardId || !input.taskIds?.length || !input.title) {
15695
- return fail("merge_tasks requires boardId, taskIds, and title.");
16598
+ case "update_board": {
16599
+ if (!input.boardId) return fail("update_board requires boardId.");
16600
+ const board = await updateBoard2(projectRoot, input.boardId, {
16601
+ ...input.title !== void 0 ? { title: input.title } : {},
16602
+ ...input.description !== void 0 ? { description: input.description } : {},
16603
+ ...input.tags !== void 0 ? { tags: input.tags } : {}
16604
+ });
16605
+ return board ? okBoard(board, "Board updated.") : fail("Board not found.");
15696
16606
  }
15697
- const result = await mergeTasks(projectRoot, input.boardId, {
15698
- taskIds: input.taskIds,
15699
- title: input.title,
15700
- ...input.description !== void 0 ? { description: input.description } : {},
15701
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
15702
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
15703
- ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
15704
- });
15705
- return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
15706
- }
15707
- case "copy_task": {
15708
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
15709
- return fail("copy_task requires boardId, taskId, and targetBoardId.");
16607
+ case "duplicate_board": {
16608
+ if (!input.boardId) return fail("duplicate_board requires boardId.");
16609
+ const board = await duplicateBoard(projectRoot, input.boardId, {
16610
+ ...input.title !== void 0 ? { title: input.title } : {},
16611
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16612
+ ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
16613
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16614
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
16615
+ });
16616
+ return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
15710
16617
  }
15711
- const result = await copyTaskToBoard(
15712
- projectRoot,
15713
- input.boardId,
15714
- input.taskId,
15715
- input.targetBoardId,
15716
- {
15717
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
15718
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
15719
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
15720
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16618
+ case "delete_board": {
16619
+ if (!input.boardId) return fail("delete_board requires boardId.");
16620
+ const removed = await removeBoard2(projectRoot, input.boardId);
16621
+ return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
16622
+ }
16623
+ case "generate_board": {
16624
+ if (!input.description) return fail("generate_board requires description.");
16625
+ const boardInput = generateBoardFromDescription({
16626
+ description: input.description,
16627
+ ...input.title !== void 0 ? { title: input.title } : {},
16628
+ ...input.context !== void 0 ? { context: input.context } : {},
16629
+ ...input.columns !== void 0 ? { columns: input.columns } : {}
16630
+ });
16631
+ const board = await createBoard2(projectRoot, boardInput);
16632
+ for (const taskInput2 of parseLinesIntoTasks(
16633
+ input.description,
16634
+ board.columns[0]?.id ?? "backlog"
16635
+ )) {
16636
+ await addTask(projectRoot, board.id, taskInput2);
15721
16637
  }
15722
- );
15723
- return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
15724
- }
15725
- case "transfer_task": {
15726
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
15727
- return fail("transfer_task requires boardId, taskId, and targetBoardId.");
16638
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
15728
16639
  }
15729
- const result = await transferTaskToBoard(
15730
- projectRoot,
15731
- input.boardId,
15732
- input.taskId,
15733
- input.targetBoardId,
15734
- {
16640
+ case "export_markdown": {
16641
+ const board = await requireBoard(projectRoot, input.boardId);
16642
+ if (!board) return fail("Board not found.");
16643
+ return {
16644
+ ok: true,
16645
+ message: "Board exported.",
16646
+ board,
16647
+ markdown: exportBoardAsMarkdown(board)
16648
+ };
16649
+ }
16650
+ case "export_task_graph": {
16651
+ if (!input.boardId) return fail("export_task_graph requires boardId.");
16652
+ const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
16653
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
16654
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
16655
+ ...input.title !== void 0 ? { title: input.title } : {},
16656
+ ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
16657
+ ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
16658
+ });
16659
+ if (!exported) return fail("Board not found.");
16660
+ return {
16661
+ ok: true,
16662
+ message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
16663
+ board: exported.board,
16664
+ taskGraph: serializeTaskGraph(exported.graph)
16665
+ };
16666
+ }
16667
+ case "sync_task_graph": {
16668
+ if (!input.boardId || !input.taskGraph) {
16669
+ return fail("sync_task_graph requires boardId and taskGraph.");
16670
+ }
16671
+ const graph = deserializeTaskGraph2(input.taskGraph);
16672
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
16673
+ ...input.title !== void 0 ? { title: input.title } : {},
16674
+ ...input.description !== void 0 ? { description: input.description } : {},
16675
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16676
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16677
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16678
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16679
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
16680
+ ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
16681
+ ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
16682
+ });
16683
+ return result2 ? {
16684
+ ok: true,
16685
+ message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
16686
+ board: result2.board
16687
+ } : fail("Board not found.");
16688
+ }
16689
+ case "create_from_graph": {
16690
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
16691
+ const graph = deserializeTaskGraph2(input.taskGraph);
16692
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16693
+ ...input.title !== void 0 ? { title: input.title } : {},
16694
+ ...input.description !== void 0 ? { description: input.description } : {},
16695
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16696
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16697
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16698
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16699
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
16700
+ });
16701
+ return {
16702
+ ok: true,
16703
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
16704
+ board
16705
+ };
16706
+ }
16707
+ case "import_session_tasks": {
16708
+ const taskPath = ctx.meta?.["task.path"];
16709
+ if (!taskPath) return fail("No session task file for this session.");
16710
+ const file = await loadTasks2(taskPath);
16711
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
16712
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
16713
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
16714
+ const tags = ["session", `session:${sessionId}`];
16715
+ const existing = (await listBoards2(projectRoot)).find(
16716
+ (b) => b.tags?.includes(`session:${sessionId}`)
16717
+ );
16718
+ if (existing) {
16719
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
16720
+ sourceSystem: "session",
16721
+ tags,
16722
+ archiveMissingTasks: true,
16723
+ includeCompletedTasks: true
16724
+ });
16725
+ return result2 ? {
16726
+ ok: true,
16727
+ message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
16728
+ board: result2.board
16729
+ } : fail("Session board vanished mid-sync.");
16730
+ }
16731
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16732
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
16733
+ sourceSystem: "session",
16734
+ tags
16735
+ });
16736
+ return {
16737
+ ok: true,
16738
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
16739
+ board
16740
+ };
16741
+ }
16742
+ case "search_tasks": {
16743
+ const tasks = await searchKanban(projectRoot, {
16744
+ query: input.query,
16745
+ boardId: input.boardId,
16746
+ assignedAgent: input.agentId,
16747
+ status: input.status,
16748
+ priority: input.priority,
16749
+ label: input.labels?.[0],
16750
+ chainId: input.chainId
16751
+ });
16752
+ return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
16753
+ }
16754
+ case "ready_tasks": {
16755
+ const tasks = await listReadyTasks(projectRoot, {
16756
+ query: input.query,
16757
+ boardId: input.boardId,
16758
+ assignedAgent: input.agentId,
16759
+ priority: input.priority,
16760
+ label: input.labels?.[0],
16761
+ chainId: input.chainId,
16762
+ limit: input.limit
16763
+ });
16764
+ return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
16765
+ }
16766
+ case "snapshot": {
16767
+ const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
16768
+ query: input.query,
16769
+ boardId: input.boardId,
16770
+ assignedAgent: input.agentId,
16771
+ status: input.status,
16772
+ priority: input.priority,
16773
+ label: input.labels?.[0],
16774
+ chainId: input.chainId
16775
+ });
16776
+ return {
16777
+ ok: true,
16778
+ message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
16779
+ snapshot
16780
+ };
16781
+ }
16782
+ case "add_column": {
16783
+ if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
16784
+ const result2 = await addColumn(projectRoot, input.boardId, {
16785
+ title: input.title,
16786
+ ...input.description !== void 0 ? { description: input.description } : {}
16787
+ });
16788
+ return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
16789
+ }
16790
+ case "update_column": {
16791
+ if (!input.boardId || !input.columnId)
16792
+ return fail("update_column requires boardId and columnId.");
16793
+ const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
16794
+ ...input.title !== void 0 ? { title: input.title } : {},
16795
+ ...input.description !== void 0 ? { description: input.description } : {},
16796
+ ...input.order !== void 0 ? { order: input.order } : {}
16797
+ });
16798
+ return board ? okBoard(board, "Column updated.") : fail("Column not found.");
16799
+ }
16800
+ case "delete_column": {
16801
+ if (!input.boardId || !input.columnId)
16802
+ return fail("delete_column requires boardId and columnId.");
16803
+ const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
16804
+ moveTasksToColumnId: input.moveTasksToColumnId
16805
+ });
16806
+ return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
16807
+ }
16808
+ case "add_task": {
16809
+ if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
16810
+ const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
16811
+ return result2 ? okTask(result2.board, result2.task, "Task added.") : fail("Board not found.");
16812
+ }
16813
+ case "split_task": {
16814
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
16815
+ return fail("split_task requires boardId, taskId, and childTitles.");
16816
+ }
16817
+ const result2 = await splitTask(projectRoot, input.boardId, input.taskId, {
16818
+ titles: input.childTitles,
16819
+ ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
16820
+ ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
16821
+ ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
16822
+ ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
16823
+ ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
16824
+ ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
16825
+ ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
16826
+ ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
16827
+ });
16828
+ return result2 ? {
16829
+ ok: true,
16830
+ message: `${result2.children.length} child task(s) created.`,
16831
+ board: result2.board,
16832
+ task: result2.parent,
16833
+ children: result2.children
16834
+ } : fail("Task not found.");
16835
+ }
16836
+ case "merge_tasks": {
16837
+ if (!input.boardId || !input.taskIds?.length || !input.title) {
16838
+ return fail("merge_tasks requires boardId, taskIds, and title.");
16839
+ }
16840
+ const result2 = await mergeTasks(projectRoot, input.boardId, {
16841
+ taskIds: input.taskIds,
16842
+ title: input.title,
16843
+ ...input.description !== void 0 ? { description: input.description } : {},
15735
16844
  ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
15736
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
15737
16845
  ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
15738
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16846
+ ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
16847
+ });
16848
+ return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
16849
+ }
16850
+ case "copy_task": {
16851
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
16852
+ return fail("copy_task requires boardId, taskId, and targetBoardId.");
15739
16853
  }
15740
- );
15741
- return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
15742
- }
15743
- case "get_task": {
15744
- if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
15745
- const task = await getTask(projectRoot, input.boardId, input.taskId);
15746
- return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
15747
- }
15748
- case "update_task": {
15749
- if (!input.boardId || !input.taskId)
15750
- return fail("update_task requires boardId and taskId.");
15751
- const board = await updateTask(
15752
- projectRoot,
15753
- input.boardId,
15754
- input.taskId,
15755
- taskPatch(input)
15756
- );
15757
- return board ? okBoard(board, "Task updated.") : fail("Task not found.");
15758
- }
15759
- case "move_task": {
15760
- if (!input.boardId || !input.taskId || !input.targetColumnId) {
15761
- return fail("move_task requires boardId, taskId, and targetColumnId.");
16854
+ const result2 = await copyTaskToBoard(
16855
+ projectRoot,
16856
+ input.boardId,
16857
+ input.taskId,
16858
+ input.targetBoardId,
16859
+ {
16860
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16861
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
16862
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16863
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16864
+ }
16865
+ );
16866
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
15762
16867
  }
15763
- const board = await moveTask(
15764
- projectRoot,
15765
- input.boardId,
15766
- input.taskId,
15767
- input.targetColumnId,
15768
- input.order
15769
- );
15770
- return board ? okBoard(board, "Task moved.") : fail("Move failed.");
15771
- }
15772
- case "delete_task": {
15773
- if (!input.boardId || !input.taskId)
15774
- return fail("delete_task requires boardId and taskId.");
15775
- const board = await removeTask(projectRoot, input.boardId, input.taskId);
15776
- return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
15777
- }
15778
- case "set_chain": {
15779
- if (!input.boardId || !input.taskIds?.length) {
15780
- return fail("set_chain requires boardId and taskIds.");
16868
+ case "transfer_task": {
16869
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
16870
+ return fail("transfer_task requires boardId, taskId, and targetBoardId.");
16871
+ }
16872
+ const result2 = await transferTaskToBoard(
16873
+ projectRoot,
16874
+ input.boardId,
16875
+ input.taskId,
16876
+ input.targetBoardId,
16877
+ {
16878
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
16879
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
16880
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
16881
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
16882
+ }
16883
+ );
16884
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
15781
16885
  }
15782
- const result = await setTaskChain(projectRoot, input.boardId, {
15783
- taskIds: input.taskIds,
15784
- ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
15785
- ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
15786
- });
15787
- return result ? {
15788
- ok: true,
15789
- message: `Chain set: ${result.chainId}`,
15790
- board: result.board,
15791
- chain: result.tasks
15792
- } : fail("Board or task not found.");
15793
- }
15794
- case "get_chain": {
15795
- if (!input.boardId || !(input.taskId || input.chainId)) {
15796
- return fail("get_chain requires boardId and taskId or chainId.");
16886
+ case "get_task": {
16887
+ if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
16888
+ const task = await getTask(projectRoot, input.boardId, input.taskId);
16889
+ return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
15797
16890
  }
15798
- const result = await getTaskChain(
15799
- projectRoot,
15800
- input.boardId,
15801
- input.taskId ?? input.chainId ?? ""
15802
- );
15803
- return result ? {
15804
- ok: true,
15805
- message: `Chain loaded: ${result.chainId}`,
15806
- board: result.board,
15807
- chain: result.tasks
15808
- } : fail("Chain not found.");
15809
- }
15810
- case "claim_task": {
15811
- const result = await claimReadyTask(projectRoot, {
15812
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
15813
- ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
15814
- ...assignmentInput(input),
15815
- status: input.assignmentStatus ?? "queued"
15816
- });
15817
- return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
15818
- }
15819
- case "release_task": {
15820
- if (!input.boardId || !input.taskId) {
15821
- return fail("release_task requires boardId and taskId.");
16891
+ case "update_task": {
16892
+ if (!input.boardId || !input.taskId)
16893
+ return fail("update_task requires boardId and taskId.");
16894
+ const board = await updateTask(
16895
+ projectRoot,
16896
+ input.boardId,
16897
+ input.taskId,
16898
+ taskPatch(input)
16899
+ );
16900
+ return board ? okBoard(board, "Task updated.") : fail("Task not found.");
15822
16901
  }
15823
- const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
15824
- ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
15825
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
15826
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
15827
- });
15828
- return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
15829
- }
15830
- case "assign_task": {
15831
- if (!input.boardId || !input.taskId)
15832
- return fail("assign_task requires boardId and taskId.");
15833
- const board = await assignTask(
15834
- projectRoot,
15835
- input.boardId,
15836
- input.taskId,
15837
- assignmentInput(input)
15838
- );
15839
- return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
15840
- }
15841
- case "mark_assignment": {
15842
- if (!input.boardId || !input.taskId)
15843
- return fail("mark_assignment requires boardId and taskId.");
15844
- const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
15845
- const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
15846
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
15847
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
15848
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
15849
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
15850
- ...input.error !== void 0 ? { error: input.error } : {},
15851
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
15852
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
15853
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
15854
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
15855
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
15856
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
15857
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
15858
- });
15859
- return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
15860
- }
15861
- case "heartbeat_assignment": {
15862
- if (!input.boardId || !input.taskId) {
15863
- return fail("heartbeat_assignment requires boardId and taskId.");
16902
+ case "transition_task": {
16903
+ if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
16904
+ return fail(
16905
+ "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
16906
+ );
16907
+ }
16908
+ const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
16909
+ to: input.lifecycleStage,
16910
+ actor: input.author,
16911
+ comment: input.transitionComment,
16912
+ ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
16913
+ ...input.attachmentUrl !== void 0 ? {
16914
+ attachment: {
16915
+ url: input.attachmentUrl,
16916
+ type: input.attachmentType ?? "url",
16917
+ ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
16918
+ }
16919
+ } : {},
16920
+ patch: taskPatch(input)
16921
+ });
16922
+ return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
15864
16923
  }
15865
- const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
15866
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
15867
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
15868
- });
15869
- return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
15870
- }
15871
- case "recover_stale": {
15872
- if (!input.boardId) return fail("recover_stale requires boardId.");
15873
- const policyFields = [
15874
- input.recoveryPolicyFailOnCostCeiling !== void 0,
15875
- input.recoveryPolicyReleaseOnFailureKinds !== void 0,
15876
- input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
15877
- input.recoveryPolicyRetryPolicyOverride !== void 0
15878
- ].some(Boolean);
15879
- const result = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
15880
- ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
15881
- ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
15882
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
15883
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
15884
- ...policyFields ? {
15885
- policy: {
15886
- ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
15887
- ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
15888
- releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
15889
- } : {},
15890
- ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
15891
- releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
15892
- } : {},
15893
- ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
15894
- retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
15895
- } : {}
15896
- }
15897
- } : {}
15898
- });
15899
- return result ? {
15900
- ok: true,
15901
- message: `Recovered ${result.tasks.length} stale assignment(s).`,
15902
- board: result.board,
15903
- recoveredTasks: result.tasks
15904
- } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
15905
- }
15906
- case "events": {
15907
- if (!input.boardId) return fail("events requires boardId.");
15908
- const eventList = await listKanbanEvents(projectRoot, input.boardId);
15909
- return {
15910
- ok: true,
15911
- message: `${eventList.length} event(s).`,
15912
- events: eventList
15913
- };
15914
- }
15915
- case "queue_health": {
15916
- const health = await getKanbanQueueHealth(projectRoot, {
15917
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
15918
- });
15919
- return {
15920
- ok: true,
15921
- message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
15922
- queueHealth: health
15923
- };
15924
- }
15925
- case "add_dependency": {
15926
- if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
15927
- return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
16924
+ case "move_task": {
16925
+ if (!input.boardId || !input.taskId || !input.targetColumnId) {
16926
+ return fail("move_task requires boardId, taskId, and targetColumnId.");
16927
+ }
16928
+ const board = await moveTask(
16929
+ projectRoot,
16930
+ input.boardId,
16931
+ input.taskId,
16932
+ input.targetColumnId,
16933
+ input.order
16934
+ );
16935
+ return board ? okBoard(board, "Task moved.") : fail("Move failed.");
15928
16936
  }
15929
- const board = await addDependency(
15930
- projectRoot,
15931
- input.boardId,
15932
- input.taskId,
15933
- input.dependencyTaskId
15934
- );
15935
- return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
15936
- }
15937
- case "add_goal_metric": {
15938
- if (!input.boardId || !input.taskId || !input.metricName) {
15939
- return fail("add_goal_metric requires boardId, taskId, and metricName.");
16937
+ case "delete_task": {
16938
+ if (!input.boardId || !input.taskId)
16939
+ return fail("delete_task requires boardId and taskId.");
16940
+ const board = await removeTask(projectRoot, input.boardId, input.taskId);
16941
+ return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
15940
16942
  }
15941
- const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
15942
- name: input.metricName,
15943
- ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
15944
- ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
15945
- ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
15946
- ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
15947
- ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
15948
- });
15949
- return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
15950
- }
15951
- case "update_goal_metric": {
15952
- if (!input.boardId || !input.taskId || !input.metricId) {
15953
- return fail("update_goal_metric requires boardId, taskId, and metricId.");
16943
+ case "set_chain": {
16944
+ if (!input.boardId || !input.taskIds?.length) {
16945
+ return fail("set_chain requires boardId and taskIds.");
16946
+ }
16947
+ const result2 = await setTaskChain(projectRoot, input.boardId, {
16948
+ taskIds: input.taskIds,
16949
+ ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
16950
+ ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
16951
+ });
16952
+ return result2 ? {
16953
+ ok: true,
16954
+ message: `Chain set: ${result2.chainId}`,
16955
+ board: result2.board,
16956
+ chain: result2.tasks
16957
+ } : fail("Board or task not found.");
15954
16958
  }
15955
- const board = await updateGoalMetricOnTask(
15956
- projectRoot,
15957
- input.boardId,
15958
- input.taskId,
15959
- input.metricId,
15960
- {
15961
- ...input.metricName !== void 0 ? { name: input.metricName } : {},
16959
+ case "get_chain": {
16960
+ if (!input.boardId || !(input.taskId || input.chainId)) {
16961
+ return fail("get_chain requires boardId and taskId or chainId.");
16962
+ }
16963
+ const result2 = await getTaskChain(
16964
+ projectRoot,
16965
+ input.boardId,
16966
+ input.taskId ?? input.chainId ?? ""
16967
+ );
16968
+ return result2 ? {
16969
+ ok: true,
16970
+ message: `Chain loaded: ${result2.chainId}`,
16971
+ board: result2.board,
16972
+ chain: result2.tasks
16973
+ } : fail("Chain not found.");
16974
+ }
16975
+ case "claim_task": {
16976
+ const result2 = await claimReadyTask(projectRoot, {
16977
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
16978
+ ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
16979
+ ...assignmentInput(input),
16980
+ status: input.assignmentStatus ?? "queued"
16981
+ });
16982
+ return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
16983
+ }
16984
+ case "release_task": {
16985
+ if (!input.boardId || !input.taskId) {
16986
+ return fail("release_task requires boardId and taskId.");
16987
+ }
16988
+ const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
16989
+ ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
16990
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
16991
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
16992
+ });
16993
+ return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
16994
+ }
16995
+ case "assign_task": {
16996
+ if (!input.boardId || !input.taskId)
16997
+ return fail("assign_task requires boardId and taskId.");
16998
+ const board = await assignTask(
16999
+ projectRoot,
17000
+ input.boardId,
17001
+ input.taskId,
17002
+ assignmentInput(input)
17003
+ );
17004
+ return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
17005
+ }
17006
+ case "mark_assignment": {
17007
+ if (!input.boardId || !input.taskId)
17008
+ return fail("mark_assignment requires boardId and taskId.");
17009
+ const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
17010
+ const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
17011
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17012
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17013
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17014
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17015
+ ...input.error !== void 0 ? { error: input.error } : {},
17016
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17017
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17018
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17019
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17020
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17021
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17022
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17023
+ });
17024
+ return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
17025
+ }
17026
+ case "heartbeat_assignment": {
17027
+ if (!input.boardId || !input.taskId) {
17028
+ return fail("heartbeat_assignment requires boardId and taskId.");
17029
+ }
17030
+ const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
17031
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17032
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
17033
+ });
17034
+ return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
17035
+ }
17036
+ case "recover_stale": {
17037
+ if (!input.boardId) return fail("recover_stale requires boardId.");
17038
+ const policyFields = [
17039
+ input.recoveryPolicyFailOnCostCeiling !== void 0,
17040
+ input.recoveryPolicyReleaseOnFailureKinds !== void 0,
17041
+ input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
17042
+ input.recoveryPolicyRetryPolicyOverride !== void 0
17043
+ ].some(Boolean);
17044
+ const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
17045
+ ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
17046
+ ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
17047
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17048
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
17049
+ ...policyFields ? {
17050
+ policy: {
17051
+ ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
17052
+ ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
17053
+ releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
17054
+ } : {},
17055
+ ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
17056
+ releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
17057
+ } : {},
17058
+ ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
17059
+ retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
17060
+ } : {}
17061
+ }
17062
+ } : {}
17063
+ });
17064
+ return result2 ? {
17065
+ ok: true,
17066
+ message: `Recovered ${result2.tasks.length} stale assignment(s).`,
17067
+ board: result2.board,
17068
+ recoveredTasks: result2.tasks
17069
+ } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
17070
+ }
17071
+ case "events": {
17072
+ if (!input.boardId) return fail("events requires boardId.");
17073
+ const eventList = await listKanbanEvents(projectRoot, input.boardId);
17074
+ return {
17075
+ ok: true,
17076
+ message: `${eventList.length} event(s).`,
17077
+ events: eventList
17078
+ };
17079
+ }
17080
+ case "queue_health": {
17081
+ const health = await getKanbanQueueHealth(projectRoot, {
17082
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
17083
+ });
17084
+ return {
17085
+ ok: true,
17086
+ message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
17087
+ queueHealth: health
17088
+ };
17089
+ }
17090
+ case "add_dependency": {
17091
+ if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
17092
+ return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
17093
+ }
17094
+ const board = await addDependency(
17095
+ projectRoot,
17096
+ input.boardId,
17097
+ input.taskId,
17098
+ input.dependencyTaskId
17099
+ );
17100
+ return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
17101
+ }
17102
+ case "add_goal_metric": {
17103
+ if (!input.boardId || !input.taskId || !input.metricName) {
17104
+ return fail("add_goal_metric requires boardId, taskId, and metricName.");
17105
+ }
17106
+ const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
17107
+ name: input.metricName,
15962
17108
  ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
15963
17109
  ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
15964
17110
  ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
15965
17111
  ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
15966
17112
  ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17113
+ });
17114
+ return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
17115
+ }
17116
+ case "update_goal_metric": {
17117
+ if (!input.boardId || !input.taskId || !input.metricId) {
17118
+ return fail("update_goal_metric requires boardId, taskId, and metricId.");
15967
17119
  }
15968
- );
15969
- return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
15970
- }
15971
- case "add_check": {
15972
- if (!input.boardId || !input.taskId || !input.checkDescription) {
15973
- return fail("add_check requires boardId, taskId, and checkDescription.");
17120
+ const board = await updateGoalMetricOnTask(
17121
+ projectRoot,
17122
+ input.boardId,
17123
+ input.taskId,
17124
+ input.metricId,
17125
+ {
17126
+ ...input.metricName !== void 0 ? { name: input.metricName } : {},
17127
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
17128
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17129
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17130
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17131
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17132
+ }
17133
+ );
17134
+ return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
15974
17135
  }
15975
- const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
15976
- description: input.checkDescription,
15977
- type: "manual",
15978
- status: input.checkStatus
15979
- });
15980
- return board ? okBoard(board, "Check added.") : fail("Task not found.");
15981
- }
15982
- case "update_check": {
15983
- if (!input.boardId || !input.taskId || !input.checkId) {
15984
- return fail("update_check requires boardId, taskId, and checkId.");
17136
+ case "add_check": {
17137
+ if (!input.boardId || !input.taskId || !input.checkDescription) {
17138
+ return fail("add_check requires boardId, taskId, and checkDescription.");
17139
+ }
17140
+ const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
17141
+ description: input.checkDescription,
17142
+ type: "manual",
17143
+ status: input.checkStatus
17144
+ });
17145
+ return board ? okBoard(board, "Check added.") : fail("Task not found.");
15985
17146
  }
15986
- const board = await updateCheckOnTask(
15987
- projectRoot,
15988
- input.boardId,
15989
- input.taskId,
15990
- input.checkId,
15991
- {
15992
- ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
15993
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17147
+ case "update_check": {
17148
+ if (!input.boardId || !input.taskId || !input.checkId) {
17149
+ return fail("update_check requires boardId, taskId, and checkId.");
15994
17150
  }
15995
- );
15996
- return board ? okBoard(board, "Check updated.") : fail("Check not found.");
15997
- }
15998
- case "add_note": {
15999
- if (!input.boardId || !input.taskId || !input.note)
16000
- return fail("add_note requires boardId, taskId, and note.");
16001
- const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
16002
- author: input.author ?? "agent",
16003
- content: input.note
16004
- });
16005
- return board ? okBoard(board, "Note added.") : fail("Task not found.");
16006
- }
16007
- case "add_link": {
16008
- if (!input.boardId || !input.taskId || !input.url)
16009
- return fail("add_link requires boardId, taskId, and url.");
16010
- const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
16011
- url: input.url,
16012
- type: input.linkType ?? "url",
16013
- ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
16014
- });
16015
- return board ? okBoard(board, "Link added.") : fail("Task not found.");
17151
+ const board = await updateCheckOnTask(
17152
+ projectRoot,
17153
+ input.boardId,
17154
+ input.taskId,
17155
+ input.checkId,
17156
+ {
17157
+ ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
17158
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17159
+ }
17160
+ );
17161
+ return board ? okBoard(board, "Check updated.") : fail("Check not found.");
17162
+ }
17163
+ case "add_note": {
17164
+ if (!input.boardId || !input.taskId || !input.note)
17165
+ return fail("add_note requires boardId, taskId, and note.");
17166
+ const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
17167
+ author: input.author ?? "agent",
17168
+ content: input.note
17169
+ });
17170
+ return board ? okBoard(board, "Note added.") : fail("Task not found.");
17171
+ }
17172
+ case "add_link": {
17173
+ if (!input.boardId || !input.taskId || !input.url)
17174
+ return fail("add_link requires boardId, taskId, and url.");
17175
+ const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
17176
+ url: input.url,
17177
+ type: input.linkType ?? "url",
17178
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17179
+ });
17180
+ return board ? okBoard(board, "Link added.") : fail("Task not found.");
17181
+ }
17182
+ default:
17183
+ return fail(`Unknown kanban action: ${input.action}`);
16016
17184
  }
16017
- default:
16018
- return fail(`Unknown kanban action: ${input.action}`);
16019
- }
17185
+ })();
17186
+ return withPresence(result);
16020
17187
  } catch (err) {
16021
17188
  return fail(err instanceof Error ? err.message : String(err));
16022
17189
  }
@@ -16032,7 +17199,7 @@ function okTask(board, task, message) {
16032
17199
  return { ok: true, message, board, task };
16033
17200
  }
16034
17201
  async function requireBoard(projectRoot, boardId) {
16035
- return boardId ? getBoard(projectRoot, boardId) : null;
17202
+ return boardId ? getBoard2(projectRoot, boardId) : null;
16036
17203
  }
16037
17204
  function taskInput(input) {
16038
17205
  const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
@@ -16040,26 +17207,99 @@ function taskInput(input) {
16040
17207
  title: input.title ?? "",
16041
17208
  columnId: input.columnId,
16042
17209
  description: input.description,
17210
+ dueDate: input.dueDate,
16043
17211
  priority: input.priority,
17212
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
16044
17213
  status: input.status,
16045
17214
  labels: input.labels,
16046
17215
  ...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
16047
17216
  ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
16048
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {},
16049
- ...assignment ? { assignment } : {}
17217
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
17218
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
17219
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
17220
+ ...assignment ? { assignment } : {},
17221
+ ...input.order !== void 0 ? { order: input.order } : {},
17222
+ // Sprint 3 durable task-level fields
17223
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
17224
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
17225
+ // Relationships — chainId alone cannot construct a KanbanTaskChainRef,
17226
+ // so chain/previousTaskId/nextTaskId must be set via update_task.
17227
+ ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
17228
+ // Detail objects
17229
+ ...input.checkDescription !== void 0 ? {
17230
+ successCriteria: [
17231
+ {
17232
+ id: randomUUID(),
17233
+ description: input.checkDescription,
17234
+ type: "manual",
17235
+ status: input.checkStatus ?? "pending"
17236
+ }
17237
+ ]
17238
+ } : {},
17239
+ ...input.metricName !== void 0 ? {
17240
+ goalMetrics: [
17241
+ {
17242
+ id: randomUUID(),
17243
+ name: input.metricName,
17244
+ status: input.metricStatus ?? "pending",
17245
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17246
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17247
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17248
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17249
+ }
17250
+ ]
17251
+ } : {},
17252
+ ...input.url !== void 0 ? {
17253
+ links: [
17254
+ {
17255
+ url: input.url,
17256
+ type: input.linkType ?? "url",
17257
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17258
+ }
17259
+ ]
17260
+ } : {},
17261
+ ...input.note !== void 0 ? {
17262
+ notes: [
17263
+ {
17264
+ id: randomUUID(),
17265
+ author: input.author ?? "agent",
17266
+ content: input.note,
17267
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
17268
+ }
17269
+ ]
17270
+ } : {},
17271
+ // Origin tracking
17272
+ ...input.graphId !== void 0 ? {
17273
+ origin: {
17274
+ system: input.sourceSystem ?? "kanban-tool",
17275
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
17276
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
17277
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
17278
+ }
17279
+ } : {}
16050
17280
  };
16051
17281
  }
17282
+ function mergedDependsOn(input) {
17283
+ const ids = [
17284
+ ...input.dependsOn ?? [],
17285
+ ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
17286
+ ].filter((id, i, arr) => id && arr.indexOf(id) === i);
17287
+ return ids.length > 0 ? ids : void 0;
17288
+ }
16052
17289
  function taskPatch(input) {
16053
17290
  return {
16054
17291
  title: input.title,
16055
17292
  description: input.description,
17293
+ dueDate: input.dueDate,
16056
17294
  columnId: input.columnId,
16057
17295
  order: input.order,
16058
17296
  priority: input.priority,
17297
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
16059
17298
  status: input.status,
16060
17299
  labels: input.labels,
16061
17300
  assignedAgent: input.agentId,
16062
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {}
17301
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
17302
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
16063
17303
  };
16064
17304
  }
16065
17305
  function assignmentInput(input) {
@@ -16804,15 +18044,15 @@ import {
16804
18044
  deriveTodosFromPlanItem,
16805
18045
  formatPlan,
16806
18046
  getPlanTemplate,
16807
- mutatePlan,
18047
+ mutatePlan as mutatePlan2,
16808
18048
  removePlanItem,
16809
18049
  setPlanItemStatus
16810
18050
  } from "@wrongstack/core";
16811
18051
  import {
16812
- mutateTasks,
18052
+ mutateTasks as mutateTasks2,
16813
18053
  formatTaskList
16814
18054
  } from "@wrongstack/core";
16815
- import { randomUUID } from "node:crypto";
18055
+ import { randomUUID as randomUUID2 } from "node:crypto";
16816
18056
  var planTool = {
16817
18057
  name: "plan",
16818
18058
  category: "Session",
@@ -16896,7 +18136,7 @@ var planTool = {
16896
18136
  let didTaskify = false;
16897
18137
  let plan;
16898
18138
  try {
16899
- plan = await mutatePlan(planPath, sessionId, async (p) => {
18139
+ plan = await mutatePlan2(planPath, sessionId, async (p) => {
16900
18140
  switch (input.action) {
16901
18141
  case "show":
16902
18142
  break;
@@ -17022,6 +18262,7 @@ var planTool = {
17022
18262
  open: 0
17023
18263
  };
17024
18264
  }
18265
+ await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
17025
18266
  if (early) return early;
17026
18267
  if (didTaskify) {
17027
18268
  const taskPathRaw = ctx.meta["task.path"];
@@ -17035,9 +18276,9 @@ var planTool = {
17035
18276
  }
17036
18277
  const now = (/* @__PURE__ */ new Date()).toISOString();
17037
18278
  try {
17038
- const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
18279
+ const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
17039
18280
  f.tasks.push({
17040
- id: `task_${randomUUID()}`,
18281
+ id: `task_${randomUUID2()}`,
17041
18282
  title: taskifyMeta.title,
17042
18283
  description: taskifyMeta.details || void 0,
17043
18284
  type: "feature",
@@ -18172,14 +19413,14 @@ import {
18172
19413
  formatTaskList as formatTaskList2
18173
19414
  } from "@wrongstack/core";
18174
19415
  import {
18175
- mutateTasks as mutateTasks2
19416
+ mutateTasks as mutateTasks3
18176
19417
  } from "@wrongstack/core";
18177
19418
  import {
18178
19419
  addPlanItem as addPlanItem2,
18179
- mutatePlan as mutatePlan2,
19420
+ mutatePlan as mutatePlan3,
18180
19421
  formatPlan as formatPlan2
18181
19422
  } from "@wrongstack/core";
18182
- import { randomUUID as randomUUID2 } from "node:crypto";
19423
+ import { randomUUID as randomUUID3 } from "node:crypto";
18183
19424
  function findTaskIndex(tasks, query) {
18184
19425
  const asNum = Number.parseInt(query, 10);
18185
19426
  if (!Number.isNaN(asNum)) {
@@ -18296,7 +19537,7 @@ var taskTool = {
18296
19537
  let todosToReplace = null;
18297
19538
  let file;
18298
19539
  try {
18299
- file = await mutateTasks2(taskPath, sessionId, async (f) => {
19540
+ file = await mutateTasks3(taskPath, sessionId, async (f) => {
18300
19541
  switch (input.action) {
18301
19542
  case "show":
18302
19543
  break;
@@ -18367,7 +19608,7 @@ var taskTool = {
18367
19608
  }
18368
19609
  const now = (/* @__PURE__ */ new Date()).toISOString();
18369
19610
  const newTask = {
18370
- id: `task_${Date.now()}_${randomUUID2().slice(0, 8)}`,
19611
+ id: `task_${Date.now()}_${randomUUID3().slice(0, 8)}`,
18371
19612
  title: t.title,
18372
19613
  description: t.description,
18373
19614
  type: t.type || "feature",
@@ -18428,7 +19669,7 @@ var taskTool = {
18428
19669
  });
18429
19670
  if (match.description) {
18430
19671
  todos.push({
18431
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
19672
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
18432
19673
  content: match.description.slice(0, 200),
18433
19674
  status: "pending",
18434
19675
  promotedFromTask: match.id
@@ -18437,7 +19678,7 @@ var taskTool = {
18437
19678
  if (input.subtasks && input.subtasks.length > 0) {
18438
19679
  for (const st of input.subtasks) {
18439
19680
  todos.push({
18440
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
19681
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
18441
19682
  content: st,
18442
19683
  status: "pending",
18443
19684
  promotedFromTask: match.id
@@ -18486,6 +19727,7 @@ var taskTool = {
18486
19727
  };
18487
19728
  }
18488
19729
  if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
19730
+ await projectSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);
18489
19731
  if (early) return early;
18490
19732
  if (didPlanify) {
18491
19733
  const { title, details } = planifyMeta;
@@ -18499,7 +19741,7 @@ var taskTool = {
18499
19741
  }
18500
19742
  let formatted = "";
18501
19743
  try {
18502
- await mutatePlan2(planPath, sessionId, (pf) => {
19744
+ await mutatePlan3(planPath, sessionId, (pf) => {
18503
19745
  const { plan: updated } = addPlanItem2(pf, title, details || void 0);
18504
19746
  formatted = formatPlan2(updated);
18505
19747
  return updated;
@@ -18731,8 +19973,7 @@ function parseResult(runner, result, duration) {
18731
19973
  }
18732
19974
 
18733
19975
  // src/todo.ts
18734
- import { loadPlan, savePlan, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
18735
- import { loadTasks, saveTasks } from "@wrongstack/core";
19976
+ import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
18736
19977
  var todoTool = {
18737
19978
  name: "todo",
18738
19979
  category: "Session",
@@ -18741,7 +19982,7 @@ var todoTool = {
18741
19982
  permission: "auto",
18742
19983
  mutating: false,
18743
19984
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
18744
- timeoutMs: 1e3,
19985
+ timeoutMs: 5e3,
18745
19986
  capabilities: ["session.todo"],
18746
19987
  icon: "todo",
18747
19988
  inputSchema: {
@@ -18793,16 +20034,21 @@ var todoTool = {
18793
20034
  }
18794
20035
  }
18795
20036
  ctx.state.replaceTodos(items);
20037
+ await projectSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
18796
20038
  const completedPlanIds = /* @__PURE__ */ new Set();
18797
20039
  const completedTaskIds = /* @__PURE__ */ new Set();
18798
20040
  const pendingPlanIds = /* @__PURE__ */ new Set();
18799
20041
  const pendingTaskIds = /* @__PURE__ */ new Set();
18800
20042
  for (const item of items) {
18801
20043
  if (item.promotedFromPlan) {
18802
- (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
20044
+ (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
20045
+ item.promotedFromPlan
20046
+ );
18803
20047
  }
18804
20048
  if (item.promotedFromTask) {
18805
- (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
20049
+ (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
20050
+ item.promotedFromTask
20051
+ );
18806
20052
  }
18807
20053
  }
18808
20054
  for (const planId of completedPlanIds) {
@@ -18810,7 +20056,7 @@ var todoTool = {
18810
20056
  const planPath = ctx.meta["plan.path"];
18811
20057
  if (typeof planPath !== "string" || !planPath) continue;
18812
20058
  try {
18813
- const plan = await loadPlan(planPath);
20059
+ const plan = await loadPlan2(planPath);
18814
20060
  if (plan) {
18815
20061
  const updated = setPlanItemStatus2(plan, planId, "done");
18816
20062
  await savePlan(planPath, updated);
@@ -18823,7 +20069,7 @@ var todoTool = {
18823
20069
  const taskPath = ctx.meta["task.path"];
18824
20070
  if (typeof taskPath !== "string" || !taskPath) continue;
18825
20071
  try {
18826
- const file = await loadTasks(taskPath);
20072
+ const file = await loadTasks3(taskPath);
18827
20073
  if (file) {
18828
20074
  const task = file.tasks.find((t) => t.id === taskId);
18829
20075
  if (task && task.status !== "completed") {
@@ -19138,8 +20384,8 @@ var DEFAULT_IGNORE5 = [
19138
20384
  var treeTool = {
19139
20385
  name: "tree",
19140
20386
  category: "Filesystem",
19141
- 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.",
19142
- usageHint: "BEST PRACTICE FOR INITIAL EXPLORATION:\n\n- Call early when working with an unfamiliar project or module.\n- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\n- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\nOutput is truncated for very large trees.",
20387
+ description: "Display a project or subpath directory tree. Use it for structural layout; use index-backed `codebase-search` first for code understanding when it is live.",
20388
+ usageHint: "DIRECTORY-LAYOUT EXPLORATION:\n\n- When `codebase-search` is live, use it first for symbols and concepts; use `tree` when the directory hierarchy itself matters.\n- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\n- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\nOutput is truncated for very large trees.",
19143
20389
  permission: "auto",
19144
20390
  mutating: false,
19145
20391
  capabilities: ["fs.read"],
@@ -19542,9 +20788,6 @@ var OPTIONAL_TOOLS = [
19542
20788
  toolUseTool,
19543
20789
  batchToolUseTool,
19544
20790
  toolHelpTool,
19545
- codebaseIndexTool,
19546
- codebaseSearchTool,
19547
- codebaseStatsTool,
19548
20791
  setWorkingDirTool
19549
20792
  ];
19550
20793
  var builtinTools = [
@@ -19553,6 +20796,9 @@ var builtinTools = [
19553
20796
  readTool,
19554
20797
  writeTool,
19555
20798
  editTool,
20799
+ codebaseStatsTool,
20800
+ codebaseSearchTool,
20801
+ codebaseIndexTool,
19556
20802
  replaceTool,
19557
20803
  globTool,
19558
20804
  grepTool,
@@ -19587,9 +20833,6 @@ var builtinTools = [
19587
20833
  toolUseTool,
19588
20834
  batchToolUseTool,
19589
20835
  toolHelpTool,
19590
- codebaseIndexTool,
19591
- codebaseSearchTool,
19592
- codebaseStatsTool,
19593
20836
  setWorkingDirTool
19594
20837
  ];
19595
20838