@wrongstack/tools 0.319.0 → 0.319.1

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 (95) hide show
  1. package/dist/_edit-match.d.ts +1 -3
  2. package/dist/_util.d.ts +7 -0
  3. package/dist/audit.js +4 -4
  4. package/dist/bash.js +2 -2
  5. package/dist/builtin.d.ts +10 -0
  6. package/dist/builtin.js +44 -42
  7. package/dist/{chunk-2472NT76.js → chunk-2OXHS3KZ.js} +17 -5
  8. package/dist/{chunk-X5K3U6GI.js → chunk-3M7LLN7W.js} +71 -7
  9. package/dist/{chunk-VFF3Q5Z3.js → chunk-5EUJF3WC.js} +36 -8
  10. package/dist/{chunk-QZDOQRF7.js → chunk-64VIJPFV.js} +34 -13
  11. package/dist/{chunk-UMXVT7ST.js → chunk-6AAOT4PW.js} +58 -10
  12. package/dist/{chunk-WNUJR3H4.js → chunk-AKHYMB4V.js} +36 -12
  13. package/dist/{chunk-ZDUGI5BC.js → chunk-ATSKPZUK.js} +2 -2
  14. package/dist/{chunk-H6AWAZ2Y.js → chunk-B4HTSV6Z.js} +57 -6
  15. package/dist/{chunk-5ZYEGP2P.js → chunk-BK732J7I.js} +72 -27
  16. package/dist/{chunk-PMHVSHMU.js → chunk-BSJSNVGA.js} +73 -14
  17. package/dist/{chunk-KD5EHYA2.js → chunk-C2DQKVLO.js} +61 -28
  18. package/dist/{chunk-PRXZOIEW.js → chunk-CFLJFDYV.js} +100 -58
  19. package/dist/{chunk-TX3R5NVV.js → chunk-CMOUCKGC.js} +2 -2
  20. package/dist/{chunk-SYXW3Y5Z.js → chunk-DC4KMPJO.js} +2 -2
  21. package/dist/{chunk-MRKWZCHZ.js → chunk-DP6CXMJZ.js} +3 -3
  22. package/dist/{chunk-ODFVG54U.js → chunk-EADULOZV.js} +36 -12
  23. package/dist/{chunk-6RNIFLG4.js → chunk-ELRRTWNP.js} +38 -6
  24. package/dist/{chunk-YZHK4TLF.js → chunk-FQNXYA6W.js} +45 -6
  25. package/dist/{chunk-65EMYN5G.js → chunk-GMZYW52L.js} +3 -3
  26. package/dist/{chunk-HBM4QNXU.js → chunk-IU4DV2PA.js} +18 -8
  27. package/dist/{chunk-COUNXG4X.js → chunk-IWS56OY7.js} +30 -7
  28. package/dist/{chunk-Z4VBTVUM.js → chunk-IYKXK356.js} +98 -11
  29. package/dist/{chunk-OJ4FIBAN.js → chunk-JDPQOR7C.js} +14 -7
  30. package/dist/{chunk-TFMVHYJI.js → chunk-KY4PXYSO.js} +9 -15
  31. package/dist/{chunk-ZKSBHXVB.js → chunk-LOBRWKCR.js} +63 -14
  32. package/dist/{chunk-HWDB2G7J.js → chunk-MNP3GE7H.js} +38 -27
  33. package/dist/{chunk-MYRMD6PZ.js → chunk-O44IMQCN.js} +31 -7
  34. package/dist/{chunk-7WV3D4S3.js → chunk-OG2BEUBY.js} +69 -34
  35. package/dist/{chunk-VALQFAY2.js → chunk-OTINMOKK.js} +153 -40
  36. package/dist/{chunk-PQTONQ4P.js → chunk-QPNYDH5B.js} +19 -5
  37. package/dist/{chunk-4Q7LFU4T.js → chunk-RLMX75EM.js} +19 -8
  38. package/dist/{chunk-C3W3V54D.js → chunk-RNWUCZ2U.js} +53 -25
  39. package/dist/{chunk-RK47IWO3.js → chunk-RTZWBNTY.js} +59 -28
  40. package/dist/{chunk-UC3RKRIV.js → chunk-TTZOK6IY.js} +2 -2
  41. package/dist/{chunk-W4GAAI3M.js → chunk-UDVKRX5R.js} +2 -2
  42. package/dist/{chunk-Y7QXSE7N.js → chunk-UQQS3VNT.js} +43 -9
  43. package/dist/{chunk-D4CXUZIF.js → chunk-VD2LV5WO.js} +82 -13
  44. package/dist/{chunk-DPKTXJQS.js → chunk-VPALDFYM.js} +2 -2
  45. package/dist/{chunk-FJEQZHZN.js → chunk-W2CECGTG.js} +24 -14
  46. package/dist/{chunk-OYVPBPCL.js → chunk-X5FOFGXX.js} +72 -33
  47. package/dist/{chunk-CWALNI6K.js → chunk-X6KXNHUO.js} +1 -1
  48. package/dist/{chunk-OCUXN6Y3.js → chunk-XRNN44QY.js} +1 -1
  49. package/dist/{chunk-N5O3FK5Q.js → chunk-YOKD4GWJ.js} +67 -27
  50. package/dist/{chunk-QLF74MSV.js → chunk-ZUFHAHTE.js} +12 -6
  51. package/dist/codebase-index/index.js +7 -7
  52. package/dist/codebase-index/parser-worker-script.js +1 -1
  53. package/dist/codebase-index/project-server.js +3 -3
  54. package/dist/codebase-index/worker.js +2 -2
  55. package/dist/design.d.ts +1 -1
  56. package/dist/design.js +1 -1
  57. package/dist/diff.js +3 -2
  58. package/dist/document.js +2 -2
  59. package/dist/e2e.js +2 -2
  60. package/dist/edit.js +6 -6
  61. package/dist/exec.js +2 -2
  62. package/dist/fetch.js +2 -2
  63. package/dist/format.js +4 -4
  64. package/dist/git.js +2 -2
  65. package/dist/glob.js +2 -2
  66. package/dist/grep.d.ts +2 -0
  67. package/dist/grep.js +4 -2
  68. package/dist/index.js +44 -44
  69. package/dist/install.js +4 -4
  70. package/dist/json.js +2 -2
  71. package/dist/kanban-tool-types.d.ts +2 -0
  72. package/dist/kanban.js +2 -2
  73. package/dist/languages/index.js +2 -2
  74. package/dist/{legacy-bridge-EC7GM2IW.js → legacy-bridge-3T7SR4SK.js} +4 -4
  75. package/dist/lint.js +4 -4
  76. package/dist/logs.js +2 -2
  77. package/dist/outdated.js +4 -2
  78. package/dist/pack.js +43 -43
  79. package/dist/{parser-dispatch-3PDBKIQ7.js → parser-dispatch-QGC4HSP5.js} +2 -2
  80. package/dist/patch.js +6 -6
  81. package/dist/plan.js +4 -4
  82. package/dist/read.js +6 -6
  83. package/dist/replace.js +7 -6
  84. package/dist/scaffold.js +2 -2
  85. package/dist/search.js +1 -1
  86. package/dist/session-kanban.js +1 -1
  87. package/dist/task.js +4 -4
  88. package/dist/test.js +4 -4
  89. package/dist/todo.js +3 -3
  90. package/dist/tool-tier.js +44 -44
  91. package/dist/{tree-sitter-parser-FGKHJGSX.js → tree-sitter-parser-GILYQLS4.js} +2 -2
  92. package/dist/tree.js +2 -2
  93. package/dist/typecheck.js +4 -4
  94. package/dist/write.js +6 -6
  95. package/package.json +5 -5
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  COMMAND_OUTPUT_MAX_BYTES,
3
3
  normalizeCommandOutput
4
- } from "./chunk-QLF74MSV.js";
4
+ } from "./chunk-ZUFHAHTE.js";
5
5
 
6
6
  // src/git.ts
7
7
  import { spawn } from "node:child_process";
8
8
  import { statSync } from "node:fs";
9
9
  import { dirname, resolve, sep } from "node:path";
10
+ import { StringDecoder } from "node:string_decoder";
10
11
  import { assessCommitSafety } from "@wrongstack/core/coordination";
12
+ import { ToolValidationError } from "@wrongstack/core/types";
11
13
  import { buildChildEnv } from "@wrongstack/core/utils";
12
14
  var TIMEOUT_MS = 3e4;
13
15
  var MAX_OUTPUT = 1e5;
@@ -89,7 +91,18 @@ var gitTool = {
89
91
  required: ["command"]
90
92
  },
91
93
  async execute(input, ctx, opts) {
92
- if (!input?.command) throw new Error("git: command is required");
94
+ if (!input?.command || typeof input.command !== "string" || !input.command.trim()) {
95
+ throw new ToolValidationError({
96
+ message: "git: command is required and cannot be empty",
97
+ field: "command"
98
+ });
99
+ }
100
+ if (input.command.startsWith("-") || input.command.includes(" --")) {
101
+ throw new ToolValidationError({
102
+ message: `git: unsafe subcommand name "${input.command}"`,
103
+ field: "command"
104
+ });
105
+ }
93
106
  if (input.command === "commit" && !input.message) {
94
107
  return {
95
108
  command: "commit",
@@ -187,7 +200,7 @@ function findGitDir(cwd, projectRoot) {
187
200
  let dir = cwd;
188
201
  for (let i = 0; i < 20; i++) {
189
202
  try {
190
- const stat = statSync(`${dir}/.git`);
203
+ const stat = statSync(resolve(dir, ".git"));
191
204
  if (stat.isDirectory() || stat.isFile()) return dir;
192
205
  } catch {
193
206
  }
@@ -199,8 +212,9 @@ function findGitDir(cwd, projectRoot) {
199
212
  return null;
200
213
  }
201
214
  function buildArgs(input) {
202
- const limit = input.limit ?? 20;
203
- const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((s) => s.trim()).filter(Boolean) : [];
215
+ const rawLimit = typeof input.limit === "number" && Number.isFinite(input.limit) && input.limit > 0 ? Math.floor(input.limit) : 20;
216
+ const limit = Math.max(1, rawLimit);
217
+ const files = input.files ? (Array.isArray(input.files) ? input.files : input.files.split(",")).map((s) => s.trim().replace(/\\/g, "/")).filter(Boolean) : [];
204
218
  switch (input.command) {
205
219
  case "status":
206
220
  return ["status", ...files.length ? ["--", ...files] : []];
@@ -269,9 +283,22 @@ function buildArgs(input) {
269
283
  }
270
284
  }
271
285
  function runGit(args, cwd, signal) {
286
+ if (signal.aborted) {
287
+ return Promise.resolve({
288
+ command: args[0],
289
+ stdout: "",
290
+ stderr: "Aborted",
291
+ exitCode: 124,
292
+ truncated: false
293
+ });
294
+ }
272
295
  return new Promise((resolve2) => {
273
296
  let stdout = "";
274
297
  let stderr = "";
298
+ let stdoutBytes = 0;
299
+ let stderrBytes = 0;
300
+ const stdoutDecoder = new StringDecoder("utf8");
301
+ const stderrDecoder = new StringDecoder("utf8");
275
302
  const child = spawn("git", args, {
276
303
  cwd,
277
304
  signal,
@@ -280,31 +307,52 @@ function runGit(args, cwd, signal) {
280
307
  windowsHide: true
281
308
  });
282
309
  child.stdout?.on("data", (chunk) => {
310
+ stdoutBytes += chunk.byteLength;
283
311
  if (stdout.length < MAX_OUTPUT) {
284
- stdout += chunk.toString();
312
+ const text = stdoutDecoder.write(chunk);
313
+ if (text) stdout += text.slice(0, MAX_OUTPUT - stdout.length);
285
314
  }
286
315
  });
287
316
  child.stderr?.on("data", (chunk) => {
317
+ stderrBytes += chunk.byteLength;
288
318
  if (stderr.length < MAX_OUTPUT) {
289
- stderr += chunk.toString();
319
+ const text = stderrDecoder.write(chunk);
320
+ if (text) stderr += text.slice(0, MAX_OUTPUT - stderr.length);
290
321
  }
291
322
  });
292
323
  child.on("error", (err) => {
324
+ const stdoutTail = stdoutDecoder.end();
325
+ if (stdoutTail && stdout.length < MAX_OUTPUT) {
326
+ stdout += stdoutTail.slice(0, MAX_OUTPUT - stdout.length);
327
+ }
328
+ const stderrTail = stderrDecoder.end();
329
+ if (stderrTail && stderr.length < MAX_OUTPUT) {
330
+ stderr += stderrTail.slice(0, MAX_OUTPUT - stderr.length);
331
+ }
293
332
  resolve2({
294
333
  command: args[0],
295
334
  stdout: normalizeCommandOutput(stdout),
296
335
  stderr: err.message,
297
336
  exitCode: 1,
298
- truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES
337
+ truncated: stdoutBytes > MAX_OUTPUT || Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES
299
338
  });
300
339
  });
301
340
  child.on("close", (code) => {
341
+ const stdoutTail = stdoutDecoder.end();
342
+ if (stdoutTail && stdout.length < MAX_OUTPUT) {
343
+ stdout += stdoutTail.slice(0, MAX_OUTPUT - stdout.length);
344
+ }
345
+ const stderrTail = stderrDecoder.end();
346
+ if (stderrTail && stderr.length < MAX_OUTPUT) {
347
+ stderr += stderrTail.slice(0, MAX_OUTPUT - stderr.length);
348
+ }
349
+ const isTruncated = stdoutBytes > MAX_OUTPUT || stderrBytes > MAX_OUTPUT || Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES;
302
350
  resolve2({
303
351
  command: args[0],
304
352
  stdout: normalizeCommandOutput(stdout),
305
353
  stderr: normalizeCommandOutput(stderr),
306
354
  exitCode: code ?? 1,
307
- truncated: Buffer.byteLength(stdout, "utf8") > COMMAND_OUTPUT_MAX_BYTES || Buffer.byteLength(stderr, "utf8") > COMMAND_OUTPUT_MAX_BYTES
355
+ truncated: isTruncated
308
356
  });
309
357
  });
310
358
  });
@@ -313,4 +361,4 @@ function runGit(args, cwd, signal) {
313
361
  export {
314
362
  gitTool
315
363
  };
316
- //# sourceMappingURL=chunk-UMXVT7ST.js.map
364
+ //# sourceMappingURL=chunk-6AAOT4PW.js.map
@@ -17,7 +17,7 @@ import {
17
17
  } from "./chunk-V43EQ6VR.js";
18
18
  import {
19
19
  normalizeCommandOutput
20
- } from "./chunk-QLF74MSV.js";
20
+ } from "./chunk-ZUFHAHTE.js";
21
21
 
22
22
  // src/bash.ts
23
23
  import { spawn } from "node:child_process";
@@ -28,6 +28,7 @@ import {
28
28
  emitProcessOutput,
29
29
  emitProcessStarted
30
30
  } from "@wrongstack/core/observability";
31
+ import { ToolValidationError } from "@wrongstack/core/types";
31
32
 
32
33
  // src/_shell-pick.ts
33
34
  var POSIX_DEFAULT = "cmd";
@@ -539,7 +540,12 @@ var bashTool = {
539
540
  return final;
540
541
  },
541
542
  async *executeStream(input, ctx, opts) {
542
- if (!input?.command) throw new Error("bash: command is required");
543
+ if (!input?.command || typeof input.command !== "string" || !input.command.trim()) {
544
+ throw new ToolValidationError({
545
+ message: "bash: command is required and cannot be empty",
546
+ field: "command"
547
+ });
548
+ }
543
549
  const registry = getProcessRegistry();
544
550
  const bypassBreaker = !!input.background;
545
551
  if (!registry.beforeCall(bypassBreaker)) {
@@ -571,7 +577,8 @@ var bashTool = {
571
577
  }
572
578
  const PIPE_TO_SHELL_PATTERN = /\|\s*(sh|bash|ksh|zsh|fish|cmd|powershell|pwsh)/i;
573
579
  const pipeToShellNote = PIPE_TO_SHELL_PATTERN.test(input.command) ? "\n\n[wrongstack] Caution: this command pipes output into a shell interpreter (pipe-to-shell). Piped content executes as arbitrary code \u2014 review the source before trusting the result, and prefer downloading to a file and inspecting it first." : "";
574
- const timeoutMs = Math.max(1, Math.min(input.timeout_ms ?? DEFAULT_TIMEOUT_MS, 6e5));
580
+ const rawTimeout = typeof input.timeout_ms === "number" && !Number.isNaN(input.timeout_ms) ? input.timeout_ms : DEFAULT_TIMEOUT_MS;
581
+ const timeoutMs = Math.max(1, Math.min(rawTimeout, 6e5));
575
582
  const isWin2 = os2.platform() === "win32";
576
583
  let plan;
577
584
  let winShellKind;
@@ -605,6 +612,18 @@ var bashTool = {
605
612
  const env = buildChildEnv(ctx.session?.id);
606
613
  const spawnCwd = ctx.workingDir ?? ctx.projectRoot;
607
614
  const callerSignal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
615
+ if (callerSignal.aborted) {
616
+ yield {
617
+ type: "final",
618
+ output: {
619
+ output: "Aborted",
620
+ exit_code: 124,
621
+ timed_out: true,
622
+ pid: null
623
+ }
624
+ };
625
+ return;
626
+ }
608
627
  const detached = !isWin2;
609
628
  const startedAt = Date.now();
610
629
  if (input.background) {
@@ -829,13 +848,15 @@ var bashTool = {
829
848
  if (stream === "stdout") stdoutBytes += chunk.byteLength;
830
849
  else stderrBytes += chunk.byteLength;
831
850
  emitProcessOutput({ pid, stream, chunk });
832
- if (buf.length < MAX_OUTPUT) {
833
- buf += text.slice(0, MAX_OUTPUT - buf.length);
851
+ if (text.length > 0) {
852
+ if (buf.length < MAX_OUTPUT) {
853
+ buf += text.slice(0, MAX_OUTPUT - buf.length);
854
+ }
855
+ spool.write(text);
856
+ pending += text;
857
+ push({ kind: "data", text });
858
+ pauseIfFlooded();
834
859
  }
835
- spool.write(text);
836
- pending += text;
837
- push({ kind: "data", text });
838
- pauseIfFlooded();
839
860
  };
840
861
  const onStdoutData = (chunk) => onData(chunk, "stdout");
841
862
  const onStderrData = (chunk) => onData(chunk, "stderr");
@@ -872,14 +893,17 @@ var bashTool = {
872
893
  }
873
894
  const spooled = spool.finalize();
874
895
  const hint = !timedOut && typeof c.code === "number" && c.code !== 0 && winShellKind ? diagnoseBashism(input.command, winShellKind) : void 0;
896
+ const isAborted = callerSignal.aborted;
875
897
  yield {
876
898
  type: "final",
877
899
  output: {
878
900
  output: normalizeCommandOutput(buf) + (spooled ? spoolNote(spooled) : "") + (hint ? `
879
901
 
880
902
  ${hint}` : "") + pipeToShellNote,
881
- exit_code: c.code,
882
- timed_out: timedOut
903
+ exit_code: timedOut || isAborted ? 124 : c.code,
904
+ timed_out: timedOut || isAborted,
905
+ pid: pid ?? null,
906
+ error: isAborted ? "Command aborted by user or signal" : void 0
883
907
  }
884
908
  };
885
909
  ctx.recordSideEffect?.({
@@ -953,4 +977,4 @@ export {
953
977
  checkAndBlockKillCommand,
954
978
  bashTool
955
979
  };
956
- //# sourceMappingURL=chunk-WNUJR3H4.js.map
980
+ //# sourceMappingURL=chunk-AKHYMB4V.js.map
@@ -2,7 +2,7 @@ import {
2
2
  executeLanguagePlan,
3
3
  executePackagePlan,
4
4
  planLanguageOperation
5
- } from "./chunk-UC3RKRIV.js";
5
+ } from "./chunk-TTZOK6IY.js";
6
6
 
7
7
  // src/languages/legacy-bridge.ts
8
8
  import * as fs from "node:fs/promises";
@@ -98,4 +98,4 @@ export {
98
98
  tryLegacyCodeOperation,
99
99
  tryLegacyPackageOperation
100
100
  };
101
- //# sourceMappingURL=chunk-ZDUGI5BC.js.map
101
+ //# sourceMappingURL=chunk-ATSKPZUK.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  todoTool
3
- } from "./chunk-5ZYEGP2P.js";
3
+ } from "./chunk-BK732J7I.js";
4
4
  import {
5
5
  projectSessionPlanToKanban
6
- } from "./chunk-CWALNI6K.js";
6
+ } from "./chunk-X6KXNHUO.js";
7
7
 
8
8
  // src/plan.ts
9
9
  import { randomUUID } from "node:crypto";
@@ -13,11 +13,13 @@ import {
13
13
  deriveTodosFromPlanItem,
14
14
  formatPlan,
15
15
  getPlanTemplate,
16
+ loadPlan,
16
17
  mutatePlan,
17
18
  mutateTasks,
18
19
  removePlanItem,
19
20
  setPlanItemStatus
20
21
  } from "@wrongstack/core/storage";
22
+ import { ToolValidationError } from "@wrongstack/core/types";
21
23
  import { formatTaskList } from "@wrongstack/core/utils";
22
24
  var planTool = {
23
25
  name: "plan",
@@ -85,8 +87,35 @@ var planTool = {
85
87
  },
86
88
  required: ["action"]
87
89
  },
88
- async execute(input, ctx) {
89
- const sessionPlanPath = ctx.meta["plan.path"];
90
+ async execute(input, ctx, _opts) {
91
+ const signal = _opts?.signal ?? ctx?.signal;
92
+ signal?.throwIfAborted();
93
+ const VALID_ACTIONS = /* @__PURE__ */ new Set([
94
+ "show",
95
+ "add",
96
+ "status",
97
+ "start",
98
+ "done",
99
+ "remove",
100
+ "promote",
101
+ "template_use",
102
+ "clear",
103
+ "taskify"
104
+ ]);
105
+ if (!input?.action || !VALID_ACTIONS.has(input.action)) {
106
+ throw new ToolValidationError({
107
+ message: `plan: unknown or missing action "${input?.action}". Allowed actions: ${[...VALID_ACTIONS].join(", ")}`,
108
+ field: "action"
109
+ });
110
+ }
111
+ if (input.scope !== void 0 && input.scope !== "session" && input.scope !== "project") {
112
+ throw new ToolValidationError({
113
+ message: `plan: invalid scope "${input.scope}". Allowed: session, project`,
114
+ field: "scope"
115
+ });
116
+ }
117
+ const meta = ctx.meta ??= {};
118
+ const sessionPlanPath = meta["plan.path"];
90
119
  let planPath;
91
120
  if (input.scope === "project") {
92
121
  if (typeof sessionPlanPath === "string") {
@@ -118,6 +147,16 @@ var planTool = {
118
147
  };
119
148
  }
120
149
  const sessionId = ctx.session?.id ?? "unknown";
150
+ if (input.action === "show") {
151
+ meta["plan.path.resolved"] = planPath;
152
+ const plan2 = await loadPlan(planPath) ?? {
153
+ version: 1,
154
+ sessionId,
155
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
156
+ items: []
157
+ };
158
+ return mkResult(plan2, true, "Plan show ok.");
159
+ }
121
160
  let early = null;
122
161
  const taskifyMeta = { title: "", details: "" };
123
162
  let didTaskify = false;
@@ -159,6 +198,7 @@ var planTool = {
159
198
  return next;
160
199
  }
161
200
  case "status": {
201
+ const VALID_PLAN_STATUSES = /* @__PURE__ */ new Set(["open", "in_progress", "done"]);
162
202
  if (!input.target || !input.status) {
163
203
  early = mkResult(
164
204
  p,
@@ -167,6 +207,14 @@ var planTool = {
167
207
  );
168
208
  return p;
169
209
  }
210
+ if (!VALID_PLAN_STATUSES.has(input.status)) {
211
+ early = mkResult(
212
+ p,
213
+ false,
214
+ `status requires valid status ('open' | 'in_progress' | 'done'), got "${input.status}".`
215
+ );
216
+ return p;
217
+ }
170
218
  const next = setPlanItemStatus(p, input.target, input.status);
171
219
  if (next === p) {
172
220
  early = mkResult(p, false, `No plan item matched "${input.target}".`);
@@ -348,7 +396,10 @@ ${formatTaskList(taskFile.tasks)}`
348
396
  }
349
397
  };
350
398
  function mkResult(plan, ok, message, todos) {
351
- const open = plan.items.filter((i) => i.status !== "done").length;
399
+ let open = 0;
400
+ for (let i = 0; i < plan.items.length; i++) {
401
+ if (plan.items[i].status !== "done") open++;
402
+ }
352
403
  const result = {
353
404
  ok,
354
405
  message,
@@ -363,4 +414,4 @@ function mkResult(plan, ok, message, todos) {
363
414
  export {
364
415
  planTool
365
416
  };
366
- //# sourceMappingURL=chunk-H6AWAZ2Y.js.map
417
+ //# sourceMappingURL=chunk-B4HTSV6Z.js.map
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  kanbanTool
3
- } from "./chunk-FJEQZHZN.js";
3
+ } from "./chunk-W2CECGTG.js";
4
4
  import {
5
5
  applyManagedKanbanBoardToTodos,
6
6
  blockingTitles,
7
7
  mirrorSessionTodosToKanban,
8
8
  takeSessionMirrorFailure
9
- } from "./chunk-CWALNI6K.js";
9
+ } from "./chunk-X6KXNHUO.js";
10
10
 
11
11
  // src/todo.ts
12
12
  import {
@@ -16,6 +16,7 @@ import {
16
16
  saveTasks,
17
17
  setPlanItemStatus
18
18
  } from "@wrongstack/core/storage";
19
+ import { ToolValidationError } from "@wrongstack/core/types";
19
20
  import { addTask, getBoard } from "@wrongstack/kanban";
20
21
  function normalizedTitle(value) {
21
22
  return value.trim().toLocaleLowerCase().replace(/\s+/g, " ");
@@ -27,6 +28,10 @@ function activeBoardId(items, ctx) {
27
28
  }
28
29
  function bindTodosToBoard(items, previous, board) {
29
30
  const previousById = new Map(previous.map((item) => [item.id, item]));
31
+ const taskById = new Map(board.tasks.map((task) => [task.id, task]));
32
+ const taskByOriginId = new Map(
33
+ board.tasks.filter((task) => task.origin?.taskId).map((task) => [task.origin.taskId, task])
34
+ );
30
35
  const available = board.tasks.filter(
31
36
  (task) => task.status !== "archived" && task.mergedIntoTaskId === void 0 && (!task.childTaskIds || task.childTaskIds.length === 0)
32
37
  ).sort(
@@ -38,9 +43,9 @@ function bindTodosToBoard(items, previous, board) {
38
43
  const requestedTaskId = item.kanbanBoardId === board.id ? item.kanbanTaskId : previousItem?.kanbanBoardId === board.id ? previousItem.kanbanTaskId : void 0;
39
44
  const title = normalizedTitle(item.content);
40
45
  const candidates = [
41
- requestedTaskId ? board.tasks.find((task2) => task2.id === requestedTaskId) : void 0,
42
- board.tasks.find((task2) => task2.id === item.id),
43
- board.tasks.find((task2) => task2.origin?.taskId === item.id),
46
+ requestedTaskId ? taskById.get(requestedTaskId) : void 0,
47
+ taskById.get(item.id),
48
+ taskByOriginId.get(item.id),
44
49
  available.find((task2) => !used.has(task2.id) && normalizedTitle(task2.title) === title)
45
50
  ];
46
51
  const task = candidates.find((candidate) => candidate && !used.has(candidate.id));
@@ -80,7 +85,10 @@ async function createMissingManagedCards(items, board, ctx, warnings) {
80
85
  description: item.activeForm?.trim() || `Added from the session todo list: ${item.content}`
81
86
  },
82
87
  // The card mirrors this session's todo row, so the session owns it.
83
- { sessionId: ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session", actor: "todo" }
88
+ {
89
+ sessionId: ctx.eventSessionId?.() ?? ctx.session?.id ?? "default-session",
90
+ actor: "todo"
91
+ }
84
92
  );
85
93
  if (!result) {
86
94
  warnings.push(`Could not open a Kanban card for "${item.content}": board not found.`);
@@ -287,8 +295,40 @@ var todoTool = {
287
295
  required: ["todos"]
288
296
  },
289
297
  async execute(input, ctx, call) {
298
+ const signal = call?.signal ?? ctx.signal ?? new AbortController().signal;
299
+ signal.throwIfAborted();
290
300
  if (!Array.isArray(input?.todos)) {
291
- throw new Error("todo: todos must be an array");
301
+ throw new ToolValidationError({
302
+ message: "todo: todos must be an array",
303
+ field: "todos"
304
+ });
305
+ }
306
+ const VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
307
+ for (const item of input.todos) {
308
+ if (!item || typeof item !== "object") {
309
+ throw new ToolValidationError({
310
+ message: "todo: each item in todos must be an object",
311
+ field: "todos"
312
+ });
313
+ }
314
+ if (!item.id || typeof item.id !== "string" || !item.id.trim()) {
315
+ throw new ToolValidationError({
316
+ message: 'todo: each item must have a non-empty string "id"',
317
+ field: "todos"
318
+ });
319
+ }
320
+ if (item.content === void 0 || typeof item.content !== "string") {
321
+ throw new ToolValidationError({
322
+ message: `todo: item "${item.id}" must have a string "content"`,
323
+ field: "todos"
324
+ });
325
+ }
326
+ if (item.status !== void 0 && !VALID_STATUSES.has(item.status)) {
327
+ throw new ToolValidationError({
328
+ message: `todo: item "${item.id}" has invalid status "${item.status}". Allowed: pending, in_progress, completed`,
329
+ field: "todos"
330
+ });
331
+ }
292
332
  }
293
333
  const items = input.todos.filter((t) => Boolean(t?.id && t.content));
294
334
  const todoIdentity = (item) => item.kanbanBoardId && item.kanbanTaskId ? `kanban:${item.kanbanBoardId}:${item.kanbanTaskId}` : item.promotedFromTask ? `task:${item.promotedFromTask}` : item.promotedFromPlan ? `plan:${item.promotedFromPlan}` : `todo:${item.id}`;
@@ -330,7 +370,7 @@ var todoTool = {
330
370
  boundItems = demoteBlockedInProgress(boundItems, creationWarnings);
331
371
  }
332
372
  ctx.state.replaceTodos(boundItems);
333
- const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, call.signal) : { synced: 0, warnings: [] };
373
+ const kanbanSync = managed && board ? await synchronizeManagedKanban(boundItems, board, ctx, signal) : { synced: 0, warnings: [] };
334
374
  kanbanSync.warnings.unshift(...creationWarnings);
335
375
  if (managed && board) {
336
376
  const unresolved = boundItems.filter(
@@ -371,34 +411,39 @@ var todoTool = {
371
411
  );
372
412
  }
373
413
  }
374
- for (const planId of completedPlanIds) {
375
- if (pendingPlanIds.has(planId)) continue;
376
- const meta = ctx.meta;
377
- const planPath = meta["plan.path.resolved"] ?? meta["plan.path"];
378
- if (typeof planPath !== "string" || !planPath) continue;
414
+ const meta = ctx.meta;
415
+ const planPath = meta?.["plan.path.resolved"] ?? meta?.["plan.path"];
416
+ if (typeof planPath === "string" && planPath && completedPlanIds.size > 0) {
379
417
  try {
380
- const plan = await loadPlan(planPath);
418
+ let plan = await loadPlan(planPath);
381
419
  if (plan) {
382
- const updated = setPlanItemStatus(plan, planId, "done");
383
- await savePlan(planPath, updated);
420
+ let modified = false;
421
+ for (const planId of completedPlanIds) {
422
+ if (pendingPlanIds.has(planId)) continue;
423
+ plan = setPlanItemStatus(plan, planId, "done");
424
+ modified = true;
425
+ }
426
+ if (modified) await savePlan(planPath, plan);
384
427
  }
385
428
  } catch {
386
429
  }
387
430
  }
388
- for (const taskId of completedTaskIds) {
389
- if (pendingTaskIds.has(taskId)) continue;
390
- const meta = ctx.meta;
391
- const taskPath = meta["task.path.resolved"] ?? meta["task.path"];
392
- if (typeof taskPath !== "string" || !taskPath) continue;
431
+ const taskPath = meta?.["task.path.resolved"] ?? meta?.["task.path"];
432
+ if (typeof taskPath === "string" && taskPath && completedTaskIds.size > 0) {
393
433
  try {
394
434
  const file = await loadTasks(taskPath);
395
435
  if (file) {
396
- const task = file.tasks.find((t) => t.id === taskId);
397
- if (task && task.status !== "completed") {
398
- task.status = "completed";
399
- task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
400
- await saveTasks(taskPath, file);
436
+ let modified = false;
437
+ for (const taskId of completedTaskIds) {
438
+ if (pendingTaskIds.has(taskId)) continue;
439
+ const task = file.tasks.find((t) => t.id === taskId);
440
+ if (task && task.status !== "completed") {
441
+ task.status = "completed";
442
+ task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
443
+ modified = true;
444
+ }
401
445
  }
446
+ if (modified) await saveTasks(taskPath, file);
402
447
  }
403
448
  } catch {
404
449
  }
@@ -431,4 +476,4 @@ var todoTool = {
431
476
  export {
432
477
  todoTool
433
478
  };
434
- //# sourceMappingURL=chunk-5ZYEGP2P.js.map
479
+ //# sourceMappingURL=chunk-BK732J7I.js.map