@jmtrin/opencode-kevin 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +559 -771
- package/dist/migrations/010_v09_native.sql +78 -0
- package/dist/migrations/011_v10_proven.sql +69 -0
- package/dist/plugin/Archiver.js +0 -1
- package/dist/plugin/ArtifactWriter.d.ts +25 -10
- package/dist/plugin/ArtifactWriter.js +43 -25
- package/dist/plugin/CausalChain.js +0 -1
- package/dist/plugin/ConflictDetector.js +0 -1
- package/dist/plugin/ContextInjector.js +0 -1
- package/dist/plugin/ConventionMiner.js +0 -1
- package/dist/plugin/Curator.js +0 -1
- package/dist/plugin/Feedback.js +0 -1
- package/dist/plugin/HookLiveness.d.ts +87 -0
- package/dist/plugin/HookLiveness.js +338 -0
- package/dist/plugin/InjectionLedger.js +0 -1
- package/dist/plugin/LessonFixer.js +0 -1
- package/dist/plugin/Materializer.d.ts +25 -0
- package/dist/plugin/Materializer.js +39 -11
- package/dist/plugin/MemoryService.js +0 -1
- package/dist/plugin/Migrate.d.ts +1 -0
- package/dist/plugin/Migrate.js +69 -6
- package/dist/plugin/PatternMiner.js +0 -1
- package/dist/plugin/QualityGate.js +0 -1
- package/dist/plugin/Reflector.js +0 -1
- package/dist/plugin/RepoIdentity.d.ts +20 -9
- package/dist/plugin/RepoIdentity.js +44 -10
- package/dist/plugin/RepoTruth.js +0 -1
- package/dist/plugin/Retrospective.js +16 -1
- package/dist/plugin/SharedLayer.js +0 -1
- package/dist/plugin/Store.js +0 -1
- package/dist/plugin/ToolCallObserver.js +0 -1
- package/dist/plugin/capabilities.js +0 -1
- package/dist/plugin/confidence.js +0 -1
- package/dist/plugin/contract.d.ts +46 -0
- package/dist/plugin/contract.js +368 -0
- package/dist/plugin/diff.js +0 -1
- package/dist/plugin/escape.d.ts +39 -0
- package/dist/plugin/escape.js +60 -0
- package/dist/plugin/fingerprint.js +0 -1
- package/dist/plugin/host.d.ts +70 -0
- package/dist/plugin/host.js +250 -0
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +542 -222
- package/dist/plugin/inferability.js +0 -1
- package/dist/plugin/kevin_approve.js +0 -1
- package/dist/plugin/kevin_audit.d.ts +60 -0
- package/dist/plugin/kevin_audit.js +144 -1
- package/dist/plugin/kevin_bench.d.ts +10 -0
- package/dist/plugin/kevin_bench.js +82 -0
- package/dist/plugin/kevin_conflicts.js +0 -1
- package/dist/plugin/kevin_contract.d.ts +8 -0
- package/dist/plugin/kevin_contract.js +59 -0
- package/dist/plugin/kevin_doctor.d.ts +62 -0
- package/dist/plugin/kevin_doctor.js +196 -0
- package/dist/plugin/kevin_facts.js +0 -1
- package/dist/plugin/kevin_native.d.ts +29 -0
- package/dist/plugin/kevin_native.js +79 -0
- package/dist/plugin/kevin_propose.js +0 -1
- package/dist/plugin/kevin_publish.js +0 -1
- package/dist/plugin/kevin_why.js +0 -1
- package/dist/plugin/memory-format.js +0 -1
- package/dist/plugin/metrics.js +0 -1
- package/dist/plugin/native.d.ts +92 -0
- package/dist/plugin/native.js +190 -0
- package/dist/plugin/okf-export.js +0 -1
- package/dist/plugin/okf-import.js +0 -1
- package/dist/plugin/okf.js +0 -1
- package/dist/plugin/perf.d.ts +35 -0
- package/dist/plugin/perf.js +174 -0
- package/dist/plugin/query-tokenizer.js +0 -1
- package/dist/plugin/redact.js +0 -1
- package/dist/plugin/replay-types.js +0 -1
- package/dist/plugin/replay.js +0 -1
- package/dist/plugin/sqlite-adapter.js +0 -1
- package/dist/plugin/uuid.js +0 -1
- package/package.json +22 -7
- package/dist/plugin/Archiver.js.map +0 -1
- package/dist/plugin/ArtifactWriter.js.map +0 -1
- package/dist/plugin/CausalChain.js.map +0 -1
- package/dist/plugin/ConflictDetector.js.map +0 -1
- package/dist/plugin/ContextInjector.js.map +0 -1
- package/dist/plugin/ConventionMiner.js.map +0 -1
- package/dist/plugin/Curator.js.map +0 -1
- package/dist/plugin/Feedback.js.map +0 -1
- package/dist/plugin/InjectionLedger.js.map +0 -1
- package/dist/plugin/LessonFixer.js.map +0 -1
- package/dist/plugin/Materializer.js.map +0 -1
- package/dist/plugin/MemoryService.js.map +0 -1
- package/dist/plugin/Migrate.js.map +0 -1
- package/dist/plugin/PatternMiner.js.map +0 -1
- package/dist/plugin/QualityGate.js.map +0 -1
- package/dist/plugin/Reflector.js.map +0 -1
- package/dist/plugin/RepoIdentity.js.map +0 -1
- package/dist/plugin/RepoTruth.js.map +0 -1
- package/dist/plugin/Retrospective.js.map +0 -1
- package/dist/plugin/SharedLayer.js.map +0 -1
- package/dist/plugin/Store.js.map +0 -1
- package/dist/plugin/ToolCallObserver.js.map +0 -1
- package/dist/plugin/capabilities.js.map +0 -1
- package/dist/plugin/confidence.js.map +0 -1
- package/dist/plugin/diff.js.map +0 -1
- package/dist/plugin/fingerprint.js.map +0 -1
- package/dist/plugin/index.js.map +0 -1
- package/dist/plugin/inferability.js.map +0 -1
- package/dist/plugin/kevin_approve.js.map +0 -1
- package/dist/plugin/kevin_audit.js.map +0 -1
- package/dist/plugin/kevin_conflicts.js.map +0 -1
- package/dist/plugin/kevin_facts.js.map +0 -1
- package/dist/plugin/kevin_propose.js.map +0 -1
- package/dist/plugin/kevin_publish.js.map +0 -1
- package/dist/plugin/kevin_why.js.map +0 -1
- package/dist/plugin/memory-format.js.map +0 -1
- package/dist/plugin/metrics.js.map +0 -1
- package/dist/plugin/okf-export.js.map +0 -1
- package/dist/plugin/okf-import.js.map +0 -1
- package/dist/plugin/okf.js.map +0 -1
- package/dist/plugin/query-tokenizer.js.map +0 -1
- package/dist/plugin/redact.js.map +0 -1
- package/dist/plugin/replay-types.js.map +0 -1
- package/dist/plugin/replay.js.map +0 -1
- package/dist/plugin/sqlite-adapter.js.map +0 -1
- package/dist/plugin/uuid.js.map +0 -1
- package/migrations/001_initial.sql +0 -92
- package/migrations/002_indexes.sql +0 -14
- package/migrations/003_v02_signal.sql +0 -58
- package/migrations/004_v03_knowledge.sql +0 -138
- package/migrations/005_v04_signal.sql +0 -57
- package/migrations/006_v05_glassbox.sql +0 -118
- package/migrations/007_v06_pull.sql +0 -145
- package/migrations/008_v07_truth.sql +0 -124
- package/migrations/009_v08_team.sql +0 -100
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { HOOK_NAMES } from "./Migrate.js";
|
|
2
|
+
const DEFAULT_THRESHOLD = 3;
|
|
3
|
+
/**
|
|
4
|
+
* v0.9.0 (K9-010 / plan §5.3, D9-09) — parse the TEXT setting
|
|
5
|
+
* `dead_hook_report_threshold` into the [1, 1000] clamp. NaN and empty
|
|
6
|
+
* fall back to 3 (never 0: a zero threshold would report every hook dead
|
|
7
|
+
* before a single session completes a checkpoint).
|
|
8
|
+
*/
|
|
9
|
+
export function parseThreshold(text) {
|
|
10
|
+
if (text === null || text === undefined || text === "")
|
|
11
|
+
return DEFAULT_THRESHOLD;
|
|
12
|
+
const n = Number.parseInt(text, 10);
|
|
13
|
+
// NaN → default; 0 is not a valid threshold (K9-010 AC: 'abc'/''/'0'
|
|
14
|
+
// all clamp to the default of three — never zero).
|
|
15
|
+
if (Number.isNaN(n) || n === 0)
|
|
16
|
+
return DEFAULT_THRESHOLD;
|
|
17
|
+
return Math.min(1000, Math.max(1, n));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* v0.9.0 (K9-009/K9-010 / plan §5.3) — wraps the plugin's hooks with a
|
|
21
|
+
* success-path recorder, dedups per-session checkpoints, and reports each
|
|
22
|
+
* hook's liveness state. Persistence is machine-scoped: the hook_liveness
|
|
23
|
+
* table carries no project_id or repo_id (D9-08).
|
|
24
|
+
*/
|
|
25
|
+
export class HookLiveness {
|
|
26
|
+
store;
|
|
27
|
+
options;
|
|
28
|
+
counters;
|
|
29
|
+
seenSessions;
|
|
30
|
+
suppressedSessions = new Set();
|
|
31
|
+
threshold;
|
|
32
|
+
pluginVersion;
|
|
33
|
+
constructor(store, options) {
|
|
34
|
+
this.store = store;
|
|
35
|
+
this.options = options;
|
|
36
|
+
this.threshold = parseThreshold(options.thresholdText);
|
|
37
|
+
this.pluginVersion = options.pluginVersion;
|
|
38
|
+
this.counters = new Map();
|
|
39
|
+
this.seenSessions = new Map();
|
|
40
|
+
for (const hook of HOOK_NAMES) {
|
|
41
|
+
this.counters.set(hook, {
|
|
42
|
+
fireCount: 0,
|
|
43
|
+
errorCount: 0,
|
|
44
|
+
expectedCount: 0,
|
|
45
|
+
firstSeenAt: null,
|
|
46
|
+
lastSeenAt: null,
|
|
47
|
+
deadSince: null,
|
|
48
|
+
});
|
|
49
|
+
this.seenSessions.set(hook, new Set());
|
|
50
|
+
}
|
|
51
|
+
this.loadFromDb();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns a new object with the same keys; function values are replaced
|
|
55
|
+
* by delegating wrappers, non-function values (the tool map) pass by
|
|
56
|
+
* reference. When liveness is disabled, returns the argument unchanged —
|
|
57
|
+
* the same object, not a copy.
|
|
58
|
+
*/
|
|
59
|
+
wrap(hooks) {
|
|
60
|
+
if (!this.options.enabled)
|
|
61
|
+
return hooks;
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [key, value] of Object.entries(hooks)) {
|
|
64
|
+
if (typeof value === "function") {
|
|
65
|
+
out[key] = this.makeWrapper(key, value);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
out[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* v0.9.0 (K9-010 / plan §5.3) — a checkpoint: the session reached a
|
|
75
|
+
* model turn (tool.execute.after fired), so the system prompt was
|
|
76
|
+
* assembled and `experimental.chat.system.transform` MUST have been
|
|
77
|
+
* offered. Deduped per session: 20 tool calls in one session count
|
|
78
|
+
* exactly one expectation.
|
|
79
|
+
*/
|
|
80
|
+
expect(hook, sessionID) {
|
|
81
|
+
const seen = this.seenSessions.get(hook);
|
|
82
|
+
if (!seen || seen.has(sessionID))
|
|
83
|
+
return;
|
|
84
|
+
seen.add(sessionID);
|
|
85
|
+
const counters = this.counters.get(hook);
|
|
86
|
+
if (!counters)
|
|
87
|
+
return;
|
|
88
|
+
counters.expectedCount += 1;
|
|
89
|
+
// The dead flag is materialized here (not only in report()) so that
|
|
90
|
+
// flush() persists dead_since even when no report() was ever called:
|
|
91
|
+
// kevin_doctor's PURE-SQL blocks read dead_since from the table.
|
|
92
|
+
if (counters.fireCount === 0 &&
|
|
93
|
+
counters.expectedCount >= this.threshold &&
|
|
94
|
+
counters.deadSince === null) {
|
|
95
|
+
counters.deadSince = counters.lastSeenAt ?? new Date().toISOString();
|
|
96
|
+
}
|
|
97
|
+
// v0.9.0 (K9-011 / plan §5.3) — when the injection hook is dead, the
|
|
98
|
+
// checkpointed session was suppressed: the host stopped offering the
|
|
99
|
+
// transform, so no injection could happen. Counted once per session
|
|
100
|
+
// (same dedup as expected_count) and persisted by flush(). This is
|
|
101
|
+
// the counter that turns "zero injections" from an ambiguous number
|
|
102
|
+
// into a diagnosis.
|
|
103
|
+
if (hook === "experimental.chat.system.transform" &&
|
|
104
|
+
counters.deadSince !== null &&
|
|
105
|
+
!this.suppressedSessions.has(sessionID)) {
|
|
106
|
+
this.suppressedSessions.add(sessionID);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Per-hook liveness verdicts in canonical HOOK_NAMES order.
|
|
111
|
+
* A hook that fired once is live forever; dead requires both zero fires
|
|
112
|
+
* and `expectedCount >= threshold`; everything else is unknown — and
|
|
113
|
+
* unknown is never rounded to healthy (D9-09).
|
|
114
|
+
*/
|
|
115
|
+
report() {
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const hook of HOOK_NAMES) {
|
|
118
|
+
const c = this.counters.get(hook);
|
|
119
|
+
if (!c)
|
|
120
|
+
continue;
|
|
121
|
+
let state;
|
|
122
|
+
if (c.fireCount > 0) {
|
|
123
|
+
state = "live";
|
|
124
|
+
}
|
|
125
|
+
else if (c.expectedCount >= this.threshold) {
|
|
126
|
+
// dead_since is set ONCE and never cleared: it documents the
|
|
127
|
+
// historical death even if the hook later recovers.
|
|
128
|
+
if (c.deadSince === null) {
|
|
129
|
+
c.deadSince = c.lastSeenAt ?? new Date().toISOString();
|
|
130
|
+
}
|
|
131
|
+
state = "dead";
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
state = "unknown";
|
|
135
|
+
}
|
|
136
|
+
out.push({
|
|
137
|
+
hook,
|
|
138
|
+
experimental: hook.startsWith("experimental."),
|
|
139
|
+
state,
|
|
140
|
+
firstSeenAt: c.firstSeenAt,
|
|
141
|
+
lastSeenAt: c.lastSeenAt,
|
|
142
|
+
fireCount: c.fireCount,
|
|
143
|
+
expectedCount: c.expectedCount,
|
|
144
|
+
deadSince: c.deadSince,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** Persists every counter row. Called on the metrics.flush() cadence. */
|
|
150
|
+
flush() {
|
|
151
|
+
const store = this.store;
|
|
152
|
+
store.transaction(() => {
|
|
153
|
+
// Created lazily on first flush, mirroring Metrics.flush(): the
|
|
154
|
+
// migration guarantees the table in production, but unit tests
|
|
155
|
+
// against pre-010 DBs must not explode at dispose.
|
|
156
|
+
store.exec(`CREATE TABLE IF NOT EXISTS hook_liveness (
|
|
157
|
+
hook TEXT PRIMARY KEY,
|
|
158
|
+
experimental INTEGER NOT NULL DEFAULT 0,
|
|
159
|
+
fire_count INTEGER NOT NULL DEFAULT 0,
|
|
160
|
+
error_count INTEGER NOT NULL DEFAULT 0,
|
|
161
|
+
expected_count INTEGER NOT NULL DEFAULT 0,
|
|
162
|
+
first_seen_at TEXT,
|
|
163
|
+
last_seen_at TEXT,
|
|
164
|
+
dead_since TEXT,
|
|
165
|
+
plugin_version TEXT
|
|
166
|
+
)`);
|
|
167
|
+
const upsert = store.prepare(`INSERT INTO hook_liveness (hook, experimental, fire_count, error_count,
|
|
168
|
+
expected_count, first_seen_at, last_seen_at, dead_since, plugin_version)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
170
|
+
ON CONFLICT(hook) DO UPDATE SET
|
|
171
|
+
experimental = excluded.experimental,
|
|
172
|
+
fire_count = excluded.fire_count,
|
|
173
|
+
error_count = excluded.error_count,
|
|
174
|
+
expected_count = excluded.expected_count,
|
|
175
|
+
first_seen_at = excluded.first_seen_at,
|
|
176
|
+
last_seen_at = excluded.last_seen_at,
|
|
177
|
+
dead_since = excluded.dead_since,
|
|
178
|
+
plugin_version = excluded.plugin_version`);
|
|
179
|
+
for (const [hook, c] of this.counters) {
|
|
180
|
+
upsert.run(hook, hook.startsWith("experimental.") ? 1 : 0, c.fireCount, c.errorCount, c.expectedCount, c.firstSeenAt, c.lastSeenAt, c.deadSince, this.pluginVersion);
|
|
181
|
+
}
|
|
182
|
+
// v0.9.0 (K9-011 / plan §5.3) — the four v0.9.0 counters are
|
|
183
|
+
// re-derived here, on the metrics cadence, exactly like the 009
|
|
184
|
+
// post-apply hook re-derives hooks_dead_total: SUM(fire_count),
|
|
185
|
+
// SUM(error_count), COUNT(dead_since) and the per-session
|
|
186
|
+
// suppression count. No Metrics dependency, no writes in the hot
|
|
187
|
+
// path — flush() is the single persistence point.
|
|
188
|
+
let fireSum = 0;
|
|
189
|
+
let errorSum = 0;
|
|
190
|
+
let deadCount = 0;
|
|
191
|
+
for (const c of this.counters.values()) {
|
|
192
|
+
fireSum += c.fireCount;
|
|
193
|
+
errorSum += c.errorCount;
|
|
194
|
+
if (c.deadSince !== null)
|
|
195
|
+
deadCount += 1;
|
|
196
|
+
}
|
|
197
|
+
store.exec(`CREATE TABLE IF NOT EXISTS kevin_metrics (
|
|
198
|
+
key TEXT PRIMARY KEY,
|
|
199
|
+
value INTEGER NOT NULL DEFAULT 0,
|
|
200
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
201
|
+
)`);
|
|
202
|
+
const derive = store.prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
|
|
203
|
+
VALUES (?, ?, datetime('now'))
|
|
204
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
205
|
+
value = excluded.value,
|
|
206
|
+
updated_at = datetime('now')`);
|
|
207
|
+
derive.run("hook_fires_total", fireSum);
|
|
208
|
+
derive.run("hook_errors_total", errorSum);
|
|
209
|
+
derive.run("hooks_dead_total", deadCount);
|
|
210
|
+
derive.run("injections_suppressed_dead_hook", this.suppressedSessions.size);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
makeWrapper(key, delegate) {
|
|
214
|
+
const record = () => {
|
|
215
|
+
// v1.0.0 (K10-013) — dispose is recorded inside its own delegate
|
|
216
|
+
// (via recordDispose()) rather than here: the wrapper's post-return
|
|
217
|
+
// record would land after the store is closed and could never be
|
|
218
|
+
// persisted — the event being detected is the process ending.
|
|
219
|
+
if (key === "dispose")
|
|
220
|
+
return;
|
|
221
|
+
if (HOOK_NAMES.includes(key)) {
|
|
222
|
+
this.recordSuccess(key);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const recordError = (e) => {
|
|
226
|
+
if (HOOK_NAMES.includes(key)) {
|
|
227
|
+
this.recordError(key);
|
|
228
|
+
}
|
|
229
|
+
throw e;
|
|
230
|
+
};
|
|
231
|
+
const n = delegate.length;
|
|
232
|
+
if (n === 0) {
|
|
233
|
+
return async () => {
|
|
234
|
+
try {
|
|
235
|
+
const result = await delegate();
|
|
236
|
+
record();
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
return recordError(e);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (n === 1) {
|
|
245
|
+
return async (a) => {
|
|
246
|
+
try {
|
|
247
|
+
const result = await delegate(a);
|
|
248
|
+
record();
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
return recordError(e);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return async (a, b) => {
|
|
257
|
+
try {
|
|
258
|
+
const result = await delegate(a, b);
|
|
259
|
+
record();
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
return recordError(e);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* v1.0.0 (K10-013 / plan §5.3) — records the `dispose` fire and
|
|
269
|
+
* flushes immediately, from inside the dispose delegate: this is the
|
|
270
|
+
* last write of the process, and nothing after it can persist.
|
|
271
|
+
*/
|
|
272
|
+
recordDispose() {
|
|
273
|
+
this.recordSuccess("dispose");
|
|
274
|
+
this.flush();
|
|
275
|
+
}
|
|
276
|
+
recordSuccess(hook) {
|
|
277
|
+
const c = this.counters.get(hook);
|
|
278
|
+
if (!c)
|
|
279
|
+
return;
|
|
280
|
+
c.fireCount += 1;
|
|
281
|
+
const now = new Date().toISOString();
|
|
282
|
+
if (c.firstSeenAt === null)
|
|
283
|
+
c.firstSeenAt = now;
|
|
284
|
+
c.lastSeenAt = now;
|
|
285
|
+
}
|
|
286
|
+
recordError(hook) {
|
|
287
|
+
const c = this.counters.get(hook);
|
|
288
|
+
if (!c)
|
|
289
|
+
return;
|
|
290
|
+
c.errorCount += 1;
|
|
291
|
+
}
|
|
292
|
+
loadFromDb() {
|
|
293
|
+
let rows = [];
|
|
294
|
+
try {
|
|
295
|
+
rows = this.store
|
|
296
|
+
.prepare("SELECT hook, fire_count, error_count, expected_count, first_seen_at, last_seen_at, dead_since FROM hook_liveness")
|
|
297
|
+
.all();
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Graceful: pre-010 DBs have no hook_liveness table; the seeded
|
|
301
|
+
// zeros remain until the first flush creates rows.
|
|
302
|
+
rows = [];
|
|
303
|
+
}
|
|
304
|
+
for (const row of rows) {
|
|
305
|
+
if (!HOOK_NAMES.includes(row.hook))
|
|
306
|
+
continue;
|
|
307
|
+
const c = this.counters.get(row.hook);
|
|
308
|
+
if (!c)
|
|
309
|
+
continue;
|
|
310
|
+
c.fireCount = row.fire_count;
|
|
311
|
+
c.errorCount = row.error_count;
|
|
312
|
+
c.expectedCount = row.expected_count;
|
|
313
|
+
c.firstSeenAt = row.first_seen_at;
|
|
314
|
+
c.lastSeenAt = row.last_seen_at;
|
|
315
|
+
c.deadSince = row.dead_since;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
export function reduceVerdict(reports) {
|
|
320
|
+
if (reports.length === 0) {
|
|
321
|
+
return { verdict: "unknown", reason: "no hook reports yet" };
|
|
322
|
+
}
|
|
323
|
+
const dead = reports.filter((r) => r.state === "dead");
|
|
324
|
+
if (dead.length > 0) {
|
|
325
|
+
return {
|
|
326
|
+
verdict: "degraded",
|
|
327
|
+
reason: `${dead.map((r) => r.hook).join(", ")} dead since ${dead[0].deadSince ?? "unknown"}; ${dead.length} affected hook(s)`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (reports.every((r) => r.state === "live")) {
|
|
331
|
+
return { verdict: "healthy", reason: "all hooks live" };
|
|
332
|
+
}
|
|
333
|
+
const pending = reports.filter((r) => r.state === "unknown").length;
|
|
334
|
+
return {
|
|
335
|
+
verdict: "unknown",
|
|
336
|
+
reason: `${pending} hook(s) without checkpoint`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -30,13 +30,32 @@ export interface BundleTarget {
|
|
|
30
30
|
readonly path: string;
|
|
31
31
|
}
|
|
32
32
|
export declare const SKILL_TOPIC = "project-knowledge";
|
|
33
|
+
/**
|
|
34
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the native registration surfaces.
|
|
35
|
+
* Registration replaces emission; they are never both active.
|
|
36
|
+
*/
|
|
37
|
+
export type NativeSurface = "skill" | "reference";
|
|
33
38
|
export declare class Materializer {
|
|
34
39
|
private readonly store;
|
|
35
40
|
/** `root` is injectable for tests; production defaults to ~/.opencode-kevin. */
|
|
36
41
|
private readonly root;
|
|
42
|
+
/**
|
|
43
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the mutual-exclusion guard.
|
|
44
|
+
* A surface `attachNative()` registered is skipped by `materialize()`
|
|
45
|
+
* for that surface only; the other surface keeps emitting.
|
|
46
|
+
*/
|
|
47
|
+
private readonly nativeRegistered;
|
|
37
48
|
constructor(store: Store, options?: {
|
|
38
49
|
root?: string;
|
|
39
50
|
});
|
|
51
|
+
/**
|
|
52
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — record whether the surface was
|
|
53
|
+
* registered natively. Called at attach time (index.ts wiring, K9-018)
|
|
54
|
+
* with each `NativeRegistration.registered` flag.
|
|
55
|
+
*/
|
|
56
|
+
markNativeRegistered(surface: NativeSurface, registered: boolean): void;
|
|
57
|
+
/** v0.9.0 (K9-016 / plan §5.4, D9-10) — is the surface registered natively? */
|
|
58
|
+
hasNativeRegistration(surface: NativeSurface): boolean;
|
|
40
59
|
/** The curated, active memories — the knowledge the pull channels publish. */
|
|
41
60
|
private curatedRows;
|
|
42
61
|
/** The rendered skill body over all curated memories; "" when empty. */
|
|
@@ -53,6 +72,12 @@ export declare class Materializer {
|
|
|
53
72
|
* a call site of `write()` — the single write FUNCTION with two
|
|
54
73
|
* constrained targets: `kevin_approve` reaches `agents_md_path`, this
|
|
55
74
|
* module reaches `~/.opencode-kevin` only (D6-07; enforced by K6-020).
|
|
75
|
+
*
|
|
76
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the guard: a surface registered
|
|
77
|
+
* natively by `attachNative()` is skipped here for that surface only.
|
|
78
|
+
* Both active would put the curated skill in front of the model twice
|
|
79
|
+
* from two sources that can disagree; neither active would silently
|
|
80
|
+
* remove the feature on hosts that gained the subpath.
|
|
56
81
|
*/
|
|
57
82
|
materialize(writer: ArtifactWriter): MaterializedBundle[];
|
|
58
83
|
}
|
|
@@ -141,10 +141,31 @@ export class Materializer {
|
|
|
141
141
|
store;
|
|
142
142
|
/** `root` is injectable for tests; production defaults to ~/.opencode-kevin. */
|
|
143
143
|
root;
|
|
144
|
+
/**
|
|
145
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the mutual-exclusion guard.
|
|
146
|
+
* A surface `attachNative()` registered is skipped by `materialize()`
|
|
147
|
+
* for that surface only; the other surface keeps emitting.
|
|
148
|
+
*/
|
|
149
|
+
nativeRegistered = new Set();
|
|
144
150
|
constructor(store, options = {}) {
|
|
145
151
|
this.store = store;
|
|
146
152
|
this.root = options.root ?? join(homedir(), ".opencode-kevin");
|
|
147
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — record whether the surface was
|
|
156
|
+
* registered natively. Called at attach time (index.ts wiring, K9-018)
|
|
157
|
+
* with each `NativeRegistration.registered` flag.
|
|
158
|
+
*/
|
|
159
|
+
markNativeRegistered(surface, registered) {
|
|
160
|
+
if (registered)
|
|
161
|
+
this.nativeRegistered.add(surface);
|
|
162
|
+
else
|
|
163
|
+
this.nativeRegistered.delete(surface);
|
|
164
|
+
}
|
|
165
|
+
/** v0.9.0 (K9-016 / plan §5.4, D9-10) — is the surface registered natively? */
|
|
166
|
+
hasNativeRegistration(surface) {
|
|
167
|
+
return this.nativeRegistered.has(surface);
|
|
168
|
+
}
|
|
148
169
|
/** The curated, active memories — the knowledge the pull channels publish. */
|
|
149
170
|
curatedRows() {
|
|
150
171
|
return this.store
|
|
@@ -204,27 +225,35 @@ export class Materializer {
|
|
|
204
225
|
* a call site of `write()` — the single write FUNCTION with two
|
|
205
226
|
* constrained targets: `kevin_approve` reaches `agents_md_path`, this
|
|
206
227
|
* module reaches `~/.opencode-kevin` only (D6-07; enforced by K6-020).
|
|
228
|
+
*
|
|
229
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the guard: a surface registered
|
|
230
|
+
* natively by `attachNative()` is skipped here for that surface only.
|
|
231
|
+
* Both active would put the curated skill in front of the model twice
|
|
232
|
+
* from two sources that can disagree; neither active would silently
|
|
233
|
+
* remove the feature on hosts that gained the subpath.
|
|
207
234
|
*/
|
|
208
235
|
materialize(writer) {
|
|
209
236
|
const pending = [];
|
|
210
237
|
const rows = this.curatedRows();
|
|
211
238
|
const skillBody = renderRows(rows);
|
|
212
|
-
if (skillBody !== "") {
|
|
239
|
+
if (skillBody !== "" && !this.nativeRegistered.has("skill")) {
|
|
213
240
|
pending.push({
|
|
214
241
|
topic: SKILL_TOPIC,
|
|
215
242
|
path: join(this.root, "skills", "project-knowledge.md"),
|
|
216
243
|
body: skillBody,
|
|
217
244
|
});
|
|
218
245
|
}
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
246
|
+
if (!this.nativeRegistered.has("reference")) {
|
|
247
|
+
for (const group of this.groupByTopic(rows)) {
|
|
248
|
+
const body = renderRows(group.rows);
|
|
249
|
+
if (body === "")
|
|
250
|
+
continue;
|
|
251
|
+
pending.push({
|
|
252
|
+
topic: group.topic,
|
|
253
|
+
path: join(this.root, "refs", `${group.topic}.md`),
|
|
254
|
+
body,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
228
257
|
}
|
|
229
258
|
return pending.map((bundle) => ({
|
|
230
259
|
topic: bundle.topic,
|
|
@@ -237,4 +266,3 @@ export class Materializer {
|
|
|
237
266
|
}));
|
|
238
267
|
}
|
|
239
268
|
}
|
|
240
|
-
//# sourceMappingURL=Materializer.js.map
|
package/dist/plugin/Migrate.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface MigrateResult {
|
|
|
5
5
|
applied: string[];
|
|
6
6
|
}
|
|
7
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"];
|
|
8
9
|
export declare class Migrate {
|
|
9
10
|
private store;
|
|
10
11
|
private migrationsDir;
|
package/dist/plugin/Migrate.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from "node:fs";
|
|
2
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
|
+
];
|
|
3
18
|
// Built-in post-apply hooks, keyed by migration version. Each hook runs inside
|
|
4
19
|
// the same transaction as the migration's DDL, so a hook failure rolls back the
|
|
5
20
|
// whole migration. Hooks run when their version is applied. Migration 008's
|
|
@@ -125,6 +140,49 @@ const DEFAULT_POST_APPLY_HOOKS = {
|
|
|
125
140
|
.prepare("UPDATE kevin_metrics SET value = (SELECT COUNT(*) FROM shared_entries) WHERE key = 'shared_entries_total'")
|
|
126
141
|
.run();
|
|
127
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
|
+
},
|
|
128
186
|
};
|
|
129
187
|
export class Migrate {
|
|
130
188
|
store;
|
|
@@ -155,11 +213,17 @@ export class Migrate {
|
|
|
155
213
|
// v0.7.0 (K7-002) — heal drift in the 008 counters on a no-op
|
|
156
214
|
// startup while preserving `applied: []` idempotency. v0.8.0
|
|
157
215
|
// (K8-002 / plan §6.1) extends the same repair to the 009
|
|
158
|
-
// back-fill and the shared_entries_total re-derivation
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
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") {
|
|
163
227
|
const repairHook = this.postApplyHooks.get(from);
|
|
164
228
|
if (repairHook)
|
|
165
229
|
this.store.transaction(() => repairHook(this.store));
|
|
@@ -200,4 +264,3 @@ export class Migrate {
|
|
|
200
264
|
.filter((m) => m !== null && m.version > current);
|
|
201
265
|
}
|
|
202
266
|
}
|
|
203
|
-
//# sourceMappingURL=Migrate.js.map
|
package/dist/plugin/Reflector.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ArtifactWriter } from "./ArtifactWriter.js";
|
|
2
|
+
import type { HostSurface } from "./host.js";
|
|
2
3
|
/**
|
|
3
|
-
* The
|
|
4
|
+
* The four sources `resolve()` tries, in order (plan §5.2, D9-13).
|
|
4
5
|
*/
|
|
5
|
-
export type IdentitySource = "declared" | "remote" | "path";
|
|
6
|
+
export type IdentitySource = "declared" | "remote" | "host" | "path";
|
|
6
7
|
/**
|
|
7
8
|
* The outcome of resolving a repository identity.
|
|
8
9
|
*/
|
|
@@ -58,8 +59,8 @@ export declare function normalizeRemote(url: string): string | null;
|
|
|
58
59
|
*/
|
|
59
60
|
export declare function computeRepoId(normalized: string): string;
|
|
60
61
|
/**
|
|
61
|
-
* Resolve the repository identity for `cwd`, trying the
|
|
62
|
-
* sources in order (plan §5.
|
|
62
|
+
* Resolve the repository identity for `cwd`, trying the four
|
|
63
|
+
* sources in order (plan §5.2, D9-13):
|
|
63
64
|
*
|
|
64
65
|
* 1. `declared` — `.kevin/project.json` → `id` (validated as
|
|
65
66
|
* exactly 16 lowercase hex characters; anything else is ignored
|
|
@@ -68,14 +69,24 @@ export declare function computeRepoId(normalized: string): string;
|
|
|
68
69
|
* onto a typo);
|
|
69
70
|
* 2. `remote` — `.git/config` → `[remote "origin"] url`,
|
|
70
71
|
* normalized and hashed;
|
|
71
|
-
* 3. `
|
|
72
|
+
* 3. `host` — the value the host resolved for this directory,
|
|
73
|
+
* `host.project.worktree` first, `host.project.directory` as
|
|
74
|
+
* fallback; both empty or absent falls through to `path`.
|
|
75
|
+
* This is strictly better than `process.cwd()` — the host's own
|
|
76
|
+
* ToolContext documents "prefer this over `process.cwd()`" —
|
|
77
|
+
* but it sits below the explicit sources, because monorepos and
|
|
78
|
+
* D8-03's confirmed re-keying depend on `.kevin/project.json`
|
|
79
|
+
* winning;
|
|
80
|
+
* 4. `path` — `fingerprint(cwd)`, the v0.7.0 behaviour, preserved
|
|
81
|
+
* exactly when `host` is absent.
|
|
72
82
|
*
|
|
73
83
|
* Never throws: a directory that is not a git repository, an
|
|
74
|
-
* unreadable `.git/config`,
|
|
75
|
-
*
|
|
76
|
-
* `repoId`, regardless of
|
|
84
|
+
* unreadable `.git/config`, a malformed `project.json`, and a host
|
|
85
|
+
* with no usable project fields all fall through to source 4.
|
|
86
|
+
* `projectId` is always returned alongside `repoId`, regardless of
|
|
87
|
+
* which source won.
|
|
77
88
|
*/
|
|
78
|
-
export declare function resolve(cwd: string): ResolvedIdentity;
|
|
89
|
+
export declare function resolve(cwd: string, host?: HostSurface): ResolvedIdentity;
|
|
79
90
|
/**
|
|
80
91
|
* The outcome of `initProjectFile` (K8-008).
|
|
81
92
|
*/
|