@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,291 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { closeSync, fsyncSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from "node:fs";
3
- import { unifiedDiff } from "./diff.js";
4
- import { escapeForFence, escapeForMarkerBlock, escapeForOkfLine, } from "./escape.js";
5
- import { uuidv7 } from "./uuid.js";
6
- // v0.6.0 (K6-005 / plan §5.1, D6-02) — the frozen marker contract. The exact
7
- // byte sequences are load-bearing: README, v1.0.0 plan C-01 and the round-trip
8
- // test all depend on them.
9
- export const MARKER_BEGIN = "<!-- kevin:begin — curated by opencode-kevin, safe to edit -->";
10
- export const MARKER_END = "<!-- kevin:end -->";
11
- function sha256(text) {
12
- return createHash("sha256").update(text, "utf8").digest("hex");
13
- }
14
- /**
15
- * Rule 5 (K6-008 / plan §5.1, D6-02) — the line-ending style comes from the
16
- * FIRST line ending in the existing file: CRLF if it is CRLF, otherwise LF.
17
- * A CRLF file whose last line lacks a terminator, and a mixed-ending file,
18
- * are both resolved by the same first-ending rule, deterministically.
19
- */
20
- function detectEol(text) {
21
- const nl = text.indexOf("\n");
22
- if (nl === -1)
23
- return "\n";
24
- return nl > 0 && text[nl - 1] === "\r" ? "\r\n" : "\n";
25
- }
26
- /**
27
- * Rule 5 (K6-008) — normalize the generated body into the file's line-ending
28
- * style so a CRLF file stays CRLF everywhere, including inside the block.
29
- */
30
- function normalizeEol(body, eol) {
31
- return body.replace(/\r\n/g, "\n").replace(/\n/g, eol);
32
- }
33
- /**
34
- * Rule 9, layer (a) — superseded in v1.0.0 by `escapeForMarkerBlock`
35
- * in `plugin/escape.ts` (K10-027), which carries the same idempotent
36
- * entity discipline.
37
- */
38
- /**
39
- * v1.0.0 (K10-027 / plan §5.7) — the boundary functions now live in
40
- * `plugin/escape.ts`; this composer applies them in fence-then-marker
41
- * order. Layer (b) — strip any line containing `kevin:begin` or
42
- * `kevin:end`, in any casing — is a filter rather than an escape and
43
- * stays here. Without layer (b), a memory containing a literal
44
- * `<!-- kevin:end -->` line would close the marker comment early and
45
- * let subsequent content escape the curated region on the next
46
- * regeneration — a marker-injection variant of the v0.1.5
47
- * prompt-injection defect (plan §3.5). The trailing `-->` strip is kept
48
- * as defence in depth although `escapeForMarkerBlock` already escapes
49
- * every `>` that could form one.
50
- */
51
- export function sanitizeArtifactBody(body) {
52
- const escaped = escapeForMarkerBlock(escapeForFence(body));
53
- const kept = escaped
54
- .split("\n")
55
- .filter((line) => !/kevin:begin|kevin:end/i.test(line))
56
- .join("\n");
57
- return kept.replace(/-->/g, "");
58
- }
59
- /**
60
- * v1.0.0 (K10-027 / plan §5.7, rule 2) — whole-file writes escape by
61
- * container. Only `.okf` files are line-oriented JSON: each line gets
62
- * `escapeForOkfLine`, which is the identity on a well-formed OKF line
63
- * (canonical JSON never contains raw control characters) and therefore
64
- * preserves the re-render `noop`. Kevin-owned markdown paths keep their
65
- * bytes — fences there are legitimate content.
66
- */
67
- export function escapeForContainer(path, content) {
68
- if (!path.endsWith(".okf"))
69
- return content;
70
- return content
71
- .split("\n")
72
- .map((line) => {
73
- // A CRLF file (healHeader preserves the original EOL) leaves a
74
- // trailing \r on each line after the split — it is line-ending
75
- // bytes, not statement content, so it must survive untouched.
76
- if (line.endsWith("\r")) {
77
- return `${escapeForOkfLine(line.slice(0, -1))}\r`;
78
- }
79
- return escapeForOkfLine(line);
80
- })
81
- .join("\n");
82
- }
83
- /**
84
- * v0.6.0 (K6-005 / plan §5.1) — the single write path to disk (D6-01).
85
- *
86
- * `plan()` is pure: it reads the target file, locates the marker pair, splices
87
- * the body between the markers and returns a {@link WritePlan}. It performs no
88
- * writes — rule 1. `apply()` is implemented by K6-007; until then it is a stub
89
- * that throws, so no caller can accidentally write before the audit trail
90
- * exists.
91
- *
92
- * `projectId` is a constructor argument rather than a per-call argument so that
93
- * every audit row is attributed without the call site having to remember.
94
- */
95
- export class ArtifactWriter {
96
- store;
97
- projectId;
98
- metrics;
99
- constructor(store, projectId, metrics) {
100
- this.store = store;
101
- this.projectId = projectId;
102
- this.metrics = metrics ?? null;
103
- }
104
- plan(pathOrRequest, body) {
105
- const request = typeof pathOrRequest === "string"
106
- ? { path: pathOrRequest, mode: "markers", content: body ?? "" }
107
- : pathOrRequest;
108
- const { path, mode, content } = request;
109
- let before;
110
- try {
111
- // Read as Buffer, not as utf8 text: readFileSync's text decoding
112
- // strips a leading BOM, which would silently drop it on the next
113
- // write. Buffer.toString keeps \uFEFF as a character of `before`.
114
- before = readFileSync(path).toString("utf8");
115
- }
116
- catch (err) {
117
- if (err instanceof Error && "code" in err && err.code === "ENOENT") {
118
- before = "";
119
- }
120
- else {
121
- throw err;
122
- }
123
- }
124
- if (mode === "whole") {
125
- // K8-019 (D8-08) — the whole-file path. The file is Kevin-owned,
126
- // so there are no markers, no sanitization and no EOL
127
- // normalization: the rendered bytes are written as-is, which is
128
- // what makes a re-render of the same content a `noop`. A
129
- // caller-side refusal leaves the file untouched and is audited
130
- // like any other refusal: after = before, both hashes recorded.
131
- const refusal = request.refusal;
132
- const refused = refusal !== undefined;
133
- // K10-027 — the container boundary: OKF lines are escaped at the
134
- // single write path, never by callers.
135
- const after = refused ? before : escapeForContainer(path, content);
136
- return {
137
- path,
138
- before,
139
- after,
140
- diff: unifiedDiff(path, before, after),
141
- outcome: refused ? "refused" : after === before ? "noop" : "written",
142
- ...(refusal !== undefined ? { reason: refusal } : {}),
143
- hashBefore: sha256(before),
144
- hashAfter: sha256(after),
145
- };
146
- }
147
- const eol = detectEol(before);
148
- // Rule 9 — sanitation happens in plan(), before hashing, so the hashes
149
- // describe what was actually written.
150
- const bodyEol = normalizeEol(sanitizeArtifactBody(content), eol);
151
- const firstBegin = before.indexOf(MARKER_BEGIN);
152
- const firstEnd = before.indexOf(MARKER_END);
153
- let after;
154
- let outcome = "written";
155
- let reason;
156
- if (firstBegin === -1 && firstEnd === -1) {
157
- // Rule 2 — create: the block is appended at the end of the content,
158
- // preceded by a blank line. For an empty file (missing file treated
159
- // as "") the result is exactly: blank line, MARKER_BEGIN, body,
160
- // MARKER_END, trailing newline.
161
- const separator = before === "" ? eol : before.endsWith(eol) ? eol : eol + eol;
162
- after =
163
- before +
164
- separator +
165
- MARKER_BEGIN +
166
- eol +
167
- bodyEol +
168
- eol +
169
- MARKER_END +
170
- eol;
171
- }
172
- else if (firstBegin === -1 || firstEnd === -1) {
173
- // Rule 3 — exactly one marker present.
174
- after = before;
175
- outcome = "refused";
176
- reason =
177
- firstBegin === -1
178
- ? "kevin:end marker present without kevin:begin"
179
- : "kevin:begin marker present without kevin:end";
180
- }
181
- else if (firstEnd < firstBegin) {
182
- // Rule 3 — MARKER_END precedes MARKER_BEGIN.
183
- after = before;
184
- outcome = "refused";
185
- reason = "kevin:end appears before kevin:begin";
186
- }
187
- else if (before.indexOf(MARKER_BEGIN, firstBegin + MARKER_BEGIN.length) !== -1 ||
188
- before.indexOf(MARKER_END, firstEnd + MARKER_END.length) !== -1) {
189
- // Rule 3 — more than one pair (any additional marker occurrence).
190
- after = before;
191
- outcome = "refused";
192
- reason = "more than one marker pair present";
193
- }
194
- else {
195
- // Rule 4 — bytes outside the marker pair are byte-identical. The
196
- // block between the markers (begin marker, old block, end marker)
197
- // is replaced by the regenerated block.
198
- const blockEnd = firstEnd + MARKER_END.length;
199
- after =
200
- before.slice(0, firstBegin) +
201
- MARKER_BEGIN +
202
- eol +
203
- bodyEol +
204
- eol +
205
- MARKER_END +
206
- before.slice(blockEnd);
207
- // Rule 6 — unchanged content is a noop, never a write.
208
- outcome = after === before ? "noop" : "written";
209
- }
210
- return {
211
- path,
212
- before,
213
- after,
214
- // v0.6.0 (K6-006 / plan §5.2, D6-05) — approval prompts show bytes,
215
- // never prose. Identical inputs yield "".
216
- diff: unifiedDiff(path, before, after),
217
- outcome,
218
- ...(reason !== undefined ? { reason } : {}),
219
- hashBefore: sha256(before),
220
- hashAfter: sha256(after),
221
- };
222
- }
223
- /**
224
- * K8-019 (D8-08) — the single write funnel: every file Kevin writes
225
- * goes through this method, which is the ONLY call site of `apply()`
226
- * in the plugin (asserted by tests/unit/single_write_path.test.ts).
227
- */
228
- write(request, proposalId) {
229
- return this.apply(this.plan(request), proposalId);
230
- }
231
- // v0.6.0 (K6-007 / plan §5.1, rules 7–8) — atomic write + audit row.
232
- apply(plan, proposalId) {
233
- // Rule 8 — refusals and noops still leave an audit trail; a refusal
234
- // that leaves no trace is indistinguishable from a write that never
235
- // happened. Rule 6 — a noop creates no temp file and writes nothing.
236
- if (plan.outcome === "noop" || plan.outcome === "refused") {
237
- if (plan.outcome === "noop") {
238
- this.metrics?.incr("artifact_writes_noop", 1);
239
- }
240
- this.audit(plan, proposalId);
241
- return plan.outcome;
242
- }
243
- // Rule 7 — write to `<path>.kevin.tmp` in the same directory (same
244
- // filesystem, so rename is atomic), fsync, close, then rename over the
245
- // target. Never write the target path directly, never truncate-then-write.
246
- const tmpPath = `${plan.path}.kevin.tmp`;
247
- let fd;
248
- try {
249
- fd = openSync(tmpPath, "w");
250
- writeSync(fd, plan.after, null, "utf8");
251
- fsyncSync(fd);
252
- closeSync(fd);
253
- fd = undefined;
254
- this.renameTemp(tmpPath, plan.path);
255
- }
256
- catch (err) {
257
- // Never leave .kevin.tmp litter next to the user's file.
258
- if (fd !== undefined) {
259
- try {
260
- closeSync(fd);
261
- }
262
- catch {
263
- // already closed or unusable; unlink below is the cleanup
264
- }
265
- }
266
- try {
267
- unlinkSync(tmpPath);
268
- }
269
- catch {
270
- // nothing to clean up
271
- }
272
- throw err;
273
- }
274
- this.metrics?.incr("artifact_writes_total", 1);
275
- this.audit(plan, proposalId);
276
- return "written";
277
- }
278
- // Fault-injection seam for the atomicity test: the rename is the point
279
- // where a failure must leave the target untouched and the temp file gone.
280
- renameTemp(tmpPath, target) {
281
- renameSync(tmpPath, target);
282
- }
283
- audit(plan, proposalId) {
284
- this.store
285
- .prepare(`INSERT INTO artifact_writes
286
- (id, proposal_id, project_id, path, bytes_before, bytes_after,
287
- hash_before, hash_after, outcome, reason)
288
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
289
- .run(uuidv7(), proposalId ?? null, this.projectId, plan.path, Buffer.byteLength(plan.before, "utf8"), Buffer.byteLength(plan.after, "utf8"), plan.hashBefore, plan.hashAfter, plan.outcome, plan.reason ?? null);
290
- }
291
- }
@@ -1,22 +0,0 @@
1
- import { type EnrichFn } from "./LessonFixer.js";
2
- import type { MemoryService } from "./MemoryService.js";
3
- import type { Store } from "./Store.js";
4
- import type { Metrics } from "./metrics.js";
5
- export declare class CausalChain {
6
- private store;
7
- private memoryService;
8
- private metrics;
9
- private enrichFn?;
10
- constructor(store: Store, memoryService: MemoryService, metrics: Metrics | null, enrichFn?: EnrichFn | undefined);
11
- onSuccess(_tool: string, _args: Record<string, unknown>, _projectId: string | null, sessionId: string): void;
12
- onSessionIdle(sessionId: string): Promise<number>;
13
- /**
14
- * v0.4.0 (K4-015) — fire the opt-in enrich hook at most once per
15
- * promoted pattern. The hook's one-line phrase replaces the
16
- * deterministic `Fixed by:` line; null keeps it. A call (phrase or
17
- * not) stamps `metadata.enriched` so repeated idle cycles stay at
18
- * one LLM call per pattern.
19
- */
20
- private enrichIfEnabled;
21
- private isLlmReflectionEnabled;
22
- }
@@ -1,197 +0,0 @@
1
- import { extractFixArgs, } from "./LessonFixer.js";
2
- import { hasColumn } from "./columns.js";
3
- import { toMs } from "./time-ms.js";
4
- /** K3-007 — a success only links to a failure within this many calls. */
5
- const MAX_LINK_DISTANCE = 10;
6
- export class CausalChain {
7
- store;
8
- memoryService;
9
- metrics;
10
- enrichFn;
11
- constructor(store, memoryService, metrics,
12
- // v0.4.0 (K4-015) — opt-in promotion-time LLM phrasing. Absent in
13
- // production (zero network calls by default); injected by tests or
14
- // by a future settings-driven wiring.
15
- enrichFn) {
16
- this.store = store;
17
- this.memoryService = memoryService;
18
- this.metrics = metrics;
19
- this.enrichFn = enrichFn;
20
- }
21
- // K3-007: link a success to the failing fingerprint only when it
22
- // occurred within MAX_LINK_DISTANCE tool calls of the failure (plan
23
- // §K3-007 acceptance: "within 24h and within 10 tool calls").
24
- // v0.3.0 fix (bug #3) — the old code linked the most recent session
25
- // success to the most recent unlinked error memory regardless of
26
- // distance, so an unrelated success (e.g. an `ls` run after a
27
- // typecheck failure) was stamped as the fix for that error.
28
- onSuccess(_tool, _args, _projectId, sessionId) {
29
- // v1.1.0 (K11-004 / plan §5.2, D11-01/D11-07) — ms-aware window: prefer _ms
30
- const hasTsMs = hasColumn(this.store, "tool_calls", "ts_ms");
31
- const successRow = (hasTsMs
32
- ? this.store.prepare(`SELECT rowid, tool, args_summary, ts, ts_ms FROM tool_calls
33
- WHERE session_id = ? AND success = 1
34
- ORDER BY rowid DESC LIMIT 1`)
35
- : this.store.prepare(`SELECT rowid, tool, args_summary, ts FROM tool_calls
36
- WHERE session_id = ? AND success = 1
37
- ORDER BY rowid DESC LIMIT 1`)).get(sessionId);
38
- if (!successRow)
39
- return;
40
- const linkedFps = new Set(this.store
41
- .prepare(`SELECT DISTINCT fix_for_fingerprint FROM tool_calls
42
- WHERE session_id = ? AND fix_for_fingerprint IS NOT NULL`)
43
- .all(sessionId).map((r) => r.fix_for_fingerprint));
44
- // Most recent failing calls in this session, newest first. The
45
- // failing call's `error_fingerprint` (stamped by Reflector via
46
- // onLinkError) is the SAME identity dimension the error memory
47
- // uses; `fingerprint` is the legacy tool|args|success hash and
48
- // simply never matches a reflector error memory.
49
- const failRows = (hasTsMs
50
- ? this.store.prepare(`SELECT rowid, COALESCE(error_fingerprint, fingerprint) AS fp, ts, ts_ms
51
- FROM tool_calls
52
- WHERE session_id = ?
53
- AND success = 0
54
- AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)
55
- ORDER BY rowid DESC LIMIT ?`)
56
- : this.store.prepare(`SELECT rowid, COALESCE(error_fingerprint, fingerprint) AS fp, ts
57
- FROM tool_calls
58
- WHERE session_id = ?
59
- AND success = 0
60
- AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)
61
- ORDER BY rowid DESC LIMIT ?`)).all(sessionId, MAX_LINK_DISTANCE);
62
- for (const fail of failRows) {
63
- if (!fail.fp || linkedFps.has(fail.fp))
64
- continue;
65
- const dist = successRow.rowid - fail.rowid;
66
- if (dist <= 0 || dist > MAX_LINK_DISTANCE)
67
- continue;
68
- // v1.1.0 — ≤24h window uses ms when available
69
- const successMs = toMs(successRow.ts, successRow.ts_ms ?? null);
70
- const failMs = toMs(fail.ts, fail.ts_ms ?? null);
71
- if (successMs !== null && failMs !== null) {
72
- const diff = successMs - failMs;
73
- if (diff < 0 || diff > 86_400_000)
74
- continue;
75
- }
76
- const mem = this.store
77
- .prepare(`SELECT 1 FROM memories
78
- WHERE fingerprint = ? AND type = 'error'
79
- AND origin = 'reflector' AND status IN ('active', 'stale')
80
- AND created_at > datetime('now', '-24 hours')
81
- LIMIT 1`)
82
- .get(fail.fp);
83
- if (!mem)
84
- continue;
85
- this.store
86
- .prepare("UPDATE tool_calls SET fix_for_fingerprint = ? WHERE rowid = ?")
87
- .run(fail.fp, successRow.rowid);
88
- // v0.4.0 (K4-014) — deterministic "Fixed by:" raw material
89
- // (plan §5.4 / D4-07): copy the linked success call's
90
- // args_summary into memories.fix_args for every active row of
91
- // that fingerprint (error + pattern), zero LLM cost.
92
- const fixArgs = extractFixArgs({
93
- tool: successRow.tool,
94
- args_summary: successRow.args_summary ?? null,
95
- });
96
- if (fixArgs) {
97
- this.store
98
- .prepare(`UPDATE memories SET fix_args = ?
99
- WHERE fingerprint = ? AND status IN ('active', 'stale')`)
100
- .run(fixArgs, fail.fp);
101
- }
102
- this.metrics?.incr("causal_links", 1);
103
- return;
104
- }
105
- }
106
- async onSessionIdle(sessionId) {
107
- const linkedErrors = this.store
108
- .prepare(`SELECT m.id, m.fingerprint, m.recurrence_count,
109
- (SELECT COUNT(*)
110
- FROM tool_calls tc_all
111
- WHERE tc_all.fix_for_fingerprint = m.fingerprint) as evidence_count
112
- FROM memories m
113
- WHERE m.type = 'error'
114
- AND m.origin = 'reflector'
115
- AND m.fingerprint IN (
116
- SELECT DISTINCT fix_for_fingerprint
117
- FROM tool_calls
118
- WHERE session_id = ? AND fix_for_fingerprint IS NOT NULL
119
- )
120
- AND m.status IN ('active', 'stale')
121
- GROUP BY m.fingerprint
122
- HAVING (
123
- SELECT MAX(tc.ts) FROM tool_calls tc
124
- WHERE tc.fix_for_fingerprint = m.fingerprint
125
- ) >= COALESCE(
126
- (SELECT MAX(m2.updated_at) FROM memories m2
127
- WHERE m2.fingerprint = m.fingerprint
128
- AND m2.type = 'pattern'
129
- AND m2.origin = 'causal'),
130
- '1970-01-01'
131
- )`)
132
- .all(sessionId);
133
- let promoted = 0;
134
- for (const err of linkedErrors) {
135
- try {
136
- const result = this.memoryService.promoteToPattern(err.id, err.evidence_count, err.recurrence_count ?? 0);
137
- if (result) {
138
- promoted++;
139
- // v0.4.0 (K4-009) — only a NEW pattern row counts as a
140
- // promotion; the idempotent refresh path no longer
141
- // inflates the metric. `patterns_causal` is deprecated
142
- // (key kept for compat, never incremented).
143
- if (result.created) {
144
- this.metrics?.incr("patterns_promoted_new", 1);
145
- // v0.4.0 (K4-015) — promotion-time LLM enrichment:
146
- // at most one call per NEW pattern, gated on
147
- // `kevin_settings.llm_reflection_enabled` and the
148
- // per-pattern `metadata.enriched` marker. Never on
149
- // the failure hot path.
150
- await this.enrichIfEnabled(err.id, result.id);
151
- }
152
- }
153
- }
154
- catch {
155
- // promoteToPattern may fail if the error memory was removed
156
- }
157
- }
158
- return promoted;
159
- }
160
- /**
161
- * v0.4.0 (K4-015) — fire the opt-in enrich hook at most once per
162
- * promoted pattern. The hook's one-line phrase replaces the
163
- * deterministic `Fixed by:` line; null keeps it. A call (phrase or
164
- * not) stamps `metadata.enriched` so repeated idle cycles stay at
165
- * one LLM call per pattern.
166
- */
167
- async enrichIfEnabled(errorId, patternId) {
168
- if (!this.enrichFn || !this.isLlmReflectionEnabled())
169
- return;
170
- const pattern = this.memoryService.getById(patternId);
171
- if (!pattern)
172
- return;
173
- const meta = (pattern.metadata ?? {});
174
- if (meta.enriched === true)
175
- return;
176
- const phrase = await this.enrichFn({
177
- lesson: pattern.content,
178
- fixArgs: pattern.fixArgs ?? null,
179
- originalError: this.memoryService.getById(errorId)?.content ?? null,
180
- });
181
- const content = phrase
182
- ? pattern.content.includes("\nFixed by: ")
183
- ? pattern.content.replace(/\nFixed by: .+$/s, `\n${phrase}`)
184
- : `${pattern.content}\n${phrase}`
185
- : pattern.content;
186
- this.memoryService.update(patternId, {
187
- content,
188
- metadata: { ...meta, enriched: true },
189
- });
190
- }
191
- isLlmReflectionEnabled() {
192
- const row = this.store
193
- .prepare("SELECT value FROM kevin_settings WHERE key = ?")
194
- .get("llm_reflection_enabled");
195
- return row?.value === "1";
196
- }
197
- }
@@ -1,41 +0,0 @@
1
- import { type PendingProposal } from "./TuiActions.js";
2
- export type BridgeDeps = {
3
- readonly getPending: () => readonly PendingProposal[];
4
- readonly approve: (proposalId: string) => unknown;
5
- readonly reject: (proposalId: string, note?: string) => unknown;
6
- readonly acknowledge: (conflictId: string) => unknown;
7
- readonly metrics?: {
8
- incr: (key: "tui_actions_invoked", by?: number) => void;
9
- } | null;
10
- };
11
- export type ParsedBridgeCommand = {
12
- readonly type: "approve";
13
- readonly proposalId: string;
14
- readonly token: string;
15
- readonly note?: string;
16
- } | {
17
- readonly type: "reject";
18
- readonly proposalId: string;
19
- readonly token: string;
20
- readonly note?: string;
21
- } | {
22
- readonly type: "ack";
23
- readonly conflictId: string;
24
- };
25
- export declare function parseBridgeCommand(text: string): ParsedBridgeCommand | null;
26
- export declare const KEVIN_COMMAND_RE: RegExp;
27
- export interface BridgeResult {
28
- readonly handled: boolean;
29
- readonly status?: "applied" | "rejected" | "stale_skipped" | "error";
30
- readonly detail?: string;
31
- }
32
- /**
33
- * Execute a chat message through the bridge.
34
- * - Non-matching → {handled:false} (byte-identical pass-through)
35
- * - Matching but stale/invalid → {handled:false, status:"stale_skipped"} (pass-through + counter)
36
- * - Valid → executes via deps handlers, {handled:true}
37
- *
38
- * Valid commands are SWALLOWED — caller must not forward to model.
39
- * Invalid/stale commands pass through untouched (D12-09).
40
- */
41
- export declare function handleBridgeCommand(text: string, deps: BridgeDeps): BridgeResult;
@@ -1,103 +0,0 @@
1
- // v1.2.0 (K12-018 / plan §4.4 R3, D12-09) — chat-command bridge (universal actions).
2
- // Hot path: ONE regex test on non-match, allocation-free.
3
- import { verifyFresh, } from "./TuiActions.js";
4
- // Exact regex for approve/reject (require 16-hex token, optional note capture).
5
- // Ack variant is token-free (acknowledge is non-destructive, matches mailbox).
6
- const APPROVE_REJECT_RE = /^\/kevin-(approve|reject)\s+(\S+)\s+([0-9a-f]{16})(?:\s+([\s\S]+))?$/;
7
- const ACK_RE = /^\/kevin-ack\s+(\S+)\s*$/;
8
- // Combined pattern for documentation / static analysis (covers both forms).
9
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
10
- const _DOC_RE = /^\/kevin-(approve|reject|ack)\s+(\S+)\s+([0-9a-f]{16})(?:\s+([\s\S]+))?$/;
11
- export function parseBridgeCommand(text) {
12
- // Approve / reject — require token
13
- const m = APPROVE_REJECT_RE.exec(text);
14
- if (m) {
15
- const type = m[1];
16
- const proposalId = m[2];
17
- const token = m[3];
18
- const note = m[4] !== undefined ? m[4] : undefined;
19
- if (type === "approve")
20
- return {
21
- type: "approve",
22
- proposalId,
23
- token,
24
- ...(note !== undefined ? { note } : {}),
25
- };
26
- return {
27
- type: "reject",
28
- proposalId,
29
- token,
30
- ...(note !== undefined ? { note } : {}),
31
- };
32
- }
33
- const ack = ACK_RE.exec(text);
34
- if (ack) {
35
- return { type: "ack", conflictId: ack[1] };
36
- }
37
- return null;
38
- }
39
- // Backward alias for tests that import by plan name
40
- export const KEVIN_COMMAND_RE = APPROVE_REJECT_RE;
41
- /**
42
- * Execute a chat message through the bridge.
43
- * - Non-matching → {handled:false} (byte-identical pass-through)
44
- * - Matching but stale/invalid → {handled:false, status:"stale_skipped"} (pass-through + counter)
45
- * - Valid → executes via deps handlers, {handled:true}
46
- *
47
- * Valid commands are SWALLOWED — caller must not forward to model.
48
- * Invalid/stale commands pass through untouched (D12-09).
49
- */
50
- export function handleBridgeCommand(text, deps) {
51
- const parsed = parseBridgeCommand(text);
52
- if (!parsed)
53
- return { handled: false };
54
- if (parsed.type === "ack") {
55
- // Ack is non-destructive, no token verification — matches mailbox acknowledge semantics.
56
- try {
57
- deps.acknowledge(parsed.conflictId);
58
- try {
59
- deps.metrics?.incr("tui_actions_invoked", 1);
60
- }
61
- catch { }
62
- return { handled: true, status: "applied" };
63
- }
64
- catch (err) {
65
- const msg = err instanceof Error ? err.message : String(err);
66
- return { handled: true, status: "error", detail: msg };
67
- }
68
- }
69
- // approve / reject — verify token against CURRENT pending state (same as mailbox)
70
- const pending = deps.getPending();
71
- const action = {
72
- type: parsed.type,
73
- proposalId: parsed.proposalId,
74
- token: parsed.token,
75
- ...(parsed.note !== undefined ? { note: parsed.note } : {}),
76
- };
77
- const fresh = verifyFresh(action, pending);
78
- if (!fresh.ok) {
79
- // Invalid/stale → pass-through untouched (do NOT swallow). Caller should forward byte-identically.
80
- // Audit counter could be incremented here, but not as contract metric (blockedSnapshot-style internal).
81
- return { handled: false, status: "stale_skipped", detail: fresh.reason };
82
- }
83
- try {
84
- if (parsed.type === "approve") {
85
- deps.approve(parsed.proposalId);
86
- try {
87
- deps.metrics?.incr("tui_actions_invoked", 1);
88
- }
89
- catch { }
90
- return { handled: true, status: "applied" };
91
- }
92
- deps.reject(parsed.proposalId, parsed.note);
93
- try {
94
- deps.metrics?.incr("tui_actions_invoked", 1);
95
- }
96
- catch { }
97
- return { handled: true, status: "rejected" };
98
- }
99
- catch (err) {
100
- const msg = err instanceof Error ? err.message : String(err);
101
- return { handled: true, status: "error", detail: msg };
102
- }
103
- }