codex-workflow-v2 2.0.0-beta.13.8 → 2.0.0-beta.14

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 (281) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +96 -32
  3. package/dist/src/alpha6/adoption.d.ts +2 -0
  4. package/dist/src/alpha6/adoption.js +22 -0
  5. package/dist/src/alpha6/adoption.js.map +1 -1
  6. package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
  7. package/dist/src/alpha6/captured-check-evidence.js +152 -0
  8. package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
  9. package/dist/src/alpha6/component-owner.d.ts +15 -1
  10. package/dist/src/alpha6/component-owner.js +44 -2
  11. package/dist/src/alpha6/component-owner.js.map +1 -1
  12. package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
  13. package/dist/src/alpha6/corrective-decision-boundary.js +47 -0
  14. package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
  15. package/dist/src/alpha6/downstream-proof.d.ts +1 -25
  16. package/dist/src/alpha6/downstream-proof.js +0 -171
  17. package/dist/src/alpha6/downstream-proof.js.map +1 -1
  18. package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
  19. package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
  20. package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
  21. package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
  22. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  23. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  24. package/dist/src/alpha6/milestone.d.ts +2 -0
  25. package/dist/src/alpha6/milestone.js +10 -1
  26. package/dist/src/alpha6/milestone.js.map +1 -1
  27. package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
  28. package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
  29. package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
  30. package/dist/src/alpha6/plan-integrity.js +7 -5
  31. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  32. package/dist/src/alpha6/plan-risk.d.ts +2 -0
  33. package/dist/src/alpha6/plan-risk.js +10 -3
  34. package/dist/src/alpha6/plan-risk.js.map +1 -1
  35. package/dist/src/alpha6/remediation.d.ts +6 -21
  36. package/dist/src/alpha6/remediation.js +161 -476
  37. package/dist/src/alpha6/remediation.js.map +1 -1
  38. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
  39. package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
  40. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  41. package/dist/src/beta1/project-transaction.d.ts +3 -1
  42. package/dist/src/beta1/project-transaction.js +6 -2
  43. package/dist/src/beta1/project-transaction.js.map +1 -1
  44. package/dist/src/checks/runner.d.ts +26 -0
  45. package/dist/src/checks/runner.js +223 -0
  46. package/dist/src/checks/runner.js.map +1 -0
  47. package/dist/src/checks/task-sync.d.ts +10 -0
  48. package/dist/src/checks/task-sync.js +41 -0
  49. package/dist/src/checks/task-sync.js.map +1 -0
  50. package/dist/src/checks/worker.d.ts +1 -0
  51. package/dist/src/checks/worker.js +154 -0
  52. package/dist/src/checks/worker.js.map +1 -0
  53. package/dist/src/cli-actions.d.ts +2 -2
  54. package/dist/src/cli-actions.js +3 -15
  55. package/dist/src/cli-actions.js.map +1 -1
  56. package/dist/src/cli.js +14 -98
  57. package/dist/src/cli.js.map +1 -1
  58. package/dist/src/contracts.d.ts +35 -32
  59. package/dist/src/dependency-provenance.d.ts +2 -2
  60. package/dist/src/dependency-provenance.js +12 -69
  61. package/dist/src/dependency-provenance.js.map +1 -1
  62. package/dist/src/domain/base-sync-conflict.d.ts +13 -0
  63. package/dist/src/domain/base-sync-conflict.js +32 -0
  64. package/dist/src/domain/base-sync-conflict.js.map +1 -0
  65. package/dist/src/domain/step-start-admission.d.ts +4 -0
  66. package/dist/src/domain/step-start-admission.js +9 -0
  67. package/dist/src/domain/step-start-admission.js.map +1 -0
  68. package/dist/src/domain/virgin-registration.d.ts +3 -0
  69. package/dist/src/domain/virgin-registration.js +65 -0
  70. package/dist/src/domain/virgin-registration.js.map +1 -0
  71. package/dist/src/errors.d.ts +1 -1
  72. package/dist/src/errors.js.map +1 -1
  73. package/dist/src/gateway-handshake.js +0 -8
  74. package/dist/src/gateway-handshake.js.map +1 -1
  75. package/dist/src/git.d.ts +3 -1
  76. package/dist/src/git.js +41 -14
  77. package/dist/src/git.js.map +1 -1
  78. package/dist/src/graph.js +25 -3
  79. package/dist/src/graph.js.map +1 -1
  80. package/dist/src/index.d.ts +1 -0
  81. package/dist/src/lifecycle/corrective-replan.js +3 -3
  82. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  83. package/dist/src/navigation-actions.d.ts +9 -0
  84. package/dist/src/navigation-actions.js +72 -0
  85. package/dist/src/navigation-actions.js.map +1 -0
  86. package/dist/src/navigation-update-artifact.d.ts +5 -0
  87. package/dist/src/navigation-update-artifact.js +227 -0
  88. package/dist/src/navigation-update-artifact.js.map +1 -0
  89. package/dist/src/navigation-update.d.ts +39 -0
  90. package/dist/src/navigation-update.js +82 -0
  91. package/dist/src/navigation-update.js.map +1 -0
  92. package/dist/src/observation.js +52 -29
  93. package/dist/src/observation.js.map +1 -1
  94. package/dist/src/observed-routes.js +5 -6
  95. package/dist/src/observed-routes.js.map +1 -1
  96. package/dist/src/pending-review-update.d.ts +0 -13
  97. package/dist/src/pending-review-update.js +1 -6
  98. package/dist/src/pending-review-update.js.map +1 -1
  99. package/dist/src/reviewer.d.ts +1 -1
  100. package/dist/src/reviewer.js +20 -17
  101. package/dist/src/reviewer.js.map +1 -1
  102. package/dist/src/runtime.d.ts +3 -0
  103. package/dist/src/runtime.js +6 -0
  104. package/dist/src/runtime.js.map +1 -0
  105. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  106. package/dist/src/state/corrective-replan-executor.js +37 -1
  107. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  108. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  109. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  110. package/dist/src/state/corrective-replan-public.js +6 -5
  111. package/dist/src/state/corrective-replan-public.js.map +1 -1
  112. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  113. package/dist/src/state/corrective-replan-transaction.js +12 -11
  114. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  115. package/dist/src/state/corrective-yield-executor.js +2 -2
  116. package/dist/src/state/corrective-yield-executor.js.map +1 -1
  117. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  118. package/dist/src/state/corrective-yield-transaction.js +11 -10
  119. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  120. package/dist/src/state/lock.d.ts +4 -0
  121. package/dist/src/state/lock.js +21 -0
  122. package/dist/src/state/lock.js.map +1 -1
  123. package/dist/src/version.d.ts +1 -1
  124. package/dist/src/version.js +1 -1
  125. package/dist/src/version.js.map +1 -1
  126. package/dist/src/workflow-blocker-route.d.ts +8 -0
  127. package/dist/src/workflow-blocker-route.js +106 -0
  128. package/dist/src/workflow-blocker-route.js.map +1 -0
  129. package/dist/src/workflow.d.ts +48 -63
  130. package/dist/src/workflow.js +698 -1470
  131. package/dist/src/workflow.js.map +1 -1
  132. package/docs/autonomy-guardrails.md +26 -304
  133. package/docs/decisions.md +14 -112
  134. package/docs/development-flow.md +35 -238
  135. package/docs/project-memory.md +31 -50
  136. package/docs/release-app-evidence.md +190 -0
  137. package/docs/release.md +95 -363
  138. package/docs/updating-existing-project.md +19 -717
  139. package/package.json +11 -13
  140. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  141. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
  142. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  143. package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
  144. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
  145. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
  146. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  147. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -740
  148. package/references/state-machine.md +4 -4
  149. package/roles/technical-planner.md +1 -1
  150. package/schemas/corrective-decision-event.schema.json +3 -1
  151. package/schemas/project-knowledge-map.schema.json +3 -1
  152. package/schemas/remediation-event.schema.json +10 -1
  153. package/schemas/task.schema.json +46 -2
  154. package/schemas/transition-payloads.schema.json +16 -1
  155. package/src/alpha6/adoption.ts +1123 -0
  156. package/src/alpha6/captured-check-evidence.ts +132 -0
  157. package/src/alpha6/check-support-anchor.ts +128 -0
  158. package/src/alpha6/component-owner.ts +168 -0
  159. package/src/alpha6/corrective-decision-boundary.ts +39 -0
  160. package/src/alpha6/downstream-proof.ts +520 -0
  161. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  162. package/src/alpha6/handoff.ts +1443 -0
  163. package/src/alpha6/journal.ts +473 -0
  164. package/src/alpha6/literal-test-invocation.ts +84 -0
  165. package/src/alpha6/mechanical-feasibility.ts +488 -0
  166. package/src/alpha6/milestone.ts +2192 -0
  167. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  168. package/src/alpha6/npm-check-contract.ts +47 -0
  169. package/src/alpha6/plan-integrity.ts +298 -0
  170. package/src/alpha6/plan-risk.ts +1480 -0
  171. package/src/alpha6/preexecution-replan.ts +187 -0
  172. package/src/alpha6/remediation-cause.ts +98 -0
  173. package/src/alpha6/remediation.ts +2438 -0
  174. package/src/alpha6/review.ts +1198 -0
  175. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  176. package/src/alpha6/store-sidecars.ts +335 -0
  177. package/src/alpha7/autonomy.ts +411 -0
  178. package/src/alpha7/corrective-recovery.ts +1332 -0
  179. package/src/artifacts.ts +130 -0
  180. package/src/beta1/project-transaction.ts +355 -0
  181. package/src/change-explanation.ts +153 -0
  182. package/src/checks/runner.ts +245 -0
  183. package/src/checks/task-sync.ts +41 -0
  184. package/src/checks/worker.ts +149 -0
  185. package/src/cli-actions.ts +107 -0
  186. package/src/cli.ts +1457 -0
  187. package/src/contracts.ts +1494 -0
  188. package/src/credential-output.ts +89 -0
  189. package/src/credential-transport.ts +215 -0
  190. package/src/delegation.ts +190 -0
  191. package/src/dependency-provenance.ts +472 -0
  192. package/src/diagnostics.ts +93 -0
  193. package/src/domain/base-sync-conflict.ts +35 -0
  194. package/src/domain/completed-step-carryover.ts +76 -0
  195. package/src/domain/discovery.ts +27 -0
  196. package/src/domain/plan-semantics.ts +58 -0
  197. package/src/domain/step-start-admission.ts +10 -0
  198. package/src/domain/validation.ts +177 -0
  199. package/src/domain/virgin-registration.ts +46 -0
  200. package/src/errors.ts +24 -0
  201. package/src/fs-utils.ts +61 -0
  202. package/src/gateway-handshake.ts +95 -0
  203. package/src/git.ts +183 -0
  204. package/src/graph.ts +342 -0
  205. package/src/historical-step-provenance.ts +136 -0
  206. package/src/index.ts +23 -0
  207. package/src/lifecycle/canonical-hash.ts +28 -0
  208. package/src/lifecycle/catalog.ts +202 -0
  209. package/src/lifecycle/compiler-inspection.ts +29 -0
  210. package/src/lifecycle/core-static-readiness.ts +132 -0
  211. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  212. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  213. package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
  214. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  215. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  216. package/src/lifecycle/corrective-replan.ts +843 -0
  217. package/src/lifecycle/evaluator.ts +48 -0
  218. package/src/lifecycle/fingerprint.ts +488 -0
  219. package/src/lifecycle/immutable.ts +8 -0
  220. package/src/lifecycle/implementation-table.ts +118 -0
  221. package/src/lifecycle/index.ts +8 -0
  222. package/src/lifecycle/schema-artifact.ts +263 -0
  223. package/src/lifecycle/semantic-registry.ts +572 -0
  224. package/src/lifecycle/types.ts +838 -0
  225. package/src/memory.ts +273 -0
  226. package/src/migration.ts +161 -0
  227. package/src/navigation-actions.ts +70 -0
  228. package/src/navigation-update-artifact.ts +198 -0
  229. package/src/navigation-update.ts +121 -0
  230. package/src/observation.ts +225 -0
  231. package/src/observed-routes.ts +660 -0
  232. package/src/operational-contract.ts +125 -0
  233. package/src/pending-review-update.ts +175 -0
  234. package/src/repository.ts +99 -0
  235. package/src/reviewer.ts +1879 -0
  236. package/src/runtime.ts +6 -0
  237. package/src/state/corrective-replan-executor.ts +818 -0
  238. package/src/state/corrective-replan-public-schema.ts +83 -0
  239. package/src/state/corrective-replan-public.ts +908 -0
  240. package/src/state/corrective-replan-transaction.ts +949 -0
  241. package/src/state/corrective-yield-executor.ts +327 -0
  242. package/src/state/corrective-yield-transaction.ts +730 -0
  243. package/src/state/lock.ts +902 -0
  244. package/src/state/store.ts +567 -0
  245. package/src/transition-core.ts +330 -0
  246. package/src/ulid.ts +24 -0
  247. package/src/version.ts +2 -0
  248. package/src/workflow-blocker-route.ts +109 -0
  249. package/src/workflow.ts +10172 -0
  250. package/docs/alpha7.1-implementation-brief.md +0 -268
  251. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  252. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  253. package/docs/beta1-stabilization-brief.md +0 -165
  254. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  255. package/docs/beta13.2-signal-review-recovery.md +0 -38
  256. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  257. package/docs/change-model.md +0 -118
  258. package/docs/delegated-approval.md +0 -254
  259. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  260. package/docs/pdf/README.md +0 -24
  261. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  262. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  263. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  264. package/docs/pdf/requirements.txt +0 -1
  265. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  266. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -508
  267. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  268. package/docs/pending-review-update.md +0 -15
  269. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  270. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  271. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  272. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  273. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  274. package/docs/problem-briefs/README.md +0 -120
  275. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  276. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  277. package/docs/split-required-recovery.md +0 -47
  278. package/docs/stable-release-defect-register.md +0 -730
  279. package/docs/validation-report.md +0 -182
  280. package/scripts/generate-pdf-docs.py +0 -524
  281. package/scripts/run-pdf-docs.mjs +0 -62
@@ -0,0 +1,1879 @@
1
+ import { SUPPORTED_NODE_VERSION, isSupportedNodeVersion } from './runtime.js';
2
+ import { verifiedPendingReviewTransport } from './pending-review-update.js';
3
+ import { createHash } from 'node:crypto';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
6
+ import { createRequire } from 'node:module';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import {
11
+ PROTOCOL_VERSION,
12
+ STATE_SCHEMA_VERSION,
13
+ type ContextEnvelope,
14
+ type ReviewInput,
15
+ type StepEvidence,
16
+ type TaskState,
17
+ } from './contracts.js';
18
+ import { canonicalJsonStringify } from './alpha6/store-sidecars.js';
19
+ import { changedFiles, headCommit, runGit } from './repository.js';
20
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
21
+
22
+ export type ReviewMode = 'ordinary' | 'security';
23
+
24
+ export interface ReviewerRuntimePreflight {
25
+ protocol: 'codex-workflow-reviewer-runtime-preflight-v1';
26
+ prepared: boolean;
27
+ node: { executable: string; version: string; requiredVersion: string; supported: boolean };
28
+ workflowPackage: {
29
+ name: string;
30
+ version: string;
31
+ runtimeModulePath: string;
32
+ runtimeModuleSha256: string;
33
+ installedPackageRoot: string;
34
+ installedPackageManifestPath: string;
35
+ installedPackageManifestSha256: string | null;
36
+ declaredVersion: string | null;
37
+ lockedVersion: string | null;
38
+ installedVersion: string | null;
39
+ exactRepositoryLocal: boolean;
40
+ };
41
+ buildReadiness: {
42
+ state: 'ready' | 'missing' | 'stale';
43
+ manifestPath: string;
44
+ manifestSha256: string | null;
45
+ manifestFingerprint: string | null;
46
+ verifiedFileCount: number;
47
+ expectedFileCount: number;
48
+ cliEntrypointPath: string;
49
+ cliEntrypointSha256: string | null;
50
+ runningCliMatchesInstalledBuild: boolean;
51
+ reviewerModulePath: string;
52
+ reviewerModuleSha256: string | null;
53
+ runningModuleMatchesInstalledBuild: boolean;
54
+ gatewayModulePath: string;
55
+ gatewayModuleSha256: string | null;
56
+ runningGatewayMatchesInstalledBuild: boolean;
57
+ workflowModulePath: string;
58
+ workflowModuleSha256: string | null;
59
+ runningWorkflowMatchesInstalledBuild: boolean;
60
+ evidenceSha256: string;
61
+ };
62
+ runtimeDependencyReadiness: {
63
+ state: 'ready' | 'missing' | 'stale' | 'not-run';
64
+ expectedPackageCount: number;
65
+ verifiedPackageCount: number;
66
+ expectedFileCount: number;
67
+ verifiedFileCount: number;
68
+ packages: Array<{
69
+ name: string;
70
+ version: string;
71
+ packageRoot: string;
72
+ packageManifestPath: string;
73
+ packageManifestSha256: string | null;
74
+ expectedFileCount: number;
75
+ verifiedFileCount: number;
76
+ state: 'ready' | 'missing' | 'stale';
77
+ }>;
78
+ evidenceSha256: string;
79
+ };
80
+ runtimeCheck: {
81
+ kind: 'repository-local-gateway-load';
82
+ state: 'passed' | 'failed' | 'not-run';
83
+ loader: 'node:createRequire';
84
+ executable: string;
85
+ gatewayEntrypointPath: string;
86
+ gatewayEntrypointSha256: string | null;
87
+ reviewerEntrypointPath: string;
88
+ reviewerEntrypointSha256: string | null;
89
+ workflowEntrypointPath: string;
90
+ workflowEntrypointSha256: string | null;
91
+ dependencyClosureEvidenceSha256: string;
92
+ observedPackageName: string | null;
93
+ observedPackageVersion: string | null;
94
+ observedProtocolVersion: number | null;
95
+ observedStateSchemaVersion: number | null;
96
+ resultSha256: string | null;
97
+ failureKind: 'module-load-failed' | 'contract-mismatch' | null;
98
+ evidenceSha256: string;
99
+ sealedCheckoutMutationAllowed: false;
100
+ };
101
+ repositoryDependencyReadiness: {
102
+ packageManifestPath: string | null;
103
+ packageManifestSha256: string | null;
104
+ packageLockPath: string | null;
105
+ packageLockSha256: string | null;
106
+ nodeModulesPath: string;
107
+ nodeModulesPresent: boolean;
108
+ state:
109
+ | 'ready'
110
+ | 'not-installed'
111
+ | 'not-applicable'
112
+ | 'version-mismatch'
113
+ | 'build-missing'
114
+ | 'build-stale'
115
+ | 'runtime-dependency-missing'
116
+ | 'runtime-dependency-stale'
117
+ | 'runtime-check-failed';
118
+ installAttempted: false;
119
+ installCommand: null;
120
+ };
121
+ ephemeralCache: {
122
+ path: string;
123
+ writable: boolean;
124
+ outsideSealedCheckout: boolean;
125
+ lifecycle: 'os-temporary';
126
+ };
127
+ sealedCheckoutMutationAllowed: false;
128
+ blockers: string[];
129
+ }
130
+
131
+ export interface ReviewerProcessResult {
132
+ status: number | null;
133
+ stdout: string;
134
+ stderr: string;
135
+ timedOut?: boolean;
136
+ }
137
+
138
+ export type ReviewerCommandRunner = (
139
+ command: string,
140
+ args: string[],
141
+ options: { cwd: string; input: string; timeoutMs: number },
142
+ ) => ReviewerProcessResult;
143
+
144
+ export interface StrictReviewOutput extends ReviewInput {
145
+ isolationProbe: 'denied' | 'written' | 'not-attempted';
146
+ }
147
+
148
+ export interface StrictReviewerLaunchResult {
149
+ review: ReviewInput;
150
+ verifiedIsolation: boolean;
151
+ command: string[];
152
+ processStatus: number | null;
153
+ timedOut: boolean;
154
+ stderr: string;
155
+ before: RepositorySeal;
156
+ after: RepositorySeal;
157
+ }
158
+
159
+ export interface StrictStepReviewerLaunchResult extends StrictReviewerLaunchResult {
160
+ reviewedCommit: string;
161
+ reviewerOutputHash: string;
162
+ }
163
+
164
+ interface ParsedStrictReviewResult {
165
+ output: StrictReviewOutput;
166
+ outputHash: string;
167
+ }
168
+
169
+ export interface RepositorySeal {
170
+ headCommit: string;
171
+ changedFiles: string[];
172
+ contentHash: string;
173
+ }
174
+
175
+ export interface ExternalStrictStepReviewPacket {
176
+ protocol: 'codex-workflow-external-strict-step-review-v1';
177
+ taskId: string;
178
+ stepId: string;
179
+ reviewedCommit: string;
180
+ packet: Record<string, unknown>;
181
+ packetHash: string;
182
+ repositorySeal: RepositorySeal;
183
+ repositorySealHash: string;
184
+ reviewMode: ReviewMode;
185
+ reviewerRuntime: ReviewerRuntimePreflight;
186
+ }
187
+
188
+ export interface ExternalStrictStepReviewInput {
189
+ protocol: 'codex-workflow-external-strict-step-review-v1';
190
+ packetHash: string;
191
+ repositorySealHash: string;
192
+ reviewerThreadId: string;
193
+ reviewMode: ReviewMode;
194
+ review: ReviewInput;
195
+ }
196
+
197
+ export interface ExternalStrictTaskReviewPacket {
198
+ protocol: 'codex-workflow-external-strict-task-review-v1';
199
+ taskId: string;
200
+ reviewedCommit: string;
201
+ packet: Record<string, unknown>;
202
+ packetHash: string;
203
+ repositorySeal: RepositorySeal;
204
+ repositorySealHash: string;
205
+ reviewMode: ReviewMode;
206
+ reviewerRuntime: ReviewerRuntimePreflight;
207
+ }
208
+
209
+ export interface ExternalStrictTaskReviewInput {
210
+ protocol: 'codex-workflow-external-strict-task-review-v1';
211
+ packetHash: string;
212
+ repositorySealHash: string;
213
+ reviewerThreadId: string;
214
+ reviewMode: ReviewMode;
215
+ review: ReviewInput;
216
+ }
217
+
218
+ export interface ReviewScopeAugmentation {
219
+ stepId: string;
220
+ paths: string[];
221
+ authority: 'machine-derived-check-support';
222
+ }
223
+
224
+ const FULL_GIT_SHA_PATTERN = /^[a-f0-9]{40}$/;
225
+ export const STRICT_REVIEWER_TIMEOUT_MS = 180_000;
226
+
227
+ export const STRICT_REVIEW_OUTPUT_SCHEMA = {
228
+ type: 'object',
229
+ additionalProperties: false,
230
+ required: ['status', 'reviewer', 'summary', 'findings', 'isolationProbe'],
231
+ properties: {
232
+ status: { enum: ['passed', 'failed', 'unverified'] },
233
+ reviewer: { type: 'string', minLength: 1 },
234
+ summary: { type: 'string', minLength: 1 },
235
+ isolationProbe: { enum: ['denied', 'written', 'not-attempted'] },
236
+ findings: {
237
+ type: 'array',
238
+ items: {
239
+ type: 'object',
240
+ additionalProperties: false,
241
+ required: [
242
+ 'id',
243
+ 'severity',
244
+ 'requirement',
245
+ 'summary',
246
+ 'evidence',
247
+ 'requiredAction',
248
+ 'causeId',
249
+ 'route',
250
+ 'planConflict',
251
+ ],
252
+ properties: {
253
+ id: { type: 'string', minLength: 1 },
254
+ severity: { enum: ['critical', 'major', 'minor'] },
255
+ requirement: { type: ['string', 'null'] },
256
+ summary: { type: 'string', minLength: 1 },
257
+ evidence: { type: 'string', minLength: 1 },
258
+ requiredAction: { type: 'string', minLength: 1 },
259
+ causeId: {
260
+ anyOf: [
261
+ { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$' },
262
+ { type: 'null' },
263
+ ],
264
+ },
265
+ route: { enum: ['fix', 'replan'] },
266
+ planConflict: {
267
+ anyOf: [
268
+ { type: 'null' },
269
+ {
270
+ type: 'object',
271
+ additionalProperties: false,
272
+ required: ['planClause', 'whyFixCannotFit', 'minimalPlanChange'],
273
+ properties: {
274
+ planClause: { type: 'string', minLength: 1 },
275
+ whyFixCannotFit: { type: 'string', minLength: 1 },
276
+ minimalPlanChange: { type: 'string', minLength: 1 },
277
+ },
278
+ },
279
+ ],
280
+ },
281
+ },
282
+ },
283
+ },
284
+ },
285
+ } as const;
286
+
287
+ /** @deprecated Use STRICT_REVIEW_OUTPUT_SCHEMA. Retained for direct-module compatibility. */
288
+ export const REVIEW_SCHEMA = STRICT_REVIEW_OUTPUT_SCHEMA;
289
+
290
+ export function launchStrictReviewer(
291
+ repositoryRoot: string,
292
+ task: TaskState,
293
+ taskRoot: string,
294
+ envelope: ContextEnvelope,
295
+ runner: ReviewerCommandRunner = spawnReviewer,
296
+ scopeAugmentations: readonly ReviewScopeAugmentation[] = [],
297
+ ): StrictReviewerLaunchResult {
298
+ const launch = runStrictReviewerLaunch(
299
+ repositoryRoot,
300
+ {
301
+ protocol: 'codex-workflow-strict-review-v1',
302
+ task,
303
+ taskRoot,
304
+ envelope,
305
+ commandSummary: 'Strict reviewer did not return valid output proving the denied write probe.',
306
+ packet: {
307
+ protocol: 'codex-workflow-strict-review-v1',
308
+ instruction: [
309
+ 'Act only as the independent reviewer described by the context envelope.',
310
+ 'Before reviewing, attempt to create .codex-reviewer-write-probe in the repository.',
311
+ 'The attempt must be denied by the sandbox. Report isolationProbe="denied" only when it was denied.',
312
+ 'For each finding use route="fix" by default. Use route="replan" only when the current Plan prevents the required correction, and include the exact planConflict proof.',
313
+ 'Give each finding a stable causeId for the underlying defect; never encode the review attempt number.',
314
+ 'Do not modify any repository file. Return only the requested JSON result.',
315
+ ],
316
+ envelope,
317
+ task: {
318
+ id: task.id,
319
+ baseCommit: task.baseCommit,
320
+ headCommit: headCommit(repositoryRoot),
321
+ brief: readFileSync(path.join(taskRoot, 'brief.md'), 'utf8'),
322
+ plan: readFileSync(path.join(taskRoot, 'plan.md'), 'utf8'),
323
+ evidence: readFileSync(path.join(taskRoot, 'evidence.json'), 'utf8'),
324
+ diff: task.baseCommit ? runGit(repositoryRoot, ['diff', '--no-ext-diff', `${task.baseCommit}..HEAD`]) : '',
325
+ scopeAugmentations,
326
+ },
327
+ },
328
+ },
329
+ runner,
330
+ );
331
+ return {
332
+ review: launch.review,
333
+ verifiedIsolation: launch.verifiedIsolation,
334
+ command: launch.command,
335
+ processStatus: launch.processStatus,
336
+ timedOut: launch.timedOut,
337
+ stderr: launch.stderr,
338
+ before: launch.before,
339
+ after: launch.after,
340
+ };
341
+ }
342
+
343
+ export function launchStrictStepReviewer(
344
+ repositoryRoot: string,
345
+ task: TaskState,
346
+ taskRoot: string,
347
+ envelope: ContextEnvelope,
348
+ stepId: string,
349
+ currentEvidence: StepEvidence,
350
+ runner: ReviewerCommandRunner = spawnReviewer,
351
+ scopeAugmentations: readonly ReviewScopeAugmentation[] = [],
352
+ ): StrictStepReviewerLaunchResult {
353
+ const step = task.steps.find((candidate) => candidate.id === stepId);
354
+ if (!step) {
355
+ return failedStepLaunch(
356
+ repositoryRoot,
357
+ [],
358
+ null,
359
+ `Step ${stepId} was not found on task ${task.id}.`,
360
+ currentEvidence.commitSha,
361
+ );
362
+ }
363
+ if (!step.evidence) {
364
+ return failedStepLaunch(
365
+ repositoryRoot,
366
+ [],
367
+ null,
368
+ `Step ${stepId} has no canonical evidence to review on task ${task.id}.`,
369
+ currentEvidence.commitSha,
370
+ );
371
+ }
372
+ if (canonicalJsonStringify(step.evidence) !== canonicalJsonStringify(currentEvidence)) {
373
+ return failedStepLaunch(
374
+ repositoryRoot,
375
+ [],
376
+ null,
377
+ `Strict step reviewer requires the exact canonical Step evidence for ${stepId} on task ${task.id}.`,
378
+ currentEvidence.commitSha,
379
+ );
380
+ }
381
+ const reviewedCommit = currentEvidence.commitSha;
382
+ if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit)) {
383
+ return failedStepLaunch(
384
+ repositoryRoot,
385
+ [],
386
+ null,
387
+ `Strict step reviewer requires a full lowercase Git SHA for ${stepId} on task ${task.id}.`,
388
+ reviewedCommit,
389
+ );
390
+ }
391
+ const currentHead = headCommit(repositoryRoot);
392
+ if (reviewedCommit !== currentHead) {
393
+ return failedStepLaunch(
394
+ repositoryRoot,
395
+ [],
396
+ null,
397
+ `Strict step reviewer requires the canonical Step evidence commit to match repository HEAD for ${stepId} on task ${task.id}.`,
398
+ reviewedCommit,
399
+ );
400
+ }
401
+ const packet = strictStepReviewPayload(
402
+ repositoryRoot,
403
+ task,
404
+ taskRoot,
405
+ envelope,
406
+ step,
407
+ reviewedCommit,
408
+ currentEvidence,
409
+ scopeAugmentations,
410
+ );
411
+ const launch = runStrictReviewerLaunch(
412
+ repositoryRoot,
413
+ {
414
+ protocol: 'codex-workflow-strict-step-review-v1',
415
+ task,
416
+ taskRoot,
417
+ envelope,
418
+ commandSummary: 'Strict step reviewer did not return valid output proving the denied write probe.',
419
+ packet,
420
+ },
421
+ runner,
422
+ );
423
+ return {
424
+ review: launch.review,
425
+ verifiedIsolation: launch.verifiedIsolation,
426
+ command: launch.command,
427
+ processStatus: launch.processStatus,
428
+ timedOut: launch.timedOut,
429
+ stderr: launch.stderr,
430
+ before: launch.before,
431
+ after: launch.after,
432
+ reviewedCommit,
433
+ reviewerOutputHash: launch.reviewerOutputHash,
434
+ };
435
+ }
436
+
437
+ export function prepareExternalStrictStepReviewPacket(
438
+ repositoryRoot: string,
439
+ task: TaskState,
440
+ taskRoot: string,
441
+ envelope: ContextEnvelope,
442
+ stepId: string,
443
+ currentEvidence: StepEvidence,
444
+ scopeAugmentations: readonly ReviewScopeAugmentation[] = [],
445
+ reviewMode: ReviewMode = 'ordinary',
446
+ ): ExternalStrictStepReviewPacket {
447
+ assertReviewMode(reviewMode);
448
+ const step = task.steps.find((candidate) => candidate.id === stepId);
449
+ if (!step || !step.evidence || canonicalJsonStringify(step.evidence) !== canonicalJsonStringify(currentEvidence)) {
450
+ throw new Error(`Strict Step Review packet requires exact canonical evidence for ${stepId}.`);
451
+ }
452
+ const reviewedCommit = currentEvidence.commitSha;
453
+ const pendingReviewUpdate = verifiedPendingReviewTransport(repositoryRoot, task, stepId, taskRoot);
454
+ if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit) || (reviewedCommit !== headCommit(repositoryRoot) && !pendingReviewUpdate)) {
455
+ throw new Error(`Strict Step Review packet requires ${stepId} evidence to match repository HEAD.`);
456
+ }
457
+ const reviewerRuntime = prepareReviewerRuntimePreflight(
458
+ repositoryRoot,
459
+ `${task.id}:${stepId}:${reviewedCommit}:${reviewMode}`,
460
+ );
461
+ assertReviewerRuntimePrepared(reviewerRuntime);
462
+ const basePacket = strictStepReviewPayload(
463
+ repositoryRoot,
464
+ task,
465
+ taskRoot,
466
+ envelope,
467
+ step,
468
+ reviewedCommit,
469
+ currentEvidence,
470
+ scopeAugmentations,
471
+ );
472
+ const packet = {
473
+ ...basePacket,
474
+ ...(pendingReviewUpdate ? { pendingReviewUpdate } : {}),
475
+ instruction: [...(basePacket.instruction as string[]), reviewInstruction(reviewMode)],
476
+ reviewMode,
477
+ reviewPolicy: reviewPolicy(reviewMode),
478
+ reviewerRuntime,
479
+ };
480
+ const repositorySeal = sealRepository(repositoryRoot);
481
+ return {
482
+ protocol: 'codex-workflow-external-strict-step-review-v1',
483
+ taskId: task.id,
484
+ stepId,
485
+ reviewedCommit,
486
+ packet,
487
+ packetHash: canonicalHash(packet),
488
+ repositorySeal,
489
+ repositorySealHash: canonicalHash(repositorySeal),
490
+ reviewMode,
491
+ reviewerRuntime,
492
+ };
493
+ }
494
+
495
+ export function prepareExternalStrictTaskReviewPacket(
496
+ repositoryRoot: string,
497
+ task: TaskState,
498
+ taskRoot: string,
499
+ envelope: ContextEnvelope,
500
+ scopeAugmentations: readonly ReviewScopeAugmentation[] = [],
501
+ reviewMode: ReviewMode = 'ordinary',
502
+ ): ExternalStrictTaskReviewPacket {
503
+ assertReviewMode(reviewMode);
504
+ const reviewedCommit = headCommit(repositoryRoot);
505
+ if (!FULL_GIT_SHA_PATTERN.test(reviewedCommit)) {
506
+ throw new Error(`Strict Task Review packet requires a full lowercase Git SHA for ${task.id}.`);
507
+ }
508
+ const reviewerRuntime = prepareReviewerRuntimePreflight(
509
+ repositoryRoot,
510
+ `${task.id}:task:${reviewedCommit}:${reviewMode}`,
511
+ );
512
+ assertReviewerRuntimePrepared(reviewerRuntime);
513
+ const packet = {
514
+ protocol: 'codex-workflow-strict-review-v1',
515
+ instruction: [
516
+ 'Act only as the independent reviewer described by the context envelope.',
517
+ 'Review the supplied Task result and bind every finding to reviewedCommit.',
518
+ 'For each finding use route="fix" by default. Use route="replan" only with an exact Plan clause, obstruction, and minimal Plan change.',
519
+ 'Do not modify any repository or Workflow state. Return one ReviewInput JSON object.',
520
+ reviewInstruction(reviewMode),
521
+ ],
522
+ reviewMode,
523
+ reviewPolicy: reviewPolicy(reviewMode),
524
+ reviewerRuntime,
525
+ envelope,
526
+ task: { ...strictTaskReviewSubject(repositoryRoot, task, taskRoot), scopeAugmentations },
527
+ };
528
+ const repositorySeal = sealRepository(repositoryRoot);
529
+ return {
530
+ protocol: 'codex-workflow-external-strict-task-review-v1',
531
+ taskId: task.id,
532
+ reviewedCommit,
533
+ packet,
534
+ packetHash: canonicalHash(packet),
535
+ repositorySeal,
536
+ repositorySealHash: canonicalHash(repositorySeal),
537
+ reviewMode,
538
+ reviewerRuntime,
539
+ };
540
+ }
541
+
542
+ export function validateExternalStrictStepReviewInput(
543
+ value: unknown,
544
+ expected: ExternalStrictStepReviewPacket,
545
+ ): ReviewInput {
546
+ return validateExternalReviewEnvelope(value, expected);
547
+ }
548
+
549
+ export function validateExternalStrictTaskReviewInput(
550
+ value: unknown,
551
+ expected: ExternalStrictTaskReviewPacket,
552
+ ): ReviewInput {
553
+ return validateExternalReviewEnvelope(value, expected);
554
+ }
555
+
556
+ export function strictReviewInputHash(review: ReviewInput): string {
557
+ return hashReviewInput(review);
558
+ }
559
+
560
+ function strictStepReviewPayload(
561
+ repositoryRoot: string,
562
+ task: TaskState,
563
+ taskRoot: string,
564
+ envelope: ContextEnvelope,
565
+ step: TaskState['steps'][number],
566
+ reviewedCommit: string,
567
+ currentEvidence: StepEvidence,
568
+ scopeAugmentations: readonly ReviewScopeAugmentation[],
569
+ ): Record<string, unknown> {
570
+ return {
571
+ protocol: 'codex-workflow-strict-step-review-v1',
572
+ instruction: [
573
+ 'Act only as the independent reviewer described by the context envelope.',
574
+ 'Review only the supplied Task Step and bind findings to reviewedCommit.',
575
+ 'For each finding set route="fix" unless the required correction cannot fit the current Plan. Use route="replan" only with an exact planConflict naming the Plan clause, why a fix cannot fit, and the minimal Plan change.',
576
+ 'When a finding has a stable semantic root cause, provide a causeId that remains unchanged across review attempts; never derive it from attempt number, prose wording, timestamps, or temporary paths.',
577
+ 'Do not modify any repository or Workflow state. Return one ReviewInput JSON object.',
578
+ ],
579
+ envelope,
580
+ task: {
581
+ id: task.id,
582
+ baseCommit: task.baseCommit,
583
+ headCommit: headCommit(repositoryRoot),
584
+ brief: readFileSync(path.join(taskRoot, 'brief.md'), 'utf8'),
585
+ plan: readFileSync(path.join(taskRoot, 'plan.md'), 'utf8'),
586
+ diff: task.baseCommit ? runGit(repositoryRoot, ['diff', '--no-ext-diff', `${task.baseCommit}..HEAD`]) : '',
587
+ },
588
+ step: {
589
+ id: step.id,
590
+ title: step.title,
591
+ objective: step.objective,
592
+ requirements: step.requirements,
593
+ dependencies: step.dependencies,
594
+ expectedOutputs: step.expectedOutputs,
595
+ allowedWrites: step.allowedWrites,
596
+ forbiddenScope: step.forbiddenScope,
597
+ checks: step.checks,
598
+ status: step.status,
599
+ reviewedCommit,
600
+ evidence: currentEvidence,
601
+ scopeAugmentations,
602
+ },
603
+ };
604
+ }
605
+
606
+ function strictTaskReviewSubject(
607
+ repositoryRoot: string,
608
+ task: TaskState,
609
+ taskRoot: string,
610
+ ): Record<string, unknown> {
611
+ return {
612
+ id: task.id,
613
+ baseCommit: task.baseCommit,
614
+ headCommit: headCommit(repositoryRoot),
615
+ brief: readFileSync(path.join(taskRoot, 'brief.md'), 'utf8'),
616
+ plan: readFileSync(path.join(taskRoot, 'plan.md'), 'utf8'),
617
+ evidence: readFileSync(path.join(taskRoot, 'evidence.json'), 'utf8'),
618
+ diff: task.baseCommit ? runGit(repositoryRoot, ['diff', '--no-ext-diff', `${task.baseCommit}..HEAD`]) : '',
619
+ };
620
+ }
621
+
622
+ function validateExternalReviewEnvelope(
623
+ value: unknown,
624
+ expected: ExternalStrictStepReviewPacket | ExternalStrictTaskReviewPacket,
625
+ ): ReviewInput {
626
+ if (!isExactRecord(value, ['protocol', 'packetHash', 'repositorySealHash', 'reviewerThreadId', 'reviewMode', 'review'])) {
627
+ throw new Error('invalid external review envelope');
628
+ }
629
+ if (value.protocol !== expected.protocol) throw new Error('invalid external review protocol');
630
+ if (value.packetHash !== expected.packetHash) throw new Error('external review packet hash mismatch');
631
+ if (value.repositorySealHash !== expected.repositorySealHash) throw new Error('external review repository seal mismatch');
632
+ if (value.reviewMode !== expected.reviewMode) throw new Error('external review mode mismatch');
633
+ if (typeof value.reviewerThreadId !== 'string' || !value.reviewerThreadId.trim()) {
634
+ throw new Error('external reviewer thread id is required');
635
+ }
636
+ if (!isExactRecord(value.review, ['status', 'reviewer', 'summary', 'findings'])) {
637
+ throw new Error('invalid external strict review');
638
+ }
639
+ const review = value.review;
640
+ if (!['passed', 'failed'].includes(String(review.status))
641
+ || typeof review.reviewer !== 'string' || !review.reviewer.trim()
642
+ || typeof review.summary !== 'string' || !review.summary.trim()
643
+ || !Array.isArray(review.findings)) {
644
+ throw new Error('invalid external strict review');
645
+ }
646
+ const findings = review.findings.map((candidate) => {
647
+ if (!isReviewFindingRecord(candidate)
648
+ || typeof candidate.id !== 'string' || !candidate.id.trim()
649
+ || !['critical', 'major', 'minor'].includes(String(candidate.severity))
650
+ || (candidate.requirement !== null && typeof candidate.requirement !== 'string')
651
+ || typeof candidate.summary !== 'string' || !candidate.summary.trim()
652
+ || typeof candidate.evidence !== 'string' || !candidate.evidence.trim()
653
+ || typeof candidate.requiredAction !== 'string' || !candidate.requiredAction.trim()) {
654
+ throw new Error('invalid external strict review finding');
655
+ }
656
+ const route = (candidate.route === undefined ? 'fix' : candidate.route) as 'fix' | 'replan';
657
+ if (route !== 'fix' && route !== 'replan') throw new Error('invalid external strict review finding route');
658
+ const causeId = candidate.causeId;
659
+ if (causeId !== undefined
660
+ && (typeof causeId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(causeId))) {
661
+ throw new Error('invalid external strict review finding causeId');
662
+ }
663
+ const planConflict = validatePlanConflict(candidate.planConflict, route);
664
+ return {
665
+ id: candidate.id,
666
+ severity: candidate.severity as 'critical' | 'major' | 'minor',
667
+ requirement: candidate.requirement as string | null,
668
+ summary: candidate.summary,
669
+ evidence: candidate.evidence,
670
+ requiredAction: candidate.requiredAction,
671
+ ...(causeId === undefined ? {} : { causeId }),
672
+ route,
673
+ planConflict,
674
+ };
675
+ });
676
+ if ((review.status === 'passed' && findings.length > 0) || (review.status === 'failed' && findings.length === 0)) {
677
+ throw new Error('invalid external strict review finding cardinality');
678
+ }
679
+ return {
680
+ status: review.status as 'passed' | 'failed',
681
+ reviewer: review.reviewer,
682
+ summary: review.summary,
683
+ findings,
684
+ };
685
+ }
686
+
687
+ export function prepareReviewerRuntimePreflight(
688
+ repositoryRoot: string,
689
+ binding: string,
690
+ ): ReviewerRuntimePreflight {
691
+ const root = path.resolve(repositoryRoot);
692
+ const manifest = path.join(root, 'package.json');
693
+ const lockfile = path.join(root, 'package-lock.json');
694
+ const nodeModulesPath = path.join(root, 'node_modules');
695
+ const runtimeModulePath = fileURLToPath(import.meta.url);
696
+ const installedPackageRoot = path.join(nodeModulesPath, PACKAGE_NAME);
697
+ const installedPackageManifestPath = path.join(installedPackageRoot, 'package.json');
698
+ const runtimeBuildManifestPath = path.join(installedPackageRoot, 'dist', 'reviewer-runtime-build.json');
699
+ const installedReviewerModulePath = path.join(installedPackageRoot, 'dist', 'src', 'reviewer.js');
700
+ const installedCliPath = path.join(installedPackageRoot, 'dist', 'src', 'cli.js');
701
+ const installedGatewayModulePath = path.join(installedPackageRoot, 'dist', 'src', 'gateway-handshake.js');
702
+ const installedWorkflowModulePath = path.join(installedPackageRoot, 'dist', 'src', 'workflow.js');
703
+ const cachePath = path.join(
704
+ os.tmpdir(),
705
+ 'codex-workflow-review-runtime',
706
+ canonicalHash({ repositoryRoot: root, binding }),
707
+ );
708
+ const nodeSupported = isSupportedNodeVersion(process.versions.node);
709
+ let cacheWritable = false;
710
+ if (nodeSupported) {
711
+ mkdirSync(cachePath, { recursive: true, mode: 0o700 });
712
+ chmodSync(cachePath, 0o700);
713
+ const cacheProbe = path.join(cachePath, '.write-probe');
714
+ try {
715
+ writeFileSync(cacheProbe, 'ok\n', { mode: 0o600 });
716
+ rmSync(cacheProbe, { force: true });
717
+ cacheWritable = true;
718
+ } catch {
719
+ rmSync(cacheProbe, { force: true });
720
+ }
721
+ }
722
+ const outsideSealedCheckout = isOutside(root, cachePath);
723
+ const packageManifestPath = existsSync(manifest) ? manifest : null;
724
+ const packageLockPath = existsSync(lockfile) ? lockfile : null;
725
+ const nodeModulesPresent = existsSync(nodeModulesPath) && statSync(nodeModulesPath).isDirectory();
726
+ const repositoryManifest = readJsonRecord(packageManifestPath);
727
+ const repositoryLock = readJsonRecord(packageLockPath);
728
+ const installedPackageDirectory = isPlainDirectory(installedPackageRoot);
729
+ const installedManifest = readJsonRecord(
730
+ installedPackageDirectory && isPlainFileInside(installedPackageRoot, installedPackageManifestPath)
731
+ ? installedPackageManifestPath
732
+ : null,
733
+ );
734
+ const declaredVersion = nestedString(repositoryManifest, ['devDependencies', PACKAGE_NAME]);
735
+ const lockedVersion = nestedString(repositoryLock, ['packages', `node_modules/${PACKAGE_NAME}`, 'version']);
736
+ const lockDeclaredVersion = nestedString(repositoryLock, ['packages', '', 'devDependencies', PACKAGE_NAME]);
737
+ const installedVersion = stringField(installedManifest, 'version');
738
+ const installedName = stringField(installedManifest, 'name');
739
+ const exactRepositoryLocal = declaredVersion === PACKAGE_VERSION
740
+ && lockedVersion === PACKAGE_VERSION
741
+ && lockDeclaredVersion === PACKAGE_VERSION
742
+ && installedName === PACKAGE_NAME
743
+ && installedVersion === PACKAGE_VERSION;
744
+ const runtimeModuleSha256 = sha256File(runtimeModulePath);
745
+ const runtimeCliModulePath = fileURLToPath(new URL('./cli.js', import.meta.url));
746
+ const runtimeGatewayModulePath = fileURLToPath(new URL('./gateway-handshake.js', import.meta.url));
747
+ const runtimeWorkflowModulePath = fileURLToPath(new URL('./workflow.js', import.meta.url));
748
+ const buildReadiness = inspectReviewerBuild(
749
+ installedPackageRoot,
750
+ runtimeBuildManifestPath,
751
+ installedPackageManifestPath,
752
+ installedCliPath,
753
+ sha256File(runtimeCliModulePath),
754
+ installedReviewerModulePath,
755
+ runtimeModuleSha256,
756
+ installedGatewayModulePath,
757
+ sha256File(runtimeGatewayModulePath),
758
+ installedWorkflowModulePath,
759
+ sha256File(runtimeWorkflowModulePath),
760
+ );
761
+ const runtimeBuildManifest = buildReadiness.state === 'ready'
762
+ ? parseRuntimeBuildManifest(readJsonRecord(runtimeBuildManifestPath))
763
+ : null;
764
+ const runtimeDependencyReadiness = inspectRuntimeDependencyClosure(
765
+ nodeModulesPath,
766
+ installedPackageRoot,
767
+ runtimeBuildManifest,
768
+ exactRepositoryLocal && buildReadiness.state === 'ready',
769
+ );
770
+ const runtimeCheck = inspectReviewerRuntimeCheck(
771
+ installedPackageManifestPath,
772
+ installedGatewayModulePath,
773
+ installedReviewerModulePath,
774
+ installedWorkflowModulePath,
775
+ runtimeDependencyReadiness.evidenceSha256,
776
+ nodeSupported && exactRepositoryLocal
777
+ && buildReadiness.state === 'ready'
778
+ && runtimeDependencyReadiness.state === 'ready',
779
+ );
780
+ const dependencyState = packageManifestPath === null
781
+ ? 'not-applicable'
782
+ : !nodeModulesPresent || installedManifest === null
783
+ ? 'not-installed'
784
+ : !exactRepositoryLocal
785
+ ? 'version-mismatch'
786
+ : buildReadiness.state === 'missing'
787
+ ? 'build-missing'
788
+ : buildReadiness.state === 'stale'
789
+ ? 'build-stale'
790
+ : runtimeDependencyReadiness.state === 'missing'
791
+ ? 'runtime-dependency-missing'
792
+ : runtimeDependencyReadiness.state === 'stale'
793
+ ? 'runtime-dependency-stale'
794
+ : runtimeCheck.state !== 'passed'
795
+ ? 'runtime-check-failed'
796
+ : 'ready';
797
+ const blockers = [
798
+ ...(nodeSupported ? [] : [`Node ${process.version} is unsupported; required exact version ${SUPPORTED_NODE_VERSION}.`]),
799
+ ...(packageManifestPath ? [] : ['Repository package.json is required to prove the exact Workflow dependency.']),
800
+ ...(declaredVersion === PACKAGE_VERSION
801
+ ? []
802
+ : [`Repository devDependencies must declare exact ${PACKAGE_NAME}@${PACKAGE_VERSION}; found ${declaredVersion ?? 'missing'}.`]),
803
+ ...(packageLockPath ? [] : ['Repository package-lock.json is required to prove the locked Workflow dependency.']),
804
+ ...(lockedVersion === PACKAGE_VERSION && lockDeclaredVersion === PACKAGE_VERSION
805
+ ? []
806
+ : [`Repository lockfile must bind both root and installed ${PACKAGE_NAME} entries to ${PACKAGE_VERSION}; found root ${lockDeclaredVersion ?? 'missing'}, package ${lockedVersion ?? 'missing'}.`]),
807
+ ...(installedManifest ? [] : [`Install exact ${PACKAGE_NAME}@${PACKAGE_VERSION} in repository-local node_modules before dispatching review.`]),
808
+ ...(installedManifest === null || (installedName === PACKAGE_NAME && installedVersion === PACKAGE_VERSION)
809
+ ? []
810
+ : [`Repository-local installed Workflow package must be ${PACKAGE_NAME}@${PACKAGE_VERSION}; found ${installedName ?? 'missing'}@${installedVersion ?? 'missing'}.`]),
811
+ ...(installedManifest === null || installedName !== PACKAGE_NAME || installedVersion !== PACKAGE_VERSION
812
+ ? []
813
+ : buildReadiness.state === 'ready'
814
+ ? []
815
+ : buildReadiness.state === 'missing'
816
+ ? [`Build evidence is missing or incomplete at ${runtimeBuildManifestPath}; reinstall the exact published package or build it before sealing review.`]
817
+ : [`Build evidence at ${runtimeBuildManifestPath} does not match installed runtime bytes; reinstall the exact package before sealing review.`]),
818
+ ...(exactRepositoryLocal && buildReadiness.state === 'ready' && runtimeDependencyReadiness.state !== 'ready'
819
+ ? runtimeDependencyReadiness.state === 'missing'
820
+ ? ['The attested repository-local runtime dependency closure is incomplete; reinstall the exact package dependencies before sealing review.']
821
+ : ['The attested repository-local runtime dependency closure has version, path, symlink, or content drift; reinstall the exact package dependencies before sealing review.']
822
+ : []),
823
+ ...(nodeSupported && exactRepositoryLocal
824
+ && buildReadiness.state === 'ready'
825
+ && runtimeDependencyReadiness.state === 'ready'
826
+ && runtimeCheck.state !== 'passed'
827
+ ? runtimeCheck.failureKind === 'contract-mismatch'
828
+ ? [`Repository-local gateway handshake did not report exact package/protocol/schema authority from ${installedGatewayModulePath}; reinstall the exact package before sealing review.`]
829
+ : [`Repository-local reviewer runtime could not load its exact gateway/reviewer/workflow graph from ${installedPackageRoot}; verify package dependencies and reinstall the exact package before sealing review.`]
830
+ : []),
831
+ ...(nodeSupported && !cacheWritable ? ['Ephemeral reviewer cache is not writable.'] : []),
832
+ ...(outsideSealedCheckout ? [] : ['Ephemeral reviewer cache is inside the sealed checkout.']),
833
+ ];
834
+ return {
835
+ protocol: 'codex-workflow-reviewer-runtime-preflight-v1',
836
+ prepared: blockers.length === 0,
837
+ node: {
838
+ executable: process.execPath,
839
+ version: process.version,
840
+ requiredVersion: SUPPORTED_NODE_VERSION,
841
+ supported: nodeSupported,
842
+ },
843
+ workflowPackage: {
844
+ name: PACKAGE_NAME,
845
+ version: PACKAGE_VERSION,
846
+ runtimeModulePath,
847
+ runtimeModuleSha256,
848
+ installedPackageRoot,
849
+ installedPackageManifestPath,
850
+ installedPackageManifestSha256: installedManifest ? sha256File(installedPackageManifestPath) : null,
851
+ declaredVersion,
852
+ lockedVersion,
853
+ installedVersion,
854
+ exactRepositoryLocal,
855
+ },
856
+ buildReadiness,
857
+ runtimeDependencyReadiness,
858
+ runtimeCheck,
859
+ repositoryDependencyReadiness: {
860
+ packageManifestPath,
861
+ packageManifestSha256: packageManifestPath ? sha256File(packageManifestPath) : null,
862
+ packageLockPath,
863
+ packageLockSha256: packageLockPath ? sha256File(packageLockPath) : null,
864
+ nodeModulesPath,
865
+ nodeModulesPresent,
866
+ state: dependencyState,
867
+ installAttempted: false,
868
+ installCommand: null,
869
+ },
870
+ ephemeralCache: {
871
+ path: cachePath,
872
+ writable: cacheWritable,
873
+ outsideSealedCheckout,
874
+ lifecycle: 'os-temporary',
875
+ },
876
+ sealedCheckoutMutationAllowed: false,
877
+ blockers,
878
+ };
879
+ }
880
+
881
+ function inspectReviewerRuntimeCheck(
882
+ installedPackageManifestPath: string,
883
+ gatewayEntrypointPath: string,
884
+ reviewerEntrypointPath: string,
885
+ workflowEntrypointPath: string,
886
+ dependencyClosureEvidenceSha256: string,
887
+ eligible: boolean,
888
+ ): ReviewerRuntimePreflight['runtimeCheck'] {
889
+ const gatewayEntrypointSha256 = eligible ? sha256File(gatewayEntrypointPath) : null;
890
+ const reviewerEntrypointSha256 = eligible ? sha256File(reviewerEntrypointPath) : null;
891
+ const workflowEntrypointSha256 = eligible ? sha256File(workflowEntrypointPath) : null;
892
+ if (!eligible) {
893
+ return runtimeCheckEvidence({
894
+ state: 'not-run',
895
+ gatewayEntrypointPath,
896
+ gatewayEntrypointSha256,
897
+ reviewerEntrypointPath,
898
+ reviewerEntrypointSha256,
899
+ workflowEntrypointPath,
900
+ workflowEntrypointSha256,
901
+ dependencyClosureEvidenceSha256,
902
+ observedPackageName: null,
903
+ observedPackageVersion: null,
904
+ observedProtocolVersion: null,
905
+ observedStateSchemaVersion: null,
906
+ resultSha256: null,
907
+ failureKind: null,
908
+ });
909
+ }
910
+ let observedPackageName: string | null = null;
911
+ let observedPackageVersion: string | null = null;
912
+ let observedProtocolVersion: number | null = null;
913
+ let observedStateSchemaVersion: number | null = null;
914
+ let resultSha256: string | null = null;
915
+ try {
916
+ const requireInstalled = createRequire(installedPackageManifestPath);
917
+ const gatewayModule = runtimeModuleRecord(requireInstalled(gatewayEntrypointPath));
918
+ const reviewerModule = runtimeModuleRecord(requireInstalled(reviewerEntrypointPath));
919
+ const workflowModule = runtimeModuleRecord(requireInstalled(workflowEntrypointPath));
920
+ const handshake = gatewayModule && typeof gatewayModule.gatewayHandshake === 'function'
921
+ ? runtimeModuleRecord(gatewayModule.gatewayHandshake())
922
+ : null;
923
+ if (handshake) {
924
+ observedPackageName = typeof handshake.packageName === 'string' ? handshake.packageName : null;
925
+ observedPackageVersion = typeof handshake.packageVersion === 'string' ? handshake.packageVersion : null;
926
+ observedProtocolVersion = typeof handshake.protocolVersion === 'number' ? handshake.protocolVersion : null;
927
+ observedStateSchemaVersion = typeof handshake.stateSchemaVersion === 'number' ? handshake.stateSchemaVersion : null;
928
+ resultSha256 = canonicalHash(handshake);
929
+ }
930
+ const passed = handshake !== null
931
+ && reviewerModule !== null
932
+ && typeof reviewerModule.prepareReviewerRuntimePreflight === 'function'
933
+ && workflowModule !== null
934
+ && typeof workflowModule.WorkflowService === 'function'
935
+ && observedPackageName === PACKAGE_NAME
936
+ && observedPackageVersion === PACKAGE_VERSION
937
+ && observedProtocolVersion === PROTOCOL_VERSION
938
+ && observedStateSchemaVersion === STATE_SCHEMA_VERSION;
939
+ return runtimeCheckEvidence({
940
+ state: passed ? 'passed' : 'failed',
941
+ gatewayEntrypointPath,
942
+ gatewayEntrypointSha256,
943
+ reviewerEntrypointPath,
944
+ reviewerEntrypointSha256,
945
+ workflowEntrypointPath,
946
+ workflowEntrypointSha256,
947
+ dependencyClosureEvidenceSha256,
948
+ observedPackageName,
949
+ observedPackageVersion,
950
+ observedProtocolVersion,
951
+ observedStateSchemaVersion,
952
+ resultSha256,
953
+ failureKind: passed ? null : 'contract-mismatch',
954
+ });
955
+ } catch {
956
+ return runtimeCheckEvidence({
957
+ state: 'failed',
958
+ gatewayEntrypointPath,
959
+ gatewayEntrypointSha256,
960
+ reviewerEntrypointPath,
961
+ reviewerEntrypointSha256,
962
+ workflowEntrypointPath,
963
+ workflowEntrypointSha256,
964
+ dependencyClosureEvidenceSha256,
965
+ observedPackageName,
966
+ observedPackageVersion,
967
+ observedProtocolVersion,
968
+ observedStateSchemaVersion,
969
+ resultSha256,
970
+ failureKind: 'module-load-failed',
971
+ });
972
+ }
973
+ }
974
+
975
+ function assertReviewerRuntimePrepared(preflight: ReviewerRuntimePreflight): void {
976
+ if (!preflight.prepared) {
977
+ throw new Error(`External sealed review packet requires a prepared reviewer runtime: ${preflight.blockers.join(' ')}`);
978
+ }
979
+ }
980
+
981
+ function runtimeCheckEvidence(
982
+ value: Omit<ReviewerRuntimePreflight['runtimeCheck'],
983
+ 'kind' | 'loader' | 'executable' | 'evidenceSha256' | 'sealedCheckoutMutationAllowed'>,
984
+ ): ReviewerRuntimePreflight['runtimeCheck'] {
985
+ const evidence = {
986
+ kind: 'repository-local-gateway-load' as const,
987
+ loader: 'node:createRequire' as const,
988
+ executable: process.execPath,
989
+ ...value,
990
+ sealedCheckoutMutationAllowed: false as const,
991
+ };
992
+ return { ...evidence, evidenceSha256: canonicalHash(evidence) };
993
+ }
994
+
995
+ function runtimeModuleRecord(value: unknown): Record<string, unknown> | null {
996
+ return value && typeof value === 'object' && !Array.isArray(value)
997
+ ? value as Record<string, unknown>
998
+ : null;
999
+ }
1000
+
1001
+ interface RuntimeBuildManifest {
1002
+ protocol: 'codex-workflow-runtime-build-manifest-v1';
1003
+ packageName: string;
1004
+ packageVersion: string;
1005
+ packageManifestSha256: string;
1006
+ requiredEntrypoints: string[];
1007
+ files: Array<{ path: string; sha256: string }>;
1008
+ runtimeDependencies: Array<{
1009
+ name: string;
1010
+ version: string;
1011
+ packageManifestSha256: string;
1012
+ files: Array<{ path: string; sha256: string }>;
1013
+ }>;
1014
+ fingerprint: string;
1015
+ }
1016
+
1017
+ function inspectRuntimeDependencyClosure(
1018
+ nodeModulesPath: string,
1019
+ installedPackageRoot: string,
1020
+ manifest: RuntimeBuildManifest | null,
1021
+ eligible: boolean,
1022
+ ): ReviewerRuntimePreflight['runtimeDependencyReadiness'] {
1023
+ const expected = manifest?.runtimeDependencies ?? [];
1024
+ const expectedFileCount = expected.reduce((total, dependency) => total + dependency.files.length, 0);
1025
+ if (!eligible || !manifest) {
1026
+ const evidence = {
1027
+ state: 'not-run' as const,
1028
+ expectedPackageCount: expected.length,
1029
+ verifiedPackageCount: 0,
1030
+ expectedFileCount,
1031
+ verifiedFileCount: 0,
1032
+ packages: [],
1033
+ };
1034
+ return { ...evidence, evidenceSha256: canonicalHash(evidence) };
1035
+ }
1036
+ const nodeModulesReady = (() => {
1037
+ try {
1038
+ return lstatSync(nodeModulesPath).isDirectory()
1039
+ && isUnredirectedPathInside(path.dirname(nodeModulesPath), nodeModulesPath);
1040
+ } catch {
1041
+ return false;
1042
+ }
1043
+ })();
1044
+ const repositoryRoot = path.dirname(nodeModulesPath);
1045
+ const expectedByName = new Map(expected.map((dependency) => [dependency.name, dependency]));
1046
+ const installedWorkflowManifest = readJsonRecord(path.join(installedPackageRoot, 'package.json'));
1047
+ const rootDependencyNames = runtimeDependencyNames(installedWorkflowManifest);
1048
+ let graphDrift = rootDependencyNames === null;
1049
+ const queue: Array<{ name: string; parentPackageRoot: string }> = (rootDependencyNames ?? [])
1050
+ .map((name) => ({ name, parentPackageRoot: installedPackageRoot }));
1051
+ const queuedEdges = new Set(queue.map(({ name, parentPackageRoot }) => `${parentPackageRoot}\0${name}`));
1052
+ const inspectedInstances = new Map<string, ReviewerRuntimePreflight['runtimeDependencyReadiness']['packages'][number]>();
1053
+ while (queue.length > 0) {
1054
+ const edge = queue.shift()!;
1055
+ const expectedDependency = expectedByName.get(edge.name);
1056
+ if (!expectedDependency) {
1057
+ graphDrift = true;
1058
+ continue;
1059
+ }
1060
+ const resolution = resolveRuntimeDependencyPackageRoot(repositoryRoot, edge.parentPackageRoot, edge.name);
1061
+ const packageRoot = resolution ?? runtimeDependencySearchPaths(
1062
+ repositoryRoot,
1063
+ edge.parentPackageRoot,
1064
+ edge.name,
1065
+ )[0] ?? path.join(nodeModulesPath, ...edge.name.split('/'));
1066
+ const instanceKey = `${edge.name}\0${packageRoot}`;
1067
+ let inspected = inspectedInstances.get(instanceKey);
1068
+ if (!inspected) {
1069
+ inspected = inspectRuntimeDependencyPackage(
1070
+ repositoryRoot,
1071
+ packageRoot,
1072
+ expectedDependency,
1073
+ nodeModulesReady,
1074
+ );
1075
+ inspectedInstances.set(instanceKey, inspected);
1076
+ }
1077
+ if (inspected.state !== 'ready') continue;
1078
+ const installedDependencyManifest = readJsonRecord(inspected.packageManifestPath);
1079
+ const childNames = runtimeDependencyNames(installedDependencyManifest);
1080
+ if (childNames === null) {
1081
+ graphDrift = true;
1082
+ continue;
1083
+ }
1084
+ for (const childName of childNames) {
1085
+ if (!expectedByName.has(childName)) {
1086
+ graphDrift = true;
1087
+ continue;
1088
+ }
1089
+ const edgeKey = `${packageRoot}\0${childName}`;
1090
+ if (queuedEdges.has(edgeKey)) continue;
1091
+ queuedEdges.add(edgeKey);
1092
+ queue.push({ name: childName, parentPackageRoot: packageRoot });
1093
+ }
1094
+ }
1095
+ const packages = [...inspectedInstances.values()].sort((left, right) => (
1096
+ left.name.localeCompare(right.name) || left.packageRoot.localeCompare(right.packageRoot)
1097
+ ));
1098
+ const packageInstancesByName = new Map<string, typeof packages>();
1099
+ for (const dependency of packages) {
1100
+ const instances = packageInstancesByName.get(dependency.name) ?? [];
1101
+ instances.push(dependency);
1102
+ packageInstancesByName.set(dependency.name, instances);
1103
+ }
1104
+ const verifiedNames = expected.filter((dependency) => {
1105
+ const instances = packageInstancesByName.get(dependency.name) ?? [];
1106
+ return instances.length > 0 && instances.every((instance) => instance.state === 'ready');
1107
+ });
1108
+ const expectedNamesReached = verifiedNames.length === expected.length;
1109
+ const state: 'ready' | 'missing' | 'stale' = graphDrift
1110
+ || packages.some((dependency) => dependency.state === 'stale')
1111
+ || (packages.length === 0 && expected.length > 0 && nodeModulesReady)
1112
+ ? 'stale'
1113
+ : packages.some((dependency) => dependency.state === 'missing')
1114
+ || !nodeModulesReady
1115
+ ? 'missing'
1116
+ : expectedNamesReached
1117
+ ? 'ready'
1118
+ : 'stale';
1119
+ const evidence = {
1120
+ state,
1121
+ expectedPackageCount: expected.length,
1122
+ verifiedPackageCount: verifiedNames.length,
1123
+ expectedFileCount,
1124
+ verifiedFileCount: verifiedNames.reduce((total, dependency) => total + dependency.files.length, 0),
1125
+ packages,
1126
+ };
1127
+ return { ...evidence, evidenceSha256: canonicalHash(evidence) };
1128
+ }
1129
+
1130
+ function inspectRuntimeDependencyPackage(
1131
+ repositoryRoot: string,
1132
+ packageRoot: string,
1133
+ expected: RuntimeBuildManifest['runtimeDependencies'][number],
1134
+ nodeModulesReady: boolean,
1135
+ ): ReviewerRuntimePreflight['runtimeDependencyReadiness']['packages'][number] {
1136
+ const packageManifestPath = path.join(packageRoot, 'package.json');
1137
+ const base = {
1138
+ name: expected.name,
1139
+ version: expected.version,
1140
+ packageRoot,
1141
+ packageManifestPath,
1142
+ expectedFileCount: expected.files.length,
1143
+ };
1144
+ if (!nodeModulesReady || !existsSync(packageRoot)) {
1145
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'missing' };
1146
+ }
1147
+ try {
1148
+ if (!isPackageName(expected.name) || !lstatSync(packageRoot).isDirectory()) {
1149
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
1150
+ }
1151
+ const realRepositoryRoot = realpathSync(repositoryRoot);
1152
+ const realPackageRoot = realpathSync(packageRoot);
1153
+ if (!isUnredirectedPathInside(repositoryRoot, packageRoot)
1154
+ || isOutside(realRepositoryRoot, realPackageRoot)) {
1155
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
1156
+ }
1157
+ const manifestFile = inspectRuntimeFile(packageRoot, packageManifestPath);
1158
+ if (manifestFile.state === 'missing') {
1159
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'missing' };
1160
+ }
1161
+ const installedManifest = readJsonRecord(manifestFile.state === 'ready' ? packageManifestPath : null);
1162
+ if (manifestFile.state !== 'ready'
1163
+ || manifestFile.sha256 !== expected.packageManifestSha256
1164
+ || stringField(installedManifest, 'name') !== expected.name
1165
+ || stringField(installedManifest, 'version') !== expected.version) {
1166
+ return { ...base, packageManifestSha256: manifestFile.sha256, verifiedFileCount: 0, state: 'stale' };
1167
+ }
1168
+ const collected = collectRuntimeDependencyFiles(packageRoot);
1169
+ if (collected.unsafe) {
1170
+ return { ...base, packageManifestSha256: manifestFile.sha256, verifiedFileCount: 0, state: 'stale' };
1171
+ }
1172
+ const observed = new Map(collected.files.map((file) => [file.path, file.sha256]));
1173
+ let missing = false;
1174
+ let drift = collected.files.length !== expected.files.length;
1175
+ let verifiedFileCount = 0;
1176
+ for (const file of expected.files) {
1177
+ if (safeRuntimeDependencyPath(file.path) === null) {
1178
+ drift = true;
1179
+ continue;
1180
+ }
1181
+ const hash = observed.get(file.path);
1182
+ if (hash === undefined) missing = true;
1183
+ else if (hash !== file.sha256) drift = true;
1184
+ else verifiedFileCount += 1;
1185
+ }
1186
+ return {
1187
+ ...base,
1188
+ packageManifestSha256: manifestFile.sha256,
1189
+ verifiedFileCount,
1190
+ state: missing ? 'missing' : drift ? 'stale' : 'ready',
1191
+ };
1192
+ } catch {
1193
+ return { ...base, packageManifestSha256: null, verifiedFileCount: 0, state: 'stale' };
1194
+ }
1195
+ }
1196
+
1197
+ function resolveRuntimeDependencyPackageRoot(
1198
+ repositoryRoot: string,
1199
+ parentPackageRoot: string,
1200
+ dependencyName: string,
1201
+ ): string | null {
1202
+ for (const candidate of runtimeDependencySearchPaths(repositoryRoot, parentPackageRoot, dependencyName)) {
1203
+ try {
1204
+ lstatSync(candidate);
1205
+ return candidate;
1206
+ } catch {
1207
+ // Continue through Node's parent-aware node_modules search order.
1208
+ }
1209
+ }
1210
+ return null;
1211
+ }
1212
+
1213
+ function runtimeDependencySearchPaths(
1214
+ repositoryRoot: string,
1215
+ parentPackageRoot: string,
1216
+ dependencyName: string,
1217
+ ): string[] {
1218
+ if (!isPackageName(dependencyName)) return [];
1219
+ const root = path.resolve(repositoryRoot);
1220
+ let cursor = path.resolve(parentPackageRoot);
1221
+ if (isOutside(root, cursor)) return [];
1222
+ const candidates: string[] = [];
1223
+ while (true) {
1224
+ if (path.basename(cursor) !== 'node_modules') {
1225
+ candidates.push(path.join(cursor, 'node_modules', ...dependencyName.split('/')));
1226
+ }
1227
+ if (cursor === root) break;
1228
+ const parent = path.dirname(cursor);
1229
+ if (parent === cursor || isOutside(root, parent)) break;
1230
+ cursor = parent;
1231
+ }
1232
+ return candidates;
1233
+ }
1234
+
1235
+ function runtimeDependencyNames(manifest: Record<string, unknown> | null): string[] | null {
1236
+ const dependencies = manifest?.dependencies;
1237
+ if (dependencies === undefined) return [];
1238
+ if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) return null;
1239
+ const names = Object.keys(dependencies);
1240
+ return names.every(isPackageName) ? names.sort((left, right) => left.localeCompare(right)) : null;
1241
+ }
1242
+
1243
+ function collectRuntimeDependencyFiles(
1244
+ packageRoot: string,
1245
+ directory: string = packageRoot,
1246
+ ): { unsafe: boolean; files: Array<{ path: string; sha256: string }> } {
1247
+ try {
1248
+ const files: Array<{ path: string; sha256: string }> = [];
1249
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
1250
+ const absolutePath = path.join(directory, entry.name);
1251
+ if (entry.isSymbolicLink()) return { unsafe: true, files: [] };
1252
+ if (entry.isDirectory()) {
1253
+ if (entry.name === 'node_modules') return { unsafe: true, files: [] };
1254
+ const nested = collectRuntimeDependencyFiles(packageRoot, absolutePath);
1255
+ if (nested.unsafe) return nested;
1256
+ files.push(...nested.files);
1257
+ continue;
1258
+ }
1259
+ if (!entry.isFile() || entry.name === 'package.json' || !/\.(?:cjs|js|json|mjs|node)$/.test(entry.name)) continue;
1260
+ const relativePath = path.relative(packageRoot, absolutePath).split(path.sep).join('/');
1261
+ if (safeRuntimeDependencyPath(relativePath) === null
1262
+ || !lstatSync(absolutePath).isFile()
1263
+ || isOutside(realpathSync(packageRoot), realpathSync(absolutePath))) {
1264
+ return { unsafe: true, files: [] };
1265
+ }
1266
+ files.push({ path: relativePath, sha256: sha256File(absolutePath) });
1267
+ }
1268
+ return { unsafe: false, files: files.sort((left, right) => left.path.localeCompare(right.path)) };
1269
+ } catch {
1270
+ return { unsafe: true, files: [] };
1271
+ }
1272
+ }
1273
+
1274
+ function inspectReviewerBuild(
1275
+ installedPackageRoot: string,
1276
+ manifestPath: string,
1277
+ installedPackageManifestPath: string,
1278
+ cliPath: string,
1279
+ runningCliSha256: string,
1280
+ reviewerPath: string,
1281
+ runningRuntimeSha256: string,
1282
+ gatewayPath: string,
1283
+ runningGatewaySha256: string,
1284
+ workflowPath: string,
1285
+ runningWorkflowSha256: string,
1286
+ ): ReviewerRuntimePreflight['buildReadiness'] {
1287
+ const manifestFile = inspectRuntimeFile(installedPackageRoot, manifestPath);
1288
+ const rawManifest = readJsonRecord(manifestFile.state === 'ready' ? manifestPath : null);
1289
+ const parsed = parseRuntimeBuildManifest(rawManifest);
1290
+ const installedPackageManifestSha256 = inspectRuntimeFile(
1291
+ installedPackageRoot,
1292
+ installedPackageManifestPath,
1293
+ ).sha256;
1294
+ let state: 'ready' | 'missing' | 'stale' = manifestFile.state === 'unsafe'
1295
+ ? 'stale'
1296
+ : parsed
1297
+ ? 'ready'
1298
+ : 'missing';
1299
+ let verifiedFileCount = 0;
1300
+ if (parsed) {
1301
+ const expectedFingerprint = runtimeBuildFingerprint(parsed);
1302
+ const filePaths = new Set(parsed.files.map((file) => file.path));
1303
+ if (
1304
+ parsed.packageName !== PACKAGE_NAME
1305
+ || parsed.packageVersion !== PACKAGE_VERSION
1306
+ || parsed.packageManifestSha256 !== installedPackageManifestSha256
1307
+ || parsed.fingerprint !== expectedFingerprint
1308
+ || canonicalJsonStringify(parsed.requiredEntrypoints) !== canonicalJsonStringify([
1309
+ 'dist/src/cli.js',
1310
+ 'dist/src/gateway-handshake.js',
1311
+ 'dist/src/reviewer.js',
1312
+ 'dist/src/workflow.js',
1313
+ ])
1314
+ || !parsed.requiredEntrypoints.every((entry) => filePaths.has(entry))
1315
+ ) {
1316
+ state = 'stale';
1317
+ }
1318
+ for (const file of parsed.files) {
1319
+ const safeRelative = safeRuntimeBuildPath(file.path);
1320
+ if (safeRelative === null) {
1321
+ state = 'stale';
1322
+ continue;
1323
+ }
1324
+ const absolutePath = path.join(installedPackageRoot, safeRelative);
1325
+ const observed = inspectRuntimeFile(installedPackageRoot, absolutePath);
1326
+ if (observed.state === 'missing') {
1327
+ state = 'missing';
1328
+ continue;
1329
+ }
1330
+ if (observed.state === 'unsafe' || observed.sha256 !== file.sha256) {
1331
+ state = 'stale';
1332
+ continue;
1333
+ }
1334
+ verifiedFileCount += 1;
1335
+ }
1336
+ }
1337
+ const cliFile = inspectRuntimeFile(installedPackageRoot, cliPath);
1338
+ const reviewerFile = inspectRuntimeFile(installedPackageRoot, reviewerPath);
1339
+ const gatewayFile = inspectRuntimeFile(installedPackageRoot, gatewayPath);
1340
+ const workflowFile = inspectRuntimeFile(installedPackageRoot, workflowPath);
1341
+ const cliSha256 = cliFile.sha256;
1342
+ const reviewerSha256 = reviewerFile.sha256;
1343
+ const gatewaySha256 = gatewayFile.sha256;
1344
+ const workflowSha256 = workflowFile.sha256;
1345
+ if (parsed && [cliFile, reviewerFile, gatewayFile, workflowFile].some((file) => file.state === 'unsafe')) {
1346
+ state = 'stale';
1347
+ } else if (parsed && [cliSha256, reviewerSha256, gatewaySha256, workflowSha256].some((hash) => hash === null)) {
1348
+ state = 'missing';
1349
+ }
1350
+ const runningCliMatchesInstalledBuild = cliSha256 !== null && cliSha256 === runningCliSha256;
1351
+ const runningModuleMatchesInstalledBuild = reviewerSha256 !== null && reviewerSha256 === runningRuntimeSha256;
1352
+ const runningGatewayMatchesInstalledBuild = gatewaySha256 !== null && gatewaySha256 === runningGatewaySha256;
1353
+ const runningWorkflowMatchesInstalledBuild = workflowSha256 !== null && workflowSha256 === runningWorkflowSha256;
1354
+ if (parsed && (!runningCliMatchesInstalledBuild
1355
+ || !runningModuleMatchesInstalledBuild
1356
+ || !runningGatewayMatchesInstalledBuild
1357
+ || !runningWorkflowMatchesInstalledBuild)) state = 'stale';
1358
+ const manifestSha256 = manifestFile.sha256;
1359
+ const manifestFingerprint = parsed?.fingerprint ?? null;
1360
+ const expectedFileCount = parsed?.files.length ?? 0;
1361
+ return {
1362
+ state,
1363
+ manifestPath,
1364
+ manifestSha256,
1365
+ manifestFingerprint,
1366
+ verifiedFileCount,
1367
+ expectedFileCount,
1368
+ cliEntrypointPath: cliPath,
1369
+ cliEntrypointSha256: cliSha256,
1370
+ runningCliMatchesInstalledBuild,
1371
+ reviewerModulePath: reviewerPath,
1372
+ reviewerModuleSha256: reviewerSha256,
1373
+ runningModuleMatchesInstalledBuild,
1374
+ gatewayModulePath: gatewayPath,
1375
+ gatewayModuleSha256: gatewaySha256,
1376
+ runningGatewayMatchesInstalledBuild,
1377
+ workflowModulePath: workflowPath,
1378
+ workflowModuleSha256: workflowSha256,
1379
+ runningWorkflowMatchesInstalledBuild,
1380
+ evidenceSha256: canonicalHash({
1381
+ installedPackageRoot,
1382
+ manifestSha256,
1383
+ manifestFingerprint,
1384
+ installedPackageManifestSha256,
1385
+ verifiedFileCount,
1386
+ expectedFileCount,
1387
+ cliSha256,
1388
+ runningCliSha256,
1389
+ reviewerSha256,
1390
+ runningRuntimeSha256,
1391
+ gatewaySha256,
1392
+ runningGatewaySha256,
1393
+ workflowSha256,
1394
+ runningWorkflowSha256,
1395
+ state,
1396
+ }),
1397
+ };
1398
+ }
1399
+
1400
+ function parseRuntimeBuildManifest(value: Record<string, unknown> | null): RuntimeBuildManifest | null {
1401
+ if (!value
1402
+ || value.protocol !== 'codex-workflow-runtime-build-manifest-v1'
1403
+ || typeof value.packageName !== 'string'
1404
+ || typeof value.packageVersion !== 'string'
1405
+ || typeof value.packageManifestSha256 !== 'string'
1406
+ || !/^[a-f0-9]{64}$/.test(value.packageManifestSha256)
1407
+ || !Array.isArray(value.requiredEntrypoints)
1408
+ || !value.requiredEntrypoints.every((entry) => typeof entry === 'string')
1409
+ || !Array.isArray(value.files)
1410
+ || !Array.isArray(value.runtimeDependencies)
1411
+ || typeof value.fingerprint !== 'string') return null;
1412
+ const files = parseAttestedFiles(value.files);
1413
+ if (files.length !== value.files.length || new Set(files.map((entry) => entry.path)).size !== files.length) return null;
1414
+ const runtimeDependencies = value.runtimeDependencies.flatMap((entry) => {
1415
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
1416
+ const candidate = entry as Record<string, unknown>;
1417
+ if (typeof candidate.name !== 'string'
1418
+ || !isPackageName(candidate.name)
1419
+ || typeof candidate.version !== 'string'
1420
+ || typeof candidate.packageManifestSha256 !== 'string'
1421
+ || !/^[a-f0-9]{64}$/.test(candidate.packageManifestSha256)
1422
+ || !Array.isArray(candidate.files)) return [];
1423
+ const dependencyFiles = parseAttestedFiles(candidate.files);
1424
+ return dependencyFiles.length === candidate.files.length
1425
+ && new Set(dependencyFiles.map((file) => file.path)).size === dependencyFiles.length
1426
+ ? [{
1427
+ name: candidate.name,
1428
+ version: candidate.version,
1429
+ packageManifestSha256: candidate.packageManifestSha256,
1430
+ files: dependencyFiles,
1431
+ }]
1432
+ : [];
1433
+ });
1434
+ if (runtimeDependencies.length !== value.runtimeDependencies.length
1435
+ || new Set(runtimeDependencies.map((entry) => entry.name)).size !== runtimeDependencies.length) return null;
1436
+ return {
1437
+ protocol: value.protocol,
1438
+ packageName: value.packageName,
1439
+ packageVersion: value.packageVersion,
1440
+ packageManifestSha256: value.packageManifestSha256,
1441
+ requiredEntrypoints: [...value.requiredEntrypoints] as string[],
1442
+ files,
1443
+ runtimeDependencies,
1444
+ fingerprint: value.fingerprint,
1445
+ };
1446
+ }
1447
+
1448
+ function parseAttestedFiles(value: unknown[]): Array<{ path: string; sha256: string }> {
1449
+ return value.flatMap((entry) => {
1450
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
1451
+ const candidate = entry as Record<string, unknown>;
1452
+ return typeof candidate.path === 'string' && /^[a-f0-9]{64}$/.test(String(candidate.sha256))
1453
+ ? [{ path: candidate.path, sha256: String(candidate.sha256) }]
1454
+ : [];
1455
+ });
1456
+ }
1457
+
1458
+ function runtimeBuildFingerprint(value: Omit<RuntimeBuildManifest, 'fingerprint'> | RuntimeBuildManifest): string {
1459
+ const lines = [
1460
+ value.protocol,
1461
+ value.packageName,
1462
+ value.packageVersion,
1463
+ value.packageManifestSha256,
1464
+ ...value.requiredEntrypoints,
1465
+ ];
1466
+ for (const file of value.files) lines.push(file.path, file.sha256);
1467
+ for (const dependency of value.runtimeDependencies) {
1468
+ lines.push(dependency.name, dependency.version, dependency.packageManifestSha256);
1469
+ for (const file of dependency.files) lines.push(file.path, file.sha256);
1470
+ }
1471
+ return createHash('sha256').update(lines.join('\0')).digest('hex');
1472
+ }
1473
+
1474
+ function safeRuntimeBuildPath(value: string): string | null {
1475
+ const normalized = value.split('\\').join('/');
1476
+ if (!normalized.startsWith('dist/src/') || !normalized.endsWith('.js') || path.posix.normalize(normalized) !== normalized) return null;
1477
+ if (path.posix.isAbsolute(normalized) || normalized.split('/').includes('..')) return null;
1478
+ return normalized;
1479
+ }
1480
+
1481
+ function safeRuntimeDependencyPath(value: string): string | null {
1482
+ const normalized = value.split('\\').join('/');
1483
+ if (!/\.(?:cjs|js|json|mjs|node)$/.test(normalized)
1484
+ || path.posix.normalize(normalized) !== normalized
1485
+ || path.posix.isAbsolute(normalized)
1486
+ || normalized.split('/').includes('..')
1487
+ || normalized.split('/').includes('node_modules')) return null;
1488
+ return normalized;
1489
+ }
1490
+
1491
+ function isPackageName(value: string): boolean {
1492
+ return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(value);
1493
+ }
1494
+
1495
+ function readJsonRecord(filePath: string | null): Record<string, unknown> | null {
1496
+ if (!filePath) return null;
1497
+ try {
1498
+ const value: unknown = JSON.parse(readFileSync(filePath, 'utf8'));
1499
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
1500
+ } catch {
1501
+ return null;
1502
+ }
1503
+ }
1504
+
1505
+ function nestedString(value: Record<string, unknown> | null, keys: string[]): string | null {
1506
+ let cursor: unknown = value;
1507
+ for (const key of keys) {
1508
+ if (!cursor || typeof cursor !== 'object' || Array.isArray(cursor)) return null;
1509
+ cursor = (cursor as Record<string, unknown>)[key];
1510
+ }
1511
+ return typeof cursor === 'string' ? cursor : null;
1512
+ }
1513
+
1514
+ function stringField(value: Record<string, unknown> | null, key: string): string | null {
1515
+ const candidate = value?.[key];
1516
+ return typeof candidate === 'string' ? candidate : null;
1517
+ }
1518
+
1519
+ function isPlainDirectory(directoryPath: string): boolean {
1520
+ try {
1521
+ return existsSync(directoryPath) && lstatSync(directoryPath).isDirectory();
1522
+ } catch {
1523
+ return false;
1524
+ }
1525
+ }
1526
+
1527
+ function isUnredirectedPathInside(root: string, candidate: string): boolean {
1528
+ try {
1529
+ const logicalRoot = path.resolve(root);
1530
+ const logicalCandidate = path.resolve(candidate);
1531
+ if (isOutside(logicalRoot, logicalCandidate)) return false;
1532
+ const expectedRealPath = path.join(realpathSync(logicalRoot), path.relative(logicalRoot, logicalCandidate));
1533
+ return realpathSync(logicalCandidate) === expectedRealPath;
1534
+ } catch {
1535
+ return false;
1536
+ }
1537
+ }
1538
+
1539
+ function isPlainFileInside(root: string, filePath: string): boolean {
1540
+ return inspectRuntimeFile(root, filePath).state === 'ready';
1541
+ }
1542
+
1543
+ function inspectRuntimeFile(
1544
+ root: string,
1545
+ filePath: string,
1546
+ ): { state: 'ready' | 'missing' | 'unsafe'; sha256: string | null } {
1547
+ try {
1548
+ if (!existsSync(filePath)) return { state: 'missing', sha256: null };
1549
+ if (!lstatSync(filePath).isFile()) return { state: 'unsafe', sha256: null };
1550
+ const realRoot = realpathSync(root);
1551
+ const realFile = realpathSync(filePath);
1552
+ if (isOutside(realRoot, realFile)) return { state: 'unsafe', sha256: null };
1553
+ return { state: 'ready', sha256: sha256File(realFile) };
1554
+ } catch {
1555
+ return { state: 'unsafe', sha256: null };
1556
+ }
1557
+ }
1558
+
1559
+ function assertReviewMode(value: string): asserts value is ReviewMode {
1560
+ if (value !== 'ordinary' && value !== 'security') {
1561
+ throw new Error('review mode must be ordinary or security');
1562
+ }
1563
+ }
1564
+
1565
+ function reviewInstruction(mode: ReviewMode): string {
1566
+ return mode === 'security'
1567
+ ? 'Perform the requested security-focused review within the supplied sealed scope.'
1568
+ : 'Perform an ordinary correctness and acceptance review. Do not initiate or substitute an implicit security scan; report security-relevant defects only when encountered in the supplied scope.';
1569
+ }
1570
+
1571
+ function reviewPolicy(mode: ReviewMode): Record<string, unknown> {
1572
+ return { mode, implicitSecurityScan: false, securityFocusRequested: mode === 'security' };
1573
+ }
1574
+
1575
+ function isOutside(root: string, candidate: string): boolean {
1576
+ const relative = path.relative(root, candidate);
1577
+ return relative.startsWith('..') && !path.isAbsolute(relative);
1578
+ }
1579
+
1580
+ function sha256File(filePath: string): string {
1581
+ return createHash('sha256').update(readFileSync(filePath)).digest('hex');
1582
+ }
1583
+
1584
+ function isReviewFindingRecord(value: unknown): value is Record<string, unknown> {
1585
+ return isExactRecord(value, [
1586
+ 'id',
1587
+ 'severity',
1588
+ 'requirement',
1589
+ 'summary',
1590
+ 'evidence',
1591
+ 'requiredAction',
1592
+ 'causeId',
1593
+ 'route',
1594
+ 'planConflict',
1595
+ ]);
1596
+ }
1597
+
1598
+ function validatePlanConflict(
1599
+ value: unknown,
1600
+ route: 'fix' | 'replan',
1601
+ ): NonNullable<ReviewInput['findings'][number]['planConflict']> | null {
1602
+ if (route === 'fix') {
1603
+ if (value !== undefined && value !== null) throw new Error('fix findings cannot carry planConflict');
1604
+ return null;
1605
+ }
1606
+ if (!isExactRecord(value, ['planClause', 'whyFixCannotFit', 'minimalPlanChange'])) {
1607
+ throw new Error('replan findings require an exact planConflict');
1608
+ }
1609
+ for (const key of ['planClause', 'whyFixCannotFit', 'minimalPlanChange'] as const) {
1610
+ if (typeof value[key] !== 'string' || !value[key].trim()) {
1611
+ throw new Error('replan findings require non-empty planConflict fields');
1612
+ }
1613
+ }
1614
+ return {
1615
+ planClause: value.planClause as string,
1616
+ whyFixCannotFit: value.whyFixCannotFit as string,
1617
+ minimalPlanChange: value.minimalPlanChange as string,
1618
+ };
1619
+ }
1620
+
1621
+ function isExactRecord(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
1622
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
1623
+ const actual = Object.keys(value).sort();
1624
+ return actual.length === keys.length && actual.every((key, index) => key === [...keys].sort()[index]);
1625
+ }
1626
+
1627
+ function canonicalHash(value: unknown): string {
1628
+ return createHash('sha256').update(canonicalJsonStringify(value)).digest('hex');
1629
+ }
1630
+
1631
+ function spawnReviewer(
1632
+ command: string,
1633
+ args: string[],
1634
+ options: { cwd: string; input: string; timeoutMs: number },
1635
+ ): ReviewerProcessResult {
1636
+ const result = spawnSync(command, args, {
1637
+ cwd: options.cwd,
1638
+ input: options.input,
1639
+ encoding: 'utf8',
1640
+ maxBuffer: 16 * 1024 * 1024,
1641
+ timeout: options.timeoutMs,
1642
+ killSignal: 'SIGKILL',
1643
+ });
1644
+ const timedOut = result.error !== undefined && 'code' in result.error && result.error.code === 'ETIMEDOUT';
1645
+ return {
1646
+ status: result.status,
1647
+ stdout: result.stdout ?? '',
1648
+ stderr: [result.stderr ?? '', result.error?.message ?? ''].filter(Boolean).join('\n'),
1649
+ timedOut,
1650
+ };
1651
+ }
1652
+
1653
+ function runStrictReviewerLaunch(
1654
+ repositoryRoot: string,
1655
+ options: {
1656
+ protocol: string;
1657
+ task: TaskState;
1658
+ taskRoot: string;
1659
+ envelope: ContextEnvelope;
1660
+ commandSummary: string;
1661
+ packet: Record<string, unknown>;
1662
+ },
1663
+ runner: ReviewerCommandRunner,
1664
+ ): StrictReviewerLaunchResult & { reviewerOutputHash: string } {
1665
+ const temporary = mkdtempSync(path.join(os.tmpdir(), 'codex-workflow-review-'));
1666
+ const schemaFile = path.join(temporary, 'review-result.schema.json');
1667
+ const outputFile = path.join(temporary, 'last-message.json');
1668
+ writeFileSync(schemaFile, `${JSON.stringify(STRICT_REVIEW_OUTPUT_SCHEMA, null, 2)}\n`, { mode: 0o600 });
1669
+ const probe = path.join(repositoryRoot, '.codex-reviewer-write-probe');
1670
+ if (existsSync(probe)) {
1671
+ rmSync(temporary, { recursive: true, force: true });
1672
+ const failed = failedLaunch(repositoryRoot, [], null, 'Reviewer probe path already exists; isolation cannot be tested.');
1673
+ return { ...failed, reviewerOutputHash: hashReviewInput(failed.review) };
1674
+ }
1675
+ const before = sealRepository(repositoryRoot);
1676
+ const command = [
1677
+ 'codex', '--ask-for-approval', 'never', 'exec',
1678
+ '--ephemeral', '--ignore-user-config', '--ignore-rules', '--sandbox', 'read-only',
1679
+ '--output-schema', schemaFile, '--output-last-message', outputFile, '--json', '-C', repositoryRoot, '-',
1680
+ ];
1681
+ let processResult: ReviewerProcessResult;
1682
+ try {
1683
+ processResult = runner(command[0]!, command.slice(1), {
1684
+ cwd: repositoryRoot,
1685
+ input: `${JSON.stringify(options.packet)}\n`,
1686
+ timeoutMs: STRICT_REVIEWER_TIMEOUT_MS,
1687
+ });
1688
+ } catch (error) {
1689
+ processResult = { status: null, stdout: '', stderr: error instanceof Error ? error.message : String(error) };
1690
+ }
1691
+ const probeWritten = existsSync(probe);
1692
+ const after = sealRepository(repositoryRoot);
1693
+ if (probeWritten) rmSync(probe, { force: true });
1694
+ let output: StrictReviewOutput | null = null;
1695
+ if (processResult.status === 0 && existsSync(outputFile)) {
1696
+ try {
1697
+ const parsed = parseStrictReviewWithHash(readFileSync(outputFile, 'utf8'));
1698
+ output = parsed.output;
1699
+ } catch {
1700
+ output = null;
1701
+ }
1702
+ }
1703
+ rmSync(temporary, { recursive: true, force: true });
1704
+ const unchanged = sealsEqual(before, after) && !probeWritten;
1705
+ const verifiedIsolation = unchanged && output?.isolationProbe === 'denied' && processResult.status === 0;
1706
+ const review = verifiedIsolation && output
1707
+ ? stripProbe(output)
1708
+ : unverifiedReview(
1709
+ processResult.timedOut
1710
+ ? `Strict reviewer exceeded the ${STRICT_REVIEWER_TIMEOUT_MS}ms execution deadline.`
1711
+ : processResult.status !== 0
1712
+ ? `Strict reviewer process failed with status ${String(processResult.status)}.`
1713
+ : probeWritten || !unchanged
1714
+ ? 'Strict reviewer modified repository state; review discarded.'
1715
+ : options.commandSummary,
1716
+ );
1717
+ const reviewerOutputHash = hashReviewInput(review);
1718
+ return {
1719
+ review,
1720
+ verifiedIsolation,
1721
+ command,
1722
+ processStatus: processResult.status,
1723
+ timedOut: processResult.timedOut === true,
1724
+ stderr: processResult.stderr,
1725
+ before,
1726
+ after,
1727
+ reviewerOutputHash,
1728
+ };
1729
+ }
1730
+
1731
+ function sealRepository(repositoryRoot: string): RepositorySeal {
1732
+ return {
1733
+ headCommit: headCommit(repositoryRoot),
1734
+ changedFiles: changedFiles(repositoryRoot),
1735
+ contentHash: hashReviewableTree(repositoryRoot),
1736
+ };
1737
+ }
1738
+
1739
+ function hashReviewableTree(repositoryRoot: string): string {
1740
+ const listed = spawnSync(
1741
+ 'git',
1742
+ ['-C', repositoryRoot, 'ls-files', '--cached', '--others', '--exclude-standard', '-z'],
1743
+ { encoding: 'utf8' },
1744
+ );
1745
+ if (listed.status !== 0) {
1746
+ throw new Error('Unable to enumerate reviewable repository files.');
1747
+ }
1748
+ const hash = createHash('sha256');
1749
+ const paths = listed.stdout.split('\0').filter(Boolean).sort();
1750
+ for (const relative of paths) {
1751
+ const absolute = path.join(repositoryRoot, relative);
1752
+ hash.update(`${relative}\0`);
1753
+ if (!existsSync(absolute)) {
1754
+ hash.update('missing');
1755
+ continue;
1756
+ }
1757
+ const stat = lstatSync(absolute);
1758
+ hash.update(`${stat.mode & 0o777}\0`);
1759
+ if (stat.isSymbolicLink()) hash.update(`link:${readlinkSync(absolute)}`);
1760
+ else if (stat.isFile()) hash.update(readFileSync(absolute));
1761
+ }
1762
+ return hash.digest('hex');
1763
+ }
1764
+
1765
+ function sealsEqual(left: RepositorySeal, right: RepositorySeal): boolean {
1766
+ return left.headCommit === right.headCommit
1767
+ && left.contentHash === right.contentHash
1768
+ && JSON.stringify(left.changedFiles) === JSON.stringify(right.changedFiles);
1769
+ }
1770
+
1771
+ function parseStrictReview(value: string): StrictReviewOutput {
1772
+ const parsed: unknown = JSON.parse(value);
1773
+ if (!isExactRecord(parsed, ['status', 'reviewer', 'summary', 'findings', 'isolationProbe'])) {
1774
+ throw new Error('invalid review');
1775
+ }
1776
+ if (!['passed', 'failed', 'unverified'].includes(String(parsed.status))) throw new Error('invalid status');
1777
+ if (typeof parsed.reviewer !== 'string' || !parsed.reviewer.trim()
1778
+ || typeof parsed.summary !== 'string' || !parsed.summary.trim()
1779
+ || !Array.isArray(parsed.findings)) throw new Error('invalid review');
1780
+ if (!['denied', 'written', 'not-attempted'].includes(String(parsed.isolationProbe))) throw new Error('invalid probe');
1781
+ const findings = parsed.findings.map((candidate) => {
1782
+ if (!isReviewFindingRecord(candidate)
1783
+ || typeof candidate.id !== 'string' || !candidate.id.trim()
1784
+ || !['critical', 'major', 'minor'].includes(String(candidate.severity))
1785
+ || (candidate.requirement !== null && typeof candidate.requirement !== 'string')
1786
+ || typeof candidate.summary !== 'string' || !candidate.summary.trim()
1787
+ || typeof candidate.evidence !== 'string' || !candidate.evidence.trim()
1788
+ || typeof candidate.requiredAction !== 'string' || !candidate.requiredAction.trim()) {
1789
+ throw new Error('invalid review finding');
1790
+ }
1791
+ if (candidate.route !== 'fix' && candidate.route !== 'replan') throw new Error('invalid review finding route');
1792
+ const route: 'fix' | 'replan' = candidate.route;
1793
+ const causeId = candidate.causeId;
1794
+ if (causeId !== null
1795
+ && (typeof causeId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(causeId))) {
1796
+ throw new Error('invalid review finding causeId');
1797
+ }
1798
+ const planConflict = validatePlanConflict(candidate.planConflict, route);
1799
+ return {
1800
+ id: candidate.id,
1801
+ severity: candidate.severity as 'critical' | 'major' | 'minor',
1802
+ requirement: candidate.requirement as string | null,
1803
+ summary: candidate.summary,
1804
+ evidence: candidate.evidence,
1805
+ requiredAction: candidate.requiredAction,
1806
+ ...(causeId === null ? {} : { causeId }),
1807
+ route,
1808
+ planConflict,
1809
+ };
1810
+ });
1811
+ if ((parsed.status === 'passed' && findings.length > 0)
1812
+ || (parsed.status === 'failed' && findings.length === 0)) throw new Error('invalid finding cardinality');
1813
+ return {
1814
+ status: parsed.status as StrictReviewOutput['status'],
1815
+ reviewer: parsed.reviewer,
1816
+ summary: parsed.summary,
1817
+ findings,
1818
+ isolationProbe: parsed.isolationProbe as StrictReviewOutput['isolationProbe'],
1819
+ };
1820
+ }
1821
+
1822
+ function parseStrictReviewWithHash(value: string): ParsedStrictReviewResult {
1823
+ const output = parseStrictReview(value);
1824
+ return {
1825
+ output,
1826
+ outputHash: hashReviewInput(stripProbe(output)),
1827
+ };
1828
+ }
1829
+
1830
+ function hashReviewInput(review: ReviewInput): string {
1831
+ return createHash('sha256').update(canonicalJsonStringify(review)).digest('hex');
1832
+ }
1833
+
1834
+ function stripProbe(output: StrictReviewOutput): ReviewInput {
1835
+ return {
1836
+ status: output.status,
1837
+ reviewer: output.reviewer,
1838
+ summary: output.summary,
1839
+ findings: output.findings,
1840
+ };
1841
+ }
1842
+
1843
+ function unverifiedReview(summary: string): ReviewInput {
1844
+ return { status: 'unverified', reviewer: 'strict-codex-reviewer', summary, findings: [] };
1845
+ }
1846
+
1847
+ function failedLaunch(
1848
+ repositoryRoot: string,
1849
+ command: string[],
1850
+ processStatus: number | null,
1851
+ summary: string,
1852
+ ): StrictReviewerLaunchResult {
1853
+ const seal = sealRepository(repositoryRoot);
1854
+ return {
1855
+ review: unverifiedReview(summary),
1856
+ verifiedIsolation: false,
1857
+ command,
1858
+ processStatus,
1859
+ timedOut: false,
1860
+ stderr: summary,
1861
+ before: seal,
1862
+ after: seal,
1863
+ };
1864
+ }
1865
+
1866
+ function failedStepLaunch(
1867
+ repositoryRoot: string,
1868
+ command: string[],
1869
+ processStatus: number | null,
1870
+ summary: string,
1871
+ reviewedCommit: string,
1872
+ ): StrictStepReviewerLaunchResult {
1873
+ const base = failedLaunch(repositoryRoot, command, processStatus, summary);
1874
+ return {
1875
+ ...base,
1876
+ reviewedCommit,
1877
+ reviewerOutputHash: hashReviewInput(base.review),
1878
+ };
1879
+ }