@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.
- package/README.md +711 -667
- package/dist/ArtifactWriter.js +1 -1
- package/dist/InjectionLedger.js +112 -110
- package/dist/Materializer.d.ts +5 -0
- package/dist/Materializer.js +11 -0
- package/dist/MemoryService.js +93 -91
- package/dist/RepoIdentity.js +1 -1
- package/dist/Retrospective.js +8 -0
- package/dist/Store.d.ts +1 -0
- package/dist/Store.js +14 -2
- package/dist/contract.d.ts +58 -1
- package/dist/contract.js +215 -3
- package/dist/import-host.d.ts +41 -0
- package/dist/import-host.js +286 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +16 -1
- package/dist/kevin_audit.d.ts +16 -0
- package/dist/kevin_audit.js +37 -0
- package/dist/metrics.d.ts +1 -1
- package/dist/metrics.js +22 -0
- package/dist/mif.d.ts +32 -0
- package/dist/mif.js +112 -0
- package/dist/migrations/014_v2_commonwealth.sql +50 -0
- package/dist/okf-shards.d.ts +10 -0
- package/dist/okf-shards.js +103 -0
- package/dist/okf.d.ts +5 -1
- package/dist/okf.js +11 -3
- package/dist/skills-emit.d.ts +38 -0
- package/dist/skills-emit.js +421 -0
- package/dist/skills-validate.d.ts +7 -0
- package/dist/skills-validate.js +236 -0
- package/dist/sources/ClaudeMemorySource.d.ts +10 -0
- package/dist/sources/ClaudeMemorySource.js +36 -0
- package/dist/sources/CodexMemoriesSource.d.ts +10 -0
- package/dist/sources/CodexMemoriesSource.js +37 -0
- package/dist/sources/IdleSync.d.ts +10 -0
- package/dist/sources/IdleSync.js +49 -0
- package/dist/sources/MemorySource.d.ts +19 -0
- package/dist/sources/MemorySource.js +1 -0
- package/dist/sources/OpencodeNativeSource.d.ts +10 -0
- package/dist/sources/OpencodeNativeSource.js +33 -0
- package/dist/sources/OpencodePluginSource.d.ts +9 -0
- package/dist/sources/OpencodePluginSource.js +13 -0
- package/dist/sqlite-adapter.js +1 -1
- 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 =
|
|
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
|
-
|
|
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: "
|
|
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 = "
|
|
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 = "
|
|
127
|
+
export const KEVIN_VERSION = "2.0.0";
|
package/dist/kevin_audit.d.ts
CHANGED
|
@@ -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
|
/**
|