@wrongstack/plugins 0.277.1 → 0.280.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 (72) hide show
  1. package/README.md +838 -0
  2. package/dist/auto-doc.d.ts +8 -0
  3. package/dist/auto-doc.js +175 -13
  4. package/dist/auto-escalate.d.ts +45 -0
  5. package/dist/auto-escalate.js +190 -0
  6. package/dist/branch-guard.d.ts +33 -0
  7. package/dist/branch-guard.js +228 -0
  8. package/dist/changelog-writer.d.ts +73 -0
  9. package/dist/changelog-writer.js +369 -0
  10. package/dist/checkpoint.d.ts +55 -0
  11. package/dist/checkpoint.js +305 -0
  12. package/dist/commit-validator.d.ts +33 -0
  13. package/dist/commit-validator.js +315 -0
  14. package/dist/config-validator.d.ts +48 -0
  15. package/dist/config-validator.js +347 -0
  16. package/dist/context-pins.d.ts +45 -0
  17. package/dist/context-pins.js +240 -0
  18. package/dist/cost-tracker.d.ts +40 -1
  19. package/dist/cost-tracker.js +105 -4
  20. package/dist/dep-guard.d.ts +65 -0
  21. package/dist/dep-guard.js +316 -0
  22. package/dist/diff-summary.d.ts +36 -0
  23. package/dist/diff-summary.js +235 -0
  24. package/dist/error-lens.d.ts +67 -0
  25. package/dist/error-lens.js +280 -0
  26. package/dist/format-on-save.d.ts +35 -0
  27. package/dist/format-on-save.js +219 -0
  28. package/dist/git-autocommit.js +186 -26
  29. package/dist/import-organizer.d.ts +52 -0
  30. package/dist/import-organizer.js +274 -0
  31. package/dist/index.d.ts +32 -6
  32. package/dist/index.js +10151 -1628
  33. package/dist/injection-shield.d.ts +49 -0
  34. package/dist/injection-shield.js +205 -0
  35. package/dist/lint-gate.d.ts +33 -0
  36. package/dist/lint-gate.js +394 -0
  37. package/dist/llm-cache.d.ts +56 -0
  38. package/dist/llm-cache.js +251 -0
  39. package/dist/loop-breaker.d.ts +43 -0
  40. package/dist/loop-breaker.js +241 -0
  41. package/dist/model-router.d.ts +69 -0
  42. package/dist/model-router.js +198 -0
  43. package/dist/notify-hub.d.ts +45 -0
  44. package/dist/notify-hub.js +304 -0
  45. package/dist/path-guard.d.ts +54 -0
  46. package/dist/path-guard.js +235 -0
  47. package/dist/prompt-firewall.d.ts +57 -0
  48. package/dist/prompt-firewall.js +290 -0
  49. package/dist/secret-scanner.d.ts +34 -0
  50. package/dist/secret-scanner.js +409 -0
  51. package/dist/semver-bump.js +45 -0
  52. package/dist/session-recap.d.ts +50 -0
  53. package/dist/session-recap.js +421 -0
  54. package/dist/shell-check.js +52 -4
  55. package/dist/spec-linker.d.ts +51 -0
  56. package/dist/spec-linker.js +541 -0
  57. package/dist/template-engine.js +19 -1
  58. package/dist/test-runner-gate.d.ts +37 -0
  59. package/dist/test-runner-gate.js +356 -0
  60. package/dist/todo-listener.d.ts +37 -0
  61. package/dist/todo-listener.js +216 -0
  62. package/dist/todo-tracker.d.ts +5 -0
  63. package/dist/todo-tracker.js +441 -0
  64. package/dist/token-budget.d.ts +40 -0
  65. package/dist/token-budget.js +254 -0
  66. package/dist/token-throttle.d.ts +54 -0
  67. package/dist/token-throttle.js +203 -0
  68. package/package.json +116 -12
  69. package/dist/json-path.d.ts +0 -18
  70. package/dist/json-path.js +0 -15
  71. package/dist/web-search.d.ts +0 -19
  72. package/dist/web-search.js +0 -15
package/README.md ADDED
@@ -0,0 +1,838 @@
1
+ # @wrongstack/plugins
2
+
3
+ First-party plugin collection for [WrongStack](https://github.com/WrongStack/WrongStack).
4
+ Twenty-one focused, single-purpose plugins ship in this package and load
5
+ automatically for every `wstack` session.
6
+
7
+ ## What this is
8
+
9
+ Each plugin is a self-contained ESM module under `src/<name>/` that
10
+ exports a default `Plugin` object. The host's plugin loader
11
+ (`@wrongstack/core/plugin/loader`) accepts, validates, and
12
+ `setup()`s them. Plugins register **tools** on the host's
13
+ `ToolRegistry` and may also register **hooks** (e.g.
14
+ `secret-scanner` registers `PreToolUse` + `PostToolUse` hooks).
15
+
16
+ Plugins are loaded lazily by `packages/cli/src/wiring/plugins.ts`
17
+ under the `BUILTIN_PLUGIN_FACTORIES` array. To opt out, add
18
+ `{ name: '<plugin>', enabled: false }` to `config.plugins`.
19
+
20
+ ## Plugin catalog
21
+
22
+ | # | Plugin | Tools | Hooks | Notes |
23
+ |---|---|---|---|---|
24
+ | 1 | [`auto-doc`](./src/auto-doc) | `auto_doc` | — | JSDoc/TSDoc generation with `dry_run` preview |
25
+ | 2 | [`git-autocommit`](./src/git-autocommit) | `git_autocommit` | — | AI-written conventional commits; warns on simultaneous worktrees |
26
+ | 3 | [`shell-check`](./src/shell-check) | `shellcheck` | — | Runs `shellcheck` on files OR directories (recursive scan) |
27
+ | 4 | [`cost-tracker`](./src/cost-tracker) | `cost_summary`, `cost_reset`, `cost_export` | — | Per-model token + USD tracking; reads from `api.modelsRegistry` (models.dev) with a `pricingOverrides` config escape hatch |
28
+ | 5 | [`file-watcher`](./src/file-watcher) | `watch_start`, `watch_stop`, `watch_list` | — | Filesystem event hook (chokidar); feeds the `dep-watcher` bridge in the CLI |
29
+ | 6 | [`cron`](./src/cron) | `cron_schedule`, `cron_list`, `cron_cancel` | — | In-session recurring tasks; lifecycle via `beforeIteration` |
30
+ | 7 | [`template-engine`](./src/template-engine) | `template_expand`, `template_render`, `template_create`, `template_list` | — | Handlebars-style `{{var}}`, `{{#if}}`, `{{#each}}` |
31
+ | 8 | [`semver-bump`](./src/semver-bump) | `semver_bump`, `semver_current`, `semver_changelog` | — | Conventional-commit → semver version bump; can tag |
32
+ | 9 | [`secret-scanner`](./src/secret-scanner) | `secret_scanner_status`, `secret_scanner_test` | `PreToolUse` (`bash\|write\|edit`) + `PostToolUse` (`*`) | Blocks/redacts input secrets; warns on output leaks |
33
+ | 10 | [`todo-tracker`](./src/todo-tracker) | `todo_tracker_list/add/complete/drop/remove/pull/status` | — | Persistent project-scoped backlog that survives across sessions; cross-session bridge via `todo_tracker_pull` |
34
+ | 11 | [`token-budget`](./src/token-budget) | `token_budget_status` | `Stop` | Enforces a per-session token budget — warns at `warnPercent`, stops agent loop at `stopPercent` |
35
+ | 12 | [`lint-gate`](./src/lint-gate) | `lint_gate_status` | `PreToolUse` (`write\|edit`) | Runs biome/eslint on would-be file content before write or edit commits; blocks or warns on lint issues |
36
+ | 13 | [`branch-guard`](./src/branch-guard) | `branch_guard_status` | `PreToolUse` (`bash\|git_autocommit`) | Blocks commits, pushes, and merges to protected branches (default: main, master) |
37
+ | 14 | [`diff-summary`](./src/diff-summary) | `diff_summary_status` | `PostToolUse` (`write\|edit`) | Injects compact git diff into LLM context after every write or edit |
38
+ | 15 | [`commit-validator`](./src/commit-validator) | `commit_validator_status` | `PreToolUse` (`bash\|git_autocommit`) | Validates conventional-commit format before git_autocommit or bash git commit runs |
39
+ | 16 | [`format-on-save`](./src/format-on-save) | `format_on_save_status` | `PostToolUse` (`write\|edit`) | Runs `biome format --write` on the file after every write or edit |
40
+ | 17 | [`test-runner-gate`](./src/test-runner-gate) | `test_gate_status` | `PostToolUse` (`write\|edit`) | Runs the relevant test file after every write or edit to a source file |
41
+ | 18 | [`import-organizer`](./src/import-organizer) | `import_organizer_status` | `PostToolUse` (`write\|edit`) | Runs `biome check --write --unsafe` (or `eslint --fix`) on the file after write or edit, re-sorting imports and applying safe fixes |
42
+ | 19 | [`todo-listener`](./src/todo-listener) | `todo_listener_status` | `PostToolUse` (`todo`) | Broadcasts a status update to the project mailbox whenever the `todo` tool is called, so other agents can see what this one is working on |
43
+ | 20 | [`session-recap`](./src/session-recap) | `session_recap_status` | `Stop` | Posts a one-page session summary (tokens, tool calls, commits, last activity) to the project mailbox when the agent loop ends |
44
+ | 21 | [`spec-linker`](./src/spec-linker) | `spec_linker_status` | `PostToolUse` (`write\|edit`) | Scans markdown files for unlinked plugin references and surfaces them to the LLM via additionalContext |
45
+
46
+ ### Removed plugins (use built-in tools instead)
47
+
48
+ | Removed | Replacement | Why |
49
+ |---|---|---|
50
+ | `web-search` (removed in `e03e39d1`) | Built-in `search` + `fetch` tools in `@wrongstack/tools` | The built-in tools have native caching, dedup, ranking, DNS-pinned SSRF protection, TurndownService markdown, binary-content rejection, and structured errors. |
51
+ | `json-path` (removed in `e03e39d1`) | Built-in `json` tool in `@wrongstack/tools` (action: `query` \| `validate` \| `transform` \| `merge`) | The built-in `json` tool already supports JMESPath queries, schema validation, transforms, and deep-merge via a single `action` parameter. |
52
+
53
+ If a user lists either name in `config.plugins`, the loader emits a
54
+ one-shot `log.warn` and skips loading. See
55
+ [`DEPRECATED_PLUGIN_NAMES`](../../cli/src/wiring/plugins.ts)
56
+ in `packages/cli/src/wiring/plugins.ts` for the canonical list and
57
+ migration hints.
58
+
59
+ ## Per-plugin quick reference
60
+
61
+ ### 1. `auto-doc` — JSDoc/TSDoc generation
62
+
63
+ **Tools**: `auto_doc` (mutating)
64
+
65
+ Generates JSDoc/TSDoc comments and either writes them to the file or
66
+ returns a preview. Pass `dry_run: true` to see what would change
67
+ without writing — the same tool, no separate preview tool.
68
+
69
+ ```jsonc
70
+ // Generate doc comments for every export in src/agent.ts, preview only
71
+ auto_doc({ files: ["src/agent.ts"], style: "tsdoc", dry_run: true })
72
+ ```
73
+
74
+ ### 2. `git-autocommit` — AI commit messages
75
+
76
+ **Tools**: `git_autocommit` (mutating, `confirm` permission)
77
+
78
+ Stages the listed files (or all changed files when `files: []`) and
79
+ creates a commit with a conventional-commit message derived from the
80
+ diff. Warns when other worktrees are active (likely parallel agents
81
+ editing the same repo) so the user can verify the diff before commit.
82
+
83
+ ```jsonc
84
+ git_autocommit({ type: "fix", scope: "session", message: "..." })
85
+ ```
86
+
87
+ ### 3. `shell-check` — bash script linting
88
+
89
+ **Tools**: `shellcheck` (mutating — writes the CSV report)
90
+
91
+ Two modes: pass `files: ['scripts/deploy.sh']` to lint specific files
92
+ or `directory: 'scripts', pattern: '*.sh'` to recursively scan.
93
+
94
+ ### 4. `cost-tracker` — token + USD tracking
95
+
96
+ **Tools**: `cost_summary`, `cost_reset`, `cost_export`
97
+
98
+ Listens to the `provider.response` event, computes cost from the
99
+ models.dev-backed `api.modelsRegistry` (Layer 2 of the lookup chain),
100
+ with a bundled `PRICING` table as the baseline (Layer 3) and a
101
+ `pricingOverrides` config field as the top-priority escape hatch
102
+ (Layer 1). The `pricingOverrides` field is the user-facing tool for
103
+ correcting a specific model's price without waiting for a plugin
104
+ release. See [cost-tracker source](./src/cost-tracker/index.ts) for
105
+ the full lookup chain.
106
+
107
+ ```jsonc
108
+ // Per-model override (USD per 1M tokens, lowercased model id)
109
+ {
110
+ "extensions": {
111
+ "cost-tracker": {
112
+ "pricingOverrides": {
113
+ "gpt-4o": { "input": 7, output: 21 },
114
+ "claude-3-5-sonnet": { "input": 4, output: 20 }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### 5. `file-watcher` — filesystem events
122
+
123
+ **Tools**: `watch_start`, `watch_stop`, `watch_list`
124
+
125
+ Wires `node:fs.watch` listeners and stores the handles in module
126
+ scope. The CLI hooks these events to the per-project mailbox via
127
+ the `dep-watcher` bridge so dependency-manifest changes
128
+ (`package.json`, `go.mod`, etc.) trigger tech-stack audits.
129
+
130
+ ### 6. `cron` — in-session recurring tasks
131
+
132
+ **Tools**: `cron_schedule`, `cron_list`, `cron_cancel`
133
+
134
+ Schedules timers with `api.extensions.register('beforeIteration', ...)`.
135
+ All timers are tracked in module-scope state and torn down on plugin
136
+ unload so a hot-reload cycle doesn't leak `setTimeout` handles
137
+ (audited 2026-06-03, see "H1 audit pattern" below).
138
+
139
+ ### 7. `template-engine` — file templates
140
+
141
+ **Tools**: `template_expand`, `template_render`, `template_create`, `template_list`
142
+
143
+ Three template forms: `{{var}}` substitution, `{{#if var}}…{{/if}}`
144
+ conditionals, `{{#each items}}…{{/each}}` loops. The store is in-memory
145
+ and module-scoped (audited 2026-06-03).
146
+
147
+ ### 8. `semver-bump` — conventional commits → version
148
+
149
+ **Tools**: `semver_bump`, `semver_current`, `semver_changelog`
150
+
151
+ Reads the git log since the last tag, infers the next version
152
+ (major/minor/patch) from the conventional-commit types, and can
153
+ tag the new commit. `changelog` generates a markdown changelog
154
+ between two refs.
155
+
156
+ ### 9. `secret-scanner` — credential blocker + output leak detector
157
+
158
+ **Tools**: `secret_scanner_status`, `secret_scanner_test`
159
+ **Hooks**:
160
+ - `PreToolUse` with matcher `bash|write|edit` (configurable via `matcher`)
161
+ - `PostToolUse` with matcher `*` (configurable via `postToolUseMatcher`)
162
+
163
+ **PreToolUse** (prevention — before the tool runs):
164
+ Mirrors 21 simple patterns from `core/src/security/secret-scrubber.ts`
165
+ (LLM provider keys, GitHub PATs v1+v2, AWS, GCP, Slack, Stripe,
166
+ Twilio, Telegram, JWT, PEM private keys, HuggingFace/Replicate/
167
+ Perplexity/Groq, Bearer tokens, mongo/postgres/mysql/redis URIs).
168
+ Read-only tools (`read`, `fetch`) are excluded from PreToolUse by
169
+ default since secrets flowing IN to them are fine.
170
+
171
+ **PostToolUse** (detection — after the tool runs):
172
+ Scans tool OUTPUT for secrets that leaked through. Since the tool
173
+ has already run, the hook cannot block — instead it injects
174
+ `additionalContext` so the LLM knows not to echo, store, or commit
175
+ the leaked value.
176
+
177
+ Three modes (`config.extensions['secret-scanner'].mode`):
178
+ - **`block` (default)**: returns `HookOutcome{ decision: 'block', reason }`
179
+ - **`redact`**: returns `HookOutcome{ decision: 'allow', modifiedInput, additionalContext }` with the offending strings replaced by `[REDACTED:type]`
180
+ - **`allow`**: only logs; never blocks
181
+
182
+ ```jsonc
183
+ // Basic config
184
+ {
185
+ "extensions": {
186
+ "secret-scanner": {
187
+ "mode": "block",
188
+ "matcher": "bash|write|edit",
189
+ "postToolUseMatcher": "*"
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ **Custom patterns** (`customPatterns`): Append your own credential
196
+ patterns alongside the 21 built-in ones. Each entry is a
197
+ `{ type, regex, description? }`. Invalid regex entries are silently
198
+ skipped.
199
+
200
+ ```jsonc
201
+ {
202
+ "extensions": {
203
+ "secret-scanner": {
204
+ "customPatterns": [
205
+ {
206
+ "type": "internal_api_key",
207
+ "regex": "IAK-[A-F0-9]{40}",
208
+ "description": "Internal API key format"
209
+ },
210
+ {
211
+ "type": "custom_jwt",
212
+ "regex": "eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"
213
+ },
214
+ {
215
+ "type": "vault_token",
216
+ "regex": "hvs\\.[A-Za-z0-9_-]{90,}"
217
+ }
218
+ ]
219
+ }
220
+ }
221
+ }
222
+ ```
223
+
224
+ Custom patterns are detected by all hooks (PreToolUse block/redact,
225
+ PostToolUse leak detection) and by the `secret_scanner_test` tool.
226
+ They are reset to base-only on teardown (H1 pattern).
227
+
228
+ The `high_entropy_env` pattern from the output scrubber is
229
+ intentionally omitted — too slow and too false-positive prone
230
+ for a synchronous pre-tool gate.
231
+
232
+ ### 10. `todo-tracker` — persistent backlog
233
+
234
+ **Tools**: `todo_tracker_list`, `todo_tracker_add`, `todo_tracker_complete`, `todo_tracker_drop`, `todo_tracker_remove`, `todo_tracker_pull`, `todo_tracker_status`
235
+
236
+ Closes a gap that no existing tool fills: a **per-project backlog**
237
+ that survives across sessions. The built-in `todo` tool mutates
238
+ `ctx.todos` (session-scoped, auto-clears when all items complete);
239
+ `PlanFile` and `TaskFile` are also session-scoped. This plugin
240
+ writes a per-project JSON file with atomic write (temp + rename).
241
+
242
+ **Cross-session bridge**: `todo_tracker_pull` returns active items
243
+ for the LLM to re-register with the built-in `todo` tool (which
244
+ mutates `ctx.todos`). The plugin never touches `ctx.todos` directly
245
+ — that separation respects the existing session/tool boundary.
246
+
247
+ **Storage**: per-project JSON at the path provided by
248
+ `paths.projectDir` (via the host's wiring) or via the explicit
249
+ `config.extensions["todo-tracker"].filePath` config field.
250
+
251
+ ```jsonc
252
+ // Explicit override (use when the host doesn't supply paths.projectDir)
253
+ {
254
+ "extensions": {
255
+ "todo-tracker": {
256
+ "filePath": "/abs/path/to/todo-tracker.json"
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ ### 11. `token-budget` — per-session token enforcement
263
+
264
+ **Tools**: `token_budget_status`
265
+ **Hooks**: `Stop` + `PostToolUse` (matcher `*`)
266
+
267
+ Complements `cost-tracker` (which tracks cost in USD) by enforcing a
268
+ **hard token budget**. When usage crosses `warnPercent`, a one-shot
269
+ `PostToolUse` injection tells the LLM to start wrapping up. When it
270
+ crosses `stopPercent`, the `Stop` hook blocks the agent loop.
271
+
272
+ ```jsonc
273
+ {
274
+ "extensions": {
275
+ "token-budget": {
276
+ "limit": 500000, // hard token limit (prompt + completion)
277
+ "warnPercent": 80, // inject "wrap up" at this %
278
+ "stopPercent": 100, // trigger Stop at this %
279
+ "model": "" // "" = all models; or restrict to one
280
+ }
281
+ }
282
+ }
283
+ ```
284
+
285
+ `limit: 0` (default) = tracking only (no enforcement). The
286
+ `token_budget_status` tool reports the exact consumed/remaining
287
+ breakdown.
288
+
289
+ ### 12. `lint-gate` — pre-write lint enforcement
290
+
291
+ **Tools**: `lint_gate_status`
292
+ **Hooks**: `PreToolUse` (matcher `write|edit`)
293
+
294
+ Runs biome (or eslint) on the would-be file content **before** the
295
+ write or edit commits it. For `write`, the full content is linted
296
+ via a temp file. For `edit`, the current file is read, the
297
+ `old_string → new_string` replacement is applied in-memory, and the
298
+ result is linted.
299
+
300
+ ```jsonc
301
+ {
302
+ "extensions": {
303
+ "lint-gate": {
304
+ "linter": "auto", // "biome" | "eslint" | "auto"
305
+ "mode": "warn", // "block" | "warn" | "fix"
306
+ "severity": "error", // "error" | "warning"
307
+ "timeoutMs": 10000, // linter process timeout
308
+ "fixRules": [] // when mode=fix, limit auto-fix to these rules only
309
+ }
310
+ }
311
+ }
312
+ ```
313
+
314
+ **Modes**:
315
+ - **`block`**: refuses the write/edit; LLM must fix lint errors first
316
+ - **`warn`** (default): injects lint errors as context; write proceeds
317
+ - **`fix`**: auto-runs `biome check --write` / `eslint --fix`, substitutes
318
+ the fixed content via `modifiedInput` (`write` only; `edit` falls back
319
+ to `warn`). Use `fixRules` to limit which rules are auto-fixed:
320
+
321
+ ```jsonc
322
+ // Only auto-fix formatting and import types; leave noExplicitAny as warning
323
+ {
324
+ "extensions": {
325
+ "lint-gate": {
326
+ "mode": "fix",
327
+ "fixRules": ["format", "lint/style/useImportType"]
328
+ }
329
+ }
330
+ }
331
+ ```
332
+
333
+ ### 13. `branch-guard` — protected branch enforcement
334
+
335
+ **Tools**: `branch_guard_status`
336
+ **Hooks**: `PreToolUse` (matcher `bash|git_autocommit`)
337
+
338
+ Blocks `git commit`, `git push`, and `git merge` on protected branches
339
+ (default: `main`, `master`). Checks the current branch via
340
+ `git branch --show-current`. When the working tree is dirty, the
341
+ block reason includes a safe stash workflow:
342
+
343
+ ```
344
+ git stash → git checkout -b feat/my-change → git stash pop → git commit ...
345
+ ```
346
+
347
+ ```jsonc
348
+ {
349
+ "extensions": {
350
+ "branch-guard": {
351
+ "branches": ["main", "master", "release/*"],
352
+ "mode": "block", // "block" | "warn"
353
+ "blockCommit": true,
354
+ "blockPush": true,
355
+ "blockMerge": true
356
+ }
357
+ }
358
+ }
359
+ ```
360
+
361
+ Each operation type can be individually toggled. `git_autocommit`
362
+ tool calls are treated as commits.
363
+
364
+ ### 14. `diff-summary` — post-write/edit diff injection
365
+
366
+ **Tools**: `diff_summary_status`
367
+ **Hooks**: `PostToolUse` (matcher `write|edit`)
368
+
369
+ After every `write` or `edit` completes, runs `git diff -- <path>`
370
+ and injects a capped unified diff into the LLM's context as
371
+ `additionalContext`. Gives the LLM immediate visibility into what its
372
+ change actually did to the file — confirming the edit applied correctly
373
+ and showing surrounding context.
374
+
375
+ ```jsonc
376
+ {
377
+ "extensions": {
378
+ "diff-summary": {
379
+ "maxLines": 50, // cap diff context at N lines
380
+ "showStat": true, // include "+N -M" summary line
381
+ "mode": "diff" // "diff" | "stat" | "off"
382
+ }
383
+ }
384
+ }
385
+ ```
386
+
387
+ **Modes**:
388
+ - **`diff`** (default): injects unified diff body (capped at `maxLines`) + `+N -M` header
389
+ - **`stat`**: injects only `+N -M` counts (no diff body)
390
+ - **`off`**: disabled entirely
391
+
392
+ For untracked/new files: uses `git diff --no-index /dev/null <path>`.
393
+ For non-git repos: silent fallback (no injection). Skips on tool errors.
394
+
395
+ ### 15. `commit-validator` — conventional-commit enforcement
396
+
397
+ **Tools**: `commit_validator_status`
398
+ **Hooks**: `PreToolUse` (matcher `bash|git_autocommit`)
399
+
400
+ Parses the commit message from `git commit -m "..."` (bash) or the
401
+ `message` field (git_autocommit) and validates it against the
402
+ conventional-commit format:
403
+
404
+ ```
405
+ <type>[(scope)][!]: <description>
406
+ ```
407
+
408
+ **Checks**:
409
+ - Valid format (type + colon + subject)
410
+ - Type is in `allowedTypes` (if configured; empty = allow all standard + custom)
411
+ - Scope is present (if `requireScope: true`)
412
+ - Subject ≤ `maxSubjectLength` (default: 72)
413
+ - Subject does not end with a period
414
+
415
+ ```jsonc
416
+ {
417
+ "extensions": {
418
+ "commit-validator": {
419
+ "mode": "block", // "block" | "warn"
420
+ "requireScope": false, // require (scope) in message
421
+ "allowedTypes": [], // empty = all; or ["feat", "fix", "docs"]
422
+ "maxSubjectLength": 72
423
+ }
424
+ }
425
+ }
426
+ ```
427
+
428
+ Standard types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`,
429
+ `test`, `build`, `ci`, `chore`, `revert`.
430
+
431
+ ### 16. `format-on-save` — automatic biome formatting
432
+
433
+ **Tools**: `format_on_save_status`
434
+ **Hooks**: `PostToolUse` (matcher `write|edit`)
435
+
436
+ After every `write` or `edit` completes, runs
437
+ `biome format --write <path>` on the file on disk. Silently reformats —
438
+ no blocking, no warnings, just clean code. If the file changed
439
+ (formatting was applied), injects `additionalContext` so the LLM knows
440
+ the file was reformatted.
441
+
442
+ ```jsonc
443
+ {
444
+ "extensions": {
445
+ "format-on-save": {
446
+ "enabled": true, // master switch
447
+ "timeoutMs": 5000 // biome process timeout
448
+ }
449
+ }
450
+ }
451
+ ```
452
+
453
+ Biome detection runs once at setup(). If biome is not installed, the
454
+ hook is a silent no-op. Works alongside lint-gate (which lints BEFORE
455
+ the write) and diff-summary (which shows the diff AFTER) — together
456
+ they form a complete write pipeline: lint → write → format → diff.
457
+
458
+ ### 17. `test-runner-gate` — automatic test execution
459
+
460
+ **Tools**: `test_gate_status`
461
+ **Hooks**: `PostToolUse` (matcher `write|edit`)
462
+
463
+ After every `write` or `edit` to a source file, maps the source path
464
+ to its test file (using configurable patterns), runs
465
+ `vitest run <test-file> --reporter=json`, and injects the result.
466
+ On failure, injects failure details (test name + error, up to 5) so
467
+ the LLM knows immediately what broke.
468
+
469
+ ```jsonc
470
+ {
471
+ "extensions": {
472
+ "test-runner-gate": {
473
+ "enabled": true,
474
+ "command": "npx vitest run",
475
+ "timeoutMs": 30000,
476
+ "testFilePatterns": [
477
+ "tests/{name}.test.ts",
478
+ "src/{name}.test.ts",
479
+ "tests/{name}-exec.test.ts"
480
+ ],
481
+ "injectOnPass": false
482
+ }
483
+ }
484
+ }
485
+ ```
486
+
487
+ Patterns use `{name}` (basename), `{path}` (path-no-ext), `{dir}` (dirname).
488
+ Skips test files themselves, tool errors, and missing test files (silent).
489
+
490
+ ### 18 — `import-organizer`
491
+
492
+ **Tools**: `import_organizer_status`
493
+ **Hooks**: `PostToolUse` (matcher `write|edit`)
494
+
495
+ After every `write` or `edit`, runs `biome check --write --unsafe`
496
+ (or `eslint --fix` if biome is not installed) on the just-saved file.
497
+ The `--unsafe` flag enables auto-organizing imports: re-sorts them
498
+ alphabetically within import groups, merges duplicate imports from
499
+ the same module, and removes unused ones. Other safe fixes (formatting,
500
+ style) are applied alongside.
501
+
502
+ The hook runs **after** the tool completes (PostToolUse) and reads the
503
+ file from disk — so `write` tool's `content` parameter and `edit` tool's
504
+ post-edit disk state are both handled correctly. If the file no longer
505
+ exists at hook time (e.g. immediately deleted), the hook silently skips.
506
+
507
+ The hook is **idempotent**: running biome twice on the same file produces
508
+ the same result. Lint issues that biome cannot auto-fix are reported in
509
+ `additionalContext` so the LLM knows to clean them up manually.
510
+
511
+ ```jsonc
512
+ {
513
+ "extensions": {
514
+ "import-organizer": {
515
+ "enabled": true,
516
+ "command": "npx @biomejs/biome check --write --unsafe",
517
+ "fallbackCommand": "npx eslint --fix",
518
+ "timeoutMs": 10000
519
+ }
520
+ }
521
+ }
522
+ ```
523
+
524
+ Use the `--unsafe` flag with care: it enables rules that can change
525
+ runtime behavior (e.g. `useImportType` may force `import type` for
526
+ type-only imports, which is a no-op at runtime but can break tooling
527
+ that introspects import statements). If you prefer the safe-only mode,
528
+ omit `--unsafe`.
529
+
530
+ ### 19 — `todo-listener`
531
+
532
+ **Tools**: `todo_listener_status`
533
+ **Hooks**: `PostToolUse` (matcher `todo`)
534
+
535
+ When the built-in `todo` tool is called, broadcasts a structured
536
+ status update to the project mailbox so other agents (terminals,
537
+ WebUIs, shadow agents) can see what this agent is working on in
538
+ real time.
539
+
540
+ ```jsonc
541
+ {
542
+ "extensions": {
543
+ "todo-listener": {
544
+ "enabled": true,
545
+ "subjectPrefix": "todo: ",
546
+ "broadcastOnChange": true,
547
+ "cooldownMs": 5000
548
+ }
549
+ }
550
+ }
551
+ ```
552
+
553
+ **Payload shape** (mailbox body, JSON):
554
+ ```json
555
+ {
556
+ "count": 4,
557
+ "inProgress": { "id": "auth-flow", "content": "implement OAuth callback" },
558
+ "pending": 2,
559
+ "completed": 1,
560
+ "items": [
561
+ { "id": "auth-flow", "status": "in_progress", "content": "..." }
562
+ ]
563
+ }
564
+ ```
565
+
566
+ **Dedup + rate limiting**:
567
+ - `broadcastOnChange: true` (default) — identical consecutive payloads
568
+ (FNV-1a hash over `id|status|content`) are suppressed. Re-issuing
569
+ the same list doesn't spam the inbox.
570
+ - `cooldownMs: 5000` (default) — minimum interval between two
571
+ consecutive broadcasts. Prevents the agent loop from flooding the
572
+ mailbox on every iteration.
573
+
574
+ **Host requirements**:
575
+ - Requires `api.mailbox` (added in this commit) on the host's
576
+ `PluginAPI`. Minimal hosts (tests, the LSP server, the standalone
577
+ TUI without a coordinator) don't construct a mailbox — the hook
578
+ logs a one-shot warning and silently no-ops.
579
+ - Subject is truncated to 200 chars to keep the inbox readable.
580
+
581
+ ### 20 — `session-recap`
582
+
583
+ **Tools**: `session_recap_status`
584
+ **Hooks**: `Stop`
585
+
586
+ When the agent loop ends, the hook posts a one-page session summary
587
+ to the project mailbox. Other agents (terminals, WebUIs, shadow
588
+ agents) can read the recap stream to see what the previous session
589
+ finished — useful for end-of-day handoff and audit.
590
+
591
+ The plugin accumulates lightweight metrics from the EventBus during
592
+ the session:
593
+
594
+ - `provider.response` events → tokens per model
595
+ - `tool.*` events → tool-call counts (top-5 reported)
596
+ - `tool.result` events → commit count (via `git_autocommit` success)
597
+ - First/last activity timestamp → wall-clock duration
598
+
599
+ ```jsonc
600
+ {
601
+ "extensions": {
602
+ "session-recap": {
603
+ "enabled": true,
604
+ "subjectPrefix": "session recap: ",
605
+ "includeTranscriptTail": 3,
606
+ "maxBodyChars": 8000
607
+ }
608
+ }
609
+ }
610
+ ```
611
+
612
+ **Recap payload** (mailbox body, JSON):
613
+ ```json
614
+ {
615
+ "session": {
616
+ "id": "sess-42",
617
+ "cwd": "/home/user/proj",
618
+ "startedAt": "2026-06-30T10:00:00Z",
619
+ "endedAt": "2026-06-30T10:32:18Z",
620
+ "duration": "32m18s"
621
+ },
622
+ "tokens": {
623
+ "total": { "input": 12345, "output": 6789 },
624
+ "perModel": [
625
+ { "model": "gpt-4o", "input": 8000, "output": 5000, "invocations": 12 }
626
+ ]
627
+ },
628
+ "tools": {
629
+ "totalCalls": 47,
630
+ "uniqueTools": 8,
631
+ "top": [["read", 18], ["bash", 12], ["edit", 9]]
632
+ },
633
+ "commits": 2,
634
+ "transcriptTail": [
635
+ { "type": "user", "ts": "...", "preview": "last user prompt..." }
636
+ ]
637
+ }
638
+ ```
639
+
640
+ **Transcript tail**: by default the recap includes the last 3 events
641
+ from `api.session.transcriptPath` (the JSONL session log) for
642
+ context. Increase `includeTranscriptTail` for more history.
643
+
644
+ **Host requirements**:
645
+ - `api.mailbox` — same as `todo-listener`. Without it, the hook
646
+ logs a one-shot warn and no-ops.
647
+ - `api.session.transcriptPath` — when missing the transcript tail
648
+ is empty but the metrics summary still publishes.
649
+
650
+ ### 21 — `spec-linker`
651
+
652
+ **Tools**: `spec_linker_status`
653
+ **Hooks**: `PostToolUse` (`write|edit`) + `PreToolUse` (`write`, when `autoFix: true`)
654
+
655
+ Two hooks working together:
656
+
657
+ **PostToolUse** (always active when the plugin is enabled):
658
+ scans markdown files for *unlinked* references to one of the 21
659
+ known plugins and surfaces them to the LLM via
660
+ `additionalContext`. The plugin does NOT modify the file — it
661
+ only injects a low-noise context block listing the unlinked
662
+ references and their canonical paths so the LLM can fix the file
663
+ in a follow-up edit.
664
+
665
+ **PreToolUse** (only when `autoFix: true`): scans the would-be
666
+ content of a `write` call and returns a `modifiedInput.content`
667
+ where each unlinked plugin reference is wrapped in a markdown
668
+ link. The tool executor then writes the fixed content instead of
669
+ the original. The fix preserves the original casing
670
+ (`Secret-Scanner` becomes `[Secret-Scanner](./src/secret-scanner)`)
671
+ and leaves markdown-link / inline-code references untouched.
672
+
673
+ **Why `write` only and not `edit`**: the `edit` tool's input is
674
+ `{ path, old_string, new_string }` — `new_string` is a small
675
+ patch, not the whole file. Auto-fixing `edit` cleanly would
676
+ require re-deriving the new `old_string` after substitution
677
+ (a hard string-diff problem), so `edit` stays read-only and
678
+ the PostToolUse context tells the LLM what to fix.
679
+
680
+ **Detection rules** (both hooks):
681
+ - Source matches `config.fileGlobs` (default: `**/*.md`, `**/*.mdx`)
682
+ - The reference matches one of the 21 known plugin names
683
+ (case-insensitive)
684
+ - It is NOT already wrapped in a markdown link `[name](...)` or
685
+ inline code `` `name` ``
686
+ - It is NOT a hyphenated/dotted continuation
687
+ (`secret-scanner-config.json` does not match `secret-scanner`)
688
+
689
+ ```jsonc
690
+ {
691
+ "extensions": {
692
+ "spec-linker": {
693
+ "enabled": true,
694
+ "fileGlobs": ["**/*.md", "**/*.mdx"],
695
+ "maxReferences": 8,
696
+ "autoFix": false // set true to enable PreToolUse auto-link
697
+ }
698
+ }
699
+ }
700
+ ```
701
+
702
+ **Injected context** (sample, when 2 unlinked references are found):
703
+ ```
704
+ 🔗 spec-linker: 2 unlinked plugin reference(s) in 'docs/feature-matrix.md'.
705
+ Consider wrapping them in markdown links to keep the docs navigable:
706
+ - `secret-scanner` → `[secret-scanner](./src/secret-scanner)`
707
+ - `token-budget` → `[token-budget](./src/token-budget)`
708
+ ```
709
+
710
+ **Why read-only by default**: the file might be referenced
711
+ elsewhere or under review. Surfacing a suggestion lets the
712
+ LLM (or the user) decide whether to fix it, instead of
713
+ silently rewriting the file on every save. Opt into `autoFix`
714
+ once you're confident in the rewrite.
715
+
716
+ ## Configuration patterns
717
+
718
+ There are two surfaces for plugin configuration:
719
+
720
+ 1. **Loading** — `config.plugins` controls which plugins load.
721
+ ```jsonc
722
+ {
723
+ "plugins": [
724
+ { "name": "auto-doc", "enabled": true },
725
+ { "name": "git-autocommit", "options": { "conventionalCommits": true } }
726
+ ]
727
+ }
728
+ ```
729
+ 2. **Options** — `config.extensions["<plugin-name>"]` stores each
730
+ plugin's runtime options. The plugin's `configSchema` validates
731
+ this section before `setup()` runs.
732
+
733
+ ```jsonc
734
+ {
735
+ "plugins": {
736
+ "auto-doc": { "enabled": true },
737
+ "git-autocommit": { "conventionalCommits": true },
738
+ "secret-scanner": { "mode": "block", "matcher": "bash|write|edit" }
739
+ }
740
+ }
741
+ ```
742
+
743
+ To disable a single built-in without removing its config:
744
+ ```jsonc
745
+ { "plugins": [{ "name": "secret-scanner", "enabled": false }] }
746
+ ```
747
+
748
+ ## H1 audit pattern
749
+
750
+ Plugins that hold module-scope state (`cron`, `file-watcher`,
751
+ `template-engine`, `git-autocommit`, `cost-tracker`, `secret-scanner`,
752
+ `todo-tracker`, `auto-doc`, `shell-check`, `semver-bump`,
753
+ `token-budget`, `lint-gate`, `branch-guard`, `diff-summary`, `commit-validator`, `format-on-save`, `test-runner-gate`, `import-organizer`, `todo-listener`, `session-recap`, `spec-linker`) follow a strict lifecycle to survive hot-reload
754
+ without leaking resources. The pattern was formalized after a
755
+ 2026-06-03 audit (the "H1 audit") found that several plugins kept
756
+ their state inside the `setup()` closure, where the loader's
757
+ `WeakMap<Plugin, PluginAPI>` could not reach it during teardown —
758
+ meaning timers, filesystem handles, and in-memory caches leaked
759
+ across plugin reloads.
760
+
761
+ The H1 pattern:
762
+
763
+ 1. **State at module scope, not in setup() closure.** Anything the
764
+ `teardown` needs to clean up lives in a `const state = {…}` block
765
+ next to the `Plugin` object.
766
+ 2. **`setup()` is idempotent.** It clears the state first, then
767
+ re-initializes from config and the host's API. Calling `setup()`
768
+ twice (e.g. across a hot-reload) leaves a clean slate.
769
+ 3. **`teardown()` releases every resource.** Timers are `clearTimeout`'d,
770
+ chokidar watchers are `close()`'d, caches are cleared. The
771
+ unregister handle returned by `api.registerHook` is called.
772
+ 4. **`teardown` does not delete on-disk state.** File-based plugins
773
+ (e.g. `todo-tracker`) leave the file in place — the user may
774
+ return in a moment to read it.
775
+ 5. **`health()` reports per-session counters** for `/diag plugins`
776
+ visibility.
777
+
778
+ Plugins that follow this pattern expose the same teardown contract
779
+ to the host, so the loader can clean up uniformly.
780
+
781
+ ## For plugin authors
782
+
783
+ The minimum viable plugin:
784
+
785
+ ```typescript
786
+ import type { Plugin } from '@wrongstack/core';
787
+
788
+ const plugin: Plugin = {
789
+ name: 'my-plugin',
790
+ version: '0.1.0',
791
+ description: 'One-line summary shown in `wstack plugins list`',
792
+ apiVersion: '^0.1.10',
793
+ capabilities: { tools: true },
794
+ defaultConfig: {},
795
+ configSchema: {
796
+ type: 'object',
797
+ properties: { /* … */ },
798
+ },
799
+ setup(api) {
800
+ api.tools.register({
801
+ name: 'my_tool',
802
+ description: 'What this tool does',
803
+ inputSchema: { type: 'object', properties: { /* … */ } },
804
+ permission: 'auto',
805
+ mutating: false,
806
+ async execute(input) {
807
+ return { ok: true };
808
+ },
809
+ });
810
+ api.log.info('my-plugin loaded', { version: '0.1.0' });
811
+ },
812
+ teardown(api) {
813
+ // If you hold state, clear it here (see H1 pattern).
814
+ api.log.info('my-plugin: teardown complete');
815
+ },
816
+ async health() {
817
+ return { ok: true, message: 'my-plugin: alive' };
818
+ },
819
+ };
820
+
821
+ export default plugin;
822
+ ```
823
+
824
+ Register the entry in `tsup.config.ts`, the subpath export in
825
+ `package.json#exports`, and the named re-export in `src/index.ts`.
826
+ Wire it into the CLI's `BUILTIN_PLUGIN_FACTORIES` if it should
827
+ auto-load.
828
+
829
+ For plugins that need host-level data not yet exposed in
830
+ `PluginAPI` (e.g. `paths.projectDir`), extend the type in
831
+ `packages/core/src/types/plugin.ts` and `PluginAPIInit` in
832
+ `packages/core/src/plugin/api.ts`, then thread it through
833
+ `DefaultPluginAPI` and the wiring layer. See how `modelsRegistry`
834
+ was added for `cost-tracker` (commit `9bed619f`).
835
+
836
+ ## License
837
+
838
+ MIT — see top-level [`LICENSE`](../../LICENSE).