@fusengine/harness 0.1.55 → 0.1.57

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 CHANGED
@@ -1,16 +1,14 @@
1
1
  # @fusengine/harness
2
2
 
3
- A **harness-agnostic enforcement engine** for AI coding agents. It ports the
4
- guard/gate logic of a Claude Code plugin into one reusable, **Bun-native** npm
5
- package that runs on **any** harness Claude Code, OpenAI Codex, Cursor, Cline,
6
- Gemini CLI plus a cli-mode fallback for Aider / Windsurf / OpenHands.
7
-
8
- It splits cleanly into a **pure policy core** (no harness coupling, fully tested)
9
- and **thin per-harness adapters** that map a hook payload to the policy and back
10
- to that harness's native response.
3
+ A **governance layer for AI coding agents**: gates backed by cited evidence
4
+ (not vibes), a decision-time lesson memory, verification receipts, and a
5
+ one-shot gate metric ported from a Claude Code plugin into one **Bun-native**
6
+ npm package. The policy core is harness-agnostic; how much of it actually
7
+ *enforces* depends on the harness's own hook system — see the
8
+ [compatibility matrix](#compatibility) below before assuming parity across harnesses.
11
9
 
12
10
  ```
13
- detect → init (pre+post hooks) → `harness hook` → guards + APEX gates → native deny/ask
11
+ detect → init (writes that harness's hook wiring) → `harness hook` → guards + APEX gates → native deny/ask/context
14
12
  ```
15
13
 
16
14
  ## Install
@@ -25,18 +23,37 @@ bun add @fusengine/harness # Bun reads the TS source directly — no build
25
23
 
26
24
  ```sh
27
25
  cd your-project
28
- harness init # detects the harness, writes its pre+post hooks
29
- # The SOLID-read gate auto-activates from discovered skills (default marketplace: fusengine-plugins).
26
+ harness init # detects the harness, writes its pre+post hook wiring
30
27
  export FUSE_HARNESS_MARKETPLACES=fusengine-plugins # (optional) which marketplaces to auto-scan
31
28
  export FUSE_HARNESS_REFS=.claude/skills # (optional) explicit refs dir, overrides auto-discovery
32
29
  ```
33
30
 
34
- That's it. `init` writes the wiring file for the detected harness
35
- (`.claude/settings.json`, `.codex/hooks.json`, `.cursor/hooks.json`,
36
- `.gemini/settings.json`, or `.clinerules/hooks/PreToolUse`+`PostToolUse`), each
37
- pointing at `harness hook <id>`. From then on every tool-use is gated, and the
38
- session activity (agents run, docs consulted, refs read) is recorded
39
- automatically under `<project>/.harness/` (track, cache, memory).
31
+ `init` writes the wiring file for the detected harness (`.claude/settings.json`,
32
+ `.codex/hooks.json`, `.cursor/hooks.json`, `.gemini/settings.json`, or
33
+ `.clinerules/hooks/PreToolUse`+`PostToolUse`) — for Claude Code, Codex, Cursor,
34
+ Gemini CLI and Cline this covers **PreToolUse + PostToolUse only**
35
+ (`src/init/templates.ts`); Hermes has no `init` runner because its config lives
36
+ outside the project at `~/.hermes/config.yaml` (`src/adapters/hermes/index.ts:3-4`).
37
+ The richer lifecycle (SessionStart, SubagentStart/Stop, Stop, PreCompact/PostCompact,
38
+ TaskCompleted, TeammateIdle, PostToolUseFailure, InstructionsLoaded — 14 distinct
39
+ Claude Code hook events in total, `src/runtime/lifecycle/dispatch.ts` +
40
+ `src/runtime/handle.ts:56-80`) is **implemented** in the runtime but only fires
41
+ when something wires those extra hook events into `.claude/settings.json` — either
42
+ by hand, or via the fusengine-plugins marketplace this repo is itself developed
43
+ under (its ~11 sibling plugins each add their own event, `src/runtime/burst-window.ts:5-11`).
44
+
45
+ **Where state lives** (two separate roots — this changed across 0.1.34→0.1.51,
46
+ don't assume either the old or the "everything in one place" story):
47
+
48
+ - Session track, deny-loop/one-shot sidecars: **out-of-tree**, under
49
+ `~/.fuse-harness/state/<8-char-md5-of-project-path>/` (`src/runtime/paths.ts:5-6,41-43`) —
50
+ deliberately outside the repo so the protected-path guard never has to bless
51
+ writes to its own enforcement state.
52
+ - MCP/WebFetch response cache and the curated `MEMORY/LESSON.md`-equivalent lessons
53
+ file: still **in-tree**, under `<project>/.harness/{cache,memory}`
54
+ (`src/config/layout.ts:15-26`) — `.harness/` is gitignored except
55
+ `memory/LESSON.md`, which is meant to be committed (`STATE_GITIGNORE`,
56
+ `src/config/layout.ts:10`).
40
57
 
41
58
  ### CLI
42
59
 
@@ -66,7 +83,7 @@ Any consumer `hooks.json` / `settings.json` that runs the harness via `bunx`
66
83
 
67
84
  ```jsonc
68
85
  // .claude/settings.json — correct: exact pin
69
- "command": "bunx @fusengine/harness@0.1.41 hook claude"
86
+ "command": "bunx @fusengine/harness@0.1.56 hook claude"
70
87
  // WRONG — may silently keep running a stale global install:
71
88
  "command": "bunx @fusengine/harness hook claude"
72
89
  ```
@@ -75,9 +92,8 @@ Any consumer `hooks.json` / `settings.json` that runs the harness via `bunx`
75
92
  prefer an already-installed **global** copy over npm-latest, and publishing a new
76
93
  version updates neither that global nor the bunx cache (`bun pm cache rm` does
77
94
  **not** remove the global). A hook wired without an exact pin can therefore keep
78
- executing an old harness indefinitely after you publish a fix the failure is
79
- silent because the smoke test (importing the local dist) never exercises the
80
- `bunx`-resolved binary. Pinning `@X.Y.Z` forces the exact version to resolve.
95
+ executing an old harness indefinitely after you publish a fix. Pinning `@X.Y.Z`
96
+ forces the exact version to resolve.
81
97
 
82
98
  Run `harness doctor` to see which version is actually executing and whether npm
83
99
  has a newer one:
@@ -85,11 +101,11 @@ has a newer one:
85
101
  ```sh
86
102
  harness doctor
87
103
  # @fusengine/harness doctor
88
- # running: 0.1.41
104
+ # running: 0.1.56
89
105
  # package: /Users/you/.bun/install/global/node_modules/@fusengine/harness
90
106
  # runtime: /Users/you/.bun/bin/bun
91
- # npm latest: 0.1.42
92
- # ! stale — npm serves 0.1.42. Pin "@fusengine/harness@0.1.42" in hooks.json.
107
+ # npm latest: 0.1.57
108
+ # ! stale — npm serves 0.1.57. Pin "@fusengine/harness@0.1.57" in hooks.json.
93
109
  ```
94
110
 
95
111
  If `doctor` reports a stale global, clear it and re-pin:
@@ -100,29 +116,88 @@ bun remove -g @fusengine/harness && bun pm cache rm
100
116
 
101
117
  [oven-sh/bun#5791]: https://github.com/oven-sh/bun/issues/5791
102
118
 
119
+ ## Compatibility
120
+
121
+ **There is no "runs the same on any harness."** Each row is a real ceiling, not
122
+ a formatting nuance — read it before assuming a gate that works on Claude Code
123
+ also works elsewhere.
124
+
125
+ | Harness | PreToolUse coverage | Lifecycle (Session/Subagent/Stop/Compact/…) | Known limit |
126
+ |---|---|---|---|
127
+ | **claude-code** | Full: `evaluate` + APEX gates via `handleHook` (`src/adapters/claude/index.ts`) | 14 event types implemented (`dispatch.ts`) — fires once wired into `.claude/settings.json` beyond the `init` default | None found; richer lifecycle needs manual/marketplace wiring (see Quickstart) |
128
+ | **codex** | Reuses the Claude reader/response shape — Codex's hook wire format matches Claude's (`src/adapters/codex/index.ts:1-8`). Bash is gated reliably. | Not wired by `harness init codex` (PreToolUse `Bash\|apply_patch` + PostToolUse only, `src/init/templates.ts:29-38`) | The SOLID/file-size gate covers `apply_patch` edits at **0%** — it keys off `tool_input.file_path` (Write/Edit shape), which Codex's diff-carrying `apply_patch` call never supplies (`codex/index.ts:6-8`). Codex also **parses but does not honor** `permissionDecision:"ask"` — deny-only (`codex/index.ts:6`). |
129
+ | **cursor** | `beforeShellExecution` can deny/ask (shell only, `cursor/index.ts:16-21`) | none | `afterFileEdit` is **observe-only** — "Cursor cannot block here" (`cursor/index.ts:23-24`); a file-edit violation is logged, never prevented (platform limit, not a bug here). |
130
+ | **gemini-cli** | `BeforeTool` denies via `{decision:"deny",reason}` (`gemini/index.ts:22-36`) | none | Thin stateless adapter — no session track, no APEX gates wired through it. |
131
+ | **cline** | `PreToolUse` only; block → `{cancel:true}`, non-block → `contextModification` (`cline/index.ts:24-36`) | none | Same as gemini-cli: stateless guard only. |
132
+ | **hermes** | `pre_tool_call` proven: reuses the Claude stdin reader, blocks via `{decision:"block",reason}` (`hermes/index.ts:12-36`) | untested — no lifecycle dispatch wired for Hermes in this repo | `ask`/`inform` degrade to non-blocking `{context}` — Hermes "has no interactive ask state" (`hermes/index.ts:27-28`). |
133
+
103
134
  ## What it enforces
104
135
 
105
- Ten portable guards + the APEX gate chain, all evaluated before a tool runs:
136
+ Guard/gate chain evaluated before a tool runs (`src/policy/guards/index.ts`,
137
+ `src/policy/apex-gates.ts`, `src/policy/evaluate.ts`):
106
138
 
107
139
  | Guard / gate | Fires on |
108
140
  |---|---|
109
- | file-size (SOLID) | a code file over `FUSE_SOLID_MAX_LINES` (default 100) |
110
- | git | destructive git (`push --force`, `reset --hard`, …) |
111
- | bash-write | `python3 -c` / `sed -i` / redirects to code files |
112
- | install | `npm/pip/brew/...` installs (asks) |
113
141
  | security | `rm -rf /`, fork bombs, `curl \| sh`; `sudo` (asks) |
142
+ | protected-path | edits to `.claude/plugins\|logs\|cache`, `.git/`, the harness's own state dirs |
143
+ | bash-write | `python3 -c` / `sed -i` / redirects to code files |
114
144
  | interface-separation | top-level interface/type/protocol in a component/controller |
115
- | protected-path | edits to `.claude/plugins\|logs\|cache`, `.git/` |
145
+ | install | `npm/pip/brew/...` installs (asks) |
146
+ | git | destructive git (`push --force`, `reset --hard`, …) — block; routine git — ask |
147
+ | file-size (SOLID) | a code file over `FUSE_SOLID_MAX_LINES` (default 100) |
148
+ | APEX brainstorm | creating a new file without brainstorming (when flagged) |
116
149
  | APEX freshness | `explore-codebase` + `research-expert` not run within the window |
117
- | APEX doc-consulted | no doc source (Context7 / Exa / fuse-browser / WebSearch / WebFetch) consulted this session |
118
- | APEX solid-read | required SOLID refs (auto-discovered, or `FUSE_HARNESS_REFS`) not read |
119
- | framework sub-skill | framework / shadcn / Tailwind code whose required skill (`skills/<name>/`) wasn't read this session |
150
+ | APEX doc-consulted | Context7 **and** Exa not consulted this session (a web-only fallback also passes) |
151
+ | APEX solid-read | required SOLID refs (auto-discovered, or `FUSE_HARNESS_REFS`) not read within the TTL |
152
+ | framework sub-skill | framework / shadcn / Tailwind code whose required skill wasn't read this session |
120
153
  | Gemini MCP (opt-in) | hand-written Tailwind UI without a `mcp__gemini-design__*` call — only when `FUSE_ENFORCE_GEMINI_MCP` is set |
121
- | brainstorm | creating a new file without brainstorming (when flagged) |
122
154
  | MCP verbosity / cache | caps exa `numResults`; serves a fresh cached MCP/WebFetch result |
123
155
 
124
- A trivial-edit fast path lets a few tiny (< 5-line, non-`replace_all`) edits
125
- through per window without the full APEX gates.
156
+ A trivial-edit fast path lets a few tiny (< 5-line, non-`replace_all`) `Edit`s
157
+ through per window without the full APEX gates (`Write` is never trivial).
158
+
159
+ ## Beyond gating: memory, receipts, one-shot metric
160
+
161
+ Features shipped since 0.1.44, each with its own test:
162
+
163
+ - **Deny-loop breaker** — an identical retried call that was already denied gets
164
+ a rewritten `[REPEAT] … STOP` message forcing a different approach, instead of
165
+ looping silently (`src/policy/deny-loop.ts`, `test/deny-loop.test.ts`).
166
+ - **Burst-window dedup** — every deployed plugin registers its own hook, so one
167
+ real tool call can fan out to ~11 sibling processes; a same-op record within
168
+ 2s is folded into the first instead of re-counted (`src/runtime/burst-window.ts`,
169
+ `test/burst-dedup.test.ts`).
170
+ - **One-shot gate metric** — every gate outcome (deny or its later fix) lands in
171
+ a 7-day sidecar keyed by a content-free op hash, so a deny→allow transition is
172
+ visible (`src/tracking/one-shot.ts`, `test/one-shot.test.ts`).
173
+ - **Verification receipts** — a `tsc`/test run is captured from PostToolUse Bash
174
+ output; `TaskCompleted` **refuses** a "done" over modified code files without a
175
+ fresh passing receipt (`src/tracking/receipts.ts`, `test/receipts.test.ts`).
176
+ - **Decision-time lessons** — a `MEMORY/LESSON.md` bullet tagged with
177
+ `[TRIGGERS tool:… path:… error:… keyword:…]` is injected as `additionalContext`
178
+ the moment a matching call is about to repeat a known mistake, cooldown-guarded
179
+ (`src/policy/lessons/lesson-gate.ts`).
180
+ - **Failure lessons** *(Claude-Code-only — no `PostToolUseFailure` hook on Codex/Hermes,
181
+ `src/runtime/lifecycle/failure-lesson.ts:8-9`)* — a tool failure's error message is
182
+ matched against `error:`-triggered lessons and injected on the spot
183
+ (`test/failure-lesson.test.ts`, `test/sim/scenarios/17-failure-lesson.json`).
184
+ - **TeammateIdle anti-false-done** *(Claude-Code-only, `teammate-idle-check.ts:9-10`)* —
185
+ files a teammate announced as changed are checked against disk; a missing file
186
+ warns the lead before it's treated as done (`test/teammate-idle-check.test.ts`).
187
+ - **PostCompact re-injection** — after context compaction, the reconciliation
188
+ snapshot is re-sent with a "reread files before editing" reminder, deduped per
189
+ session (`src/runtime/lifecycle/post-compact.ts`, `test/post-compact.test.ts`).
190
+ - **Reconciliation snapshot at SessionStart** — git state, running harness
191
+ version + drift vs. npm, `.claude/BOARD.md`, and the one-shot summary, each
192
+ collector isolated so one failure can't blank the rest
193
+ (`src/runtime/lifecycle/snapshot/index.ts`, `test/snapshot.test.ts`).
194
+ - **Injection budget cap** — harness-produced context fragments (lessons,
195
+ snapshot, APEX task context) are capped at ~8000 chars each; owner-authored
196
+ content (CLAUDE.md/rules) is never capped
197
+ (`src/runtime/inject-budget.ts`, `test/inject-budget.test.ts`).
198
+ - **Hook simulator** — 18 end-to-end scenarios (payload in, expected verdict out)
199
+ replayed against the real CLI in both `src` and built `dist` modes in CI
200
+ (`test/sim/README.md`, `test/sim/scenarios/`).
126
201
 
127
202
  ### Environment
128
203
 
@@ -130,11 +205,15 @@ through per window without the full APEX gates.
130
205
  |---|---|
131
206
  | `FUSE_SOLID_MAX_LINES` | SOLID file-size limit (default `100`). |
132
207
  | `FUSE_HARNESS_REFS` | Explicit `path.delimiter`-list of `.md` SOLID-reference dirs → activates `solidReadGate`. Overrides auto-discovery. |
133
- | `FUSE_HARNESS_MARKETPLACES` | Comma-list of marketplace names whose `solid-*` skill refs are auto-discovered when `FUSE_HARNESS_REFS` is unset (default `fusengine-plugins`; an absent marketplace contributes nothing). Standalone `.claude`/`.codex`/`.cursor`/`.agents` skills are always scanned. |
134
- | `FUSE_ENFORCE_TTL_SEC` | APEX freshness window in seconds. |
135
- | `FUSE_LESSONS_THROTTLE_MIN` | Lessons-injection throttle (memory module). |
136
- | `FUSE_ENFORCE_GEMINI_MCP` | **Opt-in (default off).** `1`/`true` blocks hand-written Tailwind UI (`.tsx/.jsx/.vue/.svelte`, ≥3 utility classes) until a `mcp__gemini-design__*` call is made this session. Read fresh per call. |
137
- | `RALPH_MODE` | **Opt-in (default off).** `1`/`true` enables autonomous "Ralph" mode: exempts the safe git commands (`add`/`commit`/`checkout -b`/`status`/`diff`/`log`) from the confirmation ASK and auto-approves project installs. Destructive git (force-push, `reset --hard`) and system installs still gate. |
208
+ | `FUSE_HARNESS_MARKETPLACES` | Comma-list of marketplace names whose `solid-*` skill refs are auto-discovered when `FUSE_HARNESS_REFS` is unset (default `fusengine-plugins`). |
209
+ | `FUSE_ENFORCE_TTL_SEC` | APEX freshness window in seconds (default `120`). |
210
+ | `FUSE_LESSONS_THROTTLE_MIN` | Lessons-injection throttle, minutes (default `5`). |
211
+ | `FUSE_ENFORCE_GEMINI_MCP` | **Opt-in (default off).** Blocks hand-written Tailwind UI (`.tsx/.jsx/.vue/.svelte`) until a `mcp__gemini-design__*` call is made this session. Read fresh per call (`src/policy/gemini-mcp-gate.ts`). |
212
+ | `FUSE_DESIGN_GEMINI` | **Opt-in (default off), a *different* gate from the one above.** Enables the design-pipeline's own Gemini gates (`create_frontend` validation + "generate before hand-writing HTML/CSS") inert unless a design agent is active (`src/policy/design/gates.ts:58-60`, see [docs/design.md](docs/design.md)). |
213
+ | `FUSE_MCP_TTL_SEC` | MCP (Context7/Exa) cache freshness, seconds (default 48h, `src/runtime/mcp-key.ts`). |
214
+ | `FUSE_WEBFETCH_TTL_SEC` | WebFetch cache freshness, seconds (default 24h — pages stale faster than docs). |
215
+ | `RALPH_MODE` | **Opt-in (default off).** Exempts safe git commands (`add`/`commit`/`checkout -b`/`status`/`diff`/`log`) from the confirmation ask and auto-approves project installs. Destructive git (force-push, `reset --hard`) and system installs still gate. |
216
+ | `CLAUDE_PROJECT_DIR` | Overrides the project root used to hash the out-of-tree state dir (`src/runtime/paths.ts:20`). |
138
217
 
139
218
  ## Library usage
140
219
 
@@ -155,7 +234,8 @@ const prompt = await gate({ sessionId, framework: "react", tool: "Write",
155
234
  ```
156
235
 
157
236
  The `Prompt` it returns (`{ kind: "block" | "ask" | "inform", title, reason, actions? }`)
158
- is portable; each adapter maps it to the harness's native shape.
237
+ is portable; each adapter maps it to the harness's native shape — but, per the
238
+ compatibility matrix above, not every harness can act on every `kind`.
159
239
 
160
240
  ### Extend it
161
241
 
@@ -177,15 +257,15 @@ registerGuard(({ tool, command }) =>
177
257
  | Subpath | What |
178
258
  |---------|------|
179
259
  | `./detect` | `detectHarness()` / `detectMode()` — 13 harnesses, `hook` vs `cli`. |
180
- | `./policy` | `evaluate(ctx)`, the 10 guards, `evaluateApex`, framework detection. |
260
+ | `./policy` | `evaluate(ctx)`, the guard chain, `evaluateApex`, framework detection. |
181
261
  | `./runtime` | `handleHook`, `gate`, `recordActivity`, `activityFor`, per-harness storage + MCP intercept. |
182
- | `./tracking` | Session track: `recordAgent/Doc/RefRead`, `agentsFresh`, trivial-edit counter. |
262
+ | `./tracking` | Session track: `recordAgent/Doc/RefRead`, `agentsFresh`, receipts, one-shot metric. |
183
263
  | `./refs` | Frontmatter parse, `loadRefs(dir)`, SOLID ref scoring/routing. |
184
264
  | `./prompt` | The portable `Prompt` type + `formatPrompt`. |
185
265
  | `./cache` | MCP/WebFetch cache: key, lookup/store, compaction, response extraction. |
186
266
  | `./memory` | Per-project "never reproduce" lessons. |
187
267
  | `./config` `./util` `./state` `./statusline` `./freshness` `./init` `./cli` | env config, project-root, locks, statusline, doc-freshness, wiring templates, staged checks. |
188
- | `./adapters/{claude,codex,cursor,cline,gemini}` | Thin per-harness adapters. |
268
+ | `./adapters/{claude,codex,cursor,cline,gemini,hermes}` | Thin per-harness adapters — see [Compatibility](#compatibility) for what each can actually enforce. |
189
269
 
190
270
  ## Documentation
191
271
 
@@ -193,20 +273,40 @@ registerGuard(({ tool, command }) =>
193
273
  |-------|------|
194
274
  | [docs/index.md](https://github.com/fusengine/harness/blob/main/docs/index.md) | architecture overview + map |
195
275
  | [docs/detect.md](https://github.com/fusengine/harness/blob/main/docs/detect.md) | harness detection (`hook` vs `cli`) |
196
- | [docs/policy.md](https://github.com/fusengine/harness/blob/main/docs/policy.md) | `evaluate`, file-size, framework, APEX gates |
276
+ | [docs/policy.md](https://github.com/fusengine/harness/blob/main/docs/policy.md) | `evaluate`, the guard chain, framework, APEX gates |
197
277
  | [docs/guards.md](https://github.com/fusengine/harness/blob/main/docs/guards.md) | the guard chain, `registerGuard`, fail-closed |
198
- | [docs/runtime.md](https://github.com/fusengine/harness/blob/main/docs/runtime.md) | `handleHook`, `gate`, tracking, MCP intercept |
199
- | [docs/config.md](https://github.com/fusengine/harness/blob/main/docs/config.md) | env config (TTL, max-lines, refs dir) |
278
+ | [docs/runtime.md](https://github.com/fusengine/harness/blob/main/docs/runtime.md) | `handleHook`, `gate`, tracking, MCP intercept, state paths |
279
+ | [docs/config.md](https://github.com/fusengine/harness/blob/main/docs/config.md) | env config (TTL, max-lines, refs dir, Gemini opt-ins) |
200
280
  | [docs/modules.md](https://github.com/fusengine/harness/blob/main/docs/modules.md) | cache · refs · state · memory · statusline · util |
201
- | [docs/adapters.md](https://github.com/fusengine/harness/blob/main/docs/adapters.md) | adapters + `harness init`/`hook` wiring |
281
+ | [docs/adapters.md](https://github.com/fusengine/harness/blob/main/docs/adapters.md) | adapters, compatibility, `harness init`/`hook` wiring |
282
+ | [docs/design.md](https://github.com/fusengine/harness/blob/main/docs/design.md) | design-agent pipeline — state machine, gates, opt-in Gemini |
202
283
  | [CHANGELOG.md](https://github.com/fusengine/harness/blob/main/CHANGELOG.md) | release history |
203
284
 
204
285
  Run `bun run docs:api` for the generated typedoc API reference.
205
286
 
287
+ ## Known limitations
288
+
289
+ - **Codex file edits are not gated.** The SOLID/file-size gate keys off
290
+ `tool_input.file_path`, which Codex's `apply_patch` call never supplies — only
291
+ Bash is reliably gated on Codex today (see [Compatibility](#compatibility)).
292
+ - **Cursor file edits are advisory-only.** `afterFileEdit` fires after the edit
293
+ already happened — a platform limit, not something this harness can work around.
294
+ - **Hook fan-out is mitigated, not eliminated.** The ~11-sibling-plugin burst is
295
+ deduped within a 2s window (`BURST_DEDUP_MS`), but that window is a heuristic,
296
+ not a protocol guarantee — an unusually slow fan-out could in theory land
297
+ outside it.
298
+ - **Sidechain hook reliability is a platform issue, worked around, not fixed.**
299
+ Sub-agent `PostToolUse` hooks don't always fire on Claude Code (documented
300
+ platform issues #43612/#27655/#34692); `SubagentStop` transcript harvesting
301
+ (`src/freshness/evidence-harvest-io.ts`) compensates, but only at that
302
+ checkpoint, not continuously.
303
+ - **Hermes coverage beyond `pre_tool_call` is unverified** — no lifecycle events
304
+ have been proven against a live Hermes install in this repo.
305
+
206
306
  ## Develop
207
307
 
208
308
  ```sh
209
- bun test # 117 tests
309
+ bun test # 484 tests (94 files)
210
310
  bunx tsc --noEmit # typecheck (isolatedDeclarations)
211
311
  bun run build # dist + .d.mts via tsdown (for Node/bundler consumers)
212
312
  bun run docs:api # generate the typedoc API reference
package/dist/cli/bin.mjs CHANGED
@@ -4,7 +4,7 @@ import { t as detectHarness } from "../harness-Cb9xR8dC.mjs";
4
4
  import { t as claudeHome } from "../home-state-D0RLWP8J.mjs";
5
5
  import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-DZvP_9xB.mjs";
6
6
  import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
7
- import { F as runDoctor, I as runningVersion, L as versionBanner, Lt as todayUtc, t as handleHook } from "../handle-L4ZNmpwN.mjs";
7
+ import { J as runningVersion, Lt as todayUtc, Y as versionBanner, q as runDoctor, t as handleHook } from "../handle-DY79I4a8.mjs";
8
8
  import { delimiter, join } from "node:path";
9
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";