@wrongstack/tools 0.286.0 → 0.287.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 (54) hide show
  1. package/README.md +2 -2
  2. package/dist/_edit-match.d.ts.map +1 -1
  3. package/dist/batch-tool-use.d.ts +1 -1
  4. package/dist/batch-tool-use.d.ts.map +1 -1
  5. package/dist/batch-tool-use.js +26 -8
  6. package/dist/batch-tool-use.js.map +2 -2
  7. package/dist/browser/index.js +1 -1
  8. package/dist/browser/index.js.map +1 -1
  9. package/dist/browser/types.d.ts +1 -1
  10. package/dist/browser/types.d.ts.map +1 -1
  11. package/dist/builtin.js +544 -118
  12. package/dist/builtin.js.map +4 -4
  13. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  14. package/dist/codebase-index/index.js +190 -96
  15. package/dist/codebase-index/index.js.map +4 -4
  16. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  17. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  18. package/dist/codebase-index/worker.js +188 -94
  19. package/dist/codebase-index/worker.js.map +4 -4
  20. package/dist/edit.d.ts.map +1 -1
  21. package/dist/edit.js +36 -12
  22. package/dist/edit.js.map +3 -3
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +818 -125
  26. package/dist/index.js.map +4 -4
  27. package/dist/kanban.d.ts +7 -2
  28. package/dist/kanban.d.ts.map +1 -1
  29. package/dist/kanban.js +158 -19
  30. package/dist/kanban.js.map +4 -4
  31. package/dist/next-steps.d.ts +6 -2
  32. package/dist/next-steps.d.ts.map +1 -1
  33. package/dist/next-steps.js +3 -3
  34. package/dist/next-steps.js.map +2 -2
  35. package/dist/pack.js +544 -118
  36. package/dist/pack.js.map +4 -4
  37. package/dist/plan.d.ts.map +1 -1
  38. package/dist/plan.js +154 -4
  39. package/dist/plan.js.map +4 -4
  40. package/dist/session-kanban.d.ts +38 -0
  41. package/dist/session-kanban.d.ts.map +1 -0
  42. package/dist/session-kanban.js +489 -0
  43. package/dist/session-kanban.js.map +7 -0
  44. package/dist/task.d.ts.map +1 -1
  45. package/dist/task.js +163 -4
  46. package/dist/task.js.map +4 -4
  47. package/dist/todo.d.ts.map +1 -1
  48. package/dist/todo.js +155 -7
  49. package/dist/todo.js.map +4 -4
  50. package/dist/tool-icon-map.d.ts.map +1 -1
  51. package/dist/tool-icons.d.ts.map +1 -1
  52. package/dist/tool-icons.js +7 -1
  53. package/dist/tool-icons.js.map +2 -2
  54. package/package.json +7 -3
package/dist/index.js CHANGED
@@ -2561,8 +2561,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, signal)
2561
2561
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2562
2562
  }
2563
2563
  }
2564
- function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2565
- const lower = basename8.toLowerCase();
2564
+ function collectFileEvidence(directory, fullPath, basename9, profiles, state) {
2565
+ const lower = basename9.toLowerCase();
2566
2566
  const extension = path4.extname(lower);
2567
2567
  for (const profile of profiles) {
2568
2568
  const detector = profile.detectors.find(
@@ -2573,7 +2573,7 @@ function collectFileEvidence(directory, fullPath, basename8, profiles, state) {
2573
2573
  candidate.evidence.push({
2574
2574
  kind: detector.kind,
2575
2575
  path: fullPath,
2576
- value: basename8,
2576
+ value: basename9,
2577
2577
  weight: detector.weight
2578
2578
  });
2579
2579
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -6373,6 +6373,9 @@ ${hint}` : ""),
6373
6373
  };
6374
6374
 
6375
6375
  // src/batch-tool-use.ts
6376
+ import {
6377
+ GOVERNED_TOOL_EXECUTOR_META_KEY
6378
+ } from "@wrongstack/core";
6376
6379
  var batchToolUseTool = {
6377
6380
  name: "batch_tool_use",
6378
6381
  category: "Meta",
@@ -6409,7 +6412,7 @@ var batchToolUseTool = {
6409
6412
  },
6410
6413
  required: ["calls"]
6411
6414
  },
6412
- async execute(input, ctx, opts) {
6415
+ async execute(input, ctx, _opts) {
6413
6416
  if (!input?.calls || input.calls.length === 0) {
6414
6417
  return {
6415
6418
  results: [],
@@ -6419,18 +6422,33 @@ var batchToolUseTool = {
6419
6422
  stop_on_error: false
6420
6423
  };
6421
6424
  }
6425
+ const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
6426
+ if (typeof governedExecute !== "function") {
6427
+ return {
6428
+ results: input.calls.map((call) => ({
6429
+ tool: call.tool,
6430
+ success: false,
6431
+ error: "governed nested execution is unavailable; call the tool directly",
6432
+ executionMs: 0
6433
+ })),
6434
+ total: input.calls.length,
6435
+ succeeded: 0,
6436
+ failed: input.calls.length,
6437
+ stop_on_error: input.stop_on_error ?? false
6438
+ };
6439
+ }
6422
6440
  const results = [];
6423
6441
  let succeeded = 0;
6424
6442
  let failed = 0;
6425
6443
  if (input.parallel !== false) {
6426
- const promises = input.calls.map(async (call) => executeSingle(call, ctx, opts));
6444
+ const promises = input.calls.map(async (call) => executeSingle(call, ctx, governedExecute));
6427
6445
  const allResults = await Promise.all(promises);
6428
6446
  results.push(...allResults);
6429
6447
  succeeded = allResults.filter((r) => r.success).length;
6430
6448
  failed = allResults.filter((r) => !r.success).length;
6431
6449
  } else {
6432
6450
  for (const call of input.calls) {
6433
- const result = await executeSingle(call, ctx, opts ?? { signal: void 0 });
6451
+ const result = await executeSingle(call, ctx, governedExecute);
6434
6452
  results.push(result);
6435
6453
  if (result.success) {
6436
6454
  succeeded++;
@@ -6449,9 +6467,9 @@ var batchToolUseTool = {
6449
6467
  };
6450
6468
  }
6451
6469
  };
6452
- async function executeSingle(call, ctx, opts) {
6470
+ async function executeSingle(call, ctx, governedExecute) {
6453
6471
  const start = Date.now();
6454
- const tool = ctx.tools.find((t) => t.name === call.tool);
6472
+ const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
6455
6473
  if (!tool) {
6456
6474
  return {
6457
6475
  tool: call.tool,
@@ -6461,11 +6479,11 @@ async function executeSingle(call, ctx, opts) {
6461
6479
  };
6462
6480
  }
6463
6481
  try {
6464
- const result = await tool.execute(call.input, ctx, opts);
6482
+ const result = await governedExecute(call.tool, call.input);
6465
6483
  return {
6466
6484
  tool: call.tool,
6467
- success: true,
6468
- result,
6485
+ success: result.success,
6486
+ ...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
6469
6487
  executionMs: Date.now() - start
6470
6488
  };
6471
6489
  } catch (e) {
@@ -6870,7 +6888,7 @@ var BrowserSessionManager = class {
6870
6888
  constructor(options, launcher = defaultLauncher) {
6871
6889
  this.launcher = launcher;
6872
6890
  this.artifacts = new BrowserArtifactStore(options.artifactRoot);
6873
- this.allowPrivateHosts = options.allowPrivateHosts ?? false;
6891
+ this.allowPrivateHosts = options.allowPrivateHosts ?? true;
6874
6892
  this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
6875
6893
  this.networkProxy = new BrowserNetworkGuardProxy({
6876
6894
  allowPrivateHosts: this.allowPrivateHosts,
@@ -8844,6 +8862,7 @@ function detectLang(file) {
8844
8862
  }
8845
8863
 
8846
8864
  // src/codebase-index/go-parser.ts
8865
+ init_win32_resolve();
8847
8866
  import { spawn as spawn4 } from "node:child_process";
8848
8867
  import * as os5 from "node:os";
8849
8868
  import * as path14 from "node:path";
@@ -9152,27 +9171,42 @@ async function syncGoParse(filePath, content, lang) {
9152
9171
  try {
9153
9172
  const scriptPath = path14.join(tmpDir, "parse.go");
9154
9173
  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(() => {
9174
+ const goBinary = resolveWin32Command("go");
9175
+ const goResult = await new Promise(
9176
+ (resolve15, reject) => {
9177
+ let settled = false;
9178
+ const proc = spawn4(goBinary, ["run", scriptPath], {
9179
+ stdio: ["pipe", "pipe", "pipe"],
9180
+ windowsHide: true
9181
+ });
9182
+ proc.on("error", (err) => {
9183
+ if (settled) return;
9184
+ settled = true;
9185
+ reject(err);
9186
+ });
9187
+ let stdout2 = "";
9188
+ proc.stdout?.on("data", (chunk) => {
9189
+ stdout2 += chunk.toString();
9190
+ });
9191
+ proc.stderr?.resume();
9192
+ proc.stdin?.write(content);
9193
+ proc.stdin?.end();
9194
+ const timer = setTimeout(() => {
9195
+ if (settled) return;
9196
+ settled = true;
9171
9197
  proc.kill("SIGKILL");
9172
9198
  reject(new Error("timeout"));
9173
- }, 15e3)
9174
- )
9175
- ]).catch(() => ({ code: -1 }));
9199
+ }, 15e3);
9200
+ timer.unref?.();
9201
+ proc.on("close", (code2) => {
9202
+ if (settled) return;
9203
+ settled = true;
9204
+ clearTimeout(timer);
9205
+ resolve15({ code: code2, stdout: stdout2 });
9206
+ });
9207
+ }
9208
+ );
9209
+ const { code, stdout } = goResult;
9176
9210
  if (code !== 0 || !stdout.trim()) {
9177
9211
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9178
9212
  }
@@ -9199,7 +9233,8 @@ async function syncGoParse(filePath, content, lang) {
9199
9233
  }
9200
9234
 
9201
9235
  // src/codebase-index/py-parser.ts
9202
- import { spawn as spawn5 } from "node:child_process";
9236
+ init_win32_resolve();
9237
+ import { spawn as spawn5, spawnSync } from "node:child_process";
9203
9238
  import * as fs11 from "node:fs/promises";
9204
9239
  import * as os6 from "node:os";
9205
9240
  import * as path15 from "node:path";
@@ -9414,36 +9449,74 @@ visitor.visit(tree)
9414
9449
 
9415
9450
  print(json.dumps([s.to_dict() for s in syms]))
9416
9451
  `;
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], {
9452
+ function resolvePython() {
9453
+ const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
9454
+ for (const name of candidates) {
9455
+ const resolved = resolveWin32Command(name);
9456
+ const result = spawnSync(resolved, ["--version"], {
9457
+ stdio: "pipe",
9458
+ timeout: 5e3
9459
+ });
9460
+ if (result.error) continue;
9461
+ if (result.status !== 0) continue;
9462
+ return resolved;
9463
+ }
9464
+ return null;
9465
+ }
9466
+ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
9467
+ return new Promise((resolve15, reject) => {
9468
+ let settled = false;
9469
+ const proc = spawn5(pyBinary, [scriptPath, filePath], {
9427
9470
  stdio: ["pipe", "pipe", "pipe"],
9428
9471
  windowsHide: true
9429
9472
  });
9473
+ proc.on("error", (err) => {
9474
+ if (settled) return;
9475
+ settled = true;
9476
+ reject(err);
9477
+ });
9430
9478
  proc.stdin?.write(content);
9431
9479
  proc.stdin?.end();
9432
9480
  let stdout = "";
9433
9481
  proc.stdout?.on("data", (chunk) => {
9434
9482
  stdout += chunk.toString();
9435
9483
  });
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 }));
9484
+ proc.stderr?.resume();
9485
+ const timer = setTimeout(() => {
9486
+ if (settled) return;
9487
+ settled = true;
9488
+ proc.kill("SIGKILL");
9489
+ reject(new Error("timeout"));
9490
+ }, 15e3);
9491
+ timer.unref?.();
9492
+ proc.on("close", (code) => {
9493
+ if (settled) return;
9494
+ settled = true;
9495
+ clearTimeout(timer);
9496
+ resolve15({ code, stdout });
9497
+ });
9498
+ });
9499
+ }
9500
+ var _cachedScriptPath = null;
9501
+ var _cachedPyBinary = null;
9502
+ async function syncPyParse(filePath, content, lang) {
9503
+ try {
9504
+ if (!_cachedScriptPath) {
9505
+ const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
9506
+ await fs11.mkdir(tmpDir, { recursive: true });
9507
+ _cachedScriptPath = path15.join(tmpDir, "parse.py");
9508
+ await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9509
+ }
9510
+ if (!_cachedPyBinary) {
9511
+ _cachedPyBinary = resolvePython();
9512
+ if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9513
+ }
9514
+ const { code, stdout } = await spawnPyParser(
9515
+ _cachedPyBinary,
9516
+ _cachedScriptPath,
9517
+ filePath,
9518
+ content
9519
+ );
9447
9520
  if (code !== 0 || !stdout.trim()) {
9448
9521
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9449
9522
  }
@@ -9468,6 +9541,7 @@ async function syncPyParse(filePath, content, lang) {
9468
9541
  }
9469
9542
 
9470
9543
  // src/codebase-index/rs-parser.ts
9544
+ init_win32_resolve();
9471
9545
  import { expectDefined as expectDefined3 } from "@wrongstack/core";
9472
9546
  import { execFileSync, spawn as spawn6 } from "node:child_process";
9473
9547
  import * as fs12 from "node:fs/promises";
@@ -9512,30 +9586,45 @@ async function tryNativeParse(file, content) {
9512
9586
  const crateDir = path16.join(toolsDir, "syn-parser");
9513
9587
  const tmpFile = path16.join(crateDir, "src", "input.rs");
9514
9588
  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(() => {
9589
+ const cargoBinary = resolveWin32Command("cargo");
9590
+ const result = await new Promise(
9591
+ (resolve15, reject) => {
9592
+ let settled = false;
9593
+ const proc = spawn6(
9594
+ cargoBinary,
9595
+ ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
9596
+ {
9597
+ cwd: process.cwd(),
9598
+ stdio: ["pipe", "pipe", "pipe"],
9599
+ windowsHide: true
9600
+ }
9601
+ );
9602
+ proc.on("error", (err) => {
9603
+ if (settled) return;
9604
+ settled = true;
9605
+ reject(err);
9606
+ });
9607
+ let stdout2 = "";
9608
+ proc.stdout?.on("data", (chunk) => {
9609
+ stdout2 += chunk.toString();
9610
+ });
9611
+ proc.stderr?.resume();
9612
+ const timer = setTimeout(() => {
9613
+ if (settled) return;
9614
+ settled = true;
9534
9615
  proc.kill("SIGKILL");
9535
9616
  reject(new Error("timeout"));
9536
- }, 15e3)
9537
- )
9538
- ]).catch(() => ({ code: -1 }));
9617
+ }, 15e3);
9618
+ timer.unref?.();
9619
+ proc.on("close", (c) => {
9620
+ if (settled) return;
9621
+ settled = true;
9622
+ clearTimeout(timer);
9623
+ resolve15({ code: c, stdout: stdout2 });
9624
+ });
9625
+ }
9626
+ );
9627
+ const { code, stdout } = result;
9539
9628
  if (code === 0 && stdout.trim()) {
9540
9629
  const symbols = JSON.parse(stdout.trim());
9541
9630
  return {
@@ -9630,9 +9719,9 @@ function parseSymbols5(opts) {
9630
9719
  function regexParse2(opts) {
9631
9720
  const { file, content, lang } = opts;
9632
9721
  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";
9722
+ const basename9 = path17.basename(file).toLowerCase();
9723
+ const isPackageJson = basename9 === "package.json";
9724
+ const isTsconfig = basename9 === "tsconfig.json" || basename9 === "tsconfig.build.json";
9636
9725
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
9637
9726
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
9638
9727
  const lines = content.split("\n");
@@ -11954,25 +12043,46 @@ function findLadderMatches(fileLf, oldLf) {
11954
12043
  const needleLines = oldLf.split("\n");
11955
12044
  if (needleLines.length > fileLines.length) return void 0;
11956
12045
  const offsets = lineOffsets(fileLines);
12046
+ const fileTrimEnd = fileLines.map((l) => l.trimEnd());
12047
+ const needleTrimEnd = needleLines.map((l) => l.trimEnd());
11957
12048
  const trailing = windowScan(
11958
- fileLines,
11959
- needleLines,
12049
+ fileTrimEnd,
12050
+ needleTrimEnd,
11960
12051
  offsets,
11961
- (a, b) => a.trimEnd() === b.trimEnd()
12052
+ fileLines,
12053
+ (a, b) => a === b
11962
12054
  );
11963
12055
  if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
11964
- const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
12056
+ const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);
11965
12057
  if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
11966
- const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
12058
+ const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());
12059
+ const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());
12060
+ const normalized = windowScan(
12061
+ fileTrimmed,
12062
+ needleTrimmed,
12063
+ offsets,
12064
+ fileLines,
12065
+ (a, b) => a === b
12066
+ );
11967
12067
  if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
11968
12068
  return fuzzyScan(fileLines, needleLines, offsets);
11969
12069
  }
11970
12070
  function lineAt(text, pos) {
12071
+ if (pos < 512) {
12072
+ let line2 = 1;
12073
+ for (let i = 0; i < pos; i++) {
12074
+ if (text.charCodeAt(i) === 10) line2++;
12075
+ }
12076
+ return line2;
12077
+ }
11971
12078
  let line = 1;
11972
- for (let i = 0; i < pos; i++) {
11973
- if (text.charCodeAt(i) === 10) line++;
12079
+ let search = 0;
12080
+ while (true) {
12081
+ const idx = text.indexOf("\n", search);
12082
+ if (idx === -1 || idx >= pos) return line;
12083
+ search = idx + 1;
12084
+ line++;
11974
12085
  }
11975
- return line;
11976
12086
  }
11977
12087
  function lineOffsets(lines) {
11978
12088
  const out = new Array(lines.length);
@@ -11991,19 +12101,19 @@ function windowToMatch(fileLines, offsets, start, windowLen) {
11991
12101
  startLine: start + 1
11992
12102
  };
11993
12103
  }
11994
- function windowScan(fileLines, needleLines, offsets, eq) {
12104
+ function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
11995
12105
  const n = needleLines.length;
11996
12106
  const out = [];
11997
- for (let i = 0; i + n <= fileLines.length; i++) {
12107
+ for (let i = 0; i + n <= comparisonLines.length; i++) {
11998
12108
  let all = true;
11999
12109
  for (let j = 0; j < n; j++) {
12000
- if (!eq(fileLines[i + j], needleLines[j])) {
12110
+ if (!eq(comparisonLines[i + j], needleLines[j])) {
12001
12111
  all = false;
12002
12112
  break;
12003
12113
  }
12004
12114
  }
12005
12115
  if (all) {
12006
- out.push(windowToMatch(fileLines, offsets, i, n));
12116
+ out.push(windowToMatch(originalLines, offsets, i, n));
12007
12117
  i += n - 1;
12008
12118
  }
12009
12119
  }
@@ -12325,8 +12435,10 @@ var editTool = {
12325
12435
  note: autoReadNote
12326
12436
  };
12327
12437
  }
12438
+ opts?.signal?.throwIfAborted();
12328
12439
  const ladder = findLadderMatches(fileLf, oldLf);
12329
12440
  if (!ladder) {
12441
+ opts?.signal?.throwIfAborted();
12330
12442
  const hint = nearestMatchHint(fileLf, oldLf);
12331
12443
  throw new ToolValidationError({
12332
12444
  message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
@@ -12394,6 +12506,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
12394
12506
  before: original,
12395
12507
  after: newFile
12396
12508
  });
12509
+ opts?.signal?.throwIfAborted();
12397
12510
  const diff = unifiedDiff(original, newFile, {
12398
12511
  fromFile: input.path,
12399
12512
  toFile: input.path
@@ -15401,7 +15514,7 @@ function toYaml(data, indent = 0) {
15401
15514
  }
15402
15515
 
15403
15516
  // src/kanban.ts
15404
- import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core";
15517
+ import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
15405
15518
  import {
15406
15519
  addCheckToTask,
15407
15520
  addColumn,
@@ -15413,18 +15526,19 @@ import {
15413
15526
  assignTask,
15414
15527
  claimReadyTask,
15415
15528
  copyTaskToBoard,
15416
- createBoard,
15529
+ createBoard as createBoard2,
15530
+ createBoardFromTaskGraph,
15417
15531
  duplicateBoard,
15418
15532
  exportBoardAsMarkdown,
15419
15533
  exportBoardToTaskGraph,
15420
15534
  generateBoardFromDescription,
15421
- getBoard,
15535
+ getBoard as getBoard2,
15422
15536
  getKanbanOrchestrationSnapshot,
15423
15537
  getKanbanQueueHealth,
15424
15538
  getTask,
15425
15539
  getTaskChain,
15426
15540
  heartbeatTaskAssignment,
15427
- listBoards,
15541
+ listBoards as listBoards2,
15428
15542
  listKanbanEvents,
15429
15543
  listReadyTasks,
15430
15544
  mergeTasks,
@@ -15432,21 +15546,494 @@ import {
15432
15546
  parseLinesIntoTasks,
15433
15547
  recoverStaleTaskAssignments,
15434
15548
  releaseTaskClaim,
15435
- removeBoard,
15549
+ removeBoard as removeBoard2,
15436
15550
  removeColumn,
15437
15551
  removeTask,
15438
15552
  searchKanban,
15439
15553
  setTaskChain,
15440
15554
  splitTask,
15441
- syncBoardFromTaskGraph,
15555
+ syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
15442
15556
  transferTaskToBoard,
15443
- updateBoard,
15557
+ updateBoard as updateBoard2,
15444
15558
  updateCheckOnTask,
15445
15559
  updateColumn,
15446
15560
  updateGoalMetricOnTask,
15447
15561
  updateTask,
15448
15562
  updateTaskAssignment
15449
15563
  } from "@wrongstack/kanban";
15564
+
15565
+ // src/session-kanban.ts
15566
+ import { watch } from "node:fs";
15567
+ import { basename as basename8, dirname as dirname13 } from "node:path";
15568
+ import {
15569
+ deserializeTaskGraph,
15570
+ loadPlan,
15571
+ loadTasks,
15572
+ mutatePlan,
15573
+ mutateTasks
15574
+ } from "@wrongstack/core";
15575
+ import {
15576
+ createBoard,
15577
+ getBoard,
15578
+ listBoards,
15579
+ removeBoard,
15580
+ syncBoardFromTaskGraph,
15581
+ updateBoard
15582
+ } from "@wrongstack/kanban";
15583
+ var SESSION_BOARD_TAG = "session-work";
15584
+ var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
15585
+ var SESSION_KANBAN_COLUMNS = [
15586
+ { id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
15587
+ { id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
15588
+ { id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
15589
+ { id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
15590
+ ];
15591
+ var boardQueue = /* @__PURE__ */ new Map();
15592
+ var boardEnsures = /* @__PURE__ */ new Map();
15593
+ var bindings = /* @__PURE__ */ new WeakMap();
15594
+ var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
15595
+ var activeSessionBoards = /* @__PURE__ */ new Map();
15596
+ function boardKey(projectRoot, sessionId) {
15597
+ return `${projectRoot}\0${sessionId}`;
15598
+ }
15599
+ function sessionTag(sessionId) {
15600
+ return `session:${sessionId}`;
15601
+ }
15602
+ function sessionBoardTitle(sessionId) {
15603
+ const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
15604
+ return `Session ${leaf.slice(0, 12)}`;
15605
+ }
15606
+ function sessionBoardTags(sessionId) {
15607
+ return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
15608
+ }
15609
+ function sessionIdFromTags(tags) {
15610
+ const tag = tags?.find((candidate) => candidate.startsWith("session:"));
15611
+ return tag?.slice("session:".length) || null;
15612
+ }
15613
+ function isOwnedSessionBoard(tags) {
15614
+ return Boolean(tags?.includes(SESSION_BOARD_TAG) && sessionIdFromTags(tags));
15615
+ }
15616
+ function retainActiveSessionBoard(projectRoot, sessionId) {
15617
+ const key = boardKey(projectRoot, sessionId);
15618
+ activeSessionBoards.set(key, (activeSessionBoards.get(key) ?? 0) + 1);
15619
+ }
15620
+ function releaseActiveSessionBoard(projectRoot, sessionId) {
15621
+ const key = boardKey(projectRoot, sessionId);
15622
+ const remaining = (activeSessionBoards.get(key) ?? 0) - 1;
15623
+ if (remaining > 0) activeSessionBoards.set(key, remaining);
15624
+ else activeSessionBoards.delete(key);
15625
+ }
15626
+ function isSessionBoardActive(projectRoot, sessionId) {
15627
+ return (activeSessionBoards.get(boardKey(projectRoot, sessionId)) ?? 0) > 0;
15628
+ }
15629
+ function sameColumns(columns) {
15630
+ return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
15631
+ }
15632
+ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
15633
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
15634
+ const key = boardKey(projectRoot, sessionId);
15635
+ const inFlight = boardEnsures.get(key);
15636
+ if (inFlight) return inFlight;
15637
+ const promise = (async () => {
15638
+ const summary = (await listBoards(projectRoot)).find(
15639
+ (board2) => board2.tags?.includes(sessionTag(sessionId))
15640
+ );
15641
+ let board = summary ? await getBoard(projectRoot, summary.id) : null;
15642
+ if (!board) {
15643
+ return createBoard(projectRoot, {
15644
+ title: sessionBoardTitle(sessionId),
15645
+ description: "Live session work: todos, tasks, and plan items.",
15646
+ tags: sessionBoardTags(sessionId),
15647
+ columns: SESSION_KANBAN_COLUMNS,
15648
+ generatedBy: `session-kanban:${sessionId}`
15649
+ });
15650
+ }
15651
+ if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
15652
+ board = await updateBoard(projectRoot, board.id, {
15653
+ title: sessionBoardTitle(sessionId),
15654
+ description: "Live session work: todos, tasks, and plan items.",
15655
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
15656
+ columns: SESSION_KANBAN_COLUMNS
15657
+ }) ?? board;
15658
+ }
15659
+ return board;
15660
+ })();
15661
+ boardEnsures.set(key, promise);
15662
+ try {
15663
+ return await promise;
15664
+ } finally {
15665
+ boardEnsures.delete(key);
15666
+ }
15667
+ }
15668
+ function enqueueBoardWork(projectRoot, sessionId, work) {
15669
+ const key = boardKey(projectRoot, sessionId);
15670
+ const previous = boardQueue.get(key) ?? Promise.resolve();
15671
+ const result = previous.catch(() => void 0).then(work);
15672
+ const tail = result.then(
15673
+ () => void 0,
15674
+ () => void 0
15675
+ );
15676
+ boardQueue.set(key, tail);
15677
+ void tail.then(() => {
15678
+ if (boardQueue.get(key) === tail) boardQueue.delete(key);
15679
+ });
15680
+ return result;
15681
+ }
15682
+ async function removeEmptySessionBoard(projectRoot, boardId, sessionId) {
15683
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
15684
+ if (isSessionBoardActive(projectRoot, sessionId)) return null;
15685
+ const board = await getBoard(projectRoot, boardId);
15686
+ if (!board || board.tasks.length > 0 || !isOwnedSessionBoard(board.tags)) return null;
15687
+ if (sessionIdFromTags(board.tags) !== sessionId) return null;
15688
+ return await removeBoard(projectRoot, board.id) ? board.id : null;
15689
+ });
15690
+ }
15691
+ async function cleanupSessionKanbanBoardIfEmpty(projectRoot, sessionId) {
15692
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return [];
15693
+ if (isSessionBoardActive(projectRoot, sessionId)) return [];
15694
+ const candidates = (await listBoards(projectRoot)).filter(
15695
+ (board) => board.taskCount === 0 && isOwnedSessionBoard(board.tags) && sessionIdFromTags(board.tags) === sessionId
15696
+ );
15697
+ const removed = await Promise.all(
15698
+ candidates.map((board) => removeEmptySessionBoard(projectRoot, board.id, sessionId))
15699
+ );
15700
+ return removed.filter((boardId) => Boolean(boardId));
15701
+ }
15702
+ async function cleanupEmptySessionKanbanBoards(projectRoot, activeSessionId = "") {
15703
+ if (!projectRoot || process.env[MIRROR_DISABLED_ENV] === "0") return [];
15704
+ const candidates = (await listBoards(projectRoot)).flatMap((board) => {
15705
+ const ownerSessionId = sessionIdFromTags(board.tags);
15706
+ return board.taskCount === 0 && isOwnedSessionBoard(board.tags) && ownerSessionId && ownerSessionId !== activeSessionId && !isSessionBoardActive(projectRoot, ownerSessionId) ? [{ boardId: board.id, sessionId: ownerSessionId }] : [];
15707
+ });
15708
+ const removed = await Promise.all(
15709
+ candidates.map(
15710
+ ({ boardId, sessionId }) => removeEmptySessionBoard(projectRoot, boardId, sessionId)
15711
+ )
15712
+ );
15713
+ return removed.filter((boardId) => Boolean(boardId));
15714
+ }
15715
+ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
15716
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
15717
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
15718
+ const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
15719
+ if (!board) return null;
15720
+ const result = await syncBoardFromTaskGraph(
15721
+ projectRoot,
15722
+ board.id,
15723
+ deserializeTaskGraph(graph),
15724
+ {
15725
+ sourceSystem,
15726
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
15727
+ archiveMissingTasks: true,
15728
+ includeCompletedTasks: true
15729
+ }
15730
+ );
15731
+ return result?.board ?? null;
15732
+ });
15733
+ }
15734
+ function todoListToSerializedGraph(todos, sessionId) {
15735
+ const nodes = todos.map((todo, index) => ({
15736
+ id: todo.id,
15737
+ title: todo.content,
15738
+ description: todo.activeForm ?? "",
15739
+ type: "chore",
15740
+ priority: "medium",
15741
+ status: todo.status,
15742
+ createdAt: index,
15743
+ updatedAt: index
15744
+ }));
15745
+ return {
15746
+ id: `todo:${sessionId}`,
15747
+ specId: `todo:${sessionId}`,
15748
+ title: "Session todos",
15749
+ nodes,
15750
+ edges: [],
15751
+ rootNodes: nodes.map((node) => node.id),
15752
+ createdAt: 0,
15753
+ updatedAt: 0
15754
+ };
15755
+ }
15756
+ function taskFileToSerializedGraph(tasks, sessionId) {
15757
+ const ids = new Set(tasks.map((task) => task.id));
15758
+ const nodes = tasks.map((task, index) => ({
15759
+ id: task.id,
15760
+ title: task.title,
15761
+ description: task.description ?? "",
15762
+ type: task.type,
15763
+ priority: task.priority,
15764
+ status: task.status,
15765
+ ...task.assignee ? { assignee: task.assignee } : {},
15766
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
15767
+ createdAt: index,
15768
+ updatedAt: index
15769
+ }));
15770
+ const edges = tasks.flatMap(
15771
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
15772
+ id: `${dependency}->${task.id}`,
15773
+ from: dependency,
15774
+ to: task.id,
15775
+ type: "depends_on"
15776
+ }))
15777
+ );
15778
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
15779
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
15780
+ return {
15781
+ // Keep the historical graph id so existing mirrored task cards are reused.
15782
+ id: `session:${sessionId}`,
15783
+ specId: `session:${sessionId}`,
15784
+ title: "Session tasks",
15785
+ nodes,
15786
+ edges,
15787
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
15788
+ createdAt: 0,
15789
+ updatedAt: 0
15790
+ };
15791
+ }
15792
+ var PLAN_STATUS_TO_TASK = {
15793
+ open: "pending",
15794
+ in_progress: "in_progress",
15795
+ done: "completed"
15796
+ };
15797
+ function planFileToSerializedGraph(items, sessionId) {
15798
+ const nodes = items.map((item, index) => ({
15799
+ id: item.id,
15800
+ title: item.title,
15801
+ description: item.details ?? "",
15802
+ type: "chore",
15803
+ priority: "medium",
15804
+ status: PLAN_STATUS_TO_TASK[item.status],
15805
+ createdAt: index,
15806
+ updatedAt: index
15807
+ }));
15808
+ return {
15809
+ id: `plan:${sessionId}`,
15810
+ specId: `plan:${sessionId}`,
15811
+ title: "Session plan",
15812
+ nodes,
15813
+ edges: [],
15814
+ rootNodes: nodes.map((node) => node.id),
15815
+ createdAt: 0,
15816
+ updatedAt: 0
15817
+ };
15818
+ }
15819
+ function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
15820
+ return projectGraph(
15821
+ projectRoot,
15822
+ sessionId,
15823
+ todoListToSerializedGraph(todos, sessionId),
15824
+ "session-todo"
15825
+ );
15826
+ }
15827
+ function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
15828
+ return projectGraph(
15829
+ projectRoot,
15830
+ sessionId,
15831
+ taskFileToSerializedGraph(tasks, sessionId),
15832
+ "session-task"
15833
+ );
15834
+ }
15835
+ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
15836
+ return projectGraph(
15837
+ projectRoot,
15838
+ sessionId,
15839
+ planFileToSerializedGraph(items, sessionId),
15840
+ "session-plan"
15841
+ );
15842
+ }
15843
+ function fireAndForget(work) {
15844
+ void work.catch(() => {
15845
+ });
15846
+ }
15847
+ function mirrorSessionTodosToKanban(projectRoot, todos, sessionId) {
15848
+ fireAndForget(projectSessionTodosToKanban(projectRoot, todos, sessionId));
15849
+ }
15850
+ function mirrorSessionTasksToKanban(projectRoot, tasks, sessionId) {
15851
+ fireAndForget(projectSessionTasksToKanban(projectRoot, tasks, sessionId));
15852
+ }
15853
+ function mirrorSessionPlanToKanban(projectRoot, items, sessionId) {
15854
+ fireAndForget(projectSessionPlanToKanban(projectRoot, items, sessionId));
15855
+ }
15856
+ function attachSessionKanbanMirror(context) {
15857
+ const existing = bindings.get(context);
15858
+ if (existing) return existing;
15859
+ const attachedProjectRoot = context.projectRoot ?? "";
15860
+ let registeredSessionId = "";
15861
+ const syncActiveSessionRegistration = () => {
15862
+ if (!attachedProjectRoot) return;
15863
+ const currentSessionId = context.session?.id ?? "";
15864
+ if (currentSessionId === registeredSessionId) return;
15865
+ if (registeredSessionId) {
15866
+ releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
15867
+ fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
15868
+ }
15869
+ registeredSessionId = currentSessionId;
15870
+ if (registeredSessionId) {
15871
+ retainActiveSessionBoard(attachedProjectRoot, registeredSessionId);
15872
+ }
15873
+ };
15874
+ syncActiveSessionRegistration();
15875
+ let watcher = null;
15876
+ let watchedDir = "";
15877
+ let timer = null;
15878
+ const sessionId = () => context.session?.id ?? "";
15879
+ const refreshFiles = async () => {
15880
+ const id = sessionId();
15881
+ if (!id) return;
15882
+ const planPath = context.meta["plan.path"];
15883
+ if (typeof planPath === "string" && planPath) {
15884
+ const plan = await loadPlan(planPath);
15885
+ if (plan) await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
15886
+ }
15887
+ const taskPath = context.meta["task.path"];
15888
+ if (typeof taskPath === "string" && taskPath) {
15889
+ const tasks = await loadTasks(taskPath);
15890
+ if (tasks) await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
15891
+ }
15892
+ };
15893
+ const configureWatcher = () => {
15894
+ const planPath = context.meta["plan.path"];
15895
+ const taskPath = context.meta["task.path"];
15896
+ const candidate = typeof planPath === "string" && planPath ? dirname13(planPath) : typeof taskPath === "string" && taskPath ? dirname13(taskPath) : "";
15897
+ if (!candidate || candidate === watchedDir) return;
15898
+ watcher?.close();
15899
+ watcher = null;
15900
+ watchedDir = candidate;
15901
+ try {
15902
+ watcher = watch(candidate, { persistent: false }, (_event, filename) => {
15903
+ const name = filename?.toString();
15904
+ const currentPlanPath = context.meta["plan.path"];
15905
+ const currentTaskPath = context.meta["task.path"];
15906
+ const planName = typeof currentPlanPath === "string" ? basename8(currentPlanPath) : "";
15907
+ const taskName = typeof currentTaskPath === "string" ? basename8(currentTaskPath) : "";
15908
+ if (name && name !== planName && name !== taskName) return;
15909
+ if (timer) clearTimeout(timer);
15910
+ timer = setTimeout(() => fireAndForget(refreshFiles()), 60);
15911
+ });
15912
+ watcher.on("error", () => watcher?.close());
15913
+ } catch {
15914
+ watcher = null;
15915
+ watchedDir = "";
15916
+ }
15917
+ };
15918
+ const unsubscribe = context.state.onChange((change) => {
15919
+ if (change.kind === "todos_replaced" && !suppressedTodoMirrors.has(context)) {
15920
+ mirrorSessionTodosToKanban(
15921
+ context.projectRoot,
15922
+ change.completedSnapshot ?? change.todos,
15923
+ sessionId()
15924
+ );
15925
+ return;
15926
+ }
15927
+ if (change.kind === "meta_set" && (change.key === "plan.path" || change.key === "task.path")) {
15928
+ syncActiveSessionRegistration();
15929
+ configureWatcher();
15930
+ fireAndForget(ensureSessionKanbanBoard(context.projectRoot, sessionId()));
15931
+ fireAndForget(refreshFiles());
15932
+ }
15933
+ });
15934
+ configureWatcher();
15935
+ const detach = () => {
15936
+ unsubscribe();
15937
+ if (timer) clearTimeout(timer);
15938
+ watcher?.close();
15939
+ bindings.delete(context);
15940
+ if (attachedProjectRoot && registeredSessionId) {
15941
+ releaseActiveSessionBoard(attachedProjectRoot, registeredSessionId);
15942
+ fireAndForget(cleanupSessionKanbanBoardIfEmpty(attachedProjectRoot, registeredSessionId));
15943
+ registeredSessionId = "";
15944
+ }
15945
+ };
15946
+ bindings.set(context, detach);
15947
+ return detach;
15948
+ }
15949
+ async function hydrateSessionKanban(context) {
15950
+ const id = context.session?.id ?? "";
15951
+ if (!id) return null;
15952
+ await cleanupEmptySessionKanbanBoards(context.projectRoot, id);
15953
+ let board = await ensureSessionKanbanBoard(context.projectRoot, id);
15954
+ if (context.todos.length) {
15955
+ board = await projectSessionTodosToKanban(context.projectRoot, context.todos, id);
15956
+ }
15957
+ const planPath = context.meta["plan.path"];
15958
+ if (typeof planPath === "string" && planPath) {
15959
+ const plan = await loadPlan(planPath);
15960
+ if (plan) board = await projectSessionPlanToKanban(context.projectRoot, plan.items, id);
15961
+ }
15962
+ const taskPath = context.meta["task.path"];
15963
+ if (typeof taskPath === "string" && taskPath) {
15964
+ const tasks = await loadTasks(taskPath);
15965
+ if (tasks) board = await projectSessionTasksToKanban(context.projectRoot, tasks.tasks, id);
15966
+ }
15967
+ return board;
15968
+ }
15969
+ function sourceStatus(task) {
15970
+ if (task.status === "completed") return "completed";
15971
+ if (task.status === "in_progress") return "in_progress";
15972
+ if (task.status === "review") return "review";
15973
+ if (task.status === "blocked") return "blocked";
15974
+ if (task.status === "failed") return "failed";
15975
+ return "pending";
15976
+ }
15977
+ async function applySessionKanbanTaskToSource(context, task, options = {}) {
15978
+ const originId = task.origin?.taskId;
15979
+ const graphId = task.origin?.graphId ?? "";
15980
+ if (!originId) return { source: null };
15981
+ if (task.origin?.system === "session-todo" || graphId.startsWith("todo:")) {
15982
+ const next = options.remove ? context.todos.filter((todo) => todo.id !== originId) : context.todos.map(
15983
+ (todo) => todo.id === originId ? {
15984
+ ...todo,
15985
+ content: task.title,
15986
+ status: sourceStatus(task) === "completed" ? "completed" : sourceStatus(task) === "in_progress" || sourceStatus(task) === "review" ? "in_progress" : "pending"
15987
+ } : todo
15988
+ );
15989
+ suppressedTodoMirrors.add(context);
15990
+ try {
15991
+ context.state.replaceTodos(next);
15992
+ } finally {
15993
+ suppressedTodoMirrors.delete(context);
15994
+ }
15995
+ return { source: "todo", todos: [...context.todos] };
15996
+ }
15997
+ const id = context.session?.id ?? "";
15998
+ if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
15999
+ const planPath = context.meta["plan.path"];
16000
+ if (typeof planPath !== "string" || !planPath) return { source: "plan" };
16001
+ const plan = await mutatePlan(planPath, id, (file) => ({
16002
+ ...file,
16003
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
16004
+ items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
16005
+ (item) => item.id === originId ? {
16006
+ ...item,
16007
+ title: task.title,
16008
+ details: task.description,
16009
+ status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
16010
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16011
+ } : item
16012
+ )
16013
+ }));
16014
+ return { source: "plan", plan };
16015
+ }
16016
+ if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
16017
+ const taskPath = context.meta["task.path"];
16018
+ if (typeof taskPath !== "string" || !taskPath) return { source: "task" };
16019
+ const tasks = await mutateTasks(taskPath, id, (file) => ({
16020
+ ...file,
16021
+ tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
16022
+ (item) => item.id === originId ? {
16023
+ ...item,
16024
+ title: task.title,
16025
+ description: task.description,
16026
+ status: sourceStatus(task),
16027
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16028
+ } : item
16029
+ )
16030
+ }));
16031
+ return { source: "task", tasks };
16032
+ }
16033
+ return { source: null };
16034
+ }
16035
+
16036
+ // src/kanban.ts
15450
16037
  var kanbanTool = {
15451
16038
  name: "kanban",
15452
16039
  category: "Project",
@@ -15473,6 +16060,8 @@ var kanbanTool = {
15473
16060
  "export_markdown",
15474
16061
  "export_task_graph",
15475
16062
  "sync_task_graph",
16063
+ "create_from_graph",
16064
+ "import_session_tasks",
15476
16065
  "search_tasks",
15477
16066
  "ready_tasks",
15478
16067
  "snapshot",
@@ -15519,6 +16108,10 @@ var kanbanTool = {
15519
16108
  tags: { type: "array", items: { type: "string" } },
15520
16109
  labels: { type: "array", items: { type: "string" } },
15521
16110
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
16111
+ taskType: {
16112
+ type: "string",
16113
+ enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
16114
+ },
15522
16115
  status: {
15523
16116
  type: "string",
15524
16117
  enum: [
@@ -15561,8 +16154,21 @@ var kanbanTool = {
15561
16154
  releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
15562
16155
  releaseReason: { type: "string" },
15563
16156
  clearAssignee: { type: "boolean" },
15564
- recoveryMode: { type: "string", enum: ["release", "retry", "fail"] },
16157
+ recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
15565
16158
  recoveryNow: { type: "string" },
16159
+ recoveryPolicyFailOnCostCeiling: { type: "boolean" },
16160
+ recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
16161
+ recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
16162
+ recoveryPolicyRetryPolicyOverride: {
16163
+ type: "string",
16164
+ enum: ["off", "incremental", "exponential"]
16165
+ },
16166
+ assignee: { type: "string" },
16167
+ costCeilingUsd: { type: "number" },
16168
+ retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
16169
+ lastFailureKind: { type: "string" },
16170
+ dependsOn: { type: "array", items: { type: "string" } },
16171
+ estimatedHours: { type: "number" },
15566
16172
  taskGraph: { type: "object" },
15567
16173
  graphId: { type: "string" },
15568
16174
  specId: { type: "string" },
@@ -15618,7 +16224,7 @@ var kanbanTool = {
15618
16224
  try {
15619
16225
  switch (input.action) {
15620
16226
  case "list_boards": {
15621
- const boards = await listBoards(projectRoot);
16227
+ const boards = await listBoards2(projectRoot);
15622
16228
  return { ok: true, message: `${boards.length} board(s).`, boards };
15623
16229
  }
15624
16230
  case "get_board": {
@@ -15627,7 +16233,7 @@ var kanbanTool = {
15627
16233
  }
15628
16234
  case "create_board": {
15629
16235
  if (!input.title) return fail("create_board requires title.");
15630
- const board = await createBoard(projectRoot, {
16236
+ const board = await createBoard2(projectRoot, {
15631
16237
  title: input.title,
15632
16238
  ...input.description !== void 0 ? { description: input.description } : {},
15633
16239
  ...input.tags !== void 0 ? { tags: input.tags } : {},
@@ -15637,7 +16243,7 @@ var kanbanTool = {
15637
16243
  }
15638
16244
  case "update_board": {
15639
16245
  if (!input.boardId) return fail("update_board requires boardId.");
15640
- const board = await updateBoard(projectRoot, input.boardId, {
16246
+ const board = await updateBoard2(projectRoot, input.boardId, {
15641
16247
  ...input.title !== void 0 ? { title: input.title } : {},
15642
16248
  ...input.description !== void 0 ? { description: input.description } : {},
15643
16249
  ...input.tags !== void 0 ? { tags: input.tags } : {}
@@ -15657,7 +16263,7 @@ var kanbanTool = {
15657
16263
  }
15658
16264
  case "delete_board": {
15659
16265
  if (!input.boardId) return fail("delete_board requires boardId.");
15660
- const removed = await removeBoard(projectRoot, input.boardId);
16266
+ const removed = await removeBoard2(projectRoot, input.boardId);
15661
16267
  return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
15662
16268
  }
15663
16269
  case "generate_board": {
@@ -15668,14 +16274,14 @@ var kanbanTool = {
15668
16274
  ...input.context !== void 0 ? { context: input.context } : {},
15669
16275
  ...input.columns !== void 0 ? { columns: input.columns } : {}
15670
16276
  });
15671
- const board = await createBoard(projectRoot, boardInput);
16277
+ const board = await createBoard2(projectRoot, boardInput);
15672
16278
  for (const taskInput2 of parseLinesIntoTasks(
15673
16279
  input.description,
15674
16280
  board.columns[0]?.id ?? "backlog"
15675
16281
  )) {
15676
16282
  await addTask(projectRoot, board.id, taskInput2);
15677
16283
  }
15678
- return okBoard(await getBoard(projectRoot, board.id) ?? board, "Board generated.");
16284
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
15679
16285
  }
15680
16286
  case "export_markdown": {
15681
16287
  const board = await requireBoard(projectRoot, input.boardId);
@@ -15708,8 +16314,8 @@ var kanbanTool = {
15708
16314
  if (!input.boardId || !input.taskGraph) {
15709
16315
  return fail("sync_task_graph requires boardId and taskGraph.");
15710
16316
  }
15711
- const graph = deserializeTaskGraph(input.taskGraph);
15712
- const result = await syncBoardFromTaskGraph(projectRoot, input.boardId, graph, {
16317
+ const graph = deserializeTaskGraph2(input.taskGraph);
16318
+ const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
15713
16319
  ...input.title !== void 0 ? { title: input.title } : {},
15714
16320
  ...input.description !== void 0 ? { description: input.description } : {},
15715
16321
  ...input.tags !== void 0 ? { tags: input.tags } : {},
@@ -15726,6 +16332,59 @@ var kanbanTool = {
15726
16332
  board: result.board
15727
16333
  } : fail("Board not found.");
15728
16334
  }
16335
+ case "create_from_graph": {
16336
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
16337
+ const graph = deserializeTaskGraph2(input.taskGraph);
16338
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16339
+ ...input.title !== void 0 ? { title: input.title } : {},
16340
+ ...input.description !== void 0 ? { description: input.description } : {},
16341
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
16342
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
16343
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
16344
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
16345
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
16346
+ });
16347
+ return {
16348
+ ok: true,
16349
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
16350
+ board
16351
+ };
16352
+ }
16353
+ case "import_session_tasks": {
16354
+ const taskPath = ctx.meta?.["task.path"];
16355
+ if (!taskPath) return fail("No session task file for this session.");
16356
+ const file = await loadTasks2(taskPath);
16357
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
16358
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
16359
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
16360
+ const tags = ["session", `session:${sessionId}`];
16361
+ const existing = (await listBoards2(projectRoot)).find(
16362
+ (b) => b.tags?.includes(`session:${sessionId}`)
16363
+ );
16364
+ if (existing) {
16365
+ const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
16366
+ sourceSystem: "session",
16367
+ tags,
16368
+ archiveMissingTasks: true,
16369
+ includeCompletedTasks: true
16370
+ });
16371
+ return result ? {
16372
+ ok: true,
16373
+ message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
16374
+ board: result.board
16375
+ } : fail("Session board vanished mid-sync.");
16376
+ }
16377
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
16378
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
16379
+ sourceSystem: "session",
16380
+ tags
16381
+ });
16382
+ return {
16383
+ ok: true,
16384
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
16385
+ board
16386
+ };
16387
+ }
15729
16388
  case "search_tasks": {
15730
16389
  const tasks = await searchKanban(projectRoot, {
15731
16390
  query: input.query,
@@ -16162,7 +16821,7 @@ function okTask(board, task, message) {
16162
16821
  return { ok: true, message, board, task };
16163
16822
  }
16164
16823
  async function requireBoard(projectRoot, boardId) {
16165
- return boardId ? getBoard(projectRoot, boardId) : null;
16824
+ return boardId ? getBoard2(projectRoot, boardId) : null;
16166
16825
  }
16167
16826
  function taskInput(input) {
16168
16827
  const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
@@ -16171,14 +16830,23 @@ function taskInput(input) {
16171
16830
  columnId: input.columnId,
16172
16831
  description: input.description,
16173
16832
  priority: input.priority,
16833
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
16174
16834
  status: input.status,
16175
16835
  labels: input.labels,
16176
16836
  ...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
16177
16837
  ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
16178
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {},
16838
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16839
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
16179
16840
  ...assignment ? { assignment } : {}
16180
16841
  };
16181
16842
  }
16843
+ function mergedDependsOn(input) {
16844
+ const ids = [
16845
+ ...input.dependsOn ?? [],
16846
+ ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
16847
+ ].filter((id, i, arr) => id && arr.indexOf(id) === i);
16848
+ return ids.length > 0 ? ids : void 0;
16849
+ }
16182
16850
  function taskPatch(input) {
16183
16851
  return {
16184
16852
  title: input.title,
@@ -16186,10 +16854,12 @@ function taskPatch(input) {
16186
16854
  columnId: input.columnId,
16187
16855
  order: input.order,
16188
16856
  priority: input.priority,
16857
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
16189
16858
  status: input.status,
16190
16859
  labels: input.labels,
16191
16860
  assignedAgent: input.agentId,
16192
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {}
16861
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16862
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
16193
16863
  };
16194
16864
  }
16195
16865
  function assignmentInput(input) {
@@ -16934,12 +17604,12 @@ import {
16934
17604
  deriveTodosFromPlanItem,
16935
17605
  formatPlan,
16936
17606
  getPlanTemplate,
16937
- mutatePlan,
17607
+ mutatePlan as mutatePlan2,
16938
17608
  removePlanItem,
16939
17609
  setPlanItemStatus
16940
17610
  } from "@wrongstack/core";
16941
17611
  import {
16942
- mutateTasks,
17612
+ mutateTasks as mutateTasks2,
16943
17613
  formatTaskList
16944
17614
  } from "@wrongstack/core";
16945
17615
  import { randomUUID } from "node:crypto";
@@ -17026,7 +17696,7 @@ var planTool = {
17026
17696
  let didTaskify = false;
17027
17697
  let plan;
17028
17698
  try {
17029
- plan = await mutatePlan(planPath, sessionId, async (p) => {
17699
+ plan = await mutatePlan2(planPath, sessionId, async (p) => {
17030
17700
  switch (input.action) {
17031
17701
  case "show":
17032
17702
  break;
@@ -17152,6 +17822,7 @@ var planTool = {
17152
17822
  open: 0
17153
17823
  };
17154
17824
  }
17825
+ await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
17155
17826
  if (early) return early;
17156
17827
  if (didTaskify) {
17157
17828
  const taskPathRaw = ctx.meta["task.path"];
@@ -17165,7 +17836,7 @@ var planTool = {
17165
17836
  }
17166
17837
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
17167
17838
  try {
17168
- const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
17839
+ const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
17169
17840
  f.tasks.push({
17170
17841
  id: `task_${randomUUID()}`,
17171
17842
  title: taskifyMeta.title,
@@ -18302,11 +18973,11 @@ import {
18302
18973
  formatTaskList as formatTaskList2
18303
18974
  } from "@wrongstack/core";
18304
18975
  import {
18305
- mutateTasks as mutateTasks2
18976
+ mutateTasks as mutateTasks3
18306
18977
  } from "@wrongstack/core";
18307
18978
  import {
18308
18979
  addPlanItem as addPlanItem2,
18309
- mutatePlan as mutatePlan2,
18980
+ mutatePlan as mutatePlan3,
18310
18981
  formatPlan as formatPlan2
18311
18982
  } from "@wrongstack/core";
18312
18983
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -18426,7 +19097,7 @@ var taskTool = {
18426
19097
  let todosToReplace = null;
18427
19098
  let file;
18428
19099
  try {
18429
- file = await mutateTasks2(taskPath, sessionId, async (f) => {
19100
+ file = await mutateTasks3(taskPath, sessionId, async (f) => {
18430
19101
  switch (input.action) {
18431
19102
  case "show":
18432
19103
  break;
@@ -18616,6 +19287,7 @@ var taskTool = {
18616
19287
  };
18617
19288
  }
18618
19289
  if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
19290
+ await projectSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);
18619
19291
  if (early) return early;
18620
19292
  if (didPlanify) {
18621
19293
  const { title, details } = planifyMeta;
@@ -18629,7 +19301,7 @@ var taskTool = {
18629
19301
  }
18630
19302
  let formatted = "";
18631
19303
  try {
18632
- await mutatePlan2(planPath, sessionId, (pf) => {
19304
+ await mutatePlan3(planPath, sessionId, (pf) => {
18633
19305
  const { plan: updated } = addPlanItem2(pf, title, details || void 0);
18634
19306
  formatted = formatPlan2(updated);
18635
19307
  return updated;
@@ -18861,8 +19533,7 @@ function parseResult(runner, result, duration) {
18861
19533
  }
18862
19534
 
18863
19535
  // src/todo.ts
18864
- import { loadPlan, savePlan, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
18865
- import { loadTasks, saveTasks } from "@wrongstack/core";
19536
+ import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
18866
19537
  var todoTool = {
18867
19538
  name: "todo",
18868
19539
  category: "Session",
@@ -18871,7 +19542,7 @@ var todoTool = {
18871
19542
  permission: "auto",
18872
19543
  mutating: false,
18873
19544
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
18874
- timeoutMs: 1e3,
19545
+ timeoutMs: 5e3,
18875
19546
  capabilities: ["session.todo"],
18876
19547
  icon: "todo",
18877
19548
  inputSchema: {
@@ -18923,16 +19594,21 @@ var todoTool = {
18923
19594
  }
18924
19595
  }
18925
19596
  ctx.state.replaceTodos(items);
19597
+ await projectSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
18926
19598
  const completedPlanIds = /* @__PURE__ */ new Set();
18927
19599
  const completedTaskIds = /* @__PURE__ */ new Set();
18928
19600
  const pendingPlanIds = /* @__PURE__ */ new Set();
18929
19601
  const pendingTaskIds = /* @__PURE__ */ new Set();
18930
19602
  for (const item of items) {
18931
19603
  if (item.promotedFromPlan) {
18932
- (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
19604
+ (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
19605
+ item.promotedFromPlan
19606
+ );
18933
19607
  }
18934
19608
  if (item.promotedFromTask) {
18935
- (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
19609
+ (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
19610
+ item.promotedFromTask
19611
+ );
18936
19612
  }
18937
19613
  }
18938
19614
  for (const planId of completedPlanIds) {
@@ -18940,7 +19616,7 @@ var todoTool = {
18940
19616
  const planPath = ctx.meta["plan.path"];
18941
19617
  if (typeof planPath !== "string" || !planPath) continue;
18942
19618
  try {
18943
- const plan = await loadPlan(planPath);
19619
+ const plan = await loadPlan2(planPath);
18944
19620
  if (plan) {
18945
19621
  const updated = setPlanItemStatus2(plan, planId, "done");
18946
19622
  await savePlan(planPath, updated);
@@ -18953,7 +19629,7 @@ var todoTool = {
18953
19629
  const taskPath = ctx.meta["task.path"];
18954
19630
  if (typeof taskPath !== "string" || !taskPath) continue;
18955
19631
  try {
18956
- const file = await loadTasks(taskPath);
19632
+ const file = await loadTasks3(taskPath);
18957
19633
  if (file) {
18958
19634
  const task = file.tasks.find((t) => t.id === taskId);
18959
19635
  if (task && task.status !== "completed") {
@@ -20922,7 +21598,13 @@ var TOOL_ICON_MAP = {
20922
21598
  think: "brain",
20923
21599
  reason: "brain",
20924
21600
  analyze: "brain",
20925
- reasoning: "brain"
21601
+ reasoning: "brain",
21602
+ // Language intelligence
21603
+ lsp_diagnostics: "code",
21604
+ lsp_definition: "search",
21605
+ lsp_completion: "code",
21606
+ lsp_rename: "edit",
21607
+ "codebase-lsp-search": "index"
20926
21608
  };
20927
21609
  function getToolIcon(toolName) {
20928
21610
  return TOOL_ICON_MAP[toolName.toLowerCase()] ?? "fallback";
@@ -20991,13 +21673,16 @@ export {
20991
21673
  LanguageProfileRegistry,
20992
21674
  OPTIONAL_TOOLS,
20993
21675
  PRIMARY_LANGUAGE_PROFILES,
21676
+ SESSION_KANBAN_COLUMNS,
20994
21677
  TIER1_TOOLS,
20995
21678
  TIER2_TOOLS,
20996
21679
  TIER3_TOOLS,
20997
21680
  TOOL_ICON_CONFIG,
20998
21681
  TOOL_ICON_MAP,
20999
21682
  _resetProcessRegistry,
21683
+ applySessionKanbanTaskToSource,
21000
21684
  assertBrowserUrlAllowed,
21685
+ attachSessionKanbanMirror,
21001
21686
  auditTool,
21002
21687
  bashTool,
21003
21688
  batchToolUseTool,
@@ -21040,6 +21725,7 @@ export {
21040
21725
  e2ePlanTool,
21041
21726
  editTool,
21042
21727
  enqueueReindex,
21728
+ ensureSessionKanbanBoard,
21043
21729
  ensureSessionShell,
21044
21730
  execTool,
21045
21731
  executeLanguagePlan,
@@ -21061,6 +21747,7 @@ export {
21061
21747
  gitTool,
21062
21748
  globTool,
21063
21749
  grepTool,
21750
+ hydrateSessionKanban,
21064
21751
  indexCircuitBreaker,
21065
21752
  installTool,
21066
21753
  isExecCommandAllowed,
@@ -21077,6 +21764,9 @@ export {
21077
21764
  listInstances,
21078
21765
  logsTool,
21079
21766
  makeSkillTool,
21767
+ mirrorSessionPlanToKanban,
21768
+ mirrorSessionTasksToKanban,
21769
+ mirrorSessionTodosToKanban,
21080
21770
  normalizeShell,
21081
21771
  onIndexStateChange,
21082
21772
  outdatedTool,
@@ -21086,6 +21776,9 @@ export {
21086
21776
  patchTool,
21087
21777
  planLanguageOperation,
21088
21778
  planTool,
21779
+ projectSessionPlanToKanban,
21780
+ projectSessionTasksToKanban,
21781
+ projectSessionTodosToKanban,
21089
21782
  readTool,
21090
21783
  redactBrowserText,
21091
21784
  relatedMemoryTool,