@jmtrin/opencode-kevin 0.6.0 → 0.8.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 +771 -580
- package/dist/migrations/001_initial.sql +91 -91
- package/dist/migrations/003_v02_signal.sql +57 -57
- package/dist/migrations/004_v03_knowledge.sql +138 -138
- package/dist/migrations/005_v04_signal.sql +57 -57
- package/dist/migrations/006_v05_glassbox.sql +118 -118
- package/dist/migrations/007_v06_pull.sql +144 -144
- package/dist/migrations/008_v07_truth.sql +124 -0
- package/dist/migrations/009_v08_team.sql +100 -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/ConflictDetector.d.ts +35 -0
- package/dist/plugin/ConflictDetector.js +283 -0
- package/dist/plugin/ConflictDetector.js.map +1 -0
- 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/ConventionMiner.d.ts +35 -0
- package/dist/plugin/ConventionMiner.js +243 -0
- package/dist/plugin/ConventionMiner.js.map +1 -0
- package/dist/plugin/Curator.d.ts +22 -2
- package/dist/plugin/Curator.js +101 -28
- package/dist/plugin/Curator.js.map +1 -1
- 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 +4 -5
- package/dist/plugin/Materializer.js +9 -6
- package/dist/plugin/Materializer.js.map +1 -1
- package/dist/plugin/MemoryService.d.ts +49 -2
- package/dist/plugin/MemoryService.js +306 -8
- package/dist/plugin/MemoryService.js.map +1 -1
- package/dist/plugin/Migrate.js +61 -2
- package/dist/plugin/Migrate.js.map +1 -1
- package/dist/plugin/Reflector.js +13 -0
- package/dist/plugin/Reflector.js.map +1 -1
- package/dist/plugin/RepoIdentity.d.ts +113 -0
- package/dist/plugin/RepoIdentity.js +266 -0
- package/dist/plugin/RepoIdentity.js.map +1 -0
- package/dist/plugin/RepoTruth.d.ts +80 -0
- package/dist/plugin/RepoTruth.js +600 -0
- package/dist/plugin/RepoTruth.js.map +1 -0
- package/dist/plugin/Retrospective.js +17 -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/index.d.ts +36 -2
- package/dist/plugin/index.js +591 -10
- 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 +59 -1
- package/dist/plugin/kevin_audit.js +190 -3
- package/dist/plugin/kevin_audit.js.map +1 -1
- package/dist/plugin/kevin_conflicts.d.ts +9 -0
- package/dist/plugin/kevin_conflicts.js +51 -0
- package/dist/plugin/kevin_conflicts.js.map +1 -0
- package/dist/plugin/kevin_facts.d.ts +42 -0
- package/dist/plugin/kevin_facts.js +37 -0
- package/dist/plugin/kevin_facts.js.map +1 -0
- package/dist/plugin/metrics.d.ts +3 -3
- package/dist/plugin/metrics.js +21 -2
- package/dist/plugin/metrics.js.map +1 -1
- 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/001_initial.sql +91 -91
- package/migrations/003_v02_signal.sql +57 -57
- package/migrations/004_v03_knowledge.sql +138 -138
- package/migrations/005_v04_signal.sql +57 -57
- package/migrations/006_v05_glassbox.sql +118 -118
- package/migrations/007_v06_pull.sql +144 -144
- package/migrations/008_v07_truth.sql +124 -0
- package/migrations/009_v08_team.sql +100 -0
- package/package.json +4 -4
package/dist/plugin/index.js
CHANGED
|
@@ -6,22 +6,29 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
6
6
|
import { Archiver } from "./Archiver.js";
|
|
7
7
|
import { ArtifactWriter } from "./ArtifactWriter.js";
|
|
8
8
|
import { CausalChain } from "./CausalChain.js";
|
|
9
|
+
import { ConflictDetector } from "./ConflictDetector.js";
|
|
9
10
|
import { ContextInjector } from "./ContextInjector.js";
|
|
11
|
+
import { ConventionMiner } from "./ConventionMiner.js";
|
|
10
12
|
import { Curator } from "./Curator.js";
|
|
11
13
|
import { Feedback } from "./Feedback.js";
|
|
12
14
|
import { InjectionLedger } from "./InjectionLedger.js";
|
|
13
15
|
import { Materializer, SKILL_TOPIC } from "./Materializer.js";
|
|
14
|
-
import { MemoryService, } from "./MemoryService.js";
|
|
16
|
+
import { MemoryService, hasRepoIdColumn, } from "./MemoryService.js";
|
|
15
17
|
import { Migrate } from "./Migrate.js";
|
|
16
18
|
import { PatternMiner } from "./PatternMiner.js";
|
|
17
19
|
import { ERROR_LINE_RE, Reflector, STRONG_ERROR_RE } from "./Reflector.js";
|
|
20
|
+
import * as RepoIdentity from "./RepoIdentity.js";
|
|
21
|
+
import { RepoTruth } from "./RepoTruth.js";
|
|
18
22
|
import { Retrospective } from "./Retrospective.js";
|
|
23
|
+
import { SharedLayer } from "./SharedLayer.js";
|
|
19
24
|
import { Store } from "./Store.js";
|
|
20
25
|
import { ToolCallObserver } from "./ToolCallObserver.js";
|
|
21
26
|
import { probe } from "./capabilities.js";
|
|
22
|
-
import {
|
|
27
|
+
import { computeConfidence } from "./confidence.js";
|
|
23
28
|
import { kevinApprove } from "./kevin_approve.js";
|
|
24
29
|
import { buildAudit } from "./kevin_audit.js";
|
|
30
|
+
import { executeKevinConflicts } from "./kevin_conflicts.js";
|
|
31
|
+
import { buildKevinFacts } from "./kevin_facts.js";
|
|
25
32
|
import { kevinPropose } from "./kevin_propose.js";
|
|
26
33
|
import { kevinPublish } from "./kevin_publish.js";
|
|
27
34
|
import { kevinWhy } from "./kevin_why.js";
|
|
@@ -58,11 +65,159 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
58
65
|
"skill_emission_enabled",
|
|
59
66
|
"reference_emission_enabled",
|
|
60
67
|
"injection_confidence_floor",
|
|
68
|
+
// v0.7.0 (K7-003 / plan §8.10) — the four keys seeded by migration 008
|
|
69
|
+
// section 5. Three feature flags plus the error lesson mode enum.
|
|
70
|
+
// Omitting these makes `kevin_config set` return { error: "unknown_key" }
|
|
71
|
+
// while `kevin_config list` still shows them.
|
|
72
|
+
"repo_truth_enabled",
|
|
73
|
+
"convention_mining_enabled",
|
|
74
|
+
"conflict_detection_enabled",
|
|
75
|
+
"error_lesson_mode",
|
|
76
|
+
// v0.8.0 (K8-003 / plan §8.10) — the five keys seeded by migration 009
|
|
77
|
+
// section 5. Omitting these makes `kevin_config set` return
|
|
78
|
+
// { error: "unknown_key" } while `kevin_config list` still shows them.
|
|
79
|
+
// shared_layer_enabled must be compared with === "1" (it is TEXT, and
|
|
80
|
+
// '0' is truthy); shared_confidence_floor is a string read with
|
|
81
|
+
// Number.parseFloat and clamped to [0, 1] (conventions, §2).
|
|
82
|
+
"shared_layer_enabled",
|
|
83
|
+
"okf_path",
|
|
84
|
+
"share_requires_approval",
|
|
85
|
+
"author_identity_mode",
|
|
86
|
+
"shared_confidence_floor",
|
|
61
87
|
];
|
|
88
|
+
// v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
|
|
89
|
+
// `error_lesson_mode`. The setting is TEXT and must be compared with
|
|
90
|
+
// `=== "triage_only"`, never by truthiness; the domain here is enforced by
|
|
91
|
+
// `kevin_config set` so a typo (`"triage"`, `"0"`, `"false"`) is rejected
|
|
92
|
+
// at the surface rather than silently changing every installation's
|
|
93
|
+
// behaviour on the next reflection.
|
|
94
|
+
export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
|
|
95
|
+
/** Plugin release version — stamped into generated files (K8-021/027). */
|
|
96
|
+
export const KEVIN_VERSION = "0.8.0";
|
|
62
97
|
function resolveMigrationsDir() {
|
|
63
98
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
64
99
|
return join(here, "..", "migrations");
|
|
65
100
|
}
|
|
101
|
+
const REKEY_TABLES = ["memories", "shared_entries", "okf_imports"];
|
|
102
|
+
export function performRekey(store, toRepoId, opts) {
|
|
103
|
+
// The 009 migration carries the repo_id column AND the shared-layer
|
|
104
|
+
// tables; without it there is nothing to re-key.
|
|
105
|
+
if (!hasRepoIdColumn(store)) {
|
|
106
|
+
return {
|
|
107
|
+
action: "rekey",
|
|
108
|
+
ok: false,
|
|
109
|
+
reason: "la migracion 009 no se ha aplicado: no existe repo_id (ni shared_entries/okf_imports) sobre el que re-key",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Rows that would move: every scoped row stored under a repo_id
|
|
113
|
+
// different from the target. NULL-repo_id rows are global by design
|
|
114
|
+
// and never move.
|
|
115
|
+
const groupRows = (table) => store
|
|
116
|
+
.prepare(`SELECT repo_id, COUNT(*) AS c FROM ${table}
|
|
117
|
+
WHERE repo_id IS NOT NULL AND repo_id != ? GROUP BY repo_id`)
|
|
118
|
+
.all(toRepoId);
|
|
119
|
+
const from = {};
|
|
120
|
+
const rows = {
|
|
121
|
+
memories: 0,
|
|
122
|
+
shared_entries: 0,
|
|
123
|
+
okf_imports: 0,
|
|
124
|
+
};
|
|
125
|
+
for (const table of REKEY_TABLES) {
|
|
126
|
+
for (const r of groupRows(table)) {
|
|
127
|
+
rows[table] += r.c;
|
|
128
|
+
from[r.repo_id] ??= {
|
|
129
|
+
memories: 0,
|
|
130
|
+
shared_entries: 0,
|
|
131
|
+
okf_imports: 0,
|
|
132
|
+
};
|
|
133
|
+
from[r.repo_id][table] = r.c;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const total = rows.memories + rows.shared_entries + rows.okf_imports;
|
|
137
|
+
if (total === 0) {
|
|
138
|
+
return {
|
|
139
|
+
action: "rekey",
|
|
140
|
+
ok: true,
|
|
141
|
+
rekeyed: false,
|
|
142
|
+
to_repo_id: toRepoId,
|
|
143
|
+
rows,
|
|
144
|
+
from,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Monorepo collision (D8-03): rows already at the target repo_id
|
|
148
|
+
// belong to a different project_id set than the rows that would move.
|
|
149
|
+
// shared_entries and okf_imports carry no project_id, so memories is
|
|
150
|
+
// the only witness.
|
|
151
|
+
const pidSet = (sql, ...params) => {
|
|
152
|
+
const out = new Set();
|
|
153
|
+
for (const r of store.prepare(sql).all(...params)) {
|
|
154
|
+
if (r.project_id !== null)
|
|
155
|
+
out.add(r.project_id);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
};
|
|
159
|
+
const targetPids = pidSet("SELECT DISTINCT project_id FROM memories WHERE repo_id = ?", toRepoId);
|
|
160
|
+
const movePids = pidSet("SELECT DISTINCT project_id FROM memories WHERE repo_id IS NOT NULL AND repo_id != ?", toRepoId);
|
|
161
|
+
const collision = targetPids.size > 0 &&
|
|
162
|
+
!(movePids.size === targetPids.size &&
|
|
163
|
+
[...movePids].every((p) => targetPids.has(p)));
|
|
164
|
+
if (!opts.confirm) {
|
|
165
|
+
return {
|
|
166
|
+
action: "rekey",
|
|
167
|
+
ok: true,
|
|
168
|
+
dry_run: true,
|
|
169
|
+
to_repo_id: toRepoId,
|
|
170
|
+
rows,
|
|
171
|
+
from,
|
|
172
|
+
collision,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (collision && opts.force !== true) {
|
|
176
|
+
return {
|
|
177
|
+
action: "rekey",
|
|
178
|
+
ok: false,
|
|
179
|
+
reason: "monorepo collision: el repo_id destino ya contiene memorias de un conjunto de project_id distinto; pasa force: true solo si quieres fusionarlos",
|
|
180
|
+
to_repo_id: toRepoId,
|
|
181
|
+
rows,
|
|
182
|
+
collision: true,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// One transaction: the row moves and the rekey_events counter move
|
|
186
|
+
// together — a mid-way failure rolls both back and the database is
|
|
187
|
+
// completely unchanged.
|
|
188
|
+
try {
|
|
189
|
+
store.transaction(() => {
|
|
190
|
+
for (const table of REKEY_TABLES) {
|
|
191
|
+
store
|
|
192
|
+
.prepare(`UPDATE ${table} SET repo_id = ?
|
|
193
|
+
WHERE repo_id IS NOT NULL AND repo_id != ?`)
|
|
194
|
+
.run(toRepoId, toRepoId);
|
|
195
|
+
}
|
|
196
|
+
store
|
|
197
|
+
.prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
|
|
198
|
+
VALUES ('rekey_events', 1, datetime('now'))
|
|
199
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
200
|
+
value = value + 1,
|
|
201
|
+
updated_at = datetime('now')`)
|
|
202
|
+
.run();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
return {
|
|
207
|
+
action: "rekey",
|
|
208
|
+
ok: false,
|
|
209
|
+
reason: `rekey fallo y se revirtio completamente: ${err?.message ?? "unknown error"}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
action: "rekey",
|
|
214
|
+
ok: true,
|
|
215
|
+
rekeyed: true,
|
|
216
|
+
to_repo_id: toRepoId,
|
|
217
|
+
rows,
|
|
218
|
+
from,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
66
221
|
export const KevinPlugin = async (input, options) => {
|
|
67
222
|
const opts = (options ?? {});
|
|
68
223
|
const dbPath = opts.dbPath ?? join(homedir(), ".opencode-kevin", "kevin.db");
|
|
@@ -78,8 +233,34 @@ export const KevinPlugin = async (input, options) => {
|
|
|
78
233
|
// v0.4.0 (K4-019): the plugin hooks expose no project field, so the
|
|
79
234
|
// project id is derived once from the plugin host's working directory
|
|
80
235
|
// (plan §5.7 fallback; D2-11 project scoping wired into the live path).
|
|
81
|
-
|
|
82
|
-
|
|
236
|
+
// v0.8.0 (K8-006 / plan §5.1): the repository identity resolves once at
|
|
237
|
+
// init, next to the project id, and never on a hot path.
|
|
238
|
+
const identity = RepoIdentity.resolve(process.cwd());
|
|
239
|
+
const projectId = identity.projectId;
|
|
240
|
+
const repoId = identity.repoId;
|
|
241
|
+
const identitySource = identity.source;
|
|
242
|
+
const identityEvidence = identity.evidence;
|
|
243
|
+
// v0.8.0 (BUG-001/002) — the SESSION identity: what the shared layer
|
|
244
|
+
// bridge, MemoryService, Curator and the tools actually use. It is
|
|
245
|
+
// derived once at init and only moves when kevin_project rekey
|
|
246
|
+
// succeeds (the rows move with it). kevin_status and the audit
|
|
247
|
+
// rollups report THIS id, never a fresh per-call resolve — a
|
|
248
|
+
// mid-session `git remote add` changes what the identity WILL be,
|
|
249
|
+
// not what the session is scoped on.
|
|
250
|
+
let sessionIdentity = identity;
|
|
251
|
+
const memoryService = new MemoryService(store, metrics, repoId);
|
|
252
|
+
// v0.7.0 (K7-009 / plan §5.1, D7-13) — the repository truth scanner reads
|
|
253
|
+
// the JSON project files. Runs once at init, gated by repo_truth_enabled.
|
|
254
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
255
|
+
const repoTruth = new RepoTruth(store, projectId, projectRoot, metrics);
|
|
256
|
+
if (memoryService.getSetting("repo_truth_enabled", "0") === "1") {
|
|
257
|
+
try {
|
|
258
|
+
repoTruth.scan();
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// A scan failure at init must not prevent the plugin from loading.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
83
264
|
const observer = new ToolCallObserver(store, metrics);
|
|
84
265
|
// v0.3.0 fix — Prepared UPDATE used by Reflector.onLinkError to stamp
|
|
85
266
|
// tool_calls.error_fingerprint with the stderr-based fingerprint the
|
|
@@ -112,12 +293,37 @@ export const KevinPlugin = async (input, options) => {
|
|
|
112
293
|
dir: opts.retrospectivesDir,
|
|
113
294
|
}, metrics);
|
|
114
295
|
const patternMiner = new PatternMiner(store, memoryService, metrics);
|
|
296
|
+
const conventionMiner = new ConventionMiner(store, memoryService, projectId, metrics);
|
|
297
|
+
const conflictDetector = new ConflictDetector(store, projectId, metrics, repoTruth, memoryService);
|
|
115
298
|
const causalChain = new CausalChain(store, memoryService, metrics);
|
|
116
299
|
// v0.6.0 (K6-014 / plan §5.4-5.5, D6-01) — the single write path: the
|
|
117
300
|
// ArtifactWriter is constructed here and only `kevin_approve` may call
|
|
118
301
|
// `apply()`. `kevin_propose` reaches the file only through `plan()`.
|
|
119
302
|
const writer = new ArtifactWriter(store, projectId, metrics);
|
|
120
|
-
|
|
303
|
+
// v0.8.0 (K8-020/021 / plan §5.5) — the shared layer bridge, built
|
|
304
|
+
// once at init next to the writer. okf_path resolves against
|
|
305
|
+
// projectRoot at call time; the writer is the only write path
|
|
306
|
+
// (D8-08).
|
|
307
|
+
let sharedLayer = new SharedLayer({
|
|
308
|
+
store,
|
|
309
|
+
repoId,
|
|
310
|
+
projectId,
|
|
311
|
+
version: KEVIN_VERSION,
|
|
312
|
+
writer,
|
|
313
|
+
});
|
|
314
|
+
// v0.8.0 (K8-022 / plan §5.5) — the shared layer re-read. sync is
|
|
315
|
+
// deliberately narrow: re-read a file that is already on disk —
|
|
316
|
+
// one read plus one hash on an unchanged repository (the
|
|
317
|
+
// okf_imports hash skip), and nothing else. No fetch, no push, no
|
|
318
|
+
// remote, no poll (D8-01, Principle 30). Reachable only from the
|
|
319
|
+
// kevin_sync tool and session.idle, never from a hot-path hook.
|
|
320
|
+
function syncSharedLayer() {
|
|
321
|
+
const okfPath = join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf"));
|
|
322
|
+
const report = sharedLayer.import(okfPath);
|
|
323
|
+
metrics.incr("shared_entries_imported", report.imported);
|
|
324
|
+
return report;
|
|
325
|
+
}
|
|
326
|
+
let curator = new Curator(store, memoryService, projectId, metrics, repoId);
|
|
121
327
|
// v0.6.0 (K6-017/018 / plan §5.6-5.7, D6-13) — pull-channel bundles and
|
|
122
328
|
// the v2 domain probe. `probe()` runs ONCE at init and the result is
|
|
123
329
|
// held; probing per-event is a hot-path cost for a value that cannot
|
|
@@ -521,6 +727,52 @@ export const KevinPlugin = async (input, options) => {
|
|
|
521
727
|
catch {
|
|
522
728
|
v06 = undefined;
|
|
523
729
|
}
|
|
730
|
+
let v07;
|
|
731
|
+
try {
|
|
732
|
+
v07 = {
|
|
733
|
+
schema_version: store
|
|
734
|
+
.prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1")
|
|
735
|
+
.get()?.version ?? "000",
|
|
736
|
+
facts_scanned: metrics.get("repo_facts_scanned"),
|
|
737
|
+
open_conflicts: store
|
|
738
|
+
.prepare("SELECT COUNT(*) AS c FROM memory_conflicts WHERE status = 'open'")
|
|
739
|
+
.get().c,
|
|
740
|
+
penalized_memories: metrics.get("memories_contradicted"),
|
|
741
|
+
error_lesson_mode: memoryService.getSetting("error_lesson_mode", "all"),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
v07 = undefined;
|
|
746
|
+
}
|
|
747
|
+
// v0.8.0 (K8-025 / plan §5.8) — identity and shared-layer
|
|
748
|
+
// fields. repo_id is always a derived hash (never a raw
|
|
749
|
+
// remote URL — the "declared" source pins the .kevin/
|
|
750
|
+
// project.json id, "remote" hashes the normalized origin
|
|
751
|
+
// URL, "path" falls back to the project fingerprint).
|
|
752
|
+
// identity_source tells the user which of the three won.
|
|
753
|
+
// shared_layer_enabled is the config key read from
|
|
754
|
+
// kevin_settings (default "0"); shared_entries counts the
|
|
755
|
+
// active (asserted) entries for this repo — best-effort
|
|
756
|
+
// on pre-009 DBs, which have no shared layer at all.
|
|
757
|
+
// BUG-001: the SESSION identity (init-time, moved only by
|
|
758
|
+
// a successful rekey) — not a fresh per-call resolve, so
|
|
759
|
+
// the count always agrees with what kevin_share/kevin_sync
|
|
760
|
+
// actually read and write.
|
|
761
|
+
const identity = sessionIdentity;
|
|
762
|
+
let v08;
|
|
763
|
+
try {
|
|
764
|
+
v08 = {
|
|
765
|
+
repo_id: identity.repoId,
|
|
766
|
+
identity_source: identity.source,
|
|
767
|
+
shared_layer_enabled: memoryService.getSetting("shared_layer_enabled", "0"),
|
|
768
|
+
shared_entries: store
|
|
769
|
+
.prepare("SELECT COUNT(*) AS c FROM shared_entries WHERE repo_id = ? AND op = 'assert'")
|
|
770
|
+
.get(identity.repoId).c,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
v08 = undefined;
|
|
775
|
+
}
|
|
524
776
|
return {
|
|
525
777
|
title: "Estado de Kevin",
|
|
526
778
|
output: JSON.stringify({
|
|
@@ -529,8 +781,11 @@ export const KevinPlugin = async (input, options) => {
|
|
|
529
781
|
// (13 v0.5 + kevin_propose + kevin_approve +
|
|
530
782
|
// kevin_publish). The frozen ladder is verified
|
|
531
783
|
// monotone across releases; K6-024 extends this
|
|
532
|
-
// block with the remaining v0.6 fields.
|
|
533
|
-
|
|
784
|
+
// block with the remaining v0.6 fields. v0.8.0
|
|
785
|
+
// (K8-025) — 18 v0.7 + kevin_project +
|
|
786
|
+
// kevin_share + kevin_sync = 21.
|
|
787
|
+
tool_count: 21,
|
|
788
|
+
v07,
|
|
534
789
|
memories_reflector: memoriesReflector,
|
|
535
790
|
memories_agent: memoriesAgent,
|
|
536
791
|
memories_pattern: memoriesPattern,
|
|
@@ -562,6 +817,130 @@ export const KevinPlugin = async (input, options) => {
|
|
|
562
817
|
},
|
|
563
818
|
// v0.6.0 (K6-024) — omitted on pre-007 DBs.
|
|
564
819
|
v06,
|
|
820
|
+
// v0.8.0 (K8-025) — omitted on pre-009 DBs.
|
|
821
|
+
v08,
|
|
822
|
+
}),
|
|
823
|
+
};
|
|
824
|
+
},
|
|
825
|
+
}),
|
|
826
|
+
kevin_project: tool({
|
|
827
|
+
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.",
|
|
828
|
+
args: {
|
|
829
|
+
action: tool.schema.enum(["show", "init", "rekey"]),
|
|
830
|
+
confirm: tool.schema.boolean().optional(),
|
|
831
|
+
force: tool.schema.boolean().optional(),
|
|
832
|
+
},
|
|
833
|
+
async execute(args) {
|
|
834
|
+
// v0.8.0 (K8-008) — identity resolves against the boot
|
|
835
|
+
// project root (the directory the plugin was wired with),
|
|
836
|
+
// never against the host's cwd.
|
|
837
|
+
const identity = RepoIdentity.resolve(projectRoot);
|
|
838
|
+
if (args.action === "init") {
|
|
839
|
+
const res = RepoIdentity.initProjectFile(projectRoot, writer);
|
|
840
|
+
return {
|
|
841
|
+
title: "Kevin project.json",
|
|
842
|
+
output: JSON.stringify(res.ok
|
|
843
|
+
? {
|
|
844
|
+
action: "init",
|
|
845
|
+
written: true,
|
|
846
|
+
path: res.path,
|
|
847
|
+
id: res.id,
|
|
848
|
+
created_at: res.createdAt,
|
|
849
|
+
generator: "opencode-kevin/0.8.0",
|
|
850
|
+
}
|
|
851
|
+
: {
|
|
852
|
+
action: "init",
|
|
853
|
+
written: false,
|
|
854
|
+
path: res.path,
|
|
855
|
+
reason: res.reason,
|
|
856
|
+
}),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
if (args.action === "rekey") {
|
|
860
|
+
// v0.8.0 (K8-009 / plan §5.1, D8-03) — the only call
|
|
861
|
+
// site of performRekey in this codebase (asserted by
|
|
862
|
+
// source scan in the K8-009 acceptance).
|
|
863
|
+
const res = performRekey(store, identity.repoId, {
|
|
864
|
+
confirm: args.confirm === true,
|
|
865
|
+
force: args.force === true,
|
|
866
|
+
});
|
|
867
|
+
// v0.8.0 (BUG-002) — a successful rekey moves the
|
|
868
|
+
// rows to the freshly resolved id; the session must
|
|
869
|
+
// follow, or kevin_share/kevin_sync/retrieval stay
|
|
870
|
+
// scoped on the old id until a restart. The bridge,
|
|
871
|
+
// MemoryService and Curator are rebuilt on the new id
|
|
872
|
+
// in the same breath the rows move — there is never
|
|
873
|
+
// a window in which the session and the store
|
|
874
|
+
// disagree. A dry run or a refusal mutates nothing.
|
|
875
|
+
if (res.ok && !res.dry_run) {
|
|
876
|
+
const live = RepoIdentity.resolve(projectRoot);
|
|
877
|
+
sessionIdentity = live;
|
|
878
|
+
memoryService.setRepoId(live.repoId);
|
|
879
|
+
sharedLayer = new SharedLayer({
|
|
880
|
+
store,
|
|
881
|
+
repoId: live.repoId,
|
|
882
|
+
projectId: live.projectId,
|
|
883
|
+
version: KEVIN_VERSION,
|
|
884
|
+
writer,
|
|
885
|
+
});
|
|
886
|
+
curator = new Curator(store, memoryService, projectId, metrics, live.repoId);
|
|
887
|
+
// v0.8.0 (BUG-003) — heal the OKF file header.
|
|
888
|
+
// Rekey changes the scope the file is written
|
|
889
|
+
// under; a stale `#repo` first line would make
|
|
890
|
+
// every later planExport/planTombstone refuse
|
|
891
|
+
// with repo_mismatch forever. The heal lives in
|
|
892
|
+
// SharedLayer so the whole-file construction
|
|
893
|
+
// stays at its single allowed site (D8-08).
|
|
894
|
+
try {
|
|
895
|
+
sharedLayer.healHeader(join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf")), live.repoId);
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
// Missing or unreadable file — nothing to heal.
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
title: "Kevin project rekey",
|
|
903
|
+
output: JSON.stringify(res),
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
// show.
|
|
907
|
+
const withRepoColumn = hasRepoIdColumn(store);
|
|
908
|
+
const memoriesTotal = store.prepare("SELECT COUNT(*) AS c FROM memories").get().c;
|
|
909
|
+
const memoriesUnderRepo = withRepoColumn
|
|
910
|
+
? store
|
|
911
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE repo_id = ?")
|
|
912
|
+
.get(identity.repoId).c
|
|
913
|
+
: 0;
|
|
914
|
+
const memoriesUnderProject = store
|
|
915
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE project_id = ?")
|
|
916
|
+
.get(identity.projectId).c;
|
|
917
|
+
// v0.8.0 (K8-008) — rekey_available: rows whose current
|
|
918
|
+
// scope differs from the resolved repo_id. On an
|
|
919
|
+
// unmigrated corpus the stored scope is project_id, so a
|
|
920
|
+
// git remote present (repoId != path fingerprint) reports
|
|
921
|
+
// true without the 009 column existing.
|
|
922
|
+
const differentlyScoped = withRepoColumn
|
|
923
|
+
? store
|
|
924
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE repo_id IS NOT NULL AND repo_id != ?")
|
|
925
|
+
.get(identity.repoId).c
|
|
926
|
+
: store
|
|
927
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE project_id IS NOT NULL AND project_id != ?")
|
|
928
|
+
.get(identity.repoId).c;
|
|
929
|
+
return {
|
|
930
|
+
title: "Identidad de repositorio",
|
|
931
|
+
output: JSON.stringify({
|
|
932
|
+
action: "show",
|
|
933
|
+
repo_id: identity.repoId,
|
|
934
|
+
source: identity.source,
|
|
935
|
+
evidence: identity.evidence,
|
|
936
|
+
project_id: identity.projectId,
|
|
937
|
+
memories_total: memoriesTotal,
|
|
938
|
+
memories_repo_id: memoriesUnderRepo,
|
|
939
|
+
memories_project_id: memoriesUnderProject,
|
|
940
|
+
rekey_available: differentlyScoped > 0,
|
|
941
|
+
project_json: existsSync(join(projectRoot, ".kevin", "project.json"))
|
|
942
|
+
? "present"
|
|
943
|
+
: "absent",
|
|
565
944
|
}),
|
|
566
945
|
};
|
|
567
946
|
},
|
|
@@ -575,7 +954,11 @@ export const KevinPlugin = async (input, options) => {
|
|
|
575
954
|
// v0.6.0 (K6-023 / plan §5.8) — capabilities come from the
|
|
576
955
|
// probe() run ONCE at init, so kevin_audit can report the
|
|
577
956
|
// three emission states ("unavailable" vs "off" vs "on").
|
|
578
|
-
|
|
957
|
+
// v0.7.0 (K7-019 / K7-006) — projectId scopes the `truth`
|
|
958
|
+
// block; on a pre-008 DB the block is omitted, partial:true.
|
|
959
|
+
// v0.8.0 (K8-007 / plan §5.7) — the penalized-memory rollup
|
|
960
|
+
// scopes on repoId once the 009 column exists.
|
|
961
|
+
const report = buildAudit(store, metrics, capabilities, projectId, sessionIdentity.repoId);
|
|
579
962
|
const payload = args.verbose
|
|
580
963
|
? report
|
|
581
964
|
: { ...report, settings: undefined };
|
|
@@ -728,9 +1111,13 @@ export const KevinPlugin = async (input, options) => {
|
|
|
728
1111
|
.describe("okf | markdown"),
|
|
729
1112
|
},
|
|
730
1113
|
async execute(args) {
|
|
1114
|
+
// v0.8.0 (K8-027) — the v1 export is scoped to this
|
|
1115
|
+
// plugin's project: the global database holds several
|
|
1116
|
+
// projects' memories, and a committed export must not
|
|
1117
|
+
// leak another project's knowledge.
|
|
731
1118
|
const output = args.format === "markdown"
|
|
732
|
-
? exportMarkdown(store)
|
|
733
|
-
: exportOkf(store);
|
|
1119
|
+
? exportMarkdown(store, projectId)
|
|
1120
|
+
: exportOkf(store, projectId);
|
|
734
1121
|
return {
|
|
735
1122
|
title: "Export completado",
|
|
736
1123
|
output,
|
|
@@ -802,6 +1189,25 @@ export const KevinPlugin = async (input, options) => {
|
|
|
802
1189
|
};
|
|
803
1190
|
}
|
|
804
1191
|
const value = args.value ?? "1";
|
|
1192
|
+
// v0.7.0 (K7-003 / plan §5.6, D7-12) — `error_lesson_mode`
|
|
1193
|
+
// is a TEXT enum with a tiny explicit domain. Reject a
|
|
1194
|
+
// value outside `all` / `triage_only` with a structured
|
|
1195
|
+
// error so a typo cannot silently change every
|
|
1196
|
+
// installation's reflection behaviour.
|
|
1197
|
+
if (args.key === "error_lesson_mode") {
|
|
1198
|
+
const allowed = ERROR_LESSON_MODE_VALUES;
|
|
1199
|
+
if (!allowed.includes(value)) {
|
|
1200
|
+
return {
|
|
1201
|
+
title: "kevin_config",
|
|
1202
|
+
output: JSON.stringify({
|
|
1203
|
+
error: "invalid_value",
|
|
1204
|
+
key: args.key,
|
|
1205
|
+
value,
|
|
1206
|
+
allowed_values: allowed,
|
|
1207
|
+
}),
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
805
1211
|
store
|
|
806
1212
|
.prepare(`INSERT INTO kevin_settings (key, value) VALUES (?, ?)
|
|
807
1213
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
|
@@ -812,6 +1218,41 @@ export const KevinPlugin = async (input, options) => {
|
|
|
812
1218
|
};
|
|
813
1219
|
},
|
|
814
1220
|
}),
|
|
1221
|
+
kevin_facts: tool({
|
|
1222
|
+
description: "Reporta la verdad del repositorio (v0.7.0): los hechos escaneados del proyecto actual y las memorias de-rankadas por contradiccion, con penalti y razones. refresh:true fuerza un re-scan (salta el skip de mtime); por defecto lee los hechos almacenados. Solo lectura salvo el re-scan opcional. Sin LLM ni red.",
|
|
1223
|
+
args: {
|
|
1224
|
+
refresh: tool.schema
|
|
1225
|
+
.boolean()
|
|
1226
|
+
.default(false)
|
|
1227
|
+
.optional()
|
|
1228
|
+
.describe("true: fuerza el re-scan de package.json/tsconfig.json. false (default): lee hechos almacenados."),
|
|
1229
|
+
},
|
|
1230
|
+
async execute(args) {
|
|
1231
|
+
const result = buildKevinFacts({ store, memoryService, repoTruth, projectId }, args.refresh ?? false);
|
|
1232
|
+
return {
|
|
1233
|
+
title: "Hechos del repositorio (v0.7.0)",
|
|
1234
|
+
output: JSON.stringify(result),
|
|
1235
|
+
};
|
|
1236
|
+
},
|
|
1237
|
+
}),
|
|
1238
|
+
kevin_conflicts: tool({
|
|
1239
|
+
description: "Lista, reconoce o resuelve conflictos detectados de Kevin.",
|
|
1240
|
+
args: {
|
|
1241
|
+
action: tool.schema.enum(["list", "acknowledge", "resolve"]),
|
|
1242
|
+
id: tool.schema.string().optional(),
|
|
1243
|
+
keep: tool.schema.string().optional(),
|
|
1244
|
+
status: tool.schema
|
|
1245
|
+
.enum(["open", "acknowledged", "resolved"])
|
|
1246
|
+
.optional(),
|
|
1247
|
+
},
|
|
1248
|
+
async execute(args) {
|
|
1249
|
+
const result = executeKevinConflicts({ store, detector: conflictDetector, projectId }, args.action, args.id, args.keep, args.status);
|
|
1250
|
+
return {
|
|
1251
|
+
title: "Conflictos de Kevin",
|
|
1252
|
+
output: JSON.stringify(result),
|
|
1253
|
+
};
|
|
1254
|
+
},
|
|
1255
|
+
}),
|
|
815
1256
|
kevin_propose: tool({
|
|
816
1257
|
description: "Genera propuestas de curacion (dry-run estricto, v0.6.0): crea filas pending y retorna sus diffs unificados. NO escribe en disco, NO marca memorias curadas (D5-08/D6-01).",
|
|
817
1258
|
args: {
|
|
@@ -868,6 +1309,103 @@ export const KevinPlugin = async (input, options) => {
|
|
|
868
1309
|
};
|
|
869
1310
|
},
|
|
870
1311
|
}),
|
|
1312
|
+
// v0.8.0 (K8-021 / plan §5.5) — kevin_share. The dry run is the
|
|
1313
|
+
// default: nothing is written until the diff is shown. The
|
|
1314
|
+
// write itself goes through the single funnel (applyExport),
|
|
1315
|
+
// and shared_entries_exported counts only entries actually
|
|
1316
|
+
// added to the file.
|
|
1317
|
+
kevin_share: tool({
|
|
1318
|
+
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.",
|
|
1319
|
+
args: {
|
|
1320
|
+
memory_ids: tool.schema.array(tool.schema.string()).optional(),
|
|
1321
|
+
dry_run: tool.schema.boolean().optional(),
|
|
1322
|
+
confirm: tool.schema.boolean().optional(),
|
|
1323
|
+
},
|
|
1324
|
+
async execute(args) {
|
|
1325
|
+
const okfPath = join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf"));
|
|
1326
|
+
const requiresApproval = memoryService.getSetting("share_requires_approval", "1") === "1";
|
|
1327
|
+
const dryRun = args.dry_run ?? true;
|
|
1328
|
+
const confirm = args.confirm ?? false;
|
|
1329
|
+
const floor = Number.parseFloat(memoryService.getSetting("shared_confidence_floor", "0.7"));
|
|
1330
|
+
const clampedFloor = Number.isNaN(floor)
|
|
1331
|
+
? 0.7
|
|
1332
|
+
: Math.min(1, Math.max(0, floor));
|
|
1333
|
+
const ids = args.memory_ids !== undefined && args.memory_ids.length > 0
|
|
1334
|
+
? args.memory_ids
|
|
1335
|
+
: store
|
|
1336
|
+
.prepare(`SELECT id, evidence_count, recurrence_count
|
|
1337
|
+
FROM memories
|
|
1338
|
+
WHERE layer = 'local' AND curated = 1
|
|
1339
|
+
AND shared_entry_id IS NULL AND repo_id = ?`)
|
|
1340
|
+
.all(sessionIdentity.repoId)
|
|
1341
|
+
.filter((row) => computeConfidence(row.evidence_count ?? 0, row.recurrence_count ?? 0) >= clampedFloor)
|
|
1342
|
+
.map((row) => row.id);
|
|
1343
|
+
const plan = sharedLayer.planExport(ids, okfPath);
|
|
1344
|
+
if (plan.write.outcome === "refused") {
|
|
1345
|
+
return {
|
|
1346
|
+
title: "Exportacion rechazada",
|
|
1347
|
+
output: JSON.stringify({
|
|
1348
|
+
refused: plan.write.reason,
|
|
1349
|
+
okf_path: okfPath,
|
|
1350
|
+
}),
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
const base = {
|
|
1354
|
+
memory_ids: ids,
|
|
1355
|
+
entries_added: plan.entriesAdded,
|
|
1356
|
+
okf_path: okfPath,
|
|
1357
|
+
diff: plan.write.diff,
|
|
1358
|
+
};
|
|
1359
|
+
if (dryRun) {
|
|
1360
|
+
return {
|
|
1361
|
+
title: "Plan de exportacion (dry-run)",
|
|
1362
|
+
output: JSON.stringify({ ...base, dry_run: true }),
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
if (requiresApproval && !confirm) {
|
|
1366
|
+
return {
|
|
1367
|
+
title: "Confirmacion requerida",
|
|
1368
|
+
output: JSON.stringify({
|
|
1369
|
+
...base,
|
|
1370
|
+
dry_run: true,
|
|
1371
|
+
confirm_required: true,
|
|
1372
|
+
}),
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
// The writer's atomic temp file lives next to the target,
|
|
1376
|
+
// so the .kevin directory must exist before the write
|
|
1377
|
+
// (same prep as RepoIdentity.initProjectFile).
|
|
1378
|
+
mkdirSync(dirname(okfPath), { recursive: true });
|
|
1379
|
+
const applied = sharedLayer.applyExport(plan);
|
|
1380
|
+
if (applied.applied === "written") {
|
|
1381
|
+
metrics.incr("shared_entries_exported", plan.entriesAdded);
|
|
1382
|
+
}
|
|
1383
|
+
return {
|
|
1384
|
+
title: applied.applied === "noop"
|
|
1385
|
+
? "Nada nuevo que compartir"
|
|
1386
|
+
: "Exportacion aplicada",
|
|
1387
|
+
output: JSON.stringify({
|
|
1388
|
+
...base,
|
|
1389
|
+
outcome: applied.applied,
|
|
1390
|
+
}),
|
|
1391
|
+
};
|
|
1392
|
+
},
|
|
1393
|
+
}),
|
|
1394
|
+
// v0.8.0 (K8-022 / plan §5.5) — kevin_sync. The name and the
|
|
1395
|
+
// scope are deliberately narrow: "sync" means re-reading a
|
|
1396
|
+
// file that is already on disk. Manual invocation works
|
|
1397
|
+
// regardless of shared_layer_enabled; the automatic idle
|
|
1398
|
+
// re-read below is the one gated by the flag.
|
|
1399
|
+
kevin_sync: tool({
|
|
1400
|
+
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.",
|
|
1401
|
+
args: {},
|
|
1402
|
+
async execute() {
|
|
1403
|
+
return {
|
|
1404
|
+
title: "Sincronizacion compartida",
|
|
1405
|
+
output: JSON.stringify(syncSharedLayer()),
|
|
1406
|
+
};
|
|
1407
|
+
},
|
|
1408
|
+
}),
|
|
871
1409
|
},
|
|
872
1410
|
"tool.execute.before": async (hookInput, output) => {
|
|
873
1411
|
rememberToolCall(hookInput.callID, hookInput.tool, output.args);
|
|
@@ -1025,6 +1563,31 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1025
1563
|
const recurred = memoryService.penalizeRecurringReflectors(sid);
|
|
1026
1564
|
injector.setRecurrences(recurred, sid);
|
|
1027
1565
|
patternMiner.mine(projectId);
|
|
1566
|
+
// v0.7.0 (K7-012 / plan §5.4, D7-10) — convention mining is
|
|
1567
|
+
// session.idle-only, default-off, and isolated from the rest of
|
|
1568
|
+
// the idle chain. Mined rules still enter the ordinary Curator
|
|
1569
|
+
// approval path; this step never writes to the repository.
|
|
1570
|
+
try {
|
|
1571
|
+
if (memoryService.getSetting("convention_mining_enabled", "0") === "1") {
|
|
1572
|
+
const conventions = conventionMiner.mine();
|
|
1573
|
+
conventionMiner.emit(conventions);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
catch {
|
|
1577
|
+
// A throwing miner must not reject or truncate the idle chain.
|
|
1578
|
+
}
|
|
1579
|
+
// v0.7.0 (K7-016 / plan §5.5, D7-06) — conflict detection is
|
|
1580
|
+
// surfacing-only on idle. It may create/open conflict rows, but
|
|
1581
|
+
// no idle path can acknowledge or resolve one.
|
|
1582
|
+
try {
|
|
1583
|
+
if (memoryService.getSetting("conflict_detection_enabled", "0") ===
|
|
1584
|
+
"1") {
|
|
1585
|
+
conflictDetector.detect();
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
catch {
|
|
1589
|
+
// Conflict surfacing is best-effort and must not reject idle.
|
|
1590
|
+
}
|
|
1028
1591
|
fireAndForget(Promise.resolve()
|
|
1029
1592
|
.then(() => causalChain.onSessionIdle(sid))
|
|
1030
1593
|
// non-blocking — promote is a best-effort pass
|
|
@@ -1054,6 +1617,24 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1054
1617
|
// best-effort, same pattern as ledger.settle — a
|
|
1055
1618
|
// curation failure must not break the idle path
|
|
1056
1619
|
}
|
|
1620
|
+
// v0.8.0 (K8-022 / plan §5.5) — the shared layer
|
|
1621
|
+
// re-read at idle. Gated by shared_layer_enabled ===
|
|
1622
|
+
// "1", a TEXT comparison: '0' is a truthy string, so a
|
|
1623
|
+
// truthiness check would turn the release on for every
|
|
1624
|
+
// installation that upgrades. Never wired into
|
|
1625
|
+
// tool.execute.*, chat.message, system.transform or
|
|
1626
|
+
// session.compacting — the hot-path rule is absolute;
|
|
1627
|
+
// the file-hash skip is what makes the idle cost one
|
|
1628
|
+
// read plus one hash on an unchanged repository.
|
|
1629
|
+
try {
|
|
1630
|
+
if (memoryService.getSetting("shared_layer_enabled", "0") === "1") {
|
|
1631
|
+
syncSharedLayer();
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
catch {
|
|
1635
|
+
// best-effort, same pattern as ledger.settle — a
|
|
1636
|
+
// sync failure must not break the idle path
|
|
1637
|
+
}
|
|
1057
1638
|
}
|
|
1058
1639
|
metrics.flush();
|
|
1059
1640
|
}
|