contextos-mcp 0.4.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 (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -0
  3. package/bin/mcp.mjs +61 -0
  4. package/bin/swarm.mjs +45 -0
  5. package/dist/agents/aider.d.ts +12 -0
  6. package/dist/agents/aider.js +192 -0
  7. package/dist/agents/aider.js.map +1 -0
  8. package/dist/agents/claude-code.d.ts +9 -0
  9. package/dist/agents/claude-code.js +236 -0
  10. package/dist/agents/claude-code.js.map +1 -0
  11. package/dist/agents/codex.d.ts +14 -0
  12. package/dist/agents/codex.js +203 -0
  13. package/dist/agents/codex.js.map +1 -0
  14. package/dist/agents/direct-llm.d.ts +22 -0
  15. package/dist/agents/direct-llm.js +254 -0
  16. package/dist/agents/direct-llm.js.map +1 -0
  17. package/dist/agents/mock.d.ts +10 -0
  18. package/dist/agents/mock.js +170 -0
  19. package/dist/agents/mock.js.map +1 -0
  20. package/dist/agents/opencode.d.ts +23 -0
  21. package/dist/agents/opencode.js +631 -0
  22. package/dist/agents/opencode.js.map +1 -0
  23. package/dist/agents/provider.d.ts +11 -0
  24. package/dist/agents/provider.js +31 -0
  25. package/dist/agents/provider.js.map +1 -0
  26. package/dist/compression/compressor.d.ts +28 -0
  27. package/dist/compression/compressor.js +268 -0
  28. package/dist/compression/compressor.js.map +1 -0
  29. package/dist/config.d.ts +43 -0
  30. package/dist/config.js +172 -0
  31. package/dist/config.js.map +1 -0
  32. package/dist/contextos/loader.d.ts +46 -0
  33. package/dist/contextos/loader.js +120 -0
  34. package/dist/contextos/loader.js.map +1 -0
  35. package/dist/contextos/selector.d.ts +46 -0
  36. package/dist/contextos/selector.js +80 -0
  37. package/dist/contextos/selector.js.map +1 -0
  38. package/dist/core/action-dispatcher.d.ts +15 -0
  39. package/dist/core/action-dispatcher.js +28 -0
  40. package/dist/core/action-dispatcher.js.map +1 -0
  41. package/dist/core/action-schema.d.ts +72 -0
  42. package/dist/core/action-schema.js +86 -0
  43. package/dist/core/action-schema.js.map +1 -0
  44. package/dist/core/repl-interface.d.ts +48 -0
  45. package/dist/core/repl-interface.js +5 -0
  46. package/dist/core/repl-interface.js.map +1 -0
  47. package/dist/core/repl.d.ts +59 -0
  48. package/dist/core/repl.js +353 -0
  49. package/dist/core/repl.js.map +1 -0
  50. package/dist/core/rlm.d.ts +65 -0
  51. package/dist/core/rlm.js +564 -0
  52. package/dist/core/rlm.js.map +1 -0
  53. package/dist/core/runtime.py +470 -0
  54. package/dist/core/types.d.ts +265 -0
  55. package/dist/core/types.js +19 -0
  56. package/dist/core/types.js.map +1 -0
  57. package/dist/env.d.ts +10 -0
  58. package/dist/env.js +75 -0
  59. package/dist/env.js.map +1 -0
  60. package/dist/hooks/runner.d.ts +43 -0
  61. package/dist/hooks/runner.js +173 -0
  62. package/dist/hooks/runner.js.map +1 -0
  63. package/dist/labs/cli.d.ts +15 -0
  64. package/dist/labs/cli.js +295 -0
  65. package/dist/labs/cli.js.map +1 -0
  66. package/dist/labs/episodic-memory.d.ts +132 -0
  67. package/dist/labs/episodic-memory.js +391 -0
  68. package/dist/labs/episodic-memory.js.map +1 -0
  69. package/dist/labs/interactive-swarm.d.ts +20 -0
  70. package/dist/labs/interactive-swarm.js +1173 -0
  71. package/dist/labs/interactive-swarm.js.map +1 -0
  72. package/dist/labs/interactive.d.ts +10 -0
  73. package/dist/labs/interactive.js +2099 -0
  74. package/dist/labs/interactive.js.map +1 -0
  75. package/dist/labs/ts-analyzer.d.ts +74 -0
  76. package/dist/labs/ts-analyzer.js +379 -0
  77. package/dist/labs/ts-analyzer.js.map +1 -0
  78. package/dist/labs/viewer.d.ts +12 -0
  79. package/dist/labs/viewer.js +1284 -0
  80. package/dist/labs/viewer.js.map +1 -0
  81. package/dist/main.d.ts +15 -0
  82. package/dist/main.js +247 -0
  83. package/dist/main.js.map +1 -0
  84. package/dist/mcp/server.d.ts +16 -0
  85. package/dist/mcp/server.js +192775 -0
  86. package/dist/mcp/server.js.map +1 -0
  87. package/dist/mcp/session.d.ts +117 -0
  88. package/dist/mcp/session.js +450 -0
  89. package/dist/mcp/session.js.map +1 -0
  90. package/dist/mcp/state.d.ts +42 -0
  91. package/dist/mcp/state.js +537 -0
  92. package/dist/mcp/state.js.map +1 -0
  93. package/dist/mcp/tools/contextos.d.ts +17 -0
  94. package/dist/mcp/tools/contextos.js +583 -0
  95. package/dist/mcp/tools/contextos.js.map +1 -0
  96. package/dist/mcp/tools.d.ts +15 -0
  97. package/dist/mcp/tools.js +393 -0
  98. package/dist/mcp/tools.js.map +1 -0
  99. package/dist/orchestration/reviewer-gate.d.ts +43 -0
  100. package/dist/orchestration/reviewer-gate.js +280 -0
  101. package/dist/orchestration/reviewer-gate.js.map +1 -0
  102. package/dist/orchestration/reviewer-prompt.d.ts +18 -0
  103. package/dist/orchestration/reviewer-prompt.js +67 -0
  104. package/dist/orchestration/reviewer-prompt.js.map +1 -0
  105. package/dist/orchestration/verification-runner.d.ts +53 -0
  106. package/dist/orchestration/verification-runner.js +352 -0
  107. package/dist/orchestration/verification-runner.js.map +1 -0
  108. package/dist/plugins/adapter.d.ts +11 -0
  109. package/dist/plugins/adapter.js +11 -0
  110. package/dist/plugins/adapter.js.map +1 -0
  111. package/dist/plugins/core/archive.d.ts +9 -0
  112. package/dist/plugins/core/archive.js +128 -0
  113. package/dist/plugins/core/archive.js.map +1 -0
  114. package/dist/plugins/core/digest.d.ts +11 -0
  115. package/dist/plugins/core/digest.js +70 -0
  116. package/dist/plugins/core/digest.js.map +1 -0
  117. package/dist/plugins/core/grants.d.ts +22 -0
  118. package/dist/plugins/core/grants.js +90 -0
  119. package/dist/plugins/core/grants.js.map +1 -0
  120. package/dist/plugins/core/index.d.ts +10 -0
  121. package/dist/plugins/core/index.js +11 -0
  122. package/dist/plugins/core/index.js.map +1 -0
  123. package/dist/plugins/core/pinning.d.ts +15 -0
  124. package/dist/plugins/core/pinning.js +86 -0
  125. package/dist/plugins/core/pinning.js.map +1 -0
  126. package/dist/plugins/core/types.d.ts +105 -0
  127. package/dist/plugins/core/types.js +22 -0
  128. package/dist/plugins/core/types.js.map +1 -0
  129. package/dist/plugins/core/updater.d.ts +16 -0
  130. package/dist/plugins/core/updater.js +64 -0
  131. package/dist/plugins/core/updater.js.map +1 -0
  132. package/dist/plugins/loader.d.ts +65 -0
  133. package/dist/plugins/loader.js +110 -0
  134. package/dist/plugins/loader.js.map +1 -0
  135. package/dist/prompts/orchestrator.d.ts +6 -0
  136. package/dist/prompts/orchestrator.js +114 -0
  137. package/dist/prompts/orchestrator.js.map +1 -0
  138. package/dist/routing/model-resolver.d.ts +27 -0
  139. package/dist/routing/model-resolver.js +265 -0
  140. package/dist/routing/model-resolver.js.map +1 -0
  141. package/dist/routing/model-router.d.ts +130 -0
  142. package/dist/routing/model-router.js +515 -0
  143. package/dist/routing/model-router.js.map +1 -0
  144. package/dist/runtime/attestations.cjs +260 -0
  145. package/dist/runtime/reviewer-pipeline.cjs +319 -0
  146. package/dist/runtime/sandbox.cjs +468 -0
  147. package/dist/runtime/state-machine.cjs +885 -0
  148. package/dist/runtime/thread-store.cjs +175 -0
  149. package/dist/runtime/transactional-git.cjs +315 -0
  150. package/dist/runtime/verification-pipeline.cjs +435 -0
  151. package/dist/sandbox/execution.d.ts +165 -0
  152. package/dist/sandbox/execution.js +497 -0
  153. package/dist/sandbox/execution.js.map +1 -0
  154. package/dist/security/file-policy.d.ts +22 -0
  155. package/dist/security/file-policy.js +53 -0
  156. package/dist/security/file-policy.js.map +1 -0
  157. package/dist/security/repository-boundary.d.ts +21 -0
  158. package/dist/security/repository-boundary.js +119 -0
  159. package/dist/security/repository-boundary.js.map +1 -0
  160. package/dist/security/secret-filter.d.ts +36 -0
  161. package/dist/security/secret-filter.js +149 -0
  162. package/dist/security/secret-filter.js.map +1 -0
  163. package/dist/swarm.d.ts +14 -0
  164. package/dist/swarm.js +624 -0
  165. package/dist/swarm.js.map +1 -0
  166. package/dist/threads/cache.d.ts +64 -0
  167. package/dist/threads/cache.js +241 -0
  168. package/dist/threads/cache.js.map +1 -0
  169. package/dist/threads/manager.d.ts +94 -0
  170. package/dist/threads/manager.js +914 -0
  171. package/dist/threads/manager.js.map +1 -0
  172. package/dist/ui/banner.d.ts +14 -0
  173. package/dist/ui/banner.js +42 -0
  174. package/dist/ui/banner.js.map +1 -0
  175. package/dist/ui/dashboard.d.ts +33 -0
  176. package/dist/ui/dashboard.js +135 -0
  177. package/dist/ui/dashboard.js.map +1 -0
  178. package/dist/ui/index.d.ts +11 -0
  179. package/dist/ui/index.js +12 -0
  180. package/dist/ui/index.js.map +1 -0
  181. package/dist/ui/log.d.ts +39 -0
  182. package/dist/ui/log.js +126 -0
  183. package/dist/ui/log.js.map +1 -0
  184. package/dist/ui/onboarding.d.ts +14 -0
  185. package/dist/ui/onboarding.js +755 -0
  186. package/dist/ui/onboarding.js.map +1 -0
  187. package/dist/ui/run-log.d.ts +50 -0
  188. package/dist/ui/run-log.js +60 -0
  189. package/dist/ui/run-log.js.map +1 -0
  190. package/dist/ui/spinner.d.ts +41 -0
  191. package/dist/ui/spinner.js +172 -0
  192. package/dist/ui/spinner.js.map +1 -0
  193. package/dist/ui/streaming-feed.d.ts +72 -0
  194. package/dist/ui/streaming-feed.js +386 -0
  195. package/dist/ui/streaming-feed.js.map +1 -0
  196. package/dist/ui/summary.d.ts +18 -0
  197. package/dist/ui/summary.js +113 -0
  198. package/dist/ui/summary.js.map +1 -0
  199. package/dist/ui/text-input.d.ts +19 -0
  200. package/dist/ui/text-input.js +266 -0
  201. package/dist/ui/text-input.js.map +1 -0
  202. package/dist/ui/theme.d.ts +63 -0
  203. package/dist/ui/theme.js +97 -0
  204. package/dist/ui/theme.js.map +1 -0
  205. package/dist/utils/command-exists.d.ts +11 -0
  206. package/dist/utils/command-exists.js +41 -0
  207. package/dist/utils/command-exists.js.map +1 -0
  208. package/dist/utils/process-security.d.ts +19 -0
  209. package/dist/utils/process-security.js +90 -0
  210. package/dist/utils/process-security.js.map +1 -0
  211. package/dist/utils/rolling-buffer.d.ts +16 -0
  212. package/dist/utils/rolling-buffer.js +30 -0
  213. package/dist/utils/rolling-buffer.js.map +1 -0
  214. package/dist/utils/safe-spawn.d.ts +13 -0
  215. package/dist/utils/safe-spawn.js +49 -0
  216. package/dist/utils/safe-spawn.js.map +1 -0
  217. package/dist/worktree/manager.d.ts +69 -0
  218. package/dist/worktree/manager.js +593 -0
  219. package/dist/worktree/manager.js.map +1 -0
  220. package/dist/worktree/merge.d.ts +58 -0
  221. package/dist/worktree/merge.js +389 -0
  222. package/dist/worktree/merge.js.map +1 -0
  223. package/package.json +63 -0
@@ -0,0 +1,885 @@
1
+ /**
2
+ * .agents/runtime/state-machine.cjs
3
+ * ContextOS — Independent Runtime State Machine & Transition Engine
4
+ *
5
+ * Implements Section 15 of CONTEXTOS_IMPLEMENTATION_PLAN.md:
6
+ * - 4 independent orthogonal lifecycle statuses (Execution, Verification, Review, Merge)
7
+ * - Immutable state transitions with optimistic concurrency control (expectedRevision)
8
+ * - Formal transition matrices prohibiting illegal skips (e.g. FAILED -> READY is impossible)
9
+ * - Automatic candidate commit drift invalidation (transitions active attestations to STALE)
10
+ * - Fail-closed Merge Readiness evaluation (requires evidence-bearing verification & review)
11
+ * - Structured append-only audit trail logging
12
+ */
13
+
14
+ "use strict";
15
+
16
+ const fs = require("node:fs");
17
+ const path = require("node:path");
18
+ const { sha256, computeScopeSha256, computeDiffSha256, verifyAttestationSubject } = require("./attestations.cjs");
19
+
20
+ // ── Status Enums (Section 15.1) ──────────────────────────────────────────────
21
+
22
+ const EXECUTION_STATUS = Object.freeze({
23
+ QUEUED: "QUEUED",
24
+ PREPARING: "PREPARING",
25
+ RUNNING: "RUNNING",
26
+ SUCCEEDED: "SUCCEEDED",
27
+ FAILED: "FAILED",
28
+ TIMED_OUT: "TIMED_OUT",
29
+ CANCELLED: "CANCELLED",
30
+ INTERRUPTED: "INTERRUPTED",
31
+ });
32
+
33
+ const VERIFICATION_STATUS = Object.freeze({
34
+ NOT_CONFIGURED: "NOT_CONFIGURED",
35
+ NOT_APPLICABLE: "NOT_APPLICABLE",
36
+ PENDING: "PENDING",
37
+ RUNNING: "RUNNING",
38
+ PASS: "PASS",
39
+ FAIL: "FAIL",
40
+ ERROR: "ERROR",
41
+ TIMEOUT: "TIMEOUT",
42
+ STALE: "STALE",
43
+ });
44
+
45
+ const REVIEW_STATUS = Object.freeze({
46
+ NOT_CONFIGURED: "NOT_CONFIGURED",
47
+ PENDING: "PENDING",
48
+ RUNNING: "RUNNING",
49
+ PASS: "PASS",
50
+ FAIL: "FAIL",
51
+ ERROR: "ERROR",
52
+ TIMEOUT: "TIMEOUT",
53
+ UNAVAILABLE: "UNAVAILABLE",
54
+ MALFORMED: "MALFORMED",
55
+ STALE: "STALE",
56
+ });
57
+
58
+ const MERGE_STATUS = Object.freeze({
59
+ NOT_READY: "NOT_READY",
60
+ READY: "READY",
61
+ MERGING: "MERGING",
62
+ MERGED: "MERGED",
63
+ BLOCKED: "BLOCKED",
64
+ CONFLICT: "CONFLICT",
65
+ STALE_BASE: "STALE_BASE",
66
+ ERROR: "ERROR",
67
+ });
68
+
69
+ // ── Legal Transition Matrices ───────────────────────────────────────────────
70
+
71
+ const LEGAL_EXECUTION_TRANSITIONS = {
72
+ [EXECUTION_STATUS.QUEUED]: [EXECUTION_STATUS.PREPARING, EXECUTION_STATUS.CANCELLED, EXECUTION_STATUS.INTERRUPTED],
73
+ [EXECUTION_STATUS.PREPARING]: [
74
+ EXECUTION_STATUS.RUNNING,
75
+ EXECUTION_STATUS.FAILED,
76
+ EXECUTION_STATUS.CANCELLED,
77
+ EXECUTION_STATUS.INTERRUPTED,
78
+ ],
79
+ [EXECUTION_STATUS.RUNNING]: [
80
+ EXECUTION_STATUS.SUCCEEDED,
81
+ EXECUTION_STATUS.FAILED,
82
+ EXECUTION_STATUS.TIMED_OUT,
83
+ EXECUTION_STATUS.CANCELLED,
84
+ EXECUTION_STATUS.INTERRUPTED,
85
+ ],
86
+ // Terminal states (unless explicit retry re-queues)
87
+ [EXECUTION_STATUS.SUCCEEDED]: [],
88
+ [EXECUTION_STATUS.FAILED]: [EXECUTION_STATUS.QUEUED], // explicit retry
89
+ [EXECUTION_STATUS.TIMED_OUT]: [EXECUTION_STATUS.QUEUED],
90
+ [EXECUTION_STATUS.CANCELLED]: [],
91
+ [EXECUTION_STATUS.INTERRUPTED]: [EXECUTION_STATUS.QUEUED],
92
+ };
93
+
94
+ const LEGAL_VERIFICATION_TRANSITIONS = {
95
+ [VERIFICATION_STATUS.NOT_CONFIGURED]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.NOT_APPLICABLE],
96
+ [VERIFICATION_STATUS.NOT_APPLICABLE]: [VERIFICATION_STATUS.PENDING],
97
+ [VERIFICATION_STATUS.PENDING]: [
98
+ VERIFICATION_STATUS.RUNNING,
99
+ VERIFICATION_STATUS.NOT_APPLICABLE,
100
+ VERIFICATION_STATUS.STALE,
101
+ ],
102
+ [VERIFICATION_STATUS.RUNNING]: [
103
+ VERIFICATION_STATUS.PASS,
104
+ VERIFICATION_STATUS.FAIL,
105
+ VERIFICATION_STATUS.ERROR,
106
+ VERIFICATION_STATUS.TIMEOUT,
107
+ VERIFICATION_STATUS.STALE,
108
+ ],
109
+ [VERIFICATION_STATUS.PASS]: [
110
+ VERIFICATION_STATUS.STALE,
111
+ VERIFICATION_STATUS.PENDING, // re-verification
112
+ ],
113
+ [VERIFICATION_STATUS.FAIL]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.STALE],
114
+ [VERIFICATION_STATUS.ERROR]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.STALE],
115
+ [VERIFICATION_STATUS.TIMEOUT]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.STALE],
116
+ [VERIFICATION_STATUS.STALE]: [VERIFICATION_STATUS.PENDING, VERIFICATION_STATUS.RUNNING],
117
+ };
118
+
119
+ const LEGAL_REVIEW_TRANSITIONS = {
120
+ [REVIEW_STATUS.NOT_CONFIGURED]: [REVIEW_STATUS.PENDING],
121
+ [REVIEW_STATUS.PENDING]: [REVIEW_STATUS.RUNNING, REVIEW_STATUS.STALE],
122
+ [REVIEW_STATUS.RUNNING]: [
123
+ REVIEW_STATUS.PASS,
124
+ REVIEW_STATUS.FAIL,
125
+ REVIEW_STATUS.ERROR,
126
+ REVIEW_STATUS.TIMEOUT,
127
+ REVIEW_STATUS.UNAVAILABLE,
128
+ REVIEW_STATUS.MALFORMED,
129
+ REVIEW_STATUS.STALE,
130
+ ],
131
+ [REVIEW_STATUS.PASS]: [REVIEW_STATUS.STALE, REVIEW_STATUS.PENDING],
132
+ [REVIEW_STATUS.FAIL]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
133
+ [REVIEW_STATUS.ERROR]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
134
+ [REVIEW_STATUS.TIMEOUT]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
135
+ [REVIEW_STATUS.UNAVAILABLE]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
136
+ [REVIEW_STATUS.MALFORMED]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.STALE],
137
+ [REVIEW_STATUS.STALE]: [REVIEW_STATUS.PENDING, REVIEW_STATUS.RUNNING],
138
+ };
139
+
140
+ const LEGAL_MERGE_TRANSITIONS = {
141
+ [MERGE_STATUS.NOT_READY]: [MERGE_STATUS.READY, MERGE_STATUS.BLOCKED, MERGE_STATUS.CONFLICT, MERGE_STATUS.STALE_BASE],
142
+ [MERGE_STATUS.READY]: [MERGE_STATUS.MERGING, MERGE_STATUS.NOT_READY, MERGE_STATUS.BLOCKED, MERGE_STATUS.STALE_BASE],
143
+ [MERGE_STATUS.MERGING]: [MERGE_STATUS.MERGED, MERGE_STATUS.CONFLICT, MERGE_STATUS.ERROR, MERGE_STATUS.BLOCKED],
144
+ [MERGE_STATUS.MERGED]: [],
145
+ [MERGE_STATUS.BLOCKED]: [MERGE_STATUS.NOT_READY, MERGE_STATUS.READY],
146
+ [MERGE_STATUS.CONFLICT]: [MERGE_STATUS.NOT_READY],
147
+ [MERGE_STATUS.STALE_BASE]: [MERGE_STATUS.NOT_READY],
148
+ [MERGE_STATUS.ERROR]: [MERGE_STATUS.NOT_READY],
149
+ };
150
+
151
+ // ── Thread Initializer ───────────────────────────────────────────────────────
152
+
153
+ /**
154
+ * Creates a brand new immutable ThreadState object.
155
+ */
156
+ function createThreadState(options = {}) {
157
+ const now = Date.now();
158
+ const id = options.id || `th-${now}-${Math.random().toString(36).slice(2, 8)}`;
159
+ const config = options.config || { task: options.task || "Autonomous coding task" };
160
+ const baseSha = options.baseSha || "0000000000000000000000000000000000000000";
161
+ const repoFingerprint = options.repositoryFingerprint || sha256(process.cwd());
162
+
163
+ const thread = {
164
+ schemaVersion: 1,
165
+ id,
166
+ revision: 0,
167
+ config: {
168
+ task: config.task,
169
+ objective: config.objective || config.task,
170
+ writeScope: config.writeScope || [],
171
+ testCommand: config.testCommand || "",
172
+ agent: config.agent || "gemini",
173
+ model: config.model || "gemini-2.5-pro",
174
+ targetBase: config.targetBase || "main",
175
+ requiredSandbox: config.requiredSandbox || "none",
176
+ },
177
+ execution: {
178
+ status: EXECUTION_STATUS.QUEUED,
179
+ startedAt: null,
180
+ completedAt: null,
181
+ attempt: 1,
182
+ maxAttempts: config.maxAttempts || 3,
183
+ error: null,
184
+ executionId: null,
185
+ worktreePath: null,
186
+ branchName: null,
187
+ },
188
+ verification: {
189
+ status: config.testCommand ? VERIFICATION_STATUS.PENDING : VERIFICATION_STATUS.NOT_CONFIGURED,
190
+ attestation: null,
191
+ reason: null,
192
+ updatedAt: now,
193
+ },
194
+ review: {
195
+ status: REVIEW_STATUS.NOT_CONFIGURED,
196
+ attestation: null,
197
+ reason: null,
198
+ updatedAt: now,
199
+ },
200
+ merge: {
201
+ status: MERGE_STATUS.NOT_READY,
202
+ readiness: {
203
+ isReady: false,
204
+ checks: [],
205
+ evaluatedAt: now,
206
+ },
207
+ mergedAt: null,
208
+ targetBranch: config.targetBase || "main",
209
+ mergeCommitSha: null,
210
+ error: null,
211
+ },
212
+ subject: {
213
+ repositoryFingerprint: repoFingerprint,
214
+ baseSha,
215
+ headSha: baseSha,
216
+ diffSha256: sha256(""),
217
+ scopeSha256: computeScopeSha256(config.writeScope || []),
218
+ filesChanged: [],
219
+ },
220
+ createdAt: now,
221
+ updatedAt: now,
222
+ };
223
+
224
+ return Object.freeze(thread);
225
+ }
226
+
227
+ // ── Merge Readiness Evaluator (Section 15.5) ────────────────────────────────
228
+
229
+ /**
230
+ * Strictly evaluates whether a thread is eligible for automated merge into target base.
231
+ * Fail-closed: requires affirmative, evidence-bearing proof across all gates.
232
+ *
233
+ * @param {Object} thread - Current ThreadState
234
+ * @param {Object} [options] - Evaluation options (current repo state)
235
+ * @returns {{ isReady: boolean, checks: Array<{ name: string, passed: boolean, message: string, code?: string }> }}
236
+ */
237
+ function evaluateMergeReadiness(thread, options = {}) {
238
+ const checks = [];
239
+ const evaluatedAt = Date.now();
240
+
241
+ if (!thread) {
242
+ return {
243
+ isReady: false,
244
+ checks: [{ name: "thread_exists", passed: false, message: "Thread does not exist", code: "CTX_MERGE_NO_THREAD" }],
245
+ evaluatedAt,
246
+ };
247
+ }
248
+
249
+ // Gate 1: Execution must be SUCCEEDED
250
+ const execPassed = thread.execution?.status === EXECUTION_STATUS.SUCCEEDED;
251
+ checks.push({
252
+ name: "execution_status",
253
+ passed: execPassed,
254
+ message: execPassed
255
+ ? `Execution SUCCEEDED (id: ${thread.execution.executionId || "unknown"})`
256
+ : `Execution is ${thread.execution?.status || "UNKNOWN"}, must be SUCCEEDED`,
257
+ code: execPassed ? "OK" : "CTX_MERGE_EXECUTION_NOT_SUCCEEDED",
258
+ });
259
+
260
+ // Gate 2: Scope check (no out-of-scope files modified)
261
+ const allowedScope = thread.config?.writeScope || [];
262
+ const filesChanged = thread.subject?.filesChanged || [];
263
+ let scopePassed = true;
264
+ let scopeViolationFile = null;
265
+
266
+ if (allowedScope.length > 0 && filesChanged.length > 0) {
267
+ for (const f of filesChanged) {
268
+ const normalized = f.replace(/\\/g, "/");
269
+ const inScope = allowedScope.some((scopePath) => {
270
+ const normScope = scopePath.replace(/\\/g, "/");
271
+ return normalized === normScope || normalized.startsWith(normScope.endsWith("/") ? normScope : `${normScope}/`);
272
+ });
273
+ if (!inScope) {
274
+ scopePassed = false;
275
+ scopeViolationFile = f;
276
+ break;
277
+ }
278
+ }
279
+ }
280
+
281
+ checks.push({
282
+ name: "scope_boundaries",
283
+ passed: scopePassed,
284
+ message: scopePassed
285
+ ? "All modified files strictly within planned write scope"
286
+ : `Blast radius violation: modified file "${scopeViolationFile}" is outside writeScope`,
287
+ code: scopePassed ? "OK" : "CTX_MERGE_SCOPE_VIOLATION",
288
+ });
289
+
290
+ // Gate 3: Evidence-bearing Verification Attestation
291
+ const verAtt = thread.verification?.attestation;
292
+ const verStatus = thread.verification?.status;
293
+ let verPassed = false;
294
+ let verReason = "";
295
+
296
+ if (verStatus !== VERIFICATION_STATUS.PASS) {
297
+ verReason = `Verification status is ${verStatus}, expected PASS`;
298
+ } else if (!verAtt || !verAtt.evidence) {
299
+ verReason = "FAIL_CLOSED: Verification marked PASS but lacks verifiable evidence (LEGACY_UNVERIFIED)";
300
+ } else if (verAtt.status === VERIFICATION_STATUS.STALE) {
301
+ verReason = "Verification attestation is STALE (candidate commit changed)";
302
+ } else if (verAtt.evidence.exitCode !== 0) {
303
+ verReason = `Verification evidence recorded non-zero exit code: ${verAtt.evidence.exitCode}`;
304
+ } else {
305
+ // Subject integrity check
306
+ const subjCheck = verifyAttestationSubject(verAtt, thread.subject);
307
+ if (!subjCheck.valid) {
308
+ verReason = `Verification attestation subject mismatch: ${subjCheck.reason}`;
309
+ } else {
310
+ verPassed = true;
311
+ verReason = `Verification PASS verified with exitCode: 0, outputSha: ${verAtt.evidence.outputSha256.slice(0, 10)}`;
312
+ }
313
+ }
314
+
315
+ checks.push({
316
+ name: "verification_attestation",
317
+ passed: verPassed,
318
+ message: verReason,
319
+ code: verPassed ? "OK" : "CTX_MERGE_VERIFICATION_FAILED",
320
+ });
321
+
322
+ // Gate 4: Review Attestation
323
+ const revAtt = thread.review?.attestation;
324
+ const revStatus = thread.review?.status;
325
+ let revPassed = false;
326
+ let revReason = "";
327
+
328
+ // If review is configured, it must PASS with an attestation
329
+ if (revStatus !== REVIEW_STATUS.PASS) {
330
+ revReason = `Review status is ${revStatus}, expected PASS`;
331
+ } else if (!revAtt) {
332
+ revReason = "Review marked PASS but lacks ReviewAttestation";
333
+ } else if (revAtt.status === REVIEW_STATUS.STALE) {
334
+ revReason = "Review attestation is STALE (candidate commit changed)";
335
+ } else {
336
+ const subjCheck = verifyAttestationSubject(revAtt, thread.subject);
337
+ if (!subjCheck.valid) {
338
+ revReason = `Review attestation subject mismatch: ${subjCheck.reason}`;
339
+ } else if (
340
+ !revAtt.llmVerdict ||
341
+ revAtt.llmVerdict.specCompliance !== "PASS" ||
342
+ revAtt.llmVerdict.codeQuality !== "PASS"
343
+ ) {
344
+ revReason = "Review verdict does not confirm PASS for spec compliance and code quality";
345
+ } else {
346
+ revPassed = true;
347
+ revReason = `Review PASS confirmed by reviewer ${revAtt.reviewerExecutionId} (${revAtt.model})`;
348
+ }
349
+ }
350
+
351
+ checks.push({
352
+ name: "review_attestation",
353
+ passed: revPassed,
354
+ message: revReason,
355
+ code: revPassed ? "OK" : "CTX_MERGE_REVIEW_FAILED",
356
+ });
357
+
358
+ // Gate 5: Sandbox level validation
359
+ const requiredSandbox = thread.config?.requiredSandbox || "none";
360
+ let sandboxPassed = true;
361
+ let sandboxReason = "Sandbox level satisfies execution requirement";
362
+
363
+ if (requiredSandbox === "oci" || requiredSandbox === "strict-sandbox") {
364
+ if (verAtt && verAtt.runnerMode !== "oci") {
365
+ sandboxPassed = false;
366
+ sandboxReason = `Required sandbox is ${requiredSandbox}, but verification ran in ${verAtt.runnerMode}`;
367
+ }
368
+ }
369
+
370
+ checks.push({
371
+ name: "sandbox_conformance",
372
+ passed: sandboxPassed,
373
+ message: sandboxReason,
374
+ code: sandboxPassed ? "OK" : "CTX_MERGE_SANDBOX_VIOLATION",
375
+ });
376
+
377
+ // Gate 6: Candidate Commit & Target Base Consistency
378
+ const currentBaseSha = options.currentBaseSha || thread.subject.baseSha;
379
+ let basePassed = true;
380
+ let baseReason = "Target base SHA matches thread base SHA";
381
+
382
+ if (currentBaseSha && currentBaseSha !== thread.subject.baseSha) {
383
+ basePassed = false;
384
+ baseReason = `Stale base detected: target base advanced to ${currentBaseSha.slice(0, 8)}, thread based on ${thread.subject.baseSha.slice(0, 8)}`;
385
+ }
386
+
387
+ checks.push({
388
+ name: "base_commitment",
389
+ passed: basePassed,
390
+ message: baseReason,
391
+ code: basePassed ? "OK" : "CTX_MERGE_STALE_BASE",
392
+ });
393
+
394
+ // Gate 7: Unresolved Error Boundary
395
+ const noErrors = !thread.execution.error && !thread.merge.error;
396
+ checks.push({
397
+ name: "clean_error_boundary",
398
+ passed: noErrors,
399
+ message: noErrors
400
+ ? "Zero unresolved exceptions or fatal error markers"
401
+ : `Unresolved error: ${thread.execution.error || thread.merge.error}`,
402
+ code: noErrors ? "OK" : "CTX_MERGE_UNRESOLVED_ERROR",
403
+ });
404
+
405
+ const isReady = checks.every((c) => c.passed);
406
+ return {
407
+ isReady,
408
+ checks,
409
+ evaluatedAt,
410
+ };
411
+ }
412
+
413
+ // ── Stale Invalidation Helper ───────────────────────────────────────────────
414
+
415
+ /**
416
+ * Detects whether new candidate commits or diff modifications invalidate
417
+ * active verification and review attestations, marking them STALE.
418
+ *
419
+ * @param {Object} thread
420
+ * @param {Object} newSubject - { headSha, diffSha256, filesChanged }
421
+ * @returns {Object} Updated ThreadState if modified, or original
422
+ */
423
+ function invalidateOnCandidateChange(thread, newSubject = {}) {
424
+ const newHead = newSubject.headSha || thread.subject.headSha;
425
+ const newDiff = newSubject.diffSha256 || thread.subject.diffSha256;
426
+
427
+ const isHeadChanged = newHead !== thread.subject.headSha;
428
+ const isDiffChanged = newDiff !== thread.subject.diffSha256;
429
+
430
+ if (!isHeadChanged && !isDiffChanged) {
431
+ return thread;
432
+ }
433
+
434
+ const now = Date.now();
435
+ const nextVerification = { ...thread.verification };
436
+ if (nextVerification.status === VERIFICATION_STATUS.PASS || nextVerification.status === VERIFICATION_STATUS.RUNNING) {
437
+ nextVerification.status = VERIFICATION_STATUS.STALE;
438
+ nextVerification.reason = "Candidate commit or diff changed; previous verification attestation invalidated";
439
+ nextVerification.updatedAt = now;
440
+ if (nextVerification.attestation) {
441
+ nextVerification.attestation = { ...nextVerification.attestation, status: VERIFICATION_STATUS.STALE };
442
+ }
443
+ }
444
+
445
+ const nextReview = { ...thread.review };
446
+ if (nextReview.status === REVIEW_STATUS.PASS || nextReview.status === REVIEW_STATUS.RUNNING) {
447
+ nextReview.status = REVIEW_STATUS.STALE;
448
+ nextReview.reason = "Candidate commit or diff changed; previous review attestation invalidated";
449
+ nextReview.updatedAt = now;
450
+ if (nextReview.attestation) {
451
+ nextReview.attestation = { ...nextReview.attestation, status: REVIEW_STATUS.STALE };
452
+ }
453
+ }
454
+
455
+ const nextSubject = {
456
+ ...thread.subject,
457
+ headSha: newHead,
458
+ diffSha256: newDiff,
459
+ filesChanged: newSubject.filesChanged || thread.subject.filesChanged,
460
+ };
461
+
462
+ const nextMerge = { ...thread.merge, status: MERGE_STATUS.NOT_READY };
463
+
464
+ const updatedThread = {
465
+ ...thread,
466
+ revision: thread.revision + 1,
467
+ verification: nextVerification,
468
+ review: nextReview,
469
+ merge: nextMerge,
470
+ subject: nextSubject,
471
+ updatedAt: now,
472
+ };
473
+
474
+ return Object.freeze(updatedThread);
475
+ }
476
+
477
+ // ── State Transition Engine (Section 15.4) ──────────────────────────────────
478
+
479
+ /**
480
+ * Transitions a thread state strictly according to the legal transition matrices.
481
+ * Direct mutation (thread.status = ...) is forbidden.
482
+ *
483
+ * @param {Object} thread - Current immutable ThreadState
484
+ * @param {Object} event - Event object: { type, payload, expectedRevision }
485
+ * @param {Object} [options] - Options like auditFilePath
486
+ * @returns {Object} Immutable next ThreadState
487
+ */
488
+ function transitionThread(thread, event = {}, options = {}) {
489
+ if (!thread) {
490
+ throw new Error("transitionThread requires a current thread state");
491
+ }
492
+ if (!event.type) {
493
+ throw new Error("transitionThread requires an event with a type");
494
+ }
495
+
496
+ // 1. Optimistic Concurrency Check
497
+ if (typeof event.expectedRevision === "number" && event.expectedRevision !== thread.revision) {
498
+ const err = new Error(
499
+ `Optimistic concurrency conflict for thread ${thread.id}: expected revision ${event.expectedRevision}, but current revision is ${thread.revision}`,
500
+ );
501
+ err.code = "CTX_THREAD_CONCURRENCY_CONFLICT";
502
+ err.threadId = thread.id;
503
+ err.expectedRevision = event.expectedRevision;
504
+ err.currentRevision = thread.revision;
505
+ throw err;
506
+ }
507
+
508
+ const now = Date.now();
509
+ const nextThread = {
510
+ ...thread,
511
+ config: { ...thread.config },
512
+ execution: { ...thread.execution },
513
+ verification: { ...thread.verification },
514
+ review: { ...thread.review },
515
+ merge: { ...thread.merge },
516
+ subject: { ...thread.subject },
517
+ revision: thread.revision + 1,
518
+ updatedAt: now,
519
+ };
520
+
521
+ const payload = event.payload || {};
522
+
523
+ // 2. Dispatch Event Type
524
+ switch (event.type) {
525
+ // ── Execution Events ──
526
+ case "EXECUTION_PREPARE": {
527
+ _assertLegalTransition(
528
+ "execution",
529
+ thread.execution.status,
530
+ EXECUTION_STATUS.PREPARING,
531
+ LEGAL_EXECUTION_TRANSITIONS,
532
+ );
533
+ nextThread.execution.status = EXECUTION_STATUS.PREPARING;
534
+ nextThread.execution.startedAt = now;
535
+ if (payload.worktreePath) nextThread.execution.worktreePath = payload.worktreePath;
536
+ if (payload.branchName) nextThread.execution.branchName = payload.branchName;
537
+ break;
538
+ }
539
+
540
+ case "EXECUTION_START": {
541
+ _assertLegalTransition(
542
+ "execution",
543
+ thread.execution.status,
544
+ EXECUTION_STATUS.RUNNING,
545
+ LEGAL_EXECUTION_TRANSITIONS,
546
+ );
547
+ nextThread.execution.status = EXECUTION_STATUS.RUNNING;
548
+ if (payload.executionId) nextThread.execution.executionId = payload.executionId;
549
+ break;
550
+ }
551
+
552
+ case "EXECUTION_SUCCEED": {
553
+ _assertLegalTransition(
554
+ "execution",
555
+ thread.execution.status,
556
+ EXECUTION_STATUS.SUCCEEDED,
557
+ LEGAL_EXECUTION_TRANSITIONS,
558
+ );
559
+ nextThread.execution.status = EXECUTION_STATUS.SUCCEEDED;
560
+ nextThread.execution.completedAt = now;
561
+ if (payload.headSha) nextThread.subject.headSha = payload.headSha;
562
+ if (payload.diffSha256) nextThread.subject.diffSha256 = payload.diffSha256;
563
+ if (payload.filesChanged) nextThread.subject.filesChanged = payload.filesChanged;
564
+ break;
565
+ }
566
+
567
+ case "EXECUTION_FAIL": {
568
+ _assertLegalTransition(
569
+ "execution",
570
+ thread.execution.status,
571
+ EXECUTION_STATUS.FAILED,
572
+ LEGAL_EXECUTION_TRANSITIONS,
573
+ );
574
+ nextThread.execution.status = EXECUTION_STATUS.FAILED;
575
+ nextThread.execution.completedAt = now;
576
+ nextThread.execution.error = payload.error || "Execution failed";
577
+ // Failed execution can never remain in READY merge status
578
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
579
+ break;
580
+ }
581
+
582
+ case "EXECUTION_TIMEOUT": {
583
+ _assertLegalTransition(
584
+ "execution",
585
+ thread.execution.status,
586
+ EXECUTION_STATUS.TIMED_OUT,
587
+ LEGAL_EXECUTION_TRANSITIONS,
588
+ );
589
+ nextThread.execution.status = EXECUTION_STATUS.TIMED_OUT;
590
+ nextThread.execution.completedAt = now;
591
+ nextThread.execution.error = payload.error || "Execution timed out";
592
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
593
+ break;
594
+ }
595
+
596
+ case "EXECUTION_CANCEL": {
597
+ _assertLegalTransition(
598
+ "execution",
599
+ thread.execution.status,
600
+ EXECUTION_STATUS.CANCELLED,
601
+ LEGAL_EXECUTION_TRANSITIONS,
602
+ );
603
+ nextThread.execution.status = EXECUTION_STATUS.CANCELLED;
604
+ nextThread.execution.completedAt = now;
605
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
606
+ break;
607
+ }
608
+
609
+ case "EXECUTION_INTERRUPT": {
610
+ _assertLegalTransition(
611
+ "execution",
612
+ thread.execution.status,
613
+ EXECUTION_STATUS.INTERRUPTED,
614
+ LEGAL_EXECUTION_TRANSITIONS,
615
+ );
616
+ nextThread.execution.status = EXECUTION_STATUS.INTERRUPTED;
617
+ nextThread.execution.completedAt = now;
618
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
619
+ break;
620
+ }
621
+
622
+ case "EXECUTION_RETRY": {
623
+ _assertLegalTransition(
624
+ "execution",
625
+ thread.execution.status,
626
+ EXECUTION_STATUS.QUEUED,
627
+ LEGAL_EXECUTION_TRANSITIONS,
628
+ );
629
+ nextThread.execution.status = EXECUTION_STATUS.QUEUED;
630
+ nextThread.execution.attempt += 1;
631
+ nextThread.execution.error = null;
632
+ nextThread.execution.startedAt = null;
633
+ nextThread.execution.completedAt = null;
634
+ break;
635
+ }
636
+
637
+ // ── Verification Events ──
638
+ case "VERIFICATION_CONFIGURED": {
639
+ _assertLegalTransition(
640
+ "verification",
641
+ thread.verification.status,
642
+ VERIFICATION_STATUS.PENDING,
643
+ LEGAL_VERIFICATION_TRANSITIONS,
644
+ );
645
+ nextThread.verification.status = VERIFICATION_STATUS.PENDING;
646
+ nextThread.verification.updatedAt = now;
647
+ break;
648
+ }
649
+
650
+ case "VERIFICATION_START": {
651
+ _assertLegalTransition(
652
+ "verification",
653
+ thread.verification.status,
654
+ VERIFICATION_STATUS.RUNNING,
655
+ LEGAL_VERIFICATION_TRANSITIONS,
656
+ );
657
+ nextThread.verification.status = VERIFICATION_STATUS.RUNNING;
658
+ nextThread.verification.updatedAt = now;
659
+ break;
660
+ }
661
+
662
+ case "VERIFICATION_PASS": {
663
+ _assertLegalTransition(
664
+ "verification",
665
+ thread.verification.status,
666
+ VERIFICATION_STATUS.PASS,
667
+ LEGAL_VERIFICATION_TRANSITIONS,
668
+ );
669
+ if (!payload.attestation) {
670
+ throw new Error("FAIL_CLOSED: VERIFICATION_PASS event requires an evidence-bearing attestation");
671
+ }
672
+ nextThread.verification.status = VERIFICATION_STATUS.PASS;
673
+ nextThread.verification.attestation = payload.attestation;
674
+ nextThread.verification.updatedAt = now;
675
+ break;
676
+ }
677
+
678
+ case "VERIFICATION_FAIL": {
679
+ _assertLegalTransition(
680
+ "verification",
681
+ thread.verification.status,
682
+ VERIFICATION_STATUS.FAIL,
683
+ LEGAL_VERIFICATION_TRANSITIONS,
684
+ );
685
+ nextThread.verification.status = VERIFICATION_STATUS.FAIL;
686
+ nextThread.verification.attestation = payload.attestation || null;
687
+ nextThread.verification.reason = payload.reason || "Tests failed";
688
+ nextThread.verification.updatedAt = now;
689
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
690
+ break;
691
+ }
692
+
693
+ case "VERIFICATION_ERROR": {
694
+ _assertLegalTransition(
695
+ "verification",
696
+ thread.verification.status,
697
+ VERIFICATION_STATUS.ERROR,
698
+ LEGAL_VERIFICATION_TRANSITIONS,
699
+ );
700
+ nextThread.verification.status = VERIFICATION_STATUS.ERROR;
701
+ nextThread.verification.reason = payload.reason || "Verification process error";
702
+ nextThread.verification.updatedAt = now;
703
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
704
+ break;
705
+ }
706
+
707
+ case "VERIFICATION_TIMEOUT": {
708
+ _assertLegalTransition(
709
+ "verification",
710
+ thread.verification.status,
711
+ VERIFICATION_STATUS.TIMEOUT,
712
+ LEGAL_VERIFICATION_TRANSITIONS,
713
+ );
714
+ nextThread.verification.status = VERIFICATION_STATUS.TIMEOUT;
715
+ nextThread.verification.reason = "Verification runner timed out";
716
+ nextThread.verification.updatedAt = now;
717
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
718
+ break;
719
+ }
720
+
721
+ // ── Review Events ──
722
+ case "REVIEW_CONFIGURED": {
723
+ _assertLegalTransition("review", thread.review.status, REVIEW_STATUS.PENDING, LEGAL_REVIEW_TRANSITIONS);
724
+ nextThread.review.status = REVIEW_STATUS.PENDING;
725
+ nextThread.review.updatedAt = now;
726
+ break;
727
+ }
728
+
729
+ case "REVIEW_START": {
730
+ _assertLegalTransition("review", thread.review.status, REVIEW_STATUS.RUNNING, LEGAL_REVIEW_TRANSITIONS);
731
+ nextThread.review.status = REVIEW_STATUS.RUNNING;
732
+ nextThread.review.updatedAt = now;
733
+ break;
734
+ }
735
+
736
+ case "REVIEW_PASS": {
737
+ _assertLegalTransition("review", thread.review.status, REVIEW_STATUS.PASS, LEGAL_REVIEW_TRANSITIONS);
738
+ if (!payload.attestation) {
739
+ throw new Error("FAIL_CLOSED: REVIEW_PASS event requires ReviewAttestation");
740
+ }
741
+ nextThread.review.status = REVIEW_STATUS.PASS;
742
+ nextThread.review.attestation = payload.attestation;
743
+ nextThread.review.updatedAt = now;
744
+ break;
745
+ }
746
+
747
+ case "REVIEW_FAIL": {
748
+ _assertLegalTransition("review", thread.review.status, REVIEW_STATUS.FAIL, LEGAL_REVIEW_TRANSITIONS);
749
+ nextThread.review.status = REVIEW_STATUS.FAIL;
750
+ nextThread.review.attestation = payload.attestation || null;
751
+ nextThread.review.reason = payload.reason || "Review rejected changes";
752
+ nextThread.review.updatedAt = now;
753
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
754
+ break;
755
+ }
756
+
757
+ case "REVIEW_UNAVAILABLE": {
758
+ _assertLegalTransition("review", thread.review.status, REVIEW_STATUS.UNAVAILABLE, LEGAL_REVIEW_TRANSITIONS);
759
+ nextThread.review.status = REVIEW_STATUS.UNAVAILABLE;
760
+ nextThread.review.reason = payload.reason || "Reviewer model/provider unavailable";
761
+ nextThread.review.updatedAt = now;
762
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
763
+ break;
764
+ }
765
+
766
+ case "REVIEW_MALFORMED": {
767
+ _assertLegalTransition("review", thread.review.status, REVIEW_STATUS.MALFORMED, LEGAL_REVIEW_TRANSITIONS);
768
+ nextThread.review.status = REVIEW_STATUS.MALFORMED;
769
+ nextThread.review.reason = payload.reason || "Reviewer output was malformed";
770
+ nextThread.review.updatedAt = now;
771
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
772
+ break;
773
+ }
774
+
775
+ // ── Merge Events ──
776
+ case "EVALUATE_MERGE_READINESS": {
777
+ const readiness = evaluateMergeReadiness(nextThread, payload.options);
778
+ nextThread.merge.readiness = readiness;
779
+ if (readiness.isReady) {
780
+ _assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.READY, LEGAL_MERGE_TRANSITIONS);
781
+ nextThread.merge.status = MERGE_STATUS.READY;
782
+ } else {
783
+ nextThread.merge.status = MERGE_STATUS.NOT_READY;
784
+ }
785
+ break;
786
+ }
787
+
788
+ case "MERGE_START": {
789
+ _assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.MERGING, LEGAL_MERGE_TRANSITIONS);
790
+ nextThread.merge.status = MERGE_STATUS.MERGING;
791
+ break;
792
+ }
793
+
794
+ case "MERGE_COMPLETE": {
795
+ _assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.MERGED, LEGAL_MERGE_TRANSITIONS);
796
+ nextThread.merge.status = MERGE_STATUS.MERGED;
797
+ nextThread.merge.mergedAt = now;
798
+ if (payload.mergeCommitSha) nextThread.merge.mergeCommitSha = payload.mergeCommitSha;
799
+ break;
800
+ }
801
+
802
+ case "MERGE_CONFLICT": {
803
+ _assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.CONFLICT, LEGAL_MERGE_TRANSITIONS);
804
+ nextThread.merge.status = MERGE_STATUS.CONFLICT;
805
+ nextThread.merge.error = payload.error || "Git merge conflict encountered";
806
+ break;
807
+ }
808
+
809
+ case "MERGE_BLOCK": {
810
+ _assertLegalTransition("merge", thread.merge.status, MERGE_STATUS.BLOCKED, LEGAL_MERGE_TRANSITIONS);
811
+ nextThread.merge.status = MERGE_STATUS.BLOCKED;
812
+ nextThread.merge.error = payload.reason || "Merge blocked by policy";
813
+ break;
814
+ }
815
+
816
+ // ── Candidate Commit Drift Event ──
817
+ case "CANDIDATE_COMMIT_UPDATE": {
818
+ return invalidateOnCandidateChange(thread, payload);
819
+ }
820
+
821
+ default:
822
+ throw new Error(`Unknown state transition event type: "${event.type}"`);
823
+ }
824
+
825
+ // 3. Write Immutable Audit Trail Event
826
+ const auditEvent = {
827
+ timestamp: now,
828
+ threadId: thread.id,
829
+ fromRevision: thread.revision,
830
+ toRevision: nextThread.revision,
831
+ eventType: event.type,
832
+ fromStates: {
833
+ execution: thread.execution.status,
834
+ verification: thread.verification.status,
835
+ review: thread.review.status,
836
+ merge: thread.merge.status,
837
+ },
838
+ toStates: {
839
+ execution: nextThread.execution.status,
840
+ verification: nextThread.verification.status,
841
+ review: nextThread.review.status,
842
+ merge: nextThread.merge.status,
843
+ },
844
+ payload,
845
+ };
846
+
847
+ if (options.auditFilePath) {
848
+ try {
849
+ fs.mkdirSync(path.dirname(options.auditFilePath), { recursive: true });
850
+ fs.appendFileSync(options.auditFilePath, `${JSON.stringify(auditEvent)}\n`);
851
+ } catch {
852
+ // ignore
853
+ }
854
+ }
855
+
856
+ return Object.freeze(nextThread);
857
+ }
858
+
859
+ /**
860
+ * Validates that fromStatus -> toStatus is legal in transition table.
861
+ */
862
+ function _assertLegalTransition(facet, fromStatus, toStatus, legalTable) {
863
+ if (fromStatus === toStatus) return; // idempotent
864
+
865
+ const allowed = legalTable[fromStatus] || [];
866
+ if (!allowed.includes(toStatus)) {
867
+ const err = new Error(`Illegal ${facet} state transition: cannot transition from ${fromStatus} to ${toStatus}`);
868
+ err.code = "CTX_ILLEGAL_STATE_TRANSITION";
869
+ err.facet = facet;
870
+ err.fromStatus = fromStatus;
871
+ err.toStatus = toStatus;
872
+ throw err;
873
+ }
874
+ }
875
+
876
+ module.exports = {
877
+ EXECUTION_STATUS,
878
+ VERIFICATION_STATUS,
879
+ REVIEW_STATUS,
880
+ MERGE_STATUS,
881
+ createThreadState,
882
+ transitionThread,
883
+ evaluateMergeReadiness,
884
+ invalidateOnCandidateChange,
885
+ };