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