@jmtrin/opencode-kevin 0.7.0 → 0.8.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 (52) hide show
  1. package/README.md +198 -9
  2. package/dist/migrations/009_v08_team.sql +100 -0
  3. package/dist/plugin/ArtifactWriter.d.ts +28 -0
  4. package/dist/plugin/ArtifactWriter.js +35 -2
  5. package/dist/plugin/ArtifactWriter.js.map +1 -1
  6. package/dist/plugin/ContextInjector.d.ts +9 -0
  7. package/dist/plugin/ContextInjector.js +14 -3
  8. package/dist/plugin/ContextInjector.js.map +1 -1
  9. package/dist/plugin/Curator.d.ts +22 -2
  10. package/dist/plugin/Curator.js +56 -14
  11. package/dist/plugin/Curator.js.map +1 -1
  12. package/dist/plugin/InjectionLedger.d.ts +6 -0
  13. package/dist/plugin/InjectionLedger.js +6 -0
  14. package/dist/plugin/InjectionLedger.js.map +1 -1
  15. package/dist/plugin/Materializer.d.ts +4 -5
  16. package/dist/plugin/Materializer.js +9 -6
  17. package/dist/plugin/Materializer.js.map +1 -1
  18. package/dist/plugin/MemoryService.d.ts +34 -5
  19. package/dist/plugin/MemoryService.js +215 -7
  20. package/dist/plugin/MemoryService.js.map +1 -1
  21. package/dist/plugin/Migrate.js +33 -3
  22. package/dist/plugin/Migrate.js.map +1 -1
  23. package/dist/plugin/RepoIdentity.d.ts +113 -0
  24. package/dist/plugin/RepoIdentity.js +266 -0
  25. package/dist/plugin/RepoIdentity.js.map +1 -0
  26. package/dist/plugin/Retrospective.js +9 -0
  27. package/dist/plugin/Retrospective.js.map +1 -1
  28. package/dist/plugin/SharedLayer.d.ts +159 -0
  29. package/dist/plugin/SharedLayer.js +463 -0
  30. package/dist/plugin/SharedLayer.js.map +1 -0
  31. package/dist/plugin/index.d.ts +30 -1
  32. package/dist/plugin/index.js +458 -10
  33. package/dist/plugin/index.js.map +1 -1
  34. package/dist/plugin/kevin_approve.js +7 -4
  35. package/dist/plugin/kevin_approve.js.map +1 -1
  36. package/dist/plugin/kevin_audit.d.ts +19 -1
  37. package/dist/plugin/kevin_audit.js +95 -3
  38. package/dist/plugin/kevin_audit.js.map +1 -1
  39. package/dist/plugin/metrics.d.ts +1 -1
  40. package/dist/plugin/metrics.js +9 -0
  41. package/dist/plugin/metrics.js.map +1 -1
  42. package/dist/plugin/okf-export.d.ts +2 -2
  43. package/dist/plugin/okf-export.js +15 -7
  44. package/dist/plugin/okf-export.js.map +1 -1
  45. package/dist/plugin/okf.d.ts +107 -0
  46. package/dist/plugin/okf.js +304 -0
  47. package/dist/plugin/okf.js.map +1 -0
  48. package/dist/plugin/replay-types.d.ts +29 -287
  49. package/dist/plugin/replay-types.js +145 -53
  50. package/dist/plugin/replay-types.js.map +1 -1
  51. package/migrations/009_v08_team.sql +100 -0
  52. 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 21 tools, including `kevin_facts`, `kevin_conflicts`, `kevin_project`, `kevin_share` and `kevin_sync`.
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 21 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
213
  // "memories_pattern": 0, "memories_causal": 1, "tool_calls": 318,
202
- // "retrospectives": 7, "tool_count": 16,
214
+ // "retrospectives": 7, "tool_count": 21,
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,6 +354,40 @@ 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
 
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
+
343
391
  ---
344
392
 
345
393
  ## How Kevin measures itself
@@ -425,6 +473,131 @@ Rejection history is never deleted: it is the evidence base for the roadmap's ki
425
473
 
426
474
  ---
427
475
 
476
+ ## The shared layer (v0.8.0)
477
+
478
+ ### Two layers, one file
479
+
480
+ Every memory lives in one of two layers:
481
+
482
+ | Layer | Storage | Visibility |
483
+ |---|---|---|
484
+ | **local** | `kevin.db` → `memories` | private to this installation (the whole v0.1–v0.7 model) |
485
+ | **shared** | `.kevin/knowledge.okf` (a committed file) → `shared_entries` projections | visible to every teammate who pulls the repo and runs Kevin |
486
+
487
+ 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.
488
+
489
+ 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.
490
+
491
+ ### Where the repo identity comes from
492
+
493
+ 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:
494
+
495
+ | Priority | Source | `identity_source` | Typical case |
496
+ |---|---|---|---|
497
+ | 1 | `.kevin/project.json` (`repo_id` pinned) | `declared` | after `kevin_project init` |
498
+ | 2 | the git `origin` remote URL, hashed | `remote` | a repo cloned from a known remote |
499
+ | 3 | the project path | `path` | a local folder without git |
500
+
501
+ 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):
502
+
503
+ ```
504
+ startup or kevin_project call
505
+
506
+
507
+ resolve(): declared ──► remote ──► path (first source that yields)
508
+
509
+
510
+ repo_id (16-hex hash — never the raw URL)
511
+
512
+ ├── kevin_project show → which source won, counts, rekey_available
513
+ ├── kevin_project init → pins the id into .kevin/project.json
514
+ └── kevin_project rekey → one transaction moves everything together:
515
+ DB corpus + live session identity
516
+ + the OKF file's #repo header
517
+ ```
518
+
519
+ 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`.
520
+
521
+ ### The round trip, step by step
522
+
523
+ ```
524
+ you (repo A) teammate (repo B)
525
+ ┌───────────────────────────────────────┐ ┌───────────────────────────────────────┐
526
+ │ kevin_share({ memory_ids, │ │ git pull │
527
+ │ confirm: true }) │ │ │ │
528
+ │ │ 1. identity gate: #repo must │ │ ▼ │
529
+ │ │ match your repo_id │ │ kevin_sync (or automatically at │
530
+ │ │ 2. entry_id = hash(type, │ │ session.idle) │
531
+ │ │ statement, scope) │ │ │ │
532
+ │ │ 3. per-line and corpus limits │ │ ├─ new asserts → shared_entries │
533
+ │ ▼ │ │ │ → projections (layer='shared', │
534
+ │ .kevin/knowledge.okf │ │ │ immutable, retrievable) │
535
+ │ │ (the only thing Kevin writes) │ │ ├─ tombstones → projections │
536
+ │ ▼ │ │ │ archived │
537
+ │ git commit + push │ │ └─ unchanged file → no-op │
538
+ └───────────────────────────────────────┘ └───────────────────────────────────────┘
539
+ ```
540
+
541
+ Kevin never commits, pushes or fetches anything: the git remote is the transport, and `git commit`/`git push` are yours.
542
+
543
+ ### The OKF v2 format
544
+
545
+ One annotated example, byte-exact (the three header lines are always first):
546
+
547
+ ```
548
+ #okf 2
549
+ #repo 2114ad162af50a25
550
+ #generated-by opencode-kevin/0.8.0
551
+ {"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"}
552
+ ```
553
+
554
+ - `#okf 2` — the format version. A file with a **higher** version is refused (`version_ahead`), never downgraded.
555
+ - `#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.
556
+ - `#generated-by opencode-kevin/<version>` — provenance of the writer.
557
+ - 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.
558
+ - Limits, enforced at export *and* import: `MAX_LINE_BYTES = 4096`, `MAX_ENTRIES = 2000`. A corpus beyond the limits is refused, never truncated.
559
+
560
+ ### When git reports a conflict in `.kevin/knowledge.okf`
561
+
562
+ 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.
563
+
564
+ 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:
565
+
566
+ ```
567
+ <<<<<<< HEAD
568
+ #okf 2
569
+ #repo 2114ad162af50a25
570
+ #generated-by opencode-kevin/0.8.0
571
+ {"op":"assert", ... "statement":"Always use the repository pattern..."}
572
+ =======
573
+ #okf 2
574
+ #repo 2114ad162af50a25
575
+ #generated-by opencode-kevin/0.8.0
576
+ {"op":"assert", ... "statement":"Always wrap file writes in a temp-file + rename helper..."}
577
+ >>>>>>> teammate
578
+ ```
579
+
580
+ 1. Resolve by keeping both entry lines (drop the `<<<<<<<`, `=======`, `>>>>>>>` markers and the duplicated headers, keep the other headers and both JSON lines).
581
+ 2. Save the file — it now contains both assertions.
582
+ 3. Run `kevin_sync`. Both entries import; both projections become active; the conflict is resolved and the corpus is the union of both sides.
583
+
584
+ 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.
585
+
586
+ ### Retiring shared knowledge
587
+
588
+ 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.
589
+
590
+ ### Non-goals (asked for, deliberately not built)
591
+
592
+ - **No server.** The file, the git remote, and your own discipline are the only transport.
593
+ - **No account.** No signup, no cloud, no telemetry — `author_identity_mode` defaults to `hashed` and no identity is ever transmitted.
594
+ - **No automatic commit.** Kevin writes the file; git is yours.
595
+ - **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).
596
+ - **No undelete.** A tombstone is a statement, not a recovery mechanism — keep your git history if you need to resurrect an entry.
597
+ - **No tombstone tool (yet).** See [Retiring shared knowledge](#retiring-shared-knowledge) — retirement happens by editing the file.
598
+
599
+ ---
600
+
428
601
  ## Replay harness
429
602
 
430
603
  `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 +615,7 @@ Kevin subscribes to 6 OpenCode hooks:
442
615
  | `experimental.chat.system.transform` | Injects relevant lessons in `<kevin-context>` (400 tokens by default, configurable) + optional `<kevin-suggestion>` |
443
616
  | `experimental.session.compacting` | Re-injects lessons in `<kevin-memory>` after compacting (2000 tokens) + optional `<kevin-suggestion>` |
444
617
  | `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 |
618
+ | `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
619
 
447
620
  **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
621
 
@@ -472,7 +645,7 @@ KevinPlugin(input, {
472
645
 
473
646
  ### Settings
474
647
 
475
- Read/write via `kevin_config({ action: "list" | "set", ... })`. All values are TEXT; booleans compare against `"1"`.
648
+ Read/write via `kevin_config({ action: "list" | "set", ... })`. All 23 values are TEXT; booleans compare against `"1"`.
476
649
 
477
650
  | Setting | Default | Effect |
478
651
  |---|---|---|
@@ -490,6 +663,17 @@ Read/write via `kevin_config({ action: "list" | "set", ... })`. All values are T
490
663
  | `skill_emission_enabled` | `"0"` | Registers the curated skill with the host at startup (v2 hosts only) |
491
664
  | `reference_emission_enabled` | `"0"` | Registers `@kevin/<topic>` references at startup (v2 hosts only) |
492
665
  | `injection_confidence_floor` | `"0.6"` | Push gate: memories below this confidence are counted and rejected |
666
+ | `repo_truth_enabled` | `"0"` | Opt-in Project Truth: fact scanning + contradiction detection at `session.idle` |
667
+ | `convention_mining_enabled` | `"0"` | Opt-in deterministic convention mining into `AGENTS.md` |
668
+ | `conflict_detection_enabled` | `"0"` | Opt-in contradiction detection between memories and observed facts |
669
+ | `error_lesson_mode` | `"all"` | Which failures produce lessons: `all` / `rules_only` / `patterns_only` |
670
+ | `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 |
671
+ | `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) |
672
+ | `share_requires_approval` | `"1"` | When `"1"`, `kevin_share` writes only with `confirm: true`; un-curated memories are refused with `not_curated` |
673
+ | `author_identity_mode` | `"hashed"` | Author attribution in exports: `"hashed"` or `"none"` (**string** enum) |
674
+ | `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 |
675
+
676
+ All 23 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`.
493
677
 
494
678
  ---
495
679
 
@@ -548,6 +732,9 @@ plugin/
548
732
  kevin_publish.ts # kevin_publish tool (bundle regeneration)
549
733
  kevin_audit.ts # Read-only audit + channels/curation blocks
550
734
  kevin_why.ts # kevin_why tool: failure→fix traces + related rules
735
+ SharedLayer.ts # v0.8: OKF plan/apply/tombstone + import → shared projections
736
+ RepoIdentity.ts # v0.8: repo_id resolution (declared → remote → path) + rekey
737
+ okf.ts # v0.8: OKF v2 parse/serialize (headers, entry_id, limits)
551
738
  okf-export.ts # kevin_export: OKF/markdown export
552
739
  okf-import.ts # kevin_import: bundle parser + import
553
740
  confidence.ts # Two-sided computeConfidence (evidence + recurrence + feedback)
@@ -565,6 +752,8 @@ migrations/
565
752
  005_v04_signal.sql # recurrence_count, fix_args, last_injected_at
566
753
  006_v05_glassbox.sql # ignored/archived/superseded_by, feedback, metrics
567
754
  007_v06_pull.sql # curation_proposals, artifact_writes, curated/inferable
755
+ 008_v07_truth.sql # facts + contradictions, conflict_detection settings
756
+ 009_v08_team.sql # shared_entries, okf_imports, the five v0.8 settings
568
757
  tests/
569
758
  unit/ # component tests
570
759
  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');
@@ -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;
@@ -84,7 +84,11 @@ export class ArtifactWriter {
84
84
  this.projectId = projectId;
85
85
  this.metrics = metrics ?? null;
86
86
  }
87
- plan(path, body) {
87
+ plan(pathOrRequest, body) {
88
+ const request = typeof pathOrRequest === "string"
89
+ ? { path: pathOrRequest, mode: "markers", content: body ?? "" }
90
+ : pathOrRequest;
91
+ const { path, mode, content } = request;
88
92
  let before;
89
93
  try {
90
94
  // Read as Buffer, not as utf8 text: readFileSync's text decoding
@@ -100,10 +104,31 @@ export class ArtifactWriter {
100
104
  throw err;
101
105
  }
102
106
  }
107
+ if (mode === "whole") {
108
+ // K8-019 (D8-08) — the whole-file path. The file is Kevin-owned,
109
+ // so there are no markers, no sanitization and no EOL
110
+ // normalization: the rendered bytes are written as-is, which is
111
+ // what makes a re-render of the same content a `noop`. A
112
+ // caller-side refusal leaves the file untouched and is audited
113
+ // like any other refusal: after = before, both hashes recorded.
114
+ const refusal = request.refusal;
115
+ const refused = refusal !== undefined;
116
+ const after = refused ? before : content;
117
+ return {
118
+ path,
119
+ before,
120
+ after,
121
+ diff: unifiedDiff(path, before, after),
122
+ outcome: refused ? "refused" : after === before ? "noop" : "written",
123
+ ...(refusal !== undefined ? { reason: refusal } : {}),
124
+ hashBefore: sha256(before),
125
+ hashAfter: sha256(after),
126
+ };
127
+ }
103
128
  const eol = detectEol(before);
104
129
  // Rule 9 — sanitation happens in plan(), before hashing, so the hashes
105
130
  // describe what was actually written.
106
- const bodyEol = normalizeEol(sanitizeArtifactBody(body), eol);
131
+ const bodyEol = normalizeEol(sanitizeArtifactBody(content), eol);
107
132
  const firstBegin = before.indexOf(MARKER_BEGIN);
108
133
  const firstEnd = before.indexOf(MARKER_END);
109
134
  let after;
@@ -176,6 +201,14 @@ export class ArtifactWriter {
176
201
  hashAfter: sha256(after),
177
202
  };
178
203
  }
204
+ /**
205
+ * K8-019 (D8-08) — the single write funnel: every file Kevin writes
206
+ * goes through this method, which is the ONLY call site of `apply()`
207
+ * in the plugin (asserted by tests/unit/single_write_path.test.ts).
208
+ */
209
+ write(request, proposalId) {
210
+ return this.apply(this.plan(request), proposalId);
211
+ }
179
212
  // v0.6.0 (K6-007 / plan §5.1, rules 7–8) — atomic write + audit row.
180
213
  apply(plan, proposalId) {
181
214
  // Rule 8 — refusals and noops still leave an audit trail; a refusal
@@ -1 +1 @@
1
- {"version":3,"file":"ArtifactWriter.js","sourceRoot":"","sources":["../../plugin/ArtifactWriter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ArtifactWriter.js","sourceRoot":"","sources":["../../plugin/ArtifactWriter.ts"],"names":[],"mappings":"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"}
@@ -195,6 +195,15 @@ export declare class ContextInjector {
195
195
  /**
196
196
  * v0.4.0 (K4-017) — one ledger row per admitted memory (plan §5.2).
197
197
  * Token attribution uses the memory's share of the final block.
198
+ *
199
+ * v0.8.0 (K8-024 / plan §5.7) — shared projections are recorded even
200
+ * though they carry NO fingerprint by design (K8-017 — it is a
201
+ * different identity dimension): the memory id is their identity in
202
+ * the ledger, and recording them is what makes `injections_from_shared`
203
+ * observable at all. No tool call can ever match a memory id, so
204
+ * settle() marks the row inconclusive — excluded from the precision
205
+ * denominator (K5-005) — and the BUG-015 skip is unchanged for local
206
+ * notes without a fingerprint.
198
207
  */
199
208
  private recordInjections;
200
209
  /**
@@ -434,21 +434,32 @@ Consider adding this convention to AGENTS.md:
434
434
  /**
435
435
  * v0.4.0 (K4-017) — one ledger row per admitted memory (plan §5.2).
436
436
  * Token attribution uses the memory's share of the final block.
437
+ *
438
+ * v0.8.0 (K8-024 / plan §5.7) — shared projections are recorded even
439
+ * though they carry NO fingerprint by design (K8-017 — it is a
440
+ * different identity dimension): the memory id is their identity in
441
+ * the ledger, and recording them is what makes `injections_from_shared`
442
+ * observable at all. No tool call can ever match a memory id, so
443
+ * settle() marks the row inconclusive — excluded from the precision
444
+ * denominator (K5-005) — and the BUG-015 skip is unchanged for local
445
+ * notes without a fingerprint.
437
446
  */
438
447
  recordInjections(admitted, sessionId, tag, block) {
439
448
  if (this.ledger === null)
440
449
  return;
441
450
  const blockTokens = estimateTokens(block);
442
- const measurable = admitted.filter((m) => m.fingerprint);
443
451
  const perMemory = Math.max(1, Math.round(blockTokens / admitted.length));
444
452
  const hook = tag === "context" ? "pre_prompt" : "compacting";
445
- for (const m of measurable) {
453
+ for (const m of admitted) {
454
+ if (!m.fingerprint && m.layer !== "shared")
455
+ continue;
446
456
  this.ledger.record({
447
457
  memoryId: m.id,
448
- fingerprint: m.fingerprint,
458
+ fingerprint: m.fingerprint ?? m.id,
449
459
  sessionId,
450
460
  hook,
451
461
  tokens: perMemory,
462
+ layer: m.layer ?? "local",
452
463
  });
453
464
  }
454
465
  }