codex-orchestrator 2.0.10 → 2.0.12

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 (279) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +29 -30
  3. package/dist/src/index.d.ts +4 -9
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +2 -5
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/v2/acceptance-proof.d.ts +53 -25
  8. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  9. package/dist/src/v2/acceptance-proof.js +189 -197
  10. package/dist/src/v2/acceptance-proof.js.map +1 -1
  11. package/dist/src/v2/active-attempt.d.ts +94 -0
  12. package/dist/src/v2/active-attempt.d.ts.map +1 -0
  13. package/dist/src/v2/active-attempt.js +200 -0
  14. package/dist/src/v2/active-attempt.js.map +1 -0
  15. package/dist/src/v2/adapters/command.d.ts +7 -0
  16. package/dist/src/v2/adapters/command.d.ts.map +1 -1
  17. package/dist/src/v2/adapters/command.js +44 -3
  18. package/dist/src/v2/adapters/command.js.map +1 -1
  19. package/dist/src/v2/adapters/gh-pull-request-adapter.js +1 -0
  20. package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
  21. package/dist/src/v2/adapters/pull-requests.d.ts +1 -0
  22. package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
  23. package/dist/src/v2/adapters/pull-requests.js +1 -0
  24. package/dist/src/v2/adapters/pull-requests.js.map +1 -1
  25. package/dist/src/v2/adapters/worktree.js +3 -3
  26. package/dist/src/v2/adapters/worktree.js.map +1 -1
  27. package/dist/src/v2/atomic-store.d.ts +15 -0
  28. package/dist/src/v2/atomic-store.d.ts.map +1 -1
  29. package/dist/src/v2/atomic-store.js +55 -8
  30. package/dist/src/v2/atomic-store.js.map +1 -1
  31. package/dist/src/v2/candidate.d.ts +136 -0
  32. package/dist/src/v2/candidate.d.ts.map +1 -0
  33. package/dist/src/v2/candidate.js +107 -0
  34. package/dist/src/v2/candidate.js.map +1 -0
  35. package/dist/src/v2/checked-change.d.ts +39 -7
  36. package/dist/src/v2/checked-change.d.ts.map +1 -1
  37. package/dist/src/v2/checked-change.js +73 -1
  38. package/dist/src/v2/checked-change.js.map +1 -1
  39. package/dist/src/v2/cli-contract.d.ts +1 -1
  40. package/dist/src/v2/cli-contract.d.ts.map +1 -1
  41. package/dist/src/v2/cli-contract.js +4 -6
  42. package/dist/src/v2/cli-contract.js.map +1 -1
  43. package/dist/src/v2/cli.d.ts +14 -0
  44. package/dist/src/v2/cli.d.ts.map +1 -1
  45. package/dist/src/v2/cli.js +70 -21
  46. package/dist/src/v2/cli.js.map +1 -1
  47. package/dist/src/v2/code-review-report.d.ts +10 -18
  48. package/dist/src/v2/code-review-report.d.ts.map +1 -1
  49. package/dist/src/v2/code-review-report.js +63 -60
  50. package/dist/src/v2/code-review-report.js.map +1 -1
  51. package/dist/src/v2/codex-process.d.ts +6 -1
  52. package/dist/src/v2/codex-process.d.ts.map +1 -1
  53. package/dist/src/v2/codex-process.js +36 -9
  54. package/dist/src/v2/codex-process.js.map +1 -1
  55. package/dist/src/v2/config.d.ts +0 -2
  56. package/dist/src/v2/config.d.ts.map +1 -1
  57. package/dist/src/v2/config.js +3 -6
  58. package/dist/src/v2/config.js.map +1 -1
  59. package/dist/src/v2/contained-report-operation.d.ts +17 -6
  60. package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
  61. package/dist/src/v2/contained-report-operation.js +11 -24
  62. package/dist/src/v2/contained-report-operation.js.map +1 -1
  63. package/dist/src/v2/containment.d.ts +1 -0
  64. package/dist/src/v2/containment.d.ts.map +1 -1
  65. package/dist/src/v2/containment.js +12 -2
  66. package/dist/src/v2/containment.js.map +1 -1
  67. package/dist/src/v2/delivery-authority.d.ts +26 -0
  68. package/dist/src/v2/delivery-authority.d.ts.map +1 -0
  69. package/dist/src/v2/delivery-authority.js +44 -0
  70. package/dist/src/v2/delivery-authority.js.map +1 -0
  71. package/dist/src/v2/direct-delivery.d.ts +19 -57
  72. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  73. package/dist/src/v2/direct-delivery.js +154 -211
  74. package/dist/src/v2/direct-delivery.js.map +1 -1
  75. package/dist/src/v2/immutable-workflow-publisher.d.ts.map +1 -1
  76. package/dist/src/v2/immutable-workflow-publisher.js +3 -1
  77. package/dist/src/v2/immutable-workflow-publisher.js.map +1 -1
  78. package/dist/src/v2/implementation-report.d.ts +3 -1
  79. package/dist/src/v2/implementation-report.d.ts.map +1 -1
  80. package/dist/src/v2/implementation-report.js +17 -4
  81. package/dist/src/v2/implementation-report.js.map +1 -1
  82. package/dist/src/v2/implementation-reviewer.d.ts +22 -13
  83. package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
  84. package/dist/src/v2/implementation-reviewer.js +96 -30
  85. package/dist/src/v2/implementation-reviewer.js.map +1 -1
  86. package/dist/src/v2/owner-control-lock.d.ts +13 -1
  87. package/dist/src/v2/owner-control-lock.d.ts.map +1 -1
  88. package/dist/src/v2/owner-control-lock.js +66 -7
  89. package/dist/src/v2/owner-control-lock.js.map +1 -1
  90. package/dist/src/v2/pending-effect-settlement.d.ts +44 -0
  91. package/dist/src/v2/pending-effect-settlement.d.ts.map +1 -0
  92. package/dist/src/v2/pending-effect-settlement.js +69 -0
  93. package/dist/src/v2/pending-effect-settlement.js.map +1 -0
  94. package/dist/src/v2/process-identity.d.ts +45 -0
  95. package/dist/src/v2/process-identity.d.ts.map +1 -0
  96. package/dist/src/v2/process-identity.js +118 -0
  97. package/dist/src/v2/process-identity.js.map +1 -0
  98. package/dist/src/v2/proof-report.d.ts +4 -2
  99. package/dist/src/v2/proof-report.d.ts.map +1 -1
  100. package/dist/src/v2/proof-report.js +16 -44
  101. package/dist/src/v2/proof-report.js.map +1 -1
  102. package/dist/src/v2/review-feedback-coordinator.d.ts +8 -2
  103. package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -1
  104. package/dist/src/v2/review-feedback-coordinator.js +98 -71
  105. package/dist/src/v2/review-feedback-coordinator.js.map +1 -1
  106. package/dist/src/v2/review-feedback.d.ts +14 -38
  107. package/dist/src/v2/review-feedback.d.ts.map +1 -1
  108. package/dist/src/v2/review-feedback.js +48 -115
  109. package/dist/src/v2/review-feedback.js.map +1 -1
  110. package/dist/src/v2/run-issue.d.ts +139 -60
  111. package/dist/src/v2/run-issue.d.ts.map +1 -1
  112. package/dist/src/v2/run-issue.js +2236 -1974
  113. package/dist/src/v2/run-issue.js.map +1 -1
  114. package/dist/src/v2/run-state-projections.d.ts +84 -0
  115. package/dist/src/v2/run-state-projections.d.ts.map +1 -0
  116. package/dist/src/v2/run-state-projections.js +142 -0
  117. package/dist/src/v2/run-state-projections.js.map +1 -0
  118. package/dist/src/v2/run-store.d.ts +114 -82
  119. package/dist/src/v2/run-store.d.ts.map +1 -1
  120. package/dist/src/v2/run-store.js +308 -224
  121. package/dist/src/v2/run-store.js.map +1 -1
  122. package/dist/src/v2/runtime-assets.d.ts +3 -0
  123. package/dist/src/v2/runtime-assets.d.ts.map +1 -1
  124. package/dist/src/v2/runtime-assets.js +104 -0
  125. package/dist/src/v2/runtime-assets.js.map +1 -1
  126. package/dist/src/v2/runtime.d.ts +52 -15
  127. package/dist/src/v2/runtime.d.ts.map +1 -1
  128. package/dist/src/v2/runtime.js +818 -348
  129. package/dist/src/v2/runtime.js.map +1 -1
  130. package/dist/src/v2/setup.js +0 -2
  131. package/dist/src/v2/setup.js.map +1 -1
  132. package/dist/src/v2/validation-progression.d.ts +70 -0
  133. package/dist/src/v2/validation-progression.d.ts.map +1 -0
  134. package/dist/src/v2/validation-progression.js +247 -0
  135. package/dist/src/v2/validation-progression.js.map +1 -0
  136. package/dist/src/v2/workflow-assets.d.ts +9 -3
  137. package/dist/src/v2/workflow-assets.d.ts.map +1 -1
  138. package/dist/src/v2/workflow-assets.js +256 -43
  139. package/dist/src/v2/workflow-assets.js.map +1 -1
  140. package/docs/deep-dive.md +52 -14
  141. package/internal-workflow/docs/agents/bug-workflow-routing.md +9 -7
  142. package/internal-workflow/docs/agents/coding-skill-routing.md +170 -120
  143. package/internal-workflow/docs/agents/tool-usage.md +23 -12
  144. package/internal-workflow/manifest.json +1 -1
  145. package/internal-workflow/operations/code-review/SKILL.md +34 -15
  146. package/internal-workflow/operations/implementation/SKILL.md +21 -16
  147. package/internal-workflow/profiles/implementer.toml +9 -0
  148. package/internal-workflow/profiles/review_coordinator.toml +9 -0
  149. package/internal-workflow/profiles/spec_reviewer.toml +9 -0
  150. package/internal-workflow/profiles/standards_reviewer.toml +9 -0
  151. package/internal-workflow/schemas/code-review-v1.json +1 -1
  152. package/internal-workflow/schemas/implementation-report-v1.json +1 -1
  153. package/internal-workflow/schemas/proof-report-v1.json +1 -1
  154. package/internal-workflow/skills/bug-root-cause-explainer/SKILL.md +114 -0
  155. package/internal-workflow/skills/bug-root-cause-explainer/agents/openai.yaml +7 -0
  156. package/internal-workflow/skills/bug-root-cause-explainer/evals/evals.json +18 -0
  157. package/internal-workflow/skills/code-review/SKILL.md +84 -288
  158. package/internal-workflow/skills/code-review/agents/openai.yaml +5 -3
  159. package/internal-workflow/skills/code-review/evals/evals.json +83 -0
  160. package/internal-workflow/skills/code-review/references/standards-smells.md +41 -0
  161. package/internal-workflow/skills/diagnosing-bugs/SKILL.md +69 -32
  162. package/internal-workflow/skills/diagnosing-bugs/agents/openai.yaml +2 -2
  163. package/internal-workflow/skills/diagnosing-bugs/evals/evals.json +63 -0
  164. package/internal-workflow/skills/grilling/SKILL.md +51 -0
  165. package/internal-workflow/skills/grilling/agents/openai.yaml +6 -0
  166. package/internal-workflow/skills/grilling/evals/evals.json +47 -0
  167. package/internal-workflow/skills/implement/SKILL.md +135 -0
  168. package/internal-workflow/skills/implement/agents/openai.yaml +6 -0
  169. package/internal-workflow/skills/implement/evals/evals.json +150 -0
  170. package/internal-workflow/skills/plan/SKILL.md +59 -0
  171. package/internal-workflow/skills/plan/agents/openai.yaml +6 -0
  172. package/internal-workflow/skills/plan/evals/evals.json +36 -0
  173. package/internal-workflow/skills/prototype/LOGIC.md +130 -0
  174. package/internal-workflow/skills/prototype/SKILL.md +69 -0
  175. package/internal-workflow/skills/prototype/UI.md +157 -0
  176. package/internal-workflow/skills/prototype/agents/openai.yaml +6 -0
  177. package/internal-workflow/skills/prototype/evals/evals.json +67 -0
  178. package/internal-workflow/skills/research/SKILL.md +110 -0
  179. package/internal-workflow/skills/research/agents/openai.yaml +6 -0
  180. package/internal-workflow/skills/research/evals/evals.json +49 -0
  181. package/internal-workflow/skills/tdd/SKILL.md +72 -67
  182. package/internal-workflow/skills/tdd/agents/openai.yaml +2 -2
  183. package/internal-workflow/skills/tdd/evals/evals.json +12 -0
  184. package/internal-workflow/skills/tdd/mocking.md +48 -1
  185. package/internal-workflow/skills/tdd/refactoring.md +3 -3
  186. package/internal-workflow/skills/tickets-orchestrator/SKILL.md +199 -0
  187. package/internal-workflow/skills/tickets-orchestrator/agents/openai.yaml +6 -0
  188. package/internal-workflow/skills/tickets-orchestrator/evals/evals.json +126 -0
  189. package/internal-workflow/skills/tickets-orchestrator/references/delegate-integrate.md +83 -0
  190. package/internal-workflow/skills/tickets-orchestrator/references/finish-delivery.md +69 -0
  191. package/internal-workflow/skills/tickets-orchestrator/references/stop-completion.md +63 -0
  192. package/internal-workflow/skills/to-spec/SKILL.md +133 -0
  193. package/internal-workflow/skills/to-spec/agents/openai.yaml +6 -0
  194. package/internal-workflow/skills/to-spec/evals/evals.json +24 -0
  195. package/internal-workflow/skills/to-tickets/SKILL.md +189 -0
  196. package/internal-workflow/skills/to-tickets/agents/openai.yaml +6 -0
  197. package/internal-workflow/skills/to-tickets/evals/evals.json +79 -0
  198. package/internal-workflow/skills/to-tickets/references/publishing-details.md +117 -0
  199. package/package.json +1 -1
  200. package/dist/src/v2/proof-store.d.ts +0 -42
  201. package/dist/src/v2/proof-store.d.ts.map +0 -1
  202. package/dist/src/v2/proof-store.js +0 -180
  203. package/dist/src/v2/proof-store.js.map +0 -1
  204. package/dist/src/v2/route-continuations.d.ts +0 -32
  205. package/dist/src/v2/route-continuations.d.ts.map +0 -1
  206. package/dist/src/v2/route-continuations.js +0 -2
  207. package/dist/src/v2/route-continuations.js.map +0 -1
  208. package/dist/src/v2/route-coordinator.d.ts +0 -77
  209. package/dist/src/v2/route-coordinator.d.ts.map +0 -1
  210. package/dist/src/v2/route-coordinator.js +0 -370
  211. package/dist/src/v2/route-coordinator.js.map +0 -1
  212. package/dist/src/v2/route-decision.d.ts +0 -129
  213. package/dist/src/v2/route-decision.d.ts.map +0 -1
  214. package/dist/src/v2/route-decision.js +0 -400
  215. package/dist/src/v2/route-decision.js.map +0 -1
  216. package/dist/src/v2/spec-coordinator.d.ts +0 -85
  217. package/dist/src/v2/spec-coordinator.d.ts.map +0 -1
  218. package/dist/src/v2/spec-coordinator.js +0 -88
  219. package/dist/src/v2/spec-coordinator.js.map +0 -1
  220. package/dist/src/v2/spec-delivery.d.ts +0 -143
  221. package/dist/src/v2/spec-delivery.d.ts.map +0 -1
  222. package/dist/src/v2/spec-delivery.js +0 -401
  223. package/dist/src/v2/spec-delivery.js.map +0 -1
  224. package/dist/src/v2/triage-route.d.ts +0 -68
  225. package/dist/src/v2/triage-route.d.ts.map +0 -1
  226. package/dist/src/v2/triage-route.js +0 -223
  227. package/dist/src/v2/triage-route.js.map +0 -1
  228. package/dist/src/v2/waiting-human-coordinator.d.ts +0 -49
  229. package/dist/src/v2/waiting-human-coordinator.d.ts.map +0 -1
  230. package/dist/src/v2/waiting-human-coordinator.js +0 -509
  231. package/dist/src/v2/waiting-human-coordinator.js.map +0 -1
  232. package/dist/src/v2/waiting-human.d.ts +0 -143
  233. package/dist/src/v2/waiting-human.d.ts.map +0 -1
  234. package/dist/src/v2/waiting-human.js +0 -408
  235. package/dist/src/v2/waiting-human.js.map +0 -1
  236. package/internal-workflow/docs/agents/contract-test-ledger.md +0 -71
  237. package/internal-workflow/docs/agents/review-gates.md +0 -42
  238. package/internal-workflow/docs/agents/review-protocol.md +0 -98
  239. package/internal-workflow/evals/coding-skill-evals.json +0 -332
  240. package/internal-workflow/operations/ambiguity-review/SKILL.md +0 -5
  241. package/internal-workflow/operations/qualification-repair/SKILL.md +0 -17
  242. package/internal-workflow/operations/spec-author/SKILL.md +0 -12
  243. package/internal-workflow/operations/spec-review/SKILL.md +0 -12
  244. package/internal-workflow/operations/triage/SKILL.md +0 -12
  245. package/internal-workflow/profiles/analyst_deep.toml +0 -9
  246. package/internal-workflow/profiles/implementer_standard.toml +0 -9
  247. package/internal-workflow/profiles/proof_agent.toml +0 -8
  248. package/internal-workflow/profiles/reviewer_deep.toml +0 -9
  249. package/internal-workflow/profiles/reviewer_standard.toml +0 -9
  250. package/internal-workflow/schemas/ambiguity-review-v1.json +0 -1
  251. package/internal-workflow/schemas/spec-author-v1.json +0 -1
  252. package/internal-workflow/schemas/spec-review-v1.json +0 -30
  253. package/internal-workflow/schemas/triage-route-v1.json +0 -1
  254. package/internal-workflow/skills/agent-auto/SKILL.md +0 -19
  255. package/internal-workflow/skills/agent-auto/agents/openai.yaml +0 -6
  256. package/internal-workflow/skills/code-debugger/SKILL.md +0 -122
  257. package/internal-workflow/skills/code-debugger/agents/openai.yaml +0 -7
  258. package/internal-workflow/skills/code-review/references/bug-classes.md +0 -56
  259. package/internal-workflow/skills/code-review/references/cleanup-lens.md +0 -52
  260. package/internal-workflow/skills/code-review/references/framework-lenses.md +0 -34
  261. package/internal-workflow/skills/code-review/references/targeted-recipes.md +0 -49
  262. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +0 -107
  263. package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +0 -6
  264. package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +0 -32
  265. package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +0 -146
  266. package/internal-workflow/skills/implementation-spec-review/SKILL.md +0 -131
  267. package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +0 -6
  268. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +0 -78
  269. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +0 -121
  270. package/internal-workflow/skills/small-task-implementer/SKILL.md +0 -112
  271. package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +0 -6
  272. package/internal-workflow/skills/spec-implementer/SKILL.md +0 -126
  273. package/internal-workflow/skills/spec-implementer/agents/openai.yaml +0 -6
  274. package/internal-workflow/skills/spec-implementer/evals/evals.json +0 -30
  275. package/internal-workflow/skills/spec-implementer/references/review-loop.md +0 -100
  276. package/internal-workflow/skills/triage/AGENT-BRIEF.md +0 -192
  277. package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +0 -101
  278. package/internal-workflow/skills/triage/SKILL.md +0 -134
  279. package/internal-workflow/skills/triage/agents/openai.yaml +0 -6
@@ -1,45 +1,411 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { spawn } from 'node:child_process';
4
- import { access, lstat, mkdir, open, readFile, readdir, readlink, realpath, rm } from 'node:fs/promises';
4
+ import { access, link, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, unlink } from 'node:fs/promises';
5
5
  import { homedir, hostname } from 'node:os';
6
6
  import { basename, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
7
7
  import { writeDurableAtomicFile } from './adapters/durable-atomic-file.js';
8
8
  import { GitWorktreeManager } from './adapters/worktree.js';
9
- import { ReviewFeedbackCoordinator } from './review-feedback-coordinator.js';
9
+ import { ReviewFeedbackObserver } from './review-feedback-coordinator.js';
10
10
  import { defaultProcessExecutor } from './adapters/command.js';
11
11
  import { RunnerAndroidProofController } from './android-proof-runner.js';
12
- import { AcceptanceProof, ProofQuiescenceError } from './acceptance-proof.js';
12
+ import { AcceptanceProof, CandidateProofInspectionError } from './acceptance-proof.js';
13
13
  import { createCheckedChangeCapabilities } from './checked-change.js';
14
14
  import { InjectedContainedReportOperation } from './contained-report-operation.js';
15
15
  import { ContainedImplementationReviewer } from './implementation-reviewer.js';
16
16
  import { parseAgentAutoConfig } from './config.js';
17
- import { WaitingHumanCoordinator } from './waiting-human-coordinator.js';
18
17
  import { canonicalJson, parseJsonWithoutDuplicateKeys, sha256, } from './containment.js';
19
- import { FileProofRecordWriter } from './proof-store.js';
20
18
  import { CheckProcessQuiescenceError, parseIssueCheckInvocation, resolveIssueCheckPolicy } from './issue-check-policy.js';
21
19
  import { acquireOwnerControlLock, OwnerControlLockBlockedError } from './owner-control-lock.js';
22
20
  import { decodeAgentReportForValidation } from './report-envelope.js';
23
21
  import { CodexProcess, ProcessQuiescenceError } from './codex-process.js';
24
22
  import { FileAndroidLeaseVerifier, FileIosLeaseVerifier } from './mobile-lease.js';
25
23
  import { publishRuntimeAssetSnapshot } from './runtime-assets.js';
26
- import { RouteCoordinator } from './route-coordinator.js';
27
- import { SpecCoordinator } from './spec-coordinator.js';
28
- import { createSpecRevision } from './spec-delivery.js';
29
- import { validateCodeReviewDefects } from './code-review-report.js';
30
- import { hashRouteDecision, validateRouteReceipt } from './route-decision.js';
31
- import { OwnerLockContentionError, OwnerLockSafetyError, RunIssue } from './run-issue.js';
24
+ import { OwnerLockContentionError, OwnerLockSafetyError, RunIssue, } from './run-issue.js';
32
25
  import { FileRunRecordWriter } from './run-store.js';
26
+ import { captureProcessStartIdentity, observeProcessGroup, observeProcessIdentity } from './process-identity.js';
33
27
  import { parseWorkflowExecutionProfile, verifyWorkflowGeneration, } from './workflow-assets.js';
28
+ import { candidateBindingId, candidateBindingSeed, candidateRef, } from './candidate.js';
34
29
  export class LocalGitRunIssueAdapter {
35
30
  executor;
31
+ repositoryRoot;
36
32
  worktrees;
37
- constructor(executor = defaultProcessExecutor) {
33
+ candidateRepositories = new Map();
34
+ candidateV2;
35
+ constructor(executor = defaultProcessExecutor, repositoryRoot) {
38
36
  this.executor = executor;
37
+ this.repositoryRoot = repositoryRoot;
39
38
  this.worktrees = new GitWorktreeManager(executor);
39
+ this.candidateV2 = {
40
+ reconcileOrphans: (input) => this.reconcileCandidateOrphans(input),
41
+ captureAndPin: (input) => this.captureAndPinCandidate(input),
42
+ inspectPin: (binding) => this.inspectCandidatePin(binding),
43
+ normalizeSharedIndex: (input) => this.candidateOperation('candidate-io-failed', async () => {
44
+ if (await this.getHead(input.worktreePath) !== input.expectedHeadSha)
45
+ throw new Error('candidate expected HEAD diverged');
46
+ await this.git(['-C', input.worktreePath, 'read-tree', input.expectedHeadSha]);
47
+ }),
48
+ prepareMaterialization: (input) => this.prepareCandidateMaterialization(input),
49
+ inspectMaterialization: (input) => this.inspectCandidateMaterialization(input),
50
+ removeMaterialization: (input) => this.removeCandidateMaterialization(input.materialization),
51
+ copyProofArtifacts: (input) => this.copyCandidateProofArtifacts(input),
52
+ createOrObserveCommit: (input) => this.createOrObserveCandidateCommit(input),
53
+ releasePin: (input) => this.releaseCandidatePin(input.binding, input.expectedPinnedCommitSha),
54
+ };
55
+ }
56
+ async reconcileCandidateOrphans(input) {
57
+ return this.candidateOperation('candidate-io-failed', async () => {
58
+ const root = (await this.git(['-C', input.repositoryRoot, 'rev-parse', '--show-toplevel'])).trim();
59
+ const activeRefs = new Set(input.activeCandidateRefs);
60
+ const adoptableRefs = new Set();
61
+ for (const pending of input.pendingCandidates) {
62
+ const first = await this.captureCandidateTree(pending.worktreePath, pending.expectedHeadSha, pending.artifactDir);
63
+ const second = await this.captureCandidateTree(pending.worktreePath, pending.expectedHeadSha, pending.artifactDir);
64
+ if (canonicalJson(first) !== canonicalJson(second))
65
+ continue;
66
+ const bindingId = candidateBindingId({
67
+ bindingSeed: candidateBindingSeed(pending.runId, pending.boundary),
68
+ expectedHeadSha: pending.expectedHeadSha,
69
+ candidateTreeSha: first.treeSha,
70
+ canonicalChangedFiles: first.changedFiles,
71
+ sourceWorktreeIdentity: first.worktreeIdentity,
72
+ });
73
+ const ref = candidateRef(pending.runId, bindingId);
74
+ const commit = await this.resolveOptionalRef(root, ref);
75
+ if (commit && await this.commitMatches(root, commit, pending.expectedHeadSha, first.treeSha))
76
+ adoptableRefs.add(ref);
77
+ }
78
+ const refs = (await this.git(['-C', root, 'for-each-ref', '--format=%(refname) %(objectname)', 'refs/codex-orchestrator/candidates/']))
79
+ .split('\n').filter(Boolean);
80
+ for (const row of refs) {
81
+ const separator = row.indexOf(' ');
82
+ if (separator < 1)
83
+ throw new Error('orphan candidate ref row is malformed');
84
+ const ref = row.slice(0, separator);
85
+ const sha = row.slice(separator + 1);
86
+ if (!activeRefs.has(ref) && !adoptableRefs.has(ref)) {
87
+ const deleted = await this.executor('git', ['-C', root, 'update-ref', '-d', ref, sha]);
88
+ if (deleted.exitCode !== 0)
89
+ throw new Error('orphan candidate ref cleanup failed');
90
+ }
91
+ }
92
+ const materializationRoot = canonicalFilesystemPath(resolve(input.workspaceRoot, '.candidate-materializations'));
93
+ const activeMaterializations = new Map(input.activeMaterializations.map((materialization) => [
94
+ canonicalFilesystemPath(materialization.path), materialization.candidateCommitSha,
95
+ ]));
96
+ for (const worktree of await this.worktrees.listWorktrees(root)) {
97
+ const path = canonicalFilesystemPath(worktree.path);
98
+ const relativePath = relative(materializationRoot, path);
99
+ if (relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))) {
100
+ const expectedCommit = activeMaterializations.get(path);
101
+ if (expectedCommit) {
102
+ if (worktree.branch !== undefined || await this.getHead(path).catch(() => '') !== expectedCommit) {
103
+ throw new Error('persisted candidate materialization diverged');
104
+ }
105
+ }
106
+ else {
107
+ if (worktree.branch !== undefined)
108
+ throw new Error('orphan candidate materialization unexpectedly owns a branch');
109
+ await this.git(['-C', root, 'worktree', 'remove', '--force', path]);
110
+ }
111
+ }
112
+ }
113
+ });
114
+ }
115
+ async captureAndPinCandidate(input) {
116
+ try {
117
+ const first = await this.captureCandidateTree(input.worktreePath, input.expectedHeadSha, input.artifactDir);
118
+ const second = await this.captureCandidateTree(input.worktreePath, input.expectedHeadSha, input.artifactDir);
119
+ if (canonicalJson(first) !== canonicalJson(second)) {
120
+ return { kind: 'failed', code: 'candidate-unstable', detailSha256: sha256(canonicalJson({ first, second })) };
121
+ }
122
+ const bindingSeed = candidateBindingSeed(input.runId, input.boundary);
123
+ const bindingId = candidateBindingId({
124
+ bindingSeed,
125
+ expectedHeadSha: input.expectedHeadSha,
126
+ candidateTreeSha: first.treeSha,
127
+ canonicalChangedFiles: first.changedFiles,
128
+ sourceWorktreeIdentity: first.worktreeIdentity,
129
+ });
130
+ const ref = candidateRef(input.runId, bindingId);
131
+ const root = (await this.git(['-C', input.worktreePath, 'rev-parse', '--show-toplevel'])).trim();
132
+ this.candidateRepositories.set(ref, root);
133
+ let commitSha = await this.resolveOptionalRef(root, ref);
134
+ if (commitSha) {
135
+ if (!await this.commitMatches(root, commitSha, input.expectedHeadSha, first.treeSha))
136
+ throw new Error('existing candidate ref diverged');
137
+ }
138
+ else {
139
+ commitSha = (await this.git([
140
+ '-C', root, '-c', 'user.name=codex-orchestrator', '-c', 'user.email=codex-orchestrator@users.noreply.github.com',
141
+ 'commit-tree', first.treeSha, '-p', input.expectedHeadSha, '-m', `codex-orchestrator candidate ${bindingId}`,
142
+ ])).trim();
143
+ const update = await this.executor('git', ['-C', root, 'update-ref', ref, commitSha, '0'.repeat(commitSha.length)]);
144
+ if (update.exitCode !== 0) {
145
+ const observed = await this.resolveOptionalRef(root, ref);
146
+ if (!observed || !await this.commitMatches(root, observed, input.expectedHeadSha, first.treeSha)) {
147
+ return { kind: 'failed', code: 'candidate-ref-update-unknown', detailSha256: sha256(update.stderr) };
148
+ }
149
+ commitSha = observed;
150
+ }
151
+ }
152
+ return { kind: 'ok', value: {
153
+ version: 2, bindingId, expectedHeadSha: input.expectedHeadSha, candidateRef: ref,
154
+ candidateCommitSha: commitSha, candidateTreeSha: first.treeSha,
155
+ canonicalChangedFiles: first.changedFiles, sourceWorktreeIdentity: first.worktreeIdentity,
156
+ } };
157
+ }
158
+ catch (error) {
159
+ return candidateFailure('candidate-io-failed', error);
160
+ }
161
+ }
162
+ async captureCandidateTree(worktreePath, expectedHeadSha, artifactDir) {
163
+ validateRelativeRoot(artifactDir);
164
+ const [head, canonicalPath, gitDirectoryRaw] = await Promise.all([
165
+ this.getHead(worktreePath), realpath(worktreePath), this.git(['-C', worktreePath, 'rev-parse', '--absolute-git-dir']),
166
+ ]);
167
+ if (head !== expectedHeadSha)
168
+ throw new Error('candidate expected HEAD diverged');
169
+ const gitDirectory = gitDirectoryRaw.trim();
170
+ const temporaryRoot = await mkdtemp(join(gitDirectory, 'candidate-index-'));
171
+ const indexPath = join(temporaryRoot, 'index');
172
+ const options = { envOverlay: { GIT_INDEX_FILE: indexPath } };
173
+ try {
174
+ await this.git(['-C', worktreePath, 'read-tree', expectedHeadSha], options);
175
+ await this.git(['-C', worktreePath, 'add', '-u', '--'], options);
176
+ const untracked = (await this.git(['-C', worktreePath, 'ls-files', '--others', '--exclude-standard', '-z'], options)).split('\0')
177
+ .filter((path) => path && path !== artifactDir && !path.startsWith(`${artifactDir}/`)).sort();
178
+ if (untracked.length > 0) {
179
+ await this.git(['-C', worktreePath, 'add', '--pathspec-from-file=-', '--pathspec-file-nul'], {
180
+ ...options, stdin: `${untracked.join('\0')}\0`,
181
+ });
182
+ }
183
+ const treeSha = (await this.git(['-C', worktreePath, 'write-tree'], options)).trim();
184
+ const changedFiles = (await this.git(['-C', worktreePath, 'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', expectedHeadSha, treeSha]))
185
+ .split('\0').filter(Boolean).sort();
186
+ return {
187
+ expectedHeadSha,
188
+ treeSha,
189
+ changedFiles,
190
+ worktreeIdentity: sha256(canonicalJson({ canonicalPath, gitDirectory })),
191
+ };
192
+ }
193
+ finally {
194
+ await rm(temporaryRoot, { recursive: true, force: true });
195
+ }
196
+ }
197
+ async inspectCandidatePin(binding) {
198
+ return this.candidateOperation('candidate-io-failed', async () => {
199
+ const root = await this.candidateRepository(binding.candidateRef);
200
+ if (!root)
201
+ return 'missing';
202
+ const observed = await this.resolveOptionalRef(root, binding.candidateRef);
203
+ if (!observed)
204
+ return 'missing';
205
+ return observed === binding.candidateCommitSha
206
+ && await this.commitMatches(root, observed, binding.expectedHeadSha, binding.candidateTreeSha) ? 'matching' : 'diverged';
207
+ });
208
+ }
209
+ async prepareCandidateMaterialization(input) {
210
+ return this.candidateOperation('candidate-materialization-io-failed', async () => {
211
+ const root = await this.repositoryFromWorkspaceRoot(input.workspaceRoot);
212
+ this.candidateRepositories.set(input.binding.candidateRef, root);
213
+ const path = join(input.workspaceRoot, '.candidate-materializations', input.runId, input.binding.bindingId, input.materializationId);
214
+ const existing = (await this.worktrees.listWorktrees(root)).find((worktree) => sameFilesystemPath(worktree.path, path));
215
+ if (existing) {
216
+ const clean = await this.worktrees.isWorktreeClean(path);
217
+ const head = await this.getHead(path).catch(() => 'missing');
218
+ if (existing.branch !== undefined || head !== input.binding.candidateCommitSha || !clean)
219
+ return { kind: 'path-diverged', path };
220
+ }
221
+ else {
222
+ await mkdir(dirname(path), { recursive: true });
223
+ await this.git(['-C', root, 'worktree', 'add', '--detach', path, input.binding.candidateCommitSha]);
224
+ }
225
+ return { kind: 'prepared', materialization: {
226
+ version: 2, bindingId: input.binding.bindingId, candidateCommitSha: input.binding.candidateCommitSha,
227
+ path,
228
+ } };
229
+ });
230
+ }
231
+ async inspectCandidateMaterialization(input) {
232
+ return this.candidateOperation('candidate-materialization-io-failed', async () => {
233
+ try {
234
+ const stat = await lstat(input.materialization.path);
235
+ if (stat.isSymbolicLink() || !stat.isDirectory())
236
+ return 'mutated';
237
+ }
238
+ catch (error) {
239
+ if (isErrorCode(error, 'ENOENT'))
240
+ return 'missing';
241
+ throw error;
242
+ }
243
+ const [head, tree, changed] = await Promise.all([
244
+ this.getHead(input.materialization.path),
245
+ this.git(['-C', input.materialization.path, 'rev-parse', 'HEAD^{tree}']).then((value) => value.trim()),
246
+ this.worktrees.listChangedFilesIgnoringUntrackedRoot(input.materialization.path, input.artifactDir),
247
+ ]);
248
+ return head === input.binding.candidateCommitSha && tree === input.binding.candidateTreeSha && changed.length === 0 ? 'matching' : 'mutated';
249
+ });
250
+ }
251
+ async removeCandidateMaterialization(materialization) {
252
+ return this.candidateOperation('candidate-materialization-io-failed', async () => {
253
+ const root = await this.repositoryFromWorkspaceRoot(materialization.path);
254
+ const registered = (await this.worktrees.listWorktrees(root)).find((worktree) => sameFilesystemPath(worktree.path, materialization.path));
255
+ if (!registered)
256
+ return;
257
+ if (registered.branch !== undefined || await this.getHead(materialization.path) !== materialization.candidateCommitSha)
258
+ throw new Error('candidate materialization diverged');
259
+ await this.git(['-C', root, 'worktree', 'remove', '--force', materialization.path]);
260
+ });
261
+ }
262
+ async copyCandidateProofArtifacts(input) {
263
+ return this.candidateOperation('candidate-materialization-io-failed', async () => {
264
+ const proofRoot = `${input.artifactDir}/${input.proofId}`;
265
+ const repositoryRoot = await this.repositoryFromWorkspaceRoot(input.materialization.path);
266
+ const registered = (await this.worktrees.listWorktrees(repositoryRoot))
267
+ .find((worktree) => sameFilesystemPath(worktree.path, input.materialization.path));
268
+ if (!registered || registered.branch !== undefined
269
+ || await this.getHead(input.materialization.path).catch(() => '') !== input.materialization.candidateCommitSha) {
270
+ return { kind: 'artifact-conflict', relativePath: proofRoot };
271
+ }
272
+ const sourceRoot = await inspectSafeArtifactRoot(input.materialization.path);
273
+ const destinationRoot = await inspectSafeArtifactRoot(input.issueWorktreePath);
274
+ for (const artifact of input.artifacts) {
275
+ if (artifact.relativePath !== proofRoot && !artifact.relativePath.startsWith(`${proofRoot}/`))
276
+ return { kind: 'artifact-conflict', relativePath: artifact.relativePath };
277
+ try {
278
+ const source = join(input.materialization.path, artifact.relativePath);
279
+ const destination = join(input.issueWorktreePath, artifact.relativePath);
280
+ const sourceBytes = await readRegularFileWithoutSymlinkAncestors(sourceRoot, source);
281
+ if (sha256(sourceBytes) !== artifact.sha256)
282
+ return { kind: 'artifact-conflict', relativePath: artifact.relativePath };
283
+ await ensureSafeDirectoryPath(destinationRoot, dirname(destination));
284
+ await publishArtifactCreateOnly(destinationRoot, destination, sourceBytes, artifact.sha256);
285
+ }
286
+ catch (error) {
287
+ if (error instanceof CandidateArtifactConflictError)
288
+ return { kind: 'artifact-conflict', relativePath: artifact.relativePath };
289
+ throw error;
290
+ }
291
+ }
292
+ return { kind: 'copied-or-observed' };
293
+ });
294
+ }
295
+ async createOrObserveCandidateCommit(input) {
296
+ return this.candidateOperation('candidate-ref-update-unknown', async () => {
297
+ const root = (await this.git(['-C', input.worktreePath, 'rev-parse', '--show-toplevel'])).trim();
298
+ const ref = `refs/heads/${input.branchName}`;
299
+ const observedHead = (await this.git(['-C', root, 'rev-parse', '--verify', `${ref}^{commit}`])).trim();
300
+ if (observedHead !== input.parentSha) {
301
+ const observed = await this.inspectCommit(root, observedHead);
302
+ if (observed.parentSha === input.parentSha && observed.treeSha === input.treeSha && observed.message === input.message) {
303
+ return { kind: 'created-or-observed', sha: observedHead, ...observed };
304
+ }
305
+ return { kind: 'branch-diverged', observedHeadSha: observedHead };
306
+ }
307
+ if (input.observeOnly)
308
+ return { kind: 'parent-unchanged' };
309
+ const pin = await this.resolveOptionalRef(root, input.candidateRef);
310
+ if (!pin || (await this.git(['-C', root, 'rev-parse', `${pin}^{tree}`])).trim() !== input.treeSha)
311
+ throw new Error('candidate pin does not authorize publication tree');
312
+ const sha = (await this.git([
313
+ '-C', root, '-c', 'user.name=codex-orchestrator', '-c', 'user.email=codex-orchestrator@users.noreply.github.com',
314
+ 'commit-tree', input.treeSha, '-p', input.parentSha, '-m', input.message,
315
+ ])).trim();
316
+ const update = await this.executor('git', ['-C', root, 'update-ref', ref, sha, input.parentSha]);
317
+ if (update.exitCode !== 0) {
318
+ const after = (await this.git(['-C', root, 'rev-parse', '--verify', `${ref}^{commit}`])).trim();
319
+ if (after === input.parentSha)
320
+ return { kind: 'parent-unchanged' };
321
+ const observed = await this.inspectCommit(root, after);
322
+ if (observed.parentSha === input.parentSha && observed.treeSha === input.treeSha && observed.message === input.message) {
323
+ return { kind: 'created-or-observed', sha: after, ...observed };
324
+ }
325
+ return { kind: 'branch-diverged', observedHeadSha: after };
326
+ }
327
+ return { kind: 'created-or-observed', sha, parentSha: input.parentSha, treeSha: input.treeSha, message: input.message };
328
+ });
329
+ }
330
+ async releaseCandidatePin(binding, expected) {
331
+ return this.candidateOperation('candidate-ref-update-unknown', async () => {
332
+ const root = await this.candidateRepository(binding.candidateRef);
333
+ if (!root)
334
+ return;
335
+ const observed = await this.resolveOptionalRef(root, binding.candidateRef);
336
+ if (!observed)
337
+ return;
338
+ if (observed !== expected)
339
+ throw new Error('candidate pin diverged');
340
+ const result = await this.executor('git', ['-C', root, 'update-ref', '-d', binding.candidateRef, expected]);
341
+ if (result.exitCode !== 0 && await this.resolveOptionalRef(root, binding.candidateRef))
342
+ throw new Error('candidate pin deletion outcome is unknown');
343
+ });
344
+ }
345
+ async candidateOperation(code, operation) {
346
+ try {
347
+ return { kind: 'ok', value: await operation() };
348
+ }
349
+ catch (error) {
350
+ return candidateFailure(code, error);
351
+ }
352
+ }
353
+ async candidateRepository(ref) {
354
+ const known = this.candidateRepositories.get(ref);
355
+ if (known)
356
+ return known;
357
+ for (const candidateRoot of [this.repositoryRoot, process.cwd()]) {
358
+ if (!candidateRoot)
359
+ continue;
360
+ try {
361
+ const root = (await this.git(['-C', candidateRoot, 'rev-parse', '--show-toplevel'])).trim();
362
+ if (await this.resolveOptionalRef(root, ref)) {
363
+ this.candidateRepositories.set(ref, root);
364
+ return root;
365
+ }
366
+ }
367
+ catch { /* this location need not be in the target repository */ }
368
+ }
369
+ return undefined;
370
+ }
371
+ async repositoryFromWorkspaceRoot(path) {
372
+ let current = resolve(path);
373
+ while (true) {
374
+ try {
375
+ return (await this.git(['-C', current, 'rev-parse', '--show-toplevel'])).trim();
376
+ }
377
+ catch {
378
+ const parent = dirname(current);
379
+ if (parent === current)
380
+ throw new Error('candidate workspace is not below a Git repository');
381
+ current = parent;
382
+ }
383
+ }
384
+ }
385
+ async resolveOptionalRef(root, ref) {
386
+ const result = await this.executor('git', ['-C', root, 'rev-parse', '--verify', `${ref}^{commit}`]);
387
+ if (result.exitCode === 1 || result.exitCode === 128)
388
+ return undefined;
389
+ if (result.exitCode !== 0)
390
+ throw new Error(`git ref inspection failed: ${result.stderr}`);
391
+ return result.stdout.trim();
392
+ }
393
+ async commitMatches(root, commit, parent, tree) {
394
+ const observed = await this.inspectCommit(root, commit);
395
+ return observed.parentSha === parent && observed.treeSha === tree;
396
+ }
397
+ async inspectCommit(root, commit) {
398
+ const output = await this.git(['-C', root, 'show', '-s', '--format=%P%n%T%n%B', commit]);
399
+ const [parentSha, treeSha, ...message] = output.split('\n');
400
+ if (!parentSha || parentSha.includes(' ') || !treeSha)
401
+ throw new Error('candidate commit is not single-parent');
402
+ return { parentSha, treeSha, message: message.join('\n').trimEnd() };
40
403
  }
41
404
  async getBaseSha(input) {
42
- return (await this.git(['-C', input.targetRoot, 'rev-parse', input.baseBranch])).trim();
405
+ await this.git([
406
+ '-C', input.targetRoot, 'fetch', '--quiet', '--no-tags', 'origin', `refs/heads/${input.baseBranch}`,
407
+ ]);
408
+ return (await this.git(['-C', input.targetRoot, 'rev-parse', '--verify', 'FETCH_HEAD^{commit}'])).trim();
43
409
  }
44
410
  async createWorktree(input) {
45
411
  await this.worktrees.createIssueWorktree({
@@ -49,6 +415,9 @@ export class LocalGitRunIssueAdapter {
49
415
  baseBranch: input.baseBranch,
50
416
  requiredBaseSha: input.baseSha,
51
417
  });
418
+ if (await this.getHead(input.worktreePath) !== input.baseSha) {
419
+ throw new Error('created issue worktree does not match the pinned base SHA');
420
+ }
52
421
  }
53
422
  async ensureContinuationWorktree(input) {
54
423
  const localHead = (await this.git([
@@ -137,6 +506,13 @@ export class LocalGitRunIssueAdapter {
137
506
  listChangedFiles(worktreePath) {
138
507
  return this.worktrees.listChangedFiles(worktreePath);
139
508
  }
509
+ async diffTrees(worktreePath, previousTreeSha, candidateTreeSha) {
510
+ const [names, patch] = await Promise.all([
511
+ this.git(['-C', worktreePath, 'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', previousTreeSha, candidateTreeSha], { maxOutputBytes: 1024 * 1024 }),
512
+ this.git(['-C', worktreePath, 'diff', '--binary', '--full-index', '--no-ext-diff', previousTreeSha, candidateTreeSha, '--'], { maxOutputBytes: 1024 * 1024 }),
513
+ ]);
514
+ return { changedFiles: names.split('\0').filter(Boolean).sort(), patch };
515
+ }
140
516
  listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot) {
141
517
  return this.worktrees.listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot);
142
518
  }
@@ -197,13 +573,204 @@ export class LocalGitRunIssueAdapter {
197
573
  push(input) {
198
574
  return this.worktrees.pushBranch(input);
199
575
  }
200
- async git(args) {
201
- const result = await this.executor('git', args);
576
+ async git(args, options) {
577
+ const result = await this.executor('git', args, options);
202
578
  if (result.exitCode !== 0)
203
579
  throw new Error(`git failed: ${result.stderr}`);
204
580
  return result.stdout;
205
581
  }
206
582
  }
583
+ function candidateFailure(code, error) {
584
+ const detail = error instanceof Error ? `${error.name}:${error.message}` : String(error);
585
+ return { kind: 'failed', code, detailSha256: sha256(detail) };
586
+ }
587
+ function processGroupIsAlive(processGroupId) {
588
+ try {
589
+ process.kill(-processGroupId, 0);
590
+ return true;
591
+ }
592
+ catch (error) {
593
+ return !isErrorCode(error, 'ESRCH');
594
+ }
595
+ }
596
+ function sameFilesystemPath(left, right) {
597
+ return canonicalFilesystemPath(left) === canonicalFilesystemPath(right);
598
+ }
599
+ function canonicalFilesystemPath(value) {
600
+ const resolved = resolve(value);
601
+ return resolved.startsWith('/private/') ? resolved.slice('/private'.length) : resolved;
602
+ }
603
+ async function inspectSafeArtifactRoot(root) {
604
+ const path = resolve(root);
605
+ const stat = await lstat(path);
606
+ if (stat.isSymbolicLink() || !stat.isDirectory())
607
+ throw new CandidateArtifactConflictError('candidate artifact root is unsafe');
608
+ const realPath = canonicalFilesystemPath(await realpath(path));
609
+ if (realPath !== canonicalFilesystemPath(path))
610
+ throw new CandidateArtifactConflictError('candidate artifact root has a symlinked ancestor');
611
+ const identity = { path, realPath, dev: stat.dev, ino: stat.ino };
612
+ await assertSafeArtifactRoot(identity);
613
+ return identity;
614
+ }
615
+ async function assertSafeArtifactRoot(root) {
616
+ const stat = await lstat(root.path);
617
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== root.dev || stat.ino !== root.ino
618
+ || canonicalFilesystemPath(await realpath(root.path)) !== root.realPath) {
619
+ throw new CandidateArtifactConflictError('candidate artifact root changed');
620
+ }
621
+ }
622
+ async function readRegularFileWithoutSymlinkAncestors(rootInput, path) {
623
+ const root = typeof rootInput === 'string' ? await inspectSafeArtifactRoot(rootInput) : rootInput;
624
+ await assertSafeArtifactRoot(root);
625
+ const relativePath = relative(root.path, resolve(path));
626
+ if (!relativePath || relativePath.startsWith('..') || isAbsolute(relativePath))
627
+ throw new CandidateArtifactConflictError('candidate artifact path escapes its root');
628
+ let current = root.path;
629
+ const rootStat = await lstat(current);
630
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
631
+ throw new CandidateArtifactConflictError('candidate artifact root is unsafe');
632
+ const ancestorIdentity = [{ path: current, dev: rootStat.dev, ino: rootStat.ino }];
633
+ const segments = relativePath.split('/');
634
+ for (const segment of segments.slice(0, -1)) {
635
+ current = join(current, segment);
636
+ const stat = await lstat(current);
637
+ if (stat.isSymbolicLink() || !stat.isDirectory())
638
+ throw new CandidateArtifactConflictError('candidate artifact ancestor is unsafe');
639
+ ancestorIdentity.push({ path: current, dev: stat.dev, ino: stat.ino });
640
+ }
641
+ const finalBeforeOpen = await lstat(path);
642
+ if (finalBeforeOpen.isSymbolicLink() || !finalBeforeOpen.isFile()) {
643
+ throw new CandidateArtifactConflictError('candidate artifact is not a regular file');
644
+ }
645
+ let handle;
646
+ try {
647
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
648
+ }
649
+ catch (error) {
650
+ if (isErrorCode(error, 'ELOOP'))
651
+ throw new CandidateArtifactConflictError('candidate artifact is not a regular file');
652
+ throw error;
653
+ }
654
+ try {
655
+ const stat = await handle.stat();
656
+ if (!stat.isFile())
657
+ throw new CandidateArtifactConflictError('candidate artifact is not a regular file');
658
+ const openedPath = await realpath(path);
659
+ const openedRelative = relative(root.realPath, canonicalFilesystemPath(openedPath));
660
+ if (!openedRelative || openedRelative.startsWith('..') || isAbsolute(openedRelative)) {
661
+ throw new CandidateArtifactConflictError('candidate artifact path escaped its root during read');
662
+ }
663
+ const finalStat = await lstat(path);
664
+ if (!finalStat.isFile() || finalStat.isSymbolicLink() || finalStat.dev !== stat.dev || finalStat.ino !== stat.ino) {
665
+ throw new CandidateArtifactConflictError('candidate artifact changed during read');
666
+ }
667
+ for (const identity of ancestorIdentity) {
668
+ const observed = await lstat(identity.path);
669
+ if (!observed.isDirectory() || observed.isSymbolicLink() || observed.dev !== identity.dev || observed.ino !== identity.ino) {
670
+ throw new CandidateArtifactConflictError('candidate artifact ancestor changed during read');
671
+ }
672
+ }
673
+ await assertSafeArtifactRoot(root);
674
+ return await handle.readFile();
675
+ }
676
+ finally {
677
+ await handle.close();
678
+ }
679
+ }
680
+ async function ensureSafeDirectoryPath(root, directory) {
681
+ await assertSafeArtifactRoot(root);
682
+ const relativeDirectory = relative(root.path, resolve(directory));
683
+ if (relativeDirectory.startsWith('..') || isAbsolute(relativeDirectory))
684
+ throw new CandidateArtifactConflictError('candidate artifact directory escapes its root');
685
+ let current = root.path;
686
+ for (const segment of relativeDirectory.split('/').filter(Boolean)) {
687
+ current = join(current, segment);
688
+ try {
689
+ await mkdir(current);
690
+ }
691
+ catch (error) {
692
+ if (!isErrorCode(error, 'EEXIST'))
693
+ throw error;
694
+ }
695
+ const stat = await lstat(current);
696
+ if (stat.isSymbolicLink() || !stat.isDirectory())
697
+ throw new CandidateArtifactConflictError('candidate artifact directory is unsafe');
698
+ }
699
+ const parentRealPath = canonicalFilesystemPath(await realpath(directory));
700
+ const parentRelative = relative(root.realPath, parentRealPath);
701
+ if (parentRelative.startsWith('..') || isAbsolute(parentRelative))
702
+ throw new CandidateArtifactConflictError('candidate artifact directory escaped its root');
703
+ await assertSafeArtifactRoot(root);
704
+ }
705
+ class CandidateArtifactConflictError extends Error {
706
+ }
707
+ async function publishArtifactCreateOnly(root, destination, bytes, expectedSha256) {
708
+ await assertSafeArtifactRoot(root);
709
+ const temporary = join(dirname(destination), `.${basename(destination)}.${expectedSha256}.tmp`);
710
+ let handle;
711
+ try {
712
+ handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
713
+ }
714
+ catch (error) {
715
+ if (!isErrorCode(error, 'EEXIST'))
716
+ throw error;
717
+ const stale = await readRegularFileWithoutSymlinkAncestors(root, temporary);
718
+ if (sha256(stale) !== expectedSha256) {
719
+ await unlink(temporary);
720
+ handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
721
+ }
722
+ }
723
+ if (handle) {
724
+ try {
725
+ await handle.writeFile(bytes);
726
+ await handle.sync();
727
+ const stat = await handle.stat();
728
+ const observed = await lstat(temporary);
729
+ const temporaryRealPath = canonicalFilesystemPath(await realpath(temporary));
730
+ const temporaryRelative = relative(root.realPath, temporaryRealPath);
731
+ if (!observed.isFile() || observed.isSymbolicLink() || observed.dev !== stat.dev || observed.ino !== stat.ino
732
+ || temporaryRelative.startsWith('..') || isAbsolute(temporaryRelative)) {
733
+ throw new CandidateArtifactConflictError('candidate artifact temporary path changed');
734
+ }
735
+ await assertSafeArtifactRoot(root);
736
+ }
737
+ finally {
738
+ await handle.close();
739
+ }
740
+ }
741
+ try {
742
+ await link(temporary, destination);
743
+ }
744
+ catch (error) {
745
+ if (!isErrorCode(error, 'EEXIST'))
746
+ throw error;
747
+ const existing = await readRegularFileWithoutSymlinkAncestors(root, destination);
748
+ if (sha256(existing) !== expectedSha256)
749
+ throw new CandidateArtifactConflictError('candidate artifact destination conflicts');
750
+ }
751
+ const published = await readRegularFileWithoutSymlinkAncestors(root, destination);
752
+ if (sha256(published) !== expectedSha256)
753
+ throw new CandidateArtifactConflictError('candidate artifact destination conflicts');
754
+ const parent = await open(dirname(destination), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
755
+ try {
756
+ const parentStat = await parent.stat();
757
+ const observedParent = await lstat(dirname(destination));
758
+ const parentRealPath = canonicalFilesystemPath(await realpath(dirname(destination)));
759
+ const parentRelative = relative(root.realPath, parentRealPath);
760
+ if (!observedParent.isDirectory() || observedParent.isSymbolicLink()
761
+ || observedParent.dev !== parentStat.dev || observedParent.ino !== parentStat.ino
762
+ || parentRelative.startsWith('..') || isAbsolute(parentRelative)) {
763
+ throw new CandidateArtifactConflictError('candidate artifact destination directory changed');
764
+ }
765
+ await parent.sync();
766
+ await assertSafeArtifactRoot(root);
767
+ }
768
+ finally {
769
+ await parent.close();
770
+ }
771
+ await unlink(temporary).catch((error) => { if (!isErrorCode(error, 'ENOENT'))
772
+ throw error; });
773
+ }
207
774
  export class ContainedImplementationAgent {
208
775
  dependencies;
209
776
  constructor(dependencies) {
@@ -212,7 +779,7 @@ export class ContainedImplementationAgent {
212
779
  async run(input) {
213
780
  const config = this.dependencies.config();
214
781
  const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
215
- const attemptId = (this.dependencies.createAttemptId ?? randomUUID)();
782
+ const attemptId = input.attemptId;
216
783
  const attempt = await prepareContainedAttempt({
217
784
  orchestratorHome: this.dependencies.orchestratorHome,
218
785
  canonicalRepository,
@@ -230,6 +797,14 @@ export class ContainedImplementationAgent {
230
797
  baseline,
231
798
  });
232
799
  try {
800
+ try {
801
+ const recovered = await readRegularFile(attempt.reportPath);
802
+ return { kind: 'completed', attemptId, report: decodeAgentReportForValidation(recovered) };
803
+ }
804
+ catch (error) {
805
+ if (!isErrorCode(error, 'ENOENT'))
806
+ return { kind: 'internal-error' };
807
+ }
233
808
  const result = await (this.dependencies.process ?? new CodexProcess()).run({
234
809
  codexPath: config.codex.command,
235
810
  cwd: input.worktreePath,
@@ -244,19 +819,12 @@ export class ContainedImplementationAgent {
244
819
  `Package profile instructions: ${attempt.profile.developerInstructions}`,
245
820
  `Follow the exact operation at ${attempt.operationPath}.`,
246
821
  `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
247
- ...(input.operation === 'qualification-repair'
248
- ? [
249
- `Pre-implementation qualification repair for issue #${input.issue.number}: ${input.issue.title}`,
250
- 'Do not implement the issue acceptance criteria yet. Repair only the reported scoped-check failures so the existing worktree qualifies for implementation.',
251
- 'You may modify the product files required by those failures. Return changedFiles as the complete cumulative worktree change set.',
252
- ]
253
- : [`Implement issue #${input.issue.number}: ${input.issue.title}`]),
822
+ `Implement issue #${input.issue.number}: ${input.issue.title}`,
254
823
  `Implementation cycle: ${input.cycle}.`,
255
824
  ...(input.reviewFeedbackRound ? [`Pull-request feedback repair round: ${input.reviewFeedbackRound}.`] : []),
256
825
  ...(input.reviewFeedback?.length ? [`Frozen trusted pull-request feedback: ${canonicalJson(input.reviewFeedback)}`] : []),
257
- ...(input.operation === 'implementation'
258
- ? [`Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`]
259
- : []),
826
+ `Exact delivery authority: ${canonicalJson(input.deliveryAuthority)}`,
827
+ `Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
260
828
  ...(input.reworkFindings.length > 0 ? [`Repair these verified findings: ${canonicalJson(input.reworkFindings)}`] : []),
261
829
  ...(input.repairOnly ? ['Report repair only: do not modify any worktree file; emit a schema-valid implementation report for the existing change.'] : []),
262
830
  'Do not commit, push, publish, or print credentials or local auth paths.',
@@ -265,13 +833,13 @@ export class ContainedImplementationAgent {
265
833
  idleTimeoutMs: config.codex.idleTimeoutMs,
266
834
  operationPolicy: attempt.policy,
267
835
  executionProfile: attempt.profile,
268
- ...(input.onLaunched ? { onSpawned: ({ pid, processGroupId }) => input.onLaunched({
269
- attemptId, pid, processGroupId, launchedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
270
- }) } : {}),
836
+ onSpawned: async ({ pid, processGroupId }) => input.onLaunched?.({
837
+ attemptId, pid, processGroupId, launchedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
838
+ }),
271
839
  }, input.signal);
272
840
  if (result.kind === 'cancelled')
273
841
  return { kind: 'cancelled' };
274
- if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
842
+ if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout', 'launch-gate-failed'].includes(result.kind)) {
275
843
  return { kind: 'transport-failed', resumable: true };
276
844
  }
277
845
  if (result.kind !== 'completed' || result.report.kind !== 'available')
@@ -285,38 +853,9 @@ export class ContainedImplementationAgent {
285
853
  catch (error) {
286
854
  if (!(error instanceof ProcessQuiescenceError))
287
855
  return { kind: 'internal-error' };
288
- return {
289
- kind: 'safe-halt',
290
- process: {
291
- pid: error.pid,
292
- processGroupId: error.processGroupId,
293
- startedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
294
- baseline: { ...baseline },
295
- },
296
- waitForAbsence: () => waitForProcessGroupAbsent(error.processGroupId),
297
- };
856
+ return { kind: 'safe-halt' };
298
857
  }
299
858
  }
300
- async recover(input) {
301
- const expected = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(input.canonicalRepository), 'runs', input.runId, 'attempts', input.invocation.attemptId, 'report.json');
302
- if (resolve(input.invocation.reportPath) !== expected) {
303
- throw new Error('review feedback implementation report path is not attempt-owned');
304
- }
305
- let bytes;
306
- try {
307
- bytes = await readRegularFile(expected);
308
- }
309
- catch (error) {
310
- if (isErrorCode(error, 'ENOENT'))
311
- return undefined;
312
- throw error;
313
- }
314
- return {
315
- kind: 'completed',
316
- attemptId: input.invocation.attemptId,
317
- report: decodeAgentReportForValidation(bytes),
318
- };
319
- }
320
859
  }
321
860
  export class ContainedProofAgent {
322
861
  dependencies;
@@ -328,12 +867,13 @@ export class ContainedProofAgent {
328
867
  throw new Error('proof workflow generation is required');
329
868
  const config = this.dependencies.config();
330
869
  const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
331
- const worktreePath = resolve(this.dependencies.targetRoot, config.runner.workspaceRoot, `issue-${input.issue.number}`);
870
+ const issueWorktreePath = resolve(this.dependencies.targetRoot, config.runner.workspaceRoot, `issue-${input.issue.number}`);
871
+ const worktreePath = input.worktreePath ? resolve(input.worktreePath) : issueWorktreePath;
332
872
  const attempt = await prepareContainedAttempt({
333
873
  orchestratorHome: this.dependencies.orchestratorHome,
334
874
  canonicalRepository,
335
875
  runId: input.runId,
336
- attemptId: (this.dependencies.createAttemptId ?? randomUUID)(),
876
+ attemptId: input.attemptId ?? (this.dependencies.createAttemptId ?? randomUUID)(),
337
877
  operationId: 'acceptance-proof',
338
878
  workflowGeneration: input.workflowGeneration,
339
879
  bootId: this.dependencies.bootId,
@@ -345,6 +885,22 @@ export class ContainedProofAgent {
345
885
  const iosTooling = await discoverIosTooling(this.dependencies.processExecutor, this.dependencies.iosXcrunPath);
346
886
  const before = await artifactInventory(artifactRoot, config.proof.artifactDir);
347
887
  try {
888
+ try {
889
+ const recoveredReport = await readRegularFile(attempt.reportPath);
890
+ const recoveredInventory = await artifactInventory(artifactRoot, config.proof.artifactDir);
891
+ const runnerPrepared = new Set(input.runnerPreparedArtifactPaths);
892
+ return {
893
+ kind: 'report',
894
+ report: decodeAgentReportForValidation(recoveredReport, ['visualEvidence', 'blocker']),
895
+ proofPhaseChangedFiles: [...recoveredInventory.keys()].filter((path) => !runnerPrepared.has(path)).sort(),
896
+ };
897
+ }
898
+ catch (error) {
899
+ if (!isErrorCode(error, 'ENOENT'))
900
+ throw error;
901
+ }
902
+ if (input.recoverOnly)
903
+ return { kind: 'internal-error' };
348
904
  const result = await (this.dependencies.process ?? new CodexProcess()).run({
349
905
  codexPath: config.codex.command,
350
906
  cwd: worktreePath,
@@ -393,10 +949,13 @@ export class ContainedProofAgent {
393
949
  idleTimeoutMs: config.codex.idleTimeoutMs,
394
950
  operationPolicy: attempt.policy,
395
951
  executionProfile: attempt.profile,
952
+ onSpawned: async ({ pid, processGroupId }) => input.onLaunched?.({
953
+ pid, processGroupId, launchedAt: new Date().toISOString(),
954
+ }),
396
955
  }, input.signal);
397
956
  if (result.kind === 'cancelled')
398
957
  return { kind: 'cancelled' };
399
- if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
958
+ if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout', 'launch-gate-failed'].includes(result.kind)) {
400
959
  return { kind: 'transport-failed', resumable: true };
401
960
  }
402
961
  if (result.kind !== 'completed' || result.report.kind !== 'available')
@@ -409,13 +968,48 @@ export class ContainedProofAgent {
409
968
  };
410
969
  }
411
970
  catch (error) {
412
- if (error instanceof ProcessQuiescenceError) {
413
- throw new ProofQuiescenceError(error.pid, error.processGroupId, () => waitForProcessGroupAbsent(error.processGroupId));
414
- }
415
- return { kind: 'internal-error' };
971
+ return error instanceof ProcessQuiescenceError ? { kind: 'safe-halt' } : { kind: 'internal-error' };
416
972
  }
417
973
  }
418
974
  }
975
+ export async function observeAttemptReadViewCleanup(input) {
976
+ const runtimeRoot = join(resolve(input.orchestratorHome), 'v2', sha256(input.canonicalRepository));
977
+ const attemptRoot = join(runtimeRoot, 'runs', input.identity.runId, 'attempts', input.identity.attemptId);
978
+ const expectedResultPath = join(attemptRoot, 'report.json');
979
+ if (input.identity.resultPath !== expectedResultPath)
980
+ return 'pending';
981
+ let canonicalRuntimeRoot;
982
+ let canonicalAttemptRoot;
983
+ try {
984
+ canonicalRuntimeRoot = await realpath(runtimeRoot);
985
+ }
986
+ catch (error) {
987
+ return isErrorCode(error, 'ENOENT') ? 'confirmed' : 'pending';
988
+ }
989
+ try {
990
+ canonicalAttemptRoot = await realpath(attemptRoot);
991
+ }
992
+ catch (error) {
993
+ return isErrorCode(error, 'ENOENT') ? 'confirmed' : 'pending';
994
+ }
995
+ const expectedRelative = relative(runtimeRoot, attemptRoot);
996
+ if (!isPathWithin(canonicalRuntimeRoot, canonicalAttemptRoot)
997
+ || relative(canonicalRuntimeRoot, canonicalAttemptRoot) !== expectedRelative)
998
+ return 'pending';
999
+ await rm(join(attemptRoot, 'read-view'), { recursive: true, force: true });
1000
+ return 'confirmed';
1001
+ }
1002
+ export function selectedConfiguredCheckPolicySha256(selectedChecks, configuredAuthority) {
1003
+ const selected = {};
1004
+ for (const check of selectedChecks) {
1005
+ if (Object.prototype.hasOwnProperty.call(selected, check.id)
1006
+ || configuredAuthority[check.id] !== check.command) {
1007
+ throw new CandidateProofInspectionError('selected check no longer matches current configured authority');
1008
+ }
1009
+ selected[check.id] = check.command;
1010
+ }
1011
+ return sha256(canonicalJson(selected));
1012
+ }
419
1013
  export async function resolveCodexExecutable(command, safePath) {
420
1014
  if (command.length === 0 || safePath.length === 0)
421
1015
  throw new Error('Codex command and safe path must be non-empty');
@@ -453,7 +1047,7 @@ export function createV2Runtime(input) {
453
1047
  codexExecutable = { command, path };
454
1048
  return path;
455
1049
  };
456
- const git = input.git ?? new LocalGitRunIssueAdapter(commandExecutor);
1050
+ const git = input.git ?? new LocalGitRunIssueAdapter(commandExecutor, targetRoot);
457
1051
  const proofFreshnessGit = git;
458
1052
  const controller = new AbortController();
459
1053
  let currentConfig;
@@ -515,6 +1109,14 @@ export function createV2Runtime(input) {
515
1109
  || !attempt.profile || !attempt.operationPath || !attempt.workflowRoot) {
516
1110
  return { status: 'blocked', kind: 'safety', code: 'report-operation-attempt-incomplete' };
517
1111
  }
1112
+ try {
1113
+ return { status: 'completed', reportBytes: await readRegularFile(attempt.reportPath) };
1114
+ }
1115
+ catch (error) {
1116
+ if (!isErrorCode(error, 'ENOENT')) {
1117
+ return { status: 'blocked', kind: 'safety', code: 'report-operation-result-read-failed' };
1118
+ }
1119
+ }
518
1120
  let readView;
519
1121
  try {
520
1122
  readView = await materializeReportReadView({
@@ -544,6 +1146,7 @@ export function createV2Runtime(input) {
544
1146
  `Follow the exact operation at ${attempt.operationPath}.`,
545
1147
  `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
546
1148
  `Runner-provided facts: ${canonicalJson(promptFacts)}`,
1149
+ `Available reviewer roles: ${canonicalJson(attempt.reviewers ?? [])}`,
547
1150
  'This is a read-only, report-only operation. Do not edit files or external state, use network or MCP tools, or request additional authority.',
548
1151
  'Do not read .env or any .env* file. The runner has removed repository credential paths from this read view.',
549
1152
  ].join('\n'),
@@ -551,7 +1154,8 @@ export function createV2Runtime(input) {
551
1154
  idleTimeoutMs: config.codex.idleTimeoutMs,
552
1155
  operationPolicy: attempt.policy,
553
1156
  executionProfile: attempt.profile,
554
- ...(onLaunched ? { onSpawned: onLaunched } : {}),
1157
+ agentProfilePaths: attempt.reviewerProfiles,
1158
+ onSpawned: async ({ pid, processGroupId }) => onLaunched?.({ pid, processGroupId }),
555
1159
  }, signal);
556
1160
  }
557
1161
  catch (error) {
@@ -562,10 +1166,6 @@ export function createV2Runtime(input) {
562
1166
  pid: error.pid,
563
1167
  processGroupId: error.processGroupId,
564
1168
  startedAt: now(),
565
- waitForAbsence: async () => {
566
- await waitForProcessGroupAbsent(error.processGroupId);
567
- await rm(readView, { recursive: true, force: true });
568
- },
569
1169
  };
570
1170
  }
571
1171
  throw error;
@@ -577,7 +1177,7 @@ export function createV2Runtime(input) {
577
1177
  if (result.kind === 'cancelled')
578
1178
  return { status: 'cancelled' };
579
1179
  if (result.kind === 'launch-gate-failed') {
580
- return { status: 'blocked', kind: 'safety', code: 'review-operation-launch-persistence-failed' };
1180
+ return { status: 'retryable', code: 'review-operation-launch-gate-failed' };
581
1181
  }
582
1182
  if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
583
1183
  return { status: 'retryable', code: `report-operation-${result.kind}` };
@@ -590,196 +1190,7 @@ export function createV2Runtime(input) {
590
1190
  });
591
1191
  const implementationReviewer = new ContainedImplementationReviewer({
592
1192
  operation: reportOperation,
593
- createAttemptId: input.createAttemptId ?? randomUUID,
594
1193
  });
595
- const createAttemptId = input.createAttemptId ?? randomUUID;
596
- const specOperation = {
597
- author: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
598
- const attemptId = createAttemptId();
599
- const sessionId = state.authorSessionId ?? randomUUID();
600
- let attempt;
601
- try {
602
- attempt = await prepareContainedAttempt({
603
- orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
604
- attemptId, operationId: 'spec-author', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
605
- });
606
- const revisionPath = join(dirname(attempt.reportPath), `revision-${state.revisions.length + 1}.md`);
607
- await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath, revisionPath });
608
- const config = requireConfig(currentConfig);
609
- const result = await containedProcess.run({
610
- codexPath: config.codex.command, cwd: dirname(attempt.reportPath), schemaPath: attempt.schemaPath,
611
- reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
612
- safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
613
- parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
614
- operationPolicy: attempt.policy, executionProfile: attempt.profile,
615
- onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
616
- prompt: [
617
- `Package profile instructions: ${attempt.profile.developerInstructions}`,
618
- `Follow the exact operation at ${attempt.operationPath}.`,
619
- `The immutable workflow root is ${attempt.workflowRoot}.`,
620
- `Author mode: ${mode}. Issue authority: ${canonicalJson(context.issue)}.`,
621
- `Frozen criteria: ${canonicalJson(context.frozenCriteria)}.`,
622
- `Prior revisions and review state: ${canonicalJson({ revisions: state.revisions, review: state.review })}.`,
623
- `Write the complete new immutable revision only to ${revisionPath}. Return that exact absolute path and its SHA-256 in the report.`,
624
- 'Do not modify the product worktree, prior revisions, external state, or any .env file.',
625
- ].join('\n'),
626
- }, signal);
627
- if (result.kind === 'cancelled')
628
- return { status: 'cancelled' };
629
- if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
630
- return { status: 'retryable', code: `spec-author-${result.kind}` };
631
- if (result.kind !== 'completed' || result.report.kind !== 'available')
632
- return { status: 'retryable', code: 'spec-author-report-invalid' };
633
- const report = decodeAgentReportForValidation(result.report.bytes);
634
- if (report.status !== 'ready' || report.specPath !== revisionPath || report.specSha256 === null)
635
- return { status: 'retryable', code: 'spec-author-report-invalid' };
636
- const content = await readRegularFile(revisionPath);
637
- if (report.specSha256 !== sha256(content))
638
- return { status: 'retryable', code: 'spec-author-report-invalid' };
639
- const previous = state.revisions.at(-1) ?? null;
640
- return { status: 'completed', value: createSpecRevision({
641
- revision: state.revisions.length + 1, path: revisionPath, content: content.toString('utf8'),
642
- evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
643
- author: { attemptId, sessionId }, previousRevision: previous,
644
- }) };
645
- }
646
- catch (error) {
647
- if (error instanceof ProcessQuiescenceError) {
648
- try {
649
- await waitForProcessGroupAbsent(error.processGroupId);
650
- return { status: 'retryable', code: 'spec-author-process-quiescence' };
651
- }
652
- catch {
653
- return { status: 'blocked', kind: 'safety', code: 'spec-author-process-absence-unconfirmed' };
654
- }
655
- }
656
- return { status: 'retryable', code: 'spec-author-report-invalid' };
657
- }
658
- },
659
- review: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
660
- const attemptId = createAttemptId();
661
- const sessionId = state.review.reviewer?.sessionId ?? randomUUID();
662
- try {
663
- const attempt = await prepareContainedAttempt({
664
- orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
665
- attemptId, operationId: 'spec-review', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
666
- });
667
- await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath });
668
- const config = requireConfig(currentConfig);
669
- const result = await containedProcess.run({
670
- codexPath: config.codex.command, cwd: context.worktreePath, schemaPath: attempt.schemaPath,
671
- reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
672
- safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
673
- parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
674
- operationPolicy: attempt.policy, executionProfile: attempt.profile,
675
- onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
676
- prompt: [
677
- `Package profile instructions: ${attempt.profile.developerInstructions}`,
678
- `Follow the exact operation at ${attempt.operationPath}.`,
679
- `Reviewer session ID: ${sessionId}. Review mode: ${mode}.`,
680
- `Issue authority and frozen criteria: ${canonicalJson({ issue: context.issue, frozenCriteria: context.frozenCriteria })}.`,
681
- `Immutable spec delivery state: ${canonicalJson(state)}.`,
682
- 'Return only the package spec-review report. Do not edit files or external state.',
683
- ].join('\n'),
684
- }, signal);
685
- if (result.kind === 'cancelled')
686
- return { status: 'cancelled' };
687
- if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
688
- return { status: 'retryable', code: `spec-review-${result.kind}` };
689
- if (result.kind !== 'completed' || result.report.kind !== 'available')
690
- return { status: 'retryable', code: 'spec-review-report-invalid' };
691
- const raw = decodeAgentReportForValidation(result.report.bytes);
692
- if (raw.mode !== mode || raw.reviewerSessionId !== sessionId || !Array.isArray(raw.coverage) || !Array.isArray(raw.defects)
693
- || !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts) || !Array.isArray(raw.acceptedRisks)
694
- || typeof raw.coverageInvalidated !== 'boolean'
695
- || !['approved', 'needs-work', 'rejected'].includes(raw.verdict))
696
- return { status: 'retryable', code: 'spec-review-report-invalid' };
697
- const target = state.revisions.at(-1);
698
- const defects = validateCodeReviewDefects(raw.defects, target.revision);
699
- const report = {
700
- version: 1, targetRevision: target.revision, targetSha256: target.revisionSha256, mode,
701
- verdict: raw.verdict, reviewer: { attemptId, sessionId },
702
- coverage: raw.coverage, defects,
703
- affectedDefectIds: raw.affectedDefectIds,
704
- affectedContracts: raw.affectedContracts,
705
- closureRequestSha256: mode === 'closure' ? state.review.closureRequestSha256 : null,
706
- acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
707
- };
708
- return { status: 'completed', value: report, reportSha256: sha256(result.report.bytes) };
709
- }
710
- catch (error) {
711
- if (error instanceof ProcessQuiescenceError) {
712
- try {
713
- await waitForProcessGroupAbsent(error.processGroupId);
714
- return { status: 'retryable', code: 'spec-review-process-quiescence' };
715
- }
716
- catch {
717
- return { status: 'blocked', kind: 'safety', code: 'spec-review-process-absence-unconfirmed' };
718
- }
719
- }
720
- return { status: 'retryable', code: 'spec-review-report-invalid' };
721
- }
722
- },
723
- recover: async ({ context, state, signal }) => {
724
- const invocation = state.invocation;
725
- if (!invocation || signal.aborted)
726
- return signal.aborted ? { status: 'cancelled' } : { status: 'blocked', kind: 'safety', code: 'spec-recovery-invocation-missing' };
727
- if (invocation.status === 'launched') {
728
- try {
729
- await waitForProcessGroupAbsent(invocation.processGroupId);
730
- }
731
- catch {
732
- return { status: 'blocked', kind: 'safety', code: 'spec-process-absence-unconfirmed' };
733
- }
734
- }
735
- if (!invocation.reportPath)
736
- return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
737
- let reportBytes;
738
- try {
739
- reportBytes = await readRegularFile(invocation.reportPath);
740
- }
741
- catch {
742
- return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
743
- }
744
- if (invocation.purpose === 'author') {
745
- if (!invocation.revisionPath)
746
- return { status: 'retryable', code: 'spec-author-report-invalid' };
747
- try {
748
- const raw = decodeAgentReportForValidation(reportBytes);
749
- const content = await readRegularFile(invocation.revisionPath);
750
- if (raw.status !== 'ready' || raw.specPath !== invocation.revisionPath || raw.specSha256 !== sha256(content))
751
- return { status: 'retryable', code: 'spec-author-report-invalid' };
752
- return { status: 'completed', value: createSpecRevision({
753
- revision: state.revisions.length + 1, path: invocation.revisionPath, content: content.toString('utf8'),
754
- evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
755
- author: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, previousRevision: state.revisions.at(-1) ?? null,
756
- }) };
757
- }
758
- catch {
759
- return { status: 'retryable', code: 'spec-author-report-invalid' };
760
- }
761
- }
762
- try {
763
- const raw = decodeAgentReportForValidation(reportBytes);
764
- if (raw.mode !== invocation.mode || raw.reviewerSessionId !== invocation.sessionId || !Array.isArray(raw.coverage)
765
- || !Array.isArray(raw.defects) || !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts)
766
- || typeof raw.coverageInvalidated !== 'boolean' || !['approved', 'needs-work', 'rejected'].includes(raw.verdict)) {
767
- return { status: 'retryable', code: 'spec-review-report-invalid' };
768
- }
769
- const defects = validateCodeReviewDefects(raw.defects, invocation.targetRevision);
770
- return { status: 'completed', reportSha256: sha256(reportBytes), value: {
771
- version: 1, targetRevision: invocation.targetRevision, targetSha256: invocation.targetSha256,
772
- mode: invocation.mode, verdict: raw.verdict,
773
- reviewer: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, coverage: raw.coverage, defects,
774
- affectedDefectIds: raw.affectedDefectIds, affectedContracts: raw.affectedContracts,
775
- closureRequestSha256: invocation.closureRequestSha256, acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
776
- } };
777
- }
778
- catch {
779
- return { status: 'retryable', code: 'spec-review-report-invalid' };
780
- }
781
- },
782
- };
783
1194
  const readConfig = async (requestedRoot) => {
784
1195
  if (resolve(requestedRoot) !== targetRoot)
785
1196
  throw new Error('runtime target root mismatch');
@@ -796,6 +1207,11 @@ export function createV2Runtime(input) {
796
1207
  return { bytes, config };
797
1208
  };
798
1209
  const records = {
1210
+ inspect: async () => {
1211
+ if (!runRecords)
1212
+ throw new Error('run store used before config');
1213
+ return runRecords.inspect();
1214
+ },
799
1215
  read: async () => {
800
1216
  if (!runRecords)
801
1217
  throw new Error('run store used before config');
@@ -813,8 +1229,8 @@ export function createV2Runtime(input) {
813
1229
  const config = requireConfig(currentConfig);
814
1230
  const checked = capabilities.verifyAndRead(proofInput.checkedChange);
815
1231
  const repoKey = sha256(checked.payload.canonicalRepository);
816
- const proofRecords = new FileProofRecordWriter(join(orchestratorHome, 'v2', repoKey, 'proofs'));
817
- const worktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${checked.payload.issueNumber}`);
1232
+ const issueWorktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${checked.payload.issueNumber}`);
1233
+ const worktreePath = proofInput.materialization?.path ?? issueWorktreePath;
818
1234
  const androidLease = new FileAndroidLeaseVerifier({
819
1235
  leaseRoot: join(orchestratorHome, 'v2', repoKey, 'leases'),
820
1236
  worktreeRoot: worktreePath,
@@ -833,21 +1249,34 @@ export function createV2Runtime(input) {
833
1249
  });
834
1250
  const acceptanceProof = new AcceptanceProof({
835
1251
  checkedChangeReader: capabilities,
836
- proofRecords,
837
1252
  proofAgent,
838
- inspectFreshness: async (payload) => ({
839
- ...await (proofFreshnessGit.snapshotIgnoringUntrackedRoot
840
- ? proofFreshnessGit.snapshotIgnoringUntrackedRoot(worktreePath, config.proof.artifactDir)
841
- : git.snapshot(worktreePath)),
842
- checkPolicySha256: sha256(canonicalJson(resolveIssueCheckPolicy(proofInput.issue.body, config.checks).checks)),
843
- }),
1253
+ inspectFreshness: async (payload, materialization) => {
1254
+ const checkPolicySha256 = selectedConfiguredCheckPolicySha256(payload.checks, resolveIssueCheckPolicy(proofInput.issue.body, config.checks).checks);
1255
+ if (payload.version === 1)
1256
+ return {
1257
+ ...await (proofFreshnessGit.snapshotIgnoringUntrackedRoot
1258
+ ? proofFreshnessGit.snapshotIgnoringUntrackedRoot(worktreePath, config.proof.artifactDir)
1259
+ : git.snapshot(worktreePath)),
1260
+ checkPolicySha256,
1261
+ };
1262
+ if (!materialization || !git.candidateV2)
1263
+ throw new CandidateProofInspectionError('candidate-git-v2-required');
1264
+ const inspection = await git.candidateV2.inspectMaterialization({
1265
+ binding: payload.binding,
1266
+ materialization,
1267
+ artifactDir: config.proof.artifactDir,
1268
+ });
1269
+ if (inspection.kind === 'failed')
1270
+ throw new CandidateProofInspectionError(inspection.code);
1271
+ if (inspection.value !== 'matching')
1272
+ throw new CandidateProofInspectionError('candidate-materialization-mutated');
1273
+ return { bindingId: payload.binding.bindingId, candidateTreeSha: payload.binding.candidateTreeSha, checkPolicySha256 };
1274
+ },
844
1275
  readArtifact: async (relativePath) => readRegularFile(resolve(worktreePath, relativePath)),
845
1276
  inspectArtifact: async (relativePath) => inspectRegularFile(resolve(worktreePath, relativePath)),
846
1277
  androidLease,
847
1278
  iosLease,
848
1279
  proofArtifactDir: config.proof.artifactDir,
849
- createAttemptId: input.createAttemptId ?? randomUUID,
850
- now,
851
1280
  signal: controller.signal,
852
1281
  });
853
1282
  const androidRelevant = !!config.proof.android && isAndroidProofRelevant(proofInput.issue, proofInput.frozenCriteria, checked.payload.changedFiles);
@@ -855,7 +1284,7 @@ export function createV2Runtime(input) {
855
1284
  const runnerPreparedArtifactSha256 = {};
856
1285
  const runnerPreparationWarnings = [];
857
1286
  let preparationAttempted = false;
858
- return acceptanceProof.proveChange({
1287
+ const result = await acceptanceProof.proveChange({
859
1288
  ...proofInput,
860
1289
  runnerPreparedArtifactPaths,
861
1290
  runnerPreparedArtifactSha256,
@@ -885,6 +1314,21 @@ export function createV2Runtime(input) {
885
1314
  }
886
1315
  },
887
1316
  });
1317
+ if (checked.payload.version === 2 && proofInput.materialization && git.candidateV2 && result.status === 'passed') {
1318
+ const artifacts = result.receipt.publishableEvidence.map((artifact) => ({ relativePath: artifact.ref, sha256: artifact.sha256 }));
1319
+ const copied = await git.candidateV2.copyProofArtifacts({
1320
+ materialization: proofInput.materialization,
1321
+ issueWorktreePath,
1322
+ artifactDir: config.proof.artifactDir,
1323
+ proofId: proofInput.proofId,
1324
+ artifacts,
1325
+ });
1326
+ if (copied.kind === 'failed')
1327
+ throw new CandidateProofInspectionError(copied.code);
1328
+ if (copied.value.kind === 'artifact-conflict')
1329
+ throw new CandidateProofInspectionError('candidate-artifact-conflict');
1330
+ }
1331
+ return result;
888
1332
  },
889
1333
  };
890
1334
  const runner = new RunIssue({
@@ -918,6 +1362,8 @@ export function createV2Runtime(input) {
918
1362
  id: comment.id,
919
1363
  body: comment.body,
920
1364
  authorAssociation: comment.authorAssociation,
1365
+ author: comment.author.login,
1366
+ authorId: comment.author.id,
921
1367
  createdAt: comment.createdAt,
922
1368
  updatedAt: comment.updatedAt,
923
1369
  })),
@@ -930,9 +1376,10 @@ export function createV2Runtime(input) {
930
1376
  removeLabels: current.filter((label) => !labels.includes(label)),
931
1377
  });
932
1378
  },
1379
+ getRepositoryPermission: (login, expectedUserId) => input.issues.getRepositoryPermission(login, expectedUserId),
933
1380
  transitionToBlocked: async (issueNumber, labels) => {
934
1381
  const current = await input.issues.getLabels(issueNumber);
935
- if (current.includes(labels.review) || current.includes(labels.waitingHuman))
1382
+ if (current.includes(labels.review))
936
1383
  return;
937
1384
  const hasRunnerStatus = current.some((label) => (label === labels.auto || label === labels.running || label === labels.blocked));
938
1385
  if (!hasRunnerStatus)
@@ -949,7 +1396,7 @@ export function createV2Runtime(input) {
949
1396
  const matches = (await input.pullRequests.listAllByHeadBranch(headBranch))
950
1397
  .filter((pullRequest) => pullRequest.state === 'OPEN' && pullRequest.baseRefName === baseBranch);
951
1398
  if (matches.length > 1)
952
- throw new Error('multiple open pull requests match publication intent');
1399
+ throw new Error('multiple open pull requests match publication pendingEffect');
953
1400
  const match = matches[0];
954
1401
  if (!match)
955
1402
  return undefined;
@@ -970,59 +1417,17 @@ export function createV2Runtime(input) {
970
1417
  return { id: comment.id, body: comment.body };
971
1418
  },
972
1419
  },
973
- reviewFeedback: new ReviewFeedbackCoordinator({ pullRequests: input.pullRequests, issues: input.issues, now }),
1420
+ reviewFeedback: new ReviewFeedbackObserver({ pullRequests: input.pullRequests, issues: input.issues, now }),
974
1421
  git,
975
- routeCoordinator: {
976
- run: ({ state, ...routeInput }) => new RouteCoordinator({
977
- state,
978
- operation: reportOperation,
979
- createAttemptId: input.createAttemptId ?? randomUUID,
980
- now,
981
- createReceipt: ({ artifact, triage, review, decidedAt }) => {
982
- if (artifact.status === 'blocked')
983
- throw new Error('blocked triage cannot create a route receipt');
984
- const receipt = {
985
- version: 1,
986
- route: artifact.status,
987
- triage,
988
- review,
989
- artifact,
990
- decisionSha256: '',
991
- decidedAt,
992
- assumptions: [...artifact.assumptions],
993
- };
994
- receipt.decisionSha256 = hashRouteDecision(receipt);
995
- return validateRouteReceipt(receipt, triage.generationHash);
996
- },
997
- }).run(routeInput),
998
- },
999
- routeContinuations: {
1000
- direct: async () => ({ status: 'completed' }),
1001
- specRequired: (context, state, signal) => new SpecCoordinator({ state, operation: specOperation }).run(context, signal),
1002
- awaitingUser: async (context, state, signal) => {
1003
- const config = requireConfig(currentConfig);
1004
- return new WaitingHumanCoordinator({
1005
- issues: input.issues,
1006
- labels: {
1007
- auto: config.github.labels.auto.name,
1008
- running: config.github.labels.running.name,
1009
- blocked: config.github.labels.blocked.name,
1010
- review: config.github.labels.review.name,
1011
- waitingHuman: config.github.labels.waitingHuman.name,
1012
- },
1013
- now,
1014
- }).run(context, state, signal);
1015
- },
1016
- },
1017
1422
  implementationAgent,
1018
1423
  implementationReviewer,
1019
- waitForReviewProcessAbsence: waitForProcessGroupAbsent,
1020
1424
  checks: {
1021
- run: async ({ source, command, cwd, signal }) => {
1425
+ supportsLaunchOwnership: true,
1426
+ run: async ({ source, command, cwd, signal, onLaunched }) => {
1022
1427
  const timeoutMs = requireConfig(currentConfig).codex.timeoutMs;
1023
1428
  return source === 'issue'
1024
- ? runProcessCheck(parseIssueCheckInvocation(command), cwd, signal, timeoutMs)
1025
- : runShellCheck(command, cwd, signal, timeoutMs);
1429
+ ? runProcessCheck(parseIssueCheckInvocation(command), cwd, signal, timeoutMs, onLaunched)
1430
+ : runShellCheck(command, cwd, signal, timeoutMs, onLaunched);
1026
1431
  },
1027
1432
  },
1028
1433
  proof,
@@ -1034,12 +1439,56 @@ export function createV2Runtime(input) {
1034
1439
  await writeDurableAtomicFile(resolve(targetRoot, relativePath), `${canonicalJson({ version: 1, runId, code, summary, recordedAt: now() })}\n`);
1035
1440
  return { id: `evidence:${runId}:${code}`, path: relativePath };
1036
1441
  },
1442
+ outcomeEvidencePath: (runId, code, summarySha256) => `${requireConfig(currentConfig).runner.stateDir}/v2/evidence/${runId}/${sha256(code)}-${summarySha256}.json`,
1443
+ inspectOutcomeEvidence: async (path) => {
1444
+ try {
1445
+ return { sha256: sha256(await readRegularFile(resolve(targetRoot, path))) };
1446
+ }
1447
+ catch (error) {
1448
+ if (isErrorCode(error, 'ENOENT'))
1449
+ return undefined;
1450
+ throw error;
1451
+ }
1452
+ },
1453
+ writeOutcomeEvidence: ({ path, bytes, sha256: expectedSha256 }) => writeExactDurableBytes(resolve(targetRoot, path), bytes, expectedSha256),
1037
1454
  packageVersion: input.packageVersion,
1038
1455
  createWorkflowGeneration: input.createWorkflowGeneration,
1039
1456
  verifyWorkflowGeneration,
1040
1457
  createRunId: input.createRunId ?? randomUUID,
1041
1458
  createProofId: input.createProofId ?? randomUUID,
1042
1459
  createReviewSessionId: input.createAttemptId ?? randomUUID,
1460
+ processIdentity: {
1461
+ host: hostname(),
1462
+ bootId: input.bootId,
1463
+ capture: async (pid, processGroupId) => {
1464
+ const captured = await captureProcessStartIdentity({ platform: process.platform, pid, processGroupId });
1465
+ return captured.status === 'available' ? captured.identity : undefined;
1466
+ },
1467
+ observe: async (identity) => ({
1468
+ leader: await observeProcessIdentity({ platform: process.platform, ...identity }),
1469
+ group: observeProcessGroup(identity.processGroupId),
1470
+ }),
1471
+ },
1472
+ inspectAttemptResult: async (path) => {
1473
+ try {
1474
+ const bytes = await readRegularFile(path);
1475
+ return { bytes, sha256: sha256(bytes) };
1476
+ }
1477
+ catch (error) {
1478
+ if (isErrorCode(error, 'ENOENT'))
1479
+ return undefined;
1480
+ throw error;
1481
+ }
1482
+ },
1483
+ observeAttemptCleanup: async (attempt) => {
1484
+ return observeAttemptReadViewCleanup({
1485
+ orchestratorHome,
1486
+ canonicalRepository: requireCanonicalRepository(currentConfig),
1487
+ identity: attempt,
1488
+ });
1489
+ },
1490
+ writeAttemptResult: ({ path, bytes, sha256: expectedSha256 }) => writeExactDurableBytes(path, bytes, expectedSha256),
1491
+ attemptResultPath: ({ canonicalRepository, runId, attemptId }) => join(resolve(orchestratorHome), 'v2', sha256(canonicalRepository), 'runs', runId, 'attempts', attemptId, 'report.json'),
1043
1492
  now,
1044
1493
  signal: controller.signal,
1045
1494
  });
@@ -1078,7 +1527,10 @@ async function acquireOwnerLock(input) {
1078
1527
  }
1079
1528
  }
1080
1529
  async function readRegularFile(path) {
1081
- const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1530
+ const beforeOpen = await lstat(path);
1531
+ if (beforeOpen.isSymbolicLink() || !beforeOpen.isFile())
1532
+ throw new Error(`${path} is not a bounded regular file`);
1533
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
1082
1534
  try {
1083
1535
  const stat = await handle.stat();
1084
1536
  if (!stat.isFile() || stat.size > 5 * 1024 * 1024)
@@ -1089,6 +1541,24 @@ async function readRegularFile(path) {
1089
1541
  await handle.close();
1090
1542
  }
1091
1543
  }
1544
+ async function writeExactDurableBytes(path, bytes, expectedSha256) {
1545
+ if (sha256(bytes) !== expectedSha256)
1546
+ throw new Error('durable result bytes do not match expected SHA-256');
1547
+ try {
1548
+ const existing = await readRegularFile(path);
1549
+ if (sha256(existing) !== expectedSha256)
1550
+ throw new Error('durable result path already contains different bytes');
1551
+ return;
1552
+ }
1553
+ catch (error) {
1554
+ if (!isErrorCode(error, 'ENOENT'))
1555
+ throw error;
1556
+ }
1557
+ await writeDurableAtomicFile(path, bytes, 0o600);
1558
+ const observed = await readRegularFile(path);
1559
+ if (sha256(observed) !== expectedSha256)
1560
+ throw new Error('durable result write postcondition failed');
1561
+ }
1092
1562
  async function inspectRegularFile(path) {
1093
1563
  const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1094
1564
  try {
@@ -1101,15 +1571,20 @@ async function inspectRegularFile(path) {
1101
1571
  await handle.close();
1102
1572
  }
1103
1573
  }
1104
- export async function runShellCheck(command, cwd, signal, timeoutMs) {
1105
- return runSpawnCheck('/bin/sh', ['-lc', command], cwd, signal, timeoutMs);
1574
+ export async function runShellCheck(command, cwd, signal, timeoutMs, onLaunched) {
1575
+ return runSpawnCheck('/bin/sh', ['-lc', command], cwd, signal, timeoutMs, onLaunched);
1106
1576
  }
1107
- async function runProcessCheck(invocation, cwd, signal, timeoutMs) {
1108
- return runSpawnCheck(invocation.file, invocation.args, cwd, signal, timeoutMs);
1577
+ async function runProcessCheck(invocation, cwd, signal, timeoutMs, onLaunched) {
1578
+ return runSpawnCheck(invocation.file, invocation.args, cwd, signal, timeoutMs, onLaunched);
1109
1579
  }
1110
- async function runSpawnCheck(file, args, cwd, signal, timeoutMs) {
1580
+ async function runSpawnCheck(file, args, cwd, signal, timeoutMs, onLaunched) {
1111
1581
  return new Promise((resolveCheck, rejectCheck) => {
1112
- const child = spawn(file, args, { cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
1582
+ const child = spawn('/bin/sh', [
1583
+ '-c', 'IFS= read -r _ || exit 125; exec "$@"', 'codex-orchestrator-check-gate', file, ...args,
1584
+ ], { cwd, detached: true, stdio: ['pipe', 'pipe', 'pipe'] });
1585
+ const launchOwnership = child.pid && onLaunched
1586
+ ? onLaunched({ pid: child.pid, processGroupId: child.pid })
1587
+ : Promise.resolve();
1113
1588
  const chunks = [];
1114
1589
  let retained = 0;
1115
1590
  const outputHash = createHash('sha256');
@@ -1150,6 +1625,7 @@ async function runSpawnCheck(file, args, cwd, signal, timeoutMs) {
1150
1625
  }, 5_000);
1151
1626
  killTimer.unref();
1152
1627
  };
1628
+ void launchOwnership.then(() => child.stdin.end('\n')).catch(() => terminate());
1153
1629
  signal.addEventListener('abort', terminate, { once: true });
1154
1630
  if (signal.aborted)
1155
1631
  terminate();
@@ -1189,10 +1665,15 @@ async function runSpawnCheck(file, args, cwd, signal, timeoutMs) {
1189
1665
  rejectCheck(new Error(`Check exceeded ${timeoutMs}ms and was terminated.`));
1190
1666
  return;
1191
1667
  }
1668
+ await launchOwnership;
1192
1669
  resolveCheck({
1193
1670
  status: code === 0 ? 'passed' : 'failed',
1194
1671
  output: Buffer.concat(chunks),
1195
1672
  outputSha256: outputHash.digest('hex'),
1673
+ observation: {
1674
+ leader: 'absent',
1675
+ group: child.pid ? observeProcessGroup(child.pid) : 'unknown',
1676
+ },
1196
1677
  });
1197
1678
  })().catch(rejectCheck);
1198
1679
  });
@@ -1303,8 +1784,7 @@ async function prepareContainedAttempt(input) {
1303
1784
  operation: input.operationId,
1304
1785
  bootId: input.bootId,
1305
1786
  });
1306
- const reportOnly = input.operationId === 'triage' || input.operationId === 'ambiguity-review'
1307
- || input.operationId === 'code-review' || input.operationId === 'spec-review';
1787
+ const reportOnly = input.operationId === 'code-review';
1308
1788
  if ((reportOnly
1309
1789
  ? snapshot.policy.sandboxMode !== 'read-only'
1310
1790
  || snapshot.policy.worktreeAccess !== 'read-only'
@@ -1340,6 +1820,8 @@ async function prepareContainedAttempt(input) {
1340
1820
  tmpDir,
1341
1821
  policy: structuredClone(snapshot.policy),
1342
1822
  profile,
1823
+ reviewers: Object.keys(snapshot.reviewerProfiles).sort(),
1824
+ reviewerProfiles: structuredClone(snapshot.reviewerProfiles),
1343
1825
  };
1344
1826
  }
1345
1827
  async function artifactInventory(root, logicalRoot) {
@@ -1355,7 +1837,9 @@ async function artifactInventory(root, logicalRoot) {
1355
1837
  }
1356
1838
  if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
1357
1839
  throw new Error('proof artifact root must be a direct directory');
1840
+ const rootIdentity = await inspectSafeArtifactRoot(root);
1358
1841
  const visit = async (directory, relative) => {
1842
+ await assertSafeArtifactRoot(rootIdentity);
1359
1843
  for (const entry of await readdir(directory, { withFileTypes: true })) {
1360
1844
  const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
1361
1845
  const childPath = join(directory, entry.name);
@@ -1364,7 +1848,7 @@ async function artifactInventory(root, logicalRoot) {
1364
1848
  if (entry.isDirectory())
1365
1849
  await visit(childPath, childRelative);
1366
1850
  else if (entry.isFile())
1367
- output.set(`${logicalRoot}/${childRelative}`, sha256(await readFile(childPath)));
1851
+ output.set(`${logicalRoot}/${childRelative}`, sha256(await readRegularFileWithoutSymlinkAncestors(rootIdentity, childPath)));
1368
1852
  else
1369
1853
  throw new Error('proof artifact tree contains a special file');
1370
1854
  }
@@ -1413,20 +1897,6 @@ async function fingerprintDeniedEntry(path, existingInfo) {
1413
1897
  }
1414
1898
  return { kind: 'directory', entries };
1415
1899
  }
1416
- async function waitForProcessGroupAbsent(processGroupId) {
1417
- while (true) {
1418
- try {
1419
- process.kill(-processGroupId, 0);
1420
- }
1421
- catch (error) {
1422
- if (isErrorCode(error, 'ESRCH'))
1423
- return;
1424
- if (!isErrorCode(error, 'EPERM'))
1425
- throw error;
1426
- }
1427
- await new Promise((resolveWait) => setTimeout(resolveWait, 1_000));
1428
- }
1429
- }
1430
1900
  export async function materializeReportReadView(input) {
1431
1901
  await assertDisjointReadViewPaths(input.worktreePath, input.destination);
1432
1902
  const deniedPaths = normalizeReadViewDeniedPaths(input.deniedPaths);