@jmtrin/opencode-kevin 1.1.0 → 1.3.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/dist/plugin/capabilities.d.ts +2 -0
  2. package/dist/plugin/capabilities.js +3 -0
  3. package/dist/plugin/index.d.ts +5 -4
  4. package/dist/plugin/index.js +401 -108
  5. package/dist/plugin/native.d.ts +2 -2
  6. package/dist/plugin/native.js +2 -2
  7. package/package.json +56 -63
  8. package/LICENSE +0 -21
  9. package/README.md +0 -576
  10. package/dist/migrations/001_initial.sql +0 -92
  11. package/dist/migrations/002_indexes.sql +0 -14
  12. package/dist/migrations/003_v02_signal.sql +0 -58
  13. package/dist/migrations/004_v03_knowledge.sql +0 -138
  14. package/dist/migrations/005_v04_signal.sql +0 -57
  15. package/dist/migrations/006_v05_glassbox.sql +0 -118
  16. package/dist/migrations/007_v06_pull.sql +0 -145
  17. package/dist/migrations/008_v07_truth.sql +0 -124
  18. package/dist/migrations/009_v08_team.sql +0 -100
  19. package/dist/migrations/010_v09_native.sql +0 -78
  20. package/dist/migrations/011_v10_proven.sql +0 -69
  21. package/dist/migrations/012_v11_drift.sql +0 -24
  22. package/dist/plugin/Archiver.d.ts +0 -42
  23. package/dist/plugin/Archiver.js +0 -83
  24. package/dist/plugin/ArtifactWriter.d.ts +0 -93
  25. package/dist/plugin/ArtifactWriter.js +0 -291
  26. package/dist/plugin/CausalChain.d.ts +0 -22
  27. package/dist/plugin/CausalChain.js +0 -197
  28. package/dist/plugin/ConflictDetector.d.ts +0 -35
  29. package/dist/plugin/ConflictDetector.js +0 -260
  30. package/dist/plugin/ContextInjector.d.ts +0 -222
  31. package/dist/plugin/ContextInjector.js +0 -533
  32. package/dist/plugin/ConventionMiner.d.ts +0 -35
  33. package/dist/plugin/ConventionMiner.js +0 -242
  34. package/dist/plugin/Curator.d.ts +0 -116
  35. package/dist/plugin/Curator.js +0 -324
  36. package/dist/plugin/Feedback.d.ts +0 -67
  37. package/dist/plugin/Feedback.js +0 -120
  38. package/dist/plugin/HookLiveness.d.ts +0 -88
  39. package/dist/plugin/HookLiveness.js +0 -322
  40. package/dist/plugin/InjectionLedger.d.ts +0 -98
  41. package/dist/plugin/InjectionLedger.js +0 -295
  42. package/dist/plugin/LessonFixer.d.ts +0 -44
  43. package/dist/plugin/LessonFixer.js +0 -45
  44. package/dist/plugin/Materializer.d.ts +0 -83
  45. package/dist/plugin/Materializer.js +0 -181
  46. package/dist/plugin/MemoryService.d.ts +0 -360
  47. package/dist/plugin/MemoryService.js +0 -1242
  48. package/dist/plugin/Migrate.d.ts +0 -17
  49. package/dist/plugin/Migrate.js +0 -271
  50. package/dist/plugin/PatternMiner.d.ts +0 -49
  51. package/dist/plugin/PatternMiner.js +0 -132
  52. package/dist/plugin/QualityGate.d.ts +0 -124
  53. package/dist/plugin/QualityGate.js +0 -119
  54. package/dist/plugin/Reflector.d.ts +0 -91
  55. package/dist/plugin/Reflector.js +0 -307
  56. package/dist/plugin/RepoIdentity.d.ts +0 -124
  57. package/dist/plugin/RepoIdentity.js +0 -300
  58. package/dist/plugin/RepoTruth.d.ts +0 -80
  59. package/dist/plugin/RepoTruth.js +0 -599
  60. package/dist/plugin/Retrospective.d.ts +0 -17
  61. package/dist/plugin/Retrospective.js +0 -213
  62. package/dist/plugin/SharedLayer.d.ts +0 -159
  63. package/dist/plugin/SharedLayer.js +0 -462
  64. package/dist/plugin/Store.d.ts +0 -29
  65. package/dist/plugin/Store.js +0 -50
  66. package/dist/plugin/ToolCallObserver.d.ts +0 -32
  67. package/dist/plugin/ToolCallObserver.js +0 -201
  68. package/dist/plugin/columns.d.ts +0 -11
  69. package/dist/plugin/columns.js +0 -54
  70. package/dist/plugin/confidence.d.ts +0 -8
  71. package/dist/plugin/confidence.js +0 -34
  72. package/dist/plugin/contract.d.ts +0 -54
  73. package/dist/plugin/contract.js +0 -383
  74. package/dist/plugin/diff.d.ts +0 -8
  75. package/dist/plugin/diff.js +0 -182
  76. package/dist/plugin/escape.d.ts +0 -39
  77. package/dist/plugin/escape.js +0 -60
  78. package/dist/plugin/fingerprint.d.ts +0 -27
  79. package/dist/plugin/fingerprint.js +0 -73
  80. package/dist/plugin/inferability.d.ts +0 -32
  81. package/dist/plugin/inferability.js +0 -88
  82. package/dist/plugin/kevin_approve.d.ts +0 -34
  83. package/dist/plugin/kevin_approve.js +0 -52
  84. package/dist/plugin/kevin_audit.d.ts +0 -213
  85. package/dist/plugin/kevin_audit.js +0 -563
  86. package/dist/plugin/kevin_bench.d.ts +0 -10
  87. package/dist/plugin/kevin_bench.js +0 -82
  88. package/dist/plugin/kevin_conflicts.d.ts +0 -9
  89. package/dist/plugin/kevin_conflicts.js +0 -50
  90. package/dist/plugin/kevin_contract.d.ts +0 -8
  91. package/dist/plugin/kevin_contract.js +0 -59
  92. package/dist/plugin/kevin_doctor.d.ts +0 -62
  93. package/dist/plugin/kevin_doctor.js +0 -196
  94. package/dist/plugin/kevin_facts.d.ts +0 -42
  95. package/dist/plugin/kevin_facts.js +0 -36
  96. package/dist/plugin/kevin_forget.d.ts +0 -33
  97. package/dist/plugin/kevin_forget.js +0 -260
  98. package/dist/plugin/kevin_native.d.ts +0 -29
  99. package/dist/plugin/kevin_native.js +0 -79
  100. package/dist/plugin/kevin_propose.d.ts +0 -23
  101. package/dist/plugin/kevin_propose.js +0 -14
  102. package/dist/plugin/kevin_publish.d.ts +0 -38
  103. package/dist/plugin/kevin_publish.js +0 -18
  104. package/dist/plugin/kevin_why.d.ts +0 -23
  105. package/dist/plugin/kevin_why.js +0 -111
  106. package/dist/plugin/memory-format.d.ts +0 -20
  107. package/dist/plugin/memory-format.js +0 -59
  108. package/dist/plugin/metrics.d.ts +0 -97
  109. package/dist/plugin/metrics.js +0 -284
  110. package/dist/plugin/okf-export.d.ts +0 -3
  111. package/dist/plugin/okf-export.js +0 -134
  112. package/dist/plugin/okf-import.d.ts +0 -76
  113. package/dist/plugin/okf-import.js +0 -271
  114. package/dist/plugin/okf.d.ts +0 -107
  115. package/dist/plugin/okf.js +0 -303
  116. package/dist/plugin/perf.d.ts +0 -35
  117. package/dist/plugin/perf.js +0 -174
  118. package/dist/plugin/query-tokenizer.d.ts +0 -13
  119. package/dist/plugin/query-tokenizer.js +0 -133
  120. package/dist/plugin/redact.d.ts +0 -2
  121. package/dist/plugin/redact.js +0 -18
  122. package/dist/plugin/replay-types.d.ts +0 -69
  123. package/dist/plugin/replay-types.js +0 -156
  124. package/dist/plugin/replay.d.ts +0 -36
  125. package/dist/plugin/replay.js +0 -202
  126. package/dist/plugin/sqlite-adapter.d.ts +0 -12
  127. package/dist/plugin/sqlite-adapter.js +0 -81
  128. package/dist/plugin/time-ms.d.ts +0 -1
  129. package/dist/plugin/time-ms.js +0 -16
  130. package/dist/plugin/uuid.d.ts +0 -1
  131. package/dist/plugin/uuid.js +0 -50
@@ -1,50 +1,56 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
+ import { createRequire } from "node:module";
2
3
  import { homedir } from "node:os";
3
4
  import { dirname, join } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { tool } from "@opencode-ai/plugin";
6
- import { Archiver } from "./Archiver.js";
7
- import { ArtifactWriter } from "./ArtifactWriter.js";
8
- import { CausalChain } from "./CausalChain.js";
9
- import { ConflictDetector } from "./ConflictDetector.js";
10
- import { ContextInjector } from "./ContextInjector.js";
11
- import { ConventionMiner } from "./ConventionMiner.js";
12
- import { Curator } from "./Curator.js";
13
- import { Feedback } from "./Feedback.js";
14
- import { HookLiveness } from "./HookLiveness.js";
15
- import { InjectionLedger } from "./InjectionLedger.js";
16
- import { Materializer, SKILL_TOPIC } from "./Materializer.js";
17
- import { MemoryService, hasRepoIdColumn, } from "./MemoryService.js";
18
- import { Migrate } from "./Migrate.js";
19
- import { PatternMiner } from "./PatternMiner.js";
20
- import { ERROR_LINE_RE, Reflector, STRONG_ERROR_RE } from "./Reflector.js";
21
- import * as RepoIdentity from "./RepoIdentity.js";
22
- import { RepoTruth } from "./RepoTruth.js";
23
- import { Retrospective } from "./Retrospective.js";
24
- import { SharedLayer } from "./SharedLayer.js";
25
- import { Store } from "./Store.js";
26
- import { ToolCallObserver } from "./ToolCallObserver.js";
7
+ import { Archiver } from "@jmtrin/kevin-core";
8
+ import { ArtifactWriter } from "@jmtrin/kevin-core";
9
+ import { CausalChain } from "@jmtrin/kevin-core";
10
+ import { handleBridgeCommand } from "@jmtrin/kevin-core";
11
+ import { ConflictDetector } from "@jmtrin/kevin-core";
12
+ import { ContextInjector } from "@jmtrin/kevin-core";
13
+ import { ConventionMiner } from "@jmtrin/kevin-core";
14
+ import { Curator } from "@jmtrin/kevin-core";
15
+ import { writeDashboard } from "@jmtrin/kevin-core";
16
+ import { Feedback } from "@jmtrin/kevin-core";
17
+ import { HookLiveness } from "@jmtrin/kevin-core";
18
+ import { InjectionLedger } from "@jmtrin/kevin-core";
19
+ import { Materializer, SKILL_TOPIC } from "@jmtrin/kevin-core";
20
+ import { MemoryService, hasRepoIdColumn, } from "@jmtrin/kevin-core";
21
+ import { Migrate } from "@jmtrin/kevin-core";
22
+ import { PatternMiner } from "@jmtrin/kevin-core";
23
+ import { ERROR_LINE_RE, Reflector, STRONG_ERROR_RE } from "@jmtrin/kevin-core";
24
+ import * as RepoIdentity from "@jmtrin/kevin-core";
25
+ import { RepoTruth } from "@jmtrin/kevin-core";
26
+ import { Retrospective } from "@jmtrin/kevin-core";
27
+ import { SharedLayer } from "@jmtrin/kevin-core";
28
+ import { Store } from "@jmtrin/kevin-core";
29
+ import { ToolCallObserver } from "@jmtrin/kevin-core";
30
+ import { deleteMailbox, processActions, readMailbox, writeResults, } from "@jmtrin/kevin-core";
31
+ import { flushSnapshots } from "@jmtrin/kevin-core";
27
32
  import { probe } from "./capabilities.js";
28
- import { computeConfidence } from "./confidence.js";
33
+ import { computeConfidence, exportMigrationsDir, composeIdlePipeline, KEVIN_VERSION } from "@jmtrin/kevin-core";
34
+ import { contractDigest, describeContract } from "@jmtrin/kevin-core";
29
35
  import { probeHost, summarize } from "./host.js";
30
- import { kevinApprove } from "./kevin_approve.js";
31
- import { buildAudit } from "./kevin_audit.js";
32
- import { buildKevinBench } from "./kevin_bench.js";
33
- import { executeKevinConflicts } from "./kevin_conflicts.js";
34
- import { buildKevinContract } from "./kevin_contract.js";
35
- import { buildDoctor } from "./kevin_doctor.js";
36
- import { buildKevinFacts } from "./kevin_facts.js";
37
- import { handleForget } from "./kevin_forget.js";
38
- import { handleNative } from "./kevin_native.js";
39
- import { kevinPropose } from "./kevin_propose.js";
40
- import { kevinPublish } from "./kevin_publish.js";
41
- import { kevinWhy } from "./kevin_why.js";
42
- import { Metrics } from "./metrics.js";
36
+ import { kevinApprove } from "@jmtrin/kevin-core";
37
+ import { buildAudit } from "@jmtrin/kevin-core";
38
+ import { buildKevinBench } from "@jmtrin/kevin-core";
39
+ import { executeKevinConflicts } from "@jmtrin/kevin-core";
40
+ import { buildKevinContract } from "@jmtrin/kevin-core";
41
+ import { buildDoctor } from "@jmtrin/kevin-core";
42
+ import { buildKevinFacts } from "@jmtrin/kevin-core";
43
+ import { handleForget } from "@jmtrin/kevin-core";
44
+ import { handleNative } from "@jmtrin/kevin-core";
45
+ import { kevinPropose } from "@jmtrin/kevin-core";
46
+ import { kevinPublish } from "@jmtrin/kevin-core";
47
+ import { kevinWhy } from "@jmtrin/kevin-core";
48
+ import { Metrics } from "@jmtrin/kevin-core";
43
49
  import { attachNative } from "./native.js";
44
- import { exportMarkdown, exportOkf } from "./okf-export.js";
45
- import { importOkf } from "./okf-import.js";
46
- import { Perf } from "./perf.js";
47
- import { uuidv7 } from "./uuid.js";
50
+ import { exportMarkdown, exportOkf } from "@jmtrin/kevin-core";
51
+ import { importOkf } from "@jmtrin/kevin-core";
52
+ import { Perf } from "@jmtrin/kevin-core";
53
+ import { uuidv7 } from "@jmtrin/kevin-core";
48
54
  // v0.6.0 (K6-019) — process-global set of reference topics already
49
55
  // registered with the host this process. Keeps re-registration idempotent
50
56
  // across sessions within one process: a topic registered once is never
@@ -112,6 +118,11 @@ export const KEVIN_CONFIG_KEYS = [
112
118
  "perf_ring_capacity",
113
119
  "perf_flush_on_idle",
114
120
  "contract_report_enabled",
121
+ // v1.2.0 (K12-001 / plan §4) — the single setting seeded by runtime
122
+ // (no migration this release). Omitting makes `kevin_config set`
123
+ // return { error: "unknown_key" } while `kevin_config list` still
124
+ // shows it.
125
+ "tui_snapshots_enabled",
115
126
  ];
116
127
  // v1.1.0 (K11-007 / D11-08) — no new settings in 1.1.0; thresholds are constants (D11-03)
117
128
  // v0.7.0 (K7-003 / plan §5.6, D7-12) — the explicit VALUE domain for
@@ -121,11 +132,41 @@ export const KEVIN_CONFIG_KEYS = [
121
132
  // at the surface rather than silently changing every installation's
122
133
  // behaviour on the next reflection.
123
134
  export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
124
- /** Plugin release version — stamped into generated files (K8-021/027). */
125
- export const KEVIN_VERSION = "1.1.0";
135
+ /** Plugin release version — single source is @jmtrin/kevin-core (B-003 drift fix). */
136
+ export { KEVIN_VERSION };
126
137
  function resolveMigrationsDir() {
138
+ // K13-008 (D13-04): migrations now owned by @jmtrin/kevin-core.
139
+ // Explicit option is handled at call site (opts.migrationsDir ?? resolve...),
140
+ // so this helper only resolves the core location.
141
+ // 1. Installed layout: walk-up via require.resolve (reuses host.ts strategy)
142
+ try {
143
+ const pkgJson = createRequire(import.meta.url).resolve("@jmtrin/kevin-core/package.json");
144
+ const cand = join(dirname(pkgJson), "dist", "migrations");
145
+ if (existsSync(cand))
146
+ return cand;
147
+ }
148
+ catch { }
149
+ // 2. Workspace sources: delegate to core's own resolver (handles both
150
+ // src via tsx and dist after build).
151
+ try {
152
+ const dir = exportMigrationsDir();
153
+ if (existsSync(dir))
154
+ return dir;
155
+ }
156
+ catch { }
157
+ // 3. Monorepo fallback probes (dev without build)
127
158
  const here = dirname(fileURLToPath(import.meta.url));
128
- return join(here, "..", "migrations");
159
+ const candidates = [
160
+ join(here, "..", "..", "core", "dist", "migrations"),
161
+ join(here, "..", "..", "core", "migrations"),
162
+ join(here, "..", "migrations"),
163
+ join(here, "..", "..", "migrations"),
164
+ ];
165
+ for (const c of candidates) {
166
+ if (existsSync(c))
167
+ return c;
168
+ }
169
+ return join(here, "..", "..", "core", "migrations");
129
170
  }
130
171
  const REKEY_TABLES = ["memories", "shared_entries", "okf_imports"];
131
172
  export function performRekey(store, toRepoId, opts) {
@@ -258,6 +299,17 @@ export const KevinPlugin = async (input, options) => {
258
299
  const store = new Store({ path: dbPath });
259
300
  const migrationsDir = opts.migrationsDir ?? resolveMigrationsDir();
260
301
  await new Migrate(store, migrationsDir).run();
302
+ // v1.2.0 (K12-001 / D12-??) — no migration this release: ensure the
303
+ // new setting exists with its default so `kevin_config list` shows it
304
+ // on a database that was already at 012.
305
+ try {
306
+ store
307
+ .prepare("INSERT OR IGNORE INTO kevin_settings (key, value) VALUES (?, ?)")
308
+ .run("tui_snapshots_enabled", "1");
309
+ }
310
+ catch {
311
+ // pre-003 DB without kevin_settings — nothing to seed
312
+ }
261
313
  const metrics = new Metrics(store);
262
314
  // v0.4.0 (K4-019): the plugin hooks expose no project field, so the
263
315
  // project id is derived once from the plugin host's working directory
@@ -316,6 +368,8 @@ export const KevinPlugin = async (input, options) => {
316
368
  // v0.7.0 (K7-009 / plan §5.1, D7-13) — the repository truth scanner reads
317
369
  // the JSON project files. Runs once at init, gated by repo_truth_enabled.
318
370
  const projectRoot = opts.projectRoot ?? process.cwd();
371
+ const materializerRoot = opts.materializerRoot ?? join(homedir(), ".opencode-kevin");
372
+ const kevinEnv = { projectRoot, dataRoot: materializerRoot };
319
373
  const repoTruth = new RepoTruth(store, projectId, projectRoot, metrics);
320
374
  if (memoryService.getSetting("repo_truth_enabled", "0") === "1") {
321
375
  try {
@@ -355,7 +409,7 @@ export const KevinPlugin = async (input, options) => {
355
409
  const archiver = new Archiver(store, memoryService, metrics);
356
410
  const retrospective = new Retrospective(store, memoryService, {
357
411
  dir: opts.retrospectivesDir,
358
- }, metrics);
412
+ }, metrics, kevinEnv);
359
413
  const patternMiner = new PatternMiner(store, memoryService, metrics);
360
414
  const conventionMiner = new ConventionMiner(store, memoryService, projectId, metrics);
361
415
  const conflictDetector = new ConflictDetector(store, projectId, metrics, repoTruth, memoryService);
@@ -387,17 +441,20 @@ export const KevinPlugin = async (input, options) => {
387
441
  metrics.incr("shared_entries_imported", report.imported);
388
442
  return report;
389
443
  }
390
- let curator = new Curator(store, memoryService, projectId, metrics, repoId);
444
+ let curator = new Curator(store, memoryService, projectId, metrics, repoId, kevinEnv);
391
445
  // v0.6.0 (K6-017/018 / plan §5.6-5.7, D6-13) — pull-channel bundles and
392
446
  // the v2 domain probe. `probe()` runs ONCE at init and the result is
393
447
  // held; probing per-event is a hot-path cost for a value that cannot
394
448
  // change within a process (K6-016). The Materializer writes next to its
395
449
  // targets, so the bundle directories are created here at init.
396
- const materializerRoot = opts.materializerRoot ?? join(homedir(), ".opencode-kevin");
397
450
  mkdirSync(join(materializerRoot, "skills"), { recursive: true });
398
451
  mkdirSync(join(materializerRoot, "refs"), { recursive: true });
399
452
  const capabilities = probe(input);
400
- const materializer = new Materializer(store, { root: materializerRoot });
453
+ // v1.2.0 (K12-012 / D12-03) permission.ask probe (best-effort, additive).
454
+ // When the host exposes permission.ask, the presence is noted; absence is silent no-op.
455
+ // No new setting — bounded to tui_snapshots_enabled host-support check per spec.
456
+ void capabilities.permissionAsk;
457
+ const materializer = new Materializer(store, { root: materializerRoot }, kevinEnv);
401
458
  // v0.9.0 (K9-016 / plan §5.4, D9-10) — native registration replaces
402
459
  // file emission. When attachNative returns a registration for a
403
460
  // surface, the corresponding *_emission_enabled path is skipped for
@@ -977,7 +1034,7 @@ export const KevinPlugin = async (input, options) => {
977
1034
  version: KEVIN_VERSION,
978
1035
  writer,
979
1036
  });
980
- curator = new Curator(store, memoryService, projectId, metrics, live.repoId);
1037
+ curator = new Curator(store, memoryService, projectId, metrics, live.repoId, kevinEnv);
981
1038
  // v0.8.0 (BUG-003) — heal the OKF file header.
982
1039
  // Rekey changes the scope the file is written
983
1040
  // under; a stale `#repo` first line would make
@@ -1052,7 +1109,7 @@ export const KevinPlugin = async (input, options) => {
1052
1109
  // block; on a pre-008 DB the block is omitted, partial:true.
1053
1110
  // v0.8.0 (K8-007 / plan §5.7) — the penalized-memory rollup
1054
1111
  // scopes on repoId once the 009 column exists.
1055
- const report = buildAudit(store, metrics, capabilities, projectId, sessionIdentity.repoId);
1112
+ const report = buildAudit(store, metrics, capabilities, projectId, sessionIdentity.repoId, undefined, kevinEnv);
1056
1113
  const payload = args.verbose
1057
1114
  ? report
1058
1115
  : { ...report, settings: undefined };
@@ -1071,7 +1128,7 @@ export const KevinPlugin = async (input, options) => {
1071
1128
  description: "Doctor de Kevin (v0.9.0): salud del host, hooks, dependencias y registros nativos. Solo lectura, sin LLM ni writes, invocable en cualquier momento. Devuelve host (version, flavour, shell, v2), hooks ordenados dead primero, dependencies (zod_copies), native (enabled, registered/verified por surface), verdict (healthy|degraded|unknown) y reason. Output pensado para pegar en un issue: sin filesystem paths ni session ids.",
1072
1129
  args: {},
1073
1130
  async execute() {
1074
- const report = buildDoctor(store, host, memoryService);
1131
+ const report = buildDoctor(store, host, memoryService, {}, kevinEnv);
1075
1132
  return {
1076
1133
  title: "Doctor de Kevin",
1077
1134
  output: JSON.stringify(report),
@@ -1140,7 +1197,7 @@ export const KevinPlugin = async (input, options) => {
1140
1197
  .describe("status | last"),
1141
1198
  },
1142
1199
  async execute(args) {
1143
- const result = buildKevinBench({ store }, args);
1200
+ const result = buildKevinBench({ store }, args, kevinEnv);
1144
1201
  return {
1145
1202
  title: "Benchmark de Kevin",
1146
1203
  output: JSON.stringify(result),
@@ -1675,15 +1732,80 @@ export const KevinPlugin = async (input, options) => {
1675
1732
  });
1676
1733
  },
1677
1734
  "chat.message": async (hookInput, output) => {
1735
+ const rawText = output.parts
1736
+ .map((p) => p)
1737
+ .filter((p) => p.type === "text")
1738
+ .map((p) => p.text ?? "")
1739
+ .join(" ");
1740
+ const trimmed = rawText.trim();
1741
+ // v1.2.0 (K12-018 / D12-09) — chat-command bridge BEFORE deriveQuery.
1742
+ // Valid commands are SWALLOWED (parts cleared) and never reach the model.
1743
+ // Must run outside perf.measure so early return actually exits the hook.
1744
+ if (trimmed.length > 0) {
1745
+ try {
1746
+ const bridgeDeps = {
1747
+ getPending: () => {
1748
+ try {
1749
+ const maybe = curator.pending;
1750
+ if (typeof maybe === "function") {
1751
+ const rows = maybe.call(curator);
1752
+ return rows.map((p) => ({
1753
+ id: p.id,
1754
+ proposedText: String(p.proposed_text ?? p.proposedText ?? ""),
1755
+ }));
1756
+ }
1757
+ }
1758
+ catch { }
1759
+ try {
1760
+ const rows = store
1761
+ .prepare("SELECT id, proposed_text FROM curation_proposals WHERE status = 'pending'")
1762
+ .all();
1763
+ return rows.map((r) => ({
1764
+ id: r.id,
1765
+ proposedText: r.proposed_text,
1766
+ }));
1767
+ }
1768
+ catch {
1769
+ return [];
1770
+ }
1771
+ },
1772
+ approve: (id) => kevinApprove(store, memoryService, curator, writer, metrics, {
1773
+ proposalId: id,
1774
+ decision: "approve",
1775
+ }),
1776
+ reject: (id, note) => kevinApprove(store, memoryService, curator, writer, metrics, {
1777
+ proposalId: id,
1778
+ decision: "reject",
1779
+ }) && void note,
1780
+ acknowledge: (conflictId) => {
1781
+ try {
1782
+ const cd = conflictDetector;
1783
+ if (typeof cd.acknowledge === "function")
1784
+ cd.acknowledge(conflictId);
1785
+ else if (typeof cd.resolve === "function")
1786
+ cd.resolve(conflictId, "a");
1787
+ }
1788
+ catch { }
1789
+ },
1790
+ metrics,
1791
+ };
1792
+ const br = handleBridgeCommand(trimmed, bridgeDeps);
1793
+ if (br.handled) {
1794
+ try {
1795
+ output.parts = [];
1796
+ }
1797
+ catch { }
1798
+ return;
1799
+ }
1800
+ }
1801
+ catch {
1802
+ // best-effort — bridge failure must not break chat flow
1803
+ }
1804
+ }
1678
1805
  perf.measure("chat.message", () => {
1679
- const text = output.parts
1680
- .map((p) => p)
1681
- .filter((p) => p.type === "text")
1682
- .map((p) => p.text ?? "")
1683
- .join(" ");
1684
- if (text.trim()) {
1806
+ if (trimmed.length > 0) {
1685
1807
  const derived = injector.deriveQuery([
1686
- { role: "user", content: text },
1808
+ { role: "user", content: rawText },
1687
1809
  ]);
1688
1810
  lastUserQuery = derived.length > 0 ? derived : null;
1689
1811
  if (hookInput.sessionID && lastUserQuery) {
@@ -1753,62 +1875,94 @@ export const KevinPlugin = async (input, options) => {
1753
1875
  // BUG-011 — the session is done: drop the global query
1754
1876
  // so the next session cannot reuse it.
1755
1877
  lastUserQuery = null;
1756
- // v0.4.0 (K4-024) — plan §5.2: settle the session's
1757
- // unmeasured injections (effective/ineffective +
1758
- // recurrence_count charges) at idle. Best-effort: a
1759
- // legacy DB without migration 005 has no ledger table.
1760
- try {
1761
- ledger.settle(sid);
1762
- }
1763
- catch {
1764
- // best-effort: a legacy DB without the ledger
1765
- // table must not break the idle path
1766
- }
1767
- // v0.5.0 (K5-012 / plan §5.4) — retire stale lessons at
1768
- // idle; pre-006 DBs degrade to a no-op.
1769
- try {
1770
- archiver.run();
1771
- }
1772
- catch {
1773
- // best-effort, same pattern as ledger.settle
1774
- }
1775
- fireAndForget(retrospective.generate(sid));
1776
- memoryService.boostPositiveReflectors(sid);
1777
- const recurred = memoryService.penalizeRecurringReflectors(sid);
1778
- injector.setRecurrences(recurred, sid);
1779
- patternMiner.mine(projectId);
1780
- // v0.7.0 (K7-012 / plan §5.4, D7-10) convention mining is
1781
- // session.idle-only, default-off, and isolated from the rest of
1782
- // the idle chain. Mined rules still enter the ordinary Curator
1783
- // approval path; this step never writes to the repository.
1784
- try {
1785
- if (memoryService.getSetting("convention_mining_enabled", "0") ===
1786
- "1") {
1787
- const conventions = conventionMiner.mine();
1788
- conventionMiner.emit(conventions);
1789
- }
1790
- }
1791
- catch {
1792
- // A throwing miner must not reject or truncate the idle chain.
1793
- }
1794
- // v0.7.0 (K7-016 / plan §5.5, D7-06) — conflict detection is
1795
- // surfacing-only on idle. It may create/open conflict rows, but
1796
- // no idle path can acknowledge or resolve one.
1878
+ // K13-010 (D13-07) — single ORDER via composeIdlePipeline.
1879
+ // Ledger/archiver/reflectors/pattern are the hand-synced core;
1880
+ // adapter and replay both consume IDLE_STEP_ORDER so re-order is visible in both.
1881
+ await composeIdlePipeline({
1882
+ "ledger.settle": () => { ledger.settle(sid); },
1883
+ "archiver.run": () => { archiver.run(); },
1884
+ retrospective: () => {
1885
+ fireAndForget(retrospective.generate(sid));
1886
+ },
1887
+ "reflectors.boost": () => { memoryService.boostPositiveReflectors(sid); },
1888
+ "reflectors.penalize": () => {
1889
+ const recurred = memoryService.penalizeRecurringReflectors(sid);
1890
+ injector.setRecurrences(recurred, sid);
1891
+ },
1892
+ "patternMiner.mine": () => { patternMiner.mine(projectId); },
1893
+ });
1894
+ // v0.7.0 (K7-012/K7-016) + causalChain — folded into the single ORDER via composeIdlePipeline (K13-010)
1895
+ await composeIdlePipeline({
1896
+ "conventionMiner.mine": () => {
1897
+ if (memoryService.getSetting("convention_mining_enabled", "0") === "1") {
1898
+ const conventions = conventionMiner.mine();
1899
+ conventionMiner.emit(conventions);
1900
+ }
1901
+ },
1902
+ "conflictDetector.detect": () => {
1903
+ if (memoryService.getSetting("conflict_detection_enabled", "0") === "1") {
1904
+ conflictDetector.detect();
1905
+ }
1906
+ },
1907
+ "causalChain.onSessionIdle": () => {
1908
+ fireAndForget(Promise.resolve().then(() => causalChain.onSessionIdle(sid)).catch(() => { }));
1909
+ },
1910
+ });
1911
+ // v1.2.0 (K12-007/K12-011 / D12-05) — TUI mailbox: actions→curate ordering.
1912
+ // Process mailbox BEFORE curator.propose so a fresh proposal created this idle
1913
+ // is NOT visible to a stale token (D12-04). Best-effort, never breaks idle.
1797
1914
  try {
1798
- if (memoryService.getSetting("conflict_detection_enabled", "0") ===
1799
- "1") {
1800
- conflictDetector.detect();
1915
+ const mb = readMailbox(materializerRoot);
1916
+ if (mb.actions.length) {
1917
+ const tuiDeps = {
1918
+ getPending: () => {
1919
+ try {
1920
+ const maybe = curator.pending;
1921
+ if (typeof maybe === "function") {
1922
+ const rows = maybe.call(curator);
1923
+ return rows.map((p) => ({
1924
+ id: p.id,
1925
+ proposedText: String(p.proposed_text ?? p.proposedText ?? ""),
1926
+ }));
1927
+ }
1928
+ }
1929
+ catch { }
1930
+ try {
1931
+ const rows = store
1932
+ .prepare("SELECT id, proposed_text FROM curation_proposals WHERE status = 'pending'")
1933
+ .all();
1934
+ return rows.map((r) => ({
1935
+ id: r.id,
1936
+ proposedText: r.proposed_text,
1937
+ }));
1938
+ }
1939
+ catch {
1940
+ return [];
1941
+ }
1942
+ },
1943
+ approve: (id) => kevinApprove(store, memoryService, curator, writer, metrics, {
1944
+ proposalId: id,
1945
+ decision: "approve",
1946
+ }),
1947
+ reject: (id, _note) => kevinApprove(store, memoryService, curator, writer, metrics, {
1948
+ proposalId: id,
1949
+ decision: "reject",
1950
+ }),
1951
+ acknowledge: (conflictId) => {
1952
+ const cd = conflictDetector;
1953
+ if (typeof cd.acknowledge === "function")
1954
+ cd.acknowledge(conflictId);
1955
+ },
1956
+ metrics,
1957
+ };
1958
+ const tuiResults = processActions(mb.actions, tuiDeps);
1959
+ writeResults(materializerRoot, tuiResults);
1960
+ deleteMailbox(materializerRoot);
1801
1961
  }
1802
1962
  }
1803
1963
  catch {
1804
- // Conflict surfacing is best-effort and must not reject idle.
1964
+ // best-effort
1805
1965
  }
1806
- fireAndForget(Promise.resolve()
1807
- .then(() => causalChain.onSessionIdle(sid))
1808
- // non-blocking — promote is a best-effort pass
1809
- // (legacy DBs pre-005 lack the recurrence_count
1810
- // column)
1811
- .catch(() => { }));
1812
1966
  // v0.6.0 (K6-015 / plan §8.14) — session-idle curation
1813
1967
  // generation. Dry-run only: `propose()` calls `plan()`
1814
1968
  // and never `apply()`, so nothing here can touch disk
@@ -1850,6 +2004,145 @@ export const KevinPlugin = async (input, options) => {
1850
2004
  // best-effort, same pattern as ledger.settle — a
1851
2005
  // sync failure must not break the idle path
1852
2006
  }
2007
+ // v1.2.0 (K12-003/K12-011/K12-017 / D12-05) — snapshot flush gated by tui_snapshots_enabled.
2008
+ // Order: actions→curate→syncSharedLayer→snapshots (D12-05) — snapshots reflect post-action truth.
2009
+ try {
2010
+ if (memoryService.getSetting("tui_snapshots_enabled", "1") === "1") {
2011
+ // Proposals
2012
+ let proposals = [];
2013
+ try {
2014
+ const rawPending = (() => {
2015
+ try {
2016
+ const maybe = curator.pending;
2017
+ if (typeof maybe === "function")
2018
+ return maybe.call(curator);
2019
+ }
2020
+ catch { }
2021
+ return store
2022
+ .prepare("SELECT id, kind, target_path, proposed_text, diff, memory_id, created_at FROM curation_proposals WHERE status = 'pending' ORDER BY created_at")
2023
+ .all();
2024
+ })();
2025
+ const { proposalToken } = await import("@jmtrin/kevin-core");
2026
+ proposals = rawPending.map((r) => {
2027
+ const row = r;
2028
+ const id = String(row.id ?? "");
2029
+ const proposed = String(row.proposed_text ?? row.proposedText ?? "");
2030
+ return {
2031
+ id,
2032
+ kind: String(row.kind ?? "agents_md"),
2033
+ target_path: String(row.target_path ?? row.targetPath ?? "AGENTS.md"),
2034
+ diff: String(row.diff ?? ""),
2035
+ memory_ids: String(row.memory_id ?? row.memoryIds ?? "")
2036
+ .split(",")
2037
+ .filter((s) => s.length > 0),
2038
+ created_at: String(row.created_at ??
2039
+ row.createdAt ??
2040
+ new Date().toISOString()),
2041
+ token: proposalToken(id, proposed),
2042
+ };
2043
+ });
2044
+ }
2045
+ catch { }
2046
+ // Conflicts
2047
+ let conflicts = [];
2048
+ try {
2049
+ const maybe = conflictDetector.openConflicts;
2050
+ if (typeof maybe === "function") {
2051
+ const raw = maybe.call(conflictDetector);
2052
+ conflicts = raw.map((c) => {
2053
+ const row = c;
2054
+ return {
2055
+ id: String(row.id ?? ""),
2056
+ kind: String(row.kind ?? ""),
2057
+ a_summary: String(row.a_summary ?? row.aSummary ?? ""),
2058
+ b_summary: String(row.b_summary ?? row.bSummary ?? ""),
2059
+ opened_at: String(row.opened_at ??
2060
+ row.openedAt ??
2061
+ new Date().toISOString()),
2062
+ };
2063
+ });
2064
+ }
2065
+ }
2066
+ catch {
2067
+ conflicts = [];
2068
+ }
2069
+ // Health
2070
+ let health;
2071
+ try {
2072
+ const doc = buildDoctor(store, host, memoryService, {}, kevinEnv);
2073
+ let perfRows = [];
2074
+ try {
2075
+ const stats = perf.stats?.();
2076
+ if (Array.isArray(stats)) {
2077
+ perfRows = stats.map((p) => {
2078
+ const row = p;
2079
+ const budget = row.budget;
2080
+ return {
2081
+ scope: String(row.scope ?? ""),
2082
+ p95: Number(row.p95 ?? 0),
2083
+ budget_p95: Number(budget?.p95Ms ??
2084
+ row.budget_p95 ??
2085
+ row.budgetP95 ??
2086
+ 0),
2087
+ within_budget: Boolean(row.withinBudget ?? row.within_budget ?? true),
2088
+ };
2089
+ });
2090
+ }
2091
+ }
2092
+ catch {
2093
+ perfRows = [];
2094
+ }
2095
+ let digest = "unknown";
2096
+ try {
2097
+ digest = contractDigest(describeContract());
2098
+ }
2099
+ catch { }
2100
+ health = {
2101
+ verdict: doc.verdict,
2102
+ reason: doc.reason,
2103
+ hooks: doc.hooks.map((h) => ({
2104
+ hook: h.hook,
2105
+ state: h.state,
2106
+ fire_count: h.fire_count,
2107
+ expected_count: h.expected_count,
2108
+ })),
2109
+ perf: perfRows,
2110
+ contract_digest: digest,
2111
+ counters: metrics.snapshot(),
2112
+ };
2113
+ }
2114
+ catch {
2115
+ health = {
2116
+ verdict: "unknown",
2117
+ reason: "health unavailable",
2118
+ hooks: [],
2119
+ perf: [],
2120
+ contract_digest: "unknown",
2121
+ counters: {},
2122
+ };
2123
+ }
2124
+ flushSnapshots({
2125
+ root: materializerRoot,
2126
+ proposals,
2127
+ conflicts,
2128
+ health,
2129
+ metrics,
2130
+ version: KEVIN_VERSION,
2131
+ });
2132
+ try {
2133
+ writeDashboard(materializerRoot, {
2134
+ generatedAt: new Date().toISOString(),
2135
+ proposals,
2136
+ conflicts,
2137
+ health,
2138
+ });
2139
+ }
2140
+ catch { }
2141
+ }
2142
+ }
2143
+ catch {
2144
+ // best-effort — snapshot failure must not break idle
2145
+ }
1853
2146
  // v1.0.0 (K10-013 / D10-08) — the session recorded work:
1854
2147
  // arm the deferred dispose settlement. The ISO timestamp
1855
2148
  // lets the next session.start compare it against
@@ -1,5 +1,5 @@
1
- import { type Materializer } from "./Materializer.js";
2
- import type { Store } from "./Store.js";
1
+ import { type Materializer } from "@jmtrin/kevin-core";
2
+ import type { Store } from "@jmtrin/kevin-core";
3
3
  import type { HostSurface } from "./host.js";
4
4
  /** The surface `attachNative()` returned. `verified` is filled by the
5
5
  * read-back checks (K9-014/015) once the host ran `setup()`. */