@jmtrin/opencode-kevin 1.2.0 → 1.3.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 (141) hide show
  1. package/dist/plugin/index.d.ts +4 -3
  2. package/dist/plugin/index.js +122 -113
  3. package/dist/plugin/native.d.ts +2 -2
  4. package/dist/plugin/native.js +2 -2
  5. package/package.json +56 -68
  6. package/LICENSE +0 -21
  7. package/README.md +0 -605
  8. package/dist/migrations/001_initial.sql +0 -92
  9. package/dist/migrations/002_indexes.sql +0 -14
  10. package/dist/migrations/003_v02_signal.sql +0 -58
  11. package/dist/migrations/004_v03_knowledge.sql +0 -138
  12. package/dist/migrations/005_v04_signal.sql +0 -57
  13. package/dist/migrations/006_v05_glassbox.sql +0 -118
  14. package/dist/migrations/007_v06_pull.sql +0 -145
  15. package/dist/migrations/008_v07_truth.sql +0 -124
  16. package/dist/migrations/009_v08_team.sql +0 -100
  17. package/dist/migrations/010_v09_native.sql +0 -78
  18. package/dist/migrations/011_v10_proven.sql +0 -69
  19. package/dist/migrations/012_v11_drift.sql +0 -24
  20. package/dist/plugin/Archiver.d.ts +0 -42
  21. package/dist/plugin/Archiver.js +0 -83
  22. package/dist/plugin/ArtifactWriter.d.ts +0 -93
  23. package/dist/plugin/ArtifactWriter.js +0 -291
  24. package/dist/plugin/CausalChain.d.ts +0 -22
  25. package/dist/plugin/CausalChain.js +0 -197
  26. package/dist/plugin/ChatBridge.d.ts +0 -41
  27. package/dist/plugin/ChatBridge.js +0 -103
  28. package/dist/plugin/ConflictDetector.d.ts +0 -35
  29. package/dist/plugin/ConflictDetector.js +0 -260
  30. package/dist/plugin/ContextInjector.d.ts +0 -222
  31. package/dist/plugin/ContextInjector.js +0 -533
  32. package/dist/plugin/ConventionMiner.d.ts +0 -35
  33. package/dist/plugin/ConventionMiner.js +0 -242
  34. package/dist/plugin/Curator.d.ts +0 -116
  35. package/dist/plugin/Curator.js +0 -324
  36. package/dist/plugin/DashboardHtml.d.ts +0 -5
  37. package/dist/plugin/DashboardHtml.js +0 -180
  38. package/dist/plugin/Feedback.d.ts +0 -67
  39. package/dist/plugin/Feedback.js +0 -120
  40. package/dist/plugin/HookLiveness.d.ts +0 -88
  41. package/dist/plugin/HookLiveness.js +0 -322
  42. package/dist/plugin/InjectionLedger.d.ts +0 -98
  43. package/dist/plugin/InjectionLedger.js +0 -295
  44. package/dist/plugin/LessonFixer.d.ts +0 -44
  45. package/dist/plugin/LessonFixer.js +0 -45
  46. package/dist/plugin/Materializer.d.ts +0 -83
  47. package/dist/plugin/Materializer.js +0 -181
  48. package/dist/plugin/MemoryService.d.ts +0 -360
  49. package/dist/plugin/MemoryService.js +0 -1242
  50. package/dist/plugin/Migrate.d.ts +0 -17
  51. package/dist/plugin/Migrate.js +0 -271
  52. package/dist/plugin/PatternMiner.d.ts +0 -49
  53. package/dist/plugin/PatternMiner.js +0 -132
  54. package/dist/plugin/QualityGate.d.ts +0 -124
  55. package/dist/plugin/QualityGate.js +0 -119
  56. package/dist/plugin/Reflector.d.ts +0 -91
  57. package/dist/plugin/Reflector.js +0 -307
  58. package/dist/plugin/RepoIdentity.d.ts +0 -124
  59. package/dist/plugin/RepoIdentity.js +0 -300
  60. package/dist/plugin/RepoTruth.d.ts +0 -80
  61. package/dist/plugin/RepoTruth.js +0 -599
  62. package/dist/plugin/Retrospective.d.ts +0 -17
  63. package/dist/plugin/Retrospective.js +0 -216
  64. package/dist/plugin/SharedLayer.d.ts +0 -159
  65. package/dist/plugin/SharedLayer.js +0 -462
  66. package/dist/plugin/Store.d.ts +0 -29
  67. package/dist/plugin/Store.js +0 -50
  68. package/dist/plugin/ToolCallObserver.d.ts +0 -32
  69. package/dist/plugin/ToolCallObserver.js +0 -201
  70. package/dist/plugin/TuiActions.d.ts +0 -43
  71. package/dist/plugin/TuiActions.js +0 -181
  72. package/dist/plugin/TuiSnapshots.d.ts +0 -24
  73. package/dist/plugin/TuiSnapshots.js +0 -158
  74. package/dist/plugin/columns.d.ts +0 -11
  75. package/dist/plugin/columns.js +0 -54
  76. package/dist/plugin/confidence.d.ts +0 -8
  77. package/dist/plugin/confidence.js +0 -34
  78. package/dist/plugin/contract.d.ts +0 -54
  79. package/dist/plugin/contract.js +0 -386
  80. package/dist/plugin/diff.d.ts +0 -8
  81. package/dist/plugin/diff.js +0 -182
  82. package/dist/plugin/escape.d.ts +0 -39
  83. package/dist/plugin/escape.js +0 -60
  84. package/dist/plugin/fingerprint.d.ts +0 -27
  85. package/dist/plugin/fingerprint.js +0 -73
  86. package/dist/plugin/inferability.d.ts +0 -32
  87. package/dist/plugin/inferability.js +0 -88
  88. package/dist/plugin/kevin_approve.d.ts +0 -34
  89. package/dist/plugin/kevin_approve.js +0 -52
  90. package/dist/plugin/kevin_audit.d.ts +0 -229
  91. package/dist/plugin/kevin_audit.js +0 -631
  92. package/dist/plugin/kevin_bench.d.ts +0 -10
  93. package/dist/plugin/kevin_bench.js +0 -82
  94. package/dist/plugin/kevin_conflicts.d.ts +0 -9
  95. package/dist/plugin/kevin_conflicts.js +0 -50
  96. package/dist/plugin/kevin_contract.d.ts +0 -8
  97. package/dist/plugin/kevin_contract.js +0 -59
  98. package/dist/plugin/kevin_doctor.d.ts +0 -62
  99. package/dist/plugin/kevin_doctor.js +0 -196
  100. package/dist/plugin/kevin_facts.d.ts +0 -42
  101. package/dist/plugin/kevin_facts.js +0 -36
  102. package/dist/plugin/kevin_forget.d.ts +0 -33
  103. package/dist/plugin/kevin_forget.js +0 -260
  104. package/dist/plugin/kevin_native.d.ts +0 -29
  105. package/dist/plugin/kevin_native.js +0 -79
  106. package/dist/plugin/kevin_propose.d.ts +0 -23
  107. package/dist/plugin/kevin_propose.js +0 -14
  108. package/dist/plugin/kevin_publish.d.ts +0 -38
  109. package/dist/plugin/kevin_publish.js +0 -18
  110. package/dist/plugin/kevin_why.d.ts +0 -23
  111. package/dist/plugin/kevin_why.js +0 -111
  112. package/dist/plugin/memory-format.d.ts +0 -20
  113. package/dist/plugin/memory-format.js +0 -59
  114. package/dist/plugin/metrics.d.ts +0 -97
  115. package/dist/plugin/metrics.js +0 -288
  116. package/dist/plugin/okf-export.d.ts +0 -3
  117. package/dist/plugin/okf-export.js +0 -134
  118. package/dist/plugin/okf-import.d.ts +0 -76
  119. package/dist/plugin/okf-import.js +0 -271
  120. package/dist/plugin/okf.d.ts +0 -107
  121. package/dist/plugin/okf.js +0 -303
  122. package/dist/plugin/perf.d.ts +0 -35
  123. package/dist/plugin/perf.js +0 -174
  124. package/dist/plugin/query-tokenizer.d.ts +0 -13
  125. package/dist/plugin/query-tokenizer.js +0 -133
  126. package/dist/plugin/redact.d.ts +0 -2
  127. package/dist/plugin/redact.js +0 -18
  128. package/dist/plugin/replay-types.d.ts +0 -69
  129. package/dist/plugin/replay-types.js +0 -156
  130. package/dist/plugin/replay.d.ts +0 -36
  131. package/dist/plugin/replay.js +0 -202
  132. package/dist/plugin/sqlite-adapter.d.ts +0 -12
  133. package/dist/plugin/sqlite-adapter.js +0 -81
  134. package/dist/plugin/time-ms.d.ts +0 -1
  135. package/dist/plugin/time-ms.js +0 -16
  136. package/dist/plugin/tui-types.d.ts +0 -59
  137. package/dist/plugin/tui-types.js +0 -4
  138. package/dist/plugin/tui.d.ts +0 -18
  139. package/dist/plugin/tui.js +0 -198
  140. package/dist/plugin/uuid.d.ts +0 -1
  141. package/dist/plugin/uuid.js +0 -50
@@ -1,295 +0,0 @@
1
- import { readOriginCallId } from "./MemoryService.js";
2
- import { hasColumn } from "./columns.js";
3
- import { toMs } from "./time-ms.js";
4
- import { uuidv7 } from "./uuid.js";
5
- export class InjectionLedger {
6
- store;
7
- metrics;
8
- constructor(store, metrics) {
9
- this.store = store;
10
- this.metrics = metrics ?? null;
11
- }
12
- /**
13
- * Records one injected memory. Idempotent at the row level (UUID PK);
14
- * duplicates are expected only via the caller's per-session seen-set.
15
- */
16
- record(input) {
17
- // v1.1.0 (K11-003 / plan §5.2, D11-01) — dual-write injected_at_ms when column exists
18
- if (hasColumn(this.store, "kevin_injections", "injected_at_ms")) {
19
- this.store
20
- .prepare(`INSERT INTO kevin_injections
21
- (id, memory_id, fingerprint, session_id, hook, tokens, outcome, injected_at_ms)
22
- VALUES (?, ?, ?, ?, ?, ?, 'unmeasured', ?)`)
23
- .run(uuidv7(), input.memoryId, input.fingerprint, input.sessionId, input.hook, input.tokens, Date.now());
24
- }
25
- else {
26
- this.store
27
- .prepare(`INSERT INTO kevin_injections
28
- (id, memory_id, fingerprint, session_id, hook, tokens, outcome)
29
- VALUES (?, ?, ?, ?, ?, ?, 'unmeasured')`)
30
- .run(uuidv7(), input.memoryId, input.fingerprint, input.sessionId, input.hook, input.tokens);
31
- }
32
- this.metrics?.incr("injections_total", 1);
33
- // v0.8.0 (K8-024 / plan §5.7) — shared-layer consumption is counted
34
- // separately so the audit can tell how much of the push channel
35
- // came from teammates' entries.
36
- if (input.layer === "shared") {
37
- this.metrics?.incr("injections_from_shared", 1);
38
- }
39
- }
40
- /**
41
- * Settles every unmeasured injection of the session: a fingerprint that
42
- * failed again (as a failing tool_call) after the injection is
43
- * `ineffective`, otherwise `effective`. Idempotent — only
44
- * `outcome = 'unmeasured'` rows are flipped, and the recurrence charge
45
- * is `MAX(recurrence_count, n)` where n = all failing calls of the
46
- * fingerprint after `injected_at` (a later idle re-computes n and
47
- * catches up — plan §5.1 rule 4: 3 recurrences → stale).
48
- *
49
- * An ineffective injection also bumps the target memory's
50
- * `recurrence_count` (negative evidence, plan §5.3) and stamps
51
- * `last_injected_at` (monotonically — an older row can never
52
- * regress a newer injection's timestamp; the column means "most
53
- * recent injection").
54
- */
55
- settle(sessionId) {
56
- // v1.1.0 (K11-003 / plan §5.2, D11-01/D11-07) — ms-aware settle: readers
57
- // prefer _ms and fall back to legacy string. Column probes are cached.
58
- const hasInjMs = hasColumn(this.store, "kevin_injections", "injected_at_ms");
59
- const hasToolMs = hasColumn(this.store, "tool_calls", "ts_ms");
60
- const injections = (hasInjMs
61
- ? this.store.prepare(`SELECT id, memory_id, fingerprint, injected_at, injected_at_ms, outcome
62
- FROM kevin_injections
63
- WHERE session_id = ?
64
- ORDER BY injected_at ASC, id ASC`)
65
- : this.store.prepare(`SELECT id, memory_id, fingerprint, injected_at, outcome
66
- FROM kevin_injections
67
- WHERE session_id = ?
68
- ORDER BY injected_at ASC, id ASC`)).all(sessionId);
69
- for (const inj of injections) {
70
- // Same identity dimension CausalChain uses: the failing call's
71
- // `error_fingerprint` (stamped by Reflector) or the legacy
72
- // `fingerprint` hash.
73
- // v1.1.0 — time comparison uses toMs helper (prefers _ms).
74
- //
75
- // BUG-003 — the exemption is now bounded to the lesson's OWN
76
- // creating call (memories.metadata.origin_call_id, stamped by
77
- // Reflector). The old code excluded the session's FIRST failing
78
- // call of the fingerprint, which is only the creating call when
79
- // the lesson was born in THIS session; a lesson created in an
80
- // earlier session had its first in-session failure (a genuine
81
- // post-injection recurrence) wrongly exempted, inflating
82
- // precision. Memories without a tracked creating call (agent-
83
- // saved, test fixtures) get no exemption: only the `ts >=
84
- // injected_at` bound applies.
85
- const metaRow = this.store
86
- .prepare("SELECT metadata FROM memories WHERE id = ?")
87
- .get(inj.memory_id);
88
- const originCallId = readOriginCallId(metaRow?.metadata ?? null);
89
- // v1.1.0 — heuristic: when legacy string and _ms diverge by >2s
90
- // (manual UPDATE of injected_at in tests), trust the string
91
- // because the ms reflects wall time at record, not the pinned
92
- // fixture time. Real rows differ by <1s (second truncation).
93
- const rawInjMs = inj.injected_at_ms ?? null;
94
- const stringMs = inj.injected_at
95
- ? Date.parse(`${inj.injected_at.replace(" ", "T")}Z`)
96
- : null;
97
- const injectedMs = rawInjMs !== null &&
98
- stringMs !== null &&
99
- !Number.isNaN(stringMs) &&
100
- Math.abs(rawInjMs - stringMs) > 2000
101
- ? stringMs
102
- : toMs(inj.injected_at, rawInjMs);
103
- // Fetch candidate failing calls for this fingerprint and filter by ms
104
- const failRows = (hasToolMs
105
- ? this.store.prepare(`SELECT id, ts, ts_ms FROM tool_calls
106
- WHERE session_id = ?
107
- AND success = 0
108
- AND COALESCE(error_fingerprint, fingerprint) = ?`)
109
- : this.store.prepare(`SELECT id, ts FROM tool_calls
110
- WHERE session_id = ?
111
- AND success = 0
112
- AND COALESCE(error_fingerprint, fingerprint) = ?`)).all(sessionId, inj.fingerprint);
113
- let n = 0;
114
- for (const r of failRows) {
115
- if (originCallId !== null && r.id === originCallId)
116
- continue;
117
- const tsMs = toMs(r.ts, r.ts_ms ?? null);
118
- if (injectedMs !== null && tsMs !== null) {
119
- if (tsMs < injectedMs)
120
- continue;
121
- }
122
- else {
123
- // fallback to string comparison when either side missing (legacy)
124
- if (r.ts < inj.injected_at)
125
- continue;
126
- }
127
- n++;
128
- }
129
- // v0.5.0 (K5-005 / plan §5.1, D5-01) — three-way settlement:
130
- // recurrences >= 1 → ineffective (existing side effects unchanged)
131
- // else fixes >= 1 → effective (a linked fix was OBSERVED)
132
- // else → inconclusive (new majority bucket; excluded
133
- // from the precision denominator)
134
- if (n >= 1) {
135
- if (inj.outcome === "unmeasured") {
136
- this.store
137
- .prepare(`UPDATE kevin_injections SET outcome = 'ineffective'
138
- WHERE id = ?`)
139
- .run(inj.id);
140
- this.metrics?.incr("injections_ineffective", 1);
141
- }
142
- this.store
143
- .prepare(`UPDATE memories
144
- SET recurrence_count = MAX(recurrence_count, ?),
145
- last_injected_at = CASE
146
- WHEN last_injected_at IS NULL
147
- OR ? > last_injected_at
148
- THEN ? ELSE last_injected_at END
149
- WHERE fingerprint = ? AND id = ?`)
150
- .run(n, inj.injected_at, inj.injected_at, inj.fingerprint, inj.memory_id);
151
- // v0.4.0 (K4-025 / plan §5.1 rule 4, D4-06) — recurrence
152
- // expels: a fingerprint at `recurrence_count >= 3` is
153
- // demoted to `status='stale'` and never injected again
154
- // (only a new causal pattern — from a linked fix —
155
- // re-admits the lesson, not the stale error row).
156
- this.store
157
- .prepare(`UPDATE memories SET status = 'stale'
158
- WHERE id = ? AND recurrence_count >= 3`)
159
- .run(inj.memory_id);
160
- }
161
- else if (inj.outcome === "unmeasured") {
162
- // Mirror of the recurrence predicate with the success flag
163
- // inverted and the fingerprint matched on
164
- // `fix_for_fingerprint` (populated by CausalChain.onSuccess,
165
- // indexed by idx_tool_calls_fix_fp since migration 004). The
166
- // `ts >= injected_at` bound and `session_id = ?` filter are
167
- // kept; there is no `origin_call_id` exemption for fixes —
168
- // a fix is not the creating call.
169
- // v1.1.0 — ms-aware: fetch and filter via toMs.
170
- const fixCandidates = (hasToolMs
171
- ? this.store.prepare(`SELECT ts, ts_ms FROM tool_calls
172
- WHERE session_id = ?
173
- AND success = 1
174
- AND fix_for_fingerprint = ?`)
175
- : this.store.prepare(`SELECT ts FROM tool_calls
176
- WHERE session_id = ?
177
- AND success = 1
178
- AND fix_for_fingerprint = ?`)).all(sessionId, inj.fingerprint);
179
- let hasFix = false;
180
- for (const fr of fixCandidates) {
181
- const tsMs = toMs(fr.ts, fr.ts_ms ?? null);
182
- if (injectedMs !== null && tsMs !== null) {
183
- if (tsMs >= injectedMs) {
184
- hasFix = true;
185
- break;
186
- }
187
- }
188
- else if (fr.ts >= inj.injected_at) {
189
- hasFix = true;
190
- break;
191
- }
192
- }
193
- if (hasFix) {
194
- this.store
195
- .prepare(`UPDATE kevin_injections SET outcome = 'effective'
196
- WHERE id = ?`)
197
- .run(inj.id);
198
- this.metrics?.incr("injections_effective", 1);
199
- }
200
- else {
201
- this.store
202
- .prepare(`UPDATE kevin_injections SET outcome = 'inconclusive'
203
- WHERE id = ?`)
204
- .run(inj.id);
205
- this.metrics?.incr("injections_inconclusive", 1);
206
- }
207
- }
208
- }
209
- }
210
- /**
211
- * Per-fingerprint failing tool-call counts for the session. Feeds
212
- * QualityGate.canInject and the HITL suggestion block.
213
- */
214
- recurrencesFor(sessionId) {
215
- const rows = this.store
216
- .prepare(`SELECT COALESCE(error_fingerprint, fingerprint) AS fp
217
- FROM tool_calls
218
- WHERE session_id = ? AND success = 0
219
- AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)`)
220
- .all(sessionId);
221
- const out = new Map();
222
- for (const r of rows) {
223
- if (!r.fp)
224
- continue;
225
- out.set(r.fp, (out.get(r.fp) ?? 0) + 1);
226
- }
227
- return out;
228
- }
229
- /**
230
- * v0.4.0 (K4-017) — recurrence counts for the QualityGate at
231
- * injection time: only failing calls that happened AFTER the
232
- * fingerprint was already injected this session count. The failure
233
- * that *created* a lesson is not a recurrence — it precedes any
234
- * injection (plan §5.1 rule 4, same `ts >= injected_at` semantics
235
- * `settle` uses).
236
- */
237
- postInjectionRecurrencesFor(sessionId) {
238
- const rows = this.store
239
- .prepare(`SELECT COALESCE(t.error_fingerprint, t.fingerprint) AS fp
240
- FROM tool_calls t
241
- WHERE t.session_id = ? AND t.success = 0
242
- AND (t.error_fingerprint IS NOT NULL OR t.fingerprint IS NOT NULL)
243
- AND t.ts >= COALESCE(
244
- (SELECT MAX(injected_at) FROM kevin_injections
245
- WHERE session_id = t.session_id
246
- AND fingerprint = COALESCE(t.error_fingerprint, t.fingerprint)),
247
- '9999-12-31')`)
248
- .all(sessionId);
249
- const out = new Map();
250
- for (const r of rows) {
251
- if (!r.fp)
252
- continue;
253
- out.set(r.fp, (out.get(r.fp) ?? 0) + 1);
254
- }
255
- return out;
256
- }
257
- /** Number of rows for the session not yet settled (drives tests and settle). */
258
- unsettledForSession(sessionId) {
259
- const row = this.store
260
- .prepare(`SELECT COUNT(*) AS n FROM kevin_injections
261
- WHERE session_id = ? AND outcome = 'unmeasured'`)
262
- .get(sessionId);
263
- return row.n;
264
- }
265
- /** Latest ledger rows for a session, newest first (used by tests/tools). */
266
- rowsForSession(sessionId) {
267
- return this.store
268
- .prepare(`SELECT id, memory_id, fingerprint, session_id, hook, tokens,
269
- injected_at, outcome
270
- FROM kevin_injections
271
- WHERE session_id = ?
272
- ORDER BY injected_at ASC, id ASC`)
273
- .all(sessionId);
274
- }
275
- /**
276
- * v0.5.0 (K5-005 / plan §8.4) — one grouped rollup over the whole
277
- * ledger, zero-filled for every outcome. Consumed by `kevin_audit`.
278
- */
279
- outcomeCounts() {
280
- const rows = this.store
281
- .prepare("SELECT outcome, COUNT(*) AS n FROM kevin_injections GROUP BY outcome")
282
- .all();
283
- const out = {
284
- unmeasured: 0,
285
- effective: 0,
286
- ineffective: 0,
287
- inconclusive: 0,
288
- };
289
- for (const r of rows) {
290
- out[r.outcome] = r.n;
291
- }
292
- return out;
293
- }
294
- }
295
- // v1.1.0 — readOriginCallId deduplicated: imported from MemoryService (K11-003/K11-013)
@@ -1,44 +0,0 @@
1
- /**
2
- * v0.4.0 (K4-014) — Deterministic fix capture (plan §5.4 / D4-07).
3
- *
4
- * Zero-cost "Fixed by:" raw material from local data: when CausalChain
5
- * links a success to a failing fingerprint, the linked tool_call's
6
- * `args_summary` becomes a deterministic fix string stored in
7
- * `memories.fix_args`. The default promotion path is pure local data —
8
- * no LLM calls (principle 14).
9
- */
10
- /** Max length of the `args_summary` embedded in a fix_args string. */
11
- export declare const FIX_ARGS_TRUNCATE = 120;
12
- export interface LinkedToolCall {
13
- tool: string;
14
- args_summary: string | null;
15
- }
16
- /**
17
- * `"bash" with args "npm i -g rg"` style. Returns null when the linked
18
- * call carries no args_summary (nothing to say).
19
- */
20
- export declare function extractFixArgs(call: LinkedToolCall): string | null;
21
- export interface EnrichInput {
22
- lesson: string;
23
- fixArgs: string | null;
24
- originalError: string | null;
25
- }
26
- /**
27
- * Opt-in LLM phrasing hook (K4-015): receives the lesson, the
28
- * deterministic fix_args and the original error; returns a one-line
29
- * `Fix:` phrasing, or null to fall back to the deterministic text.
30
- * May be async; never runs on the failure hot path.
31
- */
32
- export type EnrichFn = (input: EnrichInput) => Promise<string | null>;
33
- export interface PatternLike {
34
- content: string;
35
- fixArgs: string | null;
36
- }
37
- /** Deterministic `Fixed by: {fix_args}` line (or "" when there is none). */
38
- export declare function deterministicFixLine(pattern: PatternLike): string;
39
- /**
40
- * Promotion-time fix phrasing. Default path returns the deterministic
41
- * `Fixed by: {fix_args}` line (or "" when there is none). When an
42
- * `enrichFn` is supplied and returns a phrase, that phrase wins.
43
- */
44
- export declare function enrichAtPromotion(pattern: PatternLike, enrichFn?: EnrichFn): Promise<string>;
@@ -1,45 +0,0 @@
1
- /**
2
- * v0.4.0 (K4-014) — Deterministic fix capture (plan §5.4 / D4-07).
3
- *
4
- * Zero-cost "Fixed by:" raw material from local data: when CausalChain
5
- * links a success to a failing fingerprint, the linked tool_call's
6
- * `args_summary` becomes a deterministic fix string stored in
7
- * `memories.fix_args`. The default promotion path is pure local data —
8
- * no LLM calls (principle 14).
9
- */
10
- /** Max length of the `args_summary` embedded in a fix_args string. */
11
- export const FIX_ARGS_TRUNCATE = 120;
12
- /**
13
- * `"bash" with args "npm i -g rg"` style. Returns null when the linked
14
- * call carries no args_summary (nothing to say).
15
- */
16
- export function extractFixArgs(call) {
17
- const raw = call.args_summary?.trim();
18
- if (!raw)
19
- return null;
20
- const truncated = raw.length > FIX_ARGS_TRUNCATE
21
- ? `${raw.slice(0, FIX_ARGS_TRUNCATE)}…`
22
- : raw;
23
- return `${call.tool} with args "${truncated}"`;
24
- }
25
- /** Deterministic `Fixed by: {fix_args}` line (or "" when there is none). */
26
- export function deterministicFixLine(pattern) {
27
- return pattern.fixArgs ? `Fixed by: ${pattern.fixArgs}` : "";
28
- }
29
- /**
30
- * Promotion-time fix phrasing. Default path returns the deterministic
31
- * `Fixed by: {fix_args}` line (or "" when there is none). When an
32
- * `enrichFn` is supplied and returns a phrase, that phrase wins.
33
- */
34
- export async function enrichAtPromotion(pattern, enrichFn) {
35
- if (enrichFn) {
36
- const phrased = await enrichFn({
37
- lesson: pattern.content,
38
- fixArgs: pattern.fixArgs,
39
- originalError: null,
40
- });
41
- if (phrased)
42
- return phrased;
43
- }
44
- return deterministicFixLine(pattern);
45
- }
@@ -1,83 +0,0 @@
1
- import type { ArtifactWriter, WriteOutcome } from "./ArtifactWriter.js";
2
- import type { Store } from "./Store.js";
3
- /**
4
- * K6-017 — v0.6.0 pull — topic bundles for the pull channels (plan §5.6).
5
- *
6
- * Writes `~/.opencode-kevin/refs/<topic>.md` (one file per topic) and
7
- * `~/.opencode-kevin/skills/project-knowledge.md` (one file). Both go
8
- * through the `ArtifactWriter` — marker-scoped, atomic, hash-audited.
9
- * There is no direct raw file-write anywhere in this module (D6-01).
10
- *
11
- * Topic derivation is deterministic and semantic (D6-14): a topic is
12
- * `<type>-<dominant token>`, where the dominant token is the
13
- * highest-frequency non-stop-word token of the fingerprint-normalized
14
- * content across the memories in the group, ties broken lexicographically.
15
- * Topics are NEVER derived from a fingerprint prefix, and hash-like tokens
16
- * (hex-only, length >= 8) are excluded from the race entirely.
17
- *
18
- * Output ordering within each bundle is by memory id, matching §5.4.
19
- * Regeneration with unchanged inputs is a `noop` at the ArtifactWriter
20
- * level, so the "run twice, compare bytes" criterion holds for every
21
- * bundle.
22
- */
23
- export interface MaterializedBundle {
24
- readonly topic: string;
25
- readonly path: string;
26
- readonly outcome: WriteOutcome;
27
- }
28
- export interface BundleTarget {
29
- readonly topic: string;
30
- readonly path: string;
31
- }
32
- export declare const SKILL_TOPIC = "project-knowledge";
33
- /**
34
- * v0.9.0 (K9-016 / plan §5.4, D9-10) — the native registration surfaces.
35
- * Registration replaces emission; they are never both active.
36
- */
37
- export type NativeSurface = "skill" | "reference";
38
- export declare class Materializer {
39
- private readonly store;
40
- /** `root` is injectable for tests; production defaults to ~/.opencode-kevin. */
41
- private readonly root;
42
- /**
43
- * v0.9.0 (K9-016 / plan §5.4, D9-10) — the mutual-exclusion guard.
44
- * A surface `attachNative()` registered is skipped by `materialize()`
45
- * for that surface only; the other surface keeps emitting.
46
- */
47
- private readonly nativeRegistered;
48
- constructor(store: Store, options?: {
49
- root?: string;
50
- });
51
- /**
52
- * v0.9.0 (K9-016 / plan §5.4, D9-10) — record whether the surface was
53
- * registered natively. Called at attach time (index.ts wiring, K9-018)
54
- * with each `NativeRegistration.registered` flag.
55
- */
56
- markNativeRegistered(surface: NativeSurface, registered: boolean): void;
57
- /** v0.9.0 (K9-016 / plan §5.4, D9-10) — is the surface registered natively? */
58
- hasNativeRegistration(surface: NativeSurface): boolean;
59
- /** The curated, active memories — the knowledge the pull channels publish. */
60
- private curatedRows;
61
- /** The rendered skill body over all curated memories; "" when empty. */
62
- skillBody(): string;
63
- /** Group rows by type, deriving each type's topic from its own content. */
64
- private groupByTopic;
65
- /**
66
- * The pull-channel targets: the skill file plus one ref file per topic.
67
- * The order is deterministic (skill first, refs by topic).
68
- */
69
- bundleTargets(): BundleTarget[];
70
- /**
71
- * Regenerate every bundle through the ArtifactWriter (write). This is
72
- * a call site of `write()` — the single write FUNCTION with two
73
- * constrained targets: `kevin_approve` reaches `agents_md_path`, this
74
- * module reaches `~/.opencode-kevin` only (D6-07; enforced by K6-020).
75
- *
76
- * v0.9.0 (K9-016 / plan §5.4, D9-10) — the guard: a surface registered
77
- * natively by `attachNative()` is skipped here for that surface only.
78
- * Both active would put the curated skill in front of the model twice
79
- * from two sources that can disagree; neither active would silently
80
- * remove the feature on hosts that gained the subpath.
81
- */
82
- materialize(writer: ArtifactWriter): MaterializedBundle[];
83
- }
@@ -1,181 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- import { firstSentence } from "./Curator.js";
4
- import { normalize } from "./fingerprint.js";
5
- import { STOP_WORDS } from "./query-tokenizer.js";
6
- export const SKILL_TOPIC = "project-knowledge";
7
- /** Hash-like noise: FNV-1a output is 16 lowercase hex chars; an 8+ hex
8
- * token is indistinguishable from a fingerprint prefix and must never
9
- * become a topic (D6-14). */
10
- const HEX_LIKE_RE = /^[0-9a-f]{8,}$/;
11
- const TOKEN_RE = /[^a-z0-9]+/;
12
- /** Sanitize a memory type into a filesystem-safe topic prefix. */
13
- function sanitizeType(type) {
14
- const cleaned = type
15
- .toLowerCase()
16
- .replace(/[^a-z0-9]+/g, "-")
17
- .replace(/-+/g, "-")
18
- .replace(/^-|-$/g, "");
19
- return cleaned === "" ? "memory" : cleaned;
20
- }
21
- function tokensOf(content) {
22
- return normalize(content)
23
- .split(TOKEN_RE)
24
- .filter((t) => t !== "" && !STOP_WORDS.has(t) && !HEX_LIKE_RE.test(t));
25
- }
26
- /** Highest-frequency non-stop-word token, ties broken lexicographically
27
- * (smallest wins) so the result is stable. "" when every token is a
28
- * stop-word or hash-like noise. */
29
- function dominantToken(rows) {
30
- const counts = new Map();
31
- for (const row of rows) {
32
- for (const token of tokensOf(row.content)) {
33
- counts.set(token, (counts.get(token) ?? 0) + 1);
34
- }
35
- }
36
- let best = "";
37
- let bestCount = 0;
38
- for (const [token, count] of counts) {
39
- if (count > bestCount || (count === bestCount && token < best)) {
40
- best = token;
41
- bestCount = count;
42
- }
43
- }
44
- return best;
45
- }
46
- function renderRows(rows) {
47
- const sorted = [...rows].sort((a, b) => a.id.localeCompare(b.id));
48
- const lines = sorted
49
- .map((row) => `- ${firstSentence(row.content)}`)
50
- .filter((line) => line !== "- ");
51
- return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
52
- }
53
- export class Materializer {
54
- store;
55
- /** `root` is injectable for tests; production defaults to ~/.opencode-kevin. */
56
- root;
57
- /**
58
- * v0.9.0 (K9-016 / plan §5.4, D9-10) — the mutual-exclusion guard.
59
- * A surface `attachNative()` registered is skipped by `materialize()`
60
- * for that surface only; the other surface keeps emitting.
61
- */
62
- nativeRegistered = new Set();
63
- constructor(store, options = {}) {
64
- this.store = store;
65
- this.root = options.root ?? join(homedir(), ".opencode-kevin");
66
- }
67
- /**
68
- * v0.9.0 (K9-016 / plan §5.4, D9-10) — record whether the surface was
69
- * registered natively. Called at attach time (index.ts wiring, K9-018)
70
- * with each `NativeRegistration.registered` flag.
71
- */
72
- markNativeRegistered(surface, registered) {
73
- if (registered)
74
- this.nativeRegistered.add(surface);
75
- else
76
- this.nativeRegistered.delete(surface);
77
- }
78
- /** v0.9.0 (K9-016 / plan §5.4, D9-10) — is the surface registered natively? */
79
- hasNativeRegistration(surface) {
80
- return this.nativeRegistered.has(surface);
81
- }
82
- /** The curated, active memories — the knowledge the pull channels publish. */
83
- curatedRows() {
84
- return this.store
85
- .prepare(`SELECT id, type, content FROM memories
86
- WHERE status = 'active' AND curated = 1`)
87
- .all();
88
- }
89
- /** The rendered skill body over all curated memories; "" when empty. */
90
- skillBody() {
91
- return renderRows(this.curatedRows());
92
- }
93
- /** Group rows by type, deriving each type's topic from its own content. */
94
- groupByTopic(rows) {
95
- const byType = new Map();
96
- for (const row of rows) {
97
- const list = byType.get(row.type) ?? [];
98
- list.push(row);
99
- byType.set(row.type, list);
100
- }
101
- const groups = [];
102
- for (const [type, typeRows] of byType) {
103
- const token = dominantToken(typeRows);
104
- if (token === "")
105
- continue;
106
- groups.push({
107
- topic: `${sanitizeType(type)}-${token}`,
108
- rows: typeRows,
109
- });
110
- }
111
- return groups.sort((a, b) => a.topic.localeCompare(b.topic));
112
- }
113
- /**
114
- * The pull-channel targets: the skill file plus one ref file per topic.
115
- * The order is deterministic (skill first, refs by topic).
116
- */
117
- bundleTargets() {
118
- const rows = this.curatedRows();
119
- const targets = [];
120
- if (rows.length > 0 && renderRows(rows) !== "") {
121
- targets.push({
122
- topic: SKILL_TOPIC,
123
- path: join(this.root, "skills", "project-knowledge.md"),
124
- });
125
- }
126
- for (const group of this.groupByTopic(rows)) {
127
- if (renderRows(group.rows) === "")
128
- continue;
129
- targets.push({
130
- topic: group.topic,
131
- path: join(this.root, "refs", `${group.topic}.md`),
132
- });
133
- }
134
- return targets;
135
- }
136
- /**
137
- * Regenerate every bundle through the ArtifactWriter (write). This is
138
- * a call site of `write()` — the single write FUNCTION with two
139
- * constrained targets: `kevin_approve` reaches `agents_md_path`, this
140
- * module reaches `~/.opencode-kevin` only (D6-07; enforced by K6-020).
141
- *
142
- * v0.9.0 (K9-016 / plan §5.4, D9-10) — the guard: a surface registered
143
- * natively by `attachNative()` is skipped here for that surface only.
144
- * Both active would put the curated skill in front of the model twice
145
- * from two sources that can disagree; neither active would silently
146
- * remove the feature on hosts that gained the subpath.
147
- */
148
- materialize(writer) {
149
- const pending = [];
150
- const rows = this.curatedRows();
151
- const skillBody = renderRows(rows);
152
- if (skillBody !== "" && !this.nativeRegistered.has("skill")) {
153
- pending.push({
154
- topic: SKILL_TOPIC,
155
- path: join(this.root, "skills", "project-knowledge.md"),
156
- body: skillBody,
157
- });
158
- }
159
- if (!this.nativeRegistered.has("reference")) {
160
- for (const group of this.groupByTopic(rows)) {
161
- const body = renderRows(group.rows);
162
- if (body === "")
163
- continue;
164
- pending.push({
165
- topic: group.topic,
166
- path: join(this.root, "refs", `${group.topic}.md`),
167
- body,
168
- });
169
- }
170
- }
171
- return pending.map((bundle) => ({
172
- topic: bundle.topic,
173
- path: bundle.path,
174
- outcome: writer.write({
175
- path: bundle.path,
176
- mode: "markers",
177
- content: bundle.body,
178
- }),
179
- }));
180
- }
181
- }