codex-orchestrator 2.0.2 → 2.0.4

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 (221) hide show
  1. package/CHANGELOG.md +44 -426
  2. package/README.md +135 -34
  3. package/dist/src/index.d.ts +11 -1
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +5 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/v2/acceptance-proof.d.ts +3 -0
  8. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  9. package/dist/src/v2/acceptance-proof.js +2 -8
  10. package/dist/src/v2/acceptance-proof.js.map +1 -1
  11. package/dist/src/v2/adapters/gh-issue-adapter.d.ts +5 -3
  12. package/dist/src/v2/adapters/gh-issue-adapter.d.ts.map +1 -1
  13. package/dist/src/v2/adapters/gh-issue-adapter.js +67 -12
  14. package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
  15. package/dist/src/v2/adapters/issues.d.ts +16 -2
  16. package/dist/src/v2/adapters/issues.d.ts.map +1 -1
  17. package/dist/src/v2/adapters/issues.js +15 -5
  18. package/dist/src/v2/adapters/issues.js.map +1 -1
  19. package/dist/src/v2/adapters/mission-coordinator-lock.d.ts +1 -0
  20. package/dist/src/v2/adapters/mission-coordinator-lock.d.ts.map +1 -1
  21. package/dist/src/v2/adapters/mission-coordinator-lock.js +5 -1
  22. package/dist/src/v2/adapters/mission-coordinator-lock.js.map +1 -1
  23. package/dist/src/v2/cli-contract.d.ts +3 -3
  24. package/dist/src/v2/cli-contract.d.ts.map +1 -1
  25. package/dist/src/v2/cli-contract.js +9 -1
  26. package/dist/src/v2/cli-contract.js.map +1 -1
  27. package/dist/src/v2/cli.d.ts +24 -0
  28. package/dist/src/v2/cli.d.ts.map +1 -0
  29. package/dist/src/v2/{candidate-cli.js → cli.js} +30 -26
  30. package/dist/src/v2/cli.js.map +1 -0
  31. package/dist/src/v2/code-review-report.d.ts +66 -0
  32. package/dist/src/v2/code-review-report.d.ts.map +1 -0
  33. package/dist/src/v2/code-review-report.js +259 -0
  34. package/dist/src/v2/code-review-report.js.map +1 -0
  35. package/dist/src/v2/codex-process.d.ts +8 -1
  36. package/dist/src/v2/codex-process.d.ts.map +1 -1
  37. package/dist/src/v2/codex-process.js +22 -0
  38. package/dist/src/v2/codex-process.js.map +1 -1
  39. package/dist/src/v2/config.d.ts +4 -3
  40. package/dist/src/v2/config.d.ts.map +1 -1
  41. package/dist/src/v2/config.js +8 -3
  42. package/dist/src/v2/config.js.map +1 -1
  43. package/dist/src/v2/contained-report-operation.d.ts +100 -0
  44. package/dist/src/v2/contained-report-operation.d.ts.map +1 -0
  45. package/dist/src/v2/contained-report-operation.js +200 -0
  46. package/dist/src/v2/contained-report-operation.js.map +1 -0
  47. package/dist/src/v2/containment.d.ts +10 -0
  48. package/dist/src/v2/containment.d.ts.map +1 -1
  49. package/dist/src/v2/containment.js +49 -1
  50. package/dist/src/v2/containment.js.map +1 -1
  51. package/dist/src/v2/direct-delivery.d.ts +96 -0
  52. package/dist/src/v2/direct-delivery.d.ts.map +1 -0
  53. package/dist/src/v2/direct-delivery.js +482 -0
  54. package/dist/src/v2/direct-delivery.js.map +1 -0
  55. package/dist/src/v2/immutable-workflow-publisher.d.ts +40 -0
  56. package/dist/src/v2/immutable-workflow-publisher.d.ts.map +1 -0
  57. package/dist/src/v2/immutable-workflow-publisher.js +218 -0
  58. package/dist/src/v2/immutable-workflow-publisher.js.map +1 -0
  59. package/dist/src/v2/implementation-reviewer.d.ts +81 -0
  60. package/dist/src/v2/implementation-reviewer.d.ts.map +1 -0
  61. package/dist/src/v2/implementation-reviewer.js +157 -0
  62. package/dist/src/v2/implementation-reviewer.js.map +1 -0
  63. package/dist/src/v2/owner-control-lock.d.ts +41 -0
  64. package/dist/src/v2/owner-control-lock.d.ts.map +1 -0
  65. package/dist/src/v2/owner-control-lock.js +174 -0
  66. package/dist/src/v2/owner-control-lock.js.map +1 -0
  67. package/dist/src/v2/proof-report.d.ts.map +1 -1
  68. package/dist/src/v2/proof-report.js +55 -29
  69. package/dist/src/v2/proof-report.js.map +1 -1
  70. package/dist/src/v2/route-continuations.d.ts +32 -0
  71. package/dist/src/v2/route-continuations.d.ts.map +1 -0
  72. package/dist/src/v2/route-continuations.js +2 -0
  73. package/dist/src/v2/route-continuations.js.map +1 -0
  74. package/dist/src/v2/route-coordinator.d.ts +77 -0
  75. package/dist/src/v2/route-coordinator.d.ts.map +1 -0
  76. package/dist/src/v2/route-coordinator.js +370 -0
  77. package/dist/src/v2/route-coordinator.js.map +1 -0
  78. package/dist/src/v2/route-decision.d.ts +129 -0
  79. package/dist/src/v2/route-decision.d.ts.map +1 -0
  80. package/dist/src/v2/route-decision.js +400 -0
  81. package/dist/src/v2/route-decision.js.map +1 -0
  82. package/dist/src/v2/run-issue.d.ts +64 -6
  83. package/dist/src/v2/run-issue.d.ts.map +1 -1
  84. package/dist/src/v2/run-issue.js +962 -92
  85. package/dist/src/v2/run-issue.js.map +1 -1
  86. package/dist/src/v2/run-store.d.ts +25 -1
  87. package/dist/src/v2/run-store.d.ts.map +1 -1
  88. package/dist/src/v2/run-store.js +129 -4
  89. package/dist/src/v2/run-store.js.map +1 -1
  90. package/dist/src/v2/runtime-assets.d.ts +15 -13
  91. package/dist/src/v2/runtime-assets.d.ts.map +1 -1
  92. package/dist/src/v2/runtime-assets.js +263 -416
  93. package/dist/src/v2/runtime-assets.js.map +1 -1
  94. package/dist/src/v2/runtime.d.ts +17 -9
  95. package/dist/src/v2/runtime.d.ts.map +1 -1
  96. package/dist/src/v2/runtime.js +564 -64
  97. package/dist/src/v2/runtime.js.map +1 -1
  98. package/dist/src/v2/setup-cli.d.ts.map +1 -1
  99. package/dist/src/v2/setup-cli.js +4 -10
  100. package/dist/src/v2/setup-cli.js.map +1 -1
  101. package/dist/src/v2/setup-runtime.d.ts.map +1 -1
  102. package/dist/src/v2/setup-runtime.js +19 -131
  103. package/dist/src/v2/setup-runtime.js.map +1 -1
  104. package/dist/src/v2/setup-store.d.ts +0 -5
  105. package/dist/src/v2/setup-store.d.ts.map +1 -1
  106. package/dist/src/v2/setup-store.js +3 -106
  107. package/dist/src/v2/setup-store.js.map +1 -1
  108. package/dist/src/v2/setup.d.ts +6 -43
  109. package/dist/src/v2/setup.d.ts.map +1 -1
  110. package/dist/src/v2/setup.js +13 -192
  111. package/dist/src/v2/setup.js.map +1 -1
  112. package/dist/src/v2/spec-coordinator.d.ts +85 -0
  113. package/dist/src/v2/spec-coordinator.d.ts.map +1 -0
  114. package/dist/src/v2/spec-coordinator.js +88 -0
  115. package/dist/src/v2/spec-coordinator.js.map +1 -0
  116. package/dist/src/v2/spec-delivery.d.ts +143 -0
  117. package/dist/src/v2/spec-delivery.d.ts.map +1 -0
  118. package/dist/src/v2/spec-delivery.js +401 -0
  119. package/dist/src/v2/spec-delivery.js.map +1 -0
  120. package/dist/src/v2/triage-route.d.ts +68 -0
  121. package/dist/src/v2/triage-route.d.ts.map +1 -0
  122. package/dist/src/v2/triage-route.js +223 -0
  123. package/dist/src/v2/triage-route.js.map +1 -0
  124. package/dist/src/v2/waiting-human-coordinator.d.ts +49 -0
  125. package/dist/src/v2/waiting-human-coordinator.d.ts.map +1 -0
  126. package/dist/src/v2/waiting-human-coordinator.js +509 -0
  127. package/dist/src/v2/waiting-human-coordinator.js.map +1 -0
  128. package/dist/src/v2/waiting-human.d.ts +143 -0
  129. package/dist/src/v2/waiting-human.d.ts.map +1 -0
  130. package/dist/src/v2/waiting-human.js +408 -0
  131. package/dist/src/v2/waiting-human.js.map +1 -0
  132. package/dist/src/v2/workflow-assets.d.ts +98 -0
  133. package/dist/src/v2/workflow-assets.d.ts.map +1 -0
  134. package/dist/src/v2/workflow-assets.js +646 -0
  135. package/dist/src/v2/workflow-assets.js.map +1 -0
  136. package/docs/deep-dive.md +275 -52
  137. package/internal-workflow/docs/agents/bug-workflow-routing.md +24 -0
  138. package/internal-workflow/docs/agents/bugfix-quality-gate.md +11 -0
  139. package/internal-workflow/docs/agents/coding-skill-routing.md +123 -0
  140. package/internal-workflow/docs/agents/confidence-rubric.md +65 -0
  141. package/internal-workflow/docs/agents/contract-test-ledger.md +60 -0
  142. package/internal-workflow/docs/agents/review-gates.md +42 -0
  143. package/internal-workflow/docs/agents/review-protocol.md +98 -0
  144. package/internal-workflow/docs/agents/tool-usage.md +88 -0
  145. package/internal-workflow/evals/coding-skill-evals.json +66 -0
  146. package/internal-workflow/manifest.json +1 -0
  147. package/internal-workflow/operations/acceptance-proof/SKILL.md +9 -0
  148. package/internal-workflow/operations/ambiguity-review/SKILL.md +5 -0
  149. package/internal-workflow/operations/code-review/SKILL.md +23 -0
  150. package/internal-workflow/operations/implementation/SKILL.md +24 -0
  151. package/internal-workflow/operations/spec-author/SKILL.md +12 -0
  152. package/internal-workflow/operations/spec-review/SKILL.md +12 -0
  153. package/internal-workflow/operations/triage/SKILL.md +12 -0
  154. package/internal-workflow/profiles/analyst_deep.toml +9 -0
  155. package/internal-workflow/profiles/implementer_standard.toml +9 -0
  156. package/internal-workflow/profiles/proof_agent.toml +8 -0
  157. package/internal-workflow/profiles/reviewer_deep.toml +9 -0
  158. package/internal-workflow/profiles/reviewer_standard.toml +9 -0
  159. package/internal-workflow/schemas/ambiguity-review-v1.json +1 -0
  160. package/internal-workflow/schemas/code-review-v1.json +1 -0
  161. package/internal-workflow/schemas/implementation-report-v1.json +1 -0
  162. package/internal-workflow/schemas/proof-report-v1.json +1 -0
  163. package/internal-workflow/schemas/spec-author-v1.json +1 -0
  164. package/internal-workflow/schemas/spec-review-v1.json +30 -0
  165. package/internal-workflow/schemas/triage-route-v1.json +1 -0
  166. package/internal-workflow/skills/acceptance-proof/agents/openai.yaml +6 -0
  167. package/{internal-skills → internal-workflow/skills}/agent-auto/SKILL.md +6 -1
  168. package/internal-workflow/skills/agent-auto/agents/openai.yaml +6 -0
  169. package/internal-workflow/skills/code-debugger/SKILL.md +122 -0
  170. package/internal-workflow/skills/code-debugger/agents/openai.yaml +7 -0
  171. package/internal-workflow/skills/code-review/SKILL.md +279 -0
  172. package/internal-workflow/skills/code-review/agents/openai.yaml +4 -0
  173. package/internal-workflow/skills/code-review/references/bug-classes.md +56 -0
  174. package/internal-workflow/skills/code-review/references/cleanup-lens.md +52 -0
  175. package/internal-workflow/skills/code-review/references/framework-lenses.md +34 -0
  176. package/internal-workflow/skills/code-review/references/targeted-recipes.md +49 -0
  177. package/internal-workflow/skills/diagnosing-bugs/SKILL.md +138 -0
  178. package/internal-workflow/skills/diagnosing-bugs/agents/openai.yaml +6 -0
  179. package/internal-workflow/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  180. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +102 -0
  181. package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +6 -0
  182. package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +31 -0
  183. package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +146 -0
  184. package/internal-workflow/skills/implementation-spec-review/SKILL.md +115 -0
  185. package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +6 -0
  186. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
  187. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +93 -0
  188. package/internal-workflow/skills/small-task-implementer/SKILL.md +104 -0
  189. package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +6 -0
  190. package/internal-workflow/skills/spec-implementer/SKILL.md +126 -0
  191. package/internal-workflow/skills/spec-implementer/agents/openai.yaml +6 -0
  192. package/internal-workflow/skills/spec-implementer/evals/evals.json +30 -0
  193. package/internal-workflow/skills/spec-implementer/references/review-loop.md +94 -0
  194. package/internal-workflow/skills/tdd/SKILL.md +72 -0
  195. package/internal-workflow/skills/tdd/agents/openai.yaml +6 -0
  196. package/internal-workflow/skills/tdd/interface-design.md +31 -0
  197. package/internal-workflow/skills/tdd/mocking.md +59 -0
  198. package/internal-workflow/skills/tdd/refactoring.md +10 -0
  199. package/internal-workflow/skills/tdd/tests.md +77 -0
  200. package/internal-workflow/skills/triage/AGENT-BRIEF.md +192 -0
  201. package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +101 -0
  202. package/internal-workflow/skills/triage/SKILL.md +134 -0
  203. package/internal-workflow/skills/triage/agents/openai.yaml +6 -0
  204. package/package.json +14 -8
  205. package/dist/src/v2/adapters/target-activity-fence.d.ts +0 -23
  206. package/dist/src/v2/adapters/target-activity-fence.d.ts.map +0 -1
  207. package/dist/src/v2/adapters/target-activity-fence.js +0 -249
  208. package/dist/src/v2/adapters/target-activity-fence.js.map +0 -1
  209. package/dist/src/v2/candidate-cli.d.ts +0 -22
  210. package/dist/src/v2/candidate-cli.d.ts.map +0 -1
  211. package/dist/src/v2/candidate-cli.js.map +0 -1
  212. package/dist/src/v2/legacy-cutover.d.ts +0 -52
  213. package/dist/src/v2/legacy-cutover.d.ts.map +0 -1
  214. package/dist/src/v2/legacy-cutover.js +0 -87
  215. package/dist/src/v2/legacy-cutover.js.map +0 -1
  216. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/SKILL.md +0 -0
  217. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/android.md +0 -0
  218. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/browser.md +0 -0
  219. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/ios.md +0 -0
  220. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/android-lease.mjs +0 -0
  221. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/ios-lease.mjs +0 -0
@@ -1,24 +1,33 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { spawn } from 'node:child_process';
4
- import { lstat, mkdir, open, readFile, readdir, readlink, realpath } from 'node:fs/promises';
4
+ import { lstat, mkdir, open, readFile, readdir, readlink, realpath, rm } from 'node:fs/promises';
5
5
  import { homedir, hostname } from 'node:os';
6
- import { dirname, isAbsolute, join, posix, resolve } from 'node:path';
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
9
  import { defaultProcessExecutor } from './adapters/command.js';
10
10
  import { AcceptanceProof, ProofQuiescenceError } from './acceptance-proof.js';
11
- import { acquireExclusiveJsonFileLock } from './atomic-store.js';
12
11
  import { createCheckedChangeCapabilities } from './checked-change.js';
12
+ import { InjectedContainedReportOperation } from './contained-report-operation.js';
13
+ import { ContainedImplementationReviewer } from './implementation-reviewer.js';
13
14
  import { parseAgentAutoConfig } from './config.js';
14
- import { canonicalJson, containmentArgvPolicySha256, containmentCertificatePath, parseJsonWithoutDuplicateKeys, readContainmentCertificate, sha256, } from './containment.js';
15
+ import { WaitingHumanCoordinator } from './waiting-human-coordinator.js';
16
+ import { assertContainmentCertificateMatchesRuntime, canonicalJson, containmentArgvPolicySha256, containmentCertificatePath, parseJsonWithoutDuplicateKeys, readContainmentCertificate, sha256, } from './containment.js';
15
17
  import { FileProofRecordWriter } from './proof-store.js';
18
+ import { acquireOwnerControlLock, OwnerControlLockBlockedError } from './owner-control-lock.js';
16
19
  import { decodeAgentReportForValidation } from './report-envelope.js';
17
20
  import { CodexProcess, ProcessQuiescenceError } from './codex-process.js';
18
21
  import { FileAndroidLeaseVerifier, FileIosLeaseVerifier } from './mobile-lease.js';
19
22
  import { publishRuntimeAssetSnapshot } from './runtime-assets.js';
20
- import { OwnerLockSafetyError, RunIssue } from './run-issue.js';
23
+ import { RouteCoordinator } from './route-coordinator.js';
24
+ import { SpecCoordinator } from './spec-coordinator.js';
25
+ import { createSpecRevision } from './spec-delivery.js';
26
+ import { validateCodeReviewDefects } from './code-review-report.js';
27
+ import { hashRouteDecision, validateRouteReceipt } from './route-decision.js';
28
+ import { OwnerLockContentionError, OwnerLockSafetyError, RunIssue } from './run-issue.js';
21
29
  import { FileRunRecordWriter } from './run-store.js';
30
+ import { parseWorkflowExecutionProfile, verifyWorkflowGeneration, } from './workflow-assets.js';
22
31
  export class LocalGitRunIssueAdapter {
23
32
  executor;
24
33
  worktrees;
@@ -101,6 +110,14 @@ export class LocalGitRunIssueAdapter {
101
110
  listChangedFiles(worktreePath) {
102
111
  return this.worktrees.listChangedFiles(worktreePath);
103
112
  }
113
+ async fingerprintChangedFiles(worktreePath, changedFiles) {
114
+ const root = await realpath(worktreePath);
115
+ const entries = [];
116
+ for (const path of [...changedFiles].sort()) {
117
+ entries.push({ path, fingerprint: await fingerprintRepositoryPath(root, path) });
118
+ }
119
+ return sha256(canonicalJson(entries));
120
+ }
104
121
  async stageAll(worktreePath) {
105
122
  await this.git(['-C', worktreePath, 'add', '--all']);
106
123
  }
@@ -167,13 +184,13 @@ export class ContainedImplementationAgent {
167
184
  const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
168
185
  const attemptId = (this.dependencies.createAttemptId ?? randomUUID)();
169
186
  const attempt = await prepareContainedAttempt({
170
- packageRoot: this.dependencies.packageRoot,
171
187
  orchestratorHome: this.dependencies.orchestratorHome,
172
188
  canonicalRepository,
173
189
  runId: input.runId,
174
190
  attemptId,
175
- skill: 'agent-auto',
176
- expectedPackageVersion: this.dependencies.packageVersion,
191
+ operationId: 'implementation',
192
+ workflowGeneration: input.workflowGeneration,
193
+ bootId: this.dependencies.bootId,
177
194
  });
178
195
  const baseline = await this.dependencies.git.snapshot(input.worktreePath);
179
196
  try {
@@ -188,7 +205,9 @@ export class ContainedImplementationAgent {
188
205
  parentCodexHome: this.dependencies.parentCodexHome,
189
206
  parentEnv: process.env,
190
207
  prompt: [
191
- `Follow the exact skill at ${attempt.skillPath}.`,
208
+ `Package profile instructions: ${attempt.profile.developerInstructions}`,
209
+ `Follow the exact operation at ${attempt.operationPath}.`,
210
+ `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
192
211
  `Implement issue #${input.issue.number}: ${input.issue.title}`,
193
212
  `Implementation cycle: ${input.cycle}.`,
194
213
  `Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
@@ -198,6 +217,8 @@ export class ContainedImplementationAgent {
198
217
  ].join('\n'),
199
218
  timeoutMs: config.codex.timeoutMs,
200
219
  idleTimeoutMs: config.codex.idleTimeoutMs,
220
+ operationPolicy: attempt.policy,
221
+ executionProfile: attempt.profile,
201
222
  }, input.signal);
202
223
  if (result.kind === 'cancelled')
203
224
  return { kind: 'cancelled' };
@@ -208,6 +229,7 @@ export class ContainedImplementationAgent {
208
229
  return { kind: 'internal-error' };
209
230
  return {
210
231
  kind: 'completed',
232
+ attemptId,
211
233
  report: decodeAgentReportForValidation(result.report.bytes),
212
234
  };
213
235
  }
@@ -233,20 +255,22 @@ export class ContainedProofAgent {
233
255
  this.dependencies = dependencies;
234
256
  }
235
257
  async run(input) {
258
+ if (!input.workflowGeneration)
259
+ throw new Error('proof workflow generation is required');
236
260
  const config = this.dependencies.config();
237
261
  const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
238
262
  const worktreePath = resolve(this.dependencies.targetRoot, config.runner.workspaceRoot, `issue-${input.issue.number}`);
239
263
  const attempt = await prepareContainedAttempt({
240
- packageRoot: this.dependencies.packageRoot,
241
264
  orchestratorHome: this.dependencies.orchestratorHome,
242
265
  canonicalRepository,
243
266
  runId: input.runId,
244
267
  attemptId: (this.dependencies.createAttemptId ?? randomUUID)(),
245
- skill: 'acceptance-proof',
246
- expectedPackageVersion: this.dependencies.packageVersion,
268
+ operationId: 'acceptance-proof',
269
+ workflowGeneration: input.workflowGeneration,
270
+ bootId: this.dependencies.bootId,
247
271
  });
248
272
  const artifactRoot = resolve(worktreePath, config.proof.artifactDir);
249
- const snapshotRoot = dirname(attempt.skillPath);
273
+ const snapshotRoot = dirname(attempt.sourceSkillPath ?? attempt.operationPath);
250
274
  const androidLeaseRoot = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(canonicalRepository), 'leases');
251
275
  const androidLeaseArtifact = join(artifactRoot, input.proofId, 'android-lease.json');
252
276
  const iosLeaseRoot = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(canonicalRepository), 'leases');
@@ -265,7 +289,9 @@ export class ContainedProofAgent {
265
289
  parentCodexHome: this.dependencies.parentCodexHome,
266
290
  parentEnv: process.env,
267
291
  prompt: [
268
- `Follow the exact skill at ${attempt.skillPath}.`,
292
+ `Package profile instructions: ${attempt.profile.developerInstructions}`,
293
+ `Follow the exact operation at ${attempt.operationPath}.`,
294
+ `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
269
295
  `Independently prove issue #${input.issue.number}.`,
270
296
  `Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
271
297
  `Checked change digest: ${input.checkedChangeSha256}.`,
@@ -296,6 +322,8 @@ export class ContainedProofAgent {
296
322
  ].join('\n'),
297
323
  timeoutMs: config.codex.timeoutMs,
298
324
  idleTimeoutMs: config.codex.idleTimeoutMs,
325
+ operationPolicy: attempt.policy,
326
+ executionProfile: attempt.profile,
299
327
  }, input.signal);
300
328
  if (result.kind === 'cancelled')
301
329
  return { kind: 'cancelled' };
@@ -339,11 +367,10 @@ export function createV2Runtime(input) {
339
367
  const iosXcrunPath = resolve(input.iosXcrunPath ?? '/usr/bin/xcrun');
340
368
  const containedDependencies = () => ({
341
369
  config: () => requireConfig(currentConfig),
342
- packageRoot: requireRuntimeString(input.packageRoot, 'packageRoot'),
343
370
  orchestratorHome,
344
371
  parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
345
372
  safePath: requireRuntimeString(input.safePath, 'safePath'),
346
- packageVersion: input.packageVersion,
373
+ bootId: input.bootId,
347
374
  process: containedProcess,
348
375
  createAttemptId: input.createAttemptId,
349
376
  });
@@ -359,6 +386,292 @@ export function createV2Runtime(input) {
359
386
  iosXcrunPath,
360
387
  processExecutor: commandExecutor,
361
388
  });
389
+ const reportOperation = new InjectedContainedReportOperation({
390
+ prepare: async ({ operation, attemptId, runId, workflowGeneration }) => ({
391
+ operation,
392
+ generationHash: workflowGeneration.generationHash,
393
+ ...await prepareContainedAttempt({
394
+ orchestratorHome,
395
+ canonicalRepository: requireCanonicalRepository(currentConfig),
396
+ runId,
397
+ attemptId,
398
+ operationId: operation,
399
+ workflowGeneration,
400
+ bootId: input.bootId,
401
+ }),
402
+ }),
403
+ snapshot: (worktreePath) => git.snapshot(worktreePath),
404
+ launch: async ({ attempt, worktreePath, promptFacts, signal, onLaunched }) => {
405
+ const config = requireConfig(currentConfig);
406
+ if (!attempt.schemaPath || !attempt.reportPath || !attempt.toolHome || !attempt.tmpDir
407
+ || !attempt.profile || !attempt.operationPath || !attempt.workflowRoot) {
408
+ return { status: 'blocked', kind: 'safety', code: 'report-operation-attempt-incomplete' };
409
+ }
410
+ let readView;
411
+ try {
412
+ readView = await materializeReportReadView({
413
+ worktreePath,
414
+ destination: join(dirname(attempt.tmpDir), 'read-view'),
415
+ deniedPaths: config.deny.readPaths,
416
+ });
417
+ }
418
+ catch {
419
+ return { status: 'blocked', kind: 'safety', code: 'report-operation-read-view-failed' };
420
+ }
421
+ let result;
422
+ let cleanupAfterSafeHalt = false;
423
+ try {
424
+ result = await containedProcess.run({
425
+ codexPath: config.codex.command,
426
+ cwd: readView,
427
+ schemaPath: attempt.schemaPath,
428
+ reportPath: attempt.reportPath,
429
+ toolHome: attempt.toolHome,
430
+ tmpDir: attempt.tmpDir,
431
+ safePath: requireRuntimeString(input.safePath, 'safePath'),
432
+ parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
433
+ parentEnv: process.env,
434
+ prompt: [
435
+ `Package profile instructions: ${attempt.profile.developerInstructions}`,
436
+ `Follow the exact operation at ${attempt.operationPath}.`,
437
+ `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
438
+ `Runner-provided facts: ${canonicalJson(promptFacts)}`,
439
+ 'This is a read-only, report-only operation. Do not edit files or external state, use network or MCP tools, or request additional authority.',
440
+ 'Do not read .env or any .env* file. The runner has removed repository credential paths from this read view.',
441
+ ].join('\n'),
442
+ timeoutMs: config.codex.timeoutMs,
443
+ idleTimeoutMs: config.codex.idleTimeoutMs,
444
+ operationPolicy: attempt.policy,
445
+ executionProfile: attempt.profile,
446
+ ...(onLaunched ? { onSpawned: onLaunched } : {}),
447
+ }, signal);
448
+ }
449
+ catch (error) {
450
+ if (error instanceof ProcessQuiescenceError) {
451
+ cleanupAfterSafeHalt = true;
452
+ return {
453
+ status: 'safe-halt',
454
+ pid: error.pid,
455
+ processGroupId: error.processGroupId,
456
+ startedAt: now(),
457
+ waitForAbsence: async () => {
458
+ await waitForProcessGroupAbsent(error.processGroupId);
459
+ await rm(readView, { recursive: true, force: true });
460
+ },
461
+ };
462
+ }
463
+ throw error;
464
+ }
465
+ finally {
466
+ if (!cleanupAfterSafeHalt)
467
+ await rm(readView, { recursive: true, force: true });
468
+ }
469
+ if (result.kind === 'cancelled')
470
+ return { status: 'cancelled' };
471
+ if (result.kind === 'launch-gate-failed') {
472
+ return { status: 'blocked', kind: 'safety', code: 'review-operation-launch-persistence-failed' };
473
+ }
474
+ if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
475
+ return { status: 'retryable', code: `report-operation-${result.kind}` };
476
+ }
477
+ if (result.kind !== 'completed' || result.report.kind !== 'available') {
478
+ return { status: 'blocked', kind: 'external', code: 'report-operation-report-unavailable' };
479
+ }
480
+ return { status: 'completed', reportBytes: result.report.bytes };
481
+ },
482
+ });
483
+ const implementationReviewer = new ContainedImplementationReviewer({
484
+ operation: reportOperation,
485
+ createAttemptId: input.createAttemptId ?? randomUUID,
486
+ });
487
+ const createAttemptId = input.createAttemptId ?? randomUUID;
488
+ const specOperation = {
489
+ author: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
490
+ const attemptId = createAttemptId();
491
+ const sessionId = state.authorSessionId ?? randomUUID();
492
+ let attempt;
493
+ try {
494
+ attempt = await prepareContainedAttempt({
495
+ orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
496
+ attemptId, operationId: 'spec-author', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
497
+ });
498
+ const revisionPath = join(dirname(attempt.reportPath), `revision-${state.revisions.length + 1}.md`);
499
+ await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath, revisionPath });
500
+ const config = requireConfig(currentConfig);
501
+ const result = await containedProcess.run({
502
+ codexPath: config.codex.command, cwd: dirname(attempt.reportPath), schemaPath: attempt.schemaPath,
503
+ reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
504
+ safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
505
+ parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
506
+ operationPolicy: attempt.policy, executionProfile: attempt.profile,
507
+ onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
508
+ prompt: [
509
+ `Package profile instructions: ${attempt.profile.developerInstructions}`,
510
+ `Follow the exact operation at ${attempt.operationPath}.`,
511
+ `The immutable workflow root is ${attempt.workflowRoot}.`,
512
+ `Author mode: ${mode}. Issue authority: ${canonicalJson(context.issue)}.`,
513
+ `Frozen criteria: ${canonicalJson(context.frozenCriteria)}.`,
514
+ `Prior revisions and review state: ${canonicalJson({ revisions: state.revisions, review: state.review })}.`,
515
+ `Write the complete new immutable revision only to ${revisionPath}. Return that exact absolute path and its SHA-256 in the report.`,
516
+ 'Do not modify the product worktree, prior revisions, external state, or any .env file.',
517
+ ].join('\n'),
518
+ }, signal);
519
+ if (result.kind === 'cancelled')
520
+ return { status: 'cancelled' };
521
+ if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
522
+ return { status: 'retryable', code: `spec-author-${result.kind}` };
523
+ if (result.kind !== 'completed' || result.report.kind !== 'available')
524
+ return { status: 'retryable', code: 'spec-author-report-invalid' };
525
+ const report = decodeAgentReportForValidation(result.report.bytes);
526
+ if (report.status !== 'ready' || report.specPath !== revisionPath || report.specSha256 === null)
527
+ return { status: 'retryable', code: 'spec-author-report-invalid' };
528
+ const content = await readRegularFile(revisionPath);
529
+ if (report.specSha256 !== sha256(content))
530
+ return { status: 'retryable', code: 'spec-author-report-invalid' };
531
+ const previous = state.revisions.at(-1) ?? null;
532
+ return { status: 'completed', value: createSpecRevision({
533
+ revision: state.revisions.length + 1, path: revisionPath, content: content.toString('utf8'),
534
+ evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
535
+ author: { attemptId, sessionId }, previousRevision: previous,
536
+ }) };
537
+ }
538
+ catch (error) {
539
+ if (error instanceof ProcessQuiescenceError) {
540
+ try {
541
+ await waitForProcessGroupAbsent(error.processGroupId);
542
+ return { status: 'retryable', code: 'spec-author-process-quiescence' };
543
+ }
544
+ catch {
545
+ return { status: 'blocked', kind: 'safety', code: 'spec-author-process-absence-unconfirmed' };
546
+ }
547
+ }
548
+ return { status: 'retryable', code: 'spec-author-report-invalid' };
549
+ }
550
+ },
551
+ review: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
552
+ const attemptId = createAttemptId();
553
+ const sessionId = state.review.reviewer?.sessionId ?? randomUUID();
554
+ try {
555
+ const attempt = await prepareContainedAttempt({
556
+ orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
557
+ attemptId, operationId: 'spec-review', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
558
+ });
559
+ await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath });
560
+ const config = requireConfig(currentConfig);
561
+ const result = await containedProcess.run({
562
+ codexPath: config.codex.command, cwd: context.worktreePath, schemaPath: attempt.schemaPath,
563
+ reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
564
+ safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
565
+ parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
566
+ operationPolicy: attempt.policy, executionProfile: attempt.profile,
567
+ onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
568
+ prompt: [
569
+ `Package profile instructions: ${attempt.profile.developerInstructions}`,
570
+ `Follow the exact operation at ${attempt.operationPath}.`,
571
+ `Reviewer session ID: ${sessionId}. Review mode: ${mode}.`,
572
+ `Issue authority and frozen criteria: ${canonicalJson({ issue: context.issue, frozenCriteria: context.frozenCriteria })}.`,
573
+ `Immutable spec delivery state: ${canonicalJson(state)}.`,
574
+ 'Return only the package spec-review report. Do not edit files or external state.',
575
+ ].join('\n'),
576
+ }, signal);
577
+ if (result.kind === 'cancelled')
578
+ return { status: 'cancelled' };
579
+ if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
580
+ return { status: 'retryable', code: `spec-review-${result.kind}` };
581
+ if (result.kind !== 'completed' || result.report.kind !== 'available')
582
+ return { status: 'retryable', code: 'spec-review-report-invalid' };
583
+ const raw = decodeAgentReportForValidation(result.report.bytes);
584
+ if (raw.mode !== mode || raw.reviewerSessionId !== sessionId || !Array.isArray(raw.coverage) || !Array.isArray(raw.defects)
585
+ || !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts) || !Array.isArray(raw.acceptedRisks)
586
+ || typeof raw.coverageInvalidated !== 'boolean'
587
+ || !['approved', 'needs-work', 'rejected'].includes(raw.verdict))
588
+ return { status: 'retryable', code: 'spec-review-report-invalid' };
589
+ const target = state.revisions.at(-1);
590
+ const defects = validateCodeReviewDefects(raw.defects, target.revision);
591
+ const report = {
592
+ version: 1, targetRevision: target.revision, targetSha256: target.revisionSha256, mode,
593
+ verdict: raw.verdict, reviewer: { attemptId, sessionId },
594
+ coverage: raw.coverage, defects,
595
+ affectedDefectIds: raw.affectedDefectIds,
596
+ affectedContracts: raw.affectedContracts,
597
+ closureRequestSha256: mode === 'closure' ? state.review.closureRequestSha256 : null,
598
+ acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
599
+ };
600
+ return { status: 'completed', value: report, reportSha256: sha256(result.report.bytes) };
601
+ }
602
+ catch (error) {
603
+ if (error instanceof ProcessQuiescenceError) {
604
+ try {
605
+ await waitForProcessGroupAbsent(error.processGroupId);
606
+ return { status: 'retryable', code: 'spec-review-process-quiescence' };
607
+ }
608
+ catch {
609
+ return { status: 'blocked', kind: 'safety', code: 'spec-review-process-absence-unconfirmed' };
610
+ }
611
+ }
612
+ return { status: 'retryable', code: 'spec-review-report-invalid' };
613
+ }
614
+ },
615
+ recover: async ({ context, state, signal }) => {
616
+ const invocation = state.invocation;
617
+ if (!invocation || signal.aborted)
618
+ return signal.aborted ? { status: 'cancelled' } : { status: 'blocked', kind: 'safety', code: 'spec-recovery-invocation-missing' };
619
+ if (invocation.status === 'launched') {
620
+ try {
621
+ await waitForProcessGroupAbsent(invocation.processGroupId);
622
+ }
623
+ catch {
624
+ return { status: 'blocked', kind: 'safety', code: 'spec-process-absence-unconfirmed' };
625
+ }
626
+ }
627
+ if (!invocation.reportPath)
628
+ return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
629
+ let reportBytes;
630
+ try {
631
+ reportBytes = await readRegularFile(invocation.reportPath);
632
+ }
633
+ catch {
634
+ return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
635
+ }
636
+ if (invocation.purpose === 'author') {
637
+ if (!invocation.revisionPath)
638
+ return { status: 'retryable', code: 'spec-author-report-invalid' };
639
+ try {
640
+ const raw = decodeAgentReportForValidation(reportBytes);
641
+ const content = await readRegularFile(invocation.revisionPath);
642
+ if (raw.status !== 'ready' || raw.specPath !== invocation.revisionPath || raw.specSha256 !== sha256(content))
643
+ return { status: 'retryable', code: 'spec-author-report-invalid' };
644
+ return { status: 'completed', value: createSpecRevision({
645
+ revision: state.revisions.length + 1, path: invocation.revisionPath, content: content.toString('utf8'),
646
+ evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
647
+ author: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, previousRevision: state.revisions.at(-1) ?? null,
648
+ }) };
649
+ }
650
+ catch {
651
+ return { status: 'retryable', code: 'spec-author-report-invalid' };
652
+ }
653
+ }
654
+ try {
655
+ const raw = decodeAgentReportForValidation(reportBytes);
656
+ if (raw.mode !== invocation.mode || raw.reviewerSessionId !== invocation.sessionId || !Array.isArray(raw.coverage)
657
+ || !Array.isArray(raw.defects) || !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts)
658
+ || typeof raw.coverageInvalidated !== 'boolean' || !['approved', 'needs-work', 'rejected'].includes(raw.verdict)) {
659
+ return { status: 'retryable', code: 'spec-review-report-invalid' };
660
+ }
661
+ const defects = validateCodeReviewDefects(raw.defects, invocation.targetRevision);
662
+ return { status: 'completed', reportSha256: sha256(reportBytes), value: {
663
+ version: 1, targetRevision: invocation.targetRevision, targetSha256: invocation.targetSha256,
664
+ mode: invocation.mode, verdict: raw.verdict,
665
+ reviewer: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, coverage: raw.coverage, defects,
666
+ affectedDefectIds: raw.affectedDefectIds, affectedContracts: raw.affectedContracts,
667
+ closureRequestSha256: invocation.closureRequestSha256, acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
668
+ } };
669
+ }
670
+ catch {
671
+ return { status: 'retryable', code: 'spec-review-report-invalid' };
672
+ }
673
+ },
674
+ };
362
675
  const readConfig = async (requestedRoot) => {
363
676
  if (resolve(requestedRoot) !== targetRoot)
364
677
  throw new Error('runtime target root mismatch');
@@ -431,14 +744,13 @@ export function createV2Runtime(input) {
431
744
  readConfig,
432
745
  validateContainment: async (config) => {
433
746
  const certificate = await readContainmentCertificate(containmentCertificatePath(orchestratorHome));
434
- if (certificate.packageVersion !== input.packageVersion)
435
- throw new Error('containment package version mismatch');
436
- if (certificate.argvPolicySha256 !== containmentArgvPolicySha256())
437
- throw new Error('containment argv policy mismatch');
438
747
  const version = await commandExecutor(config.codex.command, ['--version']);
439
- if (version.exitCode !== 0 || version.stdout.trim() !== `codex-cli ${config.codex.requiredVersion}`) {
440
- throw new Error('current Codex version does not match the containment certificate');
441
- }
748
+ if (version.exitCode !== 0)
749
+ throw new Error('Codex version is unavailable');
750
+ assertContainmentCertificateMatchesRuntime(certificate, {
751
+ codexVersion: version.stdout.trim(),
752
+ argvPolicySha256: containmentArgvPolicySha256(),
753
+ });
442
754
  },
443
755
  ownerLock: {
444
756
  acquire: async ({ canonicalRepository }) => acquireOwnerLock({
@@ -475,7 +787,7 @@ export function createV2Runtime(input) {
475
787
  removeLabels: current.filter((label) => !labels.includes(label)),
476
788
  });
477
789
  },
478
- postComment: (issueNumber, body) => input.issues.postComment(issueNumber, body),
790
+ postComment: async (issueNumber, body) => { await input.issues.postComment(issueNumber, body); },
479
791
  },
480
792
  pullRequests: {
481
793
  findOpen: async ({ headBranch, baseBranch }) => {
@@ -489,7 +801,51 @@ export function createV2Runtime(input) {
489
801
  createDraft: async ({ title, body, headBranch, baseBranch }) => input.pullRequests.createDraftPullRequest({ title, body, headBranch, baseBranch }),
490
802
  },
491
803
  git,
804
+ routeCoordinator: {
805
+ run: ({ state, ...routeInput }) => new RouteCoordinator({
806
+ state,
807
+ operation: reportOperation,
808
+ createAttemptId: input.createAttemptId ?? randomUUID,
809
+ now,
810
+ createReceipt: ({ artifact, triage, review, decidedAt }) => {
811
+ if (artifact.status === 'blocked')
812
+ throw new Error('blocked triage cannot create a route receipt');
813
+ const receipt = {
814
+ version: 1,
815
+ route: artifact.status,
816
+ triage,
817
+ review,
818
+ artifact,
819
+ decisionSha256: '',
820
+ decidedAt,
821
+ assumptions: [...artifact.assumptions],
822
+ };
823
+ receipt.decisionSha256 = hashRouteDecision(receipt);
824
+ return validateRouteReceipt(receipt, triage.generationHash);
825
+ },
826
+ }).run(routeInput),
827
+ },
828
+ routeContinuations: {
829
+ direct: async () => ({ status: 'completed' }),
830
+ specRequired: (context, state, signal) => new SpecCoordinator({ state, operation: specOperation }).run(context, signal),
831
+ awaitingUser: async (context, state, signal) => {
832
+ const config = requireConfig(currentConfig);
833
+ return new WaitingHumanCoordinator({
834
+ issues: input.issues,
835
+ labels: {
836
+ auto: config.github.labels.auto.name,
837
+ running: config.github.labels.running.name,
838
+ blocked: config.github.labels.blocked.name,
839
+ review: config.github.labels.review.name,
840
+ waitingHuman: config.github.labels.waitingHuman.name,
841
+ },
842
+ now,
843
+ }).run(context, state, signal);
844
+ },
845
+ },
492
846
  implementationAgent,
847
+ implementationReviewer,
848
+ waitForReviewProcessAbsence: waitForProcessGroupAbsent,
493
849
  checks: {
494
850
  run: async ({ command, cwd, signal }) => runShellCheck(command, cwd, signal),
495
851
  },
@@ -503,9 +859,11 @@ export function createV2Runtime(input) {
503
859
  return { id: `evidence:${runId}:${code}`, path: relativePath };
504
860
  },
505
861
  packageVersion: input.packageVersion,
506
- skillHashes: structuredClone(input.skillHashes),
862
+ createWorkflowGeneration: input.createWorkflowGeneration,
863
+ verifyWorkflowGeneration,
507
864
  createRunId: input.createRunId ?? randomUUID,
508
865
  createProofId: input.createProofId ?? randomUUID,
866
+ createReviewSessionId: input.createAttemptId ?? randomUUID,
509
867
  now,
510
868
  signal: controller.signal,
511
869
  });
@@ -522,48 +880,27 @@ export function createV2Runtime(input) {
522
880
  };
523
881
  }
524
882
  async function acquireOwnerLock(input) {
525
- const token = randomUUID();
526
- const host = hostname();
527
- const record = {
528
- version: 1,
529
- token,
530
- canonicalRepository: input.canonicalRepository,
531
- host,
532
- bootId: input.bootId,
533
- pid: process.pid,
534
- acquiredAt: input.now(),
535
- };
536
- const repoKey = sha256(input.canonicalRepository);
537
883
  try {
538
- return await acquireExclusiveJsonFileLock({
539
- path: join(input.orchestratorHome, 'v2', 'owners', `${repoKey}.lock`),
540
- record,
541
- token,
542
- parse: parseOwnerLock,
543
- tokenOf: (value) => value.token,
544
- classifyExisting: (value) => value.host === host && value.bootId === input.bootId && input.processAlive(value.pid) ? 'wait' : 'block',
884
+ return await acquireOwnerControlLock({
885
+ orchestratorHome: input.orchestratorHome,
886
+ canonicalRepository: input.canonicalRepository,
887
+ bootId: input.bootId,
888
+ host: hostname(),
889
+ pid: process.pid,
890
+ now: input.now,
891
+ createToken: randomUUID,
892
+ processAlive: input.processAlive,
545
893
  });
546
894
  }
547
895
  catch (error) {
548
- if (error instanceof Error && /lock (?:owner|record|wait)/u.test(error.message))
896
+ if (error instanceof OwnerControlLockBlockedError) {
897
+ if (error.kind === 'live-contention')
898
+ throw new OwnerLockContentionError(error.message);
549
899
  throw new OwnerLockSafetyError(error.message);
900
+ }
550
901
  throw error;
551
902
  }
552
903
  }
553
- function parseOwnerLock(value) {
554
- if (typeof value !== 'object' || value === null || Array.isArray(value))
555
- throw new Error('owner lock is invalid');
556
- const keys = Object.keys(value).sort();
557
- const expected = ['version', 'token', 'canonicalRepository', 'host', 'bootId', 'pid', 'acquiredAt'].sort();
558
- if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index]))
559
- throw new Error('owner lock keys are invalid');
560
- const record = value;
561
- if (record.version !== 1 || !record.token || !record.canonicalRepository || !record.host || !record.bootId
562
- || !Number.isSafeInteger(record.pid) || record.pid <= 0 || Number.isNaN(Date.parse(record.acquiredAt))) {
563
- throw new Error('owner lock fields are invalid');
564
- }
565
- return record;
566
- }
567
904
  async function readRegularFile(path) {
568
905
  const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
569
906
  try {
@@ -647,6 +984,10 @@ function requireConfig(config) {
647
984
  throw new Error('runtime config is unavailable');
648
985
  return config;
649
986
  }
987
+ function requireCanonicalRepository(config) {
988
+ const value = requireConfig(config);
989
+ return `${value.github.owner.toLowerCase()}/${value.github.repo.toLowerCase()}`;
990
+ }
650
991
  function requireRuntimeString(value, field) {
651
992
  if (!value)
652
993
  throw new Error(`${field} is required for contained Codex agents`);
@@ -721,13 +1062,28 @@ async function prepareContainedAttempt(input) {
721
1062
  const runtimeRoot = join(resolve(input.orchestratorHome), 'v2', sha256(input.canonicalRepository));
722
1063
  const attemptRelativePath = `runs/${input.runId}/attempts/${input.attemptId}`;
723
1064
  const snapshot = await publishRuntimeAssetSnapshot({
724
- packageRoot: resolve(input.packageRoot),
1065
+ workflowGeneration: input.workflowGeneration,
725
1066
  runtimeRoot,
726
1067
  snapshotRelativePath: `${attemptRelativePath}/snapshot`,
727
- skill: input.skill,
1068
+ operation: input.operationId,
1069
+ bootId: input.bootId,
728
1070
  });
729
- if (snapshot.packageVersion !== input.expectedPackageVersion)
730
- throw new Error('runtime asset package version mismatch');
1071
+ const reportOnly = input.operationId === 'triage' || input.operationId === 'ambiguity-review'
1072
+ || input.operationId === 'code-review' || input.operationId === 'spec-review';
1073
+ if ((reportOnly
1074
+ ? snapshot.policy.sandboxMode !== 'read-only'
1075
+ || snapshot.policy.worktreeAccess !== 'read-only'
1076
+ || snapshot.policy.runnerPostcondition !== 'report-only'
1077
+ || snapshot.policy.writableRootClasses.length !== 0
1078
+ : snapshot.policy.sandboxMode !== 'workspace-write')
1079
+ || snapshot.policy.approvalCeiling !== 'never'
1080
+ || snapshot.policy.network !== 'deny'
1081
+ || snapshot.policy.networkHosts.length !== 0
1082
+ || snapshot.policy.mcpTools.length !== 0
1083
+ || snapshot.policy.externalWrite !== false) {
1084
+ throw new Error(`workflow operation policy exceeds containment: ${input.operationId}`);
1085
+ }
1086
+ const profile = parseWorkflowExecutionProfile(await readFile(snapshot.profilePath, 'utf8'), snapshot.policy);
731
1087
  const attemptRoot = join(runtimeRoot, attemptRelativePath);
732
1088
  const toolHome = join(attemptRoot, 'tool-home');
733
1089
  const tmpDir = join(attemptRoot, 'tmp');
@@ -740,11 +1096,15 @@ async function prepareContainedAttempt(input) {
740
1096
  throw error;
741
1097
  });
742
1098
  return {
743
- skillPath: snapshot.skillPath,
1099
+ workflowRoot: snapshot.snapshotRoot,
1100
+ operationPath: snapshot.operationPath,
1101
+ sourceSkillPath: snapshot.sourceSkillPath,
744
1102
  schemaPath: snapshot.schemaPath,
745
1103
  reportPath: join(attemptRoot, 'report.json'),
746
1104
  toolHome,
747
1105
  tmpDir,
1106
+ policy: structuredClone(snapshot.policy),
1107
+ profile,
748
1108
  };
749
1109
  }
750
1110
  async function artifactInventory(root, logicalRoot) {
@@ -832,6 +1192,146 @@ async function waitForProcessGroupAbsent(processGroupId) {
832
1192
  await new Promise((resolveWait) => setTimeout(resolveWait, 1_000));
833
1193
  }
834
1194
  }
1195
+ export async function materializeReportReadView(input) {
1196
+ await assertDisjointReadViewPaths(input.worktreePath, input.destination);
1197
+ const deniedPaths = normalizeReadViewDeniedPaths(input.deniedPaths);
1198
+ await rm(input.destination, { recursive: true, force: true });
1199
+ await mkdir(input.destination, { recursive: true, mode: 0o700 });
1200
+ await materializeCurrentReadView(input.worktreePath, input.destination, deniedPaths);
1201
+ await scrubReportReadView(input.destination);
1202
+ return input.destination;
1203
+ }
1204
+ async function assertDisjointReadViewPaths(worktreePath, destination) {
1205
+ const worktree = await realpath(resolve(worktreePath));
1206
+ const readView = await canonicalProspectivePath(destination);
1207
+ if (isPathWithin(worktree, readView) || isPathWithin(readView, worktree)) {
1208
+ throw new Error('report read-view destination must not overlap its source worktree');
1209
+ }
1210
+ }
1211
+ async function canonicalProspectivePath(path) {
1212
+ let current = resolve(path);
1213
+ const suffix = [];
1214
+ while (true) {
1215
+ try {
1216
+ return resolve(await realpath(current), ...suffix);
1217
+ }
1218
+ catch (error) {
1219
+ if (!isErrorCode(error, 'ENOENT'))
1220
+ throw error;
1221
+ const parent = dirname(current);
1222
+ if (parent === current)
1223
+ throw error;
1224
+ suffix.unshift(basename(current));
1225
+ current = parent;
1226
+ }
1227
+ }
1228
+ }
1229
+ function isPathWithin(parent, child) {
1230
+ const contained = relative(parent, child);
1231
+ return contained === '' || (contained !== '..' && !contained.startsWith('../') && !isAbsolute(contained));
1232
+ }
1233
+ function normalizeReadViewDeniedPaths(paths) {
1234
+ return paths.flatMap((path) => {
1235
+ if (isAbsolute(path))
1236
+ return [];
1237
+ if (path.length === 0 || posix.normalize(path) !== path
1238
+ || path.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
1239
+ throw new Error('report read-view denied path escapes destination');
1240
+ }
1241
+ return [path];
1242
+ });
1243
+ }
1244
+ async function materializeCurrentReadView(worktreePath, destination, deniedPaths) {
1245
+ const paths = new Set(await readGitPathList(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']));
1246
+ for (const path of [...paths].sort()) {
1247
+ if (isExcludedReadViewPath(path, deniedPaths))
1248
+ continue;
1249
+ const target = resolve(destination, path);
1250
+ const contained = relative(destination, target);
1251
+ if (contained === '' || contained === '..' || contained.startsWith('../') || isAbsolute(contained)) {
1252
+ throw new Error('report read-view path escapes destination');
1253
+ }
1254
+ const source = resolve(worktreePath, path);
1255
+ let metadata;
1256
+ try {
1257
+ metadata = await lstat(source);
1258
+ }
1259
+ catch (error) {
1260
+ if (!isErrorCode(error, 'ENOENT'))
1261
+ throw error;
1262
+ await rm(target, { recursive: true, force: true });
1263
+ continue;
1264
+ }
1265
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
1266
+ await rm(target, { recursive: true, force: true });
1267
+ continue;
1268
+ }
1269
+ await copyRegularFileNoFollow(source, target, metadata.mode);
1270
+ }
1271
+ }
1272
+ function isExcludedReadViewPath(path, deniedPaths) {
1273
+ if (path.split('/').some((segment) => segment.startsWith('.env')))
1274
+ return true;
1275
+ return deniedPaths.some((deniedPath) => path === deniedPath || path.startsWith(`${deniedPath}/`));
1276
+ }
1277
+ async function readGitPathList(worktreePath, args) {
1278
+ return new Promise((resolvePaths, reject) => {
1279
+ const child = spawn('git', ['-C', worktreePath, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
1280
+ const stdout = [];
1281
+ const stderr = [];
1282
+ child.once('error', reject);
1283
+ child.stdout.on('data', (chunk) => { stdout.push(chunk); });
1284
+ child.stderr.on('data', (chunk) => { stderr.push(chunk); });
1285
+ child.once('close', (code) => {
1286
+ if (code !== 0) {
1287
+ reject(new Error(`report read-view inventory failed: ${Buffer.concat(stderr).toString('utf8').slice(0, 8 * 1024)}`));
1288
+ return;
1289
+ }
1290
+ resolvePaths(Buffer.concat(stdout).toString('utf8').split('\0').filter((path) => path.length > 0));
1291
+ });
1292
+ });
1293
+ }
1294
+ async function copyRegularFileNoFollow(sourcePath, targetPath, mode) {
1295
+ const source = await open(sourcePath, constants.O_RDONLY | constants.O_NOFOLLOW);
1296
+ let target;
1297
+ try {
1298
+ const metadata = await source.stat();
1299
+ if (!metadata.isFile())
1300
+ throw new Error('report read-view source must be a regular file');
1301
+ await rm(targetPath, { recursive: true, force: true });
1302
+ await mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });
1303
+ target = await open(targetPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode & 0o777);
1304
+ const buffer = Buffer.allocUnsafe(64 * 1024);
1305
+ while (true) {
1306
+ const { bytesRead } = await source.read(buffer, 0, buffer.length, null);
1307
+ if (bytesRead === 0)
1308
+ break;
1309
+ let offset = 0;
1310
+ while (offset < bytesRead) {
1311
+ const { bytesWritten } = await target.write(buffer, offset, bytesRead - offset, null);
1312
+ offset += bytesWritten;
1313
+ }
1314
+ }
1315
+ }
1316
+ finally {
1317
+ await target?.close();
1318
+ await source.close();
1319
+ }
1320
+ }
1321
+ async function scrubReportReadView(directory) {
1322
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
1323
+ const path = join(directory, entry.name);
1324
+ if (entry.name.startsWith('.env') || entry.isSymbolicLink()) {
1325
+ await rm(path, { recursive: true, force: true });
1326
+ }
1327
+ else if (entry.isDirectory()) {
1328
+ await scrubReportReadView(path);
1329
+ }
1330
+ else if (!entry.isFile()) {
1331
+ await rm(path, { recursive: true, force: true });
1332
+ }
1333
+ }
1334
+ }
835
1335
  function isErrorCode(error, code) {
836
1336
  return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
837
1337
  }