@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.
Files changed (131) hide show
  1. package/README.md +559 -771
  2. package/dist/migrations/010_v09_native.sql +78 -0
  3. package/dist/migrations/011_v10_proven.sql +69 -0
  4. package/dist/plugin/Archiver.js +0 -1
  5. package/dist/plugin/ArtifactWriter.d.ts +25 -10
  6. package/dist/plugin/ArtifactWriter.js +43 -25
  7. package/dist/plugin/CausalChain.js +0 -1
  8. package/dist/plugin/ConflictDetector.js +0 -1
  9. package/dist/plugin/ContextInjector.js +0 -1
  10. package/dist/plugin/ConventionMiner.js +0 -1
  11. package/dist/plugin/Curator.js +0 -1
  12. package/dist/plugin/Feedback.js +0 -1
  13. package/dist/plugin/HookLiveness.d.ts +87 -0
  14. package/dist/plugin/HookLiveness.js +338 -0
  15. package/dist/plugin/InjectionLedger.js +0 -1
  16. package/dist/plugin/LessonFixer.js +0 -1
  17. package/dist/plugin/Materializer.d.ts +25 -0
  18. package/dist/plugin/Materializer.js +39 -11
  19. package/dist/plugin/MemoryService.js +0 -1
  20. package/dist/plugin/Migrate.d.ts +1 -0
  21. package/dist/plugin/Migrate.js +69 -6
  22. package/dist/plugin/PatternMiner.js +0 -1
  23. package/dist/plugin/QualityGate.js +0 -1
  24. package/dist/plugin/Reflector.js +0 -1
  25. package/dist/plugin/RepoIdentity.d.ts +20 -9
  26. package/dist/plugin/RepoIdentity.js +44 -10
  27. package/dist/plugin/RepoTruth.js +0 -1
  28. package/dist/plugin/Retrospective.js +16 -1
  29. package/dist/plugin/SharedLayer.js +0 -1
  30. package/dist/plugin/Store.js +0 -1
  31. package/dist/plugin/ToolCallObserver.js +0 -1
  32. package/dist/plugin/capabilities.js +0 -1
  33. package/dist/plugin/confidence.js +0 -1
  34. package/dist/plugin/contract.d.ts +46 -0
  35. package/dist/plugin/contract.js +368 -0
  36. package/dist/plugin/diff.js +0 -1
  37. package/dist/plugin/escape.d.ts +39 -0
  38. package/dist/plugin/escape.js +60 -0
  39. package/dist/plugin/fingerprint.js +0 -1
  40. package/dist/plugin/host.d.ts +70 -0
  41. package/dist/plugin/host.js +250 -0
  42. package/dist/plugin/index.d.ts +2 -2
  43. package/dist/plugin/index.js +542 -222
  44. package/dist/plugin/inferability.js +0 -1
  45. package/dist/plugin/kevin_approve.js +0 -1
  46. package/dist/plugin/kevin_audit.d.ts +60 -0
  47. package/dist/plugin/kevin_audit.js +144 -1
  48. package/dist/plugin/kevin_bench.d.ts +10 -0
  49. package/dist/plugin/kevin_bench.js +82 -0
  50. package/dist/plugin/kevin_conflicts.js +0 -1
  51. package/dist/plugin/kevin_contract.d.ts +8 -0
  52. package/dist/plugin/kevin_contract.js +59 -0
  53. package/dist/plugin/kevin_doctor.d.ts +62 -0
  54. package/dist/plugin/kevin_doctor.js +196 -0
  55. package/dist/plugin/kevin_facts.js +0 -1
  56. package/dist/plugin/kevin_native.d.ts +29 -0
  57. package/dist/plugin/kevin_native.js +79 -0
  58. package/dist/plugin/kevin_propose.js +0 -1
  59. package/dist/plugin/kevin_publish.js +0 -1
  60. package/dist/plugin/kevin_why.js +0 -1
  61. package/dist/plugin/memory-format.js +0 -1
  62. package/dist/plugin/metrics.js +0 -1
  63. package/dist/plugin/native.d.ts +92 -0
  64. package/dist/plugin/native.js +190 -0
  65. package/dist/plugin/okf-export.js +0 -1
  66. package/dist/plugin/okf-import.js +0 -1
  67. package/dist/plugin/okf.js +0 -1
  68. package/dist/plugin/perf.d.ts +35 -0
  69. package/dist/plugin/perf.js +174 -0
  70. package/dist/plugin/query-tokenizer.js +0 -1
  71. package/dist/plugin/redact.js +0 -1
  72. package/dist/plugin/replay-types.js +0 -1
  73. package/dist/plugin/replay.js +0 -1
  74. package/dist/plugin/sqlite-adapter.js +0 -1
  75. package/dist/plugin/uuid.js +0 -1
  76. package/package.json +22 -7
  77. package/dist/plugin/Archiver.js.map +0 -1
  78. package/dist/plugin/ArtifactWriter.js.map +0 -1
  79. package/dist/plugin/CausalChain.js.map +0 -1
  80. package/dist/plugin/ConflictDetector.js.map +0 -1
  81. package/dist/plugin/ContextInjector.js.map +0 -1
  82. package/dist/plugin/ConventionMiner.js.map +0 -1
  83. package/dist/plugin/Curator.js.map +0 -1
  84. package/dist/plugin/Feedback.js.map +0 -1
  85. package/dist/plugin/InjectionLedger.js.map +0 -1
  86. package/dist/plugin/LessonFixer.js.map +0 -1
  87. package/dist/plugin/Materializer.js.map +0 -1
  88. package/dist/plugin/MemoryService.js.map +0 -1
  89. package/dist/plugin/Migrate.js.map +0 -1
  90. package/dist/plugin/PatternMiner.js.map +0 -1
  91. package/dist/plugin/QualityGate.js.map +0 -1
  92. package/dist/plugin/Reflector.js.map +0 -1
  93. package/dist/plugin/RepoIdentity.js.map +0 -1
  94. package/dist/plugin/RepoTruth.js.map +0 -1
  95. package/dist/plugin/Retrospective.js.map +0 -1
  96. package/dist/plugin/SharedLayer.js.map +0 -1
  97. package/dist/plugin/Store.js.map +0 -1
  98. package/dist/plugin/ToolCallObserver.js.map +0 -1
  99. package/dist/plugin/capabilities.js.map +0 -1
  100. package/dist/plugin/confidence.js.map +0 -1
  101. package/dist/plugin/diff.js.map +0 -1
  102. package/dist/plugin/fingerprint.js.map +0 -1
  103. package/dist/plugin/index.js.map +0 -1
  104. package/dist/plugin/inferability.js.map +0 -1
  105. package/dist/plugin/kevin_approve.js.map +0 -1
  106. package/dist/plugin/kevin_audit.js.map +0 -1
  107. package/dist/plugin/kevin_conflicts.js.map +0 -1
  108. package/dist/plugin/kevin_facts.js.map +0 -1
  109. package/dist/plugin/kevin_propose.js.map +0 -1
  110. package/dist/plugin/kevin_publish.js.map +0 -1
  111. package/dist/plugin/kevin_why.js.map +0 -1
  112. package/dist/plugin/memory-format.js.map +0 -1
  113. package/dist/plugin/metrics.js.map +0 -1
  114. package/dist/plugin/okf-export.js.map +0 -1
  115. package/dist/plugin/okf-import.js.map +0 -1
  116. package/dist/plugin/okf.js.map +0 -1
  117. package/dist/plugin/query-tokenizer.js.map +0 -1
  118. package/dist/plugin/redact.js.map +0 -1
  119. package/dist/plugin/replay-types.js.map +0 -1
  120. package/dist/plugin/replay.js.map +0 -1
  121. package/dist/plugin/sqlite-adapter.js.map +0 -1
  122. package/dist/plugin/uuid.js.map +0 -1
  123. package/migrations/001_initial.sql +0 -92
  124. package/migrations/002_indexes.sql +0 -14
  125. package/migrations/003_v02_signal.sql +0 -58
  126. package/migrations/004_v03_knowledge.sql +0 -138
  127. package/migrations/005_v04_signal.sql +0 -57
  128. package/migrations/006_v05_glassbox.sql +0 -118
  129. package/migrations/007_v06_pull.sql +0 -145
  130. package/migrations/008_v07_truth.sql +0 -124
  131. package/migrations/009_v08_team.sql +0 -100
@@ -86,4 +86,3 @@ export function classify(memory) {
86
86
  // Rule 5.
87
87
  return "unknown";
88
88
  }
89
- //# sourceMappingURL=inferability.js.map
@@ -50,4 +50,3 @@ export function kevinApprove(store, memoryService, curator, writer, metrics, arg
50
50
  function sqliteNow() {
51
51
  return new Date().toISOString().slice(0, 19).replace("T", " ");
52
52
  }
53
- //# sourceMappingURL=kevin_approve.js.map
@@ -93,6 +93,66 @@ export interface AuditReport {
93
93
  precision_local?: number;
94
94
  reason?: "immature_db";
95
95
  };
96
+ /**
97
+ * v0.9.0 (K9-020 / plan §5.5) — the host block, alongside the v0.7.0
98
+ * `mix` and v0.8.0 `team` blocks. Pure SQL: hook states in aggregate
99
+ * (live/dead/unknown counts, fires and errors totals), the resolved
100
+ * plugin version, native registration outcomes per surface, and the
101
+ * verdict derivable from the database alone. On a pre-010 database
102
+ * the block is omitted (not zero-valued) and `"partial": true` set,
103
+ * following the channels/curation precedent.
104
+ */
105
+ host?: {
106
+ plugin_version: string | null;
107
+ hooks: {
108
+ live: number;
109
+ dead: number;
110
+ unknown: number;
111
+ fires_total: number;
112
+ errors_total: number;
113
+ };
114
+ native: {
115
+ total: number;
116
+ verified: number;
117
+ failures: number;
118
+ by_surface: Record<string, {
119
+ registered: number;
120
+ verified: number;
121
+ }>;
122
+ };
123
+ verdict: "healthy" | "degraded" | "unknown";
124
+ };
125
+ /**
126
+ * v1.0.0 (K10-020 / plan §5.6) — the perf block: per scope, the most
127
+ * recent `perf_samples` aggregate row (count/p50/p95/max) plus the
128
+ * stored budget and verdict. The stored aggregates are used DIRECTLY —
129
+ * re-aggregating aggregates would produce a number with no meaning
130
+ * (plan anti-pattern 42), and using the most recent row per scope is
131
+ * exactly what `bench:check` does, so `within_budget` agrees with it
132
+ * on the same data. Omitted (not zero-valued) on a pre-011 database,
133
+ * following the host precedent.
134
+ */
135
+ perf?: {
136
+ scopes: Record<string, {
137
+ count: number;
138
+ p50: number;
139
+ p95: number;
140
+ max: number;
141
+ budget_p95: number;
142
+ within_budget: boolean;
143
+ }>;
144
+ };
145
+ /**
146
+ * v1.0.0 (K10-020 / plan §5.6) — the contract block: the live contract's
147
+ * identity, computed from source rather than the database, so it is
148
+ * present even on a pre-011 database.
149
+ */
150
+ contract?: {
151
+ contract_version: number;
152
+ digest: string;
153
+ clause_count: number;
154
+ deprecated_count: number;
155
+ };
96
156
  partial: boolean;
97
157
  }
98
158
  /**
@@ -1,5 +1,7 @@
1
1
  import { effectivePrePromptCap } from "./ContextInjector.js";
2
2
  import { hasRepoIdColumn } from "./MemoryService.js";
3
+ import { contractDigest, describeContract } from "./contract.js";
4
+ import { BUDGETS } from "./perf.js";
3
5
  const ALL_FALSE_CAPABILITIES = {
4
6
  skills: false,
5
7
  references: false,
@@ -401,6 +403,145 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
401
403
  catch {
402
404
  partial = true;
403
405
  }
406
+ // v0.9.0 (K9-020 / plan §5.5) — the host block, mirroring the
407
+ // channels/curation precedent: gated on migration 010's schema, pure
408
+ // SQL, omitted (not zero-valued) on a pre-010 database with
409
+ // `partial` set. The verdict is derived entirely in SQL: degraded
410
+ // when any hook is dead, unknown when no hook has been observed at
411
+ // all (never rounded to healthy), healthy otherwise.
412
+ let host;
413
+ let hasSchema010 = false;
414
+ try {
415
+ const row = store
416
+ .prepare("SELECT 1 AS n FROM sqlite_master WHERE type='table' AND name='hook_liveness'")
417
+ .get();
418
+ hasSchema010 = !!row;
419
+ }
420
+ catch {
421
+ hasSchema010 = false;
422
+ }
423
+ if (hasSchema010) {
424
+ try {
425
+ const hookRow = store
426
+ .prepare(`SELECT
427
+ COUNT(*) AS total,
428
+ SUM(CASE WHEN dead_since IS NOT NULL THEN 1 ELSE 0 END) AS dead,
429
+ SUM(CASE WHEN fire_count > 0 THEN 1 ELSE 0 END) AS live,
430
+ SUM(fire_count) AS fires_total,
431
+ SUM(error_count) AS errors_total
432
+ FROM hook_liveness`)
433
+ .get();
434
+ const dead = hookRow.dead ?? 0;
435
+ const live = hookRow.live ?? 0;
436
+ const unknown = hookRow.total - dead - live;
437
+ const versionRow = store
438
+ .prepare(`SELECT plugin_version FROM host_probes
439
+ ORDER BY probed_at DESC, id DESC LIMIT 1`)
440
+ .get();
441
+ const regRows = store
442
+ .prepare(`SELECT surface, registered, verified, COUNT(*) AS n
443
+ FROM native_registrations
444
+ GROUP BY surface, registered, verified`)
445
+ .all();
446
+ const bySurface = {};
447
+ let total = 0;
448
+ let verified = 0;
449
+ let failures = 0;
450
+ for (const row of regRows) {
451
+ total += row.n;
452
+ if (row.verified === 1)
453
+ verified += row.n;
454
+ if (row.registered === 1 && row.verified === 0)
455
+ failures += row.n;
456
+ const acc = bySurface[row.surface] ?? { registered: 0, verified: 0 };
457
+ acc.registered += row.registered === 1 ? row.n : 0;
458
+ acc.verified += row.verified === 1 ? row.n : 0;
459
+ bySurface[row.surface] = acc;
460
+ }
461
+ host = {
462
+ plugin_version: versionRow?.plugin_version ?? null,
463
+ hooks: {
464
+ live,
465
+ dead,
466
+ unknown: unknown < 0 ? 0 : unknown,
467
+ fires_total: hookRow.fires_total ?? 0,
468
+ errors_total: hookRow.errors_total ?? 0,
469
+ },
470
+ native: { total, verified, failures, by_surface: bySurface },
471
+ verdict: dead > 0
472
+ ? "degraded"
473
+ : hookRow.total === 0
474
+ ? "unknown"
475
+ : unknown > 0
476
+ ? "unknown"
477
+ : "healthy",
478
+ };
479
+ }
480
+ catch {
481
+ partial = true;
482
+ host = undefined;
483
+ }
484
+ }
485
+ // v0.9.0 (K9-020): pre-010 databases simply omit `host` without
486
+ // marking the report partial — the host block is additive, unlike
487
+ // channels/curation which are the v0.6.0 scoreboard. Existing
488
+ // v0.8.0-era tests run on 007/008 stores and expect partial false.
489
+ // v1.0.0 (K10-020 / plan §5.6) — perf block: the most recent aggregate
490
+ // row per scope, read directly from perf_samples. A scope with no rows
491
+ // reports count 0, zero timings and within_budget true — never NULL,
492
+ // never NaN. Omitted on a pre-011 database (host precedent).
493
+ let perf;
494
+ let hasSchema011 = false;
495
+ try {
496
+ const row = store
497
+ .prepare("SELECT 1 AS n FROM sqlite_master WHERE type='table' AND name='perf_samples'")
498
+ .get();
499
+ hasSchema011 = !!row;
500
+ }
501
+ catch {
502
+ hasSchema011 = false;
503
+ }
504
+ if (hasSchema011) {
505
+ try {
506
+ const scopes = {};
507
+ const latest = store.prepare(`SELECT sample_count AS count, p50_ms AS p50, p95_ms AS p95,
508
+ max_ms AS max, budget_p95_ms AS budget_p95, within_budget
509
+ FROM perf_samples WHERE scope = ? ORDER BY id DESC LIMIT 1`);
510
+ for (const budget of BUDGETS) {
511
+ const row = latest.get(budget.scope);
512
+ scopes[budget.scope] = row
513
+ ? {
514
+ count: row.count,
515
+ p50: row.p50,
516
+ p95: row.p95,
517
+ max: row.max,
518
+ budget_p95: row.budget_p95,
519
+ within_budget: row.within_budget === 1,
520
+ }
521
+ : {
522
+ count: 0,
523
+ p50: 0,
524
+ p95: 0,
525
+ max: 0,
526
+ budget_p95: budget.p95Ms,
527
+ within_budget: true,
528
+ };
529
+ }
530
+ perf = { scopes };
531
+ }
532
+ catch {
533
+ perf = undefined;
534
+ }
535
+ }
536
+ // v1.0.0 (K10-020 / plan §5.6) — contract block: pure function of the
537
+ // source, no database involvement.
538
+ const liveContract = describeContract();
539
+ const contract = {
540
+ contract_version: liveContract.contractVersion,
541
+ digest: contractDigest(liveContract),
542
+ clause_count: liveContract.clauses.length,
543
+ deprecated_count: liveContract.clauses.filter((c) => c.deprecated).length,
544
+ };
404
545
  return {
405
546
  memories,
406
547
  injections,
@@ -414,7 +555,9 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
414
555
  channels,
415
556
  curation,
416
557
  team,
558
+ host,
559
+ perf,
560
+ contract,
417
561
  partial,
418
562
  };
419
563
  }
420
- //# sourceMappingURL=kevin_audit.js.map
@@ -0,0 +1,10 @@
1
+ import type { Store } from "./Store.js";
2
+ export interface KevinBenchDeps {
3
+ store: Store;
4
+ /** Working directory used to locate bench/corpus (defaults process.cwd()). */
5
+ cwd?: string;
6
+ }
7
+ export interface KevinBenchArgs {
8
+ action: "status" | "last";
9
+ }
10
+ export declare function buildKevinBench(deps: KevinBenchDeps, args: KevinBenchArgs): Record<string, unknown>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * K10-019 — kevin_bench: reports what `npm run bench` recorded.
3
+ *
4
+ * It NEVER runs the benchmark — running it from inside a live session
5
+ * would measure the session and pollute the user's database. The module
6
+ * imports nothing from scripts/bench.ts; the corpus digest of the on-disk
7
+ * corpus is recomputed here with the same fnv1a64 discipline so a result
8
+ * computed from a different corpus is visibly a different measurement.
9
+ */
10
+ import { readFileSync, statSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { fnv1a64 } from "./fingerprint.js";
13
+ function onDiskCorpusDigest(cwd) {
14
+ try {
15
+ const dir = join(cwd, "bench", "corpus");
16
+ statSync(dir);
17
+ const mem = readFileSync(join(dir, "memories.jsonl"), "utf8");
18
+ const q = readFileSync(join(dir, "queries.jsonl"), "utf8");
19
+ return {
20
+ digest: fnv1a64(`${mem}\n${q}`),
21
+ queries: q.split("\n").filter((l) => l.trim().length > 0).length,
22
+ };
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function noRuns() {
29
+ return {
30
+ has_runs: false,
31
+ hint: "npm run bench",
32
+ };
33
+ }
34
+ export function buildKevinBench(deps, args) {
35
+ if (args.action === "status") {
36
+ const count = deps.store
37
+ .prepare("SELECT COUNT(*) AS c FROM bench_runs")
38
+ .get();
39
+ if (!count || count.c === 0)
40
+ return noRuns();
41
+ const latest = deps.store
42
+ .prepare("SELECT corpus_digest, contract_digest, package_version, runtime, ran_at FROM bench_runs ORDER BY id DESC LIMIT 1")
43
+ .get();
44
+ if (!latest)
45
+ return noRuns();
46
+ const disk = onDiskCorpusDigest(deps.cwd ?? process.cwd());
47
+ return {
48
+ has_runs: true,
49
+ total_runs: count.c,
50
+ corpus_digest: latest.corpus_digest,
51
+ contract_digest: latest.contract_digest,
52
+ package_version: latest.package_version,
53
+ runtime: latest.runtime,
54
+ last_ran_at: latest.ran_at,
55
+ corpus_on_disk: disk === null ? null : disk.digest,
56
+ matches_disk_corpus: disk !== null && disk.digest === latest.corpus_digest,
57
+ };
58
+ }
59
+ const latest = deps.store
60
+ .prepare("SELECT corpus_digest, contract_digest, package_version, runtime, ran_at FROM bench_runs ORDER BY id DESC LIMIT 1")
61
+ .get();
62
+ if (!latest)
63
+ return noRuns();
64
+ const rows = deps.store
65
+ .prepare("SELECT arm, k, precision_at_k, recall_at_k, mrr FROM bench_runs WHERE corpus_digest = ? AND ran_at = ?")
66
+ .all(latest.corpus_digest, latest.ran_at);
67
+ return {
68
+ has_runs: true,
69
+ corpus_digest: latest.corpus_digest,
70
+ contract_digest: latest.contract_digest,
71
+ package_version: latest.package_version,
72
+ runtime: latest.runtime,
73
+ ran_at: latest.ran_at,
74
+ arms: rows.map((r) => ({
75
+ arm: r.arm,
76
+ k: r.k,
77
+ precision_at_k: r.precision_at_k,
78
+ recall_at_k: r.recall_at_k,
79
+ mrr: r.mrr,
80
+ })),
81
+ };
82
+ }
@@ -48,4 +48,3 @@ export function executeKevinConflicts(deps, action, id, keep, requestedStatus) {
48
48
  conflicts: rows,
49
49
  };
50
50
  }
51
- //# sourceMappingURL=kevin_conflicts.js.map
@@ -0,0 +1,8 @@
1
+ export interface KevinContractDeps {
2
+ packageVersion: string;
3
+ }
4
+ export interface KevinContractArgs {
5
+ clause?: string;
6
+ format?: "summary" | "full";
7
+ }
8
+ export declare function buildKevinContract(deps: KevinContractDeps, args: KevinContractArgs): Record<string, unknown>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * K10-018 — kevin_contract: the frozen surface, inspectable at runtime.
3
+ *
4
+ * Read-only. Returns the live contract with its digest and per-clause
5
+ * stability/since/deprecation; with `clause`, one clause's full value.
6
+ * No filesystem paths, no project ids, no LLM, no network.
7
+ */
8
+ import { contractDigest, describeContract, } from "./contract.js";
9
+ export function buildKevinContract(deps, args) {
10
+ const contract = describeContract();
11
+ const digest = contractDigest(contract);
12
+ const format = args.format ?? "summary";
13
+ if (args.clause !== undefined) {
14
+ const clause = contract.clauses.find((c) => c.id === args.clause);
15
+ if (!clause) {
16
+ return {
17
+ error: "unknown_clause",
18
+ clause: args.clause,
19
+ known_clauses: contract.clauses.map((c) => c.id),
20
+ };
21
+ }
22
+ return {
23
+ contract_version: contract.contractVersion,
24
+ digest,
25
+ package_version: deps.packageVersion,
26
+ clause: {
27
+ id: clause.id,
28
+ title: clause.title,
29
+ stability: clause.stability,
30
+ since: clause.since,
31
+ ...(clause.deprecated ? { deprecated: clause.deprecated } : {}),
32
+ ...(clause.replacement ? { replacement: clause.replacement } : {}),
33
+ value: clause.value,
34
+ },
35
+ };
36
+ }
37
+ if (format === "full") {
38
+ return {
39
+ contract_version: contract.contractVersion,
40
+ digest,
41
+ package_version: deps.packageVersion,
42
+ clauses: contract.clauses,
43
+ deprecated: contract.clauses.filter((c) => c.deprecated).map((c) => c.id),
44
+ };
45
+ }
46
+ return {
47
+ contract_version: contract.contractVersion,
48
+ digest,
49
+ package_version: deps.packageVersion,
50
+ clauses: contract.clauses.map((c) => ({
51
+ id: c.id,
52
+ title: c.title,
53
+ stability: c.stability,
54
+ since: c.since,
55
+ ...(c.deprecated ? { deprecated: c.deprecated } : {}),
56
+ })),
57
+ deprecated: contract.clauses.filter((c) => c.deprecated).map((c) => c.id),
58
+ };
59
+ }
@@ -0,0 +1,62 @@
1
+ import type { Store } from "./Store.js";
2
+ import type { HostSurface } from "./host.js";
3
+ import type { SettingsReader } from "./native.js";
4
+ export interface DoctorHook {
5
+ readonly hook: string;
6
+ readonly experimental: boolean;
7
+ readonly state: "live" | "dead" | "unknown";
8
+ readonly fire_count: number;
9
+ readonly expected_count: number;
10
+ readonly since?: string;
11
+ }
12
+ export interface DoctorReport {
13
+ readonly host: {
14
+ readonly plugin_version: string | null;
15
+ readonly flavour: string;
16
+ readonly shell_available: boolean;
17
+ readonly v2: {
18
+ readonly skill: boolean;
19
+ readonly reference: boolean;
20
+ };
21
+ };
22
+ readonly hooks: DoctorHook[];
23
+ readonly dependencies: {
24
+ readonly declared: string[];
25
+ readonly zod_copies: number | null;
26
+ readonly note?: string;
27
+ };
28
+ readonly native: {
29
+ readonly enabled: boolean;
30
+ readonly registered: {
31
+ readonly skill: boolean;
32
+ readonly reference: boolean;
33
+ };
34
+ readonly verified: {
35
+ readonly skill: boolean;
36
+ readonly reference: boolean;
37
+ };
38
+ };
39
+ /** v1.0.0 (K10-021) — scopes whose most recent sample exceeded their
40
+ * p95 budget. Omitted entirely on pre-011 databases (no table). */
41
+ readonly perf?: {
42
+ readonly scopes_over_budget: readonly string[];
43
+ };
44
+ readonly verdict: "healthy" | "degraded" | "unknown";
45
+ readonly reason: string;
46
+ readonly partial: boolean;
47
+ }
48
+ /**
49
+ * Count resolved zod copies under `cwd`'s dependency tree. The expected
50
+ * value after K9-005 is 1; a second copy is exactly the regression this
51
+ * field exists to catch. On any walk failure the count is `null` and the
52
+ * note says so — never a guess.
53
+ */
54
+ export declare function countZodCopies(cwd: string): {
55
+ copies: number | null;
56
+ note?: string;
57
+ };
58
+ export interface DoctorOptions {
59
+ /** Directory to walk for zod copies; defaults to process.cwd(). */
60
+ readonly zodRoot?: string;
61
+ }
62
+ export declare function buildDoctor(store: Store, host: HostSurface, settings: SettingsReader, options?: DoctorOptions): DoctorReport;
@@ -0,0 +1,196 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { reduceVerdict, } from "./HookLiveness.js";
4
+ import { HOOK_NAMES } from "./Migrate.js";
5
+ /** Dead first, then unknown, then live — the failure is the first thing
6
+ * on screen. Ties break on the canonical hook name. */
7
+ const STATE_ORDER = {
8
+ dead: 0,
9
+ unknown: 1,
10
+ live: 2,
11
+ };
12
+ /** The hooks block is pure SQL over the persisted hook_liveness table:
13
+ * the dead flag is materialized at expect() time (K9-010) so flush() can
14
+ * persist dead_since even when report() never runs — this block reads
15
+ * that persisted state, independent of any live HookLiveness object. */
16
+ function hooksBlock(store) {
17
+ let rows = [];
18
+ try {
19
+ rows = store
20
+ .prepare("SELECT hook, experimental, fire_count, expected_count, dead_since FROM hook_liveness")
21
+ .all();
22
+ }
23
+ catch {
24
+ return { hooks: [], partial: true };
25
+ }
26
+ // Only hooks the plugin knows about are reported; a foreign row (from
27
+ // a future release) is skipped, mirroring HookLiveness.loadFromDb.
28
+ const reports = rows
29
+ .filter((row) => HOOK_NAMES.includes(row.hook))
30
+ .map((row) => ({
31
+ hook: row.hook,
32
+ experimental: row.experimental === 1,
33
+ state: row.fire_count > 0
34
+ ? "live"
35
+ : row.dead_since !== null
36
+ ? "dead"
37
+ : "unknown",
38
+ firstSeenAt: null,
39
+ lastSeenAt: null,
40
+ fireCount: row.fire_count,
41
+ expectedCount: row.expected_count,
42
+ deadSince: row.dead_since,
43
+ }));
44
+ reports.sort((a, b) => STATE_ORDER[a.state] - STATE_ORDER[b.state] ||
45
+ a.hook.localeCompare(b.hook));
46
+ const hooks = reports.map((r) => ({
47
+ hook: r.hook,
48
+ experimental: r.experimental,
49
+ state: r.state,
50
+ fire_count: r.fireCount,
51
+ expected_count: r.expectedCount,
52
+ ...(r.deadSince !== null ? { since: r.deadSince } : {}),
53
+ }));
54
+ return { hooks, partial: false };
55
+ }
56
+ /**
57
+ * Count resolved zod copies under `cwd`'s dependency tree. The expected
58
+ * value after K9-005 is 1; a second copy is exactly the regression this
59
+ * field exists to catch. On any walk failure the count is `null` and the
60
+ * note says so — never a guess.
61
+ */
62
+ export function countZodCopies(cwd) {
63
+ try {
64
+ if (!existsSync(cwd)) {
65
+ return { copies: null, note: "zod resolution walk failed" };
66
+ }
67
+ const seen = new Set();
68
+ const stack = [cwd];
69
+ let copies = 0;
70
+ // Bounded walk: node_modules is normally shallow; nested copies
71
+ // (node_modules/<pkg>/node_modules/zod) sit within a few levels.
72
+ for (let depth = 0; stack.length > 0 && depth < 8; depth += 1) {
73
+ const next = [];
74
+ for (const dir of stack) {
75
+ const nm = join(dir, "node_modules");
76
+ if (!existsSync(nm))
77
+ continue;
78
+ let entries;
79
+ try {
80
+ entries = readdirSync(nm, { withFileTypes: true });
81
+ }
82
+ catch {
83
+ continue;
84
+ }
85
+ for (const entry of entries) {
86
+ if (!entry.isDirectory())
87
+ continue;
88
+ if (entry.name === "zod") {
89
+ const pkg = join(nm, "zod", "package.json");
90
+ if (existsSync(pkg) && !seen.has(pkg)) {
91
+ seen.add(pkg);
92
+ copies += 1;
93
+ }
94
+ }
95
+ else if (entry.name !== ".bin" && entry.name !== ".cache") {
96
+ next.push(join(nm, entry.name));
97
+ }
98
+ }
99
+ }
100
+ stack.length = 0;
101
+ stack.push(...next);
102
+ }
103
+ return { copies };
104
+ }
105
+ catch {
106
+ return { copies: null, note: "zod resolution walk failed" };
107
+ }
108
+ }
109
+ /** v1.0.0 (K10-021) — scopes whose most recent perf sample breached the
110
+ * p95 budget. The within_budget flag is materialized by Perf.flush()
111
+ * against the frozen BUDGETS; this block reads that persisted verdict
112
+ * and never re-aggregates. A missing table (pre-011 DB) yields no
113
+ * block rather than an empty one, matching the hooks precedent. */
114
+ function perfOverBudget(store) {
115
+ try {
116
+ const rows = store
117
+ .prepare("SELECT DISTINCT scope FROM perf_samples WHERE within_budget = 0 ORDER BY scope")
118
+ .all();
119
+ return rows.map((r) => r.scope);
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ function lastRegistration(store, surface) {
126
+ try {
127
+ const row = store
128
+ .prepare("SELECT surface, registered, verified FROM native_registrations WHERE surface = ? ORDER BY attached_at DESC, id DESC LIMIT 1")
129
+ .get(surface);
130
+ if (!row)
131
+ return { registered: false, verified: false };
132
+ return { registered: row.registered === 1, verified: row.verified === 1 };
133
+ }
134
+ catch {
135
+ return { registered: false, verified: false };
136
+ }
137
+ }
138
+ export function buildDoctor(store, host, settings, options = {}) {
139
+ // The host block comes from the frozen probe result captured at
140
+ // construction — kevin_doctor never re-probes (K9-004: probe once,
141
+ // freeze, reuse).
142
+ const hooks = hooksBlock(store);
143
+ let verdict = reduceVerdict(hooks.hooks.map((h) => ({
144
+ hook: h.hook,
145
+ experimental: h.experimental,
146
+ state: h.state,
147
+ firstSeenAt: null,
148
+ lastSeenAt: null,
149
+ fireCount: h.fire_count,
150
+ expectedCount: h.expected_count,
151
+ deadSince: h.since ?? null,
152
+ })));
153
+ // v1.0.0 (K10-021) — a slow plugin is not healthy even when every
154
+ // hook is live. A budget breach degrades the verdict and names the
155
+ // scope; a dead hook keeps its reason; `unknown` is never rounded up.
156
+ const breaches = perfOverBudget(store);
157
+ if (breaches !== null &&
158
+ breaches.length > 0 &&
159
+ verdict.verdict !== "degraded") {
160
+ verdict = {
161
+ verdict: "degraded",
162
+ reason: `perf budget exceeded: ${breaches.join(", ")}`,
163
+ };
164
+ }
165
+ const zod = countZodCopies(options.zodRoot ?? process.cwd());
166
+ const enabled = settings.getSetting("native_registration_enabled", "0") === "1";
167
+ return {
168
+ host: {
169
+ plugin_version: host.pluginVersion,
170
+ flavour: host.flavour,
171
+ shell_available: host.hasShell,
172
+ v2: { skill: host.v2.skill, reference: host.v2.reference },
173
+ },
174
+ hooks: hooks.hooks,
175
+ dependencies: {
176
+ declared: ["@opencode-ai/plugin"],
177
+ zod_copies: zod.copies,
178
+ ...(zod.note !== undefined ? { note: zod.note } : {}),
179
+ },
180
+ native: {
181
+ enabled,
182
+ registered: {
183
+ skill: lastRegistration(store, "skill").registered,
184
+ reference: lastRegistration(store, "reference").registered,
185
+ },
186
+ verified: {
187
+ skill: lastRegistration(store, "skill").verified,
188
+ reference: lastRegistration(store, "reference").verified,
189
+ },
190
+ },
191
+ verdict: verdict.verdict,
192
+ reason: verdict.reason,
193
+ ...(breaches !== null ? { perf: { scopes_over_budget: breaches } } : {}),
194
+ partial: hooks.partial,
195
+ };
196
+ }
@@ -34,4 +34,3 @@ export function buildKevinFacts(deps, refresh) {
34
34
  penalized,
35
35
  };
36
36
  }
37
- //# sourceMappingURL=kevin_facts.js.map