@jmtrin/opencode-kevin 0.8.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +559 -771
- package/dist/migrations/010_v09_native.sql +78 -0
- package/dist/migrations/011_v10_proven.sql +69 -0
- package/dist/plugin/Archiver.js +0 -1
- package/dist/plugin/ArtifactWriter.d.ts +25 -10
- package/dist/plugin/ArtifactWriter.js +43 -25
- package/dist/plugin/CausalChain.js +0 -1
- package/dist/plugin/ConflictDetector.js +0 -1
- package/dist/plugin/ContextInjector.js +0 -1
- package/dist/plugin/ConventionMiner.js +0 -1
- package/dist/plugin/Curator.js +0 -1
- package/dist/plugin/Feedback.js +0 -1
- package/dist/plugin/HookLiveness.d.ts +87 -0
- package/dist/plugin/HookLiveness.js +338 -0
- package/dist/plugin/InjectionLedger.js +0 -1
- package/dist/plugin/LessonFixer.js +0 -1
- package/dist/plugin/Materializer.d.ts +25 -0
- package/dist/plugin/Materializer.js +39 -11
- package/dist/plugin/MemoryService.js +0 -1
- package/dist/plugin/Migrate.d.ts +1 -0
- package/dist/plugin/Migrate.js +69 -6
- package/dist/plugin/PatternMiner.js +0 -1
- package/dist/plugin/QualityGate.js +0 -1
- package/dist/plugin/Reflector.js +0 -1
- package/dist/plugin/RepoIdentity.d.ts +20 -9
- package/dist/plugin/RepoIdentity.js +44 -10
- package/dist/plugin/RepoTruth.js +0 -1
- package/dist/plugin/Retrospective.js +16 -1
- package/dist/plugin/SharedLayer.js +0 -1
- package/dist/plugin/Store.js +0 -1
- package/dist/plugin/ToolCallObserver.js +0 -1
- package/dist/plugin/capabilities.js +0 -1
- package/dist/plugin/confidence.js +0 -1
- package/dist/plugin/contract.d.ts +46 -0
- package/dist/plugin/contract.js +368 -0
- package/dist/plugin/diff.js +0 -1
- package/dist/plugin/escape.d.ts +39 -0
- package/dist/plugin/escape.js +60 -0
- package/dist/plugin/fingerprint.js +0 -1
- package/dist/plugin/host.d.ts +70 -0
- package/dist/plugin/host.js +250 -0
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +542 -222
- package/dist/plugin/inferability.js +0 -1
- package/dist/plugin/kevin_approve.js +0 -1
- package/dist/plugin/kevin_audit.d.ts +60 -0
- package/dist/plugin/kevin_audit.js +144 -1
- package/dist/plugin/kevin_bench.d.ts +10 -0
- package/dist/plugin/kevin_bench.js +82 -0
- package/dist/plugin/kevin_conflicts.js +0 -1
- package/dist/plugin/kevin_contract.d.ts +8 -0
- package/dist/plugin/kevin_contract.js +59 -0
- package/dist/plugin/kevin_doctor.d.ts +62 -0
- package/dist/plugin/kevin_doctor.js +196 -0
- package/dist/plugin/kevin_facts.js +0 -1
- package/dist/plugin/kevin_native.d.ts +29 -0
- package/dist/plugin/kevin_native.js +79 -0
- package/dist/plugin/kevin_propose.js +0 -1
- package/dist/plugin/kevin_publish.js +0 -1
- package/dist/plugin/kevin_why.js +0 -1
- package/dist/plugin/memory-format.js +0 -1
- package/dist/plugin/metrics.js +0 -1
- package/dist/plugin/native.d.ts +92 -0
- package/dist/plugin/native.js +190 -0
- package/dist/plugin/okf-export.js +0 -1
- package/dist/plugin/okf-import.js +0 -1
- package/dist/plugin/okf.js +0 -1
- package/dist/plugin/perf.d.ts +35 -0
- package/dist/plugin/perf.js +174 -0
- package/dist/plugin/query-tokenizer.js +0 -1
- package/dist/plugin/redact.js +0 -1
- package/dist/plugin/replay-types.js +0 -1
- package/dist/plugin/replay.js +0 -1
- package/dist/plugin/sqlite-adapter.js +0 -1
- package/dist/plugin/uuid.js +0 -1
- package/package.json +22 -7
- package/dist/plugin/Archiver.js.map +0 -1
- package/dist/plugin/ArtifactWriter.js.map +0 -1
- package/dist/plugin/CausalChain.js.map +0 -1
- package/dist/plugin/ConflictDetector.js.map +0 -1
- package/dist/plugin/ContextInjector.js.map +0 -1
- package/dist/plugin/ConventionMiner.js.map +0 -1
- package/dist/plugin/Curator.js.map +0 -1
- package/dist/plugin/Feedback.js.map +0 -1
- package/dist/plugin/InjectionLedger.js.map +0 -1
- package/dist/plugin/LessonFixer.js.map +0 -1
- package/dist/plugin/Materializer.js.map +0 -1
- package/dist/plugin/MemoryService.js.map +0 -1
- package/dist/plugin/Migrate.js.map +0 -1
- package/dist/plugin/PatternMiner.js.map +0 -1
- package/dist/plugin/QualityGate.js.map +0 -1
- package/dist/plugin/Reflector.js.map +0 -1
- package/dist/plugin/RepoIdentity.js.map +0 -1
- package/dist/plugin/RepoTruth.js.map +0 -1
- package/dist/plugin/Retrospective.js.map +0 -1
- package/dist/plugin/SharedLayer.js.map +0 -1
- package/dist/plugin/Store.js.map +0 -1
- package/dist/plugin/ToolCallObserver.js.map +0 -1
- package/dist/plugin/capabilities.js.map +0 -1
- package/dist/plugin/confidence.js.map +0 -1
- package/dist/plugin/diff.js.map +0 -1
- package/dist/plugin/fingerprint.js.map +0 -1
- package/dist/plugin/index.js.map +0 -1
- package/dist/plugin/inferability.js.map +0 -1
- package/dist/plugin/kevin_approve.js.map +0 -1
- package/dist/plugin/kevin_audit.js.map +0 -1
- package/dist/plugin/kevin_conflicts.js.map +0 -1
- package/dist/plugin/kevin_facts.js.map +0 -1
- package/dist/plugin/kevin_propose.js.map +0 -1
- package/dist/plugin/kevin_publish.js.map +0 -1
- package/dist/plugin/kevin_why.js.map +0 -1
- package/dist/plugin/memory-format.js.map +0 -1
- package/dist/plugin/metrics.js.map +0 -1
- package/dist/plugin/okf-export.js.map +0 -1
- package/dist/plugin/okf-import.js.map +0 -1
- package/dist/plugin/okf.js.map +0 -1
- package/dist/plugin/query-tokenizer.js.map +0 -1
- package/dist/plugin/redact.js.map +0 -1
- package/dist/plugin/replay-types.js.map +0 -1
- package/dist/plugin/replay.js.map +0 -1
- package/dist/plugin/sqlite-adapter.js.map +0 -1
- package/dist/plugin/uuid.js.map +0 -1
- package/migrations/001_initial.sql +0 -92
- package/migrations/002_indexes.sql +0 -14
- package/migrations/003_v02_signal.sql +0 -58
- package/migrations/004_v03_knowledge.sql +0 -138
- package/migrations/005_v04_signal.sql +0 -57
- package/migrations/006_v05_glassbox.sql +0 -118
- package/migrations/007_v06_pull.sql +0 -145
- package/migrations/008_v07_truth.sql +0 -124
- package/migrations/009_v08_team.sql +0 -100
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Store } from "./Store.js";
|
|
2
|
+
import type { HostSurface } from "./host.js";
|
|
3
|
+
import type { SettingsReader } from "./native.js";
|
|
4
|
+
export type NativeAction = "show" | "enable" | "disable";
|
|
5
|
+
export interface NativeRegistrationRow {
|
|
6
|
+
readonly surface: "skill" | "reference";
|
|
7
|
+
readonly registered: boolean;
|
|
8
|
+
readonly verified: boolean;
|
|
9
|
+
readonly attached_at: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface NativeReport {
|
|
12
|
+
readonly action: NativeAction;
|
|
13
|
+
/** The setting value: '1' or '0' as stored (TEXT). */
|
|
14
|
+
readonly value: "1" | "0";
|
|
15
|
+
/** Whether registration would be effective on THIS host (v2 subpath present). */
|
|
16
|
+
readonly effective: boolean;
|
|
17
|
+
/** Present when `effective` is false: why the intent is inert. */
|
|
18
|
+
readonly reason?: string;
|
|
19
|
+
/** The latest persisted outcome per surface (show only). */
|
|
20
|
+
readonly registrations?: NativeRegistrationRow[];
|
|
21
|
+
/** The restart note (enable/disable only). */
|
|
22
|
+
readonly note?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface NativeDeps {
|
|
25
|
+
readonly host: HostSurface;
|
|
26
|
+
readonly store: Store;
|
|
27
|
+
readonly settings: SettingsReader;
|
|
28
|
+
}
|
|
29
|
+
export declare function handleNative(action: NativeAction, deps: NativeDeps): NativeReport;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// v0.9.0 (K9-019 / plan §5.5, D9-12) — `kevin_native`: inspect and toggle
|
|
2
|
+
// `native_registration_enabled`.
|
|
3
|
+
//
|
|
4
|
+
// Pure in the sense that matters: `show` reads only; `enable`/`disable`
|
|
5
|
+
// write `kevin_settings` and nothing else. Neither action re-attaches —
|
|
6
|
+
// the probe is frozen for the process lifetime (D9-12) — and the response
|
|
7
|
+
// says so explicitly, naming a restart as the requirement. `enable` on a
|
|
8
|
+
// host without the v2 subpath succeeds and reports the registration as
|
|
9
|
+
// inert: the setting is a statement of intent that becomes effective when
|
|
10
|
+
// the host catches up, and refusing would make it untestable on the
|
|
11
|
+
// majority of installations.
|
|
12
|
+
//
|
|
13
|
+
// The stored value is TEXT `'1'` or `'0'` — never a boolean, never
|
|
14
|
+
// `'true'` (kevin_settings.value is TEXT; compare with `=== "1"`).
|
|
15
|
+
function lastRows(store) {
|
|
16
|
+
try {
|
|
17
|
+
return store
|
|
18
|
+
.prepare(`SELECT surface, registered, verified, attached_at
|
|
19
|
+
FROM native_registrations
|
|
20
|
+
ORDER BY attached_at DESC, id DESC
|
|
21
|
+
LIMIT 20`)
|
|
22
|
+
.all()
|
|
23
|
+
.map((row) => {
|
|
24
|
+
const r = row;
|
|
25
|
+
return {
|
|
26
|
+
surface: r.surface,
|
|
27
|
+
registered: r.registered === 1,
|
|
28
|
+
verified: r.verified === 1,
|
|
29
|
+
attached_at: r.attached_at,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// pre-010 database: the table does not exist yet — no rows to
|
|
35
|
+
// report, and that is the truth.
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** v0.9.0 (K9-019 / plan §5.5, D9-12) — the pure decision: the probe is
|
|
40
|
+
* frozen for the process lifetime, so "effective" is derived from the
|
|
41
|
+
* resolved host surface, never from a fresh probe call. */
|
|
42
|
+
function effectiveOn(host) {
|
|
43
|
+
return host.v2.skill || host.v2.reference;
|
|
44
|
+
}
|
|
45
|
+
export function handleNative(action, deps) {
|
|
46
|
+
const current = deps.settings.getSetting("native_registration_enabled", "0");
|
|
47
|
+
const value = current === "1" ? "1" : "0";
|
|
48
|
+
if (action === "show") {
|
|
49
|
+
const report = {
|
|
50
|
+
action,
|
|
51
|
+
value,
|
|
52
|
+
effective: effectiveOn(deps.host),
|
|
53
|
+
registrations: lastRows(deps.store),
|
|
54
|
+
...(effectiveOn(deps.host)
|
|
55
|
+
? {}
|
|
56
|
+
: {
|
|
57
|
+
reason: "v2 subpath absent from the resolved host package — registration would be inert",
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
return report;
|
|
61
|
+
}
|
|
62
|
+
const next = action === "enable" ? "1" : "0";
|
|
63
|
+
deps.store
|
|
64
|
+
.prepare(`INSERT INTO kevin_settings (key, value) VALUES (?, ?)
|
|
65
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
|
|
66
|
+
.run("native_registration_enabled", next);
|
|
67
|
+
const report = {
|
|
68
|
+
action,
|
|
69
|
+
value: next,
|
|
70
|
+
effective: effectiveOn(deps.host),
|
|
71
|
+
note: "the probe is frozen for the process lifetime — restart the host for the change to take effect",
|
|
72
|
+
...(effectiveOn(deps.host)
|
|
73
|
+
? {}
|
|
74
|
+
: {
|
|
75
|
+
reason: "v2 subpath absent from the resolved host package — registration would be inert",
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
return report;
|
|
79
|
+
}
|
package/dist/plugin/kevin_why.js
CHANGED
package/dist/plugin/metrics.js
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type Materializer } from "./Materializer.js";
|
|
2
|
+
import type { Store } from "./Store.js";
|
|
3
|
+
import type { HostSurface } from "./host.js";
|
|
4
|
+
/** The surface `attachNative()` returned. `verified` is filled by the
|
|
5
|
+
* read-back checks (K9-014/015) once the host ran `setup()`. */
|
|
6
|
+
export interface NativeRegistration {
|
|
7
|
+
registered: {
|
|
8
|
+
skill: boolean;
|
|
9
|
+
reference: boolean;
|
|
10
|
+
};
|
|
11
|
+
verified: {
|
|
12
|
+
skill: boolean;
|
|
13
|
+
reference: boolean;
|
|
14
|
+
};
|
|
15
|
+
notes: string[];
|
|
16
|
+
}
|
|
17
|
+
/** Minimal settings reader — matches MemoryService.getSetting(key, fallback). */
|
|
18
|
+
export interface SettingsReader {
|
|
19
|
+
getSetting(key: string, fallback?: string): string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface NativeDeps {
|
|
22
|
+
materializer: Materializer;
|
|
23
|
+
settings: SettingsReader;
|
|
24
|
+
/**
|
|
25
|
+
* v0.9.0 (K9-014 / plan §5.4) — reports the outcome of each surface
|
|
26
|
+
* registration once the host ran `setup()`. `registered` reflects
|
|
27
|
+
* whether the transform completed; `verified` whether the read-back
|
|
28
|
+
* (`draft.list()`) contained the provided source. The host's own
|
|
29
|
+
* `setup()` runs asynchronously, so this callback may fire after
|
|
30
|
+
* `attachNative()` resolved.
|
|
31
|
+
*/
|
|
32
|
+
onVerified?: (surface: "skill" | "reference", registered: boolean, verified: boolean) => void;
|
|
33
|
+
/**
|
|
34
|
+
* v0.9.0 (K9-017 / plan §6.2) — the persistence sink. When present,
|
|
35
|
+
* every surface outcome is appended to `native_registrations` and the
|
|
36
|
+
* two live counters are bumped. Attach time is construction, not a
|
|
37
|
+
* hot path, so a direct write is correct here and the
|
|
38
|
+
* `metrics.flush()` cadence does not apply.
|
|
39
|
+
*/
|
|
40
|
+
store?: Store;
|
|
41
|
+
}
|
|
42
|
+
/** The only place in the repository allowed to name the v2 subpath. */
|
|
43
|
+
export declare const V2_SPECIFIER = "@opencode-ai/plugin/v2/promise";
|
|
44
|
+
/**
|
|
45
|
+
* Duck-typed mirror of the host's v2 PluginContext. The host's own
|
|
46
|
+
* compile-time types (dist/v2/promise/context.d.ts) are not importable
|
|
47
|
+
* here — a static type import would name the specifier outside a dynamic
|
|
48
|
+
* `import()` and break the containment scan.
|
|
49
|
+
*/
|
|
50
|
+
export interface KevinNativeContext {
|
|
51
|
+
readonly skill: {
|
|
52
|
+
transform(hook: (draft: unknown) => Promise<void> | void): Promise<void> | void;
|
|
53
|
+
};
|
|
54
|
+
readonly reference: {
|
|
55
|
+
transform(hook: (draft: unknown) => Promise<void> | void): Promise<void> | void;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Duck-typed mirror of the v2 `Plugin` contract
|
|
60
|
+
* (dist/v2/promise/plugin.d.ts). `define()` is the identity function —
|
|
61
|
+
* 54 bytes, `return plugin` — so adopting it is neither a framework nor
|
|
62
|
+
* a commitment (D9-02); the object literal below already satisfies the
|
|
63
|
+
* contract.
|
|
64
|
+
*/
|
|
65
|
+
export interface KevinNativePlugin {
|
|
66
|
+
readonly id: string;
|
|
67
|
+
readonly setup: (context: KevinNativeContext) => Promise<void> | void;
|
|
68
|
+
}
|
|
69
|
+
/** The skill body Kevin registers through `skill.transform`. */
|
|
70
|
+
export declare function kevinSkillSource(materializer: Materializer): string;
|
|
71
|
+
/**
|
|
72
|
+
* Build the v2 plugin. Registration replaces emission (D9-10): when the
|
|
73
|
+
* host runs `setup()`, the skill/reference surfaces are registered
|
|
74
|
+
* natively and the Materializer's `*_emission_enabled` file path is
|
|
75
|
+
* skipped for those surfaces (guard in K9-016).
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildNativePlugin(deps: NativeDeps): KevinNativePlugin;
|
|
78
|
+
export interface AttachOptions {
|
|
79
|
+
/** Injectable v2 loader for tests; defaults to the real dynamic import. */
|
|
80
|
+
importV2?: () => Promise<unknown>;
|
|
81
|
+
/** Collects notes for the caller when the result is null. */
|
|
82
|
+
notes?: string[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Attach the native surface when the host exposes it AND the user asked
|
|
86
|
+
* for it. Returns `null` — cleanly, with a `note` — whenever the host
|
|
87
|
+
* lacks the subpath or `native_registration_enabled` is not `'1'`; that
|
|
88
|
+
* is the default on every existing installation, so the default
|
|
89
|
+
* behaviour of this release is byte-identical to v0.8.0. Never throws
|
|
90
|
+
* (D9-12): every failure path is a `null` plus a `note`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function attachNative(host: HostSurface, deps: NativeDeps, options?: AttachOptions): Promise<NativeRegistration | null>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { SKILL_TOPIC } from "./Materializer.js";
|
|
2
|
+
import { uuidv7 } from "./uuid.js";
|
|
3
|
+
/** The only place in the repository allowed to name the v2 subpath. */
|
|
4
|
+
export const V2_SPECIFIER = "@opencode-ai/plugin/v2/promise";
|
|
5
|
+
/** The skill body Kevin registers through `skill.transform`. */
|
|
6
|
+
export function kevinSkillSource(materializer) {
|
|
7
|
+
return materializer.skillBody();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Build the v2 plugin. Registration replaces emission (D9-10): when the
|
|
11
|
+
* host runs `setup()`, the skill/reference surfaces are registered
|
|
12
|
+
* natively and the Materializer's `*_emission_enabled` file path is
|
|
13
|
+
* skipped for those surfaces (guard in K9-016).
|
|
14
|
+
*/
|
|
15
|
+
export function buildNativePlugin(deps) {
|
|
16
|
+
return {
|
|
17
|
+
id: "opencode-kevin",
|
|
18
|
+
setup: async (ctx) => {
|
|
19
|
+
try {
|
|
20
|
+
await ctx.skill.transform(async (draft) => {
|
|
21
|
+
const source = kevinSkillSource(deps.materializer);
|
|
22
|
+
// The draft is a mutable builder valid only for the
|
|
23
|
+
// duration of this callback; it never escapes (D9-10
|
|
24
|
+
// property 3). K9-014's source scan asserts that.
|
|
25
|
+
const draftApi = draft;
|
|
26
|
+
if (typeof draftApi.source !== "function") {
|
|
27
|
+
deps.onVerified?.("skill", false, false);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
draftApi.source(source);
|
|
31
|
+
// v0.9.0 (K9-014 / plan §5.4 property 2) — verification is a
|
|
32
|
+
// read-back: the registered source must show up in the
|
|
33
|
+
// draft's own list(). An unverified registration is a note
|
|
34
|
+
// (and a metric in K9-017), never a throw.
|
|
35
|
+
const verified = typeof draftApi.list === "function" &&
|
|
36
|
+
draftApi.list().some((entry) => entry === source);
|
|
37
|
+
deps.onVerified?.("skill", true, verified);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// A rejecting transform still reports, never throws (K9-014).
|
|
42
|
+
deps.onVerified?.("skill", false, false);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await ctx.reference.transform(async (draft) => {
|
|
46
|
+
// v0.9.0 (K9-015 / plan §5.4, D6-14) — one add() per
|
|
47
|
+
// materialized ref target (topic !== SKILL_TOPIC), named
|
|
48
|
+
// `@kevin/<topic>`. Topics follow the v0.6.0 rule
|
|
49
|
+
// `<type>-<dominant token>`; dominantToken already
|
|
50
|
+
// excludes hex-like tokens, so a fingerprint prefix can
|
|
51
|
+
// never become a reference name. Sources use the v2
|
|
52
|
+
// `{ type: "local", path }` shape (read from the
|
|
53
|
+
// resolved SDK's ReferenceLocalSource, duck-typed).
|
|
54
|
+
const draftApi = draft;
|
|
55
|
+
if (typeof draftApi.add !== "function") {
|
|
56
|
+
deps.onVerified?.("reference", false, false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const added = [];
|
|
60
|
+
for (const target of deps.materializer.bundleTargets()) {
|
|
61
|
+
if (target.topic === SKILL_TOPIC)
|
|
62
|
+
continue;
|
|
63
|
+
const name = `@kevin/${target.topic}`;
|
|
64
|
+
draftApi.add(name, {
|
|
65
|
+
type: "local",
|
|
66
|
+
path: target.path,
|
|
67
|
+
});
|
|
68
|
+
added.push(name);
|
|
69
|
+
}
|
|
70
|
+
// Verification is the same read-back as K9-014: every
|
|
71
|
+
// name we added must show up in the draft's own list().
|
|
72
|
+
const listed = typeof draftApi.list === "function" ? draftApi.list() : [];
|
|
73
|
+
const listedNames = new Set(listed.map((entry) => {
|
|
74
|
+
const tuple = entry;
|
|
75
|
+
return tuple[0];
|
|
76
|
+
}));
|
|
77
|
+
const verified = added.every((name) => listedNames.has(name));
|
|
78
|
+
deps.onVerified?.("reference", true, verified);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
deps.onVerified?.("reference", false, false);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Attach the native surface when the host exposes it AND the user asked
|
|
89
|
+
* for it. Returns `null` — cleanly, with a `note` — whenever the host
|
|
90
|
+
* lacks the subpath or `native_registration_enabled` is not `'1'`; that
|
|
91
|
+
* is the default on every existing installation, so the default
|
|
92
|
+
* behaviour of this release is byte-identical to v0.8.0. Never throws
|
|
93
|
+
* (D9-12): every failure path is a `null` plus a `note`.
|
|
94
|
+
*/
|
|
95
|
+
export async function attachNative(host, deps, options = {}) {
|
|
96
|
+
const notes = options.notes ?? [];
|
|
97
|
+
const enabled = deps.settings.getSetting("native_registration_enabled", "0") === "1";
|
|
98
|
+
if (!enabled)
|
|
99
|
+
return null;
|
|
100
|
+
if (!host.v2.skill && !host.v2.reference) {
|
|
101
|
+
notes.push("v2 subpath absent from the resolved host package — native registration skipped");
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
let mod;
|
|
105
|
+
try {
|
|
106
|
+
mod = await (options.importV2 ?? (() => import(/* @vite-ignore */ V2_SPECIFIER)))();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
notes.push("v2 subpath import rejected — native registration skipped");
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const record = mod;
|
|
113
|
+
const define = typeof record.define === "function"
|
|
114
|
+
? record.define
|
|
115
|
+
: null;
|
|
116
|
+
if (define === null) {
|
|
117
|
+
notes.push("v2 module exposes no define() — native registration skipped");
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
// `define()` is the identity function (D9-02): passing the built
|
|
121
|
+
// plugin through it validates the contract without adding a framework.
|
|
122
|
+
// v0.9.0 (K9-014 / plan §5.4 property 2) — the collector for the
|
|
123
|
+
// read-back results: `verified` is filled by the host running setup()
|
|
124
|
+
// (draft.list()), reported back through onVerified. The setup callback
|
|
125
|
+
// may fire after this function resolved; the caller keeps the returned
|
|
126
|
+
// registration as the final word.
|
|
127
|
+
const registered = { skill: host.v2.skill, reference: host.v2.reference };
|
|
128
|
+
const verified = { skill: false, reference: false };
|
|
129
|
+
const plugin = buildNativePlugin({
|
|
130
|
+
...deps,
|
|
131
|
+
onVerified: (surface, didRegister, isVerified) => {
|
|
132
|
+
if (!didRegister)
|
|
133
|
+
registered[surface] = false;
|
|
134
|
+
verified[surface] = isVerified;
|
|
135
|
+
if (!isVerified) {
|
|
136
|
+
notes.push(`${surface} read-back does not contain the provided source — unverified registration`);
|
|
137
|
+
}
|
|
138
|
+
// v0.9.0 (K9-017 / plan §6.2) — append the outcome row at attach
|
|
139
|
+
// time (construction, not a hot path; a direct write is correct,
|
|
140
|
+
// the flush cadence does not apply). Only surfaces the host
|
|
141
|
+
// actually exposes are persisted: a surface the host cannot
|
|
142
|
+
// serve is not an outcome to record.
|
|
143
|
+
if (deps.store && host.v2[surface]) {
|
|
144
|
+
const note = !isVerified
|
|
145
|
+
? `${surface} read-back does not contain the provided source — unverified registration`
|
|
146
|
+
: null;
|
|
147
|
+
persistRegistration(deps.store, surface, didRegister, isVerified, note);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
define(plugin);
|
|
152
|
+
return {
|
|
153
|
+
registered,
|
|
154
|
+
verified,
|
|
155
|
+
notes,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* v0.9.0 (K9-017 / plan §6.2) — one `native_registrations` row per
|
|
160
|
+
* surface per attach attempt, plus the two live counters. The counters
|
|
161
|
+
* live OUTSIDE the frozen `METRIC_KEYS` ladder (K7-004) but persist to
|
|
162
|
+
* the same `kevin_metrics` table, following the v0.6.0 `incrRegistered`
|
|
163
|
+
* precedent (K6-018/019): written immediately, never debounced. The
|
|
164
|
+
* `surface` CHECK lives in the schema (migration 010), so a bad surface
|
|
165
|
+
* fails loudly at the constraint rather than being coerced — the closed
|
|
166
|
+
* enumeration is deliberate (plan §6.2).
|
|
167
|
+
*/
|
|
168
|
+
function persistRegistration(store, surface, registered, verified, note) {
|
|
169
|
+
store
|
|
170
|
+
.prepare(`INSERT INTO native_registrations (id, surface, registered, verified, note)
|
|
171
|
+
VALUES (?, ?, ?, ?, ?)`)
|
|
172
|
+
.run(uuidv7(), surface, registered ? 1 : 0, verified ? 1 : 0, note);
|
|
173
|
+
if (verified) {
|
|
174
|
+
bumpNativeCounter(store, "native_registrations_total");
|
|
175
|
+
}
|
|
176
|
+
else if (registered) {
|
|
177
|
+
// "Registered but unverified": the host accepted the call and did
|
|
178
|
+
// not honour it — the interesting state (K9-017).
|
|
179
|
+
bumpNativeCounter(store, "native_registration_failures");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function bumpNativeCounter(store, key) {
|
|
183
|
+
store
|
|
184
|
+
.prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
|
|
185
|
+
VALUES (?, 1, datetime('now'))
|
|
186
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
187
|
+
value = value + excluded.value,
|
|
188
|
+
updated_at = datetime('now')`)
|
|
189
|
+
.run(key);
|
|
190
|
+
}
|
package/dist/plugin/okf.js
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Store } from "./Store.js";
|
|
2
|
+
export type PerfScope = "tool.execute.before" | "tool.execute.after" | "chat.message" | "chat.system.transform" | "session.compacting" | "event" | "session.idle" | "dispose";
|
|
3
|
+
export interface Budget {
|
|
4
|
+
readonly scope: PerfScope;
|
|
5
|
+
readonly p95Ms: number;
|
|
6
|
+
readonly maxMs: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const BUDGETS: readonly Budget[];
|
|
9
|
+
export interface PerfStat {
|
|
10
|
+
readonly scope: PerfScope;
|
|
11
|
+
readonly count: number;
|
|
12
|
+
readonly p50: number;
|
|
13
|
+
readonly p95: number;
|
|
14
|
+
readonly max: number;
|
|
15
|
+
readonly budget: Budget;
|
|
16
|
+
readonly withinBudget: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare class Perf {
|
|
19
|
+
private readonly enabled;
|
|
20
|
+
private readonly capacity;
|
|
21
|
+
private readonly rings;
|
|
22
|
+
private readonly cursors;
|
|
23
|
+
private readonly counts;
|
|
24
|
+
constructor(opts: {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
capacity?: number | string;
|
|
27
|
+
});
|
|
28
|
+
static fromSettings(settings: Record<string, string | null | undefined>): Perf;
|
|
29
|
+
measure<T>(scope: PerfScope, fn: () => T): T;
|
|
30
|
+
measureAsync<T>(scope: PerfScope, fn: () => Promise<T>): Promise<T>;
|
|
31
|
+
private record;
|
|
32
|
+
stats(): readonly PerfStat[];
|
|
33
|
+
flush(store: Store): void;
|
|
34
|
+
reset(): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export const BUDGETS = [
|
|
2
|
+
{ scope: "tool.execute.before", p95Ms: 2, maxMs: 10 },
|
|
3
|
+
{ scope: "tool.execute.after", p95Ms: 5, maxMs: 25 },
|
|
4
|
+
{ scope: "chat.message", p95Ms: 2, maxMs: 10 },
|
|
5
|
+
{ scope: "chat.system.transform", p95Ms: 15, maxMs: 50 },
|
|
6
|
+
{ scope: "session.compacting", p95Ms: 15, maxMs: 50 },
|
|
7
|
+
{ scope: "event", p95Ms: 5, maxMs: 25 },
|
|
8
|
+
{ scope: "session.idle", p95Ms: 150, maxMs: 600 },
|
|
9
|
+
{ scope: "dispose", p95Ms: 50, maxMs: 250 },
|
|
10
|
+
];
|
|
11
|
+
function clampCapacity(raw) {
|
|
12
|
+
if (raw === null || raw === undefined || raw === "")
|
|
13
|
+
return 512;
|
|
14
|
+
const n = Number.parseInt(raw, 10);
|
|
15
|
+
if (Number.isNaN(n))
|
|
16
|
+
return 512;
|
|
17
|
+
return Math.min(8192, Math.max(64, n));
|
|
18
|
+
}
|
|
19
|
+
export class Perf {
|
|
20
|
+
enabled;
|
|
21
|
+
capacity;
|
|
22
|
+
rings;
|
|
23
|
+
cursors;
|
|
24
|
+
counts;
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
this.enabled = opts.enabled;
|
|
27
|
+
if (typeof opts.capacity === "string") {
|
|
28
|
+
this.capacity = clampCapacity(opts.capacity);
|
|
29
|
+
}
|
|
30
|
+
else if (typeof opts.capacity === "number") {
|
|
31
|
+
this.capacity = Math.min(8192, Math.max(64, Math.floor(opts.capacity)));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
this.capacity = 512;
|
|
35
|
+
}
|
|
36
|
+
this.rings = new Map();
|
|
37
|
+
this.cursors = new Map();
|
|
38
|
+
this.counts = new Map();
|
|
39
|
+
const scopes = BUDGETS.map((b) => b.scope);
|
|
40
|
+
for (const s of scopes) {
|
|
41
|
+
this.rings.set(s, new Float64Array(this.capacity));
|
|
42
|
+
this.cursors.set(s, 0);
|
|
43
|
+
this.counts.set(s, 0);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
static fromSettings(settings) {
|
|
47
|
+
const enabled = settings.perf_enabled === "1";
|
|
48
|
+
const cap = clampCapacity(settings.perf_ring_capacity);
|
|
49
|
+
return new Perf({ enabled, capacity: cap });
|
|
50
|
+
}
|
|
51
|
+
measure(scope, fn) {
|
|
52
|
+
if (!this.enabled)
|
|
53
|
+
return fn();
|
|
54
|
+
const start = performance.now();
|
|
55
|
+
try {
|
|
56
|
+
return fn();
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
const dur = performance.now() - start;
|
|
60
|
+
this.record(scope, dur);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async measureAsync(scope, fn) {
|
|
64
|
+
if (!this.enabled)
|
|
65
|
+
return fn();
|
|
66
|
+
const start = performance.now();
|
|
67
|
+
try {
|
|
68
|
+
return await fn();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
const dur = performance.now() - start;
|
|
72
|
+
this.record(scope, dur);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
record(scope, ms) {
|
|
76
|
+
const ring = this.rings.get(scope);
|
|
77
|
+
if (!ring)
|
|
78
|
+
return;
|
|
79
|
+
const cur = this.cursors.get(scope) ?? 0;
|
|
80
|
+
ring[cur % this.capacity] = ms;
|
|
81
|
+
this.cursors.set(scope, (cur + 1) % this.capacity);
|
|
82
|
+
this.counts.set(scope, (this.counts.get(scope) ?? 0) + 1);
|
|
83
|
+
}
|
|
84
|
+
stats() {
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const b of BUDGETS) {
|
|
87
|
+
const ring = this.rings.get(b.scope);
|
|
88
|
+
if (!ring)
|
|
89
|
+
continue;
|
|
90
|
+
const count = this.counts.get(b.scope) ?? 0;
|
|
91
|
+
if (count === 0) {
|
|
92
|
+
out.push({
|
|
93
|
+
scope: b.scope,
|
|
94
|
+
count: 0,
|
|
95
|
+
p50: 0,
|
|
96
|
+
p95: 0,
|
|
97
|
+
max: 0,
|
|
98
|
+
budget: b,
|
|
99
|
+
withinBudget: true,
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const n = Math.min(count, this.capacity);
|
|
104
|
+
// Copy live samples: need to reconstruct in insertion order but for percentiles order doesn't matter.
|
|
105
|
+
// Take last n entries from ring (wrap-aware).
|
|
106
|
+
const samples = [];
|
|
107
|
+
const cur = this.cursors.get(b.scope) ?? 0;
|
|
108
|
+
// If count < capacity, samples are ring[0..count-1]; else ring[0..capacity-1] all valid but order is cur..cur+cap
|
|
109
|
+
if (count < this.capacity) {
|
|
110
|
+
for (let i = 0; i < count; i++)
|
|
111
|
+
samples.push(ring[i]);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
for (let i = 0; i < this.capacity; i++)
|
|
115
|
+
samples.push(ring[i]);
|
|
116
|
+
}
|
|
117
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
118
|
+
// Percentile method: nearest-rank — p is the value at index ceil(p/100 * n) - 1
|
|
119
|
+
// of the ascending-sorted copy. Stated here so an implementer comparing against
|
|
120
|
+
// another percentile definition can see which one was chosen.
|
|
121
|
+
const p50 = sorted[Math.max(0, Math.ceil(sorted.length * 0.5) - 1)];
|
|
122
|
+
const p95 = sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)];
|
|
123
|
+
const max = sorted[sorted.length - 1];
|
|
124
|
+
out.push({
|
|
125
|
+
scope: b.scope,
|
|
126
|
+
count,
|
|
127
|
+
p50,
|
|
128
|
+
p95,
|
|
129
|
+
max,
|
|
130
|
+
budget: b,
|
|
131
|
+
withinBudget: p95 <= b.p95Ms,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
flush(store) {
|
|
137
|
+
if (!this.enabled)
|
|
138
|
+
return;
|
|
139
|
+
const stats = this.stats();
|
|
140
|
+
let recorded = 0;
|
|
141
|
+
let breaches = 0;
|
|
142
|
+
for (const s of stats) {
|
|
143
|
+
if (s.count === 0)
|
|
144
|
+
continue;
|
|
145
|
+
store
|
|
146
|
+
.prepare("INSERT INTO perf_samples (scope, sample_count, p50_ms, p95_ms, max_ms, budget_p95_ms, within_budget) VALUES (?, ?, ?, ?, ?, ?, ?)")
|
|
147
|
+
.run(s.scope, s.count, s.p50, s.p95, s.max, s.budget.p95Ms, s.withinBudget ? 1 : 0);
|
|
148
|
+
recorded++;
|
|
149
|
+
if (!s.withinBudget)
|
|
150
|
+
breaches++;
|
|
151
|
+
}
|
|
152
|
+
if (recorded > 0) {
|
|
153
|
+
store
|
|
154
|
+
.prepare(`UPDATE kevin_metrics SET value = value + ? WHERE key = 'perf_samples_recorded'`)
|
|
155
|
+
.run(recorded);
|
|
156
|
+
}
|
|
157
|
+
if (breaches > 0) {
|
|
158
|
+
store
|
|
159
|
+
.prepare(`UPDATE kevin_metrics SET value = value + ? WHERE key = 'perf_budget_breaches'`)
|
|
160
|
+
.run(breaches);
|
|
161
|
+
}
|
|
162
|
+
this.reset();
|
|
163
|
+
}
|
|
164
|
+
reset() {
|
|
165
|
+
for (const b of BUDGETS) {
|
|
166
|
+
// Zero the ring too: cursors and counts alone would let the next
|
|
167
|
+
// period's stats() read stale pre-reset samples whenever
|
|
168
|
+
// count < capacity (review fix, v1.0.0).
|
|
169
|
+
this.rings.get(b.scope)?.fill(0);
|
|
170
|
+
this.cursors.set(b.scope, 0);
|
|
171
|
+
this.counts.set(b.scope, 0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
package/dist/plugin/redact.js
CHANGED
package/dist/plugin/replay.js
CHANGED