@kontextmind/kxm 0.6.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,1560 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, lstatSync, mkdirSync, realpathSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { VnextConfigError, validateRunEvent, vnextCanonicalJson, type JsonValue, type VnextConfigIssue, type VnextConfigOptions } from "./vnext-config.ts";
6
+ import { vnextUserStateRoot } from "./vnext-bindings.ts";
7
+
8
+ /* ------------------------------------------------------------------ *
9
+ * Runtime registry (per-user, platform state root)
10
+ * ------------------------------------------------------------------ */
11
+
12
+ export interface VnextRuntimePaths {
13
+ stateRoot: string;
14
+ runtimeDir: string;
15
+ registryDb: string;
16
+ projectsDir: string;
17
+ }
18
+
19
+ export function vnextRuntimePaths(options: { stateRoot?: string; env?: NodeJS.ProcessEnv; homeDir?: string } = {}): VnextRuntimePaths {
20
+ const stateRoot = options.stateRoot
21
+ ? resolve(options.stateRoot)
22
+ : vnextUserStateRoot({ ...(options.env ? { env: options.env } : {}), ...(options.homeDir ? { homeDir: options.homeDir } : {}) });
23
+ const runtimeDir = join(stateRoot, "runtime");
24
+ return {
25
+ stateRoot,
26
+ runtimeDir,
27
+ registryDb: join(runtimeDir, "registry.db"),
28
+ projectsDir: join(runtimeDir, "projects"),
29
+ };
30
+ }
31
+
32
+ function runtimeIssue(phase: VnextConfigIssue["phase"], code: string, file: string, message: string): VnextConfigIssue {
33
+ return { phase, code, file, message };
34
+ }
35
+
36
+ export function runtimeError(code: string, file: string, message: string): VnextConfigError {
37
+ return new VnextConfigError([runtimeIssue("semantic", code, file, message)]);
38
+ }
39
+
40
+ export function projectRuntimeKey(projectRoot: string): string {
41
+ // Canonicalize through the filesystem like the repository binding store so
42
+ // reaching a project through a link cannot mint a second key for it.
43
+ let canonical: string;
44
+ try {
45
+ canonical = realpathSync.native(resolve(projectRoot));
46
+ } catch {
47
+ canonical = resolve(projectRoot);
48
+ }
49
+ const folded = process.platform === "win32" ? canonical.toLocaleLowerCase("en-US") : canonical;
50
+ return createHash("sha256").update(folded, "utf8").digest("hex").slice(0, 24);
51
+ }
52
+
53
+ function checkedParent(path: string, description: string): void {
54
+ const parent = dirname(path);
55
+ if (!existsSync(parent)) mkdirSync(parent, { recursive: true, mode: 0o700 });
56
+ const stat = lstatSync(parent, { throwIfNoEntry: false });
57
+ if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
58
+ throw runtimeError("runtime_path_invalid", description, `${description} parent must be a regular directory, not a link`);
59
+ }
60
+ }
61
+
62
+ import {
63
+ openDatabase,
64
+ withDatabaseTransaction,
65
+ type DatabaseSchemaSpec,
66
+ } from "./database.ts";
67
+
68
+ export type VnextDatabaseSchema = DatabaseSchemaSpec;
69
+ export { openDatabase };
70
+
71
+ /* ----------------------------- registry ---------------------------- */
72
+
73
+ export interface VnextSupervisorRecord {
74
+ runtimeId: string;
75
+ pid: number;
76
+ port: number;
77
+ tokenHash: string;
78
+ startedAt: string;
79
+ heartbeatAt: string;
80
+ state: "starting" | "running" | "stopping" | "stopped";
81
+ }
82
+
83
+ export interface VnextProjectRegistration {
84
+ projectId: string;
85
+ projectRoot: string;
86
+ projectKey: string;
87
+ homeRuntimeId: string;
88
+ configRevision?: string;
89
+ registeredAt: string;
90
+ }
91
+
92
+ export const VNEXT_REGISTRY_SCHEMA_VERSION = 1;
93
+
94
+ const REGISTRY_TABLES = {
95
+ supervisor: ["singleton_id", "runtime_id", "pid", "port", "token_hash", "started_at", "heartbeat_at", "state"],
96
+ projects: ["project_id", "project_root", "project_key", "home_runtime_id", "config_revision", "registered_at"],
97
+ } as const;
98
+
99
+ const REGISTRY_SCHEMA = `
100
+ CREATE TABLE supervisor (
101
+ singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
102
+ runtime_id TEXT NOT NULL,
103
+ pid INTEGER NOT NULL,
104
+ port INTEGER NOT NULL,
105
+ token_hash TEXT NOT NULL,
106
+ started_at TEXT NOT NULL,
107
+ heartbeat_at TEXT NOT NULL,
108
+ state TEXT NOT NULL
109
+ ) STRICT;
110
+ CREATE TABLE projects (
111
+ project_id TEXT PRIMARY KEY,
112
+ project_root TEXT NOT NULL,
113
+ project_key TEXT NOT NULL UNIQUE,
114
+ home_runtime_id TEXT NOT NULL,
115
+ config_revision TEXT,
116
+ registered_at TEXT NOT NULL
117
+ ) STRICT;
118
+ `;
119
+
120
+ export class VnextRuntimeRegistry {
121
+ readonly path: string;
122
+ private readonly database: DatabaseSync;
123
+
124
+ constructor(path: string) {
125
+ this.path = resolve(path);
126
+ this.database = openDatabase(this.path, "runtime registry", {
127
+ schema: REGISTRY_SCHEMA,
128
+ version: VNEXT_REGISTRY_SCHEMA_VERSION,
129
+ tables: REGISTRY_TABLES,
130
+ });
131
+ }
132
+
133
+ close(): void {
134
+ this.database.close();
135
+ }
136
+
137
+ /** Atomically claim or refresh the supervisor singleton. Returns the record that now owns it. */
138
+ claimSupervisor(record: { runtimeId: string; pid: number; port: number; tokenHash: string; now: string }): { claimed: boolean; record: VnextSupervisorRecord } {
139
+ this.database.exec("BEGIN IMMEDIATE");
140
+ try {
141
+ const existing = this.readSupervisorRow();
142
+ if (!existing) {
143
+ this.database.prepare(`
144
+ INSERT INTO supervisor (singleton_id, runtime_id, pid, port, token_hash, started_at, heartbeat_at, state)
145
+ VALUES (1, ?, ?, ?, ?, ?, ?, 'running')
146
+ `).run(record.runtimeId, record.pid, record.port, record.tokenHash, record.now, record.now);
147
+ this.database.exec("COMMIT");
148
+ return { claimed: true, record: this.supervisor()! };
149
+ }
150
+ if (existing.runtimeId === record.runtimeId && existing.pid === record.pid) {
151
+ this.database.prepare("UPDATE supervisor SET heartbeat_at = ?, state = 'running' WHERE singleton_id = 1").run(record.now);
152
+ this.database.exec("COMMIT");
153
+ return { claimed: true, record: this.supervisor()! };
154
+ }
155
+ this.database.exec("ROLLBACK");
156
+ return { claimed: false, record: existing };
157
+ } catch (error) {
158
+ try { this.database.exec("ROLLBACK"); } catch { /* already rolled back */ }
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ /** Take over a dead supervisor's singleton, preserving its logical runtime identity and project home bindings. */
164
+ takeoverSupervisor(record: { runtimeId: string; pid: number; port: number; tokenHash: string; now: string; observedDeadPid: number; observedHeartbeatAt: string }): VnextSupervisorRecord {
165
+ this.database.exec("BEGIN IMMEDIATE");
166
+ try {
167
+ // The logical runtime id is stable across process restarts: the new
168
+ // process inherits it so immutable homeRuntimeId bindings stay valid.
169
+ // The takeover applies only to the exact record we observed as dead.
170
+ const existing = this.readSupervisorRow();
171
+ const runtimeId = existing?.runtimeId ?? record.runtimeId;
172
+ // True compare-and-swap: the takeover applies only if the observed dead
173
+ // record (pid AND heartbeat) is unchanged. A refreshed incumbent wins.
174
+ const updated = this.database.prepare(`
175
+ UPDATE supervisor
176
+ SET runtime_id = ?, pid = ?, port = ?, token_hash = ?, started_at = ?, heartbeat_at = ?, state = 'running'
177
+ WHERE singleton_id = 1 AND pid = ? AND heartbeat_at = ?
178
+ `).run(runtimeId, record.pid, record.port, record.tokenHash, record.now, record.now, record.observedDeadPid, record.observedHeartbeatAt);
179
+ if (updated.changes !== 1) {
180
+ this.database.exec("ROLLBACK");
181
+ throw runtimeError("runtime_supervisor_conflict", "supervisor", "another process claimed the supervisor singleton first");
182
+ }
183
+ this.database.exec("COMMIT");
184
+ return this.supervisor()!;
185
+ } catch (error) {
186
+ try { this.database.exec("ROLLBACK"); } catch { /* already rolled back */ }
187
+ throw error;
188
+ }
189
+ }
190
+
191
+ markStopping(pid: number, now: string): void {
192
+ // Only the owning process may mark the record; a losing takeover must
193
+ // never clobber the winner's liveness.
194
+ this.database.prepare("UPDATE supervisor SET state = 'stopping', heartbeat_at = ? WHERE singleton_id = 1 AND pid = ?").run(now, pid);
195
+ }
196
+
197
+ markStopped(pid: number, now: string): void {
198
+ this.database.prepare("UPDATE supervisor SET state = 'stopped', heartbeat_at = ? WHERE singleton_id = 1 AND pid = ?").run(now, pid);
199
+ }
200
+
201
+ heartbeat(pid: number, now: string): number {
202
+ const result = this.database.prepare("UPDATE supervisor SET heartbeat_at = ? WHERE singleton_id = 1 AND pid = ?").run(now, pid);
203
+ return Number(result.changes);
204
+ }
205
+
206
+ private readSupervisorRow(): VnextSupervisorRecord | undefined {
207
+ const row = this.database.prepare("SELECT runtime_id, pid, port, token_hash, started_at, heartbeat_at, state FROM supervisor WHERE singleton_id = 1").get() as
208
+ | { runtime_id: string; pid: number; port: number; token_hash: string; started_at: string; heartbeat_at: string; state: VnextSupervisorRecord["state"] }
209
+ | undefined;
210
+ return row
211
+ ? {
212
+ runtimeId: row.runtime_id,
213
+ pid: row.pid,
214
+ port: row.port,
215
+ tokenHash: row.token_hash,
216
+ startedAt: row.started_at,
217
+ heartbeatAt: row.heartbeat_at,
218
+ state: row.state,
219
+ }
220
+ : undefined;
221
+ }
222
+
223
+ supervisor(): VnextSupervisorRecord | undefined {
224
+ return this.readSupervisorRow();
225
+ }
226
+
227
+ /** Register or revalidate a project's home binding. Home Runtime is immutable. */
228
+ registerProject(registration: { projectId: string; projectRoot: string; homeRuntimeId: string; configRevision?: string; now: string }): VnextProjectRegistration {
229
+ const projectRoot = resolve(registration.projectRoot);
230
+ const projectKey = projectRuntimeKey(projectRoot);
231
+ this.database.exec("BEGIN IMMEDIATE");
232
+ try {
233
+ const byId = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_id = ?").get(registration.projectId) as
234
+ | { project_id: string; project_root: string; project_key: string; home_runtime_id: string; config_revision: string | null; registered_at: string }
235
+ | undefined;
236
+ const byKey = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_key = ?").get(projectKey) as
237
+ | { project_id: string; project_root: string; project_key: string; home_runtime_id: string; config_revision: string | null; registered_at: string }
238
+ | undefined;
239
+ const existing = byId ?? byKey;
240
+ if (existing) {
241
+ const problems: string[] = [];
242
+ if (byId && byId.project_key !== projectKey) problems.push(`project ${registration.projectId} is already bound to a different control root`);
243
+ if (byKey && byKey.project_id !== registration.projectId) problems.push(`control root is already bound to a different project id ${byKey.project_id}`);
244
+ if (existing.home_runtime_id !== registration.homeRuntimeId) problems.push(`project ${registration.projectId} home runtime is immutable and cannot be rebound`);
245
+ if (problems.length > 0) {
246
+ this.database.exec("ROLLBACK");
247
+ throw runtimeError("project_home_conflict", ".kxm/project.yaml", problems.join("; "));
248
+ }
249
+ const result: VnextProjectRegistration = {
250
+ projectId: existing.project_id,
251
+ projectRoot: existing.project_root,
252
+ projectKey: existing.project_key,
253
+ homeRuntimeId: existing.home_runtime_id,
254
+ ...(existing.config_revision !== null ? { configRevision: existing.config_revision } : {}),
255
+ registeredAt: existing.registered_at,
256
+ };
257
+ this.database.exec("COMMIT");
258
+ return result;
259
+ }
260
+ this.database.prepare(`
261
+ INSERT INTO projects (project_id, project_root, project_key, home_runtime_id, config_revision, registered_at)
262
+ VALUES (?, ?, ?, ?, ?, ?)
263
+ `).run(registration.projectId, projectRoot, projectKey, registration.homeRuntimeId, registration.configRevision ?? null, registration.now);
264
+ this.database.exec("COMMIT");
265
+ return {
266
+ projectId: registration.projectId,
267
+ projectRoot,
268
+ projectKey,
269
+ homeRuntimeId: registration.homeRuntimeId,
270
+ ...(registration.configRevision !== undefined ? { configRevision: registration.configRevision } : {}),
271
+ registeredAt: registration.now,
272
+ };
273
+ } catch (error) {
274
+ try { this.database.exec("ROLLBACK"); } catch { /* already rolled back */ }
275
+ throw error;
276
+ }
277
+ }
278
+
279
+ project(projectId: string): VnextProjectRegistration | undefined {
280
+ const row = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_id = ?").get(projectId) as
281
+ | { project_id: string; project_root: string; project_key: string; home_runtime_id: string; config_revision: string | null; registered_at: string }
282
+ | undefined;
283
+ return row
284
+ ? {
285
+ projectId: row.project_id,
286
+ projectRoot: row.project_root,
287
+ projectKey: row.project_key,
288
+ homeRuntimeId: row.home_runtime_id,
289
+ ...(row.config_revision !== null ? { configRevision: row.config_revision } : {}),
290
+ registeredAt: row.registered_at,
291
+ }
292
+ : undefined;
293
+ }
294
+
295
+ projectByRoot(projectRoot: string): VnextProjectRegistration | undefined {
296
+ const key = projectRuntimeKey(projectRoot);
297
+ const row = this.database.prepare("SELECT project_id, project_root, project_key, home_runtime_id, config_revision, registered_at FROM projects WHERE project_key = ?").get(key) as
298
+ | { project_id: string; project_root: string; project_key: string; home_runtime_id: string; config_revision: string | null; registered_at: string }
299
+ | undefined;
300
+ return row
301
+ ? {
302
+ projectId: row.project_id,
303
+ projectRoot: row.project_root,
304
+ projectKey: row.project_key,
305
+ homeRuntimeId: row.home_runtime_id,
306
+ ...(row.config_revision !== null ? { configRevision: row.config_revision } : {}),
307
+ registeredAt: row.registered_at,
308
+ }
309
+ : undefined;
310
+ }
311
+ }
312
+
313
+ /* --------------------------- event store --------------------------- */
314
+
315
+ export type VnextRunStatus = "created" | "preparing" | "running" | "waiting" | "blocked_uncertain" | "cancelling" | "cancelled" | "completed" | "failed";
316
+
317
+ export const VNEXT_RUN_EVENT_SCHEMA = "kxm.run-event.v1";
318
+ export const VNEXT_ABSENT_MEMORY_REVISION = "ctxrev_absent";
319
+
320
+ export interface VnextRunRecord {
321
+ runId: string;
322
+ projectId: string;
323
+ homeRuntimeId: string;
324
+ workflowId: string;
325
+ promptSha256: string;
326
+ status: VnextRunStatus;
327
+ configRevision: string;
328
+ memoryRevision: string;
329
+ executorPolicyRevision: string;
330
+ toolPolicyRevision: string;
331
+ createdAt: string;
332
+ updatedAt: string;
333
+ }
334
+
335
+ export interface VnextRunEvent {
336
+ schema: typeof VNEXT_RUN_EVENT_SCHEMA;
337
+ eventId: string;
338
+ eventType: string;
339
+ projectId: string;
340
+ runId: string;
341
+ homeRuntimeId: string;
342
+ sequence: number;
343
+ commandId?: string;
344
+ occurredAt: string;
345
+ recordedAt: string;
346
+ monotonicNs: string;
347
+ configRevision: string;
348
+ memoryRevision: string;
349
+ executorPolicyRevision: string;
350
+ toolPolicyRevision: string;
351
+ payload: Record<string, unknown>;
352
+ }
353
+
354
+ export interface VnextRunPlanRow {
355
+ runId: string;
356
+ runPlanHash: string;
357
+ envelope: string;
358
+ pinnedSequence: number;
359
+ }
360
+
361
+ export interface VnextRunStateRow {
362
+ runId: string;
363
+ lastSequence: number;
364
+ state: string;
365
+ }
366
+
367
+ export interface VnextAttemptCapabilityRow {
368
+ attemptId: string;
369
+ runId: string;
370
+ assignmentId: string;
371
+ stepId: string;
372
+ stepAttempt: number;
373
+ producerId: string;
374
+ capabilityHash: string;
375
+ state: "issued" | "settled" | "revoked";
376
+ }
377
+
378
+ export type VnextGateKind = "command" | "artifacts-exist";
379
+ export type VnextGateExpect = "pass" | "fail";
380
+ export type VnextGateCompleteness = "complete" | "incomplete" | "no-start";
381
+ export type VnextGateStopCause = "none" | "timeout" | "cancel" | "error";
382
+ export type VnextGateSignalsAttempted = "none" | "term" | "term-kill";
383
+ export type VnextGateErrorClass = "validation" | "spawn-error" | "stream-error" | "stop-error" | "recording-error" | "lost-close";
384
+ export type VnextGateEvidenceOutcome = "passed" | "implementation-failure" | "repro-missing";
385
+
386
+ export interface VnextGateAttemptRow {
387
+ attemptId: string;
388
+ runId: string;
389
+ projectId: string;
390
+ homeRuntimeId: string;
391
+ stepId: string;
392
+ stepAttempt: number;
393
+ assignmentId: string;
394
+ effectId: string;
395
+ gateId: string;
396
+ gateKind: VnextGateKind;
397
+ expect: VnextGateExpect;
398
+ gateDefinitionHash: string;
399
+ registryHash: string;
400
+ runPlanHash: string;
401
+ controlProjectKey: string;
402
+ producerId: "kxm-gate";
403
+ intentEventId: string;
404
+ contentHash: string;
405
+ }
406
+
407
+ export interface VnextGateObservationRow {
408
+ observationId: string;
409
+ attemptId: string;
410
+ runId: string;
411
+ projectId: string;
412
+ homeRuntimeId: string;
413
+ stepId: string;
414
+ stepAttempt: number;
415
+ assignmentId: string;
416
+ effectId: string;
417
+ completeness: VnextGateCompleteness;
418
+ spawned: 0 | 1;
419
+ pid: number | null;
420
+ exitCode: number | null;
421
+ signal: string | null;
422
+ exitObserved: 0 | 1;
423
+ closeObserved: 0 | 1;
424
+ stopCause: VnextGateStopCause;
425
+ signalsAttempted: VnextGateSignalsAttempted;
426
+ errorClass: VnextGateErrorClass | null;
427
+ stdoutSha256: string | null;
428
+ stdoutBytes: number | null;
429
+ stdoutComplete: 0 | 1 | null;
430
+ stderrSha256: string | null;
431
+ stderrBytes: number | null;
432
+ stderrComplete: 0 | 1 | null;
433
+ checkedCount: number | null;
434
+ failedCount: number | null;
435
+ elapsedMs: number;
436
+ startedAt: string;
437
+ finishedAt: string | null;
438
+ recordedEventId: string;
439
+ contentHash: string;
440
+ }
441
+
442
+ export interface VnextGateEvidenceRow {
443
+ evidenceId: string;
444
+ attemptId: string;
445
+ observationId: string;
446
+ runId: string;
447
+ projectId: string;
448
+ homeRuntimeId: string;
449
+ stepId: string;
450
+ stepAttempt: number;
451
+ assignmentId: string;
452
+ effectId: string;
453
+ evidenceKey: string | null;
454
+ kind: "gate";
455
+ expect: VnextGateExpect;
456
+ outcome: VnextGateEvidenceOutcome;
457
+ settledEventId: string;
458
+ contentHash: string;
459
+ }
460
+
461
+ export interface VnextGateRowCounts {
462
+ attempts: number;
463
+ observations: number;
464
+ evidence: number;
465
+ }
466
+
467
+ export interface VnextCommandRecord {
468
+ commandId: string;
469
+ runId: string;
470
+ kind: string;
471
+ result: Record<string, unknown>;
472
+ recordedAt: string;
473
+ }
474
+
475
+ export const VNEXT_EVENT_STORE_SCHEMA_VERSION = 3;
476
+
477
+ const EVENT_STORE_TABLES = {
478
+ runs: ["run_id", "project_id", "home_runtime_id", "workflow_id", "prompt_sha256", "status", "config_revision", "memory_revision", "executor_policy_revision", "tool_policy_revision", "created_at", "updated_at"],
479
+ events: ["project_id", "run_id", "sequence", "event_id", "event_type", "command_id", "occurred_at", "recorded_at", "monotonic_ns", "config_revision", "memory_revision", "executor_policy_revision", "tool_policy_revision", "payload", "schema", "home_runtime_id"],
480
+ commands: ["command_id", "run_id", "kind", "result", "recorded_at"],
481
+ run_plans: ["run_id", "run_plan_hash", "envelope", "pinned_sequence"],
482
+ run_state: ["run_id", "last_sequence", "state"],
483
+ attempt_capabilities: ["attempt_id", "run_id", "assignment_id", "step_id", "step_attempt", "producer_id", "capability_hash", "state"],
484
+ gate_attempts: [
485
+ "attempt_id", "run_id", "project_id", "home_runtime_id", "step_id", "step_attempt", "assignment_id", "effect_id",
486
+ "gate_id", "gate_kind", "expect", "gate_definition_hash", "registry_hash", "run_plan_hash", "control_project_key",
487
+ "producer_id", "intent_event_id", "content_hash",
488
+ ],
489
+ gate_observations: [
490
+ "observation_id", "attempt_id", "run_id", "project_id", "home_runtime_id", "step_id", "step_attempt", "assignment_id",
491
+ "effect_id", "completeness", "spawned", "pid", "exit_code", "signal", "exit_observed", "close_observed", "stop_cause",
492
+ "signals_attempted", "error_class", "stdout_sha256", "stdout_bytes", "stdout_complete", "stderr_sha256", "stderr_bytes",
493
+ "stderr_complete", "checked_count", "failed_count", "elapsed_ms", "started_at", "finished_at", "recorded_event_id",
494
+ "content_hash",
495
+ ],
496
+ gate_evidence: [
497
+ "evidence_id", "attempt_id", "observation_id", "run_id", "project_id", "home_runtime_id", "step_id", "step_attempt",
498
+ "assignment_id", "effect_id", "evidence_key", "kind", "expect", "outcome", "settled_event_id", "content_hash",
499
+ ],
500
+ } as const;
501
+
502
+ const EVENT_STORE_SCHEMA = `
503
+ CREATE TABLE runs (
504
+ run_id TEXT PRIMARY KEY,
505
+ project_id TEXT NOT NULL,
506
+ home_runtime_id TEXT NOT NULL,
507
+ workflow_id TEXT NOT NULL,
508
+ prompt_sha256 TEXT NOT NULL,
509
+ status TEXT NOT NULL,
510
+ config_revision TEXT NOT NULL,
511
+ memory_revision TEXT NOT NULL,
512
+ executor_policy_revision TEXT NOT NULL,
513
+ tool_policy_revision TEXT NOT NULL,
514
+ created_at TEXT NOT NULL,
515
+ updated_at TEXT NOT NULL
516
+ ) STRICT;
517
+ CREATE TABLE events (
518
+ project_id TEXT NOT NULL,
519
+ run_id TEXT NOT NULL,
520
+ sequence INTEGER NOT NULL,
521
+ event_id TEXT NOT NULL UNIQUE,
522
+ event_type TEXT NOT NULL,
523
+ command_id TEXT,
524
+ occurred_at TEXT NOT NULL,
525
+ recorded_at TEXT NOT NULL,
526
+ monotonic_ns TEXT NOT NULL,
527
+ config_revision TEXT NOT NULL,
528
+ memory_revision TEXT NOT NULL,
529
+ executor_policy_revision TEXT NOT NULL,
530
+ tool_policy_revision TEXT NOT NULL,
531
+ payload TEXT NOT NULL,
532
+ schema TEXT NOT NULL,
533
+ home_runtime_id TEXT NOT NULL,
534
+ PRIMARY KEY (project_id, run_id, sequence)
535
+ ) STRICT;
536
+ CREATE INDEX events_run ON events(run_id, sequence);
537
+ CREATE TABLE commands (
538
+ command_id TEXT PRIMARY KEY,
539
+ run_id TEXT NOT NULL,
540
+ kind TEXT NOT NULL,
541
+ result TEXT NOT NULL,
542
+ recorded_at TEXT NOT NULL
543
+ ) STRICT;
544
+ CREATE TABLE run_plans (
545
+ run_id TEXT PRIMARY KEY REFERENCES runs(run_id),
546
+ run_plan_hash TEXT NOT NULL,
547
+ envelope TEXT NOT NULL,
548
+ pinned_sequence INTEGER NOT NULL
549
+ ) STRICT;
550
+ CREATE TABLE run_state (
551
+ run_id TEXT PRIMARY KEY REFERENCES runs(run_id),
552
+ last_sequence INTEGER NOT NULL,
553
+ state TEXT NOT NULL
554
+ ) STRICT;
555
+ CREATE TABLE attempt_capabilities (
556
+ attempt_id TEXT PRIMARY KEY,
557
+ run_id TEXT NOT NULL,
558
+ assignment_id TEXT NOT NULL,
559
+ step_id TEXT NOT NULL,
560
+ step_attempt INTEGER NOT NULL,
561
+ producer_id TEXT NOT NULL,
562
+ capability_hash TEXT NOT NULL UNIQUE,
563
+ state TEXT NOT NULL CHECK (state IN ('issued','settled','revoked'))
564
+ ) STRICT;
565
+ CREATE TABLE gate_attempts (
566
+ attempt_id TEXT PRIMARY KEY REFERENCES attempt_capabilities(attempt_id),
567
+ run_id TEXT NOT NULL REFERENCES runs(run_id),
568
+ project_id TEXT NOT NULL,
569
+ home_runtime_id TEXT NOT NULL,
570
+ step_id TEXT NOT NULL,
571
+ step_attempt INTEGER NOT NULL,
572
+ assignment_id TEXT NOT NULL,
573
+ effect_id TEXT NOT NULL UNIQUE,
574
+ gate_id TEXT NOT NULL,
575
+ gate_kind TEXT NOT NULL CHECK (gate_kind IN ('command','artifacts-exist')),
576
+ expect TEXT NOT NULL CHECK (expect IN ('pass','fail')),
577
+ gate_definition_hash TEXT NOT NULL,
578
+ registry_hash TEXT NOT NULL,
579
+ run_plan_hash TEXT NOT NULL,
580
+ control_project_key TEXT NOT NULL,
581
+ producer_id TEXT NOT NULL CHECK (producer_id = 'kxm-gate'),
582
+ intent_event_id TEXT NOT NULL UNIQUE REFERENCES events(event_id),
583
+ content_hash TEXT NOT NULL
584
+ ) STRICT;
585
+ CREATE INDEX gate_attempts_run ON gate_attempts(run_id);
586
+ CREATE INDEX gate_attempts_project ON gate_attempts(project_id);
587
+ CREATE TABLE gate_observations (
588
+ observation_id TEXT PRIMARY KEY,
589
+ attempt_id TEXT NOT NULL UNIQUE REFERENCES gate_attempts(attempt_id),
590
+ run_id TEXT NOT NULL REFERENCES runs(run_id),
591
+ project_id TEXT NOT NULL,
592
+ home_runtime_id TEXT NOT NULL,
593
+ step_id TEXT NOT NULL,
594
+ step_attempt INTEGER NOT NULL,
595
+ assignment_id TEXT NOT NULL,
596
+ effect_id TEXT NOT NULL,
597
+ completeness TEXT NOT NULL CHECK (completeness IN ('complete','incomplete','no-start')),
598
+ spawned INTEGER NOT NULL CHECK (spawned IN (0, 1)),
599
+ pid INTEGER,
600
+ exit_code INTEGER,
601
+ signal TEXT,
602
+ exit_observed INTEGER NOT NULL CHECK (exit_observed IN (0, 1)),
603
+ close_observed INTEGER NOT NULL CHECK (close_observed IN (0, 1)),
604
+ stop_cause TEXT NOT NULL CHECK (stop_cause IN ('none','timeout','cancel','error')),
605
+ signals_attempted TEXT NOT NULL CHECK (signals_attempted IN ('none','term','term-kill')),
606
+ error_class TEXT CHECK (error_class IN ('validation','spawn-error','stream-error','stop-error','recording-error','lost-close')),
607
+ stdout_sha256 TEXT,
608
+ stdout_bytes INTEGER,
609
+ stdout_complete INTEGER CHECK (stdout_complete IN (0, 1)),
610
+ stderr_sha256 TEXT,
611
+ stderr_bytes INTEGER,
612
+ stderr_complete INTEGER CHECK (stderr_complete IN (0, 1)),
613
+ checked_count INTEGER,
614
+ failed_count INTEGER,
615
+ elapsed_ms INTEGER NOT NULL,
616
+ started_at TEXT NOT NULL,
617
+ finished_at TEXT,
618
+ recorded_event_id TEXT NOT NULL UNIQUE REFERENCES events(event_id),
619
+ content_hash TEXT NOT NULL
620
+ ) STRICT;
621
+ CREATE INDEX gate_observations_run ON gate_observations(run_id);
622
+ CREATE TABLE gate_evidence (
623
+ evidence_id TEXT PRIMARY KEY,
624
+ attempt_id TEXT NOT NULL UNIQUE REFERENCES gate_attempts(attempt_id),
625
+ observation_id TEXT NOT NULL UNIQUE REFERENCES gate_observations(observation_id),
626
+ run_id TEXT NOT NULL REFERENCES runs(run_id),
627
+ project_id TEXT NOT NULL,
628
+ home_runtime_id TEXT NOT NULL,
629
+ step_id TEXT NOT NULL,
630
+ step_attempt INTEGER NOT NULL,
631
+ assignment_id TEXT NOT NULL,
632
+ effect_id TEXT NOT NULL,
633
+ evidence_key TEXT,
634
+ kind TEXT NOT NULL CHECK (kind = 'gate'),
635
+ expect TEXT NOT NULL CHECK (expect IN ('pass','fail')),
636
+ outcome TEXT NOT NULL CHECK (outcome IN ('passed','implementation-failure','repro-missing')),
637
+ settled_event_id TEXT NOT NULL UNIQUE REFERENCES events(event_id),
638
+ content_hash TEXT NOT NULL
639
+ ) STRICT;
640
+ CREATE UNIQUE INDEX gate_evidence_key ON gate_evidence(run_id, step_id, step_attempt, evidence_key) WHERE evidence_key IS NOT NULL;
641
+ CREATE INDEX gate_evidence_run ON gate_evidence(run_id);
642
+ `;
643
+
644
+ export class VnextRunEventStore {
645
+ readonly path: string;
646
+ private readonly database: DatabaseSync;
647
+
648
+ constructor(path: string) {
649
+ this.path = resolve(path);
650
+ this.database = openDatabase(this.path, "run event store", {
651
+ schema: EVENT_STORE_SCHEMA,
652
+ version: VNEXT_EVENT_STORE_SCHEMA_VERSION,
653
+ tables: EVENT_STORE_TABLES,
654
+ });
655
+ }
656
+
657
+ close(): void {
658
+ this.database.close();
659
+ }
660
+
661
+ /** Run one immutable transaction, rolling back on any failure. */
662
+ transaction<T>(work: () => T): T {
663
+ return withDatabaseTransaction(this.database, work);
664
+ }
665
+
666
+ command(commandId: string): VnextCommandRecord | undefined {
667
+ const row = this.database.prepare("SELECT command_id, run_id, kind, result, recorded_at FROM commands WHERE command_id = ?").get(commandId) as
668
+ | { command_id: string; run_id: string; kind: string; result: string; recorded_at: string }
669
+ | undefined;
670
+ return row
671
+ ? { commandId: row.command_id, runId: row.run_id, kind: row.kind, result: JSON.parse(row.result) as Record<string, unknown>, recordedAt: row.recorded_at }
672
+ : undefined;
673
+ }
674
+
675
+ insertCommand(record: VnextCommandRecord): void {
676
+ this.database.prepare("INSERT INTO commands (command_id, run_id, kind, result, recorded_at) VALUES (?, ?, ?, ?, ?)")
677
+ .run(record.commandId, record.runId, record.kind, JSON.stringify(record.result), record.recordedAt);
678
+ }
679
+
680
+ insertRun(record: VnextRunRecord): void {
681
+ this.database.prepare(`
682
+ INSERT INTO runs (run_id, project_id, home_runtime_id, workflow_id, prompt_sha256, status, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, created_at, updated_at)
683
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
684
+ `).run(
685
+ record.runId,
686
+ record.projectId,
687
+ record.homeRuntimeId,
688
+ record.workflowId,
689
+ record.promptSha256,
690
+ record.status,
691
+ record.configRevision,
692
+ record.memoryRevision,
693
+ record.executorPolicyRevision,
694
+ record.toolPolicyRevision,
695
+ record.createdAt,
696
+ record.updatedAt,
697
+ );
698
+ }
699
+
700
+ updateRunStatus(runId: string, status: VnextRunStatus, updatedAt: string): void {
701
+ this.database.prepare("UPDATE runs SET status = ?, updated_at = ? WHERE run_id = ?").run(status, updatedAt, runId);
702
+ }
703
+
704
+ run(runId: string): VnextRunRecord | undefined {
705
+ const row = this.database.prepare(`
706
+ SELECT run_id, project_id, home_runtime_id, workflow_id, prompt_sha256, status, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, created_at, updated_at
707
+ FROM runs WHERE run_id = ?
708
+ `).get(runId) as
709
+ | {
710
+ run_id: string;
711
+ project_id: string;
712
+ home_runtime_id: string;
713
+ workflow_id: string;
714
+ prompt_sha256: string;
715
+ status: VnextRunStatus;
716
+ config_revision: string;
717
+ memory_revision: string;
718
+ executor_policy_revision: string;
719
+ tool_policy_revision: string;
720
+ created_at: string;
721
+ updated_at: string;
722
+ }
723
+ | undefined;
724
+ return row ? runFromRow(row) : undefined;
725
+ }
726
+
727
+ runsForProject(projectId: string, limit = 50): VnextRunRecord[] {
728
+ const rows = this.database.prepare(`
729
+ SELECT run_id, project_id, home_runtime_id, workflow_id, prompt_sha256, status, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, created_at, updated_at
730
+ FROM runs WHERE project_id = ? ORDER BY created_at DESC, run_id DESC LIMIT ?
731
+ `).all(projectId, limit) as Array<Parameters<typeof runFromRow>[0]>;
732
+ return rows.map(runFromRow);
733
+ }
734
+
735
+ nextSequence(runId: string): number {
736
+ const row = this.database.prepare("SELECT COALESCE(MAX(sequence), 0) AS max_sequence FROM events WHERE run_id = ?").get(runId) as { max_sequence: number };
737
+ return row.max_sequence + 1;
738
+ }
739
+
740
+ appendEvent(event: VnextRunEvent): void {
741
+ validateRunEvent(event, "run-event");
742
+ const run = this.run(event.runId);
743
+ if (!run) throw runtimeError("run_unknown", event.runId, "run does not exist in this event store");
744
+ if (event.homeRuntimeId !== run.homeRuntimeId || event.projectId !== run.projectId) {
745
+ throw runtimeError("run_event_owner_mismatch", event.runId, "event homeRuntimeId or projectId does not match the run");
746
+ }
747
+ this.database.prepare(`
748
+ INSERT INTO events (project_id, run_id, sequence, event_id, event_type, command_id, occurred_at, recorded_at, monotonic_ns, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, payload, schema, home_runtime_id)
749
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
750
+ `).run(
751
+ event.projectId,
752
+ event.runId,
753
+ event.sequence,
754
+ event.eventId,
755
+ event.eventType,
756
+ event.commandId ?? null,
757
+ event.occurredAt,
758
+ event.recordedAt,
759
+ event.monotonicNs,
760
+ event.configRevision,
761
+ event.memoryRevision,
762
+ event.executorPolicyRevision,
763
+ event.toolPolicyRevision,
764
+ JSON.stringify(event.payload),
765
+ event.schema,
766
+ event.homeRuntimeId,
767
+ );
768
+ }
769
+
770
+ events(runId: string, afterSequence = 0, limit = 200): VnextRunEvent[] {
771
+ const rows = this.database.prepare(`
772
+ SELECT project_id, run_id, sequence, event_id, event_type, command_id, occurred_at, recorded_at, monotonic_ns, config_revision, memory_revision, executor_policy_revision, tool_policy_revision, payload, schema, home_runtime_id
773
+ FROM events WHERE run_id = ? AND sequence > ? ORDER BY sequence ASC LIMIT ?
774
+ `).all(runId, afterSequence, limit) as Array<{
775
+ project_id: string;
776
+ run_id: string;
777
+ sequence: number;
778
+ event_id: string;
779
+ event_type: string;
780
+ command_id: string | null;
781
+ occurred_at: string;
782
+ recorded_at: string;
783
+ monotonic_ns: string;
784
+ config_revision: string;
785
+ memory_revision: string;
786
+ executor_policy_revision: string;
787
+ tool_policy_revision: string;
788
+ payload: string;
789
+ schema: typeof VNEXT_RUN_EVENT_SCHEMA;
790
+ home_runtime_id: string;
791
+ }>;
792
+ return rows.map((row) => ({
793
+ schema: row.schema,
794
+ eventId: row.event_id,
795
+ eventType: row.event_type,
796
+ projectId: row.project_id,
797
+ runId: row.run_id,
798
+ homeRuntimeId: row.home_runtime_id,
799
+ sequence: row.sequence,
800
+ ...(row.command_id !== null ? { commandId: row.command_id } : {}),
801
+ occurredAt: row.occurred_at,
802
+ recordedAt: row.recorded_at,
803
+ monotonicNs: row.monotonic_ns,
804
+ configRevision: row.config_revision,
805
+ memoryRevision: row.memory_revision,
806
+ executorPolicyRevision: row.executor_policy_revision,
807
+ toolPolicyRevision: row.tool_policy_revision,
808
+ payload: JSON.parse(row.payload) as Record<string, unknown>,
809
+ }));
810
+ }
811
+
812
+ insertRunPlan(row: VnextRunPlanRow): void {
813
+ this.database.prepare("INSERT INTO run_plans (run_id, run_plan_hash, envelope, pinned_sequence) VALUES (?, ?, ?, ?)")
814
+ .run(row.runId, row.runPlanHash, row.envelope, row.pinnedSequence);
815
+ }
816
+
817
+ runPlan(runId: string): VnextRunPlanRow | undefined {
818
+ const row = this.database.prepare("SELECT run_id, run_plan_hash, envelope, pinned_sequence FROM run_plans WHERE run_id = ?").get(runId) as
819
+ | { run_id: string; run_plan_hash: string; envelope: string; pinned_sequence: number }
820
+ | undefined;
821
+ return row
822
+ ? { runId: row.run_id, runPlanHash: row.run_plan_hash, envelope: row.envelope, pinnedSequence: row.pinned_sequence }
823
+ : undefined;
824
+ }
825
+
826
+ runState(runId: string): VnextRunStateRow | undefined {
827
+ const row = this.database.prepare("SELECT run_id, last_sequence, state FROM run_state WHERE run_id = ?").get(runId) as
828
+ | { run_id: string; last_sequence: number; state: string }
829
+ | undefined;
830
+ return row ? { runId: row.run_id, lastSequence: row.last_sequence, state: row.state } : undefined;
831
+ }
832
+
833
+ upsertRunState(row: VnextRunStateRow): void {
834
+ this.database.prepare(`
835
+ INSERT INTO run_state (run_id, last_sequence, state) VALUES (?, ?, ?)
836
+ ON CONFLICT(run_id) DO UPDATE SET last_sequence = excluded.last_sequence, state = excluded.state
837
+ `).run(row.runId, row.lastSequence, row.state);
838
+ }
839
+
840
+ insertCapability(row: VnextAttemptCapabilityRow): void {
841
+ this.database.prepare(`
842
+ INSERT INTO attempt_capabilities (attempt_id, run_id, assignment_id, step_id, step_attempt, producer_id, capability_hash, state)
843
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
844
+ `).run(row.attemptId, row.runId, row.assignmentId, row.stepId, row.stepAttempt, row.producerId, row.capabilityHash, row.state);
845
+ }
846
+
847
+ capabilityByHash(capabilityHash: string): VnextAttemptCapabilityRow | undefined {
848
+ return capabilityFromRow(this.database.prepare(`
849
+ SELECT attempt_id, run_id, assignment_id, step_id, step_attempt, producer_id, capability_hash, state
850
+ FROM attempt_capabilities WHERE capability_hash = ?
851
+ `).get(capabilityHash) as CapabilitySqlRow | undefined);
852
+ }
853
+
854
+ capabilityByAttempt(attemptId: string): VnextAttemptCapabilityRow | undefined {
855
+ return capabilityFromRow(this.database.prepare(`
856
+ SELECT attempt_id, run_id, assignment_id, step_id, step_attempt, producer_id, capability_hash, state
857
+ FROM attempt_capabilities WHERE attempt_id = ?
858
+ `).get(attemptId) as CapabilitySqlRow | undefined);
859
+ }
860
+
861
+ settleCapability(attemptId: string, state: "settled" | "revoked"): void {
862
+ const updated = this.database.prepare("UPDATE attempt_capabilities SET state = ? WHERE attempt_id = ?").run(state, attemptId);
863
+ if (Number(updated.changes) !== 1) {
864
+ throw runtimeError("capability_unknown", attemptId, "attempt capability does not exist");
865
+ }
866
+ }
867
+
868
+ insertGateAttempt(row: VnextGateAttemptRow): void {
869
+ assertGateAttemptRow(row);
870
+ this.assertEventType(row.runId, row.intentEventId, ["effect.intent_recorded"]);
871
+ const capability = this.capabilityByAttempt(row.attemptId);
872
+ if (
873
+ !capability
874
+ || capability.runId !== row.runId
875
+ || capability.assignmentId !== row.assignmentId
876
+ || capability.stepId !== row.stepId
877
+ || capability.stepAttempt !== row.stepAttempt
878
+ ) {
879
+ throw gateRowInvalid(row.attemptId, "gate attempt identity does not match attempt_capabilities");
880
+ }
881
+ this.database.prepare(`
882
+ INSERT INTO gate_attempts (
883
+ attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
884
+ gate_id, gate_kind, expect, gate_definition_hash, registry_hash, run_plan_hash, control_project_key,
885
+ producer_id, intent_event_id, content_hash
886
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
887
+ `).run(
888
+ row.attemptId, row.runId, row.projectId, row.homeRuntimeId, row.stepId, row.stepAttempt, row.assignmentId, row.effectId,
889
+ row.gateId, row.gateKind, row.expect, row.gateDefinitionHash, row.registryHash, row.runPlanHash, row.controlProjectKey,
890
+ row.producerId, row.intentEventId, row.contentHash,
891
+ );
892
+ }
893
+
894
+ insertGateObservation(row: VnextGateObservationRow): void {
895
+ assertGateObservationRow(row);
896
+ this.assertEventType(row.runId, row.recordedEventId, ["effect.observed", "effect.blocked_uncertain"]);
897
+ const attempt = this.gateAttempt(row.attemptId);
898
+ if (!attempt) throw gateRowInvalid(row.attemptId, "gate observation has no matching gate attempt");
899
+ assertSharedIdentity(row, attempt, row.observationId);
900
+ assertClosedGateObservation(attempt.gateKind, row, row.observationId);
901
+ this.database.prepare(`
902
+ INSERT INTO gate_observations (
903
+ observation_id, attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
904
+ completeness, spawned, pid, exit_code, signal, exit_observed, close_observed, stop_cause, signals_attempted,
905
+ error_class, stdout_sha256, stdout_bytes, stdout_complete, stderr_sha256, stderr_bytes, stderr_complete,
906
+ checked_count, failed_count, elapsed_ms, started_at, finished_at, recorded_event_id, content_hash
907
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
908
+ `).run(
909
+ row.observationId, row.attemptId, row.runId, row.projectId, row.homeRuntimeId, row.stepId, row.stepAttempt,
910
+ row.assignmentId, row.effectId, row.completeness, row.spawned, row.pid, row.exitCode, row.signal, row.exitObserved,
911
+ row.closeObserved, row.stopCause, row.signalsAttempted, row.errorClass, row.stdoutSha256, row.stdoutBytes,
912
+ row.stdoutComplete, row.stderrSha256, row.stderrBytes, row.stderrComplete, row.checkedCount, row.failedCount,
913
+ row.elapsedMs, row.startedAt, row.finishedAt, row.recordedEventId, row.contentHash,
914
+ );
915
+ }
916
+
917
+ insertGateEvidence(row: VnextGateEvidenceRow): void {
918
+ assertGateEvidenceRow(row);
919
+ this.assertEventType(row.runId, row.settledEventId, ["effect.settled"]);
920
+ const attempt = this.gateAttempt(row.attemptId);
921
+ if (!attempt) throw gateRowInvalid(row.attemptId, "gate evidence has no matching gate attempt");
922
+ assertSharedIdentity(row, attempt, row.evidenceId);
923
+ const observation = this.gateObservationForAttempt(row.attemptId);
924
+ if (!observation || observation.observationId !== row.observationId) {
925
+ throw gateRowInvalid(row.evidenceId, "gate evidence observation_id does not match the attempt observation");
926
+ }
927
+ this.database.prepare(`
928
+ INSERT INTO gate_evidence (
929
+ evidence_id, attempt_id, observation_id, run_id, project_id, home_runtime_id, step_id, step_attempt,
930
+ assignment_id, effect_id, evidence_key, kind, expect, outcome, settled_event_id, content_hash
931
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
932
+ `).run(
933
+ row.evidenceId, row.attemptId, row.observationId, row.runId, row.projectId, row.homeRuntimeId, row.stepId,
934
+ row.stepAttempt, row.assignmentId, row.effectId, row.evidenceKey, row.kind, row.expect, row.outcome,
935
+ row.settledEventId, row.contentHash,
936
+ );
937
+ }
938
+
939
+ gateAttempt(attemptId: string): VnextGateAttemptRow | undefined {
940
+ const row = this.database.prepare(`
941
+ SELECT attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
942
+ gate_id, gate_kind, expect, gate_definition_hash, registry_hash, run_plan_hash, control_project_key,
943
+ producer_id, intent_event_id, content_hash
944
+ FROM gate_attempts WHERE attempt_id = ?
945
+ `).get(attemptId) as GateAttemptSql | undefined;
946
+ return row ? gateAttemptFromSql(row) : undefined;
947
+ }
948
+
949
+ gateAttemptsForRun(runId: string): VnextGateAttemptRow[] {
950
+ const rows = this.database.prepare(`
951
+ SELECT attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
952
+ gate_id, gate_kind, expect, gate_definition_hash, registry_hash, run_plan_hash, control_project_key,
953
+ producer_id, intent_event_id, content_hash
954
+ FROM gate_attempts WHERE run_id = ? ORDER BY step_attempt ASC, attempt_id ASC
955
+ `).all(runId) as GateAttemptSql[];
956
+ return rows.map(gateAttemptFromSql);
957
+ }
958
+
959
+ gateAttemptsForProject(projectId: string): VnextGateAttemptRow[] {
960
+ const rows = this.database.prepare(`
961
+ SELECT attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id, effect_id,
962
+ gate_id, gate_kind, expect, gate_definition_hash, registry_hash, run_plan_hash, control_project_key,
963
+ producer_id, intent_event_id, content_hash
964
+ FROM gate_attempts WHERE project_id = ? ORDER BY run_id ASC, step_attempt ASC, attempt_id ASC
965
+ `).all(projectId) as GateAttemptSql[];
966
+ return rows.map(gateAttemptFromSql);
967
+ }
968
+
969
+ issuedOrRevokedCapabilities(): VnextAttemptCapabilityRow[] {
970
+ const rows = this.database.prepare(`
971
+ SELECT attempt_id, run_id, assignment_id, step_id, step_attempt, producer_id, capability_hash, state
972
+ FROM attempt_capabilities
973
+ WHERE state IN ('issued','revoked')
974
+ ORDER BY run_id ASC, step_attempt ASC, attempt_id ASC
975
+ `).all() as CapabilitySqlRow[];
976
+ const mapped: VnextAttemptCapabilityRow[] = [];
977
+ for (const row of rows) {
978
+ const capability = capabilityFromRow(row);
979
+ if (!capability) {
980
+ throw runtimeError("gate_recovery_corrupt", "attempt_capabilities", "issued or revoked capability row could not be read");
981
+ }
982
+ mapped.push(capability);
983
+ }
984
+ return mapped;
985
+ }
986
+
987
+ gateObservationForAttempt(attemptId: string): VnextGateObservationRow | undefined {
988
+ const row = this.database.prepare(`
989
+ SELECT observation_id, attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id,
990
+ effect_id, completeness, spawned, pid, exit_code, signal, exit_observed, close_observed, stop_cause,
991
+ signals_attempted, error_class, stdout_sha256, stdout_bytes, stdout_complete, stderr_sha256, stderr_bytes,
992
+ stderr_complete, checked_count, failed_count, elapsed_ms, started_at, finished_at, recorded_event_id, content_hash
993
+ FROM gate_observations WHERE attempt_id = ?
994
+ `).get(attemptId) as GateObservationSql | undefined;
995
+ return row ? gateObservationFromSql(row) : undefined;
996
+ }
997
+
998
+ gateObservationsForRun(runId: string): VnextGateObservationRow[] {
999
+ const rows = this.database.prepare(`
1000
+ SELECT observation_id, attempt_id, run_id, project_id, home_runtime_id, step_id, step_attempt, assignment_id,
1001
+ effect_id, completeness, spawned, pid, exit_code, signal, exit_observed, close_observed, stop_cause,
1002
+ signals_attempted, error_class, stdout_sha256, stdout_bytes, stdout_complete, stderr_sha256, stderr_bytes,
1003
+ stderr_complete, checked_count, failed_count, elapsed_ms, started_at, finished_at, recorded_event_id, content_hash
1004
+ FROM gate_observations WHERE run_id = ? ORDER BY step_attempt ASC, observation_id ASC
1005
+ `).all(runId) as GateObservationSql[];
1006
+ return rows.map(gateObservationFromSql);
1007
+ }
1008
+
1009
+ gateEvidenceForRun(runId: string): VnextGateEvidenceRow[] {
1010
+ const rows = this.database.prepare(`
1011
+ SELECT evidence_id, attempt_id, observation_id, run_id, project_id, home_runtime_id, step_id, step_attempt,
1012
+ assignment_id, effect_id, evidence_key, kind, expect, outcome, settled_event_id, content_hash
1013
+ FROM gate_evidence WHERE run_id = ? ORDER BY step_attempt ASC, evidence_id ASC
1014
+ `).all(runId) as GateEvidenceSql[];
1015
+ return rows.map(gateEvidenceFromSql);
1016
+ }
1017
+
1018
+ gateEvidenceForAttempt(attemptId: string): VnextGateEvidenceRow | undefined {
1019
+ const row = this.database.prepare(`
1020
+ SELECT evidence_id, attempt_id, observation_id, run_id, project_id, home_runtime_id, step_id, step_attempt,
1021
+ assignment_id, effect_id, evidence_key, kind, expect, outcome, settled_event_id, content_hash
1022
+ FROM gate_evidence WHERE attempt_id = ?
1023
+ `).get(attemptId) as GateEvidenceSql | undefined;
1024
+ return row ? gateEvidenceFromSql(row) : undefined;
1025
+ }
1026
+
1027
+ gateRowCounts(runId: string): VnextGateRowCounts {
1028
+ const attempts = this.database.prepare("SELECT COUNT(*) AS n FROM gate_attempts WHERE run_id = ?").get(runId) as { n: number };
1029
+ const observations = this.database.prepare("SELECT COUNT(*) AS n FROM gate_observations WHERE run_id = ?").get(runId) as { n: number };
1030
+ const evidence = this.database.prepare("SELECT COUNT(*) AS n FROM gate_evidence WHERE run_id = ?").get(runId) as { n: number };
1031
+ return { attempts: Number(attempts.n), observations: Number(observations.n), evidence: Number(evidence.n) };
1032
+ }
1033
+
1034
+ private assertEventType(runId: string, eventId: string, allowed: readonly string[]): void {
1035
+ const row = this.database.prepare("SELECT run_id, event_type FROM events WHERE event_id = ?").get(eventId) as
1036
+ | { run_id: string; event_type: string }
1037
+ | undefined;
1038
+ if (!row || row.run_id !== runId || !allowed.includes(row.event_type)) {
1039
+ throw gateRowInvalid(eventId, `referenced event is not ${allowed.join("|")} for this run`);
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ function runFromRow(row: {
1045
+ run_id: string;
1046
+ project_id: string;
1047
+ home_runtime_id: string;
1048
+ workflow_id: string;
1049
+ prompt_sha256: string;
1050
+ status: VnextRunStatus;
1051
+ config_revision: string;
1052
+ memory_revision: string;
1053
+ executor_policy_revision: string;
1054
+ tool_policy_revision: string;
1055
+ created_at: string;
1056
+ updated_at: string;
1057
+ }): VnextRunRecord {
1058
+ if (typeof row.memory_revision !== "string" || row.memory_revision.length === 0) {
1059
+ throw runtimeError("runtime_schema_shape_invalid", row.run_id, "memory_revision is required");
1060
+ }
1061
+ return {
1062
+ runId: row.run_id,
1063
+ projectId: row.project_id,
1064
+ homeRuntimeId: row.home_runtime_id,
1065
+ workflowId: row.workflow_id,
1066
+ promptSha256: row.prompt_sha256,
1067
+ status: row.status,
1068
+ configRevision: row.config_revision,
1069
+ memoryRevision: row.memory_revision,
1070
+ executorPolicyRevision: row.executor_policy_revision,
1071
+ toolPolicyRevision: row.tool_policy_revision,
1072
+ createdAt: row.created_at,
1073
+ updatedAt: row.updated_at,
1074
+ };
1075
+ }
1076
+
1077
+ /** Compute the immutable identity for a new run. */
1078
+ export function newVnextRunId(): string {
1079
+ return `run_${randomUUID().replaceAll("-", "")}`;
1080
+ }
1081
+
1082
+ export function newVnextEventId(): string {
1083
+ return `evt_${randomUUID().replaceAll("-", "")}`;
1084
+ }
1085
+
1086
+ export function newVnextCommandId(): string {
1087
+ return `cmd_${randomUUID().replaceAll("-", "")}`;
1088
+ }
1089
+
1090
+ export function newVnextAssignmentId(): string {
1091
+ return `asg_${randomUUID().replaceAll("-", "")}`;
1092
+ }
1093
+
1094
+ export function newVnextAttemptId(): string {
1095
+ return `att_${randomUUID().replaceAll("-", "")}`;
1096
+ }
1097
+
1098
+ export function newVnextEffectId(): string {
1099
+ return `eff_${randomUUID().replaceAll("-", "")}`;
1100
+ }
1101
+
1102
+ export function newVnextObservationId(): string {
1103
+ return `obs_${randomUUID().replaceAll("-", "")}`;
1104
+ }
1105
+
1106
+ export function newVnextEvidenceId(): string {
1107
+ return `gev_${randomUUID().replaceAll("-", "")}`;
1108
+ }
1109
+
1110
+ const SHA256 = /^sha256:[a-f0-9]{64}$/;
1111
+ const OPAQUE = /^[a-z][a-z0-9]{1,15}_[A-Za-z0-9][A-Za-z0-9_-]{5,127}$/;
1112
+ const FORBIDDEN_TEXT_KEYS = new Set(["stdout", "stderr", "error", "errorText", "stdoutText", "stderrText", "errorMessage", "message"]);
1113
+ const GATE_ATTEMPT_KEYS = [
1114
+ "attemptId", "runId", "projectId", "homeRuntimeId", "stepId", "stepAttempt", "assignmentId", "effectId",
1115
+ "gateId", "gateKind", "expect", "gateDefinitionHash", "registryHash", "runPlanHash", "controlProjectKey",
1116
+ "producerId", "intentEventId", "contentHash",
1117
+ ] as const;
1118
+ const GATE_OBSERVATION_KEYS = [
1119
+ "observationId", "attemptId", "runId", "projectId", "homeRuntimeId", "stepId", "stepAttempt", "assignmentId",
1120
+ "effectId", "completeness", "spawned", "pid", "exitCode", "signal", "exitObserved", "closeObserved", "stopCause",
1121
+ "signalsAttempted", "errorClass", "stdoutSha256", "stdoutBytes", "stdoutComplete", "stderrSha256", "stderrBytes",
1122
+ "stderrComplete", "checkedCount", "failedCount", "elapsedMs", "startedAt", "finishedAt", "recordedEventId", "contentHash",
1123
+ ] as const;
1124
+ const GATE_EVIDENCE_KEYS = [
1125
+ "evidenceId", "attemptId", "observationId", "runId", "projectId", "homeRuntimeId", "stepId", "stepAttempt",
1126
+ "assignmentId", "effectId", "evidenceKey", "kind", "expect", "outcome", "settledEventId", "contentHash",
1127
+ ] as const;
1128
+
1129
+ export function gateRowContentHash(table: "gate_attempts" | "gate_observations" | "gate_evidence", row: object): string {
1130
+ const copy = { ...(row as Record<string, unknown>) };
1131
+ delete copy.contentHash;
1132
+ void table;
1133
+ return `sha256:${createHash("sha256").update(vnextCanonicalJson(copy as JsonValue), "utf8").digest("hex")}`;
1134
+ }
1135
+
1136
+ export type VnextGateObservationFacts = Pick<
1137
+ VnextGateObservationRow,
1138
+ | "completeness"
1139
+ | "spawned"
1140
+ | "pid"
1141
+ | "exitCode"
1142
+ | "signal"
1143
+ | "exitObserved"
1144
+ | "closeObserved"
1145
+ | "stopCause"
1146
+ | "signalsAttempted"
1147
+ | "errorClass"
1148
+ | "stdoutSha256"
1149
+ | "stdoutBytes"
1150
+ | "stdoutComplete"
1151
+ | "stderrSha256"
1152
+ | "stderrBytes"
1153
+ | "stderrComplete"
1154
+ | "checkedCount"
1155
+ | "failedCount"
1156
+ >;
1157
+
1158
+ export function assertClosedGateObservation(
1159
+ kind: VnextGateKind,
1160
+ row: VnextGateObservationFacts,
1161
+ id = "observation",
1162
+ ): void {
1163
+ if (row.completeness === "incomplete") return;
1164
+ if (row.completeness === "no-start") {
1165
+ const legitimateStartFailure = (row.errorClass === "validation" || row.errorClass === "spawn-error") && row.stopCause === "none";
1166
+ const requestedStop = row.stopCause === "cancel" && row.errorClass === null;
1167
+ if (
1168
+ row.spawned !== 0
1169
+ || row.pid !== null
1170
+ || row.exitCode !== null
1171
+ || row.signal !== null
1172
+ || row.exitObserved !== 0
1173
+ || row.closeObserved !== 0
1174
+ || row.signalsAttempted !== "none"
1175
+ || row.stdoutSha256 !== null
1176
+ || row.stdoutBytes !== null
1177
+ || row.stdoutComplete !== null
1178
+ || row.stderrSha256 !== null
1179
+ || row.stderrBytes !== null
1180
+ || row.stderrComplete !== null
1181
+ || row.checkedCount !== null
1182
+ || row.failedCount !== null
1183
+ || (!legitimateStartFailure && !requestedStop)
1184
+ ) {
1185
+ throw gateRowInvalid(id, "no-start observation forbids PID/exit/close/stream/check facts and requires a legitimate no-start cause");
1186
+ }
1187
+ return;
1188
+ }
1189
+ if (kind === "command") {
1190
+ if (
1191
+ row.spawned !== 1
1192
+ || typeof row.pid !== "number"
1193
+ || typeof row.exitCode !== "number"
1194
+ || row.signal !== null
1195
+ || row.exitObserved !== 1
1196
+ || row.closeObserved !== 1
1197
+ || row.stopCause !== "none"
1198
+ || row.signalsAttempted !== "none"
1199
+ || row.stdoutComplete !== 1
1200
+ || row.stderrComplete !== 1
1201
+ || typeof row.stdoutSha256 !== "string"
1202
+ || typeof row.stderrSha256 !== "string"
1203
+ || typeof row.stdoutBytes !== "number"
1204
+ || typeof row.stderrBytes !== "number"
1205
+ || row.checkedCount !== null
1206
+ || row.failedCount !== null
1207
+ ) {
1208
+ throw gateRowInvalid(id, "complete command observation requires spawn, numeric exit, no signal, exit/close, complete streams, and stopCause none");
1209
+ }
1210
+ return;
1211
+ }
1212
+ if (
1213
+ row.spawned !== 0
1214
+ || row.pid !== null
1215
+ || row.exitCode !== null
1216
+ || row.signal !== null
1217
+ || row.exitObserved !== 0
1218
+ || row.closeObserved !== 0
1219
+ || row.stopCause !== "none"
1220
+ || row.signalsAttempted !== "none"
1221
+ || row.stdoutSha256 !== null
1222
+ || row.stdoutBytes !== null
1223
+ || row.stdoutComplete !== null
1224
+ || row.stderrSha256 !== null
1225
+ || row.stderrBytes !== null
1226
+ || row.stderrComplete !== null
1227
+ || typeof row.checkedCount !== "number"
1228
+ || typeof row.failedCount !== "number"
1229
+ || row.checkedCount < 0
1230
+ || row.failedCount < 0
1231
+ || row.failedCount > row.checkedCount
1232
+ ) {
1233
+ throw gateRowInvalid(id, "complete artifacts observation requires consistent counts and no command facts");
1234
+ }
1235
+ }
1236
+
1237
+ export function computeGateEvidenceOutcome(
1238
+ kind: VnextGateKind,
1239
+ expect: VnextGateExpect,
1240
+ observation: VnextGateObservationFacts,
1241
+ ): VnextGateEvidenceOutcome {
1242
+ assertClosedGateObservation(kind, observation);
1243
+ if (observation.completeness !== "complete") {
1244
+ throw gateRowInvalid("observation", "evaluated settlement requires a complete observation");
1245
+ }
1246
+ if (kind === "artifacts-exist") {
1247
+ if (expect === "fail") throw gateRowInvalid("artifacts-exist", "artifacts-exist cannot expect fail");
1248
+ return observation.failedCount === 0 ? "passed" : "implementation-failure";
1249
+ }
1250
+ if (expect === "pass") return observation.exitCode === 0 ? "passed" : "implementation-failure";
1251
+ return observation.exitCode === 0 ? "repro-missing" : "passed";
1252
+ }
1253
+
1254
+ function gateRowInvalid(file: string, message: string): VnextConfigError {
1255
+ return runtimeError("gate_row_invalid", file, message);
1256
+ }
1257
+
1258
+ function assertExactRowKeys(row: object, keys: readonly string[], id: string): void {
1259
+ const actual = Object.keys(row);
1260
+ if (actual.length !== keys.length || keys.some((key) => !Object.prototype.hasOwnProperty.call(row, key))) {
1261
+ throw gateRowInvalid(id, "gate row does not have the exact key set");
1262
+ }
1263
+ for (const key of actual) {
1264
+ if (FORBIDDEN_TEXT_KEYS.has(key)) throw gateRowInvalid(id, "gate row must not carry raw stdout/stderr/error text");
1265
+ }
1266
+ }
1267
+
1268
+ function assertId(value: string, prefix: string, id: string, label: string): void {
1269
+ if (typeof value !== "string" || !value.startsWith(prefix) || !OPAQUE.test(value)) {
1270
+ throw gateRowInvalid(id, `${label} is not a valid ${prefix} opaque id`);
1271
+ }
1272
+ }
1273
+
1274
+ function assertSha(value: string, id: string, label: string): void {
1275
+ if (typeof value !== "string" || !SHA256.test(value)) throw gateRowInvalid(id, `${label} is not a sha256 hash`);
1276
+ }
1277
+
1278
+ function assertFlag(value: unknown, id: string, label: string): asserts value is 0 | 1 {
1279
+ if (value !== 0 && value !== 1) throw gateRowInvalid(id, `${label} must be 0 or 1`);
1280
+ }
1281
+
1282
+ function assertNullInt(value: unknown, id: string, label: string): void {
1283
+ if (value !== null && (typeof value !== "number" || !Number.isInteger(value))) {
1284
+ throw gateRowInvalid(id, `${label} must be an integer or null`);
1285
+ }
1286
+ }
1287
+
1288
+ function assertGateAttemptRow(row: VnextGateAttemptRow): void {
1289
+ assertExactRowKeys(row, GATE_ATTEMPT_KEYS, row.attemptId);
1290
+ assertId(row.attemptId, "att_", row.attemptId, "attemptId");
1291
+ assertId(row.effectId, "eff_", row.attemptId, "effectId");
1292
+ assertId(row.intentEventId, "evt_", row.attemptId, "intentEventId");
1293
+ assertSha(row.gateDefinitionHash, row.attemptId, "gateDefinitionHash");
1294
+ assertSha(row.registryHash, row.attemptId, "registryHash");
1295
+ assertSha(row.runPlanHash, row.attemptId, "runPlanHash");
1296
+ if (row.producerId !== "kxm-gate") throw gateRowInvalid(row.attemptId, "producerId must be kxm-gate");
1297
+ if (row.gateKind !== "command" && row.gateKind !== "artifacts-exist") {
1298
+ throw gateRowInvalid(row.attemptId, "gateKind is invalid");
1299
+ }
1300
+ if (row.expect !== "pass" && row.expect !== "fail") throw gateRowInvalid(row.attemptId, "expect is invalid");
1301
+ if (row.gateKind === "artifacts-exist" && row.expect === "fail") {
1302
+ throw gateRowInvalid(row.attemptId, "artifacts-exist cannot expect fail");
1303
+ }
1304
+ if (typeof row.controlProjectKey !== "string" || row.controlProjectKey.length === 0) {
1305
+ throw gateRowInvalid(row.attemptId, "controlProjectKey is required");
1306
+ }
1307
+ if (gateRowContentHash("gate_attempts", row) !== row.contentHash) {
1308
+ throw gateRowInvalid(row.attemptId, "contentHash does not match the canonical row");
1309
+ }
1310
+ }
1311
+
1312
+ function assertGateObservationRow(row: VnextGateObservationRow): void {
1313
+ assertExactRowKeys(row, GATE_OBSERVATION_KEYS, row.observationId);
1314
+ assertId(row.observationId, "obs_", row.observationId, "observationId");
1315
+ assertId(row.attemptId, "att_", row.observationId, "attemptId");
1316
+ assertId(row.effectId, "eff_", row.observationId, "effectId");
1317
+ assertId(row.recordedEventId, "evt_", row.observationId, "recordedEventId");
1318
+ if (!["complete", "incomplete", "no-start"].includes(row.completeness)) {
1319
+ throw gateRowInvalid(row.observationId, "completeness is invalid");
1320
+ }
1321
+ assertFlag(row.spawned, row.observationId, "spawned");
1322
+ assertFlag(row.exitObserved, row.observationId, "exitObserved");
1323
+ assertFlag(row.closeObserved, row.observationId, "closeObserved");
1324
+ assertNullInt(row.pid, row.observationId, "pid");
1325
+ assertNullInt(row.exitCode, row.observationId, "exitCode");
1326
+ assertNullInt(row.stdoutBytes, row.observationId, "stdoutBytes");
1327
+ assertNullInt(row.stderrBytes, row.observationId, "stderrBytes");
1328
+ assertNullInt(row.checkedCount, row.observationId, "checkedCount");
1329
+ assertNullInt(row.failedCount, row.observationId, "failedCount");
1330
+ if (typeof row.elapsedMs !== "number" || !Number.isInteger(row.elapsedMs) || row.elapsedMs < 0) {
1331
+ throw gateRowInvalid(row.observationId, "elapsedMs is invalid");
1332
+ }
1333
+ if (row.stdoutSha256 !== null) assertSha(row.stdoutSha256, row.observationId, "stdoutSha256");
1334
+ if (row.stderrSha256 !== null) assertSha(row.stderrSha256, row.observationId, "stderrSha256");
1335
+ if (row.stdoutComplete !== null && row.stdoutComplete !== 0 && row.stdoutComplete !== 1) {
1336
+ throw gateRowInvalid(row.observationId, "stdoutComplete must be 0, 1, or null");
1337
+ }
1338
+ if (row.stderrComplete !== null && row.stderrComplete !== 0 && row.stderrComplete !== 1) {
1339
+ throw gateRowInvalid(row.observationId, "stderrComplete must be 0, 1, or null");
1340
+ }
1341
+ if (gateRowContentHash("gate_observations", row) !== row.contentHash) {
1342
+ throw gateRowInvalid(row.observationId, "contentHash does not match the canonical row");
1343
+ }
1344
+ }
1345
+
1346
+ function assertGateEvidenceRow(row: VnextGateEvidenceRow): void {
1347
+ assertExactRowKeys(row, GATE_EVIDENCE_KEYS, row.evidenceId);
1348
+ assertId(row.evidenceId, "gev_", row.evidenceId, "evidenceId");
1349
+ assertId(row.attemptId, "att_", row.evidenceId, "attemptId");
1350
+ assertId(row.observationId, "obs_", row.evidenceId, "observationId");
1351
+ assertId(row.effectId, "eff_", row.evidenceId, "effectId");
1352
+ assertId(row.settledEventId, "evt_", row.evidenceId, "settledEventId");
1353
+ if (row.kind !== "gate") throw gateRowInvalid(row.evidenceId, "kind must be gate");
1354
+ if (!["passed", "implementation-failure", "repro-missing"].includes(row.outcome)) {
1355
+ throw gateRowInvalid(row.evidenceId, "outcome is invalid");
1356
+ }
1357
+ if (gateRowContentHash("gate_evidence", row) !== row.contentHash) {
1358
+ throw gateRowInvalid(row.evidenceId, "contentHash does not match the canonical row");
1359
+ }
1360
+ }
1361
+
1362
+ function assertSharedIdentity(
1363
+ row: { runId: string; projectId: string; homeRuntimeId: string; stepId: string; stepAttempt: number; assignmentId: string; effectId: string },
1364
+ attempt: VnextGateAttemptRow,
1365
+ id: string,
1366
+ ): void {
1367
+ if (
1368
+ row.runId !== attempt.runId
1369
+ || row.projectId !== attempt.projectId
1370
+ || row.homeRuntimeId !== attempt.homeRuntimeId
1371
+ || row.stepId !== attempt.stepId
1372
+ || row.stepAttempt !== attempt.stepAttempt
1373
+ || row.assignmentId !== attempt.assignmentId
1374
+ || row.effectId !== attempt.effectId
1375
+ ) {
1376
+ throw gateRowInvalid(id, "row identity does not match the gate attempt");
1377
+ }
1378
+ }
1379
+
1380
+ type GateAttemptSql = {
1381
+ attempt_id: string;
1382
+ run_id: string;
1383
+ project_id: string;
1384
+ home_runtime_id: string;
1385
+ step_id: string;
1386
+ step_attempt: number;
1387
+ assignment_id: string;
1388
+ effect_id: string;
1389
+ gate_id: string;
1390
+ gate_kind: VnextGateKind;
1391
+ expect: VnextGateExpect;
1392
+ gate_definition_hash: string;
1393
+ registry_hash: string;
1394
+ run_plan_hash: string;
1395
+ control_project_key: string;
1396
+ producer_id: "kxm-gate";
1397
+ intent_event_id: string;
1398
+ content_hash: string;
1399
+ };
1400
+
1401
+ function gateAttemptFromSql(row: GateAttemptSql): VnextGateAttemptRow {
1402
+ return {
1403
+ attemptId: row.attempt_id,
1404
+ runId: row.run_id,
1405
+ projectId: row.project_id,
1406
+ homeRuntimeId: row.home_runtime_id,
1407
+ stepId: row.step_id,
1408
+ stepAttempt: row.step_attempt,
1409
+ assignmentId: row.assignment_id,
1410
+ effectId: row.effect_id,
1411
+ gateId: row.gate_id,
1412
+ gateKind: row.gate_kind,
1413
+ expect: row.expect,
1414
+ gateDefinitionHash: row.gate_definition_hash,
1415
+ registryHash: row.registry_hash,
1416
+ runPlanHash: row.run_plan_hash,
1417
+ controlProjectKey: row.control_project_key,
1418
+ producerId: row.producer_id,
1419
+ intentEventId: row.intent_event_id,
1420
+ contentHash: row.content_hash,
1421
+ };
1422
+ }
1423
+
1424
+ type GateObservationSql = {
1425
+ observation_id: string;
1426
+ attempt_id: string;
1427
+ run_id: string;
1428
+ project_id: string;
1429
+ home_runtime_id: string;
1430
+ step_id: string;
1431
+ step_attempt: number;
1432
+ assignment_id: string;
1433
+ effect_id: string;
1434
+ completeness: VnextGateCompleteness;
1435
+ spawned: number;
1436
+ pid: number | null;
1437
+ exit_code: number | null;
1438
+ signal: string | null;
1439
+ exit_observed: number;
1440
+ close_observed: number;
1441
+ stop_cause: VnextGateStopCause;
1442
+ signals_attempted: VnextGateSignalsAttempted;
1443
+ error_class: VnextGateErrorClass | null;
1444
+ stdout_sha256: string | null;
1445
+ stdout_bytes: number | null;
1446
+ stdout_complete: number | null;
1447
+ stderr_sha256: string | null;
1448
+ stderr_bytes: number | null;
1449
+ stderr_complete: number | null;
1450
+ checked_count: number | null;
1451
+ failed_count: number | null;
1452
+ elapsed_ms: number;
1453
+ started_at: string;
1454
+ finished_at: string | null;
1455
+ recorded_event_id: string;
1456
+ content_hash: string;
1457
+ };
1458
+
1459
+ function gateObservationFromSql(row: GateObservationSql): VnextGateObservationRow {
1460
+ return {
1461
+ observationId: row.observation_id,
1462
+ attemptId: row.attempt_id,
1463
+ runId: row.run_id,
1464
+ projectId: row.project_id,
1465
+ homeRuntimeId: row.home_runtime_id,
1466
+ stepId: row.step_id,
1467
+ stepAttempt: row.step_attempt,
1468
+ assignmentId: row.assignment_id,
1469
+ effectId: row.effect_id,
1470
+ completeness: row.completeness,
1471
+ spawned: row.spawned === 1 ? 1 : 0,
1472
+ pid: row.pid,
1473
+ exitCode: row.exit_code,
1474
+ signal: row.signal,
1475
+ exitObserved: row.exit_observed === 1 ? 1 : 0,
1476
+ closeObserved: row.close_observed === 1 ? 1 : 0,
1477
+ stopCause: row.stop_cause,
1478
+ signalsAttempted: row.signals_attempted,
1479
+ errorClass: row.error_class,
1480
+ stdoutSha256: row.stdout_sha256,
1481
+ stdoutBytes: row.stdout_bytes,
1482
+ stdoutComplete: row.stdout_complete === null ? null : row.stdout_complete === 1 ? 1 : 0,
1483
+ stderrSha256: row.stderr_sha256,
1484
+ stderrBytes: row.stderr_bytes,
1485
+ stderrComplete: row.stderr_complete === null ? null : row.stderr_complete === 1 ? 1 : 0,
1486
+ checkedCount: row.checked_count,
1487
+ failedCount: row.failed_count,
1488
+ elapsedMs: row.elapsed_ms,
1489
+ startedAt: row.started_at,
1490
+ finishedAt: row.finished_at,
1491
+ recordedEventId: row.recorded_event_id,
1492
+ contentHash: row.content_hash,
1493
+ };
1494
+ }
1495
+
1496
+ type GateEvidenceSql = {
1497
+ evidence_id: string;
1498
+ attempt_id: string;
1499
+ observation_id: string;
1500
+ run_id: string;
1501
+ project_id: string;
1502
+ home_runtime_id: string;
1503
+ step_id: string;
1504
+ step_attempt: number;
1505
+ assignment_id: string;
1506
+ effect_id: string;
1507
+ evidence_key: string | null;
1508
+ kind: "gate";
1509
+ expect: VnextGateExpect;
1510
+ outcome: VnextGateEvidenceOutcome;
1511
+ settled_event_id: string;
1512
+ content_hash: string;
1513
+ };
1514
+
1515
+ function gateEvidenceFromSql(row: GateEvidenceSql): VnextGateEvidenceRow {
1516
+ return {
1517
+ evidenceId: row.evidence_id,
1518
+ attemptId: row.attempt_id,
1519
+ observationId: row.observation_id,
1520
+ runId: row.run_id,
1521
+ projectId: row.project_id,
1522
+ homeRuntimeId: row.home_runtime_id,
1523
+ stepId: row.step_id,
1524
+ stepAttempt: row.step_attempt,
1525
+ assignmentId: row.assignment_id,
1526
+ effectId: row.effect_id,
1527
+ evidenceKey: row.evidence_key,
1528
+ kind: row.kind,
1529
+ expect: row.expect,
1530
+ outcome: row.outcome,
1531
+ settledEventId: row.settled_event_id,
1532
+ contentHash: row.content_hash,
1533
+ };
1534
+ }
1535
+
1536
+ type CapabilitySqlRow = {
1537
+ attempt_id: string;
1538
+ run_id: string;
1539
+ assignment_id: string;
1540
+ step_id: string;
1541
+ step_attempt: number;
1542
+ producer_id: string;
1543
+ capability_hash: string;
1544
+ state: VnextAttemptCapabilityRow["state"];
1545
+ };
1546
+
1547
+ function capabilityFromRow(row: CapabilitySqlRow | undefined): VnextAttemptCapabilityRow | undefined {
1548
+ return row
1549
+ ? {
1550
+ attemptId: row.attempt_id,
1551
+ runId: row.run_id,
1552
+ assignmentId: row.assignment_id,
1553
+ stepId: row.step_id,
1554
+ stepAttempt: row.step_attempt,
1555
+ producerId: row.producer_id,
1556
+ capabilityHash: row.capability_hash,
1557
+ state: row.state,
1558
+ }
1559
+ : undefined;
1560
+ }