@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/pack.js CHANGED
@@ -6054,6 +6054,9 @@ ${hint}` : ""),
6054
6054
  };
6055
6055
 
6056
6056
  // src/batch-tool-use.ts
6057
+ import {
6058
+ GOVERNED_TOOL_EXECUTOR_META_KEY
6059
+ } from "@wrongstack/core";
6057
6060
  var batchToolUseTool = {
6058
6061
  name: "batch_tool_use",
6059
6062
  category: "Meta",
@@ -6090,7 +6093,7 @@ var batchToolUseTool = {
6090
6093
  },
6091
6094
  required: ["calls"]
6092
6095
  },
6093
- async execute(input, ctx, opts) {
6096
+ async execute(input, ctx, _opts) {
6094
6097
  if (!input?.calls || input.calls.length === 0) {
6095
6098
  return {
6096
6099
  results: [],
@@ -6100,18 +6103,33 @@ var batchToolUseTool = {
6100
6103
  stop_on_error: false
6101
6104
  };
6102
6105
  }
6106
+ const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
6107
+ if (typeof governedExecute !== "function") {
6108
+ return {
6109
+ results: input.calls.map((call) => ({
6110
+ tool: call.tool,
6111
+ success: false,
6112
+ error: "governed nested execution is unavailable; call the tool directly",
6113
+ executionMs: 0
6114
+ })),
6115
+ total: input.calls.length,
6116
+ succeeded: 0,
6117
+ failed: input.calls.length,
6118
+ stop_on_error: input.stop_on_error ?? false
6119
+ };
6120
+ }
6103
6121
  const results = [];
6104
6122
  let succeeded = 0;
6105
6123
  let failed = 0;
6106
6124
  if (input.parallel !== false) {
6107
- const promises = input.calls.map(async (call) => executeSingle(call, ctx, opts));
6125
+ const promises = input.calls.map(async (call) => executeSingle(call, ctx, governedExecute));
6108
6126
  const allResults = await Promise.all(promises);
6109
6127
  results.push(...allResults);
6110
6128
  succeeded = allResults.filter((r) => r.success).length;
6111
6129
  failed = allResults.filter((r) => !r.success).length;
6112
6130
  } else {
6113
6131
  for (const call of input.calls) {
6114
- const result = await executeSingle(call, ctx, opts ?? { signal: void 0 });
6132
+ const result = await executeSingle(call, ctx, governedExecute);
6115
6133
  results.push(result);
6116
6134
  if (result.success) {
6117
6135
  succeeded++;
@@ -6130,9 +6148,9 @@ var batchToolUseTool = {
6130
6148
  };
6131
6149
  }
6132
6150
  };
6133
- async function executeSingle(call, ctx, opts) {
6151
+ async function executeSingle(call, ctx, governedExecute) {
6134
6152
  const start = Date.now();
6135
- const tool = ctx.tools.find((t) => t.name === call.tool);
6153
+ const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
6136
6154
  if (!tool) {
6137
6155
  return {
6138
6156
  tool: call.tool,
@@ -6142,11 +6160,11 @@ async function executeSingle(call, ctx, opts) {
6142
6160
  };
6143
6161
  }
6144
6162
  try {
6145
- const result = await tool.execute(call.input, ctx, opts);
6163
+ const result = await governedExecute(call.tool, call.input);
6146
6164
  return {
6147
6165
  tool: call.tool,
6148
- success: true,
6149
- result,
6166
+ success: result.success,
6167
+ ...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
6150
6168
  executionMs: Date.now() - start
6151
6169
  };
6152
6170
  } catch (e) {
@@ -6556,7 +6574,7 @@ var BrowserSessionManager = class {
6556
6574
  constructor(options, launcher = defaultLauncher) {
6557
6575
  this.launcher = launcher;
6558
6576
  this.artifacts = new BrowserArtifactStore(options.artifactRoot);
6559
- this.allowPrivateHosts = options.allowPrivateHosts ?? false;
6577
+ this.allowPrivateHosts = options.allowPrivateHosts ?? true;
6560
6578
  this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
6561
6579
  this.networkProxy = new BrowserNetworkGuardProxy({
6562
6580
  allowPrivateHosts: this.allowPrivateHosts,
@@ -8519,6 +8537,7 @@ function detectLang(file) {
8519
8537
  }
8520
8538
 
8521
8539
  // src/codebase-index/go-parser.ts
8540
+ init_win32_resolve();
8522
8541
  import { spawn as spawn4 } from "node:child_process";
8523
8542
  import * as os5 from "node:os";
8524
8543
  import * as path14 from "node:path";
@@ -8827,27 +8846,42 @@ async function syncGoParse(filePath, content, lang) {
8827
8846
  try {
8828
8847
  const scriptPath = path14.join(tmpDir, "parse.go");
8829
8848
  await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
8830
- const proc = spawn4("go", ["run", scriptPath], {
8831
- stdio: ["pipe", "pipe", "pipe"],
8832
- windowsHide: true
8833
- });
8834
- let stdout = "";
8835
- proc.stdout?.on("data", (chunk) => {
8836
- stdout += chunk.toString();
8837
- });
8838
- proc.stdin?.write(content);
8839
- proc.stdin?.end();
8840
- const { code } = await Promise.race([
8841
- new Promise((resolve14) => {
8842
- proc.on("close", (c) => resolve14({ code: c }));
8843
- }),
8844
- new Promise(
8845
- (_, reject) => setTimeout(() => {
8849
+ const goBinary = resolveWin32Command("go");
8850
+ const goResult = await new Promise(
8851
+ (resolve14, reject) => {
8852
+ let settled = false;
8853
+ const proc = spawn4(goBinary, ["run", scriptPath], {
8854
+ stdio: ["pipe", "pipe", "pipe"],
8855
+ windowsHide: true
8856
+ });
8857
+ proc.on("error", (err) => {
8858
+ if (settled) return;
8859
+ settled = true;
8860
+ reject(err);
8861
+ });
8862
+ let stdout2 = "";
8863
+ proc.stdout?.on("data", (chunk) => {
8864
+ stdout2 += chunk.toString();
8865
+ });
8866
+ proc.stderr?.resume();
8867
+ proc.stdin?.write(content);
8868
+ proc.stdin?.end();
8869
+ const timer = setTimeout(() => {
8870
+ if (settled) return;
8871
+ settled = true;
8846
8872
  proc.kill("SIGKILL");
8847
8873
  reject(new Error("timeout"));
8848
- }, 15e3)
8849
- )
8850
- ]).catch(() => ({ code: -1 }));
8874
+ }, 15e3);
8875
+ timer.unref?.();
8876
+ proc.on("close", (code2) => {
8877
+ if (settled) return;
8878
+ settled = true;
8879
+ clearTimeout(timer);
8880
+ resolve14({ code: code2, stdout: stdout2 });
8881
+ });
8882
+ }
8883
+ );
8884
+ const { code, stdout } = goResult;
8851
8885
  if (code !== 0 || !stdout.trim()) {
8852
8886
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
8853
8887
  }
@@ -8874,7 +8908,8 @@ async function syncGoParse(filePath, content, lang) {
8874
8908
  }
8875
8909
 
8876
8910
  // src/codebase-index/py-parser.ts
8877
- import { spawn as spawn5 } from "node:child_process";
8911
+ init_win32_resolve();
8912
+ import { spawn as spawn5, spawnSync } from "node:child_process";
8878
8913
  import * as fs11 from "node:fs/promises";
8879
8914
  import * as os6 from "node:os";
8880
8915
  import * as path15 from "node:path";
@@ -9089,36 +9124,74 @@ visitor.visit(tree)
9089
9124
 
9090
9125
  print(json.dumps([s.to_dict() for s in syms]))
9091
9126
  `;
9092
- var _cachedScriptPath = null;
9093
- async function syncPyParse(filePath, content, lang) {
9094
- try {
9095
- if (!_cachedScriptPath) {
9096
- const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
9097
- await fs11.mkdir(tmpDir, { recursive: true });
9098
- _cachedScriptPath = path15.join(tmpDir, "parse.py");
9099
- await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9100
- }
9101
- const proc = spawn5("python", [_cachedScriptPath, filePath], {
9127
+ function resolvePython() {
9128
+ const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
9129
+ for (const name of candidates) {
9130
+ const resolved = resolveWin32Command(name);
9131
+ const result = spawnSync(resolved, ["--version"], {
9132
+ stdio: "pipe",
9133
+ timeout: 5e3
9134
+ });
9135
+ if (result.error) continue;
9136
+ if (result.status !== 0) continue;
9137
+ return resolved;
9138
+ }
9139
+ return null;
9140
+ }
9141
+ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
9142
+ return new Promise((resolve14, reject) => {
9143
+ let settled = false;
9144
+ const proc = spawn5(pyBinary, [scriptPath, filePath], {
9102
9145
  stdio: ["pipe", "pipe", "pipe"],
9103
9146
  windowsHide: true
9104
9147
  });
9148
+ proc.on("error", (err) => {
9149
+ if (settled) return;
9150
+ settled = true;
9151
+ reject(err);
9152
+ });
9105
9153
  proc.stdin?.write(content);
9106
9154
  proc.stdin?.end();
9107
9155
  let stdout = "";
9108
9156
  proc.stdout?.on("data", (chunk) => {
9109
9157
  stdout += chunk.toString();
9110
9158
  });
9111
- const { code } = await Promise.race([
9112
- new Promise((resolve14) => {
9113
- proc.on("close", (c) => resolve14({ code: c }));
9114
- }),
9115
- new Promise(
9116
- (_, reject) => setTimeout(() => {
9117
- proc.kill("SIGKILL");
9118
- reject(new Error("timeout"));
9119
- }, 15e3)
9120
- )
9121
- ]).catch(() => ({ code: -1 }));
9159
+ proc.stderr?.resume();
9160
+ const timer = setTimeout(() => {
9161
+ if (settled) return;
9162
+ settled = true;
9163
+ proc.kill("SIGKILL");
9164
+ reject(new Error("timeout"));
9165
+ }, 15e3);
9166
+ timer.unref?.();
9167
+ proc.on("close", (code) => {
9168
+ if (settled) return;
9169
+ settled = true;
9170
+ clearTimeout(timer);
9171
+ resolve14({ code, stdout });
9172
+ });
9173
+ });
9174
+ }
9175
+ var _cachedScriptPath = null;
9176
+ var _cachedPyBinary = null;
9177
+ async function syncPyParse(filePath, content, lang) {
9178
+ try {
9179
+ if (!_cachedScriptPath) {
9180
+ const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
9181
+ await fs11.mkdir(tmpDir, { recursive: true });
9182
+ _cachedScriptPath = path15.join(tmpDir, "parse.py");
9183
+ await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9184
+ }
9185
+ if (!_cachedPyBinary) {
9186
+ _cachedPyBinary = resolvePython();
9187
+ if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9188
+ }
9189
+ const { code, stdout } = await spawnPyParser(
9190
+ _cachedPyBinary,
9191
+ _cachedScriptPath,
9192
+ filePath,
9193
+ content
9194
+ );
9122
9195
  if (code !== 0 || !stdout.trim()) {
9123
9196
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9124
9197
  }
@@ -9143,6 +9216,7 @@ async function syncPyParse(filePath, content, lang) {
9143
9216
  }
9144
9217
 
9145
9218
  // src/codebase-index/rs-parser.ts
9219
+ init_win32_resolve();
9146
9220
  import { expectDefined as expectDefined3 } from "@wrongstack/core";
9147
9221
  import { execFileSync, spawn as spawn6 } from "node:child_process";
9148
9222
  import * as fs12 from "node:fs/promises";
@@ -9187,30 +9261,45 @@ async function tryNativeParse(file, content) {
9187
9261
  const crateDir = path16.join(toolsDir, "syn-parser");
9188
9262
  const tmpFile = path16.join(crateDir, "src", "input.rs");
9189
9263
  await fs12.writeFile(tmpFile, content, "utf8");
9190
- const proc = spawn6(
9191
- "cargo",
9192
- ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
9193
- {
9194
- cwd: process.cwd(),
9195
- stdio: ["pipe", "pipe", "pipe"],
9196
- windowsHide: true
9197
- }
9198
- );
9199
- let stdout = "";
9200
- proc.stdout?.on("data", (chunk) => {
9201
- stdout += chunk.toString();
9202
- });
9203
- const { code } = await Promise.race([
9204
- new Promise((resolve14) => {
9205
- proc.on("close", (c) => resolve14({ code: c }));
9206
- }),
9207
- new Promise(
9208
- (_, reject) => setTimeout(() => {
9264
+ const cargoBinary = resolveWin32Command("cargo");
9265
+ const result = await new Promise(
9266
+ (resolve14, reject) => {
9267
+ let settled = false;
9268
+ const proc = spawn6(
9269
+ cargoBinary,
9270
+ ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
9271
+ {
9272
+ cwd: process.cwd(),
9273
+ stdio: ["pipe", "pipe", "pipe"],
9274
+ windowsHide: true
9275
+ }
9276
+ );
9277
+ proc.on("error", (err) => {
9278
+ if (settled) return;
9279
+ settled = true;
9280
+ reject(err);
9281
+ });
9282
+ let stdout2 = "";
9283
+ proc.stdout?.on("data", (chunk) => {
9284
+ stdout2 += chunk.toString();
9285
+ });
9286
+ proc.stderr?.resume();
9287
+ const timer = setTimeout(() => {
9288
+ if (settled) return;
9289
+ settled = true;
9209
9290
  proc.kill("SIGKILL");
9210
9291
  reject(new Error("timeout"));
9211
- }, 15e3)
9212
- )
9213
- ]).catch(() => ({ code: -1 }));
9292
+ }, 15e3);
9293
+ timer.unref?.();
9294
+ proc.on("close", (c) => {
9295
+ if (settled) return;
9296
+ settled = true;
9297
+ clearTimeout(timer);
9298
+ resolve14({ code: c, stdout: stdout2 });
9299
+ });
9300
+ }
9301
+ );
9302
+ const { code, stdout } = result;
9214
9303
  if (code === 0 && stdout.trim()) {
9215
9304
  const symbols = JSON.parse(stdout.trim());
9216
9305
  return {
@@ -11570,25 +11659,46 @@ function findLadderMatches(fileLf, oldLf) {
11570
11659
  const needleLines = oldLf.split("\n");
11571
11660
  if (needleLines.length > fileLines.length) return void 0;
11572
11661
  const offsets = lineOffsets(fileLines);
11662
+ const fileTrimEnd = fileLines.map((l) => l.trimEnd());
11663
+ const needleTrimEnd = needleLines.map((l) => l.trimEnd());
11573
11664
  const trailing = windowScan(
11574
- fileLines,
11575
- needleLines,
11665
+ fileTrimEnd,
11666
+ needleTrimEnd,
11576
11667
  offsets,
11577
- (a, b) => a.trimEnd() === b.trimEnd()
11668
+ fileLines,
11669
+ (a, b) => a === b
11578
11670
  );
11579
11671
  if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
11580
- const normalizedLen = needleLines.reduce((n, l) => n + l.trim().length, 0);
11672
+ const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);
11581
11673
  if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
11582
- const normalized = windowScan(fileLines, needleLines, offsets, (a, b) => a.trim() === b.trim());
11674
+ const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());
11675
+ const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());
11676
+ const normalized = windowScan(
11677
+ fileTrimmed,
11678
+ needleTrimmed,
11679
+ offsets,
11680
+ fileLines,
11681
+ (a, b) => a === b
11682
+ );
11583
11683
  if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
11584
11684
  return fuzzyScan(fileLines, needleLines, offsets);
11585
11685
  }
11586
11686
  function lineAt(text, pos) {
11687
+ if (pos < 512) {
11688
+ let line2 = 1;
11689
+ for (let i = 0; i < pos; i++) {
11690
+ if (text.charCodeAt(i) === 10) line2++;
11691
+ }
11692
+ return line2;
11693
+ }
11587
11694
  let line = 1;
11588
- for (let i = 0; i < pos; i++) {
11589
- if (text.charCodeAt(i) === 10) line++;
11695
+ let search = 0;
11696
+ while (true) {
11697
+ const idx = text.indexOf("\n", search);
11698
+ if (idx === -1 || idx >= pos) return line;
11699
+ search = idx + 1;
11700
+ line++;
11590
11701
  }
11591
- return line;
11592
11702
  }
11593
11703
  function lineOffsets(lines) {
11594
11704
  const out = new Array(lines.length);
@@ -11607,19 +11717,19 @@ function windowToMatch(fileLines, offsets, start, windowLen) {
11607
11717
  startLine: start + 1
11608
11718
  };
11609
11719
  }
11610
- function windowScan(fileLines, needleLines, offsets, eq) {
11720
+ function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
11611
11721
  const n = needleLines.length;
11612
11722
  const out = [];
11613
- for (let i = 0; i + n <= fileLines.length; i++) {
11723
+ for (let i = 0; i + n <= comparisonLines.length; i++) {
11614
11724
  let all = true;
11615
11725
  for (let j = 0; j < n; j++) {
11616
- if (!eq(fileLines[i + j], needleLines[j])) {
11726
+ if (!eq(comparisonLines[i + j], needleLines[j])) {
11617
11727
  all = false;
11618
11728
  break;
11619
11729
  }
11620
11730
  }
11621
11731
  if (all) {
11622
- out.push(windowToMatch(fileLines, offsets, i, n));
11732
+ out.push(windowToMatch(originalLines, offsets, i, n));
11623
11733
  i += n - 1;
11624
11734
  }
11625
11735
  }
@@ -11941,8 +12051,10 @@ var editTool = {
11941
12051
  note: autoReadNote
11942
12052
  };
11943
12053
  }
12054
+ opts?.signal?.throwIfAborted();
11944
12055
  const ladder = findLadderMatches(fileLf, oldLf);
11945
12056
  if (!ladder) {
12057
+ opts?.signal?.throwIfAborted();
11946
12058
  const hint = nearestMatchHint(fileLf, oldLf);
11947
12059
  throw new ToolValidationError({
11948
12060
  message: `edit: no match for old_string in "${input.path}".${hint ? ` Nearest match near line ${hint.line}:
@@ -12010,6 +12122,7 @@ Compare this against your old_string and retry with the file's actual text.` : "
12010
12122
  before: original,
12011
12123
  after: newFile
12012
12124
  });
12125
+ opts?.signal?.throwIfAborted();
12013
12126
  const diff = unifiedDiff(original, newFile, {
12014
12127
  fromFile: input.path,
12015
12128
  toFile: input.path
@@ -15271,7 +15384,7 @@ function toYaml(data, indent = 0) {
15271
15384
  }
15272
15385
 
15273
15386
  // src/kanban.ts
15274
- import { deserializeTaskGraph, serializeTaskGraph } from "@wrongstack/core";
15387
+ import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
15275
15388
  import {
15276
15389
  addCheckToTask,
15277
15390
  addColumn,
@@ -15283,18 +15396,19 @@ import {
15283
15396
  assignTask,
15284
15397
  claimReadyTask,
15285
15398
  copyTaskToBoard,
15286
- createBoard,
15399
+ createBoard as createBoard2,
15400
+ createBoardFromTaskGraph,
15287
15401
  duplicateBoard,
15288
15402
  exportBoardAsMarkdown,
15289
15403
  exportBoardToTaskGraph,
15290
15404
  generateBoardFromDescription,
15291
- getBoard,
15405
+ getBoard as getBoard2,
15292
15406
  getKanbanOrchestrationSnapshot,
15293
15407
  getKanbanQueueHealth,
15294
15408
  getTask,
15295
15409
  getTaskChain,
15296
15410
  heartbeatTaskAssignment,
15297
- listBoards,
15411
+ listBoards as listBoards2,
15298
15412
  listKanbanEvents,
15299
15413
  listReadyTasks,
15300
15414
  mergeTasks,
@@ -15302,21 +15416,244 @@ import {
15302
15416
  parseLinesIntoTasks,
15303
15417
  recoverStaleTaskAssignments,
15304
15418
  releaseTaskClaim,
15305
- removeBoard,
15419
+ removeBoard as removeBoard2,
15306
15420
  removeColumn,
15307
15421
  removeTask,
15308
15422
  searchKanban,
15309
15423
  setTaskChain,
15310
15424
  splitTask,
15311
- syncBoardFromTaskGraph,
15425
+ syncBoardFromTaskGraph as syncBoardFromTaskGraph2,
15312
15426
  transferTaskToBoard,
15313
- updateBoard,
15427
+ updateBoard as updateBoard2,
15314
15428
  updateCheckOnTask,
15315
15429
  updateColumn,
15316
15430
  updateGoalMetricOnTask,
15317
15431
  updateTask,
15318
15432
  updateTaskAssignment
15319
15433
  } from "@wrongstack/kanban";
15434
+
15435
+ // src/session-kanban.ts
15436
+ import {
15437
+ deserializeTaskGraph,
15438
+ loadPlan,
15439
+ loadTasks,
15440
+ mutatePlan,
15441
+ mutateTasks
15442
+ } from "@wrongstack/core";
15443
+ import {
15444
+ createBoard,
15445
+ getBoard,
15446
+ listBoards,
15447
+ removeBoard,
15448
+ syncBoardFromTaskGraph,
15449
+ updateBoard
15450
+ } from "@wrongstack/kanban";
15451
+ var SESSION_BOARD_TAG = "session-work";
15452
+ var MIRROR_DISABLED_ENV = "WRONGSTACK_KANBAN_TASK_MIRROR";
15453
+ var SESSION_KANBAN_COLUMNS = [
15454
+ { id: "todo", title: "Todo", order: 0, wipLimit: 0, color: "#2563eb" },
15455
+ { id: "in-progress", title: "Running", order: 1, wipLimit: 1, color: "#d97706" },
15456
+ { id: "review", title: "Preview", order: 2, wipLimit: 0, color: "#7c3aed" },
15457
+ { id: "done", title: "Done", order: 3, wipLimit: 0, color: "#16a34a" }
15458
+ ];
15459
+ var boardQueue = /* @__PURE__ */ new Map();
15460
+ var boardEnsures = /* @__PURE__ */ new Map();
15461
+ function boardKey(projectRoot, sessionId) {
15462
+ return `${projectRoot}\0${sessionId}`;
15463
+ }
15464
+ function sessionTag(sessionId) {
15465
+ return `session:${sessionId}`;
15466
+ }
15467
+ function sessionBoardTitle(sessionId) {
15468
+ const leaf = sessionId.split(/[\\/]/).filter(Boolean).pop() ?? sessionId;
15469
+ return `Session ${leaf.slice(0, 12)}`;
15470
+ }
15471
+ function sessionBoardTags(sessionId) {
15472
+ return ["session", SESSION_BOARD_TAG, sessionTag(sessionId)];
15473
+ }
15474
+ function sameColumns(columns) {
15475
+ return columns.length === SESSION_KANBAN_COLUMNS.length && columns.every((column, index) => column.id === SESSION_KANBAN_COLUMNS[index]?.id);
15476
+ }
15477
+ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
15478
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
15479
+ const key = boardKey(projectRoot, sessionId);
15480
+ const inFlight = boardEnsures.get(key);
15481
+ if (inFlight) return inFlight;
15482
+ const promise = (async () => {
15483
+ const summary = (await listBoards(projectRoot)).find(
15484
+ (board2) => board2.tags?.includes(sessionTag(sessionId))
15485
+ );
15486
+ let board = summary ? await getBoard(projectRoot, summary.id) : null;
15487
+ if (!board) {
15488
+ return createBoard(projectRoot, {
15489
+ title: sessionBoardTitle(sessionId),
15490
+ description: "Live session work: todos, tasks, and plan items.",
15491
+ tags: sessionBoardTags(sessionId),
15492
+ columns: SESSION_KANBAN_COLUMNS,
15493
+ generatedBy: `session-kanban:${sessionId}`
15494
+ });
15495
+ }
15496
+ if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
15497
+ board = await updateBoard(projectRoot, board.id, {
15498
+ title: sessionBoardTitle(sessionId),
15499
+ description: "Live session work: todos, tasks, and plan items.",
15500
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
15501
+ columns: SESSION_KANBAN_COLUMNS
15502
+ }) ?? board;
15503
+ }
15504
+ return board;
15505
+ })();
15506
+ boardEnsures.set(key, promise);
15507
+ try {
15508
+ return await promise;
15509
+ } finally {
15510
+ boardEnsures.delete(key);
15511
+ }
15512
+ }
15513
+ function enqueueBoardWork(projectRoot, sessionId, work) {
15514
+ const key = boardKey(projectRoot, sessionId);
15515
+ const previous = boardQueue.get(key) ?? Promise.resolve();
15516
+ const result = previous.catch(() => void 0).then(work);
15517
+ const tail = result.then(
15518
+ () => void 0,
15519
+ () => void 0
15520
+ );
15521
+ boardQueue.set(key, tail);
15522
+ void tail.then(() => {
15523
+ if (boardQueue.get(key) === tail) boardQueue.delete(key);
15524
+ });
15525
+ return result;
15526
+ }
15527
+ async function projectGraph(projectRoot, sessionId, graph, sourceSystem) {
15528
+ if (!projectRoot || !sessionId || process.env[MIRROR_DISABLED_ENV] === "0") return null;
15529
+ return enqueueBoardWork(projectRoot, sessionId, async () => {
15530
+ const board = await ensureSessionKanbanBoard(projectRoot, sessionId);
15531
+ if (!board) return null;
15532
+ const result = await syncBoardFromTaskGraph(
15533
+ projectRoot,
15534
+ board.id,
15535
+ deserializeTaskGraph(graph),
15536
+ {
15537
+ sourceSystem,
15538
+ tags: [.../* @__PURE__ */ new Set([...board.tags ?? [], ...sessionBoardTags(sessionId)])],
15539
+ archiveMissingTasks: true,
15540
+ includeCompletedTasks: true
15541
+ }
15542
+ );
15543
+ return result?.board ?? null;
15544
+ });
15545
+ }
15546
+ function todoListToSerializedGraph(todos, sessionId) {
15547
+ const nodes = todos.map((todo, index) => ({
15548
+ id: todo.id,
15549
+ title: todo.content,
15550
+ description: todo.activeForm ?? "",
15551
+ type: "chore",
15552
+ priority: "medium",
15553
+ status: todo.status,
15554
+ createdAt: index,
15555
+ updatedAt: index
15556
+ }));
15557
+ return {
15558
+ id: `todo:${sessionId}`,
15559
+ specId: `todo:${sessionId}`,
15560
+ title: "Session todos",
15561
+ nodes,
15562
+ edges: [],
15563
+ rootNodes: nodes.map((node) => node.id),
15564
+ createdAt: 0,
15565
+ updatedAt: 0
15566
+ };
15567
+ }
15568
+ function taskFileToSerializedGraph(tasks, sessionId) {
15569
+ const ids = new Set(tasks.map((task) => task.id));
15570
+ const nodes = tasks.map((task, index) => ({
15571
+ id: task.id,
15572
+ title: task.title,
15573
+ description: task.description ?? "",
15574
+ type: task.type,
15575
+ priority: task.priority,
15576
+ status: task.status,
15577
+ ...task.assignee ? { assignee: task.assignee } : {},
15578
+ ...task.estimateHours !== void 0 ? { estimateHours: task.estimateHours } : {},
15579
+ createdAt: index,
15580
+ updatedAt: index
15581
+ }));
15582
+ const edges = tasks.flatMap(
15583
+ (task) => (task.dependsOn ?? []).filter((dependency) => ids.has(dependency)).map((dependency) => ({
15584
+ id: `${dependency}->${task.id}`,
15585
+ from: dependency,
15586
+ to: task.id,
15587
+ type: "depends_on"
15588
+ }))
15589
+ );
15590
+ const hasIncoming = new Set(edges.map((edge) => edge.to));
15591
+ const rootNodes = nodes.filter((node) => !hasIncoming.has(node.id)).map((node) => node.id);
15592
+ return {
15593
+ // Keep the historical graph id so existing mirrored task cards are reused.
15594
+ id: `session:${sessionId}`,
15595
+ specId: `session:${sessionId}`,
15596
+ title: "Session tasks",
15597
+ nodes,
15598
+ edges,
15599
+ rootNodes: rootNodes.length ? rootNodes : nodes[0] ? [nodes[0].id] : [],
15600
+ createdAt: 0,
15601
+ updatedAt: 0
15602
+ };
15603
+ }
15604
+ var PLAN_STATUS_TO_TASK = {
15605
+ open: "pending",
15606
+ in_progress: "in_progress",
15607
+ done: "completed"
15608
+ };
15609
+ function planFileToSerializedGraph(items, sessionId) {
15610
+ const nodes = items.map((item, index) => ({
15611
+ id: item.id,
15612
+ title: item.title,
15613
+ description: item.details ?? "",
15614
+ type: "chore",
15615
+ priority: "medium",
15616
+ status: PLAN_STATUS_TO_TASK[item.status],
15617
+ createdAt: index,
15618
+ updatedAt: index
15619
+ }));
15620
+ return {
15621
+ id: `plan:${sessionId}`,
15622
+ specId: `plan:${sessionId}`,
15623
+ title: "Session plan",
15624
+ nodes,
15625
+ edges: [],
15626
+ rootNodes: nodes.map((node) => node.id),
15627
+ createdAt: 0,
15628
+ updatedAt: 0
15629
+ };
15630
+ }
15631
+ function projectSessionTodosToKanban(projectRoot, todos, sessionId) {
15632
+ return projectGraph(
15633
+ projectRoot,
15634
+ sessionId,
15635
+ todoListToSerializedGraph(todos, sessionId),
15636
+ "session-todo"
15637
+ );
15638
+ }
15639
+ function projectSessionTasksToKanban(projectRoot, tasks, sessionId) {
15640
+ return projectGraph(
15641
+ projectRoot,
15642
+ sessionId,
15643
+ taskFileToSerializedGraph(tasks, sessionId),
15644
+ "session-task"
15645
+ );
15646
+ }
15647
+ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
15648
+ return projectGraph(
15649
+ projectRoot,
15650
+ sessionId,
15651
+ planFileToSerializedGraph(items, sessionId),
15652
+ "session-plan"
15653
+ );
15654
+ }
15655
+
15656
+ // src/kanban.ts
15320
15657
  var kanbanTool = {
15321
15658
  name: "kanban",
15322
15659
  category: "Project",
@@ -15343,6 +15680,8 @@ var kanbanTool = {
15343
15680
  "export_markdown",
15344
15681
  "export_task_graph",
15345
15682
  "sync_task_graph",
15683
+ "create_from_graph",
15684
+ "import_session_tasks",
15346
15685
  "search_tasks",
15347
15686
  "ready_tasks",
15348
15687
  "snapshot",
@@ -15389,6 +15728,10 @@ var kanbanTool = {
15389
15728
  tags: { type: "array", items: { type: "string" } },
15390
15729
  labels: { type: "array", items: { type: "string" } },
15391
15730
  priority: { type: "string", enum: ["critical", "high", "medium", "low"] },
15731
+ taskType: {
15732
+ type: "string",
15733
+ enum: ["feature", "bugfix", "refactor", "docs", "test", "chore"]
15734
+ },
15392
15735
  status: {
15393
15736
  type: "string",
15394
15737
  enum: [
@@ -15431,8 +15774,21 @@ var kanbanTool = {
15431
15774
  releaseStatus: { type: "string", enum: ["pending", "ready", "blocked"] },
15432
15775
  releaseReason: { type: "string" },
15433
15776
  clearAssignee: { type: "boolean" },
15434
- recoveryMode: { type: "string", enum: ["release", "retry", "fail"] },
15777
+ recoveryMode: { type: "string", enum: ["auto", "release", "retry", "fail"] },
15435
15778
  recoveryNow: { type: "string" },
15779
+ recoveryPolicyFailOnCostCeiling: { type: "boolean" },
15780
+ recoveryPolicyReleaseOnFailureKinds: { type: "array", items: { type: "string" } },
15781
+ recoveryPolicyReleaseOnHeartbeatDue: { type: "boolean" },
15782
+ recoveryPolicyRetryPolicyOverride: {
15783
+ type: "string",
15784
+ enum: ["off", "incremental", "exponential"]
15785
+ },
15786
+ assignee: { type: "string" },
15787
+ costCeilingUsd: { type: "number" },
15788
+ retryPolicy: { type: "string", enum: ["off", "incremental", "exponential"] },
15789
+ lastFailureKind: { type: "string" },
15790
+ dependsOn: { type: "array", items: { type: "string" } },
15791
+ estimatedHours: { type: "number" },
15436
15792
  taskGraph: { type: "object" },
15437
15793
  graphId: { type: "string" },
15438
15794
  specId: { type: "string" },
@@ -15488,7 +15844,7 @@ var kanbanTool = {
15488
15844
  try {
15489
15845
  switch (input.action) {
15490
15846
  case "list_boards": {
15491
- const boards = await listBoards(projectRoot);
15847
+ const boards = await listBoards2(projectRoot);
15492
15848
  return { ok: true, message: `${boards.length} board(s).`, boards };
15493
15849
  }
15494
15850
  case "get_board": {
@@ -15497,7 +15853,7 @@ var kanbanTool = {
15497
15853
  }
15498
15854
  case "create_board": {
15499
15855
  if (!input.title) return fail("create_board requires title.");
15500
- const board = await createBoard(projectRoot, {
15856
+ const board = await createBoard2(projectRoot, {
15501
15857
  title: input.title,
15502
15858
  ...input.description !== void 0 ? { description: input.description } : {},
15503
15859
  ...input.tags !== void 0 ? { tags: input.tags } : {},
@@ -15507,7 +15863,7 @@ var kanbanTool = {
15507
15863
  }
15508
15864
  case "update_board": {
15509
15865
  if (!input.boardId) return fail("update_board requires boardId.");
15510
- const board = await updateBoard(projectRoot, input.boardId, {
15866
+ const board = await updateBoard2(projectRoot, input.boardId, {
15511
15867
  ...input.title !== void 0 ? { title: input.title } : {},
15512
15868
  ...input.description !== void 0 ? { description: input.description } : {},
15513
15869
  ...input.tags !== void 0 ? { tags: input.tags } : {}
@@ -15527,7 +15883,7 @@ var kanbanTool = {
15527
15883
  }
15528
15884
  case "delete_board": {
15529
15885
  if (!input.boardId) return fail("delete_board requires boardId.");
15530
- const removed = await removeBoard(projectRoot, input.boardId);
15886
+ const removed = await removeBoard2(projectRoot, input.boardId);
15531
15887
  return { ok: removed, message: removed ? "Board deleted." : "Board not found." };
15532
15888
  }
15533
15889
  case "generate_board": {
@@ -15538,14 +15894,14 @@ var kanbanTool = {
15538
15894
  ...input.context !== void 0 ? { context: input.context } : {},
15539
15895
  ...input.columns !== void 0 ? { columns: input.columns } : {}
15540
15896
  });
15541
- const board = await createBoard(projectRoot, boardInput);
15897
+ const board = await createBoard2(projectRoot, boardInput);
15542
15898
  for (const taskInput2 of parseLinesIntoTasks(
15543
15899
  input.description,
15544
15900
  board.columns[0]?.id ?? "backlog"
15545
15901
  )) {
15546
15902
  await addTask(projectRoot, board.id, taskInput2);
15547
15903
  }
15548
- return okBoard(await getBoard(projectRoot, board.id) ?? board, "Board generated.");
15904
+ return okBoard(await getBoard2(projectRoot, board.id) ?? board, "Board generated.");
15549
15905
  }
15550
15906
  case "export_markdown": {
15551
15907
  const board = await requireBoard(projectRoot, input.boardId);
@@ -15578,8 +15934,8 @@ var kanbanTool = {
15578
15934
  if (!input.boardId || !input.taskGraph) {
15579
15935
  return fail("sync_task_graph requires boardId and taskGraph.");
15580
15936
  }
15581
- const graph = deserializeTaskGraph(input.taskGraph);
15582
- const result = await syncBoardFromTaskGraph(projectRoot, input.boardId, graph, {
15937
+ const graph = deserializeTaskGraph2(input.taskGraph);
15938
+ const result = await syncBoardFromTaskGraph2(projectRoot, input.boardId, graph, {
15583
15939
  ...input.title !== void 0 ? { title: input.title } : {},
15584
15940
  ...input.description !== void 0 ? { description: input.description } : {},
15585
15941
  ...input.tags !== void 0 ? { tags: input.tags } : {},
@@ -15596,6 +15952,59 @@ var kanbanTool = {
15596
15952
  board: result.board
15597
15953
  } : fail("Board not found.");
15598
15954
  }
15955
+ case "create_from_graph": {
15956
+ if (!input.taskGraph) return fail("create_from_graph requires taskGraph.");
15957
+ const graph = deserializeTaskGraph2(input.taskGraph);
15958
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
15959
+ ...input.title !== void 0 ? { title: input.title } : {},
15960
+ ...input.description !== void 0 ? { description: input.description } : {},
15961
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
15962
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
15963
+ ...input.sourceSystem !== void 0 ? { sourceSystem: input.sourceSystem } : {},
15964
+ ...input.phaseId !== void 0 ? { phaseId: input.phaseId } : {},
15965
+ ...input.includeCompletedTasks !== void 0 ? { includeCompletedTasks: input.includeCompletedTasks } : {}
15966
+ });
15967
+ return {
15968
+ ok: true,
15969
+ message: `Created board "${board.title}" from task graph with ${board.tasks.length} tasks.`,
15970
+ board
15971
+ };
15972
+ }
15973
+ case "import_session_tasks": {
15974
+ const taskPath = ctx.meta?.["task.path"];
15975
+ if (!taskPath) return fail("No session task file for this session.");
15976
+ const file = await loadTasks2(taskPath);
15977
+ if (!file || file.tasks.length === 0) return fail("No session tasks to import.");
15978
+ const sessionId = ctx.session?.id ?? file.sessionId ?? "session";
15979
+ const graph = deserializeTaskGraph2(taskFileToSerializedGraph(file.tasks, sessionId));
15980
+ const tags = ["session", `session:${sessionId}`];
15981
+ const existing = (await listBoards2(projectRoot)).find(
15982
+ (b) => b.tags?.includes(`session:${sessionId}`)
15983
+ );
15984
+ if (existing) {
15985
+ const result = await syncBoardFromTaskGraph2(projectRoot, existing.id, graph, {
15986
+ sourceSystem: "session",
15987
+ tags,
15988
+ archiveMissingTasks: true,
15989
+ includeCompletedTasks: true
15990
+ });
15991
+ return result ? {
15992
+ ok: true,
15993
+ message: `Synced ${file.tasks.length} session tasks into board "${result.board.title}".`,
15994
+ board: result.board
15995
+ } : fail("Session board vanished mid-sync.");
15996
+ }
15997
+ const { board } = await createBoardFromTaskGraph(projectRoot, graph, {
15998
+ title: `Session tasks (${sessionId.slice(0, 8)})`,
15999
+ sourceSystem: "session",
16000
+ tags
16001
+ });
16002
+ return {
16003
+ ok: true,
16004
+ message: `Imported ${file.tasks.length} session tasks into new board "${board.title}".`,
16005
+ board
16006
+ };
16007
+ }
15599
16008
  case "search_tasks": {
15600
16009
  const tasks = await searchKanban(projectRoot, {
15601
16010
  query: input.query,
@@ -16032,7 +16441,7 @@ function okTask(board, task, message) {
16032
16441
  return { ok: true, message, board, task };
16033
16442
  }
16034
16443
  async function requireBoard(projectRoot, boardId) {
16035
- return boardId ? getBoard(projectRoot, boardId) : null;
16444
+ return boardId ? getBoard2(projectRoot, boardId) : null;
16036
16445
  }
16037
16446
  function taskInput(input) {
16038
16447
  const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
@@ -16041,14 +16450,23 @@ function taskInput(input) {
16041
16450
  columnId: input.columnId,
16042
16451
  description: input.description,
16043
16452
  priority: input.priority,
16453
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
16044
16454
  status: input.status,
16045
16455
  labels: input.labels,
16046
16456
  ...assignment?.agentId ?? assignment?.role ?? assignment?.name ? { assignedAgent: assignment.agentId ?? assignment.role ?? assignment.name } : {},
16047
16457
  ...input.assignee ?? assignment?.name ?? assignment?.agentId ? { assignee: input.assignee ?? assignment?.name ?? assignment?.agentId } : {},
16048
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {},
16458
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16459
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
16049
16460
  ...assignment ? { assignment } : {}
16050
16461
  };
16051
16462
  }
16463
+ function mergedDependsOn(input) {
16464
+ const ids = [
16465
+ ...input.dependsOn ?? [],
16466
+ ...input.dependencyTaskId !== void 0 ? [input.dependencyTaskId] : []
16467
+ ].filter((id, i, arr) => id && arr.indexOf(id) === i);
16468
+ return ids.length > 0 ? ids : void 0;
16469
+ }
16052
16470
  function taskPatch(input) {
16053
16471
  return {
16054
16472
  title: input.title,
@@ -16056,10 +16474,12 @@ function taskPatch(input) {
16056
16474
  columnId: input.columnId,
16057
16475
  order: input.order,
16058
16476
  priority: input.priority,
16477
+ ...input.taskType !== void 0 ? { type: input.taskType } : {},
16059
16478
  status: input.status,
16060
16479
  labels: input.labels,
16061
16480
  assignedAgent: input.agentId,
16062
- ...input.dependencyTaskId !== void 0 ? { dependsOn: [input.dependencyTaskId] } : {}
16481
+ ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
16482
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
16063
16483
  };
16064
16484
  }
16065
16485
  function assignmentInput(input) {
@@ -16804,12 +17224,12 @@ import {
16804
17224
  deriveTodosFromPlanItem,
16805
17225
  formatPlan,
16806
17226
  getPlanTemplate,
16807
- mutatePlan,
17227
+ mutatePlan as mutatePlan2,
16808
17228
  removePlanItem,
16809
17229
  setPlanItemStatus
16810
17230
  } from "@wrongstack/core";
16811
17231
  import {
16812
- mutateTasks,
17232
+ mutateTasks as mutateTasks2,
16813
17233
  formatTaskList
16814
17234
  } from "@wrongstack/core";
16815
17235
  import { randomUUID } from "node:crypto";
@@ -16896,7 +17316,7 @@ var planTool = {
16896
17316
  let didTaskify = false;
16897
17317
  let plan;
16898
17318
  try {
16899
- plan = await mutatePlan(planPath, sessionId, async (p) => {
17319
+ plan = await mutatePlan2(planPath, sessionId, async (p) => {
16900
17320
  switch (input.action) {
16901
17321
  case "show":
16902
17322
  break;
@@ -17022,6 +17442,7 @@ var planTool = {
17022
17442
  open: 0
17023
17443
  };
17024
17444
  }
17445
+ await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
17025
17446
  if (early) return early;
17026
17447
  if (didTaskify) {
17027
17448
  const taskPathRaw = ctx.meta["task.path"];
@@ -17035,7 +17456,7 @@ var planTool = {
17035
17456
  }
17036
17457
  const now = (/* @__PURE__ */ new Date()).toISOString();
17037
17458
  try {
17038
- const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
17459
+ const taskFile = await mutateTasks2(taskPath, sessionId, (f) => {
17039
17460
  f.tasks.push({
17040
17461
  id: `task_${randomUUID()}`,
17041
17462
  title: taskifyMeta.title,
@@ -18172,11 +18593,11 @@ import {
18172
18593
  formatTaskList as formatTaskList2
18173
18594
  } from "@wrongstack/core";
18174
18595
  import {
18175
- mutateTasks as mutateTasks2
18596
+ mutateTasks as mutateTasks3
18176
18597
  } from "@wrongstack/core";
18177
18598
  import {
18178
18599
  addPlanItem as addPlanItem2,
18179
- mutatePlan as mutatePlan2,
18600
+ mutatePlan as mutatePlan3,
18180
18601
  formatPlan as formatPlan2
18181
18602
  } from "@wrongstack/core";
18182
18603
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -18296,7 +18717,7 @@ var taskTool = {
18296
18717
  let todosToReplace = null;
18297
18718
  let file;
18298
18719
  try {
18299
- file = await mutateTasks2(taskPath, sessionId, async (f) => {
18720
+ file = await mutateTasks3(taskPath, sessionId, async (f) => {
18300
18721
  switch (input.action) {
18301
18722
  case "show":
18302
18723
  break;
@@ -18486,6 +18907,7 @@ var taskTool = {
18486
18907
  };
18487
18908
  }
18488
18909
  if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
18910
+ await projectSessionTasksToKanban(ctx.projectRoot, file.tasks, sessionId);
18489
18911
  if (early) return early;
18490
18912
  if (didPlanify) {
18491
18913
  const { title, details } = planifyMeta;
@@ -18499,7 +18921,7 @@ var taskTool = {
18499
18921
  }
18500
18922
  let formatted = "";
18501
18923
  try {
18502
- await mutatePlan2(planPath, sessionId, (pf) => {
18924
+ await mutatePlan3(planPath, sessionId, (pf) => {
18503
18925
  const { plan: updated } = addPlanItem2(pf, title, details || void 0);
18504
18926
  formatted = formatPlan2(updated);
18505
18927
  return updated;
@@ -18731,8 +19153,7 @@ function parseResult(runner, result, duration) {
18731
19153
  }
18732
19154
 
18733
19155
  // src/todo.ts
18734
- import { loadPlan, savePlan, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
18735
- import { loadTasks, saveTasks } from "@wrongstack/core";
19156
+ import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
18736
19157
  var todoTool = {
18737
19158
  name: "todo",
18738
19159
  category: "Session",
@@ -18741,7 +19162,7 @@ var todoTool = {
18741
19162
  permission: "auto",
18742
19163
  mutating: false,
18743
19164
  // mutates only conversation state (ctx.todos), not external state — no confirmation needed
18744
- timeoutMs: 1e3,
19165
+ timeoutMs: 5e3,
18745
19166
  capabilities: ["session.todo"],
18746
19167
  icon: "todo",
18747
19168
  inputSchema: {
@@ -18793,16 +19214,21 @@ var todoTool = {
18793
19214
  }
18794
19215
  }
18795
19216
  ctx.state.replaceTodos(items);
19217
+ await projectSessionTodosToKanban(ctx.projectRoot, items, ctx.session?.id ?? "session");
18796
19218
  const completedPlanIds = /* @__PURE__ */ new Set();
18797
19219
  const completedTaskIds = /* @__PURE__ */ new Set();
18798
19220
  const pendingPlanIds = /* @__PURE__ */ new Set();
18799
19221
  const pendingTaskIds = /* @__PURE__ */ new Set();
18800
19222
  for (const item of items) {
18801
19223
  if (item.promotedFromPlan) {
18802
- (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(item.promotedFromPlan);
19224
+ (item.status === "completed" ? completedPlanIds : pendingPlanIds).add(
19225
+ item.promotedFromPlan
19226
+ );
18803
19227
  }
18804
19228
  if (item.promotedFromTask) {
18805
- (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(item.promotedFromTask);
19229
+ (item.status === "completed" ? completedTaskIds : pendingTaskIds).add(
19230
+ item.promotedFromTask
19231
+ );
18806
19232
  }
18807
19233
  }
18808
19234
  for (const planId of completedPlanIds) {
@@ -18810,7 +19236,7 @@ var todoTool = {
18810
19236
  const planPath = ctx.meta["plan.path"];
18811
19237
  if (typeof planPath !== "string" || !planPath) continue;
18812
19238
  try {
18813
- const plan = await loadPlan(planPath);
19239
+ const plan = await loadPlan2(planPath);
18814
19240
  if (plan) {
18815
19241
  const updated = setPlanItemStatus2(plan, planId, "done");
18816
19242
  await savePlan(planPath, updated);
@@ -18823,7 +19249,7 @@ var todoTool = {
18823
19249
  const taskPath = ctx.meta["task.path"];
18824
19250
  if (typeof taskPath !== "string" || !taskPath) continue;
18825
19251
  try {
18826
- const file = await loadTasks(taskPath);
19252
+ const file = await loadTasks3(taskPath);
18827
19253
  if (file) {
18828
19254
  const task = file.tasks.find((t) => t.id === taskId);
18829
19255
  if (task && task.status !== "completed") {