@jmtrin/opencode-kevin 1.0.0 → 1.2.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -8
  3. package/dist/migrations/001_initial.sql +91 -91
  4. package/dist/migrations/002_indexes.sql +13 -13
  5. package/dist/migrations/003_v02_signal.sql +57 -57
  6. package/dist/migrations/004_v03_knowledge.sql +138 -138
  7. package/dist/migrations/005_v04_signal.sql +57 -57
  8. package/dist/migrations/006_v05_glassbox.sql +118 -118
  9. package/dist/migrations/007_v06_pull.sql +144 -144
  10. package/dist/migrations/012_v11_drift.sql +24 -0
  11. package/dist/plugin/Archiver.js +2 -17
  12. package/dist/plugin/CausalChain.js +32 -13
  13. package/dist/plugin/ChatBridge.d.ts +41 -0
  14. package/dist/plugin/ChatBridge.js +103 -0
  15. package/dist/plugin/ConflictDetector.js +7 -29
  16. package/dist/plugin/DashboardHtml.d.ts +5 -0
  17. package/dist/plugin/DashboardHtml.js +180 -0
  18. package/dist/plugin/Feedback.js +2 -19
  19. package/dist/plugin/HookLiveness.d.ts +1 -0
  20. package/dist/plugin/HookLiveness.js +11 -27
  21. package/dist/plugin/InjectionLedger.js +104 -57
  22. package/dist/plugin/Materializer.js +1 -88
  23. package/dist/plugin/MemoryService.d.ts +59 -1
  24. package/dist/plugin/MemoryService.js +13 -106
  25. package/dist/plugin/Migrate.js +5 -0
  26. package/dist/plugin/Retrospective.js +7 -0
  27. package/dist/plugin/ToolCallObserver.js +18 -5
  28. package/dist/plugin/TuiActions.d.ts +43 -0
  29. package/dist/plugin/TuiActions.js +181 -0
  30. package/dist/plugin/TuiSnapshots.d.ts +24 -0
  31. package/dist/plugin/TuiSnapshots.js +158 -0
  32. package/dist/plugin/capabilities.d.ts +2 -0
  33. package/dist/plugin/capabilities.js +3 -0
  34. package/dist/plugin/columns.d.ts +11 -0
  35. package/dist/plugin/columns.js +54 -0
  36. package/dist/plugin/contract.d.ts +8 -0
  37. package/dist/plugin/contract.js +23 -5
  38. package/dist/plugin/index.d.ts +2 -2
  39. package/dist/plugin/index.js +315 -10
  40. package/dist/plugin/kevin_audit.d.ts +17 -1
  41. package/dist/plugin/kevin_audit.js +69 -1
  42. package/dist/plugin/kevin_forget.d.ts +33 -0
  43. package/dist/plugin/kevin_forget.js +260 -0
  44. package/dist/plugin/kevin_why.js +1 -18
  45. package/dist/plugin/metrics.d.ts +1 -1
  46. package/dist/plugin/metrics.js +8 -0
  47. package/dist/plugin/query-tokenizer.js +56 -8
  48. package/dist/plugin/time-ms.d.ts +1 -0
  49. package/dist/plugin/time-ms.js +16 -0
  50. package/dist/plugin/tui-types.d.ts +59 -0
  51. package/dist/plugin/tui-types.js +4 -0
  52. package/dist/plugin/tui.d.ts +18 -0
  53. package/dist/plugin/tui.js +198 -0
  54. package/package.json +8 -2
@@ -0,0 +1,54 @@
1
+ const cache = new WeakMap();
2
+ export function hasColumn(store, table, column) {
3
+ const byStore = cache.get(store);
4
+ const key = `${table}.${column}`;
5
+ if (byStore?.get(key) === true)
6
+ return true;
7
+ try {
8
+ store.prepare(`SELECT ${column} FROM ${table} LIMIT 0`).get();
9
+ let m = byStore;
10
+ if (!m) {
11
+ m = new Map();
12
+ cache.set(store, m);
13
+ }
14
+ m.set(key, true);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ // v1.1.0 (K11-011 / plan §5.5, D11-06) — named helpers delegating to the registry
22
+ export function hasIgnoredColumn(store) {
23
+ return hasColumn(store, "memories", "ignored");
24
+ }
25
+ export function hasCuratedColumn(store) {
26
+ return hasColumn(store, "memories", "curated");
27
+ }
28
+ export function hasTruthColumns(store) {
29
+ return hasColumn(store, "memories", "truth_penalty");
30
+ }
31
+ export function hasRepoIdColumn(store) {
32
+ return hasColumn(store, "memories", "repo_id");
33
+ }
34
+ export function hasLayerColumn(store) {
35
+ return hasColumn(store, "memories", "layer");
36
+ }
37
+ export function hasRecurrenceColumn(store) {
38
+ return hasColumn(store, "memories", "recurrence_count");
39
+ }
40
+ export function hasArchivedColumn(store) {
41
+ return hasColumn(store, "memories", "archived_at");
42
+ }
43
+ export function hasFeedbackColumns(store) {
44
+ return hasColumn(store, "memories", "feedback_positive");
45
+ }
46
+ export function hasFeedbackTable(store) {
47
+ try {
48
+ store.prepare("SELECT COUNT(*) FROM memory_feedback LIMIT 0").get();
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
@@ -28,6 +28,14 @@ export declare const CONTRACT_TOOL_ADDITIONS: readonly {
28
28
  name: string;
29
29
  since: string;
30
30
  }[];
31
+ /**
32
+ * v1.1.0 (K11-007 / plan §5.1, D11-01) — metric keys added after the freeze,
33
+ * each carrying the `since` the deprecation policy requires (C-05).
34
+ */
35
+ export declare const CONTRACT_METRIC_ADDITIONS: readonly {
36
+ name: string;
37
+ since: string;
38
+ }[];
31
39
  /**
32
40
  * v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
33
41
  * not trusted: anything reaching an artifact or a prompt is escaped at
@@ -41,8 +41,21 @@ export const CONTRACT_TOOL_NAMES = [
41
41
  * added_bare.
42
42
  */
43
43
  export const CONTRACT_TOOL_ADDITIONS = [
44
- { name: "kevin_contract", since: "1.0.0" },
45
44
  { name: "kevin_bench", since: "1.0.0" },
45
+ { name: "kevin_contract", since: "1.0.0" },
46
+ { name: "kevin_forget", since: "1.1.0" },
47
+ ];
48
+ /**
49
+ * v1.1.0 (K11-007 / plan §5.1, D11-01) — metric keys added after the freeze,
50
+ * each carrying the `since` the deprecation policy requires (C-05).
51
+ */
52
+ export const CONTRACT_METRIC_ADDITIONS = [
53
+ { name: "bench_regression_failures", since: "1.1.0" },
54
+ { name: "forget_requests_total", since: "1.1.0" },
55
+ { name: "forget_tombstones_published", since: "1.1.0" },
56
+ // v1.2.0 (K12-001 / plan §4, D12-??) — surface metrics (no migration this release)
57
+ { name: "tui_snapshots_flushed", since: "1.2.0" },
58
+ { name: "tui_actions_invoked", since: "1.2.0" },
46
59
  ];
47
60
  /**
48
61
  * v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
@@ -76,12 +89,17 @@ export function describeContract(_input) {
76
89
  // Derive clause values from live source wherever possible (plan §5.1).
77
90
  // C-03 members added after the freeze carry their `since` as objects;
78
91
  // the original frozen set stays plain strings.
79
- const additions = [...CONTRACT_TOOL_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
92
+ const toolAdditions = [...CONTRACT_TOOL_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
80
93
  const toolValue = {
81
- tools: [[...CONTRACT_TOOL_NAMES].sort(), additions].flat(),
94
+ tools: [[...CONTRACT_TOOL_NAMES].sort(), toolAdditions].flat(),
82
95
  };
83
96
  const settingValue = { keys: [...KEVIN_CONFIG_KEYS].sort() };
84
- const metricValue = { keys: Object.keys(METRIC_KEY_LABELS).sort() };
97
+ // v1.1.0 metric keys added after freeze carry `since` (C-05)
98
+ const metricAdditions = [...CONTRACT_METRIC_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
99
+ const baseMetricKeys = Object.keys(METRIC_KEY_LABELS)
100
+ .filter((k) => !metricAdditions.some((a) => a.name === k))
101
+ .sort();
102
+ const metricValue = { keys: [...baseMetricKeys, ...metricAdditions].flat() };
85
103
  const clauses = [
86
104
  {
87
105
  id: "C-01",
@@ -149,7 +167,7 @@ export function describeContract(_input) {
149
167
  stability: "forward-only",
150
168
  since: "0.1.0",
151
169
  value: {
152
- schema_version: "011",
170
+ schema_version: "012",
153
171
  migrations_forward_only: true,
154
172
  },
155
173
  },
@@ -16,10 +16,10 @@ export interface KevinPluginOptions {
16
16
  * v0.4.0 (K4-021) — settings surfaced by `kevin_config` (plan §8.8).
17
17
  * Unknown keys are rejected on `set` unless `strict: false`.
18
18
  */
19
- export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled"];
19
+ export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled"];
20
20
  export declare const ERROR_LESSON_MODE_VALUES: readonly ["all", "triage_only"];
21
21
  /** Plugin release version — stamped into generated files (K8-021/027). */
22
- export declare const KEVIN_VERSION = "1.0.0";
22
+ export declare const KEVIN_VERSION = "1.2.0";
23
23
  export interface RekeyCounts {
24
24
  memories: number;
25
25
  shared_entries: number;
@@ -6,10 +6,12 @@ import { tool } from "@opencode-ai/plugin";
6
6
  import { Archiver } from "./Archiver.js";
7
7
  import { ArtifactWriter } from "./ArtifactWriter.js";
8
8
  import { CausalChain } from "./CausalChain.js";
9
+ import { handleBridgeCommand } from "./ChatBridge.js";
9
10
  import { ConflictDetector } from "./ConflictDetector.js";
10
11
  import { ContextInjector } from "./ContextInjector.js";
11
12
  import { ConventionMiner } from "./ConventionMiner.js";
12
13
  import { Curator } from "./Curator.js";
14
+ import { writeDashboard } from "./DashboardHtml.js";
13
15
  import { Feedback } from "./Feedback.js";
14
16
  import { HookLiveness } from "./HookLiveness.js";
15
17
  import { InjectionLedger } from "./InjectionLedger.js";
@@ -24,8 +26,11 @@ import { Retrospective } from "./Retrospective.js";
24
26
  import { SharedLayer } from "./SharedLayer.js";
25
27
  import { Store } from "./Store.js";
26
28
  import { ToolCallObserver } from "./ToolCallObserver.js";
29
+ import { deleteMailbox, processActions, readMailbox, writeResults, } from "./TuiActions.js";
30
+ import { flushSnapshots } from "./TuiSnapshots.js";
27
31
  import { probe } from "./capabilities.js";
28
32
  import { computeConfidence } from "./confidence.js";
33
+ import { contractDigest, describeContract } from "./contract.js";
29
34
  import { probeHost, summarize } from "./host.js";
30
35
  import { kevinApprove } from "./kevin_approve.js";
31
36
  import { buildAudit } from "./kevin_audit.js";
@@ -34,6 +39,7 @@ import { executeKevinConflicts } from "./kevin_conflicts.js";
34
39
  import { buildKevinContract } from "./kevin_contract.js";
35
40
  import { buildDoctor } from "./kevin_doctor.js";
36
41
  import { buildKevinFacts } from "./kevin_facts.js";
42
+ import { handleForget } from "./kevin_forget.js";
37
43
  import { handleNative } from "./kevin_native.js";
38
44
  import { kevinPropose } from "./kevin_propose.js";
39
45
  import { kevinPublish } from "./kevin_publish.js";
@@ -111,7 +117,13 @@ export const KEVIN_CONFIG_KEYS = [
111
117
  "perf_ring_capacity",
112
118
  "perf_flush_on_idle",
113
119
  "contract_report_enabled",
120
+ // v1.2.0 (K12-001 / plan §4) — the single setting seeded by runtime
121
+ // (no migration this release). Omitting makes `kevin_config set`
122
+ // return { error: "unknown_key" } while `kevin_config list` still
123
+ // shows it.
124
+ "tui_snapshots_enabled",
114
125
  ];
126
+ // v1.1.0 (K11-007 / D11-08) — no new settings in 1.1.0; thresholds are constants (D11-03)
115
127
  // v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
116
128
  // `error_lesson_mode`. The setting is TEXT and must be compared with
117
129
  // `=== "triage_only"`, never by truthiness; the domain here is enforced by
@@ -120,7 +132,7 @@ export const KEVIN_CONFIG_KEYS = [
120
132
  // behaviour on the next reflection.
121
133
  export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
122
134
  /** Plugin release version — stamped into generated files (K8-021/027). */
123
- export const KEVIN_VERSION = "1.0.0";
135
+ export const KEVIN_VERSION = "1.2.0";
124
136
  function resolveMigrationsDir() {
125
137
  const here = dirname(fileURLToPath(import.meta.url));
126
138
  return join(here, "..", "migrations");
@@ -256,6 +268,17 @@ export const KevinPlugin = async (input, options) => {
256
268
  const store = new Store({ path: dbPath });
257
269
  const migrationsDir = opts.migrationsDir ?? resolveMigrationsDir();
258
270
  await new Migrate(store, migrationsDir).run();
271
+ // v1.2.0 (K12-001 / D12-??) — no migration this release: ensure the
272
+ // new setting exists with its default so `kevin_config list` shows it
273
+ // on a database that was already at 012.
274
+ try {
275
+ store
276
+ .prepare("INSERT OR IGNORE INTO kevin_settings (key, value) VALUES (?, ?)")
277
+ .run("tui_snapshots_enabled", "1");
278
+ }
279
+ catch {
280
+ // pre-003 DB without kevin_settings — nothing to seed
281
+ }
259
282
  const metrics = new Metrics(store);
260
283
  // v0.4.0 (K4-019): the plugin hooks expose no project field, so the
261
284
  // project id is derived once from the plugin host's working directory
@@ -395,6 +418,10 @@ export const KevinPlugin = async (input, options) => {
395
418
  mkdirSync(join(materializerRoot, "skills"), { recursive: true });
396
419
  mkdirSync(join(materializerRoot, "refs"), { recursive: true });
397
420
  const capabilities = probe(input);
421
+ // v1.2.0 (K12-012 / D12-03) — permission.ask probe (best-effort, additive).
422
+ // When the host exposes permission.ask, the presence is noted; absence is silent no-op.
423
+ // No new setting — bounded to tui_snapshots_enabled host-support check per spec.
424
+ void capabilities.permissionAsk;
398
425
  const materializer = new Materializer(store, { root: materializerRoot });
399
426
  // v0.9.0 (K9-016 / plan §5.4, D9-10) — native registration replaces
400
427
  // file emission. When attachNative returns a registration for a
@@ -870,8 +897,9 @@ export const KevinPlugin = async (input, options) => {
870
897
  // monotone across releases; K6-024 extends this
871
898
  // block with the remaining v0.6 fields. v0.8.0
872
899
  // (K8-025) — 18 v0.7 + kevin_project +
873
- // kevin_native = 23.
874
- tool_count: 23,
900
+ // kevin_native = 23. v1.0.0 (K10-018) — +kevin_contract +kevin_bench = 25.
901
+ // v1.1.0 (K11-007) — +kevin_forget = 26.
902
+ tool_count: 26,
875
903
  v07,
876
904
  memories_reflector: memoriesReflector,
877
905
  memories_agent: memoriesAgent,
@@ -1461,6 +1489,24 @@ export const KevinPlugin = async (input, options) => {
1461
1489
  };
1462
1490
  },
1463
1491
  }),
1492
+ // v1.1.0 (K11-007 / plan §5.1, D11-02) — kevin_forget: closes the sharing lifecycle.
1493
+ kevin_forget: tool({
1494
+ description: "Olvida memorias y publica tombstones en la capa compartida (v1.1.0, K11-005/006 / plan §5.1): dry-run por defecto — sin confirm no muta nada y devuelve el plan (archived, tombstone planned); con confirm:true archiva localmente (status='archived') y, cuando la memoria proyecta a la capa compartida (layer='shared' o shared_entry_id), publica un tombstone via el unico write path (SharedLayer.applyExport, D8-08). Segunda invocacion identica reporta noop.",
1495
+ args: {
1496
+ ids: tool.schema.array(tool.schema.string()).min(1),
1497
+ confirm: tool.schema.boolean().optional(),
1498
+ },
1499
+ async execute(args) {
1500
+ const okfPath = join(projectRoot, memoryService.getSetting("okf_path", ".kevin/knowledge.okf"));
1501
+ const result = handleForget({ ids: args.ids, confirm: args.confirm }, { store, memoryService, sharedLayer, okfPath, metrics });
1502
+ return {
1503
+ title: result.dry_run
1504
+ ? "Plan de olvido (dry-run)"
1505
+ : "Olvido aplicado",
1506
+ output: JSON.stringify(result),
1507
+ };
1508
+ },
1509
+ }),
1464
1510
  kevin_approve: tool({
1465
1511
  description: "Aprueba o rechaza una propuesta de curacion (v0.6.0). reject: marca rejected, nada toca disco. approve: aplica el diff (unico call site de ArtifactWriter.apply, D6-01), marca applied y cura las memorias contribuyentes. Solo acepta propuestas pending.",
1466
1512
  args: {
@@ -1654,15 +1700,80 @@ export const KevinPlugin = async (input, options) => {
1654
1700
  });
1655
1701
  },
1656
1702
  "chat.message": async (hookInput, output) => {
1703
+ const rawText = output.parts
1704
+ .map((p) => p)
1705
+ .filter((p) => p.type === "text")
1706
+ .map((p) => p.text ?? "")
1707
+ .join(" ");
1708
+ const trimmed = rawText.trim();
1709
+ // v1.2.0 (K12-018 / D12-09) — chat-command bridge BEFORE deriveQuery.
1710
+ // Valid commands are SWALLOWED (parts cleared) and never reach the model.
1711
+ // Must run outside perf.measure so early return actually exits the hook.
1712
+ if (trimmed.length > 0) {
1713
+ try {
1714
+ const bridgeDeps = {
1715
+ getPending: () => {
1716
+ try {
1717
+ const maybe = curator.pending;
1718
+ if (typeof maybe === "function") {
1719
+ const rows = maybe.call(curator);
1720
+ return rows.map((p) => ({
1721
+ id: p.id,
1722
+ proposedText: String(p.proposed_text ?? p.proposedText ?? ""),
1723
+ }));
1724
+ }
1725
+ }
1726
+ catch { }
1727
+ try {
1728
+ const rows = store
1729
+ .prepare("SELECT id, proposed_text FROM curation_proposals WHERE status = 'pending'")
1730
+ .all();
1731
+ return rows.map((r) => ({
1732
+ id: r.id,
1733
+ proposedText: r.proposed_text,
1734
+ }));
1735
+ }
1736
+ catch {
1737
+ return [];
1738
+ }
1739
+ },
1740
+ approve: (id) => kevinApprove(store, memoryService, curator, writer, metrics, {
1741
+ proposalId: id,
1742
+ decision: "approve",
1743
+ }),
1744
+ reject: (id, note) => kevinApprove(store, memoryService, curator, writer, metrics, {
1745
+ proposalId: id,
1746
+ decision: "reject",
1747
+ }) && void note,
1748
+ acknowledge: (conflictId) => {
1749
+ try {
1750
+ const cd = conflictDetector;
1751
+ if (typeof cd.acknowledge === "function")
1752
+ cd.acknowledge(conflictId);
1753
+ else if (typeof cd.resolve === "function")
1754
+ cd.resolve(conflictId, "a");
1755
+ }
1756
+ catch { }
1757
+ },
1758
+ metrics,
1759
+ };
1760
+ const br = handleBridgeCommand(trimmed, bridgeDeps);
1761
+ if (br.handled) {
1762
+ try {
1763
+ output.parts = [];
1764
+ }
1765
+ catch { }
1766
+ return;
1767
+ }
1768
+ }
1769
+ catch {
1770
+ // best-effort — bridge failure must not break chat flow
1771
+ }
1772
+ }
1657
1773
  perf.measure("chat.message", () => {
1658
- const text = output.parts
1659
- .map((p) => p)
1660
- .filter((p) => p.type === "text")
1661
- .map((p) => p.text ?? "")
1662
- .join(" ");
1663
- if (text.trim()) {
1774
+ if (trimmed.length > 0) {
1664
1775
  const derived = injector.deriveQuery([
1665
- { role: "user", content: text },
1776
+ { role: "user", content: rawText },
1666
1777
  ]);
1667
1778
  lastUserQuery = derived.length > 0 ? derived : null;
1668
1779
  if (hookInput.sessionID && lastUserQuery) {
@@ -1788,6 +1899,61 @@ export const KevinPlugin = async (input, options) => {
1788
1899
  // (legacy DBs pre-005 lack the recurrence_count
1789
1900
  // column)
1790
1901
  .catch(() => { }));
1902
+ // v1.2.0 (K12-007/K12-011 / D12-05) — TUI mailbox: actions→curate ordering.
1903
+ // Process mailbox BEFORE curator.propose so a fresh proposal created this idle
1904
+ // is NOT visible to a stale token (D12-04). Best-effort, never breaks idle.
1905
+ try {
1906
+ const mb = readMailbox(materializerRoot);
1907
+ if (mb.actions.length) {
1908
+ const tuiDeps = {
1909
+ getPending: () => {
1910
+ try {
1911
+ const maybe = curator.pending;
1912
+ if (typeof maybe === "function") {
1913
+ const rows = maybe.call(curator);
1914
+ return rows.map((p) => ({
1915
+ id: p.id,
1916
+ proposedText: String(p.proposed_text ?? p.proposedText ?? ""),
1917
+ }));
1918
+ }
1919
+ }
1920
+ catch { }
1921
+ try {
1922
+ const rows = store
1923
+ .prepare("SELECT id, proposed_text FROM curation_proposals WHERE status = 'pending'")
1924
+ .all();
1925
+ return rows.map((r) => ({
1926
+ id: r.id,
1927
+ proposedText: r.proposed_text,
1928
+ }));
1929
+ }
1930
+ catch {
1931
+ return [];
1932
+ }
1933
+ },
1934
+ approve: (id) => kevinApprove(store, memoryService, curator, writer, metrics, {
1935
+ proposalId: id,
1936
+ decision: "approve",
1937
+ }),
1938
+ reject: (id, _note) => kevinApprove(store, memoryService, curator, writer, metrics, {
1939
+ proposalId: id,
1940
+ decision: "reject",
1941
+ }),
1942
+ acknowledge: (conflictId) => {
1943
+ const cd = conflictDetector;
1944
+ if (typeof cd.acknowledge === "function")
1945
+ cd.acknowledge(conflictId);
1946
+ },
1947
+ metrics,
1948
+ };
1949
+ const tuiResults = processActions(mb.actions, tuiDeps);
1950
+ writeResults(materializerRoot, tuiResults);
1951
+ deleteMailbox(materializerRoot);
1952
+ }
1953
+ }
1954
+ catch {
1955
+ // best-effort
1956
+ }
1791
1957
  // v0.6.0 (K6-015 / plan §8.14) — session-idle curation
1792
1958
  // generation. Dry-run only: `propose()` calls `plan()`
1793
1959
  // and never `apply()`, so nothing here can touch disk
@@ -1829,6 +1995,145 @@ export const KevinPlugin = async (input, options) => {
1829
1995
  // best-effort, same pattern as ledger.settle — a
1830
1996
  // sync failure must not break the idle path
1831
1997
  }
1998
+ // v1.2.0 (K12-003/K12-011/K12-017 / D12-05) — snapshot flush gated by tui_snapshots_enabled.
1999
+ // Order: actions→curate→syncSharedLayer→snapshots (D12-05) — snapshots reflect post-action truth.
2000
+ try {
2001
+ if (memoryService.getSetting("tui_snapshots_enabled", "1") === "1") {
2002
+ // Proposals
2003
+ let proposals = [];
2004
+ try {
2005
+ const rawPending = (() => {
2006
+ try {
2007
+ const maybe = curator.pending;
2008
+ if (typeof maybe === "function")
2009
+ return maybe.call(curator);
2010
+ }
2011
+ catch { }
2012
+ return store
2013
+ .prepare("SELECT id, kind, target_path, proposed_text, diff, memory_id, created_at FROM curation_proposals WHERE status = 'pending' ORDER BY created_at")
2014
+ .all();
2015
+ })();
2016
+ const { proposalToken } = await import("./TuiActions.js");
2017
+ proposals = rawPending.map((r) => {
2018
+ const row = r;
2019
+ const id = String(row.id ?? "");
2020
+ const proposed = String(row.proposed_text ?? row.proposedText ?? "");
2021
+ return {
2022
+ id,
2023
+ kind: String(row.kind ?? "agents_md"),
2024
+ target_path: String(row.target_path ?? row.targetPath ?? "AGENTS.md"),
2025
+ diff: String(row.diff ?? ""),
2026
+ memory_ids: String(row.memory_id ?? row.memoryIds ?? "")
2027
+ .split(",")
2028
+ .filter((s) => s.length > 0),
2029
+ created_at: String(row.created_at ??
2030
+ row.createdAt ??
2031
+ new Date().toISOString()),
2032
+ token: proposalToken(id, proposed),
2033
+ };
2034
+ });
2035
+ }
2036
+ catch { }
2037
+ // Conflicts
2038
+ let conflicts = [];
2039
+ try {
2040
+ const maybe = conflictDetector.openConflicts;
2041
+ if (typeof maybe === "function") {
2042
+ const raw = maybe.call(conflictDetector);
2043
+ conflicts = raw.map((c) => {
2044
+ const row = c;
2045
+ return {
2046
+ id: String(row.id ?? ""),
2047
+ kind: String(row.kind ?? ""),
2048
+ a_summary: String(row.a_summary ?? row.aSummary ?? ""),
2049
+ b_summary: String(row.b_summary ?? row.bSummary ?? ""),
2050
+ opened_at: String(row.opened_at ??
2051
+ row.openedAt ??
2052
+ new Date().toISOString()),
2053
+ };
2054
+ });
2055
+ }
2056
+ }
2057
+ catch {
2058
+ conflicts = [];
2059
+ }
2060
+ // Health
2061
+ let health;
2062
+ try {
2063
+ const doc = buildDoctor(store, host, memoryService);
2064
+ let perfRows = [];
2065
+ try {
2066
+ const stats = perf.stats?.();
2067
+ if (Array.isArray(stats)) {
2068
+ perfRows = stats.map((p) => {
2069
+ const row = p;
2070
+ const budget = row.budget;
2071
+ return {
2072
+ scope: String(row.scope ?? ""),
2073
+ p95: Number(row.p95 ?? 0),
2074
+ budget_p95: Number(budget?.p95Ms ??
2075
+ row.budget_p95 ??
2076
+ row.budgetP95 ??
2077
+ 0),
2078
+ within_budget: Boolean(row.withinBudget ?? row.within_budget ?? true),
2079
+ };
2080
+ });
2081
+ }
2082
+ }
2083
+ catch {
2084
+ perfRows = [];
2085
+ }
2086
+ let digest = "unknown";
2087
+ try {
2088
+ digest = contractDigest(describeContract());
2089
+ }
2090
+ catch { }
2091
+ health = {
2092
+ verdict: doc.verdict,
2093
+ reason: doc.reason,
2094
+ hooks: doc.hooks.map((h) => ({
2095
+ hook: h.hook,
2096
+ state: h.state,
2097
+ fire_count: h.fire_count,
2098
+ expected_count: h.expected_count,
2099
+ })),
2100
+ perf: perfRows,
2101
+ contract_digest: digest,
2102
+ counters: metrics.snapshot(),
2103
+ };
2104
+ }
2105
+ catch {
2106
+ health = {
2107
+ verdict: "unknown",
2108
+ reason: "health unavailable",
2109
+ hooks: [],
2110
+ perf: [],
2111
+ contract_digest: "unknown",
2112
+ counters: {},
2113
+ };
2114
+ }
2115
+ flushSnapshots({
2116
+ root: materializerRoot,
2117
+ proposals,
2118
+ conflicts,
2119
+ health,
2120
+ metrics,
2121
+ version: KEVIN_VERSION,
2122
+ });
2123
+ try {
2124
+ writeDashboard(materializerRoot, {
2125
+ generatedAt: new Date().toISOString(),
2126
+ proposals,
2127
+ conflicts,
2128
+ health,
2129
+ });
2130
+ }
2131
+ catch { }
2132
+ }
2133
+ }
2134
+ catch {
2135
+ // best-effort — snapshot failure must not break idle
2136
+ }
1832
2137
  // v1.0.0 (K10-013 / D10-08) — the session recorded work:
1833
2138
  // arm the deferred dispose settlement. The ISO timestamp
1834
2139
  // lets the next session.start compare it against
@@ -153,6 +153,22 @@ export interface AuditReport {
153
153
  clause_count: number;
154
154
  deprecated_count: number;
155
155
  };
156
+ /**
157
+ * v1.2.0 (K12-014 / D12-??) — TUI surface block. Read-only introspection:
158
+ * enabled_setting is the kevin_settings value, last_flush_age_s is seconds
159
+ * since meta.json generatedAt (null when absent/corrupt), mailbox_depth is
160
+ * queue length (null when file missing), last_results is the parsed
161
+ * results.json array (null when absent). Dashboard age and bridge counters
162
+ * added in K12-019.
163
+ */
164
+ tui?: {
165
+ enabled_setting: string;
166
+ last_flush_age_s: number | null;
167
+ mailbox_depth: number | null;
168
+ last_results: unknown | null;
169
+ dashboard_last_write_age_s: number | null;
170
+ bridge_interceptions: number;
171
+ };
156
172
  partial: boolean;
157
173
  }
158
174
  /**
@@ -210,4 +226,4 @@ export interface TruthReport {
210
226
  is_truncated: false;
211
227
  };
212
228
  }
213
- export declare function buildAudit(store: Store, metrics: Metrics, capabilities?: Capabilities, projectId?: string, repoId?: string | null): AuditReport;
229
+ export declare function buildAudit(store: Store, metrics: Metrics, capabilities?: Capabilities, projectId?: string, repoId?: string | null, tuiRoot?: string): AuditReport;