@jmtrin/opencode-kevin 0.9.0 → 1.0.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 (131) hide show
  1. package/README.md +558 -823
  2. package/dist/migrations/011_v10_proven.sql +69 -0
  3. package/dist/plugin/Archiver.js +0 -1
  4. package/dist/plugin/ArtifactWriter.d.ts +25 -10
  5. package/dist/plugin/ArtifactWriter.js +43 -25
  6. package/dist/plugin/CausalChain.js +0 -1
  7. package/dist/plugin/ConflictDetector.js +0 -1
  8. package/dist/plugin/ContextInjector.js +0 -1
  9. package/dist/plugin/ConventionMiner.js +0 -1
  10. package/dist/plugin/Curator.js +0 -1
  11. package/dist/plugin/Feedback.js +0 -1
  12. package/dist/plugin/HookLiveness.d.ts +6 -0
  13. package/dist/plugin/HookLiveness.js +15 -1
  14. package/dist/plugin/InjectionLedger.js +0 -1
  15. package/dist/plugin/LessonFixer.js +0 -1
  16. package/dist/plugin/Materializer.js +0 -1
  17. package/dist/plugin/MemoryService.js +0 -1
  18. package/dist/plugin/Migrate.d.ts +1 -1
  19. package/dist/plugin/Migrate.js +25 -3
  20. package/dist/plugin/PatternMiner.js +0 -1
  21. package/dist/plugin/QualityGate.js +0 -1
  22. package/dist/plugin/Reflector.js +0 -1
  23. package/dist/plugin/RepoIdentity.js +0 -1
  24. package/dist/plugin/RepoTruth.js +0 -1
  25. package/dist/plugin/Retrospective.js +7 -1
  26. package/dist/plugin/SharedLayer.js +0 -1
  27. package/dist/plugin/Store.js +0 -1
  28. package/dist/plugin/ToolCallObserver.js +0 -1
  29. package/dist/plugin/capabilities.js +0 -1
  30. package/dist/plugin/confidence.js +0 -1
  31. package/dist/plugin/contract.d.ts +46 -0
  32. package/dist/plugin/contract.js +368 -0
  33. package/dist/plugin/diff.js +0 -1
  34. package/dist/plugin/escape.d.ts +39 -0
  35. package/dist/plugin/escape.js +60 -0
  36. package/dist/plugin/fingerprint.js +0 -1
  37. package/dist/plugin/host.js +0 -1
  38. package/dist/plugin/index.d.ts +2 -2
  39. package/dist/plugin/index.js +425 -227
  40. package/dist/plugin/inferability.js +0 -1
  41. package/dist/plugin/kevin_approve.js +0 -1
  42. package/dist/plugin/kevin_audit.d.ts +31 -0
  43. package/dist/plugin/kevin_audit.js +60 -1
  44. package/dist/plugin/kevin_bench.d.ts +10 -0
  45. package/dist/plugin/kevin_bench.js +82 -0
  46. package/dist/plugin/kevin_conflicts.js +0 -1
  47. package/dist/plugin/kevin_contract.d.ts +8 -0
  48. package/dist/plugin/kevin_contract.js +59 -0
  49. package/dist/plugin/kevin_doctor.d.ts +5 -0
  50. package/dist/plugin/kevin_doctor.js +30 -2
  51. package/dist/plugin/kevin_facts.js +0 -1
  52. package/dist/plugin/kevin_native.js +0 -1
  53. package/dist/plugin/kevin_propose.js +0 -1
  54. package/dist/plugin/kevin_publish.js +0 -1
  55. package/dist/plugin/kevin_why.js +0 -1
  56. package/dist/plugin/memory-format.js +0 -1
  57. package/dist/plugin/metrics.js +0 -1
  58. package/dist/plugin/native.js +0 -1
  59. package/dist/plugin/okf-export.js +0 -1
  60. package/dist/plugin/okf-import.js +0 -1
  61. package/dist/plugin/okf.js +0 -1
  62. package/dist/plugin/perf.d.ts +35 -0
  63. package/dist/plugin/perf.js +174 -0
  64. package/dist/plugin/query-tokenizer.js +0 -1
  65. package/dist/plugin/redact.js +0 -1
  66. package/dist/plugin/replay-types.js +0 -1
  67. package/dist/plugin/replay.js +0 -1
  68. package/dist/plugin/sqlite-adapter.js +0 -1
  69. package/dist/plugin/uuid.js +0 -1
  70. package/package.json +21 -6
  71. package/dist/plugin/Archiver.js.map +0 -1
  72. package/dist/plugin/ArtifactWriter.js.map +0 -1
  73. package/dist/plugin/CausalChain.js.map +0 -1
  74. package/dist/plugin/ConflictDetector.js.map +0 -1
  75. package/dist/plugin/ContextInjector.js.map +0 -1
  76. package/dist/plugin/ConventionMiner.js.map +0 -1
  77. package/dist/plugin/Curator.js.map +0 -1
  78. package/dist/plugin/Feedback.js.map +0 -1
  79. package/dist/plugin/HookLiveness.js.map +0 -1
  80. package/dist/plugin/InjectionLedger.js.map +0 -1
  81. package/dist/plugin/LessonFixer.js.map +0 -1
  82. package/dist/plugin/Materializer.js.map +0 -1
  83. package/dist/plugin/MemoryService.js.map +0 -1
  84. package/dist/plugin/Migrate.js.map +0 -1
  85. package/dist/plugin/PatternMiner.js.map +0 -1
  86. package/dist/plugin/QualityGate.js.map +0 -1
  87. package/dist/plugin/Reflector.js.map +0 -1
  88. package/dist/plugin/RepoIdentity.js.map +0 -1
  89. package/dist/plugin/RepoTruth.js.map +0 -1
  90. package/dist/plugin/Retrospective.js.map +0 -1
  91. package/dist/plugin/SharedLayer.js.map +0 -1
  92. package/dist/plugin/Store.js.map +0 -1
  93. package/dist/plugin/ToolCallObserver.js.map +0 -1
  94. package/dist/plugin/capabilities.js.map +0 -1
  95. package/dist/plugin/confidence.js.map +0 -1
  96. package/dist/plugin/diff.js.map +0 -1
  97. package/dist/plugin/fingerprint.js.map +0 -1
  98. package/dist/plugin/host.js.map +0 -1
  99. package/dist/plugin/index.js.map +0 -1
  100. package/dist/plugin/inferability.js.map +0 -1
  101. package/dist/plugin/kevin_approve.js.map +0 -1
  102. package/dist/plugin/kevin_audit.js.map +0 -1
  103. package/dist/plugin/kevin_conflicts.js.map +0 -1
  104. package/dist/plugin/kevin_doctor.js.map +0 -1
  105. package/dist/plugin/kevin_facts.js.map +0 -1
  106. package/dist/plugin/kevin_native.js.map +0 -1
  107. package/dist/plugin/kevin_propose.js.map +0 -1
  108. package/dist/plugin/kevin_publish.js.map +0 -1
  109. package/dist/plugin/kevin_why.js.map +0 -1
  110. package/dist/plugin/memory-format.js.map +0 -1
  111. package/dist/plugin/metrics.js.map +0 -1
  112. package/dist/plugin/native.js.map +0 -1
  113. package/dist/plugin/okf-export.js.map +0 -1
  114. package/dist/plugin/okf-import.js.map +0 -1
  115. package/dist/plugin/okf.js.map +0 -1
  116. package/dist/plugin/query-tokenizer.js.map +0 -1
  117. package/dist/plugin/redact.js.map +0 -1
  118. package/dist/plugin/replay-types.js.map +0 -1
  119. package/dist/plugin/replay.js.map +0 -1
  120. package/dist/plugin/sqlite-adapter.js.map +0 -1
  121. package/dist/plugin/uuid.js.map +0 -1
  122. package/migrations/001_initial.sql +0 -92
  123. package/migrations/002_indexes.sql +0 -14
  124. package/migrations/003_v02_signal.sql +0 -58
  125. package/migrations/004_v03_knowledge.sql +0 -138
  126. package/migrations/005_v04_signal.sql +0 -57
  127. package/migrations/006_v05_glassbox.sql +0 -118
  128. package/migrations/007_v06_pull.sql +0 -145
  129. package/migrations/008_v07_truth.sql +0 -124
  130. package/migrations/009_v08_team.sql +0 -100
  131. package/migrations/010_v09_native.sql +0 -78
package/README.md CHANGED
@@ -1,824 +1,559 @@
1
- # Kevin
2
-
3
- > Observe and learn: the learning layer OpenCode was missing.
4
-
5
- Kevin is an [OpenCode](https://opencode.ai) plugin that **observes** every agent tool call, **learns** from failures by generating lessons, and **shares** what it learned proactively in future sessions. It does not plan, orchestrate, or compete with the plugin ecosystem. It only learns.
6
-
7
- - **Local-first**: SQLite + FTS5, no external services, no network calls.
8
- - **Global memory**: a single `~/.opencode-kevin/kevin.db` shared across all your projects (WAL mode → safe for concurrent sessions). No per-project folders.
9
- - **Knowledge + Causality**: causal failure→fix chains, `kevin_why` explanations, OKF export/import, a supersede model, and human-in-the-loop AGENTS.md suggestions.
10
- - **Signal over Noise**: a quality gate that stores weak lessons without injecting them, an injection ledger with honest `precision_rate`, and two-sided confidence.
11
- - **Glass Box**: honest measurement replaces estimates — three-way injection settlement (`effective` / `ineffective` / `inconclusive`), human feedback that actually moves confidence, a strict dry-run `kevin_trace`, a read-only `kevin_audit`, and a hermetic replay harness.
12
- - **Pull**: knowledge earns its way into files the model actually reads — `kevin_propose` generates a reviewable diff, a human approves, and **only then** does Kevin write, inside a frozen marker block, preserving your file's CRLF/BOM/formatting byte-for-byte outside it. Plus three distribution channels (AGENTS.md, skills, references) and a push budget gated by a confidence floor.
13
- - **Audited**: the v0.4.0 bug catalog (`docs/Kevin_v0.4.0_Bugs.md`) is fully closed — 16/16 bugs fixed and regression-tested; the v0.8.0 release audit closed 8/8 bugs (repo identity, session coherence through rekey, port normalization in remotes, refusal semantics, read-path `layer`).
14
- - **Standalone**: works without any other plugin. With the ecosystem, it learns more richly.
15
-
16
- ---
17
-
18
- ## Contents
19
-
20
- - [Installation](#installation)
21
- - [How Kevin works](#how-kevin-works)
22
- - [Tools](#tools)
23
- - [How Kevin measures itself](#how-kevin-measures-itself)
24
- - [Curation & Pull](#curation--pull)
25
- - [The shared layer (v0.8.0)](#the-shared-layer-v080)
26
- - [Replay harness](#replay-harness)
27
- - [Hooks](#hooks)
28
- - [Configuration](#configuration)
29
- - [Development](#development)
30
- - [License](#license)
31
-
32
- ---
33
-
34
- ## Installation
35
-
36
- ### 1. Declare the plugin
37
-
38
- Add Kevin to your OpenCode config. For **all projects** (global):
39
-
40
- ```jsonc
41
- // ~/.config/opencode/opencode.jsonc
42
- {
43
- "$schema": "https://opencode.ai/config.json",
44
- "plugin": [
45
- "@jmtrin/opencode-kevin@latest"
46
- ]
47
- }
48
- ```
49
-
50
- For a **single project**, put the same `plugin` array in `./opencode.json` or `.opencode/opencode.json` at the project root.
51
-
52
- ### 2. Restart OpenCode
53
-
54
- Config is loaded once at startup and is **not hot-reloaded** — quit and reopen OpenCode after editing. On start, Kevin exposes 23 tools, including `kevin_facts`, `kevin_conflicts`, `kevin_project`, `kevin_share`, `kevin_sync`, `kevin_doctor` and `kevin_native`.
55
-
56
- Contradictions de-rank memories and surface conflicts. They never delete, stale, archive, or auto-resolve a memory.
57
-
58
- ### 3. Where data lives
59
-
60
- Kevin stores everything in a single **global, shared** location under your home directory — no per-project `.kevin/` folders:
61
-
62
- | Path | Content |
63
- |---|---|
64
- | `~/.opencode-kevin/kevin.db` | SQLite database (memories, tool calls, retrospectives). WAL mode → safe for concurrent OpenCode sessions across projects. |
65
- | `~/.opencode-kevin/retrospectives/<session>.md` | Per-session retrospective markdown. |
66
-
67
- Migrations run automatically on startup.
68
-
69
- ### Requirements
70
-
71
- - **Node.js >= 22.5** (uses `node:sqlite`, the built-in SQLite module — no native binaries to compile).
72
- - OpenCode with plugin support (`@opencode-ai/plugin` >= 1.17).
73
-
74
- > **Runtimes**:
75
- > - **Bun**: uses `bun:sqlite` (built-in).
76
- > - **Node 24+**: uses `node:sqlite` directly, no flags needed (emits an experimental warning, harmless).
77
- > - **Node 22/23 without `--experimental-sqlite` flag** or **Node 20**: falls back to `better-sqlite3`, declared as `optionalDependencies`. If you need it, install it manually in your opencode config directory (`~/.config/opencode/`): `npm install better-sqlite3`.
78
-
79
- ### Verification
80
-
81
- ```bash
82
- npm run verify
83
- ```
84
-
85
- Checks Node version, SQLite, migration, MemoryService save/query, Reflector, ContextInjector, and TypeScript strict mode.
86
-
87
- ### Advanced (optional)
88
-
89
- Override defaults via the plugin tuple form:
90
-
91
- ```jsonc
92
- {
93
- "plugin": [
94
- ["@jmtrin/opencode-kevin", {
95
- "dbPath": "/custom/path/kevin.db",
96
- "retrospectivesDir": "/custom/path/retrospectives",
97
- "throttleMs": 120000
98
- }]
99
- ]
100
- }
101
- ```
102
-
103
- Use `:memory:` for `dbPath` in tests.
104
-
105
- ---
106
-
107
- ## How Kevin works
108
-
109
- Every tool call is observed; every failure becomes a lesson; every lesson is either pushed into the next prompt, written into an artifact a human approved, or retired when it stops earning its place.
110
-
111
- ```
112
- Tool call (success or failure)
113
-
114
-
115
- ┌─────────────────────────┐ OBSERVE
116
- │ ToolCallObserver │ records every call (tool, redacted args,
117
- └───────────┬─────────────┘ success, duration, error type, dedup)
118
-
119
- failure │ success
120
- ┌──────────▼───────────┐ ┌─────────────────────────┐
121
- │ Reflector │ │ CausalChain │
122
- │ heuristic lesson │ │ links the fix to the │
123
- │ per error code │ │ failure within 10 calls│
124
- │ (throttled per │ └────────────┬────────────┘
125
- │ fingerprint) │ │
126
- └──────────┬───────────┘ │ session.idle
127
- │ ▼
128
- ▼ ┌─────────────────────────┐
129
- ┌───────────────────────┐ │ promotes recurring │
130
- │ ContextInjector │◄─┤ errors → causal │
131
- │ SHARE: injects │ │ patterns (cumulative │
132
- │ <kevin-context> │ │ evidence) │
133
- │ ≤400 tokens/prompt │ └─────────────────────────┘
134
- └───────────┬───────────┘
135
- │ session.idle
136
-
137
- ┌─────────────────────────┐ RETROSPECTIVE: <session>.md with
138
- │ Retrospective │ lessons, metrics snapshot, causal
139
- └─────────────────────────┘ promotion, pattern mining (opt-in)
140
- ```
141
-
142
- At `session.idle` Kevin also settles injection outcomes, retires stale memories, drafts pull proposals for your review when curation is enabled (see [Curation & Pull](#curation--pull)), and syncs the shared OKF file when the shared layer is enabled.
143
-
144
- In plain words, the whole loop is:
145
-
146
- 1. **Watch.** Every tool call is observed what ran, what failed, what fixed it.
147
- 2. **Learn.** Failures become short lessons, deduplicated per error fingerprint and throttled so a noisy failure does not spam.
148
- 3. **Push.** At the next prompt, the best lessons are injected as `<kevin-context>` — capped, confidence-gated, and every rejection counted.
149
- 4. **Write.** With your explicit approval, a lesson can also land in `AGENTS.md`, a skill or a reference file — only ever inside the frozen marker block.
150
- 5. **Share.** With the shared layer on (v0.8.0, opt-in), approved lessons can be exported to a committed `.kevin/knowledge.okf` file that every teammate pulls and imports.
151
- 6. **Retire.** Lessons that stop being true — recurrences, staleness, a human `ignore` — stop being injected. Nothing lives forever by default.
152
-
153
- Since v0.8.0, memories live in one of two layers: **local** (private to this installation, the model above) and **shared** (a committed `.kevin/knowledge.okf` file projected into the local database, immutable, retrievable and injectable like any local memory). See [The shared layer](#the-shared-layer-v080) — the feature is opt-in and off by default.
154
-
155
- ---
156
-
157
- ## Tools
158
-
159
- Kevin exposes 23 tools callable by the agent.
160
-
161
- ### `kevin_save`
162
-
163
- Saves an explicit memory.
164
-
165
- ```
166
- kevin_save({ type: "decision", content: "We use vitest for tests", scope: "project" })
167
- // → { "id": "0195a3b2-..." }
168
- ```
169
-
170
- `type`: `error` | `pattern` | `decision` | `context` | `rule` | `solution`. `scope`: `project` (persists) | `session` (TTL 24h).
171
-
172
- Saving a `decision` or `rule` with the same `fingerprint` as an existing active row supersedes the old one (`status='superseded'`, hidden from default queries).
173
-
174
- ### `kevin_query`
175
-
176
- Searches memories by text (FTS5 + bm25). Returns a **slim** payload by default; pass `full: true` for the complete content, or `evidence: true` to include `confidence`, `evidence_count` and `last_verified_at`.
177
-
178
- ```
179
- kevin_query({ query: "typecheck", type: "error", limit: 5 })
180
- // [{ "id": "...", "type": "error", "scope": "project", "score": -0.87,
181
- // "snippet": "When bash fails with typecheck:..." }, ...]
182
- ```
183
-
184
- ### `kevin_get`
185
-
186
- Fetches a **single full memory** by id (progressive disclosure) — use it when `kevin_query` returned a slim snippet and you need the complete content.
187
-
188
- ```
189
- kevin_get({ id: "0195a3b2-..." })
190
- // → { "id": "...", "type": "error", "content": "...", "scope": "project",
191
- // "relevanceScore": 0.55, "origin": "reflector", "fingerprint": "cbf29ce484222325",
192
- // "projectId": null, "metadata": null,
193
- // "evidenceCount": 2, "recurrenceCount": 1, "lastVerifiedAt": "2026-08-01 10:00:00",
194
- // "status": "active", "confidence": 0.55, "fixArgs": "npm i -g rg" }
195
- ```
196
-
197
- ### `kevin_recall`
198
-
199
- Retrieves relevant memories (greedy fill by relevance). Without `query`, returns all memories in scope. Pass `includeSuperseded: true` to include superseded rows.
200
-
201
- ```
202
- kevin_recall({ query: "auth", limit: 3 })
203
- //[{ "id": "...", "type": "decision", ... }, ...]
204
- ```
205
-
206
- ### `kevin_status`
207
-
208
- Global counts and metrics: memory census, the precision block, the six blocked-gate counters, feedback totals, and the v0.6 block (`schema_version`, `curation_enabled`, emission states, `proposals_pending` — omitted on pre-007 databases). On 009 databases (v0.8.0) it also reports the `v08` block: `repo_id` (a 16-hex hash derived from the identity sources below — never a raw remote URL), `identity_source` (`"declared"` from `.kevin/project.json`, `"remote"` from the git origin URL, or `"path"` fallback), `shared_layer_enabled` and the repo's `shared_entries` count. Omitted on pre-009 databases.
209
-
210
- ```
211
- kevin_status({})
212
- //{ "memories": 42, "memories_reflector": 12, "memories_agent": 30,
213
- // "memories_pattern": 0, "memories_causal": 1, "tool_calls": 318,
214
- // "retrospectives": 7, "tool_count": 23,
215
- // "metrics": { "tokens_injected_pre_prompt": 51, "tokens_injected_compacting": 0,
216
- // "reflections_throttled": 3, "duplicate_suppressions": 2,
217
- // "tool_calls_deduped": 0, "patterns_mined": 0,
218
- // "patterns_causal": 1, "causal_links": 2, "memories_superseded": 0,
219
- // "injections_inconclusive": 9, ... },
220
- // "injections_total": 14, "injections_effective": 2, "injections_ineffective": 3,
221
- // "injections_inconclusive": 9, "precision_rate": 0.40, "coverage_rate": 0.36,
222
- // "blocked": { "seen": 1, "weak": 0, "recurrence": 2, "stale": 0,
223
- // "ignored": 1, "confidence": 2 },
224
- // "memories_ignored": 1, "memories_archived": 4,
225
- // "feedback": { "positive": 2, "negative": 1 },
226
- // "patterns_promoted_new": 2, "recurrence_by_origin": { "reflector": 3, "causal": 1 },
227
- // "v06": { "schema_version": "007", "curation_enabled": "1",
228
- // "skill_emission": "off", "reference_emission": "off",
229
- // "proposals_pending": 2 },
230
- // "v08": { "repo_id": "2114ad162af50a25", "identity_source": "remote",
231
- // "shared_layer_enabled": "0", "shared_entries": 3 } }
232
- ```
233
-
234
- ### `kevin_retrospective`
235
-
236
- Generates a retrospective for a session (uses the current session if `session_id` is omitted).
237
-
238
- ```
239
- kevin_retrospective({ session_id: "sess-abc" })
240
- // → { "file_path": "~/.opencode-kevin/retrospectives/sess-abc.md" }
241
- // or { "message": "No failures in session sess-abc." }
242
- ```
243
-
244
- ### `kevin_why`
245
-
246
- Explains *why* a failure keeps happening: looks up causal patterns for the query and builds a failure fix trace from memories + tool_calls, including related TypeScript error-code rules.
247
-
248
- ```
249
- kevin_why({ query: "TS2304 cannot find name" })
250
- // → { "summary": "TS2304 recurs because ... Confirmed by 2 fixes.",
251
- // "confidence": 0.7, "evidence_count": 2, "last_verified": "2026-08-01 10:00:00",
252
- // "trace": [ { "type": "error", "summary": "..." }, { "type": "fix", "tool": "bash" } ],
253
- // "related_rules": [ { "code": "TS2304", "suggestion": "import or typo" } ] }
254
- ```
255
-
256
- ### `kevin_export`
257
-
258
- Exports knowledge for sharing: `decision`/`rule`/`pattern` memories (active only, no raw errors) as YAML-frontmatter blocks (`format: "okf"`) or markdown (`format: "markdown"`). Includes `id`, `type`, `confidence`, `evidence_count`, `recurrence_count`, `last_verified_at`, `fingerprint`. Timestamps are treated as UTC — a re-import reproduces the exact source values.
259
-
260
- ### `kevin_import`
261
-
262
- Ingests an exported bundle. Each entry becomes a `context` memory with `origin='imported'`; a fingerprint collision with an existing `decision`/`rule` supersedes the old row. Returns `{ imported, superseded }`.
263
-
264
- ### `kevin_config`
265
-
266
- Reads/writes `kevin_settings` without SQL. `action: "list"` returns every setting; `action: "set"` upserts a value (default `"1"` when omitted) and rejects unknown keys unless `strict: false`.
267
-
268
- ```
269
- kevin_config({ action: "list" })
270
- // { "quality_gate_enabled": "1", "lesson_snippet_injection": "1",
271
- // "llm_reflection_enabled": "0", "pre_prompt_budget_tokens": "400",
272
- // "injection_confidence_floor": "0.6", ... }
273
-
274
- kevin_config({ action: "set", key: "quality_gate_enabled", value: "0" })
275
- // → { "ok": true, "key": "quality_gate_enabled", "value": "0" }
276
- ```
277
-
278
- All settings and their defaults are listed in [Configuration](#configuration).
279
-
280
- ### `kevin_feedback`
281
-
282
- Rates an injected memory and makes the rating count. `verdict` is `useful` | `wrong` | `outdated` | `ignore`. The first three are stored in `memory_feedback` and move `kevin_why`'s confidence (`+0.05` / `-0.1` per count); **`ignore` is a hard action** — the memory is stamped `ignored = 1` and excluded from retrieval, queries and injection.
283
-
284
- ```
285
- kevin_feedback({ memory_id: "0195a3b2-...", verdict: "wrong", note: "the fix was wrong" })
286
- // { "ok": true, "verdict": "wrong" }
287
- ```
288
-
289
- ### `kevin_trace`
290
-
291
- Strict dry-run: predicts exactly which memories `onSystemTransform` WOULD inject for a query (optionally `session_id`, `tag` and `cap`), with **zero side effects** — no counters, no ledger rows, no seen-set writes, no relevance bumps. Rejected items carry their `GateReason` (`seen_this_session` | `weak` | `recurrence` | `stale` | `ignored` | `confidence`).
292
-
293
- ```
294
- kevin_trace({ query: "tsc error" })
295
- // → { "query": "tsc error", "tag": "context", "cap": 400, "would_inject": true,
296
- // "total_tokens": 82,
297
- // "admitted": [ { "id": "...", "type": "error", "decision": "admitted", "tokens": 62 } ],
298
- // "blocked": [ { "id": "...", "type": "error", "decision": "blocked",
299
- // "reason": "confidence", "tokens": 20 } ] }
300
- ```
301
-
302
- ### `kevin_audit`
303
-
304
- Read-only report of the whole system state: memories by `status`/`origin`/`type`, injection outcomes with `precision_rate`/`coverage_rate`, the six `blocked` counters, feedback by verdict, tokens injected, the push-vs-pull `channels` comparison and the `curation` scoreboard. `verbose: true` adds the settings block. No writes, no LLM; on pre-007 databases it omits the v0.6 blocks and reports `"partial": true`.
305
-
306
- ```
307
- kevin_audit({})
308
- // → { "memories": { "total": 42, "by_status": { "active": 37, "stale": 1, "archived": 4 },
309
- // "by_origin": { "reflector": 12, "agent": 30 }, "by_type": { "error": 20, ... },
310
- // "ignored": 1, "with_feedback": 3 },
311
- // "injections": { "total": 14, "effective": 2, "ineffective": 3, "inconclusive": 9,
312
- // "unmeasured": 0, "precision_rate": 0.40, "coverage_rate": 0.36 },
313
- // "blocked": { "seen": 1, "weak": 0, "recurrence": 2, "stale": 0,
314
- // "ignored": 1, "confidence": 2 },
315
- // "feedback": { "positive": 2, "negative": 1, "by_verdict": { "useful": 2, "wrong": 1 } },
316
- // "tokens": { "pre_prompt": 51, "compacting": 0 }, "partial": false,
317
- // "channels": { "push": { "tokens_pre_prompt": 51, "injections_total": 14,
318
- // "precision_rate": 0.40, "coverage_rate": 0.36,
319
- // "budget_tokens": 400 },
320
- // "pull": { "proposals_created": 6, "proposals_approved": 1,
321
- // "proposals_rejected": 2, "artifact_writes_total": 2,
322
- // "artifact_writes_noop": 1, "references_registered": 0,
323
- // "skills_registered": 0,
324
- // "skill_emission": "off", "reference_emission": "off" } },
325
- // "curation": { "eligible": 5, "curated": 1, "inferable": 3, "non_inferable": 2,
326
- // "unknown": 1, "proposals_by_status": { "pending": 2, "applied": 1, ... } } }
327
- ```
328
-
329
- ### `kevin_propose`
330
-
331
- Creates curation proposals as `pending` rows with unified diffs — **a strict dry run**. Reads the eligible memories (`inferable != 1`), renders what would go into the artifact, and returns the minimal diff. No disk write, no `curated` marks, no side effects. Only `kevin_approve` may write.
332
-
333
- ```
334
- kevin_propose({ kind: "agents_md" }) // kind: "agents_md" | "skill" | "reference"
335
- // → { "proposals": [ { "id": "...", "kind": "agents_md", "targetPath": "AGENTS.md",
336
- // "memoryIds": ["mem-1"], "status": "pending",
337
- // "createdAt": "2026-08-14 10:00:00",
338
- // "diff": "--- a/AGENTS.md\n+++ b/AGENTS.md\n@@ ..." } ] }
339
- ```
340
-
341
- ### `kevin_approve`
342
-
343
- The **only** code path that writes a file. `approve` applies the proposal's diff atomically (temp file + rename, CRLF/BOM preserved), records an `artifact_writes` audit row, marks the proposal `applied` and its memories `curated`. `reject` records the human decision and touches nothing. Refusals and noops are audited, never silent.
344
-
345
- ```
346
- kevin_approve({ proposal_id: "...", decision: "approve" }) // or "reject"
347
- // → { "proposalId": "...", "status": "applied", "outcome": "written", "curated": 1 }
348
- // ("outcome": "noop" when the artifact already matches, "refused" when the
349
- // marker block is malformed; a rejected proposal returns
350
- // { "proposalId": "...", "status": "rejected" })
351
- ```
352
-
353
- ### `kevin_publish`
354
-
355
- Regenerates the pull-channel bundles under `~/.opencode-kevin/` — `skills/project-knowledge.md` and `refs/<topic>.md` — reporting per-bundle outcome and the emission state (`on` / `off` / `unavailable`). Registration with the host happens at plugin startup; this tool only materializes and reports.
356
-
357
- ### `kevin_project`
358
-
359
- Repository identity (v0.8.0). `action: "show"` reports the resolved `repoId`, `identity_source`, `projectId`, the memory counts under each scope and `rekey_available`; `action: "init"` writes `.kevin/project.json` pinning the derived id (refused if it already exists); `action: "rekey"` moves the whole corpus to the resolved `repo_id` in one transaction — without `confirm: true` it is a dry run that mutates nothing, and a monorepo collision is rejected unless `force: true`.
360
-
361
- A **confirmed** rekey updates the running session live — the identity, the memory service and the shared-layer bridge all move to the new `repo_id` immediately, so `kevin_status`, `kevin_audit` and `kevin_share` keep working without a restart, and the OKF file's `#repo` header is repaired so the shared channel keeps working too.
362
-
363
- ```
364
- kevin_project({ action: "show" })
365
- // → { "repoId": "2114ad162af50a25", "source": "remote", "evidence": "origin",
366
- // "projectId": "8f3c2a1b...", "projectScoped": 41, "repoScoped": 0,
367
- // "rekeyAvailable": true }
368
- ```
369
-
370
- ### `kevin_share`
371
-
372
- Promotes curated memories into the shared layer: plans an export to the OKF file and (with `confirm: true`) writes it through the single write funnel. **A strict dry run by default** — with no `confirm` it returns the plan and its diff, writing nothing. With no `memory_ids`, it selects every `layer='local'`, curated memory whose confidence clears the shared floor. Refusals: `not_okf`, `version_ahead`, `repo_mismatch`, `too_many_entries`, `line_too_long`, `below_floor`, `not_curated`, `unknown_entry`, `parse_damaged` — a typo'd or foreign memory id refuses the whole export (`unknown_entry`) instead of silently sharing a subset.
373
-
374
- ```
375
- kevin_share({ memory_ids: ["0195a3b2-..."], dry_run: false, confirm: true })
376
- // → { "memory_ids": [...], "entries_added": 1, "outcome": "written",
377
- // "okf_path": ".kevin/knowledge.okf", "diff": "--- a/...\n+++ b/..." }
378
- ```
379
-
380
- ### `kevin_sync`
381
-
382
- Ingests the OKF file (asserts → shared-layer projections; tombstones → archived) and returns `{ imported, tombstoned, skipped, reason }`. Idempotent: an unchanged file is a no-op. Also runs automatically at `session.idle` while `shared_layer_enabled = "1"`.
383
-
384
- ```
385
- kevin_sync({})
386
- // → { "path": ".kevin/knowledge.okf", "fileHash": "3d2f...", "parsed": 1,
387
- // "folded": 0, "rejected": 0, "imported": 1, "tombstoned": 0,
388
- // "skipped": false }
389
- ```
390
-
391
- ### `kevin_doctor`
392
-
393
- Read-only health check — no writes, no probe re-run, no model call. Returns `host`, `hooks`, `dependencies`, `native`, `verdict` and `reason`; `hooks` is sorted dead first so the failure is the first thing on screen. Output contains no filesystem paths or session ids — paste it into an issue report.
394
-
395
- ```
396
- kevin_doctor({})
397
- // → { "host": { "plugin_version": "1.18.18", "flavour": "v1+v2", "shell_available": true,
398
- // "v2": { "skill": true, "reference": true } },
399
- // "hooks": [{ "hook": "experimental.chat.system.transform", "state": "dead",
400
- // "fire_count": 0, "expected_count": 3, "since": "2026-08-20T10:00:00Z" },
401
- // { "hook": "tool.execute.after", "state": "live", "fire_count": 42, "expected_count": 42 }],
402
- // "dependencies": { "declared": ["@opencode-ai/plugin"], "zod_copies": 1 },
403
- // "native": { "enabled": true, "registered": { "skill": true, "reference": true },
404
- // "verified": { "skill": true, "reference": true } },
405
- // "verdict": "degraded", "reason": "experimental.chat.system.transform dead since 2026-08-20T10:00:00Z; 1 affected hook(s)", "partial": false }
406
- ```
407
-
408
- One dead hook is enough for `degraded`; every hook `live` is `healthy`; otherwise `unknown` — `unknown` is never rounded to `healthy` (D9-09). With no sessions yet, `verdict` is `unknown`.
409
-
410
- ### `kevin_native`
411
-
412
- Controls the additive v2 attachment (D9-01): `show` reports the setting, the frozen probe result and the latest `native_registrations` rows; `enable`/`disable` write `kevin_settings` only and never re-probe — a restart is required for the change to take effect. The value is TEXT `"1"`/`"0"`; `enable` on a host without the v2 subpath succeeds and reports `effective: false` (inert) rather than refusing.
413
-
414
- ```
415
- kevin_native({ action: "show" })
416
- // → { "action": "show", "value": "0", "effective": true,
417
- // "registrations": [{ "surface": "skill", "registered": true, "verified": true, "attached_at": "2026-08-20T10:00:00Z" }] }
418
-
419
- kevin_native({ action: "enable" })
420
- // → { "action": "enable", "value": "1", "effective": false,
421
- // "reason": "v2 subpath absent from the resolved host package — registration would be inert",
422
- // "note": "the probe is frozen for the process lifetime — restart the host for the change to take effect" }
423
- ```
424
-
1
+ <div align="center">
2
+
3
+ # Kevin
4
+
5
+ ```
6
+ ╔══════════════════════════════════════════════╗
7
+ ║ ║
8
+ ║ ██╗ ██╗███████╗██╗ ██╗██╗███╗ ██╗ ║
9
+ ║ ██║ ██╔╝██╔════╝██║ ██║██║████╗ ██║ ║
10
+ ║ █████╔╝ █████╗ ██║ ██║██║██╔██╗ ██║ ║
11
+ ║ ██╔═██╗ ██╔══╝ ╚██╗ ██╔╝██║██║╚██╗██║ ║
12
+ ║ ██║ ██╗███████╗ ╚████╔╝ ██║██║ ╚████║ ║
13
+ ║ ╚═╝ ╚═╝╚══════╝ ╚═══╝ ╚═╝╚═╝ ╚═══╝ ║
14
+ ║ ║
15
+ ║ Local-First Memory for OpenCode ║
16
+ ║ ║
17
+ ╚══════════════════════════════════════════════╝
18
+ ```
19
+
20
+ ### Local-first memory for OpenCode — it observes, learns, remembers and proves it.
21
+
22
+ **Kevin watches. Kevin learns. Kevin remembers.**
23
+
24
+ It turns every coding session into durable, confidence-scored knowledge,
25
+ injects exactly what matters back into the model's context, curates the best
26
+ of it into files you control, and shares it across a team through one
27
+ git-friendly file — deterministically, locally, with zero network calls.
28
+
29
+ ![version](https://img.shields.io/badge/version-1.0.0-blue)
30
+ ![node](https://img.shields.io/badge/node-%E2%89%A522.5-green)
31
+ ![tests](https://img.shields.io/badge/tests-1374%20passing-brightgreen)
32
+ ![deps](https://img.shields.io/badge/runtime%20deps-1-orange)
33
+ ![network](https://img.shields.io/badge/network-zero-black)
34
+ ![license](https://img.shields.io/badge/license-MIT-lightgrey)
35
+
36
+ </div>
37
+
38
+ > **AI agents are good at solving problems. Kevin makes sure they don't have to solve the same problem twice.**
39
+
40
+ ---
41
+
42
+ ## 📖 Contents
43
+
44
+ - [Why Kevin](#-why-kevin)
45
+ - [The Kevin loop](#-the-kevin-loop)
46
+ - [Quick start](#-quick-start)
47
+ - [What's new in 1.0.0](#-whats-new-in-100)
48
+ - [How it works](#-how-it-works)
49
+ - [The 25 tools](#-the-25-tools)
50
+ - [The benchmark: proven, not promised](#-the-benchmark-proven-not-promised)
51
+ - [Curation: from session noise to AGENTS.md](#-curation-from-session-noise-to-agentsmd)
52
+ - [Team sharing: one file, zero servers](#-team-sharing-one-file-zero-servers)
53
+ - [The contract](#-the-contract)
54
+ - [Hooks & latency budgets](#-hooks--latency-budgets)
55
+ - [Configuration](#%EF%B8%8F-configuration)
56
+ - [Supported runtimes](#-supported-runtimes)
57
+ - [Design & trust](#-design--trust)
58
+ - [Development](#-development)
59
+ - [License](#-license)
60
+
61
+ ---
62
+
63
+ ## 🤔 Why Kevin
64
+
65
+ Every coding session produces experience errors, fixes, decisions,
66
+ conventions, lessons. Without memory, most of it evaporates when the context
67
+ window closes: the next session starts near zero and walks straight into the
68
+ same wall.
69
+
70
+ Kevin closes that loop:
71
+
72
+ | 🔍 Observe | 🧠 Learn | 🎯 Recall | ✍️ Curate | 👥 Share | 📏 Prove |
73
+ |---|---|---|---|---|---|
74
+ | Tool calls, chat messages, session signals | Failures become lessons, causal chains, patterns | Relevant knowledge ranked and injected inside a token budget | The best knowledge becomes human-approved `AGENTS.md` guidance | Curated knowledge travels through one git-friendly file | Latency and retrieval quality are measured, not assumed |
75
+
76
+ **Local first, by design.** Your coding experience belongs to you:
77
+
78
+ ```
79
+ no cloud service · no telemetry · no network calls · no hidden write path
80
+ ```
81
+
82
+ Knowledge lives in a local SQLite database, is projected into files you
83
+ control, and is shared — optionally — through a single file you can read,
84
+ diff and review like code.
85
+
86
+ ---
87
+
88
+ ## 🔁 The Kevin loop
89
+
90
+ ```
91
+ CODING SESSION
92
+
93
+
94
+ 🔍 OBSERVE ────── tool calls, chat signals, failures
95
+
96
+
97
+ 🧠 LEARN ─────── lessons, causal chains, patterns
98
+
99
+
100
+ 💾 REMEMBER ───── confidence · provenance · recurrence
101
+
102
+
103
+ 🎯 RECALL ────── rank gates → token budget
104
+
105
+
106
+ MODEL CONTEXT ──── only what matters now
107
+
108
+
109
+ ✍️ CURATE ─────── propose HUMAN review approve
110
+
111
+
112
+ AGENTS.md
113
+
114
+
115
+ NEXT SESSION ─────────────────▶ 🔁
116
+ ```
117
+
118
+ **Experience compounds instead of evaporating.**
119
+
120
+ ---
121
+
122
+ ## 🚀 Quick start
123
+
124
+ ### 1. Declare the plugin
125
+
126
+ ```jsonc
127
+ // opencode.json
128
+ {
129
+ "plugin": ["@jmtrin/opencode-kevin"]
130
+ }
131
+ ```
132
+
133
+ ```bash
134
+ npm install @jmtrin/opencode-kevin
135
+ ```
136
+
137
+ ### 2. Restart OpenCode
138
+
139
+ On first boot Kevin migrates its database to schema version `011` and starts
140
+ observing. Nothing else is required.
141
+
142
+ ### 3. Talk to it
143
+
144
+ ```
145
+ kevin_status → is everything healthy?
146
+ kevin_why → why did this keep failing, and how was it fixed?
147
+ kevin_query → what does Kevin remember about X?
148
+ kevin_doctor → health report: hooks, deps, perf, verdict
149
+ ```
150
+
151
+ ### 4. Where data lives
152
+
153
+ ```
154
+ ~/.opencode-kevin/
155
+ ├── kevin.db ← everything Kevin learns (SQLite, WAL)
156
+ ├── skills/ ← generated pull channels
157
+ └── refs/ ← topic reference bundles
158
+
159
+ <repo>/.kevin/
160
+ ├── AGENTS.md ← curated knowledge (marker block, human-approved)
161
+ └── knowledge.okf ← optional team-sharing file (opt-in)
162
+ ```
163
+
164
+ ---
165
+
166
+ ## 🆕 What's new in 1.0.0
167
+
168
+ > 1.0.0 is the **proven release**: the surface is frozen as data, the cost is
169
+ > measured, the value is benchmarked — reproducibly.
170
+
171
+ - ❄️ **A frozen public contract** — nine clauses (`C-01` … `C-09`) derived from
172
+ live source, digest-stamped, enforced by an append-only golden file.
173
+ Inspect it live with `kevin_contract`.
174
+ - ⏱️ **Latency budgets** — eight instrumented scopes; `npm run bench:check`
175
+ fails if any scope exceeds its p95 budget. `dispose` joins as the seventh
176
+ hook with crash-safe deferred settlement.
177
+ - 📊 **A reproducible benchmark** — committed synthetic corpus, four arms,
178
+ committed result. See [below](#-the-benchmark-proven-not-promised).
179
+ - 🛡️ **An untrusted-input boundary** everything reaching an artifact or
180
+ prompt is escaped at the single write path; stored text is never trusted.
181
+ - 📦 **A corrected published package** — types-first exports, `dist/` only,
182
+ verified against the packed tarball by `npm run verify:pack`.
183
+
184
+ ---
185
+
186
+ ## ⚙️ How it works
187
+
188
+ Kevin is an intentionally deterministic pipeline — no LLM in the core loop:
189
+
190
+ ```
191
+ your coding session
192
+
193
+
194
+ ┌────────────────────────────────────────────┐
195
+ │ 🔍 OBSERVE │
196
+ │ tool.execute.before/after · chat.message │
197
+ Failures auto-detected from exit codes, │
198
+ │ stderr and stdout markers. │
199
+ └──────────────────┬─────────────────────────┘
200
+
201
+ ┌────────────────────────────────────────────┐
202
+ 🧠 LEARN │
203
+ Reflector lessons │
204
+ │ CausalChain → failure/fix links │
205
+ │ PatternMiner → repeated sequences │
206
+ Evidence raises confidence; recurrence │
207
+ │ lowers it and flags staleness. │
208
+ └──────────────────┬─────────────────────────┘
209
+
210
+ ┌────────────────────────────────────────────┐
211
+ │ 🎯 INJECT │
212
+ QualityGate BM25 × origin × recency × │
213
+ truth penalty token-budget fit │
214
+ Deduped per session, inside <kevin-context>│
215
+ └──────────────────┬─────────────────────────┘
216
+
217
+ ┌────────────────────────────────────────────┐
218
+ ✍️ CURATE │
219
+ propose HUMAN REVIEW → approve │
220
+ Nothing writes without approval. │
221
+ └──────────────────┬─────────────────────────┘
222
+
223
+ ┌────────────────────────────────────────────┐
224
+ 📏 PROVE │
225
+ perf budgets · retrieval benchmark · │
226
+ contract digest │
227
+ The system measures itself instead of │
228
+ merely claiming to work. │
229
+ └────────────────────────────────────────────┘
230
+ ```
231
+
232
+ ---
233
+
234
+ ## 🧰 The 25 tools
235
+
236
+ <details open>
237
+ <summary><b>🧠 Core memory</b></summary>
238
+
239
+ | Tool | What it does |
240
+ |---|---|
241
+ | `kevin_save` | Store a memory: decision, rule, pattern, context or solution |
242
+ | `kevin_query` | Search memories — slim payload; `evidence: true` exposes confidence |
243
+ | `kevin_get` | Fetch one memory in full |
244
+ | `kevin_recall` | Ranked recall with origin-aware scoring |
245
+ | `kevin_status` | Session scoreboard: counts, precision, metrics |
246
+ | `kevin_config` | List/set any of the 31 settings no SQL required |
247
+ | `kevin_project` | Show, initialize or rekey the repository identity |
248
+
249
+ </details>
250
+
251
+ <details>
252
+ <summary><b>🔎 Understanding & debugging</b></summary>
253
+
254
+ | Tool | What it does |
255
+ |---|---|
256
+ | `kevin_why` | Failure→fix trace for a recurring error, with confidence and evidence |
257
+ | `kevin_trace` | Dry-run: exactly what would be injected for a query, zero side effects |
258
+ | `kevin_feedback` | Rate an injected memory `useful` / `wrong` / `outdated` / `ignore` |
259
+ | `kevin_conflicts` | Surface contradictions between memories and repository truth |
260
+ | `kevin_facts` | Scan the repository for ground-truth facts |
261
+ | `kevin_retrospective` | Per-session markdown retrospective |
262
+
263
+ </details>
264
+
265
+ <details>
266
+ <summary><b>✍️ Curation & publishing</b></summary>
267
+
268
+ | Tool | What it does |
269
+ |---|---|
270
+ | `kevin_propose` | Dry-run curation proposals with unified diffs — writes nothing |
271
+ | `kevin_approve` | The **only** path that writes `AGENTS.md` (or rejects) |
272
+ | `kevin_publish` | Regenerate skill/ref pull bundles under `~/.opencode-kevin/` |
273
+
274
+ </details>
275
+
276
+ <details>
277
+ <summary><b>👥 Team & operations</b></summary>
278
+
279
+ | Tool | What it does |
280
+ |---|---|
281
+ | `kevin_share` | Promote curated memories into `.kevin/knowledge.okf` (approval-gated) |
282
+ | `kevin_sync` | Import the shared file into the local layer |
283
+ | `kevin_export` / `kevin_import` | Markdown/OKF bundles out and in |
284
+ | `kevin_audit` | Whole-system report: memories, injections, channels, team, perf, contract |
285
+ | `kevin_doctor` | Health verdict: `healthy` / `degraded` / `unknown`, with reasons |
286
+ | `kevin_native` | Show/enable/disable native host registration (default off) |
287
+ | `kevin_contract` | **v1.0.0** — inspect the frozen public surface at runtime |
288
+ | `kevin_bench` | **v1.0.0** — report benchmark results; never runs them in-session |
289
+
290
+ </details>
291
+
425
292
  ---
426
-
427
- ## How Kevin measures itself
428
-
429
- ### Injection outcomes
430
-
431
- Every injection is settled at `session.idle` into one of **four outcomes**:
432
-
433
- | Outcome | Meaning | Counts toward precision? |
434
- |---|---|---|
435
- | `effective` | A linked fix was observed after the injection | yes (numerator) |
436
- | `ineffective` | The same error recurred after the injection | yes (denominator) |
437
- | `inconclusive` | Neither — the error did not recur, but no fix was seen either | no |
438
- | `unmeasured` | Session went idle before settlement could run | no |
439
-
440
- - **`precision_rate`** = `effective / (effective + ineffective)`. Measuring *effect*, not absence of recurrence: a lesson that was injected and never contradicted counts as `inconclusive`, not success. **Your precision rate will look lower than before v0.5.0. That is the honest number.**
441
- - **`coverage_rate`** = `(effective + ineffective) / total` — the share of injections that were actually measured. Reported alongside precision so a low measurable fraction stays visible instead of hiding behind a large total.
442
- - **`blocked`** counts every gate rejection by reason `seen_this_session`, `weak`, `recurrence`, `stale`, `ignored`, `confidence` — a rejection you did not count did not happen.
443
-
444
- ### The quality gate
445
-
446
- Weak lessons errors the reflector cannot dispatch to a deterministic rule — are **stored but never injected** while `quality_gate_enabled = '1'` (default). Recurrences demote lessons (`recurrence_count` → `stale`) and lower confidence. Debug mode: `kevin_config({ action: "set", key: "quality_gate_enabled", value: "0" })` re-injects weak lessons with a `(low confidence)` marker.
447
-
448
- ### Seeing the whole picture
449
-
450
- `kevin_trace` shows you the plan *before* it happens (dry run, zero side effects); `kevin_audit` reads the whole state after; `kevin_feedback` lets a human correct it — and the correction moves the confidence number `kevin_why` reports.
451
-
452
- ---
453
-
454
- ## Curation & Pull
455
-
456
- ### The marker contract
457
-
458
- Kevin never edits your files directly. Every artifact write happens inside a frozen marker block, delimited verbatim by:
459
-
460
- ```
461
- <!-- kevin:begin — curated by opencode-kevin, safe to edit -->
462
- <!-- kevin:end -->
463
- ```
464
-
465
- These exact strings are **frozen for the v0.x line** — README, tests and the v1.0.0 migration plan all depend on their byte sequences. What Kevin guarantees:
466
-
467
- - **Only the block between the markers may change.** Bytes outside them are byte-identical after every write — including line endings (a CRLF file stays CRLF everywhere, even inside the generated block), a leading UTF-8 BOM, and the file's final newline.
468
- - **Malformed markers are refused, never repaired.** If the file contains a `begin` without an `end` (or vice versa), Kevin refuses the write with an explicit reason and the file is untouched. Repairing would mean guessing at user intent; refusing means the state stays visible and auditable.
469
- - **Idempotent**: applying an unchanged plan is a counted `noop` no temp file, no write, no mtime churn.
470
-
471
- ### The propose → review → approve flow
472
-
473
- ```
474
- eligible memories (inferable != 1)
475
-
476
-
477
- kevin_propose({ kind }) ── creates pending rows + unified diffs.
478
- │ NO disk write, NO curated marks.
479
-
480
- HUMAN REVIEWS THE DIFF ── this is the entire safety model:
481
- │ a memory earns its way into a file
482
- ▼ only after a human said yes.
483
- kevin_approve({ proposal_id, decision })
484
-
485
- ├── "approve" ── ArtifactWriter.apply() (the ONLY write path)
486
- │ atomic temp+rename, audit row in artifact_writes,
487
- │ memory marked curated, proposal marked applied
488
- └── "reject" ── recorded, nothing touches disk
489
- ```
490
-
491
- Rejection history is never deleted: it is the evidence base for the roadmap's kill criterion "proposals rejected more often than approved".
492
-
493
- ### Three distribution channels
494
-
495
- | Channel | Artifact | Cost when unused |
496
- |---|---|---|
497
- | **Push** | per-prompt `<kevin-context>` injection | charges on every prompt — now capped at 400 tokens by default |
498
- | **Pull AGENTS.md** | marker block in the project's `AGENTS.md` | zero |
499
- | **Pull skills** | `~/.opencode-kevin/skills/project-knowledge.md` (`skill_emission_enabled`) | zero |
500
- | **Pull references** | `~/.opencode-kevin/refs/<topic>.md` (`reference_emission_enabled`) | zero |
501
-
502
- `kevin_audit`'s `channels` block compares push vs pull on the same axes, and reports each emission channel as `"on"`, `"off"` (setting `'0'` on a capable host) or `"unavailable"` (host without the v2 domain).
503
-
504
- ### The confidence floor gate
505
-
506
- `injection_confidence_floor` (default `'0.6'`) rejects memories whose computed confidence is below the floor, counted as `injections_blocked_confidence` — the sixth gate rejection reason, measured exactly like the first five. Single-observation memories (base confidence 0.5, no confirmed evidence) stop being pushed by default; `kevin_config({ action: "set", key: "injection_confidence_floor", value: "0" })` restores v0.5 behaviour exactly.
507
-
508
- ---
509
-
510
- ## The shared layer (v0.8.0)
511
-
512
- ### Two layers, one file
513
-
514
- Every memory lives in one of two layers:
515
-
516
- | Layer | Storage | Visibility |
517
- |---|---|---|
518
- | **local** | `kevin.db` → `memories` | private to this installation (the whole v0.1–v0.7 model) |
519
- | **shared** | `.kevin/knowledge.okf` (a committed file) → `shared_entries` projections | visible to every teammate who pulls the repo and runs Kevin |
520
-
521
- The OKF file is the *only* distribution vehicle. Kevin **never** commits it, pushes it, or talks to a server — you commit and push it with the rest of your repository. A teammate's `git pull` + `kevin_sync` (or the automatic sync at `session.idle`) turns every `assert` entry into a read-only projection in their `memories` table, retrievable and injectable like any local memory — but immutable: the shared layer is an exact projection of the committed file, so it is never edited, only re-imported or tombstoned. A tombstone in the file archives the projection everywhere.
522
-
523
- The whole feature is opt-in: with `shared_layer_enabled = "0"` (the default), Kevin never reads or writes the file, and `session.idle` performs no filesystem access at all.
524
-
525
- ### Where the repo identity comes from
526
-
527
- Every repo has a `repo_id` a 16-hex hash that scopes both the OKF file (`#repo`) and the shared projections. Kevin resolves it from three sources, in priority order:
528
-
529
- | Priority | Source | `identity_source` | Typical case |
530
- |---|---|---|---|
531
- | 1 | `.kevin/project.json` (`repo_id` pinned) | `declared` | after `kevin_project init` |
532
- | 2 | the git `origin` remote URL, hashed | `remote` | a repo cloned from a known remote |
533
- | 3 | the project path | `path` | a local folder without git |
534
-
535
- Only the **hash** is ever stored, written or reported — the raw remote URL never reaches the database or the OKF file. `kevin_project` is the mirror of this resolution: `show` tells you which source won, `init` pins it, `rekey` moves the whole corpus when the identity changes (say, the repo got a new remote):
536
-
537
- ```
538
- startup or kevin_project call
539
-
540
-
541
- resolve(): declared ──► remote ──► path (first source that yields)
542
-
543
-
544
- repo_id (16-hex hash — never the raw URL)
545
-
546
- ├── kevin_project show → which source won, counts, rekey_available
547
- ├── kevin_project init → pins the id into .kevin/project.json
548
- └── kevin_project rekey → one transaction moves everything together:
549
- DB corpus + live session identity
550
- + the OKF file's #repo header
551
- ```
552
-
553
- Because `rekey` re-aligns the running session (not just the database), the natural flow "add a remote → rekey → share" works without restarting OpenCode — and the `#repo` header heal keeps `kevin_share`/`kevin_sync` from refusing the file with `repo_mismatch`.
554
-
555
- ### The round trip, step by step
556
-
557
- ```
558
- you (repo A) teammate (repo B)
559
- ┌───────────────────────────────────────┐ ┌───────────────────────────────────────┐
560
- │ kevin_share({ memory_ids, │ │ git pull │
561
- │ confirm: true }) │ │ │ │
562
- │ │ 1. identity gate: #repo must │ │ ▼ │
563
- │ │ match your repo_id │ │ kevin_sync (or automatically at │
564
- │ │ 2. entry_id = hash(type, │ │ session.idle) │
565
- │ │ statement, scope) │ │ │ │
566
- │ │ 3. per-line and corpus limits │ │ ├─ new asserts → shared_entries │
567
- │ ▼ │ │ │ → projections (layer='shared', │
568
- │ .kevin/knowledge.okf │ │ │ immutable, retrievable) │
569
- │ │ (the only thing Kevin writes) │ │ ├─ tombstones → projections │
570
- │ ▼ │ │ │ archived │
571
- │ git commit + push │ │ └─ unchanged file → no-op │
572
- └───────────────────────────────────────┘ └───────────────────────────────────────┘
573
- ```
574
-
575
- Kevin never commits, pushes or fetches anything: the git remote is the transport, and `git commit`/`git push` are yours.
576
-
577
- ### The OKF v2 format
578
-
579
- One annotated example, byte-exact (the three header lines are always first):
580
-
581
- ```
582
- #okf 2
583
- #repo 2114ad162af50a25
584
- #generated-by opencode-kevin/0.8.0
585
- {"author_hash":null,"created_at":"2026-08-17T03:34:17Z","entry_id":"2d80f0972a4b8c92","evidence":6,"op":"assert","origin":"pattern","recurrence":0,"scope":"project","statement":"Always use the repository pattern for the data layer","supersedes":null,"type":"rule"}
586
- ```
587
-
588
- - `#okf 2` the format version. A file with a **higher** version is refused (`version_ahead`), never downgraded.
589
- - `#repo <16-hex>` — the repo identity the file belongs to. A file from another repo is refused (`repo_mismatch`) the shared layer never crosses repositories.
590
- - `#generated-by opencode-kevin/<version>` provenance of the writer.
591
- - Each following line is one JSON entry. `op: "assert"` declares knowledge; `op: "tombstone"` retires the `entry_id` it carries. `entry_id` is a deterministic hash of `(type, statement, scope)`, so the same statement from any teammate collides to the same entry — that is what makes imports idempotent.
592
- - Limits, enforced at export *and* import: `MAX_LINE_BYTES = 4096`, `MAX_ENTRIES = 2000`. A corpus beyond the limits is refused, never truncated.
593
-
594
- ### When git reports a conflict in `.kevin/knowledge.okf`
595
-
596
- A merge conflict in the OKF file is expected — both sides asserted different knowledge. The counter-intuitive answer: **keep both sides, then run `kevin_sync`.** Kevin's import is merge-friendly by design: entries are idempotent (same `entry_id` no-op) and disjoint entries coexist in the same file.
597
-
598
- Worked example your branch asserted "Always use the repository pattern…" and your teammate's branch asserted "Always wrap file writes in a temp-file + rename helper". Git cannot merge two files that both changed, so it stops:
599
-
600
- ```
601
- <<<<<<< HEAD
602
- #okf 2
603
- #repo 2114ad162af50a25
604
- #generated-by opencode-kevin/0.8.0
605
- {"op":"assert", ... "statement":"Always use the repository pattern..."}
606
- =======
607
- #okf 2
608
- #repo 2114ad162af50a25
609
- #generated-by opencode-kevin/0.8.0
610
- {"op":"assert", ... "statement":"Always wrap file writes in a temp-file + rename helper..."}
611
- >>>>>>> teammate
612
- ```
613
-
614
- 1. Resolve by keeping both entry lines (drop the `<<<<<<<`, `=======`, `>>>>>>>` markers and the duplicated headers, keep the other headers and both JSON lines).
615
- 2. Save the fileit now contains both assertions.
616
- 3. Run `kevin_sync`. Both entries import; both projections become active; the conflict is resolved and the corpus is the union of both sides.
617
-
618
- Do **not** delete either side "to make it clean" — that deletes knowledge. The one thing to avoid: a file left with the conflict markers still present will refuse to import (`parse_damaged`), because a `<<<<<<<` line is not a valid entry.
619
-
620
- ### Retiring shared knowledge
621
-
622
- There is no in-product tool that writes a `tombstone` entry for you (a deliberate v0.8.0 scope decision). To retire a shared entry, edit the file by hand: replace the `assert` line with a `tombstone` line carrying the same `entry_id`, commit, and let teammates' `kevin_sync` archive the projection. The import side always honors tombstones — the export side just has no button for them yet.
623
-
624
- ### Non-goals (asked for, deliberately not built)
625
-
626
- - **No server.** The file, the git remote, and your own discipline are the only transport.
627
- - **No account.** No signup, no cloud, no telemetry — `author_identity_mode` defaults to `hashed` and no identity is ever transmitted.
628
- - **No automatic commit.** Kevin writes the file; git is yours.
629
- - **No cross-repository corpus.** `#repo` scopes the file: exports are refused (`repo_mismatch`) when the file belongs to another repository. Imports, by contrast, are deliberately tolerant — an entry's validity is decided by its own `entry_id`, so a copied file imports as if it belonged to your repo (the tested, documented behaviour).
630
- - **No undelete.** A tombstone is a statement, not a recovery mechanism — keep your git history if you need to resurrect an entry.
631
- - **No tombstone tool (yet).** See [Retiring shared knowledge](#retiring-shared-knowledge) — retirement happens by editing the file.
632
-
633
- ---
634
-
635
- ## Replay harness
636
-
637
- `npm run replay` runs every transcript in `tests/replay/fixtures/` through the plugin against an in-memory database with a frozen clock and prints one table row per transcript (memories created, injection outcomes, `precision_rate`, `coverage_rate`, tokens). Record your own session as a JSON array of typed events (`session.created`, `chat.message`, `tool.before`, `tool.after`, `system.transform`, `compacting`, `session.idle`) with ISO-8601 `at` timestamps, drop it into `tests/replay/fixtures/`, and re-run. The `at` timestamps are the only source of time during replay.
638
-
639
- ---
640
-
641
- ## Hooks
642
-
643
- Kevin subscribes to 6 OpenCode hooks:
644
-
645
- | Hook | What Kevin does |
646
- |---|---|
647
- | `tool.execute.before` | Records tool call start (callID + redacted args) |
648
- | `tool.execute.after` | Records result (id = callID); on failure → Reflector.invoke async (throttled); on success → CausalChain links the fix |
649
- | `experimental.chat.system.transform` | Injects relevant lessons in `<kevin-context>` (400 tokens by default, configurable) + optional `<kevin-suggestion>` |
650
- | `experimental.session.compacting` | Re-injects lessons in `<kevin-memory>` after compacting (2000 tokens) + optional `<kevin-suggestion>` |
651
- | `event` (`session.created`) | Captures current `sessionID` (skill/reference emissions register at plugin startup, not per session) |
652
- | `event` (`session.idle`) | Settles injection outcomes; generates the retrospective; boosts positive lessons; penalizes recurring failures; promotes causal patterns and mines patterns (opt-in); drafts curation proposals (`curation_enabled`); syncs the shared OKF file (`shared_layer_enabled`); flushes metrics |
653
-
654
- **Redaction**: absolute paths (`C:\Users\...`, `/home/...`) `<path>` and secrets (`API_KEY=`, `Bearer`, `token`) → `<redacted>` before persisting anything. `<private>…</private>` blocks are swept from tool call args and output before persistence and replaced with `<private: redacted N chars>`.
655
-
656
- **Throttle**: Reflector generates at most 1 lesson per minute per unique fingerprint (per-fingerprint, not global). Configurable via `throttleMs`.
657
-
658
- **Truncation**: content > 4KB keeps the lesson searchable; only the additional context is truncated (`metadata.truncated = true`).
659
-
660
- ---
661
-
662
- ## Configuration
663
-
664
- ### Plugin options
665
-
666
- Kevin accepts options via the plugin's tuple form (see Installation → Advanced). Programmatic defaults:
667
-
668
- ```ts
669
- import { KevinPlugin } from "@jmtrin/opencode-kevin";
670
-
671
- // defaults
672
- KevinPlugin(input, {
673
- dbPath: "~/.opencode-kevin/kevin.db", // or ":memory:" for tests
674
- migrationsDir: "<package>/dist/migrations", // resolved automatically
675
- retrospectivesDir: "~/.opencode-kevin/retrospectives",
676
- throttleMs: 60_000,
677
- });
678
- ```
679
-
680
- ### Settings
681
-
682
- Read/write via `kevin_config({ action: "list" | "set", ... })`. All 27 values are TEXT; booleans compare against `"1"`.
683
-
684
- | Setting | Default | Effect |
685
- |---|---|---|
686
- | `quality_gate_enabled` | `"1"` | Weak lessons are stored but never injected while enabled |
687
- | `lesson_snippet_injection` | `"1"` | Injects the rescued errorType snippet with each lesson |
688
- | `llm_reflection_enabled` | `"0"` | Opt-in LLM enrichment of reflector lessons |
689
- | `cross_project_enabled` | `"0"` | `kevin_query` includes imported cross-project memories |
690
- | `patternminer_enabled` | `"0"` | Opt-in deterministic 2-gram/3-gram pattern miner at `session.idle` |
691
- | `tool_calls_dedup_enabled` | `"0"` | Opt-in dedup of repeated tool calls |
692
- | `deterministic_retrieval` | `"0"` | Freezes Kevin's internal clock (recency factor 1.0, no relevance bumps) — for hermetic tests and the replay harness |
693
- | `pre_prompt_budget_tokens` | `"400"` | Pre-prompt injection cap, clamped to `[0, 4000]`; `0` turns push off |
694
- | `archive_after_days` | `"30"` | Age at which stale non-pattern memories are retired to `archived` on `session.idle` |
695
- | `curation_enabled` | `"1"` | Generates curation proposals at `session.idle` |
696
- | `agents_md_path` | `"AGENTS.md"` | Where the AGENTS.md channel writes (project-relative) |
697
- | `skill_emission_enabled` | `"0"` | Registers the curated skill with the host at startup (v2 hosts only) |
698
- | `reference_emission_enabled` | `"0"` | Registers `@kevin/<topic>` references at startup (v2 hosts only) |
699
- | `injection_confidence_floor` | `"0.6"` | Push gate: memories below this confidence are counted and rejected |
700
- | `repo_truth_enabled` | `"0"` | Opt-in Project Truth: fact scanning + contradiction detection at `session.idle` |
701
- | `convention_mining_enabled` | `"0"` | Opt-in deterministic convention mining into `AGENTS.md` |
702
- | `conflict_detection_enabled` | `"0"` | Opt-in contradiction detection between memories and observed facts |
703
- | `error_lesson_mode` | `"all"` | Which failures produce lessons: `all` / `rules_only` / `patterns_only` |
704
- | `shared_layer_enabled` | `"0"` | Master switch for the shared layer. When `"0"`, Kevin never reads or writes the OKF file and `session.idle` performs no filesystem access. Compare with `=== "1"` — a truthiness check would turn the feature on for every installation |
705
- | `okf_path` | `".kevin/knowledge.okf"` | Project-relative path of the committed OKF file (**string** — always truthy, which is exactly why the layer flag above must not be read as a boolean) |
706
- | `share_requires_approval` | `"1"` | When `"1"`, `kevin_share` writes only with `confirm: true`; un-curated memories are refused with `not_curated` |
707
- | `author_identity_mode` | `"hashed"` | Author attribution in exports: `"hashed"` or `"none"` (**string** enum) |
708
- | `shared_confidence_floor` | `"0.7"` | Gate for `kevin_share` selection and export — a **string**, read with `Number.parseFloat` and clamped to `[0, 1]`. Deliberately stricter than `injection_confidence_floor` (`0.6`): sharing is a commitment, injecting is a suggestion. `parseInt` on this setting would yield `0` and share everything |
709
- | `hook_liveness_enabled` | `"1"` | Master switch for the liveness instrument. When `"0"`, `HookLiveness.wrap()` returns hooks untouched and no counters are recorded — compare with `=== "1"` |
710
- | `native_registration_enabled` | `"0"` | Opt-in for the additive v2 attachment (`skill.transform`/`reference.transform` via `define()`). Default `"0"` keeps the release byte-identical to v0.8.0; `kevin_native enable` sets `"1"` (TEXT) but the probe is frozen — restart required (D9-01) |
711
- | `host_probe_history_enabled` | `"0"` | When `"1"`, one `host_probes` row per construction is appended (version, flavour, v2 flags, notes). Off by default because it is unbounded append-only |
712
- | `dead_hook_report_threshold` | `"3"` | How many checkpointed sessions without a hook firing before it is `dead`. TEXT holding a number, read with `Number.parseInt(v, 10)`, clamped to `[1, 1000]`, `NaN` → `3` |
713
-
714
- All 27 settings are TEXT values; booleans and flags compare against `"1"`; the three v0.8 **string-valued** settings are `okf_path`, `author_identity_mode` and `shared_confidence_floor`, and `dead_hook_report_threshold` is a numeric string.
715
-
716
- ---
717
-
718
- ## Development
719
-
720
- ```bash
721
- git clone https://github.com/jmtrin/opencode-kevin.git
722
- cd opencode-kevin
723
- npm install
724
- npm run typecheck # tsc --noEmit (strict)
725
- npm run lint # biome check .
726
- npm test # vitest run (unit + integration + e2e + replay)
727
- npm run verify # post-install verification (also checks DB migrations)
728
- npm run replay # replay report over tests/replay/fixtures
729
- ```
730
-
731
- ### Quick diagnostic
732
-
733
- ```bash
734
- # One read-only command that tells you whether Kevin is healthy:
735
- # host surface, every hook's state, dependency check, and native registration.
736
- kevin_doctor({})
737
- # → { host: { flavour: "v1+v2", v2: { skill: true, reference: true } },
738
- # hooks: [{ hook: "experimental.chat.system.transform", state: "dead", ... }],
739
- # verdict: "degraded", reason: "experimental.chat.system.transform dead since ..." }
740
-
741
- # Native registration is opt-in (default off). Enable, restart, check again:
742
- kevin_native({ action: "enable" }) # writes native_registration_enabled="1" (TEXT), no re-probe
743
- kevin_native({ action: "show" }) # → { effective: false, reason: "v2 subpath absent …" } on 1.17.x
744
- ```
745
-
746
- ### Publishing (maintainer)
747
-
748
- ```bash
749
- npm login # as the jmtrin account that owns the @jmtrin scope
750
- npm publish --access public
751
- ```
752
-
753
- `prepublishOnly` runs `npm run build` (tsc + copy migrations) automatically. The `files` field ships only `dist/plugin`, `dist/migrations`, and `migrations`. `dist/` is gitignored and rebuilt on publish.
754
-
755
- ### Structure
756
-
757
- ```
758
- plugin/
759
- index.ts # Entry point: KevinPlugin (wires hooks, tools, emissions)
760
- Store.ts # SQLite wrapper (node:sqlite / bun:sqlite / better-sqlite3 fallback)
761
- sqlite-adapter.ts # Runtime-agnostic SQLite adapter behind Store
762
- Migrate.ts # Idempotent migrations + post-apply hooks
763
- MemoryService.ts # save/query/getRelevant (FTS5 + bm25 + origin-aware rank + supersede)
764
- ToolCallObserver.ts # onBefore/onAfter + redact + inferErrorType + dedup (opt-in)
765
- Reflector.ts # Heuristic lessons + per-fingerprint throttle + LLM enrich (opt-in)
766
- ContextInjector.ts # deriveQuery + pre-prompt/compacting injection + <kevin-suggestion>
767
- Retrospective.ts # Generates retrospective.md + FP recap + metrics snapshot
768
- Feedback.ts # kevin_feedback: verdicts, confidence terms, ignored stamp
769
- Archiver.ts # Retires stale non-pattern memories past archive_after_days
770
- CausalChain.ts # Links fixes to failures + promotes causal patterns
771
- QualityGate.ts # Weak-lesson gate (stored, not injected by default)
772
- InjectionLedger.ts # Injection ledger + settle → precision_rate
773
- LessonFixer.ts # Deterministic fix_args capture + promotion enrichment
774
- PatternMiner.ts # Opt-in deterministic 2-gram/3-gram miner
775
- Curator.ts # Curation candidates + propose/approve lifecycle
776
- ArtifactWriter.ts # The SINGLE write path (markers, atomic, noop, audit rows)
777
- Materializer.ts # Pull-channel topic bundles (skills, refs)
778
- inferability.ts # Deterministic inferable/non-inferable/unknown classifier
779
- capabilities.ts # v2 domain probe (skills / references)
780
- diff.ts # Minimal unified diff for proposal review
781
- replay.ts # Hermetic replay driver over recorded transcripts
782
- replay-types.ts # Transcript/result types for the replay harness
783
- kevin_propose.ts # kevin_propose tool (strict dry run)
784
- kevin_approve.ts # kevin_approve tool (only writer call site)
785
- kevin_publish.ts # kevin_publish tool (bundle regeneration)
786
- kevin_audit.ts # Read-only audit + channels/curation blocks
787
- kevin_why.ts # kevin_why tool: failure→fix traces + related rules
788
- SharedLayer.ts # v0.8: OKF plan/apply/tombstone + import → shared projections
789
- RepoIdentity.ts # v0.8: repo_id resolution (declared → remote → path) + rekey
790
- okf.ts # v0.8: OKF v2 parse/serialize (headers, entry_id, limits)
791
- okf-export.ts # kevin_export: OKF/markdown export
792
- okf-import.ts # kevin_import: bundle parser + import
793
- confidence.ts # Two-sided computeConfidence (evidence + recurrence + feedback)
794
- query-tokenizer.ts # FTS5 tokenizer for query sanitization
795
- memory-format.ts # escapeInjectedText, formatMemories, <protect> + id: line wrappers
796
- redact.ts # redactPaths + stripPrivate
797
- fingerprint.ts # FNV-1a 64-bit (in-house, no node:crypto)
798
- metrics.ts # In-memory counters + debounced flush to kevin_metrics
799
- uuid.ts # UUIDv7
800
- migrations/
801
- 001_initial.sql # schema: memories, tool_calls, retrospectives
802
- 002_indexes.sql # FTS5 + indexes
803
- 003_v02_signal.sql # fingerprint, origin, metrics, dedup indexes
804
- 004_v03_knowledge.sql # evidence/status/supersede, error_fingerprint
805
- 005_v04_signal.sql # recurrence_count, fix_args, last_injected_at
806
- 006_v05_glassbox.sql # ignored/archived/superseded_by, feedback, metrics
807
- 007_v06_pull.sql # curation_proposals, artifact_writes, curated/inferable
808
- 008_v07_truth.sql # facts + contradictions, conflict_detection settings
809
- 009_v08_team.sql # shared_entries, okf_imports, the five v0.8 settings
810
- tests/
811
- unit/ # component tests
812
- integration/ # tool-level tests through real components
813
- e2e/ # closed-loop tests through the host hooks
814
- replay/ # transcript fixtures + replay harness tests
815
- scripts/
816
- copy-migrations.mjs # build step: copies *.sql to dist/migrations
817
- verify-install.ts # npm run verify
818
- ```
819
-
820
- ---
821
-
822
- ## License
823
-
824
- MIT
293
+
294
+ ## 📊 The benchmark: proven, not promised
295
+
296
+ Kevin ships a committed synthetic corpus and a four-arm harness measuring
297
+ whether real retrieval beats trivial baselines at surfacing labelled-relevant
298
+ memories:
299
+
300
+ ```
301
+ bench/corpus/
302
+ seed: 1262835273 (xorshift32, byte-for-byte regenerable)
303
+ digest: adecbdf4c7af82e2
304
+ result: bench/results/2026-08-21-adecbdf4c7af82e2.json (k = 5)
305
+ ```
306
+
307
+ ```
308
+ npm run bench # run the harness (also persists one row per arm)
309
+ npm run bench:check # gate: every perf scope within its p95 budget
310
+ npm run verify:pack # gate: seven properties against the packed tarball
311
+ ```
312
+
313
+ | Arm | Precision@5 | Recall@5 | MRR |
314
+ |---|---:|---:|---:|
315
+ | `none` (control) | 0.000 | 0.000 | 0.000 |
316
+ | `recent-k` (baseline) | 0.050 | 0.026 | 0.109 |
317
+ | `random-k` (floor) | 0.048 | 0.028 | 0.093 |
318
+ | **`kevin`** | **0.950** | **0.546** | **1.000** |
319
+
320
+ ```
321
+ Precision@5 Recall@5 MRR
322
+ 0.950 ██████████████████░░ 0.546 ██████████░░░░ 1.000 ████████████████████
323
+ 0.050 █░░░░░░░░░░░░░░░░░░░ 0.026 █░░░░░░░░░░░░░ 0.109 ██░░░░░░░░░░░░░░░░░░
324
+ ```
325
+
326
+ The labelling rule is mechanical and the retrieval numbers are exactly
327
+ reproducible — asserted by running the harness twice in-process.
328
+
329
+ **Honest limits, stated up front.** This benchmark measures retrieval on a
330
+ synthetic corpus built to have a ranked answer; it does **not** prove that real sessions look like this synthetic corpus. It does not prove that a surfaced memory changed what the model did. Retrieval quality is one layer of the agent loop, not the whole of it.
331
+
332
+ ---
333
+
334
+ ## ✍️ Curation: from session noise to AGENTS.md
335
+
336
+ Knowledge worth keeping becomes a proposal; a human decides; only then is it
337
+ written — once, atomically, inside a marker pair you can edit around:
338
+
339
+ ```
340
+ memories ──▶ kevin_propose ──▶ pending proposals (unified diffs, no writes)
341
+
342
+ HUMAN REVIEW
343
+
344
+ ┌── approve ────┴──── reject ──▶ decision recorded
345
+ ▼ nothing written
346
+ AGENTS.md updated atomically ◀── the single write path (D6-01)
347
+ ```
348
+
349
+ ```markdown
350
+ <!-- kevin:begin — curated by opencode-kevin, safe to edit -->
351
+ - Always run `npm run typecheck` before committing (fixed 3 CI failures)
352
+ <!-- kevin:end -->
353
+ ```
354
+
355
+ Deliberately conservative: only non-inferable memories are eligible — an
356
+ LLM-recoverable diagnostic is not something a human should have to review into
357
+ a permanent rule. Re-applying an unchanged plan is a counted noop, never a
358
+ write. **Kevin can propose. Humans decide.**
359
+
360
+ ---
361
+
362
+ ## 👥 Team sharing: one file, zero servers
363
+
364
+ Opt-in via `shared_layer_enabled='1'`. Curated knowledge exports to one
365
+ `.kevin/knowledge.okf`header lines plus one JSON entry per line, sorted by a
366
+ deterministic `entry_id`, LF-only, ≤4096 bytes per line designed so git
367
+ merges are meaningful and conflicts are parseable:
368
+
369
+ ```
370
+ DEV A DEV B
371
+ share (approval-gated) │ git pull
372
+ ▼ ▼
373
+ knowledge.okf ◀═══════ git ══════▶ knowledge.okf
374
+ │ kevin_sync
375
+
376
+ projected into local memories
377
+ (layer='shared', immutable)
378
+ ```
379
+
380
+ Repository identity resolves **once**, in order:
381
+ `.kevin/project.json` (declared) git remote hash (never a raw URL) → path.
382
+ Two clones of the same repository are one team; different repositories never
383
+ leak into each other. Sharing requires explicit approval
384
+ (`share_requires_approval='1'`), author identity is hashed by default, and
385
+ tombstones archive rather than delete.
386
+
387
+ ---
388
+
389
+ ## 📜 The contract
390
+
391
+ Kevin 1.x makes promises about its published surface **in writing**.
392
+ [`docs/CONTRACT.md`](docs/CONTRACT.md) freezes nine clauses `C-01` … `C-09` —
393
+ from the `AGENTS.md` marker bytes to the database schema, each tagged `frozen`
394
+ or `forward-only` and stamped with the release that incurred the obligation. A
395
+ test diffs the live contract against an append-only golden file on every run:
396
+ removals and silent changes fail loudly; additions must carry `since`.
397
+
398
+ ```jsonc
399
+ // kevin_contract (excerpt)
400
+ {
401
+ "contract_version": 1,
402
+ "digest": "1de9740bba2e9f95",
403
+ "clauses": [
404
+ { "id": "C-03", "title": "Tool names and argument shapes", "stability": "frozen", "since": "0.2.0" },
405
+ { "id": "C-07", "title": "Database schema", "stability": "forward-only", "since": "0.1.0" }
406
+ ]
407
+ }
408
+ ```
409
+
410
+ > 1.0.0 is not just a version number — it is where Kevin starts making explicit, testable promises about its surface.
411
+
412
+ ---
413
+
414
+ ## ⏱️ Hooks & latency budgets
415
+
416
+ Six host hooks plus Kevin's own `dispose` checkpoint — eight measured scopes,
417
+ each with a declared p95/max budget enforced by `npm run bench:check`:
418
+
419
+ | Scope | p95 budget | max |
420
+ |---|---:|---:|
421
+ | `tool.execute.before` | 2 ms | 10 ms |
422
+ | `tool.execute.after` | 5 ms | 25 ms |
423
+ | `chat.message` | 2 ms | 10 ms |
424
+ | `chat.system.transform` | 15 ms | 50 ms |
425
+ | `session.compacting` | 15 ms | 50 ms |
426
+ | `event` | 5 ms | 25 ms |
427
+ | `session.idle` | 150 ms | 600 ms |
428
+ | `dispose` | 50 ms | 250 ms |
429
+
430
+ Measured on the reference laptop: retrieval p50 ≈ 0.2 ms, p95 < 1 ms — orders
431
+ of magnitude under budget. Samples persist to `perf_samples` at idle; a breach
432
+ degrades `kevin_doctor`'s verdict, because a plugin that is technically alive
433
+ but consistently slow is not healthy.
434
+
435
+ ---
436
+
437
+ ## ⚙️ Configuration
438
+
439
+ Everything is a TEXT setting managed through `kevin_config` (or any SQLite
440
+ client). All values are TEXT — flags compare with `=== "1"`, never truthiness.
441
+
442
+ | Key | Default | Purpose |
443
+ |---|---|---|
444
+ | `quality_gate_enabled` | `'1'` | Weak lessons stored but never injected |
445
+ | `lesson_snippet_injection` | `'1'` | Compact 2-line snippets instead of full bodies |
446
+ | `patternminer_enabled` | `'0'` | Deterministic tool-sequence mining (≥5 sessions) |
447
+ | `cross_project_enabled` | `'0'` | Include imported cross-project rows |
448
+ | `llm_reflection_enabled` | `'0'` | Opt-in LLM enrichment at pattern promotion |
449
+ | `tool_calls_dedup_enabled` | `'0'` | Suppress duplicate call recordings per minute bucket |
450
+ | `deterministic_retrieval` | `'0'` | Freeze the clock for hermetic tests/replay |
451
+ | `pre_prompt_budget_tokens` | `'400'` | Pre-prompt injection cap (clamped `[0, 4000]`) |
452
+ | `archive_after_days` | `'30'` | Age threshold for idle archival |
453
+ | `curation_enabled` | `'1'` | Idle dry-run proposal generation |
454
+ | `agents_md_path` | `'AGENTS.md'` | Where curated knowledge lands |
455
+ | `skill_emission_enabled` | `'0'` | Register project-knowledge skill on v2 hosts |
456
+ | `reference_emission_enabled` | `'0'` | Register `@kevin/<topic>` mentions |
457
+ | `injection_confidence_floor` | `'0.6'` | Memories below this never inject |
458
+ | `repo_truth_enabled` | `'0'` | Repository fact scanning |
459
+ | `convention_mining_enabled` | `'0'` | Deterministic convention mining |
460
+ | `conflict_detection_enabled` | `'0'` | Contradiction surfacing |
461
+ | `error_lesson_mode` | `'all'` | Error lesson injection mode |
462
+ | `shared_layer_enabled` | `'0'` | Team sharing via `.kevin/knowledge.okf` |
463
+ | `okf_path` | `'.kevin/knowledge.okf'` | Shared file location |
464
+ | `share_requires_approval` | `'1'` | No export without human confirmation |
465
+ | `author_identity_mode` | `'hashed'` | Author identity hashed, never raw email |
466
+ | `shared_confidence_floor` | `'0.7'` | Confidence floor for shared projections |
467
+ | `hook_liveness_enabled` | `'1'` | Per-hook liveness tracking |
468
+ | `native_registration_enabled` | `'0'` | v2 native skill/reference registration |
469
+ | `host_probe_history_enabled` | `'0'` | Append-only probe history |
470
+ | `dead_hook_report_threshold` | `'3'` | Consecutive misses before a hook reads dead |
471
+ | `perf_enabled` | `'1'` | Latency instrumentation |
472
+ | `perf_ring_capacity` | `'512'` | Samples per scope (clamped `[64, 8192]`) |
473
+ | `perf_flush_on_idle` | `'1'` | Persist samples at idle |
474
+ | `contract_report_enabled` | `'1'` | Contract block in `kevin_audit` |
475
+
476
+ ---
477
+
478
+ ## 🖥️ Supported runtimes
479
+
480
+ | Runtime | SQLite backend | Status |
481
+ |---|---|---|
482
+ | Node 24.x | `node:sqlite` (stable) | ✅ **Supported** the reference row |
483
+ | Node 22.5+ | `better-sqlite3` (optional dep) | ⚠️ **Supported with a caveat** — needs a build toolchain; without one there is no backend (and npm install still succeeds silently) |
484
+ | Node 22.5+ | `node:sqlite` behind `--experimental-sqlite` | 🔶 Works, unsupported — exercised in CI, not promised |
485
+ | Bun 1.1 | `bun:sqlite` | **Supported** smoke-tested in `npm run verify` |
486
+
487
+ Zero process spawns, zero network calls — asserted by source scan on every
488
+ test run.
489
+
490
+ ---
491
+
492
+ ## 🎨 Design & trust
493
+
494
+ **Principles.** Local first · deterministic by default (no LLM in the core
495
+ loop) · evidence over vibes (every memory carries evidence, provenance,
496
+ recurrence and confidence) · signal over noise (inject *less, better* context,
497
+ not more) · humans hold the write boundary · git-friendly collaboration ·
498
+ important claims become executable checks.
499
+
500
+ **Security model.** Stored knowledge is treated as untrusted input: anything
501
+ reaching an artifact or prompt passes through idempotent escaping at the
502
+ single write path; permanent project-file changes require explicit human
503
+ approval; author identity in the shared layer is hashed
504
+ (`author_identity_mode='hashed'`). No network service is required to store,
505
+ retrieve or share knowledge.
506
+
507
+ **Without memory vs with Kevin:**
508
+
509
+ ```
510
+ without: session 1 solve ─▶ context closes ─▶ session 2 same problem ─▶ solve again
511
+
512
+ with: session 1 solve ─▶ lesson ─▶ memory ─┐
513
+ session 2 problem ◀── recall ◀───────┘ ─▶ fix ─▶ evidence ↑
514
+ ```
515
+
516
+ **Why not just `AGENTS.md`?** It is excellent for durable, human-authored
517
+ guidance Kevin treats it as a curated destination, not as a memory system.
518
+ It cannot do failure/fix traces, confidence scoring, evidence tracking, ranked
519
+ retrieval, recurrence signals, dry-run injection inspection, automatic
520
+ proposal generation, team projections, or performance and contract
521
+ instrumentation. That is what Kevin adds around it.
522
+
523
+ ---
524
+
525
+ ## 🛠️ Development
526
+
527
+ ```bash
528
+ npm install
529
+ npm run typecheck # tsc --noEmit (strict)
530
+ npm run lint # biome
531
+ npm test # vitest — 1374 tests across 190 files
532
+ npm run verify # install checks + Bun smoke + verify:pack
533
+ npm run gen:corpus # regenerate the seeded corpus (byte-identical)
534
+ npm run replay # replay recorded sessions deterministically
535
+ ```
536
+
537
+ Project layout:
538
+
539
+ ```
540
+ plugin/ 51 modules — Store, MemoryService, Reflector, Perf, Contract, …
541
+ migrations/ 001 → 011 — additive, idempotent, forward-only forever
542
+ scripts/ bench · gen-corpus · verify-pack · verify-install · …
543
+ tests/ unit · integration · e2e · replay fixtures
544
+ bench/ committed corpus + committed results
545
+ docs/ CONTRACT.md · per-release plans/tasks · roadmap
546
+ ```
547
+
548
+ ---
549
+
550
+ ## 📄 License
551
+
552
+ MIT — see the package manifest. Kevin is built by [jmtrin](https://github.com/jmtrin);
553
+ bug reports and PRs welcome at the [issue tracker](https://github.com/jmtrin/opencode-kevin/issues).
554
+
555
+ <div align="center">
556
+
557
+ ### Kevin *Observe. Learn. Remember. Improve.*
558
+
559
+ </div>