@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,1400 @@
1
+ import type { VnextCompiledPlan, VnextCompiledStep, VnextCompiledTransition, VnextTerminalStatus } from "./vnext-engine-compile.ts";
2
+ import {
3
+ runtimeError,
4
+ type VnextRunEvent,
5
+ type VnextRunRecord,
6
+ type VnextRunStatus,
7
+ } from "./vnext-runtime-store.ts";
8
+
9
+ export const VNEXT_RUN_STATE_SCHEMA = "kxm.run-state.v2";
10
+ export const FOLD_PANEL_BOUND = 1;
11
+
12
+ const RUN_STATUSES = new Set<VnextRunStatus>(["created", "preparing", "running", "waiting", "blocked_uncertain", "cancelling", "cancelled", "completed", "failed"]);
13
+ const TERMINAL_RUN = new Set<VnextRunStatus>(["cancelled", "completed", "failed"]);
14
+ const STEP_STATUSES = new Set(["pending", "preparing", "running", "passed", "failed", "cancelled"]);
15
+ const ASSIGNMENT_STATUSES = new Set(["created", "accepted", "dispatched", "executing", "result_recorded", "terminal"]);
16
+ const ATTEMPT_STATUSES = new Set(["created", "starting", "executing", "settling", "terminal"]);
17
+ const RESULT_CLASSES = new Set(["outcome", "outcome_unknown", "producer_rejected", "cancelled"]);
18
+ const SLICE_BLOCKED_RUN = new Set<VnextRunStatus>(["waiting"]);
19
+ const RUN_EDGES: Readonly<Record<string, ReadonlySet<VnextRunStatus>>> = {
20
+ created: new Set(["preparing", "cancelled", "failed"]),
21
+ preparing: new Set(["running", "cancelled", "failed"]),
22
+ running: new Set(["blocked_uncertain", "cancelling", "cancelled", "completed", "failed"]),
23
+ waiting: new Set(["running", "blocked_uncertain", "cancelling", "completed", "failed", "cancelled"]),
24
+ blocked_uncertain: new Set(["running", "cancelling", "failed"]),
25
+ cancelling: new Set(["cancelled", "failed"]),
26
+ cancelled: new Set(),
27
+ completed: new Set(),
28
+ failed: new Set(),
29
+ };
30
+ const STEP_EDGES: Readonly<Record<string, ReadonlySet<string>>> = {
31
+ pending: new Set(["preparing"]),
32
+ preparing: new Set(["running"]),
33
+ running: new Set(["passed", "failed", "cancelled"]),
34
+ };
35
+ const ASSIGNMENT_EDGES: Readonly<Record<string, ReadonlySet<string>>> = {
36
+ created: new Set(["accepted"]),
37
+ accepted: new Set(["dispatched"]),
38
+ dispatched: new Set(["executing"]),
39
+ executing: new Set(["result_recorded"]),
40
+ result_recorded: new Set(["terminal"]),
41
+ };
42
+ const ATTEMPT_EDGES: Readonly<Record<string, ReadonlySet<string>>> = {
43
+ created: new Set(["starting"]),
44
+ starting: new Set(["executing"]),
45
+ executing: new Set(["settling"]),
46
+ settling: new Set(["terminal"]),
47
+ };
48
+
49
+ export type VnextRunEffectState =
50
+ | "intent"
51
+ | "dispatched"
52
+ | "observed-complete"
53
+ | "observed-no-start"
54
+ | "observed-unknown"
55
+ | "blocked_uncertain"
56
+ | "settled"
57
+ | "settled-proof";
58
+
59
+ export interface VnextFoldOptions {
60
+ readonly observationLookup?: (
61
+ attemptId: string,
62
+ observationId: string,
63
+ ) => { completeness: "complete" | "incomplete" | "no-start" } | undefined;
64
+ readonly gateKind?: (stepId: string) => string | undefined;
65
+ }
66
+
67
+ export interface VnextFoldAttemptState {
68
+ readonly status: string;
69
+ readonly resultClass?: string | undefined;
70
+ readonly outcome?: string | undefined;
71
+ }
72
+
73
+ export interface VnextFoldAssignmentState {
74
+ readonly status: string;
75
+ readonly currentAttemptId?: string | undefined;
76
+ readonly attempts: Readonly<Record<string, VnextFoldAttemptState>>;
77
+ }
78
+
79
+ export interface VnextFoldPanel {
80
+ readonly order: readonly string[];
81
+ readonly assignments: Readonly<Record<string, VnextFoldAssignmentState>>;
82
+ }
83
+
84
+ export type VnextJoinAllResult =
85
+ | { readonly tag: "unsatisfied" }
86
+ | { readonly tag: "outcome"; readonly outcome: string }
87
+ | { readonly tag: "conflict" }
88
+ | { readonly tag: "rejected" }
89
+ | { readonly tag: "cancelled" };
90
+
91
+ export interface VnextRunCurrentStep {
92
+ readonly stepId: string;
93
+ readonly stepAttempt: number;
94
+ readonly status: string;
95
+ readonly panel: VnextFoldPanel;
96
+ readonly assignmentId?: string | undefined;
97
+ readonly attemptId?: string | undefined;
98
+ readonly assignmentStatus?: string | undefined;
99
+ readonly attemptStatus?: string | undefined;
100
+ readonly outcome?: string | undefined;
101
+ readonly effectId?: string | undefined;
102
+ readonly effectState?: VnextRunEffectState | undefined;
103
+ readonly observationId?: string | undefined;
104
+ readonly observationHash?: string | undefined;
105
+ readonly observationCompleteness?: "complete" | "no-start" | "unknown" | undefined;
106
+ readonly settledOutcome?: string | undefined;
107
+ readonly evidenceRefs?: readonly { id: string; kind: string; hash: string; status: string }[] | undefined;
108
+ }
109
+
110
+ export interface VnextRunState {
111
+ readonly schema: typeof VNEXT_RUN_STATE_SCHEMA;
112
+ readonly runId: string;
113
+ readonly status: VnextRunStatus;
114
+ readonly runPlanHash?: string | undefined;
115
+ readonly pendingStepId?: string | undefined;
116
+ readonly currentStep?: VnextRunCurrentStep | undefined;
117
+ readonly stepAttempts: Readonly<Record<string, number>>;
118
+ readonly edgeTransitions: Readonly<Record<string, number>>;
119
+ readonly transitionsUsed: number;
120
+ readonly cancelRequested: boolean;
121
+ readonly terminalReason?: string | undefined;
122
+ readonly failureReason?: string | undefined;
123
+ }
124
+
125
+ interface MutableAttempt {
126
+ status: string;
127
+ resultClass?: string | undefined;
128
+ outcome?: string | undefined;
129
+ }
130
+
131
+ interface MutableAssignment {
132
+ status: string;
133
+ currentAttemptId?: string | undefined;
134
+ attempts: Record<string, MutableAttempt>;
135
+ }
136
+
137
+ interface MutablePanel {
138
+ order: string[];
139
+ assignments: Record<string, MutableAssignment>;
140
+ }
141
+
142
+ interface MutableCurrentStep {
143
+ stepId: string;
144
+ stepAttempt: number;
145
+ status: string;
146
+ panel: MutablePanel;
147
+ outcome?: string | undefined;
148
+ effectId?: string | undefined;
149
+ effectState?: VnextRunEffectState | undefined;
150
+ observationId?: string | undefined;
151
+ observationHash?: string | undefined;
152
+ observationCompleteness?: "complete" | "no-start" | "unknown" | undefined;
153
+ settledOutcome?: string | undefined;
154
+ evidenceRefs?: Array<{ id: string; kind: string; hash: string; status: string }> | undefined;
155
+ }
156
+
157
+ interface MutableState {
158
+ schema: typeof VNEXT_RUN_STATE_SCHEMA;
159
+ runId: string;
160
+ status: VnextRunStatus;
161
+ runPlanHash?: string | undefined;
162
+ pendingStepId?: string | undefined;
163
+ currentStep?: MutableCurrentStep | undefined;
164
+ stepAttempts: Record<string, number>;
165
+ edgeTransitions: Record<string, number>;
166
+ transitionsUsed: number;
167
+ cancelRequested: boolean;
168
+ terminalReason?: string | undefined;
169
+ terminalEventSeen: boolean;
170
+ lastOutcomeEvent?: { stepId: string; stepAttempt: number; outcome: string } | undefined;
171
+ awaitingTransition: boolean;
172
+ cancelCommandId?: string | undefined;
173
+ lastTerminalTransition?: VnextTerminalStatus | undefined;
174
+ }
175
+
176
+ function emptyPanel(): MutablePanel {
177
+ return { order: [], assignments: Object.create(null) as Record<string, MutableAssignment> };
178
+ }
179
+
180
+ function panelAssignmentId(current: MutableCurrentStep): string | undefined {
181
+ return current.panel.order[0];
182
+ }
183
+
184
+ function panelAssignment(current: MutableCurrentStep, assignmentId?: string): MutableAssignment | undefined {
185
+ const id = assignmentId ?? panelAssignmentId(current);
186
+ return id ? current.panel.assignments[id] : undefined;
187
+ }
188
+
189
+ function panelAttemptId(current: MutableCurrentStep): string | undefined {
190
+ return panelAssignment(current)?.currentAttemptId;
191
+ }
192
+
193
+ function panelAttempt(current: MutableCurrentStep): MutableAttempt | undefined {
194
+ const assignment = panelAssignment(current);
195
+ const attemptId = assignment?.currentAttemptId;
196
+ return attemptId ? assignment.attempts[attemptId] : undefined;
197
+ }
198
+
199
+ export function vnextFoldPanelAttempt(
200
+ step: VnextRunCurrentStep | MutableCurrentStep | undefined,
201
+ attemptId: string,
202
+ ): { assignmentId: string; assignment: MutableAssignment | VnextFoldAssignmentState; attempt: MutableAttempt | VnextFoldAttemptState } | undefined {
203
+ if (!step) return undefined;
204
+ for (const assignmentId of step.panel.order) {
205
+ const assignment = step.panel.assignments[assignmentId];
206
+ const attempt = assignment?.attempts[attemptId];
207
+ if (attempt) return { assignmentId, assignment, attempt };
208
+ }
209
+ return undefined;
210
+ }
211
+
212
+ function findPanelAttempt(
213
+ panel: MutablePanel,
214
+ attemptId: string,
215
+ ): { assignmentId: string; assignment: MutableAssignment; attempt: MutableAttempt } | undefined {
216
+ for (const assignmentId of panel.order) {
217
+ const assignment = panel.assignments[assignmentId];
218
+ const attempt = assignment?.attempts[attemptId];
219
+ if (attempt && assignment) return { assignmentId, assignment, attempt };
220
+ }
221
+ return undefined;
222
+ }
223
+
224
+ function panelAssignmentBound(plan: VnextCompiledPlan | undefined, current: MutableCurrentStep): number {
225
+ const step = plan?.steps[current.stepId];
226
+ if (!step) return FOLD_PANEL_BOUND;
227
+ if ((step.kind === "agent" || step.kind === "moa") && (step.join.strategy === "all" || step.join.strategy === "all-settled")) {
228
+ return step.assignments.maximum;
229
+ }
230
+ return FOLD_PANEL_BOUND;
231
+ }
232
+
233
+ function panelAssignmentsTerminal(current: MutableCurrentStep): boolean {
234
+ if (current.panel.order.length === 0) return false;
235
+ for (const assignmentId of current.panel.order) {
236
+ const assignment = current.panel.assignments[assignmentId];
237
+ if (!assignment || assignment.status !== "terminal") return false;
238
+ }
239
+ return true;
240
+ }
241
+
242
+ function panelIssuedAttemptsTerminal(current: MutableCurrentStep): boolean {
243
+ for (const assignmentId of current.panel.order) {
244
+ const assignment = current.panel.assignments[assignmentId];
245
+ if (!assignment) return false;
246
+ for (const attempt of Object.values(assignment.attempts)) {
247
+ if (attempt.status !== "terminal") return false;
248
+ }
249
+ }
250
+ return true;
251
+ }
252
+
253
+ function panelInFlightCount(panel: MutablePanel): number {
254
+ let count = 0;
255
+ for (const assignmentId of panel.order) {
256
+ const assignment = panel.assignments[assignmentId];
257
+ if (!assignment) continue;
258
+ for (const attempt of Object.values(assignment.attempts)) {
259
+ if (attempt.status === "starting" || attempt.status === "executing" || attempt.status === "settling") count += 1;
260
+ }
261
+ }
262
+ return count;
263
+ }
264
+
265
+ function panelFrozen(current: MutableCurrentStep): boolean {
266
+ return current.outcome !== undefined
267
+ || current.status === "passed"
268
+ || current.status === "failed"
269
+ || current.status === "cancelled";
270
+ }
271
+
272
+ function createdOnlySingletonCancel(state: MutableState, step: VnextCompiledStep, current: MutableCurrentStep, status: string): boolean {
273
+ const assignmentId = current.panel.order[0];
274
+ const assignment = assignmentId ? current.panel.assignments[assignmentId] : undefined;
275
+ return status === "cancelled"
276
+ && state.cancelRequested
277
+ && current.panel.order.length === 1
278
+ && current.panel.order.length >= step.assignments.minimum
279
+ && assignment?.status === "created";
280
+ }
281
+
282
+ function authorizedCancelStep(state: MutableState, step: VnextCompiledStep, current: MutableCurrentStep, status: string): boolean {
283
+ if (status !== "cancelled" || !state.cancelRequested) return false;
284
+ if (createdOnlySingletonCancel(state, step, current, status)) return true;
285
+ if (!panelIssuedAttemptsTerminal(current) || !panelAssignmentsTerminal(current)) return false;
286
+ const members: Array<{ resultClass: string; outcome: string | undefined }> = [];
287
+ for (const assignmentId of current.panel.order) {
288
+ const assignment = current.panel.assignments[assignmentId];
289
+ if (!assignment) return false;
290
+ const issued = Object.values(assignment.attempts);
291
+ if (issued.length === 0) return false;
292
+ for (const attempt of issued) {
293
+ if (attempt.status !== "terminal" || !attempt.resultClass) return false;
294
+ }
295
+ const currentAttempt = currentAttemptOf(assignment);
296
+ if (!currentAttempt?.resultClass) return false;
297
+ members.push({ resultClass: currentAttempt.resultClass, outcome: currentAttempt.outcome });
298
+ }
299
+ if (members.some((member) => member.resultClass === "outcome_unknown" || member.resultClass === "producer_rejected")) {
300
+ return false;
301
+ }
302
+ const declared = members.filter((member) => member.resultClass === "outcome");
303
+ if (declared.length > 0) {
304
+ const outcome = declared[0]?.outcome;
305
+ if (typeof outcome !== "string" || declared.some((member) => member.outcome !== outcome)) return false;
306
+ }
307
+ return members.every((member) => member.resultClass === "outcome" || member.resultClass === "cancelled");
308
+ }
309
+
310
+ function currentAttemptOf(assignment: {
311
+ currentAttemptId?: string | undefined;
312
+ attempts: { readonly [id: string]: { resultClass?: string | undefined; outcome?: string | undefined } | undefined };
313
+ }): { resultClass?: string | undefined; outcome?: string | undefined } | undefined {
314
+ const attemptId = assignment.currentAttemptId;
315
+ return attemptId ? assignment.attempts[attemptId] : undefined;
316
+ }
317
+
318
+ function expectedStatusForDeclaredOutcome(step: VnextCompiledStep, outcome: string): "passed" | "failed" | "cancelled" {
319
+ // Classify by outcome name only, never terminal target
320
+ if (outcome === "passed") return "passed";
321
+ if (outcome === "cancelled") return "cancelled";
322
+ return "failed";
323
+ }
324
+
325
+ export function vnextJoinAll(step: VnextCompiledStep, panel: VnextFoldPanel | MutablePanel): VnextJoinAllResult {
326
+ if (panel.order.length < step.assignments.minimum) return { tag: "unsatisfied" };
327
+ const members: Array<{ resultClass: string; outcome?: string | undefined }> = [];
328
+ for (const assignmentId of panel.order) {
329
+ const assignment = panel.assignments[assignmentId];
330
+ if (!assignment || assignment.status !== "terminal") return { tag: "unsatisfied" };
331
+ const issued = Object.values(assignment.attempts);
332
+ if (issued.length === 0) return { tag: "unsatisfied" };
333
+ for (const attempt of issued) {
334
+ if (attempt.status !== "terminal" || !attempt.resultClass) return { tag: "unsatisfied" };
335
+ }
336
+ const current = currentAttemptOf(assignment);
337
+ if (!current?.resultClass) return { tag: "unsatisfied" };
338
+ members.push({ resultClass: current.resultClass, outcome: current.outcome });
339
+ }
340
+ if (members.some((member) => member.resultClass === "outcome_unknown" || member.resultClass === "producer_rejected")) {
341
+ return { tag: "rejected" };
342
+ }
343
+ if (members.every((member) => member.resultClass === "cancelled")) return { tag: "cancelled" };
344
+ if (step.join.strategy === "all-settled") {
345
+ const passedCount = members.filter((m) => m.resultClass === "outcome" && m.outcome === "passed").length;
346
+ const minPassed = step.join.minimumPassed ?? step.assignments.minimum;
347
+ if (passedCount >= minPassed) {
348
+ return { tag: "outcome", outcome: "passed" };
349
+ }
350
+ const nonPassedGroups = new Map<string, number>();
351
+ for (const m of members) {
352
+ if (m.resultClass === "outcome" && m.outcome && m.outcome !== "passed") {
353
+ nonPassedGroups.set(m.outcome, (nonPassedGroups.get(m.outcome) ?? 0) + 1);
354
+ }
355
+ }
356
+ for (const [outcome, count] of nonPassedGroups) {
357
+ if (count >= minPassed) {
358
+ return { tag: "outcome", outcome };
359
+ }
360
+ }
361
+ return { tag: "outcome", outcome: "quorum-not-met" };
362
+ }
363
+ if (members.every((member) => member.resultClass === "outcome")) {
364
+ const outcome = members[0]?.outcome;
365
+ if (typeof outcome === "string" && members.every((member) => member.outcome === outcome)) {
366
+ return { tag: "outcome", outcome };
367
+ }
368
+ return { tag: "conflict" };
369
+ }
370
+ return { tag: "conflict" };
371
+ }
372
+
373
+ function putAssignment(current: MutableCurrentStep, assignmentId: string, assignment: MutableAssignment): MutableCurrentStep {
374
+ return {
375
+ ...current,
376
+ panel: {
377
+ order: current.panel.order,
378
+ assignments: { ...current.panel.assignments, [assignmentId]: assignment },
379
+ },
380
+ };
381
+ }
382
+
383
+ export function vnextFoldPanelAttemptIds(step: VnextRunCurrentStep | MutableCurrentStep | undefined): string[] {
384
+ if (!step) return [];
385
+ const ids: string[] = [];
386
+ for (const assignmentId of step.panel.order) {
387
+ const assignment = step.panel.assignments[assignmentId];
388
+ if (!assignment) continue;
389
+ for (const attemptId of Object.keys(assignment.attempts)) ids.push(attemptId);
390
+ }
391
+ return ids;
392
+ }
393
+
394
+ export function foldVnextRunState(
395
+ run: VnextRunRecord,
396
+ plan: VnextCompiledPlan | undefined,
397
+ events: readonly VnextRunEvent[],
398
+ options: VnextFoldOptions = {},
399
+ ): VnextRunState {
400
+ if (events.length === 0) {
401
+ throw runtimeError("run_events_illegal", run.runId, "run has no events");
402
+ }
403
+ const state: MutableState = {
404
+ schema: VNEXT_RUN_STATE_SCHEMA,
405
+ runId: run.runId,
406
+ status: "created",
407
+ stepAttempts: Object.create(null) as Record<string, number>,
408
+ edgeTransitions: Object.create(null) as Record<string, number>,
409
+ transitionsUsed: 0,
410
+ cancelRequested: false,
411
+ terminalEventSeen: false,
412
+ awaitingTransition: false,
413
+ };
414
+
415
+ for (const [index, event] of events.entries()) {
416
+ if (event.sequence !== index + 1) {
417
+ throw runtimeError("run_events_illegal", run.runId, `event sequence is not contiguous at ${event.sequence}`);
418
+ }
419
+ assertEventIdentity(run, event);
420
+ if (state.terminalEventSeen) {
421
+ throw runtimeError("run_events_illegal", run.runId, "no events are allowed after a terminal run status");
422
+ }
423
+ if (index === 0 && event.eventType !== "run.created") {
424
+ throw runtimeError("run_events_illegal", run.runId, "first event must be run.created");
425
+ }
426
+ if (state.lastTerminalTransition !== undefined) {
427
+ if (event.eventType !== "run.status_changed" || event.payload.status !== state.lastTerminalTransition) {
428
+ throw runtimeError("run_events_illegal", run.runId, "terminal transition must be followed by the matching run.status_changed");
429
+ }
430
+ }
431
+ if (
432
+ (
433
+ event.eventType.startsWith("step.")
434
+ || event.eventType.startsWith("assignment.")
435
+ || event.eventType.startsWith("attempt.")
436
+ || event.eventType.startsWith("effect.")
437
+ )
438
+ && !plan
439
+ ) {
440
+ throw runtimeError("run_plan_missing", run.runId, "step events require a pinned run plan");
441
+ }
442
+ if (state.currentStep?.effectState === "blocked_uncertain") {
443
+ const namesFrozen = event.payload.stepId === state.currentStep.stepId
444
+ || event.payload.attemptId === panelAttemptId(state.currentStep)
445
+ || event.payload.assignmentId === panelAssignmentId(state.currentStep)
446
+ || (event.payload.effect !== undefined && typeof event.payload.effect === "object" && event.payload.effect !== null && "id" in event.payload.effect && (event.payload.effect as { id?: unknown }).id === state.currentStep.effectId);
447
+ const blockedFamily = event.eventType.startsWith("step.")
448
+ || event.eventType.startsWith("assignment.")
449
+ || event.eventType.startsWith("attempt.")
450
+ || event.eventType.startsWith("effect.");
451
+ if (blockedFamily && namesFrozen) {
452
+ throw runtimeError("run_events_illegal", run.runId, "blocked_uncertain freezes the gate attempt");
453
+ }
454
+ }
455
+ switch (event.eventType) {
456
+ case "run.created":
457
+ foldRunCreated(state, run, event, index);
458
+ break;
459
+ case "run.status_changed":
460
+ foldRunStatus(state, plan, event);
461
+ break;
462
+ case "run.cancel_requested":
463
+ foldCancelRequested(state, event);
464
+ break;
465
+ case "step.entered":
466
+ foldStepEntered(state, plan!, event);
467
+ break;
468
+ case "step.status_changed":
469
+ foldStepStatus(state, plan, event);
470
+ break;
471
+ case "step.outcome_recorded":
472
+ foldOutcome(state, plan!, event);
473
+ break;
474
+ case "step.transitioned":
475
+ foldTransitioned(state, plan!, event);
476
+ break;
477
+ case "assignment.created":
478
+ foldAssignmentCreated(state, plan, event);
479
+ break;
480
+ case "assignment.accepted":
481
+ foldAssignmentAdvance(state, plan!, event, "accepted");
482
+ break;
483
+ case "assignment.dispatched":
484
+ foldAssignmentAdvance(state, plan!, event, "dispatched");
485
+ break;
486
+ case "assignment.executing":
487
+ foldAssignmentAdvance(state, plan!, event, "executing");
488
+ break;
489
+ case "assignment.result_recorded":
490
+ foldAssignmentAdvance(state, plan!, event, "result_recorded");
491
+ break;
492
+ case "assignment.terminal":
493
+ foldAssignmentAdvance(state, plan!, event, "terminal");
494
+ break;
495
+ case "attempt.created":
496
+ foldAttemptCreated(state, event);
497
+ break;
498
+ case "attempt.status_changed":
499
+ foldAttemptStatus(state, plan!, event);
500
+ break;
501
+ case "effect.intent_recorded":
502
+ foldEffectIntent(state, plan!, event);
503
+ break;
504
+ case "effect.dispatched":
505
+ foldEffectDispatched(state, plan!, event);
506
+ break;
507
+ case "effect.observed":
508
+ foldEffectObserved(state, plan!, event, options);
509
+ break;
510
+ case "effect.settled":
511
+ foldEffectSettled(state, plan!, event);
512
+ break;
513
+ case "effect.blocked_uncertain":
514
+ foldEffectUncertain(state, plan!, event);
515
+ break;
516
+ case "routing.attempt.recorded":
517
+ break;
518
+ default:
519
+ throw runtimeError("run_events_illegal", run.runId, `event type ${event.eventType} is not legal in this engine slice`);
520
+ }
521
+ }
522
+
523
+ if ((state.status === "preparing" || state.status === "running" || state.status === "cancelling") && !plan) {
524
+ throw runtimeError("run_plan_missing", run.runId, `status ${state.status} requires a pinned run plan`);
525
+ }
526
+ return freezeState(state);
527
+ }
528
+
529
+ function assertEventIdentity(run: VnextRunRecord, event: VnextRunEvent): void {
530
+ if (event.schema !== "kxm.run-event.v1") {
531
+ throw runtimeError("run_events_illegal", run.runId, "event schema is not kxm.run-event.v1");
532
+ }
533
+ if (event.runId !== run.runId || event.projectId !== run.projectId || event.homeRuntimeId !== run.homeRuntimeId) {
534
+ throw runtimeError("run_event_owner_mismatch", run.runId, "event identity does not match the run");
535
+ }
536
+ if (
537
+ event.configRevision !== run.configRevision
538
+ || event.memoryRevision !== run.memoryRevision
539
+ || event.executorPolicyRevision !== run.executorPolicyRevision
540
+ || event.toolPolicyRevision !== run.toolPolicyRevision
541
+ ) {
542
+ throw runtimeError("run_event_owner_mismatch", run.runId, "event revisions do not match the run");
543
+ }
544
+ }
545
+
546
+ function foldRunCreated(state: MutableState, run: VnextRunRecord, event: VnextRunEvent, index: number): void {
547
+ if (index !== 0 || event.sequence !== 1) {
548
+ throw runtimeError("run_events_illegal", state.runId, "run.created is only legal at sequence 1");
549
+ }
550
+ if (event.payload.status !== "created") {
551
+ throw runtimeError("run_events_illegal", state.runId, "run.created payload status must be created");
552
+ }
553
+ if (event.payload.workflowId !== run.workflowId) {
554
+ throw runtimeError("run_events_illegal", state.runId, "run.created workflowId does not match the run");
555
+ }
556
+ state.status = "created";
557
+ }
558
+
559
+ function foldRunStatus(state: MutableState, plan: VnextCompiledPlan | undefined, event: VnextRunEvent): void {
560
+ const next = event.payload.status;
561
+ if (typeof next !== "string" || !RUN_STATUSES.has(next as VnextRunStatus)) {
562
+ throw runtimeError("run_events_illegal", state.runId, `run.status_changed has invalid status ${String(next)}`);
563
+ }
564
+ const status = next as VnextRunStatus;
565
+ if (SLICE_BLOCKED_RUN.has(status)) {
566
+ throw runtimeError("run_events_illegal", state.runId, `event type ${event.eventType} is not legal in this engine slice`);
567
+ }
568
+ const allowed = RUN_EDGES[state.status];
569
+ if (!allowed?.has(status)) {
570
+ throw runtimeError("run_events_illegal", state.runId, `illegal run transition ${state.status} -> ${status}`);
571
+ }
572
+ if (status === "cancelling") {
573
+ if (!state.cancelRequested) {
574
+ throw runtimeError("run_events_illegal", state.runId, "cancelling requires run.cancel_requested");
575
+ }
576
+ }
577
+ if (status === "blocked_uncertain") {
578
+ if (state.currentStep?.effectState !== "blocked_uncertain") {
579
+ throw runtimeError("run_events_illegal", state.runId, "blocked_uncertain requires an uncertain step effect");
580
+ }
581
+ }
582
+ if (status === "running" && state.status === "blocked_uncertain") {
583
+ if (state.currentStep) {
584
+ state.pendingStepId = state.currentStep.stepId;
585
+ state.currentStep = undefined;
586
+ }
587
+ }
588
+ if (status === "preparing") {
589
+ const hash = event.payload.runPlanHash;
590
+ if (typeof hash !== "string") {
591
+ throw runtimeError("run_events_illegal", state.runId, "preparing requires runPlanHash");
592
+ }
593
+ state.runPlanHash = hash;
594
+ }
595
+ if ((status === "running" || status === "cancelling") && !plan) {
596
+ throw runtimeError("run_plan_missing", state.runId, `status ${status} requires a pinned run plan`);
597
+ }
598
+ if (TERMINAL_RUN.has(status)) {
599
+ assertTerminalRunStatus(state, plan, status, event);
600
+ if (typeof event.payload.reason === "string") state.terminalReason = event.payload.reason;
601
+ state.terminalEventSeen = true;
602
+ state.awaitingTransition = false;
603
+ state.lastTerminalTransition = undefined;
604
+ state.currentStep = undefined;
605
+ state.pendingStepId = undefined;
606
+ }
607
+ state.status = status;
608
+ }
609
+
610
+ function assertTerminalRunStatus(
611
+ state: MutableState,
612
+ plan: VnextCompiledPlan | undefined,
613
+ status: VnextRunStatus,
614
+ event: VnextRunEvent,
615
+ ): void {
616
+ if (status === "completed") {
617
+ if (state.lastTerminalTransition !== "completed") {
618
+ throw runtimeError("run_events_illegal", state.runId, "completed requires a selected terminal transition");
619
+ }
620
+ return;
621
+ }
622
+ if (status === "cancelled") {
623
+ if (state.lastTerminalTransition === "cancelled" && state.status === "running") return;
624
+ if ((state.status === "created" || state.status === "preparing") && state.cancelRequested) return;
625
+ if (state.status === "cancelling") {
626
+ if (!state.currentStep) return;
627
+ if (state.currentStep.status === "cancelled" && panelIssuedAttemptsTerminal(state.currentStep)) {
628
+ return;
629
+ }
630
+ if (state.currentStep.effectState === "blocked_uncertain") {
631
+ return;
632
+ }
633
+ }
634
+ throw runtimeError("run_events_illegal", state.runId, "cancelled requires a selected terminal or a settled operator cancel");
635
+ }
636
+ if (status === "failed") {
637
+ if (state.lastTerminalTransition === "failed") return;
638
+ if (isProvenFailure(state, plan)) return;
639
+ if (
640
+ event.payload.reason === "budget_model_cost"
641
+ || event.payload.reason === "budget_unmetered_attempts"
642
+ || event.payload.reason === "budget_step_attempts"
643
+ ) {
644
+ return;
645
+ }
646
+ if (state.status === "blocked_uncertain" || state.currentStep?.effectState === "blocked_uncertain") return;
647
+ const currentStep = state.currentStep;
648
+ if (event.payload.reason === "executing_unrecorded" && currentStep && currentStep.panel.order.length === 1 && panelAttempt(currentStep)?.status === "starting") {
649
+ const step = plan?.steps[currentStep.stepId];
650
+ if (step?.kind === "gate") {
651
+ throw runtimeError("run_events_illegal", state.runId, "executing_unrecorded is illegal on a gate step");
652
+ }
653
+ if (step && (step.kind === "agent" || step.kind === "moa") && step.assignments.maximum > 1) {
654
+ throw runtimeError("run_events_illegal", state.runId, "executing_unrecorded is illegal on a multi-assignment panel");
655
+ }
656
+ return;
657
+ }
658
+ throw runtimeError("run_events_illegal", state.runId, "failed requires a selected failed terminal or proven rejection/budget facts");
659
+ }
660
+ }
661
+
662
+ function isProvenFailure(state: MutableState, plan: VnextCompiledPlan | undefined): boolean {
663
+ const current = state.currentStep;
664
+ if (current) {
665
+ const stepTerminal = current.status === "passed" || current.status === "failed" || current.status === "cancelled";
666
+ const settled = panelIssuedAttemptsTerminal(current) && panelAssignmentsTerminal(current);
667
+ if (stepTerminal && settled) {
668
+ const step = plan?.steps[current.stepId];
669
+ if (step && current.status === "failed") {
670
+ const joined = vnextJoinAll(step, current.panel);
671
+ if (joined.tag === "rejected" || joined.tag === "conflict") return true;
672
+ }
673
+ if (plan && current.outcome) {
674
+ const selected = plan.steps[current.stepId]?.transitions[current.outcome];
675
+ if (selected) {
676
+ const edgeKey = `${current.stepId}:${current.outcome}`;
677
+ const edgeUsed = (state.edgeTransitions[edgeKey] ?? 0) + 1;
678
+ if (selected.maxTransitions !== undefined && edgeUsed > selected.maxTransitions) return true;
679
+ if (state.transitionsUsed + 1 > plan.transitionBudget) return true;
680
+ if (selected.to === "step") {
681
+ const used = state.stepAttempts[selected.target] ?? 0;
682
+ const target = plan.steps[selected.target];
683
+ if (target && used >= target.maxAttempts) return true;
684
+ }
685
+ }
686
+ }
687
+ }
688
+ return false;
689
+ }
690
+ if (!plan) return false;
691
+ const stepId = state.pendingStepId ?? plan.entryStepId;
692
+ const step = plan.steps[stepId];
693
+ const used = state.stepAttempts[stepId] ?? 0;
694
+ return Boolean(step && used >= step.maxAttempts);
695
+ }
696
+
697
+ function foldCancelRequested(state: MutableState, event: VnextRunEvent): void {
698
+ if (state.cancelRequested || TERMINAL_RUN.has(state.status)) {
699
+ throw runtimeError("run_events_illegal", state.runId, "duplicate run.cancel_requested is not legal");
700
+ }
701
+ state.cancelRequested = true;
702
+ if (event.commandId) state.cancelCommandId = event.commandId;
703
+ }
704
+
705
+ function requireRunning(state: MutableState, eventType: string): void {
706
+ if (state.status !== "running" && !(state.status === "cancelling" && eventType !== "step.entered")) {
707
+ throw runtimeError("run_events_illegal", state.runId, `${eventType} is not legal while the run is ${state.status}`);
708
+ }
709
+ }
710
+
711
+ function foldStepEntered(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent): void {
712
+ if (state.status !== "running") {
713
+ throw runtimeError("run_events_illegal", state.runId, "step.entered is only legal while running");
714
+ }
715
+ if (state.currentStep) {
716
+ throw runtimeError("run_events_illegal", state.runId, "step.entered is illegal while a step is active");
717
+ }
718
+ const stepId = stringPayload(event, "stepId");
719
+ const stepAttempt = integerPayload(event, "stepAttempt");
720
+ const expected = state.pendingStepId ?? plan.entryStepId;
721
+ if (stepId !== expected) {
722
+ throw runtimeError("run_events_illegal", state.runId, `step.entered ${stepId} does not match pending ${expected}`);
723
+ }
724
+ const step = requireStep(plan, stepId, state.runId);
725
+ const used = state.stepAttempts[stepId] ?? 0;
726
+ if (used >= step.maxAttempts) {
727
+ throw runtimeError("run_events_illegal", state.runId, `step ${stepId} exceeds maxAttempts`);
728
+ }
729
+ if (stepAttempt !== used + 1) {
730
+ throw runtimeError("run_events_illegal", state.runId, `stepAttempt ${stepAttempt} is not the next attempt for ${stepId}`);
731
+ }
732
+ state.stepAttempts[stepId] = stepAttempt;
733
+ state.pendingStepId = undefined;
734
+ state.awaitingTransition = false;
735
+ state.lastOutcomeEvent = undefined;
736
+ state.currentStep = { stepId, stepAttempt, status: "pending", panel: emptyPanel() };
737
+ }
738
+
739
+ function foldStepStatus(state: MutableState, plan: VnextCompiledPlan | undefined, event: VnextRunEvent): void {
740
+ requireActiveStep(state, "step.status_changed");
741
+ const stepId = stringPayload(event, "stepId");
742
+ const status = stringPayload(event, "status");
743
+ if (stepId !== state.currentStep!.stepId) {
744
+ throw runtimeError("run_events_illegal", state.runId, "step.status_changed does not match the active step");
745
+ }
746
+ if (!STEP_STATUSES.has(status)) {
747
+ throw runtimeError("run_events_illegal", state.runId, `illegal step status ${status}`);
748
+ }
749
+ const allowed = STEP_EDGES[state.currentStep!.status];
750
+ if (!allowed?.has(status)) {
751
+ throw runtimeError("run_events_illegal", state.runId, `illegal step transition ${state.currentStep!.status} -> ${status}`);
752
+ }
753
+ if (status === "passed" || status === "failed" || status === "cancelled") {
754
+ const current = state.currentStep!;
755
+ if (!panelIssuedAttemptsTerminal(current)) {
756
+ throw runtimeError("run_events_illegal", state.runId, "terminal step status requires a terminal attempt");
757
+ }
758
+ const step = plan ? requireStep(plan, current.stepId, state.runId) : undefined;
759
+ if (step) {
760
+ const joined = vnextJoinAll(step, current.panel);
761
+ if (joined.tag === "outcome") {
762
+ const expectedStatus = expectedStatusForDeclaredOutcome(step, joined.outcome);
763
+ const selected = step.transitions[joined.outcome];
764
+ const isCustomOutcomeCancelled = typeof joined.outcome === "string"
765
+ && joined.outcome !== "passed"
766
+ && joined.outcome !== "cancelled"
767
+ && selected?.to === "terminal"
768
+ && selected.terminalStatus === "cancelled"
769
+ && status === "cancelled";
770
+ if (status === "cancelled" && authorizedCancelStep(state, step, current, status)) {
771
+ // Durable cancel may coexist with consistent declared outcomes.
772
+ } else if (status === "passed" && state.cancelRequested && current.panel.order.length < step.assignments.target) {
773
+ throw runtimeError("run_events_illegal", state.runId, "cancel before target refill cannot mint a passed step");
774
+ } else if (status !== expectedStatus && !isCustomOutcomeCancelled) {
775
+ if (expectedStatus === "passed") {
776
+ throw runtimeError("run_events_illegal", state.runId, "passed outcome must lead to passed status");
777
+ }
778
+ if (expectedStatus === "cancelled") {
779
+ throw runtimeError("run_events_illegal", state.runId, "cancelled outcome must lead to cancelled status");
780
+ }
781
+ throw runtimeError("run_events_illegal", state.runId, "non-passed declared outcome must lead to failed status");
782
+ }
783
+ } else if (joined.tag === "unsatisfied") {
784
+ if (!authorizedCancelStep(state, step, current, status)) {
785
+ throw runtimeError("run_events_illegal", state.runId, "terminal step status is not legal for this panel join");
786
+ }
787
+ } else if (joined.tag === "rejected" || joined.tag === "conflict") {
788
+ if (status === "cancelled") {
789
+ if (!authorizedCancelStep(state, step, current, status)) {
790
+ throw runtimeError("run_events_illegal", state.runId, "rejected or conflict cannot cancel the step");
791
+ }
792
+ } else if (status !== "failed") {
793
+ throw runtimeError("run_events_illegal", state.runId, "passed requires recordedOutcome passed");
794
+ }
795
+ } else if (joined.tag === "cancelled") {
796
+ if (!state.cancelRequested) {
797
+ throw runtimeError("run_events_illegal", state.runId, "cancelled join requires cancellation authority");
798
+ }
799
+ if (status !== "cancelled") {
800
+ throw runtimeError("run_events_illegal", state.runId, "cancelled join must lead to cancelled status");
801
+ }
802
+ }
803
+ } else if (status === "passed") {
804
+ throw runtimeError("run_events_illegal", state.runId, "passed requires recordedOutcome passed");
805
+ }
806
+ if (state.lastOutcomeEvent) state.awaitingTransition = true;
807
+ }
808
+ state.currentStep = { ...state.currentStep!, status };
809
+ }
810
+
811
+ function foldOutcome(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent): void {
812
+ requireActiveStep(state, "step.outcome_recorded");
813
+ const stepId = stringPayload(event, "stepId");
814
+ const stepAttempt = integerPayload(event, "stepAttempt");
815
+ const outcome = stringPayload(event, "outcome");
816
+ const current = state.currentStep!;
817
+ if (stepId !== current.stepId || stepAttempt !== current.stepAttempt) {
818
+ throw runtimeError("run_events_illegal", state.runId, "step.outcome_recorded does not match the active attempt");
819
+ }
820
+ if (!panelIssuedAttemptsTerminal(current) || !panelAssignmentsTerminal(current)) {
821
+ throw runtimeError("run_events_illegal", state.runId, "step.outcome_recorded requires a terminal attempt and assignment");
822
+ }
823
+ if (current.outcome !== undefined) {
824
+ throw runtimeError("run_events_illegal", state.runId, "step.outcome_recorded cannot overwrite a recorded outcome");
825
+ }
826
+ const step = requireStep(plan, stepId, state.runId);
827
+ const joined = vnextJoinAll(step, current.panel);
828
+ if (joined.tag !== "outcome" || joined.outcome !== outcome) {
829
+ throw runtimeError("run_events_illegal", state.runId, "step.outcome_recorded does not match the recorded assignment outcome");
830
+ }
831
+ if (!step.outcomes.includes(outcome)) {
832
+ throw runtimeError("run_events_illegal", state.runId, `outcome ${outcome} is not declared for ${stepId}`);
833
+ }
834
+ state.currentStep = { ...current, outcome };
835
+ state.lastOutcomeEvent = { stepId, stepAttempt, outcome };
836
+ }
837
+
838
+ function foldTransitioned(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent): void {
839
+ requireActiveStep(state, "step.transitioned");
840
+ const fromStepId = stringPayload(event, "fromStepId");
841
+ const outcome = stringPayload(event, "outcome");
842
+ const current = state.currentStep!;
843
+ if (fromStepId !== current.stepId) {
844
+ throw runtimeError("run_events_illegal", state.runId, "step.transitioned does not match the active step");
845
+ }
846
+ if (current.status !== "passed" && current.status !== "failed" && current.status !== "cancelled") {
847
+ throw runtimeError("run_events_illegal", state.runId, "step.transitioned requires a settled step status");
848
+ }
849
+ if (!state.lastOutcomeEvent || state.lastOutcomeEvent.outcome !== outcome || state.lastOutcomeEvent.stepId !== fromStepId) {
850
+ throw runtimeError("run_events_illegal", state.runId, "step.transitioned requires a prior matching step.outcome_recorded");
851
+ }
852
+ if (!panelIssuedAttemptsTerminal(current) || !panelAssignmentsTerminal(current)) {
853
+ throw runtimeError("run_events_illegal", state.runId, "step.transitioned requires every panel assignment and issued attempt to be terminal");
854
+ }
855
+ const step = requireStep(plan, fromStepId, state.runId);
856
+ const joined = vnextJoinAll(step, current.panel);
857
+ if (joined.tag !== "outcome" || joined.outcome !== outcome) {
858
+ throw runtimeError("run_events_illegal", state.runId, "step.transitioned requires a matching joined declared outcome");
859
+ }
860
+ const selected = step.transitions[outcome];
861
+ if (!selected) {
862
+ throw runtimeError("run_events_illegal", state.runId, `no compiled transition for ${fromStepId}:${outcome}`);
863
+ }
864
+ assertTransitionPayload(state.runId, selected, event);
865
+ const edgeKey = `${fromStepId}:${outcome}`;
866
+ const edgeUsed = (state.edgeTransitions[edgeKey] ?? 0) + 1;
867
+ if (selected.maxTransitions !== undefined && edgeUsed > selected.maxTransitions) {
868
+ throw runtimeError("run_events_illegal", state.runId, `edge ${edgeKey} exceeds maxTransitions`);
869
+ }
870
+ const transitionsUsed = state.transitionsUsed + 1;
871
+ if (transitionsUsed > plan.transitionBudget) {
872
+ throw runtimeError("run_events_illegal", state.runId, "transitionBudget exceeded");
873
+ }
874
+ if (selected.to === "step") {
875
+ const target = requireStep(plan, selected.target, state.runId);
876
+ const targetUsed = state.stepAttempts[selected.target] ?? 0;
877
+ if (targetUsed >= target.maxAttempts) {
878
+ throw runtimeError("run_events_illegal", state.runId, `target step ${selected.target} has no remaining attempts`);
879
+ }
880
+ state.pendingStepId = selected.target;
881
+ } else {
882
+ state.lastTerminalTransition = selected.terminalStatus;
883
+ }
884
+ state.edgeTransitions[edgeKey] = edgeUsed;
885
+ state.transitionsUsed = transitionsUsed;
886
+ state.currentStep = undefined;
887
+ state.awaitingTransition = false;
888
+ state.lastOutcomeEvent = undefined;
889
+ }
890
+
891
+ function assertTransitionPayload(runId: string, selected: VnextCompiledTransition, event: VnextRunEvent): void {
892
+ if (selected.to === "step") {
893
+ if (event.payload.toStepId !== selected.target) {
894
+ throw runtimeError("run_events_illegal", runId, "step.transitioned toStepId does not match the compiled target");
895
+ }
896
+ if (event.payload.status !== undefined) {
897
+ throw runtimeError("run_events_illegal", runId, "step-target transition must not carry a terminal status");
898
+ }
899
+ return;
900
+ }
901
+ if (event.payload.toStepId !== undefined) {
902
+ throw runtimeError("run_events_illegal", runId, "terminal step.transitioned must not set toStepId");
903
+ }
904
+ if (event.payload.status !== selected.terminalStatus) {
905
+ throw runtimeError("run_events_illegal", runId, "terminal step.transitioned status does not match the compiled terminal");
906
+ }
907
+ }
908
+
909
+ function foldAssignmentCreated(state: MutableState, plan: VnextCompiledPlan | undefined, event: VnextRunEvent): void {
910
+ requireActiveStep(state, "assignment.created");
911
+ const current = state.currentStep!;
912
+ if (state.status !== "running" || state.cancelRequested || panelFrozen(current)) {
913
+ throw runtimeError("run_events_illegal", state.runId, "assignment.created is not legal after freeze or cancel intent");
914
+ }
915
+ if (current.panel.order.length >= panelAssignmentBound(plan, current)) {
916
+ throw runtimeError("run_events_illegal", state.runId, "assignment.created repeats an assignment");
917
+ }
918
+ const assignmentId = stringPayload(event, "assignmentId");
919
+ const stepId = stringPayload(event, "stepId");
920
+ const stepAttempt = integerPayload(event, "stepAttempt");
921
+ if (stepId !== current.stepId || stepAttempt !== current.stepAttempt) {
922
+ throw runtimeError("run_events_illegal", state.runId, "assignment.created does not match the active step attempt");
923
+ }
924
+ if (current.panel.assignments[assignmentId]) {
925
+ throw runtimeError("run_events_illegal", state.runId, "assignment.created repeats an assignment");
926
+ }
927
+ state.currentStep = {
928
+ ...current,
929
+ panel: {
930
+ order: [...current.panel.order, assignmentId],
931
+ assignments: {
932
+ ...current.panel.assignments,
933
+ [assignmentId]: { status: "created", attempts: Object.create(null) as Record<string, MutableAttempt> },
934
+ },
935
+ },
936
+ };
937
+ }
938
+
939
+ function foldAssignmentAdvance(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent, next: string): void {
940
+ requireActiveStep(state, `assignment.${next}`);
941
+ const current = state.currentStep!;
942
+ if (current.panel.order.length === 0) {
943
+ throw runtimeError("run_events_illegal", state.runId, `assignment ${next} has no active assignment`);
944
+ }
945
+ const assignmentId = stringPayload(event, "assignmentId");
946
+ const assignment = current.panel.assignments[assignmentId];
947
+ if (!assignment) {
948
+ throw runtimeError("run_events_illegal", state.runId, "assignment event assignmentId does not match the active assignment");
949
+ }
950
+ if (!ASSIGNMENT_STATUSES.has(next)) {
951
+ throw runtimeError("run_events_illegal", state.runId, `illegal assignment status ${next}`);
952
+ }
953
+ const step = requireStep(plan, current.stepId, state.runId);
954
+ const allowed = ASSIGNMENT_EDGES[assignment.status];
955
+ const noStartProof = step.kind === "gate"
956
+ && assignment.status === "dispatched"
957
+ && next === "result_recorded"
958
+ && (current.effectState === "observed-no-start" || current.effectState === "settled-proof");
959
+ if (!allowed?.has(next) && !noStartProof) {
960
+ throw runtimeError("run_events_illegal", state.runId, `illegal assignment transition ${assignment.status} -> ${next}`);
961
+ }
962
+ if (next === "dispatched") {
963
+ stringPayload(event, "capabilityHash");
964
+ }
965
+ if (next === "executing" && panelFrozen(current)) {
966
+ throw runtimeError("run_events_illegal", state.runId, "assignment.executing is not legal after freeze");
967
+ }
968
+ if (next === "executing" && state.cancelRequested && step.kind !== "gate") {
969
+ throw runtimeError("run_events_illegal", state.runId, "assignment.executing is not legal after cancel intent");
970
+ }
971
+ let nextAssignment: MutableAssignment = { ...assignment, status: next, attempts: { ...assignment.attempts } };
972
+ if (next === "result_recorded") {
973
+ const resultClass = stringPayload(event, "resultClass");
974
+ if (!RESULT_CLASSES.has(resultClass)) {
975
+ throw runtimeError("run_events_illegal", state.runId, `illegal resultClass ${resultClass}`);
976
+ }
977
+ const attemptId = assignment.currentAttemptId;
978
+ const attempt = attemptId ? assignment.attempts[attemptId] : undefined;
979
+ if (resultClass === "outcome") {
980
+ const outcome = stringPayload(event, "outcome");
981
+ if (!step.outcomes.includes(outcome)) {
982
+ throw runtimeError("run_events_illegal", state.runId, `outcome ${outcome} is not declared for ${current.stepId}`);
983
+ }
984
+ if (step.kind === "gate") {
985
+ if (current.effectState !== "settled") {
986
+ throw runtimeError("run_events_illegal", state.runId, "gate outcome requires evaluated effect.settled");
987
+ }
988
+ if (outcome !== current.settledOutcome) {
989
+ throw runtimeError("run_events_illegal", state.runId, "gate result outcome does not match settledOutcome");
990
+ }
991
+ }
992
+ if (attemptId && attempt) {
993
+ nextAssignment = {
994
+ ...nextAssignment,
995
+ attempts: { ...nextAssignment.attempts, [attemptId]: { ...attempt, resultClass, outcome } },
996
+ };
997
+ }
998
+ } else if (event.payload.outcome !== undefined) {
999
+ throw runtimeError("run_events_illegal", state.runId, "non-outcome resultClass must not set outcome");
1000
+ } else {
1001
+ if (step.kind === "gate") {
1002
+ if (resultClass === "outcome_unknown") {
1003
+ throw runtimeError("run_events_illegal", state.runId, "outcome_unknown is illegal on a gate step");
1004
+ }
1005
+ if (current.effectState !== "settled-proof") {
1006
+ throw runtimeError("run_events_illegal", state.runId, "gate producer_rejected or cancelled requires proof-only effect.settled");
1007
+ }
1008
+ if (resultClass === "producer_rejected" && current.observationCompleteness !== "no-start") {
1009
+ throw runtimeError("run_events_illegal", state.runId, "producer_rejected requires a no-start observation");
1010
+ }
1011
+ }
1012
+ if (attemptId && attempt) {
1013
+ nextAssignment = {
1014
+ ...nextAssignment,
1015
+ attempts: { ...nextAssignment.attempts, [attemptId]: { ...attempt, resultClass } },
1016
+ };
1017
+ }
1018
+ }
1019
+ }
1020
+ if (next === "terminal") {
1021
+ const outcome = stringPayload(event, "outcome");
1022
+ const ownerAttempt = currentAttemptOf(nextAssignment);
1023
+ const resultClass = ownerAttempt?.resultClass;
1024
+ if (resultClass === "outcome") {
1025
+ if (outcome !== ownerAttempt?.outcome) {
1026
+ throw runtimeError("run_events_illegal", state.runId, "assignment.terminal outcome does not match result_recorded");
1027
+ }
1028
+ } else if (resultClass === "outcome_unknown" || resultClass === "producer_rejected") {
1029
+ if (outcome !== "failed") {
1030
+ throw runtimeError("run_events_illegal", state.runId, "rejected or unknown results must terminal as failed");
1031
+ }
1032
+ } else if (resultClass === "cancelled") {
1033
+ if (outcome !== "cancelled") {
1034
+ throw runtimeError("run_events_illegal", state.runId, "cancelled results must terminal as cancelled");
1035
+ }
1036
+ } else {
1037
+ throw runtimeError("run_events_illegal", state.runId, "assignment.terminal requires a prior result_recorded class");
1038
+ }
1039
+ const attemptId = nextAssignment.currentAttemptId;
1040
+ const attempt = attemptId ? nextAssignment.attempts[attemptId] : undefined;
1041
+ if (attemptId && attempt && attempt.outcome === undefined) {
1042
+ nextAssignment = {
1043
+ ...nextAssignment,
1044
+ attempts: { ...nextAssignment.attempts, [attemptId]: { ...attempt, outcome } },
1045
+ };
1046
+ }
1047
+ }
1048
+ state.currentStep = putAssignment(current, assignmentId, nextAssignment);
1049
+ }
1050
+
1051
+ function foldAttemptCreated(state: MutableState, event: VnextRunEvent): void {
1052
+ requireActiveStep(state, "attempt.created");
1053
+ const current = state.currentStep!;
1054
+ if (state.status !== "running" || state.cancelRequested || panelFrozen(current)) {
1055
+ throw runtimeError("run_events_illegal", state.runId, "attempt.created is not legal after freeze or cancel intent");
1056
+ }
1057
+ const assignmentId = stringPayload(event, "assignmentId");
1058
+ const assignment = current.panel.assignments[assignmentId];
1059
+ if (!assignment) {
1060
+ throw runtimeError("run_events_illegal", state.runId, "attempt.created assignmentId does not match");
1061
+ }
1062
+ if (assignment.currentAttemptId || Object.keys(assignment.attempts).length >= FOLD_PANEL_BOUND) {
1063
+ throw runtimeError("run_events_illegal", state.runId, "attempt.created repeats an attempt");
1064
+ }
1065
+ const attemptId = stringPayload(event, "attemptId");
1066
+ if (findPanelAttempt(current.panel, attemptId)) {
1067
+ throw runtimeError("run_events_illegal", state.runId, "attempt.created repeats an attempt");
1068
+ }
1069
+ state.currentStep = putAssignment(current, assignmentId, {
1070
+ ...assignment,
1071
+ currentAttemptId: attemptId,
1072
+ attempts: { ...assignment.attempts, [attemptId]: { status: "created" } },
1073
+ });
1074
+ }
1075
+
1076
+ function foldAttemptStatus(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent): void {
1077
+ requireActiveStep(state, "attempt.status_changed");
1078
+ const current = state.currentStep!;
1079
+ if (vnextFoldPanelAttemptIds(current).length === 0) {
1080
+ throw runtimeError("run_events_illegal", state.runId, "attempt.status_changed has no active attempt");
1081
+ }
1082
+ const attemptId = stringPayload(event, "attemptId");
1083
+ const located = findPanelAttempt(current.panel, attemptId);
1084
+ if (!located) {
1085
+ throw runtimeError("run_events_illegal", state.runId, "attempt.status_changed does not match the active attempt");
1086
+ }
1087
+ const status = stringPayload(event, "status");
1088
+ if (!ATTEMPT_STATUSES.has(status)) {
1089
+ throw runtimeError("run_events_illegal", state.runId, `illegal attempt status ${status}`);
1090
+ }
1091
+ const allowed = ATTEMPT_EDGES[located.attempt.status];
1092
+ const step = requireStep(plan, current.stepId, state.runId);
1093
+ const noStartSettling = step.kind === "gate"
1094
+ && located.attempt.status === "starting"
1095
+ && status === "settling"
1096
+ && (current.effectState === "observed-no-start" || current.effectState === "settled-proof");
1097
+ if (!allowed?.has(status) && !noStartSettling) {
1098
+ throw runtimeError("run_events_illegal", state.runId, `illegal attempt transition ${located.attempt.status} -> ${status}`);
1099
+ }
1100
+ if (panelFrozen(current) && (status === "starting" || status === "executing")) {
1101
+ throw runtimeError("run_events_illegal", state.runId, "attempt start is not legal after freeze");
1102
+ }
1103
+ if (state.cancelRequested && status === "starting") {
1104
+ throw runtimeError("run_events_illegal", state.runId, "attempt start is not legal after cancel intent");
1105
+ }
1106
+ if (state.cancelRequested && status === "executing" && step.kind !== "gate") {
1107
+ throw runtimeError("run_events_illegal", state.runId, "attempt start is not legal after cancel intent");
1108
+ }
1109
+ if (status === "starting" && panelInFlightCount(current.panel) + 1 > step.assignments.maxParallel) {
1110
+ throw runtimeError("run_events_illegal", state.runId, "maxParallel exceeded");
1111
+ }
1112
+ state.currentStep = putAssignment(current, located.assignmentId, {
1113
+ ...located.assignment,
1114
+ attempts: { ...located.assignment.attempts, [attemptId]: { ...located.attempt, status } },
1115
+ });
1116
+ }
1117
+
1118
+ function requireGateEffect(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent): NonNullable<MutableState["currentStep"]> {
1119
+ requireActiveStep(state, event.eventType);
1120
+ const current = state.currentStep!;
1121
+ const step = requireStep(plan, current.stepId, state.runId);
1122
+ if (step.kind !== "gate") {
1123
+ throw runtimeError("run_events_illegal", state.runId, "agent steps reject effect events");
1124
+ }
1125
+ assertEffectIdentity(state, event, current);
1126
+ return current;
1127
+ }
1128
+
1129
+ function assertEffectIdentity(state: MutableState, event: VnextRunEvent, current: NonNullable<MutableState["currentStep"]>): void {
1130
+ const effect = event.payload.effect;
1131
+ if (!effect || typeof effect !== "object" || Array.isArray(effect)) {
1132
+ throw runtimeError("run_events_illegal", state.runId, `${event.eventType} is missing effect`);
1133
+ }
1134
+ const effectId = (effect as { id?: unknown }).id;
1135
+ if (typeof effectId !== "string" || effectId.length === 0) {
1136
+ throw runtimeError("run_events_illegal", state.runId, `${event.eventType} is missing effect.id`);
1137
+ }
1138
+ const stepId = stringPayload(event, "stepId");
1139
+ const stepAttempt = integerPayload(event, "stepAttempt");
1140
+ const assignmentId = stringPayload(event, "assignmentId");
1141
+ const attemptId = stringPayload(event, "attemptId");
1142
+ if (stepId !== current.stepId || stepAttempt !== current.stepAttempt || assignmentId !== panelAssignmentId(current) || attemptId !== panelAttemptId(current)) {
1143
+ throw runtimeError("run_events_illegal", state.runId, `${event.eventType} identity does not match the active attempt`);
1144
+ }
1145
+ if (current.effectId && current.effectId !== effectId) {
1146
+ throw runtimeError("run_events_illegal", state.runId, `${event.eventType} effect.id does not match the active effect`);
1147
+ }
1148
+ }
1149
+
1150
+ function foldEffectIntent(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent): void {
1151
+ const current = requireGateEffect(state, plan, event);
1152
+ if (panelAttempt(current)?.status !== "starting" || current.effectState) {
1153
+ throw runtimeError("run_events_illegal", state.runId, "effect.intent_recorded requires a starting attempt with no effect");
1154
+ }
1155
+ const effectId = ((event.payload.effect as { id: string }).id);
1156
+ state.currentStep = { ...current, effectId, effectState: "intent" };
1157
+ }
1158
+
1159
+ function foldEffectDispatched(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent): void {
1160
+ const current = requireGateEffect(state, plan, event);
1161
+ if (current.effectState !== "intent") {
1162
+ throw runtimeError("run_events_illegal", state.runId, "effect.dispatched requires effect intent");
1163
+ }
1164
+ if (panelAssignment(current)?.status !== "executing" || panelAttempt(current)?.status !== "executing") {
1165
+ throw runtimeError("run_events_illegal", state.runId, "effect.dispatched requires executing assignment and attempt");
1166
+ }
1167
+ state.currentStep = { ...current, effectState: "dispatched" };
1168
+ }
1169
+
1170
+ function foldEffectObserved(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent, options: VnextFoldOptions): void {
1171
+ const current = requireGateEffect(state, plan, event);
1172
+ if (current.effectState === "observed-complete" || current.effectState === "observed-no-start" || current.effectState === "observed-unknown") {
1173
+ throw runtimeError("run_events_illegal", state.runId, "a completed observation cannot be rewritten");
1174
+ }
1175
+ if (current.effectState !== "intent" && current.effectState !== "dispatched") {
1176
+ throw runtimeError("run_events_illegal", state.runId, "effect.observed is not legal from this effect state");
1177
+ }
1178
+ const receipt = event.payload.receipt;
1179
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
1180
+ throw runtimeError("run_events_illegal", state.runId, "effect.observed is missing receipt");
1181
+ }
1182
+ const receiptId = (receipt as { id?: unknown }).id;
1183
+ const receiptHash = (receipt as { hash?: unknown }).hash;
1184
+ const provider = (receipt as { provider?: unknown }).provider;
1185
+ const kind = (receipt as { kind?: unknown }).kind;
1186
+ if (typeof receiptId !== "string" || receiptId.length === 0) {
1187
+ throw runtimeError("run_events_illegal", state.runId, "effect.observed receipt.id is invalid");
1188
+ }
1189
+ if (provider !== "kxm-gate") {
1190
+ throw runtimeError("run_events_illegal", state.runId, "effect.observed receipt.provider must be kxm-gate");
1191
+ }
1192
+ const expectedKind = options.gateKind?.(current.stepId);
1193
+ if (expectedKind && kind !== expectedKind) {
1194
+ throw runtimeError("run_events_illegal", state.runId, "effect.observed receipt.kind does not match the pinned definition");
1195
+ }
1196
+ const looked = options.observationLookup?.(panelAttemptId(current)!, receiptId);
1197
+ let effectState: VnextRunEffectState = "observed-unknown";
1198
+ if (looked) {
1199
+ if (looked.completeness === "complete") {
1200
+ if (current.effectState === "intent" && expectedKind !== "artifacts-exist") {
1201
+ throw runtimeError("run_events_illegal", state.runId, "complete command observations require effect.dispatched");
1202
+ }
1203
+ effectState = "observed-complete";
1204
+ } else if (looked.completeness === "no-start") {
1205
+ if (current.effectState !== "intent") {
1206
+ throw runtimeError("run_events_illegal", state.runId, "no-start observations are only legal from intent");
1207
+ }
1208
+ effectState = "observed-no-start";
1209
+ } else {
1210
+ throw runtimeError("run_events_illegal", state.runId, "effect.observed cannot record an incomplete observation");
1211
+ }
1212
+ }
1213
+ state.currentStep = {
1214
+ ...current,
1215
+ effectState,
1216
+ observationId: receiptId,
1217
+ observationCompleteness: effectState === "observed-complete" ? "complete" : effectState === "observed-no-start" ? "no-start" : "unknown",
1218
+ ...(typeof receiptHash === "string" ? { observationHash: receiptHash } : {}),
1219
+ };
1220
+ }
1221
+
1222
+ function foldEffectSettled(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent): void {
1223
+ const current = requireGateEffect(state, plan, event);
1224
+ const hasOutcome = event.payload.outcome !== undefined;
1225
+ const hasEvidence = event.payload.evidenceRefs !== undefined;
1226
+ const receipt = event.payload.receipt;
1227
+ if (hasOutcome || hasEvidence) {
1228
+ if (current.effectState !== "observed-complete") {
1229
+ throw runtimeError("run_events_illegal", state.runId, "evaluated effect.settled requires observed-complete");
1230
+ }
1231
+ const outcome = stringPayload(event, "outcome");
1232
+ if (outcome !== "passed" && outcome !== "implementation-failure" && outcome !== "repro-missing") {
1233
+ throw runtimeError("run_events_illegal", state.runId, "evaluated effect.settled outcome is invalid");
1234
+ }
1235
+ const step = requireStep(plan, current.stepId, state.runId);
1236
+ if (!step.outcomes.includes(outcome)) {
1237
+ throw runtimeError("run_events_illegal", state.runId, `outcome ${outcome} is not declared for ${current.stepId}`);
1238
+ }
1239
+ const refs = event.payload.evidenceRefs;
1240
+ if (!Array.isArray(refs) || refs.length !== 1) {
1241
+ throw runtimeError("run_events_illegal", state.runId, "evaluated effect.settled requires exactly one evidenceRef");
1242
+ }
1243
+ const ref = refs[0] as { id?: unknown; kind?: unknown; hash?: unknown; status?: unknown };
1244
+ if (typeof ref.id !== "string" || ref.kind !== "gate" || typeof ref.hash !== "string" || !/^sha256:[a-f0-9]{64}$/.test(ref.hash) || ref.status !== "settled") {
1245
+ throw runtimeError("run_events_illegal", state.runId, "evaluated effect.settled evidenceRef is invalid");
1246
+ }
1247
+ state.currentStep = {
1248
+ ...current,
1249
+ effectState: "settled",
1250
+ settledOutcome: outcome,
1251
+ evidenceRefs: [{ id: ref.id, kind: "gate", hash: ref.hash, status: "settled" }],
1252
+ };
1253
+ return;
1254
+ }
1255
+ if (current.effectState !== "observed-complete" && current.effectState !== "observed-no-start") {
1256
+ throw runtimeError("run_events_illegal", state.runId, "proof-only effect.settled requires a prior observation");
1257
+ }
1258
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
1259
+ throw runtimeError("run_events_illegal", state.runId, "proof-only effect.settled is missing observation receipt");
1260
+ }
1261
+ const receiptId = (receipt as { id?: unknown }).id;
1262
+ const receiptHash = (receipt as { hash?: unknown }).hash;
1263
+ if (receiptId !== current.observationId || (current.observationHash && receiptHash !== current.observationHash)) {
1264
+ throw runtimeError("run_events_illegal", state.runId, "proof-only effect.settled does not bind the prior observation");
1265
+ }
1266
+ state.currentStep = { ...current, effectState: "settled-proof" };
1267
+ }
1268
+
1269
+ function foldEffectUncertain(state: MutableState, plan: VnextCompiledPlan, event: VnextRunEvent): void {
1270
+ const current = requireGateEffect(state, plan, event);
1271
+ if (current.effectState === "observed-complete" || current.effectState === "observed-no-start" || current.effectState === "observed-unknown") {
1272
+ throw runtimeError("run_events_illegal", state.runId, "blocked_uncertain after an observation is illegal");
1273
+ }
1274
+ if (current.effectState !== "intent" && current.effectState !== "dispatched") {
1275
+ throw runtimeError("run_events_illegal", state.runId, "effect.blocked_uncertain is not legal from this effect state");
1276
+ }
1277
+ const reason = stringPayload(event, "reason");
1278
+ const allowed = new Set(["timeout", "cancel", "stream-error", "stop-error", "lost-close", "signal-termination", "recording-error"]);
1279
+ if (!allowed.has(reason)) {
1280
+ throw runtimeError("run_events_illegal", state.runId, "effect.blocked_uncertain reason is invalid");
1281
+ }
1282
+ const receipt = event.payload.receipt;
1283
+ if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
1284
+ throw runtimeError("run_events_illegal", state.runId, "effect.blocked_uncertain is missing receipt");
1285
+ }
1286
+ const receiptId = (receipt as { id?: unknown }).id;
1287
+ if (typeof receiptId !== "string" || receiptId.length === 0) {
1288
+ throw runtimeError("run_events_illegal", state.runId, "effect.blocked_uncertain receipt.id is invalid");
1289
+ }
1290
+ state.currentStep = { ...current, effectState: "blocked_uncertain", observationId: receiptId };
1291
+ }
1292
+
1293
+ function requireActiveStep(state: MutableState, eventType: string): void {
1294
+ requireRunning(state, eventType);
1295
+ if (!state.currentStep) {
1296
+ throw runtimeError("run_events_illegal", state.runId, `${eventType} requires an active step`);
1297
+ }
1298
+ }
1299
+
1300
+ function requireStep(plan: VnextCompiledPlan, stepId: string, runId: string): VnextCompiledStep {
1301
+ const step = plan.steps[stepId];
1302
+ if (!step) throw runtimeError("run_events_illegal", runId, `unknown step ${stepId}`);
1303
+ return step;
1304
+ }
1305
+
1306
+ function stringPayload(event: VnextRunEvent, field: string): string {
1307
+ const value = event.payload[field];
1308
+ if (typeof value !== "string" || value.length === 0) {
1309
+ throw runtimeError("run_events_illegal", event.runId, `${event.eventType} is missing ${field}`);
1310
+ }
1311
+ return value;
1312
+ }
1313
+
1314
+ function integerPayload(event: VnextRunEvent, field: string): number {
1315
+ const value = event.payload[field];
1316
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
1317
+ throw runtimeError("run_events_illegal", event.runId, `${event.eventType} is missing a valid ${field}`);
1318
+ }
1319
+ return value;
1320
+ }
1321
+
1322
+ function freezePanel(panel: MutablePanel): VnextFoldPanel {
1323
+ const assignments: Record<string, VnextFoldAssignmentState> = Object.create(null);
1324
+ for (const assignmentId of Object.keys(panel.assignments).sort()) {
1325
+ const assignment = panel.assignments[assignmentId]!;
1326
+ const attempts: Record<string, VnextFoldAttemptState> = Object.create(null);
1327
+ for (const attemptId of Object.keys(assignment.attempts).sort()) {
1328
+ attempts[attemptId] = Object.freeze(omitUndefined({ ...assignment.attempts[attemptId]! }));
1329
+ }
1330
+ assignments[assignmentId] = Object.freeze(omitUndefined({
1331
+ status: assignment.status,
1332
+ ...(assignment.currentAttemptId !== undefined ? { currentAttemptId: assignment.currentAttemptId } : {}),
1333
+ attempts: Object.freeze(attempts),
1334
+ }));
1335
+ }
1336
+ return Object.freeze({
1337
+ order: Object.freeze([...panel.order]),
1338
+ assignments: Object.freeze(assignments),
1339
+ });
1340
+ }
1341
+
1342
+ function freezeCurrentStep(current: MutableCurrentStep): VnextRunCurrentStep {
1343
+ const panel = freezePanel(current.panel);
1344
+ const assignmentId = panel.order.length === 1 ? panel.order[0] : undefined;
1345
+ const assignment = assignmentId ? panel.assignments[assignmentId] : undefined;
1346
+ const attemptId = assignment?.currentAttemptId;
1347
+ const attempt = attemptId ? assignment?.attempts[attemptId] : undefined;
1348
+ return Object.freeze(omitUndefined({
1349
+ stepId: current.stepId,
1350
+ stepAttempt: current.stepAttempt,
1351
+ status: current.status,
1352
+ panel,
1353
+ ...(assignmentId !== undefined ? { assignmentId } : {}),
1354
+ ...(assignment ? { assignmentStatus: assignment.status } : {}),
1355
+ ...(attemptId !== undefined ? { attemptId } : {}),
1356
+ ...(attempt ? { attemptStatus: attempt.status } : {}),
1357
+ ...(current.outcome !== undefined ? { outcome: current.outcome } : {}),
1358
+ ...(current.effectId !== undefined ? { effectId: current.effectId } : {}),
1359
+ ...(current.effectState !== undefined ? { effectState: current.effectState } : {}),
1360
+ ...(current.observationId !== undefined ? { observationId: current.observationId } : {}),
1361
+ ...(current.observationHash !== undefined ? { observationHash: current.observationHash } : {}),
1362
+ ...(current.observationCompleteness !== undefined ? { observationCompleteness: current.observationCompleteness } : {}),
1363
+ ...(current.settledOutcome !== undefined ? { settledOutcome: current.settledOutcome } : {}),
1364
+ ...(current.evidenceRefs !== undefined ? { evidenceRefs: current.evidenceRefs } : {}),
1365
+ }));
1366
+ }
1367
+
1368
+ function freezeState(state: MutableState): VnextRunState {
1369
+ const frozen: VnextRunState = {
1370
+ schema: VNEXT_RUN_STATE_SCHEMA,
1371
+ runId: state.runId,
1372
+ status: state.status,
1373
+ ...(state.runPlanHash !== undefined ? { runPlanHash: state.runPlanHash } : {}),
1374
+ ...(state.pendingStepId !== undefined ? { pendingStepId: state.pendingStepId } : {}),
1375
+ ...(state.currentStep !== undefined ? { currentStep: freezeCurrentStep(state.currentStep) } : {}),
1376
+ stepAttempts: Object.freeze({ ...state.stepAttempts }),
1377
+ edgeTransitions: Object.freeze({ ...state.edgeTransitions }),
1378
+ transitionsUsed: state.transitionsUsed,
1379
+ cancelRequested: state.cancelRequested,
1380
+ ...(state.terminalReason !== undefined ? { terminalReason: state.terminalReason } : {}),
1381
+ ...(state.terminalReason !== undefined && state.status === "failed" ? { failureReason: state.terminalReason } : {}),
1382
+ };
1383
+ return Object.freeze(frozen);
1384
+ }
1385
+
1386
+ function omitUndefined<T extends Record<string, unknown>>(value: T): T {
1387
+ const copy = { ...value };
1388
+ for (const key of Object.keys(copy)) {
1389
+ if (copy[key] === undefined) delete copy[key];
1390
+ }
1391
+ return copy;
1392
+ }
1393
+
1394
+ export function isTerminalRunStatus(status: VnextRunStatus): boolean {
1395
+ return TERMINAL_RUN.has(status);
1396
+ }
1397
+
1398
+ export function isVnextTerminalStatus(value: string): value is VnextTerminalStatus {
1399
+ return value === "completed" || value === "failed" || value === "cancelled";
1400
+ }