codex-orchestrator 2.0.10 → 2.0.11

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 (109) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +26 -1
  3. package/dist/src/index.d.ts +3 -2
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +1 -1
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/v2/acceptance-proof.d.ts +29 -16
  8. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  9. package/dist/src/v2/acceptance-proof.js +159 -129
  10. package/dist/src/v2/acceptance-proof.js.map +1 -1
  11. package/dist/src/v2/adapters/command.d.ts +1 -0
  12. package/dist/src/v2/adapters/command.d.ts.map +1 -1
  13. package/dist/src/v2/adapters/command.js +1 -1
  14. package/dist/src/v2/adapters/command.js.map +1 -1
  15. package/dist/src/v2/adapters/gh-pull-request-adapter.js +1 -0
  16. package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
  17. package/dist/src/v2/adapters/pull-requests.d.ts +1 -0
  18. package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
  19. package/dist/src/v2/adapters/pull-requests.js +1 -0
  20. package/dist/src/v2/adapters/pull-requests.js.map +1 -1
  21. package/dist/src/v2/adapters/worktree.js +3 -3
  22. package/dist/src/v2/adapters/worktree.js.map +1 -1
  23. package/dist/src/v2/atomic-store.d.ts +15 -0
  24. package/dist/src/v2/atomic-store.d.ts.map +1 -1
  25. package/dist/src/v2/atomic-store.js +55 -8
  26. package/dist/src/v2/atomic-store.js.map +1 -1
  27. package/dist/src/v2/candidate.d.ts +152 -0
  28. package/dist/src/v2/candidate.d.ts.map +1 -0
  29. package/dist/src/v2/candidate.js +129 -0
  30. package/dist/src/v2/candidate.js.map +1 -0
  31. package/dist/src/v2/checked-change.d.ts +37 -6
  32. package/dist/src/v2/checked-change.d.ts.map +1 -1
  33. package/dist/src/v2/checked-change.js +71 -0
  34. package/dist/src/v2/checked-change.js.map +1 -1
  35. package/dist/src/v2/cli.d.ts +6 -0
  36. package/dist/src/v2/cli.d.ts.map +1 -1
  37. package/dist/src/v2/cli.js +57 -21
  38. package/dist/src/v2/cli.js.map +1 -1
  39. package/dist/src/v2/codex-process.d.ts +1 -0
  40. package/dist/src/v2/codex-process.d.ts.map +1 -1
  41. package/dist/src/v2/codex-process.js +12 -1
  42. package/dist/src/v2/codex-process.js.map +1 -1
  43. package/dist/src/v2/contained-report-operation.d.ts +196 -30
  44. package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
  45. package/dist/src/v2/contained-report-operation.js +464 -150
  46. package/dist/src/v2/contained-report-operation.js.map +1 -1
  47. package/dist/src/v2/direct-delivery.d.ts +10 -28
  48. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  49. package/dist/src/v2/direct-delivery.js +44 -114
  50. package/dist/src/v2/direct-delivery.js.map +1 -1
  51. package/dist/src/v2/implementation-reviewer.d.ts +4 -24
  52. package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
  53. package/dist/src/v2/implementation-reviewer.js +29 -35
  54. package/dist/src/v2/implementation-reviewer.js.map +1 -1
  55. package/dist/src/v2/owner-control-lock.d.ts +13 -1
  56. package/dist/src/v2/owner-control-lock.d.ts.map +1 -1
  57. package/dist/src/v2/owner-control-lock.js +66 -7
  58. package/dist/src/v2/owner-control-lock.js.map +1 -1
  59. package/dist/src/v2/proof-report.d.ts +2 -1
  60. package/dist/src/v2/proof-report.d.ts.map +1 -1
  61. package/dist/src/v2/proof-report.js +6 -40
  62. package/dist/src/v2/proof-report.js.map +1 -1
  63. package/dist/src/v2/proof-store.d.ts +19 -7
  64. package/dist/src/v2/proof-store.d.ts.map +1 -1
  65. package/dist/src/v2/proof-store.js +162 -41
  66. package/dist/src/v2/proof-store.js.map +1 -1
  67. package/dist/src/v2/review-feedback-coordinator.d.ts +7 -1
  68. package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -1
  69. package/dist/src/v2/review-feedback-coordinator.js +97 -70
  70. package/dist/src/v2/review-feedback-coordinator.js.map +1 -1
  71. package/dist/src/v2/review-feedback.d.ts +0 -18
  72. package/dist/src/v2/review-feedback.d.ts.map +1 -1
  73. package/dist/src/v2/review-feedback.js +9 -34
  74. package/dist/src/v2/review-feedback.js.map +1 -1
  75. package/dist/src/v2/route-coordinator.d.ts +7 -12
  76. package/dist/src/v2/route-coordinator.d.ts.map +1 -1
  77. package/dist/src/v2/route-coordinator.js +33 -128
  78. package/dist/src/v2/route-coordinator.js.map +1 -1
  79. package/dist/src/v2/route-decision.d.ts +0 -9
  80. package/dist/src/v2/route-decision.d.ts.map +1 -1
  81. package/dist/src/v2/route-decision.js +7 -27
  82. package/dist/src/v2/route-decision.js.map +1 -1
  83. package/dist/src/v2/run-issue.d.ts +64 -26
  84. package/dist/src/v2/run-issue.d.ts.map +1 -1
  85. package/dist/src/v2/run-issue.js +1298 -620
  86. package/dist/src/v2/run-issue.js.map +1 -1
  87. package/dist/src/v2/run-store.d.ts +39 -25
  88. package/dist/src/v2/run-store.d.ts.map +1 -1
  89. package/dist/src/v2/run-store.js +469 -88
  90. package/dist/src/v2/run-store.js.map +1 -1
  91. package/dist/src/v2/runtime.d.ts +61 -36
  92. package/dist/src/v2/runtime.d.ts.map +1 -1
  93. package/dist/src/v2/runtime.js +992 -402
  94. package/dist/src/v2/runtime.js.map +1 -1
  95. package/dist/src/v2/spec-coordinator.d.ts +16 -28
  96. package/dist/src/v2/spec-coordinator.d.ts.map +1 -1
  97. package/dist/src/v2/spec-coordinator.js +81 -43
  98. package/dist/src/v2/spec-coordinator.js.map +1 -1
  99. package/dist/src/v2/spec-delivery.d.ts +3 -34
  100. package/dist/src/v2/spec-delivery.d.ts.map +1 -1
  101. package/dist/src/v2/spec-delivery.js +42 -107
  102. package/dist/src/v2/spec-delivery.js.map +1 -1
  103. package/docs/deep-dive.md +52 -14
  104. package/internal-workflow/evals/coding-skill-evals.json +58 -17
  105. package/internal-workflow/manifest.json +1 -1
  106. package/internal-workflow/schemas/proof-report-v1.json +1 -1
  107. package/internal-workflow/skills/code-review/SKILL.md +18 -0
  108. package/internal-workflow/skills/spec-implementer/SKILL.md +8 -1
  109. package/package.json +1 -1
@@ -1,17 +1,17 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
- import { spawn } from 'node:child_process';
4
- import { access, lstat, mkdir, open, readFile, readdir, readlink, realpath, rm } from 'node:fs/promises';
5
- import { homedir, hostname } from 'node:os';
3
+ import { execFile, spawn } from 'node:child_process';
4
+ import { access, link, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, unlink } from 'node:fs/promises';
5
+ import { homedir, hostname, platform } from 'node:os';
6
6
  import { basename, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
7
7
  import { writeDurableAtomicFile } from './adapters/durable-atomic-file.js';
8
8
  import { GitWorktreeManager } from './adapters/worktree.js';
9
9
  import { ReviewFeedbackCoordinator } from './review-feedback-coordinator.js';
10
10
  import { defaultProcessExecutor } from './adapters/command.js';
11
11
  import { RunnerAndroidProofController } from './android-proof-runner.js';
12
- import { AcceptanceProof, ProofQuiescenceError } from './acceptance-proof.js';
12
+ import { AcceptanceProof, CandidateProofInspectionError } from './acceptance-proof.js';
13
13
  import { createCheckedChangeCapabilities } from './checked-change.js';
14
- import { InjectedContainedReportOperation } from './contained-report-operation.js';
14
+ import { InjectedContainedMutableOperation, InjectedContainedReportOperation, } from './contained-report-operation.js';
15
15
  import { ContainedImplementationReviewer } from './implementation-reviewer.js';
16
16
  import { parseAgentAutoConfig } from './config.js';
17
17
  import { WaitingHumanCoordinator } from './waiting-human-coordinator.js';
@@ -31,15 +31,393 @@ import { hashRouteDecision, validateRouteReceipt } from './route-decision.js';
31
31
  import { OwnerLockContentionError, OwnerLockSafetyError, RunIssue } from './run-issue.js';
32
32
  import { FileRunRecordWriter } from './run-store.js';
33
33
  import { parseWorkflowExecutionProfile, verifyWorkflowGeneration, } from './workflow-assets.js';
34
+ import { candidateBindingId, candidateBindingSeed, candidateRef, } from './candidate.js';
34
35
  export class LocalGitRunIssueAdapter {
35
36
  executor;
37
+ repositoryRoot;
36
38
  worktrees;
37
- constructor(executor = defaultProcessExecutor) {
39
+ candidateRepositories = new Map();
40
+ candidateV2;
41
+ constructor(executor = defaultProcessExecutor, repositoryRoot) {
38
42
  this.executor = executor;
43
+ this.repositoryRoot = repositoryRoot;
39
44
  this.worktrees = new GitWorktreeManager(executor);
45
+ this.candidateV2 = {
46
+ reconcileOrphans: (input) => this.reconcileCandidateOrphans(input),
47
+ captureAndPin: (input) => this.captureAndPinCandidate(input),
48
+ inspectPin: (binding) => this.inspectCandidatePin(binding),
49
+ normalizeSharedIndex: (input) => this.candidateOperation('candidate-io-failed', async () => {
50
+ if (await this.getHead(input.worktreePath) !== input.expectedHeadSha)
51
+ throw new Error('candidate expected HEAD diverged');
52
+ await this.git(['-C', input.worktreePath, 'read-tree', input.expectedHeadSha]);
53
+ }),
54
+ prepareExecution: (input) => this.prepareCandidateExecution(input),
55
+ markExecutionLaunched: ({ lease, pid, processGroupId, launchedAt }) => ({
56
+ ...lease, phase: 'launched', pid, processGroupId, launchedAt,
57
+ }),
58
+ inspectExecution: (input) => this.inspectCandidateExecution(input),
59
+ removeExecution: (input) => this.removeCandidateExecution(input.lease),
60
+ copyProofArtifacts: (input) => this.copyCandidateProofArtifacts(input),
61
+ createOrObserveCommit: (input) => this.createOrObserveCandidateCommit(input),
62
+ releasePin: (input) => this.releaseCandidatePin(input.binding, input.expectedPinnedCommitSha),
63
+ };
64
+ }
65
+ async reconcileCandidateOrphans(input) {
66
+ return this.candidateOperation('candidate-io-failed', async () => {
67
+ const root = (await this.git(['-C', input.repositoryRoot, 'rev-parse', '--show-toplevel'])).trim();
68
+ const activeRefs = new Set(input.activeCandidateRefs);
69
+ const adoptableRefs = new Set();
70
+ for (const pending of input.pendingCandidates) {
71
+ const first = await this.captureCandidateTree(pending.worktreePath, pending.expectedHeadSha, pending.artifactDir);
72
+ const second = await this.captureCandidateTree(pending.worktreePath, pending.expectedHeadSha, pending.artifactDir);
73
+ if (canonicalJson(first) !== canonicalJson(second))
74
+ continue;
75
+ const bindingId = candidateBindingId({
76
+ bindingSeed: candidateBindingSeed(pending.runId, pending.boundary),
77
+ expectedHeadSha: pending.expectedHeadSha,
78
+ candidateTreeSha: first.treeSha,
79
+ canonicalChangedFiles: first.changedFiles,
80
+ sourceWorktreeIdentity: first.worktreeIdentity,
81
+ });
82
+ const ref = candidateRef(pending.runId, bindingId);
83
+ const commit = await this.resolveOptionalRef(root, ref);
84
+ if (commit && await this.commitMatches(root, commit, pending.expectedHeadSha, first.treeSha))
85
+ adoptableRefs.add(ref);
86
+ }
87
+ const refs = (await this.git(['-C', root, 'for-each-ref', '--format=%(refname) %(objectname)', 'refs/codex-orchestrator/candidates/']))
88
+ .split('\n').filter(Boolean);
89
+ for (const row of refs) {
90
+ const separator = row.indexOf(' ');
91
+ if (separator < 1)
92
+ throw new Error('orphan candidate ref row is malformed');
93
+ const ref = row.slice(0, separator);
94
+ const sha = row.slice(separator + 1);
95
+ if (!activeRefs.has(ref) && !adoptableRefs.has(ref)) {
96
+ const deleted = await this.executor('git', ['-C', root, 'update-ref', '-d', ref, sha]);
97
+ if (deleted.exitCode !== 0)
98
+ throw new Error('orphan candidate ref cleanup failed');
99
+ }
100
+ }
101
+ const executionRoot = canonicalFilesystemPath(resolve(input.workspaceRoot, '.candidate-executions'));
102
+ const activeExecutions = new Map(input.activeExecutions.map((execution) => [
103
+ canonicalFilesystemPath(execution.path), execution.candidateCommitSha,
104
+ ]));
105
+ for (const worktree of await this.worktrees.listWorktrees(root)) {
106
+ const path = canonicalFilesystemPath(worktree.path);
107
+ const relativePath = relative(executionRoot, path);
108
+ if (relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))) {
109
+ const expectedCommit = activeExecutions.get(path);
110
+ if (expectedCommit) {
111
+ if (worktree.branch !== undefined || await this.getHead(path).catch(() => '') !== expectedCommit) {
112
+ throw new Error('persisted candidate execution owner diverged');
113
+ }
114
+ }
115
+ else {
116
+ if (worktree.branch !== undefined)
117
+ throw new Error('orphan candidate execution unexpectedly owns a branch');
118
+ await this.git(['-C', root, 'worktree', 'remove', '--force', path]);
119
+ }
120
+ }
121
+ }
122
+ });
123
+ }
124
+ async captureAndPinCandidate(input) {
125
+ try {
126
+ const first = await this.captureCandidateTree(input.worktreePath, input.expectedHeadSha, input.artifactDir);
127
+ const second = await this.captureCandidateTree(input.worktreePath, input.expectedHeadSha, input.artifactDir);
128
+ if (canonicalJson(first) !== canonicalJson(second)) {
129
+ return { kind: 'failed', code: 'candidate-unstable', detailSha256: sha256(canonicalJson({ first, second })) };
130
+ }
131
+ const bindingSeed = candidateBindingSeed(input.runId, input.boundary);
132
+ const bindingId = candidateBindingId({
133
+ bindingSeed,
134
+ expectedHeadSha: input.expectedHeadSha,
135
+ candidateTreeSha: first.treeSha,
136
+ canonicalChangedFiles: first.changedFiles,
137
+ sourceWorktreeIdentity: first.worktreeIdentity,
138
+ });
139
+ const ref = candidateRef(input.runId, bindingId);
140
+ const root = (await this.git(['-C', input.worktreePath, 'rev-parse', '--show-toplevel'])).trim();
141
+ this.candidateRepositories.set(ref, root);
142
+ let commitSha = await this.resolveOptionalRef(root, ref);
143
+ if (commitSha) {
144
+ if (!await this.commitMatches(root, commitSha, input.expectedHeadSha, first.treeSha))
145
+ throw new Error('existing candidate ref diverged');
146
+ }
147
+ else {
148
+ commitSha = (await this.git([
149
+ '-C', root, '-c', 'user.name=codex-orchestrator', '-c', 'user.email=codex-orchestrator@users.noreply.github.com',
150
+ 'commit-tree', first.treeSha, '-p', input.expectedHeadSha, '-m', `codex-orchestrator candidate ${bindingId}`,
151
+ ])).trim();
152
+ const update = await this.executor('git', ['-C', root, 'update-ref', ref, commitSha, '0'.repeat(commitSha.length)]);
153
+ if (update.exitCode !== 0) {
154
+ const observed = await this.resolveOptionalRef(root, ref);
155
+ if (!observed || !await this.commitMatches(root, observed, input.expectedHeadSha, first.treeSha)) {
156
+ return { kind: 'failed', code: 'candidate-ref-update-unknown', detailSha256: sha256(update.stderr) };
157
+ }
158
+ commitSha = observed;
159
+ }
160
+ }
161
+ return { kind: 'ok', value: {
162
+ version: 2, bindingId, expectedHeadSha: input.expectedHeadSha, candidateRef: ref,
163
+ candidateCommitSha: commitSha, candidateTreeSha: first.treeSha,
164
+ canonicalChangedFiles: first.changedFiles, sourceWorktreeIdentity: first.worktreeIdentity,
165
+ } };
166
+ }
167
+ catch (error) {
168
+ return candidateFailure('candidate-io-failed', error);
169
+ }
170
+ }
171
+ async captureCandidateTree(worktreePath, expectedHeadSha, artifactDir) {
172
+ validateRelativeRoot(artifactDir);
173
+ const [head, canonicalPath, gitDirectoryRaw] = await Promise.all([
174
+ this.getHead(worktreePath), realpath(worktreePath), this.git(['-C', worktreePath, 'rev-parse', '--absolute-git-dir']),
175
+ ]);
176
+ if (head !== expectedHeadSha)
177
+ throw new Error('candidate expected HEAD diverged');
178
+ const gitDirectory = gitDirectoryRaw.trim();
179
+ const temporaryRoot = await mkdtemp(join(gitDirectory, 'candidate-index-'));
180
+ const indexPath = join(temporaryRoot, 'index');
181
+ const options = { envOverlay: { GIT_INDEX_FILE: indexPath } };
182
+ try {
183
+ await this.git(['-C', worktreePath, 'read-tree', expectedHeadSha], options);
184
+ await this.git(['-C', worktreePath, 'add', '-u', '--'], options);
185
+ const untracked = (await this.git(['-C', worktreePath, 'ls-files', '--others', '--exclude-standard', '-z'], options)).split('\0')
186
+ .filter((path) => path && path !== artifactDir && !path.startsWith(`${artifactDir}/`)).sort();
187
+ if (untracked.length > 0) {
188
+ await this.git(['-C', worktreePath, 'add', '--pathspec-from-file=-', '--pathspec-file-nul'], {
189
+ ...options, stdin: `${untracked.join('\0')}\0`,
190
+ });
191
+ }
192
+ const treeSha = (await this.git(['-C', worktreePath, 'write-tree'], options)).trim();
193
+ const changedFiles = (await this.git(['-C', worktreePath, 'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', expectedHeadSha, treeSha]))
194
+ .split('\0').filter(Boolean).sort();
195
+ return {
196
+ expectedHeadSha,
197
+ treeSha,
198
+ changedFiles,
199
+ worktreeIdentity: sha256(canonicalJson({ canonicalPath, gitDirectory })),
200
+ };
201
+ }
202
+ finally {
203
+ await rm(temporaryRoot, { recursive: true, force: true });
204
+ }
205
+ }
206
+ async inspectCandidatePin(binding) {
207
+ return this.candidateOperation('candidate-io-failed', async () => {
208
+ const root = await this.candidateRepository(binding.candidateRef);
209
+ if (!root)
210
+ return 'missing';
211
+ const observed = await this.resolveOptionalRef(root, binding.candidateRef);
212
+ if (!observed)
213
+ return 'missing';
214
+ return observed === binding.candidateCommitSha
215
+ && await this.commitMatches(root, observed, binding.expectedHeadSha, binding.candidateTreeSha) ? 'matching' : 'diverged';
216
+ });
217
+ }
218
+ async prepareCandidateExecution(input) {
219
+ return this.candidateOperation('candidate-materialization-io-failed', async () => {
220
+ const root = await this.repositoryFromWorkspaceRoot(input.workspaceRoot);
221
+ this.candidateRepositories.set(input.binding.candidateRef, root);
222
+ const path = join(input.workspaceRoot, '.candidate-executions', input.runId, input.binding.bindingId, `${input.operation}-${input.attemptId}`);
223
+ const existing = (await this.worktrees.listWorktrees(root)).find((worktree) => sameFilesystemPath(worktree.path, path));
224
+ if (existing) {
225
+ const clean = await this.worktrees.isWorktreeClean(path);
226
+ const head = await this.getHead(path).catch(() => 'missing');
227
+ if (existing.branch !== undefined || head !== input.binding.candidateCommitSha || !clean)
228
+ return { kind: 'path-diverged', path };
229
+ }
230
+ else {
231
+ await mkdir(dirname(path), { recursive: true });
232
+ await this.git(['-C', root, 'worktree', 'add', '--detach', path, input.binding.candidateCommitSha]);
233
+ }
234
+ return { kind: 'prepared', lease: {
235
+ version: 2, bindingId: input.binding.bindingId, candidateCommitSha: input.binding.candidateCommitSha,
236
+ path, operation: input.operation, attemptId: input.attemptId, phase: 'prepared', pid: null,
237
+ processGroupId: null, preparedAt: new Date().toISOString(), launchedAt: null,
238
+ } };
239
+ });
240
+ }
241
+ async inspectCandidateExecution(input) {
242
+ return this.candidateOperation('candidate-materialization-io-failed', async () => {
243
+ try {
244
+ const stat = await lstat(input.lease.path);
245
+ if (stat.isSymbolicLink() || !stat.isDirectory())
246
+ return 'mutated';
247
+ }
248
+ catch (error) {
249
+ if (isErrorCode(error, 'ENOENT'))
250
+ return 'missing';
251
+ throw error;
252
+ }
253
+ const [head, tree, changed] = await Promise.all([
254
+ this.getHead(input.lease.path),
255
+ this.git(['-C', input.lease.path, 'rev-parse', 'HEAD^{tree}']).then((value) => value.trim()),
256
+ this.worktrees.listChangedFilesIgnoringUntrackedRoot(input.lease.path, input.artifactDir),
257
+ ]);
258
+ return head === input.binding.candidateCommitSha && tree === input.binding.candidateTreeSha && changed.length === 0 ? 'matching' : 'mutated';
259
+ });
260
+ }
261
+ async removeCandidateExecution(lease) {
262
+ return this.candidateOperation('candidate-materialization-io-failed', async () => {
263
+ if (lease.phase === 'launched' && lease.processGroupId && processGroupIsAlive(lease.processGroupId))
264
+ throw new Error('candidate execution process is still active');
265
+ const root = await this.repositoryFromWorkspaceRoot(lease.path);
266
+ const registered = (await this.worktrees.listWorktrees(root)).find((worktree) => sameFilesystemPath(worktree.path, lease.path));
267
+ if (!registered)
268
+ return;
269
+ if (registered.branch !== undefined || await this.getHead(lease.path) !== lease.candidateCommitSha)
270
+ throw new Error('candidate execution owner diverged');
271
+ await this.git(['-C', root, 'worktree', 'remove', '--force', lease.path]);
272
+ });
273
+ }
274
+ async copyCandidateProofArtifacts(input) {
275
+ return this.candidateOperation('candidate-materialization-io-failed', async () => {
276
+ const proofRoot = `${input.artifactDir}/${input.proofId}`;
277
+ const repositoryRoot = await this.repositoryFromWorkspaceRoot(input.lease.path);
278
+ const registered = (await this.worktrees.listWorktrees(repositoryRoot))
279
+ .find((worktree) => sameFilesystemPath(worktree.path, input.lease.path));
280
+ if (!registered || registered.branch !== undefined
281
+ || await this.getHead(input.lease.path).catch(() => '') !== input.lease.candidateCommitSha) {
282
+ return { kind: 'artifact-conflict', relativePath: proofRoot };
283
+ }
284
+ const sourceRoot = await inspectSafeArtifactRoot(input.lease.path);
285
+ const destinationRoot = await inspectSafeArtifactRoot(input.issueWorktreePath);
286
+ for (const artifact of input.artifacts) {
287
+ if (artifact.relativePath !== proofRoot && !artifact.relativePath.startsWith(`${proofRoot}/`))
288
+ return { kind: 'artifact-conflict', relativePath: artifact.relativePath };
289
+ try {
290
+ const source = join(input.lease.path, artifact.relativePath);
291
+ const destination = join(input.issueWorktreePath, artifact.relativePath);
292
+ const sourceBytes = await readRegularFileWithoutSymlinkAncestors(sourceRoot, source);
293
+ if (sha256(sourceBytes) !== artifact.sha256)
294
+ return { kind: 'artifact-conflict', relativePath: artifact.relativePath };
295
+ await ensureSafeDirectoryPath(destinationRoot, dirname(destination));
296
+ await publishArtifactCreateOnly(destinationRoot, destination, sourceBytes, artifact.sha256);
297
+ }
298
+ catch (error) {
299
+ if (error instanceof CandidateArtifactConflictError)
300
+ return { kind: 'artifact-conflict', relativePath: artifact.relativePath };
301
+ throw error;
302
+ }
303
+ }
304
+ return { kind: 'copied-or-observed' };
305
+ });
306
+ }
307
+ async createOrObserveCandidateCommit(input) {
308
+ return this.candidateOperation('candidate-ref-update-unknown', async () => {
309
+ const root = (await this.git(['-C', input.worktreePath, 'rev-parse', '--show-toplevel'])).trim();
310
+ const ref = `refs/heads/${input.branchName}`;
311
+ const observedHead = (await this.git(['-C', root, 'rev-parse', '--verify', `${ref}^{commit}`])).trim();
312
+ if (observedHead !== input.parentSha) {
313
+ const observed = await this.inspectCommit(root, observedHead);
314
+ if (observed.parentSha === input.parentSha && observed.treeSha === input.treeSha && observed.message === input.message) {
315
+ return { kind: 'created-or-observed', sha: observedHead, ...observed };
316
+ }
317
+ return { kind: 'branch-diverged', observedHeadSha: observedHead };
318
+ }
319
+ if (input.observeOnly)
320
+ return { kind: 'parent-unchanged' };
321
+ const pin = await this.resolveOptionalRef(root, input.candidateRef);
322
+ if (!pin || (await this.git(['-C', root, 'rev-parse', `${pin}^{tree}`])).trim() !== input.treeSha)
323
+ throw new Error('candidate pin does not authorize publication tree');
324
+ const sha = (await this.git([
325
+ '-C', root, '-c', 'user.name=codex-orchestrator', '-c', 'user.email=codex-orchestrator@users.noreply.github.com',
326
+ 'commit-tree', input.treeSha, '-p', input.parentSha, '-m', input.message,
327
+ ])).trim();
328
+ const update = await this.executor('git', ['-C', root, 'update-ref', ref, sha, input.parentSha]);
329
+ if (update.exitCode !== 0) {
330
+ const after = (await this.git(['-C', root, 'rev-parse', '--verify', `${ref}^{commit}`])).trim();
331
+ if (after === input.parentSha)
332
+ return { kind: 'parent-unchanged' };
333
+ const observed = await this.inspectCommit(root, after);
334
+ if (observed.parentSha === input.parentSha && observed.treeSha === input.treeSha && observed.message === input.message) {
335
+ return { kind: 'created-or-observed', sha: after, ...observed };
336
+ }
337
+ return { kind: 'branch-diverged', observedHeadSha: after };
338
+ }
339
+ return { kind: 'created-or-observed', sha, parentSha: input.parentSha, treeSha: input.treeSha, message: input.message };
340
+ });
341
+ }
342
+ async releaseCandidatePin(binding, expected) {
343
+ return this.candidateOperation('candidate-ref-update-unknown', async () => {
344
+ const root = await this.candidateRepository(binding.candidateRef);
345
+ if (!root)
346
+ return;
347
+ const observed = await this.resolveOptionalRef(root, binding.candidateRef);
348
+ if (!observed)
349
+ return;
350
+ if (observed !== expected)
351
+ throw new Error('candidate pin diverged');
352
+ const result = await this.executor('git', ['-C', root, 'update-ref', '-d', binding.candidateRef, expected]);
353
+ if (result.exitCode !== 0 && await this.resolveOptionalRef(root, binding.candidateRef))
354
+ throw new Error('candidate pin deletion outcome is unknown');
355
+ });
356
+ }
357
+ async candidateOperation(code, operation) {
358
+ try {
359
+ return { kind: 'ok', value: await operation() };
360
+ }
361
+ catch (error) {
362
+ return candidateFailure(code, error);
363
+ }
364
+ }
365
+ async candidateRepository(ref) {
366
+ const known = this.candidateRepositories.get(ref);
367
+ if (known)
368
+ return known;
369
+ for (const candidateRoot of [this.repositoryRoot, process.cwd()]) {
370
+ if (!candidateRoot)
371
+ continue;
372
+ try {
373
+ const root = (await this.git(['-C', candidateRoot, 'rev-parse', '--show-toplevel'])).trim();
374
+ if (await this.resolveOptionalRef(root, ref)) {
375
+ this.candidateRepositories.set(ref, root);
376
+ return root;
377
+ }
378
+ }
379
+ catch { /* this location need not be in the target repository */ }
380
+ }
381
+ return undefined;
382
+ }
383
+ async repositoryFromWorkspaceRoot(path) {
384
+ let current = resolve(path);
385
+ while (true) {
386
+ try {
387
+ return (await this.git(['-C', current, 'rev-parse', '--show-toplevel'])).trim();
388
+ }
389
+ catch {
390
+ const parent = dirname(current);
391
+ if (parent === current)
392
+ throw new Error('candidate workspace is not below a Git repository');
393
+ current = parent;
394
+ }
395
+ }
396
+ }
397
+ async resolveOptionalRef(root, ref) {
398
+ const result = await this.executor('git', ['-C', root, 'rev-parse', '--verify', `${ref}^{commit}`]);
399
+ if (result.exitCode === 1 || result.exitCode === 128)
400
+ return undefined;
401
+ if (result.exitCode !== 0)
402
+ throw new Error(`git ref inspection failed: ${result.stderr}`);
403
+ return result.stdout.trim();
404
+ }
405
+ async commitMatches(root, commit, parent, tree) {
406
+ const observed = await this.inspectCommit(root, commit);
407
+ return observed.parentSha === parent && observed.treeSha === tree;
408
+ }
409
+ async inspectCommit(root, commit) {
410
+ const output = await this.git(['-C', root, 'show', '-s', '--format=%P%n%T%n%B', commit]);
411
+ const [parentSha, treeSha, ...message] = output.split('\n');
412
+ if (!parentSha || parentSha.includes(' ') || !treeSha)
413
+ throw new Error('candidate commit is not single-parent');
414
+ return { parentSha, treeSha, message: message.join('\n').trimEnd() };
40
415
  }
41
416
  async getBaseSha(input) {
42
- return (await this.git(['-C', input.targetRoot, 'rev-parse', input.baseBranch])).trim();
417
+ await this.git([
418
+ '-C', input.targetRoot, 'fetch', '--quiet', '--no-tags', 'origin', `refs/heads/${input.baseBranch}`,
419
+ ]);
420
+ return (await this.git(['-C', input.targetRoot, 'rev-parse', '--verify', 'FETCH_HEAD^{commit}'])).trim();
43
421
  }
44
422
  async createWorktree(input) {
45
423
  await this.worktrees.createIssueWorktree({
@@ -49,6 +427,9 @@ export class LocalGitRunIssueAdapter {
49
427
  baseBranch: input.baseBranch,
50
428
  requiredBaseSha: input.baseSha,
51
429
  });
430
+ if (await this.getHead(input.worktreePath) !== input.baseSha) {
431
+ throw new Error('created issue worktree does not match the pinned base SHA');
432
+ }
52
433
  }
53
434
  async ensureContinuationWorktree(input) {
54
435
  const localHead = (await this.git([
@@ -197,126 +578,311 @@ export class LocalGitRunIssueAdapter {
197
578
  push(input) {
198
579
  return this.worktrees.pushBranch(input);
199
580
  }
200
- async git(args) {
201
- const result = await this.executor('git', args);
581
+ async git(args, options) {
582
+ const result = await this.executor('git', args, options);
202
583
  if (result.exitCode !== 0)
203
584
  throw new Error(`git failed: ${result.stderr}`);
204
585
  return result.stdout;
205
586
  }
206
587
  }
207
- export class ContainedImplementationAgent {
208
- dependencies;
209
- constructor(dependencies) {
210
- this.dependencies = dependencies;
588
+ function candidateFailure(code, error) {
589
+ const detail = error instanceof Error ? `${error.name}:${error.message}` : String(error);
590
+ return { kind: 'failed', code, detailSha256: sha256(detail) };
591
+ }
592
+ function processGroupIsAlive(processGroupId) {
593
+ try {
594
+ process.kill(-processGroupId, 0);
595
+ return true;
211
596
  }
212
- async run(input) {
213
- const config = this.dependencies.config();
214
- const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
215
- const attemptId = (this.dependencies.createAttemptId ?? randomUUID)();
216
- const attempt = await prepareContainedAttempt({
217
- orchestratorHome: this.dependencies.orchestratorHome,
218
- canonicalRepository,
219
- runId: input.runId,
220
- attemptId,
221
- operationId: input.operation,
222
- workflowGeneration: input.workflowGeneration,
223
- bootId: this.dependencies.bootId,
224
- });
225
- const baseline = await this.dependencies.git.snapshot(input.worktreePath);
226
- await input.onPrepared?.({
227
- attemptId,
228
- reportPath: attempt.reportPath,
229
- preparedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
230
- baseline,
231
- });
232
- try {
233
- const result = await (this.dependencies.process ?? new CodexProcess()).run({
234
- codexPath: config.codex.command,
235
- cwd: input.worktreePath,
236
- schemaPath: attempt.schemaPath,
237
- reportPath: attempt.reportPath,
238
- toolHome: attempt.toolHome,
239
- tmpDir: attempt.tmpDir,
240
- safePath: this.dependencies.safePath,
241
- parentCodexHome: this.dependencies.parentCodexHome,
242
- parentEnv: process.env,
243
- prompt: [
244
- `Package profile instructions: ${attempt.profile.developerInstructions}`,
245
- `Follow the exact operation at ${attempt.operationPath}.`,
246
- `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
247
- ...(input.operation === 'qualification-repair'
248
- ? [
249
- `Pre-implementation qualification repair for issue #${input.issue.number}: ${input.issue.title}`,
250
- 'Do not implement the issue acceptance criteria yet. Repair only the reported scoped-check failures so the existing worktree qualifies for implementation.',
251
- 'You may modify the product files required by those failures. Return changedFiles as the complete cumulative worktree change set.',
252
- ]
253
- : [`Implement issue #${input.issue.number}: ${input.issue.title}`]),
254
- `Implementation cycle: ${input.cycle}.`,
255
- ...(input.reviewFeedbackRound ? [`Pull-request feedback repair round: ${input.reviewFeedbackRound}.`] : []),
256
- ...(input.reviewFeedback?.length ? [`Frozen trusted pull-request feedback: ${canonicalJson(input.reviewFeedback)}`] : []),
257
- ...(input.operation === 'implementation'
258
- ? [`Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`]
259
- : []),
260
- ...(input.reworkFindings.length > 0 ? [`Repair these verified findings: ${canonicalJson(input.reworkFindings)}`] : []),
261
- ...(input.repairOnly ? ['Report repair only: do not modify any worktree file; emit a schema-valid implementation report for the existing change.'] : []),
262
- 'Do not commit, push, publish, or print credentials or local auth paths.',
263
- ].join('\n'),
264
- timeoutMs: config.codex.timeoutMs,
265
- idleTimeoutMs: config.codex.idleTimeoutMs,
266
- operationPolicy: attempt.policy,
267
- executionProfile: attempt.profile,
268
- ...(input.onLaunched ? { onSpawned: ({ pid, processGroupId }) => input.onLaunched({
269
- attemptId, pid, processGroupId, launchedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
270
- }) } : {}),
271
- }, input.signal);
272
- if (result.kind === 'cancelled')
273
- return { kind: 'cancelled' };
274
- if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
275
- return { kind: 'transport-failed', resumable: true };
597
+ catch (error) {
598
+ return !isErrorCode(error, 'ESRCH');
599
+ }
600
+ }
601
+ function sameFilesystemPath(left, right) {
602
+ return canonicalFilesystemPath(left) === canonicalFilesystemPath(right);
603
+ }
604
+ function canonicalFilesystemPath(value) {
605
+ const resolved = resolve(value);
606
+ return resolved.startsWith('/private/') ? resolved.slice('/private'.length) : resolved;
607
+ }
608
+ async function inspectSafeArtifactRoot(root) {
609
+ const path = resolve(root);
610
+ const stat = await lstat(path);
611
+ if (stat.isSymbolicLink() || !stat.isDirectory())
612
+ throw new CandidateArtifactConflictError('candidate artifact root is unsafe');
613
+ const realPath = canonicalFilesystemPath(await realpath(path));
614
+ if (realPath !== canonicalFilesystemPath(path))
615
+ throw new CandidateArtifactConflictError('candidate artifact root has a symlinked ancestor');
616
+ const identity = { path, realPath, dev: stat.dev, ino: stat.ino };
617
+ await assertSafeArtifactRoot(identity);
618
+ return identity;
619
+ }
620
+ async function assertSafeArtifactRoot(root) {
621
+ const stat = await lstat(root.path);
622
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== root.dev || stat.ino !== root.ino
623
+ || canonicalFilesystemPath(await realpath(root.path)) !== root.realPath) {
624
+ throw new CandidateArtifactConflictError('candidate artifact root changed');
625
+ }
626
+ }
627
+ async function readRegularFileWithoutSymlinkAncestors(rootInput, path) {
628
+ const root = typeof rootInput === 'string' ? await inspectSafeArtifactRoot(rootInput) : rootInput;
629
+ await assertSafeArtifactRoot(root);
630
+ const relativePath = relative(root.path, resolve(path));
631
+ if (!relativePath || relativePath.startsWith('..') || isAbsolute(relativePath))
632
+ throw new CandidateArtifactConflictError('candidate artifact path escapes its root');
633
+ let current = root.path;
634
+ const rootStat = await lstat(current);
635
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
636
+ throw new CandidateArtifactConflictError('candidate artifact root is unsafe');
637
+ const ancestorIdentity = [{ path: current, dev: rootStat.dev, ino: rootStat.ino }];
638
+ const segments = relativePath.split('/');
639
+ for (const segment of segments.slice(0, -1)) {
640
+ current = join(current, segment);
641
+ const stat = await lstat(current);
642
+ if (stat.isSymbolicLink() || !stat.isDirectory())
643
+ throw new CandidateArtifactConflictError('candidate artifact ancestor is unsafe');
644
+ ancestorIdentity.push({ path: current, dev: stat.dev, ino: stat.ino });
645
+ }
646
+ const finalBeforeOpen = await lstat(path);
647
+ if (finalBeforeOpen.isSymbolicLink() || !finalBeforeOpen.isFile()) {
648
+ throw new CandidateArtifactConflictError('candidate artifact is not a regular file');
649
+ }
650
+ let handle;
651
+ try {
652
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
653
+ }
654
+ catch (error) {
655
+ if (isErrorCode(error, 'ELOOP'))
656
+ throw new CandidateArtifactConflictError('candidate artifact is not a regular file');
657
+ throw error;
658
+ }
659
+ try {
660
+ const stat = await handle.stat();
661
+ if (!stat.isFile())
662
+ throw new CandidateArtifactConflictError('candidate artifact is not a regular file');
663
+ const openedPath = await realpath(path);
664
+ const openedRelative = relative(root.realPath, canonicalFilesystemPath(openedPath));
665
+ if (!openedRelative || openedRelative.startsWith('..') || isAbsolute(openedRelative)) {
666
+ throw new CandidateArtifactConflictError('candidate artifact path escaped its root during read');
667
+ }
668
+ const finalStat = await lstat(path);
669
+ if (!finalStat.isFile() || finalStat.isSymbolicLink() || finalStat.dev !== stat.dev || finalStat.ino !== stat.ino) {
670
+ throw new CandidateArtifactConflictError('candidate artifact changed during read');
671
+ }
672
+ for (const identity of ancestorIdentity) {
673
+ const observed = await lstat(identity.path);
674
+ if (!observed.isDirectory() || observed.isSymbolicLink() || observed.dev !== identity.dev || observed.ino !== identity.ino) {
675
+ throw new CandidateArtifactConflictError('candidate artifact ancestor changed during read');
276
676
  }
277
- if (result.kind !== 'completed' || result.report.kind !== 'available')
278
- return { kind: 'internal-error' };
279
- return {
280
- kind: 'completed',
281
- attemptId,
282
- report: decodeAgentReportForValidation(result.report.bytes),
283
- };
677
+ }
678
+ await assertSafeArtifactRoot(root);
679
+ return await handle.readFile();
680
+ }
681
+ finally {
682
+ await handle.close();
683
+ }
684
+ }
685
+ async function ensureSafeDirectoryPath(root, directory) {
686
+ await assertSafeArtifactRoot(root);
687
+ const relativeDirectory = relative(root.path, resolve(directory));
688
+ if (relativeDirectory.startsWith('..') || isAbsolute(relativeDirectory))
689
+ throw new CandidateArtifactConflictError('candidate artifact directory escapes its root');
690
+ let current = root.path;
691
+ for (const segment of relativeDirectory.split('/').filter(Boolean)) {
692
+ current = join(current, segment);
693
+ try {
694
+ await mkdir(current);
284
695
  }
285
696
  catch (error) {
286
- if (!(error instanceof ProcessQuiescenceError))
287
- return { kind: 'internal-error' };
288
- return {
289
- kind: 'safe-halt',
290
- process: {
291
- pid: error.pid,
292
- processGroupId: error.processGroupId,
293
- startedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
294
- baseline: { ...baseline },
295
- },
296
- waitForAbsence: () => waitForProcessGroupAbsent(error.processGroupId),
297
- };
697
+ if (!isErrorCode(error, 'EEXIST'))
698
+ throw error;
298
699
  }
700
+ const stat = await lstat(current);
701
+ if (stat.isSymbolicLink() || !stat.isDirectory())
702
+ throw new CandidateArtifactConflictError('candidate artifact directory is unsafe');
703
+ }
704
+ const parentRealPath = canonicalFilesystemPath(await realpath(directory));
705
+ const parentRelative = relative(root.realPath, parentRealPath);
706
+ if (parentRelative.startsWith('..') || isAbsolute(parentRelative))
707
+ throw new CandidateArtifactConflictError('candidate artifact directory escaped its root');
708
+ await assertSafeArtifactRoot(root);
709
+ }
710
+ class CandidateArtifactConflictError extends Error {
711
+ }
712
+ async function publishArtifactCreateOnly(root, destination, bytes, expectedSha256) {
713
+ await assertSafeArtifactRoot(root);
714
+ const temporary = join(dirname(destination), `.${basename(destination)}.${expectedSha256}.tmp`);
715
+ let handle;
716
+ try {
717
+ handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
299
718
  }
300
- async recover(input) {
301
- const expected = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(input.canonicalRepository), 'runs', input.runId, 'attempts', input.invocation.attemptId, 'report.json');
302
- if (resolve(input.invocation.reportPath) !== expected) {
303
- throw new Error('review feedback implementation report path is not attempt-owned');
719
+ catch (error) {
720
+ if (!isErrorCode(error, 'EEXIST'))
721
+ throw error;
722
+ const stale = await readRegularFileWithoutSymlinkAncestors(root, temporary);
723
+ if (sha256(stale) !== expectedSha256) {
724
+ await unlink(temporary);
725
+ handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
304
726
  }
305
- let bytes;
727
+ }
728
+ if (handle) {
306
729
  try {
307
- bytes = await readRegularFile(expected);
730
+ await handle.writeFile(bytes);
731
+ await handle.sync();
732
+ const stat = await handle.stat();
733
+ const observed = await lstat(temporary);
734
+ const temporaryRealPath = canonicalFilesystemPath(await realpath(temporary));
735
+ const temporaryRelative = relative(root.realPath, temporaryRealPath);
736
+ if (!observed.isFile() || observed.isSymbolicLink() || observed.dev !== stat.dev || observed.ino !== stat.ino
737
+ || temporaryRelative.startsWith('..') || isAbsolute(temporaryRelative)) {
738
+ throw new CandidateArtifactConflictError('candidate artifact temporary path changed');
739
+ }
740
+ await assertSafeArtifactRoot(root);
308
741
  }
309
- catch (error) {
310
- if (isErrorCode(error, 'ENOENT'))
311
- return undefined;
742
+ finally {
743
+ await handle.close();
744
+ }
745
+ }
746
+ try {
747
+ await link(temporary, destination);
748
+ }
749
+ catch (error) {
750
+ if (!isErrorCode(error, 'EEXIST'))
312
751
  throw error;
752
+ const existing = await readRegularFileWithoutSymlinkAncestors(root, destination);
753
+ if (sha256(existing) !== expectedSha256)
754
+ throw new CandidateArtifactConflictError('candidate artifact destination conflicts');
755
+ }
756
+ const published = await readRegularFileWithoutSymlinkAncestors(root, destination);
757
+ if (sha256(published) !== expectedSha256)
758
+ throw new CandidateArtifactConflictError('candidate artifact destination conflicts');
759
+ const parent = await open(dirname(destination), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
760
+ try {
761
+ const parentStat = await parent.stat();
762
+ const observedParent = await lstat(dirname(destination));
763
+ const parentRealPath = canonicalFilesystemPath(await realpath(dirname(destination)));
764
+ const parentRelative = relative(root.realPath, parentRealPath);
765
+ if (!observedParent.isDirectory() || observedParent.isSymbolicLink()
766
+ || observedParent.dev !== parentStat.dev || observedParent.ino !== parentStat.ino
767
+ || parentRelative.startsWith('..') || isAbsolute(parentRelative)) {
768
+ throw new CandidateArtifactConflictError('candidate artifact destination directory changed');
313
769
  }
314
- return {
315
- kind: 'completed',
316
- attemptId: input.invocation.attemptId,
317
- report: decodeAgentReportForValidation(bytes),
318
- };
770
+ await parent.sync();
771
+ await assertSafeArtifactRoot(root);
319
772
  }
773
+ finally {
774
+ await parent.close();
775
+ }
776
+ await unlink(temporary).catch((error) => { if (!isErrorCode(error, 'ENOENT'))
777
+ throw error; });
778
+ }
779
+ export class ContainedImplementationAgent {
780
+ dependencies;
781
+ constructor(dependencies) {
782
+ this.dependencies = dependencies;
783
+ }
784
+ async run(input) {
785
+ const config = this.dependencies.config();
786
+ const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
787
+ const promptFacts = implementationPromptFacts(input);
788
+ const operation = this.operation(config, canonicalRepository, promptFacts);
789
+ const result = await operation.run({
790
+ operation: input.operation, runId: input.runId, worktreePath: input.worktreePath,
791
+ workflowGeneration: input.workflowGeneration, promptFacts, signal: input.signal,
792
+ context: { repairOnly: input.repairOnly, reworkFindings: [...input.reworkFindings] },
793
+ invocationState: input.invocationState, beforeLaunch: input.beforeLaunch,
794
+ });
795
+ if (result.status === 'completed')
796
+ return { kind: 'completed', attemptId: result.attemptId,
797
+ report: decodeAgentReportForValidation(result.reportBytes) };
798
+ if (result.status === 'cancelled')
799
+ return { kind: 'cancelled' };
800
+ if (result.status === 'retryable')
801
+ return { kind: 'transport-failed', resumable: true, code: result.code };
802
+ if (result.status === 'safe-halt')
803
+ return { kind: 'safe-halt', code: result.code };
804
+ return result.kind === 'external'
805
+ ? { kind: 'transport-failed', resumable: true, code: result.code }
806
+ : { kind: 'safe-halt', code: result.code };
807
+ }
808
+ async settle(input) {
809
+ const config = this.dependencies.config();
810
+ const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
811
+ const promptFacts = implementationPromptFacts(input);
812
+ const result = await this.operation(config, canonicalRepository, promptFacts).settle({
813
+ operation: input.operation, runId: input.runId, worktreePath: input.worktreePath,
814
+ workflowGeneration: input.workflowGeneration, promptFacts, signal: input.signal,
815
+ context: { repairOnly: input.repairOnly, reworkFindings: [...input.reworkFindings] },
816
+ invocationState: input.invocationState,
817
+ });
818
+ return result.status === 'settled' ? { kind: 'settled' } : { kind: 'safe-halt', code: result.code };
819
+ }
820
+ operation(config, canonicalRepository, promptFacts) {
821
+ return new InjectedContainedMutableOperation({
822
+ host: hostname(), bootId: this.dependencies.bootId,
823
+ now: this.dependencies.now ?? (() => new Date().toISOString()),
824
+ createAttemptId: this.dependencies.createAttemptId ?? randomUUID,
825
+ prepare: async ({ operation: worker, attemptId, runId, workflowGeneration }) => ({
826
+ operation: worker, generationHash: workflowGeneration.generationHash,
827
+ ...await prepareContainedAttempt({
828
+ orchestratorHome: this.dependencies.orchestratorHome, canonicalRepository, runId, attemptId,
829
+ operationId: worker, workflowGeneration, bootId: this.dependencies.bootId,
830
+ }),
831
+ }),
832
+ snapshot: (worktreePath) => this.dependencies.git.snapshot(worktreePath),
833
+ readReport: async (path) => readRegularFile(path).then((bytes) => ({ status: 'available', bytes }))
834
+ .catch((error) => isErrorCode(error, 'ENOENT') ? { status: 'absent' } : { status: 'unknown' }),
835
+ processStartIdentity: readProcessStartIdentity,
836
+ inspectProcess: inspectReportProcess,
837
+ launch: async ({ attempt, worktreePath, signal, onSpawned }) => {
838
+ try {
839
+ const result = await (this.dependencies.process ?? new CodexProcess()).run({
840
+ codexPath: config.codex.command, cwd: worktreePath, schemaPath: attempt.schemaPath,
841
+ reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
842
+ safePath: this.dependencies.safePath, parentCodexHome: this.dependencies.parentCodexHome,
843
+ parentEnv: process.env,
844
+ prompt: [
845
+ `Package profile instructions: ${attempt.profile.developerInstructions}`,
846
+ `Follow the exact operation at ${attempt.operationPath}.`,
847
+ `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
848
+ ...promptFacts,
849
+ ].join('\n'),
850
+ timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
851
+ operationPolicy: attempt.policy, executionProfile: attempt.profile, onSpawned,
852
+ }, signal);
853
+ if (result.kind === 'cancelled')
854
+ return { status: 'cancelled' };
855
+ if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
856
+ return { status: 'retryable', code: `mutable-operation-${result.kind}` };
857
+ if (result.kind !== 'completed' || result.report.kind !== 'available')
858
+ return { status: 'retryable', code: 'mutable-operation-report-unavailable' };
859
+ return { status: 'completed', reportBytes: result.report.bytes };
860
+ }
861
+ catch (error) {
862
+ return error instanceof ProcessQuiescenceError
863
+ ? { status: 'safe-halt' }
864
+ : { status: 'retryable', code: 'mutable-operation-launch-failed' };
865
+ }
866
+ },
867
+ });
868
+ }
869
+ }
870
+ function implementationPromptFacts(input) {
871
+ const task = input.operation === 'qualification-repair' ? [
872
+ `Pre-implementation qualification repair for issue #${input.issue.number}: ${input.issue.title}`,
873
+ 'Do not implement the issue acceptance criteria yet. Repair only the reported scoped-check failures so the existing worktree qualifies for implementation.',
874
+ 'You may modify the product files required by those failures. Return changedFiles as the complete cumulative worktree change set.',
875
+ ] : [`Implement issue #${input.issue.number}: ${input.issue.title}`];
876
+ return [...task,
877
+ `Implementation cycle: ${input.cycle}.`,
878
+ ...(input.reviewFeedbackRound ? [`Pull-request feedback repair round: ${input.reviewFeedbackRound}.`] : []),
879
+ ...(input.reviewFeedback?.length ? [`Frozen trusted pull-request feedback: ${canonicalJson(input.reviewFeedback)}`] : []),
880
+ ...(input.operation !== 'qualification-repair' ? [`Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`] : []),
881
+ ...(input.reworkFindings.length > 0 ? [`Repair these verified findings: ${canonicalJson(input.reworkFindings)}`] : []),
882
+ ...(input.phaseFacts?.length ? [`Phase-owned invocation correlation: ${canonicalJson(input.phaseFacts)}`] : []),
883
+ ...(input.repairOnly ? ['Report repair only: do not modify any worktree file; emit a schema-valid implementation report for the existing change.'] : []),
884
+ 'Do not commit, push, publish, or print credentials or local auth paths.',
885
+ ];
320
886
  }
321
887
  export class ContainedProofAgent {
322
888
  dependencies;
@@ -326,94 +892,53 @@ export class ContainedProofAgent {
326
892
  async run(input) {
327
893
  if (!input.workflowGeneration)
328
894
  throw new Error('proof workflow generation is required');
895
+ if (!input.iosProofInputs)
896
+ throw new Error('runner-owned iOS proof inputs are required');
329
897
  const config = this.dependencies.config();
330
- const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
331
- const worktreePath = resolve(this.dependencies.targetRoot, config.runner.workspaceRoot, `issue-${input.issue.number}`);
332
- const attempt = await prepareContainedAttempt({
333
- orchestratorHome: this.dependencies.orchestratorHome,
334
- canonicalRepository,
335
- runId: input.runId,
336
- attemptId: (this.dependencies.createAttemptId ?? randomUUID)(),
337
- operationId: 'acceptance-proof',
338
- workflowGeneration: input.workflowGeneration,
339
- bootId: this.dependencies.bootId,
340
- });
898
+ const issueWorktreePath = resolve(this.dependencies.targetRoot, config.runner.workspaceRoot, `issue-${input.issue.number}`);
899
+ const worktreePath = input.worktreePath ? resolve(input.worktreePath) : issueWorktreePath;
341
900
  const artifactRoot = resolve(worktreePath, config.proof.artifactDir);
342
- const snapshotRoot = dirname(attempt.sourceSkillPath ?? attempt.operationPath);
343
- const iosLeaseRoot = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(canonicalRepository), 'leases');
344
- const iosLeaseArtifact = join(artifactRoot, input.proofId, 'ios-lease.json');
345
- const iosTooling = await discoverIosTooling(this.dependencies.processExecutor, this.dependencies.iosXcrunPath);
346
901
  const before = await artifactInventory(artifactRoot, config.proof.artifactDir);
347
- try {
348
- const result = await (this.dependencies.process ?? new CodexProcess()).run({
349
- codexPath: config.codex.command,
350
- cwd: worktreePath,
351
- schemaPath: attempt.schemaPath,
352
- reportPath: attempt.reportPath,
353
- toolHome: attempt.toolHome,
354
- tmpDir: attempt.tmpDir,
355
- safePath: this.dependencies.safePath,
356
- parentCodexHome: this.dependencies.parentCodexHome,
357
- parentEnv: process.env,
358
- prompt: [
359
- `Package profile instructions: ${attempt.profile.developerInstructions}`,
360
- `Follow the exact operation at ${attempt.operationPath}.`,
361
- `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
362
- `Independently prove issue #${input.issue.number}.`,
363
- `Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
364
- `Checked change digest: ${input.checkedChangeSha256}.`,
365
- `Checked changed files: ${canonicalJson(input.changedFiles)}.`,
366
- `Configured check receipts: ${canonicalJson(input.checks)}.`,
367
- `Write evidence only below ${config.proof.artifactDir}.`,
368
- 'When a frozen criterion has a browser surface, follow references/browser.md from the exact acceptance-proof skill snapshot.',
369
- 'When a frozen criterion has an Android surface, follow references/android.md from the exact acceptance-proof skill snapshot.',
370
- ...(config.proof.android ? [
371
- `Runner-owned Android artifact paths: ${canonicalJson(input.runnerPreparedArtifactPaths)}.`,
372
- `Runner-owned Android preparation warnings: ${canonicalJson(input.runnerPreparationWarnings)}.`,
373
- 'The trusted Runner owns emulator, build, adb, lease, capture, and cleanup actions. Do not invoke adb, emulator, Flutter run, or an Android lease helper.',
374
- 'Inspect Runner artifacts when present. If preparation warnings are present, continue with all available non-visual evidence and preserve the warning as a residual risk; Android infrastructure failure alone must not block delivery.',
375
- ] : [
376
- 'Runner-owned Android proof is not configured for this repository. Do not invoke adb, emulator, Flutter run, or an Android lease helper; return a typed tool blocker for Android criteria.',
377
- ]),
378
- 'When a frozen criterion has an iOS surface, follow references/ios.md from the exact acceptance-proof skill snapshot.',
379
- `iOS lease helper: ${join(snapshotRoot, 'tools', 'ios-lease.mjs')}.`,
380
- `iOS lease root: ${iosLeaseRoot}.`,
381
- `iOS lease artifact: ${iosLeaseArtifact}.`,
382
- `iOS lease proof ID: ${input.proofId}.`,
383
- `iOS lease owner PID: ${process.pid}.`,
384
- `iOS xcrun path: ${this.dependencies.iosXcrunPath}.`,
385
- ...(iosTooling ? [
386
- `iOS runtime ID: ${iosTooling.runtimeId}.`,
387
- `iOS device type ID: ${iosTooling.deviceTypeId}.`,
388
- ] : ['iOS Simulator tooling discovery is unavailable; return a typed tool blocker for an iOS surface.']),
389
- ...(input.repairOnly ? [`Proof Report repair only: ${canonicalJson(input.repairFindings)} Do not modify product or evidence files.`] : []),
390
- 'Do not modify product files, commit, push, publish, or print credentials or local auth paths.',
391
- ].join('\n'),
392
- timeoutMs: config.codex.timeoutMs,
393
- idleTimeoutMs: config.codex.idleTimeoutMs,
394
- operationPolicy: attempt.policy,
395
- executionProfile: attempt.profile,
396
- }, input.signal);
397
- if (result.kind === 'cancelled')
398
- return { kind: 'cancelled' };
399
- if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
400
- return { kind: 'transport-failed', resumable: true };
401
- }
402
- if (result.kind !== 'completed' || result.report.kind !== 'available')
403
- return { kind: 'internal-error' };
404
- const after = await artifactInventory(artifactRoot, config.proof.artifactDir);
405
- return {
406
- kind: 'report',
407
- report: decodeAgentReportForValidation(result.report.bytes, ['visualEvidence', 'blocker']),
408
- proofPhaseChangedFiles: changedArtifactPaths(before, after),
409
- };
902
+ if (!input.invocationState)
903
+ return { kind: 'internal-error' };
904
+ if (input.repairOnly && await input.invocationState.read() === undefined
905
+ && !artifactInventoryMatches(before, input.repairArtifactSha256)) {
906
+ return { kind: 'internal-error', code: 'proof-report-repair-artifact-drift' };
410
907
  }
411
- catch (error) {
412
- if (error instanceof ProcessQuiescenceError) {
413
- throw new ProofQuiescenceError(error.pid, error.processGroupId, () => waitForProcessGroupAbsent(error.processGroupId));
908
+ const result = await this.dependencies.operation.run({
909
+ operation: 'acceptance-proof', runId: input.runId, worktreePath,
910
+ workflowGeneration: input.workflowGeneration, signal: input.signal, invocationState: input.invocationState,
911
+ beforeLaunch: input.beforeLaunch,
912
+ promptFacts: [
913
+ `Proof ID: ${input.proofId}. Issue: ${canonicalJson(input.issue)}.`,
914
+ `Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}.`,
915
+ `Checked change: ${input.checkedChangeSha256}; files: ${canonicalJson(input.changedFiles)}; checks: ${canonicalJson(input.checks)}.`,
916
+ `Runner artifacts: ${canonicalJson(input.runnerPreparedArtifactPaths)}; hashes: ${canonicalJson(input.runnerPreparedArtifactSha256)}; warnings: ${canonicalJson(input.runnerPreparationWarnings)}.`,
917
+ `Runner-owned immutable iOS proof inputs: ${canonicalJson(input.iosProofInputs)}.`,
918
+ ...(input.iosProofInputs.runtimeId === null || input.iosProofInputs.deviceTypeId === null
919
+ ? ['iOS Simulator tooling discovery is unavailable; return a typed tool blocker for an iOS surface.'] : []),
920
+ `Report repair: ${canonicalJson({ repairOnly: input.repairOnly, findings: input.repairFindings })}.`,
921
+ ...(input.repairOnly ? [`Immutable pre-repair artifact inventory: ${canonicalJson(input.repairArtifactSha256)}.`] : []),
922
+ ],
923
+ });
924
+ if (result.status === 'cancelled')
925
+ return { kind: 'cancelled' };
926
+ if (result.status !== 'completed') {
927
+ if (input.repairOnly && await input.invocationState.read() === undefined
928
+ && !artifactInventoryMatches(await artifactInventory(artifactRoot, config.proof.artifactDir), input.repairArtifactSha256)) {
929
+ return { kind: 'internal-error', code: 'proof-report-repair-artifact-drift' };
414
930
  }
415
- return { kind: 'internal-error' };
931
+ return { kind: 'deferred', code: result.code };
416
932
  }
933
+ const after = await artifactInventory(artifactRoot, config.proof.artifactDir);
934
+ const runnerPrepared = new Set(input.runnerPreparedArtifactPaths);
935
+ const changed = before.size === 0
936
+ ? [...after.keys()].filter((path) => !runnerPrepared.has(path)).sort()
937
+ : changedArtifactPaths(before, after);
938
+ return {
939
+ kind: 'report', report: decodeAgentReportForValidation(result.reportBytes, ['visualEvidence', 'blocker']),
940
+ proofPhaseChangedFiles: changed, proofPhaseArtifactSha256: Object.fromEntries([...after.entries()].sort()),
941
+ };
417
942
  }
418
943
  }
419
944
  export async function resolveCodexExecutable(command, safePath) {
@@ -453,7 +978,7 @@ export function createV2Runtime(input) {
453
978
  codexExecutable = { command, path };
454
979
  return path;
455
980
  };
456
- const git = input.git ?? new LocalGitRunIssueAdapter(commandExecutor);
981
+ const git = input.git ?? new LocalGitRunIssueAdapter(commandExecutor, targetRoot);
457
982
  const proofFreshnessGit = git;
458
983
  const controller = new AbortController();
459
984
  let currentConfig;
@@ -481,13 +1006,6 @@ export function createV2Runtime(input) {
481
1006
  git,
482
1007
  now,
483
1008
  });
484
- const proofAgent = input.proofAgent ?? new ContainedProofAgent({
485
- ...containedDependencies(),
486
- targetRoot,
487
- androidAdbPath,
488
- iosXcrunPath,
489
- processExecutor: commandExecutor,
490
- });
491
1009
  const androidProofController = new RunnerAndroidProofController({
492
1010
  adbPath: androidAdbPath,
493
1011
  emulatorPath: androidEmulatorPath,
@@ -495,6 +1013,10 @@ export function createV2Runtime(input) {
495
1013
  now: () => new Date(now()),
496
1014
  });
497
1015
  const reportOperation = new InjectedContainedReportOperation({
1016
+ host: hostname(),
1017
+ bootId: input.bootId,
1018
+ now,
1019
+ createAttemptId: input.createAttemptId ?? randomUUID,
498
1020
  prepare: async ({ operation, attemptId, runId, workflowGeneration }) => ({
499
1021
  operation,
500
1022
  generationHash: workflowGeneration.generationHash,
@@ -508,30 +1030,44 @@ export function createV2Runtime(input) {
508
1030
  bootId: input.bootId,
509
1031
  }),
510
1032
  }),
511
- snapshot: (worktreePath) => git.snapshot(worktreePath),
512
- launch: async ({ attempt, worktreePath, promptFacts, signal, onLaunched }) => {
1033
+ snapshot: (worktreePath, operation) => operation === 'acceptance-proof' && proofFreshnessGit.snapshotIgnoringUntrackedRoot
1034
+ ? proofFreshnessGit.snapshotIgnoringUntrackedRoot(worktreePath, requireConfig(currentConfig).proof.artifactDir)
1035
+ : git.snapshot(worktreePath),
1036
+ readReport: async (path) => readRegularFile(path).then((bytes) => ({ status: 'available', bytes }))
1037
+ .catch((error) => isErrorCode(error, 'ENOENT') ? { status: 'absent' } : { status: 'unknown' }),
1038
+ settleAttempt: async (attempt) => attempt.tmpDir
1039
+ ? rm(join(dirname(attempt.tmpDir), 'read-view'), { recursive: true, force: true })
1040
+ : Promise.reject(new Error('report operation attempt temporary directory is missing')),
1041
+ processStartIdentity: readProcessStartIdentity,
1042
+ inspectProcess: inspectReportProcess,
1043
+ launch: async ({ operation, attempt, worktreePath, promptFacts, signal, onSpawned }) => {
513
1044
  const config = requireConfig(currentConfig);
514
1045
  if (!attempt.schemaPath || !attempt.reportPath || !attempt.toolHome || !attempt.tmpDir
515
1046
  || !attempt.profile || !attempt.operationPath || !attempt.workflowRoot) {
516
1047
  return { status: 'blocked', kind: 'safety', code: 'report-operation-attempt-incomplete' };
517
1048
  }
518
1049
  let readView;
519
- try {
520
- readView = await materializeReportReadView({
521
- worktreePath,
522
- destination: join(dirname(attempt.tmpDir), 'read-view'),
523
- deniedPaths: config.deny.readPaths,
524
- });
525
- }
526
- catch {
527
- return { status: 'blocked', kind: 'safety', code: 'report-operation-read-view-failed' };
1050
+ if (operation !== 'spec-author' && operation !== 'acceptance-proof') {
1051
+ try {
1052
+ readView = await materializeReportReadView({
1053
+ worktreePath,
1054
+ destination: join(dirname(attempt.tmpDir), 'read-view'),
1055
+ deniedPaths: config.deny.readPaths,
1056
+ });
1057
+ }
1058
+ catch {
1059
+ return { status: 'blocked', kind: 'safety', code: 'report-operation-read-view-failed' };
1060
+ }
528
1061
  }
1062
+ const revisionPath = operation === 'spec-author'
1063
+ ? join(dirname(attempt.reportPath), `revision-${promptFacts[0]}.md`)
1064
+ : undefined;
529
1065
  let result;
530
1066
  let cleanupAfterSafeHalt = false;
531
1067
  try {
532
1068
  result = await containedProcess.run({
533
1069
  codexPath: config.codex.command,
534
- cwd: readView,
1070
+ cwd: operation === 'spec-author' ? dirname(attempt.reportPath) : operation === 'acceptance-proof' ? worktreePath : readView,
535
1071
  schemaPath: attempt.schemaPath,
536
1072
  reportPath: attempt.reportPath,
537
1073
  toolHome: attempt.toolHome,
@@ -539,7 +1075,21 @@ export function createV2Runtime(input) {
539
1075
  safePath: requireRuntimeString(input.safePath, 'safePath'),
540
1076
  parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
541
1077
  parentEnv: process.env,
542
- prompt: [
1078
+ prompt: operation === 'spec-author' ? [
1079
+ `Package profile instructions: ${attempt.profile.developerInstructions}`,
1080
+ `Follow the exact operation at ${attempt.operationPath}.`,
1081
+ `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
1082
+ `Runner-provided facts: ${canonicalJson(promptFacts)}`,
1083
+ `Write the complete new immutable revision only to ${revisionPath}. Return that exact absolute path and its SHA-256 in the report.`,
1084
+ 'Do not modify the product worktree, prior revisions, external state, or any .env file.',
1085
+ ].join('\n') : operation === 'acceptance-proof' ? [
1086
+ `Package profile instructions: ${attempt.profile.developerInstructions}`,
1087
+ `Follow the exact operation at ${attempt.operationPath}.`,
1088
+ `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
1089
+ `Runner-provided proof facts:\n${promptFacts.join('\n')}`,
1090
+ `Write evidence only below ${config.proof.artifactDir}. CheckedChange is the sole authority for completed checks.`,
1091
+ 'Do not modify product files, commit, push, publish, use network or MCP tools, or print credentials or local auth paths.',
1092
+ ].join('\n') : [
543
1093
  `Package profile instructions: ${attempt.profile.developerInstructions}`,
544
1094
  `Follow the exact operation at ${attempt.operationPath}.`,
545
1095
  `The operation's immutable workflow root is ${attempt.workflowRoot}.`,
@@ -551,27 +1101,18 @@ export function createV2Runtime(input) {
551
1101
  idleTimeoutMs: config.codex.idleTimeoutMs,
552
1102
  operationPolicy: attempt.policy,
553
1103
  executionProfile: attempt.profile,
554
- ...(onLaunched ? { onSpawned: onLaunched } : {}),
1104
+ onSpawned,
555
1105
  }, signal);
556
1106
  }
557
1107
  catch (error) {
558
1108
  if (error instanceof ProcessQuiescenceError) {
559
1109
  cleanupAfterSafeHalt = true;
560
- return {
561
- status: 'safe-halt',
562
- pid: error.pid,
563
- processGroupId: error.processGroupId,
564
- startedAt: now(),
565
- waitForAbsence: async () => {
566
- await waitForProcessGroupAbsent(error.processGroupId);
567
- await rm(readView, { recursive: true, force: true });
568
- },
569
- };
1110
+ return { status: 'safe-halt' };
570
1111
  }
571
1112
  throw error;
572
1113
  }
573
1114
  finally {
574
- if (!cleanupAfterSafeHalt)
1115
+ if (readView && !cleanupAfterSafeHalt)
575
1116
  await rm(readView, { recursive: true, force: true });
576
1117
  }
577
1118
  if (result.kind === 'cancelled')
@@ -588,195 +1129,101 @@ export function createV2Runtime(input) {
588
1129
  return { status: 'completed', reportBytes: result.report.bytes };
589
1130
  },
590
1131
  });
1132
+ const proofAgent = input.proofAgent ?? new ContainedProofAgent({
1133
+ config: () => requireConfig(currentConfig), targetRoot, operation: reportOperation,
1134
+ });
591
1135
  const implementationReviewer = new ContainedImplementationReviewer({
592
1136
  operation: reportOperation,
593
- createAttemptId: input.createAttemptId ?? randomUUID,
594
1137
  });
595
- const createAttemptId = input.createAttemptId ?? randomUUID;
596
1138
  const specOperation = {
597
- author: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
598
- const attemptId = createAttemptId();
599
- const sessionId = state.authorSessionId ?? randomUUID();
600
- let attempt;
1139
+ author: async ({ context, state, mode, authorSessionId: sessionId, signal, invocationState }) => {
1140
+ let completedAttemptId;
601
1141
  try {
602
- attempt = await prepareContainedAttempt({
603
- orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
604
- attemptId, operationId: 'spec-author', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
605
- });
606
- const revisionPath = join(dirname(attempt.reportPath), `revision-${state.revisions.length + 1}.md`);
607
- await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath, revisionPath });
608
- const config = requireConfig(currentConfig);
609
- const result = await containedProcess.run({
610
- codexPath: config.codex.command, cwd: dirname(attempt.reportPath), schemaPath: attempt.schemaPath,
611
- reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
612
- safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
613
- parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
614
- operationPolicy: attempt.policy, executionProfile: attempt.profile,
615
- onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
616
- prompt: [
617
- `Package profile instructions: ${attempt.profile.developerInstructions}`,
618
- `Follow the exact operation at ${attempt.operationPath}.`,
619
- `The immutable workflow root is ${attempt.workflowRoot}.`,
620
- `Author mode: ${mode}. Issue authority: ${canonicalJson(context.issue)}.`,
621
- `Frozen criteria: ${canonicalJson(context.frozenCriteria)}.`,
1142
+ const result = await reportOperation.run({
1143
+ operation: 'spec-author', runId: context.runId, worktreePath: context.worktreePath,
1144
+ workflowGeneration: context.workflowGeneration, signal, invocationState,
1145
+ promptFacts: [
1146
+ String(state.revisions.length + 1),
1147
+ `Author session ID: ${sessionId}. Author mode: ${mode}.`,
1148
+ `Issue authority and frozen criteria: ${canonicalJson({ issue: context.issue, frozenCriteria: context.frozenCriteria })}.`,
622
1149
  `Prior revisions and review state: ${canonicalJson({ revisions: state.revisions, review: state.review })}.`,
623
- `Write the complete new immutable revision only to ${revisionPath}. Return that exact absolute path and its SHA-256 in the report.`,
624
- 'Do not modify the product worktree, prior revisions, external state, or any .env file.',
625
- ].join('\n'),
626
- }, signal);
627
- if (result.kind === 'cancelled')
1150
+ ],
1151
+ });
1152
+ if (result.status === 'cancelled')
628
1153
  return { status: 'cancelled' };
629
- if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
630
- return { status: 'retryable', code: `spec-author-${result.kind}` };
631
- if (result.kind !== 'completed' || result.report.kind !== 'available')
632
- return { status: 'retryable', code: 'spec-author-report-invalid' };
633
- const report = decodeAgentReportForValidation(result.report.bytes);
634
- if (report.status !== 'ready' || report.specPath !== revisionPath || report.specSha256 === null)
635
- return { status: 'retryable', code: 'spec-author-report-invalid' };
1154
+ if (result.status === 'retryable' || result.status === 'safe-halt')
1155
+ return { status: 'retryable', code: result.code };
1156
+ if (result.status === 'blocked')
1157
+ return { status: 'blocked', kind: result.kind, code: result.code };
1158
+ completedAttemptId = result.attemptId;
1159
+ const invocation = await invocationState.read();
1160
+ if (!invocation || invocation.operation !== 'spec-author' || invocation.attemptId !== result.attemptId) {
1161
+ return { status: 'blocked', kind: 'safety', code: 'spec-author-invocation-correlation-missing' };
1162
+ }
1163
+ const revisionPath = join(dirname(invocation.reportPath), `revision-${state.revisions.length + 1}.md`);
1164
+ const report = decodeAgentReportForValidation(result.reportBytes);
1165
+ if (report.status !== 'ready' || report.specPath !== revisionPath || report.specSha256 === null) {
1166
+ return { status: 'invalid', code: 'spec-author-report-invalid', attemptId: result.attemptId };
1167
+ }
636
1168
  const content = await readRegularFile(revisionPath);
637
1169
  if (report.specSha256 !== sha256(content))
638
- return { status: 'retryable', code: 'spec-author-report-invalid' };
1170
+ return { status: 'invalid', code: 'spec-author-report-invalid', attemptId: result.attemptId };
639
1171
  const previous = state.revisions.at(-1) ?? null;
640
1172
  return { status: 'completed', value: createSpecRevision({
641
1173
  revision: state.revisions.length + 1, path: revisionPath, content: content.toString('utf8'),
642
1174
  evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
643
- author: { attemptId, sessionId }, previousRevision: previous,
1175
+ author: { attemptId: result.attemptId, sessionId }, previousRevision: previous,
644
1176
  }) };
645
1177
  }
646
1178
  catch (error) {
647
- if (error instanceof ProcessQuiescenceError) {
648
- try {
649
- await waitForProcessGroupAbsent(error.processGroupId);
650
- return { status: 'retryable', code: 'spec-author-process-quiescence' };
651
- }
652
- catch {
653
- return { status: 'blocked', kind: 'safety', code: 'spec-author-process-absence-unconfirmed' };
654
- }
655
- }
656
- return { status: 'retryable', code: 'spec-author-report-invalid' };
1179
+ return completedAttemptId
1180
+ ? { status: 'invalid', code: 'spec-author-report-invalid', attemptId: completedAttemptId }
1181
+ : { status: 'retryable', code: 'spec-author-report-invalid' };
657
1182
  }
658
1183
  },
659
- review: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
660
- const attemptId = createAttemptId();
661
- const sessionId = state.review.reviewer?.sessionId ?? randomUUID();
1184
+ review: async ({ context, state, mode, reviewerSessionId: sessionId, signal, invocationState }) => {
1185
+ let completedAttemptId;
662
1186
  try {
663
- const attempt = await prepareContainedAttempt({
664
- orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
665
- attemptId, operationId: 'spec-review', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
666
- });
667
- await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath });
668
- const config = requireConfig(currentConfig);
669
- const result = await containedProcess.run({
670
- codexPath: config.codex.command, cwd: context.worktreePath, schemaPath: attempt.schemaPath,
671
- reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
672
- safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
673
- parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
674
- operationPolicy: attempt.policy, executionProfile: attempt.profile,
675
- onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
676
- prompt: [
677
- `Package profile instructions: ${attempt.profile.developerInstructions}`,
678
- `Follow the exact operation at ${attempt.operationPath}.`,
1187
+ const result = await reportOperation.run({
1188
+ operation: 'spec-review', runId: context.runId, worktreePath: context.worktreePath,
1189
+ workflowGeneration: context.workflowGeneration, signal, invocationState,
1190
+ forbiddenAttemptIds: state.revisions.map((revision) => revision.author.attemptId),
1191
+ promptFacts: [
679
1192
  `Reviewer session ID: ${sessionId}. Review mode: ${mode}.`,
680
1193
  `Issue authority and frozen criteria: ${canonicalJson({ issue: context.issue, frozenCriteria: context.frozenCriteria })}.`,
681
1194
  `Immutable spec delivery state: ${canonicalJson(state)}.`,
682
- 'Return only the package spec-review report. Do not edit files or external state.',
683
- ].join('\n'),
684
- }, signal);
685
- if (result.kind === 'cancelled')
1195
+ ],
1196
+ });
1197
+ if (result.status === 'cancelled')
686
1198
  return { status: 'cancelled' };
687
- if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
688
- return { status: 'retryable', code: `spec-review-${result.kind}` };
689
- if (result.kind !== 'completed' || result.report.kind !== 'available')
690
- return { status: 'retryable', code: 'spec-review-report-invalid' };
691
- const raw = decodeAgentReportForValidation(result.report.bytes);
1199
+ if (result.status === 'retryable' || result.status === 'safe-halt')
1200
+ return { status: 'retryable', code: result.code };
1201
+ if (result.status === 'blocked')
1202
+ return { status: 'blocked', kind: result.kind, code: result.code };
1203
+ completedAttemptId = result.attemptId;
1204
+ const raw = decodeAgentReportForValidation(result.reportBytes);
692
1205
  if (raw.mode !== mode || raw.reviewerSessionId !== sessionId || !Array.isArray(raw.coverage) || !Array.isArray(raw.defects)
693
1206
  || !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts) || !Array.isArray(raw.acceptedRisks)
694
1207
  || typeof raw.coverageInvalidated !== 'boolean'
695
1208
  || !['approved', 'needs-work', 'rejected'].includes(raw.verdict))
696
- return { status: 'retryable', code: 'spec-review-report-invalid' };
1209
+ return { status: 'invalid', code: 'spec-review-report-invalid', attemptId: result.attemptId };
697
1210
  const target = state.revisions.at(-1);
698
1211
  const defects = validateCodeReviewDefects(raw.defects, target.revision);
699
1212
  const report = {
700
1213
  version: 1, targetRevision: target.revision, targetSha256: target.revisionSha256, mode,
701
- verdict: raw.verdict, reviewer: { attemptId, sessionId },
1214
+ verdict: raw.verdict, reviewer: { attemptId: result.attemptId, sessionId },
702
1215
  coverage: raw.coverage, defects,
703
1216
  affectedDefectIds: raw.affectedDefectIds,
704
1217
  affectedContracts: raw.affectedContracts,
705
1218
  closureRequestSha256: mode === 'closure' ? state.review.closureRequestSha256 : null,
706
1219
  acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
707
1220
  };
708
- return { status: 'completed', value: report, reportSha256: sha256(result.report.bytes) };
709
- }
710
- catch (error) {
711
- if (error instanceof ProcessQuiescenceError) {
712
- try {
713
- await waitForProcessGroupAbsent(error.processGroupId);
714
- return { status: 'retryable', code: 'spec-review-process-quiescence' };
715
- }
716
- catch {
717
- return { status: 'blocked', kind: 'safety', code: 'spec-review-process-absence-unconfirmed' };
718
- }
719
- }
720
- return { status: 'retryable', code: 'spec-review-report-invalid' };
721
- }
722
- },
723
- recover: async ({ context, state, signal }) => {
724
- const invocation = state.invocation;
725
- if (!invocation || signal.aborted)
726
- return signal.aborted ? { status: 'cancelled' } : { status: 'blocked', kind: 'safety', code: 'spec-recovery-invocation-missing' };
727
- if (invocation.status === 'launched') {
728
- try {
729
- await waitForProcessGroupAbsent(invocation.processGroupId);
730
- }
731
- catch {
732
- return { status: 'blocked', kind: 'safety', code: 'spec-process-absence-unconfirmed' };
733
- }
734
- }
735
- if (!invocation.reportPath)
736
- return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
737
- let reportBytes;
738
- try {
739
- reportBytes = await readRegularFile(invocation.reportPath);
740
- }
741
- catch {
742
- return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
743
- }
744
- if (invocation.purpose === 'author') {
745
- if (!invocation.revisionPath)
746
- return { status: 'retryable', code: 'spec-author-report-invalid' };
747
- try {
748
- const raw = decodeAgentReportForValidation(reportBytes);
749
- const content = await readRegularFile(invocation.revisionPath);
750
- if (raw.status !== 'ready' || raw.specPath !== invocation.revisionPath || raw.specSha256 !== sha256(content))
751
- return { status: 'retryable', code: 'spec-author-report-invalid' };
752
- return { status: 'completed', value: createSpecRevision({
753
- revision: state.revisions.length + 1, path: invocation.revisionPath, content: content.toString('utf8'),
754
- evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
755
- author: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, previousRevision: state.revisions.at(-1) ?? null,
756
- }) };
757
- }
758
- catch {
759
- return { status: 'retryable', code: 'spec-author-report-invalid' };
760
- }
761
- }
762
- try {
763
- const raw = decodeAgentReportForValidation(reportBytes);
764
- if (raw.mode !== invocation.mode || raw.reviewerSessionId !== invocation.sessionId || !Array.isArray(raw.coverage)
765
- || !Array.isArray(raw.defects) || !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts)
766
- || typeof raw.coverageInvalidated !== 'boolean' || !['approved', 'needs-work', 'rejected'].includes(raw.verdict)) {
767
- return { status: 'retryable', code: 'spec-review-report-invalid' };
768
- }
769
- const defects = validateCodeReviewDefects(raw.defects, invocation.targetRevision);
770
- return { status: 'completed', reportSha256: sha256(reportBytes), value: {
771
- version: 1, targetRevision: invocation.targetRevision, targetSha256: invocation.targetSha256,
772
- mode: invocation.mode, verdict: raw.verdict,
773
- reviewer: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, coverage: raw.coverage, defects,
774
- affectedDefectIds: raw.affectedDefectIds, affectedContracts: raw.affectedContracts,
775
- closureRequestSha256: invocation.closureRequestSha256, acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
776
- } };
1221
+ return { status: 'completed', value: report, reportSha256: result.reportSha256 };
777
1222
  }
778
1223
  catch {
779
- return { status: 'retryable', code: 'spec-review-report-invalid' };
1224
+ return completedAttemptId
1225
+ ? { status: 'invalid', code: 'spec-review-report-invalid', attemptId: completedAttemptId }
1226
+ : { status: 'retryable', code: 'spec-review-report-invalid' };
780
1227
  }
781
1228
  },
782
1229
  };
@@ -790,8 +1237,27 @@ export function createV2Runtime(input) {
790
1237
  codex: { ...parsed.codex, command: await resolveRuntimeCodex(parsed.codex.command) },
791
1238
  };
792
1239
  const path = join(targetRoot, config.runner.stateDir, 'v2', 'run-state.json');
793
- if (!runRecords)
794
- runRecords = new FileRunRecordWriter(path);
1240
+ if (!runRecords) {
1241
+ const processStartIdentity = await readProcessStartIdentity(process.pid);
1242
+ if (!processStartIdentity)
1243
+ throw new Error('run-store owner process identity is unavailable');
1244
+ runRecords = new FileRunRecordWriter(path, {
1245
+ host: hostname(), bootId: input.bootId, pid: process.pid, processStartIdentity,
1246
+ inspectProcessIdentity: async (pid) => {
1247
+ const identity = await readProcessStartIdentity(pid);
1248
+ if (identity)
1249
+ return { processStartIdentity: identity };
1250
+ try {
1251
+ process.kill(pid, 0);
1252
+ return 'unknown';
1253
+ }
1254
+ catch (error) {
1255
+ return isErrorCode(error, 'ESRCH') ? 'absent' : 'unknown';
1256
+ }
1257
+ },
1258
+ exclusiveRepositoryOwnership: true,
1259
+ });
1260
+ }
795
1261
  currentConfig = config;
796
1262
  return { bytes, config };
797
1263
  };
@@ -806,6 +1272,11 @@ export function createV2Runtime(input) {
806
1272
  throw new Error('run store used before config');
807
1273
  return runRecords.compareAndSwap(generation, next);
808
1274
  },
1275
+ markPublicationEffectPossible: async () => {
1276
+ if (!runRecords?.markPublicationEffectPossible)
1277
+ return;
1278
+ await runRecords.markPublicationEffectPossible();
1279
+ },
809
1280
  };
810
1281
  const capabilities = createCheckedChangeCapabilities();
811
1282
  const proof = {
@@ -813,8 +1284,27 @@ export function createV2Runtime(input) {
813
1284
  const config = requireConfig(currentConfig);
814
1285
  const checked = capabilities.verifyAndRead(proofInput.checkedChange);
815
1286
  const repoKey = sha256(checked.payload.canonicalRepository);
816
- const proofRecords = new FileProofRecordWriter(join(orchestratorHome, 'v2', repoKey, 'proofs'));
817
- const worktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${checked.payload.issueNumber}`);
1287
+ const ownerProcessIdentity = await readProcessStartIdentity(process.pid);
1288
+ if (!ownerProcessIdentity)
1289
+ throw new Error('proof-store owner process identity is unavailable');
1290
+ const proofRecords = new FileProofRecordWriter(join(orchestratorHome, 'v2', repoKey, 'proofs'), {
1291
+ host: hostname(), bootId: input.bootId, pid: process.pid, processStartIdentity: ownerProcessIdentity,
1292
+ inspectProcessIdentity: async (pid) => {
1293
+ const identity = await readProcessStartIdentity(pid);
1294
+ if (identity)
1295
+ return { processStartIdentity: identity };
1296
+ try {
1297
+ process.kill(pid, 0);
1298
+ return 'unknown';
1299
+ }
1300
+ catch (error) {
1301
+ return isErrorCode(error, 'ESRCH') ? 'absent' : 'unknown';
1302
+ }
1303
+ },
1304
+ exclusiveRepositoryOwnership: true,
1305
+ });
1306
+ const issueWorktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${checked.payload.issueNumber}`);
1307
+ const worktreePath = proofInput.executionLease?.path ?? issueWorktreePath;
818
1308
  const androidLease = new FileAndroidLeaseVerifier({
819
1309
  leaseRoot: join(orchestratorHome, 'v2', repoKey, 'leases'),
820
1310
  worktreeRoot: worktreePath,
@@ -835,18 +1325,33 @@ export function createV2Runtime(input) {
835
1325
  checkedChangeReader: capabilities,
836
1326
  proofRecords,
837
1327
  proofAgent,
838
- inspectFreshness: async (payload) => ({
839
- ...await (proofFreshnessGit.snapshotIgnoringUntrackedRoot
840
- ? proofFreshnessGit.snapshotIgnoringUntrackedRoot(worktreePath, config.proof.artifactDir)
841
- : git.snapshot(worktreePath)),
842
- checkPolicySha256: sha256(canonicalJson(resolveIssueCheckPolicy(proofInput.issue.body, config.checks).checks)),
843
- }),
1328
+ inspectFreshness: async (payload, executionLease) => {
1329
+ const checkPolicySha256 = sha256(canonicalJson(resolveIssueCheckPolicy(proofInput.issue.body, config.checks).checks));
1330
+ if (payload.version === 1)
1331
+ return {
1332
+ ...await (proofFreshnessGit.snapshotIgnoringUntrackedRoot
1333
+ ? proofFreshnessGit.snapshotIgnoringUntrackedRoot(worktreePath, config.proof.artifactDir)
1334
+ : git.snapshot(worktreePath)),
1335
+ checkPolicySha256,
1336
+ };
1337
+ if (!executionLease || !git.candidateV2)
1338
+ throw new CandidateProofInspectionError('candidate-git-v2-required');
1339
+ const inspection = await git.candidateV2.inspectExecution({
1340
+ binding: payload.binding,
1341
+ lease: executionLease,
1342
+ artifactDir: config.proof.artifactDir,
1343
+ });
1344
+ if (inspection.kind === 'failed')
1345
+ throw new CandidateProofInspectionError(inspection.code);
1346
+ if (inspection.value !== 'matching')
1347
+ throw new CandidateProofInspectionError('candidate-materialization-mutated');
1348
+ return { bindingId: payload.binding.bindingId, candidateTreeSha: payload.binding.candidateTreeSha, checkPolicySha256 };
1349
+ },
844
1350
  readArtifact: async (relativePath) => readRegularFile(resolve(worktreePath, relativePath)),
845
1351
  inspectArtifact: async (relativePath) => inspectRegularFile(resolve(worktreePath, relativePath)),
846
1352
  androidLease,
847
1353
  iosLease,
848
1354
  proofArtifactDir: config.proof.artifactDir,
849
- createAttemptId: input.createAttemptId ?? randomUUID,
850
1355
  now,
851
1356
  signal: controller.signal,
852
1357
  });
@@ -855,11 +1360,22 @@ export function createV2Runtime(input) {
855
1360
  const runnerPreparedArtifactSha256 = {};
856
1361
  const runnerPreparationWarnings = [];
857
1362
  let preparationAttempted = false;
858
- return acceptanceProof.proveChange({
1363
+ if (!proofInput.workflowGeneration)
1364
+ throw new Error('proof workflow generation is required');
1365
+ const iosTooling = await discoverIosTooling(commandExecutor, iosXcrunPath);
1366
+ const iosProofInputs = {
1367
+ helperPath: join(proofInput.workflowGeneration.generationRoot, 'skills', 'acceptance-proof', 'tools', 'ios-lease.mjs'),
1368
+ leaseRoot: join(orchestratorHome, 'v2', repoKey, 'leases'),
1369
+ leaseArtifactPath: resolve(worktreePath, config.proof.artifactDir, proofInput.proofId, 'ios-lease.json'),
1370
+ proofId: proofInput.proofId, ownerPid: process.pid, xcrunPath: iosXcrunPath,
1371
+ runtimeId: iosTooling?.runtimeId ?? null, deviceTypeId: iosTooling?.deviceTypeId ?? null,
1372
+ };
1373
+ const result = await acceptanceProof.proveChange({
859
1374
  ...proofInput,
860
1375
  runnerPreparedArtifactPaths,
861
1376
  runnerPreparedArtifactSha256,
862
1377
  runnerPreparationWarnings,
1378
+ iosProofInputs,
863
1379
  beforeAgentLaunch: async () => {
864
1380
  await proofInput.beforeAgentLaunch?.();
865
1381
  if (!androidRelevant || preparationAttempted)
@@ -885,6 +1401,21 @@ export function createV2Runtime(input) {
885
1401
  }
886
1402
  },
887
1403
  });
1404
+ if (checked.payload.version === 2 && proofInput.executionLease && git.candidateV2 && result.status === 'passed') {
1405
+ const artifacts = result.receipt.publishableEvidence.map((artifact) => ({ relativePath: artifact.ref, sha256: artifact.sha256 }));
1406
+ const copied = await git.candidateV2.copyProofArtifacts({
1407
+ lease: proofInput.executionLease,
1408
+ issueWorktreePath,
1409
+ artifactDir: config.proof.artifactDir,
1410
+ proofId: proofInput.proofId,
1411
+ artifacts,
1412
+ });
1413
+ if (copied.kind === 'failed')
1414
+ throw new CandidateProofInspectionError(copied.code);
1415
+ if (copied.value.kind === 'artifact-conflict')
1416
+ throw new CandidateProofInspectionError('candidate-artifact-conflict');
1417
+ }
1418
+ return result;
888
1419
  },
889
1420
  };
890
1421
  const runner = new RunIssue({
@@ -953,13 +1484,12 @@ export function createV2Runtime(input) {
953
1484
  const match = matches[0];
954
1485
  if (!match)
955
1486
  return undefined;
956
- const reviewTarget = await input.pullRequests.getReviewTarget(match.number);
957
1487
  return {
958
1488
  url: match.url,
959
1489
  body: match.body,
960
1490
  number: match.number,
961
1491
  nodeId: match.nodeId,
962
- ...(reviewTarget ? { headSha: reviewTarget.headRefOid } : {}),
1492
+ ...(match.headSha ? { headSha: match.headSha } : {}),
963
1493
  };
964
1494
  },
965
1495
  createDraft: async ({ title, body, headBranch, baseBranch }) => input.pullRequests.createDraftPullRequest({ title, body, headBranch, baseBranch }),
@@ -976,7 +1506,6 @@ export function createV2Runtime(input) {
976
1506
  run: ({ state, ...routeInput }) => new RouteCoordinator({
977
1507
  state,
978
1508
  operation: reportOperation,
979
- createAttemptId: input.createAttemptId ?? randomUUID,
980
1509
  now,
981
1510
  createReceipt: ({ artifact, triage, review, decidedAt }) => {
982
1511
  if (artifact.status === 'blocked')
@@ -998,7 +1527,9 @@ export function createV2Runtime(input) {
998
1527
  },
999
1528
  routeContinuations: {
1000
1529
  direct: async () => ({ status: 'completed' }),
1001
- specRequired: (context, state, signal) => new SpecCoordinator({ state, operation: specOperation }).run(context, signal),
1530
+ specRequired: (context, state, signal) => new SpecCoordinator({
1531
+ state, operation: specOperation, createAuthorSessionId: randomUUID, createReviewerSessionId: randomUUID,
1532
+ }).run(context, signal),
1002
1533
  awaitingUser: async (context, state, signal) => {
1003
1534
  const config = requireConfig(currentConfig);
1004
1535
  return new WaitingHumanCoordinator({
@@ -1018,11 +1549,12 @@ export function createV2Runtime(input) {
1018
1549
  implementationReviewer,
1019
1550
  waitForReviewProcessAbsence: waitForProcessGroupAbsent,
1020
1551
  checks: {
1021
- run: async ({ source, command, cwd, signal }) => {
1552
+ supportsLaunchOwnership: true,
1553
+ run: async ({ source, command, cwd, signal, onLaunched }) => {
1022
1554
  const timeoutMs = requireConfig(currentConfig).codex.timeoutMs;
1023
1555
  return source === 'issue'
1024
- ? runProcessCheck(parseIssueCheckInvocation(command), cwd, signal, timeoutMs)
1025
- : runShellCheck(command, cwd, signal, timeoutMs);
1556
+ ? runProcessCheck(parseIssueCheckInvocation(command), cwd, signal, timeoutMs, onLaunched)
1557
+ : runShellCheck(command, cwd, signal, timeoutMs, onLaunched);
1026
1558
  },
1027
1559
  },
1028
1560
  proof,
@@ -1078,7 +1610,10 @@ async function acquireOwnerLock(input) {
1078
1610
  }
1079
1611
  }
1080
1612
  async function readRegularFile(path) {
1081
- const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1613
+ const beforeOpen = await lstat(path);
1614
+ if (beforeOpen.isSymbolicLink() || !beforeOpen.isFile())
1615
+ throw new Error(`${path} is not a bounded regular file`);
1616
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
1082
1617
  try {
1083
1618
  const stat = await handle.stat();
1084
1619
  if (!stat.isFile() || stat.size > 5 * 1024 * 1024)
@@ -1101,15 +1636,20 @@ async function inspectRegularFile(path) {
1101
1636
  await handle.close();
1102
1637
  }
1103
1638
  }
1104
- export async function runShellCheck(command, cwd, signal, timeoutMs) {
1105
- return runSpawnCheck('/bin/sh', ['-lc', command], cwd, signal, timeoutMs);
1639
+ export async function runShellCheck(command, cwd, signal, timeoutMs, onLaunched) {
1640
+ return runSpawnCheck('/bin/sh', ['-lc', command], cwd, signal, timeoutMs, onLaunched);
1106
1641
  }
1107
- async function runProcessCheck(invocation, cwd, signal, timeoutMs) {
1108
- return runSpawnCheck(invocation.file, invocation.args, cwd, signal, timeoutMs);
1642
+ async function runProcessCheck(invocation, cwd, signal, timeoutMs, onLaunched) {
1643
+ return runSpawnCheck(invocation.file, invocation.args, cwd, signal, timeoutMs, onLaunched);
1109
1644
  }
1110
- async function runSpawnCheck(file, args, cwd, signal, timeoutMs) {
1645
+ async function runSpawnCheck(file, args, cwd, signal, timeoutMs, onLaunched) {
1111
1646
  return new Promise((resolveCheck, rejectCheck) => {
1112
- const child = spawn(file, args, { cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
1647
+ const child = spawn('/bin/sh', [
1648
+ '-c', 'IFS= read -r _ || exit 125; exec "$@"', 'codex-orchestrator-check-gate', file, ...args,
1649
+ ], { cwd, detached: true, stdio: ['pipe', 'pipe', 'pipe'] });
1650
+ const launchOwnership = child.pid && onLaunched
1651
+ ? onLaunched({ pid: child.pid, processGroupId: child.pid })
1652
+ : Promise.resolve();
1113
1653
  const chunks = [];
1114
1654
  let retained = 0;
1115
1655
  const outputHash = createHash('sha256');
@@ -1150,6 +1690,7 @@ async function runSpawnCheck(file, args, cwd, signal, timeoutMs) {
1150
1690
  }, 5_000);
1151
1691
  killTimer.unref();
1152
1692
  };
1693
+ void launchOwnership.then(() => child.stdin.end('\n')).catch(() => terminate());
1153
1694
  signal.addEventListener('abort', terminate, { once: true });
1154
1695
  if (signal.aborted)
1155
1696
  terminate();
@@ -1189,6 +1730,7 @@ async function runSpawnCheck(file, args, cwd, signal, timeoutMs) {
1189
1730
  rejectCheck(new Error(`Check exceeded ${timeoutMs}ms and was terminated.`));
1190
1731
  return;
1191
1732
  }
1733
+ await launchOwnership;
1192
1734
  resolveCheck({
1193
1735
  status: code === 0 ? 'passed' : 'failed',
1194
1736
  output: Buffer.concat(chunks),
@@ -1355,7 +1897,9 @@ async function artifactInventory(root, logicalRoot) {
1355
1897
  }
1356
1898
  if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
1357
1899
  throw new Error('proof artifact root must be a direct directory');
1900
+ const rootIdentity = await inspectSafeArtifactRoot(root);
1358
1901
  const visit = async (directory, relative) => {
1902
+ await assertSafeArtifactRoot(rootIdentity);
1359
1903
  for (const entry of await readdir(directory, { withFileTypes: true })) {
1360
1904
  const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
1361
1905
  const childPath = join(directory, entry.name);
@@ -1364,7 +1908,7 @@ async function artifactInventory(root, logicalRoot) {
1364
1908
  if (entry.isDirectory())
1365
1909
  await visit(childPath, childRelative);
1366
1910
  else if (entry.isFile())
1367
- output.set(`${logicalRoot}/${childRelative}`, sha256(await readFile(childPath)));
1911
+ output.set(`${logicalRoot}/${childRelative}`, sha256(await readRegularFileWithoutSymlinkAncestors(rootIdentity, childPath)));
1368
1912
  else
1369
1913
  throw new Error('proof artifact tree contains a special file');
1370
1914
  }
@@ -1377,6 +1921,9 @@ function changedArtifactPaths(before, after) {
1377
1921
  .filter((path) => before.get(path) !== after.get(path))
1378
1922
  .sort();
1379
1923
  }
1924
+ function artifactInventoryMatches(actual, expected) {
1925
+ return canonicalJson(Object.fromEntries([...actual.entries()].sort())) === canonicalJson(expected ?? null);
1926
+ }
1380
1927
  async function fingerprintRepositoryPath(root, relativePath) {
1381
1928
  const segments = relativePath.split('/');
1382
1929
  let current = root;
@@ -1427,6 +1974,49 @@ async function waitForProcessGroupAbsent(processGroupId) {
1427
1974
  await new Promise((resolveWait) => setTimeout(resolveWait, 1_000));
1428
1975
  }
1429
1976
  }
1977
+ async function readProcessStartIdentity(pid) {
1978
+ if (!Number.isSafeInteger(pid) || pid <= 0)
1979
+ return undefined;
1980
+ if (platform() === 'linux') {
1981
+ try {
1982
+ const text = await readFile(`/proc/${pid}/stat`, 'utf8');
1983
+ const close = text.lastIndexOf(') ');
1984
+ const fields = close < 0 ? [] : text.slice(close + 2).trim().split(/\s+/u);
1985
+ return fields[19] ? `linux:${fields[19]}` : undefined;
1986
+ }
1987
+ catch {
1988
+ return undefined;
1989
+ }
1990
+ }
1991
+ return await new Promise((resolveIdentity) => {
1992
+ execFile('/bin/ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8' }, (error, stdout) => {
1993
+ const value = error ? '' : stdout.trim().replace(/\s+/gu, ' ');
1994
+ resolveIdentity(value ? `${platform()}:${value}` : undefined);
1995
+ });
1996
+ });
1997
+ }
1998
+ async function inspectReportProcess(invocation) {
1999
+ const processStartIdentity = await readProcessStartIdentity(invocation.pid);
2000
+ let processGroupAlive;
2001
+ try {
2002
+ process.kill(-invocation.processGroupId, 0);
2003
+ processGroupAlive = true;
2004
+ }
2005
+ catch (error) {
2006
+ if (isErrorCode(error, 'ESRCH'))
2007
+ processGroupAlive = false;
2008
+ else if (isErrorCode(error, 'EPERM'))
2009
+ processGroupAlive = true;
2010
+ else
2011
+ processGroupAlive = 'unknown';
2012
+ }
2013
+ if (!processStartIdentity) {
2014
+ return processGroupAlive === false
2015
+ ? { status: 'absent', processGroupAlive: false }
2016
+ : { status: 'unknown' };
2017
+ }
2018
+ return { status: 'present', processStartIdentity, processGroupAlive };
2019
+ }
1430
2020
  export async function materializeReportReadView(input) {
1431
2021
  await assertDisjointReadViewPaths(input.worktreePath, input.destination);
1432
2022
  const deniedPaths = normalizeReadViewDeniedPaths(input.deniedPaths);