@wrongstack/plugins 1.0.8 → 1.0.10

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 (72) 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 +1534 -1080
  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/migration-planner/index.d.ts +1 -1
  39. package/dist/migration-planner.js +6 -2
  40. package/dist/notify-hub/index.d.ts +1 -1
  41. package/dist/notify-hub.js +12 -11
  42. package/dist/performance-regression-gate/index.d.ts +1 -1
  43. package/dist/performance-regression-gate.js +33 -13
  44. package/dist/pr-drafter/index.d.ts +10 -1
  45. package/dist/pr-drafter.js +57 -26
  46. package/dist/refactor-suggester/index.d.ts +1 -1
  47. package/dist/refactor-suggester.js +17 -4
  48. package/dist/release-notes-generator.js +2 -2
  49. package/dist/secret-scanner/index.d.ts +1 -1
  50. package/dist/secret-scanner.js +11 -3
  51. package/dist/security-hotspot-scanner/index.d.ts +1 -1
  52. package/dist/security-hotspot-scanner.js +6 -2
  53. package/dist/semantic-search-indexer/index.d.ts +1 -1
  54. package/dist/semantic-search-indexer.js +19 -3
  55. package/dist/semver-bump/index.d.ts +1 -1
  56. package/dist/semver-bump.js +57 -37
  57. package/dist/session-recap.js +4 -2
  58. package/dist/shell-check/index.d.ts +1 -1
  59. package/dist/shell-check.js +30 -39
  60. package/dist/smart-rename/index.d.ts +1 -1
  61. package/dist/smart-rename.js +23 -10
  62. package/dist/template-engine/index.d.ts +1 -1
  63. package/dist/template-engine.js +51 -38
  64. package/dist/test-flake-detector/index.d.ts +1 -1
  65. package/dist/test-flake-detector.js +11 -5
  66. package/dist/test-generator/index.d.ts +1 -1
  67. package/dist/test-generator.js +12 -8
  68. package/dist/todo-tracker/index.d.ts +68 -1
  69. package/dist/todo-tracker.js +579 -395
  70. package/dist/token-budget.js +7 -4
  71. package/dist/token-throttle.js +6 -3
  72. 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);
@@ -12839,6 +12963,7 @@ var loop_breaker_default = plugin35;
12839
12963
 
12840
12964
  // src/migration-planner/index.ts
12841
12965
  import { existsSync as existsSync5, readFileSync as readFileSync10 } from "node:fs";
12966
+ import { ToolValidationError as ToolValidationError17 } from "@wrongstack/core/types";
12842
12967
 
12843
12968
  // src/runtime/llm.ts
12844
12969
  import {
@@ -13107,14 +13232,14 @@ var plugin36 = {
13107
13232
  const rawPath = inp["path"] ?? inp["TargetFile"] ?? inp["filePath"] ?? inp["targetFile"] ?? inp["file_path"] ?? inp["file"];
13108
13233
  const path = typeof rawPath === "string" ? rawPath : void 0;
13109
13234
  if (!path) return;
13110
- const basename7 = path.split(/[/\\]/).pop() ?? "";
13235
+ const basename8 = path.split(/[/\\]/).pop() ?? "";
13111
13236
  if (!/^(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/i.test(
13112
- basename7
13237
+ basename8
13113
13238
  )) {
13114
13239
  return;
13115
13240
  }
13116
13241
  return {
13117
- additionalContext: `Manifest file ${basename7} changed. Consider running migration_plan if a dependency version was updated.`
13242
+ additionalContext: `Manifest file ${basename8} changed. Consider running migration_plan if a dependency version was updated.`
13118
13243
  };
13119
13244
  };
13120
13245
  state33.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook, {
@@ -13212,7 +13337,7 @@ var plugin36 = {
13212
13337
  category: "Planning",
13213
13338
  mutating: false,
13214
13339
  async execute(input, _ctx, execOpts) {
13215
- if (!cfg.enabled) return { ok: false, error: "migration-planner is disabled" };
13340
+ if (!cfg.enabled) throw new Error("migration-planner is disabled");
13216
13341
  execOpts?.signal?.throwIfAborted();
13217
13342
  const raw = input ?? {};
13218
13343
  const rawPackage = input.packageName || raw["package"] || raw["pkg"] || raw["name"] || raw["package_name"] || raw["packageName"] || raw["dependency"] || raw["dep"] || raw["module"];
@@ -13223,7 +13348,10 @@ var plugin36 = {
13223
13348
  const fromVersion = String(rawFrom ?? "").trim();
13224
13349
  const toVersion = String(rawTo ?? "").trim();
13225
13350
  if (!packageName || !fromVersion || !toVersion) {
13226
- return { ok: false, error: "packageName, fromVersion, and toVersion are required" };
13351
+ throw new ToolValidationError17({
13352
+ message: "packageName, fromVersion, and toVersion are required",
13353
+ field: !packageName ? "packageName" : !fromVersion ? "fromVersion" : "toVersion"
13354
+ });
13227
13355
  }
13228
13356
  const changelog = readChangelog(packageName, cfg);
13229
13357
  let breakingChanges;
@@ -13593,6 +13721,7 @@ var model_router_default = plugin37;
13593
13721
 
13594
13722
  // src/notify-hub/index.ts
13595
13723
  import { lookup } from "node:dns/promises";
13724
+ import { ToolValidationError as ToolValidationError18 } from "@wrongstack/core/types";
13596
13725
 
13597
13726
  // src/notify-hub/webhook-channel.ts
13598
13727
  function freshCircuit() {
@@ -13995,18 +14124,19 @@ var plugin38 = {
13995
14124
  category: "Notifications",
13996
14125
  mutating: true,
13997
14126
  async execute(input) {
13998
- if (!cfg.enabled) return { ok: false, error: "notify-hub is disabled" };
14127
+ if (!cfg.enabled) throw new Error("notify-hub is disabled");
13999
14128
  const ch = state35.channel;
14000
14129
  if (!ch) {
14001
- return {
14002
- ok: false,
14003
- error: 'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
14004
- };
14130
+ throw new Error(
14131
+ 'no webhookUrl configured \u2014 set config.extensions["notify-hub"].webhookUrl to enable deliveries'
14132
+ );
14005
14133
  }
14006
14134
  const inp = input ?? {};
14007
14135
  const rawMsg = inp["message"] ?? inp["body"] ?? inp["text"] ?? inp["content"] ?? inp["msg"];
14008
14136
  const message = typeof rawMsg === "string" && rawMsg.trim().length > 0 ? rawMsg.trim() : "";
14009
- if (!message) return { ok: false, error: "message is required" };
14137
+ if (!message) {
14138
+ throw new ToolValidationError18({ message: "message is required", field: "message" });
14139
+ }
14010
14140
  const rawTitle = inp["title"] ?? inp["subject"] ?? inp["header"];
14011
14141
  const title = typeof rawTitle === "string" && rawTitle.trim() ? rawTitle.trim() : "WrongStack notification";
14012
14142
  const result = await deliverViaChannel(ch, "manual", {
@@ -14015,11 +14145,10 @@ var plugin38 = {
14015
14145
  level: input.level === "warning" || input.level === "critical" ? input.level : "info",
14016
14146
  source: "manual"
14017
14147
  });
14018
- return {
14019
- ok: result.ok,
14020
- circuitOpen: ch.circuitStatus().open,
14021
- ...result.ok ? {} : { error: result.error ?? "delivery failed" }
14022
- };
14148
+ if (!result.ok) {
14149
+ throw new Error(`notification delivery failed: ${result.error ?? "unknown error"}`);
14150
+ }
14151
+ return { ok: true, circuitOpen: ch.circuitStatus().open };
14023
14152
  }
14024
14153
  });
14025
14154
  api.tools.register({
@@ -14238,8 +14367,8 @@ function isRootPathScope(path) {
14238
14367
  function isDirectoryAmbiguousPath(path) {
14239
14368
  const normalized = normalizePath2(path).replace(/\/$/, "");
14240
14369
  if (isRootPathScope(normalized)) return true;
14241
- const basename7 = normalized.slice(normalized.lastIndexOf("/") + 1);
14242
- return path.endsWith("/") || basename7.length > 0 && !basename7.includes(".");
14370
+ const basename8 = normalized.slice(normalized.lastIndexOf("/") + 1);
14371
+ return path.endsWith("/") || basename8.length > 0 && !basename8.includes(".");
14243
14372
  }
14244
14373
  function hasConfiguredProtectedDescendant(path, patterns) {
14245
14374
  const normalized = normalizePath2(path).replace(/\/$/, "").toLowerCase();
@@ -15516,9 +15645,9 @@ var plugin39 = {
15516
15645
  } catch {
15517
15646
  }
15518
15647
  }
15519
- const state60 = createState();
15520
- states.set(api, state60);
15521
- latestState = state60;
15648
+ const state59 = createState();
15649
+ states.set(api, state59);
15650
+ latestState = state59;
15522
15651
  const cfg = readConfig34(api.config.extensions?.["path-guard"]);
15523
15652
  const protectRes = cfg.protect.map(compilePathGlob);
15524
15653
  const allowRes = cfg.allow.map(compilePathGlob);
@@ -15526,15 +15655,15 @@ var plugin39 = {
15526
15655
  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
15656
  const matchContext = isScope ? 'its unresolved scope overlaps config.extensions["path-guard"].protect' : 'matched by config.extensions["path-guard"].protect';
15528
15657
  if (cfg.mode === "block") {
15529
- state60.blocks += 1;
15530
- state60.lastBlock = { path, tool, when: (/* @__PURE__ */ new Date()).toISOString() };
15658
+ state59.blocks += 1;
15659
+ state59.lastBlock = { path, tool, when: (/* @__PURE__ */ new Date()).toISOString() };
15531
15660
  api.metrics.counter("blocks");
15532
15661
  return {
15533
15662
  decision: "block",
15534
15663
  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
15664
  };
15536
15665
  }
15537
- state60.warns += 1;
15666
+ state59.warns += 1;
15538
15667
  api.metrics.counter("warns");
15539
15668
  return {
15540
15669
  decision: "allow",
@@ -15542,12 +15671,12 @@ var plugin39 = {
15542
15671
  };
15543
15672
  };
15544
15673
  const bumpRedos = () => {
15545
- state60.redosTimeouts += 1;
15674
+ state59.redosTimeouts += 1;
15546
15675
  api.metrics.counter("redos_timeouts");
15547
15676
  };
15548
15677
  const hook = async (input) => {
15549
15678
  if (!cfg.enabled) return;
15550
- state60.invocations += 1;
15679
+ state59.invocations += 1;
15551
15680
  const toolName = input.toolName ?? "";
15552
15681
  const ti = input.toolInput ?? {};
15553
15682
  const command = typeof ti["command"] === "string" ? ti["command"] : "";
@@ -15633,7 +15762,7 @@ var plugin39 = {
15633
15762
  }
15634
15763
  return;
15635
15764
  };
15636
- state60.hookUnregister = api.registerHook("PreToolUse", "*", hook, {
15765
+ state59.hookUnregister = api.registerHook("PreToolUse", "*", hook, {
15637
15766
  name: "path-guard",
15638
15767
  stage: "validate",
15639
15768
  failurePolicy: "closed",
@@ -15654,12 +15783,12 @@ var plugin39 = {
15654
15783
  protect: cfg.protect,
15655
15784
  allow: cfg.allow,
15656
15785
  counters: {
15657
- invocations: state60.invocations,
15658
- blocks: state60.blocks,
15659
- warns: state60.warns,
15660
- redosTimeouts: state60.redosTimeouts
15786
+ invocations: state59.invocations,
15787
+ blocks: state59.blocks,
15788
+ warns: state59.warns,
15789
+ redosTimeouts: state59.redosTimeouts
15661
15790
  },
15662
- lastBlock: state60.lastBlock
15791
+ lastBlock: state59.lastBlock
15663
15792
  };
15664
15793
  }
15665
15794
  });
@@ -15671,39 +15800,39 @@ var plugin39 = {
15671
15800
  });
15672
15801
  },
15673
15802
  teardown(api) {
15674
- const state60 = states.get(api);
15675
- if (!state60) return;
15676
- if (state60.hookUnregister) {
15803
+ const state59 = states.get(api);
15804
+ if (!state59) return;
15805
+ if (state59.hookUnregister) {
15677
15806
  try {
15678
- state60.hookUnregister();
15807
+ state59.hookUnregister();
15679
15808
  } catch {
15680
15809
  }
15681
- state60.hookUnregister = null;
15810
+ state59.hookUnregister = null;
15682
15811
  }
15683
15812
  const final = {
15684
- invocations: state60.invocations,
15685
- blocks: state60.blocks,
15686
- warns: state60.warns,
15687
- redosTimeouts: state60.redosTimeouts
15813
+ invocations: state59.invocations,
15814
+ blocks: state59.blocks,
15815
+ warns: state59.warns,
15816
+ redosTimeouts: state59.redosTimeouts
15688
15817
  };
15689
- state60.invocations = 0;
15690
- state60.blocks = 0;
15691
- state60.warns = 0;
15692
- state60.redosTimeouts = 0;
15693
- state60.lastBlock = null;
15818
+ state59.invocations = 0;
15819
+ state59.blocks = 0;
15820
+ state59.warns = 0;
15821
+ state59.redosTimeouts = 0;
15822
+ state59.lastBlock = null;
15694
15823
  states.delete(api);
15695
15824
  api.log.info("path-guard: teardown complete", { final });
15696
15825
  },
15697
15826
  async health() {
15698
- const state60 = latestState;
15827
+ const state59 = latestState;
15699
15828
  return {
15700
15829
  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}`,
15830
+ 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
15831
  counters: {
15703
- invocations: state60.invocations,
15704
- blocks: state60.blocks,
15705
- warns: state60.warns,
15706
- redosTimeouts: state60.redosTimeouts
15832
+ invocations: state59.invocations,
15833
+ blocks: state59.blocks,
15834
+ warns: state59.warns,
15835
+ redosTimeouts: state59.redosTimeouts
15707
15836
  }
15708
15837
  };
15709
15838
  }
@@ -15713,6 +15842,7 @@ var path_guard_default = plugin39;
15713
15842
  // src/performance-regression-gate/index.ts
15714
15843
  import { existsSync as existsSync7, readFileSync as readFileSync11 } from "node:fs";
15715
15844
  import { isAbsolute as isAbsolute16, relative as relative16, resolve as resolve18 } from "node:path";
15845
+ import { ToolValidationError as ToolValidationError19 } from "@wrongstack/core/types";
15716
15846
  var API_VERSION26 = "^0.1.10";
15717
15847
  var state36 = {
15718
15848
  invocationCount: 0,
@@ -15779,10 +15909,11 @@ function flattenResults(results) {
15779
15909
  function loadResults(path) {
15780
15910
  if (!path || !existsSync7(path)) return null;
15781
15911
  try {
15782
- const raw = JSON.parse(readFileSync11(path, "utf-8"));
15783
- return raw;
15784
- } catch {
15785
- return null;
15912
+ return JSON.parse(readFileSync11(path, "utf-8"));
15913
+ } catch (err) {
15914
+ throw new Error(`Could not read benchmark results at ${path}: ${String(err)}`, {
15915
+ cause: err
15916
+ });
15786
15917
  }
15787
15918
  }
15788
15919
  function stripVariantSuffix(name) {
@@ -15909,7 +16040,7 @@ var plugin40 = {
15909
16040
  mutating: false,
15910
16041
  async execute(input) {
15911
16042
  if (!cfg.enabled) {
15912
- return { ok: false, error: "performance-regression-gate is disabled" };
16043
+ throw new Error("performance-regression-gate is disabled");
15913
16044
  }
15914
16045
  state36.invocationCount += 1;
15915
16046
  const raw = input ?? {};
@@ -15920,11 +16051,23 @@ var plugin40 = {
15920
16051
  const resultsPath = resolveProjectPath6(resultsPathStr) ?? "";
15921
16052
  if (!resultsPath) {
15922
16053
  state36.errorCount += 1;
15923
- return { ok: false, error: "invalid results path (must be inside project)" };
16054
+ throw new ToolValidationError19({
16055
+ message: "invalid results path (must be inside project)",
16056
+ field: "resultsPath"
16057
+ });
16058
+ }
16059
+ let results;
16060
+ try {
16061
+ results = loadResults(resultsPath);
16062
+ } catch (err) {
16063
+ state36.errorCount += 1;
16064
+ throw err;
15924
16065
  }
15925
- const results = loadResults(resultsPath);
15926
16066
  if (!results) {
15927
16067
  state36.missingResultsCount += 1;
16068
+ if (resultsPathStr !== "bench-results.json" || rawResultsPath !== "bench-results.json") {
16069
+ throw new Error(`No benchmark results found at ${resultsPathStr}.`);
16070
+ }
15928
16071
  return {
15929
16072
  ok: true,
15930
16073
  hasResults: false,
@@ -15953,15 +16096,21 @@ var plugin40 = {
15953
16096
  const baselineResolved = resolveProjectPath6(baselinePathStr) ?? "";
15954
16097
  if (!baselineResolved) {
15955
16098
  state36.errorCount += 1;
15956
- return { ok: false, error: "invalid baseline path (must be inside project)" };
16099
+ throw new ToolValidationError19({
16100
+ message: "invalid baseline path (must be inside project)",
16101
+ field: "baselinePath"
16102
+ });
16103
+ }
16104
+ let baselineResults;
16105
+ try {
16106
+ baselineResults = loadResults(baselineResolved);
16107
+ } catch (err) {
16108
+ state36.errorCount += 1;
16109
+ throw err;
15957
16110
  }
15958
- const baselineResults = loadResults(baselineResolved);
15959
16111
  if (!baselineResults) {
15960
16112
  state36.errorCount += 1;
15961
- return {
15962
- ok: false,
15963
- error: `Could not read baseline results at ${baselinePathStr}.`
15964
- };
16113
+ throw new Error(`Could not read baseline results at ${baselinePathStr}.`);
15965
16114
  }
15966
16115
  const baseline = flattenResults(baselineResults);
15967
16116
  pairs = pairCrossFile(baseline, current);
@@ -16176,6 +16325,7 @@ var plugin_stack_observer_default = PLUGIN;
16176
16325
  import { execFile as execFile9 } from "node:child_process";
16177
16326
  import { mkdir as mkdir2, writeFile as writeFile4 } from "node:fs/promises";
16178
16327
  import { dirname as dirname7, isAbsolute as isAbsolute17, relative as relative17, resolve as resolve19 } from "node:path";
16328
+ import { ToolValidationError as ToolValidationError20 } from "@wrongstack/core/types";
16179
16329
  var API_VERSION27 = "^0.1.10";
16180
16330
  var state38 = {
16181
16331
  commits: [],
@@ -16188,8 +16338,23 @@ var state38 = {
16188
16338
  draftErrors: 0,
16189
16339
  stopInvocations: 0,
16190
16340
  stopHookUnregister: null,
16341
+ postHookUnregister: null,
16191
16342
  eventUnsubscribers: []
16192
16343
  };
16344
+ var TRACKED_FILE_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "write_to_file", "replace_file_content"]);
16345
+ function parseAutocommitResult(content) {
16346
+ const hash = /"hash":\s*"([0-9a-f]{7,64})"/.exec(content)?.[1];
16347
+ if (!hash) return null;
16348
+ const rawMessage = /"message":\s*("(?:[^"\\]|\\.)*")/.exec(content)?.[1];
16349
+ let message = "commit";
16350
+ if (rawMessage) {
16351
+ try {
16352
+ message = JSON.parse(rawMessage);
16353
+ } catch {
16354
+ }
16355
+ }
16356
+ return { hash, message };
16357
+ }
16193
16358
  var DEFAULTS36 = {
16194
16359
  enabled: true,
16195
16360
  outputPath: ".wrongstack/PR_DRAFT.md",
@@ -16372,6 +16537,7 @@ var plugin41 = {
16372
16537
  state38.draftErrors = 0;
16373
16538
  state38.stopInvocations = 0;
16374
16539
  state38.stopHookUnregister = (0, runtime_exports.releaseHandle)(state38.stopHookUnregister);
16540
+ state38.postHookUnregister = (0, runtime_exports.releaseHandle)(state38.postHookUnregister);
16375
16541
  for (const off of state38.eventUnsubscribers) {
16376
16542
  try {
16377
16543
  off();
@@ -16381,22 +16547,24 @@ var plugin41 = {
16381
16547
  state38.eventUnsubscribers = [];
16382
16548
  const cfg = readConfig37(api.config.extensions?.["pr-drafter"]);
16383
16549
  if (api.onPattern) {
16384
- const offTool = api.onPattern("tool.completed", (_event, payload) => {
16385
- const p = payload;
16386
- const toolName = p?.tool;
16550
+ const offTool = api.onPattern("tool.completed", () => {
16387
16551
  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
16552
  });
16398
16553
  state38.eventUnsubscribers.push(offTool);
16399
16554
  }
16555
+ const postHook = (input) => {
16556
+ if (!input.toolResult || input.toolResult.isError) return;
16557
+ const toolName = input.toolName;
16558
+ if (toolName === "git_autocommit") {
16559
+ const commit = parseAutocommitResult(String(input.toolResult.content ?? ""));
16560
+ if (commit) state38.commits.push(commit.message);
16561
+ return;
16562
+ }
16563
+ if (!toolName || !TRACKED_FILE_TOOLS.has(toolName)) return;
16564
+ const rawInput = input.toolInput ?? {};
16565
+ const filePath = ["path", "filePath", "file_path", "TargetFile", "targetFile", "file"].map((key) => rawInput[key]).find((value) => typeof value === "string" && value.length > 0);
16566
+ if (filePath) state38.files.add(filePath);
16567
+ };
16400
16568
  if (api.onEvent) {
16401
16569
  const offUsage = api.onEvent("provider.response", (payload) => {
16402
16570
  const p = payload;
@@ -16415,6 +16583,11 @@ var plugin41 = {
16415
16583
  await writeDraft(cfg, api.llm);
16416
16584
  };
16417
16585
  state38.stopHookUnregister = api.registerHook("Stop", void 0, stopHook);
16586
+ state38.postHookUnregister = api.registerHook(
16587
+ "PostToolUse",
16588
+ "git_autocommit|write|edit|write_to_file|replace_file_content",
16589
+ postHook
16590
+ );
16418
16591
  api.tools.register({
16419
16592
  name: "pr_draft",
16420
16593
  description: "Generate or refresh the pull-request draft for the current session. Writes the markdown file and returns its path + title.",
@@ -16437,7 +16610,7 @@ var plugin41 = {
16437
16610
  mutating: true,
16438
16611
  capabilities: ["fs.write"],
16439
16612
  async execute(input = {}) {
16440
- if (!cfg.enabled) return { ok: false, error: "pr-drafter is disabled" };
16613
+ if (!cfg.enabled) throw new Error("pr-drafter is disabled");
16441
16614
  const raw = input ?? {};
16442
16615
  const preview = Boolean(
16443
16616
  input?.preview ?? raw["dryRun"] ?? raw["dry_run"] ?? raw["dry"] ?? raw["previewOnly"]
@@ -16449,24 +16622,29 @@ var plugin41 = {
16449
16622
  return { ok: true, preview: true, title: draft.title, body: draft.body };
16450
16623
  }
16451
16624
  const resolved = resolveProjectPath7(outputPathStr);
16452
- if (!resolved) return { ok: false, error: "outputPath resolves outside project" };
16625
+ if (!resolved) {
16626
+ throw new ToolValidationError20({
16627
+ message: "outputPath resolves outside project",
16628
+ field: "outputPath"
16629
+ });
16630
+ }
16453
16631
  try {
16454
16632
  await mkdir2(dirname7(resolved), { recursive: true });
16455
16633
  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
16634
  } catch (err) {
16464
16635
  state38.draftErrors += 1;
16465
- return {
16466
- ok: false,
16467
- error: err instanceof Error ? err.message : String(err)
16468
- };
16636
+ throw new Error(
16637
+ `Could not write PR draft to ${outputPathStr}: ${err instanceof Error ? err.message : String(err)}`,
16638
+ { cause: err }
16639
+ );
16469
16640
  }
16641
+ state38.draftsWritten += 1;
16642
+ return {
16643
+ ok: true,
16644
+ path: outputPathStr,
16645
+ resolvedPath: resolved,
16646
+ title: draft.title
16647
+ };
16470
16648
  }
16471
16649
  });
16472
16650
  api.log.info("pr-drafter plugin loaded", {
@@ -16483,6 +16661,7 @@ var plugin41 = {
16483
16661
  }
16484
16662
  state38.stopHookUnregister = null;
16485
16663
  }
16664
+ state38.postHookUnregister = (0, runtime_exports.releaseHandle)(state38.postHookUnregister);
16486
16665
  for (const off of state38.eventUnsubscribers) {
16487
16666
  try {
16488
16667
  off();
@@ -17194,8 +17373,9 @@ var plugin43 = {
17194
17373
  var prompt_firewall_default = plugin43;
17195
17374
 
17196
17375
  // src/refactor-suggester/index.ts
17197
- import { readFile as readFile12 } from "node:fs/promises";
17376
+ import { readFile as readFile12, stat as stat9 } from "node:fs/promises";
17198
17377
  import { isAbsolute as isAbsolute18, relative as relative18, resolve as resolve20 } from "node:path";
17378
+ import { ToolValidationError as ToolValidationError21 } from "@wrongstack/core/types";
17199
17379
  var API_VERSION28 = "^0.1.10";
17200
17380
  var HOOK_WARNING_COOLDOWN_MS2 = 6e4;
17201
17381
  var state41 = {
@@ -17485,11 +17665,23 @@ var plugin44 = {
17485
17665
  category: "Diagnostics",
17486
17666
  mutating: false,
17487
17667
  async execute(input) {
17488
- if (!cfg.enabled) return { ok: false, error: "refactor-suggester is disabled" };
17668
+ if (!cfg.enabled) throw new Error("refactor-suggester is disabled");
17489
17669
  const raw = input ?? {};
17490
17670
  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
17671
  if (!(0, runtime_exports.withinProject)(rawPath)) {
17492
- return { ok: false, error: "path is outside the project root" };
17672
+ throw new ToolValidationError21({
17673
+ message: "path is outside the project root",
17674
+ field: "path"
17675
+ });
17676
+ }
17677
+ try {
17678
+ await stat9(resolve20(process.cwd(), rawPath));
17679
+ } catch (err) {
17680
+ throw new ToolValidationError21({
17681
+ message: `path does not exist or cannot be read: ${rawPath}`,
17682
+ field: "path",
17683
+ cause: err
17684
+ });
17493
17685
  }
17494
17686
  state41.scanCount += 1;
17495
17687
  let result;
@@ -17497,7 +17689,7 @@ var plugin44 = {
17497
17689
  result = await scanPath4(rawPath, cfg);
17498
17690
  } catch (err) {
17499
17691
  state41.errorCount += 1;
17500
- return { ok: false, error: String(err) };
17692
+ throw new Error(`refactor scan failed: ${String(err)}`, { cause: err });
17501
17693
  }
17502
17694
  state41.suggestionCount += result.suggestions.length;
17503
17695
  return {
@@ -17858,7 +18050,7 @@ var plugin45 = {
17858
18050
  category: "Development",
17859
18051
  mutating: false,
17860
18052
  async execute(input, _ctx, execOpts) {
17861
- if (!cfg.enabled) return { ok: false, error: "release-notes-generator is disabled" };
18053
+ if (!cfg.enabled) throw new Error("release-notes-generator is disabled");
17862
18054
  execOpts?.signal?.throwIfAborted();
17863
18055
  const raw = input ?? {};
17864
18056
  const rawTo = input.to ?? raw["to_ref"] ?? raw["toRef"] ?? raw["until"] ?? raw["end"];
@@ -17873,7 +18065,7 @@ var plugin45 = {
17873
18065
  commits = await getCommits(fromRef, toRef, execOpts?.signal);
17874
18066
  } catch (err) {
17875
18067
  state42.errorCount += 1;
17876
- return { ok: false, error: String(err) };
18068
+ throw new Error(`Could not read git history: ${String(err)}`, { cause: err });
17877
18069
  }
17878
18070
  state42.commitCount += commits.length;
17879
18071
  execOpts?.signal?.throwIfAborted();
@@ -18288,6 +18480,7 @@ ${body}${suffix}`;
18288
18480
  var schema_evolution_guard_default = plugin46;
18289
18481
 
18290
18482
  // src/secret-scanner/index.ts
18483
+ import { ToolValidationError as ToolValidationError22 } from "@wrongstack/core/types";
18291
18484
  var BASE_PATTERNS = cloneCredentialPatterns();
18292
18485
  var PATTERNS3 = [...BASE_PATTERNS];
18293
18486
  var GROUP_INDEX_OF_PATTERN = [];
@@ -18496,7 +18689,7 @@ function readConfig43(raw) {
18496
18689
  function buildHook(cfg, log, runtime) {
18497
18690
  return (input) => {
18498
18691
  activateRuntime(runtime);
18499
- const { state: state60 } = runtime;
18692
+ const { state: state59 } = runtime;
18500
18693
  if (!cfg.enabled) return;
18501
18694
  const toolName = input.toolName ?? "unknown";
18502
18695
  let matched;
@@ -18504,7 +18697,7 @@ function buildHook(cfg, log, runtime) {
18504
18697
  matched = scanInput(input.toolInput);
18505
18698
  } catch (err) {
18506
18699
  if (String(err).includes("ReDoS")) {
18507
- state60.timeoutCount += 1;
18700
+ state59.timeoutCount += 1;
18508
18701
  return {
18509
18702
  decision: "block",
18510
18703
  reason: "secret-scanner: ReDoS timeout \u2014 regex scan exceeded the wall-clock budget. Fail-closed: treated as a block."
@@ -18516,8 +18709,8 @@ function buildHook(cfg, log, runtime) {
18516
18709
  const summary = matched.join(", ");
18517
18710
  const when = (/* @__PURE__ */ new Date()).toISOString();
18518
18711
  if (cfg.mode === "block") {
18519
- state60.blockCount += 1;
18520
- state60.lastBlock = { toolName, matchedTypes: matched, when };
18712
+ state59.blockCount += 1;
18713
+ state59.lastBlock = { toolName, matchedTypes: matched, when };
18521
18714
  log.warn(`[secret-scanner] blocked ${toolName} \u2014 matched: ${summary}`);
18522
18715
  return {
18523
18716
  decision: "block",
@@ -18527,7 +18720,7 @@ function buildHook(cfg, log, runtime) {
18527
18720
  if (cfg.mode === "redact") {
18528
18721
  const redacted = redactInput(input.toolInput);
18529
18722
  if (redacted.ok && redacted.value !== null && typeof redacted.value === "object" && !Array.isArray(redacted.value)) {
18530
- state60.redactCount += 1;
18723
+ state59.redactCount += 1;
18531
18724
  log.info(`[secret-scanner] redacted ${toolName} \u2014 matched: ${summary}`);
18532
18725
  return {
18533
18726
  decision: "allow",
@@ -18535,15 +18728,15 @@ function buildHook(cfg, log, runtime) {
18535
18728
  additionalContext: `secret-scanner: redacted ${matched.length} credential pattern(s) from the ${toolName} arguments before execution.`
18536
18729
  };
18537
18730
  }
18538
- state60.blockCount += 1;
18539
- state60.lastBlock = { toolName, matchedTypes: matched, when };
18731
+ state59.blockCount += 1;
18732
+ state59.lastBlock = { toolName, matchedTypes: matched, when };
18540
18733
  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
18734
  return {
18542
18735
  decision: "block",
18543
18736
  reason: `secret-scanner: cannot safely redact '${toolName}' because ${detail}; refusing to run.`
18544
18737
  };
18545
18738
  }
18546
- state60.allowCount += 1;
18739
+ state59.allowCount += 1;
18547
18740
  log.warn(
18548
18741
  `[secret-scanner] allow-mode: ${toolName} matched ${summary} but mode='allow' lets it through.`
18549
18742
  );
@@ -18553,7 +18746,7 @@ function buildHook(cfg, log, runtime) {
18553
18746
  function buildPostHook(cfg, log, runtime) {
18554
18747
  return (input) => {
18555
18748
  activateRuntime(runtime);
18556
- const { state: state60 } = runtime;
18749
+ const { state: state59 } = runtime;
18557
18750
  if (!cfg.enabled) return;
18558
18751
  const result = input.toolResult;
18559
18752
  if (!result || typeof result.content !== "string") return;
@@ -18573,8 +18766,8 @@ function buildPostHook(cfg, log, runtime) {
18573
18766
  }
18574
18767
  const summary = credentialMatches.join(", ");
18575
18768
  const when = (/* @__PURE__ */ new Date()).toISOString();
18576
- state60.leakCount += 1;
18577
- state60.lastLeak = { toolName, matchedTypes: credentialMatches, when };
18769
+ state59.leakCount += 1;
18770
+ state59.lastLeak = { toolName, matchedTypes: credentialMatches, when };
18578
18771
  log.warn(`[secret-scanner] POST-TOOL LEAK: ${toolName} output matched ${summary}`);
18579
18772
  return {
18580
18773
  additionalContext: `
@@ -18652,13 +18845,13 @@ var plugin47 = {
18652
18845
  };
18653
18846
  runtimes.set(api, runtime);
18654
18847
  latestRuntime = runtime;
18655
- const { state: state60 } = runtime;
18848
+ const { state: state59 } = runtime;
18656
18849
  const log = {
18657
18850
  warn: (msg, ...rest) => api.log.warn(msg, ...rest),
18658
18851
  info: (msg, ...rest) => api.log.info(msg, ...rest)
18659
18852
  };
18660
18853
  const hook = buildHook(cfg, log, runtime);
18661
- state60.hookUnregister = api.registerHook("PreToolUse", cfg.matcher, hook, {
18854
+ state59.hookUnregister = api.registerHook("PreToolUse", cfg.matcher, hook, {
18662
18855
  name: "secret-scanner",
18663
18856
  // Redaction rewrites arguments; block/allow modes must inspect the final
18664
18857
  // result after every mutator has run so a later rewrite cannot smuggle a
@@ -18668,7 +18861,7 @@ var plugin47 = {
18668
18861
  policy: true
18669
18862
  });
18670
18863
  const postHook = buildPostHook(cfg, log, runtime);
18671
- state60.postHookUnregister = api.registerHook("PostToolUse", cfg.postToolUseMatcher, postHook);
18864
+ state59.postHookUnregister = api.registerHook("PostToolUse", cfg.postToolUseMatcher, postHook);
18672
18865
  api.tools.register({
18673
18866
  name: "secret_scanner_status",
18674
18867
  description: "Reports the current secret-scanner state: pattern count, last block (if any), and per-mode invocation counters.",
@@ -18686,14 +18879,14 @@ var plugin47 = {
18686
18879
  patternCount: PATTERNS3.length,
18687
18880
  patternTypes: PATTERNS3.map((p) => p.type),
18688
18881
  counters: {
18689
- block: state60.blockCount,
18690
- redact: state60.redactCount,
18691
- allow: state60.allowCount,
18692
- leak: state60.leakCount,
18693
- timeoutCount: state60.timeoutCount
18882
+ block: state59.blockCount,
18883
+ redact: state59.redactCount,
18884
+ allow: state59.allowCount,
18885
+ leak: state59.leakCount,
18886
+ timeoutCount: state59.timeoutCount
18694
18887
  },
18695
- lastBlock: state60.lastBlock,
18696
- lastLeak: state60.lastLeak
18888
+ lastBlock: state59.lastBlock,
18889
+ lastLeak: state59.lastLeak
18697
18890
  };
18698
18891
  }
18699
18892
  });
@@ -18711,9 +18904,14 @@ var plugin47 = {
18711
18904
  mutating: false,
18712
18905
  async execute(input) {
18713
18906
  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);
18907
+ const rawText = input["text"] ?? input["content"] ?? input["string"] ?? input["input"] ?? input["code"] ?? input["value"];
18908
+ if (typeof rawText !== "string") {
18909
+ throw new ToolValidationError22({
18910
+ message: "text is required and must be a string",
18911
+ field: "text"
18912
+ });
18913
+ }
18914
+ const matched = findMatches(rawText);
18717
18915
  return {
18718
18916
  ok: true,
18719
18917
  matched,
@@ -18731,57 +18929,58 @@ var plugin47 = {
18731
18929
  teardown(api) {
18732
18930
  const runtime = runtimes.get(api);
18733
18931
  if (!runtime) return;
18734
- const { state: state60 } = runtime;
18735
- if (state60.hookUnregister) {
18932
+ const { state: state59 } = runtime;
18933
+ if (state59.hookUnregister) {
18736
18934
  try {
18737
- state60.hookUnregister();
18935
+ state59.hookUnregister();
18738
18936
  } catch {
18739
18937
  }
18740
- state60.hookUnregister = null;
18938
+ state59.hookUnregister = null;
18741
18939
  }
18742
- if (state60.postHookUnregister) {
18940
+ if (state59.postHookUnregister) {
18743
18941
  try {
18744
- state60.postHookUnregister();
18942
+ state59.postHookUnregister();
18745
18943
  } catch {
18746
18944
  }
18747
- state60.postHookUnregister = null;
18945
+ state59.postHookUnregister = null;
18748
18946
  }
18749
18947
  const finalCounters = {
18750
- block: state60.blockCount,
18751
- redact: state60.redactCount,
18752
- allow: state60.allowCount,
18753
- leak: state60.leakCount
18948
+ block: state59.blockCount,
18949
+ redact: state59.redactCount,
18950
+ allow: state59.allowCount,
18951
+ leak: state59.leakCount
18754
18952
  };
18755
- state60.blockCount = 0;
18756
- state60.redactCount = 0;
18757
- state60.allowCount = 0;
18758
- state60.leakCount = 0;
18759
- state60.lastBlock = null;
18760
- state60.lastLeak = null;
18953
+ state59.blockCount = 0;
18954
+ state59.redactCount = 0;
18955
+ state59.allowCount = 0;
18956
+ state59.leakCount = 0;
18957
+ state59.lastBlock = null;
18958
+ state59.lastLeak = null;
18761
18959
  runtimes.delete(api);
18762
18960
  api.log.info("secret-scanner: teardown complete", { counters: finalCounters });
18763
18961
  },
18764
18962
  async health() {
18765
- const state60 = latestRuntime?.state ?? createState2();
18963
+ const state59 = latestRuntime?.state ?? createState2();
18766
18964
  return {
18767
18965
  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`,
18966
+ 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
18967
  counters: {
18770
- block: state60.blockCount,
18771
- redact: state60.redactCount,
18772
- allow: state60.allowCount,
18773
- leak: state60.leakCount
18968
+ block: state59.blockCount,
18969
+ redact: state59.redactCount,
18970
+ allow: state59.allowCount,
18971
+ leak: state59.leakCount
18774
18972
  },
18775
- lastBlock: state60.lastBlock,
18776
- lastLeak: state60.lastLeak
18973
+ lastBlock: state59.lastBlock,
18974
+ lastLeak: state59.lastLeak
18777
18975
  };
18778
18976
  }
18779
18977
  };
18780
18978
  var secret_scanner_default = plugin47;
18781
18979
 
18782
18980
  // src/security-hotspot-scanner/index.ts
18783
- import { readdir as readdir2, readFile as readFile13, stat as stat5 } from "node:fs/promises";
18981
+ import { readdir as readdir2, readFile as readFile13, stat as stat10 } from "node:fs/promises";
18784
18982
  import { extname as extname5, isAbsolute as isAbsolute19, relative as relative19, resolve as resolve21 } from "node:path";
18983
+ import { ToolValidationError as ToolValidationError23 } from "@wrongstack/core/types";
18785
18984
  var API_VERSION31 = "^0.1.10";
18786
18985
  var state44 = {
18787
18986
  scanCount: 0,
@@ -18929,7 +19128,7 @@ async function scanPath5(inputPath, cfg) {
18929
19128
  if (allFindings.length >= cfg.maxFindings) return;
18930
19129
  const full = resolve21(dir, entry);
18931
19130
  try {
18932
- const st = await stat5(full);
19131
+ const st = await stat10(full);
18933
19132
  if (st.isDirectory()) {
18934
19133
  if (entry.startsWith(".") || entry === "node_modules" || entry === "dist" || entry === "coverage") {
18935
19134
  continue;
@@ -18943,7 +19142,7 @@ async function scanPath5(inputPath, cfg) {
18943
19142
  }
18944
19143
  };
18945
19144
  try {
18946
- const st = await stat5(resolved);
19145
+ const st = await stat10(resolved);
18947
19146
  if (st.isDirectory()) {
18948
19147
  await walk(resolved);
18949
19148
  } else if (st.isFile()) {
@@ -19095,7 +19294,7 @@ Review or remove the risky pattern(s).`;
19095
19294
  category: "Security",
19096
19295
  mutating: false,
19097
19296
  async execute(input) {
19098
- if (!cfg.enabled) return { ok: false, error: "security-hotspot-scanner is disabled" };
19297
+ if (!cfg.enabled) throw new Error("security-hotspot-scanner is disabled");
19099
19298
  const raw = input;
19100
19299
  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
19300
  const result = await scanPath5(targetPath, cfg);
@@ -19103,7 +19302,10 @@ Review or remove the risky pattern(s).`;
19103
19302
  state44.fileScanCount += result.filesScanned;
19104
19303
  state44.findingCount += result.findings.length;
19105
19304
  if (!result.scanned) {
19106
- return { ok: false, error: result.error, path: targetPath };
19305
+ throw new ToolValidationError23({
19306
+ message: `cannot scan ${targetPath}: ${result.error ?? "unknown error"}`,
19307
+ field: "path"
19308
+ });
19107
19309
  }
19108
19310
  state44.lastResult = {
19109
19311
  path: result.path,
@@ -19201,6 +19403,7 @@ var security_hotspot_scanner_default = plugin48;
19201
19403
  // src/semantic-search-indexer/index.ts
19202
19404
  import * as fs2 from "node:fs/promises";
19203
19405
  import { isAbsolute as isAbsolute20, relative as relative20, resolve as resolve22 } from "node:path";
19406
+ import { ToolValidationError as ToolValidationError24 } from "@wrongstack/core/types";
19204
19407
  import { DEFAULT_WALK_IGNORE_DIRS } from "@wrongstack/core/utils";
19205
19408
  var API_VERSION32 = "^0.1.10";
19206
19409
  var escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -19710,16 +19913,31 @@ var plugin49 = {
19710
19913
  icon: "search",
19711
19914
  async execute(input) {
19712
19915
  if (!cfg.enabled) {
19713
- return { ok: false, error: "semantic-search-indexer is disabled" };
19916
+ throw new Error("semantic-search-indexer is disabled");
19714
19917
  }
19715
19918
  const rawPath = input.path ?? input["directory"] ?? input["dir"] ?? input["SearchDirectory"] ?? input["SearchPath"] ?? input["TargetFile"] ?? input["targetFile"] ?? input["filePath"] ?? input["file"];
19716
19919
  const resolved = resolveProjectPath8(typeof rawPath === "string" ? rawPath : void 0);
19717
19920
  if (!resolved) {
19718
- return { ok: false, error: "path outside project root" };
19921
+ throw new ToolValidationError24({ message: "path outside project root", field: "path" });
19719
19922
  }
19720
- await ensureIndex(resolved, cfg);
19721
19923
  const rawQuery = input.query ?? input["q"] ?? input["text"] ?? input["keyword"] ?? input["keywords"] ?? input["search"] ?? "";
19722
19924
  const query = String(rawQuery);
19925
+ if (tokenize(query, cfg.minTokenLength).length === 0) {
19926
+ throw new ToolValidationError24({
19927
+ message: `query must contain at least one keyword of ${cfg.minTokenLength}+ characters`,
19928
+ field: "query"
19929
+ });
19930
+ }
19931
+ try {
19932
+ await fs2.stat(resolved);
19933
+ } catch (err) {
19934
+ throw new ToolValidationError24({
19935
+ message: `path does not exist or cannot be read: ${typeof rawPath === "string" ? rawPath : resolved}`,
19936
+ field: "path",
19937
+ cause: err
19938
+ });
19939
+ }
19940
+ await ensureIndex(resolved, cfg);
19723
19941
  const limit = typeof input.limit === "number" && input.limit >= 1 ? Math.floor(input.limit) : cfg.defaultLimit;
19724
19942
  const results = runQuery(query, limit, cfg);
19725
19943
  const queryTokens = [...new Set(tokenize(query, cfg.minTokenLength))];
@@ -19843,10 +20061,26 @@ var semantic_search_indexer_default = plugin49;
19843
20061
  // src/semver-bump/index.ts
19844
20062
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
19845
20063
  import { toErrorMessage } from "@wrongstack/core/utils";
20064
+ import { ToolValidationError as ToolValidationError25 } from "@wrongstack/core/types";
19846
20065
  import { execFile as execFile11 } from "node:child_process";
19847
20066
  import { access as access4, readFile as readFile15, readdir as readdir4, writeFile as writeFile5 } from "node:fs/promises";
19848
20067
  import { isAbsolute as isAbsolute21, join as join5, relative as relative21, resolve as resolve23 } from "node:path";
19849
20068
  var API_VERSION33 = "^0.1.10";
20069
+ function requireProjectRoot(rawCwd) {
20070
+ const safeCwd = resolveProjectRoot(rawCwd);
20071
+ if (!safeCwd) {
20072
+ throw new ToolValidationError25({
20073
+ message: "cwd must stay within the current project directory",
20074
+ field: "cwd"
20075
+ });
20076
+ }
20077
+ return safeCwd;
20078
+ }
20079
+ function requireGitRef(field, ref) {
20080
+ if (ref !== void 0 && (typeof ref !== "string" || ref.startsWith("-"))) {
20081
+ throw new ToolValidationError25({ message: `${field} is not a valid git ref`, field });
20082
+ }
20083
+ }
19850
20084
  function resolveProjectRoot(rawCwd, root = process.cwd()) {
19851
20085
  if (typeof rawCwd !== "string" || rawCwd.length === 0) return root;
19852
20086
  const base = resolve23(root);
@@ -20092,14 +20326,10 @@ var plugin50 = {
20092
20326
  defaultPart = readDefaultPart(next);
20093
20327
  });
20094
20328
  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;
20329
+ cwd = requireProjectRoot(cwd);
20100
20330
  const pkg = await getPackageJson(cwd);
20101
20331
  if (!pkg) {
20102
- return { ok: false, error: "No package.json found" };
20332
+ throw new Error("No package.json found");
20103
20333
  }
20104
20334
  const currentVersion = pkg.version;
20105
20335
  let bumpPart = part;
@@ -20114,8 +20344,7 @@ var plugin50 = {
20114
20344
  try {
20115
20345
  commits = await getRecentCommits(lastTag, cwd);
20116
20346
  } catch (err) {
20117
- const msg = toErrorMessage(err);
20118
- return { ok: false, error: `Git error: ${msg}`, bumpPart: "patch" };
20347
+ throw new Error(`Git error: ${toErrorMessage(err)}`, { cause: err });
20119
20348
  }
20120
20349
  bumpPart = determineBump(commits);
20121
20350
  } else {
@@ -20147,8 +20376,7 @@ var plugin50 = {
20147
20376
  try {
20148
20377
  await runCommand3(process.execPath, [bumpScript, "set", newVersion], root);
20149
20378
  } catch (err) {
20150
- const msg = toErrorMessage(err);
20151
- return { ok: false, error: `bump script failed: ${msg}` };
20379
+ throw new Error(`bump script failed: ${toErrorMessage(err)}`, { cause: err });
20152
20380
  }
20153
20381
  for (const rel of ["package.json", "package-lock.json", "src/lib/utils.ts", "index.html"]) {
20154
20382
  const p = join5(root, "website", rel);
@@ -20165,16 +20393,15 @@ var plugin50 = {
20165
20393
  try {
20166
20394
  pkgData = JSON.parse(await readFile15(manifest, "utf-8"));
20167
20395
  } catch (err) {
20168
- return {
20169
- ok: false,
20170
- error: `cannot bump: ${manifest} is not readable as JSON (${toErrorMessage(err)}). No manifests were modified.`
20171
- };
20396
+ throw new Error(
20397
+ `cannot bump: ${manifest} is not readable as JSON (${toErrorMessage(err)}). No manifests were modified.`,
20398
+ { cause: err }
20399
+ );
20172
20400
  }
20173
20401
  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
- };
20402
+ throw new Error(
20403
+ `cannot bump: ${manifest} does not contain a JSON object. No manifests were modified.`
20404
+ );
20178
20405
  }
20179
20406
  pkgData.version = newVersion;
20180
20407
  pending.push({ path: manifest, contents: `${JSON.stringify(pkgData, null, 2)}
@@ -20184,16 +20411,20 @@ var plugin50 = {
20184
20411
  await writeFile5(path, contents, "utf-8");
20185
20412
  }
20186
20413
  }
20414
+ let commitError;
20187
20415
  try {
20188
20416
  await runGit6(["add", "--", ...changed], cwd);
20189
20417
  await runGit6(["commit", "-m", `chore: bump version to ${newVersion}`], cwd);
20190
- } catch {
20418
+ } catch (err) {
20419
+ commitError = toErrorMessage(err);
20191
20420
  }
20421
+ let tagError;
20192
20422
  if (autoTag) {
20193
20423
  try {
20194
20424
  const msg = tagMessage.replace("{{version}}", newVersion);
20195
20425
  await runGit6(["tag", "-a", `${tagPrefix}${newVersion}`, "-m", msg], cwd);
20196
- } catch {
20426
+ } catch (err) {
20427
+ tagError = toErrorMessage(err);
20197
20428
  }
20198
20429
  }
20199
20430
  api.log.info("semver-bump: bumped", { from: currentVersion, to: newVersion, bump: bumpPart });
@@ -20213,13 +20444,22 @@ var plugin50 = {
20213
20444
  commitCount: commits.length,
20214
20445
  breakingCount: commits.filter((c) => c.breaking).length
20215
20446
  };
20447
+ const tagged = autoTag && tagError === void 0;
20448
+ const warnings = [
20449
+ ...commitError ? [`commit failed: ${commitError}`] : [],
20450
+ ...tagError ? [`tag failed: ${tagError}`] : []
20451
+ ];
20216
20452
  return {
20217
20453
  ok: true,
20218
20454
  currentVersion,
20219
20455
  newVersion,
20220
20456
  bump: bumpPart,
20221
- tag: `${tagPrefix}${newVersion}`,
20222
- message: `Bumped ${currentVersion} \u2192 ${newVersion} (${bumpPart})`
20457
+ // Only name the tag when it was actually created.
20458
+ tag: tagged ? `${tagPrefix}${newVersion}` : null,
20459
+ committed: commitError === void 0,
20460
+ tagged,
20461
+ ...warnings.length > 0 ? { warnings } : {},
20462
+ message: `Bumped ${currentVersion} \u2192 ${newVersion} (${bumpPart})` + (warnings.length > 0 ? ` \u2014 ${warnings.join("; ")}` : "")
20223
20463
  };
20224
20464
  }
20225
20465
  api.tools.register({
@@ -20305,8 +20545,12 @@ var plugin50 = {
20305
20545
  if (!safeCwd) {
20306
20546
  return { message: "cwd must stay within the current project directory" };
20307
20547
  }
20308
- const result = await performBump(mode, dry, safeCwd);
20309
- return { message: String(result["message"] ?? result["error"] ?? JSON.stringify(result)) };
20548
+ try {
20549
+ const result = await performBump(mode, dry, safeCwd);
20550
+ return { message: String(result["message"] ?? JSON.stringify(result)) };
20551
+ } catch (err) {
20552
+ return { message: toErrorMessage(err) };
20553
+ }
20310
20554
  }
20311
20555
  });
20312
20556
  api.tools.register({
@@ -20323,11 +20567,7 @@ var plugin50 = {
20323
20567
  async execute(input) {
20324
20568
  state46.invocationCount += 1;
20325
20569
  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
- }
20570
+ const safeCwd = requireProjectRoot(input["cwd"]);
20331
20571
  const pkg = await getPackageJson(safeCwd);
20332
20572
  const currentVersion = pkg?.version ?? "unknown";
20333
20573
  let latestTag = null;
@@ -20373,22 +20613,20 @@ var plugin50 = {
20373
20613
  state46.perTool["semver_changelog"] = (state46.perTool["semver_changelog"] ?? 0) + 1;
20374
20614
  const from = input["from"];
20375
20615
  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
- }
20616
+ requireGitRef("from", from);
20617
+ requireGitRef("to", to);
20618
+ const safeCwd = requireProjectRoot(input["cwd"]);
20381
20619
  const format = input["format"] ?? "markdown";
20382
- const range = from ? `${from}..${to}` : to;
20620
+ const rangeArgs = from ? [`${from}..${to}`] : ["-30", to];
20383
20621
  let commits;
20384
20622
  try {
20385
20623
  const output = await runGit6(
20386
- ["log", range === to ? "-30" : range, "--format=%H%x1f%s%x1f%b%x1e"],
20624
+ ["log", ...rangeArgs, "--format=%H%x1f%s%x1f%b%x1e"],
20387
20625
  safeCwd
20388
20626
  );
20389
20627
  commits = parseGitLogOutput(output);
20390
20628
  } catch (err) {
20391
- return { ok: false, error: `Failed to get git log: ${err}` };
20629
+ throw new Error(`Failed to get git log: ${toErrorMessage(err)}`, { cause: err });
20392
20630
  }
20393
20631
  if (format === "json") {
20394
20632
  return {
@@ -20656,8 +20894,10 @@ var plugin51 = {
20656
20894
  const offTool = api.onPattern("tool.*", (eventName, payload) => {
20657
20895
  touchActivity();
20658
20896
  const p = payload;
20659
- const toolName = p?.tool ?? p?.name ?? eventName;
20660
- if (typeof toolName === "string") bumpToolCount(toolName);
20897
+ const rawTool = p?.tool;
20898
+ const nameOf = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.name === "string" ? v.name : void 0;
20899
+ const toolName = nameOf(rawTool) ?? nameOf(p?.name) ?? eventName;
20900
+ bumpToolCount(toolName);
20661
20901
  if (toolName === "git_autocommit" || toolName.startsWith("git ")) {
20662
20902
  }
20663
20903
  });
@@ -20903,6 +21143,7 @@ Tokens: ${recap.tokens.total.input} in / ${recap.tokens.total.output} out
20903
21143
  var session_recap_default = plugin51;
20904
21144
 
20905
21145
  // src/shell-check/index.ts
21146
+ import { ToolValidationError as ToolValidationError26 } from "@wrongstack/core/types";
20906
21147
  import { execFile as execFile12 } from "node:child_process";
20907
21148
  import { readdir as readdir5 } from "node:fs/promises";
20908
21149
  import { isAbsolute as isAbsolute22, join as join6, relative as relative22, resolve as resolve24 } from "node:path";
@@ -20976,8 +21217,11 @@ async function runShellCheck(files, severity, cwd) {
20976
21217
  }
20977
21218
  );
20978
21219
  });
20979
- } catch {
20980
- return [];
21220
+ } catch (err) {
21221
+ throw new Error(
21222
+ `shellcheck failed without output: ${err instanceof Error ? err.message : String(err)}`,
21223
+ { cause: err }
21224
+ );
20981
21225
  }
20982
21226
  if (!raw.trim()) return [];
20983
21227
  try {
@@ -20990,22 +21234,31 @@ async function runShellCheck(files, severity, cwd) {
20990
21234
  code: item.code,
20991
21235
  message: item.message
20992
21236
  }));
20993
- } catch {
20994
- return [];
21237
+ } catch (err) {
21238
+ throw new Error(`shellcheck returned unparseable output: ${raw.trim().slice(0, 500)}`, {
21239
+ cause: err
21240
+ });
20995
21241
  }
20996
21242
  }
20997
- async function findShellFiles(dir, pattern) {
21243
+ async function findShellFiles(dir, pattern, isRoot = true) {
20998
21244
  const results = [];
20999
21245
  let entries;
21000
21246
  try {
21001
21247
  entries = await readdir5(dir, { withFileTypes: true });
21002
- } catch {
21248
+ } catch (err) {
21249
+ if (isRoot) {
21250
+ throw new ToolValidationError26({
21251
+ message: `directory does not exist or cannot be read: ${dir}`,
21252
+ field: "directory",
21253
+ cause: err
21254
+ });
21255
+ }
21003
21256
  return results;
21004
21257
  }
21005
21258
  for (const entry of entries) {
21006
21259
  const full = join6(dir, entry.name);
21007
21260
  if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
21008
- results.push(...await findShellFiles(full, pattern));
21261
+ results.push(...await findShellFiles(full, pattern, false));
21009
21262
  } 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
21263
  if (!pattern || entry.name.includes(pattern)) {
21011
21264
  results.push(full);
@@ -21075,12 +21328,8 @@ var plugin52 = {
21075
21328
  enum: ["error", "warning", "info", "style"],
21076
21329
  default: "warning",
21077
21330
  description: "Minimum severity level to report"
21078
- },
21079
- fix: {
21080
- type: "boolean",
21081
- default: false,
21082
- description: "Apply safe automatic fixes where possible"
21083
21331
  }
21332
+ // `fix` was declared ("apply safe automatic fixes") but never implemented.
21084
21333
  }
21085
21334
  },
21086
21335
  permission: "auto",
@@ -21106,22 +21355,16 @@ var plugin52 = {
21106
21355
  state48.invocationCount += 1;
21107
21356
  const pathIsSafe = (p) => typeof p === "string" && p.length > 0 && p.length <= MAX_PATH_LEN && withinProject22(p);
21108
21357
  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
- };
21358
+ throw new ToolValidationError26({
21359
+ message: `directory path is outside the project root: ${directory}`,
21360
+ field: "directory"
21361
+ });
21116
21362
  }
21117
21363
  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
- };
21364
+ throw new ToolValidationError26({
21365
+ message: "one or more file paths are outside the project root",
21366
+ field: "files"
21367
+ });
21125
21368
  }
21126
21369
  let checkFiles;
21127
21370
  let scannedDirectories = false;
@@ -21147,19 +21390,7 @@ var plugin52 = {
21147
21390
  mode: scannedDirectories ? "directory" : "files"
21148
21391
  };
21149
21392
  }
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
- }
21393
+ const issues = await runShellCheck(checkFiles, severity);
21163
21394
  const byFile = {};
21164
21395
  for (const issue of issues) {
21165
21396
  if (byFile[issue.file] === void 0) {
@@ -21227,6 +21458,7 @@ var shell_check_default = plugin52;
21227
21458
  // src/smart-rename/index.ts
21228
21459
  import { readFileSync as readFileSync13, writeFileSync as writeFileSync2 } from "node:fs";
21229
21460
  import { extname as extname6, isAbsolute as isAbsolute23, relative as relative23, resolve as resolve25 } from "node:path";
21461
+ import { ToolValidationError as ToolValidationError27 } from "@wrongstack/core/types";
21230
21462
  var NEW_API_VERSION = "^0.1.10";
21231
21463
  var state49 = {
21232
21464
  renameCount: 0,
@@ -21332,32 +21564,44 @@ var plugin53 = {
21332
21564
  mutating: true,
21333
21565
  capabilities: ["fs.write"],
21334
21566
  async execute(input) {
21335
- if (!cfg.enabled) return { ok: false, error: "smart-rename is disabled" };
21567
+ if (!cfg.enabled) throw new Error("smart-rename is disabled");
21336
21568
  const inp = input ?? {};
21337
21569
  const rawPath = inp["path"] ?? inp["filePath"] ?? inp["file_path"] ?? inp["TargetFile"] ?? inp["targetFile"] ?? inp["file"];
21338
21570
  const oldName = inp["oldName"] ?? inp["old_name"] ?? inp["from"];
21339
21571
  const newName = inp["newName"] ?? inp["new_name"] ?? inp["to"];
21340
21572
  if (!rawPath || typeof rawPath !== "string") {
21341
- return { ok: false, error: "path is required" };
21573
+ throw new ToolValidationError27({ message: "path is required", field: "path" });
21342
21574
  }
21343
21575
  if (!oldName || typeof oldName !== "string" || oldName.length === 0) {
21344
- return { ok: false, error: "oldName is required" };
21576
+ throw new ToolValidationError27({ message: "oldName is required", field: "oldName" });
21345
21577
  }
21346
21578
  if (!newName || typeof newName !== "string" || newName.length === 0) {
21347
- return { ok: false, error: "newName is required" };
21579
+ throw new ToolValidationError27({ message: "newName is required", field: "newName" });
21348
21580
  }
21349
21581
  if (!isIdentifier(oldName)) {
21350
- return { ok: false, error: `oldName "${oldName}" is not a valid identifier` };
21582
+ throw new ToolValidationError27({
21583
+ message: `oldName "${oldName}" is not a valid identifier`,
21584
+ field: "oldName"
21585
+ });
21351
21586
  }
21352
21587
  if (!isIdentifier(newName)) {
21353
- return { ok: false, error: `newName "${newName}" is not a valid identifier` };
21588
+ throw new ToolValidationError27({
21589
+ message: `newName "${newName}" is not a valid identifier`,
21590
+ field: "newName"
21591
+ });
21354
21592
  }
21355
21593
  if (!withinProject23(rawPath)) {
21356
- return { ok: false, error: "path is outside the project root" };
21594
+ throw new ToolValidationError27({
21595
+ message: "path is outside the project root",
21596
+ field: "path"
21597
+ });
21357
21598
  }
21358
21599
  const ext = extname6(rawPath).toLowerCase();
21359
21600
  if (!cfg.extensions.includes(ext)) {
21360
- return { ok: false, error: `extension ${ext} is not allowed for rename` };
21601
+ throw new ToolValidationError27({
21602
+ message: `extension ${ext} is not allowed for rename`,
21603
+ field: "path"
21604
+ });
21361
21605
  }
21362
21606
  const resolved = resolve25(process.cwd(), rawPath);
21363
21607
  let content;
@@ -21365,7 +21609,7 @@ var plugin53 = {
21365
21609
  content = readFileSync13(resolved, "utf-8");
21366
21610
  } catch (err) {
21367
21611
  state49.errorCount += 1;
21368
- return { ok: false, error: String(err) };
21612
+ throw new Error(`Could not read ${rawPath}: ${String(err)}`, { cause: err });
21369
21613
  }
21370
21614
  const { preview, replacements } = renameInContent(content, oldName, newName);
21371
21615
  state49.renameCount += 1;
@@ -21378,7 +21622,7 @@ var plugin53 = {
21378
21622
  writeFileSync2(resolved, preview, "utf-8");
21379
21623
  } catch (err) {
21380
21624
  state49.errorCount += 1;
21381
- return { ok: false, error: String(err) };
21625
+ throw new Error(`Could not write ${rawPath}: ${String(err)}`, { cause: err });
21382
21626
  }
21383
21627
  }
21384
21628
  return {
@@ -21811,8 +22055,8 @@ var plugin54 = {
21811
22055
  state50.postInvocations += 1;
21812
22056
  let content;
21813
22057
  try {
21814
- const stat8 = await fs3.stat(filePath);
21815
- if (!stat8.isFile()) return;
22058
+ const stat13 = await fs3.stat(filePath);
22059
+ if (!stat13.isFile()) return;
21816
22060
  content = await fs3.readFile(filePath, "utf-8");
21817
22061
  } catch {
21818
22062
  state50.readErrorCount += 1;
@@ -21962,6 +22206,7 @@ var spec_linker_default = plugin54;
21962
22206
  // src/template-engine/index.ts
21963
22207
  import { readFile as readFile17, writeFile as writeFile6 } from "node:fs/promises";
21964
22208
  import { isAbsolute as isAbsolute24 } from "node:path";
22209
+ import { ToolValidationError as ToolValidationError28 } from "@wrongstack/core/types";
21965
22210
  var API_VERSION35 = "^0.1.10";
21966
22211
  var templates = /* @__PURE__ */ new Map();
21967
22212
  var MAX_TEMPLATES = 256;
@@ -22123,24 +22368,26 @@ var plugin55 = {
22123
22368
  const output_path = typeof rawOutputPath === "string" && rawOutputPath.trim().length > 0 ? rawOutputPath.trim() : void 0;
22124
22369
  const raw = input["raw"] ?? false;
22125
22370
  if (!template || typeof template !== "string") {
22126
- return { ok: false, error: "template is required and must be a string" };
22371
+ throw new ToolValidationError28({
22372
+ message: "template is required and must be a string",
22373
+ field: "template"
22374
+ });
22127
22375
  }
22128
22376
  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) };
22377
+ throw new ToolValidationError28({
22378
+ message: "variables is required and must be an object",
22379
+ field: "variables"
22380
+ });
22136
22381
  }
22382
+ const result = raw ? renderTemplateRaw(template, variables) : renderTemplate(template, variables, autoEscapeHtml);
22137
22383
  if (output_path) {
22138
22384
  const pathError = validateWritableTemplateTarget("output_path", output_path);
22139
- if (pathError) return { ok: false, error: pathError };
22385
+ if (pathError)
22386
+ throw new ToolValidationError28({ message: pathError, field: "output_path" });
22140
22387
  try {
22141
22388
  await writeFile6(output_path, result, "utf-8");
22142
22389
  } catch (err) {
22143
- return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
22390
+ throw new Error(`Could not write ${output_path}: ${String(err)}`, { cause: err });
22144
22391
  }
22145
22392
  return {
22146
22393
  ok: true,
@@ -22191,32 +22438,36 @@ var plugin55 = {
22191
22438
  const output_path = typeof rawOutputPath === "string" && rawOutputPath.trim().length > 0 ? rawOutputPath.trim() : void 0;
22192
22439
  const raw = input["raw"] ?? false;
22193
22440
  if (!template_path || typeof template_path !== "string") {
22194
- return { ok: false, error: "template_path is required and must be a string" };
22441
+ throw new ToolValidationError28({
22442
+ message: "template_path is required and must be a string",
22443
+ field: "template_path"
22444
+ });
22195
22445
  }
22196
22446
  const templatePathError = validateRelativeTemplatePath("template_path", template_path);
22197
- if (templatePathError) return { ok: false, error: templatePathError };
22447
+ if (templatePathError) {
22448
+ throw new ToolValidationError28({ message: templatePathError, field: "template_path" });
22449
+ }
22198
22450
  if (!variables || typeof variables !== "object") {
22199
- return { ok: false, error: "variables is required and must be an object" };
22451
+ throw new ToolValidationError28({
22452
+ message: "variables is required and must be an object",
22453
+ field: "variables"
22454
+ });
22200
22455
  }
22201
22456
  let content;
22202
22457
  try {
22203
22458
  content = await readFile17(template_path, "utf-8");
22204
22459
  } 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}` };
22460
+ throw new Error(`Could not read template file: ${String(err)}`, { cause: err });
22212
22461
  }
22462
+ const result = raw ? renderTemplateRaw(content, variables) : renderTemplate(content, variables, autoEscapeHtml);
22213
22463
  if (output_path) {
22214
22464
  const pathError = validateWritableTemplateTarget("output_path", output_path);
22215
- if (pathError) return { ok: false, error: pathError };
22465
+ if (pathError)
22466
+ throw new ToolValidationError28({ message: pathError, field: "output_path" });
22216
22467
  try {
22217
22468
  await writeFile6(output_path, result, "utf-8");
22218
22469
  } catch (err) {
22219
- return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
22470
+ throw new Error(`Could not write ${output_path}: ${String(err)}`, { cause: err });
22220
22471
  }
22221
22472
  return {
22222
22473
  ok: true,
@@ -22267,30 +22518,39 @@ var plugin55 = {
22267
22518
  const rawDesc = input["description"] ?? input["desc"] ?? input["summary"];
22268
22519
  const description = typeof rawDesc === "string" ? rawDesc : void 0;
22269
22520
  if (!name || typeof name !== "string" || name.trim() === "") {
22270
- return { ok: false, error: "name is required and must be a non-empty string" };
22521
+ throw new ToolValidationError28({
22522
+ message: "name is required and must be a non-empty string",
22523
+ field: "name"
22524
+ });
22271
22525
  }
22272
22526
  if (!content || typeof content !== "string") {
22273
- return { ok: false, error: "content is required and must be a string" };
22527
+ throw new ToolValidationError28({
22528
+ message: "content is required and must be a string",
22529
+ field: "content"
22530
+ });
22274
22531
  }
22275
22532
  if (name.length > MAX_TEMPLATE_NAME_CHARS) {
22276
- return { ok: false, error: `name exceeds ${MAX_TEMPLATE_NAME_CHARS} characters` };
22533
+ throw new ToolValidationError28({
22534
+ message: `name exceeds ${MAX_TEMPLATE_NAME_CHARS} characters`,
22535
+ field: "name"
22536
+ });
22277
22537
  }
22278
22538
  if (content.length > MAX_TEMPLATE_CONTENT_CHARS) {
22279
- return {
22280
- ok: false,
22281
- error: `content exceeds ${MAX_TEMPLATE_CONTENT_CHARS} characters`
22282
- };
22539
+ throw new ToolValidationError28({
22540
+ message: `content exceeds ${MAX_TEMPLATE_CONTENT_CHARS} characters`,
22541
+ field: "content"
22542
+ });
22283
22543
  }
22284
22544
  if (description && description.length > MAX_TEMPLATE_DESCRIPTION_CHARS) {
22285
- return {
22286
- ok: false,
22287
- error: `description exceeds ${MAX_TEMPLATE_DESCRIPTION_CHARS} characters`
22288
- };
22545
+ throw new ToolValidationError28({
22546
+ message: `description exceeds ${MAX_TEMPLATE_DESCRIPTION_CHARS} characters`,
22547
+ field: "description"
22548
+ });
22289
22549
  }
22290
22550
  const now = (/* @__PURE__ */ new Date()).toISOString();
22291
22551
  const existing = templates.get(name);
22292
22552
  if (!existing && templates.size >= MAX_TEMPLATES) {
22293
- return { ok: false, error: `template limit reached (${MAX_TEMPLATES})` };
22553
+ throw new Error(`template limit reached (${MAX_TEMPLATES})`);
22294
22554
  }
22295
22555
  const tmpl = {
22296
22556
  name,
@@ -22303,10 +22563,7 @@ var plugin55 = {
22303
22563
  for (const stored of templates.values()) retainedChars += templateChars(stored);
22304
22564
  const nextChars = retainedChars - (existing ? templateChars(existing) : 0) + templateChars(tmpl);
22305
22565
  if (nextChars > MAX_TOTAL_TEMPLATE_CHARS) {
22306
- return {
22307
- ok: false,
22308
- error: `template store exceeds ${MAX_TOTAL_TEMPLATE_CHARS} retained characters`
22309
- };
22566
+ throw new Error(`template store exceeds ${MAX_TOTAL_TEMPLATE_CHARS} retained characters`);
22310
22567
  }
22311
22568
  templates.set(name, tmpl);
22312
22569
  api.metrics.gauge("template_count", templates.size);
@@ -22635,6 +22892,7 @@ import { execFile as execFile13 } from "node:child_process";
22635
22892
  import { readFileSync as readFileSync15 } from "node:fs";
22636
22893
  import { createRequire as createRequire2 } from "node:module";
22637
22894
  import { dirname as dirname8, isAbsolute as isAbsolute25, relative as relative24, resolve as resolve26 } from "node:path";
22895
+ import { ToolValidationError as ToolValidationError29 } from "@wrongstack/core/types";
22638
22896
  var API_VERSION37 = "^0.1.10";
22639
22897
  var state52 = {
22640
22898
  invocationCount: 0,
@@ -22885,7 +23143,7 @@ var plugin57 = {
22885
23143
  mutating: false,
22886
23144
  async execute(input) {
22887
23145
  if (!cfg.enabled) {
22888
- return { ok: false, error: "test-flake-detector is disabled" };
23146
+ throw new Error("test-flake-detector is disabled");
22889
23147
  }
22890
23148
  const raw = input;
22891
23149
  const rawPattern = input.testPattern ?? raw["pattern"] ?? raw["path"] ?? raw["file"] ?? raw["filePath"] ?? raw["TargetFile"] ?? raw["targetFile"];
@@ -22896,10 +23154,10 @@ var plugin57 = {
22896
23154
  const requestedRuns = typeof rawRuns === "number" && rawRuns >= 1 ? Math.min(Math.floor(rawRuns), cfg.maxRuns) : 5;
22897
23155
  const command = resolveTestCommand(commandString, testPattern);
22898
23156
  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
- };
23157
+ throw new ToolValidationError29({
23158
+ message: "Unsupported test command or unsafe testPattern. Use vitest, jest, or mocha through a supported package runner, and keep patterns inside the project.",
23159
+ field: "command"
23160
+ });
22903
23161
  }
22904
23162
  state52.invocationCount += 1;
22905
23163
  const start = Date.now();
@@ -22927,6 +23185,11 @@ var plugin57 = {
22927
23185
  }
22928
23186
  }
22929
23187
  const all = Array.from(records.values());
23188
+ if (all.length === 0 && runErrors.length === requestedRuns) {
23189
+ throw new Error(
23190
+ `test command produced no test results in ${requestedRuns} run(s): ${runErrors.slice(0, 3).join("; ")}`
23191
+ );
23192
+ }
22930
23193
  const flakyTests = all.filter((r) => r.passCount > 0 && r.failCount > 0);
22931
23194
  const alwaysFailing = all.filter((r) => r.passCount === 0 && r.failCount > 0);
22932
23195
  const alwaysPassing = all.filter((r) => r.passCount > 0 && r.failCount === 0);
@@ -23014,6 +23277,7 @@ var test_flake_detector_default = plugin57;
23014
23277
  // src/test-generator/index.ts
23015
23278
  import { readFileSync as readFileSync16 } from "node:fs";
23016
23279
  import { isAbsolute as isAbsolute26, relative as relative25, resolve as resolve27 } from "node:path";
23280
+ import { ToolValidationError as ToolValidationError30 } from "@wrongstack/core/types";
23017
23281
  var API_VERSION38 = "^0.1.10";
23018
23282
  var state53 = {
23019
23283
  generateCount: 0,
@@ -23302,7 +23566,7 @@ var plugin58 = {
23302
23566
  category: "Development",
23303
23567
  mutating: false,
23304
23568
  async execute(input, _ctx, execOpts) {
23305
- if (!cfg.enabled) return { ok: false, error: "test-generator is disabled" };
23569
+ if (!cfg.enabled) throw new Error("test-generator is disabled");
23306
23570
  execOpts?.signal?.throwIfAborted();
23307
23571
  const inp = input ?? {};
23308
23572
  const rawFramework = inp["framework"];
@@ -23313,16 +23577,19 @@ var plugin58 = {
23313
23577
  };
23314
23578
  const rawPath = inp["path"] ?? inp["filePath"] ?? inp["file_path"] ?? inp["TargetFile"] ?? inp["targetFile"] ?? inp["file"];
23315
23579
  if (!rawPath || typeof rawPath !== "string") {
23316
- return { ok: false, error: "path is required" };
23580
+ throw new ToolValidationError30({ message: "path is required", field: "path" });
23317
23581
  }
23318
23582
  if (!withinProject27(rawPath)) {
23319
- return { ok: false, error: "path is outside the project root" };
23583
+ throw new ToolValidationError30({
23584
+ message: "path is outside the project root",
23585
+ field: "path"
23586
+ });
23320
23587
  }
23321
23588
  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
- };
23589
+ throw new ToolValidationError30({
23590
+ message: `test generation only reads source files (${SOURCE_EXTENSIONS.join(", ")}); refusing "${rawPath}"`,
23591
+ field: "path"
23592
+ });
23326
23593
  }
23327
23594
  const resolved = resolve27(process.cwd(), rawPath);
23328
23595
  state53.generateCount += 1;
@@ -23331,7 +23598,7 @@ var plugin58 = {
23331
23598
  result = generateForFile(resolved, effectiveCfg);
23332
23599
  } catch (err) {
23333
23600
  state53.errorCount += 1;
23334
- return { ok: false, error: String(err) };
23601
+ throw new Error(`Could not read ${rawPath}: ${String(err)}`, { cause: err });
23335
23602
  }
23336
23603
  state53.exportCount += result.exports.length;
23337
23604
  const raw = input ?? {};
@@ -24152,451 +24419,632 @@ var todo_listener_default = plugin60;
24152
24419
  // src/todo-tracker/index.ts
24153
24420
  import { randomUUID } from "node:crypto";
24154
24421
  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";
24422
+ import { basename as basename7, dirname as dirname10, extname as extname8 } from "node:path";
24423
+ import { ToolValidationError as ToolValidationError31 } from "@wrongstack/core/types";
24424
+ import { atomicWrite as atomicWrite3, ensureDir as ensureDir3, withFileLock } from "@wrongstack/core/utils";
24157
24425
  import { nowIso } from "@wrongstack/primitives";
24426
+ var STATUSES = ["pending", "in_progress", "completed", "dropped"];
24427
+ var PRIORITIES = ["low", "normal", "high"];
24428
+ var defaultDeps = {
24429
+ readFile: (p) => fsp.readFile(p, "utf8"),
24430
+ rename: (from, to) => fsp.rename(from, to),
24431
+ atomicWrite: (p, content, opts) => atomicWrite3(p, content, opts),
24432
+ withFileLock: (p, fn) => withFileLock(p, fn),
24433
+ ensureDir: (dir) => ensureDir3(dir)
24434
+ };
24435
+ function deriveProjectSlug(filePath) {
24436
+ const base = basename7(filePath.replace(/[\\/]+$/, "").replace(/\\/g, "/"));
24437
+ const ext = extname8(base);
24438
+ const stem = ext && ext !== base ? base.slice(0, -ext.length) : base;
24439
+ return stem || "tracker";
24440
+ }
24158
24441
  function deriveFilePath(api) {
24159
24442
  const raw = api.config.extensions?.["todo-tracker"];
24160
24443
  const rawPath = raw?.["filePath"] ?? raw?.["file_path"] ?? raw?.["path"] ?? raw?.["file"] ?? raw?.["targetFile"];
24161
24444
  const explicit = typeof rawPath === "string" && rawPath.trim().length > 0 ? rawPath.trim() : null;
24162
24445
  if (explicit) {
24163
- const base = explicit.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "tracker";
24164
- return { filePath: explicit, projectSlug: base };
24446
+ return { filePath: explicit, projectSlug: deriveProjectSlug(explicit) };
24165
24447
  }
24166
24448
  return { filePath: null, projectSlug: null };
24167
24449
  }
24168
24450
  var FILE_VERSION = 1;
24169
- async function loadFile(filePath) {
24170
- let raw;
24451
+ var isStr = (v) => typeof v === "string";
24452
+ var isOptStr = (v) => v === void 0 || v === null || typeof v === "string";
24453
+ function validateItem(raw, index) {
24454
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
24455
+ return `items[${index}] is not an object`;
24456
+ }
24457
+ const it = raw;
24458
+ if (!isStr(it["id"]) || it["id"].length === 0) return `items[${index}].id is not a string`;
24459
+ if (!isStr(it["content"])) return `items[${index}].content is not a string`;
24460
+ if (!STATUSES.includes(it["status"])) {
24461
+ return `items[${index}].status is not one of ${STATUSES.join("|")}`;
24462
+ }
24463
+ if (!PRIORITIES.includes(it["priority"])) {
24464
+ return `items[${index}].priority is not one of ${PRIORITIES.join("|")}`;
24465
+ }
24466
+ if (!Array.isArray(it["tags"]) || !it["tags"].every(isStr)) {
24467
+ return `items[${index}].tags is not a string[]`;
24468
+ }
24469
+ if (!isStr(it["createdAt"]) || !isStr(it["updatedAt"])) {
24470
+ return `items[${index}] timestamps are not strings`;
24471
+ }
24472
+ if (!isOptStr(it["completedAt"]) || !isOptStr(it["sourceSessionId"]) || !isOptStr(it["notes"])) {
24473
+ return `items[${index}] optional fields are not string|null`;
24474
+ }
24475
+ return {
24476
+ id: it["id"],
24477
+ content: it["content"],
24478
+ status: it["status"],
24479
+ priority: it["priority"],
24480
+ tags: [...it["tags"]],
24481
+ createdAt: it["createdAt"],
24482
+ updatedAt: it["updatedAt"],
24483
+ completedAt: it["completedAt"] ?? null,
24484
+ sourceSessionId: it["sourceSessionId"] ?? null,
24485
+ notes: it["notes"] ?? null
24486
+ };
24487
+ }
24488
+ function parseTrackerFile(rawText) {
24489
+ const text = rawText.charCodeAt(0) === 65279 ? rawText.slice(1) : rawText;
24490
+ let parsed;
24171
24491
  try {
24172
- raw = await fsp.readFile(filePath, "utf8");
24492
+ parsed = JSON.parse(text);
24173
24493
  } catch (err) {
24174
- if (err.code === "ENOENT") return null;
24175
- throw err;
24494
+ return { kind: "corrupt", reason: `invalid JSON: ${err.message}` };
24176
24495
  }
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;
24496
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
24497
+ return { kind: "corrupt", reason: "top-level value is not an object" };
24185
24498
  }
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 = {
24499
+ const obj = parsed;
24500
+ if (typeof obj["version"] !== "number") {
24501
+ return { kind: "corrupt", reason: "missing or non-numeric version" };
24502
+ }
24503
+ if (obj["version"] !== FILE_VERSION) {
24504
+ return { kind: "unsupportedVersion", version: obj["version"] };
24505
+ }
24506
+ if (!Array.isArray(obj["items"])) {
24507
+ return { kind: "invalidItems", reason: "items is not an array" };
24508
+ }
24509
+ const items = [];
24510
+ for (const [i, rawItem] of obj["items"].entries()) {
24511
+ const v = validateItem(rawItem, i);
24512
+ if (typeof v === "string") return { kind: "invalidItems", reason: v };
24513
+ items.push(v);
24514
+ }
24515
+ return {
24516
+ kind: "ok",
24517
+ file: {
24206
24518
  version: FILE_VERSION,
24207
- projectSlug: state56.projectSlug ?? "unconfigured",
24208
- updatedAt: nowIso(),
24209
- items: []
24210
- };
24519
+ // Any stored slug is accepted (older versions stored the basename
24520
+ // including the extension); the current slug is written on next save.
24521
+ projectSlug: isStr(obj["projectSlug"]) ? obj["projectSlug"] : "",
24522
+ updatedAt: isStr(obj["updatedAt"]) ? obj["updatedAt"] : "",
24523
+ items
24524
+ }
24525
+ };
24526
+ }
24527
+ async function loadFile(deps, filePath) {
24528
+ let raw;
24529
+ try {
24530
+ raw = await deps.readFile(filePath);
24531
+ } catch (err) {
24532
+ if (err.code === "ENOENT") return { kind: "missing" };
24533
+ throw err;
24211
24534
  }
24212
- return state56.file;
24535
+ return parseTrackerFile(raw);
24213
24536
  }
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;
24537
+ function quarantineSuffix() {
24538
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
24221
24539
  }
24222
- function findItemIndex(id) {
24223
- return ensureFile().items.findIndex((it) => it.id === id);
24540
+ function emptyFile(slug) {
24541
+ return { version: FILE_VERSION, projectSlug: slug, updatedAt: nowIso(), items: [] };
24224
24542
  }
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
- };
24543
+ function requireItemId(input) {
24544
+ const rawId = input["id"] ?? input["itemId"] ?? input["taskId"] ?? input["todoId"];
24545
+ const id = typeof rawId === "string" ? rawId.trim() : "";
24546
+ if (!id) throw new ToolValidationError31({ message: "id is required", field: "id" });
24547
+ return id;
24230
24548
  }
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."
24549
+ function commonPrefixLength(a, b) {
24550
+ const n = Math.min(a.length, b.length);
24551
+ let i = 0;
24552
+ while (i < n && a[i] === b[i]) i++;
24553
+ return i;
24554
+ }
24555
+ function requireItemIndex(file, id) {
24556
+ const idx = file.items.findIndex((it) => it.id === id);
24557
+ if (idx !== -1) return idx;
24558
+ const open = file.items.filter((it) => it.status === "pending" || it.status === "in_progress");
24559
+ const pool = open.length > 0 ? open : file.items;
24560
+ 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)}")`);
24561
+ const hint = candidates.length > 0 ? ` Known ${open.length > 0 ? "open " : ""}ids: ${candidates.join(", ")}` : " The tracker is empty.";
24562
+ throw new ToolValidationError31({ message: `no item with id ${id}.${hint}`, field: "id" });
24563
+ }
24564
+ function createTodoTrackerPlugin(overrides = {}) {
24565
+ const deps = { ...defaultDeps, ...overrides };
24566
+ const instances = /* @__PURE__ */ new WeakMap();
24567
+ let latest = null;
24568
+ function unregisterTools(inst) {
24569
+ const unregister = inst.api.tools.unregister;
24570
+ for (const name of inst.registeredTools.splice(0)) {
24571
+ if (typeof unregister !== "function") continue;
24572
+ try {
24573
+ unregister.call(inst.api.tools, name);
24574
+ } catch (err) {
24575
+ inst.api.log.warn("todo-tracker: failed to unregister tool", { name, err });
24246
24576
  }
24247
24577
  }
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'
24578
+ }
24579
+ function assertWritable(inst) {
24580
+ if (inst.readOnlyReason !== null) {
24581
+ throw new Error(`todo-tracker: store is read-only \u2014 ${inst.readOnlyReason}`);
24582
+ }
24583
+ }
24584
+ function serialize(inst, fn) {
24585
+ const run = inst.queue.then(fn, fn);
24586
+ inst.queue = run.catch(() => void 0);
24587
+ return run;
24588
+ }
24589
+ async function quarantineLocked(inst, reason) {
24590
+ const target = `${inst.filePath}.corrupt-${quarantineSuffix()}`;
24591
+ try {
24592
+ await deps.rename(inst.filePath, target);
24593
+ } catch (err) {
24594
+ inst.readOnlyReason = `${inst.filePath} is unreadable (${reason}) and could not be moved aside (${err.message}); fix or remove the file, then reload the plugin`;
24595
+ inst.api.log.error(
24596
+ "todo-tracker: corrupt file could not be quarantined; store is read-only",
24597
+ {
24598
+ filePath: inst.filePath,
24599
+ reason,
24600
+ err
24601
+ }
24261
24602
  );
24262
- return;
24603
+ return false;
24263
24604
  }
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
- };
24605
+ inst.degradedReason = `${inst.filePath} was unreadable (${reason}); original moved to ${target}`;
24606
+ inst.api.log.error(`todo-tracker: corrupt file quarantined to ${target}`, {
24607
+ filePath: inst.filePath,
24608
+ quarantinedTo: target,
24609
+ reason
24610
+ });
24611
+ return true;
24612
+ }
24613
+ async function resolveLoad(inst, res, opts) {
24614
+ switch (res.kind) {
24615
+ case "ok":
24616
+ return res.file;
24617
+ case "missing":
24618
+ return emptyFile(inst.projectSlug);
24619
+ case "unsupportedVersion": {
24620
+ inst.readOnlyReason = `${inst.filePath} has format version ${JSON.stringify(res.version)}, this plugin only writes version ${FILE_VERSION}; refusing to modify it`;
24621
+ inst.api.log.error("todo-tracker: unsupported file version; store is read-only", {
24622
+ filePath: inst.filePath,
24623
+ version: res.version
24624
+ });
24625
+ if (opts.strict) assertWritable(inst);
24626
+ return emptyFile(inst.projectSlug);
24627
+ }
24628
+ case "corrupt":
24629
+ case "invalidItems": {
24630
+ const quarantine = async () => {
24631
+ const again = opts.locked ? res : await loadFile(deps, inst.filePath);
24632
+ if (again.kind === "ok") return again.file;
24633
+ if (again.kind === "missing") return emptyFile(inst.projectSlug);
24634
+ if (again.kind === "unsupportedVersion") {
24635
+ return resolveLoad(inst, again, { locked: true, strict: opts.strict });
24636
+ }
24637
+ const moved = await quarantineLocked(inst, again.reason);
24638
+ if (!moved && opts.strict) assertWritable(inst);
24639
+ return emptyFile(inst.projectSlug);
24640
+ };
24641
+ return opts.locked ? quarantine() : deps.withFileLock(inst.filePath, quarantine);
24642
+ }
24274
24643
  }
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)" }
24644
+ }
24645
+ async function refresh(inst) {
24646
+ if (inst.readOnlyReason !== null) return inst.file;
24647
+ const res = await loadFile(deps, inst.filePath);
24648
+ const file = await resolveLoad(inst, res, { locked: false, strict: false });
24649
+ inst.file = file;
24650
+ return file;
24651
+ }
24652
+ function mutate(inst, apply) {
24653
+ return serialize(
24654
+ inst,
24655
+ () => deps.withFileLock(inst.filePath, async () => {
24656
+ assertWritable(inst);
24657
+ const res = await loadFile(deps, inst.filePath);
24658
+ const current = await resolveLoad(inst, res, { locked: true, strict: true });
24659
+ const draft = structuredClone(current);
24660
+ const now = nowIso();
24661
+ const { changed, result } = apply(draft, now);
24662
+ if (changed) {
24663
+ draft.version = FILE_VERSION;
24664
+ draft.projectSlug = inst.projectSlug;
24665
+ draft.updatedAt = now;
24666
+ await deps.ensureDir(dirname10(inst.filePath));
24667
+ await deps.atomicWrite(inst.filePath, JSON.stringify(draft, null, 2), { mode: 384 });
24668
+ inst.file = draft;
24669
+ } else {
24670
+ inst.file = current;
24289
24671
  }
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);
24672
+ return result;
24673
+ })
24674
+ );
24675
+ }
24676
+ function recordMutation(inst, op, itemId) {
24677
+ inst.lastMutation = { op, itemId, when: nowIso() };
24678
+ if (op === "add") inst.addCount += 1;
24679
+ else if (op === "complete") inst.completeCount += 1;
24680
+ else if (op === "drop") inst.dropCount += 1;
24681
+ else if (op === "remove") inst.removeCount += 1;
24682
+ }
24683
+ function sessionCounts(inst) {
24684
+ return {
24685
+ add: inst.addCount,
24686
+ complete: inst.completeCount,
24687
+ drop: inst.dropCount,
24688
+ remove: inst.removeCount,
24689
+ pull: inst.pullCount
24690
+ };
24691
+ }
24692
+ function disposeInstance(api) {
24693
+ const inst = instances.get(api);
24694
+ if (!inst) return void 0;
24695
+ unregisterTools(inst);
24696
+ instances.delete(api);
24697
+ if (latest === inst) latest = null;
24698
+ return inst;
24699
+ }
24700
+ const plugin65 = {
24701
+ name: "todo-tracker",
24702
+ version: "0.1.0",
24703
+ description: "Persistent, project-scoped todo backlog that survives across sessions",
24704
+ apiVersion: "^0.1.10",
24705
+ capabilities: { tools: true },
24706
+ defaultConfig: {
24707
+ filePath: ""
24708
+ },
24709
+ configSchema: {
24710
+ type: "object",
24711
+ properties: {
24712
+ filePath: {
24713
+ type: "string",
24714
+ description: "Override the auto-derived per-project path. Defaults to <projectDir>/todo-tracker.json when `paths.projectDir` is provided by the host."
24715
+ }
24716
+ }
24717
+ },
24718
+ async setup(api) {
24719
+ disposeInstance(api);
24720
+ const derived = deriveFilePath(api);
24721
+ if (derived.filePath === null) {
24722
+ latest = null;
24723
+ api.log.warn(
24724
+ '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'
24725
+ );
24726
+ return;
24727
+ }
24728
+ const inst = {
24729
+ api,
24730
+ filePath: derived.filePath,
24731
+ projectSlug: derived.projectSlug ?? "tracker",
24732
+ file: emptyFile(derived.projectSlug ?? "tracker"),
24733
+ readOnlyReason: null,
24734
+ degradedReason: null,
24735
+ queue: Promise.resolve(),
24736
+ registeredTools: [],
24737
+ addCount: 0,
24738
+ completeCount: 0,
24739
+ dropCount: 0,
24740
+ removeCount: 0,
24741
+ pullCount: 0,
24742
+ lastMutation: null
24743
+ };
24744
+ instances.set(api, inst);
24745
+ latest = inst;
24746
+ await refresh(inst);
24747
+ const register = (tool) => {
24748
+ api.tools.register(tool);
24749
+ inst.registeredTools.push(tool.name);
24750
+ };
24751
+ register({
24752
+ name: "todo_tracker_list",
24753
+ description: "List persistent todo-tracker items. Filterable by status, priority, and tag. By default only pending + in_progress items are shown.",
24754
+ inputSchema: {
24755
+ type: "object",
24756
+ properties: {
24757
+ status: {
24758
+ type: "string",
24759
+ enum: ["pending", "in_progress", "completed", "dropped", "all"],
24760
+ description: "Filter by status. 'all' returns every item; default is pending+in_progress."
24761
+ },
24762
+ priority: { type: "string", enum: ["low", "normal", "high"] },
24763
+ tag: { type: "string", description: "Filter by exact tag match" },
24764
+ limit: { type: "number", description: "Max items to return (default 50, max 200)" }
24765
+ }
24766
+ },
24767
+ permission: "auto",
24768
+ mutating: false,
24769
+ async execute(input) {
24770
+ const rawStatus = typeof input["status"] === "string" ? input["status"].trim().toLowerCase() : void 0;
24771
+ const status = rawStatus ?? "active";
24772
+ const priority = typeof input["priority"] === "string" ? input["priority"].trim().toLowerCase() : void 0;
24773
+ const tag = typeof input["tag"] === "string" ? input["tag"] : void 0;
24774
+ const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
24775
+ const file = await refresh(inst);
24776
+ let items = file.items;
24777
+ if (status !== "all") {
24778
+ if (status === "active") {
24779
+ items = items.filter((it) => it.status === "pending" || it.status === "in_progress");
24780
+ } else {
24781
+ items = items.filter((it) => it.status === status);
24782
+ }
24308
24783
  }
24784
+ if (priority) items = items.filter((it) => it.priority === priority);
24785
+ if (tag) items = items.filter((it) => it.tags.includes(tag));
24786
+ const total = items.length;
24787
+ const truncated = items.slice(0, limit);
24788
+ return {
24789
+ ok: true,
24790
+ total,
24791
+ returned: truncated.length,
24792
+ truncated: total > truncated.length,
24793
+ items: truncated,
24794
+ ...inst.readOnlyReason ? { readOnly: inst.readOnlyReason } : {}
24795
+ };
24309
24796
  }
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"
24797
+ });
24798
+ register({
24799
+ name: "todo_tracker_add",
24800
+ description: "Append a new item to the persistent todo-tracker backlog.",
24801
+ inputSchema: {
24802
+ type: "object",
24803
+ properties: {
24804
+ content: { type: "string", description: "What needs doing (required)" },
24805
+ priority: { type: "string", enum: ["low", "normal", "high"], default: "normal" },
24806
+ tags: {
24807
+ type: "array",
24808
+ items: { type: "string" },
24809
+ description: "Optional tags for filtering"
24810
+ },
24811
+ sourceSessionId: { type: "string", description: "Session that created this item" },
24812
+ notes: { type: "string", description: "Optional free-form notes" }
24335
24813
  },
24336
- sourceSessionId: { type: "string", description: "Session that created this item" },
24337
- notes: { type: "string", description: "Optional free-form notes" }
24814
+ required: ["content"]
24338
24815
  },
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
24816
+ permission: "auto",
24817
+ mutating: true,
24818
+ async execute(input) {
24819
+ const rawContent = input["content"] ?? input["text"] ?? input["task"] ?? input["title"] ?? input["todo"] ?? input["message"] ?? input["item"];
24820
+ const content = typeof rawContent === "string" ? rawContent.trim() : "";
24821
+ if (!content) {
24822
+ throw new ToolValidationError31({
24823
+ message: "content is required and must be a non-empty string",
24824
+ field: "content"
24825
+ });
24826
+ }
24827
+ const rawPri = typeof input["priority"] === "string" ? input["priority"].trim().toLowerCase() : "";
24828
+ const priority = rawPri === "low" || rawPri === "high" ? rawPri : "normal";
24829
+ const tags = Array.isArray(input["tags"]) ? input["tags"].filter((t) => typeof t === "string") : [];
24830
+ const sourceSessionId = typeof input["sourceSessionId"] === "string" ? input["sourceSessionId"] : null;
24831
+ const notes = typeof input["notes"] === "string" ? input["notes"] : null;
24832
+ const item = await mutate(inst, (draft, now) => {
24833
+ const created = {
24834
+ id: randomUUID(),
24835
+ content,
24836
+ status: "pending",
24837
+ priority,
24838
+ tags,
24839
+ createdAt: now,
24840
+ updatedAt: now,
24841
+ completedAt: null,
24842
+ sourceSessionId,
24843
+ notes
24844
+ };
24845
+ draft.items.push(created);
24846
+ return { changed: true, result: created };
24382
24847
  });
24383
- } catch {
24848
+ recordMutation(inst, "add", item.id);
24849
+ api.log.info("todo-tracker: added item", { id: item.id, content });
24850
+ try {
24851
+ await api.session?.append?.({
24852
+ type: "todo-tracker:add",
24853
+ ts: item.createdAt,
24854
+ id: item.id,
24855
+ content,
24856
+ priority,
24857
+ tags
24858
+ });
24859
+ } catch (err) {
24860
+ api.log.warn("todo-tracker: session.append failed (item was saved)", {
24861
+ id: item.id,
24862
+ err
24863
+ });
24864
+ }
24865
+ return { ok: true, item };
24384
24866
  }
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" }
24867
+ });
24868
+ const setTerminalStatus = async (input, target) => {
24869
+ const id = requireItemId(input);
24870
+ return mutate(inst, (draft, now) => {
24871
+ const item = draft.items[requireItemIndex(draft, id)];
24872
+ if (item.status === target) {
24873
+ return {
24874
+ changed: false,
24875
+ result: { ok: true, item, message: `already ${target} (idempotent)` }
24876
+ };
24877
+ }
24878
+ item.status = target;
24879
+ item.updatedAt = now;
24880
+ item.completedAt = now;
24881
+ return { changed: true, result: { ok: true, item } };
24882
+ });
24883
+ };
24884
+ register({
24885
+ name: "todo_tracker_complete",
24886
+ description: "Mark a tracked item as completed. Idempotent.",
24887
+ inputSchema: {
24888
+ type: "object",
24889
+ properties: {
24890
+ id: { type: "string", description: "Item id" }
24891
+ },
24892
+ required: ["id"]
24395
24893
  },
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)" };
24894
+ permission: "auto",
24895
+ mutating: true,
24896
+ async execute(input) {
24897
+ const result = await setTerminalStatus(input, "completed");
24898
+ if (!result.message) {
24899
+ recordMutation(inst, "complete", result.item.id);
24900
+ api.log.info("todo-tracker: completed item", { id: result.item.id });
24901
+ }
24902
+ return result;
24411
24903
  }
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" }
24904
+ });
24905
+ register({
24906
+ name: "todo_tracker_drop",
24907
+ description: "Mark a tracked item as dropped (skipped/obsolete). The row is kept for audit. Idempotent.",
24908
+ inputSchema: {
24909
+ type: "object",
24910
+ properties: {
24911
+ id: { type: "string", description: "Item id" }
24912
+ },
24913
+ required: ["id"]
24430
24914
  },
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)" };
24915
+ permission: "auto",
24916
+ mutating: true,
24917
+ async execute(input) {
24918
+ const result = await setTerminalStatus(input, "dropped");
24919
+ if (!result.message) recordMutation(inst, "drop", result.item.id);
24920
+ return result;
24446
24921
  }
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" }
24922
+ });
24923
+ register({
24924
+ name: "todo_tracker_remove",
24925
+ description: "Permanently delete a tracked item by id. Use todo_tracker_drop instead if you want to keep the audit row.",
24926
+ inputSchema: {
24927
+ type: "object",
24928
+ properties: {
24929
+ id: { type: "string", description: "Item id" }
24930
+ },
24931
+ required: ["id"]
24464
24932
  },
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)" }
24933
+ permission: "confirm",
24934
+ mutating: true,
24935
+ async execute(input) {
24936
+ const id = requireItemId(input);
24937
+ const removed = await mutate(inst, (draft) => {
24938
+ const [gone] = draft.items.splice(requireItemIndex(draft, id), 1);
24939
+ return { changed: true, result: gone };
24940
+ });
24941
+ recordMutation(inst, "remove", id);
24942
+ return { ok: true, removed };
24491
24943
  }
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);
24944
+ });
24945
+ register({
24946
+ name: "todo_tracker_pull",
24947
+ 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.",
24948
+ inputSchema: {
24949
+ type: "object",
24950
+ properties: {
24951
+ limit: { type: "number", description: "Max items to return (default 50, max 200)" }
24952
+ }
24953
+ },
24954
+ permission: "auto",
24955
+ mutating: false,
24956
+ async execute(input) {
24957
+ const limit = Math.min(Math.max(Number(input["limit"] ?? 50) || 50, 1), 200);
24958
+ const file = await refresh(inst);
24959
+ const open = file.items.filter(
24960
+ (it) => it.status === "pending" || it.status === "in_progress"
24961
+ );
24962
+ const items = open.slice(0, limit);
24963
+ if (items.length > 0) inst.pullCount += 1;
24964
+ return {
24965
+ ok: true,
24966
+ total: open.length,
24967
+ returned: items.length,
24968
+ truncated: open.length > items.length,
24969
+ items,
24970
+ 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."
24971
+ };
24502
24972
  }
24973
+ });
24974
+ register({
24975
+ name: "todo_tracker_status",
24976
+ description: "Report todo-tracker counters (per-status totals) + the file path + last update timestamp.",
24977
+ inputSchema: { type: "object", properties: {} },
24978
+ permission: "auto",
24979
+ mutating: false,
24980
+ async execute() {
24981
+ const file = await refresh(inst);
24982
+ const byStatus = {
24983
+ pending: 0,
24984
+ in_progress: 0,
24985
+ completed: 0,
24986
+ dropped: 0
24987
+ };
24988
+ for (const it of file.items) {
24989
+ if (STATUSES.includes(it.status)) byStatus[it.status] += 1;
24990
+ }
24991
+ return {
24992
+ ok: true,
24993
+ filePath: inst.filePath,
24994
+ projectSlug: inst.projectSlug,
24995
+ updatedAt: file.updatedAt,
24996
+ counters: byStatus,
24997
+ total: file.items.length,
24998
+ session: sessionCounts(inst),
24999
+ lastMutation: inst.lastMutation,
25000
+ readOnly: inst.readOnlyReason,
25001
+ degraded: inst.degradedReason
25002
+ };
25003
+ }
25004
+ });
25005
+ api.log.info("todo-tracker plugin loaded", {
25006
+ filePath: inst.filePath,
25007
+ projectSlug: inst.projectSlug,
25008
+ initialItemCount: inst.file.items.length,
25009
+ readOnly: inst.readOnlyReason,
25010
+ degraded: inst.degradedReason
25011
+ });
25012
+ },
25013
+ teardown(api) {
25014
+ const inst = disposeInstance(api);
25015
+ if (!inst) return;
25016
+ api.log.info("todo-tracker: teardown complete", { sessionCounts: sessionCounts(inst) });
25017
+ },
25018
+ async health() {
25019
+ const inst = latest;
25020
+ if (inst === null) {
24503
25021
  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
25022
+ ok: false,
25023
+ message: "todo-tracker: no file path configured \u2014 tools will error"
24542
25024
  };
24543
25025
  }
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) {
25026
+ const reason = inst.readOnlyReason ?? inst.degradedReason;
24572
25027
  return {
24573
- ok: false,
24574
- message: "todo-tracker: no file path configured \u2014 tools will error"
25028
+ ok: reason === null,
25029
+ message: reason === null ? `todo-tracker: ${inst.file.items.length} item(s) at ${inst.filePath}` : `todo-tracker: ${inst.readOnlyReason ? "read-only" : "degraded"} \u2014 ${reason}`,
25030
+ filePath: inst.filePath,
25031
+ projectSlug: inst.projectSlug,
25032
+ total: inst.file.items.length,
25033
+ readOnly: inst.readOnlyReason,
25034
+ degraded: inst.degradedReason,
25035
+ sessionCounts: sessionCounts(inst),
25036
+ lastMutation: inst.lastMutation
24575
25037
  };
24576
25038
  }
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
- };
25039
+ };
25040
+ return plugin65;
25041
+ }
25042
+ var plugin61 = createTodoTrackerPlugin();
24595
25043
  var todo_tracker_default = plugin61;
24596
25044
 
24597
25045
  // src/token-budget/index.ts
24598
25046
  var API_VERSION40 = "^0.1.10";
24599
- var state57 = {
25047
+ var state56 = {
24600
25048
  totalTokens: 0,
24601
25049
  totalPromptTokens: 0,
24602
25050
  totalCompletionTokens: 0,
@@ -24654,13 +25102,13 @@ function readConfig54(raw) {
24654
25102
  }
24655
25103
  function clearRegistrations3() {
24656
25104
  for (const key of ["hookUnregister", "postHookUnregister"]) {
24657
- const off = state57[key];
25105
+ const off = state56[key];
24658
25106
  if (!off) continue;
24659
25107
  try {
24660
25108
  off();
24661
25109
  } catch {
24662
25110
  }
24663
- state57[key] = null;
25111
+ state56[key] = null;
24664
25112
  }
24665
25113
  }
24666
25114
  var plugin62 = {
@@ -24701,15 +25149,15 @@ var plugin62 = {
24701
25149
  }
24702
25150
  },
24703
25151
  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;
25152
+ state56.totalTokens = 0;
25153
+ state56.totalPromptTokens = 0;
25154
+ state56.totalCompletionTokens = 0;
25155
+ state56.requestCount = 0;
25156
+ state56.warningFired = false;
25157
+ state56.stopFired = false;
25158
+ state56.warnContextInjected = false;
25159
+ state56.stopContextInjected = false;
25160
+ state56.lastRequest = null;
24713
25161
  clearRegistrations3();
24714
25162
  const cfg = readConfig54(api.config.extensions?.["token-budget"]);
24715
25163
  api.onEvent("provider.response", (payload) => {
@@ -24722,24 +25170,26 @@ var plugin62 = {
24722
25170
  if (!modelMatches(cfg.model, modelName)) return;
24723
25171
  }
24724
25172
  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);
25173
+ 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);
25174
+ const promptTokens = Number.isFinite(promptTokensRaw) ? promptTokensRaw : 0;
25175
+ 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);
25176
+ const completionTokens = Number.isFinite(completionTokensRaw) ? completionTokensRaw : 0;
24727
25177
  const total = promptTokens + completionTokens;
24728
- state57.totalPromptTokens += promptTokens;
24729
- state57.totalCompletionTokens += completionTokens;
24730
- state57.totalTokens += total;
24731
- state57.requestCount += 1;
24732
- state57.lastRequest = {
25178
+ state56.totalPromptTokens += promptTokens;
25179
+ state56.totalCompletionTokens += completionTokens;
25180
+ state56.totalTokens += total;
25181
+ state56.requestCount += 1;
25182
+ state56.lastRequest = {
24733
25183
  model: modelName,
24734
25184
  prompt: promptTokens,
24735
25185
  completion: completionTokens,
24736
25186
  when: (/* @__PURE__ */ new Date()).toISOString()
24737
25187
  };
24738
25188
  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;
25189
+ const percent = state56.totalTokens / cfg.limit * 100;
25190
+ if (!state56.warningFired && percent >= cfg.warnPercent) {
25191
+ state56.warningFired = true;
25192
+ const remaining = Math.max(cfg.limit - state56.totalTokens, 0);
24743
25193
  api.log.info("token-budget: warning threshold reached", {
24744
25194
  percent: Math.round(percent),
24745
25195
  remaining
@@ -24747,45 +25197,46 @@ var plugin62 = {
24747
25197
  api.emitCustom("token-budget:warning", {
24748
25198
  percent: Math.round(percent),
24749
25199
  remaining,
24750
- total: state57.totalTokens,
25200
+ total: state56.totalTokens,
24751
25201
  limit: cfg.limit
24752
25202
  });
24753
25203
  }
24754
- if (!state57.stopFired && percent >= cfg.stopPercent) {
24755
- state57.stopFired = true;
25204
+ if (!state56.stopFired && percent >= cfg.stopPercent) {
25205
+ state56.stopFired = true;
24756
25206
  api.log.warn("token-budget: hard limit reached \u2014 agent loop will be stopped", {
24757
- total: state57.totalTokens,
25207
+ total: state56.totalTokens,
24758
25208
  limit: cfg.limit
24759
25209
  });
24760
25210
  api.emitCustom("token-budget:limit_reached", {
24761
- total: state57.totalTokens,
25211
+ total: state56.totalTokens,
24762
25212
  limit: cfg.limit
24763
25213
  });
24764
25214
  }
24765
25215
  });
24766
- state57.hookUnregister = api.registerHook("Stop", void 0, () => {
24767
- if (cfg.limit <= 0 || !state57.stopFired) return;
25216
+ state56.hookUnregister = api.registerHook("Stop", void 0, () => {
25217
+ if (cfg.limit <= 0 || !state56.stopFired) return;
24768
25218
  return {
24769
25219
  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.`
25220
+ 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
25221
  };
24772
25222
  });
24773
- state57.postHookUnregister = api.registerHook("PostToolUse", "*", () => {
25223
+ state56.postHookUnregister = api.registerHook("PostToolUse", "*", () => {
24774
25224
  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;
25225
+ const percent = Math.round(state56.totalTokens / cfg.limit * 100);
25226
+ const remaining = Math.max(cfg.limit - state56.totalTokens, 0);
25227
+ if (state56.stopFired && !state56.stopContextInjected) {
25228
+ state56.stopContextInjected = true;
25229
+ state56.warnContextInjected = true;
24779
25230
  return {
24780
25231
  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.`
25232
+ \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
25233
  };
24783
25234
  }
24784
- if (state57.warningFired && !state57.warnContextInjected) {
24785
- state57.warnContextInjected = true;
25235
+ if (state56.warningFired && !state56.warnContextInjected) {
25236
+ state56.warnContextInjected = true;
24786
25237
  return {
24787
25238
  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.`
25239
+ \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
25240
  };
24790
25241
  }
24791
25242
  return;
@@ -24798,7 +25249,7 @@ var plugin62 = {
24798
25249
  category: "Meta",
24799
25250
  mutating: false,
24800
25251
  async execute() {
24801
- const consumed = state57.totalTokens;
25252
+ const consumed = state56.totalTokens;
24802
25253
  const limit = cfg.limit;
24803
25254
  const percent = limit > 0 ? Math.round(consumed / limit * 100) : 0;
24804
25255
  const remaining = limit > 0 ? Math.max(limit - consumed, 0) : Infinity;
@@ -24808,7 +25259,7 @@ var plugin62 = {
24808
25259
  consumed,
24809
25260
  remaining,
24810
25261
  percent,
24811
- requestCount: state57.requestCount,
25262
+ requestCount: state56.requestCount,
24812
25263
  // The EFFECTIVE thresholds, after out-of-range values fall back to
24813
25264
  // the defaults and warn/stop are ordered. Without these the user
24814
25265
  // cannot tell that a rejected or clamped setting is not in force.
@@ -24816,12 +25267,12 @@ var plugin62 = {
24816
25267
  stopPercent: cfg.stopPercent,
24817
25268
  model: cfg.model === "" ? null : cfg.model,
24818
25269
  breakdown: {
24819
- prompt: state57.totalPromptTokens,
24820
- completion: state57.totalCompletionTokens
25270
+ prompt: state56.totalPromptTokens,
25271
+ completion: state56.totalCompletionTokens
24821
25272
  },
24822
- warningFired: state57.warningFired,
24823
- stopFired: state57.stopFired,
24824
- lastRequest: state57.lastRequest
25273
+ warningFired: state56.warningFired,
25274
+ stopFired: state56.stopFired,
25275
+ lastRequest: state56.lastRequest
24825
25276
  };
24826
25277
  }
24827
25278
  });
@@ -24835,30 +25286,30 @@ var plugin62 = {
24835
25286
  teardown(api) {
24836
25287
  clearRegistrations3();
24837
25288
  const final = {
24838
- totalTokens: state57.totalTokens,
24839
- requestCount: state57.requestCount,
24840
- warningFired: state57.warningFired,
24841
- stopFired: state57.stopFired
25289
+ totalTokens: state56.totalTokens,
25290
+ requestCount: state56.requestCount,
25291
+ warningFired: state56.warningFired,
25292
+ stopFired: state56.stopFired
24842
25293
  };
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;
25294
+ state56.totalTokens = 0;
25295
+ state56.totalPromptTokens = 0;
25296
+ state56.totalCompletionTokens = 0;
25297
+ state56.requestCount = 0;
25298
+ state56.warningFired = false;
25299
+ state56.stopFired = false;
25300
+ state56.warnContextInjected = false;
25301
+ state56.stopContextInjected = false;
25302
+ state56.lastRequest = null;
24852
25303
  api.log.info("token-budget: teardown complete", { final });
24853
25304
  },
24854
25305
  async health() {
24855
25306
  return {
24856
25307
  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
25308
+ 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}`,
25309
+ totalTokens: state56.totalTokens,
25310
+ requestCount: state56.requestCount,
25311
+ warningFired: state56.warningFired,
25312
+ stopFired: state56.stopFired
24862
25313
  };
24863
25314
  }
24864
25315
  };
@@ -24905,7 +25356,7 @@ function computeThrottleDelay(entries, now, limit, projected) {
24905
25356
  const newest = sorted[sorted.length - 1];
24906
25357
  return newest ? Math.max(0, newest.at + WINDOW_MS - now) : 0;
24907
25358
  }
24908
- var state58 = {
25359
+ var state57 = {
24909
25360
  window: [],
24910
25361
  invocations: 0,
24911
25362
  throttled: 0,
@@ -24978,16 +25429,16 @@ var plugin63 = {
24978
25429
  }
24979
25430
  },
24980
25431
  setup(api) {
24981
- state58.window = [];
24982
- state58.invocations = 0;
24983
- state58.throttled = 0;
24984
- state58.totalDelayMs = 0;
24985
- if (state58.extensionUnregister) {
25432
+ state57.window = [];
25433
+ state57.invocations = 0;
25434
+ state57.throttled = 0;
25435
+ state57.totalDelayMs = 0;
25436
+ if (state57.extensionUnregister) {
24986
25437
  try {
24987
- state58.extensionUnregister();
25438
+ state57.extensionUnregister();
24988
25439
  } catch {
24989
25440
  }
24990
- state58.extensionUnregister = null;
25441
+ state57.extensionUnregister = null;
24991
25442
  }
24992
25443
  const cfg = readConfig55(api.config.extensions?.["token-throttle"]);
24993
25444
  if (cfg.enabled) {
@@ -24996,21 +25447,21 @@ var plugin63 = {
24996
25447
  kind: "throttle",
24997
25448
  wraps: ["request"]
24998
25449
  });
24999
- state58.extensionUnregister = api.extensions.register({
25450
+ state57.extensionUnregister = api.extensions.register({
25000
25451
  name: "token-throttle",
25001
25452
  owner: "token-throttle",
25002
25453
  async wrapProviderRunner(_ctx, request, inner) {
25003
25454
  const signal = _ctx?.signal;
25004
25455
  const req = request ?? {};
25005
- state58.invocations += 1;
25456
+ state57.invocations += 1;
25006
25457
  const now = Date.now();
25007
- state58.window = pruneWindow(state58.window, now);
25458
+ state57.window = pruneWindow(state57.window, now);
25008
25459
  const projected = estimateRequestTokens(req, cfg.charsPerToken);
25009
- const rawDelay = computeThrottleDelay(state58.window, now, cfg.tokensPerMinute, projected);
25460
+ const rawDelay = computeThrottleDelay(state57.window, now, cfg.tokensPerMinute, projected);
25010
25461
  const delay = Math.min(rawDelay, cfg.maxDelayMs);
25011
25462
  if (delay > 0) {
25012
- state58.throttled += 1;
25013
- state58.totalDelayMs += delay;
25463
+ state57.throttled += 1;
25464
+ state57.totalDelayMs += delay;
25014
25465
  api.metrics.counter("throttled");
25015
25466
  api.metrics.histogram("delay_ms", delay);
25016
25467
  api.log.info("token-throttle: delaying provider call", { delayMs: delay, projected });
@@ -25018,11 +25469,14 @@ var plugin63 = {
25018
25469
  }
25019
25470
  const response = await inner(_ctx, request);
25020
25471
  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;
25472
+ 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);
25473
+ const inputTokens = Number.isFinite(inputTokensRaw) ? inputTokensRaw : 0;
25474
+ 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);
25475
+ const outputTokens = Number.isFinite(outputTokensRaw) ? outputTokensRaw : 0;
25476
+ 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;
25477
+ const totalTokens = Number.isFinite(totalTokensRaw) ? totalTokensRaw : 0;
25024
25478
  const used = totalTokens > 0 ? totalTokens : projected;
25025
- state58.window.push({ at: Date.now(), tokens: used });
25479
+ state57.window.push({ at: Date.now(), tokens: used });
25026
25480
  return response;
25027
25481
  }
25028
25482
  });
@@ -25036,7 +25490,7 @@ var plugin63 = {
25036
25490
  mutating: false,
25037
25491
  async execute() {
25038
25492
  const now = Date.now();
25039
- const live = pruneWindow(state58.window, now);
25493
+ const live = pruneWindow(state57.window, now);
25040
25494
  return {
25041
25495
  ok: true,
25042
25496
  enabled: cfg.enabled,
@@ -25045,9 +25499,9 @@ var plugin63 = {
25045
25499
  windowSpend: windowSpend(live),
25046
25500
  windowEntries: live.length,
25047
25501
  counters: {
25048
- invocations: state58.invocations,
25049
- throttled: state58.throttled,
25050
- totalDelayMs: state58.totalDelayMs
25502
+ invocations: state57.invocations,
25503
+ throttled: state57.throttled,
25504
+ totalDelayMs: state57.totalDelayMs
25051
25505
  }
25052
25506
  };
25053
25507
  }
@@ -25059,32 +25513,32 @@ var plugin63 = {
25059
25513
  });
25060
25514
  },
25061
25515
  teardown(api) {
25062
- if (state58.extensionUnregister) {
25516
+ if (state57.extensionUnregister) {
25063
25517
  try {
25064
- state58.extensionUnregister();
25518
+ state57.extensionUnregister();
25065
25519
  } catch {
25066
25520
  }
25067
- state58.extensionUnregister = null;
25521
+ state57.extensionUnregister = null;
25068
25522
  }
25069
25523
  const final = {
25070
- invocations: state58.invocations,
25071
- throttled: state58.throttled,
25072
- totalDelayMs: state58.totalDelayMs
25524
+ invocations: state57.invocations,
25525
+ throttled: state57.throttled,
25526
+ totalDelayMs: state57.totalDelayMs
25073
25527
  };
25074
- state58.window = [];
25075
- state58.invocations = 0;
25076
- state58.throttled = 0;
25077
- state58.totalDelayMs = 0;
25528
+ state57.window = [];
25529
+ state57.invocations = 0;
25530
+ state57.throttled = 0;
25531
+ state57.totalDelayMs = 0;
25078
25532
  api.log.info("token-throttle: teardown complete", { final });
25079
25533
  },
25080
25534
  async health() {
25081
25535
  return {
25082
25536
  ok: true,
25083
- message: `token-throttle: ${state58.throttled} throttle(s) of ${state58.invocations} call(s), ${state58.totalDelayMs}ms total delay`,
25537
+ message: `token-throttle: ${state57.throttled} throttle(s) of ${state57.invocations} call(s), ${state57.totalDelayMs}ms total delay`,
25084
25538
  counters: {
25085
- invocations: state58.invocations,
25086
- throttled: state58.throttled,
25087
- totalDelayMs: state58.totalDelayMs
25539
+ invocations: state57.invocations,
25540
+ throttled: state57.throttled,
25541
+ totalDelayMs: state57.totalDelayMs
25088
25542
  }
25089
25543
  };
25090
25544
  }
@@ -25094,7 +25548,7 @@ var token_throttle_default = plugin63;
25094
25548
  // src/type-gate/index.ts
25095
25549
  import { existsSync as existsSync8 } from "node:fs";
25096
25550
  var API_VERSION41 = "^0.1.10";
25097
- var state59 = {
25551
+ var state58 = {
25098
25552
  invocationCount: 0,
25099
25553
  runCount: 0,
25100
25554
  passCount: 0,
@@ -25260,14 +25714,14 @@ var plugin64 = {
25260
25714
  }
25261
25715
  },
25262
25716
  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);
25717
+ state58.invocationCount = 0;
25718
+ state58.runCount = 0;
25719
+ state58.passCount = 0;
25720
+ state58.failCount = 0;
25721
+ state58.errorCount = 0;
25722
+ state58.skippedCount = 0;
25723
+ state58.lastResult = null;
25724
+ state58.hookUnregister = (0, runtime_exports.releaseHandle)(state58.hookUnregister);
25271
25725
  const cfg = readConfig56(api.config.extensions?.["type-gate"]);
25272
25726
  const hook = async (input) => {
25273
25727
  if (!cfg.enabled) return;
@@ -25279,27 +25733,27 @@ var plugin64 = {
25279
25733
  if (!(0, runtime_exports.withinProject)(sourcePath)) return;
25280
25734
  const ext = sourcePath.includes(".") ? sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() : "";
25281
25735
  if (!runOnChangeSet2.has(ext)) {
25282
- state59.skippedCount += 1;
25736
+ state58.skippedCount += 1;
25283
25737
  return;
25284
25738
  }
25285
- state59.invocationCount += 1;
25739
+ state58.invocationCount += 1;
25286
25740
  const result = await runTypeCheck(cfg);
25287
25741
  if (!result) {
25288
- state59.errorCount += 1;
25742
+ state58.errorCount += 1;
25289
25743
  return;
25290
25744
  }
25291
- state59.runCount += 1;
25292
- state59.lastResult = {
25745
+ state58.runCount += 1;
25746
+ state58.lastResult = {
25293
25747
  passed: result.passed,
25294
25748
  errorCount: result.errorCount,
25295
25749
  durationMs: result.durationMs,
25296
25750
  when: (/* @__PURE__ */ new Date()).toISOString()
25297
25751
  };
25298
25752
  if (result.passed) {
25299
- state59.passCount += 1;
25753
+ state58.passCount += 1;
25300
25754
  return;
25301
25755
  }
25302
- state59.failCount += 1;
25756
+ state58.failCount += 1;
25303
25757
  const errorList = result.errors.map((e) => ` \u274C ${e}`).join("\n");
25304
25758
  const message = `
25305
25759
  \u274C type-gate: Type check failed after editing ${sourcePath} (${result.durationMs}ms).
@@ -25314,7 +25768,7 @@ Fix the type error(s) or adjust the change.`;
25314
25768
  }
25315
25769
  return { additionalContext: message };
25316
25770
  };
25317
- state59.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook, {
25771
+ state58.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook, {
25318
25772
  background: true
25319
25773
  });
25320
25774
  api.tools.register({
@@ -25334,14 +25788,14 @@ Fix the type error(s) or adjust the change.`;
25334
25788
  maxErrors: cfg.maxErrors,
25335
25789
  runOnChange: cfg.runOnChange,
25336
25790
  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
25791
+ invocations: state58.invocationCount,
25792
+ runs: state58.runCount,
25793
+ passed: state58.passCount,
25794
+ failed: state58.failCount,
25795
+ errors: state58.errorCount,
25796
+ skipped: state58.skippedCount
25343
25797
  },
25344
- lastResult: state59.lastResult
25798
+ lastResult: state58.lastResult
25345
25799
  };
25346
25800
  }
25347
25801
  });
@@ -25353,43 +25807,43 @@ Fix the type error(s) or adjust the change.`;
25353
25807
  });
25354
25808
  },
25355
25809
  teardown(api) {
25356
- if (state59.hookUnregister) {
25810
+ if (state58.hookUnregister) {
25357
25811
  try {
25358
- state59.hookUnregister();
25812
+ state58.hookUnregister();
25359
25813
  } catch {
25360
25814
  }
25361
- state59.hookUnregister = null;
25815
+ state58.hookUnregister = null;
25362
25816
  }
25363
25817
  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
25818
+ invocations: state58.invocationCount,
25819
+ runs: state58.runCount,
25820
+ passed: state58.passCount,
25821
+ failed: state58.failCount,
25822
+ errors: state58.errorCount,
25823
+ skipped: state58.skippedCount
25370
25824
  };
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;
25825
+ state58.invocationCount = 0;
25826
+ state58.runCount = 0;
25827
+ state58.passCount = 0;
25828
+ state58.failCount = 0;
25829
+ state58.errorCount = 0;
25830
+ state58.skippedCount = 0;
25831
+ state58.lastResult = null;
25378
25832
  api.log.info("type-gate: teardown complete", { final });
25379
25833
  },
25380
25834
  async health() {
25381
25835
  return {
25382
25836
  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)`,
25837
+ 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
25838
  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
25839
+ invocations: state58.invocationCount,
25840
+ runs: state58.runCount,
25841
+ passed: state58.passCount,
25842
+ failed: state58.failCount,
25843
+ errors: state58.errorCount,
25844
+ skipped: state58.skippedCount
25845
+ },
25846
+ lastResult: state58.lastResult
25393
25847
  };
25394
25848
  }
25395
25849
  };