@jmtrin/opencode-kevin 0.7.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 +198 -9
- 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/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/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 +34 -5
- package/dist/plugin/MemoryService.js +215 -7
- package/dist/plugin/MemoryService.js.map +1 -1
- package/dist/plugin/Migrate.js +33 -3
- package/dist/plugin/Migrate.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/Retrospective.js +9 -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 +30 -1
- package/dist/plugin/index.js +458 -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 +19 -1
- package/dist/plugin/kevin_audit.js +95 -3
- package/dist/plugin/kevin_audit.js.map +1 -1
- 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/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/package.json +2 -3
package/dist/plugin/index.js
CHANGED
|
@@ -13,16 +13,18 @@ import { Curator } from "./Curator.js";
|
|
|
13
13
|
import { Feedback } from "./Feedback.js";
|
|
14
14
|
import { InjectionLedger } from "./InjectionLedger.js";
|
|
15
15
|
import { Materializer, SKILL_TOPIC } from "./Materializer.js";
|
|
16
|
-
import { MemoryService, } from "./MemoryService.js";
|
|
16
|
+
import { MemoryService, hasRepoIdColumn, } from "./MemoryService.js";
|
|
17
17
|
import { Migrate } from "./Migrate.js";
|
|
18
18
|
import { PatternMiner } from "./PatternMiner.js";
|
|
19
19
|
import { ERROR_LINE_RE, Reflector, STRONG_ERROR_RE } from "./Reflector.js";
|
|
20
|
+
import * as RepoIdentity from "./RepoIdentity.js";
|
|
20
21
|
import { RepoTruth } from "./RepoTruth.js";
|
|
21
22
|
import { Retrospective } from "./Retrospective.js";
|
|
23
|
+
import { SharedLayer } from "./SharedLayer.js";
|
|
22
24
|
import { Store } from "./Store.js";
|
|
23
25
|
import { ToolCallObserver } from "./ToolCallObserver.js";
|
|
24
26
|
import { probe } from "./capabilities.js";
|
|
25
|
-
import {
|
|
27
|
+
import { computeConfidence } from "./confidence.js";
|
|
26
28
|
import { kevinApprove } from "./kevin_approve.js";
|
|
27
29
|
import { buildAudit } from "./kevin_audit.js";
|
|
28
30
|
import { executeKevinConflicts } from "./kevin_conflicts.js";
|
|
@@ -71,6 +73,17 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
71
73
|
"convention_mining_enabled",
|
|
72
74
|
"conflict_detection_enabled",
|
|
73
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",
|
|
74
87
|
];
|
|
75
88
|
// v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
|
|
76
89
|
// `error_lesson_mode`. The setting is TEXT and must be compared with
|
|
@@ -79,10 +92,132 @@ export const KEVIN_CONFIG_KEYS = [
|
|
|
79
92
|
// at the surface rather than silently changing every installation's
|
|
80
93
|
// behaviour on the next reflection.
|
|
81
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";
|
|
82
97
|
function resolveMigrationsDir() {
|
|
83
98
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
84
99
|
return join(here, "..", "migrations");
|
|
85
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
|
+
}
|
|
86
221
|
export const KevinPlugin = async (input, options) => {
|
|
87
222
|
const opts = (options ?? {});
|
|
88
223
|
const dbPath = opts.dbPath ?? join(homedir(), ".opencode-kevin", "kevin.db");
|
|
@@ -98,8 +233,22 @@ export const KevinPlugin = async (input, options) => {
|
|
|
98
233
|
// v0.4.0 (K4-019): the plugin hooks expose no project field, so the
|
|
99
234
|
// project id is derived once from the plugin host's working directory
|
|
100
235
|
// (plan §5.7 fallback; D2-11 project scoping wired into the live path).
|
|
101
|
-
|
|
102
|
-
|
|
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);
|
|
103
252
|
// v0.7.0 (K7-009 / plan §5.1, D7-13) — the repository truth scanner reads
|
|
104
253
|
// the JSON project files. Runs once at init, gated by repo_truth_enabled.
|
|
105
254
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
@@ -151,7 +300,30 @@ export const KevinPlugin = async (input, options) => {
|
|
|
151
300
|
// ArtifactWriter is constructed here and only `kevin_approve` may call
|
|
152
301
|
// `apply()`. `kevin_propose` reaches the file only through `plan()`.
|
|
153
302
|
const writer = new ArtifactWriter(store, projectId, metrics);
|
|
154
|
-
|
|
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);
|
|
155
327
|
// v0.6.0 (K6-017/018 / plan §5.6-5.7, D6-13) — pull-channel bundles and
|
|
156
328
|
// the v2 domain probe. `probe()` runs ONCE at init and the result is
|
|
157
329
|
// held; probing per-event is a hot-path cost for a value that cannot
|
|
@@ -572,6 +744,35 @@ export const KevinPlugin = async (input, options) => {
|
|
|
572
744
|
catch {
|
|
573
745
|
v07 = undefined;
|
|
574
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
|
+
}
|
|
575
776
|
return {
|
|
576
777
|
title: "Estado de Kevin",
|
|
577
778
|
output: JSON.stringify({
|
|
@@ -580,8 +781,10 @@ export const KevinPlugin = async (input, options) => {
|
|
|
580
781
|
// (13 v0.5 + kevin_propose + kevin_approve +
|
|
581
782
|
// kevin_publish). The frozen ladder is verified
|
|
582
783
|
// monotone across releases; K6-024 extends this
|
|
583
|
-
// block with the remaining v0.6 fields.
|
|
584
|
-
|
|
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,
|
|
585
788
|
v07,
|
|
586
789
|
memories_reflector: memoriesReflector,
|
|
587
790
|
memories_agent: memoriesAgent,
|
|
@@ -614,6 +817,130 @@ export const KevinPlugin = async (input, options) => {
|
|
|
614
817
|
},
|
|
615
818
|
// v0.6.0 (K6-024) — omitted on pre-007 DBs.
|
|
616
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",
|
|
617
944
|
}),
|
|
618
945
|
};
|
|
619
946
|
},
|
|
@@ -629,7 +956,9 @@ export const KevinPlugin = async (input, options) => {
|
|
|
629
956
|
// three emission states ("unavailable" vs "off" vs "on").
|
|
630
957
|
// v0.7.0 (K7-019 / K7-006) — projectId scopes the `truth`
|
|
631
958
|
// block; on a pre-008 DB the block is omitted, partial:true.
|
|
632
|
-
|
|
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);
|
|
633
962
|
const payload = args.verbose
|
|
634
963
|
? report
|
|
635
964
|
: { ...report, settings: undefined };
|
|
@@ -782,9 +1111,13 @@ export const KevinPlugin = async (input, options) => {
|
|
|
782
1111
|
.describe("okf | markdown"),
|
|
783
1112
|
},
|
|
784
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.
|
|
785
1118
|
const output = args.format === "markdown"
|
|
786
|
-
? exportMarkdown(store)
|
|
787
|
-
: exportOkf(store);
|
|
1119
|
+
? exportMarkdown(store, projectId)
|
|
1120
|
+
: exportOkf(store, projectId);
|
|
788
1121
|
return {
|
|
789
1122
|
title: "Export completado",
|
|
790
1123
|
output,
|
|
@@ -976,6 +1309,103 @@ export const KevinPlugin = async (input, options) => {
|
|
|
976
1309
|
};
|
|
977
1310
|
},
|
|
978
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
|
+
}),
|
|
979
1409
|
},
|
|
980
1410
|
"tool.execute.before": async (hookInput, output) => {
|
|
981
1411
|
rememberToolCall(hookInput.callID, hookInput.tool, output.args);
|
|
@@ -1187,6 +1617,24 @@ export const KevinPlugin = async (input, options) => {
|
|
|
1187
1617
|
// best-effort, same pattern as ledger.settle — a
|
|
1188
1618
|
// curation failure must not break the idle path
|
|
1189
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
|
+
}
|
|
1190
1638
|
}
|
|
1191
1639
|
metrics.flush();
|
|
1192
1640
|
}
|