@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.
- package/README.md +838 -0
- package/dist/auto-doc.d.ts +8 -0
- package/dist/auto-doc.js +175 -13
- package/dist/auto-escalate.d.ts +45 -0
- package/dist/auto-escalate.js +190 -0
- package/dist/branch-guard.d.ts +33 -0
- package/dist/branch-guard.js +228 -0
- package/dist/changelog-writer.d.ts +73 -0
- package/dist/changelog-writer.js +369 -0
- package/dist/checkpoint.d.ts +55 -0
- package/dist/checkpoint.js +305 -0
- package/dist/commit-validator.d.ts +33 -0
- package/dist/commit-validator.js +315 -0
- package/dist/config-validator.d.ts +48 -0
- package/dist/config-validator.js +347 -0
- package/dist/context-pins.d.ts +45 -0
- package/dist/context-pins.js +240 -0
- package/dist/cost-tracker.d.ts +40 -1
- package/dist/cost-tracker.js +105 -4
- package/dist/dep-guard.d.ts +65 -0
- package/dist/dep-guard.js +316 -0
- package/dist/diff-summary.d.ts +36 -0
- package/dist/diff-summary.js +235 -0
- package/dist/error-lens.d.ts +67 -0
- package/dist/error-lens.js +280 -0
- package/dist/format-on-save.d.ts +35 -0
- package/dist/format-on-save.js +219 -0
- package/dist/git-autocommit.js +186 -26
- package/dist/import-organizer.d.ts +52 -0
- package/dist/import-organizer.js +274 -0
- package/dist/index.d.ts +32 -6
- package/dist/index.js +10151 -1628
- package/dist/injection-shield.d.ts +49 -0
- package/dist/injection-shield.js +205 -0
- package/dist/lint-gate.d.ts +33 -0
- package/dist/lint-gate.js +394 -0
- package/dist/llm-cache.d.ts +56 -0
- package/dist/llm-cache.js +251 -0
- package/dist/loop-breaker.d.ts +43 -0
- package/dist/loop-breaker.js +241 -0
- package/dist/model-router.d.ts +69 -0
- package/dist/model-router.js +198 -0
- package/dist/notify-hub.d.ts +45 -0
- package/dist/notify-hub.js +304 -0
- package/dist/path-guard.d.ts +54 -0
- package/dist/path-guard.js +235 -0
- package/dist/prompt-firewall.d.ts +57 -0
- package/dist/prompt-firewall.js +290 -0
- package/dist/secret-scanner.d.ts +34 -0
- package/dist/secret-scanner.js +409 -0
- package/dist/semver-bump.js +45 -0
- package/dist/session-recap.d.ts +50 -0
- package/dist/session-recap.js +421 -0
- package/dist/shell-check.js +52 -4
- package/dist/spec-linker.d.ts +51 -0
- package/dist/spec-linker.js +541 -0
- package/dist/template-engine.js +19 -1
- package/dist/test-runner-gate.d.ts +37 -0
- package/dist/test-runner-gate.js +356 -0
- package/dist/todo-listener.d.ts +37 -0
- package/dist/todo-listener.js +216 -0
- package/dist/todo-tracker.d.ts +5 -0
- package/dist/todo-tracker.js +441 -0
- package/dist/token-budget.d.ts +40 -0
- package/dist/token-budget.js +254 -0
- package/dist/token-throttle.d.ts +54 -0
- package/dist/token-throttle.js +203 -0
- package/package.json +116 -12
- package/dist/json-path.d.ts +0 -18
- package/dist/json-path.js +0 -15
- package/dist/web-search.d.ts +0 -19
- 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).
|