@jmtrin/opencode-kevin 0.6.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 (83) hide show
  1. package/README.md +771 -580
  2. package/dist/migrations/001_initial.sql +91 -91
  3. package/dist/migrations/003_v02_signal.sql +57 -57
  4. package/dist/migrations/004_v03_knowledge.sql +138 -138
  5. package/dist/migrations/005_v04_signal.sql +57 -57
  6. package/dist/migrations/006_v05_glassbox.sql +118 -118
  7. package/dist/migrations/007_v06_pull.sql +144 -144
  8. package/dist/migrations/008_v07_truth.sql +124 -0
  9. package/dist/migrations/009_v08_team.sql +100 -0
  10. package/dist/plugin/ArtifactWriter.d.ts +28 -0
  11. package/dist/plugin/ArtifactWriter.js +35 -2
  12. package/dist/plugin/ArtifactWriter.js.map +1 -1
  13. package/dist/plugin/ConflictDetector.d.ts +35 -0
  14. package/dist/plugin/ConflictDetector.js +283 -0
  15. package/dist/plugin/ConflictDetector.js.map +1 -0
  16. package/dist/plugin/ContextInjector.d.ts +9 -0
  17. package/dist/plugin/ContextInjector.js +14 -3
  18. package/dist/plugin/ContextInjector.js.map +1 -1
  19. package/dist/plugin/ConventionMiner.d.ts +35 -0
  20. package/dist/plugin/ConventionMiner.js +243 -0
  21. package/dist/plugin/ConventionMiner.js.map +1 -0
  22. package/dist/plugin/Curator.d.ts +22 -2
  23. package/dist/plugin/Curator.js +101 -28
  24. package/dist/plugin/Curator.js.map +1 -1
  25. package/dist/plugin/InjectionLedger.d.ts +6 -0
  26. package/dist/plugin/InjectionLedger.js +6 -0
  27. package/dist/plugin/InjectionLedger.js.map +1 -1
  28. package/dist/plugin/Materializer.d.ts +4 -5
  29. package/dist/plugin/Materializer.js +9 -6
  30. package/dist/plugin/Materializer.js.map +1 -1
  31. package/dist/plugin/MemoryService.d.ts +49 -2
  32. package/dist/plugin/MemoryService.js +306 -8
  33. package/dist/plugin/MemoryService.js.map +1 -1
  34. package/dist/plugin/Migrate.js +61 -2
  35. package/dist/plugin/Migrate.js.map +1 -1
  36. package/dist/plugin/Reflector.js +13 -0
  37. package/dist/plugin/Reflector.js.map +1 -1
  38. package/dist/plugin/RepoIdentity.d.ts +113 -0
  39. package/dist/plugin/RepoIdentity.js +266 -0
  40. package/dist/plugin/RepoIdentity.js.map +1 -0
  41. package/dist/plugin/RepoTruth.d.ts +80 -0
  42. package/dist/plugin/RepoTruth.js +600 -0
  43. package/dist/plugin/RepoTruth.js.map +1 -0
  44. package/dist/plugin/Retrospective.js +17 -0
  45. package/dist/plugin/Retrospective.js.map +1 -1
  46. package/dist/plugin/SharedLayer.d.ts +159 -0
  47. package/dist/plugin/SharedLayer.js +463 -0
  48. package/dist/plugin/SharedLayer.js.map +1 -0
  49. package/dist/plugin/index.d.ts +36 -2
  50. package/dist/plugin/index.js +591 -10
  51. package/dist/plugin/index.js.map +1 -1
  52. package/dist/plugin/kevin_approve.js +7 -4
  53. package/dist/plugin/kevin_approve.js.map +1 -1
  54. package/dist/plugin/kevin_audit.d.ts +59 -1
  55. package/dist/plugin/kevin_audit.js +190 -3
  56. package/dist/plugin/kevin_audit.js.map +1 -1
  57. package/dist/plugin/kevin_conflicts.d.ts +9 -0
  58. package/dist/plugin/kevin_conflicts.js +51 -0
  59. package/dist/plugin/kevin_conflicts.js.map +1 -0
  60. package/dist/plugin/kevin_facts.d.ts +42 -0
  61. package/dist/plugin/kevin_facts.js +37 -0
  62. package/dist/plugin/kevin_facts.js.map +1 -0
  63. package/dist/plugin/metrics.d.ts +3 -3
  64. package/dist/plugin/metrics.js +21 -2
  65. package/dist/plugin/metrics.js.map +1 -1
  66. package/dist/plugin/okf-export.d.ts +2 -2
  67. package/dist/plugin/okf-export.js +15 -7
  68. package/dist/plugin/okf-export.js.map +1 -1
  69. package/dist/plugin/okf.d.ts +107 -0
  70. package/dist/plugin/okf.js +304 -0
  71. package/dist/plugin/okf.js.map +1 -0
  72. package/dist/plugin/replay-types.d.ts +29 -287
  73. package/dist/plugin/replay-types.js +145 -53
  74. package/dist/plugin/replay-types.js.map +1 -1
  75. package/migrations/001_initial.sql +91 -91
  76. package/migrations/003_v02_signal.sql +57 -57
  77. package/migrations/004_v03_knowledge.sql +138 -138
  78. package/migrations/005_v04_signal.sql +57 -57
  79. package/migrations/006_v05_glassbox.sql +118 -118
  80. package/migrations/007_v06_pull.sql +144 -144
  81. package/migrations/008_v07_truth.sql +124 -0
  82. package/migrations/009_v08_team.sql +100 -0
  83. package/package.json +4 -4
@@ -6,22 +6,29 @@ import { tool } from "@opencode-ai/plugin";
6
6
  import { Archiver } from "./Archiver.js";
7
7
  import { ArtifactWriter } from "./ArtifactWriter.js";
8
8
  import { CausalChain } from "./CausalChain.js";
9
+ import { ConflictDetector } from "./ConflictDetector.js";
9
10
  import { ContextInjector } from "./ContextInjector.js";
11
+ import { ConventionMiner } from "./ConventionMiner.js";
10
12
  import { Curator } from "./Curator.js";
11
13
  import { Feedback } from "./Feedback.js";
12
14
  import { InjectionLedger } from "./InjectionLedger.js";
13
15
  import { Materializer, SKILL_TOPIC } from "./Materializer.js";
14
- import { MemoryService, } from "./MemoryService.js";
16
+ import { MemoryService, hasRepoIdColumn, } from "./MemoryService.js";
15
17
  import { Migrate } from "./Migrate.js";
16
18
  import { PatternMiner } from "./PatternMiner.js";
17
19
  import { ERROR_LINE_RE, Reflector, STRONG_ERROR_RE } from "./Reflector.js";
20
+ import * as RepoIdentity from "./RepoIdentity.js";
21
+ import { RepoTruth } from "./RepoTruth.js";
18
22
  import { Retrospective } from "./Retrospective.js";
23
+ import { SharedLayer } from "./SharedLayer.js";
19
24
  import { Store } from "./Store.js";
20
25
  import { ToolCallObserver } from "./ToolCallObserver.js";
21
26
  import { probe } from "./capabilities.js";
22
- import { fingerprint } from "./fingerprint.js";
27
+ import { computeConfidence } from "./confidence.js";
23
28
  import { kevinApprove } from "./kevin_approve.js";
24
29
  import { buildAudit } from "./kevin_audit.js";
30
+ import { executeKevinConflicts } from "./kevin_conflicts.js";
31
+ import { buildKevinFacts } from "./kevin_facts.js";
25
32
  import { kevinPropose } from "./kevin_propose.js";
26
33
  import { kevinPublish } from "./kevin_publish.js";
27
34
  import { kevinWhy } from "./kevin_why.js";
@@ -58,11 +65,159 @@ export const KEVIN_CONFIG_KEYS = [
58
65
  "skill_emission_enabled",
59
66
  "reference_emission_enabled",
60
67
  "injection_confidence_floor",
68
+ // v0.7.0 (K7-003 / plan §8.10) — the four keys seeded by migration 008
69
+ // section 5. Three feature flags plus the error lesson mode enum.
70
+ // Omitting these makes `kevin_config set` return { error: "unknown_key" }
71
+ // while `kevin_config list` still shows them.
72
+ "repo_truth_enabled",
73
+ "convention_mining_enabled",
74
+ "conflict_detection_enabled",
75
+ "error_lesson_mode",
76
+ // v0.8.0 (K8-003 / plan §8.10) — the five keys seeded by migration 009
77
+ // section 5. Omitting these makes `kevin_config set` return
78
+ // { error: "unknown_key" } while `kevin_config list` still shows them.
79
+ // shared_layer_enabled must be compared with === "1" (it is TEXT, and
80
+ // '0' is truthy); shared_confidence_floor is a string read with
81
+ // Number.parseFloat and clamped to [0, 1] (conventions, §2).
82
+ "shared_layer_enabled",
83
+ "okf_path",
84
+ "share_requires_approval",
85
+ "author_identity_mode",
86
+ "shared_confidence_floor",
61
87
  ];
88
+ // v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
89
+ // `error_lesson_mode`. The setting is TEXT and must be compared with
90
+ // `=== "triage_only"`, never by truthiness; the domain here is enforced by
91
+ // `kevin_config set` so a typo (`"triage"`, `"0"`, `"false"`) is rejected
92
+ // at the surface rather than silently changing every installation's
93
+ // behaviour on the next reflection.
94
+ export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
95
+ /** Plugin release version — stamped into generated files (K8-021/027). */
96
+ export const KEVIN_VERSION = "0.8.0";
62
97
  function resolveMigrationsDir() {
63
98
  const here = dirname(fileURLToPath(import.meta.url));
64
99
  return join(here, "..", "migrations");
65
100
  }
101
+ const REKEY_TABLES = ["memories", "shared_entries", "okf_imports"];
102
+ export function performRekey(store, toRepoId, opts) {
103
+ // The 009 migration carries the repo_id column AND the shared-layer
104
+ // tables; without it there is nothing to re-key.
105
+ if (!hasRepoIdColumn(store)) {
106
+ return {
107
+ action: "rekey",
108
+ ok: false,
109
+ reason: "la migracion 009 no se ha aplicado: no existe repo_id (ni shared_entries/okf_imports) sobre el que re-key",
110
+ };
111
+ }
112
+ // Rows that would move: every scoped row stored under a repo_id
113
+ // different from the target. NULL-repo_id rows are global by design
114
+ // and never move.
115
+ const groupRows = (table) => store
116
+ .prepare(`SELECT repo_id, COUNT(*) AS c FROM ${table}
117
+ WHERE repo_id IS NOT NULL AND repo_id != ? GROUP BY repo_id`)
118
+ .all(toRepoId);
119
+ const from = {};
120
+ const rows = {
121
+ memories: 0,
122
+ shared_entries: 0,
123
+ okf_imports: 0,
124
+ };
125
+ for (const table of REKEY_TABLES) {
126
+ for (const r of groupRows(table)) {
127
+ rows[table] += r.c;
128
+ from[r.repo_id] ??= {
129
+ memories: 0,
130
+ shared_entries: 0,
131
+ okf_imports: 0,
132
+ };
133
+ from[r.repo_id][table] = r.c;
134
+ }
135
+ }
136
+ const total = rows.memories + rows.shared_entries + rows.okf_imports;
137
+ if (total === 0) {
138
+ return {
139
+ action: "rekey",
140
+ ok: true,
141
+ rekeyed: false,
142
+ to_repo_id: toRepoId,
143
+ rows,
144
+ from,
145
+ };
146
+ }
147
+ // Monorepo collision (D8-03): rows already at the target repo_id
148
+ // belong to a different project_id set than the rows that would move.
149
+ // shared_entries and okf_imports carry no project_id, so memories is
150
+ // the only witness.
151
+ const pidSet = (sql, ...params) => {
152
+ const out = new Set();
153
+ for (const r of store.prepare(sql).all(...params)) {
154
+ if (r.project_id !== null)
155
+ out.add(r.project_id);
156
+ }
157
+ return out;
158
+ };
159
+ const targetPids = pidSet("SELECT DISTINCT project_id FROM memories WHERE repo_id = ?", toRepoId);
160
+ const movePids = pidSet("SELECT DISTINCT project_id FROM memories WHERE repo_id IS NOT NULL AND repo_id != ?", toRepoId);
161
+ const collision = targetPids.size > 0 &&
162
+ !(movePids.size === targetPids.size &&
163
+ [...movePids].every((p) => targetPids.has(p)));
164
+ if (!opts.confirm) {
165
+ return {
166
+ action: "rekey",
167
+ ok: true,
168
+ dry_run: true,
169
+ to_repo_id: toRepoId,
170
+ rows,
171
+ from,
172
+ collision,
173
+ };
174
+ }
175
+ if (collision && opts.force !== true) {
176
+ return {
177
+ action: "rekey",
178
+ ok: false,
179
+ reason: "monorepo collision: el repo_id destino ya contiene memorias de un conjunto de project_id distinto; pasa force: true solo si quieres fusionarlos",
180
+ to_repo_id: toRepoId,
181
+ rows,
182
+ collision: true,
183
+ };
184
+ }
185
+ // One transaction: the row moves and the rekey_events counter move
186
+ // together — a mid-way failure rolls both back and the database is
187
+ // completely unchanged.
188
+ try {
189
+ store.transaction(() => {
190
+ for (const table of REKEY_TABLES) {
191
+ store
192
+ .prepare(`UPDATE ${table} SET repo_id = ?
193
+ WHERE repo_id IS NOT NULL AND repo_id != ?`)
194
+ .run(toRepoId, toRepoId);
195
+ }
196
+ store
197
+ .prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
198
+ VALUES ('rekey_events', 1, datetime('now'))
199
+ ON CONFLICT(key) DO UPDATE SET
200
+ value = value + 1,
201
+ updated_at = datetime('now')`)
202
+ .run();
203
+ });
204
+ }
205
+ catch (err) {
206
+ return {
207
+ action: "rekey",
208
+ ok: false,
209
+ reason: `rekey fallo y se revirtio completamente: ${err?.message ?? "unknown error"}`,
210
+ };
211
+ }
212
+ return {
213
+ action: "rekey",
214
+ ok: true,
215
+ rekeyed: true,
216
+ to_repo_id: toRepoId,
217
+ rows,
218
+ from,
219
+ };
220
+ }
66
221
  export const KevinPlugin = async (input, options) => {
67
222
  const opts = (options ?? {});
68
223
  const dbPath = opts.dbPath ?? join(homedir(), ".opencode-kevin", "kevin.db");
@@ -78,8 +233,34 @@ export const KevinPlugin = async (input, options) => {
78
233
  // v0.4.0 (K4-019): the plugin hooks expose no project field, so the
79
234
  // project id is derived once from the plugin host's working directory
80
235
  // (plan §5.7 fallback; D2-11 project scoping wired into the live path).
81
- const projectId = fingerprint(process.cwd());
82
- const memoryService = new MemoryService(store, metrics);
236
+ // v0.8.0 (K8-006 / plan §5.1): the repository identity resolves once at
237
+ // init, next to the project id, and never on a hot path.
238
+ const identity = RepoIdentity.resolve(process.cwd());
239
+ const projectId = identity.projectId;
240
+ const repoId = identity.repoId;
241
+ const identitySource = identity.source;
242
+ const identityEvidence = identity.evidence;
243
+ // v0.8.0 (BUG-001/002) — the SESSION identity: what the shared layer
244
+ // bridge, MemoryService, Curator and the tools actually use. It is
245
+ // derived once at init and only moves when kevin_project rekey
246
+ // succeeds (the rows move with it). kevin_status and the audit
247
+ // rollups report THIS id, never a fresh per-call resolve — a
248
+ // mid-session `git remote add` changes what the identity WILL be,
249
+ // not what the session is scoped on.
250
+ let sessionIdentity = identity;
251
+ const memoryService = new MemoryService(store, metrics, repoId);
252
+ // v0.7.0 (K7-009 / plan §5.1, D7-13) — the repository truth scanner reads
253
+ // the JSON project files. Runs once at init, gated by repo_truth_enabled.
254
+ const projectRoot = opts.projectRoot ?? process.cwd();
255
+ const repoTruth = new RepoTruth(store, projectId, projectRoot, metrics);
256
+ if (memoryService.getSetting("repo_truth_enabled", "0") === "1") {
257
+ try {
258
+ repoTruth.scan();
259
+ }
260
+ catch {
261
+ // A scan failure at init must not prevent the plugin from loading.
262
+ }
263
+ }
83
264
  const observer = new ToolCallObserver(store, metrics);
84
265
  // v0.3.0 fix — Prepared UPDATE used by Reflector.onLinkError to stamp
85
266
  // tool_calls.error_fingerprint with the stderr-based fingerprint the
@@ -112,12 +293,37 @@ export const KevinPlugin = async (input, options) => {
112
293
  dir: opts.retrospectivesDir,
113
294
  }, metrics);
114
295
  const patternMiner = new PatternMiner(store, memoryService, metrics);
296
+ const conventionMiner = new ConventionMiner(store, memoryService, projectId, metrics);
297
+ const conflictDetector = new ConflictDetector(store, projectId, metrics, repoTruth, memoryService);
115
298
  const causalChain = new CausalChain(store, memoryService, metrics);
116
299
  // v0.6.0 (K6-014 / plan §5.4-5.5, D6-01) — the single write path: the
117
300
  // ArtifactWriter is constructed here and only `kevin_approve` may call
118
301
  // `apply()`. `kevin_propose` reaches the file only through `plan()`.
119
302
  const writer = new ArtifactWriter(store, projectId, metrics);
120
- const curator = new Curator(store, memoryService, projectId, metrics);
303
+ // v0.8.0 (K8-020/021 / plan §5.5) — the shared layer bridge, built
304
+ // once at init next to the writer. okf_path resolves against
305
+ // projectRoot at call time; the writer is the only write path
306
+ // (D8-08).
307
+ let sharedLayer = new SharedLayer({
308
+ store,
309
+ repoId,
310
+ projectId,
311
+ version: KEVIN_VERSION,
312
+ writer,
313
+ });
314
+ // v0.8.0 (K8-022 / plan §5.5) — the shared layer re-read. sync is
315
+ // deliberately narrow: re-read a file that is already on disk —
316
+ // one read plus one hash on an unchanged repository (the
317
+ // okf_imports hash skip), and nothing else. No fetch, no push, no
318
+ // remote, no poll (D8-01, Principle 30). Reachable only from the
319
+ // kevin_sync tool and session.idle, never from a hot-path hook.
320
+ function syncSharedLayer() {
321
+ const okfPath = join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf"));
322
+ const report = sharedLayer.import(okfPath);
323
+ metrics.incr("shared_entries_imported", report.imported);
324
+ return report;
325
+ }
326
+ let curator = new Curator(store, memoryService, projectId, metrics, repoId);
121
327
  // v0.6.0 (K6-017/018 / plan §5.6-5.7, D6-13) — pull-channel bundles and
122
328
  // the v2 domain probe. `probe()` runs ONCE at init and the result is
123
329
  // held; probing per-event is a hot-path cost for a value that cannot
@@ -521,6 +727,52 @@ export const KevinPlugin = async (input, options) => {
521
727
  catch {
522
728
  v06 = undefined;
523
729
  }
730
+ let v07;
731
+ try {
732
+ v07 = {
733
+ schema_version: store
734
+ .prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1")
735
+ .get()?.version ?? "000",
736
+ facts_scanned: metrics.get("repo_facts_scanned"),
737
+ open_conflicts: store
738
+ .prepare("SELECT COUNT(*) AS c FROM memory_conflicts WHERE status = 'open'")
739
+ .get().c,
740
+ penalized_memories: metrics.get("memories_contradicted"),
741
+ error_lesson_mode: memoryService.getSetting("error_lesson_mode", "all"),
742
+ };
743
+ }
744
+ catch {
745
+ v07 = undefined;
746
+ }
747
+ // v0.8.0 (K8-025 / plan §5.8) — identity and shared-layer
748
+ // fields. repo_id is always a derived hash (never a raw
749
+ // remote URL — the "declared" source pins the .kevin/
750
+ // project.json id, "remote" hashes the normalized origin
751
+ // URL, "path" falls back to the project fingerprint).
752
+ // identity_source tells the user which of the three won.
753
+ // shared_layer_enabled is the config key read from
754
+ // kevin_settings (default "0"); shared_entries counts the
755
+ // active (asserted) entries for this repo — best-effort
756
+ // on pre-009 DBs, which have no shared layer at all.
757
+ // BUG-001: the SESSION identity (init-time, moved only by
758
+ // a successful rekey) — not a fresh per-call resolve, so
759
+ // the count always agrees with what kevin_share/kevin_sync
760
+ // actually read and write.
761
+ const identity = sessionIdentity;
762
+ let v08;
763
+ try {
764
+ v08 = {
765
+ repo_id: identity.repoId,
766
+ identity_source: identity.source,
767
+ shared_layer_enabled: memoryService.getSetting("shared_layer_enabled", "0"),
768
+ shared_entries: store
769
+ .prepare("SELECT COUNT(*) AS c FROM shared_entries WHERE repo_id = ? AND op = 'assert'")
770
+ .get(identity.repoId).c,
771
+ };
772
+ }
773
+ catch {
774
+ v08 = undefined;
775
+ }
524
776
  return {
525
777
  title: "Estado de Kevin",
526
778
  output: JSON.stringify({
@@ -529,8 +781,11 @@ export const KevinPlugin = async (input, options) => {
529
781
  // (13 v0.5 + kevin_propose + kevin_approve +
530
782
  // kevin_publish). The frozen ladder is verified
531
783
  // monotone across releases; K6-024 extends this
532
- // block with the remaining v0.6 fields.
533
- tool_count: 16,
784
+ // block with the remaining v0.6 fields. v0.8.0
785
+ // (K8-025) — 18 v0.7 + kevin_project +
786
+ // kevin_share + kevin_sync = 21.
787
+ tool_count: 21,
788
+ v07,
534
789
  memories_reflector: memoriesReflector,
535
790
  memories_agent: memoriesAgent,
536
791
  memories_pattern: memoriesPattern,
@@ -562,6 +817,130 @@ export const KevinPlugin = async (input, options) => {
562
817
  },
563
818
  // v0.6.0 (K6-024) — omitted on pre-007 DBs.
564
819
  v06,
820
+ // v0.8.0 (K8-025) — omitted on pre-009 DBs.
821
+ v08,
822
+ }),
823
+ };
824
+ },
825
+ }),
826
+ kevin_project: tool({
827
+ description: "Identidad de repositorio (v0.8.0, K8-008/009 / plan §5.8): show reporta repoId, source, evidence, projectId, los conteos de memorias bajo cada scope y rekey_available (true cuando el corpus esta almacenado bajo un scope distinto del resuelto); init escribe .kevin/project.json fijando el id derivado (refusa si ya existe); rekey mueve el corpus entero al repo_id resuelto en UNA transaccion — sin confirm: true es un dry run que no muta nada, y la colision monorepo se rechaza salvo force: true.",
828
+ args: {
829
+ action: tool.schema.enum(["show", "init", "rekey"]),
830
+ confirm: tool.schema.boolean().optional(),
831
+ force: tool.schema.boolean().optional(),
832
+ },
833
+ async execute(args) {
834
+ // v0.8.0 (K8-008) — identity resolves against the boot
835
+ // project root (the directory the plugin was wired with),
836
+ // never against the host's cwd.
837
+ const identity = RepoIdentity.resolve(projectRoot);
838
+ if (args.action === "init") {
839
+ const res = RepoIdentity.initProjectFile(projectRoot, writer);
840
+ return {
841
+ title: "Kevin project.json",
842
+ output: JSON.stringify(res.ok
843
+ ? {
844
+ action: "init",
845
+ written: true,
846
+ path: res.path,
847
+ id: res.id,
848
+ created_at: res.createdAt,
849
+ generator: "opencode-kevin/0.8.0",
850
+ }
851
+ : {
852
+ action: "init",
853
+ written: false,
854
+ path: res.path,
855
+ reason: res.reason,
856
+ }),
857
+ };
858
+ }
859
+ if (args.action === "rekey") {
860
+ // v0.8.0 (K8-009 / plan §5.1, D8-03) — the only call
861
+ // site of performRekey in this codebase (asserted by
862
+ // source scan in the K8-009 acceptance).
863
+ const res = performRekey(store, identity.repoId, {
864
+ confirm: args.confirm === true,
865
+ force: args.force === true,
866
+ });
867
+ // v0.8.0 (BUG-002) — a successful rekey moves the
868
+ // rows to the freshly resolved id; the session must
869
+ // follow, or kevin_share/kevin_sync/retrieval stay
870
+ // scoped on the old id until a restart. The bridge,
871
+ // MemoryService and Curator are rebuilt on the new id
872
+ // in the same breath the rows move — there is never
873
+ // a window in which the session and the store
874
+ // disagree. A dry run or a refusal mutates nothing.
875
+ if (res.ok && !res.dry_run) {
876
+ const live = RepoIdentity.resolve(projectRoot);
877
+ sessionIdentity = live;
878
+ memoryService.setRepoId(live.repoId);
879
+ sharedLayer = new SharedLayer({
880
+ store,
881
+ repoId: live.repoId,
882
+ projectId: live.projectId,
883
+ version: KEVIN_VERSION,
884
+ writer,
885
+ });
886
+ curator = new Curator(store, memoryService, projectId, metrics, live.repoId);
887
+ // v0.8.0 (BUG-003) — heal the OKF file header.
888
+ // Rekey changes the scope the file is written
889
+ // under; a stale `#repo` first line would make
890
+ // every later planExport/planTombstone refuse
891
+ // with repo_mismatch forever. The heal lives in
892
+ // SharedLayer so the whole-file construction
893
+ // stays at its single allowed site (D8-08).
894
+ try {
895
+ sharedLayer.healHeader(join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf")), live.repoId);
896
+ }
897
+ catch {
898
+ // Missing or unreadable file — nothing to heal.
899
+ }
900
+ }
901
+ return {
902
+ title: "Kevin project rekey",
903
+ output: JSON.stringify(res),
904
+ };
905
+ }
906
+ // show.
907
+ const withRepoColumn = hasRepoIdColumn(store);
908
+ const memoriesTotal = store.prepare("SELECT COUNT(*) AS c FROM memories").get().c;
909
+ const memoriesUnderRepo = withRepoColumn
910
+ ? store
911
+ .prepare("SELECT COUNT(*) AS c FROM memories WHERE repo_id = ?")
912
+ .get(identity.repoId).c
913
+ : 0;
914
+ const memoriesUnderProject = store
915
+ .prepare("SELECT COUNT(*) AS c FROM memories WHERE project_id = ?")
916
+ .get(identity.projectId).c;
917
+ // v0.8.0 (K8-008) — rekey_available: rows whose current
918
+ // scope differs from the resolved repo_id. On an
919
+ // unmigrated corpus the stored scope is project_id, so a
920
+ // git remote present (repoId != path fingerprint) reports
921
+ // true without the 009 column existing.
922
+ const differentlyScoped = withRepoColumn
923
+ ? store
924
+ .prepare("SELECT COUNT(*) AS c FROM memories WHERE repo_id IS NOT NULL AND repo_id != ?")
925
+ .get(identity.repoId).c
926
+ : store
927
+ .prepare("SELECT COUNT(*) AS c FROM memories WHERE project_id IS NOT NULL AND project_id != ?")
928
+ .get(identity.repoId).c;
929
+ return {
930
+ title: "Identidad de repositorio",
931
+ output: JSON.stringify({
932
+ action: "show",
933
+ repo_id: identity.repoId,
934
+ source: identity.source,
935
+ evidence: identity.evidence,
936
+ project_id: identity.projectId,
937
+ memories_total: memoriesTotal,
938
+ memories_repo_id: memoriesUnderRepo,
939
+ memories_project_id: memoriesUnderProject,
940
+ rekey_available: differentlyScoped > 0,
941
+ project_json: existsSync(join(projectRoot, ".kevin", "project.json"))
942
+ ? "present"
943
+ : "absent",
565
944
  }),
566
945
  };
567
946
  },
@@ -575,7 +954,11 @@ export const KevinPlugin = async (input, options) => {
575
954
  // v0.6.0 (K6-023 / plan §5.8) — capabilities come from the
576
955
  // probe() run ONCE at init, so kevin_audit can report the
577
956
  // three emission states ("unavailable" vs "off" vs "on").
578
- const report = buildAudit(store, metrics, capabilities);
957
+ // v0.7.0 (K7-019 / K7-006) — projectId scopes the `truth`
958
+ // block; on a pre-008 DB the block is omitted, partial:true.
959
+ // v0.8.0 (K8-007 / plan §5.7) — the penalized-memory rollup
960
+ // scopes on repoId once the 009 column exists.
961
+ const report = buildAudit(store, metrics, capabilities, projectId, sessionIdentity.repoId);
579
962
  const payload = args.verbose
580
963
  ? report
581
964
  : { ...report, settings: undefined };
@@ -728,9 +1111,13 @@ export const KevinPlugin = async (input, options) => {
728
1111
  .describe("okf | markdown"),
729
1112
  },
730
1113
  async execute(args) {
1114
+ // v0.8.0 (K8-027) — the v1 export is scoped to this
1115
+ // plugin's project: the global database holds several
1116
+ // projects' memories, and a committed export must not
1117
+ // leak another project's knowledge.
731
1118
  const output = args.format === "markdown"
732
- ? exportMarkdown(store)
733
- : exportOkf(store);
1119
+ ? exportMarkdown(store, projectId)
1120
+ : exportOkf(store, projectId);
734
1121
  return {
735
1122
  title: "Export completado",
736
1123
  output,
@@ -802,6 +1189,25 @@ export const KevinPlugin = async (input, options) => {
802
1189
  };
803
1190
  }
804
1191
  const value = args.value ?? "1";
1192
+ // v0.7.0 (K7-003 / plan §5.6, D7-12) — `error_lesson_mode`
1193
+ // is a TEXT enum with a tiny explicit domain. Reject a
1194
+ // value outside `all` / `triage_only` with a structured
1195
+ // error so a typo cannot silently change every
1196
+ // installation's reflection behaviour.
1197
+ if (args.key === "error_lesson_mode") {
1198
+ const allowed = ERROR_LESSON_MODE_VALUES;
1199
+ if (!allowed.includes(value)) {
1200
+ return {
1201
+ title: "kevin_config",
1202
+ output: JSON.stringify({
1203
+ error: "invalid_value",
1204
+ key: args.key,
1205
+ value,
1206
+ allowed_values: allowed,
1207
+ }),
1208
+ };
1209
+ }
1210
+ }
805
1211
  store
806
1212
  .prepare(`INSERT INTO kevin_settings (key, value) VALUES (?, ?)
807
1213
  ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
@@ -812,6 +1218,41 @@ export const KevinPlugin = async (input, options) => {
812
1218
  };
813
1219
  },
814
1220
  }),
1221
+ kevin_facts: tool({
1222
+ description: "Reporta la verdad del repositorio (v0.7.0): los hechos escaneados del proyecto actual y las memorias de-rankadas por contradiccion, con penalti y razones. refresh:true fuerza un re-scan (salta el skip de mtime); por defecto lee los hechos almacenados. Solo lectura salvo el re-scan opcional. Sin LLM ni red.",
1223
+ args: {
1224
+ refresh: tool.schema
1225
+ .boolean()
1226
+ .default(false)
1227
+ .optional()
1228
+ .describe("true: fuerza el re-scan de package.json/tsconfig.json. false (default): lee hechos almacenados."),
1229
+ },
1230
+ async execute(args) {
1231
+ const result = buildKevinFacts({ store, memoryService, repoTruth, projectId }, args.refresh ?? false);
1232
+ return {
1233
+ title: "Hechos del repositorio (v0.7.0)",
1234
+ output: JSON.stringify(result),
1235
+ };
1236
+ },
1237
+ }),
1238
+ kevin_conflicts: tool({
1239
+ description: "Lista, reconoce o resuelve conflictos detectados de Kevin.",
1240
+ args: {
1241
+ action: tool.schema.enum(["list", "acknowledge", "resolve"]),
1242
+ id: tool.schema.string().optional(),
1243
+ keep: tool.schema.string().optional(),
1244
+ status: tool.schema
1245
+ .enum(["open", "acknowledged", "resolved"])
1246
+ .optional(),
1247
+ },
1248
+ async execute(args) {
1249
+ const result = executeKevinConflicts({ store, detector: conflictDetector, projectId }, args.action, args.id, args.keep, args.status);
1250
+ return {
1251
+ title: "Conflictos de Kevin",
1252
+ output: JSON.stringify(result),
1253
+ };
1254
+ },
1255
+ }),
815
1256
  kevin_propose: tool({
816
1257
  description: "Genera propuestas de curacion (dry-run estricto, v0.6.0): crea filas pending y retorna sus diffs unificados. NO escribe en disco, NO marca memorias curadas (D5-08/D6-01).",
817
1258
  args: {
@@ -868,6 +1309,103 @@ export const KevinPlugin = async (input, options) => {
868
1309
  };
869
1310
  },
870
1311
  }),
1312
+ // v0.8.0 (K8-021 / plan §5.5) — kevin_share. The dry run is the
1313
+ // default: nothing is written until the diff is shown. The
1314
+ // write itself goes through the single funnel (applyExport),
1315
+ // and shared_entries_exported counts only entries actually
1316
+ // added to the file.
1317
+ kevin_share: tool({
1318
+ description: "Comparte memorias locales al archivo OKF compartido (v0.8.0, K8-021 / plan 5.5): dry-run por defecto - sin memory_ids selecciona toda memoria curated=1 en o sobre shared_confidence_floor no ya compartida, y devuelve el plan con su diff sin escribir nada. confirm: true escribe via el unico write path (ArtifactWriter, D8-08); con share_requires_approval='1' (default) ademas se requiere confirm para escribir y las memorias no curadas se rechazan con not_curated; las que no alcanzan el floor se rechazan con below_floor; compartir la misma memoria dos veces es un noop en el segundo intento. Nunca escribe otro fichero que no sea okf_path.",
1319
+ args: {
1320
+ memory_ids: tool.schema.array(tool.schema.string()).optional(),
1321
+ dry_run: tool.schema.boolean().optional(),
1322
+ confirm: tool.schema.boolean().optional(),
1323
+ },
1324
+ async execute(args) {
1325
+ const okfPath = join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf"));
1326
+ const requiresApproval = memoryService.getSetting("share_requires_approval", "1") === "1";
1327
+ const dryRun = args.dry_run ?? true;
1328
+ const confirm = args.confirm ?? false;
1329
+ const floor = Number.parseFloat(memoryService.getSetting("shared_confidence_floor", "0.7"));
1330
+ const clampedFloor = Number.isNaN(floor)
1331
+ ? 0.7
1332
+ : Math.min(1, Math.max(0, floor));
1333
+ const ids = args.memory_ids !== undefined && args.memory_ids.length > 0
1334
+ ? args.memory_ids
1335
+ : store
1336
+ .prepare(`SELECT id, evidence_count, recurrence_count
1337
+ FROM memories
1338
+ WHERE layer = 'local' AND curated = 1
1339
+ AND shared_entry_id IS NULL AND repo_id = ?`)
1340
+ .all(sessionIdentity.repoId)
1341
+ .filter((row) => computeConfidence(row.evidence_count ?? 0, row.recurrence_count ?? 0) >= clampedFloor)
1342
+ .map((row) => row.id);
1343
+ const plan = sharedLayer.planExport(ids, okfPath);
1344
+ if (plan.write.outcome === "refused") {
1345
+ return {
1346
+ title: "Exportacion rechazada",
1347
+ output: JSON.stringify({
1348
+ refused: plan.write.reason,
1349
+ okf_path: okfPath,
1350
+ }),
1351
+ };
1352
+ }
1353
+ const base = {
1354
+ memory_ids: ids,
1355
+ entries_added: plan.entriesAdded,
1356
+ okf_path: okfPath,
1357
+ diff: plan.write.diff,
1358
+ };
1359
+ if (dryRun) {
1360
+ return {
1361
+ title: "Plan de exportacion (dry-run)",
1362
+ output: JSON.stringify({ ...base, dry_run: true }),
1363
+ };
1364
+ }
1365
+ if (requiresApproval && !confirm) {
1366
+ return {
1367
+ title: "Confirmacion requerida",
1368
+ output: JSON.stringify({
1369
+ ...base,
1370
+ dry_run: true,
1371
+ confirm_required: true,
1372
+ }),
1373
+ };
1374
+ }
1375
+ // The writer's atomic temp file lives next to the target,
1376
+ // so the .kevin directory must exist before the write
1377
+ // (same prep as RepoIdentity.initProjectFile).
1378
+ mkdirSync(dirname(okfPath), { recursive: true });
1379
+ const applied = sharedLayer.applyExport(plan);
1380
+ if (applied.applied === "written") {
1381
+ metrics.incr("shared_entries_exported", plan.entriesAdded);
1382
+ }
1383
+ return {
1384
+ title: applied.applied === "noop"
1385
+ ? "Nada nuevo que compartir"
1386
+ : "Exportacion aplicada",
1387
+ output: JSON.stringify({
1388
+ ...base,
1389
+ outcome: applied.applied,
1390
+ }),
1391
+ };
1392
+ },
1393
+ }),
1394
+ // v0.8.0 (K8-022 / plan §5.5) — kevin_sync. The name and the
1395
+ // scope are deliberately narrow: "sync" means re-reading a
1396
+ // file that is already on disk. Manual invocation works
1397
+ // regardless of shared_layer_enabled; the automatic idle
1398
+ // re-read below is the one gated by the flag.
1399
+ kevin_sync: tool({
1400
+ description: "Re-lee el archivo OKF compartido ya en disco (v0.8.0, K8-022 / plan 5.5): importa las entradas del archivo okf_path al store compartido y reporta el ImportReport (parsed/folded/rejected/imported/tombstoned/skipped). No hay fetch, push, remoto ni polling (D8-01, Principle 30). Funciona con shared_layer_enabled en '0'; la sincronizacion automatica en session.idle esta gated por shared_layer_enabled === '1' y nunca corre en hot paths.",
1401
+ args: {},
1402
+ async execute() {
1403
+ return {
1404
+ title: "Sincronizacion compartida",
1405
+ output: JSON.stringify(syncSharedLayer()),
1406
+ };
1407
+ },
1408
+ }),
871
1409
  },
872
1410
  "tool.execute.before": async (hookInput, output) => {
873
1411
  rememberToolCall(hookInput.callID, hookInput.tool, output.args);
@@ -1025,6 +1563,31 @@ export const KevinPlugin = async (input, options) => {
1025
1563
  const recurred = memoryService.penalizeRecurringReflectors(sid);
1026
1564
  injector.setRecurrences(recurred, sid);
1027
1565
  patternMiner.mine(projectId);
1566
+ // v0.7.0 (K7-012 / plan §5.4, D7-10) — convention mining is
1567
+ // session.idle-only, default-off, and isolated from the rest of
1568
+ // the idle chain. Mined rules still enter the ordinary Curator
1569
+ // approval path; this step never writes to the repository.
1570
+ try {
1571
+ if (memoryService.getSetting("convention_mining_enabled", "0") === "1") {
1572
+ const conventions = conventionMiner.mine();
1573
+ conventionMiner.emit(conventions);
1574
+ }
1575
+ }
1576
+ catch {
1577
+ // A throwing miner must not reject or truncate the idle chain.
1578
+ }
1579
+ // v0.7.0 (K7-016 / plan §5.5, D7-06) — conflict detection is
1580
+ // surfacing-only on idle. It may create/open conflict rows, but
1581
+ // no idle path can acknowledge or resolve one.
1582
+ try {
1583
+ if (memoryService.getSetting("conflict_detection_enabled", "0") ===
1584
+ "1") {
1585
+ conflictDetector.detect();
1586
+ }
1587
+ }
1588
+ catch {
1589
+ // Conflict surfacing is best-effort and must not reject idle.
1590
+ }
1028
1591
  fireAndForget(Promise.resolve()
1029
1592
  .then(() => causalChain.onSessionIdle(sid))
1030
1593
  // non-blocking — promote is a best-effort pass
@@ -1054,6 +1617,24 @@ export const KevinPlugin = async (input, options) => {
1054
1617
  // best-effort, same pattern as ledger.settle — a
1055
1618
  // curation failure must not break the idle path
1056
1619
  }
1620
+ // v0.8.0 (K8-022 / plan §5.5) — the shared layer
1621
+ // re-read at idle. Gated by shared_layer_enabled ===
1622
+ // "1", a TEXT comparison: '0' is a truthy string, so a
1623
+ // truthiness check would turn the release on for every
1624
+ // installation that upgrades. Never wired into
1625
+ // tool.execute.*, chat.message, system.transform or
1626
+ // session.compacting — the hot-path rule is absolute;
1627
+ // the file-hash skip is what makes the idle cost one
1628
+ // read plus one hash on an unchanged repository.
1629
+ try {
1630
+ if (memoryService.getSetting("shared_layer_enabled", "0") === "1") {
1631
+ syncSharedLayer();
1632
+ }
1633
+ }
1634
+ catch {
1635
+ // best-effort, same pattern as ledger.settle — a
1636
+ // sync failure must not break the idle path
1637
+ }
1057
1638
  }
1058
1639
  metrics.flush();
1059
1640
  }