@jmtrin/kevin-core 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +711 -667
  2. package/dist/ArtifactWriter.js +1 -1
  3. package/dist/InjectionLedger.js +112 -110
  4. package/dist/Materializer.d.ts +5 -0
  5. package/dist/Materializer.js +11 -0
  6. package/dist/MemoryService.js +93 -91
  7. package/dist/RepoIdentity.js +1 -1
  8. package/dist/Retrospective.js +8 -0
  9. package/dist/Store.d.ts +1 -0
  10. package/dist/Store.js +14 -2
  11. package/dist/contract.d.ts +58 -1
  12. package/dist/contract.js +215 -3
  13. package/dist/import-host.d.ts +41 -0
  14. package/dist/import-host.js +286 -0
  15. package/dist/index.d.ts +9 -2
  16. package/dist/index.js +16 -1
  17. package/dist/kevin_audit.d.ts +16 -0
  18. package/dist/kevin_audit.js +37 -0
  19. package/dist/metrics.d.ts +1 -1
  20. package/dist/metrics.js +22 -0
  21. package/dist/mif.d.ts +32 -0
  22. package/dist/mif.js +112 -0
  23. package/dist/migrations/014_v2_commonwealth.sql +50 -0
  24. package/dist/okf-shards.d.ts +10 -0
  25. package/dist/okf-shards.js +103 -0
  26. package/dist/okf.d.ts +5 -1
  27. package/dist/okf.js +11 -3
  28. package/dist/skills-emit.d.ts +38 -0
  29. package/dist/skills-emit.js +421 -0
  30. package/dist/skills-validate.d.ts +7 -0
  31. package/dist/skills-validate.js +236 -0
  32. package/dist/sources/ClaudeMemorySource.d.ts +10 -0
  33. package/dist/sources/ClaudeMemorySource.js +36 -0
  34. package/dist/sources/CodexMemoriesSource.d.ts +10 -0
  35. package/dist/sources/CodexMemoriesSource.js +37 -0
  36. package/dist/sources/IdleSync.d.ts +10 -0
  37. package/dist/sources/IdleSync.js +49 -0
  38. package/dist/sources/MemorySource.d.ts +19 -0
  39. package/dist/sources/MemorySource.js +1 -0
  40. package/dist/sources/OpencodeNativeSource.d.ts +10 -0
  41. package/dist/sources/OpencodeNativeSource.js +33 -0
  42. package/dist/sources/OpencodePluginSource.d.ts +9 -0
  43. package/dist/sources/OpencodePluginSource.js +13 -0
  44. package/dist/sqlite-adapter.js +1 -1
  45. package/package.json +24 -26
package/dist/contract.js CHANGED
@@ -45,7 +45,115 @@ export function resolveScanRoots(env) {
45
45
  // Fallback to monorepo relative (even if not yet built)
46
46
  return [monorepoPlugin, monorepoCore];
47
47
  }
48
- export const CONTRACT_VERSION = 1;
48
+ export const CONTRACT_VERSION = 2;
49
+ // v2.0.0 (K16-001 / plan §4.1) — C-10 core exports (names + kinds, since 1.3.0)
50
+ // Explicit export list at packages/core/src/index.ts — keep minimal.
51
+ export const CONTRACT_CORE_EXPORTS = [
52
+ { name: "Archiver", kind: "class" },
53
+ { name: "ArtifactWriter", kind: "class" },
54
+ { name: "CausalChain", kind: "class" },
55
+ { name: "ChatBridge", kind: "module" },
56
+ { name: "ConflictDetector", kind: "class" },
57
+ { name: "ContextInjector", kind: "class" },
58
+ { name: "ConventionMiner", kind: "class" },
59
+ { name: "Curator", kind: "class" },
60
+ { name: "DashboardHtml", kind: "module" },
61
+ { name: "Feedback", kind: "class" },
62
+ { name: "HookLiveness", kind: "class" },
63
+ { name: "InjectionLedger", kind: "class" },
64
+ { name: "LessonFixer", kind: "module" },
65
+ { name: "Materializer", kind: "class" },
66
+ { name: "MemoryService", kind: "class" },
67
+ { name: "Migrate", kind: "class" },
68
+ { name: "PatternMiner", kind: "class" },
69
+ { name: "QualityGate", kind: "class" },
70
+ { name: "Reflector", kind: "class" },
71
+ { name: "RepoIdentity", kind: "module" },
72
+ { name: "RepoTruth", kind: "class" },
73
+ { name: "Retrospective", kind: "class" },
74
+ { name: "SharedLayer", kind: "class" },
75
+ { name: "Store", kind: "class" },
76
+ { name: "ToolCallObserver", kind: "class" },
77
+ { name: "TuiSnapshots", kind: "module" },
78
+ { name: "buildAudit", kind: "function" },
79
+ { name: "buildDoctor", kind: "function" },
80
+ { name: "buildKevinContract", kind: "function" },
81
+ { name: "computeConfidence", kind: "function" },
82
+ { name: "describeContract", kind: "function" },
83
+ { name: "escapeForFence", kind: "function" },
84
+ { name: "escapeForMarkerBlock", kind: "function" },
85
+ { name: "fingerprint", kind: "function" },
86
+ { name: "fnv1a64", kind: "function" },
87
+ { name: "kevin_approve", kind: "function" },
88
+ { name: "kevin_bench", kind: "function" },
89
+ { name: "kevin_forget", kind: "function" },
90
+ { name: "kevin_why", kind: "function" },
91
+ { name: "resolveEnv", kind: "function" },
92
+ { name: "uuidv7", kind: "function" },
93
+ ];
94
+ // v2.0.0 (K16-001) — C-11 MCP tool names/shapes + refusal vocabulary, since 1.4.0
95
+ export const CONTRACT_MCP_TOOLS = [
96
+ "approve",
97
+ "feedback",
98
+ "get",
99
+ "ping",
100
+ "query",
101
+ "recall",
102
+ "save",
103
+ "share",
104
+ "status",
105
+ "trace",
106
+ "why",
107
+ ];
108
+ export const CONTRACT_MCP_REFUSALS = [
109
+ "disabled",
110
+ "repo_mismatch",
111
+ "not_okf",
112
+ "version_ahead",
113
+ "unknown_entry",
114
+ "parse_damaged",
115
+ "line_too_long",
116
+ "below_floor",
117
+ "not_curated",
118
+ "too_many_entries",
119
+ ];
120
+ // v2.0.0 (K16-001) — C-12 emitted skills layout, since 1.5.0
121
+ export const CONTRACT_SKILLS_LAYOUT = {
122
+ canonical_dir: ".agents/skills",
123
+ skill_dir: "kevin-knowledge",
124
+ files: ["SKILL.md", "references/*.md"],
125
+ frontmatter_fields: ["name", "description", "metadata.generator", "metadata.repo_id"],
126
+ mirror_policy: "copy (not symlink) to .claude/skills and .cursor/skills when enabled",
127
+ };
128
+ // v2.0.0 (K16-001) — C-13 MIF profile, since 1.5.0
129
+ export const CONTRACT_MIF_PROFILE = {
130
+ envelope_version: 1,
131
+ format: "mif",
132
+ field_mapping: {
133
+ entry_id: "id",
134
+ statement: "content",
135
+ type: "type",
136
+ scope: "metadata.scope",
137
+ fingerprint: "metadata.fingerprint",
138
+ confidence: "metadata.confidence",
139
+ created_at: "timestamp",
140
+ },
141
+ redaction: "SECRET_PATTERNS (PII)",
142
+ dedup: "content-hash (fingerprint)",
143
+ vendor_extensions_preserved: true,
144
+ };
145
+ // v2.0.0 (K16-001) — C-14 MemorySources, since 2.0.0
146
+ export const CONTRACT_MEMORY_SOURCES = {
147
+ sources: [
148
+ { name: "opencode-plugin", precedence: 10 },
149
+ { name: "claude-memory", precedence: 20 },
150
+ { name: "codex-memories", precedence: 30 },
151
+ { name: "opencode-native", precedence: 40 },
152
+ ],
153
+ dedup_rule: "lower precedence wins attribution; losers counted in source_dedup_skips_total",
154
+ conflict_kind: "source_pair",
155
+ precedence_order: [10, 20, 30, 40],
156
+ };
49
157
  // Tool names — the live source of truth for C-03. Any rename here must
50
158
  // flow to the contract (K10-006 AC: renaming a tool changes C-03).
51
159
  // Keep this array adjacent to the actual tool registrations in index.ts
@@ -85,6 +193,7 @@ export const CONTRACT_TOOL_ADDITIONS = [
85
193
  { name: "kevin_bench", since: "1.0.0" },
86
194
  { name: "kevin_contract", since: "1.0.0" },
87
195
  { name: "kevin_forget", since: "1.1.0" },
196
+ { name: "kevin_sources", since: "2.0.0" },
88
197
  ];
89
198
  /**
90
199
  * v1.1.0 (K11-007 / plan §5.1, D11-01) — metric keys added after the freeze,
@@ -103,6 +212,14 @@ export const CONTRACT_METRIC_ADDITIONS = [
103
212
  { name: "mcp_writes_accepted", since: "1.4.0" },
104
213
  { name: "mcp_writes_refused", since: "1.4.0" },
105
214
  { name: "mcp_errors_total", since: "1.4.0" },
215
+ // v1.5.0 (K15-001 / plan §4) — Diaspora metrics; lazy-incr.
216
+ { name: "mif_exports_total", since: "1.5.0" },
217
+ { name: "mif_imports_total", since: "1.5.0" },
218
+ { name: "skills_emitted_total", since: "1.5.0" },
219
+ // v2.0.0 (K16-012 / plan §4.4) — Commonwealth metrics
220
+ { name: "source_syncs_total", since: "2.0.0" },
221
+ { name: "source_dedup_skips_total", since: "2.0.0" },
222
+ { name: "okf_v3_files_written", since: "2.0.0" },
106
223
  ];
107
224
  /**
108
225
  * v1.4.0 (K14-006 / plan §4.3) — config keys added after the freeze,
@@ -112,6 +229,29 @@ export const CONTRACT_CONFIG_ADDITIONS = [
112
229
  { name: "mcp_approve_enabled", since: "1.4.0" },
113
230
  { name: "mcp_repo_override", since: "1.4.0" },
114
231
  { name: "mcp_write_enabled", since: "1.4.0" },
232
+ // v1.5.0 (K15-001 / plan §4) — Diaspora settings; since 1.5.0.
233
+ { name: "import_host_memory", since: "1.5.0" },
234
+ { name: "skills_canonical_dir", since: "1.5.0" },
235
+ { name: "skills_mirror_claude", since: "1.5.0" },
236
+ { name: "skills_mirror_cursor", since: "1.5.0" },
237
+ // v2.0.0 (K16-013 / plan §4.4) — Commonwealth settings (retirement of import_host_memory handled via removals)
238
+ { name: "okf_write_version", since: "2.0.0" },
239
+ { name: "source_claude_memory", since: "2.0.0" },
240
+ { name: "source_codex_memories", since: "2.0.0" },
241
+ { name: "source_opencode_native", since: "2.0.0" },
242
+ { name: "sources_enabled", since: "2.0.0" },
243
+ ];
244
+ /**
245
+ * v2.0.0 (K16-004 / plan §5.1) — removed settings (retirements)
246
+ * Each entry carries `since` 2.0.0 and optional replacement pointer.
247
+ * Succession test treats these as LEGITIMATE removals via `removed` annotation.
248
+ */
249
+ export const CONTRACT_REMOVED_CONFIG_KEYS = [
250
+ {
251
+ name: "import_host_memory",
252
+ since: "2.0.0",
253
+ replacement: "sources_enabled + source_claude_memory/source_codex_memories",
254
+ },
115
255
  ];
116
256
  /**
117
257
  * v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
@@ -157,7 +297,14 @@ export function describeContract(_input) {
157
297
  const baseConfigKeys = [...KEVIN_CONFIG_KEYS]
158
298
  .filter((k) => !configAdditions.some((a) => a.name === k))
159
299
  .sort();
160
- const settingValue = { keys: [...baseConfigKeys, ...configAdditions].flat() };
300
+ // v2.0.0 (K16-004/005) removed keys are filtered from `keys` and surfaced via `removed` annotation with since 2.0.0
301
+ const removedConfig = [...CONTRACT_REMOVED_CONFIG_KEYS].sort((a, b) => a.name.localeCompare(b.name));
302
+ const filteredConfigAdditions = configAdditions.filter((a) => !removedConfig.some((r) => r.name === a.name));
303
+ const settingValue = {
304
+ keys: [...baseConfigKeys, ...filteredConfigAdditions].flat(),
305
+ };
306
+ if (removedConfig.length > 0)
307
+ settingValue.removed = removedConfig;
161
308
  // v1.1.0 — metric keys added after freeze carry `since` (C-05)
162
309
  const metricAdditions = [...CONTRACT_METRIC_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
163
310
  const baseMetricKeys = Object.keys(METRIC_KEY_LABELS)
@@ -231,7 +378,7 @@ export function describeContract(_input) {
231
378
  stability: "forward-only",
232
379
  since: "0.1.0",
233
380
  value: {
234
- schema_version: "013",
381
+ schema_version: "014",
235
382
  migrations_forward_only: true,
236
383
  },
237
384
  },
@@ -265,6 +412,44 @@ export function describeContract(_input) {
265
412
  boundary: [{ name: BOUNDARY_INVARIANT, since: "1.0.0" }],
266
413
  },
267
414
  },
415
+ {
416
+ id: "C-10",
417
+ title: "Core public exports",
418
+ stability: "frozen",
419
+ since: "1.3.0",
420
+ value: { exports: [...CONTRACT_CORE_EXPORTS].sort((a, b) => a.name.localeCompare(b.name)) },
421
+ },
422
+ {
423
+ id: "C-11",
424
+ title: "MCP tool names and refusal vocabulary",
425
+ stability: "frozen",
426
+ since: "1.4.0",
427
+ value: {
428
+ tools: [...CONTRACT_MCP_TOOLS].sort(),
429
+ refusals: [...CONTRACT_MCP_REFUSALS].sort(),
430
+ },
431
+ },
432
+ {
433
+ id: "C-12",
434
+ title: "Emitted skills layout",
435
+ stability: "frozen",
436
+ since: "1.5.0",
437
+ value: CONTRACT_SKILLS_LAYOUT,
438
+ },
439
+ {
440
+ id: "C-13",
441
+ title: "MIF profile",
442
+ stability: "frozen",
443
+ since: "1.5.0",
444
+ value: CONTRACT_MIF_PROFILE,
445
+ },
446
+ {
447
+ id: "C-14",
448
+ title: "MemorySources",
449
+ stability: "frozen",
450
+ since: "2.0.0",
451
+ value: CONTRACT_MEMORY_SOURCES,
452
+ },
268
453
  ];
269
454
  return { contractVersion: CONTRACT_VERSION, clauses };
270
455
  }
@@ -368,6 +553,33 @@ export function diffContract(golden, live) {
368
553
  // Normalize members: for arrays we care about set equality
369
554
  for (const [mem, _gv] of gMembers) {
370
555
  if (!lMembers.has(mem)) {
556
+ // v2.0.0 (K16-005) — allow removals that are annotated via `removed` array with since (e.g. import_host_memory)
557
+ const liveVal = l.value;
558
+ if (Array.isArray(liveVal.removed)) {
559
+ const entry = liveVal.removed.find((e) => {
560
+ if (typeof e === "string")
561
+ return e === mem;
562
+ if (e !== null &&
563
+ typeof e === "object" &&
564
+ "name" in e)
565
+ return e.name === mem;
566
+ return false;
567
+ });
568
+ const hasSince = entry !== null &&
569
+ typeof entry === "object" &&
570
+ typeof entry.since === "string" &&
571
+ entry.since !== "";
572
+ if (entry !== undefined && hasSince) {
573
+ // Annotated removal — treat as allowed (like added_ok). Do not flag as confusing.
574
+ diffs.push({
575
+ clauseId: g.id,
576
+ path: `${g.id}.${mem}`,
577
+ kind: "added_ok",
578
+ remedy: `Member ${mem} in ${g.id} was removed with since. Allowed in 2.0.0.`,
579
+ });
580
+ continue;
581
+ }
582
+ }
371
583
  diffs.push({
372
584
  clauseId: g.id,
373
585
  path: `${g.id}.${mem}`,
@@ -0,0 +1,41 @@
1
+ import { type KevinEnv } from "./env.js";
2
+ import type { MemoryService } from "./MemoryService.js";
3
+ import type { Store } from "./Store.js";
4
+ import type { Metrics } from "./metrics.js";
5
+ export interface HostImportReport {
6
+ files_scanned: number;
7
+ candidates: number;
8
+ saved: number;
9
+ duplicates: number;
10
+ skipped_weak: number;
11
+ error?: string;
12
+ hint?: string;
13
+ truncated?: boolean;
14
+ skipped_files?: number;
15
+ }
16
+ export declare function parseClaudeMemory(dataRoot: string, env?: KevinEnv): {
17
+ candidates: {
18
+ content: string;
19
+ type: string;
20
+ }[];
21
+ files_scanned: number;
22
+ skipped_files: number;
23
+ truncated: boolean;
24
+ };
25
+ export declare function parseCodexMemories(dataRoot: string, env?: KevinEnv): {
26
+ candidates: {
27
+ content: string;
28
+ type: string;
29
+ }[];
30
+ files_scanned: number;
31
+ skipped_files: number;
32
+ truncated: boolean;
33
+ };
34
+ export declare function importHostMemories(opts: {
35
+ store: Store;
36
+ memoryService: MemoryService;
37
+ metrics?: Metrics;
38
+ env?: KevinEnv;
39
+ dataRoot?: string;
40
+ source: "claude-memory" | "codex-memories";
41
+ }): HostImportReport;
@@ -0,0 +1,286 @@
1
+ // K15-011/012/013 — Host importers (plan §4.5)
2
+ // Defensive markdown parsers, no YAML lib, same naive parser as validator.
3
+ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { resolveEnv } from "./env.js";
6
+ import { fingerprint as computeFingerprint } from "./fingerprint.js";
7
+ import { QualityGate } from "./QualityGate.js";
8
+ const MAX_FILE_BYTES = 1 * 1024 * 1024;
9
+ const MAX_CANDIDATES = 5000;
10
+ const CLAUDE_TYPE_MAP = {
11
+ user_preference: "context",
12
+ project_context: "context",
13
+ correction: "rule",
14
+ code_pattern: "pattern",
15
+ };
16
+ // naive frontmatter parse for Claude topic files: extract type field
17
+ function parseFrontmatterType(content) {
18
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
19
+ if (lines[0]?.trim() !== "---")
20
+ return null;
21
+ let type = null;
22
+ for (let i = 1; i < lines.length; i++) {
23
+ if (lines[i].trim() === "---")
24
+ break;
25
+ const m = lines[i].match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
26
+ if (m && m[1].toLowerCase() === "type") {
27
+ type = m[2].trim().replace(/^["']|["']$/g, "");
28
+ }
29
+ }
30
+ return type;
31
+ }
32
+ function extractBullets(content) {
33
+ // remove frontmatter block first
34
+ const withoutFm = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
35
+ const lines = withoutFm.split("\n");
36
+ const out = [];
37
+ for (const line of lines) {
38
+ const m = line.match(/^\s*[-*]\s+(.*)$/);
39
+ if (m) {
40
+ const trimmed = m[1].trim();
41
+ if (trimmed !== "")
42
+ out.push(trimmed);
43
+ }
44
+ else {
45
+ // also handle heading/bullet? codex may have headings
46
+ const hm = line.match(/^\s*#{1,6}\s+(.*)$/);
47
+ if (hm && hm[1].trim() !== "") {
48
+ // treat heading text as candidate? For codex, headings+bulles both
49
+ // but we will extract bullets only for claude; codex extractor handles headings separately
50
+ }
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+ function safeRead(filePath) {
56
+ try {
57
+ const st = lstatSync(filePath);
58
+ if (st.isSymbolicLink())
59
+ return null;
60
+ if (!st.isFile())
61
+ return null;
62
+ if (st.size > MAX_FILE_BYTES)
63
+ return null;
64
+ return readFileSync(filePath, "utf8");
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ export function parseClaudeMemory(dataRoot, env) {
71
+ const root = join(resolveEnv(env).dataRoot ?? dataRoot, "claude", "projects");
72
+ // but dataRoot is already resolveEnv(...).dataRoot; we accept direct
73
+ const scanRoot = join(dataRoot, "claude", "projects");
74
+ const candidates = [];
75
+ let files_scanned = 0;
76
+ let skipped_files = 0;
77
+ if (!existsSync(scanRoot))
78
+ return { candidates, files_scanned, skipped_files, truncated: false };
79
+ let projectDirs = [];
80
+ try {
81
+ projectDirs = readdirSync(scanRoot);
82
+ }
83
+ catch {
84
+ return { candidates, files_scanned, skipped_files, truncated: false };
85
+ }
86
+ for (const proj of projectDirs) {
87
+ const memDir = join(scanRoot, proj, "memory");
88
+ if (!existsSync(memDir))
89
+ continue;
90
+ let files = [];
91
+ try {
92
+ files = readdirSync(memDir);
93
+ }
94
+ catch {
95
+ continue;
96
+ }
97
+ for (const f of files) {
98
+ if (!f.endsWith(".md"))
99
+ continue;
100
+ const full = join(memDir, f);
101
+ files_scanned++;
102
+ if (f === "MEMORY.md") {
103
+ // index only, skip harvesting
104
+ continue;
105
+ }
106
+ const content = safeRead(full);
107
+ if (content === null) {
108
+ skipped_files++;
109
+ continue;
110
+ }
111
+ // M-04: frontmatter without closing --- should skip
112
+ if (content.trimStart().startsWith("---")) {
113
+ const fmLines = content.replace(/\r\n/g, "\n").split("\n");
114
+ let hasClosing = false;
115
+ for (let i = 1; i < fmLines.length; i++) {
116
+ if (fmLines[i].trim() === "---") {
117
+ hasClosing = true;
118
+ break;
119
+ }
120
+ }
121
+ if (!hasClosing) {
122
+ skipped_files++;
123
+ continue;
124
+ }
125
+ }
126
+ const rawType = parseFrontmatterType(content);
127
+ if (rawType === null) {
128
+ // check if frontmatter missing or malformed: count as skipped but continue
129
+ // if file has no frontmatter, we still skip but not throw
130
+ // For test: one malformed topic file should be counted as skipped
131
+ if (!content.trim().startsWith("---")) {
132
+ skipped_files++;
133
+ continue;
134
+ }
135
+ // if frontmatter exists but type missing, map to context
136
+ }
137
+ const mapped = rawType ? (CLAUDE_TYPE_MAP[rawType.toLowerCase()] ?? "context") : "context";
138
+ // bullets
139
+ const bullets = extractBullets(content);
140
+ if (bullets.length === 0) {
141
+ // no bullets -> skip but not error
142
+ continue;
143
+ }
144
+ for (const b of bullets) {
145
+ if (candidates.length >= MAX_CANDIDATES)
146
+ break;
147
+ candidates.push({ content: b, type: mapped });
148
+ }
149
+ }
150
+ }
151
+ const truncated = candidates.length >= MAX_CANDIDATES;
152
+ return { candidates, files_scanned, skipped_files, truncated };
153
+ }
154
+ export function parseCodexMemories(dataRoot, env) {
155
+ const scanRoot = join(dataRoot, "codex", "memories");
156
+ const candidates = [];
157
+ let files_scanned = 0;
158
+ let skipped_files = 0;
159
+ if (!existsSync(scanRoot))
160
+ return { candidates, files_scanned, skipped_files, truncated: false };
161
+ let files = [];
162
+ try {
163
+ files = readdirSync(scanRoot);
164
+ }
165
+ catch {
166
+ return { candidates, files_scanned, skipped_files, truncated: false };
167
+ }
168
+ const targets = ["memory_summary.md", "MEMORY.md"];
169
+ for (const t of targets) {
170
+ if (!files.includes(t))
171
+ continue;
172
+ const full = join(scanRoot, t);
173
+ files_scanned++;
174
+ const content = safeRead(full);
175
+ if (content === null) {
176
+ skipped_files++;
177
+ continue;
178
+ }
179
+ // extract headings and bullets: both become candidates type context
180
+ const lines = content.split("\n");
181
+ for (const line of lines) {
182
+ const bullet = line.match(/^\s*[-*]\s+(.*)$/);
183
+ if (bullet && bullet[1].trim() !== "") {
184
+ if (candidates.length >= MAX_CANDIDATES)
185
+ break;
186
+ candidates.push({ content: bullet[1].trim(), type: "context" });
187
+ continue;
188
+ }
189
+ const heading = line.match(/^\s*#{1,6}\s+(.*)$/);
190
+ if (heading && heading[1].trim() !== "") {
191
+ // heading text as candidate (avoid duplicates where heading is also bullet)
192
+ // we treat heading as candidate as well
193
+ if (candidates.length >= MAX_CANDIDATES)
194
+ break;
195
+ candidates.push({ content: heading[1].trim(), type: "context" });
196
+ }
197
+ }
198
+ }
199
+ const truncated2 = candidates.length >= MAX_CANDIDATES;
200
+ return { candidates, files_scanned, skipped_files, truncated: truncated2 };
201
+ }
202
+ export function importHostMemories(opts) {
203
+ const dataRoot = opts.dataRoot ?? resolveEnv(opts.env).dataRoot;
204
+ // gate
205
+ const gate = opts.memoryService.getSetting("import_host_memory", "0");
206
+ if (gate !== "1") {
207
+ return {
208
+ files_scanned: 0,
209
+ candidates: 0,
210
+ saved: 0,
211
+ duplicates: 0,
212
+ skipped_weak: 0,
213
+ error: "disabled",
214
+ hint: "Enable with kevin_config set import_host_memory 1",
215
+ };
216
+ }
217
+ let parsed;
218
+ if (opts.source === "claude-memory") {
219
+ parsed = parseClaudeMemory(dataRoot, opts.env);
220
+ }
221
+ else {
222
+ parsed = parseCodexMemories(dataRoot, opts.env);
223
+ }
224
+ let candidates = parsed.candidates;
225
+ // M-05: propagate truncated correctly from parsers
226
+ const truncated = (parsed.truncated ?? false) || candidates.length >= MAX_CANDIDATES;
227
+ if (candidates.length > MAX_CANDIDATES)
228
+ candidates = candidates.slice(0, MAX_CANDIDATES);
229
+ // pipeline per candidate: redact -> fingerprint dedup (existing rows) -> quality-gate classification -> save
230
+ let saved = 0;
231
+ let duplicates = 0;
232
+ let skipped_weak = 0;
233
+ // dedup intra-run via fingerprint set
234
+ const seenFingerprints = new Set();
235
+ // existing fingerprints from DB
236
+ const existingRows = opts.store.prepare("SELECT fingerprint FROM memories WHERE fingerprint IS NOT NULL").all();
237
+ for (const r of existingRows)
238
+ seenFingerprints.add(r.fingerprint);
239
+ for (const c of candidates) {
240
+ // redact (paths and private)
241
+ let content = c.content;
242
+ // simple redact paths: reuse import? For now strip private blocks
243
+ content = content.replace(/<private\b[^>]*>[\s\S]*?<\/private>/gi, (m) => `<private: redacted ${m.length} chars>`);
244
+ // fingerprint dedup
245
+ const fp = computeFingerprint(content);
246
+ if (seenFingerprints.has(fp)) {
247
+ duplicates++;
248
+ continue;
249
+ }
250
+ seenFingerprints.add(fp);
251
+ // quality-gate classification: weak stored-not-injected naturally
252
+ // We simulate: isActionable and strength via QualityGate.evaluate
253
+ // For host import, we consider type context is weak if generic? But we will use evaluate to decide
254
+ const q = QualityGate.evaluate({ errorType: "unknown", suggestion: content }, null, "unknown");
255
+ // If weak and not actionable, count as skipped_weak but still store? Plan says weak entries stored-but-NOT-injected
256
+ // So we still save but count as weak
257
+ const isWeak = q.strength === "weak" && !q.isActionable;
258
+ if (isWeak)
259
+ skipped_weak++;
260
+ // save
261
+ try {
262
+ opts.memoryService.save({
263
+ type: c.type,
264
+ content,
265
+ scope: "project",
266
+ origin: "imported",
267
+ fingerprint: fp,
268
+ metadata: { source: opts.source },
269
+ // evidence_count 0 => confidence low naturally, stored but not injected
270
+ });
271
+ saved++;
272
+ }
273
+ catch {
274
+ // skip on error
275
+ }
276
+ }
277
+ return {
278
+ files_scanned: parsed.files_scanned,
279
+ candidates: parsed.candidates.length,
280
+ saved,
281
+ duplicates,
282
+ skipped_weak,
283
+ truncated: truncated || undefined,
284
+ skipped_files: parsed.skipped_files || undefined,
285
+ };
286
+ }
package/dist/index.d.ts CHANGED
@@ -66,7 +66,14 @@ export * from "./kevin_propose.js";
66
66
  export { kevinPublish } from "./kevin_publish.js";
67
67
  export type { PublishResult } from "./kevin_publish.js";
68
68
  export * from "./kevin_why.js";
69
+ export * from "./skills-validate.js";
70
+ export * from "./skills-emit.js";
71
+ export * from "./mif.js";
72
+ export * from "./import-host.js";
73
+ export * from "./okf-shards.js";
74
+ export * from "./sources/MemorySource.js";
75
+ export * from "./sources/IdleSync.js";
69
76
  export * from "./env.js";
70
- export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled", "mcp_write_enabled", "mcp_approve_enabled", "mcp_repo_override"];
77
+ export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled", "mcp_write_enabled", "mcp_approve_enabled", "mcp_repo_override", "skills_canonical_dir", "skills_mirror_claude", "skills_mirror_cursor", "sources_enabled", "source_claude_memory", "source_codex_memories", "source_opencode_native", "okf_write_version"];
71
78
  export declare const ERROR_LESSON_MODE_VALUES: readonly ["all", "triage_only"];
72
- export declare const KEVIN_VERSION = "1.4.0";
79
+ export declare const KEVIN_VERSION = "2.0.0";
package/dist/index.js CHANGED
@@ -68,6 +68,13 @@ export { handleNative } from "./kevin_native.js";
68
68
  export * from "./kevin_propose.js";
69
69
  export { kevinPublish } from "./kevin_publish.js";
70
70
  export * from "./kevin_why.js";
71
+ export * from "./skills-validate.js";
72
+ export * from "./skills-emit.js";
73
+ export * from "./mif.js";
74
+ export * from "./import-host.js";
75
+ export * from "./okf-shards.js";
76
+ export * from "./sources/MemorySource.js";
77
+ export * from "./sources/IdleSync.js";
71
78
  // KEVIN_CONFIG_KEYS and related constants — moved from adapter index so core owns the source of truth (C-04).
72
79
  // Duplicated here to allow adapter to import from core; adapter will re-export them.
73
80
  export * from "./env.js";
@@ -107,6 +114,14 @@ export const KEVIN_CONFIG_KEYS = [
107
114
  "mcp_write_enabled",
108
115
  "mcp_approve_enabled",
109
116
  "mcp_repo_override",
117
+ "skills_canonical_dir",
118
+ "skills_mirror_claude",
119
+ "skills_mirror_cursor",
120
+ "sources_enabled",
121
+ "source_claude_memory",
122
+ "source_codex_memories",
123
+ "source_opencode_native",
124
+ "okf_write_version",
110
125
  ];
111
126
  export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
112
- export const KEVIN_VERSION = "1.4.0";
127
+ export const KEVIN_VERSION = "2.0.0";
@@ -185,6 +185,22 @@ export interface AuditReport {
185
185
  mcp: number;
186
186
  };
187
187
  };
188
+ channels_v2?: {
189
+ push: {
190
+ injections_total: number;
191
+ precision_rate: number;
192
+ coverage_rate: number;
193
+ };
194
+ mcp: {
195
+ injections_total: number;
196
+ precision_rate?: number;
197
+ coverage_rate?: number;
198
+ };
199
+ pull: {
200
+ registered_surfaces: number;
201
+ note: string;
202
+ };
203
+ };
188
204
  partial: boolean;
189
205
  }
190
206
  /**