@jmtrin/opencode-kevin 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -10
- package/dist/migrations/010_v09_native.sql +78 -0
- package/dist/plugin/HookLiveness.d.ts +81 -0
- package/dist/plugin/HookLiveness.js +324 -0
- package/dist/plugin/HookLiveness.js.map +1 -0
- package/dist/plugin/Materializer.d.ts +25 -0
- package/dist/plugin/Materializer.js +39 -10
- package/dist/plugin/Materializer.js.map +1 -1
- package/dist/plugin/Migrate.d.ts +1 -0
- package/dist/plugin/Migrate.js +46 -5
- package/dist/plugin/Migrate.js.map +1 -1
- package/dist/plugin/RepoIdentity.d.ts +20 -9
- package/dist/plugin/RepoIdentity.js +44 -9
- package/dist/plugin/RepoIdentity.js.map +1 -1
- package/dist/plugin/Retrospective.js +9 -0
- package/dist/plugin/Retrospective.js.map +1 -1
- package/dist/plugin/host.d.ts +70 -0
- package/dist/plugin/host.js +251 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +129 -7
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/kevin_audit.d.ts +29 -0
- package/dist/plugin/kevin_audit.js +84 -0
- package/dist/plugin/kevin_audit.js.map +1 -1
- package/dist/plugin/kevin_doctor.d.ts +57 -0
- package/dist/plugin/kevin_doctor.js +168 -0
- package/dist/plugin/kevin_doctor.js.map +1 -0
- package/dist/plugin/kevin_native.d.ts +29 -0
- package/dist/plugin/kevin_native.js +80 -0
- package/dist/plugin/kevin_native.js.map +1 -0
- package/dist/plugin/native.d.ts +92 -0
- package/dist/plugin/native.js +191 -0
- package/dist/plugin/native.js.map +1 -0
- package/migrations/010_v09_native.sql +78 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ For a **single project**, put the same `plugin` array in `./opencode.json` or `.
|
|
|
51
51
|
|
|
52
52
|
### 2. Restart OpenCode
|
|
53
53
|
|
|
54
|
-
Config is loaded once at startup and is **not hot-reloaded** — quit and reopen OpenCode after editing. On start, Kevin exposes
|
|
54
|
+
Config is loaded once at startup and is **not hot-reloaded** — quit and reopen OpenCode after editing. On start, Kevin exposes 23 tools, including `kevin_facts`, `kevin_conflicts`, `kevin_project`, `kevin_share`, `kevin_sync`, `kevin_doctor` and `kevin_native`.
|
|
55
55
|
|
|
56
56
|
Contradictions de-rank memories and surface conflicts. They never delete, stale, archive, or auto-resolve a memory.
|
|
57
57
|
|
|
@@ -156,7 +156,7 @@ Since v0.8.0, memories live in one of two layers: **local** (private to this ins
|
|
|
156
156
|
|
|
157
157
|
## Tools
|
|
158
158
|
|
|
159
|
-
Kevin exposes
|
|
159
|
+
Kevin exposes 23 tools callable by the agent.
|
|
160
160
|
|
|
161
161
|
### `kevin_save`
|
|
162
162
|
|
|
@@ -210,8 +210,8 @@ Global counts and metrics: memory census, the precision block, the six blocked-g
|
|
|
210
210
|
```
|
|
211
211
|
kevin_status({})
|
|
212
212
|
// → { "memories": 42, "memories_reflector": 12, "memories_agent": 30,
|
|
213
|
-
// "memories_pattern": 0, "memories_causal": 1, "tool_calls": 318,
|
|
214
|
-
// "retrospectives": 7, "tool_count":
|
|
213
|
+
// "memories_pattern": 0, "memories_causal": 1, "tool_calls": 318,
|
|
214
|
+
// "retrospectives": 7, "tool_count": 23,
|
|
215
215
|
// "metrics": { "tokens_injected_pre_prompt": 51, "tokens_injected_compacting": 0,
|
|
216
216
|
// "reflections_throttled": 3, "duplicate_suppressions": 2,
|
|
217
217
|
// "tool_calls_deduped": 0, "patterns_mined": 0,
|
|
@@ -384,11 +384,45 @@ Ingests the OKF file (asserts → shared-layer projections; tombstones → archi
|
|
|
384
384
|
```
|
|
385
385
|
kevin_sync({})
|
|
386
386
|
// → { "path": ".kevin/knowledge.okf", "fileHash": "3d2f...", "parsed": 1,
|
|
387
|
-
// "folded": 0, "rejected": 0, "imported": 1, "tombstoned": 0,
|
|
388
|
-
// "skipped": false }
|
|
387
|
+
// "folded": 0, "rejected": 0, "imported": 1, "tombstoned": 0,
|
|
388
|
+
// "skipped": false }
|
|
389
389
|
```
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
### `kevin_doctor`
|
|
392
|
+
|
|
393
|
+
Read-only health check — no writes, no probe re-run, no model call. Returns `host`, `hooks`, `dependencies`, `native`, `verdict` and `reason`; `hooks` is sorted dead first so the failure is the first thing on screen. Output contains no filesystem paths or session ids — paste it into an issue report.
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
kevin_doctor({})
|
|
397
|
+
// → { "host": { "plugin_version": "1.18.18", "flavour": "v1+v2", "shell_available": true,
|
|
398
|
+
// "v2": { "skill": true, "reference": true } },
|
|
399
|
+
// "hooks": [{ "hook": "experimental.chat.system.transform", "state": "dead",
|
|
400
|
+
// "fire_count": 0, "expected_count": 3, "since": "2026-08-20T10:00:00Z" },
|
|
401
|
+
// { "hook": "tool.execute.after", "state": "live", "fire_count": 42, "expected_count": 42 }],
|
|
402
|
+
// "dependencies": { "declared": ["@opencode-ai/plugin"], "zod_copies": 1 },
|
|
403
|
+
// "native": { "enabled": true, "registered": { "skill": true, "reference": true },
|
|
404
|
+
// "verified": { "skill": true, "reference": true } },
|
|
405
|
+
// "verdict": "degraded", "reason": "experimental.chat.system.transform dead since 2026-08-20T10:00:00Z; 1 affected hook(s)", "partial": false }
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
One dead hook is enough for `degraded`; every hook `live` is `healthy`; otherwise `unknown` — `unknown` is never rounded to `healthy` (D9-09). With no sessions yet, `verdict` is `unknown`.
|
|
409
|
+
|
|
410
|
+
### `kevin_native`
|
|
411
|
+
|
|
412
|
+
Controls the additive v2 attachment (D9-01): `show` reports the setting, the frozen probe result and the latest `native_registrations` rows; `enable`/`disable` write `kevin_settings` only and never re-probe — a restart is required for the change to take effect. The value is TEXT `"1"`/`"0"`; `enable` on a host without the v2 subpath succeeds and reports `effective: false` (inert) rather than refusing.
|
|
413
|
+
|
|
414
|
+
```
|
|
415
|
+
kevin_native({ action: "show" })
|
|
416
|
+
// → { "action": "show", "value": "0", "effective": true,
|
|
417
|
+
// "registrations": [{ "surface": "skill", "registered": true, "verified": true, "attached_at": "2026-08-20T10:00:00Z" }] }
|
|
418
|
+
|
|
419
|
+
kevin_native({ action: "enable" })
|
|
420
|
+
// → { "action": "enable", "value": "1", "effective": false,
|
|
421
|
+
// "reason": "v2 subpath absent from the resolved host package — registration would be inert",
|
|
422
|
+
// "note": "the probe is frozen for the process lifetime — restart the host for the change to take effect" }
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
392
426
|
|
|
393
427
|
## How Kevin measures itself
|
|
394
428
|
|
|
@@ -645,7 +679,7 @@ KevinPlugin(input, {
|
|
|
645
679
|
|
|
646
680
|
### Settings
|
|
647
681
|
|
|
648
|
-
Read/write via `kevin_config({ action: "list" | "set", ... })`. All
|
|
682
|
+
Read/write via `kevin_config({ action: "list" | "set", ... })`. All 27 values are TEXT; booleans compare against `"1"`.
|
|
649
683
|
|
|
650
684
|
| Setting | Default | Effect |
|
|
651
685
|
|---|---|---|
|
|
@@ -672,8 +706,12 @@ Read/write via `kevin_config({ action: "list" | "set", ... })`. All 23 values ar
|
|
|
672
706
|
| `share_requires_approval` | `"1"` | When `"1"`, `kevin_share` writes only with `confirm: true`; un-curated memories are refused with `not_curated` |
|
|
673
707
|
| `author_identity_mode` | `"hashed"` | Author attribution in exports: `"hashed"` or `"none"` (**string** enum) |
|
|
674
708
|
| `shared_confidence_floor` | `"0.7"` | Gate for `kevin_share` selection and export — a **string**, read with `Number.parseFloat` and clamped to `[0, 1]`. Deliberately stricter than `injection_confidence_floor` (`0.6`): sharing is a commitment, injecting is a suggestion. `parseInt` on this setting would yield `0` and share everything |
|
|
709
|
+
| `hook_liveness_enabled` | `"1"` | Master switch for the liveness instrument. When `"0"`, `HookLiveness.wrap()` returns hooks untouched and no counters are recorded — compare with `=== "1"` |
|
|
710
|
+
| `native_registration_enabled` | `"0"` | Opt-in for the additive v2 attachment (`skill.transform`/`reference.transform` via `define()`). Default `"0"` keeps the release byte-identical to v0.8.0; `kevin_native enable` sets `"1"` (TEXT) but the probe is frozen — restart required (D9-01) |
|
|
711
|
+
| `host_probe_history_enabled` | `"0"` | When `"1"`, one `host_probes` row per construction is appended (version, flavour, v2 flags, notes). Off by default because it is unbounded append-only |
|
|
712
|
+
| `dead_hook_report_threshold` | `"3"` | How many checkpointed sessions without a hook firing before it is `dead`. TEXT holding a number, read with `Number.parseInt(v, 10)`, clamped to `[1, 1000]`, `NaN` → `3` |
|
|
675
713
|
|
|
676
|
-
All
|
|
714
|
+
All 27 settings are TEXT values; booleans and flags compare against `"1"`; the three v0.8 **string-valued** settings are `okf_path`, `author_identity_mode` and `shared_confidence_floor`, and `dead_hook_report_threshold` is a numeric string.
|
|
677
715
|
|
|
678
716
|
---
|
|
679
717
|
|
|
@@ -686,10 +724,25 @@ npm install
|
|
|
686
724
|
npm run typecheck # tsc --noEmit (strict)
|
|
687
725
|
npm run lint # biome check .
|
|
688
726
|
npm test # vitest run (unit + integration + e2e + replay)
|
|
689
|
-
npm run verify # post-install verification
|
|
727
|
+
npm run verify # post-install verification (also checks DB migrations)
|
|
690
728
|
npm run replay # replay report over tests/replay/fixtures
|
|
691
729
|
```
|
|
692
730
|
|
|
731
|
+
### Quick diagnostic
|
|
732
|
+
|
|
733
|
+
```bash
|
|
734
|
+
# One read-only command that tells you whether Kevin is healthy:
|
|
735
|
+
# host surface, every hook's state, dependency check, and native registration.
|
|
736
|
+
kevin_doctor({})
|
|
737
|
+
# → { host: { flavour: "v1+v2", v2: { skill: true, reference: true } },
|
|
738
|
+
# hooks: [{ hook: "experimental.chat.system.transform", state: "dead", ... }],
|
|
739
|
+
# verdict: "degraded", reason: "experimental.chat.system.transform dead since ..." }
|
|
740
|
+
|
|
741
|
+
# Native registration is opt-in (default off). Enable, restart, check again:
|
|
742
|
+
kevin_native({ action: "enable" }) # writes native_registration_enabled="1" (TEXT), no re-probe
|
|
743
|
+
kevin_native({ action: "show" }) # → { effective: false, reason: "v2 subpath absent …" } on 1.17.x
|
|
744
|
+
```
|
|
745
|
+
|
|
693
746
|
### Publishing (maintainer)
|
|
694
747
|
|
|
695
748
|
```bash
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- Kevin v0.9.0 "Native" — host surface and hook liveness
|
|
3
|
+
-- Migration 010. Additive only. No table rebuild.
|
|
4
|
+
-- ============================================================
|
|
5
|
+
|
|
6
|
+
-- 1. Hook liveness. Deliberately NOT project-scoped: a hook is a
|
|
7
|
+
-- property of the host binary, not of a checkout. One row per
|
|
8
|
+
-- hook name, updated in place. See D9-08.
|
|
9
|
+
CREATE TABLE IF NOT EXISTS hook_liveness (
|
|
10
|
+
hook TEXT PRIMARY KEY,
|
|
11
|
+
experimental INTEGER NOT NULL DEFAULT 0,
|
|
12
|
+
fire_count INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
error_count INTEGER NOT NULL DEFAULT 0,
|
|
14
|
+
expected_count INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
first_seen_at TEXT,
|
|
16
|
+
last_seen_at TEXT,
|
|
17
|
+
dead_since TEXT,
|
|
18
|
+
plugin_version TEXT
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_hook_liveness_dead
|
|
22
|
+
ON hook_liveness(dead_since);
|
|
23
|
+
|
|
24
|
+
-- 2. Host probe history. Append-only. Off by default; exists so a
|
|
25
|
+
-- user chasing an intermittent fault can turn it on and get a
|
|
26
|
+
-- timeline instead of a single current value.
|
|
27
|
+
CREATE TABLE IF NOT EXISTS host_probes (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
probed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
30
|
+
plugin_version TEXT,
|
|
31
|
+
flavour TEXT NOT NULL,
|
|
32
|
+
has_shell INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
v2_skill INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
v2_reference INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
notes TEXT
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_host_probes_at
|
|
39
|
+
ON host_probes(probed_at);
|
|
40
|
+
|
|
41
|
+
-- 3. Native registration outcomes. One row per attach attempt, so
|
|
42
|
+
-- "registered but unverified" is a queryable state rather than a
|
|
43
|
+
-- log line.
|
|
44
|
+
CREATE TABLE IF NOT EXISTS native_registrations (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
attached_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
47
|
+
surface TEXT NOT NULL CHECK (surface IN ('skill', 'reference')),
|
|
48
|
+
registered INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
verified INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
note TEXT
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_native_registrations_surface
|
|
54
|
+
ON native_registrations(surface, attached_at);
|
|
55
|
+
|
|
56
|
+
-- 4. Metric seeds (39 -> 45).
|
|
57
|
+
INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
|
|
58
|
+
('hook_fires_total', 0),
|
|
59
|
+
('hook_errors_total', 0),
|
|
60
|
+
('hooks_dead_total', 0),
|
|
61
|
+
('injections_suppressed_dead_hook', 0),
|
|
62
|
+
('native_registrations_total', 0),
|
|
63
|
+
('native_registration_failures', 0);
|
|
64
|
+
|
|
65
|
+
-- 5. Setting seeds (23 -> 27).
|
|
66
|
+
-- hook_liveness_enabled defaults ON: it is a read-only instrument
|
|
67
|
+
-- on the success path, and an instrument nobody switches on is an
|
|
68
|
+
-- instrument nobody has.
|
|
69
|
+
-- native_registration_enabled defaults OFF: it changes where a
|
|
70
|
+
-- curated skill comes from, and that is a change a user opts into.
|
|
71
|
+
INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
|
|
72
|
+
('hook_liveness_enabled', '1'),
|
|
73
|
+
('native_registration_enabled', '0'),
|
|
74
|
+
('host_probe_history_enabled', '0'),
|
|
75
|
+
('dead_hook_report_threshold', '3');
|
|
76
|
+
|
|
77
|
+
-- 6. Version marker.
|
|
78
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES ('010');
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { HOOK_NAMES } from "./Migrate.js";
|
|
2
|
+
import type { Store } from "./Store.js";
|
|
3
|
+
export type HookName = (typeof HOOK_NAMES)[number];
|
|
4
|
+
export type LivenessState = "live" | "dead" | "unknown";
|
|
5
|
+
export interface HookReport {
|
|
6
|
+
hook: HookName;
|
|
7
|
+
experimental: boolean;
|
|
8
|
+
state: LivenessState;
|
|
9
|
+
firstSeenAt: string | null;
|
|
10
|
+
lastSeenAt: string | null;
|
|
11
|
+
fireCount: number;
|
|
12
|
+
expectedCount: number;
|
|
13
|
+
/** v0.9.0 (K9-012) — set once when the hook went dead; never cleared. */
|
|
14
|
+
deadSince: string | null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* v0.9.0 (K9-010 / plan §5.3, D9-09) — parse the TEXT setting
|
|
18
|
+
* `dead_hook_report_threshold` into the [1, 1000] clamp. NaN and empty
|
|
19
|
+
* fall back to 3 (never 0: a zero threshold would report every hook dead
|
|
20
|
+
* before a single session completes a checkpoint).
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseThreshold(text: string | null | undefined): number;
|
|
23
|
+
interface HookLivenessOptions {
|
|
24
|
+
/** `hook_liveness_enabled === "1"` (TEXT comparison, decided at construction). */
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
/** Raw TEXT of `dead_hook_report_threshold`; parsed+clamped in report(). */
|
|
27
|
+
thresholdText: string | null | undefined;
|
|
28
|
+
/** `host.pluginVersion` at construction, stored on every flushed row. */
|
|
29
|
+
pluginVersion: string | null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* v0.9.0 (K9-009/K9-010 / plan §5.3) — wraps the plugin's hooks with a
|
|
33
|
+
* success-path recorder, dedups per-session checkpoints, and reports each
|
|
34
|
+
* hook's liveness state. Persistence is machine-scoped: the hook_liveness
|
|
35
|
+
* table carries no project_id or repo_id (D9-08).
|
|
36
|
+
*/
|
|
37
|
+
export declare class HookLiveness {
|
|
38
|
+
private readonly store;
|
|
39
|
+
private readonly options;
|
|
40
|
+
private readonly counters;
|
|
41
|
+
private readonly seenSessions;
|
|
42
|
+
private readonly suppressedSessions;
|
|
43
|
+
private readonly threshold;
|
|
44
|
+
private readonly pluginVersion;
|
|
45
|
+
constructor(store: Store, options: HookLivenessOptions);
|
|
46
|
+
/**
|
|
47
|
+
* Returns a new object with the same keys; function values are replaced
|
|
48
|
+
* by delegating wrappers, non-function values (the tool map) pass by
|
|
49
|
+
* reference. When liveness is disabled, returns the argument unchanged —
|
|
50
|
+
* the same object, not a copy.
|
|
51
|
+
*/
|
|
52
|
+
wrap<T>(hooks: T): T;
|
|
53
|
+
/**
|
|
54
|
+
* v0.9.0 (K9-010 / plan §5.3) — a checkpoint: the session reached a
|
|
55
|
+
* model turn (tool.execute.after fired), so the system prompt was
|
|
56
|
+
* assembled and `experimental.chat.system.transform` MUST have been
|
|
57
|
+
* offered. Deduped per session: 20 tool calls in one session count
|
|
58
|
+
* exactly one expectation.
|
|
59
|
+
*/
|
|
60
|
+
expect(hook: HookName, sessionID: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Per-hook liveness verdicts in canonical HOOK_NAMES order.
|
|
63
|
+
* A hook that fired once is live forever; dead requires both zero fires
|
|
64
|
+
* and `expectedCount >= threshold`; everything else is unknown — and
|
|
65
|
+
* unknown is never rounded to healthy (D9-09).
|
|
66
|
+
*/
|
|
67
|
+
report(): HookReport[];
|
|
68
|
+
/** Persists every counter row. Called on the metrics.flush() cadence. */
|
|
69
|
+
flush(): void;
|
|
70
|
+
private makeWrapper;
|
|
71
|
+
private recordSuccess;
|
|
72
|
+
private recordError;
|
|
73
|
+
private loadFromDb;
|
|
74
|
+
}
|
|
75
|
+
export type HostVerdict = "healthy" | "degraded" | "unknown";
|
|
76
|
+
export interface HostVerdictResult {
|
|
77
|
+
verdict: HostVerdict;
|
|
78
|
+
reason: string;
|
|
79
|
+
}
|
|
80
|
+
export declare function reduceVerdict(reports: readonly HookReport[]): HostVerdictResult;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { HOOK_NAMES } from "./Migrate.js";
|
|
2
|
+
const DEFAULT_THRESHOLD = 3;
|
|
3
|
+
/**
|
|
4
|
+
* v0.9.0 (K9-010 / plan §5.3, D9-09) — parse the TEXT setting
|
|
5
|
+
* `dead_hook_report_threshold` into the [1, 1000] clamp. NaN and empty
|
|
6
|
+
* fall back to 3 (never 0: a zero threshold would report every hook dead
|
|
7
|
+
* before a single session completes a checkpoint).
|
|
8
|
+
*/
|
|
9
|
+
export function parseThreshold(text) {
|
|
10
|
+
if (text === null || text === undefined || text === "")
|
|
11
|
+
return DEFAULT_THRESHOLD;
|
|
12
|
+
const n = Number.parseInt(text, 10);
|
|
13
|
+
// NaN → default; 0 is not a valid threshold (K9-010 AC: 'abc'/''/'0'
|
|
14
|
+
// all clamp to the default of three — never zero).
|
|
15
|
+
if (Number.isNaN(n) || n === 0)
|
|
16
|
+
return DEFAULT_THRESHOLD;
|
|
17
|
+
return Math.min(1000, Math.max(1, n));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* v0.9.0 (K9-009/K9-010 / plan §5.3) — wraps the plugin's hooks with a
|
|
21
|
+
* success-path recorder, dedups per-session checkpoints, and reports each
|
|
22
|
+
* hook's liveness state. Persistence is machine-scoped: the hook_liveness
|
|
23
|
+
* table carries no project_id or repo_id (D9-08).
|
|
24
|
+
*/
|
|
25
|
+
export class HookLiveness {
|
|
26
|
+
store;
|
|
27
|
+
options;
|
|
28
|
+
counters;
|
|
29
|
+
seenSessions;
|
|
30
|
+
suppressedSessions = new Set();
|
|
31
|
+
threshold;
|
|
32
|
+
pluginVersion;
|
|
33
|
+
constructor(store, options) {
|
|
34
|
+
this.store = store;
|
|
35
|
+
this.options = options;
|
|
36
|
+
this.threshold = parseThreshold(options.thresholdText);
|
|
37
|
+
this.pluginVersion = options.pluginVersion;
|
|
38
|
+
this.counters = new Map();
|
|
39
|
+
this.seenSessions = new Map();
|
|
40
|
+
for (const hook of HOOK_NAMES) {
|
|
41
|
+
this.counters.set(hook, {
|
|
42
|
+
fireCount: 0,
|
|
43
|
+
errorCount: 0,
|
|
44
|
+
expectedCount: 0,
|
|
45
|
+
firstSeenAt: null,
|
|
46
|
+
lastSeenAt: null,
|
|
47
|
+
deadSince: null,
|
|
48
|
+
});
|
|
49
|
+
this.seenSessions.set(hook, new Set());
|
|
50
|
+
}
|
|
51
|
+
this.loadFromDb();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns a new object with the same keys; function values are replaced
|
|
55
|
+
* by delegating wrappers, non-function values (the tool map) pass by
|
|
56
|
+
* reference. When liveness is disabled, returns the argument unchanged —
|
|
57
|
+
* the same object, not a copy.
|
|
58
|
+
*/
|
|
59
|
+
wrap(hooks) {
|
|
60
|
+
if (!this.options.enabled)
|
|
61
|
+
return hooks;
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [key, value] of Object.entries(hooks)) {
|
|
64
|
+
if (typeof value === "function") {
|
|
65
|
+
out[key] = this.makeWrapper(key, value);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
out[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* v0.9.0 (K9-010 / plan §5.3) — a checkpoint: the session reached a
|
|
75
|
+
* model turn (tool.execute.after fired), so the system prompt was
|
|
76
|
+
* assembled and `experimental.chat.system.transform` MUST have been
|
|
77
|
+
* offered. Deduped per session: 20 tool calls in one session count
|
|
78
|
+
* exactly one expectation.
|
|
79
|
+
*/
|
|
80
|
+
expect(hook, sessionID) {
|
|
81
|
+
const seen = this.seenSessions.get(hook);
|
|
82
|
+
if (!seen || seen.has(sessionID))
|
|
83
|
+
return;
|
|
84
|
+
seen.add(sessionID);
|
|
85
|
+
const counters = this.counters.get(hook);
|
|
86
|
+
if (!counters)
|
|
87
|
+
return;
|
|
88
|
+
counters.expectedCount += 1;
|
|
89
|
+
// The dead flag is materialized here (not only in report()) so that
|
|
90
|
+
// flush() persists dead_since even when no report() was ever called:
|
|
91
|
+
// kevin_doctor's PURE-SQL blocks read dead_since from the table.
|
|
92
|
+
if (counters.fireCount === 0 &&
|
|
93
|
+
counters.expectedCount >= this.threshold &&
|
|
94
|
+
counters.deadSince === null) {
|
|
95
|
+
counters.deadSince = counters.lastSeenAt ?? new Date().toISOString();
|
|
96
|
+
}
|
|
97
|
+
// v0.9.0 (K9-011 / plan §5.3) — when the injection hook is dead, the
|
|
98
|
+
// checkpointed session was suppressed: the host stopped offering the
|
|
99
|
+
// transform, so no injection could happen. Counted once per session
|
|
100
|
+
// (same dedup as expected_count) and persisted by flush(). This is
|
|
101
|
+
// the counter that turns "zero injections" from an ambiguous number
|
|
102
|
+
// into a diagnosis.
|
|
103
|
+
if (hook === "experimental.chat.system.transform" &&
|
|
104
|
+
counters.deadSince !== null &&
|
|
105
|
+
!this.suppressedSessions.has(sessionID)) {
|
|
106
|
+
this.suppressedSessions.add(sessionID);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Per-hook liveness verdicts in canonical HOOK_NAMES order.
|
|
111
|
+
* A hook that fired once is live forever; dead requires both zero fires
|
|
112
|
+
* and `expectedCount >= threshold`; everything else is unknown — and
|
|
113
|
+
* unknown is never rounded to healthy (D9-09).
|
|
114
|
+
*/
|
|
115
|
+
report() {
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const hook of HOOK_NAMES) {
|
|
118
|
+
const c = this.counters.get(hook);
|
|
119
|
+
if (!c)
|
|
120
|
+
continue;
|
|
121
|
+
let state;
|
|
122
|
+
if (c.fireCount > 0) {
|
|
123
|
+
state = "live";
|
|
124
|
+
}
|
|
125
|
+
else if (c.expectedCount >= this.threshold) {
|
|
126
|
+
// dead_since is set ONCE and never cleared: it documents the
|
|
127
|
+
// historical death even if the hook later recovers.
|
|
128
|
+
if (c.deadSince === null) {
|
|
129
|
+
c.deadSince = c.lastSeenAt ?? new Date().toISOString();
|
|
130
|
+
}
|
|
131
|
+
state = "dead";
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
state = "unknown";
|
|
135
|
+
}
|
|
136
|
+
out.push({
|
|
137
|
+
hook,
|
|
138
|
+
experimental: hook.startsWith("experimental."),
|
|
139
|
+
state,
|
|
140
|
+
firstSeenAt: c.firstSeenAt,
|
|
141
|
+
lastSeenAt: c.lastSeenAt,
|
|
142
|
+
fireCount: c.fireCount,
|
|
143
|
+
expectedCount: c.expectedCount,
|
|
144
|
+
deadSince: c.deadSince,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** Persists every counter row. Called on the metrics.flush() cadence. */
|
|
150
|
+
flush() {
|
|
151
|
+
const store = this.store;
|
|
152
|
+
store.transaction(() => {
|
|
153
|
+
// Created lazily on first flush, mirroring Metrics.flush(): the
|
|
154
|
+
// migration guarantees the table in production, but unit tests
|
|
155
|
+
// against pre-010 DBs must not explode at dispose.
|
|
156
|
+
store.exec(`CREATE TABLE IF NOT EXISTS hook_liveness (
|
|
157
|
+
hook TEXT PRIMARY KEY,
|
|
158
|
+
experimental INTEGER NOT NULL DEFAULT 0,
|
|
159
|
+
fire_count INTEGER NOT NULL DEFAULT 0,
|
|
160
|
+
error_count INTEGER NOT NULL DEFAULT 0,
|
|
161
|
+
expected_count INTEGER NOT NULL DEFAULT 0,
|
|
162
|
+
first_seen_at TEXT,
|
|
163
|
+
last_seen_at TEXT,
|
|
164
|
+
dead_since TEXT,
|
|
165
|
+
plugin_version TEXT
|
|
166
|
+
)`);
|
|
167
|
+
const upsert = store.prepare(`INSERT INTO hook_liveness (hook, experimental, fire_count, error_count,
|
|
168
|
+
expected_count, first_seen_at, last_seen_at, dead_since, plugin_version)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
170
|
+
ON CONFLICT(hook) DO UPDATE SET
|
|
171
|
+
experimental = excluded.experimental,
|
|
172
|
+
fire_count = excluded.fire_count,
|
|
173
|
+
error_count = excluded.error_count,
|
|
174
|
+
expected_count = excluded.expected_count,
|
|
175
|
+
first_seen_at = excluded.first_seen_at,
|
|
176
|
+
last_seen_at = excluded.last_seen_at,
|
|
177
|
+
dead_since = excluded.dead_since,
|
|
178
|
+
plugin_version = excluded.plugin_version`);
|
|
179
|
+
for (const [hook, c] of this.counters) {
|
|
180
|
+
upsert.run(hook, hook.startsWith("experimental.") ? 1 : 0, c.fireCount, c.errorCount, c.expectedCount, c.firstSeenAt, c.lastSeenAt, c.deadSince, this.pluginVersion);
|
|
181
|
+
}
|
|
182
|
+
// v0.9.0 (K9-011 / plan §5.3) — the four v0.9.0 counters are
|
|
183
|
+
// re-derived here, on the metrics cadence, exactly like the 009
|
|
184
|
+
// post-apply hook re-derives hooks_dead_total: SUM(fire_count),
|
|
185
|
+
// SUM(error_count), COUNT(dead_since) and the per-session
|
|
186
|
+
// suppression count. No Metrics dependency, no writes in the hot
|
|
187
|
+
// path — flush() is the single persistence point.
|
|
188
|
+
let fireSum = 0;
|
|
189
|
+
let errorSum = 0;
|
|
190
|
+
let deadCount = 0;
|
|
191
|
+
for (const c of this.counters.values()) {
|
|
192
|
+
fireSum += c.fireCount;
|
|
193
|
+
errorSum += c.errorCount;
|
|
194
|
+
if (c.deadSince !== null)
|
|
195
|
+
deadCount += 1;
|
|
196
|
+
}
|
|
197
|
+
store.exec(`CREATE TABLE IF NOT EXISTS kevin_metrics (
|
|
198
|
+
key TEXT PRIMARY KEY,
|
|
199
|
+
value INTEGER NOT NULL DEFAULT 0,
|
|
200
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
201
|
+
)`);
|
|
202
|
+
const derive = store.prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
|
|
203
|
+
VALUES (?, ?, datetime('now'))
|
|
204
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
205
|
+
value = excluded.value,
|
|
206
|
+
updated_at = datetime('now')`);
|
|
207
|
+
derive.run("hook_fires_total", fireSum);
|
|
208
|
+
derive.run("hook_errors_total", errorSum);
|
|
209
|
+
derive.run("hooks_dead_total", deadCount);
|
|
210
|
+
derive.run("injections_suppressed_dead_hook", this.suppressedSessions.size);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
makeWrapper(key, delegate) {
|
|
214
|
+
const record = () => {
|
|
215
|
+
if (HOOK_NAMES.includes(key)) {
|
|
216
|
+
this.recordSuccess(key);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const recordError = (e) => {
|
|
220
|
+
if (HOOK_NAMES.includes(key)) {
|
|
221
|
+
this.recordError(key);
|
|
222
|
+
}
|
|
223
|
+
throw e;
|
|
224
|
+
};
|
|
225
|
+
const n = delegate.length;
|
|
226
|
+
if (n === 0) {
|
|
227
|
+
return async () => {
|
|
228
|
+
try {
|
|
229
|
+
const result = await delegate();
|
|
230
|
+
record();
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
return recordError(e);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (n === 1) {
|
|
239
|
+
return async (a) => {
|
|
240
|
+
try {
|
|
241
|
+
const result = await delegate(a);
|
|
242
|
+
record();
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
return recordError(e);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return async (a, b) => {
|
|
251
|
+
try {
|
|
252
|
+
const result = await delegate(a, b);
|
|
253
|
+
record();
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
return recordError(e);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
recordSuccess(hook) {
|
|
262
|
+
const c = this.counters.get(hook);
|
|
263
|
+
if (!c)
|
|
264
|
+
return;
|
|
265
|
+
c.fireCount += 1;
|
|
266
|
+
const now = new Date().toISOString();
|
|
267
|
+
if (c.firstSeenAt === null)
|
|
268
|
+
c.firstSeenAt = now;
|
|
269
|
+
c.lastSeenAt = now;
|
|
270
|
+
}
|
|
271
|
+
recordError(hook) {
|
|
272
|
+
const c = this.counters.get(hook);
|
|
273
|
+
if (!c)
|
|
274
|
+
return;
|
|
275
|
+
c.errorCount += 1;
|
|
276
|
+
}
|
|
277
|
+
loadFromDb() {
|
|
278
|
+
let rows = [];
|
|
279
|
+
try {
|
|
280
|
+
rows = this.store
|
|
281
|
+
.prepare("SELECT hook, fire_count, error_count, expected_count, first_seen_at, last_seen_at, dead_since FROM hook_liveness")
|
|
282
|
+
.all();
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Graceful: pre-010 DBs have no hook_liveness table; the seeded
|
|
286
|
+
// zeros remain until the first flush creates rows.
|
|
287
|
+
rows = [];
|
|
288
|
+
}
|
|
289
|
+
for (const row of rows) {
|
|
290
|
+
if (!HOOK_NAMES.includes(row.hook))
|
|
291
|
+
continue;
|
|
292
|
+
const c = this.counters.get(row.hook);
|
|
293
|
+
if (!c)
|
|
294
|
+
continue;
|
|
295
|
+
c.fireCount = row.fire_count;
|
|
296
|
+
c.errorCount = row.error_count;
|
|
297
|
+
c.expectedCount = row.expected_count;
|
|
298
|
+
c.firstSeenAt = row.first_seen_at;
|
|
299
|
+
c.lastSeenAt = row.last_seen_at;
|
|
300
|
+
c.deadSince = row.dead_since;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
export function reduceVerdict(reports) {
|
|
305
|
+
if (reports.length === 0) {
|
|
306
|
+
return { verdict: "unknown", reason: "no hook reports yet" };
|
|
307
|
+
}
|
|
308
|
+
const dead = reports.filter((r) => r.state === "dead");
|
|
309
|
+
if (dead.length > 0) {
|
|
310
|
+
return {
|
|
311
|
+
verdict: "degraded",
|
|
312
|
+
reason: `${dead.map((r) => r.hook).join(", ")} dead since ${dead[0].deadSince ?? "unknown"}; ${dead.length} affected hook(s)`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (reports.every((r) => r.state === "live")) {
|
|
316
|
+
return { verdict: "healthy", reason: "all hooks live" };
|
|
317
|
+
}
|
|
318
|
+
const pending = reports.filter((r) => r.state === "unknown").length;
|
|
319
|
+
return {
|
|
320
|
+
verdict: "unknown",
|
|
321
|
+
reason: `${pending} hook(s) without checkpoint`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=HookLiveness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HookLiveness.js","sourceRoot":"","sources":["../../plugin/HookLiveness.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,13 +30,32 @@ export interface BundleTarget {
|
|
|
30
30
|
readonly path: string;
|
|
31
31
|
}
|
|
32
32
|
export declare const SKILL_TOPIC = "project-knowledge";
|
|
33
|
+
/**
|
|
34
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the native registration surfaces.
|
|
35
|
+
* Registration replaces emission; they are never both active.
|
|
36
|
+
*/
|
|
37
|
+
export type NativeSurface = "skill" | "reference";
|
|
33
38
|
export declare class Materializer {
|
|
34
39
|
private readonly store;
|
|
35
40
|
/** `root` is injectable for tests; production defaults to ~/.opencode-kevin. */
|
|
36
41
|
private readonly root;
|
|
42
|
+
/**
|
|
43
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the mutual-exclusion guard.
|
|
44
|
+
* A surface `attachNative()` registered is skipped by `materialize()`
|
|
45
|
+
* for that surface only; the other surface keeps emitting.
|
|
46
|
+
*/
|
|
47
|
+
private readonly nativeRegistered;
|
|
37
48
|
constructor(store: Store, options?: {
|
|
38
49
|
root?: string;
|
|
39
50
|
});
|
|
51
|
+
/**
|
|
52
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — record whether the surface was
|
|
53
|
+
* registered natively. Called at attach time (index.ts wiring, K9-018)
|
|
54
|
+
* with each `NativeRegistration.registered` flag.
|
|
55
|
+
*/
|
|
56
|
+
markNativeRegistered(surface: NativeSurface, registered: boolean): void;
|
|
57
|
+
/** v0.9.0 (K9-016 / plan §5.4, D9-10) — is the surface registered natively? */
|
|
58
|
+
hasNativeRegistration(surface: NativeSurface): boolean;
|
|
40
59
|
/** The curated, active memories — the knowledge the pull channels publish. */
|
|
41
60
|
private curatedRows;
|
|
42
61
|
/** The rendered skill body over all curated memories; "" when empty. */
|
|
@@ -53,6 +72,12 @@ export declare class Materializer {
|
|
|
53
72
|
* a call site of `write()` — the single write FUNCTION with two
|
|
54
73
|
* constrained targets: `kevin_approve` reaches `agents_md_path`, this
|
|
55
74
|
* module reaches `~/.opencode-kevin` only (D6-07; enforced by K6-020).
|
|
75
|
+
*
|
|
76
|
+
* v0.9.0 (K9-016 / plan §5.4, D9-10) — the guard: a surface registered
|
|
77
|
+
* natively by `attachNative()` is skipped here for that surface only.
|
|
78
|
+
* Both active would put the curated skill in front of the model twice
|
|
79
|
+
* from two sources that can disagree; neither active would silently
|
|
80
|
+
* remove the feature on hosts that gained the subpath.
|
|
56
81
|
*/
|
|
57
82
|
materialize(writer: ArtifactWriter): MaterializedBundle[];
|
|
58
83
|
}
|