@jmtrin/opencode-kevin 1.2.0 → 1.4.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 (140) hide show
  1. package/README.md +84 -22
  2. package/dist/plugin/index.d.ts +5 -4
  3. package/dist/plugin/index.js +127 -113
  4. package/dist/plugin/native.d.ts +2 -2
  5. package/dist/plugin/native.js +2 -2
  6. package/package.json +56 -68
  7. package/dist/migrations/001_initial.sql +0 -92
  8. package/dist/migrations/002_indexes.sql +0 -14
  9. package/dist/migrations/003_v02_signal.sql +0 -58
  10. package/dist/migrations/004_v03_knowledge.sql +0 -138
  11. package/dist/migrations/005_v04_signal.sql +0 -57
  12. package/dist/migrations/006_v05_glassbox.sql +0 -118
  13. package/dist/migrations/007_v06_pull.sql +0 -145
  14. package/dist/migrations/008_v07_truth.sql +0 -124
  15. package/dist/migrations/009_v08_team.sql +0 -100
  16. package/dist/migrations/010_v09_native.sql +0 -78
  17. package/dist/migrations/011_v10_proven.sql +0 -69
  18. package/dist/migrations/012_v11_drift.sql +0 -24
  19. package/dist/plugin/Archiver.d.ts +0 -42
  20. package/dist/plugin/Archiver.js +0 -83
  21. package/dist/plugin/ArtifactWriter.d.ts +0 -93
  22. package/dist/plugin/ArtifactWriter.js +0 -291
  23. package/dist/plugin/CausalChain.d.ts +0 -22
  24. package/dist/plugin/CausalChain.js +0 -197
  25. package/dist/plugin/ChatBridge.d.ts +0 -41
  26. package/dist/plugin/ChatBridge.js +0 -103
  27. package/dist/plugin/ConflictDetector.d.ts +0 -35
  28. package/dist/plugin/ConflictDetector.js +0 -260
  29. package/dist/plugin/ContextInjector.d.ts +0 -222
  30. package/dist/plugin/ContextInjector.js +0 -533
  31. package/dist/plugin/ConventionMiner.d.ts +0 -35
  32. package/dist/plugin/ConventionMiner.js +0 -242
  33. package/dist/plugin/Curator.d.ts +0 -116
  34. package/dist/plugin/Curator.js +0 -324
  35. package/dist/plugin/DashboardHtml.d.ts +0 -5
  36. package/dist/plugin/DashboardHtml.js +0 -180
  37. package/dist/plugin/Feedback.d.ts +0 -67
  38. package/dist/plugin/Feedback.js +0 -120
  39. package/dist/plugin/HookLiveness.d.ts +0 -88
  40. package/dist/plugin/HookLiveness.js +0 -322
  41. package/dist/plugin/InjectionLedger.d.ts +0 -98
  42. package/dist/plugin/InjectionLedger.js +0 -295
  43. package/dist/plugin/LessonFixer.d.ts +0 -44
  44. package/dist/plugin/LessonFixer.js +0 -45
  45. package/dist/plugin/Materializer.d.ts +0 -83
  46. package/dist/plugin/Materializer.js +0 -181
  47. package/dist/plugin/MemoryService.d.ts +0 -360
  48. package/dist/plugin/MemoryService.js +0 -1242
  49. package/dist/plugin/Migrate.d.ts +0 -17
  50. package/dist/plugin/Migrate.js +0 -271
  51. package/dist/plugin/PatternMiner.d.ts +0 -49
  52. package/dist/plugin/PatternMiner.js +0 -132
  53. package/dist/plugin/QualityGate.d.ts +0 -124
  54. package/dist/plugin/QualityGate.js +0 -119
  55. package/dist/plugin/Reflector.d.ts +0 -91
  56. package/dist/plugin/Reflector.js +0 -307
  57. package/dist/plugin/RepoIdentity.d.ts +0 -124
  58. package/dist/plugin/RepoIdentity.js +0 -300
  59. package/dist/plugin/RepoTruth.d.ts +0 -80
  60. package/dist/plugin/RepoTruth.js +0 -599
  61. package/dist/plugin/Retrospective.d.ts +0 -17
  62. package/dist/plugin/Retrospective.js +0 -216
  63. package/dist/plugin/SharedLayer.d.ts +0 -159
  64. package/dist/plugin/SharedLayer.js +0 -462
  65. package/dist/plugin/Store.d.ts +0 -29
  66. package/dist/plugin/Store.js +0 -50
  67. package/dist/plugin/ToolCallObserver.d.ts +0 -32
  68. package/dist/plugin/ToolCallObserver.js +0 -201
  69. package/dist/plugin/TuiActions.d.ts +0 -43
  70. package/dist/plugin/TuiActions.js +0 -181
  71. package/dist/plugin/TuiSnapshots.d.ts +0 -24
  72. package/dist/plugin/TuiSnapshots.js +0 -158
  73. package/dist/plugin/columns.d.ts +0 -11
  74. package/dist/plugin/columns.js +0 -54
  75. package/dist/plugin/confidence.d.ts +0 -8
  76. package/dist/plugin/confidence.js +0 -34
  77. package/dist/plugin/contract.d.ts +0 -54
  78. package/dist/plugin/contract.js +0 -386
  79. package/dist/plugin/diff.d.ts +0 -8
  80. package/dist/plugin/diff.js +0 -182
  81. package/dist/plugin/escape.d.ts +0 -39
  82. package/dist/plugin/escape.js +0 -60
  83. package/dist/plugin/fingerprint.d.ts +0 -27
  84. package/dist/plugin/fingerprint.js +0 -73
  85. package/dist/plugin/inferability.d.ts +0 -32
  86. package/dist/plugin/inferability.js +0 -88
  87. package/dist/plugin/kevin_approve.d.ts +0 -34
  88. package/dist/plugin/kevin_approve.js +0 -52
  89. package/dist/plugin/kevin_audit.d.ts +0 -229
  90. package/dist/plugin/kevin_audit.js +0 -631
  91. package/dist/plugin/kevin_bench.d.ts +0 -10
  92. package/dist/plugin/kevin_bench.js +0 -82
  93. package/dist/plugin/kevin_conflicts.d.ts +0 -9
  94. package/dist/plugin/kevin_conflicts.js +0 -50
  95. package/dist/plugin/kevin_contract.d.ts +0 -8
  96. package/dist/plugin/kevin_contract.js +0 -59
  97. package/dist/plugin/kevin_doctor.d.ts +0 -62
  98. package/dist/plugin/kevin_doctor.js +0 -196
  99. package/dist/plugin/kevin_facts.d.ts +0 -42
  100. package/dist/plugin/kevin_facts.js +0 -36
  101. package/dist/plugin/kevin_forget.d.ts +0 -33
  102. package/dist/plugin/kevin_forget.js +0 -260
  103. package/dist/plugin/kevin_native.d.ts +0 -29
  104. package/dist/plugin/kevin_native.js +0 -79
  105. package/dist/plugin/kevin_propose.d.ts +0 -23
  106. package/dist/plugin/kevin_propose.js +0 -14
  107. package/dist/plugin/kevin_publish.d.ts +0 -38
  108. package/dist/plugin/kevin_publish.js +0 -18
  109. package/dist/plugin/kevin_why.d.ts +0 -23
  110. package/dist/plugin/kevin_why.js +0 -111
  111. package/dist/plugin/memory-format.d.ts +0 -20
  112. package/dist/plugin/memory-format.js +0 -59
  113. package/dist/plugin/metrics.d.ts +0 -97
  114. package/dist/plugin/metrics.js +0 -288
  115. package/dist/plugin/okf-export.d.ts +0 -3
  116. package/dist/plugin/okf-export.js +0 -134
  117. package/dist/plugin/okf-import.d.ts +0 -76
  118. package/dist/plugin/okf-import.js +0 -271
  119. package/dist/plugin/okf.d.ts +0 -107
  120. package/dist/plugin/okf.js +0 -303
  121. package/dist/plugin/perf.d.ts +0 -35
  122. package/dist/plugin/perf.js +0 -174
  123. package/dist/plugin/query-tokenizer.d.ts +0 -13
  124. package/dist/plugin/query-tokenizer.js +0 -133
  125. package/dist/plugin/redact.d.ts +0 -2
  126. package/dist/plugin/redact.js +0 -18
  127. package/dist/plugin/replay-types.d.ts +0 -69
  128. package/dist/plugin/replay-types.js +0 -156
  129. package/dist/plugin/replay.d.ts +0 -36
  130. package/dist/plugin/replay.js +0 -202
  131. package/dist/plugin/sqlite-adapter.d.ts +0 -12
  132. package/dist/plugin/sqlite-adapter.js +0 -81
  133. package/dist/plugin/time-ms.d.ts +0 -1
  134. package/dist/plugin/time-ms.js +0 -16
  135. package/dist/plugin/tui-types.d.ts +0 -59
  136. package/dist/plugin/tui-types.js +0 -4
  137. package/dist/plugin/tui.d.ts +0 -18
  138. package/dist/plugin/tui.js +0 -198
  139. package/dist/plugin/uuid.d.ts +0 -1
  140. package/dist/plugin/uuid.js +0 -50
@@ -1,17 +0,0 @@
1
- import type { Store } from "./Store.js";
2
- export interface MigrateResult {
3
- from: string;
4
- to: string;
5
- applied: string[];
6
- }
7
- export type PostApplyHook = (store: Store) => void;
8
- export declare const HOOK_NAMES: readonly ["tool.execute.before", "tool.execute.after", "chat.message", "experimental.chat.system.transform", "experimental.session.compacting", "event", "dispose"];
9
- export declare class Migrate {
10
- private store;
11
- private migrationsDir;
12
- private readonly postApplyHooks;
13
- constructor(store: Store, migrationsDir: string, postApplyHooks?: Record<string, PostApplyHook>);
14
- registerPostApply(version: string, hook: PostApplyHook): void;
15
- run(): Promise<MigrateResult>;
16
- private listPending;
17
- }
@@ -1,271 +0,0 @@
1
- import { readFileSync, readdirSync } from "node:fs";
2
- import { join } from "node:path";
3
- // v0.9.0 (K9-001 / plan §5.3, D9-08) — the six hooks Kevin registers, in
4
- // hook-object order, plus `dispose` (v1.0.0 K10-013): the seventh
5
- // instrumented hook. Canonical list: the "010" post-apply hook seeds
6
- // hook_liveness from it, and HookLiveness (K9-009) wraps exactly these
7
- // names, so a future hook added here is visible in the table on the next
8
- // migration run.
9
- export const HOOK_NAMES = [
10
- "tool.execute.before",
11
- "tool.execute.after",
12
- "chat.message",
13
- "experimental.chat.system.transform",
14
- "experimental.session.compacting",
15
- "event",
16
- "dispose",
17
- ];
18
- // Built-in post-apply hooks, keyed by migration version. Each hook runs inside
19
- // the same transaction as the migration's DDL, so a hook failure rolls back the
20
- // whole migration. Hooks run when their version is applied. Migration 008's
21
- // reconciliation hook is also safe to run on later no-op starts (K7-002).
22
- const DEFAULT_POST_APPLY_HOOKS = {
23
- // v0.2.0 Signal Quality: defensive backfill of memories.origin for legacy
24
- // rows. The column is NOT NULL DEFAULT 'agent', so SQLite already populates
25
- // pre-existing rows with 'agent' on ALTER TABLE. This hook is a belt-and-
26
- // braces UPDATE that coerces any NULL/empty stragglers (which would only
27
- // exist if a partial DB skipped the DEFAULT) back to 'agent'.
28
- "003": (store) => {
29
- store
30
- .prepare("UPDATE memories SET origin = 'agent' WHERE origin IS NULL OR origin = ''")
31
- .run();
32
- },
33
- // v0.3.0 Knowledge + Causality: backfill evidence_count and status for
34
- // legacy rows. Columns have NOT NULL DEFAULT, so SQLite already populates
35
- // pre-existing rows. This hook is belt-and-braces in case a partial DB
36
- // skipped the defaults.
37
- "004": (store) => {
38
- store
39
- .prepare("UPDATE memories SET evidence_count = 0 WHERE evidence_count IS NULL")
40
- .run();
41
- store
42
- .prepare("UPDATE memories SET status = 'active' WHERE status IS NULL OR status = ''")
43
- .run();
44
- },
45
- // v0.4.0 Signal over Noise: backfill recurrence_count for legacy rows.
46
- // recurrence_count has NOT NULL DEFAULT 0, so SQLite already populates
47
- // pre-existing rows; fix_args and last_injected_at are nullable and need
48
- // no coercion. This hook is belt-and-braces in case a partial DB skipped
49
- // the defaults.
50
- "005": (store) => {
51
- store
52
- .prepare("UPDATE memories SET recurrence_count = 0 WHERE recurrence_count IS NULL")
53
- .run();
54
- },
55
- // v0.5.0 (K5-002 / plan §6, D5-13) — Glass Box: re-derive the four
56
- // injection counters from the ledger table instead of incrementing them.
57
- // The rebuild remapped prior `effective` rows to `inconclusive`, so any
58
- // pre-existing counter values would otherwise carry the v0.4 confound
59
- // forward. Re-derivation is idempotent by construction and self-healing
60
- // on earlier drift. No INSERTs: the rows are seeded by the migration SQL;
61
- // a missing row makes the UPDATE a harmless no-op.
62
- "006": (store) => {
63
- store
64
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections) WHERE key = 'injections_total'")
65
- .run();
66
- store
67
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections WHERE outcome = 'effective') WHERE key = 'injections_effective'")
68
- .run();
69
- store
70
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections WHERE outcome = 'ineffective') WHERE key = 'injections_ineffective'")
71
- .run();
72
- store
73
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM kevin_injections WHERE outcome = 'inconclusive') WHERE key = 'injections_inconclusive'")
74
- .run();
75
- },
76
- // v0.6.0 (K6-002 / plan §6, D6-08) — Pull: three re-derivations, all
77
- // idempotent by re-derivation (same discipline as "006", D5-13).
78
- // 1. Back-fill `inferable = 0` for the four non-`error` types still NULL:
79
- // they are `non_inferable` by rules 1 and 2 of plan §5.3
80
- // unconditionally. Guarded by `inferable IS NULL` so a re-run cannot
81
- // overwrite a classification produced later by inferability.classify().
82
- // `error` rows are left NULL and classified lazily.
83
- // 2-3. proposals_created / artifact_writes_total are re-derived from
84
- // their tables; a missing row makes the UPDATE a harmless no-op.
85
- "007": (store) => {
86
- store
87
- .prepare("UPDATE memories SET inferable = 0 WHERE inferable IS NULL AND type IN ('decision','rule','solution','pattern')")
88
- .run();
89
- store
90
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM curation_proposals) WHERE key = 'proposals_created'")
91
- .run();
92
- store
93
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM artifact_writes WHERE outcome = 'written') WHERE key = 'artifact_writes_total'")
94
- .run();
95
- },
96
- // v0.7.0 (K7-002 / plan §6, D7-02) — Truth: four re-derivations, all
97
- // idempotent by re-derivation (same discipline as "006"/"007", D5-13).
98
- // 1. Normalize any NULL truth_penalty to 0.0 (belt-and-braces; the column
99
- // is NOT NULL DEFAULT 0.0, so SQLite already back-fills on ALTER TABLE).
100
- // 2-4. repo_facts_scanned / conflicts_detected / memories_contradicted
101
- // are re-derived from their tables, NOT trusted from the counters, so
102
- // the metrics survive a database restored from backup or edited by
103
- // hand. A missing row makes the UPDATE a harmless no-op.
104
- "008": (store) => {
105
- store
106
- .prepare("UPDATE memories SET truth_penalty = 0.0 WHERE truth_penalty IS NULL")
107
- .run();
108
- store
109
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM repo_facts) WHERE key = 'repo_facts_scanned'")
110
- .run();
111
- store
112
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM memory_conflicts) WHERE key = 'conflicts_detected'")
113
- .run();
114
- store
115
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM memories WHERE truth_penalty > 0.0) WHERE key = 'memories_contradicted'")
116
- .run();
117
- },
118
- // v0.8.0 (K8-002 / plan §6.1, D8-03) — Team: three operations, all
119
- // idempotent by re-derivation (same discipline as "006"/"007"/"008").
120
- // 1. Back-fill the new scope from the old one: repo_id = project_id
121
- // keeps every pre-v0.8 row retrievable on the same machine with an
122
- // identical result set. Guarded by `repo_id IS NULL`; a row whose
123
- // project_id is also NULL keeps a NULL repo_id — the retrieval
124
- // path handles NULL as global (K8-007) instead of the hook faking
125
- // a value. No git-derived identity here: the hook runs inside a
126
- // migration and must not read the filesystem (K8-002 criterion).
127
- // 2. Normalize the layer marker for any row written by a concurrent
128
- // v0.7.0 process between ALTER and hook (belt and braces; the
129
- // column DEFAULT already covers the ordinary case).
130
- // 3. Re-derive shared_entries_total from state rather than trusting
131
- // an incremented value that may predate a crash.
132
- "009": (store) => {
133
- store
134
- .prepare("UPDATE memories SET repo_id = project_id WHERE repo_id IS NULL")
135
- .run();
136
- store
137
- .prepare("UPDATE memories SET layer = 'local' WHERE layer IS NULL OR layer = ''")
138
- .run();
139
- store
140
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM shared_entries) WHERE key = 'shared_entries_total'")
141
- .run();
142
- },
143
- // v0.9.0 (K9-001 / plan §6.1, D9-08) — Native: three operations, all
144
- // idempotent by design.
145
- // 1. Seed one hook_liveness row per name in HOOK_NAMES, with the
146
- // experimental flag derived from the hook's own `experimental.`
147
- // prefix and every counter at zero. Seeding eagerly (INSERT OR
148
- // IGNORE) makes a hook that has never fired a visible row with
149
- // fire_count = 0, not an absent row indistinguishable from a hook
150
- // Kevin does not register.
151
- // 2. Re-derive hooks_dead_total from hook_liveness state rather than
152
- // trusting an incremented value (same discipline as "006"-"009").
153
- // 3. Normalize any experimental flag that disagrees with its own
154
- // hook column's prefix — cheap, and it repairs a row hand-edited
155
- // during debugging.
156
- "010": (store) => {
157
- const seed = store.prepare("INSERT OR IGNORE INTO hook_liveness (hook, experimental) VALUES (?, ?)");
158
- for (const name of HOOK_NAMES) {
159
- seed.run(name, name.startsWith("experimental.") ? 1 : 0);
160
- }
161
- store
162
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM hook_liveness WHERE dead_since IS NOT NULL) WHERE key = 'hooks_dead_total'")
163
- .run();
164
- store
165
- .prepare("UPDATE hook_liveness SET experimental = CASE WHEN hook LIKE 'experimental.%' THEN 1 ELSE 0 END")
166
- .run();
167
- },
168
- // v1.0.0 (K10-005 / plan §6.1) — Proven: four operations, all idempotent.
169
- // 1. Seed dispose row defensively.
170
- // 2-3. Re-derive perf_budget_breaches and bench_runs_total.
171
- // 4. Normalise NULL within_budget to 1.
172
- "011": (store) => {
173
- store
174
- .prepare("INSERT OR IGNORE INTO hook_liveness (hook) VALUES ('dispose')")
175
- .run();
176
- store
177
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM perf_samples WHERE within_budget = 0) WHERE key = 'perf_budget_breaches'")
178
- .run();
179
- store
180
- .prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM bench_runs) WHERE key = 'bench_runs_total'")
181
- .run();
182
- store
183
- .prepare("UPDATE perf_samples SET within_budget = 1 WHERE within_budget IS NULL")
184
- .run();
185
- },
186
- };
187
- export class Migrate {
188
- store;
189
- migrationsDir;
190
- postApplyHooks;
191
- constructor(store, migrationsDir, postApplyHooks) {
192
- this.store = store;
193
- this.migrationsDir = migrationsDir;
194
- this.postApplyHooks = new Map(Object.entries({
195
- ...DEFAULT_POST_APPLY_HOOKS,
196
- ...(postApplyHooks ?? {}),
197
- }));
198
- }
199
- registerPostApply(version, hook) {
200
- this.postApplyHooks.set(version, hook);
201
- }
202
- async run() {
203
- this.store.exec(`CREATE TABLE IF NOT EXISTS schema_version (
204
- version TEXT PRIMARY KEY,
205
- applied_at TEXT NOT NULL DEFAULT (datetime('now'))
206
- );`);
207
- const currentRow = this.store
208
- .prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1")
209
- .get();
210
- const from = currentRow?.version ?? "000";
211
- const pending = this.listPending(from);
212
- if (pending.length === 0) {
213
- // v0.7.0 (K7-002) — heal drift in the 008 counters on a no-op
214
- // startup while preserving `applied: []` idempotency. v0.8.0
215
- // (K8-002 / plan §6.1) extends the same repair to the 009
216
- // back-fill and the shared_entries_total re-derivation, and
217
- // v0.9.0 (K9-001 / plan §6.1) to the 010 seeding,
218
- // hooks_dead_total re-derivation and experimental
219
- // normalization: all three hooks are idempotent by guarded
220
- // updates and re-derivation, so a no-op startup can heal a
221
- // crash that landed between the DDL and the hook without
222
- // re-applying DDL.
223
- if (from === "008" ||
224
- from === "009" ||
225
- from === "010" ||
226
- from === "011") {
227
- const repairHook = this.postApplyHooks.get(from);
228
- if (repairHook)
229
- this.store.transaction(() => repairHook(this.store));
230
- }
231
- return { from, to: from, applied: [] };
232
- }
233
- const insertVersion = this.store.prepare("INSERT OR IGNORE INTO schema_version (version) VALUES (?)");
234
- for (const migration of pending) {
235
- const sql = readFileSync(join(this.migrationsDir, migration.file), "utf8");
236
- this.store.transaction(() => {
237
- this.store.exec(sql);
238
- const hook = this.postApplyHooks.get(migration.version);
239
- if (hook)
240
- hook(this.store);
241
- insertVersion.run(migration.version);
242
- });
243
- }
244
- return {
245
- from,
246
- to: pending[pending.length - 1].version,
247
- applied: pending.map((m) => m.version),
248
- };
249
- }
250
- // v1.1.0 (K11-015) — lexicographic ordering is valid through "999" because
251
- // versions are zero-padded 3-digit strings ("001" … "999"). Any future
252
- // migration beyond 999 must use a 4-digit prefix and this comparison must
253
- // become numeric (parseInt). Until then, string > works and keeps the
254
- // migration idempotency simple (plan §5.5, D11-??).
255
- listPending(current) {
256
- let files = [];
257
- try {
258
- files = readdirSync(this.migrationsDir).filter((f) => f.endsWith(".sql"));
259
- }
260
- catch {
261
- return [];
262
- }
263
- files.sort();
264
- return files
265
- .map((file) => {
266
- const match = file.match(/^(\w+?)_/);
267
- return match ? { version: match[1], file } : null;
268
- })
269
- .filter((m) => m !== null && m.version > current);
270
- }
271
- }
@@ -1,49 +0,0 @@
1
- import type { MemoryService } from "./MemoryService.js";
2
- import type { Store } from "./Store.js";
3
- import type { Metrics } from "./metrics.js";
4
- /**
5
- * v0.2.0 PatternMiner (K2-021, plan §B6.10 / D2-08).
6
- *
7
- * Opt-in deterministic pattern miner. Reads recent `tool_calls` for a given
8
- * project, groups consecutive ordered 2-grams of `(tool_name)` and 3-grams
9
- * where the *middle* tool was a failure, and when a group reaches the
10
- * threshold (default `N ≥ 5` distinct sessions), emits a single
11
- * `type='pattern'`, `origin='pattern'` memory describing the pattern.
12
- *
13
- * Default OFF — must be enabled by setting `kevin_settings.patternminer_enabled`
14
- * to `'1'`. Idempotent via SELECT-before-INSERT keyed on
15
- * `(project_id, fingerprint, type='pattern', origin='pattern')`. Migration 003's
16
- * partial UNIQUE index only covers `type='error' AND origin='reflector'`, so
17
- * pattern memories cannot rely on a database uniqueness constraint — the
18
- * SELECT check inside `mine()` is the single idempotency mechanism.
19
- *
20
- * NO LLM hop (D2-08). The emitted suggestion is a deterministic template built
21
- * from the captured tool names.
22
- */
23
- export interface PatternMinerOptions {
24
- /** Minimum distinct sessions a pattern must appear in before emission.
25
- * Default 5 (D2-08). */
26
- threshold?: number;
27
- }
28
- export declare class PatternMiner {
29
- private readonly store;
30
- private readonly memoryService;
31
- private readonly metrics;
32
- private readonly threshold;
33
- constructor(store: Store, memoryService: MemoryService, metrics?: Metrics | null, options?: PatternMinerOptions);
34
- /**
35
- * Mine patterns observed in `tool_calls` for the given project. Returns the
36
- * number of NEW `pattern` memories emitted this cycle.
37
- *
38
- * When the opt-in flag `kevin_settings.patternminer_enabled` is not set to
39
- * `'1'` (default), this is a no-op and returns 0.
40
- *
41
- * When `projectId` is null/undefined, mines tool_calls whose `project_id`
42
- * IS NULL (legacy / opt-out flow). When `projectId` is a string, mines
43
- * tool_calls scoped to that project only.
44
- */
45
- mine(projectId?: string | null): number;
46
- private isEnabled;
47
- private fetchToolCalls;
48
- private collectCandidates;
49
- }
@@ -1,132 +0,0 @@
1
- import { fingerprint as computeFingerprint } from "./fingerprint.js";
2
- const DEFAULT_THRESHOLD = 5;
3
- const SETTING_KEY = "patternminer_enabled";
4
- export class PatternMiner {
5
- store;
6
- memoryService;
7
- metrics;
8
- threshold;
9
- constructor(store, memoryService, metrics, options) {
10
- this.store = store;
11
- this.memoryService = memoryService;
12
- this.metrics = metrics ?? null;
13
- this.threshold = options?.threshold ?? DEFAULT_THRESHOLD;
14
- }
15
- /**
16
- * Mine patterns observed in `tool_calls` for the given project. Returns the
17
- * number of NEW `pattern` memories emitted this cycle.
18
- *
19
- * When the opt-in flag `kevin_settings.patternminer_enabled` is not set to
20
- * `'1'` (default), this is a no-op and returns 0.
21
- *
22
- * When `projectId` is null/undefined, mines tool_calls whose `project_id`
23
- * IS NULL (legacy / opt-out flow). When `projectId` is a string, mines
24
- * tool_calls scoped to that project only.
25
- */
26
- mine(projectId) {
27
- if (!this.isEnabled())
28
- return 0;
29
- const rows = this.fetchToolCalls(projectId ?? null);
30
- if (rows.length === 0)
31
- return 0;
32
- const candidates = this.collectCandidates(rows);
33
- if (candidates.length === 0)
34
- return 0;
35
- let emitted = 0;
36
- for (const c of candidates) {
37
- if (c.sessions.size < this.threshold)
38
- continue;
39
- const fp = computeFingerprint(c.content, projectId ?? undefined);
40
- // Idempotency: migration 003's partial unique only covers
41
- // (type='error', origin='reflector'). For pattern memories we
42
- // SELECT to detect a prior emission with the same
43
- // (project_id, fingerprint, type='pattern', origin='pattern').
44
- const existing = this.store
45
- .prepare(`SELECT id FROM memories
46
- WHERE type = 'pattern' AND origin = 'pattern'
47
- AND fingerprint = ?
48
- AND (project_id IS ? OR (project_id IS NULL AND ? IS NULL))
49
- LIMIT 1`)
50
- .get(fp, projectId ?? null, projectId ?? null);
51
- if (existing)
52
- continue;
53
- this.memoryService.save({
54
- type: "pattern",
55
- origin: "pattern",
56
- fingerprint: fp,
57
- content: c.content,
58
- scope: "project",
59
- projectId: projectId ?? undefined,
60
- relevanceScore: 0.5,
61
- sourceTool: "PatternMiner",
62
- });
63
- this.metrics?.incr("patterns_mined", 1);
64
- emitted += 1;
65
- }
66
- return emitted;
67
- }
68
- isEnabled() {
69
- const row = this.store
70
- .prepare("SELECT value FROM kevin_settings WHERE key = ?")
71
- .get(SETTING_KEY);
72
- return row?.value === "1";
73
- }
74
- fetchToolCalls(projectId) {
75
- const nullPid = projectId === null || projectId === undefined;
76
- const sql = nullPid
77
- ? `SELECT id, session_id, ts, tool, success FROM tool_calls
78
- WHERE project_id IS NULL
79
- ORDER BY session_id ASC, ts ASC`
80
- : `SELECT id, session_id, ts, tool, success FROM tool_calls
81
- WHERE project_id = ?
82
- ORDER BY session_id ASC, ts ASC`;
83
- const stmt = this.store.prepare(sql);
84
- const rows = (nullPid ? stmt.all() : stmt.all(projectId));
85
- return rows;
86
- }
87
- collectCandidates(rows) {
88
- // Group rows by session_id preserving arrival order. The SQL already
89
- // orders by (session_id ASC, ts ASC), so a sequential scan yields each
90
- // session's tool_calls in execution order.
91
- const bySession = new Map();
92
- for (const r of rows) {
93
- let list = bySession.get(r.session_id);
94
- if (!list) {
95
- list = [];
96
- bySession.set(r.session_id, list);
97
- }
98
- list.push(r);
99
- }
100
- // 2-grams: ordered pair (a, b) of consecutive tool names per session.
101
- // 3-grams: ordered triple (a, b, c) where the middle tool b failed
102
- // (success = 0). These capture the "X→Y fails then Z" lifecycle that
103
- // the plan §B6.10 calls out.
104
- const map = new Map();
105
- const record = (key, content, sessionId) => {
106
- let cand = map.get(key);
107
- if (!cand) {
108
- cand = { key, content, sessions: new Set() };
109
- map.set(key, cand);
110
- }
111
- cand.sessions.add(sessionId);
112
- };
113
- for (const [sessionId, list] of bySession) {
114
- for (let i = 0; i < list.length - 1; i++) {
115
- const a = list[i];
116
- const b = list[i + 1];
117
- const key2 = `2g::${a.tool}::${b.tool}`;
118
- const content2 = `Pattern: tool "${a.tool}" followed by tool "${b.tool}". Review the ${a.tool}→${b.tool} contract before retrying.`;
119
- record(key2, content2, sessionId);
120
- if (i + 2 < list.length) {
121
- const c = list[i + 2];
122
- if (b.success === 0) {
123
- const key3 = `3g::${a.tool}::${b.tool}::${c.tool}`;
124
- const content3 = `Pattern: tool "${a.tool}" followed by failing tool "${b.tool}" then tool "${c.tool}". Review the ${a.tool}→${b.tool}(failed)→${c.tool} contract before retrying.`;
125
- record(key3, content3, sessionId);
126
- }
127
- }
128
- }
129
- }
130
- return Array.from(map.values());
131
- }
132
- }
@@ -1,124 +0,0 @@
1
- import type { DispatchedLesson } from "./Reflector.js";
2
- /**
3
- * v0.4.0 (K4-003/K4-004 / plan §5.1) — QualityGate.
4
- *
5
- * Pure, NO-LLM predicates deciding whether a lesson is worth injecting into
6
- * the model's prompt. Principle 11: never inject noise.
7
- *
8
- * - `evaluate` — classifies a lesson (rescued errorType, non-generic
9
- * suggestion, isActionable, strength).
10
- * - `rescueErrorType` — a dispatched code overrides the coarse
11
- * `errorType` (e.g. `'unknown'` → `'TS2304'`), fixing the defect where
12
- * lessons said "fails with unknown" even when the Reflector DID identify
13
- * the error code.
14
- * - `canInject` — per-session + per-fingerprint admission rules (K4-004).
15
- */
16
- /** The v0.1.x fallback suggestions. A lesson whose suggestion is one of these
17
- * (and has no dispatched code) carries no actionable information. */
18
- export declare const GENERIC_SUGGESTIONS: ReadonlySet<string>;
19
- export declare function isGenericSuggestion(suggestion: string): boolean;
20
- export interface LessonQuality {
21
- /** Rescued errorType: the dispatched code when one matched, else the
22
- * coarse errorType (may be `'unknown'`). */
23
- errorType: string;
24
- /** The lesson's suggestion text (rule-produced or generic fallback). */
25
- suggestion: string;
26
- /** True when the lesson carries actionable information: a dispatched
27
- * code exists OR the suggestion is not one of the generic fallbacks. */
28
- isActionable: boolean;
29
- /** `strong` when a code matched or the rescued errorType is not
30
- * `'unknown'`; `weak` otherwise. */
31
- strength: "strong" | "weak";
32
- }
33
- export interface QualityLesson {
34
- errorType: string;
35
- suggestion: string;
36
- }
37
- export interface InjectionContext {
38
- /** Memory ids already injected in the current session. */
39
- seenThisSession: Set<string>;
40
- /** Failing tool_calls count for this fingerprint in the current session
41
- * (0 = never recurred after injection). */
42
- recurrenceCount: number;
43
- /** v0.6.0 (K6-022 / plan §5.8) — the `injection_confidence_floor`
44
- * setting, read once per plan/inject call by the ContextInjector.
45
- * When present, a memory whose `confidence` is below it is rejected by
46
- * the zero branch (before the seen-set). Undefined disables the branch:
47
- * memories without a computed confidence are never dropped by it. */
48
- confidenceFloor?: number;
49
- }
50
- /**
51
- * v0.5.0 (K5-006 / plan §5.2, D5-04) — the reason a memory was rejected
52
- * (or admitted). Every rejection reason maps 1:1 to an
53
- * `injections_blocked_*` counter; a silent boolean is an unmeasurable
54
- * policy (principle 16).
55
- */
56
- export type GateReason = "ok" | "low_confidence" | "seen_this_session" | "ignored" | "not_active" | "recurrence" | "weak";
57
- export interface GateVerdict {
58
- readonly allowed: boolean;
59
- readonly reason: GateReason;
60
- }
61
- export declare const QualityGate: {
62
- /**
63
- * A dispatched code always overrides the coarse errorType for display:
64
- * `'unknown'` + `TS2304` → `'TS2304'`.
65
- */
66
- rescueErrorType(dispatch: DispatchedLesson | null, errorType: string): string;
67
- /** Classify a lesson per §5.1 rule 1 (rescue) and the generic ban. */
68
- evaluate(lesson: QualityLesson, dispatch: DispatchedLesson | null, errorType: string): LessonQuality;
69
- /**
70
- * v0.4.0 (K4-004 / plan §5.1 rules 2-4) — admission gate evaluated at
71
- * injection time. A lesson is injectable only when:
72
- *
73
- * 1. its memory id was not already injected this session (seen-set),
74
- * 2. its memory is not `stale`,
75
- * 3. its fingerprint has not recurred since injection (`recurrenceCount
76
- * === 0` — a lesson that failed to prevent the error leaves the
77
- * prompt),
78
- * 4. it is actionable AND strong — weak lessons with generic suggestions
79
- * are never injected (unless `qualityGateEnabled = false` for debug,
80
- * K4-023).
81
- *
82
- * v0.5.0 (K5-006 / plan §5.2, D5-04) — the boolean form is a thin
83
- * wrapper over `canInjectVerdict`; the reason is never discarded by
84
- * callers that use the verdict form.
85
- */
86
- canInject(memory: {
87
- id: string;
88
- status?: string;
89
- strength?: "strong" | "weak";
90
- isActionable?: boolean;
91
- }, ctx: InjectionContext, qualityGateEnabled?: boolean): boolean;
92
- /**
93
- * v0.5.0 (K5-006 / plan §5.2, D5-04) — the verdict form of `canInject`.
94
- * Branch order preserves the v0.4.0 sequence with `ignored` inserted
95
- * second:
96
- *
97
- * 0. `confidence < ctx.confidenceFloor` → `low_confidence` (v0.6.0,
98
- * K6-022 — runs FIRST: it is the cheapest check, and a memory below
99
- * the floor should not consume a seen-set slot it will never use)
100
- * 1. seen this session → `seen_this_session`
101
- * 2. `memory.ignored === true` → `ignored`
102
- * 3. status !== 'active' → `not_active`
103
- * 4. recurrence in session → `recurrence`
104
- * 5. weak / not actionable → `weak` (skipped entirely when
105
- * `qualityGateEnabled === false` — the debug flag bypasses the
106
- * quality check only, never the seen/ignored/status/recurrence bans)
107
- *
108
- * `ignored` is a human verdict (D5-07) and is enforced even in debug
109
- * mode: the flag excludes a memory from retrieval entirely, so the gate
110
- * is only ever asked about it defensively.
111
- *
112
- * The floor comparison is `>=`-admissive: a memory exactly AT the floor
113
- * passes the branch (asserted in K6-022). A floor of '0' admits
114
- * everything the other five branches allow.
115
- */
116
- canInjectVerdict(memory: {
117
- id: string;
118
- status?: string;
119
- strength?: "strong" | "weak";
120
- isActionable?: boolean;
121
- ignored?: boolean;
122
- confidence?: number;
123
- }, ctx: InjectionContext, qualityGateEnabled?: boolean): GateVerdict;
124
- };