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.
- package/CHANGELOG.md +37 -0
- package/README.md +26 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/v2/acceptance-proof.d.ts +29 -16
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +159 -129
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/command.d.ts +1 -0
- package/dist/src/v2/adapters/command.d.ts.map +1 -1
- package/dist/src/v2/adapters/command.js +1 -1
- package/dist/src/v2/adapters/command.js.map +1 -1
- package/dist/src/v2/adapters/gh-pull-request-adapter.js +1 -0
- package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
- package/dist/src/v2/adapters/pull-requests.d.ts +1 -0
- package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
- package/dist/src/v2/adapters/pull-requests.js +1 -0
- package/dist/src/v2/adapters/pull-requests.js.map +1 -1
- package/dist/src/v2/adapters/worktree.js +3 -3
- package/dist/src/v2/adapters/worktree.js.map +1 -1
- package/dist/src/v2/atomic-store.d.ts +15 -0
- package/dist/src/v2/atomic-store.d.ts.map +1 -1
- package/dist/src/v2/atomic-store.js +55 -8
- package/dist/src/v2/atomic-store.js.map +1 -1
- package/dist/src/v2/candidate.d.ts +152 -0
- package/dist/src/v2/candidate.d.ts.map +1 -0
- package/dist/src/v2/candidate.js +129 -0
- package/dist/src/v2/candidate.js.map +1 -0
- package/dist/src/v2/checked-change.d.ts +37 -6
- package/dist/src/v2/checked-change.d.ts.map +1 -1
- package/dist/src/v2/checked-change.js +71 -0
- package/dist/src/v2/checked-change.js.map +1 -1
- package/dist/src/v2/cli.d.ts +6 -0
- package/dist/src/v2/cli.d.ts.map +1 -1
- package/dist/src/v2/cli.js +57 -21
- package/dist/src/v2/cli.js.map +1 -1
- package/dist/src/v2/codex-process.d.ts +1 -0
- package/dist/src/v2/codex-process.d.ts.map +1 -1
- package/dist/src/v2/codex-process.js +12 -1
- package/dist/src/v2/codex-process.js.map +1 -1
- package/dist/src/v2/contained-report-operation.d.ts +196 -30
- package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
- package/dist/src/v2/contained-report-operation.js +464 -150
- package/dist/src/v2/contained-report-operation.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +10 -28
- package/dist/src/v2/direct-delivery.d.ts.map +1 -1
- package/dist/src/v2/direct-delivery.js +44 -114
- package/dist/src/v2/direct-delivery.js.map +1 -1
- package/dist/src/v2/implementation-reviewer.d.ts +4 -24
- package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
- package/dist/src/v2/implementation-reviewer.js +29 -35
- package/dist/src/v2/implementation-reviewer.js.map +1 -1
- package/dist/src/v2/owner-control-lock.d.ts +13 -1
- package/dist/src/v2/owner-control-lock.d.ts.map +1 -1
- package/dist/src/v2/owner-control-lock.js +66 -7
- package/dist/src/v2/owner-control-lock.js.map +1 -1
- package/dist/src/v2/proof-report.d.ts +2 -1
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +6 -40
- package/dist/src/v2/proof-report.js.map +1 -1
- package/dist/src/v2/proof-store.d.ts +19 -7
- package/dist/src/v2/proof-store.d.ts.map +1 -1
- package/dist/src/v2/proof-store.js +162 -41
- package/dist/src/v2/proof-store.js.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.d.ts +7 -1
- package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.js +97 -70
- package/dist/src/v2/review-feedback-coordinator.js.map +1 -1
- package/dist/src/v2/review-feedback.d.ts +0 -18
- package/dist/src/v2/review-feedback.d.ts.map +1 -1
- package/dist/src/v2/review-feedback.js +9 -34
- package/dist/src/v2/review-feedback.js.map +1 -1
- package/dist/src/v2/route-coordinator.d.ts +7 -12
- package/dist/src/v2/route-coordinator.d.ts.map +1 -1
- package/dist/src/v2/route-coordinator.js +33 -128
- package/dist/src/v2/route-coordinator.js.map +1 -1
- package/dist/src/v2/route-decision.d.ts +0 -9
- package/dist/src/v2/route-decision.d.ts.map +1 -1
- package/dist/src/v2/route-decision.js +7 -27
- package/dist/src/v2/route-decision.js.map +1 -1
- package/dist/src/v2/run-issue.d.ts +64 -26
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +1298 -620
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +39 -25
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +469 -88
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +61 -36
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +992 -402
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/spec-coordinator.d.ts +16 -28
- package/dist/src/v2/spec-coordinator.d.ts.map +1 -1
- package/dist/src/v2/spec-coordinator.js +81 -43
- package/dist/src/v2/spec-coordinator.js.map +1 -1
- package/dist/src/v2/spec-delivery.d.ts +3 -34
- package/dist/src/v2/spec-delivery.d.ts.map +1 -1
- package/dist/src/v2/spec-delivery.js +42 -107
- package/dist/src/v2/spec-delivery.js.map +1 -1
- package/docs/deep-dive.md +52 -14
- package/internal-workflow/evals/coding-skill-evals.json +58 -17
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/schemas/proof-report-v1.json +1 -1
- package/internal-workflow/skills/code-review/SKILL.md +18 -0
- package/internal-workflow/skills/spec-implementer/SKILL.md +8 -1
- package/package.json +1 -1
package/dist/src/v2/runtime.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
287
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
727
|
+
}
|
|
728
|
+
if (handle) {
|
|
306
729
|
try {
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
|
331
|
-
const worktreePath =
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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: '
|
|
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) =>
|
|
512
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
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,
|
|
598
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}, signal);
|
|
627
|
-
if (result.kind === 'cancelled')
|
|
1150
|
+
],
|
|
1151
|
+
});
|
|
1152
|
+
if (result.status === 'cancelled')
|
|
628
1153
|
return { status: 'cancelled' };
|
|
629
|
-
if (
|
|
630
|
-
return { status: 'retryable', code:
|
|
631
|
-
if (result.
|
|
632
|
-
return { status: '
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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: '
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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,
|
|
660
|
-
|
|
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
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (result.kind === 'cancelled')
|
|
1195
|
+
],
|
|
1196
|
+
});
|
|
1197
|
+
if (result.status === 'cancelled')
|
|
686
1198
|
return { status: 'cancelled' };
|
|
687
|
-
if (
|
|
688
|
-
return { status: 'retryable', code:
|
|
689
|
-
if (result.
|
|
690
|
-
return { status: '
|
|
691
|
-
|
|
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: '
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
817
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
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({
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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);
|