@wrongstack/plugins 1.0.8 → 1.0.11

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 (73) hide show
  1. package/dist/accessibility-auditor/index.d.ts +1 -1
  2. package/dist/accessibility-auditor.js +15 -3
  3. package/dist/agent-handoff.js +6 -6
  4. package/dist/auto-doc/index.d.ts +1 -1
  5. package/dist/auto-doc.js +31 -20
  6. package/dist/auto-i18n-extractor/index.d.ts +1 -1
  7. package/dist/auto-i18n-extractor.js +12 -8
  8. package/dist/branch-guard.js +3 -3
  9. package/dist/changelog-writer/index.d.ts +1 -1
  10. package/dist/changelog-writer.js +19 -10
  11. package/dist/checkpoint/index.d.ts +1 -1
  12. package/dist/checkpoint.js +38 -24
  13. package/dist/code-metrics/index.d.ts +1 -1
  14. package/dist/code-metrics.js +12 -4
  15. package/dist/commit-validator.js +5 -5
  16. package/dist/context-pins/index.d.ts +1 -1
  17. package/dist/context-pins.js +12 -9
  18. package/dist/cost-tracker.js +21 -9
  19. package/dist/cron/index.d.ts +1 -1
  20. package/dist/cron.js +18 -5
  21. package/dist/dead-code-detector/index.d.ts +1 -1
  22. package/dist/dead-code-detector.js +14 -12
  23. package/dist/duplicate-code-detector/index.d.ts +1 -1
  24. package/dist/duplicate-code-detector.js +11 -3
  25. package/dist/feature-flag-tracker/index.d.ts +1 -1
  26. package/dist/feature-flag-tracker.js +12 -4
  27. package/dist/file-watcher/index.d.ts +1 -1
  28. package/dist/file-watcher.js +37 -38
  29. package/dist/git-autocommit/index.d.ts +1 -1
  30. package/dist/git-autocommit.js +44 -39
  31. package/dist/gitignore-guard/index.d.ts +1 -1
  32. package/dist/gitignore-guard.js +12 -6
  33. package/dist/index.js +1553 -1086
  34. package/dist/interface-contract-guard/index.d.ts +1 -1
  35. package/dist/interface-contract-guard.js +12 -4
  36. package/dist/knowledge-graph/index.d.ts +1 -1
  37. package/dist/knowledge-graph.js +11 -9
  38. package/dist/loop-breaker.js +11 -4
  39. package/dist/migration-planner/index.d.ts +1 -1
  40. package/dist/migration-planner.js +6 -2
  41. package/dist/notify-hub/index.d.ts +1 -1
  42. package/dist/notify-hub.js +19 -12
  43. package/dist/performance-regression-gate/index.d.ts +1 -1
  44. package/dist/performance-regression-gate.js +33 -13
  45. package/dist/pr-drafter/index.d.ts +10 -1
  46. package/dist/pr-drafter.js +57 -26
  47. package/dist/refactor-suggester/index.d.ts +1 -1
  48. package/dist/refactor-suggester.js +17 -4
  49. package/dist/release-notes-generator.js +2 -2
  50. package/dist/secret-scanner/index.d.ts +1 -1
  51. package/dist/secret-scanner.js +11 -3
  52. package/dist/security-hotspot-scanner/index.d.ts +1 -1
  53. package/dist/security-hotspot-scanner.js +6 -2
  54. package/dist/semantic-search-indexer/index.d.ts +1 -1
  55. package/dist/semantic-search-indexer.js +19 -3
  56. package/dist/semver-bump/index.d.ts +1 -1
  57. package/dist/semver-bump.js +57 -37
  58. package/dist/session-recap.js +4 -2
  59. package/dist/shell-check/index.d.ts +1 -1
  60. package/dist/shell-check.js +30 -39
  61. package/dist/smart-rename/index.d.ts +1 -1
  62. package/dist/smart-rename.js +23 -10
  63. package/dist/template-engine/index.d.ts +1 -1
  64. package/dist/template-engine.js +51 -38
  65. package/dist/test-flake-detector/index.d.ts +1 -1
  66. package/dist/test-flake-detector.js +11 -5
  67. package/dist/test-generator/index.d.ts +1 -1
  68. package/dist/test-generator.js +12 -8
  69. package/dist/todo-tracker/index.d.ts +68 -1
  70. package/dist/todo-tracker.js +579 -395
  71. package/dist/token-budget.js +7 -4
  72. package/dist/token-throttle.js +6 -3
  73. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -13,8 +13,9 @@ var __copyProps = (to, from, except, desc) => {
13
13
  var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
14
14
 
15
15
  // src/accessibility-auditor/index.ts
16
- import { readFile } from "node:fs/promises";
16
+ import { readFile, stat } from "node:fs/promises";
17
17
  import { isAbsolute, relative, resolve } from "node:path";
18
+ import { ToolValidationError } from "@wrongstack/core/types";
18
19
 
19
20
  // src/runtime/index.ts
20
21
  var runtime_exports = {};
@@ -53,6 +54,13 @@ function readConfig(raw) {
53
54
  onWriteEdit: (r["onWriteEdit"] ?? r["on_write_edit"] ?? r["onSave"]) !== false
54
55
  };
55
56
  }
57
+ async function assertPathExists(rawPath) {
58
+ try {
59
+ await stat(resolve(process.cwd(), rawPath));
60
+ } catch (err) {
61
+ throw new Error(`path not found: ${rawPath}`, { cause: err });
62
+ }
63
+ }
56
64
  function normalizeExtensions(exts) {
57
65
  return exts.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`);
58
66
  }
@@ -381,12 +389,16 @@ var plugin = {
381
389
  category: "Diagnostics",
382
390
  mutating: false,
383
391
  async execute(input) {
384
- if (!cfg.enabled) return { ok: false, error: "accessibility-auditor is disabled" };
392
+ if (!cfg.enabled) throw new Error("accessibility-auditor is disabled");
385
393
  const raw = input;
386
394
  const rawPath = (typeof input.path === "string" && input.path.trim().length > 0 ? input.path.trim() : void 0) ?? (typeof raw["directory"] === "string" ? raw["directory"] : void 0) ?? (typeof raw["dir"] === "string" ? raw["dir"] : void 0) ?? (typeof raw["SearchDirectory"] === "string" ? raw["SearchDirectory"] : void 0) ?? (typeof raw["TargetFile"] === "string" ? raw["TargetFile"] : void 0) ?? (typeof raw["filePath"] === "string" ? raw["filePath"] : void 0) ?? (typeof raw["file_path"] === "string" ? raw["file_path"] : void 0) ?? (typeof raw["targetFile"] === "string" ? raw["targetFile"] : void 0) ?? (typeof raw["file"] === "string" ? raw["file"] : void 0) ?? ".";
387
395
  if (!(0, runtime_exports.withinProject)(rawPath)) {
388
- return { ok: false, error: "path must be inside the project" };
396
+ throw new ToolValidationError({
397
+ message: "path must be inside the project",
398
+ field: "path"
399
+ });
389
400
  }
401
+ await assertPathExists(rawPath);
390
402
  state.auditCount += 1;
391
403
  const result = await auditPath(rawPath, cfg);
392
404
  state.fileCount += result.fileCount;
@@ -701,8 +713,8 @@ var plugin2 = {
701
713
  category: "Coordination",
702
714
  mutating: false,
703
715
  async execute(input) {
704
- if (!cfg.enabled) return { ok: false, error: "agent-handoff is disabled" };
705
- if (!mailbox) return { ok: false, error: "mailbox not available" };
716
+ if (!cfg.enabled) throw new Error("agent-handoff is disabled");
717
+ if (!mailbox) throw new Error("mailbox not available");
706
718
  const raw = input;
707
719
  const summary = (typeof raw["summary"] === "string" ? raw["summary"] : void 0) ?? (typeof raw["note"] === "string" ? raw["note"] : void 0) ?? (typeof raw["message"] === "string" ? raw["message"] : void 0) ?? (typeof raw["content"] === "string" ? raw["content"] : void 0) ?? (typeof raw["text"] === "string" ? raw["text"] : void 0) ?? (typeof raw["body"] === "string" ? raw["body"] : void 0);
708
720
  const task = (typeof raw["task"] === "string" ? raw["task"] : void 0) ?? (typeof raw["title"] === "string" ? raw["title"] : void 0) ?? (typeof raw["subject"] === "string" ? raw["subject"] : void 0);
@@ -729,10 +741,10 @@ var plugin2 = {
729
741
  return { ok: true, messageId: result.id, to: recipient };
730
742
  } catch (err) {
731
743
  state2.errorCount += 1;
732
- return {
733
- ok: false,
734
- error: err instanceof Error ? err.message : String(err)
735
- };
744
+ throw new Error(
745
+ `handoff_note: mailbox send to "${recipient}" failed: ${err instanceof Error ? err.message : String(err)}`,
746
+ { cause: err }
747
+ );
736
748
  }
737
749
  }
738
750
  });
@@ -1171,6 +1183,7 @@ var api_compatibility_gate_default = plugin3;
1171
1183
 
1172
1184
  // src/auto-doc/index.ts
1173
1185
  import { isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
1186
+ import { ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
1174
1187
  var AUTO_DOC_API_VERSION = "^0.1.10";
1175
1188
  function resolveProjectPath(rawPath, cwd = process.cwd()) {
1176
1189
  if (typeof rawPath !== "string" || rawPath.length === 0) return null;
@@ -1334,41 +1347,37 @@ function injectDocComment(content, entity, doc) {
1334
1347
  async function runAutoDoc(input, api) {
1335
1348
  const rawInput = input;
1336
1349
  if (rawInput["files"] !== void 0 && !Array.isArray(rawInput["files"])) {
1337
- return {
1338
- ok: false,
1339
- error: "input.files must be an array of file paths",
1340
- filesProcessed: 0,
1341
- changes: []
1342
- };
1350
+ throw new ToolValidationError2({
1351
+ message: "input.files must be an array of file paths",
1352
+ field: "files"
1353
+ });
1343
1354
  }
1344
1355
  const rawFiles = rawInput["files"] ?? rawInput["file"] ?? rawInput["path"] ?? rawInput["filePath"] ?? rawInput["TargetFile"] ?? rawInput["targetFile"];
1345
1356
  const files = Array.isArray(rawFiles) ? rawFiles.filter((f) => typeof f === "string" && f.trim().length > 0).map((f) => f.trim()) : typeof rawFiles === "string" && rawFiles.trim().length > 0 ? [rawFiles.trim()] : void 0;
1346
1357
  if (!files || !Array.isArray(files)) {
1347
- return {
1348
- ok: false,
1349
- error: "input.files must be an array of file paths",
1350
- filesProcessed: 0,
1351
- changes: []
1352
- };
1358
+ throw new ToolValidationError2({
1359
+ message: "input.files must be an array of file paths",
1360
+ field: "files"
1361
+ });
1353
1362
  }
1354
1363
  if (files.length === 0) {
1355
- return {
1356
- ok: false,
1357
- error: "input.files is empty \u2014 provide at least one file path",
1358
- filesProcessed: 0,
1359
- changes: []
1360
- };
1364
+ throw new ToolValidationError2({
1365
+ message: "input.files is empty \u2014 provide at least one file path",
1366
+ field: "files"
1367
+ });
1361
1368
  }
1362
1369
  const extConfig = api.config.extensions?.["auto-doc"] ?? {};
1363
1370
  const includeTypes = extConfig["includeTypes"] ?? false;
1364
1371
  const useLlm = (input.use_llm ?? rawInput["useLlm"] ?? extConfig["useLlm"] ?? false) === true && Boolean(api.llm);
1365
1372
  const maxLlmEntities = typeof extConfig["maxLlmEntities"] === "number" && extConfig["maxLlmEntities"] >= 0 ? extConfig["maxLlmEntities"] : 25;
1366
1373
  const results = [];
1374
+ const skipped = [];
1367
1375
  let llmBudget = maxLlmEntities;
1368
1376
  for (const rawFile of files) {
1369
1377
  const safeFile = resolveProjectPath(rawFile);
1370
1378
  if (!safeFile) {
1371
1379
  api.log.warn(`auto-doc: skipped file outside project directory: ${rawFile}`);
1380
+ skipped.push({ file: rawFile, reason: "outside project directory" });
1372
1381
  continue;
1373
1382
  }
1374
1383
  try {
@@ -1376,8 +1385,12 @@ async function runAutoDoc(input, api) {
1376
1385
  let content;
1377
1386
  try {
1378
1387
  content = readFileSync17(safeFile, "utf-8");
1379
- } catch {
1388
+ } catch (err) {
1380
1389
  api.log.warn(`auto-doc: could not read file ${safeFile}`);
1390
+ skipped.push({
1391
+ file: safeFile,
1392
+ reason: `could not read: ${err instanceof Error ? err.message : String(err)}`
1393
+ });
1381
1394
  continue;
1382
1395
  }
1383
1396
  const entities = parseSource(content);
@@ -1417,11 +1430,21 @@ async function runAutoDoc(input, api) {
1417
1430
  }
1418
1431
  } catch (err) {
1419
1432
  api.log.error(`auto-doc: error processing ${safeFile}: ${err}`);
1433
+ skipped.push({
1434
+ file: safeFile,
1435
+ reason: err instanceof Error ? err.message : String(err)
1436
+ });
1420
1437
  }
1421
1438
  }
1439
+ if (skipped.length === files.length) {
1440
+ throw new Error(
1441
+ `auto_doc processed no files: ${skipped.map((s) => `${s.file} (${s.reason})`).join("; ")}`
1442
+ );
1443
+ }
1422
1444
  return {
1423
1445
  ok: true,
1424
- filesProcessed: files.length,
1446
+ filesProcessed: files.length - skipped.length,
1447
+ ...skipped.length > 0 ? { skipped } : {},
1425
1448
  changes: results,
1426
1449
  llm: useLlm ? { docs: state4.llmDocs, fallbacks: state4.llmFallbacks } : void 0
1427
1450
  };
@@ -1738,6 +1761,7 @@ var auto_escalate_default = plugin5;
1738
1761
 
1739
1762
  // src/auto-i18n-extractor/index.ts
1740
1763
  import { readFileSync as readFileSync2 } from "node:fs";
1764
+ import { ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
1741
1765
  var API_VERSION4 = "^0.1.10";
1742
1766
  var state6 = {
1743
1767
  filesScanned: 0,
@@ -1961,28 +1985,31 @@ ${lines.join("\n")}${more}`;
1961
1985
  category: "Diagnostics",
1962
1986
  mutating: false,
1963
1987
  async execute(input) {
1964
- if (!cfg.enabled) return { ok: false, error: "auto-i18n-extractor is disabled" };
1988
+ if (!cfg.enabled) throw new Error("auto-i18n-extractor is disabled");
1965
1989
  const raw = input;
1966
1990
  const rawPath = (typeof input.path === "string" && input.path.trim().length > 0 ? input.path.trim() : void 0) ?? (typeof raw["TargetFile"] === "string" && raw["TargetFile"].trim().length > 0 ? raw["TargetFile"].trim() : void 0) ?? (typeof raw["targetFile"] === "string" && raw["targetFile"].trim().length > 0 ? raw["targetFile"].trim() : void 0) ?? (typeof raw["filePath"] === "string" && raw["filePath"].trim().length > 0 ? raw["filePath"].trim() : void 0) ?? (typeof raw["file_path"] === "string" && raw["file_path"].trim().length > 0 ? raw["file_path"].trim() : void 0) ?? (typeof raw["file"] === "string" && raw["file"].trim().length > 0 ? raw["file"].trim() : void 0);
1967
1991
  const filePath = typeof rawPath === "string" ? rawPath.trim() : "";
1968
1992
  if (!filePath) {
1969
- return { ok: false, error: "path is required" };
1993
+ throw new ToolValidationError3({ message: "path is required", field: "path" });
1970
1994
  }
1971
1995
  if (!(0, runtime_exports.withinProject)(filePath)) {
1972
- return { ok: false, error: "path must be inside the project" };
1996
+ throw new ToolValidationError3({
1997
+ message: "path must be inside the project",
1998
+ field: "path"
1999
+ });
1973
2000
  }
1974
2001
  const ext = fileExtension(filePath);
1975
2002
  if (!cfg.fileExtensions.includes(ext)) {
1976
- return {
1977
- ok: false,
1978
- error: `unsupported extension "${ext}"; allowed: ${cfg.fileExtensions.join(", ")}`
1979
- };
2003
+ throw new ToolValidationError3({
2004
+ message: `unsupported extension "${ext}"; allowed: ${cfg.fileExtensions.join(", ")}`,
2005
+ field: "path"
2006
+ });
1980
2007
  }
1981
2008
  state6.filesScanned += 1;
1982
2009
  const content = readSourceFile(filePath);
1983
2010
  if (content === null) {
1984
2011
  state6.readErrors += 1;
1985
- return { ok: false, error: `could not read ${filePath}` };
2012
+ throw new Error(`could not read ${filePath}`);
1986
2013
  }
1987
2014
  const extracted = extractStrings(content, cfg);
1988
2015
  state6.stringsFound += extracted.length;
@@ -2169,13 +2196,13 @@ async function detectUncommittedChanges(cwd, signal) {
2169
2196
  }
2170
2197
  function detectGitCommand(command) {
2171
2198
  const cmd = command.trim();
2172
- if (/\bgit\s+commit\b/.test(cmd)) {
2199
+ if (/\bgit\s+commit(?![a-zA-Z0-9_-])/.test(cmd)) {
2173
2200
  return { type: "commit", snippet: cmd.slice(0, 120) };
2174
2201
  }
2175
- if (/\bgit\s+push\b/.test(cmd)) {
2202
+ if (/\bgit\s+push(?![a-zA-Z0-9_-])/.test(cmd)) {
2176
2203
  return { type: "push", snippet: cmd.slice(0, 120) };
2177
2204
  }
2178
- if (/\bgit\s+merge\s/.test(cmd)) {
2205
+ if (/\bgit\s+merge(?![a-zA-Z0-9_-])/.test(cmd)) {
2179
2206
  return { type: "merge", snippet: cmd.slice(0, 120) };
2180
2207
  }
2181
2208
  return null;
@@ -2389,6 +2416,7 @@ var branch_guard_default = plugin7;
2389
2416
  // src/changelog-writer/index.ts
2390
2417
  import { readFileSync as readFileSync3, writeFileSync } from "node:fs";
2391
2418
  import { isAbsolute as isAbsolute4, relative as relative4, resolve as resolve4 } from "node:path";
2419
+ import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
2392
2420
  var state8 = {
2393
2421
  entries: [],
2394
2422
  filesTouched: new runtime_exports.BoundedSet({ max: 2e3 }),
@@ -2685,11 +2713,13 @@ var plugin8 = {
2685
2713
  category: "Docs",
2686
2714
  mutating: false,
2687
2715
  async execute(input) {
2688
- if (!cfg.enabled) return { ok: false, error: "changelog-writer is disabled" };
2716
+ if (!cfg.enabled) throw new Error("changelog-writer is disabled");
2689
2717
  const raw = input ?? {};
2690
2718
  const rawText = input.text ?? raw["message"] ?? raw["entry"] ?? raw["content"] ?? raw["description"] ?? raw["desc"] ?? raw["summary"] ?? raw["body"] ?? raw["note"] ?? raw["item"];
2691
2719
  const text = String(rawText ?? "").trim();
2692
- if (!text) return { ok: false, error: "entry text must not be empty" };
2720
+ if (!text) {
2721
+ throw new ToolValidationError4({ message: "entry text must not be empty", field: "text" });
2722
+ }
2693
2723
  const section = SECTION_ORDER.includes(input.section) ? input.section : "Changed";
2694
2724
  addEntry({ section, text, origin: "manual", when: (/* @__PURE__ */ new Date()).toISOString() });
2695
2725
  return { ok: true, pendingEntries: state8.entries.length };
@@ -2744,12 +2774,12 @@ var plugin8 = {
2744
2774
  category: "Docs",
2745
2775
  mutating: true,
2746
2776
  async execute(input) {
2747
- if (!cfg.enabled) return { ok: false, error: "changelog-writer is disabled" };
2777
+ if (!cfg.enabled) throw new Error("changelog-writer is disabled");
2748
2778
  if (!cfg.filePath) {
2749
- return { ok: false, error: "filePath must stay within the current project directory" };
2779
+ throw new Error("filePath must stay within the current project directory");
2750
2780
  }
2751
2781
  if (state8.entries.length === 0) {
2752
- return { ok: false, error: "no pending entries \u2014 add some with changelog_add first" };
2782
+ throw new Error("no pending entries \u2014 add some with changelog_add first");
2753
2783
  }
2754
2784
  const rawInput = input ?? {};
2755
2785
  const shouldPolish = input.polish === true || rawInput["use_llm"] === true || rawInput["useLlm"] === true || rawInput["ai"] === true || rawInput["use_ai"] === true;
@@ -2757,16 +2787,22 @@ var plugin8 = {
2757
2787
  let existing = null;
2758
2788
  try {
2759
2789
  existing = readFileSync3(cfg.filePath, "utf-8");
2760
- } catch {
2790
+ } catch (err) {
2791
+ if (err.code !== "ENOENT") {
2792
+ throw new Error(
2793
+ `failed to read ${cfg.filePath}: ${err instanceof Error ? err.message : String(err)}`,
2794
+ { cause: err }
2795
+ );
2796
+ }
2761
2797
  existing = null;
2762
2798
  }
2763
2799
  try {
2764
2800
  writeFileSync(cfg.filePath, mergeIntoChangelog(existing, block));
2765
2801
  } catch (err) {
2766
- return {
2767
- ok: false,
2768
- error: `failed to write ${cfg.filePath}: ${err instanceof Error ? err.message : String(err)}`
2769
- };
2802
+ throw new Error(
2803
+ `failed to write ${cfg.filePath}: ${err instanceof Error ? err.message : String(err)}`,
2804
+ { cause: err }
2805
+ );
2770
2806
  }
2771
2807
  const written = state8.entries.length;
2772
2808
  state8.entries = [];
@@ -2823,8 +2859,9 @@ var plugin8 = {
2823
2859
  var changelog_writer_default = plugin8;
2824
2860
 
2825
2861
  // src/checkpoint/index.ts
2826
- import { mkdir, readFile as readFile3, realpath, stat, writeFile } from "node:fs/promises";
2862
+ import { mkdir, readFile as readFile3, realpath, stat as stat2, writeFile } from "node:fs/promises";
2827
2863
  import { dirname as dirname2, isAbsolute as isAbsolute5, relative as relative5, resolve as resolve5 } from "node:path";
2864
+ import { ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
2828
2865
  var state9 = {
2829
2866
  snapshots: [],
2830
2867
  nextId: 1,
@@ -2887,7 +2924,7 @@ async function realResolutionEscapes(absPath, root) {
2887
2924
  }
2888
2925
  async function pathExists(p) {
2889
2926
  try {
2890
- await stat(p);
2927
+ await stat2(p);
2891
2928
  return true;
2892
2929
  } catch {
2893
2930
  return false;
@@ -2903,18 +2940,24 @@ function hashContent(s) {
2903
2940
  }
2904
2941
  async function captureFile(path, maxBytes) {
2905
2942
  try {
2906
- const st = await stat(path);
2943
+ const st = await stat2(path);
2907
2944
  if (st.size > maxBytes) return "too-large";
2908
2945
  const content = await readFile3(path, "utf-8");
2909
2946
  return { path, content, bytes: st.size };
2910
- } catch {
2947
+ } catch (err) {
2948
+ if (err.code !== "ENOENT") {
2949
+ throw new Error(
2950
+ `could not snapshot ${path}: ${err instanceof Error ? err.message : String(err)}`,
2951
+ { cause: err }
2952
+ );
2953
+ }
2911
2954
  return { path, content: null, bytes: 0 };
2912
2955
  }
2913
2956
  }
2914
2957
  async function captureFileForHook(path, maxBytes, signal) {
2915
2958
  try {
2916
2959
  signal.throwIfAborted();
2917
- const st = await stat(path);
2960
+ const st = await stat2(path);
2918
2961
  if (st.size > maxBytes) return "too-large";
2919
2962
  const content = await readFile3(path, "utf-8");
2920
2963
  signal.throwIfAborted();
@@ -3049,7 +3092,7 @@ var plugin9 = {
3049
3092
  category: "Safety",
3050
3093
  mutating: false,
3051
3094
  async execute(input) {
3052
- if (!cfg.enabled) return { ok: false, error: "checkpoint is disabled" };
3095
+ if (!cfg.enabled) throw new Error("checkpoint is disabled");
3053
3096
  let paths = [];
3054
3097
  const rawInput = input;
3055
3098
  const raw = rawInput["paths"] ?? rawInput["path"] ?? rawInput["files"] ?? rawInput["file"] ?? rawInput["filePath"] ?? rawInput["file_path"] ?? rawInput["TargetFile"] ?? rawInput["targetFile"];
@@ -3058,16 +3101,25 @@ var plugin9 = {
3058
3101
  } else if (Array.isArray(raw)) {
3059
3102
  paths = raw.filter((p) => typeof p === "string" && p.trim().length > 0);
3060
3103
  }
3061
- if (paths.length === 0) return { ok: false, error: "paths must not be empty" };
3062
- const files = [];
3104
+ if (paths.length === 0) {
3105
+ throw new ToolValidationError5({ message: "paths must not be empty", field: "paths" });
3106
+ }
3063
3107
  const rejectedOutsideProject = [];
3064
- let skipped = 0;
3108
+ const safePaths = [];
3065
3109
  for (const p of paths) {
3066
3110
  const safePath = await resolveProjectPath3(p);
3067
- if (!safePath) {
3068
- rejectedOutsideProject.push(p);
3069
- continue;
3070
- }
3111
+ if (safePath) safePaths.push(safePath);
3112
+ else rejectedOutsideProject.push(p);
3113
+ }
3114
+ if (rejectedOutsideProject.length > 0) {
3115
+ throw new ToolValidationError5({
3116
+ message: `paths must stay within the current project directory: ${rejectedOutsideProject.join(", ")}`,
3117
+ field: "paths"
3118
+ });
3119
+ }
3120
+ const files = [];
3121
+ let skipped = 0;
3122
+ for (const safePath of safePaths) {
3071
3123
  const captured = await captureFile(safePath, cfg.maxFileBytes);
3072
3124
  if (captured === "too-large") {
3073
3125
  skipped += 1;
@@ -3076,15 +3128,10 @@ var plugin9 = {
3076
3128
  }
3077
3129
  files.push(captured);
3078
3130
  }
3079
- if (rejectedOutsideProject.length > 0) {
3080
- return {
3081
- ok: false,
3082
- error: "paths must stay within the current project directory",
3083
- rejectedOutsideProject
3084
- };
3085
- }
3086
3131
  if (files.length === 0) {
3087
- return { ok: false, error: "all files were skipped (too large)" };
3132
+ throw new Error(
3133
+ `all files were skipped (larger than maxFileBytes=${cfg.maxFileBytes}); nothing was snapshotted`
3134
+ );
3088
3135
  }
3089
3136
  const snapshot = {
3090
3137
  id: `cp-${state9.nextId++}`,
@@ -3159,21 +3206,18 @@ var plugin9 = {
3159
3206
  category: "Safety",
3160
3207
  mutating: true,
3161
3208
  async execute(input) {
3162
- if (!cfg.enabled) return { ok: false, error: "checkpoint is disabled" };
3209
+ if (!cfg.enabled) throw new Error("checkpoint is disabled");
3163
3210
  const raw = input ?? {};
3164
3211
  const rawId = (typeof input.id === "string" && input.id.trim().length > 0 ? input.id.trim() : void 0) ?? (typeof raw["snapshotId"] === "string" ? raw["snapshotId"] : void 0) ?? (typeof raw["snapshot_id"] === "string" ? raw["snapshot_id"] : void 0);
3165
3212
  const rawPath = (typeof input.path === "string" && input.path.trim().length > 0 ? input.path.trim() : void 0) ?? (typeof raw["filePath"] === "string" ? raw["filePath"] : void 0) ?? (typeof raw["file_path"] === "string" ? raw["file_path"] : void 0) ?? (typeof raw["TargetFile"] === "string" ? raw["TargetFile"] : void 0) ?? (typeof raw["targetFile"] === "string" ? raw["targetFile"] : void 0) ?? (typeof raw["file"] === "string" ? raw["file"] : void 0);
3166
3213
  const snapshot = rawId ? state9.snapshots.find((s) => s.id === rawId) : state9.snapshots[state9.snapshots.length - 1];
3167
3214
  if (!snapshot) {
3168
- return {
3169
- ok: false,
3170
- error: rawId ? `no snapshot with id "${rawId}"` : "no snapshots captured yet"
3171
- };
3215
+ throw new Error(rawId ? `no snapshot with id "${rawId}"` : "no snapshots captured yet");
3172
3216
  }
3173
3217
  const targetPath = rawPath ? await resolveProjectPath3(rawPath) ?? rawPath : null;
3174
3218
  const targets = targetPath ? snapshot.files.filter((f) => f.path === targetPath || f.path === rawPath) : snapshot.files;
3175
3219
  if (targets.length === 0) {
3176
- return { ok: false, error: `snapshot ${snapshot.id} has no entry for "${rawPath}"` };
3220
+ throw new Error(`snapshot ${snapshot.id} has no entry for "${rawPath}"`);
3177
3221
  }
3178
3222
  const restored = [];
3179
3223
  const createdByTool = [];
@@ -3195,8 +3239,14 @@ var plugin9 = {
3195
3239
  state9.restores += 1;
3196
3240
  api.metrics.counter("restores");
3197
3241
  }
3242
+ if (errors.length > 0) {
3243
+ const failed = errors.map((e) => `${e.path} (${e.error})`).join("; ");
3244
+ throw new Error(
3245
+ `checkpoint_restore ${snapshot.id}: ${errors.length} file(s) failed to restore: ${failed}` + (restored.length > 0 ? `. Restored: ${restored.join(", ")}` : "")
3246
+ );
3247
+ }
3198
3248
  return {
3199
- ok: errors.length === 0,
3249
+ ok: true,
3200
3250
  snapshotId: snapshot.id,
3201
3251
  restored,
3202
3252
  notRestoredFileDidNotExist: createdByTool,
@@ -3255,8 +3305,9 @@ var plugin9 = {
3255
3305
  var checkpoint_default = plugin9;
3256
3306
 
3257
3307
  // src/code-metrics/index.ts
3258
- import { readFile as readFile4 } from "node:fs/promises";
3308
+ import { readFile as readFile4, stat as stat3 } from "node:fs/promises";
3259
3309
  import { isAbsolute as isAbsolute6, relative as relative6, resolve as resolve6 } from "node:path";
3310
+ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
3260
3311
  var API_VERSION6 = "^0.1.10";
3261
3312
  var state10 = {
3262
3313
  measureCount: 0,
@@ -3492,19 +3543,26 @@ var plugin10 = {
3492
3543
  category: "Diagnostics",
3493
3544
  mutating: false,
3494
3545
  async execute(input) {
3495
- if (!cfg.enabled) return { ok: false, error: "code-metrics is disabled" };
3546
+ if (!cfg.enabled) throw new Error("code-metrics is disabled");
3496
3547
  const raw = input ?? {};
3497
3548
  const rawPath = (typeof input.path === "string" && input.path.trim().length > 0 ? input.path.trim() : void 0) ?? (typeof raw["directory"] === "string" ? raw["directory"] : void 0) ?? (typeof raw["dir"] === "string" ? raw["dir"] : void 0) ?? (typeof raw["SearchDirectory"] === "string" ? raw["SearchDirectory"] : void 0) ?? (typeof raw["filePath"] === "string" ? raw["filePath"] : void 0) ?? (typeof raw["TargetFile"] === "string" ? raw["TargetFile"] : void 0) ?? (typeof raw["targetFile"] === "string" ? raw["targetFile"] : void 0) ?? (typeof raw["file_path"] === "string" ? raw["file_path"] : void 0) ?? (typeof raw["file"] === "string" ? raw["file"] : void 0) ?? ".";
3498
3549
  if (!(0, runtime_exports.withinProject)(rawPath)) {
3499
- return { ok: false, error: "path is outside the project root" };
3550
+ throw new ToolValidationError6({
3551
+ message: "path is outside the project root",
3552
+ field: "path"
3553
+ });
3500
3554
  }
3501
3555
  state10.measureCount += 1;
3502
3556
  let result;
3503
3557
  try {
3558
+ await stat3(resolve6(process.cwd(), rawPath));
3504
3559
  result = await measurePath(rawPath, cfg);
3505
3560
  } catch (err) {
3506
3561
  state10.errorCount += 1;
3507
- return { ok: false, error: String(err) };
3562
+ throw new Error(
3563
+ `measure_code_metrics failed for ${rawPath}: ${err instanceof Error ? err.message : String(err)}`,
3564
+ { cause: err }
3565
+ );
3508
3566
  }
3509
3567
  state10.fileCount += result.files.length;
3510
3568
  return {
@@ -3707,11 +3765,11 @@ function parseCommitMessage(message, cfg) {
3707
3765
  }
3708
3766
  var GIT_MESSAGE_FLAG_RE = new RegExp(
3709
3767
  [
3710
- // -m "…" | -m '…' | -m=… ; --message "…" | --message='…' | --message=…
3711
- String.raw`(?:^|\s)(?:-m|--message)(?:\s+|=)"([^"]*)"`,
3712
- String.raw`(?:^|\s)(?:-m|--message)(?:\s+|=)'([^']*)'`,
3768
+ // -m "…" | -am "…" | -m '…' | -m=… ; --message "…" | --message='…' | --message=…
3769
+ String.raw`(?:^|\s)(?:-[a-zA-Z]*m|--message)(?:\s+|=)"([^"]*)"`,
3770
+ String.raw`(?:^|\s)(?:-[a-zA-Z]*m|--message)(?:\s+|=)'([^']*)'`,
3713
3771
  // Bare value: stops at whitespace or a shell separator.
3714
- String.raw`(?:^|\s)(?:-m|--message)(?:\s+|=)([^\s;&|"']+)`
3772
+ String.raw`(?:^|\s)(?:-[a-zA-Z]*m|--message)(?:\s+|=)([^\s;&|"']+)`
3715
3773
  ].join("|"),
3716
3774
  "g"
3717
3775
  );
@@ -3821,7 +3879,7 @@ var plugin11 = {
3821
3879
  } else if (toolName === "bash") {
3822
3880
  const command = inp["command"] ?? inp["CommandLine"] ?? inp["cmd"] ?? inp["script"] ?? inp["input"];
3823
3881
  if (typeof command !== "string") return;
3824
- if (!/\bgit\s+commit\b/.test(command)) return;
3882
+ if (!/\bgit\s+commit(?![a-zA-Z0-9_-])/.test(command)) return;
3825
3883
  message = extractMessageFromBash(command);
3826
3884
  if (!message) return;
3827
3885
  } else {
@@ -4357,6 +4415,7 @@ var config_validator_default = plugin12;
4357
4415
  // src/context-pins/index.ts
4358
4416
  import * as fs from "node:fs";
4359
4417
  import { dirname as dirname3, isAbsolute as isAbsolute7, relative as relative7, resolve as resolve7 } from "node:path";
4418
+ import { ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
4360
4419
  import { atomicWrite, ensureDir } from "@wrongstack/core/utils";
4361
4420
  var state13 = {
4362
4421
  pins: [],
@@ -4497,16 +4556,17 @@ var plugin13 = {
4497
4556
  category: "Memory",
4498
4557
  mutating: true,
4499
4558
  async execute(input) {
4500
- if (!cfg.enabled) return { ok: false, error: "context-pins is disabled" };
4559
+ if (!cfg.enabled) throw new Error("context-pins is disabled");
4501
4560
  const raw = input ?? {};
4502
4561
  const rawText = input.text ?? raw["pin"] ?? raw["content"] ?? raw["message"] ?? raw["note"] ?? raw["fact"] ?? raw["data"];
4503
4562
  const text = String(rawText ?? "").trim();
4504
- if (!text) return { ok: false, error: "pin text must not be empty" };
4563
+ if (!text) {
4564
+ throw new ToolValidationError7({ message: "pin text must not be empty", field: "text" });
4565
+ }
4505
4566
  if (state13.pins.length >= cfg.maxPins) {
4506
- return {
4507
- ok: false,
4508
- error: `pin limit reached (${cfg.maxPins}). Remove a pin first with pin_remove.`
4509
- };
4567
+ throw new Error(
4568
+ `pin limit reached (${cfg.maxPins}). Remove a pin first with pin_remove.`
4569
+ );
4510
4570
  }
4511
4571
  const pin = {
4512
4572
  id: `pin-${state13.nextId++}`,
@@ -4535,16 +4595,17 @@ var plugin13 = {
4535
4595
  category: "Memory",
4536
4596
  mutating: true,
4537
4597
  async execute(input) {
4538
- if (!cfg.enabled) return { ok: false, error: "context-pins is disabled" };
4598
+ if (!cfg.enabled) throw new Error("context-pins is disabled");
4539
4599
  const raw = input ?? {};
4540
4600
  const key = String(
4541
4601
  input.id ?? input.label ?? raw["pinId"] ?? raw["pin_id"] ?? raw["name"] ?? raw["key"] ?? ""
4542
4602
  ).trim();
4543
- if (!key) return { ok: false, error: "id or label is required" };
4603
+ if (!key)
4604
+ throw new ToolValidationError7({ message: "id or label is required", field: "id" });
4544
4605
  const before = state13.pins.length;
4545
4606
  state13.pins = state13.pins.filter((p) => p.id !== key && p.label !== key);
4546
4607
  const removed = before - state13.pins.length;
4547
- if (removed === 0) return { ok: false, error: `no pin matches "${key}"` };
4608
+ if (removed === 0) throw new Error(`no pin matches "${key}"`);
4548
4609
  state13.removals += removed;
4549
4610
  api.metrics.counter("removals", removed);
4550
4611
  const persisted = await persistPins(cfg.filePath);
@@ -4662,6 +4723,9 @@ function estimateCost(model, freshTokens, completionTokens, cachedTokens = 0) {
4662
4723
  const outputCost = completionTokens / 1e6 * pricing.output;
4663
4724
  return inputCost + outputCost;
4664
4725
  }
4726
+ function toFiniteNumber(value) {
4727
+ return Number.isFinite(value) ? value : 0;
4728
+ }
4665
4729
  var plugin14 = {
4666
4730
  name: "cost-tracker",
4667
4731
  version: "0.1.0",
@@ -4735,11 +4799,12 @@ var plugin14 = {
4735
4799
  const input = v["input"];
4736
4800
  const output = v["output"];
4737
4801
  if (typeof input !== "number" || typeof output !== "number") continue;
4802
+ if (!Number.isFinite(input) || !Number.isFinite(output)) continue;
4738
4803
  const cacheRead = v["cacheRead"];
4739
4804
  pricingOverrides[model.toLowerCase()] = {
4740
4805
  input,
4741
4806
  output,
4742
- ...typeof cacheRead === "number" ? { cacheRead } : {}
4807
+ ...typeof cacheRead === "number" && Number.isFinite(cacheRead) ? { cacheRead } : {}
4743
4808
  };
4744
4809
  }
4745
4810
  }
@@ -4752,11 +4817,11 @@ var plugin14 = {
4752
4817
  if (!providerModels) continue;
4753
4818
  for (const [modelId, model] of Object.entries(providerModels)) {
4754
4819
  const cost = model?.cost;
4755
- if (cost && typeof cost.input === "number" && typeof cost.output === "number") {
4820
+ if (cost && typeof cost.input === "number" && typeof cost.output === "number" && Number.isFinite(cost.input) && Number.isFinite(cost.output)) {
4756
4821
  bundledFromRegistry[modelId.toLowerCase()] = {
4757
4822
  input: cost.input,
4758
4823
  output: cost.output,
4759
- ...typeof cost.cache_read === "number" ? { cacheRead: cost.cache_read } : {}
4824
+ ...typeof cost.cache_read === "number" && Number.isFinite(cost.cache_read) ? { cacheRead: cost.cache_read } : {}
4760
4825
  };
4761
4826
  hydrated += 1;
4762
4827
  }
@@ -4784,14 +4849,22 @@ var plugin14 = {
4784
4849
  const usage = payload.usage;
4785
4850
  const model = payload.ctx?.model ?? "unknown";
4786
4851
  const u = usage ?? {};
4787
- const cachedTokens = Number(u["cacheRead"] ?? u["cache_read_input_tokens"] ?? u["cached_prompt_tokens"] ?? 0) || 0;
4788
- const rawInput = Number(u["input"] ?? u["prompt_tokens"] ?? u["inputTokens"] ?? u["promptTokens"] ?? 0) || 0;
4789
- const rawCacheWrite = Number(u["cacheWrite"] ?? u["cache_creation_input_tokens"] ?? 0) || 0;
4852
+ const cachedTokens = toFiniteNumber(
4853
+ Number(u["cacheRead"] ?? u["cache_read_input_tokens"] ?? u["cached_prompt_tokens"] ?? 0)
4854
+ );
4855
+ const rawInput = toFiniteNumber(
4856
+ Number(u["input"] ?? u["prompt_tokens"] ?? u["inputTokens"] ?? u["promptTokens"] ?? 0)
4857
+ );
4858
+ const rawCacheWrite = toFiniteNumber(
4859
+ Number(u["cacheWrite"] ?? u["cache_creation_input_tokens"] ?? 0)
4860
+ );
4790
4861
  const freshTokens = rawInput + rawCacheWrite;
4791
4862
  const promptTokens = freshTokens + cachedTokens;
4792
- const completionTokens = Number(
4793
- u["output"] ?? u["completion_tokens"] ?? u["outputTokens"] ?? u["completionTokens"] ?? 0
4794
- ) || 0;
4863
+ const completionTokens = toFiniteNumber(
4864
+ Number(
4865
+ u["output"] ?? u["completion_tokens"] ?? u["outputTokens"] ?? u["completionTokens"] ?? 0
4866
+ )
4867
+ );
4795
4868
  const totalTokens = promptTokens + completionTokens;
4796
4869
  const costUsd = estimateCost(model, freshTokens, completionTokens, cachedTokens);
4797
4870
  const record = {
@@ -5012,6 +5085,7 @@ by model: ${top || "(none)"}`
5012
5085
  var cost_tracker_default = plugin14;
5013
5086
 
5014
5087
  // src/cron/index.ts
5088
+ import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
5015
5089
  var COORDINATION_CRON_CAPABILITY = "coordination.cron";
5016
5090
  var API_VERSION9 = "^0.1.10";
5017
5091
  var state14 = {
@@ -5180,16 +5254,28 @@ var plugin15 = {
5180
5254
  const action = input["action"] ?? input["task"] ?? input["command"] ?? input["run"];
5181
5255
  const enabled = input["enabled"] ?? true;
5182
5256
  if (!name || typeof name !== "string" || name.trim() === "") {
5183
- return { ok: false, error: "name is required and must be a non-empty string" };
5257
+ throw new ToolValidationError8({
5258
+ message: "name is required and must be a non-empty string",
5259
+ field: "name"
5260
+ });
5184
5261
  }
5185
5262
  if (Number.isNaN(intervalMs) || rawInterval === void 0 || rawInterval === null) {
5186
- return { ok: false, error: "intervalMs must be a number >= 1000" };
5263
+ throw new ToolValidationError8({
5264
+ message: "intervalMs must be a number >= 1000",
5265
+ field: "intervalMs"
5266
+ });
5267
+ }
5268
+ if (typeof action !== "string" || action.trim() === "") {
5269
+ throw new ToolValidationError8({
5270
+ message: "action is required and must be a non-empty string",
5271
+ field: "action"
5272
+ });
5187
5273
  }
5188
5274
  if (state14.jobs.has(name)) {
5189
- return { ok: false, error: `Cron job '${name}' already exists. Use cron_cancel first.` };
5275
+ throw new Error(`Cron job '${name}' already exists. Use cron_cancel first.`);
5190
5276
  }
5191
5277
  if (state14.jobs.size >= maxConcurrent) {
5192
- return { ok: false, error: `Maximum concurrent jobs (${maxConcurrent}) reached.` };
5278
+ throw new Error(`Maximum concurrent jobs (${maxConcurrent}) reached.`);
5193
5279
  }
5194
5280
  const job = {
5195
5281
  name,
@@ -5255,7 +5341,7 @@ var plugin15 = {
5255
5341
  async execute(input) {
5256
5342
  const name = input["name"] ?? input["jobName"] ?? input["job_name"] ?? input["job"] ?? input["id"];
5257
5343
  if (!name || typeof name !== "string" || !state14.jobs.has(name)) {
5258
- return { ok: false, error: `No cron job named '${name}'` };
5344
+ throw new Error(`No cron job named '${name}'`);
5259
5345
  }
5260
5346
  cancelJob(name);
5261
5347
  api.metrics.gauge("cron_active_jobs", state14.jobs.size);
@@ -5290,8 +5376,9 @@ var plugin15 = {
5290
5376
  var cron_default = plugin15;
5291
5377
 
5292
5378
  // src/dead-code-detector/index.ts
5293
- import { readdir, readFile as readFile5, stat as stat2 } from "node:fs/promises";
5379
+ import { readdir, readFile as readFile5, stat as stat4 } from "node:fs/promises";
5294
5380
  import { extname as extname2, join, relative as relative8, resolve as resolve8 } from "node:path";
5381
+ import { ToolValidationError as ToolValidationError9 } from "@wrongstack/core/types";
5295
5382
  var API_VERSION10 = "^0.1.10";
5296
5383
  var state15 = {
5297
5384
  scanCount: 0,
@@ -5438,14 +5525,8 @@ async function scan(root, depth, cfg) {
5438
5525
  }
5439
5526
  async function resolveScanRoot(rawPath) {
5440
5527
  const resolved = resolve8(process.cwd(), rawPath);
5441
- try {
5442
- const stats = await stat2(resolved);
5443
- if (!stats.isDirectory()) {
5444
- return resolve8(resolved, "..");
5445
- }
5446
- } catch {
5447
- }
5448
- return resolved;
5528
+ const stats = await stat4(resolved);
5529
+ return stats.isDirectory() ? resolved : resolve8(resolved, "..");
5449
5530
  }
5450
5531
  function toPosix2(p) {
5451
5532
  return p.replace(/\\/g, "/");
@@ -5564,22 +5645,29 @@ Consider removing the export if it is not part of the public API.`;
5564
5645
  category: "Diagnostics",
5565
5646
  mutating: false,
5566
5647
  async execute(input) {
5567
- if (!cfg.enabled) return { ok: false, error: "dead-code-detector is disabled" };
5648
+ if (!cfg.enabled) throw new Error("dead-code-detector is disabled");
5568
5649
  const raw = input ?? {};
5569
5650
  const rawPath = (typeof input.path === "string" && input.path.trim().length > 0 ? input.path.trim() : void 0) ?? (typeof raw["directory"] === "string" ? raw["directory"] : void 0) ?? (typeof raw["dir"] === "string" ? raw["dir"] : void 0) ?? (typeof raw["SearchDirectory"] === "string" ? raw["SearchDirectory"] : void 0) ?? (typeof raw["filePath"] === "string" ? raw["filePath"] : void 0) ?? (typeof raw["TargetFile"] === "string" ? raw["TargetFile"] : void 0) ?? (typeof raw["targetFile"] === "string" ? raw["targetFile"] : void 0) ?? (typeof raw["file_path"] === "string" ? raw["file_path"] : void 0) ?? (typeof raw["file"] === "string" ? raw["file"] : void 0) ?? ".";
5570
5651
  const rawDepth = typeof input.depth === "number" ? input.depth : cfg.defaultDepth;
5571
5652
  const depth = Math.max(0, Math.min(Math.floor(rawDepth), cfg.maxDepth));
5572
5653
  if (!(0, runtime_exports.withinProject)(rawPath)) {
5573
- return { ok: false, error: "scan path is outside the project root" };
5654
+ throw new ToolValidationError9({
5655
+ message: "scan path is outside the project root",
5656
+ field: "path"
5657
+ });
5574
5658
  }
5575
5659
  state15.scanCount += 1;
5576
- const scanRoot = await resolveScanRoot(rawPath);
5577
5660
  let result;
5661
+ let scanRoot;
5578
5662
  try {
5663
+ scanRoot = await resolveScanRoot(rawPath);
5579
5664
  result = await scan(scanRoot, depth, cfg);
5580
5665
  } catch (err) {
5581
5666
  state15.errorCount += 1;
5582
- return { ok: false, error: String(err) };
5667
+ throw new Error(
5668
+ `dead_code_scan failed for ${rawPath}: ${err instanceof Error ? err.message : String(err)}`,
5669
+ { cause: err }
5670
+ );
5583
5671
  }
5584
5672
  return {
5585
5673
  ok: true,
@@ -6982,8 +7070,9 @@ Consider mentioning these changes in the documentation.`;
6982
7070
  var doc_sync_guard_default = plugin20;
6983
7071
 
6984
7072
  // src/duplicate-code-detector/index.ts
6985
- import { readFile as readFile6, realpath as realpath2, stat as stat3 } from "node:fs/promises";
7073
+ import { readFile as readFile6, realpath as realpath2, stat as stat5 } from "node:fs/promises";
6986
7074
  import { extname as extname4, isAbsolute as isAbsolute8, relative as relative9, resolve as resolve9, sep } from "node:path";
7075
+ import { ToolValidationError as ToolValidationError10 } from "@wrongstack/core/types";
6987
7076
  var API_VERSION14 = "^0.1.10";
6988
7077
  var HOOK_WARNING_COOLDOWN_MS = 6e4;
6989
7078
  var state20 = {
@@ -7289,7 +7378,7 @@ var plugin21 = {
7289
7378
  async function readCachedFingerprints(filePath, minLines) {
7290
7379
  let st;
7291
7380
  try {
7292
- st = await stat3(filePath);
7381
+ st = await stat5(filePath);
7293
7382
  } catch {
7294
7383
  return null;
7295
7384
  }
@@ -7404,19 +7493,26 @@ var plugin21 = {
7404
7493
  category: "Diagnostics",
7405
7494
  mutating: false,
7406
7495
  async execute(input) {
7407
- if (!cfg.enabled) return { ok: false, error: "duplicate-code-detector is disabled" };
7496
+ if (!cfg.enabled) throw new Error("duplicate-code-detector is disabled");
7408
7497
  const raw = input;
7409
7498
  const rawPath = (typeof raw["path"] === "string" ? raw["path"] : void 0) ?? (typeof raw["directory"] === "string" ? raw["directory"] : void 0) ?? (typeof raw["dir"] === "string" ? raw["dir"] : void 0) ?? (typeof raw["SearchDirectory"] === "string" ? raw["SearchDirectory"] : void 0) ?? (typeof raw["filePath"] === "string" ? raw["filePath"] : void 0) ?? (typeof raw["file_path"] === "string" ? raw["file_path"] : void 0) ?? (typeof raw["TargetFile"] === "string" ? raw["TargetFile"] : void 0) ?? (typeof raw["targetFile"] === "string" ? raw["targetFile"] : void 0) ?? (typeof raw["file"] === "string" ? raw["file"] : void 0) ?? ".";
7410
7499
  if (!(0, runtime_exports.withinProject)(rawPath)) {
7411
- return { ok: false, error: "scan path is outside the project root" };
7500
+ throw new ToolValidationError10({
7501
+ message: "scan path is outside the project root",
7502
+ field: "path"
7503
+ });
7412
7504
  }
7413
7505
  state20.scanCount += 1;
7414
7506
  let result;
7415
7507
  try {
7508
+ await stat5(resolve9(process.cwd(), rawPath));
7416
7509
  result = await scanPath(rawPath, cfg);
7417
7510
  } catch (err) {
7418
7511
  state20.errorCount += 1;
7419
- return { ok: false, error: String(err) };
7512
+ throw new Error(
7513
+ `detect_duplicate_code failed for ${rawPath}: ${err instanceof Error ? err.message : String(err)}`,
7514
+ { cause: err }
7515
+ );
7420
7516
  }
7421
7517
  state20.findingCount += result.findings.length;
7422
7518
  return {
@@ -7800,8 +7896,9 @@ ${errorLine ?? "(no error line)"}
7800
7896
  var error_lens_default = plugin22;
7801
7897
 
7802
7898
  // src/feature-flag-tracker/index.ts
7803
- import { readFile as readFile7 } from "node:fs/promises";
7899
+ import { readFile as readFile7, stat as stat6 } from "node:fs/promises";
7804
7900
  import { isAbsolute as isAbsolute9, relative as relative10, resolve as resolve10 } from "node:path";
7901
+ import { ToolValidationError as ToolValidationError11 } from "@wrongstack/core/types";
7805
7902
  var API_VERSION15 = "^0.1.10";
7806
7903
  var state22 = {
7807
7904
  scanCount: 0,
@@ -8026,19 +8123,26 @@ Make sure flag behavior is intentional and consider updating flag inventory/docs
8026
8123
  category: "Diagnostics",
8027
8124
  mutating: false,
8028
8125
  async execute(input) {
8029
- if (!cfg.enabled) return { ok: false, error: "feature-flag-tracker is disabled" };
8126
+ if (!cfg.enabled) throw new Error("feature-flag-tracker is disabled");
8030
8127
  const raw = input ?? {};
8031
8128
  const rawPath = (typeof input.path === "string" && input.path.trim().length > 0 ? input.path.trim() : void 0) ?? (typeof raw["directory"] === "string" ? raw["directory"] : void 0) ?? (typeof raw["SearchDirectory"] === "string" ? raw["SearchDirectory"] : void 0) ?? (typeof raw["dir"] === "string" ? raw["dir"] : void 0) ?? (typeof raw["TargetFile"] === "string" ? raw["TargetFile"] : void 0) ?? (typeof raw["targetFile"] === "string" ? raw["targetFile"] : void 0) ?? (typeof raw["filePath"] === "string" ? raw["filePath"] : void 0) ?? (typeof raw["file_path"] === "string" ? raw["file_path"] : void 0) ?? (typeof raw["file"] === "string" ? raw["file"] : void 0) ?? ".";
8032
8129
  if (!(0, runtime_exports.withinProject)(rawPath)) {
8033
- return { ok: false, error: "path is outside the project root" };
8130
+ throw new ToolValidationError11({
8131
+ message: "path is outside the project root",
8132
+ field: "path"
8133
+ });
8034
8134
  }
8035
8135
  state22.scanCount += 1;
8036
8136
  let result;
8037
8137
  try {
8138
+ await stat6(resolve10(process.cwd(), rawPath));
8038
8139
  result = await scanPath2(rawPath, cfg);
8039
8140
  } catch (err) {
8040
8141
  state22.errorCount += 1;
8041
- return { ok: false, error: String(err) };
8142
+ throw new Error(
8143
+ `scan_feature_flags failed for ${rawPath}: ${err instanceof Error ? err.message : String(err)}`,
8144
+ { cause: err }
8145
+ );
8042
8146
  }
8043
8147
  state22.flagCount += result.usages.length;
8044
8148
  return {
@@ -8124,6 +8228,7 @@ var feature_flag_tracker_default = plugin23;
8124
8228
  // src/file-watcher/index.ts
8125
8229
  import { watch as fsWatch } from "node:fs";
8126
8230
  import { join as join2 } from "node:path";
8231
+ import { ToolValidationError as ToolValidationError12 } from "@wrongstack/core/types";
8127
8232
  var API_VERSION16 = "^0.1.10";
8128
8233
  var watch_idCounter = 0;
8129
8234
  function nextId() {
@@ -8311,11 +8416,10 @@ var plugin24 = {
8311
8416
  let rawPaths;
8312
8417
  if (explicitPaths !== void 0) {
8313
8418
  if (!Array.isArray(explicitPaths)) {
8314
- return {
8315
- ok: false,
8316
- error: "paths must be an array of file/directory paths",
8317
- watch_id: null
8318
- };
8419
+ throw new ToolValidationError12({
8420
+ message: "paths must be an array of file/directory paths",
8421
+ field: "paths"
8422
+ });
8319
8423
  }
8320
8424
  rawPaths = explicitPaths;
8321
8425
  } else {
@@ -8323,55 +8427,46 @@ var plugin24 = {
8323
8427
  rawPaths = Array.isArray(fallback) ? fallback : typeof fallback === "string" && fallback.trim().length > 0 ? [fallback.trim()] : void 0;
8324
8428
  }
8325
8429
  if (!rawPaths || !Array.isArray(rawPaths)) {
8326
- return {
8327
- ok: false,
8328
- error: "paths must be an array of file/directory paths",
8329
- watch_id: null
8330
- };
8430
+ throw new ToolValidationError12({
8431
+ message: "paths must be an array of file/directory paths",
8432
+ field: "paths"
8433
+ });
8331
8434
  }
8332
8435
  const paths = [...new Set(rawPaths)];
8333
8436
  if (paths.length === 0) {
8334
- return {
8335
- ok: false,
8336
- error: "paths array is empty \u2014 provide at least one path",
8337
- watch_id: null
8338
- };
8437
+ throw new ToolValidationError12({
8438
+ message: "paths array is empty \u2014 provide at least one path",
8439
+ field: "paths"
8440
+ });
8339
8441
  }
8340
8442
  if (paths.length > MAX_PATHS_PER_WATCH) {
8341
- return {
8342
- ok: false,
8343
- error: `a watch may contain at most ${MAX_PATHS_PER_WATCH} unique paths`,
8344
- watch_id: null
8345
- };
8443
+ throw new ToolValidationError12({
8444
+ message: `a watch may contain at most ${MAX_PATHS_PER_WATCH} unique paths`,
8445
+ field: "paths"
8446
+ });
8346
8447
  }
8347
8448
  if (watches.size >= MAX_WATCH_GROUPS) {
8348
- return {
8349
- ok: false,
8350
- error: `active watch group limit reached (${MAX_WATCH_GROUPS})`,
8351
- watch_id: null
8352
- };
8449
+ throw new Error(
8450
+ `active watch group limit reached (${MAX_WATCH_GROUPS}); stop a watch with watch_stop first`
8451
+ );
8353
8452
  }
8354
8453
  const activeFilesystemWatchers = [...watches.values()].reduce(
8355
8454
  (total, handle2) => total + handle2.watchers.length,
8356
8455
  0
8357
8456
  );
8358
8457
  if (activeFilesystemWatchers + paths.length > MAX_FILESYSTEM_WATCHERS) {
8359
- return {
8360
- ok: false,
8361
- error: `filesystem watcher limit reached (${MAX_FILESYSTEM_WATCHERS})`,
8362
- watch_id: null
8363
- };
8458
+ throw new Error(
8459
+ `filesystem watcher limit reached (${MAX_FILESYSTEM_WATCHERS}); stop a watch with watch_stop first`
8460
+ );
8364
8461
  }
8365
8462
  const events = input["events"] ?? ["change", "add", "delete"];
8366
8463
  const recursive = input["recursive"] ?? true;
8367
8464
  const bad = paths.find((p) => !(0, runtime_exports.withinProject)(p));
8368
8465
  if (bad !== void 0) {
8369
- return {
8370
- ok: false,
8371
- error: `path is outside the project root: ${bad}`,
8372
- watch_id: null,
8373
- rejectedOutsideProject: true
8374
- };
8466
+ throw new ToolValidationError12({
8467
+ message: `path is outside the project root: ${bad}`,
8468
+ field: "paths"
8469
+ });
8375
8470
  }
8376
8471
  const id = nextId();
8377
8472
  const handle = {
@@ -8383,8 +8478,15 @@ var plugin24 = {
8383
8478
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
8384
8479
  };
8385
8480
  const watchedPaths = [];
8481
+ const failedPaths = [];
8386
8482
  for (const p of paths) {
8387
8483
  if (safeWatchDir(p, recursive, handle)) watchedPaths.push(p);
8484
+ else failedPaths.push(p);
8485
+ }
8486
+ if (watchedPaths.length === 0) {
8487
+ throw new Error(
8488
+ `could not watch any of the requested paths: ${failedPaths.join(", ")} (missing path or OS watcher limit)`
8489
+ );
8388
8490
  }
8389
8491
  handle.paths = watchedPaths;
8390
8492
  watches.set(id, handle);
@@ -8393,9 +8495,10 @@ var plugin24 = {
8393
8495
  ok: true,
8394
8496
  watch_id: id,
8395
8497
  paths: watchedPaths,
8498
+ ...failedPaths.length > 0 ? { failedPaths } : {},
8396
8499
  events,
8397
8500
  recursive,
8398
- message: `Started watching ${watchedPaths.length} path(s). Use watch_stop to cancel.`
8501
+ message: `Started watching ${watchedPaths.length} path(s). Use watch_stop to cancel.` + (failedPaths.length > 0 ? ` Could not watch: ${failedPaths.join(", ")}.` : "")
8399
8502
  };
8400
8503
  }
8401
8504
  });
@@ -8417,7 +8520,7 @@ var plugin24 = {
8417
8520
  const watch_id = typeof rawId === "string" ? rawId.trim() : "";
8418
8521
  const handle = watches.get(watch_id);
8419
8522
  if (!handle) {
8420
- return { ok: false, error: `No active watch with ID: ${watch_id}` };
8523
+ throw new Error(`No active watch with ID: ${watch_id}`);
8421
8524
  }
8422
8525
  for (const w of handle.watchers) {
8423
8526
  try {
@@ -8501,7 +8604,7 @@ var file_watcher_default = plugin24;
8501
8604
  // src/format-on-save/index.ts
8502
8605
  import { execFile as execFile5 } from "node:child_process";
8503
8606
  import { createHash } from "node:crypto";
8504
- import { access as access2, readFile as readFile8, stat as stat4 } from "node:fs/promises";
8607
+ import { access as access2, readFile as readFile8, stat as stat7 } from "node:fs/promises";
8505
8608
  var API_VERSION17 = "^0.1.10";
8506
8609
  var state23 = {
8507
8610
  invocationCount: 0,
@@ -8624,7 +8727,7 @@ async function formatFile(filePath, timeoutMs) {
8624
8727
  const started = Date.now();
8625
8728
  let bytesBefore;
8626
8729
  try {
8627
- bytesBefore = (await stat4(filePath)).size;
8730
+ bytesBefore = (await stat7(filePath)).size;
8628
8731
  } catch {
8629
8732
  return null;
8630
8733
  }
@@ -8662,7 +8765,7 @@ async function formatFile(filePath, timeoutMs) {
8662
8765
  }
8663
8766
  let bytesAfter;
8664
8767
  try {
8665
- bytesAfter = (await stat4(filePath)).size;
8768
+ bytesAfter = (await stat7(filePath)).size;
8666
8769
  } catch {
8667
8770
  return null;
8668
8771
  }
@@ -8877,6 +8980,7 @@ var format_on_save_default = plugin25;
8877
8980
  import { execFile as execFile6 } from "node:child_process";
8878
8981
  import { existsSync as existsSync3 } from "node:fs";
8879
8982
  import { resolve as resolve11 } from "node:path";
8983
+ import { ToolValidationError as ToolValidationError13 } from "@wrongstack/core/types";
8880
8984
  var API_VERSION18 = "^0.1.10";
8881
8985
  var commitCount = { value: 0 };
8882
8986
  var lastCommit = { hash: null, at: null };
@@ -9053,22 +9157,22 @@ async function simultaneousEditWarning(cwd) {
9053
9157
  }
9054
9158
  async function getStagedDiff(cwd) {
9055
9159
  try {
9056
- const stat8 = await runGit3(["diff", "--cached", "--stat"], cwd);
9160
+ const stat13 = await runGit3(["diff", "--cached", "--stat"], cwd);
9057
9161
  const diff = await runGit3(["diff", "--cached"], cwd);
9058
9162
  const MAX_DIFF = 2e4;
9059
9163
  const truncated = diff.length > MAX_DIFF ? diff.slice(0, MAX_DIFF) + "\n\n... (diff truncated)" : diff;
9060
- return { stat: stat8 || "(no stat)", diff: truncated || "(clean)" };
9164
+ return { stat: stat13 || "(no stat)", diff: truncated || "(clean)" };
9061
9165
  } catch {
9062
9166
  return { stat: "(unavailable)", diff: "(unavailable)" };
9063
9167
  }
9064
9168
  }
9065
9169
  async function getScopedStagedDiff(paths, cwd) {
9066
9170
  try {
9067
- const stat8 = await runGit3(["diff", "--cached", "--stat", "--", ...paths], cwd);
9171
+ const stat13 = await runGit3(["diff", "--cached", "--stat", "--", ...paths], cwd);
9068
9172
  const diff = await runGit3(["diff", "--cached", "--", ...paths], cwd);
9069
9173
  const MAX_DIFF = 2e4;
9070
9174
  const truncated = diff.length > MAX_DIFF ? diff.slice(0, MAX_DIFF) + "\n\n... (diff truncated)" : diff;
9071
- return { stat: stat8 || "(no stat)", diff: truncated || "(clean)" };
9175
+ return { stat: stat13 || "(no stat)", diff: truncated || "(clean)" };
9072
9176
  } catch {
9073
9177
  return { stat: "(unavailable)", diff: "(unavailable)" };
9074
9178
  }
@@ -9109,14 +9213,14 @@ var VALID_TYPES = [
9109
9213
  "build",
9110
9214
  "revert"
9111
9215
  ];
9112
- async function generateCommitFromDiff(api, stat8, diff) {
9216
+ async function generateCommitFromDiff(api, stat13, diff) {
9113
9217
  if (!api.llm) return null;
9114
9218
  try {
9115
9219
  const result = await api.llm.complete(
9116
9220
  `Write a Conventional Commits message for this staged git diff. Respond with ONLY a JSON object of the form {"type": string, "scope": string, "summary": string, "body": string}. type is one of: ${VALID_TYPES.join(", ")}. scope is a short area (empty string if unclear). summary is an imperative, lower-case, <=72-char subject with no trailing period. body is an optional short explanation (empty string if not needed). No prose outside the JSON.
9117
9221
 
9118
9222
  Stat:
9119
- ${stat8}
9223
+ ${stat13}
9120
9224
 
9121
9225
  Diff:
9122
9226
  ${diff}`,
@@ -9257,7 +9361,10 @@ var plugin26 = {
9257
9361
  const rawFiles = input["files"] ?? input["fileList"] ?? input["file_list"];
9258
9362
  if (rawFiles !== void 0) {
9259
9363
  if (!Array.isArray(rawFiles)) {
9260
- return { ok: false, error: "files must be an array of file paths" };
9364
+ throw new ToolValidationError13({
9365
+ message: "files must be an array of file paths",
9366
+ field: "files"
9367
+ });
9261
9368
  }
9262
9369
  files = rawFiles;
9263
9370
  } else if (typeof (input["file"] ?? input["file_path"]) === "string" && String(input["file"] ?? input["file_path"]).trim().length > 0) {
@@ -9271,20 +9378,26 @@ var plugin26 = {
9271
9378
  const rawPaths = input["paths"] ?? input["pathList"] ?? input["path_list"];
9272
9379
  if (rawPaths !== void 0) {
9273
9380
  if (!Array.isArray(rawPaths)) {
9274
- return { ok: false, error: "paths must be an array of pathspec patterns" };
9381
+ throw new ToolValidationError13({
9382
+ message: "paths must be an array of pathspec patterns",
9383
+ field: "paths"
9384
+ });
9275
9385
  }
9276
9386
  pathspecs = rawPaths.filter((p) => typeof p === "string" && p.length > 0);
9277
9387
  if (pathspecs.length === 0) {
9278
- return { ok: false, error: "paths must contain at least one non-empty pattern" };
9388
+ throw new ToolValidationError13({
9389
+ message: "paths must contain at least one non-empty pattern",
9390
+ field: "paths"
9391
+ });
9279
9392
  }
9280
9393
  } else if (typeof input["path"] === "string" && input["path"].trim().length > 0) {
9281
9394
  pathspecs = [input["path"].trim()];
9282
9395
  }
9283
9396
  if (rawPaths !== void 0 && files && files.length > 0) {
9284
- return {
9285
- ok: false,
9286
- error: "Pass either files (exact paths) or paths (pathspec globs), not both \u2014 the other would be silently ignored."
9287
- };
9397
+ throw new ToolValidationError13({
9398
+ message: "Pass either files (exact paths) or paths (pathspec globs), not both \u2014 the other would be silently ignored.",
9399
+ field: "paths"
9400
+ });
9288
9401
  }
9289
9402
  let commitScope;
9290
9403
  let staged = [];
@@ -9292,10 +9405,10 @@ var plugin26 = {
9292
9405
  try {
9293
9406
  await stageFiles(pathspecs);
9294
9407
  } catch (err) {
9295
- return {
9296
- ok: false,
9297
- error: `Failed to stage files matching paths: ${err instanceof Error ? err.message : String(err)}`
9298
- };
9408
+ throw new Error(
9409
+ `Failed to stage files matching paths: ${err instanceof Error ? err.message : String(err)}`,
9410
+ { cause: err }
9411
+ );
9299
9412
  }
9300
9413
  try {
9301
9414
  staged = await getScopedStagedFiles(pathspecs);
@@ -9303,10 +9416,9 @@ var plugin26 = {
9303
9416
  staged = [];
9304
9417
  }
9305
9418
  if (staged.length === 0) {
9306
- return {
9307
- ok: false,
9308
- error: "No changed files match the given paths \u2014 refusing to commit anything else."
9309
- };
9419
+ throw new Error(
9420
+ "No changed files match the given paths \u2014 refusing to commit anything else."
9421
+ );
9310
9422
  }
9311
9423
  commitScope = staged;
9312
9424
  try {
@@ -9318,10 +9430,10 @@ var plugin26 = {
9318
9430
  try {
9319
9431
  commitScope = await stageFiles(files);
9320
9432
  } catch (err) {
9321
- return {
9322
- ok: false,
9323
- error: `Failed to stage files: ${err instanceof Error ? err.message : String(err)}`
9324
- };
9433
+ throw new Error(
9434
+ `Failed to stage files: ${err instanceof Error ? err.message : String(err)}`,
9435
+ { cause: err }
9436
+ );
9325
9437
  }
9326
9438
  try {
9327
9439
  staged = await getStagedFiles();
@@ -9352,10 +9464,10 @@ var plugin26 = {
9352
9464
  }
9353
9465
  }
9354
9466
  }
9355
- const { stat: stat8, diff: stagedDiff } = commitScope ? await getScopedStagedDiff(commitScope) : await getStagedDiff();
9467
+ const { stat: stat13, diff: stagedDiff } = commitScope ? await getScopedStagedDiff(commitScope) : await getStagedDiff();
9356
9468
  let generatedByLlm = false;
9357
9469
  if (wantGenerate && staged.length > 0) {
9358
- const g = await generateCommitFromDiff(api, stat8, stagedDiff);
9470
+ const g = await generateCommitFromDiff(api, stat13, stagedDiff);
9359
9471
  if (g) {
9360
9472
  type = g.type;
9361
9473
  if (g.scope) scope = g.scope;
@@ -9386,17 +9498,16 @@ var plugin26 = {
9386
9498
  message: `Would create: ${summary || "update code"}`
9387
9499
  };
9388
9500
  }
9389
- return {
9390
- ok: false,
9391
- error: "type is required and must be a valid conventional commit type"
9392
- };
9501
+ throw new ToolValidationError13({
9502
+ message: "type is required and must be a valid conventional commit type",
9503
+ field: "type"
9504
+ });
9393
9505
  }
9394
9506
  const msg = generateCommitMessage(type, scope, summary || "update code", body);
9395
9507
  if (staged.length === 0) {
9396
- return {
9397
- ok: false,
9398
- error: 'Nothing staged. Pass files (exact paths) or paths (pathspec globs) to scope this commit, stage with git add beforehand, or set extensions["git-autocommit"].autoStage=true to allow staging every changed file (legacy whole-tree behavior).'
9399
- };
9508
+ throw new Error(
9509
+ 'Nothing staged. Pass files (exact paths) or paths (pathspec globs) to scope this commit, stage with git add beforehand, or set extensions["git-autocommit"].autoStage=true to allow staging every changed file (legacy whole-tree behavior).'
9510
+ );
9400
9511
  }
9401
9512
  let scopeWarning = null;
9402
9513
  if (commitScope) {
@@ -9426,7 +9537,7 @@ var plugin26 = {
9426
9537
  stagedDiff: `
9427
9538
  ## Staged changes (dry run)
9428
9539
 
9429
- ${stat8}
9540
+ ${stat13}
9430
9541
 
9431
9542
  \`\`\`diff
9432
9543
  ${stagedDiff}
@@ -9438,20 +9549,19 @@ ${stagedDiff}
9438
9549
  if (drifted.length > 0) {
9439
9550
  const preview = drifted.slice(0, 10).join(", ");
9440
9551
  const suffix = drifted.length > 10 ? ` and ${drifted.length - 10} more` : "";
9441
- return {
9442
- ok: false,
9443
- error: `Working tree changed after staging for: ${preview}${suffix}. A scoped commit takes working-tree content, so committing now could include changes that were never staged or previewed. Re-run the tool to re-stage the current content.`
9444
- };
9552
+ throw new Error(
9553
+ `Working tree changed after staging for: ${preview}${suffix}. A scoped commit takes working-tree content, so committing now could include changes that were never staged or previewed. Re-run the tool to re-stage the current content.`
9554
+ );
9445
9555
  }
9446
9556
  }
9447
9557
  let hash = "";
9448
9558
  try {
9449
9559
  hash = await commitWithMessage(msg, void 0, commitScope);
9450
9560
  } catch (err) {
9451
- return {
9452
- ok: false,
9453
- error: `Failed to commit: ${err instanceof Error ? err.message : String(err)}`
9454
- };
9561
+ throw new Error(
9562
+ `Failed to commit: ${err instanceof Error ? err.message : String(err)}`,
9563
+ { cause: err }
9564
+ );
9455
9565
  }
9456
9566
  api.log.info("git-autocommit: created commit", { hash, type, scope });
9457
9567
  commitCount.value += 1;
@@ -9483,17 +9593,15 @@ ${stagedDiff}
9483
9593
  diff: `
9484
9594
  ## Staged diff
9485
9595
 
9486
- ${stat8}
9596
+ ${stat13}
9487
9597
 
9488
9598
  \`\`\`diff
9489
9599
  ${stagedDiff}
9490
9600
  \`\`\``
9491
9601
  };
9492
9602
  } catch (err) {
9493
- return {
9494
- ok: false,
9495
- error: `Uncaught error in git_autocommit: ${err instanceof Error ? err.message : String(err)}`
9496
- };
9603
+ if (err instanceof Error) throw err;
9604
+ throw new Error(`Uncaught error in git_autocommit: ${String(err)}`, { cause: err });
9497
9605
  }
9498
9606
  }
9499
9607
  });
@@ -9532,6 +9640,7 @@ var git_autocommit_default = plugin26;
9532
9640
  // src/gitignore-guard/index.ts
9533
9641
  import { access as access3, readFile as readFile9, writeFile as writeFile2 } from "node:fs/promises";
9534
9642
  import { basename as basename3, isAbsolute as isAbsolute10, join as join3, relative as relative11, resolve as resolve12, sep as sep2 } from "node:path";
9643
+ import { ToolValidationError as ToolValidationError14 } from "@wrongstack/core/types";
9535
9644
  var API_VERSION19 = "^0.1.10";
9536
9645
  var DEFAULT_ARTIFACT_PATTERNS = Object.freeze([
9537
9646
  "dist/",
@@ -9900,19 +10009,24 @@ var plugin27 = {
9900
10009
  const rawInp = input ?? {};
9901
10010
  const raw = rawInp["path"] ?? rawInp["filePath"] ?? rawInp["file_path"] ?? rawInp["TargetFile"] ?? rawInp["targetFile"] ?? rawInp["file"];
9902
10011
  const rawPath = typeof raw === "string" ? raw.trim() : "";
9903
- if (rawPath.length === 0) return { ok: false, reason: "path is required" };
10012
+ if (rawPath.length === 0) {
10013
+ throw new ToolValidationError14({ message: "path is required", field: "path" });
10014
+ }
9904
10015
  const resolved = projectRelativePath(rawPath, process.cwd());
9905
10016
  if (!resolved) {
9906
- return { ok: false, reason: `path outside project root: ${rawPath}` };
10017
+ throw new ToolValidationError14({
10018
+ message: `path outside project root: ${rawPath}`,
10019
+ field: "path"
10020
+ });
9907
10021
  }
9908
10022
  const cfg = readConfig22(api.config.extensions?.["gitignore-guard"]);
9909
10023
  const explicit = typeof input?.pattern === "string" && input.pattern.trim().length > 0 ? input.pattern.trim() : null;
9910
10024
  const pattern = explicit ?? classifyArtifact(resolved.rel, cfg.artifactPatterns);
9911
10025
  if (!pattern) {
9912
- return {
9913
- ok: false,
9914
- reason: `'${resolved.rel}' does not match any configured artifact pattern; pass an explicit pattern`
9915
- };
10026
+ throw new ToolValidationError14({
10027
+ message: `'${resolved.rel}' does not match any configured artifact pattern; pass an explicit pattern`,
10028
+ field: "pattern"
10029
+ });
9916
10030
  }
9917
10031
  const candidates = gitignoreCandidates(relDirOf(resolved.rel), resolved.root);
9918
10032
  const nearest = await firstExistingGitignore(candidates);
@@ -10620,8 +10734,9 @@ var plugin29 = {
10620
10734
  var injection_shield_default = plugin29;
10621
10735
 
10622
10736
  // src/interface-contract-guard/index.ts
10623
- import { readFile as readFile10 } from "node:fs/promises";
10737
+ import { readFile as readFile10, stat as stat8 } from "node:fs/promises";
10624
10738
  import { isAbsolute as isAbsolute12, relative as relative12, resolve as resolve13 } from "node:path";
10739
+ import { ToolValidationError as ToolValidationError15 } from "@wrongstack/core/types";
10625
10740
  var API_VERSION21 = "^0.1.10";
10626
10741
  var state27 = {
10627
10742
  scanCount: 0,
@@ -10816,19 +10931,26 @@ If you changed member shapes, search the project for implementers/\`satisfies\`/
10816
10931
  category: "Diagnostics",
10817
10932
  mutating: false,
10818
10933
  async execute(input) {
10819
- if (!cfg.enabled) return { ok: false, error: "interface-contract-guard is disabled" };
10934
+ if (!cfg.enabled) throw new Error("interface-contract-guard is disabled");
10820
10935
  const raw = input ?? {};
10821
10936
  const rawPath = (typeof input.path === "string" && input.path.trim().length > 0 ? input.path.trim() : void 0) ?? (typeof raw["directory"] === "string" ? raw["directory"] : void 0) ?? (typeof raw["dir"] === "string" ? raw["dir"] : void 0) ?? (typeof raw["SearchDirectory"] === "string" ? raw["SearchDirectory"] : void 0) ?? (typeof raw["TargetFile"] === "string" ? raw["TargetFile"] : void 0) ?? (typeof raw["filePath"] === "string" ? raw["filePath"] : void 0) ?? (typeof raw["targetFile"] === "string" ? raw["targetFile"] : void 0) ?? (typeof raw["file"] === "string" ? raw["file"] : void 0) ?? ".";
10822
10937
  if (!(0, runtime_exports.withinProject)(rawPath)) {
10823
- return { ok: false, error: "path is outside the project root" };
10938
+ throw new ToolValidationError15({
10939
+ message: "path is outside the project root",
10940
+ field: "path"
10941
+ });
10824
10942
  }
10825
10943
  state27.scanCount += 1;
10826
10944
  let result;
10827
10945
  try {
10946
+ await stat8(resolve13(process.cwd(), rawPath));
10828
10947
  result = await scanPath3(rawPath, cfg);
10829
10948
  } catch (err) {
10830
10949
  state27.errorCount += 1;
10831
- return { ok: false, error: String(err) };
10950
+ throw new Error(
10951
+ `check_interface_contracts failed for ${rawPath}: ${err instanceof Error ? err.message : String(err)}`,
10952
+ { cause: err }
10953
+ );
10832
10954
  }
10833
10955
  state27.findingCount += result.findings.length;
10834
10956
  return {
@@ -10915,6 +11037,7 @@ var interface_contract_guard_default = plugin30;
10915
11037
  // src/knowledge-graph/index.ts
10916
11038
  import { readFileSync as readFileSync7 } from "node:fs";
10917
11039
  import { dirname as dirname4, isAbsolute as isAbsolute13, relative as relative13, resolve as resolve14 } from "node:path";
11040
+ import { ToolValidationError as ToolValidationError16 } from "@wrongstack/core/types";
10918
11041
  import { atomicWrite as atomicWrite2, ensureDir as ensureDir2 } from "@wrongstack/core/utils";
10919
11042
  var API_VERSION22 = "^0.1.10";
10920
11043
  var state28 = {
@@ -11085,12 +11208,9 @@ var plugin31 = {
11085
11208
  category: "Memory",
11086
11209
  mutating: true,
11087
11210
  async execute(input) {
11088
- if (!cfg.enabled) return { ok: false, error: "knowledge-graph is disabled" };
11211
+ if (!cfg.enabled) throw new Error("knowledge-graph is disabled");
11089
11212
  if (state28.facts.length >= cfg.maxFacts) {
11090
- return {
11091
- ok: false,
11092
- error: `fact limit reached (${cfg.maxFacts}). Remove old facts first.`
11093
- };
11213
+ throw new Error(`fact limit reached (${cfg.maxFacts}). Remove old facts first.`);
11094
11214
  }
11095
11215
  const trim = (s) => String(s ?? "").trim().slice(0, cfg.maxFactChars);
11096
11216
  const raw = input;
@@ -11104,7 +11224,10 @@ var plugin31 = {
11104
11224
  input.object ?? raw["target"] ?? raw["val"] ?? raw["value"] ?? raw["obj"] ?? raw["targetEntity"]
11105
11225
  );
11106
11226
  if (!subject || !relation || !object) {
11107
- return { ok: false, error: "subject, relation, and object are required" };
11227
+ throw new ToolValidationError16({
11228
+ message: "subject, relation, and object are required",
11229
+ field: !subject ? "subject" : !relation ? "relation" : "object"
11230
+ });
11108
11231
  }
11109
11232
  const rawConf = typeof input.confidence === "string" ? input.confidence.trim().toLowerCase() : "";
11110
11233
  const confidence = rawConf === "low" || rawConf === "high" || rawConf === "medium" ? rawConf : "medium";
@@ -11141,7 +11264,7 @@ var plugin31 = {
11141
11264
  category: "Memory",
11142
11265
  mutating: false,
11143
11266
  async execute(input) {
11144
- if (!cfg.enabled) return { ok: false, error: "knowledge-graph is disabled" };
11267
+ if (!cfg.enabled) throw new Error("knowledge-graph is disabled");
11145
11268
  state28.queries += 1;
11146
11269
  const raw = input;
11147
11270
  const rawQ = (typeof raw["query"] === "string" ? raw["query"] : void 0) ?? (typeof raw["q"] === "string" ? raw["q"] : void 0);
@@ -11186,16 +11309,17 @@ var plugin31 = {
11186
11309
  category: "Memory",
11187
11310
  mutating: true,
11188
11311
  async execute(input) {
11189
- if (!cfg.enabled) return { ok: false, error: "knowledge-graph is disabled" };
11312
+ if (!cfg.enabled) throw new Error("knowledge-graph is disabled");
11190
11313
  const before = state28.facts.length;
11191
11314
  const raw = input ?? {};
11192
11315
  const rawId = String(input.id ?? raw["factId"] ?? raw["fact_id"] ?? "").trim();
11316
+ if (!rawId) throw new ToolValidationError16({ message: "id is required", field: "id" });
11193
11317
  const normalized = rawId.toLowerCase().startsWith("kg-") ? rawId.toLowerCase() : `kg-${rawId.toLowerCase()}`;
11194
11318
  state28.facts = state28.facts.filter(
11195
11319
  (f) => f.id.toLowerCase() !== normalized && f.id !== rawId
11196
11320
  );
11197
11321
  const removed = before - state28.facts.length;
11198
- if (removed === 0) return { ok: false, error: `no fact matches "${input.id ?? rawId}"` };
11322
+ if (removed === 0) throw new Error(`no fact matches "${input.id ?? rawId}"`);
11199
11323
  state28.removals += removed;
11200
11324
  api.metrics.counter("removals", removed);
11201
11325
  const persisted = await persistFacts(resolved);
@@ -12364,7 +12488,10 @@ var state32 = {
12364
12488
  };
12365
12489
  var DEFAULTS30 = {
12366
12490
  enabled: true,
12367
- mode: "warn",
12491
+ // Documented contract (feature matrix, plugin description): warn, then
12492
+ // block. A warn-only default meant an agent stuck re-issuing the same call
12493
+ // was never actually stopped unless the user happened to set any option.
12494
+ mode: "block",
12368
12495
  warnAfter: 3,
12369
12496
  blockAfter: 5,
12370
12497
  oscillationWindow: 8,
@@ -12390,7 +12517,7 @@ function readConfig30(raw) {
12390
12517
  const ignoreTools = Array.isArray(rawIgnore) ? rawIgnore.filter((t) => typeof t === "string") : [];
12391
12518
  ignoreToolsSet = new Set(ignoreTools);
12392
12519
  const rawMode = typeof (r["mode"] ?? r["action"] ?? r["behavior"]) === "string" ? String(r["mode"] ?? r["action"] ?? r["behavior"]).trim().toLowerCase() : void 0;
12393
- const mode = rawMode === "warn" ? "warn" : "block";
12520
+ const mode = rawMode === "warn" || rawMode === "block" ? rawMode : DEFAULTS30.mode;
12394
12521
  const rawOsc = r["oscillationWindow"] ?? r["oscillation_window"] ?? r["window"];
12395
12522
  const rawMaxSteps = r["maxSteps"] ?? r["max_steps"] ?? r["stepLimit"] ?? r["step_limit"];
12396
12523
  return {
@@ -12419,7 +12546,11 @@ function sortKeys(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
12419
12546
  if (value === null || typeof value !== "object") return value;
12420
12547
  if (seen.has(value)) return "[circular]";
12421
12548
  if (depth >= CANONICALIZE_MAX_DEPTH) {
12422
- return Array.isArray(value) ? `[array:${value.length}]` : "[deep-object]";
12549
+ try {
12550
+ return `[deep:${JSON.stringify(value)}]`;
12551
+ } catch {
12552
+ return Array.isArray(value) ? `[array:${value.length}]` : "[deep-object]";
12553
+ }
12423
12554
  }
12424
12555
  seen.add(value);
12425
12556
  try {
@@ -12507,7 +12638,7 @@ var plugin35 = {
12507
12638
  mode: {
12508
12639
  type: "string",
12509
12640
  enum: ["warn", "block"],
12510
- default: "warn",
12641
+ default: "block",
12511
12642
  description: "block = refuse the repeated call; warn = only inject context."
12512
12643
  },
12513
12644
  warnAfter: {
@@ -12839,6 +12970,7 @@ var loop_breaker_default = plugin35;
12839
12970
 
12840
12971
  // src/migration-planner/index.ts
12841
12972
  import { existsSync as existsSync5, readFileSync as readFileSync10 } from "node:fs";
12973
+ import { ToolValidationError as ToolValidationError17 } from "@wrongstack/core/types";
12842
12974
 
12843
12975
  // src/runtime/llm.ts
12844
12976
  import {
@@ -13107,14 +13239,14 @@ var plugin36 = {
13107
13239
  const rawPath = inp["path"] ?? inp["TargetFile"] ?? inp["filePath"] ?? inp["targetFile"] ?? inp["file_path"] ?? inp["file"];
13108
13240
  const path = typeof rawPath === "string" ? rawPath : void 0;
13109
13241
  if (!path) return;
13110
- const basename7 = path.split(/[/\\]/).pop() ?? "";
13242
+ const basename8 = path.split(/[/\\]/).pop() ?? "";
13111
13243
  if (!/^(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/i.test(
13112
- basename7
13244
+ basename8
13113
13245
  )) {
13114
13246
  return;
13115
13247
  }
13116
13248
  return {
13117
- additionalContext: `Manifest file ${basename7} changed. Consider running migration_plan if a dependency version was updated.`
13249
+ additionalContext: `Manifest file ${basename8} changed. Consider running migration_plan if a dependency version was updated.`
13118
13250
  };
13119
13251
  };
13120
13252
  state33.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook, {
@@ -13212,7 +13344,7 @@ var plugin36 = {
13212
13344
  category: "Planning",
13213
13345
  mutating: false,
13214
13346
  async execute(input, _ctx, execOpts) {
13215
- if (!cfg.enabled) return { ok: false, error: "migration-planner is disabled" };
13347
+ if (!cfg.enabled) throw new Error("migration-planner is disabled");
13216
13348
  execOpts?.signal?.throwIfAborted();
13217
13349
  const raw = input ?? {};
13218
13350
  const rawPackage = input.packageName || raw["package"] || raw["pkg"] || raw["name"] || raw["package_name"] || raw["packageName"] || raw["dependency"] || raw["dep"] || raw["module"];
@@ -13223,7 +13355,10 @@ var plugin36 = {
13223
13355
  const fromVersion = String(rawFrom ?? "").trim();
13224
13356
  const toVersion = String(rawTo ?? "").trim();
13225
13357
  if (!packageName || !fromVersion || !toVersion) {
13226
- return { ok: false, error: "packageName, fromVersion, and toVersion are required" };
13358
+ throw new ToolValidationError17({
13359
+ message: "packageName, fromVersion, and toVersion are required",
13360
+ field: !packageName ? "packageName" : !fromVersion ? "fromVersion" : "toVersion"
13361
+ });
13227
13362
  }
13228
13363
  const changelog = readChangelog(packageName, cfg);
13229
13364
  let breakingChanges;
@@ -13593,6 +13728,7 @@ var model_router_default = plugin37;
13593
13728
 
13594
13729
  // src/notify-hub/index.ts
13595
13730
  import { lookup } from "node:dns/promises";
13731
+ import { ToolValidationError as ToolValidationError18 } from "@wrongstack/core/types";
13596
13732
 
13597
13733
  // src/notify-hub/webhook-channel.ts
13598
13734
  function freshCircuit() {
@@ -13755,7 +13891,13 @@ var DEFAULTS33 = {
13755
13891
  maxConsecutiveFailures: 5
13756
13892
  };
13757
13893
  function isPrivateIPv4(hostname) {
13758
- const normalised = hostname.replace(/^::ffff:/i, "");
13894
+ const hexMapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname);
13895
+ const normalised = hexMapped ? [
13896
+ Number.parseInt(hexMapped[1], 16) >> 8,
13897
+ Number.parseInt(hexMapped[1], 16) & 255,
13898
+ Number.parseInt(hexMapped[2], 16) >> 8,
13899
+ Number.parseInt(hexMapped[2], 16) & 255
13900
+ ].join(".") : hostname.replace(/^::ffff:/i, "");
13759
13901
  const parts = normalised.split(".").map((p) => Number(p));
13760
13902
  if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
13761
13903
  return false;
@@ -13995,18 +14137,19 @@ var plugin38 = {
13995
14137
  category: "Notifications",
13996
14138
  mutating: true,
13997
14139
  async execute(input) {
13998
- if (!cfg.enabled) return { ok: false, error: "notify-hub is disabled" };
14140
+ if (!cfg.enabled) throw new Error("notify-hub is disabled");
13999
14141
  const ch = state35.channel;
14000
14142
  if (!ch) {
14001
- return {
14002
- ok: false,
14003
- error: 'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
14004
- };
14143
+ throw new Error(
14144
+ 'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
14145
+ );
14005
14146
  }
14006
14147
  const inp = input ?? {};
14007
14148
  const rawMsg = inp["message"] ?? inp["body"] ?? inp["text"] ?? inp["content"] ?? inp["msg"];
14008
14149
  const message = typeof rawMsg === "string" && rawMsg.trim().length > 0 ? rawMsg.trim() : "";
14009
- if (!message) return { ok: false, error: "message is required" };
14150
+ if (!message) {
14151
+ throw new ToolValidationError18({ message: "message is required", field: "message" });
14152
+ }
14010
14153
  const rawTitle = inp["title"] ?? inp["subject"] ?? inp["header"];
14011
14154
  const title = typeof rawTitle === "string" && rawTitle.trim() ? rawTitle.trim() : "WrongStack notification";
14012
14155
  const result = await deliverViaChannel(ch, "manual", {
@@ -14015,11 +14158,10 @@ var plugin38 = {
14015
14158
  level: input.level === "warning" || input.level === "critical" ? input.level : "info",
14016
14159
  source: "manual"
14017
14160
  });
14018
- return {
14019
- ok: result.ok,
14020
- circuitOpen: ch.circuitStatus().open,
14021
- ...result.ok ? {} : { error: result.error ?? "delivery failed" }
14022
- };
14161
+ if (!result.ok) {
14162
+ throw new Error(`notification delivery failed: ${result.error ?? "unknown error"}`);
14163
+ }
14164
+ return { ok: true, circuitOpen: ch.circuitStatus().open };
14023
14165
  }
14024
14166
  });
14025
14167
  api.tools.register({
@@ -14238,8 +14380,8 @@ function isRootPathScope(path) {
14238
14380
  function isDirectoryAmbiguousPath(path) {
14239
14381
  const normalized = normalizePath2(path).replace(/\/$/, "");
14240
14382
  if (isRootPathScope(normalized)) return true;
14241
- const basename7 = normalized.slice(normalized.lastIndexOf("/") + 1);
14242
- return path.endsWith("/") || basename7.length > 0 && !basename7.includes(".");
14383
+ const basename8 = normalized.slice(normalized.lastIndexOf("/") + 1);
14384
+ return path.endsWith("/") || basename8.length > 0 && !basename8.includes(".");
14243
14385
  }
14244
14386
  function hasConfiguredProtectedDescendant(path, patterns) {
14245
14387
  const normalized = normalizePath2(path).replace(/\/$/, "").toLowerCase();
@@ -15516,9 +15658,9 @@ var plugin39 = {
15516
15658
  } catch {
15517
15659
  }
15518
15660
  }
15519
- const state60 = createState();
15520
- states.set(api, state60);
15521
- latestState = state60;
15661
+ const state59 = createState();
15662
+ states.set(api, state59);
15663
+ latestState = state59;
15522
15664
  const cfg = readConfig34(api.config.extensions?.["path-guard"]);
15523
15665
  const protectRes = cfg.protect.map(compilePathGlob);
15524
15666
  const allowRes = cfg.allow.map(compilePathGlob);
@@ -15526,15 +15668,15 @@ var plugin39 = {
15526
15668
  const subject = isScope ? `write scope "${path}" may include a protected path \u2014 narrow it or add an \`allow\` glob` : `"${path}" is a protected path`;
15527
15669
  const matchContext = isScope ? 'its unresolved scope overlaps config.extensions["path-guard"].protect' : 'matched by config.extensions["path-guard"].protect';
15528
15670
  if (cfg.mode === "block") {
15529
- state60.blocks += 1;
15530
- state60.lastBlock = { path, tool, when: (/* @__PURE__ */ new Date()).toISOString() };
15671
+ state59.blocks += 1;
15672
+ state59.lastBlock = { path, tool, when: (/* @__PURE__ */ new Date()).toISOString() };
15531
15673
  api.metrics.counter("blocks");
15532
15674
  return {
15533
15675
  decision: "block",
15534
15676
  reason: `path-guard: ${subject} (${matchContext}) \u2014 ${operation} refused. If this change is intentional, ask the user to do it, add an \`allow\` glob, or set mode: "warn".`
15535
15677
  };
15536
15678
  }
15537
- state60.warns += 1;
15679
+ state59.warns += 1;
15538
15680
  api.metrics.counter("warns");
15539
15681
  return {
15540
15682
  decision: "allow",
@@ -15542,12 +15684,12 @@ var plugin39 = {
15542
15684
  };
15543
15685
  };
15544
15686
  const bumpRedos = () => {
15545
- state60.redosTimeouts += 1;
15687
+ state59.redosTimeouts += 1;
15546
15688
  api.metrics.counter("redos_timeouts");
15547
15689
  };
15548
15690
  const hook = async (input) => {
15549
15691
  if (!cfg.enabled) return;
15550
- state60.invocations += 1;
15692
+ state59.invocations += 1;
15551
15693
  const toolName = input.toolName ?? "";
15552
15694
  const ti = input.toolInput ?? {};
15553
15695
  const command = typeof ti["command"] === "string" ? ti["command"] : "";
@@ -15633,7 +15775,7 @@ var plugin39 = {
15633
15775
  }
15634
15776
  return;
15635
15777
  };
15636
- state60.hookUnregister = api.registerHook("PreToolUse", "*", hook, {
15778
+ state59.hookUnregister = api.registerHook("PreToolUse", "*", hook, {
15637
15779
  name: "path-guard",
15638
15780
  stage: "validate",
15639
15781
  failurePolicy: "closed",
@@ -15654,12 +15796,12 @@ var plugin39 = {
15654
15796
  protect: cfg.protect,
15655
15797
  allow: cfg.allow,
15656
15798
  counters: {
15657
- invocations: state60.invocations,
15658
- blocks: state60.blocks,
15659
- warns: state60.warns,
15660
- redosTimeouts: state60.redosTimeouts
15799
+ invocations: state59.invocations,
15800
+ blocks: state59.blocks,
15801
+ warns: state59.warns,
15802
+ redosTimeouts: state59.redosTimeouts
15661
15803
  },
15662
- lastBlock: state60.lastBlock
15804
+ lastBlock: state59.lastBlock
15663
15805
  };
15664
15806
  }
15665
15807
  });
@@ -15671,39 +15813,39 @@ var plugin39 = {
15671
15813
  });
15672
15814
  },
15673
15815
  teardown(api) {
15674
- const state60 = states.get(api);
15675
- if (!state60) return;
15676
- if (state60.hookUnregister) {
15816
+ const state59 = states.get(api);
15817
+ if (!state59) return;
15818
+ if (state59.hookUnregister) {
15677
15819
  try {
15678
- state60.hookUnregister();
15820
+ state59.hookUnregister();
15679
15821
  } catch {
15680
15822
  }
15681
- state60.hookUnregister = null;
15823
+ state59.hookUnregister = null;
15682
15824
  }
15683
15825
  const final = {
15684
- invocations: state60.invocations,
15685
- blocks: state60.blocks,
15686
- warns: state60.warns,
15687
- redosTimeouts: state60.redosTimeouts
15826
+ invocations: state59.invocations,
15827
+ blocks: state59.blocks,
15828
+ warns: state59.warns,
15829
+ redosTimeouts: state59.redosTimeouts
15688
15830
  };
15689
- state60.invocations = 0;
15690
- state60.blocks = 0;
15691
- state60.warns = 0;
15692
- state60.redosTimeouts = 0;
15693
- state60.lastBlock = null;
15831
+ state59.invocations = 0;
15832
+ state59.blocks = 0;
15833
+ state59.warns = 0;
15834
+ state59.redosTimeouts = 0;
15835
+ state59.lastBlock = null;
15694
15836
  states.delete(api);
15695
15837
  api.log.info("path-guard: teardown complete", { final });
15696
15838
  },
15697
15839
  async health() {
15698
- const state60 = latestState;
15840
+ const state59 = latestState;
15699
15841
  return {
15700
15842
  ok: true,
15701
- message: state60.lastBlock === null ? `path-guard: ${state60.invocations} invocation(s), ${state60.blocks} block(s), ${state60.warns} warn(s)` : `path-guard: last block on "${state60.lastBlock.path}" (${state60.lastBlock.tool}) at ${state60.lastBlock.when}`,
15843
+ message: state59.lastBlock === null ? `path-guard: ${state59.invocations} invocation(s), ${state59.blocks} block(s), ${state59.warns} warn(s)` : `path-guard: last block on "${state59.lastBlock.path}" (${state59.lastBlock.tool}) at ${state59.lastBlock.when}`,
15702
15844
  counters: {
15703
- invocations: state60.invocations,
15704
- blocks: state60.blocks,
15705
- warns: state60.warns,
15706
- redosTimeouts: state60.redosTimeouts
15845
+ invocations: state59.invocations,
15846
+ blocks: state59.blocks,
15847
+ warns: state59.warns,
15848
+ redosTimeouts: state59.redosTimeouts
15707
15849
  }
15708
15850
  };
15709
15851
  }
@@ -15713,6 +15855,7 @@ var path_guard_default = plugin39;
15713
15855
  // src/performance-regression-gate/index.ts
15714
15856
  import { existsSync as existsSync7, readFileSync as readFileSync11 } from "node:fs";
15715
15857
  import { isAbsolute as isAbsolute16, relative as relative16, resolve as resolve18 } from "node:path";
15858
+ import { ToolValidationError as ToolValidationError19 } from "@wrongstack/core/types";
15716
15859
  var API_VERSION26 = "^0.1.10";
15717
15860
  var state36 = {
15718
15861
  invocationCount: 0,
@@ -15779,10 +15922,11 @@ function flattenResults(results) {
15779
15922
  function loadResults(path) {
15780
15923
  if (!path || !existsSync7(path)) return null;
15781
15924
  try {
15782
- const raw = JSON.parse(readFileSync11(path, "utf-8"));
15783
- return raw;
15784
- } catch {
15785
- return null;
15925
+ return JSON.parse(readFileSync11(path, "utf-8"));
15926
+ } catch (err) {
15927
+ throw new Error(`Could not read benchmark results at ${path}: ${String(err)}`, {
15928
+ cause: err
15929
+ });
15786
15930
  }
15787
15931
  }
15788
15932
  function stripVariantSuffix(name) {
@@ -15909,7 +16053,7 @@ var plugin40 = {
15909
16053
  mutating: false,
15910
16054
  async execute(input) {
15911
16055
  if (!cfg.enabled) {
15912
- return { ok: false, error: "performance-regression-gate is disabled" };
16056
+ throw new Error("performance-regression-gate is disabled");
15913
16057
  }
15914
16058
  state36.invocationCount += 1;
15915
16059
  const raw = input ?? {};
@@ -15920,11 +16064,23 @@ var plugin40 = {
15920
16064
  const resultsPath = resolveProjectPath6(resultsPathStr) ?? "";
15921
16065
  if (!resultsPath) {
15922
16066
  state36.errorCount += 1;
15923
- return { ok: false, error: "invalid results path (must be inside project)" };
16067
+ throw new ToolValidationError19({
16068
+ message: "invalid results path (must be inside project)",
16069
+ field: "resultsPath"
16070
+ });
16071
+ }
16072
+ let results;
16073
+ try {
16074
+ results = loadResults(resultsPath);
16075
+ } catch (err) {
16076
+ state36.errorCount += 1;
16077
+ throw err;
15924
16078
  }
15925
- const results = loadResults(resultsPath);
15926
16079
  if (!results) {
15927
16080
  state36.missingResultsCount += 1;
16081
+ if (resultsPathStr !== "bench-results.json" || rawResultsPath !== "bench-results.json") {
16082
+ throw new Error(`No benchmark results found at ${resultsPathStr}.`);
16083
+ }
15928
16084
  return {
15929
16085
  ok: true,
15930
16086
  hasResults: false,
@@ -15953,15 +16109,21 @@ var plugin40 = {
15953
16109
  const baselineResolved = resolveProjectPath6(baselinePathStr) ?? "";
15954
16110
  if (!baselineResolved) {
15955
16111
  state36.errorCount += 1;
15956
- return { ok: false, error: "invalid baseline path (must be inside project)" };
16112
+ throw new ToolValidationError19({
16113
+ message: "invalid baseline path (must be inside project)",
16114
+ field: "baselinePath"
16115
+ });
16116
+ }
16117
+ let baselineResults;
16118
+ try {
16119
+ baselineResults = loadResults(baselineResolved);
16120
+ } catch (err) {
16121
+ state36.errorCount += 1;
16122
+ throw err;
15957
16123
  }
15958
- const baselineResults = loadResults(baselineResolved);
15959
16124
  if (!baselineResults) {
15960
16125
  state36.errorCount += 1;
15961
- return {
15962
- ok: false,
15963
- error: `Could not read baseline results at ${baselinePathStr}.`
15964
- };
16126
+ throw new Error(`Could not read baseline results at ${baselinePathStr}.`);
15965
16127
  }
15966
16128
  const baseline = flattenResults(baselineResults);
15967
16129
  pairs = pairCrossFile(baseline, current);
@@ -16176,6 +16338,7 @@ var plugin_stack_observer_default = PLUGIN;
16176
16338
  import { execFile as execFile9 } from "node:child_process";
16177
16339
  import { mkdir as mkdir2, writeFile as writeFile4 } from "node:fs/promises";
16178
16340
  import { dirname as dirname7, isAbsolute as isAbsolute17, relative as relative17, resolve as resolve19 } from "node:path";
16341
+ import { ToolValidationError as ToolValidationError20 } from "@wrongstack/core/types";
16179
16342
  var API_VERSION27 = "^0.1.10";
16180
16343
  var state38 = {
16181
16344
  commits: [],
@@ -16188,8 +16351,23 @@ var state38 = {
16188
16351
  draftErrors: 0,
16189
16352
  stopInvocations: 0,
16190
16353
  stopHookUnregister: null,
16354
+ postHookUnregister: null,
16191
16355
  eventUnsubscribers: []
16192
16356
  };
16357
+ var TRACKED_FILE_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "write_to_file", "replace_file_content"]);
16358
+ function parseAutocommitResult(content) {
16359
+ const hash = /"hash":\s*"([0-9a-f]{7,64})"/.exec(content)?.[1];
16360
+ if (!hash) return null;
16361
+ const rawMessage = /"message":\s*("(?:[^"\\]|\\.)*")/.exec(content)?.[1];
16362
+ let message = "commit";
16363
+ if (rawMessage) {
16364
+ try {
16365
+ message = JSON.parse(rawMessage);
16366
+ } catch {
16367
+ }
16368
+ }
16369
+ return { hash, message };
16370
+ }
16193
16371
  var DEFAULTS36 = {
16194
16372
  enabled: true,
16195
16373
  outputPath: ".wrongstack/PR_DRAFT.md",
@@ -16372,6 +16550,7 @@ var plugin41 = {
16372
16550
  state38.draftErrors = 0;
16373
16551
  state38.stopInvocations = 0;
16374
16552
  state38.stopHookUnregister = (0, runtime_exports.releaseHandle)(state38.stopHookUnregister);
16553
+ state38.postHookUnregister = (0, runtime_exports.releaseHandle)(state38.postHookUnregister);
16375
16554
  for (const off of state38.eventUnsubscribers) {
16376
16555
  try {
16377
16556
  off();
@@ -16381,22 +16560,24 @@ var plugin41 = {
16381
16560
  state38.eventUnsubscribers = [];
16382
16561
  const cfg = readConfig37(api.config.extensions?.["pr-drafter"]);
16383
16562
  if (api.onPattern) {
16384
- const offTool = api.onPattern("tool.completed", (_event, payload) => {
16385
- const p = payload;
16386
- const toolName = p?.tool;
16563
+ const offTool = api.onPattern("tool.completed", () => {
16387
16564
  state38.toolCalls += 1;
16388
- if (toolName === "git_autocommit" && p?.result?.committed) {
16389
- const msg = p.result.commitMessage ?? p.input?.message ?? "commit";
16390
- state38.commits.push(msg);
16391
- }
16392
- const rawInput = p?.input ?? {};
16393
- const filePath = typeof rawInput["path"] === "string" && rawInput["path"] || typeof rawInput["filePath"] === "string" && rawInput["filePath"] || typeof rawInput["file_path"] === "string" && rawInput["file_path"] || typeof rawInput["TargetFile"] === "string" && rawInput["TargetFile"] || typeof rawInput["targetFile"] === "string" && rawInput["targetFile"] || typeof rawInput["file"] === "string" && rawInput["file"];
16394
- if ((toolName === "write" || toolName === "edit" || toolName === "write_to_file" || toolName === "replace_file_content") && filePath) {
16395
- state38.files.add(filePath);
16396
- }
16397
16565
  });
16398
16566
  state38.eventUnsubscribers.push(offTool);
16399
16567
  }
16568
+ const postHook = (input) => {
16569
+ if (!input.toolResult || input.toolResult.isError) return;
16570
+ const toolName = input.toolName;
16571
+ if (toolName === "git_autocommit") {
16572
+ const commit = parseAutocommitResult(String(input.toolResult.content ?? ""));
16573
+ if (commit) state38.commits.push(commit.message);
16574
+ return;
16575
+ }
16576
+ if (!toolName || !TRACKED_FILE_TOOLS.has(toolName)) return;
16577
+ const rawInput = input.toolInput ?? {};
16578
+ const filePath = ["path", "filePath", "file_path", "TargetFile", "targetFile", "file"].map((key) => rawInput[key]).find((value) => typeof value === "string" && value.length > 0);
16579
+ if (filePath) state38.files.add(filePath);
16580
+ };
16400
16581
  if (api.onEvent) {
16401
16582
  const offUsage = api.onEvent("provider.response", (payload) => {
16402
16583
  const p = payload;
@@ -16415,6 +16596,11 @@ var plugin41 = {
16415
16596
  await writeDraft(cfg, api.llm);
16416
16597
  };
16417
16598
  state38.stopHookUnregister = api.registerHook("Stop", void 0, stopHook);
16599
+ state38.postHookUnregister = api.registerHook(
16600
+ "PostToolUse",
16601
+ "git_autocommit|write|edit|write_to_file|replace_file_content",
16602
+ postHook
16603
+ );
16418
16604
  api.tools.register({
16419
16605
  name: "pr_draft",
16420
16606
  description: "Generate or refresh the pull-request draft for the current session. Writes the markdown file and returns its path + title.",
@@ -16437,7 +16623,7 @@ var plugin41 = {
16437
16623
  mutating: true,
16438
16624
  capabilities: ["fs.write"],
16439
16625
  async execute(input = {}) {
16440
- if (!cfg.enabled) return { ok: false, error: "pr-drafter is disabled" };
16626
+ if (!cfg.enabled) throw new Error("pr-drafter is disabled");
16441
16627
  const raw = input ?? {};
16442
16628
  const preview = Boolean(
16443
16629
  input?.preview ?? raw["dryRun"] ?? raw["dry_run"] ?? raw["dry"] ?? raw["previewOnly"]
@@ -16449,24 +16635,29 @@ var plugin41 = {
16449
16635
  return { ok: true, preview: true, title: draft.title, body: draft.body };
16450
16636
  }
16451
16637
  const resolved = resolveProjectPath7(outputPathStr);
16452
- if (!resolved) return { ok: false, error: "outputPath resolves outside project" };
16638
+ if (!resolved) {
16639
+ throw new ToolValidationError20({
16640
+ message: "outputPath resolves outside project",
16641
+ field: "outputPath"
16642
+ });
16643
+ }
16453
16644
  try {
16454
16645
  await mkdir2(dirname7(resolved), { recursive: true });
16455
16646
  await writeFile4(resolved, draft.body);
16456
- state38.draftsWritten += 1;
16457
- return {
16458
- ok: true,
16459
- path: outputPathStr,
16460
- resolvedPath: resolved,
16461
- title: draft.title
16462
- };
16463
16647
  } catch (err) {
16464
16648
  state38.draftErrors += 1;
16465
- return {
16466
- ok: false,
16467
- error: err instanceof Error ? err.message : String(err)
16468
- };
16649
+ throw new Error(
16650
+ `Could not write PR draft to ${outputPathStr}: ${err instanceof Error ? err.message : String(err)}`,
16651
+ { cause: err }
16652
+ );
16469
16653
  }
16654
+ state38.draftsWritten += 1;
16655
+ return {
16656
+ ok: true,
16657
+ path: outputPathStr,
16658
+ resolvedPath: resolved,
16659
+ title: draft.title
16660
+ };
16470
16661
  }
16471
16662
  });
16472
16663
  api.log.info("pr-drafter plugin loaded", {
@@ -16483,6 +16674,7 @@ var plugin41 = {
16483
16674
  }
16484
16675
  state38.stopHookUnregister = null;
16485
16676
  }
16677
+ state38.postHookUnregister = (0, runtime_exports.releaseHandle)(state38.postHookUnregister);
16486
16678
  for (const off of state38.eventUnsubscribers) {
16487
16679
  try {
16488
16680
  off();
@@ -17194,8 +17386,9 @@ var plugin43 = {
17194
17386
  var prompt_firewall_default = plugin43;
17195
17387
 
17196
17388
  // src/refactor-suggester/index.ts
17197
- import { readFile as readFile12 } from "node:fs/promises";
17389
+ import { readFile as readFile12, stat as stat9 } from "node:fs/promises";
17198
17390
  import { isAbsolute as isAbsolute18, relative as relative18, resolve as resolve20 } from "node:path";
17391
+ import { ToolValidationError as ToolValidationError21 } from "@wrongstack/core/types";
17199
17392
  var API_VERSION28 = "^0.1.10";
17200
17393
  var HOOK_WARNING_COOLDOWN_MS2 = 6e4;
17201
17394
  var state41 = {
@@ -17485,11 +17678,23 @@ var plugin44 = {
17485
17678
  category: "Diagnostics",
17486
17679
  mutating: false,
17487
17680
  async execute(input) {
17488
- if (!cfg.enabled) return { ok: false, error: "refactor-suggester is disabled" };
17681
+ if (!cfg.enabled) throw new Error("refactor-suggester is disabled");
17489
17682
  const raw = input ?? {};
17490
17683
  const rawPath = (typeof input.path === "string" && input.path.trim().length > 0 ? input.path.trim() : void 0) ?? (typeof raw["directory"] === "string" ? raw["directory"] : void 0) ?? (typeof raw["dir"] === "string" ? raw["dir"] : void 0) ?? (typeof raw["SearchDirectory"] === "string" ? raw["SearchDirectory"] : void 0) ?? (typeof raw["TargetFile"] === "string" ? raw["TargetFile"] : void 0) ?? (typeof raw["filePath"] === "string" ? raw["filePath"] : void 0) ?? (typeof raw["targetFile"] === "string" ? raw["targetFile"] : void 0) ?? ".";
17491
17684
  if (!(0, runtime_exports.withinProject)(rawPath)) {
17492
- return { ok: false, error: "path is outside the project root" };
17685
+ throw new ToolValidationError21({
17686
+ message: "path is outside the project root",
17687
+ field: "path"
17688
+ });
17689
+ }
17690
+ try {
17691
+ await stat9(resolve20(process.cwd(), rawPath));
17692
+ } catch (err) {
17693
+ throw new ToolValidationError21({
17694
+ message: `path does not exist or cannot be read: ${rawPath}`,
17695
+ field: "path",
17696
+ cause: err
17697
+ });
17493
17698
  }
17494
17699
  state41.scanCount += 1;
17495
17700
  let result;
@@ -17497,7 +17702,7 @@ var plugin44 = {
17497
17702
  result = await scanPath4(rawPath, cfg);
17498
17703
  } catch (err) {
17499
17704
  state41.errorCount += 1;
17500
- return { ok: false, error: String(err) };
17705
+ throw new Error(`refactor scan failed: ${String(err)}`, { cause: err });
17501
17706
  }
17502
17707
  state41.suggestionCount += result.suggestions.length;
17503
17708
  return {
@@ -17858,7 +18063,7 @@ var plugin45 = {
17858
18063
  category: "Development",
17859
18064
  mutating: false,
17860
18065
  async execute(input, _ctx, execOpts) {
17861
- if (!cfg.enabled) return { ok: false, error: "release-notes-generator is disabled" };
18066
+ if (!cfg.enabled) throw new Error("release-notes-generator is disabled");
17862
18067
  execOpts?.signal?.throwIfAborted();
17863
18068
  const raw = input ?? {};
17864
18069
  const rawTo = input.to ?? raw["to_ref"] ?? raw["toRef"] ?? raw["until"] ?? raw["end"];
@@ -17873,7 +18078,7 @@ var plugin45 = {
17873
18078
  commits = await getCommits(fromRef, toRef, execOpts?.signal);
17874
18079
  } catch (err) {
17875
18080
  state42.errorCount += 1;
17876
- return { ok: false, error: String(err) };
18081
+ throw new Error(`Could not read git history: ${String(err)}`, { cause: err });
17877
18082
  }
17878
18083
  state42.commitCount += commits.length;
17879
18084
  execOpts?.signal?.throwIfAborted();
@@ -18288,6 +18493,7 @@ ${body}${suffix}`;
18288
18493
  var schema_evolution_guard_default = plugin46;
18289
18494
 
18290
18495
  // src/secret-scanner/index.ts
18496
+ import { ToolValidationError as ToolValidationError22 } from "@wrongstack/core/types";
18291
18497
  var BASE_PATTERNS = cloneCredentialPatterns();
18292
18498
  var PATTERNS3 = [...BASE_PATTERNS];
18293
18499
  var GROUP_INDEX_OF_PATTERN = [];
@@ -18496,7 +18702,7 @@ function readConfig43(raw) {
18496
18702
  function buildHook(cfg, log, runtime) {
18497
18703
  return (input) => {
18498
18704
  activateRuntime(runtime);
18499
- const { state: state60 } = runtime;
18705
+ const { state: state59 } = runtime;
18500
18706
  if (!cfg.enabled) return;
18501
18707
  const toolName = input.toolName ?? "unknown";
18502
18708
  let matched;
@@ -18504,7 +18710,7 @@ function buildHook(cfg, log, runtime) {
18504
18710
  matched = scanInput(input.toolInput);
18505
18711
  } catch (err) {
18506
18712
  if (String(err).includes("ReDoS")) {
18507
- state60.timeoutCount += 1;
18713
+ state59.timeoutCount += 1;
18508
18714
  return {
18509
18715
  decision: "block",
18510
18716
  reason: "secret-scanner: ReDoS timeout \u2014 regex scan exceeded the wall-clock budget. Fail-closed: treated as a block."
@@ -18516,8 +18722,8 @@ function buildHook(cfg, log, runtime) {
18516
18722
  const summary = matched.join(", ");
18517
18723
  const when = (/* @__PURE__ */ new Date()).toISOString();
18518
18724
  if (cfg.mode === "block") {
18519
- state60.blockCount += 1;
18520
- state60.lastBlock = { toolName, matchedTypes: matched, when };
18725
+ state59.blockCount += 1;
18726
+ state59.lastBlock = { toolName, matchedTypes: matched, when };
18521
18727
  log.warn(`[secret-scanner] blocked ${toolName} \u2014 matched: ${summary}`);
18522
18728
  return {
18523
18729
  decision: "block",
@@ -18527,7 +18733,7 @@ function buildHook(cfg, log, runtime) {
18527
18733
  if (cfg.mode === "redact") {
18528
18734
  const redacted = redactInput(input.toolInput);
18529
18735
  if (redacted.ok && redacted.value !== null && typeof redacted.value === "object" && !Array.isArray(redacted.value)) {
18530
- state60.redactCount += 1;
18736
+ state59.redactCount += 1;
18531
18737
  log.info(`[secret-scanner] redacted ${toolName} \u2014 matched: ${summary}`);
18532
18738
  return {
18533
18739
  decision: "allow",
@@ -18535,15 +18741,15 @@ function buildHook(cfg, log, runtime) {
18535
18741
  additionalContext: `secret-scanner: redacted ${matched.length} credential pattern(s) from the ${toolName} arguments before execution.`
18536
18742
  };
18537
18743
  }
18538
- state60.blockCount += 1;
18539
- state60.lastBlock = { toolName, matchedTypes: matched, when };
18744
+ state59.blockCount += 1;
18745
+ state59.lastBlock = { toolName, matchedTypes: matched, when };
18540
18746
  const detail = !redacted.ok ? redacted.reason === "oversized_input" ? "an input field exceeds the safe scan limit" : "the input exceeds the safe nesting depth" : "the input has a non-object shape";
18541
18747
  return {
18542
18748
  decision: "block",
18543
18749
  reason: `secret-scanner: cannot safely redact '${toolName}' because ${detail}; refusing to run.`
18544
18750
  };
18545
18751
  }
18546
- state60.allowCount += 1;
18752
+ state59.allowCount += 1;
18547
18753
  log.warn(
18548
18754
  `[secret-scanner] allow-mode: ${toolName} matched ${summary} but mode='allow' lets it through.`
18549
18755
  );
@@ -18553,7 +18759,7 @@ function buildHook(cfg, log, runtime) {
18553
18759
  function buildPostHook(cfg, log, runtime) {
18554
18760
  return (input) => {
18555
18761
  activateRuntime(runtime);
18556
- const { state: state60 } = runtime;
18762
+ const { state: state59 } = runtime;
18557
18763
  if (!cfg.enabled) return;
18558
18764
  const result = input.toolResult;
18559
18765
  if (!result || typeof result.content !== "string") return;
@@ -18573,8 +18779,8 @@ function buildPostHook(cfg, log, runtime) {
18573
18779
  }
18574
18780
  const summary = credentialMatches.join(", ");
18575
18781
  const when = (/* @__PURE__ */ new Date()).toISOString();
18576
- state60.leakCount += 1;
18577
- state60.lastLeak = { toolName, matchedTypes: credentialMatches, when };
18782
+ state59.leakCount += 1;
18783
+ state59.lastLeak = { toolName, matchedTypes: credentialMatches, when };
18578
18784
  log.warn(`[secret-scanner] POST-TOOL LEAK: ${toolName} output matched ${summary}`);
18579
18785
  return {
18580
18786
  additionalContext: `
@@ -18652,13 +18858,13 @@ var plugin47 = {
18652
18858
  };
18653
18859
  runtimes.set(api, runtime);
18654
18860
  latestRuntime = runtime;
18655
- const { state: state60 } = runtime;
18861
+ const { state: state59 } = runtime;
18656
18862
  const log = {
18657
18863
  warn: (msg, ...rest) => api.log.warn(msg, ...rest),
18658
18864
  info: (msg, ...rest) => api.log.info(msg, ...rest)
18659
18865
  };
18660
18866
  const hook = buildHook(cfg, log, runtime);
18661
- state60.hookUnregister = api.registerHook("PreToolUse", cfg.matcher, hook, {
18867
+ state59.hookUnregister = api.registerHook("PreToolUse", cfg.matcher, hook, {
18662
18868
  name: "secret-scanner",
18663
18869
  // Redaction rewrites arguments; block/allow modes must inspect the final
18664
18870
  // result after every mutator has run so a later rewrite cannot smuggle a
@@ -18668,7 +18874,7 @@ var plugin47 = {
18668
18874
  policy: true
18669
18875
  });
18670
18876
  const postHook = buildPostHook(cfg, log, runtime);
18671
- state60.postHookUnregister = api.registerHook("PostToolUse", cfg.postToolUseMatcher, postHook);
18877
+ state59.postHookUnregister = api.registerHook("PostToolUse", cfg.postToolUseMatcher, postHook);
18672
18878
  api.tools.register({
18673
18879
  name: "secret_scanner_status",
18674
18880
  description: "Reports the current secret-scanner state: pattern count, last block (if any), and per-mode invocation counters.",
@@ -18686,14 +18892,14 @@ var plugin47 = {
18686
18892
  patternCount: PATTERNS3.length,
18687
18893
  patternTypes: PATTERNS3.map((p) => p.type),
18688
18894
  counters: {
18689
- block: state60.blockCount,
18690
- redact: state60.redactCount,
18691
- allow: state60.allowCount,
18692
- leak: state60.leakCount,
18693
- timeoutCount: state60.timeoutCount
18895
+ block: state59.blockCount,
18896
+ redact: state59.redactCount,
18897
+ allow: state59.allowCount,
18898
+ leak: state59.leakCount,
18899
+ timeoutCount: state59.timeoutCount
18694
18900
  },
18695
- lastBlock: state60.lastBlock,
18696
- lastLeak: state60.lastLeak
18901
+ lastBlock: state59.lastBlock,
18902
+ lastLeak: state59.lastLeak
18697
18903
  };
18698
18904
  }
18699
18905
  });
@@ -18711,9 +18917,14 @@ var plugin47 = {
18711
18917
  mutating: false,
18712
18918
  async execute(input) {
18713
18919
  activateRuntime(runtime);
18714
- const rawText = input["text"] ?? input["content"] ?? input["string"] ?? input["input"] ?? input["code"] ?? input["value"] ?? "";
18715
- const text = typeof rawText === "string" ? rawText : "";
18716
- const matched = findMatches(text);
18920
+ const rawText = input["text"] ?? input["content"] ?? input["string"] ?? input["input"] ?? input["code"] ?? input["value"];
18921
+ if (typeof rawText !== "string") {
18922
+ throw new ToolValidationError22({
18923
+ message: "text is required and must be a string",
18924
+ field: "text"
18925
+ });
18926
+ }
18927
+ const matched = findMatches(rawText);
18717
18928
  return {
18718
18929
  ok: true,
18719
18930
  matched,
@@ -18731,57 +18942,58 @@ var plugin47 = {
18731
18942
  teardown(api) {
18732
18943
  const runtime = runtimes.get(api);
18733
18944
  if (!runtime) return;
18734
- const { state: state60 } = runtime;
18735
- if (state60.hookUnregister) {
18945
+ const { state: state59 } = runtime;
18946
+ if (state59.hookUnregister) {
18736
18947
  try {
18737
- state60.hookUnregister();
18948
+ state59.hookUnregister();
18738
18949
  } catch {
18739
18950
  }
18740
- state60.hookUnregister = null;
18951
+ state59.hookUnregister = null;
18741
18952
  }
18742
- if (state60.postHookUnregister) {
18953
+ if (state59.postHookUnregister) {
18743
18954
  try {
18744
- state60.postHookUnregister();
18955
+ state59.postHookUnregister();
18745
18956
  } catch {
18746
18957
  }
18747
- state60.postHookUnregister = null;
18958
+ state59.postHookUnregister = null;
18748
18959
  }
18749
18960
  const finalCounters = {
18750
- block: state60.blockCount,
18751
- redact: state60.redactCount,
18752
- allow: state60.allowCount,
18753
- leak: state60.leakCount
18961
+ block: state59.blockCount,
18962
+ redact: state59.redactCount,
18963
+ allow: state59.allowCount,
18964
+ leak: state59.leakCount
18754
18965
  };
18755
- state60.blockCount = 0;
18756
- state60.redactCount = 0;
18757
- state60.allowCount = 0;
18758
- state60.leakCount = 0;
18759
- state60.lastBlock = null;
18760
- state60.lastLeak = null;
18966
+ state59.blockCount = 0;
18967
+ state59.redactCount = 0;
18968
+ state59.allowCount = 0;
18969
+ state59.leakCount = 0;
18970
+ state59.lastBlock = null;
18971
+ state59.lastLeak = null;
18761
18972
  runtimes.delete(api);
18762
18973
  api.log.info("secret-scanner: teardown complete", { counters: finalCounters });
18763
18974
  },
18764
18975
  async health() {
18765
- const state60 = latestRuntime?.state ?? createState2();
18976
+ const state59 = latestRuntime?.state ?? createState2();
18766
18977
  return {
18767
18978
  ok: true,
18768
- message: state60.lastLeak !== null ? `secret-scanner: last leak at ${state60.lastLeak.when} on ${state60.lastLeak.toolName} (${state60.lastLeak.matchedTypes.join(", ")})` : state60.lastBlock !== null ? `secret-scanner: last block at ${state60.lastBlock.when} on ${state60.lastBlock.toolName} (${state60.lastBlock.matchedTypes.join(", ")})` : `secret-scanner: ${state60.blockCount + state60.redactCount + state60.allowCount + state60.leakCount} invocations, no blocks or leaks`,
18979
+ message: state59.lastLeak !== null ? `secret-scanner: last leak at ${state59.lastLeak.when} on ${state59.lastLeak.toolName} (${state59.lastLeak.matchedTypes.join(", ")})` : state59.lastBlock !== null ? `secret-scanner: last block at ${state59.lastBlock.when} on ${state59.lastBlock.toolName} (${state59.lastBlock.matchedTypes.join(", ")})` : `secret-scanner: ${state59.blockCount + state59.redactCount + state59.allowCount + state59.leakCount} invocations, no blocks or leaks`,
18769
18980
  counters: {
18770
- block: state60.blockCount,
18771
- redact: state60.redactCount,
18772
- allow: state60.allowCount,
18773
- leak: state60.leakCount
18981
+ block: state59.blockCount,
18982
+ redact: state59.redactCount,
18983
+ allow: state59.allowCount,
18984
+ leak: state59.leakCount
18774
18985
  },
18775
- lastBlock: state60.lastBlock,
18776
- lastLeak: state60.lastLeak
18986
+ lastBlock: state59.lastBlock,
18987
+ lastLeak: state59.lastLeak
18777
18988
  };
18778
18989
  }
18779
18990
  };
18780
18991
  var secret_scanner_default = plugin47;
18781
18992
 
18782
18993
  // src/security-hotspot-scanner/index.ts
18783
- import { readdir as readdir2, readFile as readFile13, stat as stat5 } from "node:fs/promises";
18994
+ import { readdir as readdir2, readFile as readFile13, stat as stat10 } from "node:fs/promises";
18784
18995
  import { extname as extname5, isAbsolute as isAbsolute19, relative as relative19, resolve as resolve21 } from "node:path";
18996
+ import { ToolValidationError as ToolValidationError23 } from "@wrongstack/core/types";
18785
18997
  var API_VERSION31 = "^0.1.10";
18786
18998
  var state44 = {
18787
18999
  scanCount: 0,
@@ -18929,7 +19141,7 @@ async function scanPath5(inputPath, cfg) {
18929
19141
  if (allFindings.length >= cfg.maxFindings) return;
18930
19142
  const full = resolve21(dir, entry);
18931
19143
  try {
18932
- const st = await stat5(full);
19144
+ const st = await stat10(full);
18933
19145
  if (st.isDirectory()) {
18934
19146
  if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "coverage") {
18935
19147
  continue;
@@ -18943,7 +19155,7 @@ async function scanPath5(inputPath, cfg) {
18943
19155
  }
18944
19156
  };
18945
19157
  try {
18946
- const st = await stat5(resolved);
19158
+ const st = await stat10(resolved);
18947
19159
  if (st.isDirectory()) {
18948
19160
  await walk(resolved);
18949
19161
  } else if (st.isFile()) {
@@ -19095,7 +19307,7 @@ Review or remove the risky pattern(s).`;
19095
19307
  category: "Security",
19096
19308
  mutating: false,
19097
19309
  async execute(input) {
19098
- if (!cfg.enabled) return { ok: false, error: "security-hotspot-scanner is disabled" };
19310
+ if (!cfg.enabled) throw new Error("security-hotspot-scanner is disabled");
19099
19311
  const raw = input;
19100
19312
  const targetPath = (typeof input.path === "string" ? input.path : void 0) ?? (typeof raw["directory"] === "string" ? raw["directory"] : void 0) ?? (typeof raw["dir"] === "string" ? raw["dir"] : void 0) ?? (typeof raw["SearchDirectory"] === "string" ? raw["SearchDirectory"] : void 0) ?? (typeof raw["TargetFile"] === "string" ? raw["TargetFile"] : void 0) ?? (typeof raw["filePath"] === "string" ? raw["filePath"] : void 0) ?? (typeof raw["targetFile"] === "string" ? raw["targetFile"] : void 0) ?? ".";
19101
19313
  const result = await scanPath5(targetPath, cfg);
@@ -19103,7 +19315,10 @@ Review or remove the risky pattern(s).`;
19103
19315
  state44.fileScanCount += result.filesScanned;
19104
19316
  state44.findingCount += result.findings.length;
19105
19317
  if (!result.scanned) {
19106
- return { ok: false, error: result.error, path: targetPath };
19318
+ throw new ToolValidationError23({
19319
+ message: `cannot scan ${targetPath}: ${result.error ?? "unknown error"}`,
19320
+ field: "path"
19321
+ });
19107
19322
  }
19108
19323
  state44.lastResult = {
19109
19324
  path: result.path,
@@ -19201,6 +19416,7 @@ var security_hotspot_scanner_default = plugin48;
19201
19416
  // src/semantic-search-indexer/index.ts
19202
19417
  import * as fs2 from "node:fs/promises";
19203
19418
  import { isAbsolute as isAbsolute20, relative as relative20, resolve as resolve22 } from "node:path";
19419
+ import { ToolValidationError as ToolValidationError24 } from "@wrongstack/core/types";
19204
19420
  import { DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
19205
19421
  var API_VERSION32 = "^0.1.10";
19206
19422
  var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -19710,16 +19926,31 @@ var plugin49 = {
19710
19926
  icon: "search",
19711
19927
  async execute(input) {
19712
19928
  if (!cfg.enabled) {
19713
- return { ok: false, error: "semantic-search-indexer is disabled" };
19929
+ throw new Error("semantic-search-indexer is disabled");
19714
19930
  }
19715
19931
  const rawPath = input.path ?? input["directory"] ?? input["dir"] ?? input["SearchDirectory"] ?? input["SearchPath"] ?? input["TargetFile"] ?? input["targetFile"] ?? input["filePath"] ?? input["file"];
19716
19932
  const resolved = resolveProjectPath8(typeof rawPath === "string" ? rawPath : void 0);
19717
19933
  if (!resolved) {
19718
- return { ok: false, error: "path outside project root" };
19934
+ throw new ToolValidationError24({ message: "path outside project root", field: "path" });
19719
19935
  }
19720
- await ensureIndex(resolved, cfg);
19721
19936
  const rawQuery = input.query ?? input["q"] ?? input["text"] ?? input["keyword"] ?? input["keywords"] ?? input["search"] ?? "";
19722
19937
  const query = String(rawQuery);
19938
+ if (tokenize(query, cfg.minTokenLength).length === 0) {
19939
+ throw new ToolValidationError24({
19940
+ message: `query must contain at least one keyword of ${cfg.minTokenLength}+ characters`,
19941
+ field: "query"
19942
+ });
19943
+ }
19944
+ try {
19945
+ await fs2.stat(resolved);
19946
+ } catch (err) {
19947
+ throw new ToolValidationError24({
19948
+ message: `path does not exist or cannot be read: ${typeof rawPath === "string" ? rawPath : resolved}`,
19949
+ field: "path",
19950
+ cause: err
19951
+ });
19952
+ }
19953
+ await ensureIndex(resolved, cfg);
19723
19954
  const limit = typeof input.limit === "number" && input.limit >= 1 ? Math.floor(input.limit) : cfg.defaultLimit;
19724
19955
  const results = runQuery(query, limit, cfg);
19725
19956
  const queryTokens = [...new Set(tokenize(query, cfg.minTokenLength))];
@@ -19843,10 +20074,26 @@ var semantic_search_indexer_default = plugin49;
19843
20074
  // src/semver-bump/index.ts
19844
20075
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
19845
20076
  import { toErrorMessage } from "@wrongstack/core/utils";
20077
+ import { ToolValidationError as ToolValidationError25 } from "@wrongstack/core/types";
19846
20078
  import { execFile as execFile11 } from "node:child_process";
19847
20079
  import { access as access4, readFile as readFile15, readdir as readdir4, writeFile as writeFile5 } from "node:fs/promises";
19848
20080
  import { isAbsolute as isAbsolute21, join as join5, relative as relative21, resolve as resolve23 } from "node:path";
19849
20081
  var API_VERSION33 = "^0.1.10";
20082
+ function requireProjectRoot(rawCwd) {
20083
+ const safeCwd = resolveProjectRoot(rawCwd);
20084
+ if (!safeCwd) {
20085
+ throw new ToolValidationError25({
20086
+ message: "cwd must stay within the current project directory",
20087
+ field: "cwd"
20088
+ });
20089
+ }
20090
+ return safeCwd;
20091
+ }
20092
+ function requireGitRef(field, ref) {
20093
+ if (ref !== void 0 && (typeof ref !== "string" || ref.startsWith("-"))) {
20094
+ throw new ToolValidationError25({ message: `${field} is not a valid git ref`, field });
20095
+ }
20096
+ }
19850
20097
  function resolveProjectRoot(rawCwd, root = process.cwd()) {
19851
20098
  if (typeof rawCwd !== "string" || rawCwd.length === 0) return root;
19852
20099
  const base = resolve23(root);
@@ -20092,14 +20339,10 @@ var plugin50 = {
20092
20339
  defaultPart = readDefaultPart(next);
20093
20340
  });
20094
20341
  async function performBump(part, dryRun, cwd) {
20095
- const safeCwd = resolveProjectRoot(cwd);
20096
- if (!safeCwd) {
20097
- return { ok: false, error: "cwd must stay within the current project directory" };
20098
- }
20099
- cwd = safeCwd;
20342
+ cwd = requireProjectRoot(cwd);
20100
20343
  const pkg = await getPackageJson(cwd);
20101
20344
  if (!pkg) {
20102
- return { ok: false, error: "No package.json found" };
20345
+ throw new Error("No package.json found");
20103
20346
  }
20104
20347
  const currentVersion = pkg.version;
20105
20348
  let bumpPart = part;
@@ -20114,8 +20357,7 @@ var plugin50 = {
20114
20357
  try {
20115
20358
  commits = await getRecentCommits(lastTag, cwd);
20116
20359
  } catch (err) {
20117
- const msg = toErrorMessage(err);
20118
- return { ok: false, error: `Git error: ${msg}`, bumpPart: "patch" };
20360
+ throw new Error(`Git error: ${toErrorMessage(err)}`, { cause: err });
20119
20361
  }
20120
20362
  bumpPart = determineBump(commits);
20121
20363
  } else {
@@ -20147,8 +20389,7 @@ var plugin50 = {
20147
20389
  try {
20148
20390
  await runCommand3(process.execPath, [bumpScript, "set", newVersion], root);
20149
20391
  } catch (err) {
20150
- const msg = toErrorMessage(err);
20151
- return { ok: false, error: `bump script failed: ${msg}` };
20392
+ throw new Error(`bump script failed: ${toErrorMessage(err)}`, { cause: err });
20152
20393
  }
20153
20394
  for (const rel of ["package.json", "package-lock.json", "src/lib/utils.ts", "index.html"]) {
20154
20395
  const p = join5(root, "website", rel);
@@ -20165,16 +20406,15 @@ var plugin50 = {
20165
20406
  try {
20166
20407
  pkgData = JSON.parse(await readFile15(manifest, "utf-8"));
20167
20408
  } catch (err) {
20168
- return {
20169
- ok: false,
20170
- error: `cannot bump: ${manifest} is not readable as JSON (${toErrorMessage(err)}). No manifests were modified.`
20171
- };
20409
+ throw new Error(
20410
+ `cannot bump: ${manifest} is not readable as JSON (${toErrorMessage(err)}). No manifests were modified.`,
20411
+ { cause: err }
20412
+ );
20172
20413
  }
20173
20414
  if (!pkgData || typeof pkgData !== "object") {
20174
- return {
20175
- ok: false,
20176
- error: `cannot bump: ${manifest} does not contain a JSON object. No manifests were modified.`
20177
- };
20415
+ throw new Error(
20416
+ `cannot bump: ${manifest} does not contain a JSON object. No manifests were modified.`
20417
+ );
20178
20418
  }
20179
20419
  pkgData.version = newVersion;
20180
20420
  pending.push({ path: manifest, contents: `${JSON.stringify(pkgData, null, 2)}
@@ -20184,16 +20424,20 @@ var plugin50 = {
20184
20424
  await writeFile5(path, contents, "utf-8");
20185
20425
  }
20186
20426
  }
20427
+ let commitError;
20187
20428
  try {
20188
20429
  await runGit6(["add", "--", ...changed], cwd);
20189
20430
  await runGit6(["commit", "-m", `chore: bump version to ${newVersion}`], cwd);
20190
- } catch {
20431
+ } catch (err) {
20432
+ commitError = toErrorMessage(err);
20191
20433
  }
20434
+ let tagError;
20192
20435
  if (autoTag) {
20193
20436
  try {
20194
20437
  const msg = tagMessage.replace("{{version}}", newVersion);
20195
20438
  await runGit6(["tag", "-a", `${tagPrefix}${newVersion}`, "-m", msg], cwd);
20196
- } catch {
20439
+ } catch (err) {
20440
+ tagError = toErrorMessage(err);
20197
20441
  }
20198
20442
  }
20199
20443
  api.log.info("semver-bump: bumped", { from: currentVersion, to: newVersion, bump: bumpPart });
@@ -20213,13 +20457,22 @@ var plugin50 = {
20213
20457
  commitCount: commits.length,
20214
20458
  breakingCount: commits.filter((c) => c.breaking).length
20215
20459
  };
20460
+ const tagged = autoTag && tagError === void 0;
20461
+ const warnings = [
20462
+ ...commitError ? [`commit failed: ${commitError}`] : [],
20463
+ ...tagError ? [`tag failed: ${tagError}`] : []
20464
+ ];
20216
20465
  return {
20217
20466
  ok: true,
20218
20467
  currentVersion,
20219
20468
  newVersion,
20220
20469
  bump: bumpPart,
20221
- tag: `${tagPrefix}${newVersion}`,
20222
- message: `Bumped ${currentVersion} \u2192 ${newVersion} (${bumpPart})`
20470
+ // Only name the tag when it was actually created.
20471
+ tag: tagged ? `${tagPrefix}${newVersion}` : null,
20472
+ committed: commitError === void 0,
20473
+ tagged,
20474
+ ...warnings.length > 0 ? { warnings } : {},
20475
+ message: `Bumped ${currentVersion} \u2192 ${newVersion} (${bumpPart})` + (warnings.length > 0 ? ` \u2014 ${warnings.join("; ")}` : "")
20223
20476
  };
20224
20477
  }
20225
20478
  api.tools.register({
@@ -20305,9 +20558,13 @@ var plugin50 = {
20305
20558
  if (!safeCwd) {
20306
20559
  return { message: "cwd must stay within the current project directory" };
20307
20560
  }
20308
- const result = await performBump(mode, dry, safeCwd);
20309
- return { message: String(result["message"] ?? result["error"] ?? JSON.stringify(result)) };
20310
- }
20561
+ try {
20562
+ const result = await performBump(mode, dry, safeCwd);
20563
+ return { message: String(result["message"] ?? JSON.stringify(result)) };
20564
+ } catch (err) {
20565
+ return { message: toErrorMessage(err) };
20566
+ }
20567
+ }
20311
20568
  });
20312
20569
  api.tools.register({
20313
20570
  name: "semver_current",
@@ -20323,11 +20580,7 @@ var plugin50 = {
20323
20580
  async execute(input) {
20324
20581
  state46.invocationCount += 1;
20325
20582
  state46.perTool["semver_current"] = (state46.perTool["semver_current"] ?? 0) + 1;
20326
- const cwdInput = input["cwd"];
20327
- const safeCwd = resolveProjectRoot(cwdInput);
20328
- if (!safeCwd) {
20329
- return { ok: false, error: "cwd must stay within the current project directory" };
20330
- }
20583
+ const safeCwd = requireProjectRoot(input["cwd"]);
20331
20584
  const pkg = await getPackageJson(safeCwd);
20332
20585
  const currentVersion = pkg?.version ?? "unknown";
20333
20586
  let latestTag = null;
@@ -20373,22 +20626,20 @@ var plugin50 = {
20373
20626
  state46.perTool["semver_changelog"] = (state46.perTool["semver_changelog"] ?? 0) + 1;
20374
20627
  const from = input["from"];
20375
20628
  const to = input["to"] ?? "HEAD";
20376
- const cwd = input["cwd"];
20377
- const safeCwd = resolveProjectRoot(cwd);
20378
- if (!safeCwd) {
20379
- return { ok: false, error: "cwd must stay within the current project directory" };
20380
- }
20629
+ requireGitRef("from", from);
20630
+ requireGitRef("to", to);
20631
+ const safeCwd = requireProjectRoot(input["cwd"]);
20381
20632
  const format = input["format"] ?? "markdown";
20382
- const range = from ? `${from}..${to}` : to;
20633
+ const rangeArgs = from ? [`${from}..${to}`] : ["-30", to];
20383
20634
  let commits;
20384
20635
  try {
20385
20636
  const output = await runGit6(
20386
- ["log", range === to ? "-30" : range, "--format=%H%x1f%s%x1f%b%x1e"],
20637
+ ["log", ...rangeArgs, "--format=%H%x1f%s%x1f%b%x1e"],
20387
20638
  safeCwd
20388
20639
  );
20389
20640
  commits = parseGitLogOutput(output);
20390
20641
  } catch (err) {
20391
- return { ok: false, error: `Failed to get git log: ${err}` };
20642
+ throw new Error(`Failed to get git log: ${toErrorMessage(err)}`, { cause: err });
20392
20643
  }
20393
20644
  if (format === "json") {
20394
20645
  return {
@@ -20656,8 +20907,10 @@ var plugin51 = {
20656
20907
  const offTool = api.onPattern("tool.*", (eventName, payload) => {
20657
20908
  touchActivity();
20658
20909
  const p = payload;
20659
- const toolName = p?.tool ?? p?.name ?? eventName;
20660
- if (typeof toolName === "string") bumpToolCount(toolName);
20910
+ const rawTool = p?.tool;
20911
+ const nameOf = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.name === "string" ? v.name : void 0;
20912
+ const toolName = nameOf(rawTool) ?? nameOf(p?.name) ?? eventName;
20913
+ bumpToolCount(toolName);
20661
20914
  if (toolName === "git_autocommit" || toolName.startsWith("git ")) {
20662
20915
  }
20663
20916
  });
@@ -20903,6 +21156,7 @@ Tokens: ${recap.tokens.total.input} in / ${recap.tokens.total.output} out
20903
21156
  var session_recap_default = plugin51;
20904
21157
 
20905
21158
  // src/shell-check/index.ts
21159
+ import { ToolValidationError as ToolValidationError26 } from "@wrongstack/core/types";
20906
21160
  import { execFile as execFile12 } from "node:child_process";
20907
21161
  import { readdir as readdir5 } from "node:fs/promises";
20908
21162
  import { isAbsolute as isAbsolute22, join as join6, relative as relative22, resolve as resolve24 } from "node:path";
@@ -20976,8 +21230,11 @@ async function runShellCheck(files, severity, cwd) {
20976
21230
  }
20977
21231
  );
20978
21232
  });
20979
- } catch {
20980
- return [];
21233
+ } catch (err) {
21234
+ throw new Error(
21235
+ `shellcheck failed without output: ${err instanceof Error ? err.message : String(err)}`,
21236
+ { cause: err }
21237
+ );
20981
21238
  }
20982
21239
  if (!raw.trim()) return [];
20983
21240
  try {
@@ -20990,22 +21247,31 @@ async function runShellCheck(files, severity, cwd) {
20990
21247
  code: item.code,
20991
21248
  message: item.message
20992
21249
  }));
20993
- } catch {
20994
- return [];
21250
+ } catch (err) {
21251
+ throw new Error(`shellcheck returned unparseable output: ${raw.trim().slice(0, 500)}`, {
21252
+ cause: err
21253
+ });
20995
21254
  }
20996
21255
  }
20997
- async function findShellFiles(dir, pattern) {
21256
+ async function findShellFiles(dir, pattern, isRoot = true) {
20998
21257
  const results = [];
20999
21258
  let entries;
21000
21259
  try {
21001
21260
  entries = await readdir5(dir, { withFileTypes: true });
21002
- } catch {
21261
+ } catch (err) {
21262
+ if (isRoot) {
21263
+ throw new ToolValidationError26({
21264
+ message: `directory does not exist or cannot be read: ${dir}`,
21265
+ field: "directory",
21266
+ cause: err
21267
+ });
21268
+ }
21003
21269
  return results;
21004
21270
  }
21005
21271
  for (const entry of entries) {
21006
21272
  const full = join6(dir, entry.name);
21007
21273
  if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
21008
- results.push(...await findShellFiles(full, pattern));
21274
+ results.push(...await findShellFiles(full, pattern, false));
21009
21275
  } else if (entry.isFile() && (entry.name.endsWith(".sh") || entry.name.endsWith(".bash") || entry.name.endsWith(".zsh") || entry.name === "Dockerfile" || entry.name === ".bashrc" || entry.name === ".zshrc")) {
21010
21276
  if (!pattern || entry.name.includes(pattern)) {
21011
21277
  results.push(full);
@@ -21075,12 +21341,8 @@ var plugin52 = {
21075
21341
  enum: ["error", "warning", "info", "style"],
21076
21342
  default: "warning",
21077
21343
  description: "Minimum severity level to report"
21078
- },
21079
- fix: {
21080
- type: "boolean",
21081
- default: false,
21082
- description: "Apply safe automatic fixes where possible"
21083
21344
  }
21345
+ // `fix` was declared ("apply safe automatic fixes") but never implemented.
21084
21346
  }
21085
21347
  },
21086
21348
  permission: "auto",
@@ -21106,22 +21368,16 @@ var plugin52 = {
21106
21368
  state48.invocationCount += 1;
21107
21369
  const pathIsSafe = (p) => typeof p === "string" && p.length > 0 && p.length <= MAX_PATH_LEN && withinProject22(p);
21108
21370
  if (!pathIsSafe(directory)) {
21109
- return {
21110
- ok: false,
21111
- error: `directory path is outside the project root: ${directory}`,
21112
- issues: [],
21113
- filesScanned: 0,
21114
- rejectedOutsideProject: true
21115
- };
21371
+ throw new ToolValidationError26({
21372
+ message: `directory path is outside the project root: ${directory}`,
21373
+ field: "directory"
21374
+ });
21116
21375
  }
21117
21376
  if (files?.some((f) => !pathIsSafe(f))) {
21118
- return {
21119
- ok: false,
21120
- error: "one or more file paths are outside the project root",
21121
- issues: [],
21122
- filesScanned: 0,
21123
- rejectedOutsideProject: true
21124
- };
21377
+ throw new ToolValidationError26({
21378
+ message: "one or more file paths are outside the project root",
21379
+ field: "files"
21380
+ });
21125
21381
  }
21126
21382
  let checkFiles;
21127
21383
  let scannedDirectories = false;
@@ -21147,19 +21403,7 @@ var plugin52 = {
21147
21403
  mode: scannedDirectories ? "directory" : "files"
21148
21404
  };
21149
21405
  }
21150
- let issues;
21151
- try {
21152
- issues = await runShellCheck(checkFiles, severity);
21153
- } catch (err) {
21154
- const msg = err instanceof Error ? err.message : String(err);
21155
- return {
21156
- ok: false,
21157
- error: msg,
21158
- issues: [],
21159
- filesScanned: 0,
21160
- mode: scannedDirectories ? "directory" : "files"
21161
- };
21162
- }
21406
+ const issues = await runShellCheck(checkFiles, severity);
21163
21407
  const byFile = {};
21164
21408
  for (const issue of issues) {
21165
21409
  if (byFile[issue.file] === void 0) {
@@ -21227,6 +21471,7 @@ var shell_check_default = plugin52;
21227
21471
  // src/smart-rename/index.ts
21228
21472
  import { readFileSync as readFileSync13, writeFileSync as writeFileSync2 } from "node:fs";
21229
21473
  import { extname as extname6, isAbsolute as isAbsolute23, relative as relative23, resolve as resolve25 } from "node:path";
21474
+ import { ToolValidationError as ToolValidationError27 } from "@wrongstack/core/types";
21230
21475
  var NEW_API_VERSION = "^0.1.10";
21231
21476
  var state49 = {
21232
21477
  renameCount: 0,
@@ -21332,32 +21577,44 @@ var plugin53 = {
21332
21577
  mutating: true,
21333
21578
  capabilities: ["fs.write"],
21334
21579
  async execute(input) {
21335
- if (!cfg.enabled) return { ok: false, error: "smart-rename is disabled" };
21580
+ if (!cfg.enabled) throw new Error("smart-rename is disabled");
21336
21581
  const inp = input ?? {};
21337
21582
  const rawPath = inp["path"] ?? inp["filePath"] ?? inp["file_path"] ?? inp["TargetFile"] ?? inp["targetFile"] ?? inp["file"];
21338
21583
  const oldName = inp["oldName"] ?? inp["old_name"] ?? inp["from"];
21339
21584
  const newName = inp["newName"] ?? inp["new_name"] ?? inp["to"];
21340
21585
  if (!rawPath || typeof rawPath !== "string") {
21341
- return { ok: false, error: "path is required" };
21586
+ throw new ToolValidationError27({ message: "path is required", field: "path" });
21342
21587
  }
21343
21588
  if (!oldName || typeof oldName !== "string" || oldName.length === 0) {
21344
- return { ok: false, error: "oldName is required" };
21589
+ throw new ToolValidationError27({ message: "oldName is required", field: "oldName" });
21345
21590
  }
21346
21591
  if (!newName || typeof newName !== "string" || newName.length === 0) {
21347
- return { ok: false, error: "newName is required" };
21592
+ throw new ToolValidationError27({ message: "newName is required", field: "newName" });
21348
21593
  }
21349
21594
  if (!isIdentifier(oldName)) {
21350
- return { ok: false, error: `oldName "${oldName}" is not a valid identifier` };
21595
+ throw new ToolValidationError27({
21596
+ message: `oldName "${oldName}" is not a valid identifier`,
21597
+ field: "oldName"
21598
+ });
21351
21599
  }
21352
21600
  if (!isIdentifier(newName)) {
21353
- return { ok: false, error: `newName "${newName}" is not a valid identifier` };
21601
+ throw new ToolValidationError27({
21602
+ message: `newName "${newName}" is not a valid identifier`,
21603
+ field: "newName"
21604
+ });
21354
21605
  }
21355
21606
  if (!withinProject23(rawPath)) {
21356
- return { ok: false, error: "path is outside the project root" };
21607
+ throw new ToolValidationError27({
21608
+ message: "path is outside the project root",
21609
+ field: "path"
21610
+ });
21357
21611
  }
21358
21612
  const ext = extname6(rawPath).toLowerCase();
21359
21613
  if (!cfg.extensions.includes(ext)) {
21360
- return { ok: false, error: `extension ${ext} is not allowed for rename` };
21614
+ throw new ToolValidationError27({
21615
+ message: `extension ${ext} is not allowed for rename`,
21616
+ field: "path"
21617
+ });
21361
21618
  }
21362
21619
  const resolved = resolve25(process.cwd(), rawPath);
21363
21620
  let content;
@@ -21365,7 +21622,7 @@ var plugin53 = {
21365
21622
  content = readFileSync13(resolved, "utf-8");
21366
21623
  } catch (err) {
21367
21624
  state49.errorCount += 1;
21368
- return { ok: false, error: String(err) };
21625
+ throw new Error(`Could not read ${rawPath}: ${String(err)}`, { cause: err });
21369
21626
  }
21370
21627
  const { preview, replacements } = renameInContent(content, oldName, newName);
21371
21628
  state49.renameCount += 1;
@@ -21378,7 +21635,7 @@ var plugin53 = {
21378
21635
  writeFileSync2(resolved, preview, "utf-8");
21379
21636
  } catch (err) {
21380
21637
  state49.errorCount += 1;
21381
- return { ok: false, error: String(err) };
21638
+ throw new Error(`Could not write ${rawPath}: ${String(err)}`, { cause: err });
21382
21639
  }
21383
21640
  }
21384
21641
  return {
@@ -21811,8 +22068,8 @@ var plugin54 = {
21811
22068
  state50.postInvocations += 1;
21812
22069
  let content;
21813
22070
  try {
21814
- const stat8 = await fs3.stat(filePath);
21815
- if (!stat8.isFile()) return;
22071
+ const stat13 = await fs3.stat(filePath);
22072
+ if (!stat13.isFile()) return;
21816
22073
  content = await fs3.readFile(filePath, "utf-8");
21817
22074
  } catch {
21818
22075
  state50.readErrorCount += 1;
@@ -21962,6 +22219,7 @@ var spec_linker_default = plugin54;
21962
22219
  // src/template-engine/index.ts
21963
22220
  import { readFile as readFile17, writeFile as writeFile6 } from "node:fs/promises";
21964
22221
  import { isAbsolute as isAbsolute24 } from "node:path";
22222
+ import { ToolValidationError as ToolValidationError28 } from "@wrongstack/core/types";
21965
22223
  var API_VERSION35 = "^0.1.10";
21966
22224
  var templates = /* @__PURE__ */ new Map();
21967
22225
  var MAX_TEMPLATES = 256;
@@ -22123,24 +22381,26 @@ var plugin55 = {
22123
22381
  const output_path = typeof rawOutputPath === "string" && rawOutputPath.trim().length > 0 ? rawOutputPath.trim() : void 0;
22124
22382
  const raw = input["raw"] ?? false;
22125
22383
  if (!template || typeof template !== "string") {
22126
- return { ok: false, error: "template is required and must be a string" };
22384
+ throw new ToolValidationError28({
22385
+ message: "template is required and must be a string",
22386
+ field: "template"
22387
+ });
22127
22388
  }
22128
22389
  if (!variables || typeof variables !== "object") {
22129
- return { ok: false, error: "variables is required and must be an object" };
22130
- }
22131
- let result;
22132
- try {
22133
- result = raw ? renderTemplateRaw(template, variables) : renderTemplate(template, variables, autoEscapeHtml);
22134
- } catch (err) {
22135
- return { ok: false, error: String(err) };
22390
+ throw new ToolValidationError28({
22391
+ message: "variables is required and must be an object",
22392
+ field: "variables"
22393
+ });
22136
22394
  }
22395
+ const result = raw ? renderTemplateRaw(template, variables) : renderTemplate(template, variables, autoEscapeHtml);
22137
22396
  if (output_path) {
22138
22397
  const pathError = validateWritableTemplateTarget("output_path", output_path);
22139
- if (pathError) return { ok: false, error: pathError };
22398
+ if (pathError)
22399
+ throw new ToolValidationError28({ message: pathError, field: "output_path" });
22140
22400
  try {
22141
22401
  await writeFile6(output_path, result, "utf-8");
22142
22402
  } catch (err) {
22143
- return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
22403
+ throw new Error(`Could not write ${output_path}: ${String(err)}`, { cause: err });
22144
22404
  }
22145
22405
  return {
22146
22406
  ok: true,
@@ -22191,32 +22451,36 @@ var plugin55 = {
22191
22451
  const output_path = typeof rawOutputPath === "string" && rawOutputPath.trim().length > 0 ? rawOutputPath.trim() : void 0;
22192
22452
  const raw = input["raw"] ?? false;
22193
22453
  if (!template_path || typeof template_path !== "string") {
22194
- return { ok: false, error: "template_path is required and must be a string" };
22454
+ throw new ToolValidationError28({
22455
+ message: "template_path is required and must be a string",
22456
+ field: "template_path"
22457
+ });
22195
22458
  }
22196
22459
  const templatePathError = validateRelativeTemplatePath("template_path", template_path);
22197
- if (templatePathError) return { ok: false, error: templatePathError };
22460
+ if (templatePathError) {
22461
+ throw new ToolValidationError28({ message: templatePathError, field: "template_path" });
22462
+ }
22198
22463
  if (!variables || typeof variables !== "object") {
22199
- return { ok: false, error: "variables is required and must be an object" };
22464
+ throw new ToolValidationError28({
22465
+ message: "variables is required and must be an object",
22466
+ field: "variables"
22467
+ });
22200
22468
  }
22201
22469
  let content;
22202
22470
  try {
22203
22471
  content = await readFile17(template_path, "utf-8");
22204
22472
  } catch (err) {
22205
- return { ok: false, error: `Could not read template file: ${err}` };
22206
- }
22207
- let result;
22208
- try {
22209
- result = raw ? renderTemplateRaw(content, variables) : renderTemplate(content, variables, autoEscapeHtml);
22210
- } catch (err) {
22211
- return { ok: false, error: `Template rendering failed: ${err}` };
22473
+ throw new Error(`Could not read template file: ${String(err)}`, { cause: err });
22212
22474
  }
22475
+ const result = raw ? renderTemplateRaw(content, variables) : renderTemplate(content, variables, autoEscapeHtml);
22213
22476
  if (output_path) {
22214
22477
  const pathError = validateWritableTemplateTarget("output_path", output_path);
22215
- if (pathError) return { ok: false, error: pathError };
22478
+ if (pathError)
22479
+ throw new ToolValidationError28({ message: pathError, field: "output_path" });
22216
22480
  try {
22217
22481
  await writeFile6(output_path, result, "utf-8");
22218
22482
  } catch (err) {
22219
- return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
22483
+ throw new Error(`Could not write ${output_path}: ${String(err)}`, { cause: err });
22220
22484
  }
22221
22485
  return {
22222
22486
  ok: true,
@@ -22267,30 +22531,39 @@ var plugin55 = {
22267
22531
  const rawDesc = input["description"] ?? input["desc"] ?? input["summary"];
22268
22532
  const description = typeof rawDesc === "string" ? rawDesc : void 0;
22269
22533
  if (!name || typeof name !== "string" || name.trim() === "") {
22270
- return { ok: false, error: "name is required and must be a non-empty string" };
22534
+ throw new ToolValidationError28({
22535
+ message: "name is required and must be a non-empty string",
22536
+ field: "name"
22537
+ });
22271
22538
  }
22272
22539
  if (!content || typeof content !== "string") {
22273
- return { ok: false, error: "content is required and must be a string" };
22540
+ throw new ToolValidationError28({
22541
+ message: "content is required and must be a string",
22542
+ field: "content"
22543
+ });
22274
22544
  }
22275
22545
  if (name.length > MAX_TEMPLATE_NAME_CHARS) {
22276
- return { ok: false, error: `name exceeds ${MAX_TEMPLATE_NAME_CHARS} characters` };
22546
+ throw new ToolValidationError28({
22547
+ message: `name exceeds ${MAX_TEMPLATE_NAME_CHARS} characters`,
22548
+ field: "name"
22549
+ });
22277
22550
  }
22278
22551
  if (content.length > MAX_TEMPLATE_CONTENT_CHARS) {
22279
- return {
22280
- ok: false,
22281
- error: `content exceeds ${MAX_TEMPLATE_CONTENT_CHARS} characters`
22282
- };
22552
+ throw new ToolValidationError28({
22553
+ message: `content exceeds ${MAX_TEMPLATE_CONTENT_CHARS} characters`,
22554
+ field: "content"
22555
+ });
22283
22556
  }
22284
22557
  if (description && description.length > MAX_TEMPLATE_DESCRIPTION_CHARS) {
22285
- return {
22286
- ok: false,
22287
- error: `description exceeds ${MAX_TEMPLATE_DESCRIPTION_CHARS} characters`
22288
- };
22558
+ throw new ToolValidationError28({
22559
+ message: `description exceeds ${MAX_TEMPLATE_DESCRIPTION_CHARS} characters`,
22560
+ field: "description"
22561
+ });
22289
22562
  }
22290
22563
  const now = (/* @__PURE__ */ new Date()).toISOString();
22291
22564
  const existing = templates.get(name);
22292
22565
  if (!existing && templates.size >= MAX_TEMPLATES) {
22293
- return { ok: false, error: `template limit reached (${MAX_TEMPLATES})` };
22566
+ throw new Error(`template limit reached (${MAX_TEMPLATES})`);
22294
22567
  }
22295
22568
  const tmpl = {
22296
22569
  name,
@@ -22303,10 +22576,7 @@ var plugin55 = {
22303
22576
  for (const stored of templates.values()) retainedChars += templateChars(stored);
22304
22577
  const nextChars = retainedChars - (existing ? templateChars(existing) : 0) + templateChars(tmpl);
22305
22578
  if (nextChars > MAX_TOTAL_TEMPLATE_CHARS) {
22306
- return {
22307
- ok: false,
22308
- error: `template store exceeds ${MAX_TOTAL_TEMPLATE_CHARS} retained characters`
22309
- };
22579
+ throw new Error(`template store exceeds ${MAX_TOTAL_TEMPLATE_CHARS} retained characters`);
22310
22580
  }
22311
22581
  templates.set(name, tmpl);
22312
22582
  api.metrics.gauge("template_count", templates.size);
@@ -22635,6 +22905,7 @@ import { execFile as execFile13 } from "node:child_process";
22635
22905
  import { readFileSync as readFileSync15 } from "node:fs";
22636
22906
  import { createRequire as createRequire2 } from "node:module";
22637
22907
  import { dirname as dirname8, isAbsolute as isAbsolute25, relative as relative24, resolve as resolve26 } from "node:path";
22908
+ import { ToolValidationError as ToolValidationError29 } from "@wrongstack/core/types";
22638
22909
  var API_VERSION37 = "^0.1.10";
22639
22910
  var state52 = {
22640
22911
  invocationCount: 0,
@@ -22885,7 +23156,7 @@ var plugin57 = {
22885
23156
  mutating: false,
22886
23157
  async execute(input) {
22887
23158
  if (!cfg.enabled) {
22888
- return { ok: false, error: "test-flake-detector is disabled" };
23159
+ throw new Error("test-flake-detector is disabled");
22889
23160
  }
22890
23161
  const raw = input;
22891
23162
  const rawPattern = input.testPattern ?? raw["pattern"] ?? raw["path"] ?? raw["file"] ?? raw["filePath"] ?? raw["TargetFile"] ?? raw["targetFile"];
@@ -22896,10 +23167,10 @@ var plugin57 = {
22896
23167
  const requestedRuns = typeof rawRuns === "number" && rawRuns >= 1 ? Math.min(Math.floor(rawRuns), cfg.maxRuns) : 5;
22897
23168
  const command = resolveTestCommand(commandString, testPattern);
22898
23169
  if (!command) {
22899
- return {
22900
- ok: false,
22901
- error: "Unsupported test command or unsafe testPattern. Use vitest, jest, or mocha through a supported package runner, and keep patterns inside the project."
22902
- };
23170
+ throw new ToolValidationError29({
23171
+ message: "Unsupported test command or unsafe testPattern. Use vitest, jest, or mocha through a supported package runner, and keep patterns inside the project.",
23172
+ field: "command"
23173
+ });
22903
23174
  }
22904
23175
  state52.invocationCount += 1;
22905
23176
  const start = Date.now();
@@ -22927,6 +23198,11 @@ var plugin57 = {
22927
23198
  }
22928
23199
  }
22929
23200
  const all = Array.from(records.values());
23201
+ if (all.length === 0 && runErrors.length === requestedRuns) {
23202
+ throw new Error(
23203
+ `test command produced no test results in ${requestedRuns} run(s): ${runErrors.slice(0, 3).join("; ")}`
23204
+ );
23205
+ }
22930
23206
  const flakyTests = all.filter((r) => r.passCount > 0 && r.failCount > 0);
22931
23207
  const alwaysFailing = all.filter((r) => r.passCount === 0 && r.failCount > 0);
22932
23208
  const alwaysPassing = all.filter((r) => r.passCount > 0 && r.failCount === 0);
@@ -23014,6 +23290,7 @@ var test_flake_detector_default = plugin57;
23014
23290
  // src/test-generator/index.ts
23015
23291
  import { readFileSync as readFileSync16 } from "node:fs";
23016
23292
  import { isAbsolute as isAbsolute26, relative as relative25, resolve as resolve27 } from "node:path";
23293
+ import { ToolValidationError as ToolValidationError30 } from "@wrongstack/core/types";
23017
23294
  var API_VERSION38 = "^0.1.10";
23018
23295
  var state53 = {
23019
23296
  generateCount: 0,
@@ -23302,7 +23579,7 @@ var plugin58 = {
23302
23579
  category: "Development",
23303
23580
  mutating: false,
23304
23581
  async execute(input, _ctx, execOpts) {
23305
- if (!cfg.enabled) return { ok: false, error: "test-generator is disabled" };
23582
+ if (!cfg.enabled) throw new Error("test-generator is disabled");
23306
23583
  execOpts?.signal?.throwIfAborted();
23307
23584
  const inp = input ?? {};
23308
23585
  const rawFramework = inp["framework"];
@@ -23313,16 +23590,19 @@ var plugin58 = {
23313
23590
  };
23314
23591
  const rawPath = inp["path"] ?? inp["filePath"] ?? inp["file_path"] ?? inp["TargetFile"] ?? inp["targetFile"] ?? inp["file"];
23315
23592
  if (!rawPath || typeof rawPath !== "string") {
23316
- return { ok: false, error: "path is required" };
23593
+ throw new ToolValidationError30({ message: "path is required", field: "path" });
23317
23594
  }
23318
23595
  if (!withinProject27(rawPath)) {
23319
- return { ok: false, error: "path is outside the project root" };
23596
+ throw new ToolValidationError30({
23597
+ message: "path is outside the project root",
23598
+ field: "path"
23599
+ });
23320
23600
  }
23321
23601
  if (!SOURCE_EXTENSIONS.some((ext) => rawPath.toLowerCase().endsWith(ext))) {
23322
- return {
23323
- ok: false,
23324
- error: `test generation only reads source files (${SOURCE_EXTENSIONS.join(", ")}); refusing "${rawPath}"`
23325
- };
23602
+ throw new ToolValidationError30({
23603
+ message: `test generation only reads source files (${SOURCE_EXTENSIONS.join(", ")}); refusing "${rawPath}"`,
23604
+ field: "path"
23605
+ });
23326
23606
  }
23327
23607
  const resolved = resolve27(process.cwd(), rawPath);
23328
23608
  state53.generateCount += 1;
@@ -23331,7 +23611,7 @@ var plugin58 = {
23331
23611
  result = generateForFile(resolved, effectiveCfg);
23332
23612
  } catch (err) {
23333
23613
  state53.errorCount += 1;
23334
- return { ok: false, error: String(err) };
23614
+ throw new Error(`Could not read ${rawPath}: ${String(err)}`, { cause: err });
23335
23615
  }
23336
23616
  state53.exportCount += result.exports.length;
23337
23617
  const raw = input ?? {};
@@ -24152,451 +24432,632 @@ var todo_listener_default = plugin60;
24152
24432
  // src/todo-tracker/index.ts
24153
24433
  import { randomUUID } from "node:crypto";
24154
24434
  import * as fsp from "node:fs/promises";
24155
- import { dirname as dirname10 } from "node:path";
24156
- import { atomicWrite as atomicWrite3, ensureDir as ensureDir3 } from "@wrongstack/core/utils";
24435
+ import { basename as basename7, dirname as dirname10, extname as extname8 } from "node:path";
24436
+ import { ToolValidationError as ToolValidationError31 } from "@wrongstack/core/types";
24437
+ import { atomicWrite as atomicWrite3, ensureDir as ensureDir3, withFileLock } from "@wrongstack/core/utils";
24157
24438
  import { nowIso } from "@wrongstack/primitives";
24439
+ var STATUSES = ["pending", "in_progress", "completed", "dropped"];
24440
+ var PRIORITIES = ["low", "normal", "high"];
24441
+ var defaultDeps = {
24442
+ readFile: (p) => fsp.readFile(p, "utf8"),
24443
+ rename: (from, to) => fsp.rename(from, to),
24444
+ atomicWrite: (p, content, opts) => atomicWrite3(p, content, opts),
24445
+ withFileLock: (p, fn) => withFileLock(p, fn),
24446
+ ensureDir: (dir) => ensureDir3(dir)
24447
+ };
24448
+ function deriveProjectSlug(filePath) {
24449
+ const base = basename7(filePath.replace(/[\\/]+$/, "").replace(/\\/g, "/"));
24450
+ const ext = extname8(base);
24451
+ const stem = ext && ext !== base ? base.slice(0, -ext.length) : base;
24452
+ return stem || "tracker";
24453
+ }
24158
24454
  function deriveFilePath(api) {
24159
24455
  const raw = api.config.extensions?.["todo-tracker"];
24160
24456
  const rawPath = raw?.["filePath"] ?? raw?.["file_path"] ?? raw?.["path"] ?? raw?.["file"] ?? raw?.["targetFile"];
24161
24457
  const explicit = typeof rawPath === "string" && rawPath.trim().length > 0 ? rawPath.trim() : null;
24162
24458
  if (explicit) {
24163
- const base = explicit.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "tracker";
24164
- return { filePath: explicit, projectSlug: base };
24459
+ return { filePath: explicit, projectSlug: deriveProjectSlug(explicit) };
24165
24460
  }
24166
24461
  return { filePath: null, projectSlug: null };
24167
24462
  }
24168
24463
  var FILE_VERSION = 1;
24169
- async function loadFile(filePath) {
24170
- let raw;
24464
+ var isStr = (v) => typeof v === "string";
24465
+ var isOptStr = (v) => v === void 0 || v === null || typeof v === "string";
24466
+ function validateItem(raw, index) {
24467
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
24468
+ return `items[${index}] is not an object`;
24469
+ }
24470
+ const it = raw;
24471
+ if (!isStr(it["id"]) || it["id"].length === 0) return `items[${index}].id is not a string`;
24472
+ if (!isStr(it["content"])) return `items[${index}].content is not a string`;
24473
+ if (!STATUSES.includes(it["status"])) {
24474
+ return `items[${index}].status is not one of ${STATUSES.join("|")}`;
24475
+ }
24476
+ if (!PRIORITIES.includes(it["priority"])) {
24477
+ return `items[${index}].priority is not one of ${PRIORITIES.join("|")}`;
24478
+ }
24479
+ if (!Array.isArray(it["tags"]) || !it["tags"].every(isStr)) {
24480
+ return `items[${index}].tags is not a string[]`;
24481
+ }
24482
+ if (!isStr(it["createdAt"]) || !isStr(it["updatedAt"])) {
24483
+ return `items[${index}] timestamps are not strings`;
24484
+ }
24485
+ if (!isOptStr(it["completedAt"]) || !isOptStr(it["sourceSessionId"]) || !isOptStr(it["notes"])) {
24486
+ return `items[${index}] optional fields are not string|null`;
24487
+ }
24488
+ return {
24489
+ id: it["id"],
24490
+ content: it["content"],
24491
+ status: it["status"],
24492
+ priority: it["priority"],
24493
+ tags: [...it["tags"]],
24494
+ createdAt: it["createdAt"],
24495
+ updatedAt: it["updatedAt"],
24496
+ completedAt: it["completedAt"] ?? null,
24497
+ sourceSessionId: it["sourceSessionId"] ?? null,
24498
+ notes: it["notes"] ?? null
24499
+ };
24500
+ }
24501
+ function parseTrackerFile(rawText) {
24502
+ const text = rawText.charCodeAt(0) === 65279 ? rawText.slice(1) : rawText;
24503
+ let parsed;
24171
24504
  try {
24172
- raw = await fsp.readFile(filePath, "utf8");
24505
+ parsed = JSON.parse(text);
24173
24506
  } catch (err) {
24174
- if (err.code === "ENOENT") return null;
24175
- throw err;
24507
+ return { kind: "corrupt", reason: `invalid JSON: ${err.message}` };
24176
24508
  }
24177
- try {
24178
- const parsed = JSON.parse(raw);
24179
- if (parsed.version !== FILE_VERSION || !Array.isArray(parsed.items)) {
24180
- return null;
24181
- }
24182
- return parsed;
24183
- } catch {
24184
- return null;
24509
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
24510
+ return { kind: "corrupt", reason: "top-level value is not an object" };
24185
24511
  }
24186
- }
24187
- async function saveFile(filePath, file) {
24188
- await ensureDir3(dirname10(filePath));
24189
- await atomicWrite3(filePath, JSON.stringify(file, null, 2), { mode: 384 });
24190
- }
24191
- var state56 = {
24192
- filePath: null,
24193
- projectSlug: null,
24194
- file: null,
24195
- addCount: 0,
24196
- completeCount: 0,
24197
- dropCount: 0,
24198
- removeCount: 0,
24199
- pullCount: 0,
24200
- /** Most recent mutation for /diag plugins visibility. */
24201
- lastMutation: null
24202
- };
24203
- function ensureFile() {
24204
- if (!state56.file) {
24205
- state56.file = {
24512
+ const obj = parsed;
24513
+ if (typeof obj["version"] !== "number") {
24514
+ return { kind: "corrupt", reason: "missing or non-numeric version" };
24515
+ }
24516
+ if (obj["version"] !== FILE_VERSION) {
24517
+ return { kind: "unsupportedVersion", version: obj["version"] };
24518
+ }
24519
+ if (!Array.isArray(obj["items"])) {
24520
+ return { kind: "invalidItems", reason: "items is not an array" };
24521
+ }
24522
+ const items = [];
24523
+ for (const [i, rawItem] of obj["items"].entries()) {
24524
+ const v = validateItem(rawItem, i);
24525
+ if (typeof v === "string") return { kind: "invalidItems", reason: v };
24526
+ items.push(v);
24527
+ }
24528
+ return {
24529
+ kind: "ok",
24530
+ file: {
24206
24531
  version: FILE_VERSION,
24207
- projectSlug: state56.projectSlug ?? "unconfigured",
24208
- updatedAt: nowIso(),
24209
- items: []
24210
- };
24532
+ // Any stored slug is accepted (older versions stored the basename
24533
+ // including the extension); the current slug is written on next save.
24534
+ projectSlug: isStr(obj["projectSlug"]) ? obj["projectSlug"] : "",
24535
+ updatedAt: isStr(obj["updatedAt"]) ? obj["updatedAt"] : "",
24536
+ items
24537
+ }
24538
+ };
24539
+ }
24540
+ async function loadFile(deps, filePath) {
24541
+ let raw;
24542
+ try {
24543
+ raw = await deps.readFile(filePath);
24544
+ } catch (err) {
24545
+ if (err.code === "ENOENT") return { kind: "missing" };
24546
+ throw err;
24211
24547
  }
24212
- return state56.file;
24548
+ return parseTrackerFile(raw);
24213
24549
  }
24214
- function recordMutation(op, itemId) {
24215
- state56.lastMutation = { op, itemId, when: nowIso() };
24216
- if (op === "add") state56.addCount += 1;
24217
- else if (op === "complete") state56.completeCount += 1;
24218
- else if (op === "drop") state56.dropCount += 1;
24219
- else if (op === "remove") state56.removeCount += 1;
24220
- else if (op === "pull") state56.pullCount += 1;
24550
+ function quarantineSuffix() {
24551
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
24221
24552
  }
24222
- function findItemIndex(id) {
24223
- return ensureFile().items.findIndex((it) => it.id === id);
24553
+ function emptyFile(slug) {
24554
+ return { version: FILE_VERSION, projectSlug: slug, updatedAt: nowIso(), items: [] };
24224
24555
  }
24225
- function notConfiguredError() {
24226
- return {
24227
- ok: false,
24228
- error: 'todo-tracker: no file path configured. Set `filePath` under `config.extensions["todo-tracker"]` or run inside a session where `paths.projectDir` is provided by the host (e.g. `wstack` CLI).'
24229
- };
24556
+ function requireItemId(input) {
24557
+ const rawId = input["id"] ?? input["itemId"] ?? input["taskId"] ?? input["todoId"];
24558
+ const id = typeof rawId === "string" ? rawId.trim() : "";
24559
+ if (!id) throw new ToolValidationError31({ message: "id is required", field: "id" });
24560
+ return id;
24230
24561
  }
24231
- var plugin61 = {
24232
- name: "todo-tracker",
24233
- version: "0.1.0",
24234
- description: "Persistent, project-scoped todo backlog that survives across sessions",
24235
- apiVersion: "^0.1.10",
24236
- capabilities: { tools: true },
24237
- defaultConfig: {
24238
- filePath: ""
24239
- },
24240
- configSchema: {
24241
- type: "object",
24242
- properties: {
24243
- filePath: {
24244
- type: "string",
24245
- description: "Override the auto-derived per-project path. Defaults to <projectDir>/todo-tracker.json when `paths.projectDir` is provided by the host."
24562
+ function commonPrefixLength(a, b) {
24563
+ const n = Math.min(a.length, b.length);
24564
+ let i = 0;
24565
+ while (i < n && a[i] === b[i]) i++;
24566
+ return i;
24567
+ }
24568
+ function requireItemIndex(file, id) {
24569
+ const idx = file.items.findIndex((it) => it.id === id);
24570
+ if (idx !== -1) return idx;
24571
+ const open = file.items.filter((it) => it.status === "pending" || it.status === "in_progress");
24572
+ const pool = open.length > 0 ? open : file.items;
24573
+ const candidates = [...pool].sort((a, b) => commonPrefixLength(b.id, id) - commonPrefixLength(a.id, id)).slice(0, 5).map((it) => `${it.id} ("${it.content.slice(0, 40)}")`);
24574
+ const hint = candidates.length > 0 ? ` Known ${open.length > 0 ? "open " : ""}ids: ${candidates.join(", ")}` : " The tracker is empty.";
24575
+ throw new ToolValidationError31({ message: `no item with id ${id}.${hint}`, field: "id" });
24576
+ }
24577
+ function createTodoTrackerPlugin(overrides = {}) {
24578
+ const deps = { ...defaultDeps, ...overrides };
24579
+ const instances = /* @__PURE__ */ new WeakMap();
24580
+ let latest = null;
24581
+ function unregisterTools(inst) {
24582
+ const unregister = inst.api.tools.unregister;
24583
+ for (const name of inst.registeredTools.splice(0)) {
24584
+ if (typeof unregister !== "function") continue;
24585
+ try {
24586
+ unregister.call(inst.api.tools, name);
24587
+ } catch (err) {
24588
+ inst.api.log.warn("todo-tracker: failed to unregister tool", { name, err });
24246
24589
  }
24247
24590
  }
24248
- },
24249
- async setup(api) {
24250
- state56.addCount = 0;
24251
- state56.completeCount = 0;
24252
- state56.dropCount = 0;
24253
- state56.removeCount = 0;
24254
- state56.pullCount = 0;
24255
- state56.lastMutation = null;
24256
- state56.file = null;
24257
- const derived = deriveFilePath(api);
24258
- if (derived.filePath === null) {
24259
- api.log.warn(
24260
- 'todo-tracker: no file path configured (set `config.extensions["todo-tracker"].filePath` or wire `paths.projectDir` through PluginAPI) \u2014 tools will report a clear error'
24591
+ }
24592
+ function assertWritable(inst) {
24593
+ if (inst.readOnlyReason !== null) {
24594
+ throw new Error(`todo-tracker: store is read-only \u2014 ${inst.readOnlyReason}`);
24595
+ }
24596
+ }
24597
+ function serialize(inst, fn) {
24598
+ const run = inst.queue.then(fn, fn);
24599
+ inst.queue = run.catch(() => void 0);
24600
+ return run;
24601
+ }
24602
+ async function quarantineLocked(inst, reason) {
24603
+ const target = `${inst.filePath}.corrupt-${quarantineSuffix()}`;
24604
+ try {
24605
+ await deps.rename(inst.filePath, target);
24606
+ } catch (err) {
24607
+ inst.readOnlyReason = `${inst.filePath} is unreadable (${reason}) and could not be moved aside (${err.message}); fix or remove the file, then reload the plugin`;
24608
+ inst.api.log.error(
24609
+ "todo-tracker: corrupt file could not be quarantined; store is read-only",
24610
+ {
24611
+ filePath: inst.filePath,
24612
+ reason,
24613
+ err
24614
+ }
24261
24615
  );
24262
- return;
24616
+ return false;
24263
24617
  }
24264
- state56.filePath = derived.filePath;
24265
- state56.projectSlug = derived.projectSlug;
24266
- state56.file = await loadFile(state56.filePath);
24267
- if (state56.file === null) {
24268
- state56.file = {
24269
- version: FILE_VERSION,
24270
- projectSlug: state56.projectSlug ?? "tracker",
24271
- updatedAt: nowIso(),
24272
- items: []
24273
- };
24618
+ inst.degradedReason = `${inst.filePath} was unreadable (${reason}); original moved to ${target}`;
24619
+ inst.api.log.error(`todo-tracker: corrupt file quarantined to ${target}`, {
24620
+ filePath: inst.filePath,
24621
+ quarantinedTo: target,
24622
+ reason
24623
+ });
24624
+ return true;
24625
+ }
24626
+ async function resolveLoad(inst, res, opts) {
24627
+ switch (res.kind) {
24628
+ case "ok":
24629
+ return res.file;
24630
+ case "missing":
24631
+ return emptyFile(inst.projectSlug);
24632
+ case "unsupportedVersion": {
24633
+ inst.readOnlyReason = `${inst.filePath} has format version ${JSON.stringify(res.version)}, this plugin only writes version ${FILE_VERSION}; refusing to modify it`;
24634
+ inst.api.log.error("todo-tracker: unsupported file version; store is read-only", {
24635
+ filePath: inst.filePath,
24636
+ version: res.version
24637
+ });
24638
+ if (opts.strict) assertWritable(inst);
24639
+ return emptyFile(inst.projectSlug);
24640
+ }
24641
+ case "corrupt":
24642
+ case "invalidItems": {
24643
+ const quarantine = async () => {
24644
+ const again = opts.locked ? res : await loadFile(deps, inst.filePath);
24645
+ if (again.kind === "ok") return again.file;
24646
+ if (again.kind === "missing") return emptyFile(inst.projectSlug);
24647
+ if (again.kind === "unsupportedVersion") {
24648
+ return resolveLoad(inst, again, { locked: true, strict: opts.strict });
24649
+ }
24650
+ const moved = await quarantineLocked(inst, again.reason);
24651
+ if (!moved && opts.strict) assertWritable(inst);
24652
+ return emptyFile(inst.projectSlug);
24653
+ };
24654
+ return opts.locked ? quarantine() : deps.withFileLock(inst.filePath, quarantine);
24655
+ }
24274
24656
  }
24275
- api.tools.register({
24276
- name: "todo_tracker_list",
24277
- description: "List persistent todo-tracker items. Filterable by status, priority, and tag. By default only pending + in_progress items are shown.",
24278
- inputSchema: {
24279
- type: "object",
24280
- properties: {
24281
- status: {
24282
- type: "string",
24283
- enum: ["pending", "in_progress", "completed", "dropped", "all"],
24284
- description: "Filter by status. 'all' returns every item; default is pending+in_progress."
24285
- },
24286
- priority: { type: "string", enum: ["low", "normal", "high"] },
24287
- tag: { type: "string", description: "Filter by exact tag match" },
24288
- limit: { type: "number", description: "Max items to return (default 50, max 200)" }
24657
+ }
24658
+ async function refresh(inst) {
24659
+ if (inst.readOnlyReason !== null) return inst.file;
24660
+ const res = await loadFile(deps, inst.filePath);
24661
+ const file = await resolveLoad(inst, res, { locked: false, strict: false });
24662
+ inst.file = file;
24663
+ return file;
24664
+ }
24665
+ function mutate(inst, apply) {
24666
+ return serialize(
24667
+ inst,
24668
+ () => deps.withFileLock(inst.filePath, async () => {
24669
+ assertWritable(inst);
24670
+ const res = await loadFile(deps, inst.filePath);
24671
+ const current = await resolveLoad(inst, res, { locked: true, strict: true });
24672
+ const draft = structuredClone(current);
24673
+ const now = nowIso();
24674
+ const { changed, result } = apply(draft, now);
24675
+ if (changed) {
24676
+ draft.version = FILE_VERSION;
24677
+ draft.projectSlug = inst.projectSlug;
24678
+ draft.updatedAt = now;
24679
+ await deps.ensureDir(dirname10(inst.filePath));
24680
+ await deps.atomicWrite(inst.filePath, JSON.stringify(draft, null, 2), { mode: 384 });
24681
+ inst.file = draft;
24682
+ } else {
24683
+ inst.file = current;
24289
24684
  }
24290
- },
24291
- permission: "auto",
24292
- mutating: false,
24293
- async execute(input) {
24294
- if (state56.filePath === null) return notConfiguredError();
24295
- const rawStatus = typeof input["status"] === "string" ? input["status"].trim().toLowerCase() : void 0;
24296
- const status = rawStatus ?? "active";
24297
- const rawPriority = typeof input["priority"] === "string" ? input["priority"].trim().toLowerCase() : void 0;
24298
- const priority = rawPriority;
24299
- const tag = input["tag"];
24300
- const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
24301
- const file = ensureFile();
24302
- let items = file.items;
24303
- if (status !== "all") {
24304
- if (status === "active") {
24305
- items = items.filter((it) => it.status === "pending" || it.status === "in_progress");
24306
- } else {
24307
- items = items.filter((it) => it.status.toLowerCase() === status);
24685
+ return result;
24686
+ })
24687
+ );
24688
+ }
24689
+ function recordMutation(inst, op, itemId) {
24690
+ inst.lastMutation = { op, itemId, when: nowIso() };
24691
+ if (op === "add") inst.addCount += 1;
24692
+ else if (op === "complete") inst.completeCount += 1;
24693
+ else if (op === "drop") inst.dropCount += 1;
24694
+ else if (op === "remove") inst.removeCount += 1;
24695
+ }
24696
+ function sessionCounts(inst) {
24697
+ return {
24698
+ add: inst.addCount,
24699
+ complete: inst.completeCount,
24700
+ drop: inst.dropCount,
24701
+ remove: inst.removeCount,
24702
+ pull: inst.pullCount
24703
+ };
24704
+ }
24705
+ function disposeInstance(api) {
24706
+ const inst = instances.get(api);
24707
+ if (!inst) return void 0;
24708
+ unregisterTools(inst);
24709
+ instances.delete(api);
24710
+ if (latest === inst) latest = null;
24711
+ return inst;
24712
+ }
24713
+ const plugin65 = {
24714
+ name: "todo-tracker",
24715
+ version: "0.1.0",
24716
+ description: "Persistent, project-scoped todo backlog that survives across sessions",
24717
+ apiVersion: "^0.1.10",
24718
+ capabilities: { tools: true },
24719
+ defaultConfig: {
24720
+ filePath: ""
24721
+ },
24722
+ configSchema: {
24723
+ type: "object",
24724
+ properties: {
24725
+ filePath: {
24726
+ type: "string",
24727
+ description: "Override the auto-derived per-project path. Defaults to <projectDir>/todo-tracker.json when `paths.projectDir` is provided by the host."
24728
+ }
24729
+ }
24730
+ },
24731
+ async setup(api) {
24732
+ disposeInstance(api);
24733
+ const derived = deriveFilePath(api);
24734
+ if (derived.filePath === null) {
24735
+ latest = null;
24736
+ api.log.warn(
24737
+ 'todo-tracker: no file path configured (set `config.extensions["todo-tracker"].filePath` or wire `paths.projectDir` through PluginAPI) \u2014 tools will report a clear error'
24738
+ );
24739
+ return;
24740
+ }
24741
+ const inst = {
24742
+ api,
24743
+ filePath: derived.filePath,
24744
+ projectSlug: derived.projectSlug ?? "tracker",
24745
+ file: emptyFile(derived.projectSlug ?? "tracker"),
24746
+ readOnlyReason: null,
24747
+ degradedReason: null,
24748
+ queue: Promise.resolve(),
24749
+ registeredTools: [],
24750
+ addCount: 0,
24751
+ completeCount: 0,
24752
+ dropCount: 0,
24753
+ removeCount: 0,
24754
+ pullCount: 0,
24755
+ lastMutation: null
24756
+ };
24757
+ instances.set(api, inst);
24758
+ latest = inst;
24759
+ await refresh(inst);
24760
+ const register = (tool) => {
24761
+ api.tools.register(tool);
24762
+ inst.registeredTools.push(tool.name);
24763
+ };
24764
+ register({
24765
+ name: "todo_tracker_list",
24766
+ description: "List persistent todo-tracker items. Filterable by status, priority, and tag. By default only pending + in_progress items are shown.",
24767
+ inputSchema: {
24768
+ type: "object",
24769
+ properties: {
24770
+ status: {
24771
+ type: "string",
24772
+ enum: ["pending", "in_progress", "completed", "dropped", "all"],
24773
+ description: "Filter by status. 'all' returns every item; default is pending+in_progress."
24774
+ },
24775
+ priority: { type: "string", enum: ["low", "normal", "high"] },
24776
+ tag: { type: "string", description: "Filter by exact tag match" },
24777
+ limit: { type: "number", description: "Max items to return (default 50, max 200)" }
24778
+ }
24779
+ },
24780
+ permission: "auto",
24781
+ mutating: false,
24782
+ async execute(input) {
24783
+ const rawStatus = typeof input["status"] === "string" ? input["status"].trim().toLowerCase() : void 0;
24784
+ const status = rawStatus ?? "active";
24785
+ const priority = typeof input["priority"] === "string" ? input["priority"].trim().toLowerCase() : void 0;
24786
+ const tag = typeof input["tag"] === "string" ? input["tag"] : void 0;
24787
+ const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
24788
+ const file = await refresh(inst);
24789
+ let items = file.items;
24790
+ if (status !== "all") {
24791
+ if (status === "active") {
24792
+ items = items.filter((it) => it.status === "pending" || it.status === "in_progress");
24793
+ } else {
24794
+ items = items.filter((it) => it.status === status);
24795
+ }
24308
24796
  }
24797
+ if (priority) items = items.filter((it) => it.priority === priority);
24798
+ if (tag) items = items.filter((it) => it.tags.includes(tag));
24799
+ const total = items.length;
24800
+ const truncated = items.slice(0, limit);
24801
+ return {
24802
+ ok: true,
24803
+ total,
24804
+ returned: truncated.length,
24805
+ truncated: total > truncated.length,
24806
+ items: truncated,
24807
+ ...inst.readOnlyReason ? { readOnly: inst.readOnlyReason } : {}
24808
+ };
24309
24809
  }
24310
- if (priority) items = items.filter((it) => it.priority.toLowerCase() === priority);
24311
- if (tag) items = items.filter((it) => it.tags.includes(tag));
24312
- const total = items.length;
24313
- const truncated = items.slice(0, limit);
24314
- return {
24315
- ok: true,
24316
- total,
24317
- returned: truncated.length,
24318
- truncated: total > truncated.length,
24319
- items: truncated
24320
- };
24321
- }
24322
- });
24323
- api.tools.register({
24324
- name: "todo_tracker_add",
24325
- description: "Append a new item to the persistent todo-tracker backlog.",
24326
- inputSchema: {
24327
- type: "object",
24328
- properties: {
24329
- content: { type: "string", description: "What needs doing (required)" },
24330
- priority: { type: "string", enum: ["low", "normal", "high"], default: "normal" },
24331
- tags: {
24332
- type: "array",
24333
- items: { type: "string" },
24334
- description: "Optional tags for filtering"
24810
+ });
24811
+ register({
24812
+ name: "todo_tracker_add",
24813
+ description: "Append a new item to the persistent todo-tracker backlog.",
24814
+ inputSchema: {
24815
+ type: "object",
24816
+ properties: {
24817
+ content: { type: "string", description: "What needs doing (required)" },
24818
+ priority: { type: "string", enum: ["low", "normal", "high"], default: "normal" },
24819
+ tags: {
24820
+ type: "array",
24821
+ items: { type: "string" },
24822
+ description: "Optional tags for filtering"
24823
+ },
24824
+ sourceSessionId: { type: "string", description: "Session that created this item" },
24825
+ notes: { type: "string", description: "Optional free-form notes" }
24335
24826
  },
24336
- sourceSessionId: { type: "string", description: "Session that created this item" },
24337
- notes: { type: "string", description: "Optional free-form notes" }
24827
+ required: ["content"]
24338
24828
  },
24339
- required: ["content"]
24340
- },
24341
- permission: "auto",
24342
- mutating: true,
24343
- async execute(input) {
24344
- if (state56.filePath === null) return notConfiguredError();
24345
- const rawContent = input["content"] ?? input["text"] ?? input["task"] ?? input["title"] ?? input["todo"] ?? input["message"] ?? input["item"];
24346
- const content = typeof rawContent === "string" ? rawContent.trim() : "";
24347
- if (!content) {
24348
- return { ok: false, error: "content is required and must be a non-empty string" };
24349
- }
24350
- const rawPri = typeof input["priority"] === "string" ? input["priority"].trim().toLowerCase() : "";
24351
- const priority = rawPri === "low" || rawPri === "high" ? rawPri : "normal";
24352
- const tags = Array.isArray(input["tags"]) ? input["tags"].filter((t) => typeof t === "string") : [];
24353
- const sourceSessionId = typeof input["sourceSessionId"] === "string" ? input["sourceSessionId"] : null;
24354
- const notes = typeof input["notes"] === "string" ? input["notes"] : null;
24355
- const now = nowIso();
24356
- const item = {
24357
- id: randomUUID(),
24358
- content,
24359
- status: "pending",
24360
- priority,
24361
- tags,
24362
- createdAt: now,
24363
- updatedAt: now,
24364
- completedAt: null,
24365
- sourceSessionId,
24366
- notes
24367
- };
24368
- const file = ensureFile();
24369
- file.items.push(item);
24370
- file.updatedAt = now;
24371
- await saveFile(state56.filePath, file);
24372
- recordMutation("add", item.id);
24373
- api.log.info("todo-tracker: added item", { id: item.id, content });
24374
- try {
24375
- await api.session?.append?.({
24376
- type: "todo-tracker:add",
24377
- ts: now,
24378
- id: item.id,
24379
- content,
24380
- priority,
24381
- tags
24829
+ permission: "auto",
24830
+ mutating: true,
24831
+ async execute(input) {
24832
+ const rawContent = input["content"] ?? input["text"] ?? input["task"] ?? input["title"] ?? input["todo"] ?? input["message"] ?? input["item"];
24833
+ const content = typeof rawContent === "string" ? rawContent.trim() : "";
24834
+ if (!content) {
24835
+ throw new ToolValidationError31({
24836
+ message: "content is required and must be a non-empty string",
24837
+ field: "content"
24838
+ });
24839
+ }
24840
+ const rawPri = typeof input["priority"] === "string" ? input["priority"].trim().toLowerCase() : "";
24841
+ const priority = rawPri === "low" || rawPri === "high" ? rawPri : "normal";
24842
+ const tags = Array.isArray(input["tags"]) ? input["tags"].filter((t) => typeof t === "string") : [];
24843
+ const sourceSessionId = typeof input["sourceSessionId"] === "string" ? input["sourceSessionId"] : null;
24844
+ const notes = typeof input["notes"] === "string" ? input["notes"] : null;
24845
+ const item = await mutate(inst, (draft, now) => {
24846
+ const created = {
24847
+ id: randomUUID(),
24848
+ content,
24849
+ status: "pending",
24850
+ priority,
24851
+ tags,
24852
+ createdAt: now,
24853
+ updatedAt: now,
24854
+ completedAt: null,
24855
+ sourceSessionId,
24856
+ notes
24857
+ };
24858
+ draft.items.push(created);
24859
+ return { changed: true, result: created };
24382
24860
  });
24383
- } catch {
24861
+ recordMutation(inst, "add", item.id);
24862
+ api.log.info("todo-tracker: added item", { id: item.id, content });
24863
+ try {
24864
+ await api.session?.append?.({
24865
+ type: "todo-tracker:add",
24866
+ ts: item.createdAt,
24867
+ id: item.id,
24868
+ content,
24869
+ priority,
24870
+ tags
24871
+ });
24872
+ } catch (err) {
24873
+ api.log.warn("todo-tracker: session.append failed (item was saved)", {
24874
+ id: item.id,
24875
+ err
24876
+ });
24877
+ }
24878
+ return { ok: true, item };
24384
24879
  }
24385
- return { ok: true, item };
24386
- }
24387
- });
24388
- api.tools.register({
24389
- name: "todo_tracker_complete",
24390
- description: "Mark a tracked item as completed. Idempotent.",
24391
- inputSchema: {
24392
- type: "object",
24393
- properties: {
24394
- id: { type: "string", description: "Item id" }
24880
+ });
24881
+ const setTerminalStatus = async (input, target) => {
24882
+ const id = requireItemId(input);
24883
+ return mutate(inst, (draft, now) => {
24884
+ const item = draft.items[requireItemIndex(draft, id)];
24885
+ if (item.status === target) {
24886
+ return {
24887
+ changed: false,
24888
+ result: { ok: true, item, message: `already ${target} (idempotent)` }
24889
+ };
24890
+ }
24891
+ item.status = target;
24892
+ item.updatedAt = now;
24893
+ item.completedAt = now;
24894
+ return { changed: true, result: { ok: true, item } };
24895
+ });
24896
+ };
24897
+ register({
24898
+ name: "todo_tracker_complete",
24899
+ description: "Mark a tracked item as completed. Idempotent.",
24900
+ inputSchema: {
24901
+ type: "object",
24902
+ properties: {
24903
+ id: { type: "string", description: "Item id" }
24904
+ },
24905
+ required: ["id"]
24395
24906
  },
24396
- required: ["id"]
24397
- },
24398
- permission: "auto",
24399
- mutating: true,
24400
- async execute(input) {
24401
- if (state56.filePath === null) return notConfiguredError();
24402
- const rawId = input["id"] ?? input["itemId"] ?? input["taskId"] ?? input["todoId"];
24403
- const id = typeof rawId === "string" ? rawId.trim() : "";
24404
- if (!id) return { ok: false, error: "id is required" };
24405
- const idx = findItemIndex(id);
24406
- if (idx === -1) return { ok: false, error: `no item with id ${id}` };
24407
- const file = ensureFile();
24408
- const item = file.items[idx];
24409
- if (item.status === "completed") {
24410
- return { ok: true, item, message: "already completed (idempotent)" };
24907
+ permission: "auto",
24908
+ mutating: true,
24909
+ async execute(input) {
24910
+ const result = await setTerminalStatus(input, "completed");
24911
+ if (!result.message) {
24912
+ recordMutation(inst, "complete", result.item.id);
24913
+ api.log.info("todo-tracker: completed item", { id: result.item.id });
24914
+ }
24915
+ return result;
24411
24916
  }
24412
- const now = nowIso();
24413
- item.status = "completed";
24414
- item.updatedAt = now;
24415
- item.completedAt = now;
24416
- file.updatedAt = now;
24417
- await saveFile(state56.filePath, file);
24418
- recordMutation("complete", id);
24419
- api.log.info("todo-tracker: completed item", { id });
24420
- return { ok: true, item };
24421
- }
24422
- });
24423
- api.tools.register({
24424
- name: "todo_tracker_drop",
24425
- description: "Mark a tracked item as dropped (skipped/obsolete). The row is kept for audit. Idempotent.",
24426
- inputSchema: {
24427
- type: "object",
24428
- properties: {
24429
- id: { type: "string", description: "Item id" }
24917
+ });
24918
+ register({
24919
+ name: "todo_tracker_drop",
24920
+ description: "Mark a tracked item as dropped (skipped/obsolete). The row is kept for audit. Idempotent.",
24921
+ inputSchema: {
24922
+ type: "object",
24923
+ properties: {
24924
+ id: { type: "string", description: "Item id" }
24925
+ },
24926
+ required: ["id"]
24430
24927
  },
24431
- required: ["id"]
24432
- },
24433
- permission: "auto",
24434
- mutating: true,
24435
- async execute(input) {
24436
- if (state56.filePath === null) return notConfiguredError();
24437
- const rawId = input["id"] ?? input["itemId"] ?? input["taskId"] ?? input["todoId"];
24438
- const id = typeof rawId === "string" ? rawId.trim() : "";
24439
- if (!id) return { ok: false, error: "id is required" };
24440
- const idx = findItemIndex(id);
24441
- if (idx === -1) return { ok: false, error: `no item with id ${id}` };
24442
- const file = ensureFile();
24443
- const item = file.items[idx];
24444
- if (item.status === "dropped") {
24445
- return { ok: true, item, message: "already dropped (idempotent)" };
24928
+ permission: "auto",
24929
+ mutating: true,
24930
+ async execute(input) {
24931
+ const result = await setTerminalStatus(input, "dropped");
24932
+ if (!result.message) recordMutation(inst, "drop", result.item.id);
24933
+ return result;
24446
24934
  }
24447
- const now = nowIso();
24448
- item.status = "dropped";
24449
- item.updatedAt = now;
24450
- item.completedAt = now;
24451
- file.updatedAt = now;
24452
- await saveFile(state56.filePath, file);
24453
- recordMutation("drop", id);
24454
- return { ok: true, item };
24455
- }
24456
- });
24457
- api.tools.register({
24458
- name: "todo_tracker_remove",
24459
- description: "Permanently delete a tracked item by id. Use todo_tracker_drop instead if you want to keep the audit row.",
24460
- inputSchema: {
24461
- type: "object",
24462
- properties: {
24463
- id: { type: "string", description: "Item id" }
24935
+ });
24936
+ register({
24937
+ name: "todo_tracker_remove",
24938
+ description: "Permanently delete a tracked item by id. Use todo_tracker_drop instead if you want to keep the audit row.",
24939
+ inputSchema: {
24940
+ type: "object",
24941
+ properties: {
24942
+ id: { type: "string", description: "Item id" }
24943
+ },
24944
+ required: ["id"]
24464
24945
  },
24465
- required: ["id"]
24466
- },
24467
- permission: "confirm",
24468
- mutating: true,
24469
- async execute(input) {
24470
- if (state56.filePath === null) return notConfiguredError();
24471
- const rawId = input["id"] ?? input["itemId"] ?? input["taskId"] ?? input["todoId"];
24472
- const id = typeof rawId === "string" ? rawId.trim() : "";
24473
- if (!id) return { ok: false, error: "id is required" };
24474
- const idx = findItemIndex(id);
24475
- if (idx === -1) return { ok: false, error: `no item with id ${id}` };
24476
- const file = ensureFile();
24477
- const [removed] = file.items.splice(idx, 1);
24478
- file.updatedAt = nowIso();
24479
- await saveFile(state56.filePath, file);
24480
- recordMutation("remove", id);
24481
- return { ok: true, removed };
24482
- }
24483
- });
24484
- api.tools.register({
24485
- name: "todo_tracker_pull",
24486
- description: "Return all pending + in_progress items. The LLM is expected to take this list and re-register each entry with the session-local `todo` tool (which mutates ctx.todos). After pull, the LLM may also choose to call todo_tracker_complete on items it finishes mid-session.",
24487
- inputSchema: {
24488
- type: "object",
24489
- properties: {
24490
- limit: { type: "number", description: "Max items to return (default 50, max 200)" }
24946
+ permission: "confirm",
24947
+ mutating: true,
24948
+ async execute(input) {
24949
+ const id = requireItemId(input);
24950
+ const removed = await mutate(inst, (draft) => {
24951
+ const [gone] = draft.items.splice(requireItemIndex(draft, id), 1);
24952
+ return { changed: true, result: gone };
24953
+ });
24954
+ recordMutation(inst, "remove", id);
24955
+ return { ok: true, removed };
24491
24956
  }
24492
- },
24493
- permission: "auto",
24494
- mutating: false,
24495
- async execute(input) {
24496
- if (state56.filePath === null) return notConfiguredError();
24497
- const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
24498
- const file = ensureFile();
24499
- const items = file.items.filter((it) => it.status === "pending" || it.status === "in_progress").slice(0, limit);
24500
- if (items.length > 0) {
24501
- recordMutation("pull", items[0].id);
24957
+ });
24958
+ register({
24959
+ name: "todo_tracker_pull",
24960
+ description: "Return all pending + in_progress items. The LLM is expected to take this list and re-register each entry with the session-local `todo` tool (which mutates ctx.todos). After pull, the LLM may also choose to call todo_tracker_complete on items it finishes mid-session.",
24961
+ inputSchema: {
24962
+ type: "object",
24963
+ properties: {
24964
+ limit: { type: "number", description: "Max items to return (default 50, max 200)" }
24965
+ }
24966
+ },
24967
+ permission: "auto",
24968
+ mutating: false,
24969
+ async execute(input) {
24970
+ const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
24971
+ const file = await refresh(inst);
24972
+ const open = file.items.filter(
24973
+ (it) => it.status === "pending" || it.status === "in_progress"
24974
+ );
24975
+ const items = open.slice(0, limit);
24976
+ if (items.length > 0) inst.pullCount += 1;
24977
+ return {
24978
+ ok: true,
24979
+ total: open.length,
24980
+ returned: items.length,
24981
+ truncated: open.length > items.length,
24982
+ items,
24983
+ hint: "These are persistent items. To work on them this session, register each one with the built-in `todo` tool. Mark them `completed` via todo_tracker_complete when done."
24984
+ };
24502
24985
  }
24986
+ });
24987
+ register({
24988
+ name: "todo_tracker_status",
24989
+ description: "Report todo-tracker counters (per-status totals) + the file path + last update timestamp.",
24990
+ inputSchema: { type: "object", properties: {} },
24991
+ permission: "auto",
24992
+ mutating: false,
24993
+ async execute() {
24994
+ const file = await refresh(inst);
24995
+ const byStatus = {
24996
+ pending: 0,
24997
+ in_progress: 0,
24998
+ completed: 0,
24999
+ dropped: 0
25000
+ };
25001
+ for (const it of file.items) {
25002
+ if (STATUSES.includes(it.status)) byStatus[it.status] += 1;
25003
+ }
25004
+ return {
25005
+ ok: true,
25006
+ filePath: inst.filePath,
25007
+ projectSlug: inst.projectSlug,
25008
+ updatedAt: file.updatedAt,
25009
+ counters: byStatus,
25010
+ total: file.items.length,
25011
+ session: sessionCounts(inst),
25012
+ lastMutation: inst.lastMutation,
25013
+ readOnly: inst.readOnlyReason,
25014
+ degraded: inst.degradedReason
25015
+ };
25016
+ }
25017
+ });
25018
+ api.log.info("todo-tracker plugin loaded", {
25019
+ filePath: inst.filePath,
25020
+ projectSlug: inst.projectSlug,
25021
+ initialItemCount: inst.file.items.length,
25022
+ readOnly: inst.readOnlyReason,
25023
+ degraded: inst.degradedReason
25024
+ });
25025
+ },
25026
+ teardown(api) {
25027
+ const inst = disposeInstance(api);
25028
+ if (!inst) return;
25029
+ api.log.info("todo-tracker: teardown complete", { sessionCounts: sessionCounts(inst) });
25030
+ },
25031
+ async health() {
25032
+ const inst = latest;
25033
+ if (inst === null) {
24503
25034
  return {
24504
- ok: true,
24505
- total: items.length,
24506
- items,
24507
- hint: "These are persistent items. To work on them this session, register each one with the built-in `todo` tool. Mark them `completed` via todo_tracker_complete when done."
24508
- };
24509
- }
24510
- });
24511
- api.tools.register({
24512
- name: "todo_tracker_status",
24513
- description: "Report todo-tracker counters (per-status totals) + the file path + last update timestamp.",
24514
- inputSchema: { type: "object", properties: {} },
24515
- permission: "auto",
24516
- mutating: false,
24517
- async execute() {
24518
- if (state56.filePath === null) return notConfiguredError();
24519
- const file = ensureFile();
24520
- const byStatus = {
24521
- pending: 0,
24522
- in_progress: 0,
24523
- completed: 0,
24524
- dropped: 0
24525
- };
24526
- for (const it of file.items) byStatus[it.status] += 1;
24527
- return {
24528
- ok: true,
24529
- filePath: state56.filePath,
24530
- projectSlug: state56.projectSlug,
24531
- updatedAt: file.updatedAt,
24532
- counters: byStatus,
24533
- total: file.items.length,
24534
- session: {
24535
- add: state56.addCount,
24536
- complete: state56.completeCount,
24537
- drop: state56.dropCount,
24538
- remove: state56.removeCount,
24539
- pull: state56.pullCount
24540
- },
24541
- lastMutation: state56.lastMutation
25035
+ ok: false,
25036
+ message: "todo-tracker: no file path configured \u2014 tools will error"
24542
25037
  };
24543
25038
  }
24544
- });
24545
- api.log.info("todo-tracker plugin loaded", {
24546
- filePath: state56.filePath,
24547
- projectSlug: state56.projectSlug,
24548
- initialItemCount: state56.file.items.length
24549
- });
24550
- },
24551
- teardown(api) {
24552
- const finalCounts = {
24553
- add: state56.addCount,
24554
- complete: state56.completeCount,
24555
- drop: state56.dropCount,
24556
- remove: state56.removeCount,
24557
- pull: state56.pullCount
24558
- };
24559
- state56.addCount = 0;
24560
- state56.completeCount = 0;
24561
- state56.dropCount = 0;
24562
- state56.removeCount = 0;
24563
- state56.pullCount = 0;
24564
- state56.lastMutation = null;
24565
- state56.file = null;
24566
- state56.filePath = null;
24567
- state56.projectSlug = null;
24568
- api.log.info("todo-tracker: teardown complete", { sessionCounts: finalCounts });
24569
- },
24570
- async health() {
24571
- if (state56.filePath === null) {
25039
+ const reason = inst.readOnlyReason ?? inst.degradedReason;
24572
25040
  return {
24573
- ok: false,
24574
- message: "todo-tracker: no file path configured \u2014 tools will error"
25041
+ ok: reason === null,
25042
+ message: reason === null ? `todo-tracker: ${inst.file.items.length} item(s) at ${inst.filePath}` : `todo-tracker: ${inst.readOnlyReason ? "read-only" : "degraded"} \u2014 ${reason}`,
25043
+ filePath: inst.filePath,
25044
+ projectSlug: inst.projectSlug,
25045
+ total: inst.file.items.length,
25046
+ readOnly: inst.readOnlyReason,
25047
+ degraded: inst.degradedReason,
25048
+ sessionCounts: sessionCounts(inst),
25049
+ lastMutation: inst.lastMutation
24575
25050
  };
24576
25051
  }
24577
- const file = ensureFile();
24578
- return {
24579
- ok: true,
24580
- message: `todo-tracker: ${file.items.length} item(s) at ${state56.filePath}`,
24581
- filePath: state56.filePath,
24582
- projectSlug: state56.projectSlug,
24583
- total: file.items.length,
24584
- sessionCounts: {
24585
- add: state56.addCount,
24586
- complete: state56.completeCount,
24587
- drop: state56.dropCount,
24588
- remove: state56.removeCount,
24589
- pull: state56.pullCount
24590
- },
24591
- lastMutation: state56.lastMutation
24592
- };
24593
- }
24594
- };
25052
+ };
25053
+ return plugin65;
25054
+ }
25055
+ var plugin61 = createTodoTrackerPlugin();
24595
25056
  var todo_tracker_default = plugin61;
24596
25057
 
24597
25058
  // src/token-budget/index.ts
24598
25059
  var API_VERSION40 = "^0.1.10";
24599
- var state57 = {
25060
+ var state56 = {
24600
25061
  totalTokens: 0,
24601
25062
  totalPromptTokens: 0,
24602
25063
  totalCompletionTokens: 0,
@@ -24654,13 +25115,13 @@ function readConfig54(raw) {
24654
25115
  }
24655
25116
  function clearRegistrations3() {
24656
25117
  for (const key of ["hookUnregister", "postHookUnregister"]) {
24657
- const off = state57[key];
25118
+ const off = state56[key];
24658
25119
  if (!off) continue;
24659
25120
  try {
24660
25121
  off();
24661
25122
  } catch {
24662
25123
  }
24663
- state57[key] = null;
25124
+ state56[key] = null;
24664
25125
  }
24665
25126
  }
24666
25127
  var plugin62 = {
@@ -24701,15 +25162,15 @@ var plugin62 = {
24701
25162
  }
24702
25163
  },
24703
25164
  setup(api) {
24704
- state57.totalTokens = 0;
24705
- state57.totalPromptTokens = 0;
24706
- state57.totalCompletionTokens = 0;
24707
- state57.requestCount = 0;
24708
- state57.warningFired = false;
24709
- state57.stopFired = false;
24710
- state57.warnContextInjected = false;
24711
- state57.stopContextInjected = false;
24712
- state57.lastRequest = null;
25165
+ state56.totalTokens = 0;
25166
+ state56.totalPromptTokens = 0;
25167
+ state56.totalCompletionTokens = 0;
25168
+ state56.requestCount = 0;
25169
+ state56.warningFired = false;
25170
+ state56.stopFired = false;
25171
+ state56.warnContextInjected = false;
25172
+ state56.stopContextInjected = false;
25173
+ state56.lastRequest = null;
24713
25174
  clearRegistrations3();
24714
25175
  const cfg = readConfig54(api.config.extensions?.["token-budget"]);
24715
25176
  api.onEvent("provider.response", (payload) => {
@@ -24722,24 +25183,26 @@ var plugin62 = {
24722
25183
  if (!modelMatches(cfg.model, modelName)) return;
24723
25184
  }
24724
25185
  const rawUsage = usage;
24725
- const promptTokens = (typeof rawUsage["input"] === "number" ? rawUsage["input"] : void 0) ?? (typeof rawUsage["prompt_tokens"] === "number" ? rawUsage["prompt_tokens"] : void 0) ?? (typeof rawUsage["input_tokens"] === "number" ? rawUsage["input_tokens"] : void 0) ?? (typeof rawUsage["promptTokens"] === "number" ? rawUsage["promptTokens"] : 0);
24726
- const completionTokens = (typeof rawUsage["output"] === "number" ? rawUsage["output"] : void 0) ?? (typeof rawUsage["completion_tokens"] === "number" ? rawUsage["completion_tokens"] : void 0) ?? (typeof rawUsage["output_tokens"] === "number" ? rawUsage["output_tokens"] : void 0) ?? (typeof rawUsage["completionTokens"] === "number" ? rawUsage["completionTokens"] : 0);
25186
+ const promptTokensRaw = (typeof rawUsage["input"] === "number" ? rawUsage["input"] : void 0) ?? (typeof rawUsage["prompt_tokens"] === "number" ? rawUsage["prompt_tokens"] : void 0) ?? (typeof rawUsage["input_tokens"] === "number" ? rawUsage["input_tokens"] : void 0) ?? (typeof rawUsage["promptTokens"] === "number" ? rawUsage["promptTokens"] : 0);
25187
+ const promptTokens = Number.isFinite(promptTokensRaw) ? promptTokensRaw : 0;
25188
+ const completionTokensRaw = (typeof rawUsage["output"] === "number" ? rawUsage["output"] : void 0) ?? (typeof rawUsage["completion_tokens"] === "number" ? rawUsage["completion_tokens"] : void 0) ?? (typeof rawUsage["output_tokens"] === "number" ? rawUsage["output_tokens"] : void 0) ?? (typeof rawUsage["completionTokens"] === "number" ? rawUsage["completionTokens"] : 0);
25189
+ const completionTokens = Number.isFinite(completionTokensRaw) ? completionTokensRaw : 0;
24727
25190
  const total = promptTokens + completionTokens;
24728
- state57.totalPromptTokens += promptTokens;
24729
- state57.totalCompletionTokens += completionTokens;
24730
- state57.totalTokens += total;
24731
- state57.requestCount += 1;
24732
- state57.lastRequest = {
25191
+ state56.totalPromptTokens += promptTokens;
25192
+ state56.totalCompletionTokens += completionTokens;
25193
+ state56.totalTokens += total;
25194
+ state56.requestCount += 1;
25195
+ state56.lastRequest = {
24733
25196
  model: modelName,
24734
25197
  prompt: promptTokens,
24735
25198
  completion: completionTokens,
24736
25199
  when: (/* @__PURE__ */ new Date()).toISOString()
24737
25200
  };
24738
25201
  if (cfg.limit <= 0) return;
24739
- const percent = state57.totalTokens / cfg.limit * 100;
24740
- if (!state57.warningFired && percent >= cfg.warnPercent && percent < cfg.stopPercent) {
24741
- state57.warningFired = true;
24742
- const remaining = cfg.limit - state57.totalTokens;
25202
+ const percent = state56.totalTokens / cfg.limit * 100;
25203
+ if (!state56.warningFired && percent >= cfg.warnPercent) {
25204
+ state56.warningFired = true;
25205
+ const remaining = Math.max(cfg.limit - state56.totalTokens, 0);
24743
25206
  api.log.info("token-budget: warning threshold reached", {
24744
25207
  percent: Math.round(percent),
24745
25208
  remaining
@@ -24747,45 +25210,46 @@ var plugin62 = {
24747
25210
  api.emitCustom("token-budget:warning", {
24748
25211
  percent: Math.round(percent),
24749
25212
  remaining,
24750
- total: state57.totalTokens,
25213
+ total: state56.totalTokens,
24751
25214
  limit: cfg.limit
24752
25215
  });
24753
25216
  }
24754
- if (!state57.stopFired && percent >= cfg.stopPercent) {
24755
- state57.stopFired = true;
25217
+ if (!state56.stopFired && percent >= cfg.stopPercent) {
25218
+ state56.stopFired = true;
24756
25219
  api.log.warn("token-budget: hard limit reached \u2014 agent loop will be stopped", {
24757
- total: state57.totalTokens,
25220
+ total: state56.totalTokens,
24758
25221
  limit: cfg.limit
24759
25222
  });
24760
25223
  api.emitCustom("token-budget:limit_reached", {
24761
- total: state57.totalTokens,
25224
+ total: state56.totalTokens,
24762
25225
  limit: cfg.limit
24763
25226
  });
24764
25227
  }
24765
25228
  });
24766
- state57.hookUnregister = api.registerHook("Stop", void 0, () => {
24767
- if (cfg.limit <= 0 || !state57.stopFired) return;
25229
+ state56.hookUnregister = api.registerHook("Stop", void 0, () => {
25230
+ if (cfg.limit <= 0 || !state56.stopFired) return;
24768
25231
  return {
24769
25232
  decision: "block",
24770
- reason: `token-budget: session token limit reached (${state57.totalTokens.toLocaleString()} / ${cfg.limit.toLocaleString()} tokens). The budget is exhausted \u2014 wrap up the current task and summarize what was accomplished.`
25233
+ reason: `token-budget: session token limit reached (${state56.totalTokens.toLocaleString()} / ${cfg.limit.toLocaleString()} tokens). The budget is exhausted \u2014 wrap up the current task and summarize what was accomplished.`
24771
25234
  };
24772
25235
  });
24773
- state57.postHookUnregister = api.registerHook("PostToolUse", "*", () => {
25236
+ state56.postHookUnregister = api.registerHook("PostToolUse", "*", () => {
24774
25237
  if (cfg.limit <= 0) return;
24775
- const percent = Math.round(state57.totalTokens / cfg.limit * 100);
24776
- const remaining = Math.max(cfg.limit - state57.totalTokens, 0);
24777
- if (state57.stopFired && !state57.stopContextInjected) {
24778
- state57.stopContextInjected = true;
25238
+ const percent = Math.round(state56.totalTokens / cfg.limit * 100);
25239
+ const remaining = Math.max(cfg.limit - state56.totalTokens, 0);
25240
+ if (state56.stopFired && !state56.stopContextInjected) {
25241
+ state56.stopContextInjected = true;
25242
+ state56.warnContextInjected = true;
24779
25243
  return {
24780
25244
  additionalContext: `
24781
- \u{1F6D1} token-budget: HARD LIMIT REACHED \u2014 ${state57.totalTokens.toLocaleString()} / ${cfg.limit.toLocaleString()} tokens (${percent}%). You must stop here. Do NOT start any new task. Summarize what was accomplished and list any remaining work.`
25245
+ \u{1F6D1} token-budget: HARD LIMIT REACHED \u2014 ${state56.totalTokens.toLocaleString()} / ${cfg.limit.toLocaleString()} tokens (${percent}%). You must stop here. Do NOT start any new task. Summarize what was accomplished and list any remaining work.`
24782
25246
  };
24783
25247
  }
24784
- if (state57.warningFired && !state57.warnContextInjected) {
24785
- state57.warnContextInjected = true;
25248
+ if (state56.warningFired && !state56.warnContextInjected) {
25249
+ state56.warnContextInjected = true;
24786
25250
  return {
24787
25251
  additionalContext: `
24788
- \u26A0\uFE0F token-budget: ${percent}% of budget used (${state57.totalTokens.toLocaleString()} / ${cfg.limit.toLocaleString()} tokens, ${remaining.toLocaleString()} remaining). Start wrapping up \u2014 prioritize finishing the current task over starting new ones.`
25252
+ \u26A0\uFE0F token-budget: ${percent}% of budget used (${state56.totalTokens.toLocaleString()} / ${cfg.limit.toLocaleString()} tokens, ${remaining.toLocaleString()} remaining). Start wrapping up \u2014 prioritize finishing the current task over starting new ones.`
24789
25253
  };
24790
25254
  }
24791
25255
  return;
@@ -24798,7 +25262,7 @@ var plugin62 = {
24798
25262
  category: "Meta",
24799
25263
  mutating: false,
24800
25264
  async execute() {
24801
- const consumed = state57.totalTokens;
25265
+ const consumed = state56.totalTokens;
24802
25266
  const limit = cfg.limit;
24803
25267
  const percent = limit > 0 ? Math.round(consumed / limit * 100) : 0;
24804
25268
  const remaining = limit > 0 ? Math.max(limit - consumed, 0) : Infinity;
@@ -24808,7 +25272,7 @@ var plugin62 = {
24808
25272
  consumed,
24809
25273
  remaining,
24810
25274
  percent,
24811
- requestCount: state57.requestCount,
25275
+ requestCount: state56.requestCount,
24812
25276
  // The EFFECTIVE thresholds, after out-of-range values fall back to
24813
25277
  // the defaults and warn/stop are ordered. Without these the user
24814
25278
  // cannot tell that a rejected or clamped setting is not in force.
@@ -24816,12 +25280,12 @@ var plugin62 = {
24816
25280
  stopPercent: cfg.stopPercent,
24817
25281
  model: cfg.model === "" ? null : cfg.model,
24818
25282
  breakdown: {
24819
- prompt: state57.totalPromptTokens,
24820
- completion: state57.totalCompletionTokens
25283
+ prompt: state56.totalPromptTokens,
25284
+ completion: state56.totalCompletionTokens
24821
25285
  },
24822
- warningFired: state57.warningFired,
24823
- stopFired: state57.stopFired,
24824
- lastRequest: state57.lastRequest
25286
+ warningFired: state56.warningFired,
25287
+ stopFired: state56.stopFired,
25288
+ lastRequest: state56.lastRequest
24825
25289
  };
24826
25290
  }
24827
25291
  });
@@ -24835,30 +25299,30 @@ var plugin62 = {
24835
25299
  teardown(api) {
24836
25300
  clearRegistrations3();
24837
25301
  const final = {
24838
- totalTokens: state57.totalTokens,
24839
- requestCount: state57.requestCount,
24840
- warningFired: state57.warningFired,
24841
- stopFired: state57.stopFired
25302
+ totalTokens: state56.totalTokens,
25303
+ requestCount: state56.requestCount,
25304
+ warningFired: state56.warningFired,
25305
+ stopFired: state56.stopFired
24842
25306
  };
24843
- state57.totalTokens = 0;
24844
- state57.totalPromptTokens = 0;
24845
- state57.totalCompletionTokens = 0;
24846
- state57.requestCount = 0;
24847
- state57.warningFired = false;
24848
- state57.stopFired = false;
24849
- state57.warnContextInjected = false;
24850
- state57.stopContextInjected = false;
24851
- state57.lastRequest = null;
25307
+ state56.totalTokens = 0;
25308
+ state56.totalPromptTokens = 0;
25309
+ state56.totalCompletionTokens = 0;
25310
+ state56.requestCount = 0;
25311
+ state56.warningFired = false;
25312
+ state56.stopFired = false;
25313
+ state56.warnContextInjected = false;
25314
+ state56.stopContextInjected = false;
25315
+ state56.lastRequest = null;
24852
25316
  api.log.info("token-budget: teardown complete", { final });
24853
25317
  },
24854
25318
  async health() {
24855
25319
  return {
24856
25320
  ok: true,
24857
- message: state57.lastRequest === null ? `token-budget: ${state57.totalTokens.toLocaleString()} tokens across ${state57.requestCount} request(s)` : `token-budget: ${state57.totalTokens.toLocaleString()} tokens, last ${state57.lastRequest.model} at ${state57.lastRequest.when}`,
24858
- totalTokens: state57.totalTokens,
24859
- requestCount: state57.requestCount,
24860
- warningFired: state57.warningFired,
24861
- stopFired: state57.stopFired
25321
+ message: state56.lastRequest === null ? `token-budget: ${state56.totalTokens.toLocaleString()} tokens across ${state56.requestCount} request(s)` : `token-budget: ${state56.totalTokens.toLocaleString()} tokens, last ${state56.lastRequest.model} at ${state56.lastRequest.when}`,
25322
+ totalTokens: state56.totalTokens,
25323
+ requestCount: state56.requestCount,
25324
+ warningFired: state56.warningFired,
25325
+ stopFired: state56.stopFired
24862
25326
  };
24863
25327
  }
24864
25328
  };
@@ -24905,7 +25369,7 @@ function computeThrottleDelay(entries, now, limit, projected) {
24905
25369
  const newest = sorted[sorted.length - 1];
24906
25370
  return newest ? Math.max(0, newest.at + WINDOW_MS - now) : 0;
24907
25371
  }
24908
- var state58 = {
25372
+ var state57 = {
24909
25373
  window: [],
24910
25374
  invocations: 0,
24911
25375
  throttled: 0,
@@ -24978,16 +25442,16 @@ var plugin63 = {
24978
25442
  }
24979
25443
  },
24980
25444
  setup(api) {
24981
- state58.window = [];
24982
- state58.invocations = 0;
24983
- state58.throttled = 0;
24984
- state58.totalDelayMs = 0;
24985
- if (state58.extensionUnregister) {
25445
+ state57.window = [];
25446
+ state57.invocations = 0;
25447
+ state57.throttled = 0;
25448
+ state57.totalDelayMs = 0;
25449
+ if (state57.extensionUnregister) {
24986
25450
  try {
24987
- state58.extensionUnregister();
25451
+ state57.extensionUnregister();
24988
25452
  } catch {
24989
25453
  }
24990
- state58.extensionUnregister = null;
25454
+ state57.extensionUnregister = null;
24991
25455
  }
24992
25456
  const cfg = readConfig55(api.config.extensions?.["token-throttle"]);
24993
25457
  if (cfg.enabled) {
@@ -24996,21 +25460,21 @@ var plugin63 = {
24996
25460
  kind: "throttle",
24997
25461
  wraps: ["request"]
24998
25462
  });
24999
- state58.extensionUnregister = api.extensions.register({
25463
+ state57.extensionUnregister = api.extensions.register({
25000
25464
  name: "token-throttle",
25001
25465
  owner: "token-throttle",
25002
25466
  async wrapProviderRunner(_ctx, request, inner) {
25003
25467
  const signal = _ctx?.signal;
25004
25468
  const req = request ?? {};
25005
- state58.invocations += 1;
25469
+ state57.invocations += 1;
25006
25470
  const now = Date.now();
25007
- state58.window = pruneWindow(state58.window, now);
25471
+ state57.window = pruneWindow(state57.window, now);
25008
25472
  const projected = estimateRequestTokens(req, cfg.charsPerToken);
25009
- const rawDelay = computeThrottleDelay(state58.window, now, cfg.tokensPerMinute, projected);
25473
+ const rawDelay = computeThrottleDelay(state57.window, now, cfg.tokensPerMinute, projected);
25010
25474
  const delay = Math.min(rawDelay, cfg.maxDelayMs);
25011
25475
  if (delay > 0) {
25012
- state58.throttled += 1;
25013
- state58.totalDelayMs += delay;
25476
+ state57.throttled += 1;
25477
+ state57.totalDelayMs += delay;
25014
25478
  api.metrics.counter("throttled");
25015
25479
  api.metrics.histogram("delay_ms", delay);
25016
25480
  api.log.info("token-throttle: delaying provider call", { delayMs: delay, projected });
@@ -25018,11 +25482,14 @@ var plugin63 = {
25018
25482
  }
25019
25483
  const response = await inner(_ctx, request);
25020
25484
  const rawUsage = response?.usage;
25021
- const inputTokens = (typeof rawUsage?.["input"] === "number" ? rawUsage["input"] : void 0) ?? (typeof rawUsage?.["prompt_tokens"] === "number" ? rawUsage["prompt_tokens"] : void 0) ?? (typeof rawUsage?.["input_tokens"] === "number" ? rawUsage["input_tokens"] : void 0) ?? (typeof rawUsage?.["promptTokens"] === "number" ? rawUsage["promptTokens"] : void 0) ?? (typeof rawUsage?.["inputTokens"] === "number" ? rawUsage["inputTokens"] : 0);
25022
- const outputTokens = (typeof rawUsage?.["output"] === "number" ? rawUsage["output"] : void 0) ?? (typeof rawUsage?.["completion_tokens"] === "number" ? rawUsage["completion_tokens"] : void 0) ?? (typeof rawUsage?.["output_tokens"] === "number" ? rawUsage["output_tokens"] : void 0) ?? (typeof rawUsage?.["completionTokens"] === "number" ? rawUsage["completionTokens"] : void 0) ?? (typeof rawUsage?.["outputTokens"] === "number" ? rawUsage["outputTokens"] : 0);
25023
- const totalTokens = (typeof rawUsage?.["total_tokens"] === "number" ? rawUsage["total_tokens"] : void 0) ?? (typeof rawUsage?.["totalTokens"] === "number" ? rawUsage["totalTokens"] : void 0) ?? (typeof rawUsage?.["total"] === "number" ? rawUsage["total"] : void 0) ?? inputTokens + outputTokens;
25485
+ const inputTokensRaw = (typeof rawUsage?.["input"] === "number" ? rawUsage["input"] : void 0) ?? (typeof rawUsage?.["prompt_tokens"] === "number" ? rawUsage["prompt_tokens"] : void 0) ?? (typeof rawUsage?.["input_tokens"] === "number" ? rawUsage["input_tokens"] : void 0) ?? (typeof rawUsage?.["promptTokens"] === "number" ? rawUsage["promptTokens"] : void 0) ?? (typeof rawUsage?.["inputTokens"] === "number" ? rawUsage["inputTokens"] : 0);
25486
+ const inputTokens = Number.isFinite(inputTokensRaw) ? inputTokensRaw : 0;
25487
+ const outputTokensRaw = (typeof rawUsage?.["output"] === "number" ? rawUsage["output"] : void 0) ?? (typeof rawUsage?.["completion_tokens"] === "number" ? rawUsage["completion_tokens"] : void 0) ?? (typeof rawUsage?.["output_tokens"] === "number" ? rawUsage["output_tokens"] : void 0) ?? (typeof rawUsage?.["completionTokens"] === "number" ? rawUsage["completionTokens"] : void 0) ?? (typeof rawUsage?.["outputTokens"] === "number" ? rawUsage["outputTokens"] : 0);
25488
+ const outputTokens = Number.isFinite(outputTokensRaw) ? outputTokensRaw : 0;
25489
+ const totalTokensRaw = (typeof rawUsage?.["total_tokens"] === "number" ? rawUsage["total_tokens"] : void 0) ?? (typeof rawUsage?.["totalTokens"] === "number" ? rawUsage["totalTokens"] : void 0) ?? (typeof rawUsage?.["total"] === "number" ? rawUsage["total"] : void 0) ?? inputTokens + outputTokens;
25490
+ const totalTokens = Number.isFinite(totalTokensRaw) ? totalTokensRaw : 0;
25024
25491
  const used = totalTokens > 0 ? totalTokens : projected;
25025
- state58.window.push({ at: Date.now(), tokens: used });
25492
+ state57.window.push({ at: Date.now(), tokens: used });
25026
25493
  return response;
25027
25494
  }
25028
25495
  });
@@ -25036,7 +25503,7 @@ var plugin63 = {
25036
25503
  mutating: false,
25037
25504
  async execute() {
25038
25505
  const now = Date.now();
25039
- const live = pruneWindow(state58.window, now);
25506
+ const live = pruneWindow(state57.window, now);
25040
25507
  return {
25041
25508
  ok: true,
25042
25509
  enabled: cfg.enabled,
@@ -25045,9 +25512,9 @@ var plugin63 = {
25045
25512
  windowSpend: windowSpend(live),
25046
25513
  windowEntries: live.length,
25047
25514
  counters: {
25048
- invocations: state58.invocations,
25049
- throttled: state58.throttled,
25050
- totalDelayMs: state58.totalDelayMs
25515
+ invocations: state57.invocations,
25516
+ throttled: state57.throttled,
25517
+ totalDelayMs: state57.totalDelayMs
25051
25518
  }
25052
25519
  };
25053
25520
  }
@@ -25059,32 +25526,32 @@ var plugin63 = {
25059
25526
  });
25060
25527
  },
25061
25528
  teardown(api) {
25062
- if (state58.extensionUnregister) {
25529
+ if (state57.extensionUnregister) {
25063
25530
  try {
25064
- state58.extensionUnregister();
25531
+ state57.extensionUnregister();
25065
25532
  } catch {
25066
25533
  }
25067
- state58.extensionUnregister = null;
25534
+ state57.extensionUnregister = null;
25068
25535
  }
25069
25536
  const final = {
25070
- invocations: state58.invocations,
25071
- throttled: state58.throttled,
25072
- totalDelayMs: state58.totalDelayMs
25537
+ invocations: state57.invocations,
25538
+ throttled: state57.throttled,
25539
+ totalDelayMs: state57.totalDelayMs
25073
25540
  };
25074
- state58.window = [];
25075
- state58.invocations = 0;
25076
- state58.throttled = 0;
25077
- state58.totalDelayMs = 0;
25541
+ state57.window = [];
25542
+ state57.invocations = 0;
25543
+ state57.throttled = 0;
25544
+ state57.totalDelayMs = 0;
25078
25545
  api.log.info("token-throttle: teardown complete", { final });
25079
25546
  },
25080
25547
  async health() {
25081
25548
  return {
25082
25549
  ok: true,
25083
- message: `token-throttle: ${state58.throttled} throttle(s) of ${state58.invocations} call(s), ${state58.totalDelayMs}ms total delay`,
25550
+ message: `token-throttle: ${state57.throttled} throttle(s) of ${state57.invocations} call(s), ${state57.totalDelayMs}ms total delay`,
25084
25551
  counters: {
25085
- invocations: state58.invocations,
25086
- throttled: state58.throttled,
25087
- totalDelayMs: state58.totalDelayMs
25552
+ invocations: state57.invocations,
25553
+ throttled: state57.throttled,
25554
+ totalDelayMs: state57.totalDelayMs
25088
25555
  }
25089
25556
  };
25090
25557
  }
@@ -25094,7 +25561,7 @@ var token_throttle_default = plugin63;
25094
25561
  // src/type-gate/index.ts
25095
25562
  import { existsSync as existsSync8 } from "node:fs";
25096
25563
  var API_VERSION41 = "^0.1.10";
25097
- var state59 = {
25564
+ var state58 = {
25098
25565
  invocationCount: 0,
25099
25566
  runCount: 0,
25100
25567
  passCount: 0,
@@ -25260,14 +25727,14 @@ var plugin64 = {
25260
25727
  }
25261
25728
  },
25262
25729
  setup(api) {
25263
- state59.invocationCount = 0;
25264
- state59.runCount = 0;
25265
- state59.passCount = 0;
25266
- state59.failCount = 0;
25267
- state59.errorCount = 0;
25268
- state59.skippedCount = 0;
25269
- state59.lastResult = null;
25270
- state59.hookUnregister = (0, runtime_exports.releaseHandle)(state59.hookUnregister);
25730
+ state58.invocationCount = 0;
25731
+ state58.runCount = 0;
25732
+ state58.passCount = 0;
25733
+ state58.failCount = 0;
25734
+ state58.errorCount = 0;
25735
+ state58.skippedCount = 0;
25736
+ state58.lastResult = null;
25737
+ state58.hookUnregister = (0, runtime_exports.releaseHandle)(state58.hookUnregister);
25271
25738
  const cfg = readConfig56(api.config.extensions?.["type-gate"]);
25272
25739
  const hook = async (input) => {
25273
25740
  if (!cfg.enabled) return;
@@ -25279,27 +25746,27 @@ var plugin64 = {
25279
25746
  if (!(0, runtime_exports.withinProject)(sourcePath)) return;
25280
25747
  const ext = sourcePath.includes(".") ? sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() : "";
25281
25748
  if (!runOnChangeSet2.has(ext)) {
25282
- state59.skippedCount += 1;
25749
+ state58.skippedCount += 1;
25283
25750
  return;
25284
25751
  }
25285
- state59.invocationCount += 1;
25752
+ state58.invocationCount += 1;
25286
25753
  const result = await runTypeCheck(cfg);
25287
25754
  if (!result) {
25288
- state59.errorCount += 1;
25755
+ state58.errorCount += 1;
25289
25756
  return;
25290
25757
  }
25291
- state59.runCount += 1;
25292
- state59.lastResult = {
25758
+ state58.runCount += 1;
25759
+ state58.lastResult = {
25293
25760
  passed: result.passed,
25294
25761
  errorCount: result.errorCount,
25295
25762
  durationMs: result.durationMs,
25296
25763
  when: (/* @__PURE__ */ new Date()).toISOString()
25297
25764
  };
25298
25765
  if (result.passed) {
25299
- state59.passCount += 1;
25766
+ state58.passCount += 1;
25300
25767
  return;
25301
25768
  }
25302
- state59.failCount += 1;
25769
+ state58.failCount += 1;
25303
25770
  const errorList = result.errors.map((e) => ` \u274C ${e}`).join("\n");
25304
25771
  const message = `
25305
25772
  \u274C type-gate: Type check failed after editing ${sourcePath} (${result.durationMs}ms).
@@ -25314,7 +25781,7 @@ Fix the type error(s) or adjust the change.`;
25314
25781
  }
25315
25782
  return { additionalContext: message };
25316
25783
  };
25317
- state59.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook, {
25784
+ state58.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook, {
25318
25785
  background: true
25319
25786
  });
25320
25787
  api.tools.register({
@@ -25334,14 +25801,14 @@ Fix the type error(s) or adjust the change.`;
25334
25801
  maxErrors: cfg.maxErrors,
25335
25802
  runOnChange: cfg.runOnChange,
25336
25803
  counters: {
25337
- invocations: state59.invocationCount,
25338
- runs: state59.runCount,
25339
- passed: state59.passCount,
25340
- failed: state59.failCount,
25341
- errors: state59.errorCount,
25342
- skipped: state59.skippedCount
25804
+ invocations: state58.invocationCount,
25805
+ runs: state58.runCount,
25806
+ passed: state58.passCount,
25807
+ failed: state58.failCount,
25808
+ errors: state58.errorCount,
25809
+ skipped: state58.skippedCount
25343
25810
  },
25344
- lastResult: state59.lastResult
25811
+ lastResult: state58.lastResult
25345
25812
  };
25346
25813
  }
25347
25814
  });
@@ -25353,43 +25820,43 @@ Fix the type error(s) or adjust the change.`;
25353
25820
  });
25354
25821
  },
25355
25822
  teardown(api) {
25356
- if (state59.hookUnregister) {
25823
+ if (state58.hookUnregister) {
25357
25824
  try {
25358
- state59.hookUnregister();
25825
+ state58.hookUnregister();
25359
25826
  } catch {
25360
25827
  }
25361
- state59.hookUnregister = null;
25828
+ state58.hookUnregister = null;
25362
25829
  }
25363
25830
  const final = {
25364
- invocations: state59.invocationCount,
25365
- runs: state59.runCount,
25366
- passed: state59.passCount,
25367
- failed: state59.failCount,
25368
- errors: state59.errorCount,
25369
- skipped: state59.skippedCount
25831
+ invocations: state58.invocationCount,
25832
+ runs: state58.runCount,
25833
+ passed: state58.passCount,
25834
+ failed: state58.failCount,
25835
+ errors: state58.errorCount,
25836
+ skipped: state58.skippedCount
25370
25837
  };
25371
- state59.invocationCount = 0;
25372
- state59.runCount = 0;
25373
- state59.passCount = 0;
25374
- state59.failCount = 0;
25375
- state59.errorCount = 0;
25376
- state59.skippedCount = 0;
25377
- state59.lastResult = null;
25838
+ state58.invocationCount = 0;
25839
+ state58.runCount = 0;
25840
+ state58.passCount = 0;
25841
+ state58.failCount = 0;
25842
+ state58.errorCount = 0;
25843
+ state58.skippedCount = 0;
25844
+ state58.lastResult = null;
25378
25845
  api.log.info("type-gate: teardown complete", { final });
25379
25846
  },
25380
25847
  async health() {
25381
25848
  return {
25382
25849
  ok: true,
25383
- message: state59.lastResult ? `type-gate: ${state59.runCount} run(s), last ${state59.lastResult.passed ? "PASSED" : "FAILED"} (${state59.lastResult.errorCount} errors)` : `type-gate: ${state59.invocationCount} invocation(s), ${state59.runCount} run(s)`,
25850
+ message: state58.lastResult ? `type-gate: ${state58.runCount} run(s), last ${state58.lastResult.passed ? "PASSED" : "FAILED"} (${state58.lastResult.errorCount} errors)` : `type-gate: ${state58.invocationCount} invocation(s), ${state58.runCount} run(s)`,
25384
25851
  counters: {
25385
- invocations: state59.invocationCount,
25386
- runs: state59.runCount,
25387
- passed: state59.passCount,
25388
- failed: state59.failCount,
25389
- errors: state59.errorCount,
25390
- skipped: state59.skippedCount
25391
- },
25392
- lastResult: state59.lastResult
25852
+ invocations: state58.invocationCount,
25853
+ runs: state58.runCount,
25854
+ passed: state58.passCount,
25855
+ failed: state58.failCount,
25856
+ errors: state58.errorCount,
25857
+ skipped: state58.skippedCount
25858
+ },
25859
+ lastResult: state58.lastResult
25393
25860
  };
25394
25861
  }
25395
25862
  };