@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/index.js CHANGED
@@ -404,8 +404,10 @@ function redactCommand(cmd) {
404
404
  const flag = match.slice(0, match.indexOf(expectDefined(delim)) + 1);
405
405
  return `${flag}[REDACTED]`;
406
406
  }
407
- const flagEnd = match.match(/^--?[a-zA-Z][a-zA-Z0-9_-]*/)?.[0] ?? match;
408
- return `${flagEnd}=**redacted**`;
407
+ if (match.startsWith("--")) {
408
+ return match;
409
+ }
410
+ return `${match.slice(0, 2)}[REDACTED]`;
409
411
  });
410
412
  }
411
413
  return result;
@@ -417,9 +419,14 @@ var init_redact_command = __esm({
417
419
  SENSITIVE_FLAG_PATTERNS = [
418
420
  // --flag=value or --flag "value" (value captured up to next space or comma)
419
421
  /--(?:token|password|passwd|pwd|secret|api[-_]?key|api[-_]?secret|auth|credential|private[-_]?key|access[-_]?key|github[-_]?token|gh[-_]?token|bearer|jwt|oauth|pin|pincode|passphrase|access[-_]?token)(?:[=\s,][^\s]*)?/gi,
420
- // -f "value" style short flags
421
- /(?<!\w)-t(?:\s+|\s*=\s*)[^\s,]+/,
422
- /(?<!\w)-(?:p|password)(?:\s+|\s*=\s*)[^\s,]+/gi,
422
+ // -t short flag (token): attached (-tVALUE), separated (-t VALUE), or -t=VALUE.
423
+ // (?<![-\w]) anchors to a token start so we don't match the `-t` inside `--token`.
424
+ // NOTE: synced with @wrongstack/core observability/redact-command.ts.
425
+ /(?<![-\w])-t(?:[=\s]+)?[^\s,-]+/,
426
+ // -p|-password|-a (redis auth) short flags: attached + separated + =value.
427
+ // Same token-start anchor; over-redaction is an accepted tradeoff for a
428
+ // redaction function. Synced with core copy.
429
+ /(?<![-\w])-(?:password|p|a)(?:[=\s]+)?[^\s,-]+/gi,
423
430
  // env var–style secrets: TOKEN=x, API_KEY=y, etc.
424
431
  /(?:TOKEN|API_KEY|API_SECRET|AUTH_TOKEN|GITHUB_TOKEN|GH_TOKEN|BEARER|JWT|OAUTH|CREDENTIAL|SECRET|PRIVATE_KEY|PASSWORD|PASSWD)\s*[=:]\s*[^\s,]+/gi,
425
432
  // Generic high-entropy look: base64 strings >32 chars or hex strings >32 digits — but only
@@ -802,7 +809,12 @@ var init_process_registry = __esm({
802
809
 
803
810
  // src/_spawn-stream.ts
804
811
  import { spawn as spawn2 } from "node:child_process";
805
- import { buildChildEnv } from "@wrongstack/core";
812
+ import {
813
+ buildChildEnv,
814
+ emitProcessCompleted,
815
+ emitProcessOutput,
816
+ emitProcessStarted
817
+ } from "@wrongstack/core";
806
818
  async function* spawnStream(opts) {
807
819
  const max = opts.maxBytes ?? 2e5;
808
820
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -827,6 +839,19 @@ async function* spawnStream(opts) {
827
839
  });
828
840
  const registry = getProcessRegistry();
829
841
  const pid = child.pid;
842
+ const processStartedAt = Date.now();
843
+ let stdoutBytes = 0;
844
+ let stderrBytes = 0;
845
+ let telemetryCompleted = false;
846
+ emitProcessStarted({
847
+ ...pid !== void 0 ? { pid } : {},
848
+ parentPid: process.pid,
849
+ command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
850
+ args: redactCommand(opts.args.join(" ")).split(" ").filter(Boolean),
851
+ cwd: opts.cwd,
852
+ background: false,
853
+ startedAt: new Date(processStartedAt).toISOString()
854
+ });
830
855
  if (typeof pid === "number") {
831
856
  registry.register({
832
857
  pid,
@@ -855,6 +880,8 @@ async function* spawnStream(opts) {
855
880
  };
856
881
  const onOut = (c) => {
857
882
  const s = c.toString();
883
+ stdoutBytes += c.byteLength;
884
+ emitProcessOutput({ pid, stream: "stdout", chunk: c });
858
885
  if (stdout.length < max) stdout += s;
859
886
  spool.write(s);
860
887
  queue.push({ kind: "out", data: s });
@@ -867,6 +894,8 @@ async function* spawnStream(opts) {
867
894
  };
868
895
  const onErr = (c) => {
869
896
  const s = c.toString();
897
+ stderrBytes += c.byteLength;
898
+ emitProcessOutput({ pid, stream: "stderr", chunk: c });
870
899
  if (stderr.length < max) stderr += s;
871
900
  spool.write(s);
872
901
  queue.push({ kind: "err", data: s });
@@ -884,9 +913,25 @@ async function* spawnStream(opts) {
884
913
  queue.push({ kind: "error", data: e.message });
885
914
  wake();
886
915
  });
887
- child.on("close", (code) => {
916
+ const completeTelemetry = (code, signal, timedOut = false) => {
917
+ if (telemetryCompleted) return;
918
+ telemetryCompleted = true;
919
+ emitProcessCompleted({
920
+ ...pid !== void 0 ? { pid } : {},
921
+ exitCode: code,
922
+ ...signal ? { signal } : {},
923
+ durationMs: Date.now() - processStartedAt,
924
+ stdoutBytes,
925
+ stderrBytes,
926
+ timedOut,
927
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
928
+ });
929
+ };
930
+ child.on("close", (code, signal) => {
888
931
  if (typeof pid === "number") registry.unregister(pid);
889
- queue.push({ kind: "close", data: "", code: code ?? 0 });
932
+ const exitCode2 = code ?? (signal ? 1 : 0);
933
+ completeTelemetry(exitCode2, signal ?? void 0);
934
+ queue.push({ kind: "close", data: "", code: exitCode2, ...signal ? { signal } : {} });
890
935
  wake();
891
936
  });
892
937
  const onAbort = () => {
@@ -899,6 +944,7 @@ async function* spawnStream(opts) {
899
944
  }
900
945
  }
901
946
  queue.push({ kind: "close", data: "", code: 124 });
947
+ completeTelemetry(124, "SIGKILL", true);
902
948
  wake();
903
949
  };
904
950
  if (isWin) {
@@ -2506,13 +2552,14 @@ async function detectLanguageWorkspaces(options) {
2506
2552
  const target = options.target ? await canonicalInside(resolveFrom(cwd, options.target), projectRoot, "target") : void 0;
2507
2553
  const profiles = (options.profiles ?? languageProfileRegistry.list()).filter((profile) => !options.language || profile.id === options.language).slice().sort((a, b) => a.id.localeCompare(b.id));
2508
2554
  const limits = normalizeLimits(options.limits);
2555
+ const extraIgnores = new Set(options.ignoredDirectories ?? []);
2509
2556
  const state = {
2510
2557
  entries: 0,
2511
2558
  truncated: false,
2512
2559
  sourcePaths: /* @__PURE__ */ new Map(),
2513
2560
  candidates: /* @__PURE__ */ new Map()
2514
2561
  };
2515
- await scanDirectory(projectRoot, 0, profiles, limits, state, options.signal);
2562
+ await scanDirectory(projectRoot, 0, profiles, limits, state, extraIgnores, options.signal);
2516
2563
  addSourceFallbacks(projectRoot, profiles, state);
2517
2564
  if (target) addTargetEvidence(target, projectRoot, profiles, state);
2518
2565
  const workspaces = await Promise.all(
@@ -2526,7 +2573,7 @@ async function detectLanguageWorkspaces(options) {
2526
2573
  workspaces
2527
2574
  };
2528
2575
  }
2529
- async function scanDirectory(directory, depth, profiles, limits, state, signal) {
2576
+ async function scanDirectory(directory, depth, profiles, limits, state, extraIgnores, signal) {
2530
2577
  signal?.throwIfAborted();
2531
2578
  if (depth > limits.maxDepth || state.entries >= limits.maxEntries) {
2532
2579
  state.truncated = true;
@@ -2549,20 +2596,20 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
2549
2596
  const fullPath = path4.join(directory, entry.name);
2550
2597
  if (entry.isSymbolicLink()) continue;
2551
2598
  if (entry.isDirectory()) {
2552
- if (shouldIgnoreDirectory(entry.name, profiles)) continue;
2599
+ if (shouldIgnoreDirectory(entry.name, profiles, extraIgnores)) continue;
2553
2600
  if (depth >= limits.maxDepth) {
2554
2601
  state.truncated = true;
2555
2602
  continue;
2556
2603
  }
2557
- await scanDirectory(fullPath, depth + 1, profiles, limits, state, signal);
2604
+ await scanDirectory(fullPath, depth + 1, profiles, limits, state, extraIgnores, signal);
2558
2605
  continue;
2559
2606
  }
2560
2607
  if (!entry.isFile()) continue;
2561
2608
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2562
2609
  }
2563
2610
  }
2564
- function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2565
- const lower = basename8.toLowerCase();
2611
+ function collectFileEvidence(directory, fullPath, basename10, profiles, state) {
2612
+ const lower = basename10.toLowerCase();
2566
2613
  const extension = path4.extname(lower);
2567
2614
  for (const profile of profiles) {
2568
2615
  const detector = profile.detectors.find(
@@ -2573,7 +2620,7 @@ function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2573
2620
  candidate.evidence.push({
2574
2621
  kind: detector.kind,
2575
2622
  path: fullPath,
2576
- value: basename8,
2623
+ value: basename10,
2577
2624
  weight: detector.weight
2578
2625
  });
2579
2626
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2683,8 +2730,8 @@ function getCandidate(state, profile, root) {
2683
2730
  }
2684
2731
  return candidate;
2685
2732
  }
2686
- function shouldIgnoreDirectory(name, profiles) {
2687
- if (GLOBAL_IGNORES.has(name) || name.startsWith(".")) return true;
2733
+ function shouldIgnoreDirectory(name, profiles, extraIgnores) {
2734
+ if (GLOBAL_IGNORES.has(name) || extraIgnores.has(name) || name.startsWith(".")) return true;
2688
2735
  return profiles.some((profile) => profile.ignoredDirectories.includes(name));
2689
2736
  }
2690
2737
  function normalizeLimits(input) {
@@ -5210,6 +5257,11 @@ function parseAuditOutput(json2, exitCode) {
5210
5257
  // src/bash.ts
5211
5258
  import { spawn as spawn3 } from "node:child_process";
5212
5259
  import * as os4 from "node:os";
5260
+ import {
5261
+ emitProcessCompleted as emitProcessCompleted2,
5262
+ emitProcessOutput as emitProcessOutput2,
5263
+ emitProcessStarted as emitProcessStarted2
5264
+ } from "@wrongstack/core";
5213
5265
 
5214
5266
  // src/_env.ts
5215
5267
  import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core";
@@ -6085,6 +6137,32 @@ var bashTool = {
6085
6137
  }
6086
6138
  }
6087
6139
  const pid2 = child2.pid;
6140
+ let stdoutBytes2 = 0;
6141
+ let stderrBytes2 = 0;
6142
+ let telemetryCompleted2 = false;
6143
+ emitProcessStarted2({
6144
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
6145
+ parentPid: process.pid,
6146
+ command: redactCommand(`${shell} ${args.join(" ")}`),
6147
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
6148
+ cwd: ctx.projectRoot,
6149
+ background: true,
6150
+ startedAt: new Date(startedAt).toISOString()
6151
+ });
6152
+ const completeBackground = (exitCode, signal) => {
6153
+ if (telemetryCompleted2) return;
6154
+ telemetryCompleted2 = true;
6155
+ emitProcessCompleted2({
6156
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
6157
+ exitCode,
6158
+ ...signal ? { signal } : {},
6159
+ durationMs: Date.now() - startedAt,
6160
+ stdoutBytes: stdoutBytes2,
6161
+ stderrBytes: stderrBytes2,
6162
+ timedOut: false,
6163
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
6164
+ });
6165
+ };
6088
6166
  if (typeof pid2 === "number") {
6089
6167
  registry.register({
6090
6168
  pid: pid2,
@@ -6097,7 +6175,10 @@ var bashTool = {
6097
6175
  });
6098
6176
  child2.on("close", () => registry.unregister(pid2));
6099
6177
  }
6100
- const onBgData = (chunk) => {
6178
+ const onBgData = (chunk, stream) => {
6179
+ if (stream === "stdout") stdoutBytes2 += chunk.byteLength;
6180
+ else stderrBytes2 += chunk.byteLength;
6181
+ emitProcessOutput2({ pid: pid2, stream, chunk });
6101
6182
  if (truncated) return;
6102
6183
  const remain = MAX_OUTPUT - buf2.length;
6103
6184
  if (remain > 0) {
@@ -6105,24 +6186,28 @@ var bashTool = {
6105
6186
  }
6106
6187
  if (buf2.length >= MAX_OUTPUT) {
6107
6188
  truncated = true;
6108
- child2.stdout?.off("data", onBgData);
6109
- child2.stderr?.off("data", onBgData);
6189
+ child2.stdout?.off("data", onBgStdout);
6190
+ child2.stderr?.off("data", onBgStderr);
6110
6191
  }
6111
6192
  };
6112
- child2.stdout?.on("data", onBgData);
6113
- child2.stderr?.on("data", onBgData);
6193
+ const onBgStdout = (chunk) => onBgData(chunk, "stdout");
6194
+ const onBgStderr = (chunk) => onBgData(chunk, "stderr");
6195
+ child2.stdout?.on("data", onBgStdout);
6196
+ child2.stderr?.on("data", onBgStderr);
6114
6197
  const cleanupBackground = () => {
6115
- child2.stdout?.off("data", onBgData);
6116
- child2.stderr?.off("data", onBgData);
6198
+ child2.stdout?.off("data", onBgStdout);
6199
+ child2.stderr?.off("data", onBgStderr);
6117
6200
  };
6118
6201
  child2.on("error", () => {
6119
6202
  cleanupBackground();
6120
6203
  if (typeof pid2 === "number") registry.unregister(pid2);
6121
6204
  registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
6205
+ completeBackground(1);
6122
6206
  });
6123
- child2.on("close", () => {
6207
+ child2.on("close", (code, signal) => {
6124
6208
  cleanupBackground();
6125
6209
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
6210
+ completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
6126
6211
  });
6127
6212
  if (typeof pid2 === "number") child2.unref();
6128
6213
  yield {
@@ -6162,6 +6247,32 @@ var bashTool = {
6162
6247
  }
6163
6248
  }
6164
6249
  const pid = child.pid;
6250
+ let stdoutBytes = 0;
6251
+ let stderrBytes = 0;
6252
+ let telemetryCompleted = false;
6253
+ emitProcessStarted2({
6254
+ ...pid !== void 0 ? { pid } : {},
6255
+ parentPid: process.pid,
6256
+ command: redactCommand(`${shell} ${args.join(" ")}`),
6257
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
6258
+ cwd: ctx.projectRoot,
6259
+ background: false,
6260
+ startedAt: new Date(startedAt).toISOString()
6261
+ });
6262
+ const completeForeground = (exitCode, signal) => {
6263
+ if (telemetryCompleted) return;
6264
+ telemetryCompleted = true;
6265
+ emitProcessCompleted2({
6266
+ ...pid !== void 0 ? { pid } : {},
6267
+ exitCode,
6268
+ ...signal ? { signal } : {},
6269
+ durationMs: Date.now() - startedAt,
6270
+ stdoutBytes,
6271
+ stderrBytes,
6272
+ timedOut,
6273
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
6274
+ });
6275
+ };
6165
6276
  if (typeof pid === "number") {
6166
6277
  registry.register({
6167
6278
  pid,
@@ -6263,8 +6374,11 @@ var bashTool = {
6263
6374
  child.stderr?.resume();
6264
6375
  }
6265
6376
  };
6266
- const onData = (chunk) => {
6377
+ const onData = (chunk, stream) => {
6267
6378
  const text = chunk.toString();
6379
+ if (stream === "stdout") stdoutBytes += chunk.byteLength;
6380
+ else stderrBytes += chunk.byteLength;
6381
+ emitProcessOutput2({ pid, stream, chunk });
6268
6382
  if (buf.length < MAX_OUTPUT) {
6269
6383
  buf += text.slice(0, MAX_OUTPUT - buf.length);
6270
6384
  }
@@ -6273,17 +6387,21 @@ var bashTool = {
6273
6387
  push({ kind: "data", text });
6274
6388
  pauseIfFlooded();
6275
6389
  };
6276
- child.stdout?.on("data", onData);
6277
- child.stderr?.on("data", onData);
6390
+ const onStdoutData = (chunk) => onData(chunk, "stdout");
6391
+ const onStderrData = (chunk) => onData(chunk, "stderr");
6392
+ child.stdout?.on("data", onStdoutData);
6393
+ child.stderr?.on("data", onStderrData);
6278
6394
  child.on("error", (err) => {
6279
6395
  for (const t of timers) clearTimeout(t);
6280
6396
  registry.afterCall(Date.now() - startedAt, true);
6397
+ completeForeground(1);
6281
6398
  push({ kind: "error", err });
6282
6399
  });
6283
- child.on("close", (code) => {
6400
+ child.on("close", (code, signal) => {
6284
6401
  for (const t of timers) clearTimeout(t);
6285
6402
  if (typeof pid === "number") registry.unregister(pid);
6286
6403
  registry.afterCall(Date.now() - startedAt, code !== 0 && code !== null);
6404
+ completeForeground(timedOut ? 124 : code ?? (signal ? 1 : 0), signal ?? void 0);
6287
6405
  push({ kind: "end", code });
6288
6406
  });
6289
6407
  try {
@@ -6328,8 +6446,8 @@ ${hint}` : ""),
6328
6446
  for (const t of timers) clearTimeout(t);
6329
6447
  spool.finalize();
6330
6448
  if (isWin4) opts.signal.removeEventListener("abort", onAbort);
6331
- child.stdout?.off("data", onData);
6332
- child.stderr?.off("data", onData);
6449
+ child.stdout?.off("data", onStdoutData);
6450
+ child.stderr?.off("data", onStderrData);
6333
6451
  child.stdout?.destroy();
6334
6452
  child.stderr?.destroy();
6335
6453
  if (child.exitCode === null && !child.killed) {
@@ -6373,6 +6491,9 @@ ${hint}` : ""),
6373
6491
  };
6374
6492
 
6375
6493
  // src/batch-tool-use.ts
6494
+ import {
6495
+ GOVERNED_TOOL_EXECUTOR_META_KEY
6496
+ } from "@wrongstack/core";
6376
6497
  var batchToolUseTool = {
6377
6498
  name: "batch_tool_use",
6378
6499
  category: "Meta",
@@ -6409,7 +6530,7 @@ var batchToolUseTool = {
6409
6530
  },
6410
6531
  required: ["calls"]
6411
6532
  },
6412
- async execute(input, ctx, opts) {
6533
+ async execute(input, ctx, _opts) {
6413
6534
  if (!input?.calls || input.calls.length === 0) {
6414
6535
  return {
6415
6536
  results: [],
@@ -6419,18 +6540,33 @@ var batchToolUseTool = {
6419
6540
  stop_on_error: false
6420
6541
  };
6421
6542
  }
6543
+ const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
6544
+ if (typeof governedExecute !== "function") {
6545
+ return {
6546
+ results: input.calls.map((call) => ({
6547
+ tool: call.tool,
6548
+ success: false,
6549
+ error: "governed nested execution is unavailable; call the tool directly",
6550
+ executionMs: 0
6551
+ })),
6552
+ total: input.calls.length,
6553
+ succeeded: 0,
6554
+ failed: input.calls.length,
6555
+ stop_on_error: input.stop_on_error ?? false
6556
+ };
6557
+ }
6422
6558
  const results = [];
6423
6559
  let succeeded = 0;
6424
6560
  let failed = 0;
6425
6561
  if (input.parallel !== false) {
6426
- const promises = input.calls.map(async (call) => executeSingle(call, ctx, opts));
6562
+ const promises = input.calls.map(async (call) => executeSingle(call, ctx, governedExecute));
6427
6563
  const allResults = await Promise.all(promises);
6428
6564
  results.push(...allResults);
6429
6565
  succeeded = allResults.filter((r) => r.success).length;
6430
6566
  failed = allResults.filter((r) => !r.success).length;
6431
6567
  } else {
6432
6568
  for (const call of input.calls) {
6433
- const result = await executeSingle(call, ctx, opts ?? { signal: void 0 });
6569
+ const result = await executeSingle(call, ctx, governedExecute);
6434
6570
  results.push(result);
6435
6571
  if (result.success) {
6436
6572
  succeeded++;
@@ -6449,9 +6585,9 @@ var batchToolUseTool = {
6449
6585
  };
6450
6586
  }
6451
6587
  };
6452
- async function executeSingle(call, ctx, opts) {
6588
+ async function executeSingle(call, ctx, governedExecute) {
6453
6589
  const start = Date.now();
6454
- const tool = ctx.tools.find((t) => t.name === call.tool);
6590
+ const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
6455
6591
  if (!tool) {
6456
6592
  return {
6457
6593
  tool: call.tool,
@@ -6461,11 +6597,11 @@ async function executeSingle(call, ctx, opts) {
6461
6597
  };
6462
6598
  }
6463
6599
  try {
6464
- const result = await tool.execute(call.input, ctx, opts);
6600
+ const result = await governedExecute(call.tool, call.input);
6465
6601
  return {
6466
6602
  tool: call.tool,
6467
- success: true,
6468
- result,
6603
+ success: result.success,
6604
+ ...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
6469
6605
  executionMs: Date.now() - start
6470
6606
  };
6471
6607
  } catch (e) {
@@ -6870,7 +7006,7 @@ var BrowserSessionManager = class {
6870
7006
  constructor(options, launcher = defaultLauncher) {
6871
7007
  this.launcher = launcher;
6872
7008
  this.artifacts = new BrowserArtifactStore(options.artifactRoot);
6873
- this.allowPrivateHosts = options.allowPrivateHosts ?? false;
7009
+ this.allowPrivateHosts = options.allowPrivateHosts ?? true;
6874
7010
  this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
6875
7011
  this.networkProxy = new BrowserNetworkGuardProxy({
6876
7012
  allowPrivateHosts: this.allowPrivateHosts,
@@ -7807,7 +7943,7 @@ import * as path13 from "node:path";
7807
7943
  import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
7808
7944
 
7809
7945
  // src/codebase-index/schema.ts
7810
- var SCHEMA_VERSION = 2;
7946
+ var SCHEMA_VERSION = 3;
7811
7947
 
7812
7948
  // src/codebase-index/lsp-kind.ts
7813
7949
  function lspKindToInternalKind(k) {
@@ -7850,7 +7986,7 @@ function tokenise(text) {
7850
7986
  return sanitised.toLowerCase().split(" ").filter(Boolean);
7851
7987
  }
7852
7988
  function splitName(name) {
7853
- return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
7989
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([\p{L}])(\d)/gu, "$1 $2").replace(/(\d)([\p{L}])/gu, "$1 $2").replace(/[_-]+/g, " ").trim();
7854
7990
  }
7855
7991
  function buildIndexableText(name, signature, docComment) {
7856
7992
  return [splitName(name), name, signature, docComment].filter(Boolean).join(" ");
@@ -7876,14 +8012,12 @@ function buildBm25Index(docs) {
7876
8012
  return new Bm25Index(documents, df, N, avgLen);
7877
8013
  }
7878
8014
  var Bm25Index = class {
7879
- constructor(documents, df, N, avgLen) {
8015
+ constructor(documents, _df, N, avgLen) {
7880
8016
  this.documents = documents;
7881
- this.df = df;
7882
8017
  this.N = N;
7883
8018
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
7884
8019
  }
7885
8020
  documents;
7886
- df;
7887
8021
  N;
7888
8022
  safeAvgLen;
7889
8023
  score(query, filter) {
@@ -7896,10 +8030,13 @@ var Bm25Index = class {
7896
8030
  for (const qTerm of qTokens) {
7897
8031
  let tf = 0;
7898
8032
  for (const t of doc.tokens) {
7899
- if (t === qTerm) tf++;
8033
+ if (t.startsWith(qTerm)) tf++;
7900
8034
  }
7901
8035
  if (tf === 0) continue;
7902
- const dfVal = this.df[qTerm] ?? 0;
8036
+ let dfVal = 0;
8037
+ for (const candidate of this.documents) {
8038
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
8039
+ }
7903
8040
  if (dfVal === 0) continue;
7904
8041
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
7905
8042
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -7932,6 +8069,29 @@ var Bm25Index = class {
7932
8069
 
7933
8070
  // src/codebase-index/writer.ts
7934
8071
  var DB_FILE = "index.db";
8072
+ function escapeLike(value) {
8073
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
8074
+ }
8075
+ function assignRefsToSymbols(refs, symbols) {
8076
+ if (refs.length === 0 || symbols.length === 0) return [];
8077
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
8078
+ const seen = /* @__PURE__ */ new Set();
8079
+ const assigned = [];
8080
+ for (const ref of refs) {
8081
+ let owner2;
8082
+ for (const symbol of ordered) {
8083
+ if (symbol.line > ref.line) break;
8084
+ owner2 = symbol;
8085
+ }
8086
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
8087
+ if (!owner2 || owner2.id <= 0) continue;
8088
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
8089
+ if (seen.has(key)) continue;
8090
+ seen.add(key);
8091
+ assigned.push({ ...ref, fromId: owner2.id });
8092
+ }
8093
+ return assigned;
8094
+ }
7935
8095
  function resolveIndexDir(projectRoot, override) {
7936
8096
  return override ?? resolveWstackPaths2({ projectRoot }).projectCodebaseIndex;
7937
8097
  }
@@ -7985,7 +8145,7 @@ function sleepSync(ms) {
7985
8145
  } catch {
7986
8146
  }
7987
8147
  }
7988
- var IndexStore = class {
8148
+ var IndexStore = class _IndexStore {
7989
8149
  db;
7990
8150
  /** Absolute path to this project's index directory. */
7991
8151
  indexDir;
@@ -8037,10 +8197,7 @@ var IndexStore = class {
8037
8197
  const msg = lastError instanceof Error ? lastError.message : String(lastError);
8038
8198
  throw new LockError(`SQLite lock conflict after ${MAX_LOCK_RETRIES} retries: ${msg}`);
8039
8199
  }
8040
- const delay = Math.min(
8041
- LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt,
8042
- LOCK_RETRY_MAX_DELAY_MS
8043
- );
8200
+ const delay = Math.min(LOCK_RETRY_BASE_DELAY_MS * 2 ** attempt, LOCK_RETRY_MAX_DELAY_MS);
8044
8201
  sleepSync(delay);
8045
8202
  }
8046
8203
  }
@@ -8122,8 +8279,24 @@ var IndexStore = class {
8122
8279
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_to_name ON refs(to_name)");
8123
8280
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_r_call_type ON refs(call_type)");
8124
8281
  try {
8125
- this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')");
8282
+ this.db.exec(
8283
+ "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')"
8284
+ );
8126
8285
  this.ftsAvailable = true;
8286
+ const symbolCount = Number(
8287
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8288
+ );
8289
+ const ftsCount = Number(
8290
+ this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8291
+ );
8292
+ if (symbolCount !== ftsCount) {
8293
+ this.db.exec("DELETE FROM symbols_fts");
8294
+ const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
8295
+ const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8296
+ for (const row of rows) {
8297
+ insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
8298
+ }
8299
+ }
8127
8300
  } catch {
8128
8301
  this.ftsAvailable = false;
8129
8302
  }
@@ -8181,7 +8354,9 @@ var IndexStore = class {
8181
8354
  deleteSymbolsForFile(file) {
8182
8355
  this.runWithRetry(() => {
8183
8356
  if (this.ftsAvailable) {
8184
- this.stmt("DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8357
+ this.stmt(
8358
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8359
+ ).run(file);
8185
8360
  }
8186
8361
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
8187
8362
  });
@@ -8193,9 +8368,21 @@ var IndexStore = class {
8193
8368
  */
8194
8369
  deleteFile(file) {
8195
8370
  this.runWithRetry(() => {
8196
- this.deleteRefsForFile(file);
8197
- this.deleteSymbolsForFile(file);
8198
- this.stmt("DELETE FROM files WHERE file = ?").run(file);
8371
+ this.db.exec("BEGIN IMMEDIATE");
8372
+ try {
8373
+ if (this.ftsAvailable) {
8374
+ this.db.prepare(
8375
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8376
+ ).run(file);
8377
+ }
8378
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8379
+ this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
8380
+ this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
8381
+ this.db.exec("COMMIT");
8382
+ } catch (err) {
8383
+ this.db.exec("ROLLBACK");
8384
+ throw err;
8385
+ }
8199
8386
  });
8200
8387
  }
8201
8388
  // ─── File metadata ──────────────────────────────────────────────────────────
@@ -8218,14 +8405,22 @@ var IndexStore = class {
8218
8405
  ).all(file);
8219
8406
  if (!rows.length) return null;
8220
8407
  const r = expectDefined2(rows[0]);
8221
- return { file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed };
8408
+ return {
8409
+ file: r.file,
8410
+ lang: r.lang,
8411
+ mtimeMs: r.mtime_ms,
8412
+ symbolCount: r.symbol_count,
8413
+ lastIndexed: r.last_indexed
8414
+ };
8222
8415
  }
8223
8416
  getAllFileMetas() {
8224
- return this.db.prepare(
8225
- "SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files"
8226
- ).all().map(
8227
- (r) => ({ file: r.file, lang: r.lang, mtimeMs: r.mtime_ms, symbolCount: r.symbol_count, lastIndexed: r.last_indexed })
8228
- );
8417
+ return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8418
+ file: r.file,
8419
+ lang: r.lang,
8420
+ mtimeMs: r.mtime_ms,
8421
+ symbolCount: r.symbol_count,
8422
+ lastIndexed: r.last_indexed
8423
+ }));
8229
8424
  }
8230
8425
  // ─── Search ──────────────────────────────────────────────────────────────────
8231
8426
  search(query, filter) {
@@ -8249,8 +8444,8 @@ var IndexStore = class {
8249
8444
  values.push(filter.lang);
8250
8445
  }
8251
8446
  if (filter?.file) {
8252
- conditions.push("file LIKE ?");
8253
- values.push(`%${filter.file}%`);
8447
+ conditions.push("replace(file, '\\', '/') LIKE ? ESCAPE '\\'");
8448
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8254
8449
  }
8255
8450
  if (query.trim()) {
8256
8451
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -8288,9 +8483,11 @@ var IndexStore = class {
8288
8483
  * "users", camelCase-split text makes "complex" find "complexOperation").
8289
8484
  */
8290
8485
  searchRanked(query, filter, limit) {
8486
+ const rawLimit = Number.isFinite(limit) ? Math.trunc(limit) : 20;
8487
+ const safeLimit = Math.max(1, Math.min(rawLimit, 100));
8291
8488
  const tokens = tokenise(query);
8292
8489
  if (tokens.length === 0 || !this.ftsAvailable) {
8293
- return this.searchRankedFallback(query, filter, limit);
8490
+ return this.searchRankedFallback(query, filter, safeLimit);
8294
8491
  }
8295
8492
  let effectiveKind = filter?.kind;
8296
8493
  if (filter?.lspKind !== void 0) {
@@ -8310,11 +8507,13 @@ var IndexStore = class {
8310
8507
  values.push(filter.lang);
8311
8508
  }
8312
8509
  if (filter?.file) {
8313
- conditions.push("s.file LIKE ?");
8314
- values.push(`%${filter.file}%`);
8510
+ conditions.push("replace(s.file, '\\', '/') LIKE ? ESCAPE '\\'");
8511
+ values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8315
8512
  }
8316
8513
  const where = conditions.join(" AND ");
8317
- const countRows = this.db.prepare(`SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`).all(...values);
8514
+ const countRows = this.db.prepare(
8515
+ `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8516
+ ).all(...values);
8318
8517
  const total = countRows[0] ? Number(countRows[0].n) : 0;
8319
8518
  if (total === 0) return { results: [], total: 0 };
8320
8519
  const rows = this.db.prepare(
@@ -8323,9 +8522,13 @@ var IndexStore = class {
8323
8522
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
8324
8523
  FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid
8325
8524
  WHERE ${where}
8326
- ORDER BY bm25(symbols_fts)
8525
+ ORDER BY
8526
+ CASE WHEN lower(s.name) = lower(?) THEN 0
8527
+ WHEN lower(s.name) LIKE lower(?) ESCAPE '\\' THEN 1
8528
+ ELSE 2 END,
8529
+ bm25(symbols_fts), lower(s.name), s.file, s.line, s.col, s.id
8327
8530
  LIMIT ?`
8328
- ).all(...values, limit);
8531
+ ).all(...values, query.trim(), `${escapeLike(query.trim())}%`, safeLimit);
8329
8532
  return {
8330
8533
  results: rows.map((r) => ({
8331
8534
  id: r.id,
@@ -8355,10 +8558,28 @@ var IndexStore = class {
8355
8558
  }
8356
8559
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8357
8560
  const bm25 = buildBm25Index(
8358
- candidates.map((c) => ({ id: c.id, text: buildIndexableText(c.name, c.signature, c.docComment) }))
8561
+ candidates.map((c) => ({
8562
+ id: c.id,
8563
+ text: buildIndexableText(c.name, c.signature, c.docComment)
8564
+ }))
8359
8565
  );
8360
8566
  const scored = bm25.score(query, (id) => candidateById.has(id));
8361
- scored.sort((a, b) => b.score - a.score);
8567
+ const q = query.trim().toLowerCase();
8568
+ const rank = (id) => {
8569
+ const name = candidateById.get(id)?.name.toLowerCase() ?? "";
8570
+ if (name === q) return 0;
8571
+ if (name.startsWith(q)) return 1;
8572
+ return 2;
8573
+ };
8574
+ scored.sort((a, b) => {
8575
+ const rankDiff = rank(a.id) - rank(b.id);
8576
+ if (rankDiff !== 0) return rankDiff;
8577
+ const scoreDiff = b.score - a.score;
8578
+ if (scoreDiff !== 0) return scoreDiff;
8579
+ const left = expectDefined2(candidateById.get(a.id));
8580
+ const right = expectDefined2(candidateById.get(b.id));
8581
+ return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
8582
+ });
8362
8583
  const qTokens = tokenise(query);
8363
8584
  const results = scored.slice(0, limit).map(({ id, score }) => {
8364
8585
  const c = expectDefined2(candidateById.get(id));
@@ -8367,9 +8588,7 @@ var IndexStore = class {
8367
8588
  return { results, total: candidates.length };
8368
8589
  }
8369
8590
  getAllIndexable() {
8370
- return this.db.prepare("SELECT id, text FROM symbols").all().map(
8371
- ({ id, text }) => ({ id, text })
8372
- );
8591
+ return this.db.prepare("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
8373
8592
  }
8374
8593
  /**
8375
8594
  * Largest symbol id currently in the table (0 when empty). New ids must be
@@ -8385,22 +8604,16 @@ var IndexStore = class {
8385
8604
  // ─── Stats ───────────────────────────────────────────────────────────────────
8386
8605
  getStats() {
8387
8606
  const sizeBytes = this.sizeBytes();
8388
- const lastRows = this.db.prepare(
8389
- "SELECT value FROM metadata WHERE key = 'last_indexed'"
8390
- ).all();
8607
+ const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8391
8608
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8392
8609
  const totalRows = this.db.prepare("SELECT COUNT(*) FROM symbols").all();
8393
8610
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
8394
8611
  const fileRows = this.db.prepare("SELECT COUNT(*) FROM files").all();
8395
8612
  const totalFiles = fileRows[0] ? Number(fileRows[0]["COUNT(*)"]) : 0;
8396
- const langRows = this.db.prepare(
8397
- "SELECT lang, COUNT(*) FROM symbols GROUP BY lang"
8398
- ).all();
8613
+ const langRows = this.db.prepare("SELECT lang, COUNT(*) FROM symbols GROUP BY lang").all();
8399
8614
  const byLang = {};
8400
8615
  for (const row of langRows) byLang[row.lang] = Number(row["COUNT(*)"]);
8401
- const kindRows = this.db.prepare(
8402
- "SELECT kind, COUNT(*) FROM symbols GROUP BY kind"
8403
- ).all();
8616
+ const kindRows = this.db.prepare("SELECT kind, COUNT(*) FROM symbols GROUP BY kind").all();
8404
8617
  const byKind = {};
8405
8618
  for (const row of kindRows) byKind[row.kind] = Number(row["COUNT(*)"]);
8406
8619
  return {
@@ -8416,17 +8629,31 @@ var IndexStore = class {
8416
8629
  }
8417
8630
  setLastIndexed(ts2) {
8418
8631
  this.runWithRetry(() => {
8419
- this.db.prepare(
8420
- "INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)"
8421
- ).run(String(ts2));
8632
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8633
+ });
8634
+ }
8635
+ getMetadata(key) {
8636
+ const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
8637
+ return rows[0]?.value;
8638
+ }
8639
+ setMetadata(key, value) {
8640
+ this.runWithRetry(() => {
8641
+ this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8422
8642
  });
8423
8643
  }
8424
8644
  clearAll() {
8425
8645
  this.runWithRetry(() => {
8426
- this.db.exec("DELETE FROM symbols");
8427
- this.db.exec("DELETE FROM files");
8428
- this.db.exec("DELETE FROM refs");
8429
- if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8646
+ this.db.exec("BEGIN IMMEDIATE");
8647
+ try {
8648
+ this.db.exec("DELETE FROM refs");
8649
+ this.db.exec("DELETE FROM symbols");
8650
+ this.db.exec("DELETE FROM files");
8651
+ if (this.ftsAvailable) this.db.exec("DELETE FROM symbols_fts");
8652
+ this.db.exec("COMMIT");
8653
+ } catch (err) {
8654
+ this.db.exec("ROLLBACK");
8655
+ throw err;
8656
+ }
8430
8657
  });
8431
8658
  }
8432
8659
  // ─── Ref CRUD ────────────────────────────────────────────────────────────────
@@ -8517,6 +8744,7 @@ var IndexStore = class {
8517
8744
  const allInserted = [];
8518
8745
  const refsToInsert = [];
8519
8746
  for (const entry of entries) {
8747
+ const insertedForEntry = [];
8520
8748
  for (const s of entry.symbols) {
8521
8749
  const id = nextId++;
8522
8750
  symStmt.run(
@@ -8534,9 +8762,11 @@ var IndexStore = class {
8534
8762
  s.file
8535
8763
  );
8536
8764
  ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8537
- allInserted.push({ ...s, id });
8765
+ const inserted = { ...s, id };
8766
+ allInserted.push(inserted);
8767
+ insertedForEntry.push(inserted);
8538
8768
  }
8539
- for (const r of entry.refs) refsToInsert.push(r);
8769
+ refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8540
8770
  }
8541
8771
  if (refsToInsert.length > 0) {
8542
8772
  const refStmt = this.db.prepare(
@@ -8574,9 +8804,7 @@ var IndexStore = class {
8574
8804
  */
8575
8805
  deleteRefsForFile(file) {
8576
8806
  this.runWithRetry(() => {
8577
- this.db.prepare(
8578
- "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
8579
- ).run(file);
8807
+ this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8580
8808
  });
8581
8809
  }
8582
8810
  /**
@@ -8617,9 +8845,7 @@ var IndexStore = class {
8617
8845
  * Find all references FROM a given symbol (what does this symbol call/use?).
8618
8846
  */
8619
8847
  findRefsFrom(symbolId) {
8620
- return this.db.prepare(
8621
- "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?"
8622
- ).all(symbolId).map((r) => ({
8848
+ return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
8623
8849
  id: r.id,
8624
8850
  fromId: r.from_id,
8625
8851
  toName: r.to_name,
@@ -8636,6 +8862,325 @@ var IndexStore = class {
8636
8862
  return 0;
8637
8863
  }
8638
8864
  }
8865
+ // ─── CodeMap graph aggregation ──────────────────────────────────────────────
8866
+ /**
8867
+ * Derive a monorepo package name from an absolute file path.
8868
+ * Handles both `packages/<name>/...` (workspace) and `src/<name>/...` layouts.
8869
+ * Returns `undefined` for files outside any recognisable package structure.
8870
+ */
8871
+ static derivePackage(filePath) {
8872
+ const f = filePath.replace(/\\/g, "/");
8873
+ const pkgsIdx = f.indexOf("/packages/");
8874
+ if (pkgsIdx !== -1) {
8875
+ const rest = f.slice(pkgsIdx + "/packages/".length);
8876
+ const seg = rest.split("/")[0];
8877
+ return seg ? `@wrongstack/${seg}` : void 0;
8878
+ }
8879
+ const appsIdx = f.indexOf("/apps/");
8880
+ if (appsIdx !== -1) {
8881
+ const rest = f.slice(appsIdx + "/apps/".length);
8882
+ const seg = rest.split("/")[0];
8883
+ return seg ? `app:${seg}` : void 0;
8884
+ }
8885
+ return void 0;
8886
+ }
8887
+ static packageFromImport(moduleName) {
8888
+ if (!moduleName.startsWith("@wrongstack/")) return void 0;
8889
+ const parts = moduleName.split("/");
8890
+ return parts[1] ? `@wrongstack/${parts[1]}` : void 0;
8891
+ }
8892
+ static resolveRelativeImport(fromFile, moduleName, indexedFiles) {
8893
+ if (!moduleName.startsWith(".")) return void 0;
8894
+ const normalizedFrom = fromFile.replace(/\\/g, "/");
8895
+ const absolute = path13.posix.normalize(
8896
+ path13.posix.join(path13.posix.dirname(normalizedFrom), moduleName)
8897
+ );
8898
+ const extension = path13.posix.extname(absolute);
8899
+ const base = extension ? absolute.slice(0, -extension.length) : absolute;
8900
+ const candidates = [
8901
+ absolute,
8902
+ ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
8903
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(absolute, `index${ext}`)),
8904
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path13.posix.join(base, `index${ext}`))
8905
+ ];
8906
+ const indexedByPortablePath = new Map(
8907
+ [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
8908
+ );
8909
+ for (const candidate of candidates) {
8910
+ const indexed = indexedByPortablePath.get(candidate.toLocaleLowerCase());
8911
+ if (indexed) return indexed;
8912
+ }
8913
+ return void 0;
8914
+ }
8915
+ /**
8916
+ * Package-level graph: each workspace package is a node; edges are derived
8917
+ * from cross-package symbol references (a symbol in package A references a
8918
+ * symbol resolved in package B). Node metadata includes symbol/file counts.
8919
+ */
8920
+ getPackageGraph() {
8921
+ const symbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
8922
+ const pkgNodes = /* @__PURE__ */ new Map();
8923
+ const fileToPkg = /* @__PURE__ */ new Map();
8924
+ const symbolToPkg = /* @__PURE__ */ new Map();
8925
+ for (const s of symbols) {
8926
+ const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
8927
+ fileToPkg.set(s.file, pkg);
8928
+ symbolToPkg.set(s.id, pkg);
8929
+ const node = pkgNodes.get(pkg);
8930
+ if (node) {
8931
+ node.symbolCount = (node.symbolCount ?? 0) + 1;
8932
+ } else {
8933
+ pkgNodes.set(pkg, {
8934
+ id: `pkg:${pkg}`,
8935
+ label: pkg,
8936
+ kind: "package",
8937
+ package: pkg,
8938
+ symbolCount: 1,
8939
+ fileCount: 0
8940
+ });
8941
+ }
8942
+ }
8943
+ const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
8944
+ for (const { file } of files) {
8945
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
8946
+ const node = pkgNodes.get(pkg);
8947
+ if (node) node.fileCount = (node.fileCount ?? 0) + 1;
8948
+ }
8949
+ const refRows = this.db.prepare(
8950
+ `SELECT r.from_id, r.to_id, r.call_type
8951
+ FROM refs r
8952
+ WHERE r.to_id IS NOT NULL`
8953
+ ).all();
8954
+ const edgeMap = /* @__PURE__ */ new Map();
8955
+ for (const r of refRows) {
8956
+ if (r.call_type === "import") continue;
8957
+ const fromPkg = symbolToPkg.get(r.from_id);
8958
+ const toPkg = symbolToPkg.get(r.to_id);
8959
+ if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
8960
+ const key = `${fromPkg}\0${toPkg}`;
8961
+ let e = edgeMap.get(key);
8962
+ if (!e) {
8963
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
8964
+ edgeMap.set(key, e);
8965
+ }
8966
+ e.weight++;
8967
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
8968
+ }
8969
+ const importRows = this.db.prepare(
8970
+ `SELECT r.from_id, r.to_name
8971
+ FROM refs r
8972
+ WHERE r.call_type = 'import'`
8973
+ ).all();
8974
+ for (const r of importRows) {
8975
+ const fromPkg = symbolToPkg.get(r.from_id);
8976
+ const toPkg = _IndexStore.packageFromImport(r.to_name);
8977
+ if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
8978
+ const key = `${fromPkg}\0${toPkg}`;
8979
+ let edge = edgeMap.get(key);
8980
+ if (!edge) {
8981
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8982
+ edgeMap.set(key, edge);
8983
+ }
8984
+ edge.weight++;
8985
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
8986
+ }
8987
+ const edges = [];
8988
+ for (const [key, e] of edgeMap) {
8989
+ const [source, target] = key.split("\0");
8990
+ let bestType = "call";
8991
+ let bestCount = 0;
8992
+ for (const [t, c] of e.types) {
8993
+ if (c > bestCount) {
8994
+ bestType = t;
8995
+ bestCount = c;
8996
+ }
8997
+ }
8998
+ edges.push({
8999
+ source: `pkg:${source}`,
9000
+ target: `pkg:${target}`,
9001
+ weight: e.weight,
9002
+ refType: bestType
9003
+ });
9004
+ }
9005
+ return { nodes: [...pkgNodes.values()], edges };
9006
+ }
9007
+ /**
9008
+ * File-level graph for a single package: each file is a node; edges are
9009
+ * derived from cross-file symbol references within the package.
9010
+ */
9011
+ getFileGraph(packageFilter) {
9012
+ const allSymbols = this.db.prepare("SELECT file, id, name, kind, lang, line FROM symbols ORDER BY id").all();
9013
+ const pkgSyms = allSymbols.filter(
9014
+ (s) => (_IndexStore.derivePackage(s.file) ?? "(root)") === packageFilter
9015
+ );
9016
+ if (pkgSyms.length === 0) return { nodes: [], edges: [] };
9017
+ const fileNodes = /* @__PURE__ */ new Map();
9018
+ const symToFile = /* @__PURE__ */ new Map();
9019
+ const fileStats = /* @__PURE__ */ new Map();
9020
+ for (const s of allSymbols) {
9021
+ symToFile.set(s.id, s.file);
9022
+ const current = fileStats.get(s.file);
9023
+ fileStats.set(s.file, {
9024
+ count: (current?.count ?? 0) + 1,
9025
+ lang: current?.lang ?? s.lang
9026
+ });
9027
+ }
9028
+ const localFiles = new Set(pkgSyms.map((s) => s.file));
9029
+ const indexedFiles = new Set(allSymbols.map((s) => s.file));
9030
+ const ensureFileNode = (file) => {
9031
+ if (fileNodes.has(file)) return;
9032
+ const stats = fileStats.get(file);
9033
+ fileNodes.set(file, {
9034
+ id: `file:${file}`,
9035
+ label: file.replace(/\\/g, "/").split("/").pop() ?? file,
9036
+ kind: "file",
9037
+ package: _IndexStore.derivePackage(file) ?? "(root)",
9038
+ file,
9039
+ symbolCount: stats?.count ?? 0,
9040
+ lang: stats?.lang,
9041
+ external: !localFiles.has(file)
9042
+ });
9043
+ };
9044
+ for (const file of localFiles) {
9045
+ ensureFileNode(file);
9046
+ }
9047
+ const refRows = this.db.prepare(
9048
+ `SELECT r.from_id, r.to_id, r.call_type
9049
+ FROM refs r
9050
+ WHERE r.to_id IS NOT NULL`
9051
+ ).all();
9052
+ const edgeMap = /* @__PURE__ */ new Map();
9053
+ for (const r of refRows) {
9054
+ if (r.call_type === "import") continue;
9055
+ const fromFile = symToFile.get(r.from_id);
9056
+ const toFile = symToFile.get(r.to_id);
9057
+ if (!fromFile || !toFile || fromFile === toFile) continue;
9058
+ if (!localFiles.has(fromFile) && !localFiles.has(toFile)) continue;
9059
+ ensureFileNode(fromFile);
9060
+ ensureFileNode(toFile);
9061
+ const key = `${fromFile}\0${toFile}`;
9062
+ let e = edgeMap.get(key);
9063
+ if (!e) {
9064
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
9065
+ edgeMap.set(key, e);
9066
+ }
9067
+ e.weight++;
9068
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9069
+ }
9070
+ const importRows = this.db.prepare(
9071
+ `SELECT r.from_id, r.to_name
9072
+ FROM refs r
9073
+ WHERE r.call_type = 'import'`
9074
+ ).all();
9075
+ for (const r of importRows) {
9076
+ const fromFile = symToFile.get(r.from_id);
9077
+ if (!fromFile || !localFiles.has(fromFile)) continue;
9078
+ const toFile = _IndexStore.resolveRelativeImport(fromFile, r.to_name, indexedFiles);
9079
+ if (!toFile || fromFile === toFile) continue;
9080
+ ensureFileNode(fromFile);
9081
+ ensureFileNode(toFile);
9082
+ const key = `${fromFile}\0${toFile}`;
9083
+ let edge = edgeMap.get(key);
9084
+ if (!edge) {
9085
+ edge = { weight: 0, types: /* @__PURE__ */ new Map() };
9086
+ edgeMap.set(key, edge);
9087
+ }
9088
+ edge.weight++;
9089
+ edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
9090
+ }
9091
+ const edges = [];
9092
+ for (const [key, e] of edgeMap) {
9093
+ const [source, target] = key.split("\0");
9094
+ let bestType = "call";
9095
+ let bestCount = 0;
9096
+ for (const [t, c] of e.types) {
9097
+ if (c > bestCount) {
9098
+ bestType = t;
9099
+ bestCount = c;
9100
+ }
9101
+ }
9102
+ edges.push({
9103
+ source: `file:${source}`,
9104
+ target: `file:${target}`,
9105
+ weight: e.weight,
9106
+ refType: bestType
9107
+ });
9108
+ }
9109
+ return { nodes: [...fileNodes.values()], edges };
9110
+ }
9111
+ /**
9112
+ * Symbol-level graph for a single file: each symbol is a node; edges are
9113
+ * derived from intra-file and cross-file symbol references (who calls whom).
9114
+ */
9115
+ getSymbolGraph(fileFilter) {
9116
+ const allSymbols = this.db.prepare(
9117
+ "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols ORDER BY file, line, id"
9118
+ ).all();
9119
+ const syms = allSymbols.filter((symbol) => symbol.file === fileFilter);
9120
+ if (syms.length === 0) return { nodes: [], edges: [] };
9121
+ const symById = new Map(allSymbols.map((symbol) => [symbol.id, symbol]));
9122
+ const relatedIds = new Set(syms.map((symbol) => symbol.id));
9123
+ const toGraphNode = (s) => ({
9124
+ id: `sym:${s.id}`,
9125
+ label: s.name,
9126
+ kind: "symbol",
9127
+ symbolId: s.id,
9128
+ symbolKind: s.kind,
9129
+ file: s.file,
9130
+ package: _IndexStore.derivePackage(s.file) ?? "(root)",
9131
+ lang: s.lang,
9132
+ line: s.line,
9133
+ signature: s.signature,
9134
+ scope: s.scope,
9135
+ external: s.file !== fileFilter
9136
+ });
9137
+ const symIds = new Set(syms.map((s) => s.id));
9138
+ const refRows = this.db.prepare(
9139
+ `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9140
+ FROM refs r
9141
+ WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
9142
+ OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
9143
+ ).all(fileFilter, fileFilter);
9144
+ const edgeMap = /* @__PURE__ */ new Map();
9145
+ for (const r of refRows) {
9146
+ if (r.to_id === null) continue;
9147
+ if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
9148
+ relatedIds.add(r.from_id);
9149
+ relatedIds.add(r.to_id);
9150
+ const key = `${r.from_id}\0${r.to_id}`;
9151
+ let e = edgeMap.get(key);
9152
+ if (!e) {
9153
+ e = { weight: 0, types: /* @__PURE__ */ new Map() };
9154
+ edgeMap.set(key, e);
9155
+ }
9156
+ e.weight++;
9157
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9158
+ }
9159
+ const edges = [];
9160
+ for (const [key, e] of edgeMap) {
9161
+ const [fromId, toId] = key.split("\0").map(Number);
9162
+ let bestType = "call";
9163
+ let bestCount = 0;
9164
+ for (const [t, c] of e.types) {
9165
+ if (c > bestCount) {
9166
+ bestType = t;
9167
+ bestCount = c;
9168
+ }
9169
+ }
9170
+ edges.push({
9171
+ source: `sym:${fromId}`,
9172
+ target: `sym:${toId}`,
9173
+ weight: e.weight,
9174
+ refType: bestType
9175
+ });
9176
+ }
9177
+ const nodes = [...relatedIds].map((id) => symById.get(id)).filter((symbol) => symbol !== void 0).sort((a, b) => {
9178
+ const aExternal = a.file === fileFilter ? 0 : 1;
9179
+ const bExternal = b.file === fileFilter ? 0 : 1;
9180
+ return aExternal - bExternal || a.file.localeCompare(b.file) || a.line - b.line || a.id - b.id;
9181
+ }).map(toGraphNode);
9182
+ return { nodes, edges };
9183
+ }
8639
9184
  close() {
8640
9185
  this.stmtCache.clear();
8641
9186
  try {
@@ -8730,6 +9275,14 @@ function getJsDoc(node, sourceFile) {
8730
9275
  }
8731
9276
  return "";
8732
9277
  }
9278
+ function hasFunctionLikeAncestor(node) {
9279
+ let current = node.parent;
9280
+ while (current) {
9281
+ if (ts.isFunctionLike(current)) return true;
9282
+ current = current.parent;
9283
+ }
9284
+ return false;
9285
+ }
8733
9286
  function buildScope(node) {
8734
9287
  const parts = [];
8735
9288
  let current = node.parent;
@@ -8757,6 +9310,10 @@ function parseSymbols(opts) {
8757
9310
  function visit(node) {
8758
9311
  const kind = kindOf(node);
8759
9312
  if (kind) {
9313
+ if ((kind === "const" || kind === "let" || kind === "var" || kind === "parameter") && hasFunctionLikeAncestor(node)) {
9314
+ ts.forEachChild(node, visit);
9315
+ return;
9316
+ }
8760
9317
  const nameNode = node.name;
8761
9318
  if (!nameNode || !ts.isIdentifier(nameNode)) return;
8762
9319
  const name = nameNode.text;
@@ -8844,6 +9401,7 @@ function detectLang(file) {
8844
9401
  }
8845
9402
 
8846
9403
  // src/codebase-index/go-parser.ts
9404
+ init_win32_resolve();
8847
9405
  import { spawn as spawn4 } from "node:child_process";
8848
9406
  import * as os5 from "node:os";
8849
9407
  import * as path14 from "node:path";
@@ -9152,27 +9710,42 @@ async function syncGoParse(filePath, content, lang) {
9152
9710
  try {
9153
9711
  const scriptPath = path14.join(tmpDir, "parse.go");
9154
9712
  await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
9155
- const proc = spawn4("go", ["run", scriptPath], {
9156
- stdio: ["pipe", "pipe", "pipe"],
9157
- windowsHide: true
9158
- });
9159
- let stdout = "";
9160
- proc.stdout?.on("data", (chunk) => {
9161
- stdout += chunk.toString();
9162
- });
9163
- proc.stdin?.write(content);
9164
- proc.stdin?.end();
9165
- const { code } = await Promise.race([
9166
- new Promise((resolve15) => {
9167
- proc.on("close", (c) => resolve15({ code: c }));
9168
- }),
9169
- new Promise(
9170
- (_, reject) => setTimeout(() => {
9713
+ const goBinary = resolveWin32Command("go");
9714
+ const goResult = await new Promise(
9715
+ (resolve15, reject) => {
9716
+ let settled = false;
9717
+ const proc = spawn4(goBinary, ["run", scriptPath], {
9718
+ stdio: ["pipe", "pipe", "pipe"],
9719
+ windowsHide: true
9720
+ });
9721
+ proc.on("error", (err) => {
9722
+ if (settled) return;
9723
+ settled = true;
9724
+ reject(err);
9725
+ });
9726
+ let stdout2 = "";
9727
+ proc.stdout?.on("data", (chunk) => {
9728
+ stdout2 += chunk.toString();
9729
+ });
9730
+ proc.stderr?.resume();
9731
+ proc.stdin?.write(content);
9732
+ proc.stdin?.end();
9733
+ const timer = setTimeout(() => {
9734
+ if (settled) return;
9735
+ settled = true;
9171
9736
  proc.kill("SIGKILL");
9172
9737
  reject(new Error("timeout"));
9173
- }, 15e3)
9174
- )
9175
- ]).catch(() => ({ code: -1 }));
9738
+ }, 15e3);
9739
+ timer.unref?.();
9740
+ proc.on("close", (code2) => {
9741
+ if (settled) return;
9742
+ settled = true;
9743
+ clearTimeout(timer);
9744
+ resolve15({ code: code2, stdout: stdout2 });
9745
+ });
9746
+ }
9747
+ );
9748
+ const { code, stdout } = goResult;
9176
9749
  if (code !== 0 || !stdout.trim()) {
9177
9750
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9178
9751
  }
@@ -9199,7 +9772,8 @@ async function syncGoParse(filePath, content, lang) {
9199
9772
  }
9200
9773
 
9201
9774
  // src/codebase-index/py-parser.ts
9202
- import { spawn as spawn5 } from "node:child_process";
9775
+ init_win32_resolve();
9776
+ import { spawn as spawn5, spawnSync } from "node:child_process";
9203
9777
  import * as fs11 from "node:fs/promises";
9204
9778
  import * as os6 from "node:os";
9205
9779
  import * as path15 from "node:path";
@@ -9414,36 +9988,74 @@ visitor.visit(tree)
9414
9988
 
9415
9989
  print(json.dumps([s.to_dict() for s in syms]))
9416
9990
  `;
9417
- var _cachedScriptPath = null;
9418
- async function syncPyParse(filePath, content, lang) {
9419
- try {
9420
- if (!_cachedScriptPath) {
9421
- const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
9422
- await fs11.mkdir(tmpDir, { recursive: true });
9423
- _cachedScriptPath = path15.join(tmpDir, "parse.py");
9424
- await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9425
- }
9426
- const proc = spawn5("python", [_cachedScriptPath, filePath], {
9991
+ function resolvePython() {
9992
+ const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
9993
+ for (const name of candidates) {
9994
+ const resolved = resolveWin32Command(name);
9995
+ const result = spawnSync(resolved, ["--version"], {
9996
+ stdio: "pipe",
9997
+ timeout: 5e3
9998
+ });
9999
+ if (result.error) continue;
10000
+ if (result.status !== 0) continue;
10001
+ return resolved;
10002
+ }
10003
+ return null;
10004
+ }
10005
+ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
10006
+ return new Promise((resolve15, reject) => {
10007
+ let settled = false;
10008
+ const proc = spawn5(pyBinary, [scriptPath, filePath], {
9427
10009
  stdio: ["pipe", "pipe", "pipe"],
9428
10010
  windowsHide: true
9429
10011
  });
10012
+ proc.on("error", (err) => {
10013
+ if (settled) return;
10014
+ settled = true;
10015
+ reject(err);
10016
+ });
9430
10017
  proc.stdin?.write(content);
9431
10018
  proc.stdin?.end();
9432
10019
  let stdout = "";
9433
10020
  proc.stdout?.on("data", (chunk) => {
9434
10021
  stdout += chunk.toString();
9435
10022
  });
9436
- const { code } = await Promise.race([
9437
- new Promise((resolve15) => {
9438
- proc.on("close", (c) => resolve15({ code: c }));
9439
- }),
9440
- new Promise(
9441
- (_, reject) => setTimeout(() => {
9442
- proc.kill("SIGKILL");
9443
- reject(new Error("timeout"));
9444
- }, 15e3)
9445
- )
9446
- ]).catch(() => ({ code: -1 }));
10023
+ proc.stderr?.resume();
10024
+ const timer = setTimeout(() => {
10025
+ if (settled) return;
10026
+ settled = true;
10027
+ proc.kill("SIGKILL");
10028
+ reject(new Error("timeout"));
10029
+ }, 15e3);
10030
+ timer.unref?.();
10031
+ proc.on("close", (code) => {
10032
+ if (settled) return;
10033
+ settled = true;
10034
+ clearTimeout(timer);
10035
+ resolve15({ code, stdout });
10036
+ });
10037
+ });
10038
+ }
10039
+ var _cachedScriptPath = null;
10040
+ var _cachedPyBinary = null;
10041
+ async function syncPyParse(filePath, content, lang) {
10042
+ try {
10043
+ if (!_cachedScriptPath) {
10044
+ const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
10045
+ await fs11.mkdir(tmpDir, { recursive: true });
10046
+ _cachedScriptPath = path15.join(tmpDir, "parse.py");
10047
+ await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
10048
+ }
10049
+ if (!_cachedPyBinary) {
10050
+ _cachedPyBinary = resolvePython();
10051
+ if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
10052
+ }
10053
+ const { code, stdout } = await spawnPyParser(
10054
+ _cachedPyBinary,
10055
+ _cachedScriptPath,
10056
+ filePath,
10057
+ content
10058
+ );
9447
10059
  if (code !== 0 || !stdout.trim()) {
9448
10060
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9449
10061
  }
@@ -9468,6 +10080,7 @@ async function syncPyParse(filePath, content, lang) {
9468
10080
  }
9469
10081
 
9470
10082
  // src/codebase-index/rs-parser.ts
10083
+ init_win32_resolve();
9471
10084
  import { expectDefined as expectDefined3 } from "@wrongstack/core";
9472
10085
  import { execFileSync, spawn as spawn6 } from "node:child_process";
9473
10086
  import * as fs12 from "node:fs/promises";
@@ -9512,30 +10125,45 @@ async function tryNativeParse(file, content) {
9512
10125
  const crateDir = path16.join(toolsDir, "syn-parser");
9513
10126
  const tmpFile = path16.join(crateDir, "src", "input.rs");
9514
10127
  await fs12.writeFile(tmpFile, content, "utf8");
9515
- const proc = spawn6(
9516
- "cargo",
9517
- ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
9518
- {
9519
- cwd: process.cwd(),
9520
- stdio: ["pipe", "pipe", "pipe"],
9521
- windowsHide: true
9522
- }
9523
- );
9524
- let stdout = "";
9525
- proc.stdout?.on("data", (chunk) => {
9526
- stdout += chunk.toString();
9527
- });
9528
- const { code } = await Promise.race([
9529
- new Promise((resolve15) => {
9530
- proc.on("close", (c) => resolve15({ code: c }));
9531
- }),
9532
- new Promise(
9533
- (_, reject) => setTimeout(() => {
10128
+ const cargoBinary = resolveWin32Command("cargo");
10129
+ const result = await new Promise(
10130
+ (resolve15, reject) => {
10131
+ let settled = false;
10132
+ const proc = spawn6(
10133
+ cargoBinary,
10134
+ ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
10135
+ {
10136
+ cwd: process.cwd(),
10137
+ stdio: ["pipe", "pipe", "pipe"],
10138
+ windowsHide: true
10139
+ }
10140
+ );
10141
+ proc.on("error", (err) => {
10142
+ if (settled) return;
10143
+ settled = true;
10144
+ reject(err);
10145
+ });
10146
+ let stdout2 = "";
10147
+ proc.stdout?.on("data", (chunk) => {
10148
+ stdout2 += chunk.toString();
10149
+ });
10150
+ proc.stderr?.resume();
10151
+ const timer = setTimeout(() => {
10152
+ if (settled) return;
10153
+ settled = true;
9534
10154
  proc.kill("SIGKILL");
9535
10155
  reject(new Error("timeout"));
9536
- }, 15e3)
9537
- )
9538
- ]).catch(() => ({ code: -1 }));
10156
+ }, 15e3);
10157
+ timer.unref?.();
10158
+ proc.on("close", (c) => {
10159
+ if (settled) return;
10160
+ settled = true;
10161
+ clearTimeout(timer);
10162
+ resolve15({ code: c, stdout: stdout2 });
10163
+ });
10164
+ }
10165
+ );
10166
+ const { code, stdout } = result;
9539
10167
  if (code === 0 && stdout.trim()) {
9540
10168
  const symbols = JSON.parse(stdout.trim());
9541
10169
  return {
@@ -9630,9 +10258,9 @@ function parseSymbols5(opts) {
9630
10258
  function regexParse2(opts) {
9631
10259
  const { file, content, lang } = opts;
9632
10260
  const symbols = [];
9633
- const basename8 = path17.basename(file).toLowerCase();
9634
- const isPackageJson = basename8 === "package.json";
9635
- const isTsconfig = basename8 === "tsconfig.json" || basename8 === "tsconfig.build.json";
10261
+ const basename10 = path17.basename(file).toLowerCase();
10262
+ const isPackageJson = basename10 === "package.json";
10263
+ const isTsconfig = basename10 === "tsconfig.json" || basename10 === "tsconfig.build.json";
9636
10264
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
9637
10265
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
9638
10266
  const lines = content.split("\n");
@@ -10034,9 +10662,7 @@ function yieldEventLoop() {
10034
10662
  function throwIfAborted(signal) {
10035
10663
  if (!signal?.aborted) return;
10036
10664
  if (signal.reason instanceof Error) throw signal.reason;
10037
- throw new Error(
10038
- typeof signal.reason === "string" ? signal.reason : "Indexing cancelled"
10039
- );
10665
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
10040
10666
  }
10041
10667
  function isAbortError(err) {
10042
10668
  return err instanceof DOMException && err.name === "AbortError";
@@ -10052,8 +10678,20 @@ var DEFAULT_IGNORE = [
10052
10678
  "__snapshots__",
10053
10679
  ".nyc_output"
10054
10680
  ];
10681
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10682
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10683
+ function isWithinProject(projectRoot, file) {
10684
+ const rel = path19.relative(projectRoot, file);
10685
+ return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
10686
+ }
10687
+ function isMissingPathError(err) {
10688
+ const code = err?.code;
10689
+ return code === "ENOENT" || code === "ENOTDIR";
10690
+ }
10055
10691
  async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10056
10692
  const results = [];
10693
+ const errors = [];
10694
+ let complete = true;
10057
10695
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
10058
10696
  const globs = [
10059
10697
  { ext: ".ts", pat: compileGlob2("**/*.ts") },
@@ -10077,7 +10715,9 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10077
10715
  let entries;
10078
10716
  try {
10079
10717
  entries = await fs14.readdir(dir, { withFileTypes: true });
10080
- } catch {
10718
+ } catch (err) {
10719
+ complete = false;
10720
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
10081
10721
  return;
10082
10722
  }
10083
10723
  dirCount++;
@@ -10089,8 +10729,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10089
10729
  if (isGitIgnored(rel, true)) continue;
10090
10730
  await walk2(full);
10091
10731
  } else if (e.isFile()) {
10092
- if (isGitIgnored(rel, false)) continue;
10093
- const ext = path19.extname(e.name);
10732
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
10733
+ const ext = path19.extname(e.name).toLowerCase();
10094
10734
  for (const { ext: extName, pat } of globs) {
10095
10735
  if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
10096
10736
  results.push(full);
@@ -10101,7 +10741,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10101
10741
  }
10102
10742
  };
10103
10743
  await walk2(projectRoot);
10104
- return results;
10744
+ return { files: results, complete, errors };
10105
10745
  }
10106
10746
  async function parseFile(file, content, lang) {
10107
10747
  switch (lang) {
@@ -10124,6 +10764,26 @@ async function parseFile(file, content, lang) {
10124
10764
  return { file, lang, symbols: [], mtimeMs: Date.now() };
10125
10765
  }
10126
10766
  }
10767
+ function assignRefsToSymbols2(refs, symbols) {
10768
+ if (refs.length === 0 || symbols.length === 0) return [];
10769
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
10770
+ const seen = /* @__PURE__ */ new Set();
10771
+ const assigned = [];
10772
+ for (const ref of refs) {
10773
+ let owner2;
10774
+ for (const symbol of ordered) {
10775
+ if (symbol.line > ref.line) break;
10776
+ owner2 = symbol;
10777
+ }
10778
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
10779
+ if (!owner2 || owner2.id <= 0) continue;
10780
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
10781
+ if (seen.has(key)) continue;
10782
+ seen.add(key);
10783
+ assigned.push({ ...ref, fromId: owner2.id });
10784
+ }
10785
+ return assigned;
10786
+ }
10127
10787
  async function runIndexer(_ctx, opts) {
10128
10788
  const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
10129
10789
  try {
@@ -10136,7 +10796,9 @@ async function runIndexer(_ctx, opts) {
10136
10796
  }
10137
10797
  }
10138
10798
  async function runIndexerWithStore(store, opts) {
10139
- const { projectRoot, force = false, langs, ignore = [], signal } = opts;
10799
+ const { projectRoot, langs, ignore = [], signal } = opts;
10800
+ const relationGraphVersion = "2";
10801
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
10140
10802
  const startMs = Date.now();
10141
10803
  const errors = [];
10142
10804
  const langStats = {};
@@ -10145,10 +10807,18 @@ async function runIndexerWithStore(store, opts) {
10145
10807
  const isGitIgnored = await loadGitignoreMatcher(projectRoot);
10146
10808
  let files;
10147
10809
  let discoveredFiles = null;
10810
+ let discoveryComplete = true;
10148
10811
  if (opts.files && opts.files.length > 0) {
10149
- files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => !isGitIgnored(path19.relative(projectRoot, f).replace(/\\/g, "/"), false));
10812
+ files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
10813
+ if (!isWithinProject(projectRoot, f)) return false;
10814
+ const rel = path19.relative(projectRoot, f).replace(/\\/g, "/");
10815
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path19.basename(f)) && !isGitIgnored(rel, false);
10816
+ });
10150
10817
  } else {
10151
- files = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10818
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
10819
+ files = discovery.files;
10820
+ errors.push(...discovery.errors);
10821
+ discoveryComplete = discovery.complete;
10152
10822
  discoveredFiles = new Set(files);
10153
10823
  }
10154
10824
  if (langs && langs.length > 0) {
@@ -10173,36 +10843,66 @@ async function runIndexerWithStore(store, opts) {
10173
10843
  }
10174
10844
  const statOpts = signal ? { signal } : {};
10175
10845
  const statReadParse = await Promise.allSettled(
10176
- batchFiles.map(async (file) => {
10177
- let stat18;
10178
- try {
10179
- stat18 = await fs14.stat(file, statOpts);
10180
- } catch (e) {
10181
- if (isAbortError(e)) throw e;
10182
- return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
10183
- }
10184
- if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
10185
- const lang = detectLang(file);
10186
- if (!lang) return { file, stat: stat18, lang: "", parsed: null };
10187
- const meta = existingMeta.get(file);
10188
- if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
10189
- return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
10190
- }
10191
- let content;
10192
- try {
10193
- content = await fs14.readFile(file, { encoding: "utf8", signal });
10194
- } catch (e) {
10195
- if (isAbortError(e)) throw e;
10196
- return { file, stat: stat18, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
10197
- }
10198
- let parsed;
10199
- try {
10200
- parsed = await parseFile(file, content, lang);
10201
- } catch (e) {
10202
- return { file, stat: stat18, lang, parsed: null, error: `parse error: ${e instanceof Error ? e.message : String(e)}` };
10846
+ batchFiles.map(
10847
+ async (file) => {
10848
+ let stat18;
10849
+ try {
10850
+ stat18 = await fs14.stat(file, statOpts);
10851
+ } catch (e) {
10852
+ if (isAbortError(e)) throw e;
10853
+ return {
10854
+ file,
10855
+ stat: null,
10856
+ lang: "",
10857
+ parsed: null,
10858
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
10859
+ missing: isMissingPathError(e)
10860
+ };
10861
+ }
10862
+ if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
10863
+ const lang = detectLang(file);
10864
+ if (!lang) return { file, stat: stat18, lang: "", parsed: null };
10865
+ if (stat18.size > MAX_INDEX_FILE_BYTES) {
10866
+ return {
10867
+ file,
10868
+ stat: stat18,
10869
+ lang,
10870
+ parsed: null,
10871
+ error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
10872
+ };
10873
+ }
10874
+ const meta = existingMeta.get(file);
10875
+ if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
10876
+ return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
10877
+ }
10878
+ let content;
10879
+ try {
10880
+ content = await fs14.readFile(file, { encoding: "utf8", signal });
10881
+ } catch (e) {
10882
+ if (isAbortError(e)) throw e;
10883
+ return {
10884
+ file,
10885
+ stat: stat18,
10886
+ lang,
10887
+ parsed: null,
10888
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
10889
+ };
10890
+ }
10891
+ let parsed;
10892
+ try {
10893
+ parsed = await parseFile(file, content, lang);
10894
+ } catch (e) {
10895
+ return {
10896
+ file,
10897
+ stat: stat18,
10898
+ lang,
10899
+ parsed: null,
10900
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
10901
+ };
10902
+ }
10903
+ return { file, stat: stat18, lang, parsed, content };
10203
10904
  }
10204
- return { file, stat: stat18, lang, parsed, content };
10205
- })
10905
+ )
10206
10906
  );
10207
10907
  const batchEntries = [];
10208
10908
  const deleteForFiles = [];
@@ -10217,8 +10917,8 @@ async function runIndexerWithStore(store, opts) {
10217
10917
  }
10218
10918
  const result = settled.value;
10219
10919
  if (result.error) {
10220
- if (result.stat) store.deleteFile(file);
10221
- if (result.error.includes("error:")) errors.push(result.error);
10920
+ if (result.missing) store.deleteFile(file);
10921
+ errors.push(`${file}: ${result.error}`);
10222
10922
  continue;
10223
10923
  }
10224
10924
  const { stat: stat18, lang, parsed } = result;
@@ -10242,6 +10942,8 @@ async function runIndexerWithStore(store, opts) {
10242
10942
  continue;
10243
10943
  }
10244
10944
  if (parsed.symbols.length === 0) {
10945
+ store.deleteRefsForFile(file);
10946
+ store.deleteSymbolsForFile(file);
10245
10947
  store.upsertFile({
10246
10948
  file,
10247
10949
  lang,
@@ -10252,15 +10954,11 @@ async function runIndexerWithStore(store, opts) {
10252
10954
  filesIndexed++;
10253
10955
  continue;
10254
10956
  }
10255
- const refs = [];
10256
- if (parsed.refs && parsed.refs.length > 0) {
10257
- for (const r of parsed.refs) refs.push({ ...r, fromId: 0 });
10258
- }
10259
10957
  batchEntries.push({
10260
10958
  file,
10261
10959
  lang,
10262
10960
  symbols: parsed.symbols,
10263
- refs,
10961
+ refs: parsed.refs ?? [],
10264
10962
  mtimeMs: Math.floor(stat18.mtimeMs),
10265
10963
  symbolCount: parsed.symbols.length
10266
10964
  });
@@ -10268,34 +10966,12 @@ async function runIndexerWithStore(store, opts) {
10268
10966
  }
10269
10967
  if (batchEntries.length > 0) {
10270
10968
  try {
10271
- const inserted = store.commitBatch(batchEntries, { deleteForFiles });
10272
- let cursor = 0;
10969
+ store.commitBatch(batchEntries, { deleteForFiles });
10273
10970
  for (const entry of batchEntries) {
10274
10971
  const count = entry.symbols.length;
10275
- const symbolsWithIds = inserted.slice(cursor, cursor + count);
10276
- cursor += count;
10277
10972
  symbolsIndexed += count;
10278
10973
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
10279
10974
  filesIndexed++;
10280
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
10281
- const refsByLine = /* @__PURE__ */ new Map();
10282
- for (let i = 0; i < symbolsWithIds.length; i++) {
10283
- const sym = symbolsWithIds[i];
10284
- let arr = refsByLine.get(sym.line);
10285
- if (!arr) {
10286
- arr = [];
10287
- refsByLine.set(sym.line, arr);
10288
- }
10289
- arr.push(i);
10290
- }
10291
- for (const ref of entry.refs) {
10292
- const indices = refsByLine.get(ref.line);
10293
- if (indices && indices.length > 0) {
10294
- const idx = indices.shift();
10295
- ref.fromId = symbolsWithIds[idx].id;
10296
- }
10297
- }
10298
- }
10299
10975
  }
10300
10976
  } catch (err) {
10301
10977
  const message = err instanceof Error ? err.message : String(err);
@@ -10309,23 +10985,7 @@ async function runIndexerWithStore(store, opts) {
10309
10985
  langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
10310
10986
  filesIndexed++;
10311
10987
  if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
10312
- const refsByLine = /* @__PURE__ */ new Map();
10313
- for (const sym of symbolsWithIds) {
10314
- let arr = refsByLine.get(sym.line);
10315
- if (!arr) {
10316
- arr = [];
10317
- refsByLine.set(sym.line, arr);
10318
- }
10319
- arr.push(sym);
10320
- }
10321
- const fallbackBatch = [];
10322
- for (const ref of entry.refs) {
10323
- const syms = refsByLine.get(ref.line);
10324
- if (syms && syms.length > 0) {
10325
- const sym = syms.shift();
10326
- fallbackBatch.push({ ...ref, fromId: sym.id });
10327
- }
10328
- }
10988
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
10329
10989
  if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
10330
10990
  }
10331
10991
  store.upsertFile({
@@ -10336,19 +10996,23 @@ async function runIndexerWithStore(store, opts) {
10336
10996
  lastIndexed: Date.now()
10337
10997
  });
10338
10998
  } catch (innerErr) {
10339
- errors.push(`fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`);
10999
+ errors.push(
11000
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
11001
+ );
10340
11002
  }
10341
11003
  }
10342
11004
  }
10343
11005
  }
10344
11006
  }
10345
- if (discoveredFiles) {
11007
+ if (discoveredFiles && discoveryComplete) {
10346
11008
  for (const [file_] of existingMeta) {
10347
11009
  if (!discoveredFiles.has(file_)) {
10348
11010
  store.deleteFile(file_);
10349
11011
  }
10350
11012
  }
10351
11013
  }
11014
+ store.resolveRefs();
11015
+ store.setMetadata("relation_graph_version", relationGraphVersion);
10352
11016
  const durationMs = Date.now() - startMs;
10353
11017
  store.setLastIndexed(Date.now());
10354
11018
  return {
@@ -10408,11 +11072,35 @@ function statsService(args) {
10408
11072
  store.close();
10409
11073
  }
10410
11074
  }
11075
+ function packageGraphService(args) {
11076
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11077
+ try {
11078
+ return store.getPackageGraph();
11079
+ } finally {
11080
+ store.close();
11081
+ }
11082
+ }
11083
+ function fileGraphService(args) {
11084
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11085
+ try {
11086
+ return store.getFileGraph(args.packageFilter);
11087
+ } finally {
11088
+ store.close();
11089
+ }
11090
+ }
11091
+ function symbolGraphService(args) {
11092
+ const store = new IndexStore(args.projectRoot, { indexDir: args.indexDir });
11093
+ try {
11094
+ return store.getSymbolGraph(args.fileFilter);
11095
+ } finally {
11096
+ store.close();
11097
+ }
11098
+ }
10411
11099
 
10412
11100
  // src/codebase-index/background-indexer.ts
10413
11101
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
10414
11102
  var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
10415
- var DEFAULT_QUERY_TIMEOUT_MS = 8e3;
11103
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
10416
11104
  var _ready = false;
10417
11105
  var _indexing = false;
10418
11106
  var _currentFile = 0;
@@ -10490,14 +11178,22 @@ function ensureWorker() {
10490
11178
  if (!entry) return;
10491
11179
  pending.delete(msg.id);
10492
11180
  if (msg.ok) entry.resolve(msg.result);
10493
- else entry.reject(new Error(msg.error));
11181
+ else {
11182
+ const error = msg.errorName === "LockError" ? new LockError(msg.error) : new Error(msg.error);
11183
+ if (msg.errorName && msg.errorName !== "Error") {
11184
+ error.name = msg.errorName;
11185
+ }
11186
+ entry.reject(error);
11187
+ }
10494
11188
  });
10495
11189
  w.on("error", (err) => {
11190
+ if (worker !== w) return;
10496
11191
  worker = null;
10497
11192
  failAllPending(err);
10498
11193
  });
10499
11194
  w.on("exit", () => {
10500
- if (worker === w) worker = null;
11195
+ if (worker !== w) return;
11196
+ worker = null;
10501
11197
  failAllPending(new Error("codebase-index worker exited"));
10502
11198
  });
10503
11199
  worker = w;
@@ -10522,6 +11218,11 @@ function shutdownCodebaseIndexHost() {
10522
11218
  function callIndexOp(op, args, opts) {
10523
11219
  const w = ensureWorker();
10524
11220
  if (!w) return callInline(op, args, opts);
11221
+ if (opts.signal?.aborted) {
11222
+ return Promise.reject(
11223
+ opts.signal.reason instanceof Error ? opts.signal.reason : new Error("Indexing cancelled")
11224
+ );
11225
+ }
10525
11226
  return new Promise((resolve15, reject) => {
10526
11227
  const id = nextRpcId++;
10527
11228
  const timer = setTimeout(() => {
@@ -10536,8 +11237,7 @@ function callIndexOp(op, args, opts) {
10536
11237
  const onAbort = () => {
10537
11238
  w.postMessage({ type: "cancel", id });
10538
11239
  };
10539
- if (opts.signal?.aborted) onAbort();
10540
- else opts.signal?.addEventListener("abort", onAbort, { once: true });
11240
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
10541
11241
  const cleanup = () => {
10542
11242
  clearTimeout(timer);
10543
11243
  opts.signal?.removeEventListener("abort", onAbort);
@@ -10725,7 +11425,7 @@ var codebaseIndexTool = {
10725
11425
  category: "Project",
10726
11426
  icon: "index",
10727
11427
  description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
10728
- usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
11428
+ usageHint: "CREATE OR REFRESH THE SEARCH INDEX:\n\n- When `codebase-stats` or `codebase-search` reports no persisted index, call this without arguments, then retry the search.\n- Normal and first-time usage: call without arguments for an incremental build.\n- Use `force: true` only for a corrupt/stale index or an explicitly requested clean rebuild.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
10729
11429
  permission: "confirm",
10730
11430
  mutating: true,
10731
11431
  capabilities: ["fs.write.outside-project"],
@@ -10781,12 +11481,14 @@ var codebaseSearchTool = {
10781
11481
  name: "codebase-search",
10782
11482
  category: "Project",
10783
11483
  icon: "index",
10784
- 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).",
10785
- usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
11484
+ description: "Search code symbols using a fast SQLite+BM25 index, with optional LSP fallback. Prefer this before broad `tree`, `glob`, or `grep` exploration when finding code by name or concept. Set `preferLsp: true` for live precision when the LSP plugin is active (supersedes codebase-lsp-search).",
11485
+ usageHint: "FIRST CHOICE FOR INDEXABLE CODE UNDERSTANDING:\n\n- Call before broad `tree`, `glob`, or `grep` exploration when locating symbols, concepts, definitions, or candidate modules.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\n- If `indexStatus` reports no persisted data, run `codebase-index` and retry.\nUse `grep` afterwards for exact text, regexes, unsupported content, or concrete usage sites.",
10786
11486
  permission: "auto",
10787
11487
  mutating: false,
10788
11488
  capabilities: ["fs.read"],
10789
- timeoutMs: 1e4,
11489
+ // The index host has its own 30s read watchdog. Leave enough headroom for
11490
+ // worker teardown and structured timeout reporting.
11491
+ timeoutMs: 35e3,
10790
11492
  inputSchema: {
10791
11493
  type: "object",
10792
11494
  properties: {
@@ -10796,11 +11498,34 @@ var codebaseSearchTool = {
10796
11498
  },
10797
11499
  kind: {
10798
11500
  type: "string",
10799
- description: "Filter by symbol kind: class, function, interface, method, const, let, var, property, type, enum"
11501
+ enum: [
11502
+ "class",
11503
+ "interface",
11504
+ "enum",
11505
+ "type",
11506
+ "function",
11507
+ "method",
11508
+ "var",
11509
+ "const",
11510
+ "let",
11511
+ "property",
11512
+ "parameter",
11513
+ "namespace",
11514
+ "object",
11515
+ "literal",
11516
+ "schema",
11517
+ "struct",
11518
+ "trait",
11519
+ "impl",
11520
+ "static",
11521
+ "mod"
11522
+ ],
11523
+ description: "Filter by indexed symbol kind"
10800
11524
  },
10801
11525
  lang: {
10802
11526
  type: "string",
10803
- description: "Filter by language: ts, tsx, js, jsx"
11527
+ enum: ["ts", "tsx", "js", "jsx", "go", "py", "rs", "json", "yaml"],
11528
+ description: "Filter by indexed language"
10804
11529
  },
10805
11530
  lspKind: {
10806
11531
  type: "integer",
@@ -10825,20 +11550,12 @@ var codebaseSearchTool = {
10825
11550
  },
10826
11551
  async execute(input, ctx, execOpts) {
10827
11552
  const state = getIndexState();
10828
- if (!state.ready) {
11553
+ if (state.indexing && !state.ready) {
10829
11554
  return {
10830
11555
  results: [],
10831
11556
  total: 0,
10832
11557
  query: input.query,
10833
- 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."
10834
- };
10835
- }
10836
- if (state.indexing) {
10837
- return {
10838
- results: [],
10839
- total: 0,
10840
- query: input.query,
10841
- indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files). Results may be incomplete.`
11558
+ indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
10842
11559
  };
10843
11560
  }
10844
11561
  if (state.lastError) {
@@ -10851,21 +11568,37 @@ var codebaseSearchTool = {
10851
11568
  indexStatus: `Index build failed: ${state.lastError}. ${retryHint}`
10852
11569
  };
10853
11570
  }
10854
- const limit = Math.min(input.limit ?? 20, 100);
11571
+ const limit = Math.max(1, Math.min(Math.trunc(input.limit ?? 20), 100));
10855
11572
  const { results, total } = await searchCodebaseIndex(
10856
11573
  {
10857
11574
  projectRoot: ctx.projectRoot,
10858
11575
  indexDir: codebaseIndexDirOverride(ctx),
10859
11576
  query: input.query,
10860
- kind: input.kind,
10861
- lang: input.lang,
11577
+ kind: input.kind?.toLowerCase(),
11578
+ lang: input.lang?.toLowerCase(),
10862
11579
  file: input.file,
10863
11580
  lspKind: input.lspKind,
10864
11581
  limit
10865
11582
  },
10866
11583
  { signal: execOpts?.signal }
10867
11584
  );
10868
- return { results, total, query: input.query };
11585
+ let hasPersistedIndex = state.ready || total > 0;
11586
+ if (!hasPersistedIndex) {
11587
+ try {
11588
+ const stats = await codebaseIndexStats(
11589
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11590
+ { signal: execOpts?.signal }
11591
+ );
11592
+ hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11593
+ } catch {
11594
+ }
11595
+ }
11596
+ return {
11597
+ results,
11598
+ total,
11599
+ query: input.query,
11600
+ ...hasPersistedIndex ? {} : { indexStatus: "No persisted index data found. Run codebase-index to build it." }
11601
+ };
10869
11602
  }
10870
11603
  };
10871
11604
 
@@ -10874,12 +11607,15 @@ var codebaseStatsTool = {
10874
11607
  name: "codebase-stats",
10875
11608
  category: "Project",
10876
11609
  icon: "index",
10877
- 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.",
10878
- usageHint: "CALL BEFORE HEAVY CODEBASE-SEARCH WORK:\n\n- Use to see if the index is up-to-date or needs a refresh.\n- No arguments required.\n- Helps avoid wasting tokens on searches against a stale index.\nLightweight and safe to call frequently.",
11610
+ description: "Check whether a persisted codebase index exists and report its health and statistics (symbols, files, language/kind breakdown, size, last update).",
11611
+ usageHint: "CHECK ONCE BEFORE BROAD CODE EXPLORATION:\n\n- Call before broad `tree`, `glob`, or `grep` exploration to see whether index-backed search is available.\n- If it reports no persisted data, run `codebase-index`, then use `codebase-search`.\n- No arguments required.\nLightweight and safe to call frequently.",
10879
11612
  permission: "auto",
10880
11613
  mutating: false,
10881
11614
  capabilities: ["fs.read"],
10882
- timeoutMs: 5e3,
11615
+ // The index host has its own 30s read watchdog. Keep the outer tool budget
11616
+ // longer so the host can return a structured timeout instead of being
11617
+ // pre-empted by the generic tool executor.
11618
+ timeoutMs: 35e3,
10883
11619
  inputSchema: {
10884
11620
  type: "object",
10885
11621
  properties: {},
@@ -10887,7 +11623,8 @@ var codebaseStatsTool = {
10887
11623
  },
10888
11624
  async execute(_input, ctx, execOpts) {
10889
11625
  const idxState = getIndexState();
10890
- if (!idxState.ready) {
11626
+ const indexPath = resolveIndexDir(ctx.projectRoot, codebaseIndexDirOverride(ctx));
11627
+ if (idxState.indexing) {
10891
11628
  return {
10892
11629
  totalSymbols: 0,
10893
11630
  totalFiles: 0,
@@ -10895,22 +11632,47 @@ var codebaseStatsTool = {
10895
11632
  byKind: {},
10896
11633
  lastIndexed: null,
10897
11634
  sizeBytes: 0,
10898
- indexPath: "",
11635
+ indexPath,
10899
11636
  version: SCHEMA_VERSION,
10900
- indexStatus: idxState.indexing ? `Indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files).` : "Index not yet built."
11637
+ statsAvailable: false,
11638
+ indexing: {
11639
+ currentFile: idxState.currentFile,
11640
+ totalFiles: idxState.totalFiles
11641
+ },
11642
+ indexStatus: idxState.ready ? `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). The persisted snapshot remains available to codebase-search; retry stats after refresh.` : `Startup indexing in progress (${idxState.currentFile}/${idxState.totalFiles} files). Do not start another index; retry stats or codebase-search after completion.`
10901
11643
  };
10902
11644
  }
10903
- const stats = await codebaseIndexStats(
10904
- { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
10905
- { signal: execOpts?.signal }
10906
- );
10907
- if (idxState.indexing) {
11645
+ let stats;
11646
+ try {
11647
+ stats = await codebaseIndexStats(
11648
+ { projectRoot: ctx.projectRoot, indexDir: codebaseIndexDirOverride(ctx) },
11649
+ { signal: execOpts?.signal }
11650
+ );
11651
+ } catch (err) {
11652
+ if (!(err instanceof IndexTimeoutError) && err?.name !== "IndexTimeoutError") {
11653
+ throw err;
11654
+ }
10908
11655
  return {
10909
- ...stats,
10910
- indexStatus: `Index refresh in progress (${idxState.currentFile}/${idxState.totalFiles} files). Stats may be incomplete.`
11656
+ totalSymbols: 0,
11657
+ totalFiles: 0,
11658
+ byLang: {},
11659
+ byKind: {},
11660
+ lastIndexed: null,
11661
+ sizeBytes: 0,
11662
+ indexPath,
11663
+ version: SCHEMA_VERSION,
11664
+ statsAvailable: false,
11665
+ indexStatus: "Index statistics timed out. Do not repeatedly retry stats; try codebase-search directly or use the appropriate grep/glob/tree fallback."
10911
11666
  };
10912
11667
  }
10913
11668
  const circuit = idxState.circuit;
11669
+ const hasPersistedIndex = stats.totalFiles > 0 || stats.lastIndexed !== null;
11670
+ let indexStatus;
11671
+ if (circuit.state === "open") {
11672
+ indexStatus = `${hasPersistedIndex ? "Indexing" : "No persisted index data found, and indexing"} is paused after repeated failures (last: ${circuit.lastFailure ?? "unknown"}); auto-retry in ${Math.ceil(circuit.cooldownRemainingMs / 1e3)}s, or run /codebase-reindex. ` + (hasPersistedIndex ? "Stats reflect the last successful build." : "Build the index after recovery.");
11673
+ } else if (!hasPersistedIndex) {
11674
+ indexStatus = "No persisted index data found. Run codebase-index to build it before broad code exploration.";
11675
+ }
10914
11676
  return {
10915
11677
  totalSymbols: stats.totalSymbols,
10916
11678
  totalFiles: stats.totalFiles,
@@ -10920,9 +11682,8 @@ var codebaseStatsTool = {
10920
11682
  sizeBytes: stats.sizeBytes,
10921
11683
  indexPath: stats.indexPath,
10922
11684
  version: stats.version,
10923
- ...circuit.state === "open" ? {
10924
- 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.`
10925
- } : {}
11685
+ statsAvailable: true,
11686
+ ...indexStatus ? { indexStatus } : {}
10926
11687
  };
10927
11688
  }
10928
11689
  };
@@ -11954,25 +12715,46 @@ function findLadderMatches(fileLf, oldLf) {
11954
12715
  const needleLines = oldLf.split("\n");
11955
12716
  if (needleLines.length > fileLines.length) return void 0;
11956
12717
  const offsets = lineOffsets(fileLines);
12718
+ const fileTrimEnd = fileLines.map((l) => l.trimEnd());
12719
+ const needleTrimEnd = needleLines.map((l) => l.trimEnd());
11957
12720
  const trailing = windowScan(
11958
- fileLines,
11959
- needleLines,
12721
+ fileTrimEnd,
12722
+ needleTrimEnd,
11960
12723
  offsets,
11961
- (a, b) => a.trimEnd() === b.trimEnd()
12724
+ fileLines,
12725
+ (a, b) => a === b
11962
12726
  );
11963
12727
  if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
11964
- const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
12728
+ const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);
11965
12729
  if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
11966
- const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
12730
+ const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());
12731
+ const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());
12732
+ const normalized = windowScan(
12733
+ fileTrimmed,
12734
+ needleTrimmed,
12735
+ offsets,
12736
+ fileLines,
12737
+ (a, b) => a === b
12738
+ );
11967
12739
  if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
11968
12740
  return fuzzyScan(fileLines, needleLines, offsets);
11969
12741
  }
11970
12742
  function lineAt(text, pos) {
12743
+ if (pos < 512) {
12744
+ let line2 = 1;
12745
+ for (let i = 0; i < pos; i++) {
12746
+ if (text.charCodeAt(i) === 10) line2++;
12747
+ }
12748
+ return line2;
12749
+ }
11971
12750
  let line = 1;
11972
- for (let i = 0; i < pos; i++) {
11973
- if (text.charCodeAt(i) === 10) line++;
12751
+ let search = 0;
12752
+ while (true) {
12753
+ const idx = text.indexOf("\n", search);
12754
+ if (idx === -1 || idx >= pos) return line;
12755
+ search = idx + 1;
12756
+ line++;
11974
12757
  }
11975
- return line;
11976
12758
  }
11977
12759
  function lineOffsets(lines) {
11978
12760
  const out = new Array(lines.length);
@@ -11991,19 +12773,19 @@ function windowToMatch(fileLines, offsets, start, windowLen) {
11991
12773
  startLine: start + 1
11992
12774
  };
11993
12775
  }
11994
- function windowScan(fileLines, needleLines, offsets, eq) {
12776
+ function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
11995
12777
  const n = needleLines.length;
11996
12778
  const out = [];
11997
- for (let i = 0; i + n <= fileLines.length; i++) {
12779
+ for (let i = 0; i + n <= comparisonLines.length; i++) {
11998
12780
  let all = true;
11999
12781
  for (let j = 0; j < n; j++) {
12000
- if (!eq(fileLines[i + j], needleLines[j])) {
12782
+ if (!eq(comparisonLines[i + j], needleLines[j])) {
12001
12783
  all = false;
12002
12784
  break;
12003
12785
  }
12004
12786
  }
12005
12787
  if (all) {
12006
- out.push(windowToMatch(fileLines, offsets, i, n));
12788
+ out.push(windowToMatch(originalLines, offsets, i, n));
12007
12789
  i += n - 1;
12008
12790
  }
12009
12791
  }
@@ -12325,8 +13107,10 @@ var editTool = {
12325
13107
  note: autoReadNote
12326
13108
  };
12327
13109
  }
13110
+ opts?.signal?.throwIfAborted();
12328
13111
  const ladder = findLadderMatches(fileLf, oldLf);
12329
13112
  if (!ladder) {
13113
+ opts?.signal?.throwIfAborted();
12330
13114
  const hint = nearestMatchHint(fileLf, oldLf);
12331
13115
  throw new ToolValidationError({
12332
13116
  message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
@@ -12394,6 +13178,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
12394
13178
  before: original,
12395
13179
  after: newFile
12396
13180
  });
13181
+ opts?.signal?.throwIfAborted();
12397
13182
  const diff = unifiedDiff(original, newFile, {
12398
13183
  fromFile: input.path,
12399
13184
  toFile: input.path
@@ -12417,6 +13202,11 @@ Compare this against your old_string and retry with the file's actual text.` : "
12417
13202
 
12418
13203
  // src/exec.ts
12419
13204
  import { spawn as spawn8 } from "node:child_process";
13205
+ import {
13206
+ emitProcessCompleted as emitProcessCompleted3,
13207
+ emitProcessOutput as emitProcessOutput3,
13208
+ emitProcessStarted as emitProcessStarted3
13209
+ } from "@wrongstack/core";
12420
13210
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
12421
13211
  init_output_spool();
12422
13212
  init_util();
@@ -13271,12 +14061,30 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13271
14061
  resolve15(result);
13272
14062
  };
13273
14063
  const startedAt = Date.now();
14064
+ let stdoutBytes = 0;
14065
+ let stderrBytes = 0;
14066
+ let telemetryCompleted = false;
14067
+ let timedOut = false;
13274
14068
  const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
13275
14069
  const resolved = resolveWin32Command(cmd);
13276
14070
  const needsShell = isWin3 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
13277
14071
  const shim = needsShell ? buildWin32CmdShimInvocation(resolved, args) : null;
13278
14072
  const spawnCmd = shim?.command ?? resolved;
13279
14073
  const spawnArgs = shim?.args ?? args;
14074
+ const emitCompletedOnce = (exitCode, pid2, signal2) => {
14075
+ if (telemetryCompleted) return;
14076
+ telemetryCompleted = true;
14077
+ emitProcessCompleted3({
14078
+ ...pid2 !== void 0 ? { pid: pid2 } : {},
14079
+ exitCode,
14080
+ ...signal2 ? { signal: signal2 } : {},
14081
+ durationMs: Date.now() - startedAt,
14082
+ stdoutBytes,
14083
+ stderrBytes,
14084
+ timedOut,
14085
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
14086
+ });
14087
+ };
13280
14088
  let child;
13281
14089
  try {
13282
14090
  child = spawn8(spawnCmd, spawnArgs, {
@@ -13289,6 +14097,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13289
14097
  });
13290
14098
  } catch (err) {
13291
14099
  spool.finalize();
14100
+ emitProcessStarted3({
14101
+ parentPid: process.pid,
14102
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
14103
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
14104
+ cwd,
14105
+ background: false,
14106
+ startedAt: new Date(startedAt).toISOString()
14107
+ });
14108
+ emitCompletedOnce(1, void 0);
13292
14109
  finish({
13293
14110
  command: cmd,
13294
14111
  args,
@@ -13301,6 +14118,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13301
14118
  });
13302
14119
  return;
13303
14120
  }
14121
+ emitProcessStarted3({
14122
+ ...child.pid !== void 0 ? { pid: child.pid } : {},
14123
+ parentPid: process.pid,
14124
+ command: redactCommand(`${cmd} ${args.join(" ")}`),
14125
+ args: redactCommand(args.join(" ")).split(" ").filter(Boolean),
14126
+ cwd,
14127
+ background: false,
14128
+ startedAt: new Date(startedAt).toISOString()
14129
+ });
13304
14130
  child.on("error", (err) => {
13305
14131
  const isAbort = err && err.code === "ABORT_ERR";
13306
14132
  const stderrText = isAbort ? `Aborted: ${err.message}` : err.message;
@@ -13309,6 +14135,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13309
14135
  if (typeof pid === "number") registry.unregister(pid);
13310
14136
  registry.afterCall(Date.now() - startedAt, true);
13311
14137
  spool.finalize();
14138
+ emitCompletedOnce(isAbort ? 124 : 1, child.pid, isAbort ? "ABORT" : void 0);
13312
14139
  finish({
13313
14140
  command: cmd,
13314
14141
  args,
@@ -13328,6 +14155,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13328
14155
  }
13329
14156
  const timer = setTimeout(() => {
13330
14157
  killed = true;
14158
+ timedOut = true;
13331
14159
  if (typeof pid === "number") registry.kill(pid);
13332
14160
  else child.kill("SIGTERM");
13333
14161
  }, timeout);
@@ -13342,11 +14170,15 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13342
14170
  }
13343
14171
  child.stdout?.on("data", (chunk) => {
13344
14172
  const text = chunk.toString();
14173
+ stdoutBytes += chunk.byteLength;
14174
+ emitProcessOutput3({ pid, stream: "stdout", chunk });
13345
14175
  if (stdout.length < MAX_OUTPUT2) stdout += text;
13346
14176
  spool.write(text);
13347
14177
  });
13348
14178
  child.stderr?.on("data", (chunk) => {
13349
14179
  const text = chunk.toString();
14180
+ stderrBytes += chunk.byteLength;
14181
+ emitProcessOutput3({ pid, stream: "stderr", chunk });
13350
14182
  if (stderr.length < MAX_OUTPUT2) stderr += text;
13351
14183
  spool.write(text);
13352
14184
  });
@@ -13356,6 +14188,7 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
13356
14188
  if (typeof pid === "number") registry.unregister(pid);
13357
14189
  const durationMs = Date.now() - startedAt;
13358
14190
  const exitCode = killed ? 124 : code ?? 1;
14191
+ emitCompletedOnce(exitCode, pid);
13359
14192
  registry.afterCall(durationMs, exitCode !== 0);
13360
14193
  const spooled = spool.finalize();
13361
14194
  finish({
@@ -13431,7 +14264,7 @@ function guardedLookup(hostname4, options, callback) {
13431
14264
  var pinnedAgent;
13432
14265
  function getPinnedDispatcher() {
13433
14266
  if (!pinnedAgent) {
13434
- pinnedAgent = new Agent({ connect: { lookup: guardedLookup } });
14267
+ pinnedAgent = new Agent({ allowH2: false, connect: { lookup: guardedLookup } });
13435
14268
  }
13436
14269
  return pinnedAgent;
13437
14270
  }
@@ -13533,7 +14366,7 @@ async function assertNotPrivate(hostname4) {
13533
14366
  }
13534
14367
  }
13535
14368
  } catch (err) {
13536
- if (err instanceof Error && err.message.startsWith("fetch:")) throw err;
14369
+ if (err instanceof ToolValidationError2) throw err;
13537
14370
  }
13538
14371
  }
13539
14372
  }
@@ -13881,7 +14714,7 @@ async function detectFixer(cwd) {
13881
14714
  init_util();
13882
14715
  import { spawn as spawn9 } from "node:child_process";
13883
14716
  import { statSync as statSync3 } from "node:fs";
13884
- import { dirname as dirname12, resolve as resolve11, sep as sep4 } from "node:path";
14717
+ import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
13885
14718
  import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
13886
14719
  var TIMEOUT_MS2 = 3e4;
13887
14720
  var MAX_OUTPUT3 = 1e5;
@@ -14026,7 +14859,7 @@ function validateWorktreeInput(input, projectRoot) {
14026
14859
  if ((input.worktreeAction === "add" || input.worktreeAction === "remove") && input.worktreePath) {
14027
14860
  const root = resolve11(projectRoot);
14028
14861
  const abs = resolve11(root, input.worktreePath);
14029
- if (abs !== root && !abs.startsWith(root + sep4)) {
14862
+ if (abs !== root && !abs.startsWith(root + sep5)) {
14030
14863
  return reject(`unsafe worktree path (escapes project root): ${input.worktreePath}`);
14031
14864
  }
14032
14865
  }
@@ -14189,8 +15022,8 @@ var WALK_CONCURRENCY = 16;
14189
15022
  var globTool = {
14190
15023
  name: "glob",
14191
15024
  category: "Filesystem",
14192
- description: "Find files matching a glob pattern. Fast way to discover relevant files before reading, grepping, or editing them.",
14193
- usageHint: "RECOMMENDED FOR SCOPING SEARCHES:\n\n- Use early to get a list of files you actually care about.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
15025
+ description: "Find files by path pattern. Use index-backed `codebase-search` first for code symbols or concepts when it is live.",
15026
+ usageHint: "PATH DISCOVERY AND SEARCH SCOPING:\n\n- When `codebase-search` is live, use it first for code concepts; use `glob` for filenames, path patterns, and non-indexed files.\n- Combine with `path` and `limit`.\n- Default ignores common build/dependency directories.\nMuch more efficient than shell `find` for most use cases inside the agent.",
14194
15027
  selection: {
14195
15028
  doNotUseWhen: "you need to search inside file contents.",
14196
15029
  useInstead: ["grep"]
@@ -14378,8 +15211,8 @@ var NATIVE_MAX_FILE_BYTES = 1e6;
14378
15211
  var grepTool = {
14379
15212
  name: "grep",
14380
15213
  category: "Search",
14381
- 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.",
14382
- usageHint: 'POWERFUL CODE SEARCH TOOL:\n\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
15214
+ description: "Search exact file contents using a regular expression. Use index-backed `codebase-search` first for symbol or concept discovery when it is live. Prefers ripgrep for speed and features when available.",
15215
+ usageHint: 'EXACT-TEXT AND REGEX SEARCH:\n\n- When `codebase-search` is live, use it first for symbols and concepts; use `grep` for exact text, regexes, unsupported content, and concrete usage sites.\n- `pattern` is a regular expression.\n- Use `output_mode: "content"` (default) to get matching lines with context.\n- Use `"files_with_matches"` when you only need the list of files.\n- Use `"count"` for quick statistics.\n- `glob` and `path` let you narrow the search scope significantly.\n- Always prefer this over `bash grep` when searching code.',
14383
15216
  selection: {
14384
15217
  doNotUseWhen: "you only need to locate files by name or path pattern.",
14385
15218
  useInstead: ["glob"]
@@ -15401,7 +16234,8 @@ function toYaml(data, indent = 0) {
15401
16234
  }
15402
16235
 
15403
16236
  // src/kanban.ts
15404
- import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core";
16237
+ import { randomUUID } from "node:crypto";
16238
+ import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
15405
16239
  import {
15406
16240
  addCheckToTask,
15407
16241
  addColumn,
@@ -15413,18 +16247,19 @@ import {
15413
16247
  assignTask,
15414
16248
  claimReadyTask,
15415
16249
  copyTaskToBoard,
15416
- createBoard,
16250
+ createBoard as createBoard2,
16251
+ createBoardFromTaskGraph,
15417
16252
  duplicateBoard,
15418
16253
  exportBoardAsMarkdown,
15419
16254
  exportBoardToTaskGraph,
15420
16255
  generateBoardFromDescription,
15421
- getBoard,
16256
+ getBoard as getBoard2,
15422
16257
  getKanbanOrchestrationSnapshot,
15423
16258
  getKanbanQueueHealth,
15424
16259
  getTask,
15425
16260
  getTaskChain,
15426
16261
  heartbeatTaskAssignment,
15427
- listBoards,
16262
+ listBoards as listBoards2,
15428
16263
  listKanbanEvents,
15429
16264
  listReadyTasks,
15430
16265
  mergeTasks,
@@ -15432,26 +16267,637 @@ import {
15432
16267
  parseLinesIntoTasks,
15433
16268
  recoverStaleTaskAssignments,
15434
16269
  releaseTaskClaim,
15435
- removeBoard,
16270
+ removeBoard as removeBoard2,
15436
16271
  removeColumn,
15437
16272
  removeTask,
15438
16273
  searchKanban,
15439
16274
  setTaskChain,
15440
16275
  splitTask,
15441
- syncBoardFromTaskGraph,
16276
+ syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
16277
+ transitionTask,
15442
16278
  transferTaskToBoard,
15443
- updateBoard,
16279
+ touchKanbanPresence as touchKanbanPresence2,
16280
+ updateBoard as updateBoard2,
15444
16281
  updateCheckOnTask,
15445
16282
  updateColumn,
15446
16283
  updateGoalMetricOnTask,
15447
16284
  updateTask,
15448
16285
  updateTaskAssignment
15449
16286
  } from "@wrongstack/kanban";
16287
+
16288
+ // src/session-kanban.ts
16289
+ import { watch } from "node:fs";
16290
+ import { basename as basename9, dirname as dirname13 } from "node:path";
16291
+ import {
16292
+ deserializeTaskGraph,
16293
+ GlobalMailbox,
16294
+ loadPlan,
16295
+ loadTasks,
16296
+ mutatePlan,
16297
+ mutateTasks
16298
+ } from "@wrongstack/core";
16299
+ import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
16300
+ import {
16301
+ createBoard,
16302
+ getBoard,
16303
+ getKanbanDir,
16304
+ listBoards,
16305
+ removeBoard,
16306
+ syncBoardFromTaskGraph,
16307
+ touchKanbanPresence,
16308
+ updateBoard
16309
+ } from "@wrongstack/kanban";
16310
+ var SESSION_BOARD_TAG = "session-work";
16311
+ var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
16312
+ var SESSION_KANBAN_COLUMNS = [
16313
+ { id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
16314
+ { id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
16315
+ { id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
16316
+ { id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
16317
+ ];
16318
+ var boardQueue = /* @__PURE__ */ new Map();
16319
+ var boardEnsures = /* @__PURE__ */ new Map();
16320
+ var bindings = /* @__PURE__ */ new WeakMap();
16321
+ var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
16322
+ var activeSessionBoards = /* @__PURE__ */ new Map();
16323
+ function boardKey(projectRoot, sessionId) {
16324
+ return `${projectRoot}\0${sessionId}`;
16325
+ }
16326
+ function sessionTag(sessionId) {
16327
+ return `session:${sessionId}`;
16328
+ }
16329
+ function sessionBoardTitle(sessionId) {
16330
+ const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
16331
+ return `Session ${leaf.slice(0, 12)}`;
16332
+ }
16333
+ function sessionBoardTags(sessionId) {
16334
+ return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
16335
+ }
16336
+ function sessionIdFromTags(tags) {
16337
+ const tag = tags?.find((candidate) => candidate.startsWith("session:"));
16338
+ return tag?.slice("session:".length) || null;
16339
+ }
16340
+ function isOwnedSessionBoard(tags) {
16341
+ return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));
16342
+ }
16343
+ function retainActiveSessionBoard(projectRoot, sessionId) {
16344
+ const key = boardKey(projectRoot, sessionId);
16345
+ activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);
16346
+ }
16347
+ function releaseActiveSessionBoard(projectRoot, sessionId) {
16348
+ const key = boardKey(projectRoot, sessionId);
16349
+ const remaining = (activeSessionBoards.get(key) ?? 0) - 1;
16350
+ if (remaining > 0) activeSessionBoards.set(key, remaining);
16351
+ else activeSessionBoards.delete(key);
16352
+ }
16353
+ function isSessionBoardActive(projectRoot, sessionId) {
16354
+ return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;
16355
+ }
16356
+ function sameColumns(columns) {
16357
+ return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
16358
+ }
16359
+ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
16360
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
16361
+ const key = boardKey(projectRoot, sessionId);
16362
+ const inFlight = boardEnsures.get(key);
16363
+ if (inFlight) return inFlight;
16364
+ const promise = (async () => {
16365
+ const summary = (await listBoards(projectRoot)).find(
16366
+ (board2) => board2.tags?.includes(sessionTag(sessionId))
16367
+ );
16368
+ let board = summary ? await getBoard(projectRoot, summary.id) : null;
16369
+ if (!board) {
16370
+ return createBoard(projectRoot, {
16371
+ title: sessionBoardTitle(sessionId),
16372
+ description: "Live session work: todos, tasks, and plan items.",
16373
+ tags: sessionBoardTags(sessionId),
16374
+ columns: SESSION_KANBAN_COLUMNS,
16375
+ generatedBy: `session-kanban:${sessionId}`
16376
+ });
16377
+ }
16378
+ if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
16379
+ board = await updateBoard(projectRoot, board.id, {
16380
+ title: sessionBoardTitle(sessionId),
16381
+ description: "Live session work: todos, tasks, and plan items.",
16382
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
16383
+ columns: SESSION_KANBAN_COLUMNS
16384
+ }) ?? board;
16385
+ }
16386
+ return board;
16387
+ })();
16388
+ boardEnsures.set(key, promise);
16389
+ try {
16390
+ return await promise;
16391
+ } finally {
16392
+ boardEnsures.delete(key);
16393
+ }
16394
+ }
16395
+ function enqueueBoardWork(projectRoot, sessionId, work) {
16396
+ const key = boardKey(projectRoot, sessionId);
16397
+ const previous = boardQueue.get(key) ?? Promise.resolve();
16398
+ const result = previous.catch(() => void 0).then(work);
16399
+ const tail = result.then(
16400
+ () => void 0,
16401
+ () => void 0
16402
+ );
16403
+ boardQueue.set(key, tail);
16404
+ void tail.then(() => {
16405
+ if (boardQueue.get(key) === tail) boardQueue.delete(key);
16406
+ });
16407
+ return result;
16408
+ }
16409
+ async function removeEmptySessionBoard(projectRoot, boardId, sessionId) {
16410
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
16411
+ if (isSessionBoardActive(projectRoot, sessionId)) return null;
16412
+ const board = await getBoard(projectRoot, boardId);
16413
+ if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;
16414
+ if (sessionIdFromTags(board.tags) !== sessionId) return null;
16415
+ return await removeBoard(projectRoot, board.id) ? board.id : null;
16416
+ });
16417
+ }
16418
+ async function cleanupSessionKanbanBoardIfEmpty(projectRoot, sessionId) {
16419
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
16420
+ if (isSessionBoardActive(projectRoot, sessionId)) return [];
16421
+ const candidates = (await listBoards(projectRoot)).filter(
16422
+ (board) => board.taskCount === 0 && isOwnedSessionBoard(board.tags) && sessionIdFromTags(board.tags) === sessionId
16423
+ );
16424
+ const removed = await Promise.all(
16425
+ candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId))
16426
+ );
16427
+ return removed.filter((boardId) => Boolean(boardId));
16428
+ }
16429
+ async function cleanupEmptySessionKanbanBoards(projectRoot, activeSessionId = "") {
16430
+ if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === "0") return [];
16431
+ const candidates = (await listBoards(projectRoot)).flatMap((board) => {
16432
+ const ownerSessionId = sessionIdFromTags(board.tags);
16433
+ return board.taskCount === 0 && isOwnedSessionBoard(board.tags) && ownerSessionId && ownerSessionId !== activeSessionId && !isSessionBoardActive(projectRoot, ownerSessionId) ? [{ boardId: board.id, sessionId: ownerSessionId }] : [];
16434
+ });
16435
+ const removed = await Promise.all(
16436
+ candidates.map(
16437
+ ({ boardId, sessionId }) => removeEmptySessionBoard(projectRoot, boardId, sessionId)
16438
+ )
16439
+ );
16440
+ return removed.filter((boardId) => Boolean(boardId));
16441
+ }
16442
+ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
16443
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
16444
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
16445
+ const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
16446
+ if (!board) return null;
16447
+ const result = await syncBoardFromTaskGraph(
16448
+ projectRoot,
16449
+ board.id,
16450
+ deserializeTaskGraph(graph),
16451
+ {
16452
+ sourceSystem,
16453
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
16454
+ archiveMissingTasks: true,
16455
+ includeCompletedTasks: true
16456
+ }
16457
+ );
16458
+ return result?.board ?? null;
16459
+ });
16460
+ }
16461
+ function todoListToSerializedGraph(todos, sessionId) {
16462
+ const nodes = todos.map((todo, index) => ({
16463
+ id: todo.id,
16464
+ title: todo.content,
16465
+ description: todo.activeForm ?? "",
16466
+ type: "chore",
16467
+ priority: "medium",
16468
+ status: todo.status,
16469
+ createdAt: index,
16470
+ updatedAt: index
16471
+ }));
16472
+ return {
16473
+ id: `todo:${sessionId}`,
16474
+ specId: `todo:${sessionId}`,
16475
+ title: "Session todos",
16476
+ nodes,
16477
+ edges: [],
16478
+ rootNodes: nodes.map((node) => node.id),
16479
+ createdAt: 0,
16480
+ updatedAt: 0
16481
+ };
16482
+ }
16483
+ function taskFileToSerializedGraph(tasks, sessionId) {
16484
+ const ids = new Set(tasks.map((task) => task.id));
16485
+ const nodes = tasks.map((task, index) => ({
16486
+ id: task.id,
16487
+ title: task.title,
16488
+ description: task.description ?? "",
16489
+ type: task.type,
16490
+ priority: task.priority,
16491
+ status: task.status,
16492
+ ...task.assignee ? { assignee: task.assignee } : {},
16493
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
16494
+ createdAt: index,
16495
+ updatedAt: index
16496
+ }));
16497
+ const edges = tasks.flatMap(
16498
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
16499
+ id: `${dependency}->${task.id}`,
16500
+ from: dependency,
16501
+ to: task.id,
16502
+ type: "depends_on"
16503
+ }))
16504
+ );
16505
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
16506
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
16507
+ return {
16508
+ // Keep the historical graph id so existing mirrored task cards are reused.
16509
+ id: `session:${sessionId}`,
16510
+ specId: `session:${sessionId}`,
16511
+ title: "Session tasks",
16512
+ nodes,
16513
+ edges,
16514
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
16515
+ createdAt: 0,
16516
+ updatedAt: 0
16517
+ };
16518
+ }
16519
+ var PLAN_STATUS_TO_TASK = {
16520
+ open: "pending",
16521
+ in_progress: "in_progress",
16522
+ done: "completed"
16523
+ };
16524
+ function planFileToSerializedGraph(items, sessionId) {
16525
+ const nodes = items.map((item, index) => ({
16526
+ id: item.id,
16527
+ title: item.title,
16528
+ description: item.details ?? "",
16529
+ type: "chore",
16530
+ priority: "medium",
16531
+ status: PLAN_STATUS_TO_TASK[item.status],
16532
+ createdAt: index,
16533
+ updatedAt: index
16534
+ }));
16535
+ return {
16536
+ id: `plan:${sessionId}`,
16537
+ specId: `plan:${sessionId}`,
16538
+ title: "Session plan",
16539
+ nodes,
16540
+ edges: [],
16541
+ rootNodes: nodes.map((node) => node.id),
16542
+ createdAt: 0,
16543
+ updatedAt: 0
16544
+ };
16545
+ }
16546
+ function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
16547
+ return projectGraph(
16548
+ projectRoot,
16549
+ sessionId,
16550
+ todoListToSerializedGraph(todos, sessionId),
16551
+ "session-todo"
16552
+ );
16553
+ }
16554
+ function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
16555
+ return projectGraph(
16556
+ projectRoot,
16557
+ sessionId,
16558
+ taskFileToSerializedGraph(tasks, sessionId),
16559
+ "session-task"
16560
+ );
16561
+ }
16562
+ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
16563
+ return projectGraph(
16564
+ projectRoot,
16565
+ sessionId,
16566
+ planFileToSerializedGraph(items, sessionId),
16567
+ "session-plan"
16568
+ );
16569
+ }
16570
+ function fireAndForget(work) {
16571
+ void work.catch(() => {
16572
+ });
16573
+ }
16574
+ function broadcastTodoUpdate(context, todos) {
16575
+ const sessionId = context.session?.id ?? "";
16576
+ if (!context.agentId || !sessionId) return;
16577
+ const projectDir = resolveWstackPaths3({ projectRoot: context.projectRoot }).projectDir;
16578
+ const mailbox = new GlobalMailbox(projectDir);
16579
+ void mailbox.send({
16580
+ from: context.agentId,
16581
+ to: "*",
16582
+ type: "status",
16583
+ subject: `Kanban todo list updated (${todos.length} item${todos.length === 1 ? "" : "s"})`,
16584
+ body: JSON.stringify({
16585
+ kind: "kanban.todos.updated",
16586
+ sessionId,
16587
+ revision: context.state.revision,
16588
+ todos
16589
+ }),
16590
+ priority: "normal",
16591
+ senderSessionId: sessionId,
16592
+ ttlMs: 6 * 60 * 60 * 1e3
16593
+ }).catch(() => {
16594
+ });
16595
+ }
16596
+ function notifyTodoUpdate(context, todos) {
16597
+ const summary = todos.length ? todos.map((todo) => `- [${todo.status}] ${todo.content} (${todo.id})`).join("\n") : "- No active todos remain.";
16598
+ const text = `[KANBAN TODO UPDATE]
16599
+ Another Kanban agent reassessed the shared board. The canonical todo list is now:
16600
+ ${summary}
16601
+ Reassess your current plan before continuing; do not rely on the initial todo snapshot.`;
16602
+ const state = context.state;
16603
+ if (typeof state.appendBlockToLastUserMessage === "function") {
16604
+ if (state.appendBlockToLastUserMessage({ type: "text", text })) return;
16605
+ }
16606
+ if (typeof state.appendMessage === "function") {
16607
+ state.appendMessage({ role: "user", content: [{ type: "text", text }] });
16608
+ }
16609
+ }
16610
+ function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
16611
+ fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));
16612
+ }
16613
+ function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
16614
+ fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));
16615
+ }
16616
+ function mirrorSessionPlanToKanban(projectRoot, items, sessionId) {
16617
+ fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));
16618
+ }
16619
+ function attachSessionKanbanMirror(context) {
16620
+ const existing = bindings.get(context);
16621
+ if (existing) return existing;
16622
+ const attachedProjectRoot = context.projectRoot ?? "";
16623
+ let registeredSessionId = "";
16624
+ const syncActiveSessionRegistration = () => {
16625
+ if (!attachedProjectRoot) return;
16626
+ const currentSessionId = context.session?.id ?? "";
16627
+ if (currentSessionId === registeredSessionId) return;
16628
+ if (registeredSessionId) {
16629
+ releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
16630
+ fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
16631
+ }
16632
+ registeredSessionId = currentSessionId;
16633
+ if (registeredSessionId) {
16634
+ retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);
16635
+ }
16636
+ };
16637
+ syncActiveSessionRegistration();
16638
+ let watcher = null;
16639
+ let watchedDir = "";
16640
+ let timer = null;
16641
+ let boardWatcher = null;
16642
+ let watchedBoardId = "";
16643
+ let boardTimer = null;
16644
+ let presenceTimer = null;
16645
+ const sessionId = () => context.session?.id ?? "";
16646
+ const refreshFiles = async () => {
16647
+ const id = sessionId();
16648
+ if (!id) return;
16649
+ const planPath = context.meta["plan.path"];
16650
+ if (typeof planPath === "string" && planPath) {
16651
+ const plan = await loadPlan(planPath);
16652
+ if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
16653
+ }
16654
+ const taskPath = context.meta["task.path"];
16655
+ if (typeof taskPath === "string" && taskPath) {
16656
+ const tasks = await loadTasks(taskPath);
16657
+ if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
16658
+ }
16659
+ };
16660
+ const refreshBoard = async () => {
16661
+ if (!watchedBoardId) return;
16662
+ const board = await getBoard(context.projectRoot, watchedBoardId);
16663
+ if (board) applySessionKanbanBoardToTodos(context, board);
16664
+ };
16665
+ const configureBoardWatcher = async () => {
16666
+ const id = sessionId();
16667
+ const board = id ? await ensureSessionKanbanBoard(context.projectRoot, id) : null;
16668
+ if (!board || board.id === watchedBoardId) return;
16669
+ boardWatcher?.close();
16670
+ boardWatcher = null;
16671
+ watchedBoardId = board.id;
16672
+ try {
16673
+ const boardFileName = `${board.id}.json`;
16674
+ boardWatcher = watch(
16675
+ getKanbanDir(context.projectRoot),
16676
+ { persistent: false },
16677
+ (_event, filename) => {
16678
+ if (filename?.toString() !== boardFileName) return;
16679
+ if (boardTimer) clearTimeout(boardTimer);
16680
+ boardTimer = setTimeout(() => fireAndForget(refreshBoard()), 60);
16681
+ }
16682
+ );
16683
+ const touchPresence = () => touchKanbanPresence(context.projectRoot, board.id, {
16684
+ sessionId: id,
16685
+ agentId: context.agentId,
16686
+ agentName: context.agentName
16687
+ });
16688
+ fireAndForget(touchPresence());
16689
+ if (presenceTimer) clearInterval(presenceTimer);
16690
+ presenceTimer = setInterval(() => fireAndForget(touchPresence()), 6e4);
16691
+ presenceTimer.unref?.();
16692
+ boardWatcher.on("error", () => {
16693
+ boardWatcher?.close();
16694
+ boardWatcher = null;
16695
+ watchedBoardId = "";
16696
+ });
16697
+ } catch {
16698
+ boardWatcher = null;
16699
+ watchedBoardId = "";
16700
+ }
16701
+ };
16702
+ const configureWatcher = () => {
16703
+ const planPath = context.meta["plan.path"];
16704
+ const taskPath = context.meta["task.path"];
16705
+ const candidate = typeof planPath === "string" && planPath ? dirname13(planPath) : typeof taskPath === "string" && taskPath ? dirname13(taskPath) : "";
16706
+ if (!candidate || candidate === watchedDir) return;
16707
+ watcher?.close();
16708
+ watcher = null;
16709
+ watchedDir = candidate;
16710
+ try {
16711
+ watcher = watch(candidate, { persistent: false }, (_event, filename) => {
16712
+ const name = filename?.toString();
16713
+ const currentPlanPath = context.meta["plan.path"];
16714
+ const currentTaskPath = context.meta["task.path"];
16715
+ const planName = typeof currentPlanPath === "string" ? basename9(currentPlanPath) : "";
16716
+ const taskName = typeof currentTaskPath === "string" ? basename9(currentTaskPath) : "";
16717
+ if (name && name !== planName && name !== taskName) return;
16718
+ if (timer) clearTimeout(timer);
16719
+ timer = setTimeout(() => fireAndForget(refreshFiles()), 60);
16720
+ });
16721
+ watcher.on("error", () => watcher?.close());
16722
+ } catch {
16723
+ watcher = null;
16724
+ watchedDir = "";
16725
+ }
16726
+ };
16727
+ const unsubscribe = context.state.onChange((change) => {
16728
+ if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
16729
+ mirrorSessionTodosToKanban(
16730
+ context.projectRoot,
16731
+ change.completedSnapshot ?? change.todos,
16732
+ sessionId()
16733
+ );
16734
+ return;
16735
+ }
16736
+ if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path")) {
16737
+ syncActiveSessionRegistration();
16738
+ configureWatcher();
16739
+ fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
16740
+ fireAndForget(configureBoardWatcher());
16741
+ fireAndForget(refreshFiles());
16742
+ }
16743
+ });
16744
+ configureWatcher();
16745
+ fireAndForget(configureBoardWatcher());
16746
+ const detach = () => {
16747
+ unsubscribe();
16748
+ if (timer) clearTimeout(timer);
16749
+ if (boardTimer) clearTimeout(boardTimer);
16750
+ if (presenceTimer) clearInterval(presenceTimer);
16751
+ watcher?.close();
16752
+ boardWatcher?.close();
16753
+ bindings.delete(context);
16754
+ if (attachedProjectRoot && registeredSessionId) {
16755
+ releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
16756
+ fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
16757
+ registeredSessionId = "";
16758
+ }
16759
+ };
16760
+ bindings.set(context, detach);
16761
+ return detach;
16762
+ }
16763
+ async function hydrateSessionKanban(context) {
16764
+ const id = context.session?.id ?? "";
16765
+ if (!id) return null;
16766
+ await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
16767
+ let board = await ensureSessionKanbanBoard(context.projectRoot, id);
16768
+ if (context.todos.length) {
16769
+ board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
16770
+ }
16771
+ const planPath = context.meta["plan.path"];
16772
+ if (typeof planPath === "string" && planPath) {
16773
+ const plan = await loadPlan(planPath);
16774
+ if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
16775
+ }
16776
+ const taskPath = context.meta["task.path"];
16777
+ if (typeof taskPath === "string" && taskPath) {
16778
+ const tasks = await loadTasks(taskPath);
16779
+ if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
16780
+ }
16781
+ return board;
16782
+ }
16783
+ function sourceStatus(task) {
16784
+ if (task.status === "completed") return "completed";
16785
+ if (task.status === "in_progress") return "in_progress";
16786
+ if (task.status === "review") return "review";
16787
+ if (task.status === "blocked") return "blocked";
16788
+ if (task.status === "failed") return "failed";
16789
+ return "pending";
16790
+ }
16791
+ function todoStatus(task) {
16792
+ const status = sourceStatus(task);
16793
+ if (status === "completed") return "completed";
16794
+ if (status === "in_progress" || status === "review") return "in_progress";
16795
+ return "pending";
16796
+ }
16797
+ function sessionTodoFromTask(task) {
16798
+ return {
16799
+ id: task.origin?.taskId ?? task.id,
16800
+ content: task.title,
16801
+ status: todoStatus(task),
16802
+ ...task.description ? { activeForm: task.description } : {}
16803
+ };
16804
+ }
16805
+ function sameTodos(left, right) {
16806
+ return left.length === right.length && left.every((todo, index) => {
16807
+ const candidate = right[index];
16808
+ return candidate?.id === todo.id && candidate.content === todo.content && candidate.status === todo.status && candidate.activeForm === todo.activeForm && candidate.promotedFromPlan === todo.promotedFromPlan && candidate.promotedFromTask === todo.promotedFromTask;
16809
+ });
16810
+ }
16811
+ function applySessionKanbanBoardToTodos(context, board) {
16812
+ const sessionId = context.session?.id ?? "";
16813
+ if (!sessionId || sessionIdFromTags(board.tags) !== sessionId || !isOwnedSessionBoard(board.tags)) {
16814
+ return [...context.todos];
16815
+ }
16816
+ const projectedTodos = board.tasks.filter(
16817
+ (task) => task.status !== "archived" && (!task.origin || task.origin.system === "session-todo" || (task.origin.graphId ?? "").startsWith("todo:"))
16818
+ ).sort((left, right) => {
16819
+ const leftColumn = board.columns.find((column) => column.id === left.columnId)?.order ?? 0;
16820
+ const rightColumn = board.columns.find((column) => column.id === right.columnId)?.order ?? 0;
16821
+ return leftColumn - rightColumn || left.order - right.order || left.createdAt.localeCompare(right.createdAt);
16822
+ }).map(sessionTodoFromTask);
16823
+ const allCompleted = projectedTodos.length > 0 && projectedTodos.every((todo) => todo.status === "completed");
16824
+ const effectiveTodos = allCompleted ? [] : projectedTodos;
16825
+ if (sameTodos(context.todos, effectiveTodos)) return [...context.todos];
16826
+ suppressedTodoMirrors.add(context);
16827
+ try {
16828
+ context.state.replaceTodos(projectedTodos);
16829
+ } finally {
16830
+ suppressedTodoMirrors.delete(context);
16831
+ }
16832
+ notifyTodoUpdate(context, context.todos);
16833
+ broadcastTodoUpdate(context, context.todos);
16834
+ return [...context.todos];
16835
+ }
16836
+ async function applySessionKanbanTaskToSource(context, task, options = {}) {
16837
+ const originId = task.origin?.taskId;
16838
+ const graphId = task.origin?.graphId ?? "";
16839
+ if (!originId) return { source: null };
16840
+ if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
16841
+ const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
16842
+ (todo) => todo.id === originId ? {
16843
+ ...todo,
16844
+ content: task.title,
16845
+ status: sourceStatus(task) === "completed" ? "completed" : sourceStatus(task) === "in_progress" || sourceStatus(task) === "review" ? "in_progress" : "pending"
16846
+ } : todo
16847
+ );
16848
+ suppressedTodoMirrors.add(context);
16849
+ try {
16850
+ context.state.replaceTodos(next);
16851
+ } finally {
16852
+ suppressedTodoMirrors.delete(context);
16853
+ }
16854
+ return { source: "todo", todos: [...context.todos] };
16855
+ }
16856
+ const id = context.session?.id ?? "";
16857
+ if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
16858
+ const planPath = context.meta["plan.path"];
16859
+ if (typeof planPath !== "string" || !planPath) return { source: "plan" };
16860
+ const plan = await mutatePlan(planPath, id, (file) => ({
16861
+ ...file,
16862
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
16863
+ items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
16864
+ (item) => item.id === originId ? {
16865
+ ...item,
16866
+ title: task.title,
16867
+ details: task.description,
16868
+ status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
16869
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16870
+ } : item
16871
+ )
16872
+ }));
16873
+ return { source: "plan", plan };
16874
+ }
16875
+ if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
16876
+ const taskPath = context.meta["task.path"];
16877
+ if (typeof taskPath !== "string" || !taskPath) return { source: "task" };
16878
+ const tasks = await mutateTasks(taskPath, id, (file) => ({
16879
+ ...file,
16880
+ tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
16881
+ (item) => item.id === originId ? {
16882
+ ...item,
16883
+ title: task.title,
16884
+ description: task.description,
16885
+ status: sourceStatus(task),
16886
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16887
+ } : item
16888
+ )
16889
+ }));
16890
+ return { source: "task", tasks };
16891
+ }
16892
+ return { source: null };
16893
+ }
16894
+
16895
+ // src/kanban.ts
15450
16896
  var kanbanTool = {
15451
16897
  name: "kanban",
15452
16898
  category: "Project",
15453
- 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.",
15454
- usageHint: "Use this for durable project kanban state. Agents should call snapshot/ready_tasks, then claim_task before working. Use set_chain for ordered work, split_task/merge_tasks when task scope changes, assign_task with provider/model/fallback hints before spawning, mark_assignment when starting or finishing, and release_task if the claim cannot be worked.",
16899
+ description: "Manage and audit project-scoped multi-kanban boards stored under .wrongstack/kanbans. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Successful board access records live agent/session presence.",
16900
+ usageHint: "Use this for durable project kanban state. Reassess with get_board whenever evidence changes; agents may add, update, split, merge, reprioritize, or remove tasks so the board remains a live plan. Presence includes active/last-seen session and agent metadata. For managed boards, fully fill card details, use transition_task after every material step, attach truthful evidence, and never use update_task/move_task to bypass lifecycle guards. Worker completion enters Review; only passed acceptance criteria plus review evidence allow Done.",
15455
16901
  permission: "confirm",
15456
16902
  mutating: true,
15457
16903
  capabilities: ["fs.write"],
@@ -15473,6 +16919,8 @@ var kanbanTool = {
15473
16919
  "export_markdown",
15474
16920
  "export_task_graph",
15475
16921
  "sync_task_graph",
16922
+ "create_from_graph",
16923
+ "import_session_tasks",
15476
16924
  "search_tasks",
15477
16925
  "ready_tasks",
15478
16926
  "snapshot",
@@ -15486,6 +16934,7 @@ var kanbanTool = {
15486
16934
  "transfer_task",
15487
16935
  "get_task",
15488
16936
  "update_task",
16937
+ "transition_task",
15489
16938
  "move_task",
15490
16939
  "delete_task",
15491
16940
  "set_chain",
@@ -15516,9 +16965,14 @@ var kanbanTool = {
15516
16965
  targetColumnId: { type: "string" },
15517
16966
  title: { type: "string" },
15518
16967
  description: { type: "string" },
16968
+ dueDate: { type: "string" },
15519
16969
  tags: { type: "array", items: { type: "string" } },
15520
16970
  labels: { type: "array", items: { type: "string" } },
15521
16971
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
16972
+ taskType: {
16973
+ type: "string",
16974
+ enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
16975
+ },
15522
16976
  status: {
15523
16977
  type: "string",
15524
16978
  enum: [
@@ -15558,11 +17012,36 @@ var kanbanTool = {
15558
17012
  type: "string",
15559
17013
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
15560
17014
  },
17015
+ lifecycleStage: {
17016
+ type: "string",
17017
+ enum: ["backlog", "todo", "running", "review", "done"]
17018
+ },
17019
+ transitionAction: { type: "string" },
17020
+ transitionComment: { type: "string" },
17021
+ attachmentUrl: { type: "string" },
17022
+ attachmentTitle: { type: "string" },
17023
+ attachmentType: {
17024
+ type: "string",
17025
+ enum: ["issue", "pr", "doc", "commit", "design", "file", "url", "other"]
17026
+ },
15561
17027
  releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
15562
17028
  releaseReason: { type: "string" },
15563
17029
  clearAssignee: { type: "boolean" },
15564
- recoveryMode: { type: "string", enum: ["release", "retry", "fail"] },
17030
+ recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
15565
17031
  recoveryNow: { type: "string" },
17032
+ recoveryPolicyFailOnCostCeiling: { type: "boolean" },
17033
+ recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
17034
+ recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
17035
+ recoveryPolicyRetryPolicyOverride: {
17036
+ type: "string",
17037
+ enum: ["off", "incremental", "exponential"]
17038
+ },
17039
+ assignee: { type: "string" },
17040
+ costCeilingUsd: { type: "number" },
17041
+ retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
17042
+ lastFailureKind: { type: "string" },
17043
+ dependsOn: { type: "array", items: { type: "string" } },
17044
+ estimatedHours: { type: "number" },
15566
17045
  taskGraph: { type: "object" },
15567
17046
  graphId: { type: "string" },
15568
17047
  specId: { type: "string" },
@@ -15615,538 +17094,632 @@ var kanbanTool = {
15615
17094
  async execute(input, ctx) {
15616
17095
  const projectRoot = ctx.projectRoot;
15617
17096
  if (!projectRoot) return fail("No project root is available.");
17097
+ const withPresence = async (result) => {
17098
+ const boardId = result.board?.id ?? input.boardId;
17099
+ if (!result.ok || !boardId || !ctx.session?.id || !ctx.agentId) return result;
17100
+ try {
17101
+ const board = await touchKanbanPresence2(projectRoot, boardId, {
17102
+ sessionId: ctx.session.id,
17103
+ agentId: ctx.agentId,
17104
+ agentName: ctx.agentName,
17105
+ taskId: input.taskId ?? result.task?.id,
17106
+ runTaskId: input.runTaskId
17107
+ });
17108
+ return board ? { ...result, board } : result;
17109
+ } catch {
17110
+ return result;
17111
+ }
17112
+ };
15618
17113
  try {
15619
- switch (input.action) {
15620
- case "list_boards": {
15621
- const boards = await listBoards(projectRoot);
15622
- return { ok: true, message: `${boards.length} board(s).`, boards };
15623
- }
15624
- case "get_board": {
15625
- const board = await requireBoard(projectRoot, input.boardId);
15626
- return board ? okBoard(board) : fail("Board not found.");
15627
- }
15628
- case "create_board": {
15629
- if (!input.title) return fail("create_board requires title.");
15630
- const board = await createBoard(projectRoot, {
15631
- title: input.title,
15632
- ...input.description !== void 0 ? { description: input.description } : {},
15633
- ...input.tags !== void 0 ? { tags: input.tags } : {},
15634
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
15635
- });
15636
- return { ok: true, message: `Board created: ${board.title}`, board };
15637
- }
15638
- case "update_board": {
15639
- if (!input.boardId) return fail("update_board requires boardId.");
15640
- const board = await updateBoard(projectRoot, input.boardId, {
15641
- ...input.title !== void 0 ? { title: input.title } : {},
15642
- ...input.description !== void 0 ? { description: input.description } : {},
15643
- ...input.tags !== void 0 ? { tags: input.tags } : {}
15644
- });
15645
- return board ? okBoard(board, "Board updated.") : fail("Board not found.");
15646
- }
15647
- case "duplicate_board": {
15648
- if (!input.boardId) return fail("duplicate_board requires boardId.");
15649
- const board = await duplicateBoard(projectRoot, input.boardId, {
15650
- ...input.title !== void 0 ? { title: input.title } : {},
15651
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
15652
- ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
15653
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
15654
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
15655
- });
15656
- return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
15657
- }
15658
- case "delete_board": {
15659
- if (!input.boardId) return fail("delete_board requires boardId.");
15660
- const removed = await removeBoard(projectRoot, input.boardId);
15661
- return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
15662
- }
15663
- case "generate_board": {
15664
- if (!input.description) return fail("generate_board requires description.");
15665
- const boardInput = generateBoardFromDescription({
15666
- description: input.description,
15667
- ...input.title !== void 0 ? { title: input.title } : {},
15668
- ...input.context !== void 0 ? { context: input.context } : {},
15669
- ...input.columns !== void 0 ? { columns: input.columns } : {}
15670
- });
15671
- const board = await createBoard(projectRoot, boardInput);
15672
- for (const taskInput2 of parseLinesIntoTasks(
15673
- input.description,
15674
- board.columns[0]?.id ?? "backlog"
15675
- )) {
15676
- await addTask(projectRoot, board.id, taskInput2);
17114
+ const result = await (async () => {
17115
+ switch (input.action) {
17116
+ case "list_boards": {
17117
+ const boards = await listBoards2(projectRoot);
17118
+ return { ok: true, message: `${boards.length} board(s).`, boards };
15677
17119
  }
15678
- return okBoard(await getBoard(projectRoot, board.id) ?? board, "Board generated.");
15679
- }
15680
- case "export_markdown": {
15681
- const board = await requireBoard(projectRoot, input.boardId);
15682
- if (!board) return fail("Board not found.");
15683
- return {
15684
- ok: true,
15685
- message: "Board exported.",
15686
- board,
15687
- markdown: exportBoardAsMarkdown(board)
15688
- };
15689
- }
15690
- case "export_task_graph": {
15691
- if (!input.boardId) return fail("export_task_graph requires boardId.");
15692
- const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
15693
- ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
15694
- ...input.specId !== void 0 ? { specId: input.specId } : {},
15695
- ...input.title !== void 0 ? { title: input.title } : {},
15696
- ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
15697
- ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
15698
- });
15699
- if (!exported) return fail("Board not found.");
15700
- return {
15701
- ok: true,
15702
- message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
15703
- board: exported.board,
15704
- taskGraph: serializeTaskGraph(exported.graph)
15705
- };
15706
- }
15707
- case "sync_task_graph": {
15708
- if (!input.boardId || !input.taskGraph) {
15709
- return fail("sync_task_graph requires boardId and taskGraph.");
17120
+ case "get_board": {
17121
+ const board = await requireBoard(projectRoot, input.boardId);
17122
+ return board ? okBoard(board) : fail("Board not found.");
15710
17123
  }
15711
- const graph = deserializeTaskGraph(input.taskGraph);
15712
- const result = await syncBoardFromTaskGraph(projectRoot, input.boardId, graph, {
15713
- ...input.title !== void 0 ? { title: input.title } : {},
15714
- ...input.description !== void 0 ? { description: input.description } : {},
15715
- ...input.tags !== void 0 ? { tags: input.tags } : {},
15716
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
15717
- ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
15718
- ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
15719
- ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
15720
- ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
15721
- ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
15722
- });
15723
- return result ? {
15724
- ok: true,
15725
- message: `Task graph synced: ${result.createdTaskIds.length} created, ${result.updatedTaskIds.length} updated, ${result.archivedTaskIds.length} archived.`,
15726
- board: result.board
15727
- } : fail("Board not found.");
15728
- }
15729
- case "search_tasks": {
15730
- const tasks = await searchKanban(projectRoot, {
15731
- query: input.query,
15732
- boardId: input.boardId,
15733
- assignedAgent: input.agentId,
15734
- status: input.status,
15735
- priority: input.priority,
15736
- label: input.labels?.[0],
15737
- chainId: input.chainId
15738
- });
15739
- return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
15740
- }
15741
- case "ready_tasks": {
15742
- const tasks = await listReadyTasks(projectRoot, {
15743
- query: input.query,
15744
- boardId: input.boardId,
15745
- assignedAgent: input.agentId,
15746
- priority: input.priority,
15747
- label: input.labels?.[0],
15748
- chainId: input.chainId,
15749
- limit: input.limit
15750
- });
15751
- return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
15752
- }
15753
- case "snapshot": {
15754
- const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
15755
- query: input.query,
15756
- boardId: input.boardId,
15757
- assignedAgent: input.agentId,
15758
- status: input.status,
15759
- priority: input.priority,
15760
- label: input.labels?.[0],
15761
- chainId: input.chainId
15762
- });
15763
- return {
15764
- ok: true,
15765
- message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
15766
- snapshot
15767
- };
15768
- }
15769
- case "add_column": {
15770
- if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
15771
- const result = await addColumn(projectRoot, input.boardId, {
15772
- title: input.title,
15773
- ...input.description !== void 0 ? { description: input.description } : {}
15774
- });
15775
- return result ? okBoard(result.board, "Column added.") : fail("Board not found.");
15776
- }
15777
- case "update_column": {
15778
- if (!input.boardId || !input.columnId)
15779
- return fail("update_column requires boardId and columnId.");
15780
- const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
15781
- ...input.title !== void 0 ? { title: input.title } : {},
15782
- ...input.description !== void 0 ? { description: input.description } : {},
15783
- ...input.order !== void 0 ? { order: input.order } : {}
15784
- });
15785
- return board ? okBoard(board, "Column updated.") : fail("Column not found.");
15786
- }
15787
- case "delete_column": {
15788
- if (!input.boardId || !input.columnId)
15789
- return fail("delete_column requires boardId and columnId.");
15790
- const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
15791
- moveTasksToColumnId: input.moveTasksToColumnId
15792
- });
15793
- return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
15794
- }
15795
- case "add_task": {
15796
- if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
15797
- const result = await addTask(projectRoot, input.boardId, taskInput(input));
15798
- return result ? okTask(result.board, result.task, "Task added.") : fail("Board not found.");
15799
- }
15800
- case "split_task": {
15801
- if (!input.boardId || !input.taskId || !input.childTitles?.length) {
15802
- return fail("split_task requires boardId, taskId, and childTitles.");
17124
+ case "create_board": {
17125
+ if (!input.title) return fail("create_board requires title.");
17126
+ const board = await createBoard2(projectRoot, {
17127
+ title: input.title,
17128
+ ...input.description !== void 0 ? { description: input.description } : {},
17129
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17130
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
17131
+ });
17132
+ return { ok: true, message: `Board created: ${board.title}`, board };
15803
17133
  }
15804
- const result = await splitTask(projectRoot, input.boardId, input.taskId, {
15805
- titles: input.childTitles,
15806
- ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
15807
- ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
15808
- ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
15809
- ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
15810
- ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
15811
- ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
15812
- ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
15813
- ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
15814
- });
15815
- return result ? {
15816
- ok: true,
15817
- message: `${result.children.length} child task(s) created.`,
15818
- board: result.board,
15819
- task: result.parent,
15820
- children: result.children
15821
- } : fail("Task not found.");
15822
- }
15823
- case "merge_tasks": {
15824
- if (!input.boardId || !input.taskIds?.length || !input.title) {
15825
- return fail("merge_tasks requires boardId, taskIds, and title.");
17134
+ case "update_board": {
17135
+ if (!input.boardId) return fail("update_board requires boardId.");
17136
+ const board = await updateBoard2(projectRoot, input.boardId, {
17137
+ ...input.title !== void 0 ? { title: input.title } : {},
17138
+ ...input.description !== void 0 ? { description: input.description } : {},
17139
+ ...input.tags !== void 0 ? { tags: input.tags } : {}
17140
+ });
17141
+ return board ? okBoard(board, "Board updated.") : fail("Board not found.");
15826
17142
  }
15827
- const result = await mergeTasks(projectRoot, input.boardId, {
15828
- taskIds: input.taskIds,
15829
- title: input.title,
15830
- ...input.description !== void 0 ? { description: input.description } : {},
15831
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
15832
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
15833
- ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
15834
- });
15835
- return result ? okTask(result.board, result.task, "Tasks merged.") : fail("Board or task not found.");
15836
- }
15837
- case "copy_task": {
15838
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
15839
- return fail("copy_task requires boardId, taskId, and targetBoardId.");
17143
+ case "duplicate_board": {
17144
+ if (!input.boardId) return fail("duplicate_board requires boardId.");
17145
+ const board = await duplicateBoard(projectRoot, input.boardId, {
17146
+ ...input.title !== void 0 ? { title: input.title } : {},
17147
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17148
+ ...input.includeTasks !== void 0 ? { includeTasks: input.includeTasks } : {},
17149
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
17150
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {}
17151
+ });
17152
+ return board ? okBoard(board, "Board duplicated.") : fail("Board not found.");
15840
17153
  }
15841
- const result = await copyTaskToBoard(
15842
- projectRoot,
15843
- input.boardId,
15844
- input.taskId,
15845
- input.targetBoardId,
15846
- {
15847
- ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
15848
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
15849
- ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
15850
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17154
+ case "delete_board": {
17155
+ if (!input.boardId) return fail("delete_board requires boardId.");
17156
+ const removed = await removeBoard2(projectRoot, input.boardId);
17157
+ return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
17158
+ }
17159
+ case "generate_board": {
17160
+ if (!input.description) return fail("generate_board requires description.");
17161
+ const boardInput = generateBoardFromDescription({
17162
+ description: input.description,
17163
+ ...input.title !== void 0 ? { title: input.title } : {},
17164
+ ...input.context !== void 0 ? { context: input.context } : {},
17165
+ ...input.columns !== void 0 ? { columns: input.columns } : {}
17166
+ });
17167
+ const board = await createBoard2(projectRoot, boardInput);
17168
+ for (const taskInput2 of parseLinesIntoTasks(
17169
+ input.description,
17170
+ board.columns[0]?.id ?? "backlog"
17171
+ )) {
17172
+ await addTask(projectRoot, board.id, taskInput2);
15851
17173
  }
15852
- );
15853
- return result ? okTask(result.targetBoard, result.task, "Task copied to target board.") : fail("Board or task not found.");
15854
- }
15855
- case "transfer_task": {
15856
- if (!input.boardId || !input.taskId || !input.targetBoardId) {
15857
- return fail("transfer_task requires boardId, taskId, and targetBoardId.");
17174
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
15858
17175
  }
15859
- const result = await transferTaskToBoard(
15860
- projectRoot,
15861
- input.boardId,
15862
- input.taskId,
15863
- input.targetBoardId,
15864
- {
17176
+ case "export_markdown": {
17177
+ const board = await requireBoard(projectRoot, input.boardId);
17178
+ if (!board) return fail("Board not found.");
17179
+ return {
17180
+ ok: true,
17181
+ message: "Board exported.",
17182
+ board,
17183
+ markdown: exportBoardAsMarkdown(board)
17184
+ };
17185
+ }
17186
+ case "export_task_graph": {
17187
+ if (!input.boardId) return fail("export_task_graph requires boardId.");
17188
+ const exported = await exportBoardToTaskGraph(projectRoot, input.boardId, {
17189
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
17190
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
17191
+ ...input.title !== void 0 ? { title: input.title } : {},
17192
+ ...input.preserveOriginTaskIds !== void 0 ? { preserveOriginTaskIds: input.preserveOriginTaskIds } : {},
17193
+ ...input.includeArchived !== void 0 ? { includeArchived: input.includeArchived } : {}
17194
+ });
17195
+ if (!exported) return fail("Board not found.");
17196
+ return {
17197
+ ok: true,
17198
+ message: `Task graph exported with ${exported.graph.nodes.size} node(s).`,
17199
+ board: exported.board,
17200
+ taskGraph: serializeTaskGraph(exported.graph)
17201
+ };
17202
+ }
17203
+ case "sync_task_graph": {
17204
+ if (!input.boardId || !input.taskGraph) {
17205
+ return fail("sync_task_graph requires boardId and taskGraph.");
17206
+ }
17207
+ const graph = deserializeTaskGraph2(input.taskGraph);
17208
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
17209
+ ...input.title !== void 0 ? { title: input.title } : {},
17210
+ ...input.description !== void 0 ? { description: input.description } : {},
17211
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17212
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17213
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
17214
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
17215
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {},
17216
+ ...input.archiveMissingTasks !== void 0 ? { archiveMissingTasks: input.archiveMissingTasks } : {},
17217
+ ...input.preserveManualDependencies !== void 0 ? { preserveManualDependencies: input.preserveManualDependencies } : {}
17218
+ });
17219
+ return result2 ? {
17220
+ ok: true,
17221
+ message: `Task graph synced: ${result2.createdTaskIds.length} created, ${result2.updatedTaskIds.length} updated, ${result2.archivedTaskIds.length} archived.`,
17222
+ board: result2.board
17223
+ } : fail("Board not found.");
17224
+ }
17225
+ case "create_from_graph": {
17226
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
17227
+ const graph = deserializeTaskGraph2(input.taskGraph);
17228
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
17229
+ ...input.title !== void 0 ? { title: input.title } : {},
17230
+ ...input.description !== void 0 ? { description: input.description } : {},
17231
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17232
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17233
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
17234
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
17235
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
17236
+ });
17237
+ return {
17238
+ ok: true,
17239
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
17240
+ board
17241
+ };
17242
+ }
17243
+ case "import_session_tasks": {
17244
+ const taskPath = ctx.meta?.["task.path"];
17245
+ if (!taskPath) return fail("No session task file for this session.");
17246
+ const file = await loadTasks2(taskPath);
17247
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
17248
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
17249
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
17250
+ const tags = ["session", `session:${sessionId}`];
17251
+ const existing = (await listBoards2(projectRoot)).find(
17252
+ (b) => b.tags?.includes(`session:${sessionId}`)
17253
+ );
17254
+ if (existing) {
17255
+ const result2 = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
17256
+ sourceSystem: "session",
17257
+ tags,
17258
+ archiveMissingTasks: true,
17259
+ includeCompletedTasks: true
17260
+ });
17261
+ return result2 ? {
17262
+ ok: true,
17263
+ message: `Synced ${file.tasks.length} session tasks into board "${result2.board.title}".`,
17264
+ board: result2.board
17265
+ } : fail("Session board vanished mid-sync.");
17266
+ }
17267
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
17268
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
17269
+ sourceSystem: "session",
17270
+ tags
17271
+ });
17272
+ return {
17273
+ ok: true,
17274
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
17275
+ board
17276
+ };
17277
+ }
17278
+ case "search_tasks": {
17279
+ const tasks = await searchKanban(projectRoot, {
17280
+ query: input.query,
17281
+ boardId: input.boardId,
17282
+ assignedAgent: input.agentId,
17283
+ status: input.status,
17284
+ priority: input.priority,
17285
+ label: input.labels?.[0],
17286
+ chainId: input.chainId
17287
+ });
17288
+ return { ok: true, message: `${tasks.length} task(s) matched.`, tasks };
17289
+ }
17290
+ case "ready_tasks": {
17291
+ const tasks = await listReadyTasks(projectRoot, {
17292
+ query: input.query,
17293
+ boardId: input.boardId,
17294
+ assignedAgent: input.agentId,
17295
+ priority: input.priority,
17296
+ label: input.labels?.[0],
17297
+ chainId: input.chainId,
17298
+ limit: input.limit
17299
+ });
17300
+ return { ok: true, message: `${tasks.length} ready task(s).`, tasks };
17301
+ }
17302
+ case "snapshot": {
17303
+ const snapshot = await getKanbanOrchestrationSnapshot(projectRoot, {
17304
+ query: input.query,
17305
+ boardId: input.boardId,
17306
+ assignedAgent: input.agentId,
17307
+ status: input.status,
17308
+ priority: input.priority,
17309
+ label: input.labels?.[0],
17310
+ chainId: input.chainId
17311
+ });
17312
+ return {
17313
+ ok: true,
17314
+ message: `${snapshot.ready.length} ready, ${snapshot.running.length} running, ${snapshot.blocked.length} blocked.`,
17315
+ snapshot
17316
+ };
17317
+ }
17318
+ case "add_column": {
17319
+ if (!input.boardId || !input.title) return fail("add_column requires boardId and title.");
17320
+ const result2 = await addColumn(projectRoot, input.boardId, {
17321
+ title: input.title,
17322
+ ...input.description !== void 0 ? { description: input.description } : {}
17323
+ });
17324
+ return result2 ? okBoard(result2.board, "Column added.") : fail("Board not found.");
17325
+ }
17326
+ case "update_column": {
17327
+ if (!input.boardId || !input.columnId)
17328
+ return fail("update_column requires boardId and columnId.");
17329
+ const board = await updateColumn(projectRoot, input.boardId, input.columnId, {
17330
+ ...input.title !== void 0 ? { title: input.title } : {},
17331
+ ...input.description !== void 0 ? { description: input.description } : {},
17332
+ ...input.order !== void 0 ? { order: input.order } : {}
17333
+ });
17334
+ return board ? okBoard(board, "Column updated.") : fail("Column not found.");
17335
+ }
17336
+ case "delete_column": {
17337
+ if (!input.boardId || !input.columnId)
17338
+ return fail("delete_column requires boardId and columnId.");
17339
+ const board = await removeColumn(projectRoot, input.boardId, input.columnId, {
17340
+ moveTasksToColumnId: input.moveTasksToColumnId
17341
+ });
17342
+ return board ? okBoard(board, "Column deleted.") : fail("Column not found.");
17343
+ }
17344
+ case "add_task": {
17345
+ if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
17346
+ const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
17347
+ return result2 ? okTask(result2.board, result2.task, "Task added.") : fail("Board not found.");
17348
+ }
17349
+ case "split_task": {
17350
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
17351
+ return fail("split_task requires boardId, taskId, and childTitles.");
17352
+ }
17353
+ const result2 = await splitTask(projectRoot, input.boardId, input.taskId, {
17354
+ titles: input.childTitles,
17355
+ ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
17356
+ ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
17357
+ ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
17358
+ ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
17359
+ ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
17360
+ ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
17361
+ ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
17362
+ ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
17363
+ });
17364
+ return result2 ? {
17365
+ ok: true,
17366
+ message: `${result2.children.length} child task(s) created.`,
17367
+ board: result2.board,
17368
+ task: result2.parent,
17369
+ children: result2.children
17370
+ } : fail("Task not found.");
17371
+ }
17372
+ case "merge_tasks": {
17373
+ if (!input.boardId || !input.taskIds?.length || !input.title) {
17374
+ return fail("merge_tasks requires boardId, taskIds, and title.");
17375
+ }
17376
+ const result2 = await mergeTasks(projectRoot, input.boardId, {
17377
+ taskIds: input.taskIds,
17378
+ title: input.title,
17379
+ ...input.description !== void 0 ? { description: input.description } : {},
15865
17380
  ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
15866
- ...input.order !== void 0 ? { targetOrder: input.order } : {},
15867
17381
  ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
15868
- ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17382
+ ...input.closeSourceTasks !== void 0 ? { closeSourceTasks: input.closeSourceTasks } : {}
17383
+ });
17384
+ return result2 ? okTask(result2.board, result2.task, "Tasks merged.") : fail("Board or task not found.");
17385
+ }
17386
+ case "copy_task": {
17387
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
17388
+ return fail("copy_task requires boardId, taskId, and targetBoardId.");
15869
17389
  }
15870
- );
15871
- return result ? okTask(result.targetBoard, result.task, "Task transferred to target board.") : fail("Board or task not found.");
15872
- }
15873
- case "get_task": {
15874
- if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
15875
- const task = await getTask(projectRoot, input.boardId, input.taskId);
15876
- return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
15877
- }
15878
- case "update_task": {
15879
- if (!input.boardId || !input.taskId)
15880
- return fail("update_task requires boardId and taskId.");
15881
- const board = await updateTask(
15882
- projectRoot,
15883
- input.boardId,
15884
- input.taskId,
15885
- taskPatch(input)
15886
- );
15887
- return board ? okBoard(board, "Task updated.") : fail("Task not found.");
15888
- }
15889
- case "move_task": {
15890
- if (!input.boardId || !input.taskId || !input.targetColumnId) {
15891
- return fail("move_task requires boardId, taskId, and targetColumnId.");
17390
+ const result2 = await copyTaskToBoard(
17391
+ projectRoot,
17392
+ input.boardId,
17393
+ input.taskId,
17394
+ input.targetBoardId,
17395
+ {
17396
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
17397
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
17398
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
17399
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17400
+ }
17401
+ );
17402
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task copied to target board.") : fail("Board or task not found.");
15892
17403
  }
15893
- const board = await moveTask(
15894
- projectRoot,
15895
- input.boardId,
15896
- input.taskId,
15897
- input.targetColumnId,
15898
- input.order
15899
- );
15900
- return board ? okBoard(board, "Task moved.") : fail("Move failed.");
15901
- }
15902
- case "delete_task": {
15903
- if (!input.boardId || !input.taskId)
15904
- return fail("delete_task requires boardId and taskId.");
15905
- const board = await removeTask(projectRoot, input.boardId, input.taskId);
15906
- return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
15907
- }
15908
- case "set_chain": {
15909
- if (!input.boardId || !input.taskIds?.length) {
15910
- return fail("set_chain requires boardId and taskIds.");
17404
+ case "transfer_task": {
17405
+ if (!input.boardId || !input.taskId || !input.targetBoardId) {
17406
+ return fail("transfer_task requires boardId, taskId, and targetBoardId.");
17407
+ }
17408
+ const result2 = await transferTaskToBoard(
17409
+ projectRoot,
17410
+ input.boardId,
17411
+ input.taskId,
17412
+ input.targetBoardId,
17413
+ {
17414
+ ...input.targetColumnId !== void 0 ? { targetColumnId: input.targetColumnId } : {},
17415
+ ...input.order !== void 0 ? { targetOrder: input.order } : {},
17416
+ ...input.preserveAssignment !== void 0 ? { preserveAssignment: input.preserveAssignment } : {},
17417
+ ...input.preserveDependencies !== void 0 ? { preserveDependencies: input.preserveDependencies } : {}
17418
+ }
17419
+ );
17420
+ return result2 ? okTask(result2.targetBoard, result2.task, "Task transferred to target board.") : fail("Board or task not found.");
15911
17421
  }
15912
- const result = await setTaskChain(projectRoot, input.boardId, {
15913
- taskIds: input.taskIds,
15914
- ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
15915
- ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
15916
- });
15917
- return result ? {
15918
- ok: true,
15919
- message: `Chain set: ${result.chainId}`,
15920
- board: result.board,
15921
- chain: result.tasks
15922
- } : fail("Board or task not found.");
15923
- }
15924
- case "get_chain": {
15925
- if (!input.boardId || !(input.taskId || input.chainId)) {
15926
- return fail("get_chain requires boardId and taskId or chainId.");
17422
+ case "get_task": {
17423
+ if (!input.boardId || !input.taskId) return fail("get_task requires boardId and taskId.");
17424
+ const task = await getTask(projectRoot, input.boardId, input.taskId);
17425
+ return task ? { ok: true, message: "Task loaded.", task } : fail("Task not found.");
15927
17426
  }
15928
- const result = await getTaskChain(
15929
- projectRoot,
15930
- input.boardId,
15931
- input.taskId ?? input.chainId ?? ""
15932
- );
15933
- return result ? {
15934
- ok: true,
15935
- message: `Chain loaded: ${result.chainId}`,
15936
- board: result.board,
15937
- chain: result.tasks
15938
- } : fail("Chain not found.");
15939
- }
15940
- case "claim_task": {
15941
- const result = await claimReadyTask(projectRoot, {
15942
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
15943
- ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
15944
- ...assignmentInput(input),
15945
- status: input.assignmentStatus ?? "queued"
15946
- });
15947
- return result ? okTask(result.board, result.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
15948
- }
15949
- case "release_task": {
15950
- if (!input.boardId || !input.taskId) {
15951
- return fail("release_task requires boardId and taskId.");
17427
+ case "update_task": {
17428
+ if (!input.boardId || !input.taskId)
17429
+ return fail("update_task requires boardId and taskId.");
17430
+ const board = await updateTask(
17431
+ projectRoot,
17432
+ input.boardId,
17433
+ input.taskId,
17434
+ taskPatch(input)
17435
+ );
17436
+ return board ? okBoard(board, "Task updated.") : fail("Task not found.");
15952
17437
  }
15953
- const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
15954
- ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
15955
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
15956
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
15957
- });
15958
- return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
15959
- }
15960
- case "assign_task": {
15961
- if (!input.boardId || !input.taskId)
15962
- return fail("assign_task requires boardId and taskId.");
15963
- const board = await assignTask(
15964
- projectRoot,
15965
- input.boardId,
15966
- input.taskId,
15967
- assignmentInput(input)
15968
- );
15969
- return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
15970
- }
15971
- case "mark_assignment": {
15972
- if (!input.boardId || !input.taskId)
15973
- return fail("mark_assignment requires boardId and taskId.");
15974
- const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
15975
- const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
15976
- ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
15977
- ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
15978
- ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
15979
- ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
15980
- ...input.error !== void 0 ? { error: input.error } : {},
15981
- ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
15982
- ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
15983
- ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
15984
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
15985
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
15986
- ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
15987
- ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
15988
- });
15989
- return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
15990
- }
15991
- case "heartbeat_assignment": {
15992
- if (!input.boardId || !input.taskId) {
15993
- return fail("heartbeat_assignment requires boardId and taskId.");
17438
+ case "transition_task": {
17439
+ if (!input.boardId || !input.taskId || !input.lifecycleStage || !input.author || !input.transitionComment) {
17440
+ return fail(
17441
+ "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
17442
+ );
17443
+ }
17444
+ const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
17445
+ to: input.lifecycleStage,
17446
+ actor: input.author,
17447
+ comment: input.transitionComment,
17448
+ ...input.transitionAction !== void 0 ? { action: input.transitionAction } : {},
17449
+ ...input.attachmentUrl !== void 0 ? {
17450
+ attachment: {
17451
+ url: input.attachmentUrl,
17452
+ type: input.attachmentType ?? "url",
17453
+ ...input.attachmentTitle !== void 0 ? { title: input.attachmentTitle } : {}
17454
+ }
17455
+ } : {},
17456
+ patch: taskPatch(input)
17457
+ });
17458
+ return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
15994
17459
  }
15995
- const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
15996
- ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
15997
- ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
15998
- });
15999
- return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
16000
- }
16001
- case "recover_stale": {
16002
- if (!input.boardId) return fail("recover_stale requires boardId.");
16003
- const policyFields = [
16004
- input.recoveryPolicyFailOnCostCeiling !== void 0,
16005
- input.recoveryPolicyReleaseOnFailureKinds !== void 0,
16006
- input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
16007
- input.recoveryPolicyRetryPolicyOverride !== void 0
16008
- ].some(Boolean);
16009
- const result = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
16010
- ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
16011
- ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
16012
- ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
16013
- ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
16014
- ...policyFields ? {
16015
- policy: {
16016
- ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
16017
- ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
16018
- releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
16019
- } : {},
16020
- ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
16021
- releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
16022
- } : {},
16023
- ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
16024
- retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
16025
- } : {}
16026
- }
16027
- } : {}
16028
- });
16029
- return result ? {
16030
- ok: true,
16031
- message: `Recovered ${result.tasks.length} stale assignment(s).`,
16032
- board: result.board,
16033
- recoveredTasks: result.tasks
16034
- } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
16035
- }
16036
- case "events": {
16037
- if (!input.boardId) return fail("events requires boardId.");
16038
- const eventList = await listKanbanEvents(projectRoot, input.boardId);
16039
- return {
16040
- ok: true,
16041
- message: `${eventList.length} event(s).`,
16042
- events: eventList
16043
- };
16044
- }
16045
- case "queue_health": {
16046
- const health = await getKanbanQueueHealth(projectRoot, {
16047
- ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
16048
- });
16049
- return {
16050
- ok: true,
16051
- message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
16052
- queueHealth: health
16053
- };
16054
- }
16055
- case "add_dependency": {
16056
- if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
16057
- return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
17460
+ case "move_task": {
17461
+ if (!input.boardId || !input.taskId || !input.targetColumnId) {
17462
+ return fail("move_task requires boardId, taskId, and targetColumnId.");
17463
+ }
17464
+ const board = await moveTask(
17465
+ projectRoot,
17466
+ input.boardId,
17467
+ input.taskId,
17468
+ input.targetColumnId,
17469
+ input.order
17470
+ );
17471
+ return board ? okBoard(board, "Task moved.") : fail("Move failed.");
16058
17472
  }
16059
- const board = await addDependency(
16060
- projectRoot,
16061
- input.boardId,
16062
- input.taskId,
16063
- input.dependencyTaskId
16064
- );
16065
- return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
16066
- }
16067
- case "add_goal_metric": {
16068
- if (!input.boardId || !input.taskId || !input.metricName) {
16069
- return fail("add_goal_metric requires boardId, taskId, and metricName.");
17473
+ case "delete_task": {
17474
+ if (!input.boardId || !input.taskId)
17475
+ return fail("delete_task requires boardId and taskId.");
17476
+ const board = await removeTask(projectRoot, input.boardId, input.taskId);
17477
+ return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
16070
17478
  }
16071
- const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
16072
- name: input.metricName,
16073
- ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
16074
- ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
16075
- ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
16076
- ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
16077
- ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
16078
- });
16079
- return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
16080
- }
16081
- case "update_goal_metric": {
16082
- if (!input.boardId || !input.taskId || !input.metricId) {
16083
- return fail("update_goal_metric requires boardId, taskId, and metricId.");
17479
+ case "set_chain": {
17480
+ if (!input.boardId || !input.taskIds?.length) {
17481
+ return fail("set_chain requires boardId and taskIds.");
17482
+ }
17483
+ const result2 = await setTaskChain(projectRoot, input.boardId, {
17484
+ taskIds: input.taskIds,
17485
+ ...input.chainId !== void 0 ? { chainId: input.chainId } : {},
17486
+ ...input.enforceDependencies !== void 0 ? { enforceDependencies: input.enforceDependencies } : {}
17487
+ });
17488
+ return result2 ? {
17489
+ ok: true,
17490
+ message: `Chain set: ${result2.chainId}`,
17491
+ board: result2.board,
17492
+ chain: result2.tasks
17493
+ } : fail("Board or task not found.");
16084
17494
  }
16085
- const board = await updateGoalMetricOnTask(
16086
- projectRoot,
16087
- input.boardId,
16088
- input.taskId,
16089
- input.metricId,
16090
- {
16091
- ...input.metricName !== void 0 ? { name: input.metricName } : {},
17495
+ case "get_chain": {
17496
+ if (!input.boardId || !(input.taskId || input.chainId)) {
17497
+ return fail("get_chain requires boardId and taskId or chainId.");
17498
+ }
17499
+ const result2 = await getTaskChain(
17500
+ projectRoot,
17501
+ input.boardId,
17502
+ input.taskId ?? input.chainId ?? ""
17503
+ );
17504
+ return result2 ? {
17505
+ ok: true,
17506
+ message: `Chain loaded: ${result2.chainId}`,
17507
+ board: result2.board,
17508
+ chain: result2.tasks
17509
+ } : fail("Chain not found.");
17510
+ }
17511
+ case "claim_task": {
17512
+ const result2 = await claimReadyTask(projectRoot, {
17513
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {},
17514
+ ...input.taskId !== void 0 ? { taskId: input.taskId } : {},
17515
+ ...assignmentInput(input),
17516
+ status: input.assignmentStatus ?? "queued"
17517
+ });
17518
+ return result2 ? okTask(result2.board, result2.task, "Task claimed.") : fail("No ready kanban task matched the claim.");
17519
+ }
17520
+ case "release_task": {
17521
+ if (!input.boardId || !input.taskId) {
17522
+ return fail("release_task requires boardId and taskId.");
17523
+ }
17524
+ const board = await releaseTaskClaim(projectRoot, input.boardId, input.taskId, {
17525
+ ...input.releaseStatus !== void 0 ? { status: input.releaseStatus } : {},
17526
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17527
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {}
17528
+ });
17529
+ return board ? okBoard(board, "Task claim released.") : fail("Task not found.");
17530
+ }
17531
+ case "assign_task": {
17532
+ if (!input.boardId || !input.taskId)
17533
+ return fail("assign_task requires boardId and taskId.");
17534
+ const board = await assignTask(
17535
+ projectRoot,
17536
+ input.boardId,
17537
+ input.taskId,
17538
+ assignmentInput(input)
17539
+ );
17540
+ return board ? okBoard(board, "Task assigned.") : fail("Task not found.");
17541
+ }
17542
+ case "mark_assignment": {
17543
+ if (!input.boardId || !input.taskId)
17544
+ return fail("mark_assignment requires boardId and taskId.");
17545
+ const assignmentStatus = input.assignmentStatus ?? (input.status === "completed" ? "completed" : input.error ? "failed" : void 0);
17546
+ const board = await updateTaskAssignment(projectRoot, input.boardId, input.taskId, {
17547
+ ...assignmentStatus !== void 0 ? { status: assignmentStatus } : {},
17548
+ ...input.subagentId !== void 0 ? { subagentId: input.subagentId } : {},
17549
+ ...input.runTaskId !== void 0 ? { runTaskId: input.runTaskId } : {},
17550
+ ...input.lastResult !== void 0 ? { lastResult: input.lastResult } : {},
17551
+ ...input.error !== void 0 ? { error: input.error } : {},
17552
+ ...input.agentId !== void 0 ? { agentId: input.agentId } : {},
17553
+ ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
17554
+ ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
17555
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17556
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {},
17557
+ ...input.attempt !== void 0 ? { attempt: input.attempt } : {},
17558
+ ...input.maxAttempts !== void 0 ? { maxAttempts: input.maxAttempts } : {}
17559
+ });
17560
+ return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
17561
+ }
17562
+ case "heartbeat_assignment": {
17563
+ if (!input.boardId || !input.taskId) {
17564
+ return fail("heartbeat_assignment requires boardId and taskId.");
17565
+ }
17566
+ const board = await heartbeatTaskAssignment(projectRoot, input.boardId, input.taskId, {
17567
+ ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
17568
+ ...input.leaseExpiresAt !== void 0 ? { leaseExpiresAt: input.leaseExpiresAt } : {}
17569
+ });
17570
+ return board ? okBoard(board, "Assignment heartbeat updated.") : fail("Task assignment not found.");
17571
+ }
17572
+ case "recover_stale": {
17573
+ if (!input.boardId) return fail("recover_stale requires boardId.");
17574
+ const policyFields = [
17575
+ input.recoveryPolicyFailOnCostCeiling !== void 0,
17576
+ input.recoveryPolicyReleaseOnFailureKinds !== void 0,
17577
+ input.recoveryPolicyReleaseOnHeartbeatDue !== void 0,
17578
+ input.recoveryPolicyRetryPolicyOverride !== void 0
17579
+ ].some(Boolean);
17580
+ const result2 = await recoverStaleTaskAssignments(projectRoot, input.boardId, {
17581
+ ...input.recoveryMode !== void 0 ? { mode: input.recoveryMode } : {},
17582
+ ...input.recoveryNow !== void 0 ? { now: input.recoveryNow } : {},
17583
+ ...input.releaseReason !== void 0 ? { reason: input.releaseReason } : {},
17584
+ ...input.clearAssignee !== void 0 ? { clearAssignee: input.clearAssignee } : {},
17585
+ ...policyFields ? {
17586
+ policy: {
17587
+ ...input.recoveryPolicyFailOnCostCeiling !== void 0 ? { failWhenCostCeilingSet: input.recoveryPolicyFailOnCostCeiling } : {},
17588
+ ...input.recoveryPolicyReleaseOnFailureKinds !== void 0 ? {
17589
+ releaseOnFailureKinds: input.recoveryPolicyReleaseOnFailureKinds
17590
+ } : {},
17591
+ ...input.recoveryPolicyReleaseOnHeartbeatDue !== void 0 ? {
17592
+ releaseOnHeartbeatDue: input.recoveryPolicyReleaseOnHeartbeatDue
17593
+ } : {},
17594
+ ...input.recoveryPolicyRetryPolicyOverride !== void 0 ? {
17595
+ retryPolicyOverride: input.recoveryPolicyRetryPolicyOverride
17596
+ } : {}
17597
+ }
17598
+ } : {}
17599
+ });
17600
+ return result2 ? {
17601
+ ok: true,
17602
+ message: `Recovered ${result2.tasks.length} stale assignment(s).`,
17603
+ board: result2.board,
17604
+ recoveredTasks: result2.tasks
17605
+ } : { ok: true, message: "No stale assignment matched.", recoveredTasks: [] };
17606
+ }
17607
+ case "events": {
17608
+ if (!input.boardId) return fail("events requires boardId.");
17609
+ const eventList = await listKanbanEvents(projectRoot, input.boardId);
17610
+ return {
17611
+ ok: true,
17612
+ message: `${eventList.length} event(s).`,
17613
+ events: eventList
17614
+ };
17615
+ }
17616
+ case "queue_health": {
17617
+ const health = await getKanbanQueueHealth(projectRoot, {
17618
+ ...input.boardId !== void 0 ? { boardId: input.boardId } : {}
17619
+ });
17620
+ return {
17621
+ ok: true,
17622
+ message: `Counts: ready=${health.counts.ready}, running=${health.counts.running}, stale=${health.staleAssignments.count}.`,
17623
+ queueHealth: health
17624
+ };
17625
+ }
17626
+ case "add_dependency": {
17627
+ if (!input.boardId || !input.taskId || !input.dependencyTaskId) {
17628
+ return fail("add_dependency requires boardId, taskId, and dependencyTaskId.");
17629
+ }
17630
+ const board = await addDependency(
17631
+ projectRoot,
17632
+ input.boardId,
17633
+ input.taskId,
17634
+ input.dependencyTaskId
17635
+ );
17636
+ return board ? okBoard(board, "Dependency added.") : fail("Task not found.");
17637
+ }
17638
+ case "add_goal_metric": {
17639
+ if (!input.boardId || !input.taskId || !input.metricName) {
17640
+ return fail("add_goal_metric requires boardId, taskId, and metricName.");
17641
+ }
17642
+ const board = await addGoalMetricToTask(projectRoot, input.boardId, input.taskId, {
17643
+ name: input.metricName,
16092
17644
  ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
16093
17645
  ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
16094
17646
  ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
16095
17647
  ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
16096
17648
  ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17649
+ });
17650
+ return board ? okBoard(board, "Goal metric added.") : fail("Task not found.");
17651
+ }
17652
+ case "update_goal_metric": {
17653
+ if (!input.boardId || !input.taskId || !input.metricId) {
17654
+ return fail("update_goal_metric requires boardId, taskId, and metricId.");
16097
17655
  }
16098
- );
16099
- return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
16100
- }
16101
- case "add_check": {
16102
- if (!input.boardId || !input.taskId || !input.checkDescription) {
16103
- return fail("add_check requires boardId, taskId, and checkDescription.");
17656
+ const board = await updateGoalMetricOnTask(
17657
+ projectRoot,
17658
+ input.boardId,
17659
+ input.taskId,
17660
+ input.metricId,
17661
+ {
17662
+ ...input.metricName !== void 0 ? { name: input.metricName } : {},
17663
+ ...input.metricStatus !== void 0 ? { status: input.metricStatus } : {},
17664
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17665
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17666
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17667
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17668
+ }
17669
+ );
17670
+ return board ? okBoard(board, "Goal metric updated.") : fail("Metric not found.");
16104
17671
  }
16105
- const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
16106
- description: input.checkDescription,
16107
- type: "manual",
16108
- status: input.checkStatus
16109
- });
16110
- return board ? okBoard(board, "Check added.") : fail("Task not found.");
16111
- }
16112
- case "update_check": {
16113
- if (!input.boardId || !input.taskId || !input.checkId) {
16114
- return fail("update_check requires boardId, taskId, and checkId.");
17672
+ case "add_check": {
17673
+ if (!input.boardId || !input.taskId || !input.checkDescription) {
17674
+ return fail("add_check requires boardId, taskId, and checkDescription.");
17675
+ }
17676
+ const board = await addCheckToTask(projectRoot, input.boardId, input.taskId, {
17677
+ description: input.checkDescription,
17678
+ type: "manual",
17679
+ status: input.checkStatus
17680
+ });
17681
+ return board ? okBoard(board, "Check added.") : fail("Task not found.");
16115
17682
  }
16116
- const board = await updateCheckOnTask(
16117
- projectRoot,
16118
- input.boardId,
16119
- input.taskId,
16120
- input.checkId,
16121
- {
16122
- ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
16123
- ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17683
+ case "update_check": {
17684
+ if (!input.boardId || !input.taskId || !input.checkId) {
17685
+ return fail("update_check requires boardId, taskId, and checkId.");
16124
17686
  }
16125
- );
16126
- return board ? okBoard(board, "Check updated.") : fail("Check not found.");
16127
- }
16128
- case "add_note": {
16129
- if (!input.boardId || !input.taskId || !input.note)
16130
- return fail("add_note requires boardId, taskId, and note.");
16131
- const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
16132
- author: input.author ?? "agent",
16133
- content: input.note
16134
- });
16135
- return board ? okBoard(board, "Note added.") : fail("Task not found.");
16136
- }
16137
- case "add_link": {
16138
- if (!input.boardId || !input.taskId || !input.url)
16139
- return fail("add_link requires boardId, taskId, and url.");
16140
- const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
16141
- url: input.url,
16142
- type: input.linkType ?? "url",
16143
- ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
16144
- });
16145
- return board ? okBoard(board, "Link added.") : fail("Task not found.");
17687
+ const board = await updateCheckOnTask(
17688
+ projectRoot,
17689
+ input.boardId,
17690
+ input.taskId,
17691
+ input.checkId,
17692
+ {
17693
+ ...input.checkDescription !== void 0 ? { description: input.checkDescription } : {},
17694
+ ...input.checkStatus !== void 0 ? { status: input.checkStatus } : {}
17695
+ }
17696
+ );
17697
+ return board ? okBoard(board, "Check updated.") : fail("Check not found.");
17698
+ }
17699
+ case "add_note": {
17700
+ if (!input.boardId || !input.taskId || !input.note)
17701
+ return fail("add_note requires boardId, taskId, and note.");
17702
+ const board = await addNoteToTask(projectRoot, input.boardId, input.taskId, {
17703
+ author: input.author ?? "agent",
17704
+ content: input.note
17705
+ });
17706
+ return board ? okBoard(board, "Note added.") : fail("Task not found.");
17707
+ }
17708
+ case "add_link": {
17709
+ if (!input.boardId || !input.taskId || !input.url)
17710
+ return fail("add_link requires boardId, taskId, and url.");
17711
+ const board = await addLinkToTask(projectRoot, input.boardId, input.taskId, {
17712
+ url: input.url,
17713
+ type: input.linkType ?? "url",
17714
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17715
+ });
17716
+ return board ? okBoard(board, "Link added.") : fail("Task not found.");
17717
+ }
17718
+ default:
17719
+ return fail(`Unknown kanban action: ${input.action}`);
16146
17720
  }
16147
- default:
16148
- return fail(`Unknown kanban action: ${input.action}`);
16149
- }
17721
+ })();
17722
+ return withPresence(result);
16150
17723
  } catch (err) {
16151
17724
  return fail(err instanceof Error ? err.message : String(err));
16152
17725
  }
@@ -16162,7 +17735,7 @@ function okTask(board, task, message) {
16162
17735
  return { ok: true, message, board, task };
16163
17736
  }
16164
17737
  async function requireBoard(projectRoot, boardId) {
16165
- return boardId ? getBoard(projectRoot, boardId) : null;
17738
+ return boardId ? getBoard2(projectRoot, boardId) : null;
16166
17739
  }
16167
17740
  function taskInput(input) {
16168
17741
  const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
@@ -16170,26 +17743,99 @@ function taskInput(input) {
16170
17743
  title: input.title ?? "",
16171
17744
  columnId: input.columnId,
16172
17745
  description: input.description,
17746
+ dueDate: input.dueDate,
16173
17747
  priority: input.priority,
17748
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
16174
17749
  status: input.status,
16175
17750
  labels: input.labels,
16176
17751
  ...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
16177
17752
  ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
16178
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {},
16179
- ...assignment ? { assignment } : {}
17753
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
17754
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
17755
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {},
17756
+ ...assignment ? { assignment } : {},
17757
+ ...input.order !== void 0 ? { order: input.order } : {},
17758
+ // Sprint 3 durable task-level fields
17759
+ ...input.retryPolicy !== void 0 ? { retryPolicy: input.retryPolicy } : {},
17760
+ ...input.costCeilingUsd !== void 0 ? { costCeilingUsd: input.costCeilingUsd } : {},
17761
+ // Relationships — chainId alone cannot construct a KanbanTaskChainRef,
17762
+ // so chain/previousTaskId/nextTaskId must be set via update_task.
17763
+ ...input.childTitles !== void 0 ? { childTaskIds: input.childTitles } : {},
17764
+ // Detail objects
17765
+ ...input.checkDescription !== void 0 ? {
17766
+ successCriteria: [
17767
+ {
17768
+ id: randomUUID(),
17769
+ description: input.checkDescription,
17770
+ type: "manual",
17771
+ status: input.checkStatus ?? "pending"
17772
+ }
17773
+ ]
17774
+ } : {},
17775
+ ...input.metricName !== void 0 ? {
17776
+ goalMetrics: [
17777
+ {
17778
+ id: randomUUID(),
17779
+ name: input.metricName,
17780
+ status: input.metricStatus ?? "pending",
17781
+ ...input.metricTarget !== void 0 ? { target: input.metricTarget } : {},
17782
+ ...input.metricCurrent !== void 0 ? { current: input.metricCurrent } : {},
17783
+ ...input.metricUnit !== void 0 ? { unit: input.metricUnit } : {},
17784
+ ...input.metricNotes !== void 0 ? { notes: input.metricNotes } : {}
17785
+ }
17786
+ ]
17787
+ } : {},
17788
+ ...input.url !== void 0 ? {
17789
+ links: [
17790
+ {
17791
+ url: input.url,
17792
+ type: input.linkType ?? "url",
17793
+ ...input.linkTitle !== void 0 ? { title: input.linkTitle } : {}
17794
+ }
17795
+ ]
17796
+ } : {},
17797
+ ...input.note !== void 0 ? {
17798
+ notes: [
17799
+ {
17800
+ id: randomUUID(),
17801
+ author: input.author ?? "agent",
17802
+ content: input.note,
17803
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
17804
+ }
17805
+ ]
17806
+ } : {},
17807
+ // Origin tracking
17808
+ ...input.graphId !== void 0 ? {
17809
+ origin: {
17810
+ system: input.sourceSystem ?? "kanban-tool",
17811
+ ...input.graphId !== void 0 ? { graphId: input.graphId } : {},
17812
+ ...input.specId !== void 0 ? { specId: input.specId } : {},
17813
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {}
17814
+ }
17815
+ } : {}
16180
17816
  };
16181
17817
  }
17818
+ function mergedDependsOn(input) {
17819
+ const ids = [
17820
+ ...input.dependsOn ?? [],
17821
+ ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
17822
+ ].filter((id, i, arr) => id && arr.indexOf(id) === i);
17823
+ return ids.length > 0 ? ids : void 0;
17824
+ }
16182
17825
  function taskPatch(input) {
16183
17826
  return {
16184
17827
  title: input.title,
16185
17828
  description: input.description,
17829
+ dueDate: input.dueDate,
16186
17830
  columnId: input.columnId,
16187
17831
  order: input.order,
16188
17832
  priority: input.priority,
17833
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
16189
17834
  status: input.status,
16190
17835
  labels: input.labels,
16191
17836
  assignedAgent: input.agentId,
16192
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {}
17837
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
17838
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
16193
17839
  };
16194
17840
  }
16195
17841
  function assignmentInput(input) {
@@ -16934,15 +18580,15 @@ import {
16934
18580
  deriveTodosFromPlanItem,
16935
18581
  formatPlan,
16936
18582
  getPlanTemplate,
16937
- mutatePlan,
18583
+ mutatePlan as mutatePlan2,
16938
18584
  removePlanItem,
16939
18585
  setPlanItemStatus
16940
18586
  } from "@wrongstack/core";
16941
18587
  import {
16942
- mutateTasks,
18588
+ mutateTasks as mutateTasks2,
16943
18589
  formatTaskList
16944
18590
  } from "@wrongstack/core";
16945
- import { randomUUID } from "node:crypto";
18591
+ import { randomUUID as randomUUID2 } from "node:crypto";
16946
18592
  var planTool = {
16947
18593
  name: "plan",
16948
18594
  category: "Session",
@@ -17026,7 +18672,7 @@ var planTool = {
17026
18672
  let didTaskify = false;
17027
18673
  let plan;
17028
18674
  try {
17029
- plan = await mutatePlan(planPath, sessionId, async (p) => {
18675
+ plan = await mutatePlan2(planPath, sessionId, async (p) => {
17030
18676
  switch (input.action) {
17031
18677
  case "show":
17032
18678
  break;
@@ -17152,6 +18798,7 @@ var planTool = {
17152
18798
  open: 0
17153
18799
  };
17154
18800
  }
18801
+ await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
17155
18802
  if (early) return early;
17156
18803
  if (didTaskify) {
17157
18804
  const taskPathRaw = ctx.meta["task.path"];
@@ -17165,9 +18812,9 @@ var planTool = {
17165
18812
  }
17166
18813
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
17167
18814
  try {
17168
- const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
18815
+ const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
17169
18816
  f.tasks.push({
17170
- id: `task_${randomUUID()}`,
18817
+ id: `task_${randomUUID2()}`,
17171
18818
  title: taskifyMeta.title,
17172
18819
  description: taskifyMeta.details || void 0,
17173
18820
  type: "feature",
@@ -18302,14 +19949,14 @@ import {
18302
19949
  formatTaskList as formatTaskList2
18303
19950
  } from "@wrongstack/core";
18304
19951
  import {
18305
- mutateTasks as mutateTasks2
19952
+ mutateTasks as mutateTasks3
18306
19953
  } from "@wrongstack/core";
18307
19954
  import {
18308
19955
  addPlanItem as addPlanItem2,
18309
- mutatePlan as mutatePlan2,
19956
+ mutatePlan as mutatePlan3,
18310
19957
  formatPlan as formatPlan2
18311
19958
  } from "@wrongstack/core";
18312
- import { randomUUID as randomUUID2 } from "node:crypto";
19959
+ import { randomUUID as randomUUID3 } from "node:crypto";
18313
19960
  function findTaskIndex(tasks, query) {
18314
19961
  const asNum = Number.parseInt(query, 10);
18315
19962
  if (!Number.isNaN(asNum)) {
@@ -18426,7 +20073,7 @@ var taskTool = {
18426
20073
  let todosToReplace = null;
18427
20074
  let file;
18428
20075
  try {
18429
- file = await mutateTasks2(taskPath, sessionId, async (f) => {
20076
+ file = await mutateTasks3(taskPath, sessionId, async (f) => {
18430
20077
  switch (input.action) {
18431
20078
  case "show":
18432
20079
  break;
@@ -18497,7 +20144,7 @@ var taskTool = {
18497
20144
  }
18498
20145
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
18499
20146
  const newTask = {
18500
- id: `task_${Date.now()}_${randomUUID2().slice(0, 8)}`,
20147
+ id: `task_${Date.now()}_${randomUUID3().slice(0, 8)}`,
18501
20148
  title: t.title,
18502
20149
  description: t.description,
18503
20150
  type: t.type || "feature",
@@ -18558,7 +20205,7 @@ var taskTool = {
18558
20205
  });
18559
20206
  if (match.description) {
18560
20207
  todos.push({
18561
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
20208
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
18562
20209
  content: match.description.slice(0, 200),
18563
20210
  status: "pending",
18564
20211
  promotedFromTask: match.id
@@ -18567,7 +20214,7 @@ var taskTool = {
18567
20214
  if (input.subtasks && input.subtasks.length > 0) {
18568
20215
  for (const st of input.subtasks) {
18569
20216
  todos.push({
18570
- id: `todo_${ts2}_${randomUUID2().slice(0, 6)}`,
20217
+ id: `todo_${ts2}_${randomUUID3().slice(0, 6)}`,
18571
20218
  content: st,
18572
20219
  status: "pending",
18573
20220
  promotedFromTask: match.id
@@ -18616,6 +20263,7 @@ var taskTool = {
18616
20263
  };
18617
20264
  }
18618
20265
  if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
20266
+ await projectSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);
18619
20267
  if (early) return early;
18620
20268
  if (didPlanify) {
18621
20269
  const { title, details } = planifyMeta;
@@ -18629,7 +20277,7 @@ var taskTool = {
18629
20277
  }
18630
20278
  let formatted = "";
18631
20279
  try {
18632
- await mutatePlan2(planPath, sessionId, (pf) => {
20280
+ await mutatePlan3(planPath, sessionId, (pf) => {
18633
20281
  const { plan: updated } = addPlanItem2(pf, title, details || void 0);
18634
20282
  formatted = formatPlan2(updated);
18635
20283
  return updated;
@@ -18861,8 +20509,7 @@ function parseResult(runner, result, duration) {
18861
20509
  }
18862
20510
 
18863
20511
  // src/todo.ts
18864
- import { loadPlan, savePlan, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
18865
- import { loadTasks, saveTasks } from "@wrongstack/core";
20512
+ import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
18866
20513
  var todoTool = {
18867
20514
  name: "todo",
18868
20515
  category: "Session",
@@ -18871,7 +20518,7 @@ var todoTool = {
18871
20518
  permission: "auto",
18872
20519
  mutating: false,
18873
20520
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
18874
- timeoutMs: 1e3,
20521
+ timeoutMs: 5e3,
18875
20522
  capabilities: ["session.todo"],
18876
20523
  icon: "todo",
18877
20524
  inputSchema: {
@@ -18923,16 +20570,21 @@ var todoTool = {
18923
20570
  }
18924
20571
  }
18925
20572
  ctx.state.replaceTodos(items);
20573
+ await projectSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
18926
20574
  const completedPlanIds = /* @__PURE__ */ new Set();
18927
20575
  const completedTaskIds = /* @__PURE__ */ new Set();
18928
20576
  const pendingPlanIds = /* @__PURE__ */ new Set();
18929
20577
  const pendingTaskIds = /* @__PURE__ */ new Set();
18930
20578
  for (const item of items) {
18931
20579
  if (item.promotedFromPlan) {
18932
- (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
20580
+ (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
20581
+ item.promotedFromPlan
20582
+ );
18933
20583
  }
18934
20584
  if (item.promotedFromTask) {
18935
- (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
20585
+ (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
20586
+ item.promotedFromTask
20587
+ );
18936
20588
  }
18937
20589
  }
18938
20590
  for (const planId of completedPlanIds) {
@@ -18940,7 +20592,7 @@ var todoTool = {
18940
20592
  const planPath = ctx.meta["plan.path"];
18941
20593
  if (typeof planPath !== "string" || !planPath) continue;
18942
20594
  try {
18943
- const plan = await loadPlan(planPath);
20595
+ const plan = await loadPlan2(planPath);
18944
20596
  if (plan) {
18945
20597
  const updated = setPlanItemStatus2(plan, planId, "done");
18946
20598
  await savePlan(planPath, updated);
@@ -18953,7 +20605,7 @@ var todoTool = {
18953
20605
  const taskPath = ctx.meta["task.path"];
18954
20606
  if (typeof taskPath !== "string" || !taskPath) continue;
18955
20607
  try {
18956
- const file = await loadTasks(taskPath);
20608
+ const file = await loadTasks3(taskPath);
18957
20609
  if (file) {
18958
20610
  const task = file.tasks.find((t) => t.id === taskId);
18959
20611
  if (task && task.status !== "completed") {
@@ -19268,8 +20920,8 @@ var DEFAULT_IGNORE5 = [
19268
20920
  var treeTool = {
19269
20921
  name: "tree",
19270
20922
  category: "Filesystem",
19271
- 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.",
19272
- usageHint: "BEST PRACTICE FOR INITIAL EXPLORATION:\n\n- Call early when working with an unfamiliar project or module.\n- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\n- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\nOutput is truncated for very large trees.",
20923
+ description: "Display a project or subpath directory tree. Use it for structural layout; use index-backed `codebase-search` first for code understanding when it is live.",
20924
+ usageHint: "DIRECTORY-LAYOUT EXPLORATION:\n\n- When `codebase-search` is live, use it first for symbols and concepts; use `tree` when the directory hierarchy itself matters.\n- Tune `depth` (default 3) and use `glob`/`exclude` to focus the view.\n- Prefer this over raw `bash find` or `glob` + manual reading when you need a quick structural overview.\nOutput is truncated for very large trees.",
19273
20925
  permission: "auto",
19274
20926
  mutating: false,
19275
20927
  capabilities: ["fs.read"],
@@ -19672,15 +21324,15 @@ var OPTIONAL_TOOLS = [
19672
21324
  toolUseTool,
19673
21325
  batchToolUseTool,
19674
21326
  toolHelpTool,
19675
- codebaseIndexTool,
19676
- codebaseSearchTool,
19677
- codebaseStatsTool,
19678
21327
  setWorkingDirTool
19679
21328
  ];
19680
21329
  var TIER1_TOOLS = [
19681
21330
  readTool,
19682
21331
  writeTool,
19683
21332
  editTool,
21333
+ codebaseStatsTool,
21334
+ codebaseSearchTool,
21335
+ codebaseIndexTool,
19684
21336
  bashTool,
19685
21337
  grepTool,
19686
21338
  globTool,
@@ -19719,9 +21371,6 @@ var TIER3_TOOLS = [
19719
21371
  toolUseTool,
19720
21372
  batchToolUseTool,
19721
21373
  toolHelpTool,
19722
- codebaseIndexTool,
19723
- codebaseSearchTool,
19724
- codebaseStatsTool,
19725
21374
  setWorkingDirTool
19726
21375
  ];
19727
21376
  var builtinTools = [
@@ -19730,6 +21379,9 @@ var builtinTools = [
19730
21379
  readTool,
19731
21380
  writeTool,
19732
21381
  editTool,
21382
+ codebaseStatsTool,
21383
+ codebaseSearchTool,
21384
+ codebaseIndexTool,
19733
21385
  replaceTool,
19734
21386
  globTool,
19735
21387
  grepTool,
@@ -19764,9 +21416,6 @@ var builtinTools = [
19764
21416
  toolUseTool,
19765
21417
  batchToolUseTool,
19766
21418
  toolHelpTool,
19767
- codebaseIndexTool,
19768
- codebaseSearchTool,
19769
- codebaseStatsTool,
19770
21419
  setWorkingDirTool
19771
21420
  ];
19772
21421
 
@@ -20922,7 +22571,13 @@ var TOOL_ICON_MAP = {
20922
22571
  think: "brain",
20923
22572
  reason: "brain",
20924
22573
  analyze: "brain",
20925
- reasoning: "brain"
22574
+ reasoning: "brain",
22575
+ // Language intelligence
22576
+ lsp_diagnostics: "code",
22577
+ lsp_definition: "search",
22578
+ lsp_completion: "code",
22579
+ lsp_rename: "edit",
22580
+ "codebase-lsp-search": "index"
20926
22581
  };
20927
22582
  function getToolIcon(toolName) {
20928
22583
  return TOOL_ICON_MAP[toolName.toLowerCase()] ?? "fallback";
@@ -20991,13 +22646,17 @@ export {
20991
22646
  LanguageProfileRegistry,
20992
22647
  OPTIONAL_TOOLS,
20993
22648
  PRIMARY_LANGUAGE_PROFILES,
22649
+ SESSION_KANBAN_COLUMNS,
20994
22650
  TIER1_TOOLS,
20995
22651
  TIER2_TOOLS,
20996
22652
  TIER3_TOOLS,
20997
22653
  TOOL_ICON_CONFIG,
20998
22654
  TOOL_ICON_MAP,
20999
22655
  _resetProcessRegistry,
22656
+ applySessionKanbanBoardToTodos,
22657
+ applySessionKanbanTaskToSource,
21000
22658
  assertBrowserUrlAllowed,
22659
+ attachSessionKanbanMirror,
21001
22660
  auditTool,
21002
22661
  bashTool,
21003
22662
  batchToolUseTool,
@@ -21040,11 +22699,13 @@ export {
21040
22699
  e2ePlanTool,
21041
22700
  editTool,
21042
22701
  enqueueReindex,
22702
+ ensureSessionKanbanBoard,
21043
22703
  ensureSessionShell,
21044
22704
  execTool,
21045
22705
  executeLanguagePlan,
21046
22706
  executePackagePlan,
21047
22707
  fetchTool,
22708
+ fileGraphService,
21048
22709
  forgetTool,
21049
22710
  formatGlobalStatus,
21050
22711
  formatInstanceList,
@@ -21061,6 +22722,7 @@ export {
21061
22722
  gitTool,
21062
22723
  globTool,
21063
22724
  grepTool,
22725
+ hydrateSessionKanban,
21064
22726
  indexCircuitBreaker,
21065
22727
  installTool,
21066
22728
  isExecCommandAllowed,
@@ -21077,15 +22739,22 @@ export {
21077
22739
  listInstances,
21078
22740
  logsTool,
21079
22741
  makeSkillTool,
22742
+ mirrorSessionPlanToKanban,
22743
+ mirrorSessionTasksToKanban,
22744
+ mirrorSessionTodosToKanban,
21080
22745
  normalizeShell,
21081
22746
  onIndexStateChange,
21082
22747
  outdatedTool,
22748
+ packageGraphService,
21083
22749
  parseLanguageDiagnostics,
21084
22750
  parsePackageReports,
21085
22751
  parsePrivateOriginAllowlist,
21086
22752
  patchTool,
21087
22753
  planLanguageOperation,
21088
22754
  planTool,
22755
+ projectSessionPlanToKanban,
22756
+ projectSessionTasksToKanban,
22757
+ projectSessionTodosToKanban,
21089
22758
  readTool,
21090
22759
  redactBrowserText,
21091
22760
  relatedMemoryTool,
@@ -21107,6 +22776,7 @@ export {
21107
22776
  shutdownCodebaseIndexHost,
21108
22777
  startProcessGuardian,
21109
22778
  stopProcessGuardian,
22779
+ symbolGraphService,
21110
22780
  testTool,
21111
22781
  todoTool,
21112
22782
  toolHelpTool,