codex-orchestrator 2.0.3 → 2.0.5
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 +51 -427
- package/README.md +161 -37
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.d.ts +5 -0
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +10 -2
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.d.ts.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.js +6 -7
- package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
- package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts +7 -1
- package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts.map +1 -1
- package/dist/src/v2/adapters/gh-pull-request-adapter.js +288 -0
- package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
- package/dist/src/v2/adapters/pull-requests.d.ts +69 -0
- package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
- package/dist/src/v2/adapters/pull-requests.js +48 -0
- package/dist/src/v2/adapters/pull-requests.js.map +1 -1
- package/dist/src/v2/adapters/worktree.d.ts +1 -0
- package/dist/src/v2/adapters/worktree.d.ts.map +1 -1
- package/dist/src/v2/adapters/worktree.js +10 -1
- package/dist/src/v2/adapters/worktree.js.map +1 -1
- package/dist/src/v2/cli-contract.d.ts +3 -3
- package/dist/src/v2/cli-contract.d.ts.map +1 -1
- package/dist/src/v2/cli-contract.js +1 -3
- package/dist/src/v2/cli-contract.js.map +1 -1
- package/dist/src/v2/cli.d.ts +33 -0
- package/dist/src/v2/cli.d.ts.map +1 -0
- package/dist/src/v2/{candidate-cli.js → cli.js} +55 -39
- package/dist/src/v2/cli.js.map +1 -0
- package/dist/src/v2/code-review-report.d.ts +1 -1
- package/dist/src/v2/code-review-report.d.ts.map +1 -1
- package/dist/src/v2/code-review-report.js +2 -2
- package/dist/src/v2/code-review-report.js.map +1 -1
- 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/config.d.ts +2 -3
- package/dist/src/v2/config.d.ts.map +1 -1
- package/dist/src/v2/config.js +0 -3
- package/dist/src/v2/config.js.map +1 -1
- package/dist/src/v2/contained-report-operation.d.ts +2 -2
- package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
- package/dist/src/v2/contained-report-operation.js +1 -1
- package/dist/src/v2/contained-report-operation.js.map +1 -1
- package/dist/src/v2/containment.d.ts +15 -2
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +43 -6
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +5 -10
- package/dist/src/v2/direct-delivery.d.ts.map +1 -1
- package/dist/src/v2/direct-delivery.js +32 -91
- package/dist/src/v2/direct-delivery.js.map +1 -1
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +55 -29
- package/dist/src/v2/proof-report.js.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.d.ts +54 -0
- package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -0
- package/dist/src/v2/review-feedback-coordinator.js +245 -0
- package/dist/src/v2/review-feedback-coordinator.js.map +1 -0
- package/dist/src/v2/review-feedback.d.ts +127 -0
- package/dist/src/v2/review-feedback.d.ts.map +1 -0
- package/dist/src/v2/review-feedback.js +436 -0
- package/dist/src/v2/review-feedback.js.map +1 -0
- package/dist/src/v2/run-issue.d.ts +63 -9
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +798 -78
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +49 -5
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +138 -44
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +40 -3
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +245 -52
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/setup-cli.d.ts.map +1 -1
- package/dist/src/v2/setup-cli.js +4 -11
- package/dist/src/v2/setup-cli.js.map +1 -1
- package/dist/src/v2/setup-runtime.d.ts.map +1 -1
- package/dist/src/v2/setup-runtime.js +1 -61
- package/dist/src/v2/setup-runtime.js.map +1 -1
- package/dist/src/v2/setup-store.d.ts +0 -5
- package/dist/src/v2/setup-store.d.ts.map +1 -1
- package/dist/src/v2/setup-store.js +3 -106
- package/dist/src/v2/setup-store.js.map +1 -1
- package/dist/src/v2/setup.d.ts +6 -46
- package/dist/src/v2/setup.d.ts.map +1 -1
- package/dist/src/v2/setup.js +12 -294
- package/dist/src/v2/setup.js.map +1 -1
- package/dist/src/v2/workflow-assets.d.ts +19 -11
- package/dist/src/v2/workflow-assets.d.ts.map +1 -1
- package/dist/src/v2/workflow-assets.js +132 -40
- package/dist/src/v2/workflow-assets.js.map +1 -1
- package/docs/deep-dive.md +328 -56
- package/internal-workflow/docs/agents/bugfix-quality-gate.md +11 -0
- package/internal-workflow/docs/agents/coding-skill-routing.md +116 -196
- package/internal-workflow/docs/agents/contract-test-ledger.md +11 -1
- package/internal-workflow/docs/agents/review-gates.md +32 -39
- package/internal-workflow/docs/agents/review-protocol.md +75 -147
- package/internal-workflow/evals/coding-skill-evals.json +84 -0
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/operations/acceptance-proof/SKILL.md +7 -1
- package/internal-workflow/operations/ambiguity-review/SKILL.md +2 -0
- package/internal-workflow/operations/code-review/SKILL.md +21 -1
- package/internal-workflow/operations/implementation/SKILL.md +22 -1
- package/internal-workflow/operations/spec-author/SKILL.md +10 -1
- package/internal-workflow/operations/spec-review/SKILL.md +10 -1
- package/internal-workflow/operations/triage/SKILL.md +10 -1
- package/internal-workflow/schemas/code-review-v1.json +1 -1
- package/internal-workflow/schemas/proof-report-v1.json +1 -1
- package/internal-workflow/skills/agent-auto/SKILL.md +6 -1
- package/internal-workflow/skills/code-debugger/SKILL.md +122 -0
- package/internal-workflow/skills/code-debugger/agents/openai.yaml +7 -0
- package/internal-workflow/skills/code-review/SKILL.md +51 -17
- package/internal-workflow/skills/code-review/references/cleanup-lens.md +52 -0
- package/internal-workflow/skills/implementation-spec-maker/SKILL.md +15 -6
- package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +1 -1
- package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +2 -2
- package/internal-workflow/skills/implementation-spec-review/SKILL.md +108 -204
- package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
- package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +93 -0
- package/internal-workflow/skills/small-task-implementer/SKILL.md +15 -8
- package/internal-workflow/skills/spec-implementer/SKILL.md +101 -172
- package/internal-workflow/skills/spec-implementer/evals/evals.json +30 -0
- package/internal-workflow/skills/spec-implementer/references/review-loop.md +100 -0
- package/internal-workflow/skills/tdd/SKILL.md +20 -6
- package/internal-workflow/skills/tdd/agents/openai.yaml +2 -2
- package/internal-workflow/skills/tdd/evals/evals.json +18 -0
- package/internal-workflow/skills/tdd/mocking.md +3 -42
- package/internal-workflow/skills/tdd/refactoring.md +6 -8
- package/package.json +9 -6
- package/dist/src/v2/adapters/target-activity-fence.d.ts +0 -23
- package/dist/src/v2/adapters/target-activity-fence.d.ts.map +0 -1
- package/dist/src/v2/adapters/target-activity-fence.js +0 -249
- package/dist/src/v2/adapters/target-activity-fence.js.map +0 -1
- package/dist/src/v2/candidate-cli.d.ts +0 -26
- package/dist/src/v2/candidate-cli.d.ts.map +0 -1
- package/dist/src/v2/candidate-cli.js.map +0 -1
- package/dist/src/v2/legacy-cutover.d.ts +0 -52
- package/dist/src/v2/legacy-cutover.d.ts.map +0 -1
- package/dist/src/v2/legacy-cutover.js +0 -87
- package/dist/src/v2/legacy-cutover.js.map +0 -1
- package/internal-workflow/docs/agents/artifact-review-loop.md +0 -267
- package/internal-workflow/docs/agents/implementation-review-loop.md +0 -302
- package/internal-workflow/operations/cleanup-review/SKILL.md +0 -3
- package/internal-workflow/operations/spec-implementation/SKILL.md +0 -3
- package/internal-workflow/profiles/implementer_deep.toml +0 -9
- package/internal-workflow/profiles/researcher_standard.toml +0 -9
- package/internal-workflow/profiles/reviewer_fast.toml +0 -9
- package/internal-workflow/skills/cleanup-review/SKILL.md +0 -84
- package/internal-workflow/skills/cleanup-review/agents/openai.yaml +0 -6
- package/internal-workflow/skills/codebase-design/DEEPENING.md +0 -35
- package/internal-workflow/skills/codebase-design/DESIGN-IT-TWICE.md +0 -50
- package/internal-workflow/skills/codebase-design/SKILL.md +0 -82
- package/internal-workflow/skills/codebase-design/agents/openai.yaml +0 -6
- package/internal-workflow/skills/research/SKILL.md +0 -107
- package/internal-workflow/skills/research/agents/openai.yaml +0 -6
- package/internal-workflow/skills/ui-evidence-proof/SKILL.md +0 -123
- package/internal-workflow/skills/ui-evidence-proof/agents/openai.yaml +0 -6
package/dist/src/v2/runtime.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { lstat, mkdir, open, readFile, readdir, readlink, realpath, rm } from 'node:fs/promises';
|
|
4
|
+
import { access, lstat, mkdir, open, readFile, readdir, readlink, realpath, rm } from 'node:fs/promises';
|
|
5
5
|
import { homedir, hostname } from 'node:os';
|
|
6
|
-
import { dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
|
|
6
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
|
|
7
7
|
import { writeDurableAtomicFile } from './adapters/durable-atomic-file.js';
|
|
8
8
|
import { GitWorktreeManager } from './adapters/worktree.js';
|
|
9
|
+
import { ReviewFeedbackCoordinator } from './review-feedback-coordinator.js';
|
|
9
10
|
import { defaultProcessExecutor } from './adapters/command.js';
|
|
10
11
|
import { AcceptanceProof, ProofQuiescenceError } from './acceptance-proof.js';
|
|
11
12
|
import { createCheckedChangeCapabilities } from './checked-change.js';
|
|
@@ -13,7 +14,7 @@ import { InjectedContainedReportOperation } from './contained-report-operation.j
|
|
|
13
14
|
import { ContainedImplementationReviewer } from './implementation-reviewer.js';
|
|
14
15
|
import { parseAgentAutoConfig } from './config.js';
|
|
15
16
|
import { WaitingHumanCoordinator } from './waiting-human-coordinator.js';
|
|
16
|
-
import { canonicalJson, containmentArgvPolicySha256, containmentCertificatePath, parseJsonWithoutDuplicateKeys, readContainmentCertificate, sha256, } from './containment.js';
|
|
17
|
+
import { assertContainmentCertificateMatchesRuntime, canonicalJson, containmentArgvPolicySha256, containmentCertificatePath, parseJsonWithoutDuplicateKeys, readContainmentCertificate, sha256, } from './containment.js';
|
|
17
18
|
import { FileProofRecordWriter } from './proof-store.js';
|
|
18
19
|
import { acquireOwnerControlLock, OwnerControlLockBlockedError } from './owner-control-lock.js';
|
|
19
20
|
import { decodeAgentReportForValidation } from './report-envelope.js';
|
|
@@ -47,6 +48,30 @@ export class LocalGitRunIssueAdapter {
|
|
|
47
48
|
requiredBaseSha: input.baseSha,
|
|
48
49
|
});
|
|
49
50
|
}
|
|
51
|
+
async ensureContinuationWorktree(input) {
|
|
52
|
+
const localHead = (await this.git([
|
|
53
|
+
'-C', input.targetRoot, 'rev-parse', '--verify', `refs/heads/${input.branchName}`,
|
|
54
|
+
])).trim();
|
|
55
|
+
const remoteHead = await this.getRemoteBranchSha(input.targetRoot, input.branchName);
|
|
56
|
+
if (localHead !== input.publishedHeadSha || remoteHead !== input.publishedHeadSha) {
|
|
57
|
+
throw new Error('continuation branch refs do not exactly match the published head');
|
|
58
|
+
}
|
|
59
|
+
await this.worktrees.ensureIssueWorktree({
|
|
60
|
+
targetRoot: input.targetRoot,
|
|
61
|
+
workspacePath: input.worktreePath,
|
|
62
|
+
branchName: input.branchName,
|
|
63
|
+
baseBranch: input.baseBranch,
|
|
64
|
+
requiredBaseSha: input.publishedHeadSha,
|
|
65
|
+
allowResume: true,
|
|
66
|
+
});
|
|
67
|
+
const observed = await this.inspectWorktree({
|
|
68
|
+
worktreePath: input.worktreePath,
|
|
69
|
+
branchName: input.branchName,
|
|
70
|
+
baseSha: input.publishedHeadSha,
|
|
71
|
+
});
|
|
72
|
+
if (observed !== 'matching')
|
|
73
|
+
throw new Error('continuation worktree does not match the published head');
|
|
74
|
+
}
|
|
50
75
|
async inspectWorktree(input) {
|
|
51
76
|
try {
|
|
52
77
|
const stat = await lstat(input.worktreePath);
|
|
@@ -110,6 +135,9 @@ export class LocalGitRunIssueAdapter {
|
|
|
110
135
|
listChangedFiles(worktreePath) {
|
|
111
136
|
return this.worktrees.listChangedFiles(worktreePath);
|
|
112
137
|
}
|
|
138
|
+
listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot) {
|
|
139
|
+
return this.worktrees.listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot);
|
|
140
|
+
}
|
|
113
141
|
async fingerprintChangedFiles(worktreePath, changedFiles) {
|
|
114
142
|
const root = await realpath(worktreePath);
|
|
115
143
|
const entries = [];
|
|
@@ -193,6 +221,12 @@ export class ContainedImplementationAgent {
|
|
|
193
221
|
bootId: this.dependencies.bootId,
|
|
194
222
|
});
|
|
195
223
|
const baseline = await this.dependencies.git.snapshot(input.worktreePath);
|
|
224
|
+
await input.onPrepared?.({
|
|
225
|
+
attemptId,
|
|
226
|
+
reportPath: attempt.reportPath,
|
|
227
|
+
preparedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
|
|
228
|
+
baseline,
|
|
229
|
+
});
|
|
196
230
|
try {
|
|
197
231
|
const result = await (this.dependencies.process ?? new CodexProcess()).run({
|
|
198
232
|
codexPath: config.codex.command,
|
|
@@ -210,6 +244,8 @@ export class ContainedImplementationAgent {
|
|
|
210
244
|
`The operation's immutable workflow root is ${attempt.workflowRoot}.`,
|
|
211
245
|
`Implement issue #${input.issue.number}: ${input.issue.title}`,
|
|
212
246
|
`Implementation cycle: ${input.cycle}.`,
|
|
247
|
+
...(input.reviewFeedbackRound ? [`Pull-request feedback repair round: ${input.reviewFeedbackRound}.`] : []),
|
|
248
|
+
...(input.reviewFeedback?.length ? [`Frozen trusted pull-request feedback: ${canonicalJson(input.reviewFeedback)}`] : []),
|
|
213
249
|
`Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
|
|
214
250
|
...(input.reworkFindings.length > 0 ? [`Repair these verified findings: ${canonicalJson(input.reworkFindings)}`] : []),
|
|
215
251
|
...(input.repairOnly ? ['Report repair only: do not modify any worktree file; emit a schema-valid implementation report for the existing change.'] : []),
|
|
@@ -219,6 +255,9 @@ export class ContainedImplementationAgent {
|
|
|
219
255
|
idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
220
256
|
operationPolicy: attempt.policy,
|
|
221
257
|
executionProfile: attempt.profile,
|
|
258
|
+
...(input.onLaunched ? { onSpawned: ({ pid, processGroupId }) => input.onLaunched({
|
|
259
|
+
attemptId, pid, processGroupId, launchedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
|
|
260
|
+
}) } : {}),
|
|
222
261
|
}, input.signal);
|
|
223
262
|
if (result.kind === 'cancelled')
|
|
224
263
|
return { kind: 'cancelled' };
|
|
@@ -248,6 +287,26 @@ export class ContainedImplementationAgent {
|
|
|
248
287
|
};
|
|
249
288
|
}
|
|
250
289
|
}
|
|
290
|
+
async recover(input) {
|
|
291
|
+
const expected = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(input.canonicalRepository), 'runs', input.runId, 'attempts', input.invocation.attemptId, 'report.json');
|
|
292
|
+
if (resolve(input.invocation.reportPath) !== expected) {
|
|
293
|
+
throw new Error('review feedback implementation report path is not attempt-owned');
|
|
294
|
+
}
|
|
295
|
+
let bytes;
|
|
296
|
+
try {
|
|
297
|
+
bytes = await readRegularFile(expected);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
if (isErrorCode(error, 'ENOENT'))
|
|
301
|
+
return undefined;
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
kind: 'completed',
|
|
306
|
+
attemptId: input.invocation.attemptId,
|
|
307
|
+
report: decodeAgentReportForValidation(bytes),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
251
310
|
}
|
|
252
311
|
export class ContainedProofAgent {
|
|
253
312
|
dependencies;
|
|
@@ -347,11 +406,43 @@ export class ContainedProofAgent {
|
|
|
347
406
|
}
|
|
348
407
|
}
|
|
349
408
|
}
|
|
409
|
+
export async function resolveCodexExecutable(command, safePath) {
|
|
410
|
+
if (command.length === 0 || safePath.length === 0)
|
|
411
|
+
throw new Error('Codex command and safe path must be non-empty');
|
|
412
|
+
if (!isAbsolute(command) && command.includes('/'))
|
|
413
|
+
throw new Error('relative Codex command paths are not supported');
|
|
414
|
+
const candidates = isAbsolute(command)
|
|
415
|
+
? [command]
|
|
416
|
+
: safePath.split(':').filter((entry) => entry.length > 0).map((entry) => join(entry, command));
|
|
417
|
+
for (const candidate of candidates) {
|
|
418
|
+
try {
|
|
419
|
+
const canonical = await realpath(candidate);
|
|
420
|
+
const stat = await lstat(canonical);
|
|
421
|
+
if (!stat.isFile())
|
|
422
|
+
continue;
|
|
423
|
+
await access(canonical, constants.X_OK);
|
|
424
|
+
return canonical;
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// Try the next safe-path entry.
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
throw new Error('Codex executable is unavailable in the configured safe path');
|
|
431
|
+
}
|
|
350
432
|
export function createV2Runtime(input) {
|
|
351
433
|
const targetRoot = resolve(input.targetRoot);
|
|
352
434
|
const orchestratorHome = resolve(input.orchestratorHome);
|
|
353
435
|
const now = input.now ?? (() => new Date().toISOString());
|
|
354
436
|
const commandExecutor = input.processExecutor ?? defaultProcessExecutor;
|
|
437
|
+
const runtimeSafePath = requireRuntimeString(input.safePath, 'safePath');
|
|
438
|
+
let codexExecutable;
|
|
439
|
+
const resolveRuntimeCodex = async (command) => {
|
|
440
|
+
if (codexExecutable?.command === command)
|
|
441
|
+
return codexExecutable.path;
|
|
442
|
+
const path = await resolveCodexExecutable(command, runtimeSafePath);
|
|
443
|
+
codexExecutable = { command, path };
|
|
444
|
+
return path;
|
|
445
|
+
};
|
|
355
446
|
const git = input.git ?? new LocalGitRunIssueAdapter(commandExecutor);
|
|
356
447
|
const proofFreshnessGit = git;
|
|
357
448
|
const controller = new AbortController();
|
|
@@ -369,7 +460,7 @@ export function createV2Runtime(input) {
|
|
|
369
460
|
config: () => requireConfig(currentConfig),
|
|
370
461
|
orchestratorHome,
|
|
371
462
|
parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
|
|
372
|
-
safePath:
|
|
463
|
+
safePath: runtimeSafePath,
|
|
373
464
|
bootId: input.bootId,
|
|
374
465
|
process: containedProcess,
|
|
375
466
|
createAttemptId: input.createAttemptId,
|
|
@@ -676,7 +767,11 @@ export function createV2Runtime(input) {
|
|
|
676
767
|
if (resolve(requestedRoot) !== targetRoot)
|
|
677
768
|
throw new Error('runtime target root mismatch');
|
|
678
769
|
const bytes = await readRegularFile(join(targetRoot, '.codex-orchestrator', 'config.json'));
|
|
679
|
-
const
|
|
770
|
+
const parsed = parseAgentAutoConfig(parseJsonWithoutDuplicateKeys(bytes.toString('utf8')));
|
|
771
|
+
const config = {
|
|
772
|
+
...parsed,
|
|
773
|
+
codex: { ...parsed.codex, command: await resolveRuntimeCodex(parsed.codex.command) },
|
|
774
|
+
};
|
|
680
775
|
const path = join(targetRoot, config.runner.stateDir, 'v2', 'run-state.json');
|
|
681
776
|
if (!runRecords)
|
|
682
777
|
runRecords = new FileRunRecordWriter(path);
|
|
@@ -744,14 +839,17 @@ export function createV2Runtime(input) {
|
|
|
744
839
|
readConfig,
|
|
745
840
|
validateContainment: async (config) => {
|
|
746
841
|
const certificate = await readContainmentCertificate(containmentCertificatePath(orchestratorHome));
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
throw new Error('
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
842
|
+
const executablePath = await resolveRuntimeCodex(config.codex.command);
|
|
843
|
+
const version = await commandExecutor(executablePath, ['--version']);
|
|
844
|
+
if (version.exitCode !== 0)
|
|
845
|
+
throw new Error('Codex version is unavailable');
|
|
846
|
+
assertContainmentCertificateMatchesRuntime(certificate, {
|
|
847
|
+
codexVersion: version.stdout.trim(),
|
|
848
|
+
codexExecutablePath: executablePath,
|
|
849
|
+
codexExecutableSha256: sha256(await readFile(executablePath)),
|
|
850
|
+
packageVersion: input.packageVersion,
|
|
851
|
+
argvPolicySha256: containmentArgvPolicySha256(),
|
|
852
|
+
});
|
|
755
853
|
},
|
|
756
854
|
ownerLock: {
|
|
757
855
|
acquire: async ({ canonicalRepository }) => acquireOwnerLock({
|
|
@@ -797,10 +895,26 @@ export function createV2Runtime(input) {
|
|
|
797
895
|
if (matches.length > 1)
|
|
798
896
|
throw new Error('multiple open pull requests match publication intent');
|
|
799
897
|
const match = matches[0];
|
|
800
|
-
|
|
898
|
+
if (!match)
|
|
899
|
+
return undefined;
|
|
900
|
+
const reviewTarget = await input.pullRequests.getReviewTarget(match.number);
|
|
901
|
+
return {
|
|
902
|
+
url: match.url,
|
|
903
|
+
body: match.body,
|
|
904
|
+
number: match.number,
|
|
905
|
+
nodeId: match.nodeId,
|
|
906
|
+
...(reviewTarget ? { headSha: reviewTarget.headRefOid } : {}),
|
|
907
|
+
};
|
|
801
908
|
},
|
|
802
909
|
createDraft: async ({ title, body, headBranch, baseBranch }) => input.pullRequests.createDraftPullRequest({ title, body, headBranch, baseBranch }),
|
|
910
|
+
listConversationComments: async (number) => (await input.pullRequests.listConversationComments(number))
|
|
911
|
+
.map((comment) => ({ id: comment.id, body: comment.body })),
|
|
912
|
+
postConversationComment: async (number, body) => {
|
|
913
|
+
const comment = await input.pullRequests.postConversationComment(number, body);
|
|
914
|
+
return { id: comment.id, body: comment.body };
|
|
915
|
+
},
|
|
803
916
|
},
|
|
917
|
+
reviewFeedback: new ReviewFeedbackCoordinator({ pullRequests: input.pullRequests, issues: input.issues, now }),
|
|
804
918
|
git,
|
|
805
919
|
routeCoordinator: {
|
|
806
920
|
run: ({ state, ...routeInput }) => new RouteCoordinator({
|
|
@@ -1070,7 +1184,7 @@ async function prepareContainedAttempt(input) {
|
|
|
1070
1184
|
bootId: input.bootId,
|
|
1071
1185
|
});
|
|
1072
1186
|
const reportOnly = input.operationId === 'triage' || input.operationId === 'ambiguity-review'
|
|
1073
|
-
|| input.operationId === '
|
|
1187
|
+
|| input.operationId === 'code-review' || input.operationId === 'spec-review';
|
|
1074
1188
|
if ((reportOnly
|
|
1075
1189
|
? snapshot.policy.sandboxMode !== 'read-only'
|
|
1076
1190
|
|| snapshot.policy.worktreeAccess !== 'read-only'
|
|
@@ -1194,56 +1308,135 @@ async function waitForProcessGroupAbsent(processGroupId) {
|
|
|
1194
1308
|
}
|
|
1195
1309
|
}
|
|
1196
1310
|
export async function materializeReportReadView(input) {
|
|
1311
|
+
await assertDisjointReadViewPaths(input.worktreePath, input.destination);
|
|
1312
|
+
const deniedPaths = normalizeReadViewDeniedPaths(input.deniedPaths);
|
|
1197
1313
|
await rm(input.destination, { recursive: true, force: true });
|
|
1198
1314
|
await mkdir(input.destination, { recursive: true, mode: 0o700 });
|
|
1199
|
-
await
|
|
1200
|
-
|
|
1201
|
-
|
|
1315
|
+
await materializeCurrentReadView(input.worktreePath, input.destination, deniedPaths);
|
|
1316
|
+
await scrubReportReadView(input.destination);
|
|
1317
|
+
return input.destination;
|
|
1318
|
+
}
|
|
1319
|
+
async function assertDisjointReadViewPaths(worktreePath, destination) {
|
|
1320
|
+
const worktree = await realpath(resolve(worktreePath));
|
|
1321
|
+
const readView = await canonicalProspectivePath(destination);
|
|
1322
|
+
if (isPathWithin(worktree, readView) || isPathWithin(readView, worktree)) {
|
|
1323
|
+
throw new Error('report read-view destination must not overlap its source worktree');
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async function canonicalProspectivePath(path) {
|
|
1327
|
+
let current = resolve(path);
|
|
1328
|
+
const suffix = [];
|
|
1329
|
+
while (true) {
|
|
1330
|
+
try {
|
|
1331
|
+
return resolve(await realpath(current), ...suffix);
|
|
1332
|
+
}
|
|
1333
|
+
catch (error) {
|
|
1334
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
1335
|
+
throw error;
|
|
1336
|
+
const parent = dirname(current);
|
|
1337
|
+
if (parent === current)
|
|
1338
|
+
throw error;
|
|
1339
|
+
suffix.unshift(basename(current));
|
|
1340
|
+
current = parent;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function isPathWithin(parent, child) {
|
|
1345
|
+
const contained = relative(parent, child);
|
|
1346
|
+
return contained === '' || (contained !== '..' && !contained.startsWith('../') && !isAbsolute(contained));
|
|
1347
|
+
}
|
|
1348
|
+
function normalizeReadViewDeniedPaths(paths) {
|
|
1349
|
+
return paths.flatMap((path) => {
|
|
1350
|
+
if (isAbsolute(path))
|
|
1351
|
+
return [];
|
|
1352
|
+
if (path.length === 0 || posix.normalize(path) !== path
|
|
1353
|
+
|| path.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
1354
|
+
throw new Error('report read-view denied path escapes destination');
|
|
1355
|
+
}
|
|
1356
|
+
return [path];
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
async function materializeCurrentReadView(worktreePath, destination, deniedPaths) {
|
|
1360
|
+
const paths = new Set(await readGitPathList(worktreePath, ['ls-files', '--cached', '--others', '--exclude-standard', '-z']));
|
|
1361
|
+
for (const path of [...paths].sort()) {
|
|
1362
|
+
if (isExcludedReadViewPath(path, deniedPaths))
|
|
1202
1363
|
continue;
|
|
1203
|
-
const
|
|
1204
|
-
const contained = relative(
|
|
1364
|
+
const target = resolve(destination, path);
|
|
1365
|
+
const contained = relative(destination, target);
|
|
1205
1366
|
if (contained === '' || contained === '..' || contained.startsWith('../') || isAbsolute(contained)) {
|
|
1206
|
-
throw new Error('report read-view
|
|
1367
|
+
throw new Error('report read-view path escapes destination');
|
|
1368
|
+
}
|
|
1369
|
+
const source = resolve(worktreePath, path);
|
|
1370
|
+
let metadata;
|
|
1371
|
+
try {
|
|
1372
|
+
metadata = await lstat(source);
|
|
1373
|
+
}
|
|
1374
|
+
catch (error) {
|
|
1375
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
1376
|
+
throw error;
|
|
1377
|
+
await rm(target, { recursive: true, force: true });
|
|
1378
|
+
continue;
|
|
1207
1379
|
}
|
|
1208
|
-
|
|
1380
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
1381
|
+
await rm(target, { recursive: true, force: true });
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
await copyRegularFileNoFollow(source, target, metadata.mode);
|
|
1209
1385
|
}
|
|
1210
|
-
await scrubReportReadView(input.destination);
|
|
1211
|
-
return input.destination;
|
|
1212
1386
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
let archiveCode = null;
|
|
1230
|
-
let extractCode = null;
|
|
1231
|
-
const settle = () => {
|
|
1232
|
-
if (archiveCode === null || extractCode === null)
|
|
1387
|
+
function isExcludedReadViewPath(path, deniedPaths) {
|
|
1388
|
+
if (path.split('/').some((segment) => segment.startsWith('.env')))
|
|
1389
|
+
return true;
|
|
1390
|
+
return deniedPaths.some((deniedPath) => path === deniedPath || path.startsWith(`${deniedPath}/`));
|
|
1391
|
+
}
|
|
1392
|
+
async function readGitPathList(worktreePath, args) {
|
|
1393
|
+
return new Promise((resolvePaths, reject) => {
|
|
1394
|
+
const child = spawn('git', ['-C', worktreePath, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1395
|
+
const stdout = [];
|
|
1396
|
+
const stderr = [];
|
|
1397
|
+
child.once('error', reject);
|
|
1398
|
+
child.stdout.on('data', (chunk) => { stdout.push(chunk); });
|
|
1399
|
+
child.stderr.on('data', (chunk) => { stderr.push(chunk); });
|
|
1400
|
+
child.once('close', (code) => {
|
|
1401
|
+
if (code !== 0) {
|
|
1402
|
+
reject(new Error(`report read-view inventory failed: ${Buffer.concat(stderr).toString('utf8').slice(0, 8 * 1024)}`));
|
|
1233
1403
|
return;
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
resolveExtract();
|
|
1238
|
-
};
|
|
1239
|
-
archive.once('close', (code) => { archiveCode = code ?? 1; settle(); });
|
|
1240
|
-
extract.once('close', (code) => { extractCode = code ?? 1; settle(); });
|
|
1404
|
+
}
|
|
1405
|
+
resolvePaths(Buffer.concat(stdout).toString('utf8').split('\0').filter((path) => path.length > 0));
|
|
1406
|
+
});
|
|
1241
1407
|
});
|
|
1242
1408
|
}
|
|
1409
|
+
async function copyRegularFileNoFollow(sourcePath, targetPath, mode) {
|
|
1410
|
+
const source = await open(sourcePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1411
|
+
let target;
|
|
1412
|
+
try {
|
|
1413
|
+
const metadata = await source.stat();
|
|
1414
|
+
if (!metadata.isFile())
|
|
1415
|
+
throw new Error('report read-view source must be a regular file');
|
|
1416
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
1417
|
+
await mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
1418
|
+
target = await open(targetPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, mode & 0o777);
|
|
1419
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
1420
|
+
while (true) {
|
|
1421
|
+
const { bytesRead } = await source.read(buffer, 0, buffer.length, null);
|
|
1422
|
+
if (bytesRead === 0)
|
|
1423
|
+
break;
|
|
1424
|
+
let offset = 0;
|
|
1425
|
+
while (offset < bytesRead) {
|
|
1426
|
+
const { bytesWritten } = await target.write(buffer, offset, bytesRead - offset, null);
|
|
1427
|
+
offset += bytesWritten;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
finally {
|
|
1432
|
+
await target?.close();
|
|
1433
|
+
await source.close();
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1243
1436
|
async function scrubReportReadView(directory) {
|
|
1244
1437
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
1245
1438
|
const path = join(directory, entry.name);
|
|
1246
|
-
if (entry.name
|
|
1439
|
+
if (entry.name.startsWith('.env') || entry.isSymbolicLink()) {
|
|
1247
1440
|
await rm(path, { recursive: true, force: true });
|
|
1248
1441
|
}
|
|
1249
1442
|
else if (entry.isDirectory()) {
|