@jmtrin/opencode-kevin 0.7.0 → 0.9.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 +254 -12
- package/dist/migrations/009_v08_team.sql +100 -0
- package/dist/migrations/010_v09_native.sql +78 -0
- package/dist/plugin/ArtifactWriter.d.ts +28 -0
- package/dist/plugin/ArtifactWriter.js +35 -2
- package/dist/plugin/ArtifactWriter.js.map +1 -1
- package/dist/plugin/ContextInjector.d.ts +9 -0
- package/dist/plugin/ContextInjector.js +14 -3
- package/dist/plugin/ContextInjector.js.map +1 -1
- package/dist/plugin/Curator.d.ts +22 -2
- package/dist/plugin/Curator.js +56 -14
- package/dist/plugin/Curator.js.map +1 -1
- package/dist/plugin/HookLiveness.d.ts +81 -0
- package/dist/plugin/HookLiveness.js +324 -0
- package/dist/plugin/HookLiveness.js.map +1 -0
- package/dist/plugin/InjectionLedger.d.ts +6 -0
- package/dist/plugin/InjectionLedger.js +6 -0
- package/dist/plugin/InjectionLedger.js.map +1 -1
- package/dist/plugin/Materializer.d.ts +29 -5
- package/dist/plugin/Materializer.js +48 -16
- package/dist/plugin/Materializer.js.map +1 -1
- package/dist/plugin/MemoryService.d.ts +34 -5
- package/dist/plugin/MemoryService.js +215 -7
- package/dist/plugin/MemoryService.js.map +1 -1
- package/dist/plugin/Migrate.d.ts +1 -0
- package/dist/plugin/Migrate.js +74 -3
- package/dist/plugin/Migrate.js.map +1 -1
- package/dist/plugin/RepoIdentity.d.ts +124 -0
- package/dist/plugin/RepoIdentity.js +301 -0
- package/dist/plugin/RepoIdentity.js.map +1 -0
- package/dist/plugin/Retrospective.js +18 -0
- package/dist/plugin/Retrospective.js.map +1 -1
- package/dist/plugin/SharedLayer.d.ts +159 -0
- package/dist/plugin/SharedLayer.js +463 -0
- package/dist/plugin/SharedLayer.js.map +1 -0
- package/dist/plugin/host.d.ts +70 -0
- package/dist/plugin/host.js +251 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/index.d.ts +30 -1
- package/dist/plugin/index.js +582 -12
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/kevin_approve.js +7 -4
- package/dist/plugin/kevin_approve.js.map +1 -1
- package/dist/plugin/kevin_audit.d.ts +48 -1
- package/dist/plugin/kevin_audit.js +179 -3
- package/dist/plugin/kevin_audit.js.map +1 -1
- package/dist/plugin/kevin_doctor.d.ts +57 -0
- package/dist/plugin/kevin_doctor.js +168 -0
- package/dist/plugin/kevin_doctor.js.map +1 -0
- package/dist/plugin/kevin_native.d.ts +29 -0
- package/dist/plugin/kevin_native.js +80 -0
- package/dist/plugin/kevin_native.js.map +1 -0
- package/dist/plugin/metrics.d.ts +1 -1
- package/dist/plugin/metrics.js +9 -0
- package/dist/plugin/metrics.js.map +1 -1
- package/dist/plugin/native.d.ts +92 -0
- package/dist/plugin/native.js +191 -0
- package/dist/plugin/native.js.map +1 -0
- package/dist/plugin/okf-export.d.ts +2 -2
- package/dist/plugin/okf-export.js +15 -7
- package/dist/plugin/okf-export.js.map +1 -1
- package/dist/plugin/okf.d.ts +107 -0
- package/dist/plugin/okf.js +304 -0
- package/dist/plugin/okf.js.map +1 -0
- package/dist/plugin/replay-types.d.ts +29 -287
- package/dist/plugin/replay-types.js +145 -53
- package/dist/plugin/replay-types.js.map +1 -1
- package/migrations/009_v08_team.sql +100 -0
- package/migrations/010_v09_native.sql +78 -0
- package/package.json +2 -3
package/dist/plugin/index.js
CHANGED
|
@@ -11,28 +11,36 @@ import { ContextInjector } from "./ContextInjector.js";
|
|
|
11
11
|
import { ConventionMiner } from "./ConventionMiner.js";
|
|
12
12
|
import { Curator } from "./Curator.js";
|
|
13
13
|
import { Feedback } from "./Feedback.js";
|
|
14
|
+
import { HookLiveness } from "./HookLiveness.js";
|
|
14
15
|
import { InjectionLedger } from "./InjectionLedger.js";
|
|
15
16
|
import { Materializer, SKILL_TOPIC } from "./Materializer.js";
|
|
16
|
-
import { MemoryService, } from "./MemoryService.js";
|
|
17
|
+
import { MemoryService, hasRepoIdColumn, } from "./MemoryService.js";
|
|
17
18
|
import { Migrate } from "./Migrate.js";
|
|
18
19
|
import { PatternMiner } from "./PatternMiner.js";
|
|
19
20
|
import { ERROR_LINE_RE, Reflector, STRONG_ERROR_RE } from "./Reflector.js";
|
|
21
|
+
import * as RepoIdentity from "./RepoIdentity.js";
|
|
20
22
|
import { RepoTruth } from "./RepoTruth.js";
|
|
21
23
|
import { Retrospective } from "./Retrospective.js";
|
|
24
|
+
import { SharedLayer } from "./SharedLayer.js";
|
|
22
25
|
import { Store } from "./Store.js";
|
|
23
26
|
import { ToolCallObserver } from "./ToolCallObserver.js";
|
|
24
27
|
import { probe } from "./capabilities.js";
|
|
25
|
-
import {
|
|
28
|
+
import { computeConfidence } from "./confidence.js";
|
|
29
|
+
import { probeHost, summarize } from "./host.js";
|
|
26
30
|
import { kevinApprove } from "./kevin_approve.js";
|
|
27
31
|
import { buildAudit } from "./kevin_audit.js";
|
|
28
32
|
import { executeKevinConflicts } from "./kevin_conflicts.js";
|
|
33
|
+
import { buildDoctor } from "./kevin_doctor.js";
|
|
29
34
|
import { buildKevinFacts } from "./kevin_facts.js";
|
|
35
|
+
import { handleNative } from "./kevin_native.js";
|
|
30
36
|
import { kevinPropose } from "./kevin_propose.js";
|
|
31
37
|
import { kevinPublish } from "./kevin_publish.js";
|
|
32
38
|
import { kevinWhy } from "./kevin_why.js";
|
|
33
39
|
import { Metrics } from "./metrics.js";
|
|
40
|
+
import { attachNative } from "./native.js";
|
|
34
41
|
import { exportMarkdown, exportOkf } from "./okf-export.js";
|
|
35
42
|
import { importOkf } from "./okf-import.js";
|
|
43
|
+
import { uuidv7 } from "./uuid.js";
|
|
36
44
|
// v0.6.0 (K6-019) — process-global set of reference topics already
|
|
37
45
|
// registered with the host this process. Keeps re-registration idempotent
|
|
38
46
|
// across sessions within one process: a topic registered once is never
|
|
@@ -71,6 +79,28 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
71
79
|
"convention_mining_enabled",
|
|
72
80
|
"conflict_detection_enabled",
|
|
73
81
|
"error_lesson_mode",
|
|
82
|
+
// v0.8.0 (K8-003 / plan §8.10) — the five keys seeded by migration 009
|
|
83
|
+
// section 5. Omitting these makes `kevin_config set` return
|
|
84
|
+
// { error: "unknown_key" } while `kevin_config list` still shows them.
|
|
85
|
+
// shared_layer_enabled must be compared with === "1" (it is TEXT, and
|
|
86
|
+
// '0' is truthy); shared_confidence_floor is a string read with
|
|
87
|
+
// Number.parseFloat and clamped to [0, 1] (conventions, §2).
|
|
88
|
+
"shared_layer_enabled",
|
|
89
|
+
"okf_path",
|
|
90
|
+
"share_requires_approval",
|
|
91
|
+
"author_identity_mode",
|
|
92
|
+
"shared_confidence_floor",
|
|
93
|
+
// v0.9.0 (K9-003 / plan §8.10) — the four keys seeded by migration 010
|
|
94
|
+
// section 5. Omitting these makes `kevin_config set` return
|
|
95
|
+
// { error: "unknown_key" } while `kevin_config list` still shows them.
|
|
96
|
+
// hook_liveness_enabled and the two registration/history flags must be
|
|
97
|
+
// compared with === "1" (they are TEXT, and '0' is truthy);
|
|
98
|
+
// dead_hook_report_threshold is a string read with Number.parseInt and
|
|
99
|
+
// clamped to [1, 1000], NaN defaulting to 3 (conventions, §2; D9-09).
|
|
100
|
+
"hook_liveness_enabled",
|
|
101
|
+
"native_registration_enabled",
|
|
102
|
+
"host_probe_history_enabled",
|
|
103
|
+
"dead_hook_report_threshold",
|
|
74
104
|
];
|
|
75
105
|
// v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
|
|
76
106
|
// `error_lesson_mode`. The setting is TEXT and must be compared with
|
|
@@ -79,10 +109,132 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
79
109
|
// at the surface rather than silently changing every installation's
|
|
80
110
|
// behaviour on the next reflection.
|
|
81
111
|
export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
|
|
112
|
+
/** Plugin release version — stamped into generated files (K8-021/027). */
|
|
113
|
+
export const KEVIN_VERSION = "0.9.0";
|
|
82
114
|
function resolveMigrationsDir() {
|
|
83
115
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
84
116
|
return join(here, "..", "migrations");
|
|
85
117
|
}
|
|
118
|
+
const REKEY_TABLES = ["memories", "shared_entries", "okf_imports"];
|
|
119
|
+
export function performRekey(store, toRepoId, opts) {
|
|
120
|
+
// The 009 migration carries the repo_id column AND the shared-layer
|
|
121
|
+
// tables; without it there is nothing to re-key.
|
|
122
|
+
if (!hasRepoIdColumn(store)) {
|
|
123
|
+
return {
|
|
124
|
+
action: "rekey",
|
|
125
|
+
ok: false,
|
|
126
|
+
reason: "la migracion 009 no se ha aplicado: no existe repo_id (ni shared_entries/okf_imports) sobre el que re-key",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Rows that would move: every scoped row stored under a repo_id
|
|
130
|
+
// different from the target. NULL-repo_id rows are global by design
|
|
131
|
+
// and never move.
|
|
132
|
+
const groupRows = (table) => store
|
|
133
|
+
.prepare(`SELECT repo_id, COUNT(*) AS c FROM ${table}
|
|
134
|
+
WHERE repo_id IS NOT NULL AND repo_id != ? GROUP BY repo_id`)
|
|
135
|
+
.all(toRepoId);
|
|
136
|
+
const from = {};
|
|
137
|
+
const rows = {
|
|
138
|
+
memories: 0,
|
|
139
|
+
shared_entries: 0,
|
|
140
|
+
okf_imports: 0,
|
|
141
|
+
};
|
|
142
|
+
for (const table of REKEY_TABLES) {
|
|
143
|
+
for (const r of groupRows(table)) {
|
|
144
|
+
rows[table] += r.c;
|
|
145
|
+
from[r.repo_id] ??= {
|
|
146
|
+
memories: 0,
|
|
147
|
+
shared_entries: 0,
|
|
148
|
+
okf_imports: 0,
|
|
149
|
+
};
|
|
150
|
+
from[r.repo_id][table] = r.c;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const total = rows.memories + rows.shared_entries + rows.okf_imports;
|
|
154
|
+
if (total === 0) {
|
|
155
|
+
return {
|
|
156
|
+
action: "rekey",
|
|
157
|
+
ok: true,
|
|
158
|
+
rekeyed: false,
|
|
159
|
+
to_repo_id: toRepoId,
|
|
160
|
+
rows,
|
|
161
|
+
from,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// Monorepo collision (D8-03): rows already at the target repo_id
|
|
165
|
+
// belong to a different project_id set than the rows that would move.
|
|
166
|
+
// shared_entries and okf_imports carry no project_id, so memories is
|
|
167
|
+
// the only witness.
|
|
168
|
+
const pidSet = (sql, ...params) => {
|
|
169
|
+
const out = new Set();
|
|
170
|
+
for (const r of store.prepare(sql).all(...params)) {
|
|
171
|
+
if (r.project_id !== null)
|
|
172
|
+
out.add(r.project_id);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
};
|
|
176
|
+
const targetPids = pidSet("SELECT DISTINCT project_id FROM memories WHERE repo_id = ?", toRepoId);
|
|
177
|
+
const movePids = pidSet("SELECT DISTINCT project_id FROM memories WHERE repo_id IS NOT NULL AND repo_id != ?", toRepoId);
|
|
178
|
+
const collision = targetPids.size > 0 &&
|
|
179
|
+
!(movePids.size === targetPids.size &&
|
|
180
|
+
[...movePids].every((p) => targetPids.has(p)));
|
|
181
|
+
if (!opts.confirm) {
|
|
182
|
+
return {
|
|
183
|
+
action: "rekey",
|
|
184
|
+
ok: true,
|
|
185
|
+
dry_run: true,
|
|
186
|
+
to_repo_id: toRepoId,
|
|
187
|
+
rows,
|
|
188
|
+
from,
|
|
189
|
+
collision,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (collision && opts.force !== true) {
|
|
193
|
+
return {
|
|
194
|
+
action: "rekey",
|
|
195
|
+
ok: false,
|
|
196
|
+
reason: "monorepo collision: el repo_id destino ya contiene memorias de un conjunto de project_id distinto; pasa force: true solo si quieres fusionarlos",
|
|
197
|
+
to_repo_id: toRepoId,
|
|
198
|
+
rows,
|
|
199
|
+
collision: true,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// One transaction: the row moves and the rekey_events counter move
|
|
203
|
+
// together — a mid-way failure rolls both back and the database is
|
|
204
|
+
// completely unchanged.
|
|
205
|
+
try {
|
|
206
|
+
store.transaction(() => {
|
|
207
|
+
for (const table of REKEY_TABLES) {
|
|
208
|
+
store
|
|
209
|
+
.prepare(`UPDATE ${table} SET repo_id = ?
|
|
210
|
+
WHERE repo_id IS NOT NULL AND repo_id != ?`)
|
|
211
|
+
.run(toRepoId, toRepoId);
|
|
212
|
+
}
|
|
213
|
+
store
|
|
214
|
+
.prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
|
|
215
|
+
VALUES ('rekey_events', 1, datetime('now'))
|
|
216
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
217
|
+
value = value + 1,
|
|
218
|
+
updated_at = datetime('now')`)
|
|
219
|
+
.run();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
return {
|
|
224
|
+
action: "rekey",
|
|
225
|
+
ok: false,
|
|
226
|
+
reason: `rekey fallo y se revirtio completamente: ${err?.message ?? "unknown error"}`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
action: "rekey",
|
|
231
|
+
ok: true,
|
|
232
|
+
rekeyed: true,
|
|
233
|
+
to_repo_id: toRepoId,
|
|
234
|
+
rows,
|
|
235
|
+
from,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
86
238
|
export const KevinPlugin = async (input, options) => {
|
|
87
239
|
const opts = (options ?? {});
|
|
88
240
|
const dbPath = opts.dbPath ?? join(homedir(), ".opencode-kevin", "kevin.db");
|
|
@@ -98,8 +250,48 @@ export const KevinPlugin = async (input, options) => {
|
|
|
98
250
|
// v0.4.0 (K4-019): the plugin hooks expose no project field, so the
|
|
99
251
|
// project id is derived once from the plugin host's working directory
|
|
100
252
|
// (plan §5.7 fallback; D2-11 project scoping wired into the live path).
|
|
101
|
-
|
|
102
|
-
|
|
253
|
+
// v0.8.0 (K8-006 / plan §5.1): the repository identity resolves once at
|
|
254
|
+
// init, next to the project id, and never on a hot path.
|
|
255
|
+
// v0.9.0 (K9-004/K9-006 / plan §5.1-5.2, D9-12/D9-13): the host surface
|
|
256
|
+
// is probed once at construction — frozen, never re-probed — and feeds
|
|
257
|
+
// the identity chain as the third source, above `path` and below
|
|
258
|
+
// `declared`/`remote`.
|
|
259
|
+
const host = await probeHost(input);
|
|
260
|
+
const identity = RepoIdentity.resolve(process.cwd(), host);
|
|
261
|
+
const projectId = identity.projectId;
|
|
262
|
+
const repoId = identity.repoId;
|
|
263
|
+
const identitySource = identity.source;
|
|
264
|
+
const identityEvidence = identity.evidence;
|
|
265
|
+
// v0.8.0 (BUG-001/002) — the SESSION identity: what the shared layer
|
|
266
|
+
// bridge, MemoryService, Curator and the tools actually use. It is
|
|
267
|
+
// derived once at init and only moves when kevin_project rekey
|
|
268
|
+
// succeeds (the rows move with it). kevin_status and the audit
|
|
269
|
+
// rollups report THIS id, never a fresh per-call resolve — a
|
|
270
|
+
// mid-session `git remote add` changes what the identity WILL be,
|
|
271
|
+
// not what the session is scoped on.
|
|
272
|
+
let sessionIdentity = identity;
|
|
273
|
+
const memoryService = new MemoryService(store, metrics, repoId);
|
|
274
|
+
// v0.9.0 (K9-008 / plan §5.1, D9-08) — host probe history: one
|
|
275
|
+
// host_probes row per construction when host_probe_history_enabled
|
|
276
|
+
// is explicitly '1' (TEXT comparison — truthiness would fire on
|
|
277
|
+
// '0'). Append-only and unbounded by design: off by default, one
|
|
278
|
+
// row per process start, no retention policy.
|
|
279
|
+
if (memoryService.getSetting("host_probe_history_enabled", "0") === "1") {
|
|
280
|
+
store
|
|
281
|
+
.prepare(`INSERT INTO host_probes
|
|
282
|
+
(id, plugin_version, flavour, has_shell, v2_skill, v2_reference, notes)
|
|
283
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
284
|
+
.run(uuidv7(), host.pluginVersion, host.flavour, host.hasShell ? 1 : 0, host.v2.skill ? 1 : 0, host.v2.reference ? 1 : 0, host.notes.join("\n"));
|
|
285
|
+
}
|
|
286
|
+
// v0.9.0 (K9-009 / plan §5.3, D9-07/D9-08) — hook liveness. Wraps every
|
|
287
|
+
// hook with a success-path recorder; counters persist on the
|
|
288
|
+
// metrics.flush() cadence. Explicit === "1" TEXT comparison
|
|
289
|
+
// (kevin_settings.value is TEXT).
|
|
290
|
+
const liveness = new HookLiveness(store, {
|
|
291
|
+
enabled: memoryService.getSetting("hook_liveness_enabled", "1") === "1",
|
|
292
|
+
thresholdText: memoryService.getSetting("dead_hook_report_threshold", "3"),
|
|
293
|
+
pluginVersion: host.pluginVersion,
|
|
294
|
+
});
|
|
103
295
|
// v0.7.0 (K7-009 / plan §5.1, D7-13) — the repository truth scanner reads
|
|
104
296
|
// the JSON project files. Runs once at init, gated by repo_truth_enabled.
|
|
105
297
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
@@ -151,7 +343,30 @@ export const KevinPlugin = async (input, options) => {
|
|
|
151
343
|
// ArtifactWriter is constructed here and only `kevin_approve` may call
|
|
152
344
|
// `apply()`. `kevin_propose` reaches the file only through `plan()`.
|
|
153
345
|
const writer = new ArtifactWriter(store, projectId, metrics);
|
|
154
|
-
|
|
346
|
+
// v0.8.0 (K8-020/021 / plan §5.5) — the shared layer bridge, built
|
|
347
|
+
// once at init next to the writer. okf_path resolves against
|
|
348
|
+
// projectRoot at call time; the writer is the only write path
|
|
349
|
+
// (D8-08).
|
|
350
|
+
let sharedLayer = new SharedLayer({
|
|
351
|
+
store,
|
|
352
|
+
repoId,
|
|
353
|
+
projectId,
|
|
354
|
+
version: KEVIN_VERSION,
|
|
355
|
+
writer,
|
|
356
|
+
});
|
|
357
|
+
// v0.8.0 (K8-022 / plan §5.5) — the shared layer re-read. sync is
|
|
358
|
+
// deliberately narrow: re-read a file that is already on disk —
|
|
359
|
+
// one read plus one hash on an unchanged repository (the
|
|
360
|
+
// okf_imports hash skip), and nothing else. No fetch, no push, no
|
|
361
|
+
// remote, no poll (D8-01, Principle 30). Reachable only from the
|
|
362
|
+
// kevin_sync tool and session.idle, never from a hot-path hook.
|
|
363
|
+
function syncSharedLayer() {
|
|
364
|
+
const okfPath = join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf"));
|
|
365
|
+
const report = sharedLayer.import(okfPath);
|
|
366
|
+
metrics.incr("shared_entries_imported", report.imported);
|
|
367
|
+
return report;
|
|
368
|
+
}
|
|
369
|
+
let curator = new Curator(store, memoryService, projectId, metrics, repoId);
|
|
155
370
|
// v0.6.0 (K6-017/018 / plan §5.6-5.7, D6-13) — pull-channel bundles and
|
|
156
371
|
// the v2 domain probe. `probe()` runs ONCE at init and the result is
|
|
157
372
|
// held; probing per-event is a hot-path cost for a value that cannot
|
|
@@ -162,6 +377,24 @@ export const KevinPlugin = async (input, options) => {
|
|
|
162
377
|
mkdirSync(join(materializerRoot, "refs"), { recursive: true });
|
|
163
378
|
const capabilities = probe(input);
|
|
164
379
|
const materializer = new Materializer(store, { root: materializerRoot });
|
|
380
|
+
// v0.9.0 (K9-016 / plan §5.4, D9-10) — native registration replaces
|
|
381
|
+
// file emission. When attachNative returns a registration for a
|
|
382
|
+
// surface, the corresponding *_emission_enabled path is skipped for
|
|
383
|
+
// that surface only. The guard lives in Materializer.
|
|
384
|
+
try {
|
|
385
|
+
const nativeReg = await attachNative(host, {
|
|
386
|
+
materializer,
|
|
387
|
+
settings: memoryService,
|
|
388
|
+
store,
|
|
389
|
+
});
|
|
390
|
+
if (nativeReg) {
|
|
391
|
+
materializer.markNativeRegistered("skill", nativeReg.registered.skill);
|
|
392
|
+
materializer.markNativeRegistered("reference", nativeReg.registered.reference);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// attachNative never throws (D9-12) — guard against unexpected rejection.
|
|
397
|
+
}
|
|
165
398
|
// v0.6.0 (K6-018 / plan §8.14, D6-13) — skill emission at session
|
|
166
399
|
// start, behind the capability probe AND `skill_emission_enabled`
|
|
167
400
|
// (default '0'). Both no-op paths are silent: "unavailable" (v1 host)
|
|
@@ -273,7 +506,11 @@ export const KevinPlugin = async (input, options) => {
|
|
|
273
506
|
projectId,
|
|
274
507
|
}));
|
|
275
508
|
}
|
|
276
|
-
|
|
509
|
+
// v0.9.0 (K9-009 / plan §5.3) — the hooks object is wrapped here at the
|
|
510
|
+
// point of return: transparent (identical keys/arity/returns/errors),
|
|
511
|
+
// recording fires on the success path only. With hook_liveness_enabled
|
|
512
|
+
// '0' the wrapper returns this same object untouched.
|
|
513
|
+
return liveness.wrap({
|
|
277
514
|
tool: {
|
|
278
515
|
kevin_save: tool({
|
|
279
516
|
description: "Guarda una memoria en el conocimiento persistente de Kevin (error, pattern, decision o context).",
|
|
@@ -572,6 +809,35 @@ export const KevinPlugin = async (input, options) => {
|
|
|
572
809
|
catch {
|
|
573
810
|
v07 = undefined;
|
|
574
811
|
}
|
|
812
|
+
// v0.8.0 (K8-025 / plan §5.8) — identity and shared-layer
|
|
813
|
+
// fields. repo_id is always a derived hash (never a raw
|
|
814
|
+
// remote URL — the "declared" source pins the .kevin/
|
|
815
|
+
// project.json id, "remote" hashes the normalized origin
|
|
816
|
+
// URL, "path" falls back to the project fingerprint).
|
|
817
|
+
// identity_source tells the user which of the three won.
|
|
818
|
+
// shared_layer_enabled is the config key read from
|
|
819
|
+
// kevin_settings (default "0"); shared_entries counts the
|
|
820
|
+
// active (asserted) entries for this repo — best-effort
|
|
821
|
+
// on pre-009 DBs, which have no shared layer at all.
|
|
822
|
+
// BUG-001: the SESSION identity (init-time, moved only by
|
|
823
|
+
// a successful rekey) — not a fresh per-call resolve, so
|
|
824
|
+
// the count always agrees with what kevin_share/kevin_sync
|
|
825
|
+
// actually read and write.
|
|
826
|
+
const identity = sessionIdentity;
|
|
827
|
+
let v08;
|
|
828
|
+
try {
|
|
829
|
+
v08 = {
|
|
830
|
+
repo_id: identity.repoId,
|
|
831
|
+
identity_source: identity.source,
|
|
832
|
+
shared_layer_enabled: memoryService.getSetting("shared_layer_enabled", "0"),
|
|
833
|
+
shared_entries: store
|
|
834
|
+
.prepare("SELECT COUNT(*) AS c FROM shared_entries WHERE repo_id = ? AND op = 'assert'")
|
|
835
|
+
.get(identity.repoId).c,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
v08 = undefined;
|
|
840
|
+
}
|
|
575
841
|
return {
|
|
576
842
|
title: "Estado de Kevin",
|
|
577
843
|
output: JSON.stringify({
|
|
@@ -580,8 +846,10 @@ export const KevinPlugin = async (input, options) => {
|
|
|
580
846
|
// (13 v0.5 + kevin_propose + kevin_approve +
|
|
581
847
|
// kevin_publish). The frozen ladder is verified
|
|
582
848
|
// monotone across releases; K6-024 extends this
|
|
583
|
-
// block with the remaining v0.6 fields.
|
|
584
|
-
|
|
849
|
+
// block with the remaining v0.6 fields. v0.8.0
|
|
850
|
+
// (K8-025) — 18 v0.7 + kevin_project +
|
|
851
|
+
// kevin_native = 23.
|
|
852
|
+
tool_count: 23,
|
|
585
853
|
v07,
|
|
586
854
|
memories_reflector: memoriesReflector,
|
|
587
855
|
memories_agent: memoriesAgent,
|
|
@@ -614,6 +882,134 @@ export const KevinPlugin = async (input, options) => {
|
|
|
614
882
|
},
|
|
615
883
|
// v0.6.0 (K6-024) — omitted on pre-007 DBs.
|
|
616
884
|
v06,
|
|
885
|
+
// v0.8.0 (K8-025) — omitted on pre-009 DBs.
|
|
886
|
+
v08,
|
|
887
|
+
// v0.9.0 (K9-008 / plan §5.1) — one-line host
|
|
888
|
+
// summary: version, flavour, shell, v2 flags;
|
|
889
|
+
// no paths, no ids (charset-safe for log lines).
|
|
890
|
+
host_summary: summarize(host),
|
|
891
|
+
}),
|
|
892
|
+
};
|
|
893
|
+
},
|
|
894
|
+
}),
|
|
895
|
+
kevin_project: tool({
|
|
896
|
+
description: "Identidad de repositorio (v0.8.0, K8-008/009 / plan §5.8): show reporta repoId, source, evidence, projectId, los conteos de memorias bajo cada scope y rekey_available (true cuando el corpus esta almacenado bajo un scope distinto del resuelto); init escribe .kevin/project.json fijando el id derivado (refusa si ya existe); rekey mueve el corpus entero al repo_id resuelto en UNA transaccion — sin confirm: true es un dry run que no muta nada, y la colision monorepo se rechaza salvo force: true.",
|
|
897
|
+
args: {
|
|
898
|
+
action: tool.schema.enum(["show", "init", "rekey"]),
|
|
899
|
+
confirm: tool.schema.boolean().optional(),
|
|
900
|
+
force: tool.schema.boolean().optional(),
|
|
901
|
+
},
|
|
902
|
+
async execute(args) {
|
|
903
|
+
// v0.8.0 (K8-008) — identity resolves against the boot
|
|
904
|
+
// project root (the directory the plugin was wired with),
|
|
905
|
+
// never against the host's cwd.
|
|
906
|
+
const identity = RepoIdentity.resolve(projectRoot);
|
|
907
|
+
if (args.action === "init") {
|
|
908
|
+
const res = RepoIdentity.initProjectFile(projectRoot, writer);
|
|
909
|
+
return {
|
|
910
|
+
title: "Kevin project.json",
|
|
911
|
+
output: JSON.stringify(res.ok
|
|
912
|
+
? {
|
|
913
|
+
action: "init",
|
|
914
|
+
written: true,
|
|
915
|
+
path: res.path,
|
|
916
|
+
id: res.id,
|
|
917
|
+
created_at: res.createdAt,
|
|
918
|
+
generator: "opencode-kevin/0.9.0",
|
|
919
|
+
}
|
|
920
|
+
: {
|
|
921
|
+
action: "init",
|
|
922
|
+
written: false,
|
|
923
|
+
path: res.path,
|
|
924
|
+
reason: res.reason,
|
|
925
|
+
}),
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
if (args.action === "rekey") {
|
|
929
|
+
// v0.8.0 (K8-009 / plan §5.1, D8-03) — the only call
|
|
930
|
+
// site of performRekey in this codebase (asserted by
|
|
931
|
+
// source scan in the K8-009 acceptance).
|
|
932
|
+
const res = performRekey(store, identity.repoId, {
|
|
933
|
+
confirm: args.confirm === true,
|
|
934
|
+
force: args.force === true,
|
|
935
|
+
});
|
|
936
|
+
// v0.8.0 (BUG-002) — a successful rekey moves the
|
|
937
|
+
// rows to the freshly resolved id; the session must
|
|
938
|
+
// follow, or kevin_share/kevin_sync/retrieval stay
|
|
939
|
+
// scoped on the old id until a restart. The bridge,
|
|
940
|
+
// MemoryService and Curator are rebuilt on the new id
|
|
941
|
+
// in the same breath the rows move — there is never
|
|
942
|
+
// a window in which the session and the store
|
|
943
|
+
// disagree. A dry run or a refusal mutates nothing.
|
|
944
|
+
if (res.ok && !res.dry_run) {
|
|
945
|
+
const live = RepoIdentity.resolve(projectRoot);
|
|
946
|
+
sessionIdentity = live;
|
|
947
|
+
memoryService.setRepoId(live.repoId);
|
|
948
|
+
sharedLayer = new SharedLayer({
|
|
949
|
+
store,
|
|
950
|
+
repoId: live.repoId,
|
|
951
|
+
projectId: live.projectId,
|
|
952
|
+
version: KEVIN_VERSION,
|
|
953
|
+
writer,
|
|
954
|
+
});
|
|
955
|
+
curator = new Curator(store, memoryService, projectId, metrics, live.repoId);
|
|
956
|
+
// v0.8.0 (BUG-003) — heal the OKF file header.
|
|
957
|
+
// Rekey changes the scope the file is written
|
|
958
|
+
// under; a stale `#repo` first line would make
|
|
959
|
+
// every later planExport/planTombstone refuse
|
|
960
|
+
// with repo_mismatch forever. The heal lives in
|
|
961
|
+
// SharedLayer so the whole-file construction
|
|
962
|
+
// stays at its single allowed site (D8-08).
|
|
963
|
+
try {
|
|
964
|
+
sharedLayer.healHeader(join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf")), live.repoId);
|
|
965
|
+
}
|
|
966
|
+
catch {
|
|
967
|
+
// Missing or unreadable file — nothing to heal.
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
title: "Kevin project rekey",
|
|
972
|
+
output: JSON.stringify(res),
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
// show.
|
|
976
|
+
const withRepoColumn = hasRepoIdColumn(store);
|
|
977
|
+
const memoriesTotal = store.prepare("SELECT COUNT(*) AS c FROM memories").get().c;
|
|
978
|
+
const memoriesUnderRepo = withRepoColumn
|
|
979
|
+
? store
|
|
980
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE repo_id = ?")
|
|
981
|
+
.get(identity.repoId).c
|
|
982
|
+
: 0;
|
|
983
|
+
const memoriesUnderProject = store
|
|
984
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE project_id = ?")
|
|
985
|
+
.get(identity.projectId).c;
|
|
986
|
+
// v0.8.0 (K8-008) — rekey_available: rows whose current
|
|
987
|
+
// scope differs from the resolved repo_id. On an
|
|
988
|
+
// unmigrated corpus the stored scope is project_id, so a
|
|
989
|
+
// git remote present (repoId != path fingerprint) reports
|
|
990
|
+
// true without the 009 column existing.
|
|
991
|
+
const differentlyScoped = withRepoColumn
|
|
992
|
+
? store
|
|
993
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE repo_id IS NOT NULL AND repo_id != ?")
|
|
994
|
+
.get(identity.repoId).c
|
|
995
|
+
: store
|
|
996
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE project_id IS NOT NULL AND project_id != ?")
|
|
997
|
+
.get(identity.repoId).c;
|
|
998
|
+
return {
|
|
999
|
+
title: "Identidad de repositorio",
|
|
1000
|
+
output: JSON.stringify({
|
|
1001
|
+
action: "show",
|
|
1002
|
+
repo_id: identity.repoId,
|
|
1003
|
+
source: identity.source,
|
|
1004
|
+
evidence: identity.evidence,
|
|
1005
|
+
project_id: identity.projectId,
|
|
1006
|
+
memories_total: memoriesTotal,
|
|
1007
|
+
memories_repo_id: memoriesUnderRepo,
|
|
1008
|
+
memories_project_id: memoriesUnderProject,
|
|
1009
|
+
rekey_available: differentlyScoped > 0,
|
|
1010
|
+
project_json: existsSync(join(projectRoot, ".kevin", "project.json"))
|
|
1011
|
+
? "present"
|
|
1012
|
+
: "absent",
|
|
617
1013
|
}),
|
|
618
1014
|
};
|
|
619
1015
|
},
|
|
@@ -629,7 +1025,9 @@ export const KevinPlugin = async (input, options) => {
|
|
|
629
1025
|
// three emission states ("unavailable" vs "off" vs "on").
|
|
630
1026
|
// v0.7.0 (K7-019 / K7-006) — projectId scopes the `truth`
|
|
631
1027
|
// block; on a pre-008 DB the block is omitted, partial:true.
|
|
632
|
-
|
|
1028
|
+
// v0.8.0 (K8-007 / plan §5.7) — the penalized-memory rollup
|
|
1029
|
+
// scopes on repoId once the 009 column exists.
|
|
1030
|
+
const report = buildAudit(store, metrics, capabilities, projectId, sessionIdentity.repoId);
|
|
633
1031
|
const payload = args.verbose
|
|
634
1032
|
? report
|
|
635
1033
|
: { ...report, settings: undefined };
|
|
@@ -639,6 +1037,48 @@ export const KevinPlugin = async (input, options) => {
|
|
|
639
1037
|
};
|
|
640
1038
|
},
|
|
641
1039
|
}),
|
|
1040
|
+
// v0.9.0 (K9-018 / plan §5.5, D9-09) — kevin_doctor: pure
|
|
1041
|
+
// reads, no writes, no probe re-run, no model call. The host
|
|
1042
|
+
// block comes from the frozen init-time probe, hooks from the
|
|
1043
|
+
// persisted hook_liveness table, native from the last
|
|
1044
|
+
// registration per surface, verdict from reduceVerdict.
|
|
1045
|
+
kevin_doctor: tool({
|
|
1046
|
+
description: "Doctor de Kevin (v0.9.0): salud del host, hooks, dependencias y registros nativos. Solo lectura, sin LLM ni writes, invocable en cualquier momento. Devuelve host (version, flavour, shell, v2), hooks ordenados dead primero, dependencies (zod_copies), native (enabled, registered/verified por surface), verdict (healthy|degraded|unknown) y reason. Output pensado para pegar en un issue: sin filesystem paths ni session ids.",
|
|
1047
|
+
args: {},
|
|
1048
|
+
async execute() {
|
|
1049
|
+
const report = buildDoctor(store, host, memoryService);
|
|
1050
|
+
return {
|
|
1051
|
+
title: "Doctor de Kevin",
|
|
1052
|
+
output: JSON.stringify(report),
|
|
1053
|
+
};
|
|
1054
|
+
},
|
|
1055
|
+
}),
|
|
1056
|
+
// v0.9.0 (K9-019 / plan §5.5, D9-12) — kevin_native: inspect
|
|
1057
|
+
// and toggle native_registration_enabled. enable/disable write
|
|
1058
|
+
// kevin_settings only and never re-attach — the probe is
|
|
1059
|
+
// frozen for the process lifetime, so a restart is the
|
|
1060
|
+
// requirement for the change to take effect. `enable` on a
|
|
1061
|
+
// host without the v2 subpath succeeds and reports the
|
|
1062
|
+
// registration as inert: the setting is a statement of intent.
|
|
1063
|
+
kevin_native: tool({
|
|
1064
|
+
description: "Registros nativos v2 (v0.9.0): show reporta el setting native_registration_enabled, si el host resuelto expone el subpath v2 (effective) y las ultimas filas de native_registrations por surface; enable/disable escriben el setting ('1'/'0' TEXT) y NO re-adjuntan — el probe es frozen para la vida del proceso, un restart del host es el requisito para que el cambio tenga efecto; enable en un host sin subpath v2 exito con registration inert.",
|
|
1065
|
+
args: {
|
|
1066
|
+
action: tool.schema
|
|
1067
|
+
.enum(["show", "enable", "disable"])
|
|
1068
|
+
.default("show"),
|
|
1069
|
+
},
|
|
1070
|
+
async execute(args) {
|
|
1071
|
+
const report = handleNative(args.action, {
|
|
1072
|
+
host,
|
|
1073
|
+
store,
|
|
1074
|
+
settings: memoryService,
|
|
1075
|
+
});
|
|
1076
|
+
return {
|
|
1077
|
+
title: "Kevin native",
|
|
1078
|
+
output: JSON.stringify(report),
|
|
1079
|
+
};
|
|
1080
|
+
},
|
|
1081
|
+
}),
|
|
642
1082
|
kevin_retrospective: tool({
|
|
643
1083
|
description: "Genera un retrospective markdown para una sesion (resume tools que fallaron y lecciones aprendidas).",
|
|
644
1084
|
args: {
|
|
@@ -782,9 +1222,13 @@ export const KevinPlugin = async (input, options) => {
|
|
|
782
1222
|
.describe("okf | markdown"),
|
|
783
1223
|
},
|
|
784
1224
|
async execute(args) {
|
|
1225
|
+
// v0.8.0 (K8-027) — the v1 export is scoped to this
|
|
1226
|
+
// plugin's project: the global database holds several
|
|
1227
|
+
// projects' memories, and a committed export must not
|
|
1228
|
+
// leak another project's knowledge.
|
|
785
1229
|
const output = args.format === "markdown"
|
|
786
|
-
? exportMarkdown(store)
|
|
787
|
-
: exportOkf(store);
|
|
1230
|
+
? exportMarkdown(store, projectId)
|
|
1231
|
+
: exportOkf(store, projectId);
|
|
788
1232
|
return {
|
|
789
1233
|
title: "Export completado",
|
|
790
1234
|
output,
|
|
@@ -976,6 +1420,103 @@ export const KevinPlugin = async (input, options) => {
|
|
|
976
1420
|
};
|
|
977
1421
|
},
|
|
978
1422
|
}),
|
|
1423
|
+
// v0.8.0 (K8-021 / plan §5.5) — kevin_share. The dry run is the
|
|
1424
|
+
// default: nothing is written until the diff is shown. The
|
|
1425
|
+
// write itself goes through the single funnel (applyExport),
|
|
1426
|
+
// and shared_entries_exported counts only entries actually
|
|
1427
|
+
// added to the file.
|
|
1428
|
+
kevin_share: tool({
|
|
1429
|
+
description: "Comparte memorias locales al archivo OKF compartido (v0.8.0, K8-021 / plan 5.5): dry-run por defecto - sin memory_ids selecciona toda memoria curated=1 en o sobre shared_confidence_floor no ya compartida, y devuelve el plan con su diff sin escribir nada. confirm: true escribe via el unico write path (ArtifactWriter, D8-08); con share_requires_approval='1' (default) ademas se requiere confirm para escribir y las memorias no curadas se rechazan con not_curated; las que no alcanzan el floor se rechazan con below_floor; compartir la misma memoria dos veces es un noop en el segundo intento. Nunca escribe otro fichero que no sea okf_path.",
|
|
1430
|
+
args: {
|
|
1431
|
+
memory_ids: tool.schema.array(tool.schema.string()).optional(),
|
|
1432
|
+
dry_run: tool.schema.boolean().optional(),
|
|
1433
|
+
confirm: tool.schema.boolean().optional(),
|
|
1434
|
+
},
|
|
1435
|
+
async execute(args) {
|
|
1436
|
+
const okfPath = join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf"));
|
|
1437
|
+
const requiresApproval = memoryService.getSetting("share_requires_approval", "1") === "1";
|
|
1438
|
+
const dryRun = args.dry_run ?? true;
|
|
1439
|
+
const confirm = args.confirm ?? false;
|
|
1440
|
+
const floor = Number.parseFloat(memoryService.getSetting("shared_confidence_floor", "0.7"));
|
|
1441
|
+
const clampedFloor = Number.isNaN(floor)
|
|
1442
|
+
? 0.7
|
|
1443
|
+
: Math.min(1, Math.max(0, floor));
|
|
1444
|
+
const ids = args.memory_ids !== undefined && args.memory_ids.length > 0
|
|
1445
|
+
? args.memory_ids
|
|
1446
|
+
: store
|
|
1447
|
+
.prepare(`SELECT id, evidence_count, recurrence_count
|
|
1448
|
+
FROM memories
|
|
1449
|
+
WHERE layer = 'local' AND curated = 1
|
|
1450
|
+
AND shared_entry_id IS NULL AND repo_id = ?`)
|
|
1451
|
+
.all(sessionIdentity.repoId)
|
|
1452
|
+
.filter((row) => computeConfidence(row.evidence_count ?? 0, row.recurrence_count ?? 0) >= clampedFloor)
|
|
1453
|
+
.map((row) => row.id);
|
|
1454
|
+
const plan = sharedLayer.planExport(ids, okfPath);
|
|
1455
|
+
if (plan.write.outcome === "refused") {
|
|
1456
|
+
return {
|
|
1457
|
+
title: "Exportacion rechazada",
|
|
1458
|
+
output: JSON.stringify({
|
|
1459
|
+
refused: plan.write.reason,
|
|
1460
|
+
okf_path: okfPath,
|
|
1461
|
+
}),
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
const base = {
|
|
1465
|
+
memory_ids: ids,
|
|
1466
|
+
entries_added: plan.entriesAdded,
|
|
1467
|
+
okf_path: okfPath,
|
|
1468
|
+
diff: plan.write.diff,
|
|
1469
|
+
};
|
|
1470
|
+
if (dryRun) {
|
|
1471
|
+
return {
|
|
1472
|
+
title: "Plan de exportacion (dry-run)",
|
|
1473
|
+
output: JSON.stringify({ ...base, dry_run: true }),
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
if (requiresApproval && !confirm) {
|
|
1477
|
+
return {
|
|
1478
|
+
title: "Confirmacion requerida",
|
|
1479
|
+
output: JSON.stringify({
|
|
1480
|
+
...base,
|
|
1481
|
+
dry_run: true,
|
|
1482
|
+
confirm_required: true,
|
|
1483
|
+
}),
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
// The writer's atomic temp file lives next to the target,
|
|
1487
|
+
// so the .kevin directory must exist before the write
|
|
1488
|
+
// (same prep as RepoIdentity.initProjectFile).
|
|
1489
|
+
mkdirSync(dirname(okfPath), { recursive: true });
|
|
1490
|
+
const applied = sharedLayer.applyExport(plan);
|
|
1491
|
+
if (applied.applied === "written") {
|
|
1492
|
+
metrics.incr("shared_entries_exported", plan.entriesAdded);
|
|
1493
|
+
}
|
|
1494
|
+
return {
|
|
1495
|
+
title: applied.applied === "noop"
|
|
1496
|
+
? "Nada nuevo que compartir"
|
|
1497
|
+
: "Exportacion aplicada",
|
|
1498
|
+
output: JSON.stringify({
|
|
1499
|
+
...base,
|
|
1500
|
+
outcome: applied.applied,
|
|
1501
|
+
}),
|
|
1502
|
+
};
|
|
1503
|
+
},
|
|
1504
|
+
}),
|
|
1505
|
+
// v0.8.0 (K8-022 / plan §5.5) — kevin_sync. The name and the
|
|
1506
|
+
// scope are deliberately narrow: "sync" means re-reading a
|
|
1507
|
+
// file that is already on disk. Manual invocation works
|
|
1508
|
+
// regardless of shared_layer_enabled; the automatic idle
|
|
1509
|
+
// re-read below is the one gated by the flag.
|
|
1510
|
+
kevin_sync: tool({
|
|
1511
|
+
description: "Re-lee el archivo OKF compartido ya en disco (v0.8.0, K8-022 / plan 5.5): importa las entradas del archivo okf_path al store compartido y reporta el ImportReport (parsed/folded/rejected/imported/tombstoned/skipped). No hay fetch, push, remoto ni polling (D8-01, Principle 30). Funciona con shared_layer_enabled en '0'; la sincronizacion automatica en session.idle esta gated por shared_layer_enabled === '1' y nunca corre en hot paths.",
|
|
1512
|
+
args: {},
|
|
1513
|
+
async execute() {
|
|
1514
|
+
return {
|
|
1515
|
+
title: "Sincronizacion compartida",
|
|
1516
|
+
output: JSON.stringify(syncSharedLayer()),
|
|
1517
|
+
};
|
|
1518
|
+
},
|
|
1519
|
+
}),
|
|
979
1520
|
},
|
|
980
1521
|
"tool.execute.before": async (hookInput, output) => {
|
|
981
1522
|
rememberToolCall(hookInput.callID, hookInput.tool, output.args);
|
|
@@ -988,6 +1529,13 @@ export const KevinPlugin = async (input, options) => {
|
|
|
988
1529
|
}, {});
|
|
989
1530
|
},
|
|
990
1531
|
"tool.execute.after": async (hookInput, output) => {
|
|
1532
|
+
// v0.9.0 (K9-010 / plan §5.3) — checkpoint: the session reached a
|
|
1533
|
+
// model turn, so the system prompt was assembled and
|
|
1534
|
+
// `experimental.chat.system.transform` MUST have been offered.
|
|
1535
|
+
// Deduped per session inside HookLiveness.expect.
|
|
1536
|
+
if (hookInput.sessionID) {
|
|
1537
|
+
liveness.expect("experimental.chat.system.transform", hookInput.sessionID);
|
|
1538
|
+
}
|
|
991
1539
|
const meta = (output.metadata ?? {});
|
|
992
1540
|
const outputText = String(output.output ?? "");
|
|
993
1541
|
const stderr = String(meta.stderr ?? "");
|
|
@@ -1187,8 +1735,29 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1187
1735
|
// best-effort, same pattern as ledger.settle — a
|
|
1188
1736
|
// curation failure must not break the idle path
|
|
1189
1737
|
}
|
|
1738
|
+
// v0.8.0 (K8-022 / plan §5.5) — the shared layer
|
|
1739
|
+
// re-read at idle. Gated by shared_layer_enabled ===
|
|
1740
|
+
// "1", a TEXT comparison: '0' is a truthy string, so a
|
|
1741
|
+
// truthiness check would turn the release on for every
|
|
1742
|
+
// installation that upgrades. Never wired into
|
|
1743
|
+
// tool.execute.*, chat.message, system.transform or
|
|
1744
|
+
// session.compacting — the hot-path rule is absolute;
|
|
1745
|
+
// the file-hash skip is what makes the idle cost one
|
|
1746
|
+
// read plus one hash on an unchanged repository.
|
|
1747
|
+
try {
|
|
1748
|
+
if (memoryService.getSetting("shared_layer_enabled", "0") === "1") {
|
|
1749
|
+
syncSharedLayer();
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
catch {
|
|
1753
|
+
// best-effort, same pattern as ledger.settle — a
|
|
1754
|
+
// sync failure must not break the idle path
|
|
1755
|
+
}
|
|
1190
1756
|
}
|
|
1191
1757
|
metrics.flush();
|
|
1758
|
+
// v0.9.0 (K9-009 / plan §5.3) — liveness counters persist on
|
|
1759
|
+
// the same cadence as the metrics.
|
|
1760
|
+
liveness.flush();
|
|
1192
1761
|
}
|
|
1193
1762
|
else if (type === "session.next.tool.failed") {
|
|
1194
1763
|
const callID = props.callID;
|
|
@@ -1206,10 +1775,11 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1206
1775
|
},
|
|
1207
1776
|
dispose: async () => {
|
|
1208
1777
|
await Promise.allSettled([...pending]);
|
|
1778
|
+
liveness.flush();
|
|
1209
1779
|
metrics.close();
|
|
1210
1780
|
store.close();
|
|
1211
1781
|
},
|
|
1212
|
-
};
|
|
1782
|
+
});
|
|
1213
1783
|
};
|
|
1214
1784
|
export default KevinPlugin;
|
|
1215
1785
|
//# sourceMappingURL=index.js.map
|