codex-orchestrator 2.0.2 → 2.0.3

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 (211) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +12 -9
  3. package/dist/src/index.d.ts +10 -0
  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 +63 -7
  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/candidate-cli.d.ts +4 -0
  24. package/dist/src/v2/candidate-cli.d.ts.map +1 -1
  25. package/dist/src/v2/candidate-cli.js +26 -11
  26. package/dist/src/v2/candidate-cli.js.map +1 -1
  27. package/dist/src/v2/cli-contract.d.ts +1 -1
  28. package/dist/src/v2/cli-contract.d.ts.map +1 -1
  29. package/dist/src/v2/cli-contract.js +10 -0
  30. package/dist/src/v2/cli-contract.js.map +1 -1
  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 +11 -0
  38. package/dist/src/v2/codex-process.js.map +1 -1
  39. package/dist/src/v2/config.d.ts +2 -1
  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 +6 -0
  48. package/dist/src/v2/containment.d.ts.map +1 -1
  49. package/dist/src/v2/containment.js +40 -1
  50. package/dist/src/v2/containment.js.map +1 -1
  51. package/dist/src/v2/direct-delivery.d.ts +101 -0
  52. package/dist/src/v2/direct-delivery.d.ts.map +1 -0
  53. package/dist/src/v2/direct-delivery.js +547 -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/route-continuations.d.ts +32 -0
  68. package/dist/src/v2/route-continuations.d.ts.map +1 -0
  69. package/dist/src/v2/route-continuations.js +2 -0
  70. package/dist/src/v2/route-continuations.js.map +1 -0
  71. package/dist/src/v2/route-coordinator.d.ts +77 -0
  72. package/dist/src/v2/route-coordinator.d.ts.map +1 -0
  73. package/dist/src/v2/route-coordinator.js +370 -0
  74. package/dist/src/v2/route-coordinator.js.map +1 -0
  75. package/dist/src/v2/route-decision.d.ts +129 -0
  76. package/dist/src/v2/route-decision.d.ts.map +1 -0
  77. package/dist/src/v2/route-decision.js +400 -0
  78. package/dist/src/v2/route-decision.js.map +1 -0
  79. package/dist/src/v2/run-issue.d.ts +63 -2
  80. package/dist/src/v2/run-issue.d.ts.map +1 -1
  81. package/dist/src/v2/run-issue.js +906 -91
  82. package/dist/src/v2/run-issue.js.map +1 -1
  83. package/dist/src/v2/run-store.d.ts +25 -1
  84. package/dist/src/v2/run-store.d.ts.map +1 -1
  85. package/dist/src/v2/run-store.js +143 -3
  86. package/dist/src/v2/run-store.js.map +1 -1
  87. package/dist/src/v2/runtime-assets.d.ts +15 -13
  88. package/dist/src/v2/runtime-assets.d.ts.map +1 -1
  89. package/dist/src/v2/runtime-assets.js +263 -416
  90. package/dist/src/v2/runtime-assets.js.map +1 -1
  91. package/dist/src/v2/runtime.d.ts +14 -6
  92. package/dist/src/v2/runtime.d.ts.map +1 -1
  93. package/dist/src/v2/runtime.js +478 -56
  94. package/dist/src/v2/runtime.js.map +1 -1
  95. package/dist/src/v2/setup-cli.d.ts.map +1 -1
  96. package/dist/src/v2/setup-cli.js +1 -0
  97. package/dist/src/v2/setup-cli.js.map +1 -1
  98. package/dist/src/v2/setup-runtime.d.ts.map +1 -1
  99. package/dist/src/v2/setup-runtime.js +20 -72
  100. package/dist/src/v2/setup-runtime.js.map +1 -1
  101. package/dist/src/v2/setup.d.ts +4 -1
  102. package/dist/src/v2/setup.d.ts.map +1 -1
  103. package/dist/src/v2/setup.js +104 -1
  104. package/dist/src/v2/setup.js.map +1 -1
  105. package/dist/src/v2/spec-coordinator.d.ts +85 -0
  106. package/dist/src/v2/spec-coordinator.d.ts.map +1 -0
  107. package/dist/src/v2/spec-coordinator.js +88 -0
  108. package/dist/src/v2/spec-coordinator.js.map +1 -0
  109. package/dist/src/v2/spec-delivery.d.ts +143 -0
  110. package/dist/src/v2/spec-delivery.d.ts.map +1 -0
  111. package/dist/src/v2/spec-delivery.js +401 -0
  112. package/dist/src/v2/spec-delivery.js.map +1 -0
  113. package/dist/src/v2/triage-route.d.ts +68 -0
  114. package/dist/src/v2/triage-route.d.ts.map +1 -0
  115. package/dist/src/v2/triage-route.js +223 -0
  116. package/dist/src/v2/triage-route.js.map +1 -0
  117. package/dist/src/v2/waiting-human-coordinator.d.ts +49 -0
  118. package/dist/src/v2/waiting-human-coordinator.d.ts.map +1 -0
  119. package/dist/src/v2/waiting-human-coordinator.js +509 -0
  120. package/dist/src/v2/waiting-human-coordinator.js.map +1 -0
  121. package/dist/src/v2/waiting-human.d.ts +143 -0
  122. package/dist/src/v2/waiting-human.d.ts.map +1 -0
  123. package/dist/src/v2/waiting-human.js +408 -0
  124. package/dist/src/v2/waiting-human.js.map +1 -0
  125. package/dist/src/v2/workflow-assets.d.ts +90 -0
  126. package/dist/src/v2/workflow-assets.d.ts.map +1 -0
  127. package/dist/src/v2/workflow-assets.js +554 -0
  128. package/dist/src/v2/workflow-assets.js.map +1 -0
  129. package/docs/deep-dive.md +15 -8
  130. package/internal-workflow/docs/agents/artifact-review-loop.md +267 -0
  131. package/internal-workflow/docs/agents/bug-workflow-routing.md +24 -0
  132. package/internal-workflow/docs/agents/coding-skill-routing.md +203 -0
  133. package/internal-workflow/docs/agents/confidence-rubric.md +65 -0
  134. package/internal-workflow/docs/agents/contract-test-ledger.md +60 -0
  135. package/internal-workflow/docs/agents/implementation-review-loop.md +302 -0
  136. package/internal-workflow/docs/agents/review-gates.md +49 -0
  137. package/internal-workflow/docs/agents/review-protocol.md +170 -0
  138. package/internal-workflow/docs/agents/tool-usage.md +88 -0
  139. package/internal-workflow/manifest.json +1 -0
  140. package/internal-workflow/operations/acceptance-proof/SKILL.md +3 -0
  141. package/internal-workflow/operations/ambiguity-review/SKILL.md +3 -0
  142. package/internal-workflow/operations/cleanup-review/SKILL.md +3 -0
  143. package/internal-workflow/operations/code-review/SKILL.md +3 -0
  144. package/internal-workflow/operations/implementation/SKILL.md +3 -0
  145. package/internal-workflow/operations/spec-author/SKILL.md +3 -0
  146. package/internal-workflow/operations/spec-implementation/SKILL.md +3 -0
  147. package/internal-workflow/operations/spec-review/SKILL.md +3 -0
  148. package/internal-workflow/operations/triage/SKILL.md +3 -0
  149. package/internal-workflow/profiles/analyst_deep.toml +9 -0
  150. package/internal-workflow/profiles/implementer_deep.toml +9 -0
  151. package/internal-workflow/profiles/implementer_standard.toml +9 -0
  152. package/internal-workflow/profiles/proof_agent.toml +8 -0
  153. package/internal-workflow/profiles/researcher_standard.toml +9 -0
  154. package/internal-workflow/profiles/reviewer_deep.toml +9 -0
  155. package/internal-workflow/profiles/reviewer_fast.toml +9 -0
  156. package/internal-workflow/profiles/reviewer_standard.toml +9 -0
  157. package/internal-workflow/schemas/ambiguity-review-v1.json +1 -0
  158. package/internal-workflow/schemas/code-review-v1.json +1 -0
  159. package/internal-workflow/schemas/implementation-report-v1.json +1 -0
  160. package/internal-workflow/schemas/proof-report-v1.json +1 -0
  161. package/internal-workflow/schemas/spec-author-v1.json +1 -0
  162. package/internal-workflow/schemas/spec-review-v1.json +30 -0
  163. package/internal-workflow/schemas/triage-route-v1.json +1 -0
  164. package/internal-workflow/skills/acceptance-proof/agents/openai.yaml +6 -0
  165. package/internal-workflow/skills/agent-auto/agents/openai.yaml +6 -0
  166. package/internal-workflow/skills/cleanup-review/SKILL.md +84 -0
  167. package/internal-workflow/skills/cleanup-review/agents/openai.yaml +6 -0
  168. package/internal-workflow/skills/code-review/SKILL.md +257 -0
  169. package/internal-workflow/skills/code-review/agents/openai.yaml +4 -0
  170. package/internal-workflow/skills/code-review/references/bug-classes.md +56 -0
  171. package/internal-workflow/skills/code-review/references/framework-lenses.md +34 -0
  172. package/internal-workflow/skills/code-review/references/targeted-recipes.md +49 -0
  173. package/internal-workflow/skills/codebase-design/DEEPENING.md +35 -0
  174. package/internal-workflow/skills/codebase-design/DESIGN-IT-TWICE.md +50 -0
  175. package/internal-workflow/skills/codebase-design/SKILL.md +82 -0
  176. package/internal-workflow/skills/codebase-design/agents/openai.yaml +6 -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 +93 -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 +211 -0
  185. package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +6 -0
  186. package/internal-workflow/skills/research/SKILL.md +107 -0
  187. package/internal-workflow/skills/research/agents/openai.yaml +6 -0
  188. package/internal-workflow/skills/small-task-implementer/SKILL.md +97 -0
  189. package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +6 -0
  190. package/internal-workflow/skills/spec-implementer/SKILL.md +197 -0
  191. package/internal-workflow/skills/spec-implementer/agents/openai.yaml +6 -0
  192. package/internal-workflow/skills/tdd/SKILL.md +59 -0
  193. package/internal-workflow/skills/tdd/agents/openai.yaml +6 -0
  194. package/internal-workflow/skills/tdd/interface-design.md +31 -0
  195. package/internal-workflow/skills/tdd/mocking.md +59 -0
  196. package/internal-workflow/skills/tdd/refactoring.md +10 -0
  197. package/internal-workflow/skills/tdd/tests.md +77 -0
  198. package/internal-workflow/skills/triage/AGENT-BRIEF.md +192 -0
  199. package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +101 -0
  200. package/internal-workflow/skills/triage/SKILL.md +134 -0
  201. package/internal-workflow/skills/triage/agents/openai.yaml +6 -0
  202. package/internal-workflow/skills/ui-evidence-proof/SKILL.md +123 -0
  203. package/internal-workflow/skills/ui-evidence-proof/agents/openai.yaml +6 -0
  204. package/package.json +6 -3
  205. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/SKILL.md +0 -0
  206. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/android.md +0 -0
  207. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/browser.md +0 -0
  208. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/ios.md +0 -0
  209. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/android-lease.mjs +0 -0
  210. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/ios-lease.mjs +0 -0
  211. /package/{internal-skills → internal-workflow/skills}/agent-auto/SKILL.md +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 { 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';
15
+ import { WaitingHumanCoordinator } from './waiting-human-coordinator.js';
14
16
  import { 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');
@@ -475,7 +788,7 @@ export function createV2Runtime(input) {
475
788
  removeLabels: current.filter((label) => !labels.includes(label)),
476
789
  });
477
790
  },
478
- postComment: (issueNumber, body) => input.issues.postComment(issueNumber, body),
791
+ postComment: async (issueNumber, body) => { await input.issues.postComment(issueNumber, body); },
479
792
  },
480
793
  pullRequests: {
481
794
  findOpen: async ({ headBranch, baseBranch }) => {
@@ -489,7 +802,51 @@ export function createV2Runtime(input) {
489
802
  createDraft: async ({ title, body, headBranch, baseBranch }) => input.pullRequests.createDraftPullRequest({ title, body, headBranch, baseBranch }),
490
803
  },
491
804
  git,
805
+ routeCoordinator: {
806
+ run: ({ state, ...routeInput }) => new RouteCoordinator({
807
+ state,
808
+ operation: reportOperation,
809
+ createAttemptId: input.createAttemptId ?? randomUUID,
810
+ now,
811
+ createReceipt: ({ artifact, triage, review, decidedAt }) => {
812
+ if (artifact.status === 'blocked')
813
+ throw new Error('blocked triage cannot create a route receipt');
814
+ const receipt = {
815
+ version: 1,
816
+ route: artifact.status,
817
+ triage,
818
+ review,
819
+ artifact,
820
+ decisionSha256: '',
821
+ decidedAt,
822
+ assumptions: [...artifact.assumptions],
823
+ };
824
+ receipt.decisionSha256 = hashRouteDecision(receipt);
825
+ return validateRouteReceipt(receipt, triage.generationHash);
826
+ },
827
+ }).run(routeInput),
828
+ },
829
+ routeContinuations: {
830
+ direct: async () => ({ status: 'completed' }),
831
+ specRequired: (context, state, signal) => new SpecCoordinator({ state, operation: specOperation }).run(context, signal),
832
+ awaitingUser: async (context, state, signal) => {
833
+ const config = requireConfig(currentConfig);
834
+ return new WaitingHumanCoordinator({
835
+ issues: input.issues,
836
+ labels: {
837
+ auto: config.github.labels.auto.name,
838
+ running: config.github.labels.running.name,
839
+ blocked: config.github.labels.blocked.name,
840
+ review: config.github.labels.review.name,
841
+ waitingHuman: config.github.labels.waitingHuman.name,
842
+ },
843
+ now,
844
+ }).run(context, state, signal);
845
+ },
846
+ },
492
847
  implementationAgent,
848
+ implementationReviewer,
849
+ waitForReviewProcessAbsence: waitForProcessGroupAbsent,
493
850
  checks: {
494
851
  run: async ({ command, cwd, signal }) => runShellCheck(command, cwd, signal),
495
852
  },
@@ -503,9 +860,11 @@ export function createV2Runtime(input) {
503
860
  return { id: `evidence:${runId}:${code}`, path: relativePath };
504
861
  },
505
862
  packageVersion: input.packageVersion,
506
- skillHashes: structuredClone(input.skillHashes),
863
+ createWorkflowGeneration: input.createWorkflowGeneration,
864
+ verifyWorkflowGeneration,
507
865
  createRunId: input.createRunId ?? randomUUID,
508
866
  createProofId: input.createProofId ?? randomUUID,
867
+ createReviewSessionId: input.createAttemptId ?? randomUUID,
509
868
  now,
510
869
  signal: controller.signal,
511
870
  });
@@ -522,48 +881,27 @@ export function createV2Runtime(input) {
522
881
  };
523
882
  }
524
883
  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
884
  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',
885
+ return await acquireOwnerControlLock({
886
+ orchestratorHome: input.orchestratorHome,
887
+ canonicalRepository: input.canonicalRepository,
888
+ bootId: input.bootId,
889
+ host: hostname(),
890
+ pid: process.pid,
891
+ now: input.now,
892
+ createToken: randomUUID,
893
+ processAlive: input.processAlive,
545
894
  });
546
895
  }
547
896
  catch (error) {
548
- if (error instanceof Error && /lock (?:owner|record|wait)/u.test(error.message))
897
+ if (error instanceof OwnerControlLockBlockedError) {
898
+ if (error.kind === 'live-contention')
899
+ throw new OwnerLockContentionError(error.message);
549
900
  throw new OwnerLockSafetyError(error.message);
901
+ }
550
902
  throw error;
551
903
  }
552
904
  }
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
905
  async function readRegularFile(path) {
568
906
  const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
569
907
  try {
@@ -647,6 +985,10 @@ function requireConfig(config) {
647
985
  throw new Error('runtime config is unavailable');
648
986
  return config;
649
987
  }
988
+ function requireCanonicalRepository(config) {
989
+ const value = requireConfig(config);
990
+ return `${value.github.owner.toLowerCase()}/${value.github.repo.toLowerCase()}`;
991
+ }
650
992
  function requireRuntimeString(value, field) {
651
993
  if (!value)
652
994
  throw new Error(`${field} is required for contained Codex agents`);
@@ -721,13 +1063,28 @@ async function prepareContainedAttempt(input) {
721
1063
  const runtimeRoot = join(resolve(input.orchestratorHome), 'v2', sha256(input.canonicalRepository));
722
1064
  const attemptRelativePath = `runs/${input.runId}/attempts/${input.attemptId}`;
723
1065
  const snapshot = await publishRuntimeAssetSnapshot({
724
- packageRoot: resolve(input.packageRoot),
1066
+ workflowGeneration: input.workflowGeneration,
725
1067
  runtimeRoot,
726
1068
  snapshotRelativePath: `${attemptRelativePath}/snapshot`,
727
- skill: input.skill,
1069
+ operation: input.operationId,
1070
+ bootId: input.bootId,
728
1071
  });
729
- if (snapshot.packageVersion !== input.expectedPackageVersion)
730
- throw new Error('runtime asset package version mismatch');
1072
+ const reportOnly = input.operationId === 'triage' || input.operationId === 'ambiguity-review'
1073
+ || input.operationId === 'cleanup-review' || input.operationId === 'code-review' || input.operationId === 'spec-review';
1074
+ if ((reportOnly
1075
+ ? snapshot.policy.sandboxMode !== 'read-only'
1076
+ || snapshot.policy.worktreeAccess !== 'read-only'
1077
+ || snapshot.policy.runnerPostcondition !== 'report-only'
1078
+ || snapshot.policy.writableRootClasses.length !== 0
1079
+ : snapshot.policy.sandboxMode !== 'workspace-write')
1080
+ || snapshot.policy.approvalCeiling !== 'never'
1081
+ || snapshot.policy.network !== 'deny'
1082
+ || snapshot.policy.networkHosts.length !== 0
1083
+ || snapshot.policy.mcpTools.length !== 0
1084
+ || snapshot.policy.externalWrite !== false) {
1085
+ throw new Error(`workflow operation policy exceeds containment: ${input.operationId}`);
1086
+ }
1087
+ const profile = parseWorkflowExecutionProfile(await readFile(snapshot.profilePath, 'utf8'), snapshot.policy);
731
1088
  const attemptRoot = join(runtimeRoot, attemptRelativePath);
732
1089
  const toolHome = join(attemptRoot, 'tool-home');
733
1090
  const tmpDir = join(attemptRoot, 'tmp');
@@ -740,11 +1097,15 @@ async function prepareContainedAttempt(input) {
740
1097
  throw error;
741
1098
  });
742
1099
  return {
743
- skillPath: snapshot.skillPath,
1100
+ workflowRoot: snapshot.snapshotRoot,
1101
+ operationPath: snapshot.operationPath,
1102
+ sourceSkillPath: snapshot.sourceSkillPath,
744
1103
  schemaPath: snapshot.schemaPath,
745
1104
  reportPath: join(attemptRoot, 'report.json'),
746
1105
  toolHome,
747
1106
  tmpDir,
1107
+ policy: structuredClone(snapshot.policy),
1108
+ profile,
748
1109
  };
749
1110
  }
750
1111
  async function artifactInventory(root, logicalRoot) {
@@ -832,6 +1193,67 @@ async function waitForProcessGroupAbsent(processGroupId) {
832
1193
  await new Promise((resolveWait) => setTimeout(resolveWait, 1_000));
833
1194
  }
834
1195
  }
1196
+ export async function materializeReportReadView(input) {
1197
+ await rm(input.destination, { recursive: true, force: true });
1198
+ await mkdir(input.destination, { recursive: true, mode: 0o700 });
1199
+ await extractHeadArchive(input.worktreePath, input.destination);
1200
+ for (const relativePath of input.deniedPaths) {
1201
+ if (isAbsolute(relativePath))
1202
+ continue;
1203
+ const deniedPath = resolve(input.destination, relativePath);
1204
+ const contained = relative(input.destination, deniedPath);
1205
+ if (contained === '' || contained === '..' || contained.startsWith('../') || isAbsolute(contained)) {
1206
+ throw new Error('report read-view denied path escapes destination');
1207
+ }
1208
+ await rm(deniedPath, { recursive: true, force: true });
1209
+ }
1210
+ await scrubReportReadView(input.destination);
1211
+ return input.destination;
1212
+ }
1213
+ async function extractHeadArchive(worktreePath, destination) {
1214
+ await new Promise((resolveExtract, reject) => {
1215
+ const archive = spawn('git', ['-C', worktreePath, 'archive', '--format=tar', 'HEAD'], { stdio: ['ignore', 'pipe', 'pipe'] });
1216
+ const extract = spawn('tar', ['-xf', '-', '-C', destination], { stdio: ['pipe', 'ignore', 'pipe'] });
1217
+ const failures = [];
1218
+ archive.once('error', reject);
1219
+ extract.once('error', reject);
1220
+ archive.stderr.on('data', (chunk) => {
1221
+ if (failures.join('').length < 8 * 1024)
1222
+ failures.push(chunk.toString('utf8'));
1223
+ });
1224
+ extract.stderr.on('data', (chunk) => {
1225
+ if (failures.join('').length < 8 * 1024)
1226
+ failures.push(chunk.toString('utf8'));
1227
+ });
1228
+ archive.stdout.pipe(extract.stdin);
1229
+ let archiveCode = null;
1230
+ let extractCode = null;
1231
+ const settle = () => {
1232
+ if (archiveCode === null || extractCode === null)
1233
+ return;
1234
+ if (archiveCode !== 0 || extractCode !== 0)
1235
+ reject(new Error(`report read-view archive failed: ${failures.join('').slice(0, 8 * 1024)}`));
1236
+ else
1237
+ resolveExtract();
1238
+ };
1239
+ archive.once('close', (code) => { archiveCode = code ?? 1; settle(); });
1240
+ extract.once('close', (code) => { extractCode = code ?? 1; settle(); });
1241
+ });
1242
+ }
1243
+ async function scrubReportReadView(directory) {
1244
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
1245
+ const path = join(directory, entry.name);
1246
+ if (entry.name === '.env' || entry.name.startsWith('.env.') || entry.isSymbolicLink()) {
1247
+ await rm(path, { recursive: true, force: true });
1248
+ }
1249
+ else if (entry.isDirectory()) {
1250
+ await scrubReportReadView(path);
1251
+ }
1252
+ else if (!entry.isFile()) {
1253
+ await rm(path, { recursive: true, force: true });
1254
+ }
1255
+ }
1256
+ }
835
1257
  function isErrorCode(error, code) {
836
1258
  return typeof error === 'object' && error !== null && 'code' in error && error.code === code;
837
1259
  }