@jmtrin/opencode-kevin 0.7.0 → 0.9.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 (70) hide show
  1. package/README.md +254 -12
  2. package/dist/migrations/009_v08_team.sql +100 -0
  3. package/dist/migrations/010_v09_native.sql +78 -0
  4. package/dist/plugin/ArtifactWriter.d.ts +28 -0
  5. package/dist/plugin/ArtifactWriter.js +35 -2
  6. package/dist/plugin/ArtifactWriter.js.map +1 -1
  7. package/dist/plugin/ContextInjector.d.ts +9 -0
  8. package/dist/plugin/ContextInjector.js +14 -3
  9. package/dist/plugin/ContextInjector.js.map +1 -1
  10. package/dist/plugin/Curator.d.ts +22 -2
  11. package/dist/plugin/Curator.js +56 -14
  12. package/dist/plugin/Curator.js.map +1 -1
  13. package/dist/plugin/HookLiveness.d.ts +81 -0
  14. package/dist/plugin/HookLiveness.js +324 -0
  15. package/dist/plugin/HookLiveness.js.map +1 -0
  16. package/dist/plugin/InjectionLedger.d.ts +6 -0
  17. package/dist/plugin/InjectionLedger.js +6 -0
  18. package/dist/plugin/InjectionLedger.js.map +1 -1
  19. package/dist/plugin/Materializer.d.ts +29 -5
  20. package/dist/plugin/Materializer.js +48 -16
  21. package/dist/plugin/Materializer.js.map +1 -1
  22. package/dist/plugin/MemoryService.d.ts +34 -5
  23. package/dist/plugin/MemoryService.js +215 -7
  24. package/dist/plugin/MemoryService.js.map +1 -1
  25. package/dist/plugin/Migrate.d.ts +1 -0
  26. package/dist/plugin/Migrate.js +74 -3
  27. package/dist/plugin/Migrate.js.map +1 -1
  28. package/dist/plugin/RepoIdentity.d.ts +124 -0
  29. package/dist/plugin/RepoIdentity.js +301 -0
  30. package/dist/plugin/RepoIdentity.js.map +1 -0
  31. package/dist/plugin/Retrospective.js +18 -0
  32. package/dist/plugin/Retrospective.js.map +1 -1
  33. package/dist/plugin/SharedLayer.d.ts +159 -0
  34. package/dist/plugin/SharedLayer.js +463 -0
  35. package/dist/plugin/SharedLayer.js.map +1 -0
  36. package/dist/plugin/host.d.ts +70 -0
  37. package/dist/plugin/host.js +251 -0
  38. package/dist/plugin/host.js.map +1 -0
  39. package/dist/plugin/index.d.ts +30 -1
  40. package/dist/plugin/index.js +582 -12
  41. package/dist/plugin/index.js.map +1 -1
  42. package/dist/plugin/kevin_approve.js +7 -4
  43. package/dist/plugin/kevin_approve.js.map +1 -1
  44. package/dist/plugin/kevin_audit.d.ts +48 -1
  45. package/dist/plugin/kevin_audit.js +179 -3
  46. package/dist/plugin/kevin_audit.js.map +1 -1
  47. package/dist/plugin/kevin_doctor.d.ts +57 -0
  48. package/dist/plugin/kevin_doctor.js +168 -0
  49. package/dist/plugin/kevin_doctor.js.map +1 -0
  50. package/dist/plugin/kevin_native.d.ts +29 -0
  51. package/dist/plugin/kevin_native.js +80 -0
  52. package/dist/plugin/kevin_native.js.map +1 -0
  53. package/dist/plugin/metrics.d.ts +1 -1
  54. package/dist/plugin/metrics.js +9 -0
  55. package/dist/plugin/metrics.js.map +1 -1
  56. package/dist/plugin/native.d.ts +92 -0
  57. package/dist/plugin/native.js +191 -0
  58. package/dist/plugin/native.js.map +1 -0
  59. package/dist/plugin/okf-export.d.ts +2 -2
  60. package/dist/plugin/okf-export.js +15 -7
  61. package/dist/plugin/okf-export.js.map +1 -1
  62. package/dist/plugin/okf.d.ts +107 -0
  63. package/dist/plugin/okf.js +304 -0
  64. package/dist/plugin/okf.js.map +1 -0
  65. package/dist/plugin/replay-types.d.ts +29 -287
  66. package/dist/plugin/replay-types.js +145 -53
  67. package/dist/plugin/replay-types.js.map +1 -1
  68. package/migrations/009_v08_team.sql +100 -0
  69. package/migrations/010_v09_native.sql +78 -0
  70. package/package.json +2 -3
package/README.md CHANGED
@@ -10,7 +10,7 @@ Kevin is an [OpenCode](https://opencode.ai) plugin that **observes** every agent
10
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
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
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.
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
14
  - **Standalone**: works without any other plugin. With the ecosystem, it learns more richly.
15
15
 
16
16
  ---
@@ -22,6 +22,7 @@ Kevin is an [OpenCode](https://opencode.ai) plugin that **observes** every agent
22
22
  - [Tools](#tools)
23
23
  - [How Kevin measures itself](#how-kevin-measures-itself)
24
24
  - [Curation & Pull](#curation--pull)
25
+ - [The shared layer (v0.8.0)](#the-shared-layer-v080)
25
26
  - [Replay harness](#replay-harness)
26
27
  - [Hooks](#hooks)
27
28
  - [Configuration](#configuration)
@@ -50,7 +51,7 @@ For a **single project**, put the same `plugin` array in `./opencode.json` or `.
50
51
 
51
52
  ### 2. Restart OpenCode
52
53
 
53
- Config is loaded once at startup and is **not hot-reloaded** — quit and reopen OpenCode after editing. On start, Kevin exposes 18 tools, including `kevin_facts` and `kevin_conflicts`.
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`.
54
55
 
55
56
  Contradictions de-rank memories and surface conflicts. They never delete, stale, archive, or auto-resolve a memory.
56
57
 
@@ -138,13 +139,24 @@ Every tool call is observed; every failure becomes a lesson; every lesson is eit
138
139
  └─────────────────────────┘ promotion, pattern mining (opt-in)
139
140
  ```
140
141
 
141
- At `session.idle` Kevin also settles injection outcomes, retires stale memories, and — when curation is enabled — drafts pull proposals for your review (see [Curation & Pull](#curation--pull)).
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.
142
154
 
143
155
  ---
144
156
 
145
157
  ## Tools
146
158
 
147
- Kevin exposes 16 tools callable by the agent.
159
+ Kevin exposes 23 tools callable by the agent.
148
160
 
149
161
  ### `kevin_save`
150
162
 
@@ -193,13 +205,13 @@ kevin_recall({ query: "auth", limit: 3 })
193
205
 
194
206
  ### `kevin_status`
195
207
 
196
- 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).
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.
197
209
 
198
210
  ```
199
211
  kevin_status({})
200
212
  // → { "memories": 42, "memories_reflector": 12, "memories_agent": 30,
201
- // "memories_pattern": 0, "memories_causal": 1, "tool_calls": 318,
202
- // "retrospectives": 7, "tool_count": 16,
213
+ // "memories_pattern": 0, "memories_causal": 1, "tool_calls": 318,
214
+ // "retrospectives": 7, "tool_count": 23,
203
215
  // "metrics": { "tokens_injected_pre_prompt": 51, "tokens_injected_compacting": 0,
204
216
  // "reflections_throttled": 3, "duplicate_suppressions": 2,
205
217
  // "tool_calls_deduped": 0, "patterns_mined": 0,
@@ -214,7 +226,9 @@ kevin_status({})
214
226
  // "patterns_promoted_new": 2, "recurrence_by_origin": { "reflector": 3, "causal": 1 },
215
227
  // "v06": { "schema_version": "007", "curation_enabled": "1",
216
228
  // "skill_emission": "off", "reference_emission": "off",
217
- // "proposals_pending": 2 } }
229
+ // "proposals_pending": 2 },
230
+ // "v08": { "repo_id": "2114ad162af50a25", "identity_source": "remote",
231
+ // "shared_layer_enabled": "0", "shared_entries": 3 } }
218
232
  ```
219
233
 
220
234
  ### `kevin_retrospective`
@@ -340,7 +354,75 @@ kevin_approve({ proposal_id: "...", decision: "approve" }) // or "reject"
340
354
 
341
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.
342
356
 
343
- ---
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
+
425
+ ---
344
426
 
345
427
  ## How Kevin measures itself
346
428
 
@@ -425,6 +507,131 @@ Rejection history is never deleted: it is the evidence base for the roadmap's ki
425
507
 
426
508
  ---
427
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 file — it 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
+
428
635
  ## Replay harness
429
636
 
430
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.
@@ -442,7 +649,7 @@ Kevin subscribes to 6 OpenCode hooks:
442
649
  | `experimental.chat.system.transform` | Injects relevant lessons in `<kevin-context>` (400 tokens by default, configurable) + optional `<kevin-suggestion>` |
443
650
  | `experimental.session.compacting` | Re-injects lessons in `<kevin-memory>` after compacting (2000 tokens) + optional `<kevin-suggestion>` |
444
651
  | `event` (`session.created`) | Captures current `sessionID` (skill/reference emissions register at plugin startup, not per session) |
445
- | `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`); flushes metrics |
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 |
446
653
 
447
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>`.
448
655
 
@@ -472,7 +679,7 @@ KevinPlugin(input, {
472
679
 
473
680
  ### Settings
474
681
 
475
- Read/write via `kevin_config({ action: "list" | "set", ... })`. All values are TEXT; booleans compare against `"1"`.
682
+ Read/write via `kevin_config({ action: "list" | "set", ... })`. All 27 values are TEXT; booleans compare against `"1"`.
476
683
 
477
684
  | Setting | Default | Effect |
478
685
  |---|---|---|
@@ -490,6 +697,21 @@ Read/write via `kevin_config({ action: "list" | "set", ... })`. All values are T
490
697
  | `skill_emission_enabled` | `"0"` | Registers the curated skill with the host at startup (v2 hosts only) |
491
698
  | `reference_emission_enabled` | `"0"` | Registers `@kevin/<topic>` references at startup (v2 hosts only) |
492
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.
493
715
 
494
716
  ---
495
717
 
@@ -502,10 +724,25 @@ npm install
502
724
  npm run typecheck # tsc --noEmit (strict)
503
725
  npm run lint # biome check .
504
726
  npm test # vitest run (unit + integration + e2e + replay)
505
- npm run verify # post-install verification
727
+ npm run verify # post-install verification (also checks DB migrations)
506
728
  npm run replay # replay report over tests/replay/fixtures
507
729
  ```
508
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
+
509
746
  ### Publishing (maintainer)
510
747
 
511
748
  ```bash
@@ -548,6 +785,9 @@ plugin/
548
785
  kevin_publish.ts # kevin_publish tool (bundle regeneration)
549
786
  kevin_audit.ts # Read-only audit + channels/curation blocks
550
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)
551
791
  okf-export.ts # kevin_export: OKF/markdown export
552
792
  okf-import.ts # kevin_import: bundle parser + import
553
793
  confidence.ts # Two-sided computeConfidence (evidence + recurrence + feedback)
@@ -565,6 +805,8 @@ migrations/
565
805
  005_v04_signal.sql # recurrence_count, fix_args, last_injected_at
566
806
  006_v05_glassbox.sql # ignored/archived/superseded_by, feedback, metrics
567
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
568
810
  tests/
569
811
  unit/ # component tests
570
812
  integration/ # tool-level tests through real components
@@ -0,0 +1,100 @@
1
+ -- ============================================================
2
+ -- Kevin 0.8.0 - Migration 009: Team (additive)
3
+ -- ============================================================
4
+ -- Backward-compatible, additive only. All new columns are
5
+ -- nullable or carry a NOT NULL DEFAULT so legacy rows keep
6
+ -- working without a destructive rebuild.
7
+ --
8
+ -- Scope note: this migration introduces `repo_id`, a SECOND
9
+ -- scoping dimension. `project_id` is retained on every table,
10
+ -- unchanged, as local-path provenance (D8-02). Nothing that
11
+ -- reads `project_id` today stops working.
12
+ -- ============================================================
13
+
14
+ -- 1. shared_entries: the local projection of the committed OKF file.
15
+ -- One row per (repo_id, entry_id). Rewritten by SharedLayer.import(),
16
+ -- never edited by hand, never the source of truth - the file is.
17
+ -- No REFERENCES to memories: an entry may arrive from a teammate
18
+ -- before any local memory corresponds to it (D8-12).
19
+ CREATE TABLE IF NOT EXISTS shared_entries (
20
+ id TEXT PRIMARY KEY,
21
+ repo_id TEXT NOT NULL,
22
+ entry_id TEXT NOT NULL,
23
+ type TEXT NOT NULL CHECK (type IN ('decision', 'rule', 'pattern', 'solution')),
24
+ statement TEXT NOT NULL,
25
+ scope TEXT,
26
+ confidence REAL NOT NULL DEFAULT 0.0,
27
+ evidence INTEGER NOT NULL DEFAULT 0,
28
+ origin TEXT NOT NULL DEFAULT 'shared',
29
+ author_hash TEXT,
30
+ op TEXT NOT NULL CHECK (op IN ('assert', 'tombstone')) DEFAULT 'assert',
31
+ supersedes TEXT,
32
+ created_at TEXT NOT NULL,
33
+ imported_at TEXT NOT NULL DEFAULT (datetime('now'))
34
+ );
35
+
36
+ -- 1b. Identity is (repo_id, entry_id). The UNIQUE index is what makes
37
+ -- import() an idempotent upsert instead of an append.
38
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_shared_entries
39
+ ON shared_entries(repo_id, entry_id);
40
+ CREATE INDEX IF NOT EXISTS idx_shared_entries_op
41
+ ON shared_entries(op);
42
+ CREATE INDEX IF NOT EXISTS idx_shared_entries_type
43
+ ON shared_entries(type);
44
+
45
+ -- 2. okf_imports: append-only audit of every read of the shared file,
46
+ -- including no-op reads and refusals. `file_hash` drives the skip
47
+ -- path in SharedLayer.import() (D8-14).
48
+ CREATE TABLE IF NOT EXISTS okf_imports (
49
+ id TEXT PRIMARY KEY,
50
+ repo_id TEXT NOT NULL,
51
+ path TEXT NOT NULL,
52
+ file_hash TEXT,
53
+ entries_parsed INTEGER NOT NULL DEFAULT 0,
54
+ entries_folded INTEGER NOT NULL DEFAULT 0,
55
+ entries_rejected INTEGER NOT NULL DEFAULT 0,
56
+ skipped INTEGER NOT NULL DEFAULT 0,
57
+ imported_at TEXT NOT NULL DEFAULT (datetime('now'))
58
+ );
59
+
60
+ CREATE INDEX IF NOT EXISTS idx_okf_imports_repo
61
+ ON okf_imports(repo_id, imported_at);
62
+
63
+ -- 3. memories: the second scoping dimension and the layer marker.
64
+ -- repo_id is NULLABLE and back-filled by the post-apply hook, not by
65
+ -- a DEFAULT: the value depends on the row's existing project_id and
66
+ -- SQLite cannot express that in a column default.
67
+ -- layer carries NO CHECK constraint - widening it later would force
68
+ -- the migration-004 rebuild path (D8-07). Enforced in TypeScript.
69
+ ALTER TABLE memories ADD COLUMN repo_id TEXT;
70
+ ALTER TABLE memories ADD COLUMN layer TEXT NOT NULL DEFAULT 'local';
71
+ ALTER TABLE memories ADD COLUMN shared_entry_id TEXT;
72
+
73
+ CREATE INDEX IF NOT EXISTS idx_memories_repo_id
74
+ ON memories(repo_id, status);
75
+ CREATE INDEX IF NOT EXISTS idx_memories_layer
76
+ ON memories(layer);
77
+
78
+ -- 4. kevin_metrics: seed the six v0.8 counters (33 -> 39).
79
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
80
+ ('shared_entries_total', 0),
81
+ ('shared_entries_imported', 0),
82
+ ('shared_entries_exported', 0),
83
+ ('okf_merge_folds', 0),
84
+ ('rekey_events', 0),
85
+ ('injections_from_shared', 0);
86
+
87
+ -- 5. kevin_settings: seed the five v0.8 flags (18 -> 23).
88
+ -- shared_layer_enabled defaults OFF: this release must be opted into,
89
+ -- because its first side effect is a new file in the user's repository.
90
+ -- shared_confidence_floor (0.7) is deliberately STRICTER than
91
+ -- injection_confidence_floor (0.6) - see 5.7.
92
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
93
+ ('shared_layer_enabled', '0'),
94
+ ('okf_path', '.kevin/knowledge.okf'),
95
+ ('share_requires_approval', '1'),
96
+ ('author_identity_mode', 'hashed'),
97
+ ('shared_confidence_floor', '0.7');
98
+
99
+ -- 6. Seed version 009.
100
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('009');
@@ -0,0 +1,78 @@
1
+ -- ============================================================
2
+ -- Kevin v0.9.0 "Native" — host surface and hook liveness
3
+ -- Migration 010. Additive only. No table rebuild.
4
+ -- ============================================================
5
+
6
+ -- 1. Hook liveness. Deliberately NOT project-scoped: a hook is a
7
+ -- property of the host binary, not of a checkout. One row per
8
+ -- hook name, updated in place. See D9-08.
9
+ CREATE TABLE IF NOT EXISTS hook_liveness (
10
+ hook TEXT PRIMARY KEY,
11
+ experimental INTEGER NOT NULL DEFAULT 0,
12
+ fire_count INTEGER NOT NULL DEFAULT 0,
13
+ error_count INTEGER NOT NULL DEFAULT 0,
14
+ expected_count INTEGER NOT NULL DEFAULT 0,
15
+ first_seen_at TEXT,
16
+ last_seen_at TEXT,
17
+ dead_since TEXT,
18
+ plugin_version TEXT
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS idx_hook_liveness_dead
22
+ ON hook_liveness(dead_since);
23
+
24
+ -- 2. Host probe history. Append-only. Off by default; exists so a
25
+ -- user chasing an intermittent fault can turn it on and get a
26
+ -- timeline instead of a single current value.
27
+ CREATE TABLE IF NOT EXISTS host_probes (
28
+ id TEXT PRIMARY KEY,
29
+ probed_at TEXT NOT NULL DEFAULT (datetime('now')),
30
+ plugin_version TEXT,
31
+ flavour TEXT NOT NULL,
32
+ has_shell INTEGER NOT NULL DEFAULT 0,
33
+ v2_skill INTEGER NOT NULL DEFAULT 0,
34
+ v2_reference INTEGER NOT NULL DEFAULT 0,
35
+ notes TEXT
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_host_probes_at
39
+ ON host_probes(probed_at);
40
+
41
+ -- 3. Native registration outcomes. One row per attach attempt, so
42
+ -- "registered but unverified" is a queryable state rather than a
43
+ -- log line.
44
+ CREATE TABLE IF NOT EXISTS native_registrations (
45
+ id TEXT PRIMARY KEY,
46
+ attached_at TEXT NOT NULL DEFAULT (datetime('now')),
47
+ surface TEXT NOT NULL CHECK (surface IN ('skill', 'reference')),
48
+ registered INTEGER NOT NULL DEFAULT 0,
49
+ verified INTEGER NOT NULL DEFAULT 0,
50
+ note TEXT
51
+ );
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_native_registrations_surface
54
+ ON native_registrations(surface, attached_at);
55
+
56
+ -- 4. Metric seeds (39 -> 45).
57
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
58
+ ('hook_fires_total', 0),
59
+ ('hook_errors_total', 0),
60
+ ('hooks_dead_total', 0),
61
+ ('injections_suppressed_dead_hook', 0),
62
+ ('native_registrations_total', 0),
63
+ ('native_registration_failures', 0);
64
+
65
+ -- 5. Setting seeds (23 -> 27).
66
+ -- hook_liveness_enabled defaults ON: it is a read-only instrument
67
+ -- on the success path, and an instrument nobody switches on is an
68
+ -- instrument nobody has.
69
+ -- native_registration_enabled defaults OFF: it changes where a
70
+ -- curated skill comes from, and that is a change a user opts into.
71
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
72
+ ('hook_liveness_enabled', '1'),
73
+ ('native_registration_enabled', '0'),
74
+ ('host_probe_history_enabled', '0'),
75
+ ('dead_hook_report_threshold', '3');
76
+
77
+ -- 6. Version marker.
78
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('010');
@@ -3,6 +3,27 @@ import type { Metrics } from "./metrics.js";
3
3
  export declare const MARKER_BEGIN = "<!-- kevin:begin \u2014 curated by opencode-kevin, safe to edit -->";
4
4
  export declare const MARKER_END = "<!-- kevin:end -->";
5
5
  export type WriteOutcome = "written" | "noop" | "refused";
6
+ /**
7
+ * v0.8.0 (K8-019 / D8-08) — the two write modes. `markers` is the
8
+ * v0.6.0 behaviour, byte for byte: a splice between the two marker
9
+ * comments, used for `AGENTS.md`, a file humans edit. `whole` replaces
10
+ * the entire file and is used only for Kevin-owned paths such as
11
+ * `.kevin/knowledge.okf` — a file humans do not hand-edit.
12
+ */
13
+ export type WriteMode = "markers" | "whole";
14
+ export interface WriteRequest {
15
+ readonly path: string;
16
+ readonly mode: WriteMode;
17
+ /** marker block body ("markers"), or whole-file content ("whole"). */
18
+ readonly content: string;
19
+ /**
20
+ * Caller-side refusal reason (K8-020 / D6-03): when present, the plan
21
+ * is refused — nothing is written, and the refusal is audited with
22
+ * both hashes. The refusal conditions belong to the caller; the
23
+ * writer only records them.
24
+ */
25
+ readonly refusal?: string;
26
+ }
6
27
  export interface WritePlan {
7
28
  readonly path: string;
8
29
  readonly before: string;
@@ -44,6 +65,13 @@ export declare class ArtifactWriter {
44
65
  private readonly metrics;
45
66
  constructor(store: Store, projectId: string, metrics?: Metrics | null);
46
67
  plan(path: string, body: string): WritePlan;
68
+ plan(request: WriteRequest): WritePlan;
69
+ /**
70
+ * K8-019 (D8-08) — the single write funnel: every file Kevin writes
71
+ * goes through this method, which is the ONLY call site of `apply()`
72
+ * in the plugin (asserted by tests/unit/single_write_path.test.ts).
73
+ */
74
+ write(request: WriteRequest, proposalId?: string): WriteOutcome;
47
75
  apply(plan: WritePlan, proposalId?: string): WriteOutcome;
48
76
  private renameTemp;
49
77
  private audit;