@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
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @wrongstack/plugins
2
2
 
3
3
  First-party plugin collection for [WrongStack](https://github.com/WrongStack/WrongStack).
4
- Thirty-six focused, single-purpose plugins ship in this package. Core safety
4
+ Sixty-two focused, single-purpose plugins ship in this package. Core safety
5
5
  plugins load automatically for every `wstack` session; provider-wire plugins
6
6
  are opt-in because they can change model-call semantics.
7
7
 
@@ -58,6 +58,32 @@ under the `BUILTIN_PLUGIN_FACTORIES` array. To opt out, add
58
58
  | 34 | [`prompt-firewall`](./src/prompt-firewall) | `prompt_firewall_status` | Provider runner wrapper | Opt-in provider-wire prompt/secret scanning and redaction/blocking |
59
59
  | 35 | [`auto-escalate`](./src/auto-escalate) | `auto_escalate_status` | `onError` extension | Opt-in retry/escalation ladder for transient provider errors |
60
60
  | 36 | [`token-throttle`](./src/token-throttle) | `token_throttle_status` | Provider runner wrapper | Opt-in rolling-window token throttling for provider calls |
61
+ | 37 | [`type-gate`](./src/type-gate) | `type_gate_status` | `PostToolUse` | Runs `tsc --noEmit` after `write`/`edit` to catch type regressions |
62
+ | 38 | [`test-coverage-gate`](./src/test-coverage-gate) | `coverage_gate_status` | `PostToolUse` | Detects test-coverage regressions after source-file edits |
63
+ | 39 | [`pr-drafter`](./src/pr-drafter) | `pr_draft` | `Stop` + event subscriptions | Drafts a pull-request description from session commits/files/diff |
64
+ | 40 | [`agent-handoff`](./src/agent-handoff) | `handoff_note`, `handoff_status` | `subagent.done` event listener | Posts structured handoff notes when subagents finish |
65
+ | 41 | [`knowledge-graph`](./src/knowledge-graph) | `kg_add_fact`, `kg_query`, `kg_remove_fact`, `kg_status` | System prompt contributor | Persistent structured (subject, relation, object) project facts |
66
+ | 42 | [`dead-code-detector`](./src/dead-code-detector) | `dead_code_scan` | `PostToolUse` | Regex-based scan for exported identifiers that appear unused |
67
+ | 43 | [`dependency-vulnerability-gate`](./src/dependency-vulnerability-gate) | `dependency_audit_status` | `PostToolUse` | Runs `npm/pnpm audit` after installs and blocks on severe vulnerabilities |
68
+ | 44 | [`migration-planner`](./src/migration-planner) | `migration_plan`, `migration_status` | `PostToolUse` | Reads changelogs and produces version-migration checklists |
69
+ | 45 | [`semantic-search-indexer`](./src/semantic-search-indexer) | `semantic_search`, `semantic_index_status` | — | Lightweight keyword index over project source files |
70
+ | 46 | [`auto-i18n-extractor`](./src/auto-i18n-extractor) | `i18n_extract`, `i18n_status` | `PostToolUse` | Detects hardcoded user-facing strings and suggests i18n keys |
71
+ | 47 | [`doc-sync-guard`](./src/doc-sync-guard) | `doc_sync_status` | `PostToolUse` | Warns when README/docs edits omit recently changed source files |
72
+ | 48 | [`api-compatibility-gate`](./src/api-compatibility-gate) | `api_compat_status` | `PostToolUse` | Detects removed exports in entry-point files as breaking changes |
73
+ | 49 | [`performance-regression-gate`](./src/performance-regression-gate) | `perf_regression_status` | — | Compares benchmark results and reports regressions |
74
+ | 50 | [`test-flake-detector`](./src/test-flake-detector) | `flake_detect`, `flake_status` | — | Runs a test command N times to identify flaky tests |
75
+ | 51 | [`schema-evolution-guard`](./src/schema-evolution-guard) | `schema_evolution_status` | `PostToolUse` | Detects destructive patterns in DB/API schema files |
76
+ | 52 | [`license-audit-gate`](./src/license-audit-gate) | `license_audit_status` | `PostToolUse` | Audits newly added dependency licenses against an allowlist |
77
+ | 53 | [`accessibility-auditor`](./src/accessibility-auditor) | `a11y_audit`, `a11y_status` | `PostToolUse` | Audits UI files for missing alt, labels, button text, duplicate ids |
78
+ | 54 | [`security-hotspot-scanner`](./src/security-hotspot-scanner) | `security_hotspot_scan`, `security_hotspot_status` | `PostToolUse` | Scans source for security anti-patterns after writes/edits |
79
+ | 55 | [`duplicate-code-detector`](./src/duplicate-code-detector) | `detect_duplicate_code`, `duplicate_code_status` | `PostToolUse` | Detects duplicate/similar code blocks using normalized-line fingerprints |
80
+ | 56 | [`code-metrics`](./src/code-metrics) | `measure_code_metrics`, `metrics_status` | `PostToolUse` | Computes LOC, comment ratio, function count, and approximate cyclomatic complexity |
81
+ | 57 | [`refactor-suggester`](./src/refactor-suggester) | `suggest_refactors`, `refactor_status` | `PostToolUse` | Flags long functions, deep nesting, many parameters, magic numbers, console.log |
82
+ | 58 | [`test-generator`](./src/test-generator) | `generate_unit_tests` | — | Generates a unit-test skeleton from exported functions/classes |
83
+ | 59 | [`release-notes-generator`](./src/release-notes-generator) | `generate_release_notes` | — | Groups conventional commits into a release-notes markdown draft |
84
+ | 60 | [`smart-rename`](./src/smart-rename) | `smart_rename` | — | Whole-word symbol rename with preview and optional apply |
85
+ | 61 | [`feature-flag-tracker`](./src/feature-flag-tracker) | `scan_feature_flags`, `feature_flag_status` | `PostToolUse` | Scans source files for feature-flag usage and reports flag inventory |
86
+ | 62 | [`interface-contract-guard`](./src/interface-contract-guard) | `check_interface_contracts`, `interface_contract_status` | `PostToolUse` | Warns when TypeScript interfaces change or lack implementers |
61
87
 
62
88
  ### Removed plugins (use built-in tools instead)
63
89
 
@@ -672,7 +698,7 @@ context. Increase `includeTranscriptTail` for more history.
672
698
  Two hooks working together:
673
699
 
674
700
  **PostToolUse** (always active when the plugin is enabled):
675
- scans markdown files for *unlinked* references to one of the 36
701
+ scans markdown files for *unlinked* references to one of the 62
676
702
  known plugins and surfaces them to the LLM via
677
703
  `additionalContext`. The plugin does NOT modify the file — it
678
704
  only injects a low-noise context block listing the unlinked
@@ -696,7 +722,7 @@ the PostToolUse context tells the LLM what to fix.
696
722
 
697
723
  **Detection rules** (both hooks):
698
724
  - Source matches `config.fileGlobs` (default: `**/*.md`, `**/*.mdx`)
699
- - The reference matches one of the 36 known plugin names
725
+ - The reference matches one of the 62 known plugin names
700
726
  (case-insensitive)
701
727
  - It is NOT already wrapped in a markdown link `[name](...)` or
702
728
  inline code `` `name` ``
@@ -0,0 +1,40 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * accessibility-auditor plugin — audits UI files for common accessibility
5
+ * issues using fast regex-based heuristics.
6
+ *
7
+ * Tools registered:
8
+ * - a11y_audit : Scan a file or directory for a11y issues.
9
+ * - a11y_status : Show config + per-session counters.
10
+ *
11
+ * Hooks registered:
12
+ * - PostToolUse with matcher `write|edit` to UI files, injecting a short
13
+ * additionalContext summary of any new accessibility issues.
14
+ *
15
+ * Config (`config.extensions['accessibility-auditor']`):
16
+ *
17
+ * ```jsonc
18
+ * {
19
+ * "enabled": true,
20
+ * "includeExtensions": [".tsx", ".jsx", ".html", ".vue"],
21
+ * "maxFindings": 50,
22
+ * "severity": "warn", // "warn" | "block"
23
+ * "onWriteEdit": true
24
+ * }
25
+ * ```
26
+ *
27
+ * @public
28
+ */
29
+
30
+ type A11yRule = 'missing-alt' | 'missing-input-label' | 'low-contrast-placeholder' | 'missing-button-text' | 'duplicate-id';
31
+ interface A11yFinding {
32
+ file: string;
33
+ line: number;
34
+ rule: A11yRule;
35
+ severity: 'error' | 'warning';
36
+ message: string;
37
+ }
38
+ declare const plugin: Plugin;
39
+
40
+ export { type A11yFinding, type A11yRule, plugin as default };
@@ -0,0 +1,411 @@
1
+ import { existsSync, statSync, readFileSync, readdirSync } from 'fs';
2
+ import { isAbsolute, resolve, relative } from 'path';
3
+
4
+ // src/accessibility-auditor/index.ts
5
+ var API_VERSION = "^0.1.10";
6
+ var state = {
7
+ auditCount: 0,
8
+ fileCount: 0,
9
+ findingCount: 0,
10
+ hookInvocationCount: 0,
11
+ lastResult: null,
12
+ hookUnregister: null
13
+ };
14
+ var DEFAULTS = {
15
+ enabled: false,
16
+ includeExtensions: [".tsx", ".jsx", ".html", ".vue"],
17
+ maxFindings: 50,
18
+ severity: "warn",
19
+ onWriteEdit: true
20
+ };
21
+ function readConfig(raw) {
22
+ if (!raw || typeof raw !== "object") return { ...DEFAULTS };
23
+ const r = raw;
24
+ return {
25
+ enabled: r["enabled"] !== false,
26
+ includeExtensions: Array.isArray(r["includeExtensions"]) ? r["includeExtensions"].filter((x) => typeof x === "string") : DEFAULTS.includeExtensions,
27
+ maxFindings: typeof r["maxFindings"] === "number" && r["maxFindings"] >= 1 && r["maxFindings"] <= 500 ? r["maxFindings"] : DEFAULTS.maxFindings,
28
+ severity: r["severity"] === "block" ? "block" : DEFAULTS.severity,
29
+ onWriteEdit: r["onWriteEdit"] !== false
30
+ };
31
+ }
32
+ function withinProject(p) {
33
+ if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
34
+ const root = process.cwd();
35
+ const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
36
+ const rel = relative(root, resolved);
37
+ if (rel === "" || rel === ".") return true;
38
+ if (rel.startsWith("..")) return false;
39
+ if (isAbsolute(rel)) return false;
40
+ return true;
41
+ }
42
+ function normalizeExtensions(exts) {
43
+ return exts.map((e) => e.startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`);
44
+ }
45
+ function matchesExtension(p, exts) {
46
+ const dot = p.lastIndexOf(".");
47
+ if (dot === -1) return false;
48
+ return exts.includes(p.slice(dot).toLowerCase());
49
+ }
50
+ function collectUiFiles(root, exts) {
51
+ const files = [];
52
+ if (!existsSync(root)) return files;
53
+ const s = statSync(root);
54
+ if (s.isFile()) {
55
+ if (matchesExtension(root, exts)) files.push(root);
56
+ return files;
57
+ }
58
+ if (!s.isDirectory()) return files;
59
+ function walk(dir) {
60
+ let entries;
61
+ try {
62
+ entries = readdirSync(dir);
63
+ } catch {
64
+ return;
65
+ }
66
+ for (const entry of entries) {
67
+ const full = resolve(dir, entry);
68
+ let st;
69
+ try {
70
+ st = statSync(full);
71
+ } catch {
72
+ continue;
73
+ }
74
+ if (st.isDirectory()) {
75
+ walk(full);
76
+ } else if (st.isFile() && matchesExtension(full, exts)) {
77
+ files.push(full);
78
+ }
79
+ }
80
+ }
81
+ walk(root);
82
+ return files;
83
+ }
84
+ var TAG_IMG = /<img\b[^>]*>/gi;
85
+ var TAG_INPUT = /<input\b[^>]*\/?>/gi;
86
+ var TAG_BUTTON = /<button\b[^>]*>([\s\S]*?)<\/button>/gi;
87
+ var INPUT_BUTTON = /<input\b[^>]*\btype\s*=\s*["'](submit|button|reset)["'][^>]*\/?>/gi;
88
+ var ATTR_ID = /\bid\s*=\s*["']([^"']+)["']/gi;
89
+ var ATTR_ALT = /\balt\s*=/i;
90
+ var ATTR_ARIA_LABEL = /\b(?:aria-label|aria-labelledby)\s*=/i;
91
+ var ATTR_TITLE = /\btitle\s*=/i;
92
+ var ATTR_PLACEHOLDER = /\bplaceholder\s*=/i;
93
+ var ATTR_VALUE = /\bvalue\s*=/i;
94
+ function hasMeaningfulAlt(tag) {
95
+ const m = ATTR_ALT.exec(tag);
96
+ ATTR_ALT.lastIndex = 0;
97
+ if (!m) return false;
98
+ const valMatch = tag.match(/\balt\s*=\s*["']?([^"'\s>]*)["']?/i);
99
+ return valMatch ? valMatch[1].trim().length > 0 : false;
100
+ }
101
+ function auditFile(filePath, projectRoot) {
102
+ let content;
103
+ try {
104
+ content = readFileSync(filePath, "utf-8");
105
+ } catch {
106
+ return [];
107
+ }
108
+ const findings = [];
109
+ const lines = content.split(/\r?\n/);
110
+ const idsByValue = /* @__PURE__ */ new Map();
111
+ function add(line, rule, severity, message) {
112
+ findings.push({
113
+ file: relative(projectRoot, filePath),
114
+ line,
115
+ rule,
116
+ severity,
117
+ message
118
+ });
119
+ }
120
+ for (let i = 0; i < lines.length; i++) {
121
+ const line = lines[i];
122
+ const lineNo = i + 1;
123
+ let idMatch;
124
+ while ((idMatch = ATTR_ID.exec(line)) !== null) {
125
+ const id = idMatch[1];
126
+ const list = idsByValue.get(id) ?? [];
127
+ list.push(lineNo);
128
+ idsByValue.set(id, list);
129
+ }
130
+ ATTR_ID.lastIndex = 0;
131
+ let imgMatch;
132
+ while ((imgMatch = TAG_IMG.exec(line)) !== null) {
133
+ const tag = imgMatch[0];
134
+ if (!hasMeaningfulAlt(tag)) {
135
+ add(lineNo, "missing-alt", "error", "<img> is missing meaningful alt text");
136
+ }
137
+ }
138
+ TAG_IMG.lastIndex = 0;
139
+ let inputMatch;
140
+ while ((inputMatch = TAG_INPUT.exec(line)) !== null) {
141
+ const tag = inputMatch[0];
142
+ const typeMatch = tag.match(/\btype\s*=\s*["']?([^"'\s>]*)["']?/i);
143
+ const type = typeMatch ? typeMatch[1].toLowerCase() : "text";
144
+ if (type === "hidden" || type === "button" || type === "submit" || type === "reset" || type === "image") {
145
+ continue;
146
+ }
147
+ const hasAriaLabel = ATTR_ARIA_LABEL.test(tag);
148
+ const hasTitle = ATTR_TITLE.test(tag);
149
+ const idMatchLocal = tag.match(/\bid\s*=\s*["']([^"']+)["']/i);
150
+ const id = idMatchLocal ? idMatchLocal[1] : null;
151
+ let hasLabelFor = false;
152
+ if (id) {
153
+ const labelForRe = new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`, "i");
154
+ hasLabelFor = labelForRe.test(content);
155
+ }
156
+ const wrappedInLabel = /<label\b[\s\S]*?<input\b[\s\S]*?<\/label>/i.test(content);
157
+ if (!hasAriaLabel && !hasTitle && !hasLabelFor && !wrappedInLabel) {
158
+ add(lineNo, "missing-input-label", "error", `<input type="${type}"> is missing an associated label`);
159
+ }
160
+ if (ATTR_PLACEHOLDER.test(tag)) {
161
+ add(lineNo, "low-contrast-placeholder", "warning", "<input> uses placeholder text (often low contrast and disappears on input)");
162
+ }
163
+ }
164
+ TAG_INPUT.lastIndex = 0;
165
+ let buttonMatch;
166
+ while ((buttonMatch = TAG_BUTTON.exec(line)) !== null) {
167
+ const tag = buttonMatch[0];
168
+ const inner = buttonMatch[1] ?? "";
169
+ const hasText = inner.replace(/\s+/g, "").length > 0;
170
+ const hasAriaLabel = ATTR_ARIA_LABEL.test(tag);
171
+ const hasTitle = ATTR_TITLE.test(tag);
172
+ if (!hasText && !hasAriaLabel && !hasTitle) {
173
+ add(lineNo, "missing-button-text", "error", "<button> has no visible text or accessible label");
174
+ }
175
+ }
176
+ TAG_BUTTON.lastIndex = 0;
177
+ let inputButtonMatch;
178
+ while ((inputButtonMatch = INPUT_BUTTON.exec(line)) !== null) {
179
+ const tag = inputButtonMatch[0];
180
+ if (!ATTR_VALUE.test(tag) && !ATTR_ARIA_LABEL.test(tag) && !ATTR_TITLE.test(tag)) {
181
+ add(lineNo, "missing-button-text", "error", `<input type="${inputButtonMatch[1]}"> is missing value/aria-label/title`);
182
+ }
183
+ }
184
+ INPUT_BUTTON.lastIndex = 0;
185
+ }
186
+ for (const [id, lineNos] of idsByValue.entries()) {
187
+ if (lineNos.length > 1) {
188
+ for (const lineNo of lineNos) {
189
+ add(lineNo, "duplicate-id", "error", `Duplicate id "${id}"`);
190
+ }
191
+ }
192
+ }
193
+ return findings;
194
+ }
195
+ function auditPath(rawPath, cfg) {
196
+ const root = process.cwd();
197
+ const resolved = isAbsolute(rawPath) ? resolve(rawPath) : resolve(root, rawPath);
198
+ const exts = normalizeExtensions(cfg.includeExtensions);
199
+ const files = collectUiFiles(resolved, exts);
200
+ const findings = [];
201
+ for (const file of files) {
202
+ const fileFindings = auditFile(file, root);
203
+ findings.push(...fileFindings);
204
+ if (findings.length >= cfg.maxFindings) break;
205
+ }
206
+ return {
207
+ path: relative(root, resolved),
208
+ findings: findings.slice(0, cfg.maxFindings),
209
+ fileCount: files.length
210
+ };
211
+ }
212
+ function formatSummary(result) {
213
+ if (result.findings.length === 0) {
214
+ return `
215
+ \u2705 accessibility-auditor: no issues found in ${result.path} (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}).`;
216
+ }
217
+ const lines = result.findings.map((f) => ` - ${f.file}:${f.line} \u2014 ${f.message} (${f.rule})`);
218
+ return `
219
+ \u26A0\uFE0F accessibility-auditor: ${result.findings.length} issue(s) in ${result.path} (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}):
220
+ ` + lines.join("\n") + "\nConsider adding missing labels/alt text or resolving duplicate ids.";
221
+ }
222
+ var plugin = {
223
+ name: "accessibility-auditor",
224
+ version: "0.1.0",
225
+ description: "Audits .tsx/.jsx/.html/.vue files for common accessibility issues and reports findings after writes/edits",
226
+ apiVersion: API_VERSION,
227
+ capabilities: { tools: true, hooks: true },
228
+ defaultConfig: { ...DEFAULTS },
229
+ configSchema: {
230
+ type: "object",
231
+ properties: {
232
+ enabled: {
233
+ type: "boolean",
234
+ default: true,
235
+ description: "Master switch."
236
+ },
237
+ includeExtensions: {
238
+ type: "array",
239
+ items: { type: "string" },
240
+ default: [".tsx", ".jsx", ".html", ".vue"],
241
+ description: "File extensions to audit."
242
+ },
243
+ maxFindings: {
244
+ type: "number",
245
+ minimum: 1,
246
+ maximum: 500,
247
+ default: 50,
248
+ description: "Maximum findings returned per audit."
249
+ },
250
+ severity: {
251
+ type: "string",
252
+ enum: ["warn", "block"],
253
+ default: "warn",
254
+ description: "warn = inject findings as additionalContext; block = refuse the mutating tool when issues appear."
255
+ },
256
+ onWriteEdit: {
257
+ type: "boolean",
258
+ default: true,
259
+ description: "Run audit after write|edit to UI files."
260
+ }
261
+ }
262
+ },
263
+ setup(api) {
264
+ state.auditCount = 0;
265
+ state.fileCount = 0;
266
+ state.findingCount = 0;
267
+ state.hookInvocationCount = 0;
268
+ state.lastResult = null;
269
+ state.hookUnregister = null;
270
+ const cfg = readConfig(api.config.extensions?.["accessibility-auditor"]);
271
+ const hook = (input) => {
272
+ if (!cfg.enabled || !cfg.onWriteEdit) return;
273
+ if (input.toolResult?.isError) return;
274
+ const inp = input.toolInput ?? {};
275
+ const sourcePath = inp["path"];
276
+ if (!sourcePath || typeof sourcePath !== "string") return;
277
+ if (!withinProject(sourcePath)) return;
278
+ const exts = normalizeExtensions(cfg.includeExtensions);
279
+ if (!matchesExtension(sourcePath, exts)) return;
280
+ state.hookInvocationCount += 1;
281
+ const result = auditPath(sourcePath, cfg);
282
+ state.auditCount += 1;
283
+ state.fileCount += result.fileCount;
284
+ state.findingCount += result.findings.length;
285
+ state.lastResult = {
286
+ path: result.path,
287
+ fileCount: result.fileCount,
288
+ findingCount: result.findings.length,
289
+ when: (/* @__PURE__ */ new Date()).toISOString()
290
+ };
291
+ if (result.findings.length === 0) return;
292
+ const summary = formatSummary(result);
293
+ if (cfg.severity === "block") {
294
+ return { decision: "block", reason: summary };
295
+ }
296
+ return { additionalContext: summary };
297
+ };
298
+ state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
299
+ api.tools.register({
300
+ name: "a11y_audit",
301
+ description: "Audit a file or directory for accessibility issues. Scans .tsx/.jsx/.html/.vue files for missing alt text, missing labels, low-contrast placeholders, missing button text, and duplicate ids.",
302
+ inputSchema: {
303
+ type: "object",
304
+ properties: {
305
+ path: {
306
+ type: "string",
307
+ description: "File or directory path to audit (relative to project root)."
308
+ }
309
+ },
310
+ required: ["path"]
311
+ },
312
+ permission: "auto",
313
+ category: "Diagnostics",
314
+ mutating: false,
315
+ async execute(input) {
316
+ if (!cfg.enabled) return { ok: false, error: "accessibility-auditor is disabled" };
317
+ const rawPath = input.path;
318
+ if (!rawPath || typeof rawPath !== "string") {
319
+ return { ok: false, error: "path is required" };
320
+ }
321
+ if (!withinProject(rawPath)) {
322
+ return { ok: false, error: "path must be inside the project" };
323
+ }
324
+ state.auditCount += 1;
325
+ const result = auditPath(rawPath, cfg);
326
+ state.fileCount += result.fileCount;
327
+ state.findingCount += result.findings.length;
328
+ state.lastResult = {
329
+ path: result.path,
330
+ fileCount: result.fileCount,
331
+ findingCount: result.findings.length,
332
+ when: (/* @__PURE__ */ new Date()).toISOString()
333
+ };
334
+ return {
335
+ ok: true,
336
+ path: result.path,
337
+ fileCount: result.fileCount,
338
+ findingCount: result.findings.length,
339
+ findings: result.findings
340
+ };
341
+ }
342
+ });
343
+ api.tools.register({
344
+ name: "a11y_status",
345
+ description: "Reports accessibility-auditor state: config, per-session counters, and the most recent scan result.",
346
+ inputSchema: { type: "object", properties: {} },
347
+ permission: "auto",
348
+ category: "Diagnostics",
349
+ mutating: false,
350
+ async execute() {
351
+ return {
352
+ ok: true,
353
+ enabled: cfg.enabled,
354
+ includeExtensions: cfg.includeExtensions,
355
+ maxFindings: cfg.maxFindings,
356
+ severity: cfg.severity,
357
+ onWriteEdit: cfg.onWriteEdit,
358
+ counters: {
359
+ audits: state.auditCount,
360
+ files: state.fileCount,
361
+ findings: state.findingCount,
362
+ hookInvocations: state.hookInvocationCount
363
+ },
364
+ lastResult: state.lastResult
365
+ };
366
+ }
367
+ });
368
+ api.log.info("accessibility-auditor plugin loaded", {
369
+ version: "0.1.0",
370
+ includeExtensions: cfg.includeExtensions,
371
+ severity: cfg.severity
372
+ });
373
+ },
374
+ teardown(api) {
375
+ if (state.hookUnregister) {
376
+ try {
377
+ state.hookUnregister();
378
+ } catch {
379
+ }
380
+ state.hookUnregister = null;
381
+ }
382
+ const final = {
383
+ audits: state.auditCount,
384
+ files: state.fileCount,
385
+ findings: state.findingCount,
386
+ hookInvocations: state.hookInvocationCount
387
+ };
388
+ state.auditCount = 0;
389
+ state.fileCount = 0;
390
+ state.findingCount = 0;
391
+ state.hookInvocationCount = 0;
392
+ state.lastResult = null;
393
+ api.log.info("accessibility-auditor: teardown complete", { final });
394
+ },
395
+ async health() {
396
+ return {
397
+ ok: true,
398
+ message: state.lastResult ? `accessibility-auditor: ${state.auditCount} audit(s), last scan ${state.lastResult.path} had ${state.lastResult.findingCount} finding(s)` : `accessibility-auditor: ${state.auditCount} audit(s), ${state.findingCount} finding(s)`,
399
+ counters: {
400
+ audits: state.auditCount,
401
+ files: state.fileCount,
402
+ findings: state.findingCount,
403
+ hookInvocations: state.hookInvocationCount
404
+ },
405
+ lastResult: state.lastResult
406
+ };
407
+ }
408
+ };
409
+ var accessibility_auditor_default = plugin;
410
+
411
+ export { accessibility_auditor_default as default };
@@ -0,0 +1,37 @@
1
+ import { Plugin } from '@wrongstack/core';
2
+
3
+ /**
4
+ * agent-handoff plugin — turns subagent completions into structured
5
+ * handoff notes for the next agent.
6
+ *
7
+ * When a subagent finishes (`subagent.done` event), this plugin extracts
8
+ * its identity, task, result, and any todo context, then posts a compact
9
+ * mailbox message addressed to the parent/leader agent so the next
10
+ * worker or the leader can pick up where it left off.
11
+ *
12
+ * Tools registered:
13
+ * - handoff_note : Manually compose and send a handoff note.
14
+ * - handoff_status : Counters + last handoff id.
15
+ *
16
+ * Events subscribed:
17
+ * - `subagent.done`
18
+ *
19
+ * Config (`config.extensions['agent-handoff']`):
20
+ *
21
+ * ```jsonc
22
+ * {
23
+ * "enabled": true,
24
+ * "subjectPrefix": "handoff: ",
25
+ * "includeResult": true,
26
+ * "includeTodos": true,
27
+ * "maxBodyChars": 4000,
28
+ * "to": "leader" // default recipient alias
29
+ * }
30
+ * ```
31
+ *
32
+ * @public
33
+ */
34
+
35
+ declare const plugin: Plugin;
36
+
37
+ export { plugin as default };