@wrongstack/plugins 0.281.1 → 0.282.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +29 -3
  2. package/dist/accessibility-auditor.d.ts +40 -0
  3. package/dist/accessibility-auditor.js +411 -0
  4. package/dist/agent-handoff.d.ts +37 -0
  5. package/dist/agent-handoff.js +298 -0
  6. package/dist/api-compatibility-gate.d.ts +38 -0
  7. package/dist/api-compatibility-gate.js +357 -0
  8. package/dist/auto-i18n-extractor.d.ts +36 -0
  9. package/dist/auto-i18n-extractor.js +335 -0
  10. package/dist/checkpoint.js +18 -0
  11. package/dist/code-metrics.d.ts +31 -0
  12. package/dist/code-metrics.js +338 -0
  13. package/dist/commit-validator.js +67 -12
  14. package/dist/cost-tracker.js +58 -16
  15. package/dist/dead-code-detector.d.ts +34 -0
  16. package/dist/dead-code-detector.js +354 -0
  17. package/dist/dep-guard.js +47 -6
  18. package/dist/dependency-vulnerability-gate.d.ts +35 -0
  19. package/dist/dependency-vulnerability-gate.js +308 -0
  20. package/dist/diff-summary.js +97 -10
  21. package/dist/doc-sync-guard.d.ts +33 -0
  22. package/dist/doc-sync-guard.js +223 -0
  23. package/dist/duplicate-code-detector.d.ts +33 -0
  24. package/dist/duplicate-code-detector.js +384 -0
  25. package/dist/feature-flag-tracker.d.ts +38 -0
  26. package/dist/feature-flag-tracker.js +316 -0
  27. package/dist/file-watcher.js +85 -36
  28. package/dist/format-on-save.js +76 -10
  29. package/dist/import-organizer.js +73 -14
  30. package/dist/index.d.ts +27 -0
  31. package/dist/index.js +14067 -5054
  32. package/dist/interface-contract-guard.d.ts +37 -0
  33. package/dist/interface-contract-guard.js +302 -0
  34. package/dist/knowledge-graph.d.ts +45 -0
  35. package/dist/knowledge-graph.js +325 -0
  36. package/dist/license-audit-gate.d.ts +34 -0
  37. package/dist/license-audit-gate.js +260 -0
  38. package/dist/llm-cache.js +5 -0
  39. package/dist/loop-breaker.d.ts +0 -38
  40. package/dist/loop-breaker.js +209 -8
  41. package/dist/migration-planner.d.ts +30 -0
  42. package/dist/migration-planner.js +349 -0
  43. package/dist/model-router.js +5 -0
  44. package/dist/performance-regression-gate.d.ts +33 -0
  45. package/dist/performance-regression-gate.js +315 -0
  46. package/dist/plugin-stack-observer.d.ts +35 -0
  47. package/dist/plugin-stack-observer.js +125 -0
  48. package/dist/pr-drafter.d.ts +35 -0
  49. package/dist/pr-drafter.js +334 -0
  50. package/dist/prompt-firewall.js +5 -0
  51. package/dist/refactor-suggester.d.ts +38 -0
  52. package/dist/refactor-suggester.js +382 -0
  53. package/dist/release-notes-generator.d.ts +27 -0
  54. package/dist/release-notes-generator.js +209 -0
  55. package/dist/schema-evolution-guard.d.ts +42 -0
  56. package/dist/schema-evolution-guard.js +319 -0
  57. package/dist/security-hotspot-scanner.d.ts +30 -0
  58. package/dist/security-hotspot-scanner.js +402 -0
  59. package/dist/semantic-search-indexer.d.ts +38 -0
  60. package/dist/semantic-search-indexer.js +436 -0
  61. package/dist/shell-check.js +38 -3
  62. package/dist/smart-rename.d.ts +26 -0
  63. package/dist/smart-rename.js +170 -0
  64. package/dist/spec-linker.js +273 -133
  65. package/dist/test-coverage-gate.d.ts +37 -0
  66. package/dist/test-coverage-gate.js +263 -0
  67. package/dist/test-flake-detector.d.ts +27 -0
  68. package/dist/test-flake-detector.js +274 -0
  69. package/dist/test-generator.d.ts +32 -0
  70. package/dist/test-generator.js +243 -0
  71. package/dist/test-runner-gate.js +154 -22
  72. package/dist/todo-listener.d.ts +2 -2
  73. package/dist/todo-listener.js +5 -5
  74. package/dist/token-throttle.js +5 -0
  75. package/dist/type-gate.d.ts +37 -0
  76. package/dist/type-gate.js +311 -0
  77. package/package.json +112 -4
  78. package/LICENSE +0 -21
@@ -0,0 +1,243 @@
1
+ import { readFileSync } from 'fs';
2
+ import { resolve, isAbsolute, relative } from 'path';
3
+
4
+ // src/test-generator/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ generateCount: 0,
8
+ exportCount: 0,
9
+ errorCount: 0
10
+ };
11
+ var DEFAULTS = {
12
+ enabled: true,
13
+ framework: "vitest",
14
+ testSuffix: ".test",
15
+ includeImports: true
16
+ };
17
+ function readConfig(raw) {
18
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
19
+ const r = raw;
20
+ return {
21
+ enabled: r["enabled"] !== false,
22
+ framework: typeof r["framework"] === "string" ? r["framework"] : DEFAULTS.framework,
23
+ testSuffix: typeof r["testSuffix"] === "string" ? r["testSuffix"] : DEFAULTS.testSuffix,
24
+ includeImports: r["includeImports"] !== false
25
+ };
26
+ }
27
+ function withinProject(p) {
28
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
29
+ const root = process.cwd();
30
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
31
+ const rel = relative(root, resolved);
32
+ if (rel === "" || rel === ".") return true;
33
+ if (rel.startsWith("..")) return false;
34
+ if (isAbsolute(rel)) return false;
35
+ return true;
36
+ }
37
+ function toPosix(p) {
38
+ return p.replace(/\\/g, "/");
39
+ }
40
+ function relativePath(p) {
41
+ return toPosix(relative(process.cwd(), p));
42
+ }
43
+ function detectExports(content) {
44
+ const exports = [];
45
+ const seen = /* @__PURE__ */ new Set();
46
+ const functionRe = /export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
47
+ const arrowRe = /export\s+(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/g;
48
+ const classRe = /export\s+(?:abstract\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g;
49
+ const namedRe = /export\s*\{([^}]+)\}/g;
50
+ let m;
51
+ functionRe.lastIndex = 0;
52
+ while ((m = functionRe.exec(content)) !== null) {
53
+ if (!seen.has(m[1])) {
54
+ seen.add(m[1]);
55
+ exports.push({ name: m[1], kind: "function" });
56
+ }
57
+ }
58
+ arrowRe.lastIndex = 0;
59
+ while ((m = arrowRe.exec(content)) !== null) {
60
+ if (!seen.has(m[1])) {
61
+ seen.add(m[1]);
62
+ exports.push({ name: m[1], kind: "arrow" });
63
+ }
64
+ }
65
+ classRe.lastIndex = 0;
66
+ while ((m = classRe.exec(content)) !== null) {
67
+ if (!seen.has(m[1])) {
68
+ seen.add(m[1]);
69
+ exports.push({ name: m[1], kind: "class" });
70
+ }
71
+ }
72
+ namedRe.lastIndex = 0;
73
+ while ((m = namedRe.exec(content)) !== null) {
74
+ const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
75
+ for (const raw of names) {
76
+ const parts = raw.split(/\s+as\s+/);
77
+ const name = (parts.length > 1 ? parts[parts.length - 1] : raw).trim();
78
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) continue;
79
+ if (!seen.has(name)) {
80
+ seen.add(name);
81
+ exports.push({ name, kind: "named" });
82
+ }
83
+ }
84
+ }
85
+ return exports;
86
+ }
87
+ function generateTestContent(filePath, detected, cfg) {
88
+ const sourcePath = relativePath(filePath);
89
+ const importNames = detected.map((e) => e.name).join(", ");
90
+ const lines = [];
91
+ if (cfg.framework === "vitest") {
92
+ lines.push(`import { describe, it, expect } from 'vitest';`);
93
+ } else {
94
+ lines.push(`const { describe, it, expect } = require('${cfg.framework}');`);
95
+ }
96
+ if (cfg.includeImports && detected.length > 0) {
97
+ lines.push(`import { ${importNames} } from './${sourcePath.replace(/\.[^.]+$/, "")}';`);
98
+ }
99
+ lines.push("");
100
+ lines.push(`describe('${sourcePath}', () => {`);
101
+ for (const exp of detected) {
102
+ lines.push(` it('${exp.name} behaves as expected', () => {`);
103
+ lines.push(` // TODO: replace with a real assertion for ${exp.name}`);
104
+ if (exp.kind === "class") {
105
+ lines.push(` const instance = new ${exp.name}();`);
106
+ lines.push(` expect(instance).toBeDefined();`);
107
+ } else {
108
+ lines.push(` expect(${exp.name}).toBeDefined();`);
109
+ }
110
+ lines.push(` });`);
111
+ lines.push("");
112
+ }
113
+ if (detected.length === 0) {
114
+ lines.push(` it('has no exported symbols to test', () => {`);
115
+ lines.push(` expect(true).toBe(true);`);
116
+ lines.push(` });`);
117
+ }
118
+ lines.push(`});`);
119
+ return lines.join("\n");
120
+ }
121
+ function generateForFile(filePath, cfg) {
122
+ const content = readFileSync(filePath, "utf-8");
123
+ const detected = detectExports(content);
124
+ const sourceName = relativePath(filePath).split("/").pop();
125
+ const baseName = sourceName.replace(/\.[^.]+$/, "");
126
+ const testFile = `${baseName}${cfg.testSuffix}.ts`;
127
+ return {
128
+ sourceFile: relativePath(filePath),
129
+ testFile,
130
+ exports: detected,
131
+ content: generateTestContent(filePath, detected, cfg)
132
+ };
133
+ }
134
+ var plugin = {
135
+ name: "test-generator",
136
+ version: "0.1.0",
137
+ description: "Generates a test file skeleton from exported functions, classes, and arrow functions",
138
+ apiVersion: API_VERSION,
139
+ capabilities: { tools: true },
140
+ defaultConfig: { ...DEFAULTS },
141
+ configSchema: {
142
+ type: "object",
143
+ properties: {
144
+ enabled: { type: "boolean", default: true, description: "Master switch." },
145
+ framework: {
146
+ type: "string",
147
+ default: "vitest",
148
+ description: "Test framework to target."
149
+ },
150
+ testSuffix: {
151
+ type: "string",
152
+ default: ".test",
153
+ description: "Suffix inserted before the extension of the generated test filename."
154
+ },
155
+ includeImports: {
156
+ type: "boolean",
157
+ default: true,
158
+ description: "Emit import statements for detected exports."
159
+ }
160
+ }
161
+ },
162
+ setup(api) {
163
+ state.generateCount = 0;
164
+ state.exportCount = 0;
165
+ state.errorCount = 0;
166
+ const cfg = readConfig(api.config.extensions?.["test-generator"]);
167
+ api.tools.register({
168
+ name: "generate_unit_tests",
169
+ description: "Generate a test skeleton for a source file. Detects exported functions, arrow functions, classes, and named exports. Returns the test content as a string; it does not write to disk.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ path: {
174
+ type: "string",
175
+ description: "Source file path (relative to project root)."
176
+ }
177
+ },
178
+ required: ["path"]
179
+ },
180
+ permission: "auto",
181
+ category: "Development",
182
+ mutating: false,
183
+ async execute(input) {
184
+ if (!cfg.enabled) return { ok: false, error: "test-generator is disabled" };
185
+ const rawPath = input.path;
186
+ if (!rawPath || typeof rawPath !== "string") {
187
+ return { ok: false, error: "path is required" };
188
+ }
189
+ if (!withinProject(rawPath)) {
190
+ return { ok: false, error: "path is outside the project root" };
191
+ }
192
+ const resolved = resolve(process.cwd(), rawPath);
193
+ state.generateCount += 1;
194
+ let result;
195
+ try {
196
+ result = generateForFile(resolved, cfg);
197
+ } catch (err) {
198
+ state.errorCount += 1;
199
+ return { ok: false, error: String(err) };
200
+ }
201
+ state.exportCount += result.exports.length;
202
+ return {
203
+ ok: true,
204
+ sourceFile: result.sourceFile,
205
+ testFile: result.testFile,
206
+ framework: cfg.framework,
207
+ exports: result.exports,
208
+ content: result.content
209
+ };
210
+ }
211
+ });
212
+ api.log.info("test-generator plugin loaded", {
213
+ version: "0.1.0",
214
+ framework: cfg.framework,
215
+ testSuffix: cfg.testSuffix
216
+ });
217
+ },
218
+ teardown(api) {
219
+ const final = {
220
+ generated: state.generateCount,
221
+ exports: state.exportCount,
222
+ errors: state.errorCount
223
+ };
224
+ state.generateCount = 0;
225
+ state.exportCount = 0;
226
+ state.errorCount = 0;
227
+ api.log.info("test-generator: teardown complete", { final });
228
+ },
229
+ async health() {
230
+ return {
231
+ ok: state.errorCount === 0,
232
+ message: state.errorCount ? `test-generator: ${state.errorCount} error(s)` : `test-generator: ${state.generateCount} generation(s), ${state.exportCount} export(s)`,
233
+ counters: {
234
+ generated: state.generateCount,
235
+ exports: state.exportCount,
236
+ errors: state.errorCount
237
+ }
238
+ };
239
+ }
240
+ };
241
+ var test_generator_default = plugin;
242
+
243
+ export { test_generator_default as default };
@@ -1,6 +1,6 @@
1
- import { execSync } from 'child_process';
1
+ import { execSync, execFileSync } from 'child_process';
2
2
  import { existsSync } from 'fs';
3
- import { basename, dirname, join } from 'path';
3
+ import { isAbsolute, resolve, relative, basename, dirname, join } from 'path';
4
4
 
5
5
  // src/test-runner-gate/index.ts
6
6
  var API_VERSION = "^0.1.10";
@@ -16,36 +16,82 @@ var state = {
16
16
  noTestCount: 0,
17
17
  /** Times the test runner itself failed (timeout, crash). */
18
18
  errorCount: 0,
19
+ /** Times the extension filter short-circuited the hook (.json, .md, etc.). */
20
+ extensionSkippedCount: 0,
21
+ /** Times a re-run was skipped because the source hash matched a previous PASS. */
22
+ cachedSkipCount: 0,
19
23
  /** Hook handle for teardown. */
20
24
  hookUnregister: null,
21
25
  /** Last test result — surfaced by health() + status tool. */
22
26
  lastResult: null
23
27
  };
24
28
  var DEFAULTS = {
25
- enabled: true,
29
+ enabled: false,
26
30
  runner: "auto",
27
31
  command: "",
28
32
  timeoutMs: 3e4,
29
- testFilePatterns: [
30
- "src/{name}.test.ts",
31
- "tests/{name}.test.ts",
32
- "tests/{name}-exec.test.ts"
33
- ],
34
- injectOnPass: false
33
+ testFilePatterns: ["src/{name}.test.ts", "tests/{name}.test.ts", "tests/{name}-exec.test.ts"],
34
+ injectOnPass: false,
35
+ enableContentHashCache: true,
36
+ enableExtensionFilter: true
35
37
  };
36
38
  function readConfig(raw) {
37
39
  if (!raw || typeof raw !== "object") return { ...DEFAULTS };
38
40
  const r = raw;
39
41
  const runner = r["runner"] === "vitest" || r["runner"] === "jest" || r["runner"] === "mocha" ? r["runner"] : "auto";
40
42
  return {
41
- enabled: r["enabled"] !== false,
43
+ enabled: r["enabled"] === true,
42
44
  runner,
43
45
  command: typeof r["command"] === "string" ? r["command"] : DEFAULTS.command,
44
46
  timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] : DEFAULTS.timeoutMs,
45
47
  testFilePatterns: Array.isArray(r["testFilePatterns"]) && r["testFilePatterns"].length > 0 ? r["testFilePatterns"].filter((x) => typeof x === "string") : DEFAULTS.testFilePatterns,
46
- injectOnPass: r["injectOnPass"] === true
48
+ injectOnPass: r["injectOnPass"] === true,
49
+ enableContentHashCache: r["enableContentHashCache"] !== false,
50
+ enableExtensionFilter: r["enableExtensionFilter"] !== false
47
51
  };
48
52
  }
53
+ var TESTABLE_DEFAULT_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".cts"]);
54
+ function getResolvableExtensions(patterns) {
55
+ const exts = /* @__PURE__ */ new Set();
56
+ for (const p of patterns) {
57
+ const m = /\.([a-z0-9]+)$/i.exec(p);
58
+ if (m?.[1]) exts.add(`.${m[1].toLowerCase()}`);
59
+ }
60
+ return exts;
61
+ }
62
+ function pathContentHash(content) {
63
+ const cap = Math.min(content.length, 65536);
64
+ let h = 5381;
65
+ for (let i = 0; i < cap; i++) {
66
+ h = (h << 5) + h + content.charCodeAt(i) | 0;
67
+ }
68
+ return h >>> 0;
69
+ }
70
+ var lastPassedHash = /* @__PURE__ */ new Map();
71
+ function withinProject(p) {
72
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
73
+ const root = process.cwd();
74
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
75
+ const rel = relative(root, resolved);
76
+ if (rel === "" || rel === ".") return true;
77
+ if (rel.startsWith("..")) return false;
78
+ if (isAbsolute(rel)) return false;
79
+ return true;
80
+ }
81
+ var ALLOWED_COMMAND_TOKENS = /* @__PURE__ */ new Set([
82
+ "npx",
83
+ "pnpm",
84
+ "pnpm",
85
+ "npm",
86
+ "yarn",
87
+ "vitest",
88
+ "jest",
89
+ "mocha",
90
+ // Useful for `command: "node ./scripts/run-tests.js"` style configs.
91
+ "node"
92
+ // Direct binary paths under the project's node_modules — resolved
93
+ // by basename in resolveAllowedCommand().
94
+ ]);
49
95
  function resolveTestFiles(sourcePath, patterns) {
50
96
  const name = basename(sourcePath).replace(/\.[^.]+$/, "");
51
97
  const pathNoExt = sourcePath.replace(/\.[^.]+$/, "");
@@ -106,16 +152,49 @@ function detectRunner(requested) {
106
152
  }
107
153
  return null;
108
154
  }
155
+ function resolveAllowedCommand(customCommand) {
156
+ const tokens = customCommand.split(/\s+/).filter(Boolean);
157
+ if (tokens.length === 0) return null;
158
+ const head = tokens[0];
159
+ if (ALLOWED_COMMAND_TOKENS.has(head)) {
160
+ return { cmd: head, args: tokens.slice(1) };
161
+ }
162
+ if (isAbsolute(head)) {
163
+ if (!withinProject(head)) return null;
164
+ const base = basename(head);
165
+ if (ALLOWED_COMMAND_TOKENS.has(base)) {
166
+ return { cmd: head, args: tokens.slice(1) };
167
+ }
168
+ }
169
+ return null;
170
+ }
109
171
  function runTests(testFile, runner, customCommand, timeoutMs) {
110
- const baseCommand = customCommand || runner.command;
111
- const fullCommand = `${baseCommand} "${testFile}" ${runner.jsonFlags}`;
172
+ if (!withinProject(testFile)) return null;
173
+ let cmd;
174
+ let cmdArgs;
175
+ let trailingFlag;
176
+ if (customCommand) {
177
+ const resolved = resolveAllowedCommand(customCommand);
178
+ if (!resolved) return null;
179
+ cmd = resolved.cmd;
180
+ cmdArgs = [...resolved.args, testFile];
181
+ trailingFlag = runner.jsonFlags;
182
+ } else {
183
+ const tokens = runner.command.split(/\s+/).filter(Boolean);
184
+ cmd = tokens[0];
185
+ cmdArgs = [...tokens.slice(1), testFile];
186
+ trailingFlag = runner.jsonFlags;
187
+ }
188
+ const trailing = trailingFlag.split(/\s+/).filter(Boolean);
189
+ const fullArgs = [...cmdArgs, ...trailing];
112
190
  let stdout = "";
113
191
  try {
114
- stdout = execSync(fullCommand, {
192
+ stdout = execFileSync(cmd, fullArgs, {
115
193
  encoding: "utf-8",
116
194
  timeout: timeoutMs,
117
195
  cwd: process.cwd(),
118
- stdio: ["pipe", "pipe", "pipe"]
196
+ stdio: ["pipe", "pipe", "pipe"],
197
+ shell: false
119
198
  });
120
199
  } catch (err) {
121
200
  const e = err;
@@ -176,7 +255,7 @@ var plugin = {
176
255
  properties: {
177
256
  enabled: {
178
257
  type: "boolean",
179
- default: true,
258
+ default: false,
180
259
  description: "Master switch."
181
260
  },
182
261
  runner: {
@@ -206,6 +285,16 @@ var plugin = {
206
285
  type: "boolean",
207
286
  default: false,
208
287
  description: "Inject additionalContext when tests pass too (default: only on failure)."
288
+ },
289
+ enableContentHashCache: {
290
+ type: "boolean",
291
+ default: true,
292
+ description: "Skip re-running tests when the source path is touched again with the same content hash as a previous PASS in this session."
293
+ },
294
+ enableExtensionFilter: {
295
+ type: "boolean",
296
+ default: true,
297
+ description: "Fast-path skip for non-TS/JS files (.json, .md, .lock, .txt, ...) before the test-file resolve walk."
209
298
  }
210
299
  }
211
300
  },
@@ -216,14 +305,20 @@ var plugin = {
216
305
  state.failCount = 0;
217
306
  state.noTestCount = 0;
218
307
  state.errorCount = 0;
308
+ state.extensionSkippedCount = 0;
309
+ state.cachedSkipCount = 0;
219
310
  state.hookUnregister = null;
220
311
  state.lastResult = null;
312
+ lastPassedHash.clear();
221
313
  const cfg = readConfig(api.config.extensions?.["test-runner-gate"]);
222
314
  const runner = detectRunner(cfg.runner);
223
315
  if (!runner) {
224
- api.log.warn("test-runner-gate: no test runner found (vitest, jest, mocha) \u2014 hook will be a no-op", {
225
- requested: cfg.runner
226
- });
316
+ api.log.warn(
317
+ "test-runner-gate: no test runner found (vitest, jest, mocha) \u2014 hook will be a no-op",
318
+ {
319
+ requested: cfg.runner
320
+ }
321
+ );
227
322
  } else {
228
323
  api.log.info("test-runner-gate: detected runner", { name: runner.name });
229
324
  }
@@ -233,8 +328,30 @@ var plugin = {
233
328
  const inp = input.toolInput ?? {};
234
329
  const sourcePath = inp["path"];
235
330
  if (!sourcePath || typeof sourcePath !== "string") return;
331
+ if (!withinProject(sourcePath)) return;
236
332
  if (sourcePath.includes(".test.") || sourcePath.includes(".spec.")) return;
333
+ if (cfg.enableExtensionFilter) {
334
+ const ext = sourcePath.includes(".") ? sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() : "";
335
+ const resolvable = getResolvableExtensions(cfg.testFilePatterns);
336
+ if (ext && !resolvable.has(ext) && !TESTABLE_DEFAULT_EXTS.has(ext)) {
337
+ state.extensionSkippedCount += 1;
338
+ api.metrics.counter("extension_skipped");
339
+ return;
340
+ }
341
+ }
237
342
  state.invocationCount += 1;
343
+ if (cfg.enableContentHashCache) {
344
+ const content = input.toolName === "write" ? inp["content"] ?? "" : input.toolName === "edit" ? inp["new_string"] ?? "" : "";
345
+ if (typeof content === "string" && content.length > 0) {
346
+ const hash = pathContentHash(content);
347
+ const last = lastPassedHash.get(sourcePath);
348
+ if (last !== void 0 && last === hash) {
349
+ state.cachedSkipCount += 1;
350
+ api.metrics.counter("cached_skip");
351
+ return;
352
+ }
353
+ }
354
+ }
238
355
  const testFile = findTestFile(sourcePath, cfg.testFilePatterns);
239
356
  if (!testFile) {
240
357
  state.noTestCount += 1;
@@ -256,6 +373,12 @@ var plugin = {
256
373
  };
257
374
  if (result.passed) {
258
375
  state.passCount += 1;
376
+ if (cfg.enableContentHashCache) {
377
+ const content = input.toolName === "write" ? inp["content"] ?? "" : input.toolName === "edit" ? inp["new_string"] ?? "" : "";
378
+ if (typeof content === "string" && content.length > 0) {
379
+ lastPassedHash.set(sourcePath, pathContentHash(content));
380
+ }
381
+ }
259
382
  if (!cfg.injectOnPass) return;
260
383
  return {
261
384
  additionalContext: `
@@ -298,7 +421,9 @@ Fix the failing tests or revert the change if it broke something.`
298
421
  passed: state.passCount,
299
422
  failed: state.failCount,
300
423
  noTest: state.noTestCount,
301
- errors: state.errorCount
424
+ errors: state.errorCount,
425
+ extensionSkipped: state.extensionSkippedCount,
426
+ cachedSkips: state.cachedSkipCount
302
427
  },
303
428
  lastResult: state.lastResult
304
429
  };
@@ -324,7 +449,9 @@ Fix the failing tests or revert the change if it broke something.`
324
449
  passed: state.passCount,
325
450
  failed: state.failCount,
326
451
  noTest: state.noTestCount,
327
- errors: state.errorCount
452
+ errors: state.errorCount,
453
+ extensionSkipped: state.extensionSkippedCount,
454
+ cachedSkips: state.cachedSkipCount
328
455
  };
329
456
  state.invocationCount = 0;
330
457
  state.runCount = 0;
@@ -332,7 +459,10 @@ Fix the failing tests or revert the change if it broke something.`
332
459
  state.failCount = 0;
333
460
  state.noTestCount = 0;
334
461
  state.errorCount = 0;
462
+ state.extensionSkippedCount = 0;
463
+ state.cachedSkipCount = 0;
335
464
  state.lastResult = null;
465
+ lastPassedHash.clear();
336
466
  api.log.info("test-runner-gate: teardown complete", { final });
337
467
  },
338
468
  async health() {
@@ -345,7 +475,9 @@ Fix the failing tests or revert the change if it broke something.`
345
475
  passed: state.passCount,
346
476
  failed: state.failCount,
347
477
  noTest: state.noTestCount,
348
- errors: state.errorCount
478
+ errors: state.errorCount,
479
+ extensionSkipped: state.extensionSkippedCount,
480
+ cachedSkips: state.cachedSkipCount
349
481
  },
350
482
  lastResult: state.lastResult
351
483
  };
@@ -22,10 +22,10 @@ import { Plugin } from '@wrongstack/core';
22
22
  *
23
23
  * ```jsonc
24
24
  * {
25
- * "enabled": true,
25
+ * "enabled": false,
26
26
  * "subjectPrefix": "todo: ",
27
27
  * "broadcastOnChange": true,
28
- * "cooldownMs": 5000
28
+ * "cooldownMs": 30000
29
29
  * }
30
30
  * ```
31
31
  *
@@ -10,16 +10,16 @@ var state = {
10
10
  hookUnregister: null
11
11
  };
12
12
  var DEFAULTS = {
13
- enabled: true,
13
+ enabled: false,
14
14
  subjectPrefix: "todo: ",
15
15
  broadcastOnChange: true,
16
- cooldownMs: 5e3
16
+ cooldownMs: 3e4
17
17
  };
18
18
  function readConfig(raw) {
19
19
  if (!raw || typeof raw !== "object") return { ...DEFAULTS };
20
20
  const r = raw;
21
21
  return {
22
- enabled: r["enabled"] !== false,
22
+ enabled: r["enabled"] === true,
23
23
  subjectPrefix: typeof r["subjectPrefix"] === "string" ? r["subjectPrefix"] : DEFAULTS.subjectPrefix,
24
24
  broadcastOnChange: r["broadcastOnChange"] !== false,
25
25
  cooldownMs: typeof r["cooldownMs"] === "number" && r["cooldownMs"] >= 0 ? r["cooldownMs"] : DEFAULTS.cooldownMs
@@ -44,7 +44,7 @@ var plugin = {
44
44
  configSchema: {
45
45
  type: "object",
46
46
  properties: {
47
- enabled: { type: "boolean", default: true, description: "Master switch." },
47
+ enabled: { type: "boolean", default: false, description: "Master switch." },
48
48
  subjectPrefix: {
49
49
  type: "string",
50
50
  default: DEFAULTS.subjectPrefix,
@@ -58,7 +58,7 @@ var plugin = {
58
58
  cooldownMs: {
59
59
  type: "number",
60
60
  minimum: 0,
61
- default: 5e3,
61
+ default: DEFAULTS.cooldownMs,
62
62
  description: "Minimum interval between consecutive broadcasts (ms)."
63
63
  }
64
64
  }
@@ -110,6 +110,11 @@ var plugin = {
110
110
  }
111
111
  const cfg = readConfig(api.config.extensions?.["token-throttle"]);
112
112
  if (cfg.enabled) {
113
+ api.emitCustom?.("provider.wrap:loaded", {
114
+ plugin: "token-throttle",
115
+ kind: "throttle",
116
+ wraps: ["request"]
117
+ });
113
118
  state.extensionUnregister = api.extensions.register({
114
119
  name: "token-throttle",
115
120
  owner: "token-throttle",
@@ -0,0 +1,37 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * type-gate plugin — PostToolUse hook that runs TypeScript type-checking
5
+ * after every `write` or `edit` to a source file.
6
+ *
7
+ * `lint-gate` validates style; this plugin validates types. It runs
8
+ * `tsc --noEmit` (or a user-supplied command) and injects the first few
9
+ * errors as `additionalContext` so the model can fix type regressions in
10
+ * the same turn.
11
+ *
12
+ * Tools registered:
13
+ * - type_gate_status : Show config + per-session counters.
14
+ *
15
+ * Hooks registered:
16
+ * - PostToolUse with matcher `write|edit`.
17
+ *
18
+ * Config (`config.extensions['type-gate']`):
19
+ *
20
+ * ```jsonc
21
+ * {
22
+ * "enabled": true,
23
+ * "command": "npx tsc --noEmit", // base type-check command
24
+ * "tsConfigPath": "tsconfig.json",
25
+ * "timeoutMs": 60000,
26
+ * "failSeverity": "warn", // "warn" | "block"
27
+ * "maxErrors": 5, // errors shown in context
28
+ * "runOnChange": [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"]
29
+ * }
30
+ * ```
31
+ *
32
+ * @public
33
+ */
34
+
35
+ declare const plugin: Plugin;
36
+
37
+ export { plugin as default };