@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,180 +0,0 @@
1
- // v1.2.0 (K12-017 / plan §4.4 R2, D12-10) — static dashboard generator.
2
- // Single self-contained file: inline CSS/JS, snapshot data embedded as const DATA.
3
- // Zero network: no fetch, no XHR, no WebSocket, no external asset.
4
- import { createHash } from "node:crypto";
5
- import { mkdirSync, renameSync, writeFileSync } from "node:fs";
6
- import { join } from "node:path";
7
- const CAP_BYTES = 512 * 1024;
8
- function byteLen(s) {
9
- return Buffer.byteLength(s, "utf8");
10
- }
11
- // HTML escaping — mirrors escapeInjectedText discipline (C-09 family).
12
- export function escapeHtml(text) {
13
- return text
14
- .replace(/&/g, "&")
15
- .replace(/</g, "&lt;")
16
- .replace(/>/g, "&gt;")
17
- .replace(/"/g, "&quot;");
18
- }
19
- export function proposalToken(proposalId, proposedText) {
20
- return createHash("sha256")
21
- .update(`${proposalId}\0${proposedText}`, "utf8")
22
- .digest("hex")
23
- .slice(0, 16);
24
- }
25
- function truncateForHtml(views, cap) {
26
- // Estimate html size; if over cap, truncate diffs progressively.
27
- let html = renderDashboardInner(views, false);
28
- if (byteLen(html) <= cap)
29
- return views;
30
- // First pass: cap diffs to 2000 chars
31
- let truncated = {
32
- ...views,
33
- proposals: views.proposals.map((p) => {
34
- if (p.diff.length <= 2000)
35
- return p;
36
- return {
37
- ...p,
38
- diff: `${p.diff.slice(0, 2000)}\n…[truncated]`,
39
- truncated: true,
40
- };
41
- }),
42
- };
43
- html = renderDashboardInner(truncated, false);
44
- if (byteLen(html) <= cap)
45
- return truncated;
46
- // Second: 800 chars
47
- truncated = {
48
- ...views,
49
- proposals: views.proposals.map((p) => ({
50
- ...p,
51
- diff: p.diff.length > 800 ? `${p.diff.slice(0, 800)}\n…[truncated]` : p.diff,
52
- truncated: p.diff.length > 800 ? true : p.truncated,
53
- })),
54
- conflicts: views.conflicts.map((c) => ({
55
- ...c,
56
- a_summary: c.a_summary.slice(0, 200),
57
- b_summary: c.b_summary.slice(0, 200),
58
- })),
59
- };
60
- html = renderDashboardInner(truncated, false);
61
- if (byteLen(html) <= cap)
62
- return truncated;
63
- // Third: drop diffs to 400 chars
64
- truncated = {
65
- ...views,
66
- proposals: views.proposals.map((p) => ({
67
- ...p,
68
- diff: `${p.diff.slice(0, 400)}\n…[truncated]`,
69
- truncated: true,
70
- })),
71
- };
72
- return truncated;
73
- }
74
- function renderDashboardInner(views, _withTruncation) {
75
- const css = "*{box-sizing:border-box}body{font-family:ui-monospace,monospace;margin:0;padding:16px;background:#0f1115;color:#e6e6e6}a{color:#8ab4ff}header{border-bottom:1px solid #2a2e39;padding-bottom:12px;margin-bottom:16px}h1{margin:0;font-size:20px}h2{font-size:16px;margin:24px 0 8px;border-bottom:1px solid #222;padding-bottom:4px}.card{border:1px solid #2a2e39;border-radius:8px;padding:12px;margin:8px 0;background:#151821}.muted{color:#9aa0b2;font-size:12px}.badge{display:inline-block;padding:2px 6px;border-radius:999px;font-size:11px;border:1px solid #2a2e39}.badge-healthy{background:#12331a;color:#8ef0a0}.badge-degraded{background:#331a1a;color:#f0a0a0}.badge-unknown{background:#2a2a33;color:#c0c0d0}pre{white-space:pre-wrap;word-break:break-word;background:#0b0d12;padding:8px;border-radius:6px;overflow:auto;max-height:320px;font-size:12px}table{width:100%;border-collapse:collapse;font-size:12px}th,td{border:1px solid #2a2e39;padding:4px 6px;text-align:left}button{cursor:pointer;background:#1f2330;color:#e6e6e6;border:1px solid #2a2e39;border-radius:6px;padding:4px 8px;font-size:12px}button:hover{background:#2a3045}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}.cols{display:grid;grid-template-columns:1fr 1fr;gap:12px}.count{font-size:11px;color:#9aa0b2}";
76
- // Build proposals HTML server-side
77
- const proposalsHtml = views.proposals.length === 0
78
- ? `<div class="muted">No pending proposals.</div>`
79
- : views.proposals
80
- .map((p) => {
81
- const token = p.token ?? proposalToken(p.id, p.diff);
82
- const approveCmd = `/kevin-approve ${p.id} ${token}`;
83
- const rejectCmd = `/kevin-reject ${p.id} ${token}`;
84
- return `<div class="card">
85
- <div><strong>${escapeHtml(p.kind)}</strong> <span class="muted">${escapeHtml(p.id)}</span> <span class="badge">${escapeHtml(p.target_path)}</span> <span class="muted">${escapeHtml(p.created_at)}</span>${p.truncated ? ` <span class="badge">truncated</span>` : ""}</div>
86
- <div class="muted">memories: ${escapeHtml(p.memory_ids.join(", "))}</div>
87
- <pre>${escapeHtml(p.diff)}</pre>
88
- <div style="display:flex;gap:8px;margin-top:8px">
89
- <button data-copy="${escapeHtml(approveCmd)}" onclick="copyCmd(this)">Copy approve</button>
90
- <button data-copy="${escapeHtml(rejectCmd)}" onclick="copyCmd(this)">Copy reject</button>
91
- </div>
92
- <div class="muted copy-hint"></div>
93
- </div>`;
94
- })
95
- .join("\n");
96
- const conflictsHtml = views.conflicts.length === 0
97
- ? `<div class="muted">No open conflicts.</div>`
98
- : views.conflicts
99
- .map((c) => {
100
- const ackCmd = `/kevin-ack ${c.id}`;
101
- return `<div class="card">
102
- <div><strong>${escapeHtml(c.kind)}</strong> <span class="muted">${escapeHtml(c.id)}</span> <span class="muted">${escapeHtml(c.opened_at)}</span></div>
103
- <div class="cols"><div><div class="muted">A</div><pre>${escapeHtml(c.a_summary)}</pre></div><div><div class="muted">B</div><pre>${escapeHtml(c.b_summary)}</pre></div></div>
104
- <div style="margin-top:8px"><button data-copy="${escapeHtml(ackCmd)}" onclick="copyCmd(this)">Copy ack</button> <span class="muted copy-hint"></span></div>
105
- </div>`;
106
- })
107
- .join("\n");
108
- const verdictClass = views.health.verdict === "healthy"
109
- ? "badge-healthy"
110
- : views.health.verdict === "degraded"
111
- ? "badge-degraded"
112
- : "badge-unknown";
113
- const hooksRows = views.health.hooks.length === 0
114
- ? `<tr><td colspan="4" class="muted">No hooks</td></tr>`
115
- : views.health.hooks
116
- .map((h) => `<tr><td>${escapeHtml(h.hook)}</td><td>${escapeHtml(h.state)}</td><td>${h.fire_count}</td><td>${h.expected_count}</td></tr>`)
117
- .join("\n");
118
- const perfRows = views.health.perf.length === 0
119
- ? `<tr><td colspan="4" class="muted">No perf data</td></tr>`
120
- : views.health.perf
121
- .map((p) => `<tr><td>${escapeHtml(p.scope)}</td><td>${p.p95}</td><td>${p.budget_p95}</td><td>${p.within_budget ? "yes" : "no"}</td></tr>`)
122
- .join("\n");
123
- const countersHtml = Object.keys(views.health.counters).length === 0
124
- ? `<div class="muted">No counters</div>`
125
- : `<div class="grid">${Object.entries(views.health.counters)
126
- .map(([k, v]) => `<div class="card"><div class="muted">${escapeHtml(k)}</div><div><strong>${v}</strong></div></div>`)
127
- .join("\n")}</div>`;
128
- // Embedded DATA — escape every "<" to \u003c so hostile "<script>" can never appear verbatim inside the <script> block.
129
- const dataJson = JSON.stringify(views).replace(/</g, "\\u003c");
130
- const js = `function copyCmd(btn){var cmd=btn.getAttribute('data-copy');var hint=btn.parentElement.nextElementSibling;function done(t){if(hint)hint.textContent=t;setTimeout(function(){if(hint)hint.textContent='';},2500);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(cmd).then(function(){done('Copied: '+cmd+' — paste into your opencode session');},function(){fallback(cmd,done);});}else{fallback(cmd,done);}}function fallback(cmd,done){var ta=document.createElement('textarea');ta.value=cmd;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();try{document.execCommand('copy');done('Copied: '+cmd+' — paste into your opencode session');}catch(e){done('Copy failed — manually copy: '+cmd);}document.body.removeChild(ta);}`;
131
- return `<!doctype html>
132
- <html lang="en">
133
- <head>
134
- <meta charset="utf-8">
135
- <meta name="viewport" content="width=device-width,initial-scale=1">
136
- <title>Kevin — Dashboard</title>
137
- <style>${css}</style>
138
- </head>
139
- <body>
140
- <header>
141
- <h1>Kevin — Surface Dashboard</h1>
142
- <div class="muted">Generated at ${escapeHtml(views.generatedAt)} · <span class="badge ${verdictClass}">${escapeHtml(views.health.verdict)}</span> ${escapeHtml(views.health.reason)} · contract ${escapeHtml(views.health.contract_digest)}</div>
143
- <div class="muted">Proposals ${views.proposals.length} · Conflicts ${views.conflicts.length} · Paste copied <code>/kevin-*</code> commands into your opencode session (Desktop or CLI).</div>
144
- </header>
145
-
146
- <section>
147
- <h2>Proposals</h2>
148
- ${proposalsHtml}
149
- </section>
150
-
151
- <section>
152
- <h2>Conflicts</h2>
153
- ${conflictsHtml}
154
- </section>
155
-
156
- <section>
157
- <h2>Health</h2>
158
- <table><thead><tr><th>hook</th><th>state</th><th>fires</th><th>expected</th></tr></thead><tbody>${hooksRows}</tbody></table>
159
- <table style="margin-top:12px"><thead><tr><th>scope</th><th>p95 ms</th><th>budget p95</th><th>within</th></tr></thead><tbody>${perfRows}</tbody></table>
160
- <div style="margin-top:12px">${countersHtml}</div>
161
- </section>
162
-
163
- <script>const DATA=${dataJson};${js}</script>
164
- </body>
165
- </html>`;
166
- }
167
- export function renderDashboard(views) {
168
- const capped = truncateForHtml(views, CAP_BYTES);
169
- return renderDashboardInner(capped, true);
170
- }
171
- export function writeDashboard(root, views) {
172
- const dir = join(root, "tui");
173
- mkdirSync(dir, { recursive: true });
174
- const html = renderDashboard(views);
175
- const target = join(dir, "dashboard.html");
176
- const tmp = `${target}.tmp`;
177
- writeFileSync(tmp, html, "utf8");
178
- renameSync(tmp, target);
179
- return target;
180
- }
@@ -1,67 +0,0 @@
1
- import type { Store } from "./Store.js";
2
- import type { Metrics } from "./metrics.js";
3
- /**
4
- * v0.5.0 Feedback (K5-009 / plan §5.3, D5-02).
5
- *
6
- * The human-judgement half of the glassbox: the agent can report whether a
7
- * memory was useful, wrong, outdated, or should be ignored. Verdicts are
8
- * stored in `memory_feedback` (migration 006_v05_glassbox.sql) and folded
9
- * into the memory's `feedback_positive` / `feedback_negative` counters —
10
- * kept SEPARATE from `evidence_count` / `recurrence_count` by design: human
11
- * judgement is evidence about the memory, causal counters are evidence
12
- * about the world (the confidence-poisoning defect closed in v0.4.0).
13
- *
14
- * D5-07 — the `ignore` verdict is a hard lifecycle action, not a soft
15
- * signal: the memory is stamped `ignored = 1`, which excludes it from
16
- * retrieval (K5-008) and from the quality gate (K5-007).
17
- *
18
- * Schema:
19
- * memory_feedback(id PK, memory_id, verdict, session_id, note, created_at)
20
- * memories(feedback_positive, feedback_negative, ignored)
21
- */
22
- export type FeedbackVerdict = "useful" | "wrong" | "outdated" | "ignore";
23
- export interface FeedbackRecordInput {
24
- memoryId: string;
25
- verdict: FeedbackVerdict;
26
- sessionId?: string | null;
27
- note?: string | null;
28
- }
29
- export interface FeedbackRow {
30
- id: string;
31
- memoryId: string;
32
- verdict: FeedbackVerdict;
33
- sessionId: string | null;
34
- note: string | null;
35
- createdAt: string;
36
- }
37
- export interface FeedbackCounts {
38
- positive: number;
39
- negative: number;
40
- }
41
- export declare class Feedback {
42
- private readonly store;
43
- private readonly metrics;
44
- private readonly now;
45
- constructor(store: Store, metrics?: Metrics | null, now?: () => Date);
46
- /**
47
- * Records one human verdict and folds it into the memory's counters.
48
- * Counters are RECOMPUTED from the table (never incremented) so rows
49
- * deleted in tests or by hand cannot drift them.
50
- *
51
- * The `ignore` verdict also stamps `memories.ignored = 1` (D5-07).
52
- * Returns the feedback row id.
53
- */
54
- record(input: FeedbackRecordInput): string;
55
- /** Human-judgement counters for one memory (from the row, not the table). */
56
- countsFor(memoryId: string): FeedbackCounts;
57
- /**
58
- * Raw verdict history, newest first. Used by `kevin_feedback` (K5-011)
59
- * and by the audit tool (K5-016).
60
- */
61
- list(memoryId?: string, limit?: number): FeedbackRow[];
62
- /**
63
- * Recompute a memory's positive/negative counters straight from the
64
- * verdict table. Kept private — `record` is the only mutation path.
65
- */
66
- private recomputeCounters;
67
- }
@@ -1,120 +0,0 @@
1
- import { hasFeedbackTable } from "./columns.js";
2
- import { uuidv7 } from "./uuid.js";
3
- const POSITIVE_VERDICTS = ["useful"];
4
- const NEGATIVE_VERDICTS = [
5
- "wrong",
6
- "outdated",
7
- "ignore",
8
- ];
9
- // v1.1.0 (K11-011) — table probe delegates to columns registry
10
- export class Feedback {
11
- store;
12
- metrics;
13
- now;
14
- constructor(store, metrics, now = () => new Date()) {
15
- this.store = store;
16
- this.metrics = metrics ?? null;
17
- this.now = now;
18
- }
19
- /**
20
- * Records one human verdict and folds it into the memory's counters.
21
- * Counters are RECOMPUTED from the table (never incremented) so rows
22
- * deleted in tests or by hand cannot drift them.
23
- *
24
- * The `ignore` verdict also stamps `memories.ignored = 1` (D5-07).
25
- * Returns the feedback row id.
26
- */
27
- record(input) {
28
- if (!hasFeedbackTable(this.store)) {
29
- throw new Error("kevin_feedback requires migration 006_v05_glassbox.sql (missing table memory_feedback)");
30
- }
31
- const id = uuidv7();
32
- this.store
33
- .prepare(`INSERT INTO memory_feedback
34
- (id, memory_id, verdict, session_id, note)
35
- VALUES (?, ?, ?, ?, ?)`)
36
- .run(id, input.memoryId, input.verdict, input.sessionId ?? null, input.note ?? null);
37
- this.recomputeCounters(input.memoryId);
38
- if (input.verdict === "ignore") {
39
- this.store
40
- .prepare("UPDATE memories SET ignored = 1 WHERE id = ?")
41
- .run(input.memoryId);
42
- }
43
- // v0.5.0 (K5-023 / plan §5.3, D5-06) — lifecycle action of a
44
- // negative verdict. `wrong` is an opinion and deserves a second
45
- // opinion: it demotes only at feedback_negative >= 2. `outdated`
46
- // is a self-verifying claim about the world and acts at once.
47
- // `useful` confirms the memory and refreshes its verification.
48
- if (input.verdict === "outdated") {
49
- this.store
50
- .prepare("UPDATE memories SET status = 'stale' WHERE id = ?")
51
- .run(input.memoryId);
52
- }
53
- else if (input.verdict === "wrong") {
54
- const counts = this.countsFor(input.memoryId);
55
- if (counts.negative >= 2) {
56
- this.store
57
- .prepare("UPDATE memories SET status = 'stale' WHERE id = ?")
58
- .run(input.memoryId);
59
- }
60
- }
61
- else if (input.verdict === "useful") {
62
- this.store
63
- .prepare("UPDATE memories SET last_verified_at = datetime('now') WHERE id = ?")
64
- .run(input.memoryId);
65
- }
66
- if (POSITIVE_VERDICTS.includes(input.verdict)) {
67
- this.metrics?.incr("feedback_positive_total", 1);
68
- }
69
- else if (NEGATIVE_VERDICTS.includes(input.verdict)) {
70
- this.metrics?.incr("feedback_negative_total", 1);
71
- }
72
- return id;
73
- }
74
- /** Human-judgement counters for one memory (from the row, not the table). */
75
- countsFor(memoryId) {
76
- const row = this.store
77
- .prepare("SELECT feedback_positive, feedback_negative FROM memories WHERE id = ?")
78
- .get(memoryId);
79
- if (!row)
80
- return { positive: 0, negative: 0 };
81
- return { positive: row.feedback_positive, negative: row.feedback_negative };
82
- }
83
- /**
84
- * Raw verdict history, newest first. Used by `kevin_feedback` (K5-011)
85
- * and by the audit tool (K5-016).
86
- */
87
- list(memoryId, limit = 50) {
88
- const rows = this.store
89
- .prepare(`SELECT id, memory_id, verdict, session_id, note, created_at
90
- FROM memory_feedback
91
- ${memoryId ? "WHERE memory_id = ?" : ""}
92
- ORDER BY created_at DESC, rowid DESC
93
- LIMIT ?`)
94
- .all(...(memoryId ? [memoryId, limit] : [limit]));
95
- return rows.map((r) => ({
96
- id: r.id,
97
- memoryId: r.memory_id,
98
- verdict: r.verdict,
99
- sessionId: r.session_id,
100
- note: r.note,
101
- createdAt: r.created_at,
102
- }));
103
- }
104
- /**
105
- * Recompute a memory's positive/negative counters straight from the
106
- * verdict table. Kept private — `record` is the only mutation path.
107
- */
108
- recomputeCounters(memoryId) {
109
- this.store
110
- .prepare(`UPDATE memories SET
111
- feedback_positive = (
112
- SELECT COUNT(*) FROM memory_feedback
113
- WHERE memory_id = ? AND verdict = 'useful'),
114
- feedback_negative = (
115
- SELECT COUNT(*) FROM memory_feedback
116
- WHERE memory_id = ? AND verdict IN ('wrong','outdated','ignore'))
117
- WHERE id = ?`)
118
- .run(memoryId, memoryId, memoryId);
119
- }
120
- }
@@ -1,88 +0,0 @@
1
- import { HOOK_NAMES } from "./Migrate.js";
2
- import type { Store } from "./Store.js";
3
- export type HookName = (typeof HOOK_NAMES)[number];
4
- export type LivenessState = "live" | "dead" | "unknown";
5
- export interface HookReport {
6
- hook: HookName;
7
- experimental: boolean;
8
- state: LivenessState;
9
- firstSeenAt: string | null;
10
- lastSeenAt: string | null;
11
- fireCount: number;
12
- expectedCount: number;
13
- /** v0.9.0 (K9-012) — set once when the hook went dead; never cleared. */
14
- deadSince: string | null;
15
- }
16
- /**
17
- * v0.9.0 (K9-010 / plan §5.3, D9-09) — parse the TEXT setting
18
- * `dead_hook_report_threshold` into the [1, 1000] clamp. NaN and empty
19
- * fall back to 3 (never 0: a zero threshold would report every hook dead
20
- * before a single session completes a checkpoint).
21
- */
22
- export declare function parseThreshold(text: string | null | undefined): number;
23
- interface HookLivenessOptions {
24
- /** `hook_liveness_enabled === "1"` (TEXT comparison, decided at construction). */
25
- enabled: boolean;
26
- /** Raw TEXT of `dead_hook_report_threshold`; parsed+clamped in report(). */
27
- thresholdText: string | null | undefined;
28
- /** `host.pluginVersion` at construction, stored on every flushed row. */
29
- pluginVersion: string | null;
30
- }
31
- /**
32
- * v0.9.0 (K9-009/K9-010 / plan §5.3) — wraps the plugin's hooks with a
33
- * success-path recorder, dedups per-session checkpoints, and reports each
34
- * hook's liveness state. Persistence is machine-scoped: the hook_liveness
35
- * table carries no project_id or repo_id (D9-08).
36
- */
37
- export declare class HookLiveness {
38
- private readonly store;
39
- private readonly options;
40
- excessArityCount: number;
41
- private readonly counters;
42
- private readonly seenSessions;
43
- private readonly suppressedSessions;
44
- private readonly threshold;
45
- private readonly pluginVersion;
46
- constructor(store: Store, options: HookLivenessOptions);
47
- /**
48
- * Returns a new object with the same keys; function values are replaced
49
- * by delegating wrappers, non-function values (the tool map) pass by
50
- * reference. When liveness is disabled, returns the argument unchanged —
51
- * the same object, not a copy.
52
- */
53
- wrap<T>(hooks: T): T;
54
- /**
55
- * v0.9.0 (K9-010 / plan §5.3) — a checkpoint: the session reached a
56
- * model turn (tool.execute.after fired), so the system prompt was
57
- * assembled and `experimental.chat.system.transform` MUST have been
58
- * offered. Deduped per session: 20 tool calls in one session count
59
- * exactly one expectation.
60
- */
61
- expect(hook: HookName, sessionID: string): void;
62
- /**
63
- * Per-hook liveness verdicts in canonical HOOK_NAMES order.
64
- * A hook that fired once is live forever; dead requires both zero fires
65
- * and `expectedCount >= threshold`; everything else is unknown — and
66
- * unknown is never rounded to healthy (D9-09).
67
- */
68
- report(): HookReport[];
69
- /** Persists every counter row. Called on the metrics.flush() cadence. */
70
- flush(): void;
71
- private makeWrapper;
72
- /**
73
- * v1.0.0 (K10-013 / plan §5.3) — records the `dispose` fire and
74
- * flushes immediately, from inside the dispose delegate: this is the
75
- * last write of the process, and nothing after it can persist.
76
- */
77
- recordDispose(): void;
78
- private recordSuccess;
79
- private recordError;
80
- private loadFromDb;
81
- }
82
- export type HostVerdict = "healthy" | "degraded" | "unknown";
83
- export interface HostVerdictResult {
84
- verdict: HostVerdict;
85
- reason: string;
86
- }
87
- export declare function reduceVerdict(reports: readonly HookReport[]): HostVerdictResult;
88
- export {};