codex-orchestrator 2.0.10 → 2.0.12
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 +52 -0
- package/README.md +29 -30
- package/dist/src/index.d.ts +4 -9
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/v2/acceptance-proof.d.ts +53 -25
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +189 -197
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/active-attempt.d.ts +94 -0
- package/dist/src/v2/active-attempt.d.ts.map +1 -0
- package/dist/src/v2/active-attempt.js +200 -0
- package/dist/src/v2/active-attempt.js.map +1 -0
- package/dist/src/v2/adapters/command.d.ts +7 -0
- package/dist/src/v2/adapters/command.d.ts.map +1 -1
- package/dist/src/v2/adapters/command.js +44 -3
- 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 +136 -0
- package/dist/src/v2/candidate.d.ts.map +1 -0
- package/dist/src/v2/candidate.js +107 -0
- package/dist/src/v2/candidate.js.map +1 -0
- package/dist/src/v2/checked-change.d.ts +39 -7
- package/dist/src/v2/checked-change.d.ts.map +1 -1
- package/dist/src/v2/checked-change.js +73 -1
- package/dist/src/v2/checked-change.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 +4 -6
- package/dist/src/v2/cli-contract.js.map +1 -1
- package/dist/src/v2/cli.d.ts +14 -0
- package/dist/src/v2/cli.d.ts.map +1 -1
- package/dist/src/v2/cli.js +70 -21
- package/dist/src/v2/cli.js.map +1 -1
- package/dist/src/v2/code-review-report.d.ts +10 -18
- package/dist/src/v2/code-review-report.d.ts.map +1 -1
- package/dist/src/v2/code-review-report.js +63 -60
- package/dist/src/v2/code-review-report.js.map +1 -1
- package/dist/src/v2/codex-process.d.ts +6 -1
- package/dist/src/v2/codex-process.d.ts.map +1 -1
- package/dist/src/v2/codex-process.js +36 -9
- package/dist/src/v2/codex-process.js.map +1 -1
- package/dist/src/v2/config.d.ts +0 -2
- package/dist/src/v2/config.d.ts.map +1 -1
- package/dist/src/v2/config.js +3 -6
- package/dist/src/v2/config.js.map +1 -1
- package/dist/src/v2/contained-report-operation.d.ts +17 -6
- package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
- package/dist/src/v2/contained-report-operation.js +11 -24
- package/dist/src/v2/contained-report-operation.js.map +1 -1
- package/dist/src/v2/containment.d.ts +1 -0
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +12 -2
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/delivery-authority.d.ts +26 -0
- package/dist/src/v2/delivery-authority.d.ts.map +1 -0
- package/dist/src/v2/delivery-authority.js +44 -0
- package/dist/src/v2/delivery-authority.js.map +1 -0
- package/dist/src/v2/direct-delivery.d.ts +19 -57
- package/dist/src/v2/direct-delivery.d.ts.map +1 -1
- package/dist/src/v2/direct-delivery.js +154 -211
- package/dist/src/v2/direct-delivery.js.map +1 -1
- package/dist/src/v2/immutable-workflow-publisher.d.ts.map +1 -1
- package/dist/src/v2/immutable-workflow-publisher.js +3 -1
- package/dist/src/v2/immutable-workflow-publisher.js.map +1 -1
- package/dist/src/v2/implementation-report.d.ts +3 -1
- package/dist/src/v2/implementation-report.d.ts.map +1 -1
- package/dist/src/v2/implementation-report.js +17 -4
- package/dist/src/v2/implementation-report.js.map +1 -1
- package/dist/src/v2/implementation-reviewer.d.ts +22 -13
- package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
- package/dist/src/v2/implementation-reviewer.js +96 -30
- 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/pending-effect-settlement.d.ts +44 -0
- package/dist/src/v2/pending-effect-settlement.d.ts.map +1 -0
- package/dist/src/v2/pending-effect-settlement.js +69 -0
- package/dist/src/v2/pending-effect-settlement.js.map +1 -0
- package/dist/src/v2/process-identity.d.ts +45 -0
- package/dist/src/v2/process-identity.d.ts.map +1 -0
- package/dist/src/v2/process-identity.js +118 -0
- package/dist/src/v2/process-identity.js.map +1 -0
- package/dist/src/v2/proof-report.d.ts +4 -2
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +16 -44
- package/dist/src/v2/proof-report.js.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.d.ts +8 -2
- package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.js +98 -71
- package/dist/src/v2/review-feedback-coordinator.js.map +1 -1
- package/dist/src/v2/review-feedback.d.ts +14 -38
- package/dist/src/v2/review-feedback.d.ts.map +1 -1
- package/dist/src/v2/review-feedback.js +48 -115
- package/dist/src/v2/review-feedback.js.map +1 -1
- package/dist/src/v2/run-issue.d.ts +139 -60
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +2236 -1974
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-state-projections.d.ts +84 -0
- package/dist/src/v2/run-state-projections.d.ts.map +1 -0
- package/dist/src/v2/run-state-projections.js +142 -0
- package/dist/src/v2/run-state-projections.js.map +1 -0
- package/dist/src/v2/run-store.d.ts +114 -82
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +308 -224
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime-assets.d.ts +3 -0
- package/dist/src/v2/runtime-assets.d.ts.map +1 -1
- package/dist/src/v2/runtime-assets.js +104 -0
- package/dist/src/v2/runtime-assets.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +52 -15
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +818 -348
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/setup.js +0 -2
- package/dist/src/v2/setup.js.map +1 -1
- package/dist/src/v2/validation-progression.d.ts +70 -0
- package/dist/src/v2/validation-progression.d.ts.map +1 -0
- package/dist/src/v2/validation-progression.js +247 -0
- package/dist/src/v2/validation-progression.js.map +1 -0
- package/dist/src/v2/workflow-assets.d.ts +9 -3
- package/dist/src/v2/workflow-assets.d.ts.map +1 -1
- package/dist/src/v2/workflow-assets.js +256 -43
- package/dist/src/v2/workflow-assets.js.map +1 -1
- package/docs/deep-dive.md +52 -14
- package/internal-workflow/docs/agents/bug-workflow-routing.md +9 -7
- package/internal-workflow/docs/agents/coding-skill-routing.md +170 -120
- package/internal-workflow/docs/agents/tool-usage.md +23 -12
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/operations/code-review/SKILL.md +34 -15
- package/internal-workflow/operations/implementation/SKILL.md +21 -16
- package/internal-workflow/profiles/implementer.toml +9 -0
- package/internal-workflow/profiles/review_coordinator.toml +9 -0
- package/internal-workflow/profiles/spec_reviewer.toml +9 -0
- package/internal-workflow/profiles/standards_reviewer.toml +9 -0
- package/internal-workflow/schemas/code-review-v1.json +1 -1
- package/internal-workflow/schemas/implementation-report-v1.json +1 -1
- package/internal-workflow/schemas/proof-report-v1.json +1 -1
- package/internal-workflow/skills/bug-root-cause-explainer/SKILL.md +114 -0
- package/internal-workflow/skills/bug-root-cause-explainer/agents/openai.yaml +7 -0
- package/internal-workflow/skills/bug-root-cause-explainer/evals/evals.json +18 -0
- package/internal-workflow/skills/code-review/SKILL.md +84 -288
- package/internal-workflow/skills/code-review/agents/openai.yaml +5 -3
- package/internal-workflow/skills/code-review/evals/evals.json +83 -0
- package/internal-workflow/skills/code-review/references/standards-smells.md +41 -0
- package/internal-workflow/skills/diagnosing-bugs/SKILL.md +69 -32
- package/internal-workflow/skills/diagnosing-bugs/agents/openai.yaml +2 -2
- package/internal-workflow/skills/diagnosing-bugs/evals/evals.json +63 -0
- package/internal-workflow/skills/grilling/SKILL.md +51 -0
- package/internal-workflow/skills/grilling/agents/openai.yaml +6 -0
- package/internal-workflow/skills/grilling/evals/evals.json +47 -0
- package/internal-workflow/skills/implement/SKILL.md +135 -0
- package/internal-workflow/skills/implement/agents/openai.yaml +6 -0
- package/internal-workflow/skills/implement/evals/evals.json +150 -0
- package/internal-workflow/skills/plan/SKILL.md +59 -0
- package/internal-workflow/skills/plan/agents/openai.yaml +6 -0
- package/internal-workflow/skills/plan/evals/evals.json +36 -0
- package/internal-workflow/skills/prototype/LOGIC.md +130 -0
- package/internal-workflow/skills/prototype/SKILL.md +69 -0
- package/internal-workflow/skills/prototype/UI.md +157 -0
- package/internal-workflow/skills/prototype/agents/openai.yaml +6 -0
- package/internal-workflow/skills/prototype/evals/evals.json +67 -0
- package/internal-workflow/skills/research/SKILL.md +110 -0
- package/internal-workflow/skills/research/agents/openai.yaml +6 -0
- package/internal-workflow/skills/research/evals/evals.json +49 -0
- package/internal-workflow/skills/tdd/SKILL.md +72 -67
- package/internal-workflow/skills/tdd/agents/openai.yaml +2 -2
- package/internal-workflow/skills/tdd/evals/evals.json +12 -0
- package/internal-workflow/skills/tdd/mocking.md +48 -1
- package/internal-workflow/skills/tdd/refactoring.md +3 -3
- package/internal-workflow/skills/tickets-orchestrator/SKILL.md +199 -0
- package/internal-workflow/skills/tickets-orchestrator/agents/openai.yaml +6 -0
- package/internal-workflow/skills/tickets-orchestrator/evals/evals.json +126 -0
- package/internal-workflow/skills/tickets-orchestrator/references/delegate-integrate.md +83 -0
- package/internal-workflow/skills/tickets-orchestrator/references/finish-delivery.md +69 -0
- package/internal-workflow/skills/tickets-orchestrator/references/stop-completion.md +63 -0
- package/internal-workflow/skills/to-spec/SKILL.md +133 -0
- package/internal-workflow/skills/to-spec/agents/openai.yaml +6 -0
- package/internal-workflow/skills/to-spec/evals/evals.json +24 -0
- package/internal-workflow/skills/to-tickets/SKILL.md +189 -0
- package/internal-workflow/skills/to-tickets/agents/openai.yaml +6 -0
- package/internal-workflow/skills/to-tickets/evals/evals.json +79 -0
- package/internal-workflow/skills/to-tickets/references/publishing-details.md +117 -0
- package/package.json +1 -1
- package/dist/src/v2/proof-store.d.ts +0 -42
- package/dist/src/v2/proof-store.d.ts.map +0 -1
- package/dist/src/v2/proof-store.js +0 -180
- package/dist/src/v2/proof-store.js.map +0 -1
- package/dist/src/v2/route-continuations.d.ts +0 -32
- package/dist/src/v2/route-continuations.d.ts.map +0 -1
- package/dist/src/v2/route-continuations.js +0 -2
- package/dist/src/v2/route-continuations.js.map +0 -1
- package/dist/src/v2/route-coordinator.d.ts +0 -77
- package/dist/src/v2/route-coordinator.d.ts.map +0 -1
- package/dist/src/v2/route-coordinator.js +0 -370
- package/dist/src/v2/route-coordinator.js.map +0 -1
- package/dist/src/v2/route-decision.d.ts +0 -129
- package/dist/src/v2/route-decision.d.ts.map +0 -1
- package/dist/src/v2/route-decision.js +0 -400
- package/dist/src/v2/route-decision.js.map +0 -1
- package/dist/src/v2/spec-coordinator.d.ts +0 -85
- package/dist/src/v2/spec-coordinator.d.ts.map +0 -1
- package/dist/src/v2/spec-coordinator.js +0 -88
- package/dist/src/v2/spec-coordinator.js.map +0 -1
- package/dist/src/v2/spec-delivery.d.ts +0 -143
- package/dist/src/v2/spec-delivery.d.ts.map +0 -1
- package/dist/src/v2/spec-delivery.js +0 -401
- package/dist/src/v2/spec-delivery.js.map +0 -1
- package/dist/src/v2/triage-route.d.ts +0 -68
- package/dist/src/v2/triage-route.d.ts.map +0 -1
- package/dist/src/v2/triage-route.js +0 -223
- package/dist/src/v2/triage-route.js.map +0 -1
- package/dist/src/v2/waiting-human-coordinator.d.ts +0 -49
- package/dist/src/v2/waiting-human-coordinator.d.ts.map +0 -1
- package/dist/src/v2/waiting-human-coordinator.js +0 -509
- package/dist/src/v2/waiting-human-coordinator.js.map +0 -1
- package/dist/src/v2/waiting-human.d.ts +0 -143
- package/dist/src/v2/waiting-human.d.ts.map +0 -1
- package/dist/src/v2/waiting-human.js +0 -408
- package/dist/src/v2/waiting-human.js.map +0 -1
- package/internal-workflow/docs/agents/contract-test-ledger.md +0 -71
- package/internal-workflow/docs/agents/review-gates.md +0 -42
- package/internal-workflow/docs/agents/review-protocol.md +0 -98
- package/internal-workflow/evals/coding-skill-evals.json +0 -332
- package/internal-workflow/operations/ambiguity-review/SKILL.md +0 -5
- package/internal-workflow/operations/qualification-repair/SKILL.md +0 -17
- package/internal-workflow/operations/spec-author/SKILL.md +0 -12
- package/internal-workflow/operations/spec-review/SKILL.md +0 -12
- package/internal-workflow/operations/triage/SKILL.md +0 -12
- package/internal-workflow/profiles/analyst_deep.toml +0 -9
- package/internal-workflow/profiles/implementer_standard.toml +0 -9
- package/internal-workflow/profiles/proof_agent.toml +0 -8
- package/internal-workflow/profiles/reviewer_deep.toml +0 -9
- package/internal-workflow/profiles/reviewer_standard.toml +0 -9
- package/internal-workflow/schemas/ambiguity-review-v1.json +0 -1
- package/internal-workflow/schemas/spec-author-v1.json +0 -1
- package/internal-workflow/schemas/spec-review-v1.json +0 -30
- package/internal-workflow/schemas/triage-route-v1.json +0 -1
- package/internal-workflow/skills/agent-auto/SKILL.md +0 -19
- package/internal-workflow/skills/agent-auto/agents/openai.yaml +0 -6
- package/internal-workflow/skills/code-debugger/SKILL.md +0 -122
- package/internal-workflow/skills/code-debugger/agents/openai.yaml +0 -7
- package/internal-workflow/skills/code-review/references/bug-classes.md +0 -56
- package/internal-workflow/skills/code-review/references/cleanup-lens.md +0 -52
- package/internal-workflow/skills/code-review/references/framework-lenses.md +0 -34
- package/internal-workflow/skills/code-review/references/targeted-recipes.md +0 -49
- package/internal-workflow/skills/implementation-spec-maker/SKILL.md +0 -107
- package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +0 -6
- package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +0 -32
- package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +0 -146
- package/internal-workflow/skills/implementation-spec-review/SKILL.md +0 -131
- package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +0 -6
- package/internal-workflow/skills/implementation-spec-review/evals/evals.json +0 -78
- package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +0 -121
- package/internal-workflow/skills/small-task-implementer/SKILL.md +0 -112
- package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +0 -6
- package/internal-workflow/skills/spec-implementer/SKILL.md +0 -126
- package/internal-workflow/skills/spec-implementer/agents/openai.yaml +0 -6
- package/internal-workflow/skills/spec-implementer/evals/evals.json +0 -30
- package/internal-workflow/skills/spec-implementer/references/review-loop.md +0 -100
- package/internal-workflow/skills/triage/AGENT-BRIEF.md +0 -192
- package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +0 -101
- package/internal-workflow/skills/triage/SKILL.md +0 -134
- package/internal-workflow/skills/triage/agents/openai.yaml +0 -6
package/dist/src/v2/runtime.js
CHANGED
|
@@ -1,45 +1,411 @@
|
|
|
1
1
|
import { constants } from 'node:fs';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { access, lstat, mkdir, open, readFile, readdir, readlink, realpath, rm } from 'node:fs/promises';
|
|
4
|
+
import { access, link, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, unlink } from 'node:fs/promises';
|
|
5
5
|
import { homedir, hostname } 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
|
-
import {
|
|
9
|
+
import { ReviewFeedbackObserver } 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
14
|
import { InjectedContainedReportOperation } from './contained-report-operation.js';
|
|
15
15
|
import { ContainedImplementationReviewer } from './implementation-reviewer.js';
|
|
16
16
|
import { parseAgentAutoConfig } from './config.js';
|
|
17
|
-
import { WaitingHumanCoordinator } from './waiting-human-coordinator.js';
|
|
18
17
|
import { canonicalJson, parseJsonWithoutDuplicateKeys, sha256, } from './containment.js';
|
|
19
|
-
import { FileProofRecordWriter } from './proof-store.js';
|
|
20
18
|
import { CheckProcessQuiescenceError, parseIssueCheckInvocation, resolveIssueCheckPolicy } from './issue-check-policy.js';
|
|
21
19
|
import { acquireOwnerControlLock, OwnerControlLockBlockedError } from './owner-control-lock.js';
|
|
22
20
|
import { decodeAgentReportForValidation } from './report-envelope.js';
|
|
23
21
|
import { CodexProcess, ProcessQuiescenceError } from './codex-process.js';
|
|
24
22
|
import { FileAndroidLeaseVerifier, FileIosLeaseVerifier } from './mobile-lease.js';
|
|
25
23
|
import { publishRuntimeAssetSnapshot } from './runtime-assets.js';
|
|
26
|
-
import {
|
|
27
|
-
import { SpecCoordinator } from './spec-coordinator.js';
|
|
28
|
-
import { createSpecRevision } from './spec-delivery.js';
|
|
29
|
-
import { validateCodeReviewDefects } from './code-review-report.js';
|
|
30
|
-
import { hashRouteDecision, validateRouteReceipt } from './route-decision.js';
|
|
31
|
-
import { OwnerLockContentionError, OwnerLockSafetyError, RunIssue } from './run-issue.js';
|
|
24
|
+
import { OwnerLockContentionError, OwnerLockSafetyError, RunIssue, } from './run-issue.js';
|
|
32
25
|
import { FileRunRecordWriter } from './run-store.js';
|
|
26
|
+
import { captureProcessStartIdentity, observeProcessGroup, observeProcessIdentity } from './process-identity.js';
|
|
33
27
|
import { parseWorkflowExecutionProfile, verifyWorkflowGeneration, } from './workflow-assets.js';
|
|
28
|
+
import { candidateBindingId, candidateBindingSeed, candidateRef, } from './candidate.js';
|
|
34
29
|
export class LocalGitRunIssueAdapter {
|
|
35
30
|
executor;
|
|
31
|
+
repositoryRoot;
|
|
36
32
|
worktrees;
|
|
37
|
-
|
|
33
|
+
candidateRepositories = new Map();
|
|
34
|
+
candidateV2;
|
|
35
|
+
constructor(executor = defaultProcessExecutor, repositoryRoot) {
|
|
38
36
|
this.executor = executor;
|
|
37
|
+
this.repositoryRoot = repositoryRoot;
|
|
39
38
|
this.worktrees = new GitWorktreeManager(executor);
|
|
39
|
+
this.candidateV2 = {
|
|
40
|
+
reconcileOrphans: (input) => this.reconcileCandidateOrphans(input),
|
|
41
|
+
captureAndPin: (input) => this.captureAndPinCandidate(input),
|
|
42
|
+
inspectPin: (binding) => this.inspectCandidatePin(binding),
|
|
43
|
+
normalizeSharedIndex: (input) => this.candidateOperation('candidate-io-failed', async () => {
|
|
44
|
+
if (await this.getHead(input.worktreePath) !== input.expectedHeadSha)
|
|
45
|
+
throw new Error('candidate expected HEAD diverged');
|
|
46
|
+
await this.git(['-C', input.worktreePath, 'read-tree', input.expectedHeadSha]);
|
|
47
|
+
}),
|
|
48
|
+
prepareMaterialization: (input) => this.prepareCandidateMaterialization(input),
|
|
49
|
+
inspectMaterialization: (input) => this.inspectCandidateMaterialization(input),
|
|
50
|
+
removeMaterialization: (input) => this.removeCandidateMaterialization(input.materialization),
|
|
51
|
+
copyProofArtifacts: (input) => this.copyCandidateProofArtifacts(input),
|
|
52
|
+
createOrObserveCommit: (input) => this.createOrObserveCandidateCommit(input),
|
|
53
|
+
releasePin: (input) => this.releaseCandidatePin(input.binding, input.expectedPinnedCommitSha),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async reconcileCandidateOrphans(input) {
|
|
57
|
+
return this.candidateOperation('candidate-io-failed', async () => {
|
|
58
|
+
const root = (await this.git(['-C', input.repositoryRoot, 'rev-parse', '--show-toplevel'])).trim();
|
|
59
|
+
const activeRefs = new Set(input.activeCandidateRefs);
|
|
60
|
+
const adoptableRefs = new Set();
|
|
61
|
+
for (const pending of input.pendingCandidates) {
|
|
62
|
+
const first = await this.captureCandidateTree(pending.worktreePath, pending.expectedHeadSha, pending.artifactDir);
|
|
63
|
+
const second = await this.captureCandidateTree(pending.worktreePath, pending.expectedHeadSha, pending.artifactDir);
|
|
64
|
+
if (canonicalJson(first) !== canonicalJson(second))
|
|
65
|
+
continue;
|
|
66
|
+
const bindingId = candidateBindingId({
|
|
67
|
+
bindingSeed: candidateBindingSeed(pending.runId, pending.boundary),
|
|
68
|
+
expectedHeadSha: pending.expectedHeadSha,
|
|
69
|
+
candidateTreeSha: first.treeSha,
|
|
70
|
+
canonicalChangedFiles: first.changedFiles,
|
|
71
|
+
sourceWorktreeIdentity: first.worktreeIdentity,
|
|
72
|
+
});
|
|
73
|
+
const ref = candidateRef(pending.runId, bindingId);
|
|
74
|
+
const commit = await this.resolveOptionalRef(root, ref);
|
|
75
|
+
if (commit && await this.commitMatches(root, commit, pending.expectedHeadSha, first.treeSha))
|
|
76
|
+
adoptableRefs.add(ref);
|
|
77
|
+
}
|
|
78
|
+
const refs = (await this.git(['-C', root, 'for-each-ref', '--format=%(refname) %(objectname)', 'refs/codex-orchestrator/candidates/']))
|
|
79
|
+
.split('\n').filter(Boolean);
|
|
80
|
+
for (const row of refs) {
|
|
81
|
+
const separator = row.indexOf(' ');
|
|
82
|
+
if (separator < 1)
|
|
83
|
+
throw new Error('orphan candidate ref row is malformed');
|
|
84
|
+
const ref = row.slice(0, separator);
|
|
85
|
+
const sha = row.slice(separator + 1);
|
|
86
|
+
if (!activeRefs.has(ref) && !adoptableRefs.has(ref)) {
|
|
87
|
+
const deleted = await this.executor('git', ['-C', root, 'update-ref', '-d', ref, sha]);
|
|
88
|
+
if (deleted.exitCode !== 0)
|
|
89
|
+
throw new Error('orphan candidate ref cleanup failed');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const materializationRoot = canonicalFilesystemPath(resolve(input.workspaceRoot, '.candidate-materializations'));
|
|
93
|
+
const activeMaterializations = new Map(input.activeMaterializations.map((materialization) => [
|
|
94
|
+
canonicalFilesystemPath(materialization.path), materialization.candidateCommitSha,
|
|
95
|
+
]));
|
|
96
|
+
for (const worktree of await this.worktrees.listWorktrees(root)) {
|
|
97
|
+
const path = canonicalFilesystemPath(worktree.path);
|
|
98
|
+
const relativePath = relative(materializationRoot, path);
|
|
99
|
+
if (relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))) {
|
|
100
|
+
const expectedCommit = activeMaterializations.get(path);
|
|
101
|
+
if (expectedCommit) {
|
|
102
|
+
if (worktree.branch !== undefined || await this.getHead(path).catch(() => '') !== expectedCommit) {
|
|
103
|
+
throw new Error('persisted candidate materialization diverged');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (worktree.branch !== undefined)
|
|
108
|
+
throw new Error('orphan candidate materialization unexpectedly owns a branch');
|
|
109
|
+
await this.git(['-C', root, 'worktree', 'remove', '--force', path]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async captureAndPinCandidate(input) {
|
|
116
|
+
try {
|
|
117
|
+
const first = await this.captureCandidateTree(input.worktreePath, input.expectedHeadSha, input.artifactDir);
|
|
118
|
+
const second = await this.captureCandidateTree(input.worktreePath, input.expectedHeadSha, input.artifactDir);
|
|
119
|
+
if (canonicalJson(first) !== canonicalJson(second)) {
|
|
120
|
+
return { kind: 'failed', code: 'candidate-unstable', detailSha256: sha256(canonicalJson({ first, second })) };
|
|
121
|
+
}
|
|
122
|
+
const bindingSeed = candidateBindingSeed(input.runId, input.boundary);
|
|
123
|
+
const bindingId = candidateBindingId({
|
|
124
|
+
bindingSeed,
|
|
125
|
+
expectedHeadSha: input.expectedHeadSha,
|
|
126
|
+
candidateTreeSha: first.treeSha,
|
|
127
|
+
canonicalChangedFiles: first.changedFiles,
|
|
128
|
+
sourceWorktreeIdentity: first.worktreeIdentity,
|
|
129
|
+
});
|
|
130
|
+
const ref = candidateRef(input.runId, bindingId);
|
|
131
|
+
const root = (await this.git(['-C', input.worktreePath, 'rev-parse', '--show-toplevel'])).trim();
|
|
132
|
+
this.candidateRepositories.set(ref, root);
|
|
133
|
+
let commitSha = await this.resolveOptionalRef(root, ref);
|
|
134
|
+
if (commitSha) {
|
|
135
|
+
if (!await this.commitMatches(root, commitSha, input.expectedHeadSha, first.treeSha))
|
|
136
|
+
throw new Error('existing candidate ref diverged');
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
commitSha = (await this.git([
|
|
140
|
+
'-C', root, '-c', 'user.name=codex-orchestrator', '-c', 'user.email=codex-orchestrator@users.noreply.github.com',
|
|
141
|
+
'commit-tree', first.treeSha, '-p', input.expectedHeadSha, '-m', `codex-orchestrator candidate ${bindingId}`,
|
|
142
|
+
])).trim();
|
|
143
|
+
const update = await this.executor('git', ['-C', root, 'update-ref', ref, commitSha, '0'.repeat(commitSha.length)]);
|
|
144
|
+
if (update.exitCode !== 0) {
|
|
145
|
+
const observed = await this.resolveOptionalRef(root, ref);
|
|
146
|
+
if (!observed || !await this.commitMatches(root, observed, input.expectedHeadSha, first.treeSha)) {
|
|
147
|
+
return { kind: 'failed', code: 'candidate-ref-update-unknown', detailSha256: sha256(update.stderr) };
|
|
148
|
+
}
|
|
149
|
+
commitSha = observed;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { kind: 'ok', value: {
|
|
153
|
+
version: 2, bindingId, expectedHeadSha: input.expectedHeadSha, candidateRef: ref,
|
|
154
|
+
candidateCommitSha: commitSha, candidateTreeSha: first.treeSha,
|
|
155
|
+
canonicalChangedFiles: first.changedFiles, sourceWorktreeIdentity: first.worktreeIdentity,
|
|
156
|
+
} };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return candidateFailure('candidate-io-failed', error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async captureCandidateTree(worktreePath, expectedHeadSha, artifactDir) {
|
|
163
|
+
validateRelativeRoot(artifactDir);
|
|
164
|
+
const [head, canonicalPath, gitDirectoryRaw] = await Promise.all([
|
|
165
|
+
this.getHead(worktreePath), realpath(worktreePath), this.git(['-C', worktreePath, 'rev-parse', '--absolute-git-dir']),
|
|
166
|
+
]);
|
|
167
|
+
if (head !== expectedHeadSha)
|
|
168
|
+
throw new Error('candidate expected HEAD diverged');
|
|
169
|
+
const gitDirectory = gitDirectoryRaw.trim();
|
|
170
|
+
const temporaryRoot = await mkdtemp(join(gitDirectory, 'candidate-index-'));
|
|
171
|
+
const indexPath = join(temporaryRoot, 'index');
|
|
172
|
+
const options = { envOverlay: { GIT_INDEX_FILE: indexPath } };
|
|
173
|
+
try {
|
|
174
|
+
await this.git(['-C', worktreePath, 'read-tree', expectedHeadSha], options);
|
|
175
|
+
await this.git(['-C', worktreePath, 'add', '-u', '--'], options);
|
|
176
|
+
const untracked = (await this.git(['-C', worktreePath, 'ls-files', '--others', '--exclude-standard', '-z'], options)).split('\0')
|
|
177
|
+
.filter((path) => path && path !== artifactDir && !path.startsWith(`${artifactDir}/`)).sort();
|
|
178
|
+
if (untracked.length > 0) {
|
|
179
|
+
await this.git(['-C', worktreePath, 'add', '--pathspec-from-file=-', '--pathspec-file-nul'], {
|
|
180
|
+
...options, stdin: `${untracked.join('\0')}\0`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const treeSha = (await this.git(['-C', worktreePath, 'write-tree'], options)).trim();
|
|
184
|
+
const changedFiles = (await this.git(['-C', worktreePath, 'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', expectedHeadSha, treeSha]))
|
|
185
|
+
.split('\0').filter(Boolean).sort();
|
|
186
|
+
return {
|
|
187
|
+
expectedHeadSha,
|
|
188
|
+
treeSha,
|
|
189
|
+
changedFiles,
|
|
190
|
+
worktreeIdentity: sha256(canonicalJson({ canonicalPath, gitDirectory })),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async inspectCandidatePin(binding) {
|
|
198
|
+
return this.candidateOperation('candidate-io-failed', async () => {
|
|
199
|
+
const root = await this.candidateRepository(binding.candidateRef);
|
|
200
|
+
if (!root)
|
|
201
|
+
return 'missing';
|
|
202
|
+
const observed = await this.resolveOptionalRef(root, binding.candidateRef);
|
|
203
|
+
if (!observed)
|
|
204
|
+
return 'missing';
|
|
205
|
+
return observed === binding.candidateCommitSha
|
|
206
|
+
&& await this.commitMatches(root, observed, binding.expectedHeadSha, binding.candidateTreeSha) ? 'matching' : 'diverged';
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async prepareCandidateMaterialization(input) {
|
|
210
|
+
return this.candidateOperation('candidate-materialization-io-failed', async () => {
|
|
211
|
+
const root = await this.repositoryFromWorkspaceRoot(input.workspaceRoot);
|
|
212
|
+
this.candidateRepositories.set(input.binding.candidateRef, root);
|
|
213
|
+
const path = join(input.workspaceRoot, '.candidate-materializations', input.runId, input.binding.bindingId, input.materializationId);
|
|
214
|
+
const existing = (await this.worktrees.listWorktrees(root)).find((worktree) => sameFilesystemPath(worktree.path, path));
|
|
215
|
+
if (existing) {
|
|
216
|
+
const clean = await this.worktrees.isWorktreeClean(path);
|
|
217
|
+
const head = await this.getHead(path).catch(() => 'missing');
|
|
218
|
+
if (existing.branch !== undefined || head !== input.binding.candidateCommitSha || !clean)
|
|
219
|
+
return { kind: 'path-diverged', path };
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
await mkdir(dirname(path), { recursive: true });
|
|
223
|
+
await this.git(['-C', root, 'worktree', 'add', '--detach', path, input.binding.candidateCommitSha]);
|
|
224
|
+
}
|
|
225
|
+
return { kind: 'prepared', materialization: {
|
|
226
|
+
version: 2, bindingId: input.binding.bindingId, candidateCommitSha: input.binding.candidateCommitSha,
|
|
227
|
+
path,
|
|
228
|
+
} };
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async inspectCandidateMaterialization(input) {
|
|
232
|
+
return this.candidateOperation('candidate-materialization-io-failed', async () => {
|
|
233
|
+
try {
|
|
234
|
+
const stat = await lstat(input.materialization.path);
|
|
235
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
236
|
+
return 'mutated';
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (isErrorCode(error, 'ENOENT'))
|
|
240
|
+
return 'missing';
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
const [head, tree, changed] = await Promise.all([
|
|
244
|
+
this.getHead(input.materialization.path),
|
|
245
|
+
this.git(['-C', input.materialization.path, 'rev-parse', 'HEAD^{tree}']).then((value) => value.trim()),
|
|
246
|
+
this.worktrees.listChangedFilesIgnoringUntrackedRoot(input.materialization.path, input.artifactDir),
|
|
247
|
+
]);
|
|
248
|
+
return head === input.binding.candidateCommitSha && tree === input.binding.candidateTreeSha && changed.length === 0 ? 'matching' : 'mutated';
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async removeCandidateMaterialization(materialization) {
|
|
252
|
+
return this.candidateOperation('candidate-materialization-io-failed', async () => {
|
|
253
|
+
const root = await this.repositoryFromWorkspaceRoot(materialization.path);
|
|
254
|
+
const registered = (await this.worktrees.listWorktrees(root)).find((worktree) => sameFilesystemPath(worktree.path, materialization.path));
|
|
255
|
+
if (!registered)
|
|
256
|
+
return;
|
|
257
|
+
if (registered.branch !== undefined || await this.getHead(materialization.path) !== materialization.candidateCommitSha)
|
|
258
|
+
throw new Error('candidate materialization diverged');
|
|
259
|
+
await this.git(['-C', root, 'worktree', 'remove', '--force', materialization.path]);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async copyCandidateProofArtifacts(input) {
|
|
263
|
+
return this.candidateOperation('candidate-materialization-io-failed', async () => {
|
|
264
|
+
const proofRoot = `${input.artifactDir}/${input.proofId}`;
|
|
265
|
+
const repositoryRoot = await this.repositoryFromWorkspaceRoot(input.materialization.path);
|
|
266
|
+
const registered = (await this.worktrees.listWorktrees(repositoryRoot))
|
|
267
|
+
.find((worktree) => sameFilesystemPath(worktree.path, input.materialization.path));
|
|
268
|
+
if (!registered || registered.branch !== undefined
|
|
269
|
+
|| await this.getHead(input.materialization.path).catch(() => '') !== input.materialization.candidateCommitSha) {
|
|
270
|
+
return { kind: 'artifact-conflict', relativePath: proofRoot };
|
|
271
|
+
}
|
|
272
|
+
const sourceRoot = await inspectSafeArtifactRoot(input.materialization.path);
|
|
273
|
+
const destinationRoot = await inspectSafeArtifactRoot(input.issueWorktreePath);
|
|
274
|
+
for (const artifact of input.artifacts) {
|
|
275
|
+
if (artifact.relativePath !== proofRoot && !artifact.relativePath.startsWith(`${proofRoot}/`))
|
|
276
|
+
return { kind: 'artifact-conflict', relativePath: artifact.relativePath };
|
|
277
|
+
try {
|
|
278
|
+
const source = join(input.materialization.path, artifact.relativePath);
|
|
279
|
+
const destination = join(input.issueWorktreePath, artifact.relativePath);
|
|
280
|
+
const sourceBytes = await readRegularFileWithoutSymlinkAncestors(sourceRoot, source);
|
|
281
|
+
if (sha256(sourceBytes) !== artifact.sha256)
|
|
282
|
+
return { kind: 'artifact-conflict', relativePath: artifact.relativePath };
|
|
283
|
+
await ensureSafeDirectoryPath(destinationRoot, dirname(destination));
|
|
284
|
+
await publishArtifactCreateOnly(destinationRoot, destination, sourceBytes, artifact.sha256);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (error instanceof CandidateArtifactConflictError)
|
|
288
|
+
return { kind: 'artifact-conflict', relativePath: artifact.relativePath };
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { kind: 'copied-or-observed' };
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async createOrObserveCandidateCommit(input) {
|
|
296
|
+
return this.candidateOperation('candidate-ref-update-unknown', async () => {
|
|
297
|
+
const root = (await this.git(['-C', input.worktreePath, 'rev-parse', '--show-toplevel'])).trim();
|
|
298
|
+
const ref = `refs/heads/${input.branchName}`;
|
|
299
|
+
const observedHead = (await this.git(['-C', root, 'rev-parse', '--verify', `${ref}^{commit}`])).trim();
|
|
300
|
+
if (observedHead !== input.parentSha) {
|
|
301
|
+
const observed = await this.inspectCommit(root, observedHead);
|
|
302
|
+
if (observed.parentSha === input.parentSha && observed.treeSha === input.treeSha && observed.message === input.message) {
|
|
303
|
+
return { kind: 'created-or-observed', sha: observedHead, ...observed };
|
|
304
|
+
}
|
|
305
|
+
return { kind: 'branch-diverged', observedHeadSha: observedHead };
|
|
306
|
+
}
|
|
307
|
+
if (input.observeOnly)
|
|
308
|
+
return { kind: 'parent-unchanged' };
|
|
309
|
+
const pin = await this.resolveOptionalRef(root, input.candidateRef);
|
|
310
|
+
if (!pin || (await this.git(['-C', root, 'rev-parse', `${pin}^{tree}`])).trim() !== input.treeSha)
|
|
311
|
+
throw new Error('candidate pin does not authorize publication tree');
|
|
312
|
+
const sha = (await this.git([
|
|
313
|
+
'-C', root, '-c', 'user.name=codex-orchestrator', '-c', 'user.email=codex-orchestrator@users.noreply.github.com',
|
|
314
|
+
'commit-tree', input.treeSha, '-p', input.parentSha, '-m', input.message,
|
|
315
|
+
])).trim();
|
|
316
|
+
const update = await this.executor('git', ['-C', root, 'update-ref', ref, sha, input.parentSha]);
|
|
317
|
+
if (update.exitCode !== 0) {
|
|
318
|
+
const after = (await this.git(['-C', root, 'rev-parse', '--verify', `${ref}^{commit}`])).trim();
|
|
319
|
+
if (after === input.parentSha)
|
|
320
|
+
return { kind: 'parent-unchanged' };
|
|
321
|
+
const observed = await this.inspectCommit(root, after);
|
|
322
|
+
if (observed.parentSha === input.parentSha && observed.treeSha === input.treeSha && observed.message === input.message) {
|
|
323
|
+
return { kind: 'created-or-observed', sha: after, ...observed };
|
|
324
|
+
}
|
|
325
|
+
return { kind: 'branch-diverged', observedHeadSha: after };
|
|
326
|
+
}
|
|
327
|
+
return { kind: 'created-or-observed', sha, parentSha: input.parentSha, treeSha: input.treeSha, message: input.message };
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
async releaseCandidatePin(binding, expected) {
|
|
331
|
+
return this.candidateOperation('candidate-ref-update-unknown', async () => {
|
|
332
|
+
const root = await this.candidateRepository(binding.candidateRef);
|
|
333
|
+
if (!root)
|
|
334
|
+
return;
|
|
335
|
+
const observed = await this.resolveOptionalRef(root, binding.candidateRef);
|
|
336
|
+
if (!observed)
|
|
337
|
+
return;
|
|
338
|
+
if (observed !== expected)
|
|
339
|
+
throw new Error('candidate pin diverged');
|
|
340
|
+
const result = await this.executor('git', ['-C', root, 'update-ref', '-d', binding.candidateRef, expected]);
|
|
341
|
+
if (result.exitCode !== 0 && await this.resolveOptionalRef(root, binding.candidateRef))
|
|
342
|
+
throw new Error('candidate pin deletion outcome is unknown');
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async candidateOperation(code, operation) {
|
|
346
|
+
try {
|
|
347
|
+
return { kind: 'ok', value: await operation() };
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
return candidateFailure(code, error);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async candidateRepository(ref) {
|
|
354
|
+
const known = this.candidateRepositories.get(ref);
|
|
355
|
+
if (known)
|
|
356
|
+
return known;
|
|
357
|
+
for (const candidateRoot of [this.repositoryRoot, process.cwd()]) {
|
|
358
|
+
if (!candidateRoot)
|
|
359
|
+
continue;
|
|
360
|
+
try {
|
|
361
|
+
const root = (await this.git(['-C', candidateRoot, 'rev-parse', '--show-toplevel'])).trim();
|
|
362
|
+
if (await this.resolveOptionalRef(root, ref)) {
|
|
363
|
+
this.candidateRepositories.set(ref, root);
|
|
364
|
+
return root;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch { /* this location need not be in the target repository */ }
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
async repositoryFromWorkspaceRoot(path) {
|
|
372
|
+
let current = resolve(path);
|
|
373
|
+
while (true) {
|
|
374
|
+
try {
|
|
375
|
+
return (await this.git(['-C', current, 'rev-parse', '--show-toplevel'])).trim();
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
const parent = dirname(current);
|
|
379
|
+
if (parent === current)
|
|
380
|
+
throw new Error('candidate workspace is not below a Git repository');
|
|
381
|
+
current = parent;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async resolveOptionalRef(root, ref) {
|
|
386
|
+
const result = await this.executor('git', ['-C', root, 'rev-parse', '--verify', `${ref}^{commit}`]);
|
|
387
|
+
if (result.exitCode === 1 || result.exitCode === 128)
|
|
388
|
+
return undefined;
|
|
389
|
+
if (result.exitCode !== 0)
|
|
390
|
+
throw new Error(`git ref inspection failed: ${result.stderr}`);
|
|
391
|
+
return result.stdout.trim();
|
|
392
|
+
}
|
|
393
|
+
async commitMatches(root, commit, parent, tree) {
|
|
394
|
+
const observed = await this.inspectCommit(root, commit);
|
|
395
|
+
return observed.parentSha === parent && observed.treeSha === tree;
|
|
396
|
+
}
|
|
397
|
+
async inspectCommit(root, commit) {
|
|
398
|
+
const output = await this.git(['-C', root, 'show', '-s', '--format=%P%n%T%n%B', commit]);
|
|
399
|
+
const [parentSha, treeSha, ...message] = output.split('\n');
|
|
400
|
+
if (!parentSha || parentSha.includes(' ') || !treeSha)
|
|
401
|
+
throw new Error('candidate commit is not single-parent');
|
|
402
|
+
return { parentSha, treeSha, message: message.join('\n').trimEnd() };
|
|
40
403
|
}
|
|
41
404
|
async getBaseSha(input) {
|
|
42
|
-
|
|
405
|
+
await this.git([
|
|
406
|
+
'-C', input.targetRoot, 'fetch', '--quiet', '--no-tags', 'origin', `refs/heads/${input.baseBranch}`,
|
|
407
|
+
]);
|
|
408
|
+
return (await this.git(['-C', input.targetRoot, 'rev-parse', '--verify', 'FETCH_HEAD^{commit}'])).trim();
|
|
43
409
|
}
|
|
44
410
|
async createWorktree(input) {
|
|
45
411
|
await this.worktrees.createIssueWorktree({
|
|
@@ -49,6 +415,9 @@ export class LocalGitRunIssueAdapter {
|
|
|
49
415
|
baseBranch: input.baseBranch,
|
|
50
416
|
requiredBaseSha: input.baseSha,
|
|
51
417
|
});
|
|
418
|
+
if (await this.getHead(input.worktreePath) !== input.baseSha) {
|
|
419
|
+
throw new Error('created issue worktree does not match the pinned base SHA');
|
|
420
|
+
}
|
|
52
421
|
}
|
|
53
422
|
async ensureContinuationWorktree(input) {
|
|
54
423
|
const localHead = (await this.git([
|
|
@@ -137,6 +506,13 @@ export class LocalGitRunIssueAdapter {
|
|
|
137
506
|
listChangedFiles(worktreePath) {
|
|
138
507
|
return this.worktrees.listChangedFiles(worktreePath);
|
|
139
508
|
}
|
|
509
|
+
async diffTrees(worktreePath, previousTreeSha, candidateTreeSha) {
|
|
510
|
+
const [names, patch] = await Promise.all([
|
|
511
|
+
this.git(['-C', worktreePath, 'diff-tree', '--no-commit-id', '--name-only', '-r', '-z', previousTreeSha, candidateTreeSha], { maxOutputBytes: 1024 * 1024 }),
|
|
512
|
+
this.git(['-C', worktreePath, 'diff', '--binary', '--full-index', '--no-ext-diff', previousTreeSha, candidateTreeSha, '--'], { maxOutputBytes: 1024 * 1024 }),
|
|
513
|
+
]);
|
|
514
|
+
return { changedFiles: names.split('\0').filter(Boolean).sort(), patch };
|
|
515
|
+
}
|
|
140
516
|
listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot) {
|
|
141
517
|
return this.worktrees.listChangedFilesIgnoringUntrackedRoot(worktreePath, ignoredRoot);
|
|
142
518
|
}
|
|
@@ -197,13 +573,204 @@ export class LocalGitRunIssueAdapter {
|
|
|
197
573
|
push(input) {
|
|
198
574
|
return this.worktrees.pushBranch(input);
|
|
199
575
|
}
|
|
200
|
-
async git(args) {
|
|
201
|
-
const result = await this.executor('git', args);
|
|
576
|
+
async git(args, options) {
|
|
577
|
+
const result = await this.executor('git', args, options);
|
|
202
578
|
if (result.exitCode !== 0)
|
|
203
579
|
throw new Error(`git failed: ${result.stderr}`);
|
|
204
580
|
return result.stdout;
|
|
205
581
|
}
|
|
206
582
|
}
|
|
583
|
+
function candidateFailure(code, error) {
|
|
584
|
+
const detail = error instanceof Error ? `${error.name}:${error.message}` : String(error);
|
|
585
|
+
return { kind: 'failed', code, detailSha256: sha256(detail) };
|
|
586
|
+
}
|
|
587
|
+
function processGroupIsAlive(processGroupId) {
|
|
588
|
+
try {
|
|
589
|
+
process.kill(-processGroupId, 0);
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
return !isErrorCode(error, 'ESRCH');
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function sameFilesystemPath(left, right) {
|
|
597
|
+
return canonicalFilesystemPath(left) === canonicalFilesystemPath(right);
|
|
598
|
+
}
|
|
599
|
+
function canonicalFilesystemPath(value) {
|
|
600
|
+
const resolved = resolve(value);
|
|
601
|
+
return resolved.startsWith('/private/') ? resolved.slice('/private'.length) : resolved;
|
|
602
|
+
}
|
|
603
|
+
async function inspectSafeArtifactRoot(root) {
|
|
604
|
+
const path = resolve(root);
|
|
605
|
+
const stat = await lstat(path);
|
|
606
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
607
|
+
throw new CandidateArtifactConflictError('candidate artifact root is unsafe');
|
|
608
|
+
const realPath = canonicalFilesystemPath(await realpath(path));
|
|
609
|
+
if (realPath !== canonicalFilesystemPath(path))
|
|
610
|
+
throw new CandidateArtifactConflictError('candidate artifact root has a symlinked ancestor');
|
|
611
|
+
const identity = { path, realPath, dev: stat.dev, ino: stat.ino };
|
|
612
|
+
await assertSafeArtifactRoot(identity);
|
|
613
|
+
return identity;
|
|
614
|
+
}
|
|
615
|
+
async function assertSafeArtifactRoot(root) {
|
|
616
|
+
const stat = await lstat(root.path);
|
|
617
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== root.dev || stat.ino !== root.ino
|
|
618
|
+
|| canonicalFilesystemPath(await realpath(root.path)) !== root.realPath) {
|
|
619
|
+
throw new CandidateArtifactConflictError('candidate artifact root changed');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function readRegularFileWithoutSymlinkAncestors(rootInput, path) {
|
|
623
|
+
const root = typeof rootInput === 'string' ? await inspectSafeArtifactRoot(rootInput) : rootInput;
|
|
624
|
+
await assertSafeArtifactRoot(root);
|
|
625
|
+
const relativePath = relative(root.path, resolve(path));
|
|
626
|
+
if (!relativePath || relativePath.startsWith('..') || isAbsolute(relativePath))
|
|
627
|
+
throw new CandidateArtifactConflictError('candidate artifact path escapes its root');
|
|
628
|
+
let current = root.path;
|
|
629
|
+
const rootStat = await lstat(current);
|
|
630
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
|
|
631
|
+
throw new CandidateArtifactConflictError('candidate artifact root is unsafe');
|
|
632
|
+
const ancestorIdentity = [{ path: current, dev: rootStat.dev, ino: rootStat.ino }];
|
|
633
|
+
const segments = relativePath.split('/');
|
|
634
|
+
for (const segment of segments.slice(0, -1)) {
|
|
635
|
+
current = join(current, segment);
|
|
636
|
+
const stat = await lstat(current);
|
|
637
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
638
|
+
throw new CandidateArtifactConflictError('candidate artifact ancestor is unsafe');
|
|
639
|
+
ancestorIdentity.push({ path: current, dev: stat.dev, ino: stat.ino });
|
|
640
|
+
}
|
|
641
|
+
const finalBeforeOpen = await lstat(path);
|
|
642
|
+
if (finalBeforeOpen.isSymbolicLink() || !finalBeforeOpen.isFile()) {
|
|
643
|
+
throw new CandidateArtifactConflictError('candidate artifact is not a regular file');
|
|
644
|
+
}
|
|
645
|
+
let handle;
|
|
646
|
+
try {
|
|
647
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
if (isErrorCode(error, 'ELOOP'))
|
|
651
|
+
throw new CandidateArtifactConflictError('candidate artifact is not a regular file');
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
const stat = await handle.stat();
|
|
656
|
+
if (!stat.isFile())
|
|
657
|
+
throw new CandidateArtifactConflictError('candidate artifact is not a regular file');
|
|
658
|
+
const openedPath = await realpath(path);
|
|
659
|
+
const openedRelative = relative(root.realPath, canonicalFilesystemPath(openedPath));
|
|
660
|
+
if (!openedRelative || openedRelative.startsWith('..') || isAbsolute(openedRelative)) {
|
|
661
|
+
throw new CandidateArtifactConflictError('candidate artifact path escaped its root during read');
|
|
662
|
+
}
|
|
663
|
+
const finalStat = await lstat(path);
|
|
664
|
+
if (!finalStat.isFile() || finalStat.isSymbolicLink() || finalStat.dev !== stat.dev || finalStat.ino !== stat.ino) {
|
|
665
|
+
throw new CandidateArtifactConflictError('candidate artifact changed during read');
|
|
666
|
+
}
|
|
667
|
+
for (const identity of ancestorIdentity) {
|
|
668
|
+
const observed = await lstat(identity.path);
|
|
669
|
+
if (!observed.isDirectory() || observed.isSymbolicLink() || observed.dev !== identity.dev || observed.ino !== identity.ino) {
|
|
670
|
+
throw new CandidateArtifactConflictError('candidate artifact ancestor changed during read');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
await assertSafeArtifactRoot(root);
|
|
674
|
+
return await handle.readFile();
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
await handle.close();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function ensureSafeDirectoryPath(root, directory) {
|
|
681
|
+
await assertSafeArtifactRoot(root);
|
|
682
|
+
const relativeDirectory = relative(root.path, resolve(directory));
|
|
683
|
+
if (relativeDirectory.startsWith('..') || isAbsolute(relativeDirectory))
|
|
684
|
+
throw new CandidateArtifactConflictError('candidate artifact directory escapes its root');
|
|
685
|
+
let current = root.path;
|
|
686
|
+
for (const segment of relativeDirectory.split('/').filter(Boolean)) {
|
|
687
|
+
current = join(current, segment);
|
|
688
|
+
try {
|
|
689
|
+
await mkdir(current);
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
if (!isErrorCode(error, 'EEXIST'))
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
const stat = await lstat(current);
|
|
696
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
697
|
+
throw new CandidateArtifactConflictError('candidate artifact directory is unsafe');
|
|
698
|
+
}
|
|
699
|
+
const parentRealPath = canonicalFilesystemPath(await realpath(directory));
|
|
700
|
+
const parentRelative = relative(root.realPath, parentRealPath);
|
|
701
|
+
if (parentRelative.startsWith('..') || isAbsolute(parentRelative))
|
|
702
|
+
throw new CandidateArtifactConflictError('candidate artifact directory escaped its root');
|
|
703
|
+
await assertSafeArtifactRoot(root);
|
|
704
|
+
}
|
|
705
|
+
class CandidateArtifactConflictError extends Error {
|
|
706
|
+
}
|
|
707
|
+
async function publishArtifactCreateOnly(root, destination, bytes, expectedSha256) {
|
|
708
|
+
await assertSafeArtifactRoot(root);
|
|
709
|
+
const temporary = join(dirname(destination), `.${basename(destination)}.${expectedSha256}.tmp`);
|
|
710
|
+
let handle;
|
|
711
|
+
try {
|
|
712
|
+
handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
if (!isErrorCode(error, 'EEXIST'))
|
|
716
|
+
throw error;
|
|
717
|
+
const stale = await readRegularFileWithoutSymlinkAncestors(root, temporary);
|
|
718
|
+
if (sha256(stale) !== expectedSha256) {
|
|
719
|
+
await unlink(temporary);
|
|
720
|
+
handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (handle) {
|
|
724
|
+
try {
|
|
725
|
+
await handle.writeFile(bytes);
|
|
726
|
+
await handle.sync();
|
|
727
|
+
const stat = await handle.stat();
|
|
728
|
+
const observed = await lstat(temporary);
|
|
729
|
+
const temporaryRealPath = canonicalFilesystemPath(await realpath(temporary));
|
|
730
|
+
const temporaryRelative = relative(root.realPath, temporaryRealPath);
|
|
731
|
+
if (!observed.isFile() || observed.isSymbolicLink() || observed.dev !== stat.dev || observed.ino !== stat.ino
|
|
732
|
+
|| temporaryRelative.startsWith('..') || isAbsolute(temporaryRelative)) {
|
|
733
|
+
throw new CandidateArtifactConflictError('candidate artifact temporary path changed');
|
|
734
|
+
}
|
|
735
|
+
await assertSafeArtifactRoot(root);
|
|
736
|
+
}
|
|
737
|
+
finally {
|
|
738
|
+
await handle.close();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
await link(temporary, destination);
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
if (!isErrorCode(error, 'EEXIST'))
|
|
746
|
+
throw error;
|
|
747
|
+
const existing = await readRegularFileWithoutSymlinkAncestors(root, destination);
|
|
748
|
+
if (sha256(existing) !== expectedSha256)
|
|
749
|
+
throw new CandidateArtifactConflictError('candidate artifact destination conflicts');
|
|
750
|
+
}
|
|
751
|
+
const published = await readRegularFileWithoutSymlinkAncestors(root, destination);
|
|
752
|
+
if (sha256(published) !== expectedSha256)
|
|
753
|
+
throw new CandidateArtifactConflictError('candidate artifact destination conflicts');
|
|
754
|
+
const parent = await open(dirname(destination), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
755
|
+
try {
|
|
756
|
+
const parentStat = await parent.stat();
|
|
757
|
+
const observedParent = await lstat(dirname(destination));
|
|
758
|
+
const parentRealPath = canonicalFilesystemPath(await realpath(dirname(destination)));
|
|
759
|
+
const parentRelative = relative(root.realPath, parentRealPath);
|
|
760
|
+
if (!observedParent.isDirectory() || observedParent.isSymbolicLink()
|
|
761
|
+
|| observedParent.dev !== parentStat.dev || observedParent.ino !== parentStat.ino
|
|
762
|
+
|| parentRelative.startsWith('..') || isAbsolute(parentRelative)) {
|
|
763
|
+
throw new CandidateArtifactConflictError('candidate artifact destination directory changed');
|
|
764
|
+
}
|
|
765
|
+
await parent.sync();
|
|
766
|
+
await assertSafeArtifactRoot(root);
|
|
767
|
+
}
|
|
768
|
+
finally {
|
|
769
|
+
await parent.close();
|
|
770
|
+
}
|
|
771
|
+
await unlink(temporary).catch((error) => { if (!isErrorCode(error, 'ENOENT'))
|
|
772
|
+
throw error; });
|
|
773
|
+
}
|
|
207
774
|
export class ContainedImplementationAgent {
|
|
208
775
|
dependencies;
|
|
209
776
|
constructor(dependencies) {
|
|
@@ -212,7 +779,7 @@ export class ContainedImplementationAgent {
|
|
|
212
779
|
async run(input) {
|
|
213
780
|
const config = this.dependencies.config();
|
|
214
781
|
const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
|
|
215
|
-
const attemptId =
|
|
782
|
+
const attemptId = input.attemptId;
|
|
216
783
|
const attempt = await prepareContainedAttempt({
|
|
217
784
|
orchestratorHome: this.dependencies.orchestratorHome,
|
|
218
785
|
canonicalRepository,
|
|
@@ -230,6 +797,14 @@ export class ContainedImplementationAgent {
|
|
|
230
797
|
baseline,
|
|
231
798
|
});
|
|
232
799
|
try {
|
|
800
|
+
try {
|
|
801
|
+
const recovered = await readRegularFile(attempt.reportPath);
|
|
802
|
+
return { kind: 'completed', attemptId, report: decodeAgentReportForValidation(recovered) };
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
806
|
+
return { kind: 'internal-error' };
|
|
807
|
+
}
|
|
233
808
|
const result = await (this.dependencies.process ?? new CodexProcess()).run({
|
|
234
809
|
codexPath: config.codex.command,
|
|
235
810
|
cwd: input.worktreePath,
|
|
@@ -244,19 +819,12 @@ export class ContainedImplementationAgent {
|
|
|
244
819
|
`Package profile instructions: ${attempt.profile.developerInstructions}`,
|
|
245
820
|
`Follow the exact operation at ${attempt.operationPath}.`,
|
|
246
821
|
`The operation's immutable workflow root is ${attempt.workflowRoot}.`,
|
|
247
|
-
|
|
248
|
-
? [
|
|
249
|
-
`Pre-implementation qualification repair for issue #${input.issue.number}: ${input.issue.title}`,
|
|
250
|
-
'Do not implement the issue acceptance criteria yet. Repair only the reported scoped-check failures so the existing worktree qualifies for implementation.',
|
|
251
|
-
'You may modify the product files required by those failures. Return changedFiles as the complete cumulative worktree change set.',
|
|
252
|
-
]
|
|
253
|
-
: [`Implement issue #${input.issue.number}: ${input.issue.title}`]),
|
|
822
|
+
`Implement issue #${input.issue.number}: ${input.issue.title}`,
|
|
254
823
|
`Implementation cycle: ${input.cycle}.`,
|
|
255
824
|
...(input.reviewFeedbackRound ? [`Pull-request feedback repair round: ${input.reviewFeedbackRound}.`] : []),
|
|
256
825
|
...(input.reviewFeedback?.length ? [`Frozen trusted pull-request feedback: ${canonicalJson(input.reviewFeedback)}`] : []),
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
: []),
|
|
826
|
+
`Exact delivery authority: ${canonicalJson(input.deliveryAuthority)}`,
|
|
827
|
+
`Frozen acceptance criteria: ${canonicalJson(input.frozenCriteria)}`,
|
|
260
828
|
...(input.reworkFindings.length > 0 ? [`Repair these verified findings: ${canonicalJson(input.reworkFindings)}`] : []),
|
|
261
829
|
...(input.repairOnly ? ['Report repair only: do not modify any worktree file; emit a schema-valid implementation report for the existing change.'] : []),
|
|
262
830
|
'Do not commit, push, publish, or print credentials or local auth paths.',
|
|
@@ -265,13 +833,13 @@ export class ContainedImplementationAgent {
|
|
|
265
833
|
idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
266
834
|
operationPolicy: attempt.policy,
|
|
267
835
|
executionProfile: attempt.profile,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
836
|
+
onSpawned: async ({ pid, processGroupId }) => input.onLaunched?.({
|
|
837
|
+
attemptId, pid, processGroupId, launchedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
|
|
838
|
+
}),
|
|
271
839
|
}, input.signal);
|
|
272
840
|
if (result.kind === 'cancelled')
|
|
273
841
|
return { kind: 'cancelled' };
|
|
274
|
-
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
|
|
842
|
+
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout', 'launch-gate-failed'].includes(result.kind)) {
|
|
275
843
|
return { kind: 'transport-failed', resumable: true };
|
|
276
844
|
}
|
|
277
845
|
if (result.kind !== 'completed' || result.report.kind !== 'available')
|
|
@@ -285,38 +853,9 @@ export class ContainedImplementationAgent {
|
|
|
285
853
|
catch (error) {
|
|
286
854
|
if (!(error instanceof ProcessQuiescenceError))
|
|
287
855
|
return { kind: 'internal-error' };
|
|
288
|
-
return {
|
|
289
|
-
kind: 'safe-halt',
|
|
290
|
-
process: {
|
|
291
|
-
pid: error.pid,
|
|
292
|
-
processGroupId: error.processGroupId,
|
|
293
|
-
startedAt: (this.dependencies.now ?? (() => new Date().toISOString()))(),
|
|
294
|
-
baseline: { ...baseline },
|
|
295
|
-
},
|
|
296
|
-
waitForAbsence: () => waitForProcessGroupAbsent(error.processGroupId),
|
|
297
|
-
};
|
|
856
|
+
return { kind: 'safe-halt' };
|
|
298
857
|
}
|
|
299
858
|
}
|
|
300
|
-
async recover(input) {
|
|
301
|
-
const expected = join(resolve(this.dependencies.orchestratorHome), 'v2', sha256(input.canonicalRepository), 'runs', input.runId, 'attempts', input.invocation.attemptId, 'report.json');
|
|
302
|
-
if (resolve(input.invocation.reportPath) !== expected) {
|
|
303
|
-
throw new Error('review feedback implementation report path is not attempt-owned');
|
|
304
|
-
}
|
|
305
|
-
let bytes;
|
|
306
|
-
try {
|
|
307
|
-
bytes = await readRegularFile(expected);
|
|
308
|
-
}
|
|
309
|
-
catch (error) {
|
|
310
|
-
if (isErrorCode(error, 'ENOENT'))
|
|
311
|
-
return undefined;
|
|
312
|
-
throw error;
|
|
313
|
-
}
|
|
314
|
-
return {
|
|
315
|
-
kind: 'completed',
|
|
316
|
-
attemptId: input.invocation.attemptId,
|
|
317
|
-
report: decodeAgentReportForValidation(bytes),
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
859
|
}
|
|
321
860
|
export class ContainedProofAgent {
|
|
322
861
|
dependencies;
|
|
@@ -328,12 +867,13 @@ export class ContainedProofAgent {
|
|
|
328
867
|
throw new Error('proof workflow generation is required');
|
|
329
868
|
const config = this.dependencies.config();
|
|
330
869
|
const canonicalRepository = `${config.github.owner.toLowerCase()}/${config.github.repo.toLowerCase()}`;
|
|
331
|
-
const
|
|
870
|
+
const issueWorktreePath = resolve(this.dependencies.targetRoot, config.runner.workspaceRoot, `issue-${input.issue.number}`);
|
|
871
|
+
const worktreePath = input.worktreePath ? resolve(input.worktreePath) : issueWorktreePath;
|
|
332
872
|
const attempt = await prepareContainedAttempt({
|
|
333
873
|
orchestratorHome: this.dependencies.orchestratorHome,
|
|
334
874
|
canonicalRepository,
|
|
335
875
|
runId: input.runId,
|
|
336
|
-
attemptId: (this.dependencies.createAttemptId ?? randomUUID)(),
|
|
876
|
+
attemptId: input.attemptId ?? (this.dependencies.createAttemptId ?? randomUUID)(),
|
|
337
877
|
operationId: 'acceptance-proof',
|
|
338
878
|
workflowGeneration: input.workflowGeneration,
|
|
339
879
|
bootId: this.dependencies.bootId,
|
|
@@ -345,6 +885,22 @@ export class ContainedProofAgent {
|
|
|
345
885
|
const iosTooling = await discoverIosTooling(this.dependencies.processExecutor, this.dependencies.iosXcrunPath);
|
|
346
886
|
const before = await artifactInventory(artifactRoot, config.proof.artifactDir);
|
|
347
887
|
try {
|
|
888
|
+
try {
|
|
889
|
+
const recoveredReport = await readRegularFile(attempt.reportPath);
|
|
890
|
+
const recoveredInventory = await artifactInventory(artifactRoot, config.proof.artifactDir);
|
|
891
|
+
const runnerPrepared = new Set(input.runnerPreparedArtifactPaths);
|
|
892
|
+
return {
|
|
893
|
+
kind: 'report',
|
|
894
|
+
report: decodeAgentReportForValidation(recoveredReport, ['visualEvidence', 'blocker']),
|
|
895
|
+
proofPhaseChangedFiles: [...recoveredInventory.keys()].filter((path) => !runnerPrepared.has(path)).sort(),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
900
|
+
throw error;
|
|
901
|
+
}
|
|
902
|
+
if (input.recoverOnly)
|
|
903
|
+
return { kind: 'internal-error' };
|
|
348
904
|
const result = await (this.dependencies.process ?? new CodexProcess()).run({
|
|
349
905
|
codexPath: config.codex.command,
|
|
350
906
|
cwd: worktreePath,
|
|
@@ -393,10 +949,13 @@ export class ContainedProofAgent {
|
|
|
393
949
|
idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
394
950
|
operationPolicy: attempt.policy,
|
|
395
951
|
executionProfile: attempt.profile,
|
|
952
|
+
onSpawned: async ({ pid, processGroupId }) => input.onLaunched?.({
|
|
953
|
+
pid, processGroupId, launchedAt: new Date().toISOString(),
|
|
954
|
+
}),
|
|
396
955
|
}, input.signal);
|
|
397
956
|
if (result.kind === 'cancelled')
|
|
398
957
|
return { kind: 'cancelled' };
|
|
399
|
-
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
|
|
958
|
+
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout', 'launch-gate-failed'].includes(result.kind)) {
|
|
400
959
|
return { kind: 'transport-failed', resumable: true };
|
|
401
960
|
}
|
|
402
961
|
if (result.kind !== 'completed' || result.report.kind !== 'available')
|
|
@@ -409,13 +968,48 @@ export class ContainedProofAgent {
|
|
|
409
968
|
};
|
|
410
969
|
}
|
|
411
970
|
catch (error) {
|
|
412
|
-
|
|
413
|
-
throw new ProofQuiescenceError(error.pid, error.processGroupId, () => waitForProcessGroupAbsent(error.processGroupId));
|
|
414
|
-
}
|
|
415
|
-
return { kind: 'internal-error' };
|
|
971
|
+
return error instanceof ProcessQuiescenceError ? { kind: 'safe-halt' } : { kind: 'internal-error' };
|
|
416
972
|
}
|
|
417
973
|
}
|
|
418
974
|
}
|
|
975
|
+
export async function observeAttemptReadViewCleanup(input) {
|
|
976
|
+
const runtimeRoot = join(resolve(input.orchestratorHome), 'v2', sha256(input.canonicalRepository));
|
|
977
|
+
const attemptRoot = join(runtimeRoot, 'runs', input.identity.runId, 'attempts', input.identity.attemptId);
|
|
978
|
+
const expectedResultPath = join(attemptRoot, 'report.json');
|
|
979
|
+
if (input.identity.resultPath !== expectedResultPath)
|
|
980
|
+
return 'pending';
|
|
981
|
+
let canonicalRuntimeRoot;
|
|
982
|
+
let canonicalAttemptRoot;
|
|
983
|
+
try {
|
|
984
|
+
canonicalRuntimeRoot = await realpath(runtimeRoot);
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
return isErrorCode(error, 'ENOENT') ? 'confirmed' : 'pending';
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
canonicalAttemptRoot = await realpath(attemptRoot);
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
return isErrorCode(error, 'ENOENT') ? 'confirmed' : 'pending';
|
|
994
|
+
}
|
|
995
|
+
const expectedRelative = relative(runtimeRoot, attemptRoot);
|
|
996
|
+
if (!isPathWithin(canonicalRuntimeRoot, canonicalAttemptRoot)
|
|
997
|
+
|| relative(canonicalRuntimeRoot, canonicalAttemptRoot) !== expectedRelative)
|
|
998
|
+
return 'pending';
|
|
999
|
+
await rm(join(attemptRoot, 'read-view'), { recursive: true, force: true });
|
|
1000
|
+
return 'confirmed';
|
|
1001
|
+
}
|
|
1002
|
+
export function selectedConfiguredCheckPolicySha256(selectedChecks, configuredAuthority) {
|
|
1003
|
+
const selected = {};
|
|
1004
|
+
for (const check of selectedChecks) {
|
|
1005
|
+
if (Object.prototype.hasOwnProperty.call(selected, check.id)
|
|
1006
|
+
|| configuredAuthority[check.id] !== check.command) {
|
|
1007
|
+
throw new CandidateProofInspectionError('selected check no longer matches current configured authority');
|
|
1008
|
+
}
|
|
1009
|
+
selected[check.id] = check.command;
|
|
1010
|
+
}
|
|
1011
|
+
return sha256(canonicalJson(selected));
|
|
1012
|
+
}
|
|
419
1013
|
export async function resolveCodexExecutable(command, safePath) {
|
|
420
1014
|
if (command.length === 0 || safePath.length === 0)
|
|
421
1015
|
throw new Error('Codex command and safe path must be non-empty');
|
|
@@ -453,7 +1047,7 @@ export function createV2Runtime(input) {
|
|
|
453
1047
|
codexExecutable = { command, path };
|
|
454
1048
|
return path;
|
|
455
1049
|
};
|
|
456
|
-
const git = input.git ?? new LocalGitRunIssueAdapter(commandExecutor);
|
|
1050
|
+
const git = input.git ?? new LocalGitRunIssueAdapter(commandExecutor, targetRoot);
|
|
457
1051
|
const proofFreshnessGit = git;
|
|
458
1052
|
const controller = new AbortController();
|
|
459
1053
|
let currentConfig;
|
|
@@ -515,6 +1109,14 @@ export function createV2Runtime(input) {
|
|
|
515
1109
|
|| !attempt.profile || !attempt.operationPath || !attempt.workflowRoot) {
|
|
516
1110
|
return { status: 'blocked', kind: 'safety', code: 'report-operation-attempt-incomplete' };
|
|
517
1111
|
}
|
|
1112
|
+
try {
|
|
1113
|
+
return { status: 'completed', reportBytes: await readRegularFile(attempt.reportPath) };
|
|
1114
|
+
}
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
if (!isErrorCode(error, 'ENOENT')) {
|
|
1117
|
+
return { status: 'blocked', kind: 'safety', code: 'report-operation-result-read-failed' };
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
518
1120
|
let readView;
|
|
519
1121
|
try {
|
|
520
1122
|
readView = await materializeReportReadView({
|
|
@@ -544,6 +1146,7 @@ export function createV2Runtime(input) {
|
|
|
544
1146
|
`Follow the exact operation at ${attempt.operationPath}.`,
|
|
545
1147
|
`The operation's immutable workflow root is ${attempt.workflowRoot}.`,
|
|
546
1148
|
`Runner-provided facts: ${canonicalJson(promptFacts)}`,
|
|
1149
|
+
`Available reviewer roles: ${canonicalJson(attempt.reviewers ?? [])}`,
|
|
547
1150
|
'This is a read-only, report-only operation. Do not edit files or external state, use network or MCP tools, or request additional authority.',
|
|
548
1151
|
'Do not read .env or any .env* file. The runner has removed repository credential paths from this read view.',
|
|
549
1152
|
].join('\n'),
|
|
@@ -551,7 +1154,8 @@ export function createV2Runtime(input) {
|
|
|
551
1154
|
idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
552
1155
|
operationPolicy: attempt.policy,
|
|
553
1156
|
executionProfile: attempt.profile,
|
|
554
|
-
|
|
1157
|
+
agentProfilePaths: attempt.reviewerProfiles,
|
|
1158
|
+
onSpawned: async ({ pid, processGroupId }) => onLaunched?.({ pid, processGroupId }),
|
|
555
1159
|
}, signal);
|
|
556
1160
|
}
|
|
557
1161
|
catch (error) {
|
|
@@ -562,10 +1166,6 @@ export function createV2Runtime(input) {
|
|
|
562
1166
|
pid: error.pid,
|
|
563
1167
|
processGroupId: error.processGroupId,
|
|
564
1168
|
startedAt: now(),
|
|
565
|
-
waitForAbsence: async () => {
|
|
566
|
-
await waitForProcessGroupAbsent(error.processGroupId);
|
|
567
|
-
await rm(readView, { recursive: true, force: true });
|
|
568
|
-
},
|
|
569
1169
|
};
|
|
570
1170
|
}
|
|
571
1171
|
throw error;
|
|
@@ -577,7 +1177,7 @@ export function createV2Runtime(input) {
|
|
|
577
1177
|
if (result.kind === 'cancelled')
|
|
578
1178
|
return { status: 'cancelled' };
|
|
579
1179
|
if (result.kind === 'launch-gate-failed') {
|
|
580
|
-
return { status: '
|
|
1180
|
+
return { status: 'retryable', code: 'review-operation-launch-gate-failed' };
|
|
581
1181
|
}
|
|
582
1182
|
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind)) {
|
|
583
1183
|
return { status: 'retryable', code: `report-operation-${result.kind}` };
|
|
@@ -590,196 +1190,7 @@ export function createV2Runtime(input) {
|
|
|
590
1190
|
});
|
|
591
1191
|
const implementationReviewer = new ContainedImplementationReviewer({
|
|
592
1192
|
operation: reportOperation,
|
|
593
|
-
createAttemptId: input.createAttemptId ?? randomUUID,
|
|
594
1193
|
});
|
|
595
|
-
const createAttemptId = input.createAttemptId ?? randomUUID;
|
|
596
|
-
const specOperation = {
|
|
597
|
-
author: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
|
|
598
|
-
const attemptId = createAttemptId();
|
|
599
|
-
const sessionId = state.authorSessionId ?? randomUUID();
|
|
600
|
-
let attempt;
|
|
601
|
-
try {
|
|
602
|
-
attempt = await prepareContainedAttempt({
|
|
603
|
-
orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
|
|
604
|
-
attemptId, operationId: 'spec-author', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
|
|
605
|
-
});
|
|
606
|
-
const revisionPath = join(dirname(attempt.reportPath), `revision-${state.revisions.length + 1}.md`);
|
|
607
|
-
await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath, revisionPath });
|
|
608
|
-
const config = requireConfig(currentConfig);
|
|
609
|
-
const result = await containedProcess.run({
|
|
610
|
-
codexPath: config.codex.command, cwd: dirname(attempt.reportPath), schemaPath: attempt.schemaPath,
|
|
611
|
-
reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
|
|
612
|
-
safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
|
|
613
|
-
parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
614
|
-
operationPolicy: attempt.policy, executionProfile: attempt.profile,
|
|
615
|
-
onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
|
|
616
|
-
prompt: [
|
|
617
|
-
`Package profile instructions: ${attempt.profile.developerInstructions}`,
|
|
618
|
-
`Follow the exact operation at ${attempt.operationPath}.`,
|
|
619
|
-
`The immutable workflow root is ${attempt.workflowRoot}.`,
|
|
620
|
-
`Author mode: ${mode}. Issue authority: ${canonicalJson(context.issue)}.`,
|
|
621
|
-
`Frozen criteria: ${canonicalJson(context.frozenCriteria)}.`,
|
|
622
|
-
`Prior revisions and review state: ${canonicalJson({ revisions: state.revisions, review: state.review })}.`,
|
|
623
|
-
`Write the complete new immutable revision only to ${revisionPath}. Return that exact absolute path and its SHA-256 in the report.`,
|
|
624
|
-
'Do not modify the product worktree, prior revisions, external state, or any .env file.',
|
|
625
|
-
].join('\n'),
|
|
626
|
-
}, signal);
|
|
627
|
-
if (result.kind === 'cancelled')
|
|
628
|
-
return { status: 'cancelled' };
|
|
629
|
-
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
|
|
630
|
-
return { status: 'retryable', code: `spec-author-${result.kind}` };
|
|
631
|
-
if (result.kind !== 'completed' || result.report.kind !== 'available')
|
|
632
|
-
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
633
|
-
const report = decodeAgentReportForValidation(result.report.bytes);
|
|
634
|
-
if (report.status !== 'ready' || report.specPath !== revisionPath || report.specSha256 === null)
|
|
635
|
-
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
636
|
-
const content = await readRegularFile(revisionPath);
|
|
637
|
-
if (report.specSha256 !== sha256(content))
|
|
638
|
-
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
639
|
-
const previous = state.revisions.at(-1) ?? null;
|
|
640
|
-
return { status: 'completed', value: createSpecRevision({
|
|
641
|
-
revision: state.revisions.length + 1, path: revisionPath, content: content.toString('utf8'),
|
|
642
|
-
evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
|
|
643
|
-
author: { attemptId, sessionId }, previousRevision: previous,
|
|
644
|
-
}) };
|
|
645
|
-
}
|
|
646
|
-
catch (error) {
|
|
647
|
-
if (error instanceof ProcessQuiescenceError) {
|
|
648
|
-
try {
|
|
649
|
-
await waitForProcessGroupAbsent(error.processGroupId);
|
|
650
|
-
return { status: 'retryable', code: 'spec-author-process-quiescence' };
|
|
651
|
-
}
|
|
652
|
-
catch {
|
|
653
|
-
return { status: 'blocked', kind: 'safety', code: 'spec-author-process-absence-unconfirmed' };
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
657
|
-
}
|
|
658
|
-
},
|
|
659
|
-
review: async ({ context, state, mode, signal, onPrepared, onLaunched }) => {
|
|
660
|
-
const attemptId = createAttemptId();
|
|
661
|
-
const sessionId = state.review.reviewer?.sessionId ?? randomUUID();
|
|
662
|
-
try {
|
|
663
|
-
const attempt = await prepareContainedAttempt({
|
|
664
|
-
orchestratorHome, canonicalRepository: requireCanonicalRepository(currentConfig), runId: context.runId,
|
|
665
|
-
attemptId, operationId: 'spec-review', workflowGeneration: context.workflowGeneration, bootId: input.bootId,
|
|
666
|
-
});
|
|
667
|
-
await onPrepared({ attemptId, sessionId, reportPath: attempt.reportPath });
|
|
668
|
-
const config = requireConfig(currentConfig);
|
|
669
|
-
const result = await containedProcess.run({
|
|
670
|
-
codexPath: config.codex.command, cwd: context.worktreePath, schemaPath: attempt.schemaPath,
|
|
671
|
-
reportPath: attempt.reportPath, toolHome: attempt.toolHome, tmpDir: attempt.tmpDir,
|
|
672
|
-
safePath: requireRuntimeString(input.safePath, 'safePath'), parentCodexHome: requireRuntimeString(input.parentCodexHome, 'parentCodexHome'),
|
|
673
|
-
parentEnv: process.env, timeoutMs: config.codex.timeoutMs, idleTimeoutMs: config.codex.idleTimeoutMs,
|
|
674
|
-
operationPolicy: attempt.policy, executionProfile: attempt.profile,
|
|
675
|
-
onSpawned: ({ pid, processGroupId }) => onLaunched({ attemptId, sessionId, pid, processGroupId }),
|
|
676
|
-
prompt: [
|
|
677
|
-
`Package profile instructions: ${attempt.profile.developerInstructions}`,
|
|
678
|
-
`Follow the exact operation at ${attempt.operationPath}.`,
|
|
679
|
-
`Reviewer session ID: ${sessionId}. Review mode: ${mode}.`,
|
|
680
|
-
`Issue authority and frozen criteria: ${canonicalJson({ issue: context.issue, frozenCriteria: context.frozenCriteria })}.`,
|
|
681
|
-
`Immutable spec delivery state: ${canonicalJson(state)}.`,
|
|
682
|
-
'Return only the package spec-review report. Do not edit files or external state.',
|
|
683
|
-
].join('\n'),
|
|
684
|
-
}, signal);
|
|
685
|
-
if (result.kind === 'cancelled')
|
|
686
|
-
return { status: 'cancelled' };
|
|
687
|
-
if (['spawn-failed', 'transport-failed', 'timeout', 'idle-timeout'].includes(result.kind))
|
|
688
|
-
return { status: 'retryable', code: `spec-review-${result.kind}` };
|
|
689
|
-
if (result.kind !== 'completed' || result.report.kind !== 'available')
|
|
690
|
-
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
691
|
-
const raw = decodeAgentReportForValidation(result.report.bytes);
|
|
692
|
-
if (raw.mode !== mode || raw.reviewerSessionId !== sessionId || !Array.isArray(raw.coverage) || !Array.isArray(raw.defects)
|
|
693
|
-
|| !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts) || !Array.isArray(raw.acceptedRisks)
|
|
694
|
-
|| typeof raw.coverageInvalidated !== 'boolean'
|
|
695
|
-
|| !['approved', 'needs-work', 'rejected'].includes(raw.verdict))
|
|
696
|
-
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
697
|
-
const target = state.revisions.at(-1);
|
|
698
|
-
const defects = validateCodeReviewDefects(raw.defects, target.revision);
|
|
699
|
-
const report = {
|
|
700
|
-
version: 1, targetRevision: target.revision, targetSha256: target.revisionSha256, mode,
|
|
701
|
-
verdict: raw.verdict, reviewer: { attemptId, sessionId },
|
|
702
|
-
coverage: raw.coverage, defects,
|
|
703
|
-
affectedDefectIds: raw.affectedDefectIds,
|
|
704
|
-
affectedContracts: raw.affectedContracts,
|
|
705
|
-
closureRequestSha256: mode === 'closure' ? state.review.closureRequestSha256 : null,
|
|
706
|
-
acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
|
|
707
|
-
};
|
|
708
|
-
return { status: 'completed', value: report, reportSha256: sha256(result.report.bytes) };
|
|
709
|
-
}
|
|
710
|
-
catch (error) {
|
|
711
|
-
if (error instanceof ProcessQuiescenceError) {
|
|
712
|
-
try {
|
|
713
|
-
await waitForProcessGroupAbsent(error.processGroupId);
|
|
714
|
-
return { status: 'retryable', code: 'spec-review-process-quiescence' };
|
|
715
|
-
}
|
|
716
|
-
catch {
|
|
717
|
-
return { status: 'blocked', kind: 'safety', code: 'spec-review-process-absence-unconfirmed' };
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
721
|
-
}
|
|
722
|
-
},
|
|
723
|
-
recover: async ({ context, state, signal }) => {
|
|
724
|
-
const invocation = state.invocation;
|
|
725
|
-
if (!invocation || signal.aborted)
|
|
726
|
-
return signal.aborted ? { status: 'cancelled' } : { status: 'blocked', kind: 'safety', code: 'spec-recovery-invocation-missing' };
|
|
727
|
-
if (invocation.status === 'launched') {
|
|
728
|
-
try {
|
|
729
|
-
await waitForProcessGroupAbsent(invocation.processGroupId);
|
|
730
|
-
}
|
|
731
|
-
catch {
|
|
732
|
-
return { status: 'blocked', kind: 'safety', code: 'spec-process-absence-unconfirmed' };
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
if (!invocation.reportPath)
|
|
736
|
-
return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
|
|
737
|
-
let reportBytes;
|
|
738
|
-
try {
|
|
739
|
-
reportBytes = await readRegularFile(invocation.reportPath);
|
|
740
|
-
}
|
|
741
|
-
catch {
|
|
742
|
-
return { status: 'retryable', code: `spec-${invocation.purpose}-transport-recovery` };
|
|
743
|
-
}
|
|
744
|
-
if (invocation.purpose === 'author') {
|
|
745
|
-
if (!invocation.revisionPath)
|
|
746
|
-
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
747
|
-
try {
|
|
748
|
-
const raw = decodeAgentReportForValidation(reportBytes);
|
|
749
|
-
const content = await readRegularFile(invocation.revisionPath);
|
|
750
|
-
if (raw.status !== 'ready' || raw.specPath !== invocation.revisionPath || raw.specSha256 !== sha256(content))
|
|
751
|
-
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
752
|
-
return { status: 'completed', value: createSpecRevision({
|
|
753
|
-
revision: state.revisions.length + 1, path: invocation.revisionPath, content: content.toString('utf8'),
|
|
754
|
-
evidence: [{ path: context.issue.url, sha256: sha256(canonicalJson(context.issue)), description: 'Frozen issue authority' }],
|
|
755
|
-
author: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, previousRevision: state.revisions.at(-1) ?? null,
|
|
756
|
-
}) };
|
|
757
|
-
}
|
|
758
|
-
catch {
|
|
759
|
-
return { status: 'retryable', code: 'spec-author-report-invalid' };
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
try {
|
|
763
|
-
const raw = decodeAgentReportForValidation(reportBytes);
|
|
764
|
-
if (raw.mode !== invocation.mode || raw.reviewerSessionId !== invocation.sessionId || !Array.isArray(raw.coverage)
|
|
765
|
-
|| !Array.isArray(raw.defects) || !Array.isArray(raw.affectedDefectIds) || !Array.isArray(raw.affectedContracts)
|
|
766
|
-
|| typeof raw.coverageInvalidated !== 'boolean' || !['approved', 'needs-work', 'rejected'].includes(raw.verdict)) {
|
|
767
|
-
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
768
|
-
}
|
|
769
|
-
const defects = validateCodeReviewDefects(raw.defects, invocation.targetRevision);
|
|
770
|
-
return { status: 'completed', reportSha256: sha256(reportBytes), value: {
|
|
771
|
-
version: 1, targetRevision: invocation.targetRevision, targetSha256: invocation.targetSha256,
|
|
772
|
-
mode: invocation.mode, verdict: raw.verdict,
|
|
773
|
-
reviewer: { attemptId: invocation.attemptId, sessionId: invocation.sessionId }, coverage: raw.coverage, defects,
|
|
774
|
-
affectedDefectIds: raw.affectedDefectIds, affectedContracts: raw.affectedContracts,
|
|
775
|
-
closureRequestSha256: invocation.closureRequestSha256, acceptedRisks: [], coverageInvalidated: raw.coverageInvalidated,
|
|
776
|
-
} };
|
|
777
|
-
}
|
|
778
|
-
catch {
|
|
779
|
-
return { status: 'retryable', code: 'spec-review-report-invalid' };
|
|
780
|
-
}
|
|
781
|
-
},
|
|
782
|
-
};
|
|
783
1194
|
const readConfig = async (requestedRoot) => {
|
|
784
1195
|
if (resolve(requestedRoot) !== targetRoot)
|
|
785
1196
|
throw new Error('runtime target root mismatch');
|
|
@@ -796,6 +1207,11 @@ export function createV2Runtime(input) {
|
|
|
796
1207
|
return { bytes, config };
|
|
797
1208
|
};
|
|
798
1209
|
const records = {
|
|
1210
|
+
inspect: async () => {
|
|
1211
|
+
if (!runRecords)
|
|
1212
|
+
throw new Error('run store used before config');
|
|
1213
|
+
return runRecords.inspect();
|
|
1214
|
+
},
|
|
799
1215
|
read: async () => {
|
|
800
1216
|
if (!runRecords)
|
|
801
1217
|
throw new Error('run store used before config');
|
|
@@ -813,8 +1229,8 @@ export function createV2Runtime(input) {
|
|
|
813
1229
|
const config = requireConfig(currentConfig);
|
|
814
1230
|
const checked = capabilities.verifyAndRead(proofInput.checkedChange);
|
|
815
1231
|
const repoKey = sha256(checked.payload.canonicalRepository);
|
|
816
|
-
const
|
|
817
|
-
const worktreePath =
|
|
1232
|
+
const issueWorktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${checked.payload.issueNumber}`);
|
|
1233
|
+
const worktreePath = proofInput.materialization?.path ?? issueWorktreePath;
|
|
818
1234
|
const androidLease = new FileAndroidLeaseVerifier({
|
|
819
1235
|
leaseRoot: join(orchestratorHome, 'v2', repoKey, 'leases'),
|
|
820
1236
|
worktreeRoot: worktreePath,
|
|
@@ -833,21 +1249,34 @@ export function createV2Runtime(input) {
|
|
|
833
1249
|
});
|
|
834
1250
|
const acceptanceProof = new AcceptanceProof({
|
|
835
1251
|
checkedChangeReader: capabilities,
|
|
836
|
-
proofRecords,
|
|
837
1252
|
proofAgent,
|
|
838
|
-
inspectFreshness: async (payload) =>
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1253
|
+
inspectFreshness: async (payload, materialization) => {
|
|
1254
|
+
const checkPolicySha256 = selectedConfiguredCheckPolicySha256(payload.checks, resolveIssueCheckPolicy(proofInput.issue.body, config.checks).checks);
|
|
1255
|
+
if (payload.version === 1)
|
|
1256
|
+
return {
|
|
1257
|
+
...await (proofFreshnessGit.snapshotIgnoringUntrackedRoot
|
|
1258
|
+
? proofFreshnessGit.snapshotIgnoringUntrackedRoot(worktreePath, config.proof.artifactDir)
|
|
1259
|
+
: git.snapshot(worktreePath)),
|
|
1260
|
+
checkPolicySha256,
|
|
1261
|
+
};
|
|
1262
|
+
if (!materialization || !git.candidateV2)
|
|
1263
|
+
throw new CandidateProofInspectionError('candidate-git-v2-required');
|
|
1264
|
+
const inspection = await git.candidateV2.inspectMaterialization({
|
|
1265
|
+
binding: payload.binding,
|
|
1266
|
+
materialization,
|
|
1267
|
+
artifactDir: config.proof.artifactDir,
|
|
1268
|
+
});
|
|
1269
|
+
if (inspection.kind === 'failed')
|
|
1270
|
+
throw new CandidateProofInspectionError(inspection.code);
|
|
1271
|
+
if (inspection.value !== 'matching')
|
|
1272
|
+
throw new CandidateProofInspectionError('candidate-materialization-mutated');
|
|
1273
|
+
return { bindingId: payload.binding.bindingId, candidateTreeSha: payload.binding.candidateTreeSha, checkPolicySha256 };
|
|
1274
|
+
},
|
|
844
1275
|
readArtifact: async (relativePath) => readRegularFile(resolve(worktreePath, relativePath)),
|
|
845
1276
|
inspectArtifact: async (relativePath) => inspectRegularFile(resolve(worktreePath, relativePath)),
|
|
846
1277
|
androidLease,
|
|
847
1278
|
iosLease,
|
|
848
1279
|
proofArtifactDir: config.proof.artifactDir,
|
|
849
|
-
createAttemptId: input.createAttemptId ?? randomUUID,
|
|
850
|
-
now,
|
|
851
1280
|
signal: controller.signal,
|
|
852
1281
|
});
|
|
853
1282
|
const androidRelevant = !!config.proof.android && isAndroidProofRelevant(proofInput.issue, proofInput.frozenCriteria, checked.payload.changedFiles);
|
|
@@ -855,7 +1284,7 @@ export function createV2Runtime(input) {
|
|
|
855
1284
|
const runnerPreparedArtifactSha256 = {};
|
|
856
1285
|
const runnerPreparationWarnings = [];
|
|
857
1286
|
let preparationAttempted = false;
|
|
858
|
-
|
|
1287
|
+
const result = await acceptanceProof.proveChange({
|
|
859
1288
|
...proofInput,
|
|
860
1289
|
runnerPreparedArtifactPaths,
|
|
861
1290
|
runnerPreparedArtifactSha256,
|
|
@@ -885,6 +1314,21 @@ export function createV2Runtime(input) {
|
|
|
885
1314
|
}
|
|
886
1315
|
},
|
|
887
1316
|
});
|
|
1317
|
+
if (checked.payload.version === 2 && proofInput.materialization && git.candidateV2 && result.status === 'passed') {
|
|
1318
|
+
const artifacts = result.receipt.publishableEvidence.map((artifact) => ({ relativePath: artifact.ref, sha256: artifact.sha256 }));
|
|
1319
|
+
const copied = await git.candidateV2.copyProofArtifacts({
|
|
1320
|
+
materialization: proofInput.materialization,
|
|
1321
|
+
issueWorktreePath,
|
|
1322
|
+
artifactDir: config.proof.artifactDir,
|
|
1323
|
+
proofId: proofInput.proofId,
|
|
1324
|
+
artifacts,
|
|
1325
|
+
});
|
|
1326
|
+
if (copied.kind === 'failed')
|
|
1327
|
+
throw new CandidateProofInspectionError(copied.code);
|
|
1328
|
+
if (copied.value.kind === 'artifact-conflict')
|
|
1329
|
+
throw new CandidateProofInspectionError('candidate-artifact-conflict');
|
|
1330
|
+
}
|
|
1331
|
+
return result;
|
|
888
1332
|
},
|
|
889
1333
|
};
|
|
890
1334
|
const runner = new RunIssue({
|
|
@@ -918,6 +1362,8 @@ export function createV2Runtime(input) {
|
|
|
918
1362
|
id: comment.id,
|
|
919
1363
|
body: comment.body,
|
|
920
1364
|
authorAssociation: comment.authorAssociation,
|
|
1365
|
+
author: comment.author.login,
|
|
1366
|
+
authorId: comment.author.id,
|
|
921
1367
|
createdAt: comment.createdAt,
|
|
922
1368
|
updatedAt: comment.updatedAt,
|
|
923
1369
|
})),
|
|
@@ -930,9 +1376,10 @@ export function createV2Runtime(input) {
|
|
|
930
1376
|
removeLabels: current.filter((label) => !labels.includes(label)),
|
|
931
1377
|
});
|
|
932
1378
|
},
|
|
1379
|
+
getRepositoryPermission: (login, expectedUserId) => input.issues.getRepositoryPermission(login, expectedUserId),
|
|
933
1380
|
transitionToBlocked: async (issueNumber, labels) => {
|
|
934
1381
|
const current = await input.issues.getLabels(issueNumber);
|
|
935
|
-
if (current.includes(labels.review)
|
|
1382
|
+
if (current.includes(labels.review))
|
|
936
1383
|
return;
|
|
937
1384
|
const hasRunnerStatus = current.some((label) => (label === labels.auto || label === labels.running || label === labels.blocked));
|
|
938
1385
|
if (!hasRunnerStatus)
|
|
@@ -949,7 +1396,7 @@ export function createV2Runtime(input) {
|
|
|
949
1396
|
const matches = (await input.pullRequests.listAllByHeadBranch(headBranch))
|
|
950
1397
|
.filter((pullRequest) => pullRequest.state === 'OPEN' && pullRequest.baseRefName === baseBranch);
|
|
951
1398
|
if (matches.length > 1)
|
|
952
|
-
throw new Error('multiple open pull requests match publication
|
|
1399
|
+
throw new Error('multiple open pull requests match publication pendingEffect');
|
|
953
1400
|
const match = matches[0];
|
|
954
1401
|
if (!match)
|
|
955
1402
|
return undefined;
|
|
@@ -970,59 +1417,17 @@ export function createV2Runtime(input) {
|
|
|
970
1417
|
return { id: comment.id, body: comment.body };
|
|
971
1418
|
},
|
|
972
1419
|
},
|
|
973
|
-
reviewFeedback: new
|
|
1420
|
+
reviewFeedback: new ReviewFeedbackObserver({ pullRequests: input.pullRequests, issues: input.issues, now }),
|
|
974
1421
|
git,
|
|
975
|
-
routeCoordinator: {
|
|
976
|
-
run: ({ state, ...routeInput }) => new RouteCoordinator({
|
|
977
|
-
state,
|
|
978
|
-
operation: reportOperation,
|
|
979
|
-
createAttemptId: input.createAttemptId ?? randomUUID,
|
|
980
|
-
now,
|
|
981
|
-
createReceipt: ({ artifact, triage, review, decidedAt }) => {
|
|
982
|
-
if (artifact.status === 'blocked')
|
|
983
|
-
throw new Error('blocked triage cannot create a route receipt');
|
|
984
|
-
const receipt = {
|
|
985
|
-
version: 1,
|
|
986
|
-
route: artifact.status,
|
|
987
|
-
triage,
|
|
988
|
-
review,
|
|
989
|
-
artifact,
|
|
990
|
-
decisionSha256: '',
|
|
991
|
-
decidedAt,
|
|
992
|
-
assumptions: [...artifact.assumptions],
|
|
993
|
-
};
|
|
994
|
-
receipt.decisionSha256 = hashRouteDecision(receipt);
|
|
995
|
-
return validateRouteReceipt(receipt, triage.generationHash);
|
|
996
|
-
},
|
|
997
|
-
}).run(routeInput),
|
|
998
|
-
},
|
|
999
|
-
routeContinuations: {
|
|
1000
|
-
direct: async () => ({ status: 'completed' }),
|
|
1001
|
-
specRequired: (context, state, signal) => new SpecCoordinator({ state, operation: specOperation }).run(context, signal),
|
|
1002
|
-
awaitingUser: async (context, state, signal) => {
|
|
1003
|
-
const config = requireConfig(currentConfig);
|
|
1004
|
-
return new WaitingHumanCoordinator({
|
|
1005
|
-
issues: input.issues,
|
|
1006
|
-
labels: {
|
|
1007
|
-
auto: config.github.labels.auto.name,
|
|
1008
|
-
running: config.github.labels.running.name,
|
|
1009
|
-
blocked: config.github.labels.blocked.name,
|
|
1010
|
-
review: config.github.labels.review.name,
|
|
1011
|
-
waitingHuman: config.github.labels.waitingHuman.name,
|
|
1012
|
-
},
|
|
1013
|
-
now,
|
|
1014
|
-
}).run(context, state, signal);
|
|
1015
|
-
},
|
|
1016
|
-
},
|
|
1017
1422
|
implementationAgent,
|
|
1018
1423
|
implementationReviewer,
|
|
1019
|
-
waitForReviewProcessAbsence: waitForProcessGroupAbsent,
|
|
1020
1424
|
checks: {
|
|
1021
|
-
|
|
1425
|
+
supportsLaunchOwnership: true,
|
|
1426
|
+
run: async ({ source, command, cwd, signal, onLaunched }) => {
|
|
1022
1427
|
const timeoutMs = requireConfig(currentConfig).codex.timeoutMs;
|
|
1023
1428
|
return source === 'issue'
|
|
1024
|
-
? runProcessCheck(parseIssueCheckInvocation(command), cwd, signal, timeoutMs)
|
|
1025
|
-
: runShellCheck(command, cwd, signal, timeoutMs);
|
|
1429
|
+
? runProcessCheck(parseIssueCheckInvocation(command), cwd, signal, timeoutMs, onLaunched)
|
|
1430
|
+
: runShellCheck(command, cwd, signal, timeoutMs, onLaunched);
|
|
1026
1431
|
},
|
|
1027
1432
|
},
|
|
1028
1433
|
proof,
|
|
@@ -1034,12 +1439,56 @@ export function createV2Runtime(input) {
|
|
|
1034
1439
|
await writeDurableAtomicFile(resolve(targetRoot, relativePath), `${canonicalJson({ version: 1, runId, code, summary, recordedAt: now() })}\n`);
|
|
1035
1440
|
return { id: `evidence:${runId}:${code}`, path: relativePath };
|
|
1036
1441
|
},
|
|
1442
|
+
outcomeEvidencePath: (runId, code, summarySha256) => `${requireConfig(currentConfig).runner.stateDir}/v2/evidence/${runId}/${sha256(code)}-${summarySha256}.json`,
|
|
1443
|
+
inspectOutcomeEvidence: async (path) => {
|
|
1444
|
+
try {
|
|
1445
|
+
return { sha256: sha256(await readRegularFile(resolve(targetRoot, path))) };
|
|
1446
|
+
}
|
|
1447
|
+
catch (error) {
|
|
1448
|
+
if (isErrorCode(error, 'ENOENT'))
|
|
1449
|
+
return undefined;
|
|
1450
|
+
throw error;
|
|
1451
|
+
}
|
|
1452
|
+
},
|
|
1453
|
+
writeOutcomeEvidence: ({ path, bytes, sha256: expectedSha256 }) => writeExactDurableBytes(resolve(targetRoot, path), bytes, expectedSha256),
|
|
1037
1454
|
packageVersion: input.packageVersion,
|
|
1038
1455
|
createWorkflowGeneration: input.createWorkflowGeneration,
|
|
1039
1456
|
verifyWorkflowGeneration,
|
|
1040
1457
|
createRunId: input.createRunId ?? randomUUID,
|
|
1041
1458
|
createProofId: input.createProofId ?? randomUUID,
|
|
1042
1459
|
createReviewSessionId: input.createAttemptId ?? randomUUID,
|
|
1460
|
+
processIdentity: {
|
|
1461
|
+
host: hostname(),
|
|
1462
|
+
bootId: input.bootId,
|
|
1463
|
+
capture: async (pid, processGroupId) => {
|
|
1464
|
+
const captured = await captureProcessStartIdentity({ platform: process.platform, pid, processGroupId });
|
|
1465
|
+
return captured.status === 'available' ? captured.identity : undefined;
|
|
1466
|
+
},
|
|
1467
|
+
observe: async (identity) => ({
|
|
1468
|
+
leader: await observeProcessIdentity({ platform: process.platform, ...identity }),
|
|
1469
|
+
group: observeProcessGroup(identity.processGroupId),
|
|
1470
|
+
}),
|
|
1471
|
+
},
|
|
1472
|
+
inspectAttemptResult: async (path) => {
|
|
1473
|
+
try {
|
|
1474
|
+
const bytes = await readRegularFile(path);
|
|
1475
|
+
return { bytes, sha256: sha256(bytes) };
|
|
1476
|
+
}
|
|
1477
|
+
catch (error) {
|
|
1478
|
+
if (isErrorCode(error, 'ENOENT'))
|
|
1479
|
+
return undefined;
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
observeAttemptCleanup: async (attempt) => {
|
|
1484
|
+
return observeAttemptReadViewCleanup({
|
|
1485
|
+
orchestratorHome,
|
|
1486
|
+
canonicalRepository: requireCanonicalRepository(currentConfig),
|
|
1487
|
+
identity: attempt,
|
|
1488
|
+
});
|
|
1489
|
+
},
|
|
1490
|
+
writeAttemptResult: ({ path, bytes, sha256: expectedSha256 }) => writeExactDurableBytes(path, bytes, expectedSha256),
|
|
1491
|
+
attemptResultPath: ({ canonicalRepository, runId, attemptId }) => join(resolve(orchestratorHome), 'v2', sha256(canonicalRepository), 'runs', runId, 'attempts', attemptId, 'report.json'),
|
|
1043
1492
|
now,
|
|
1044
1493
|
signal: controller.signal,
|
|
1045
1494
|
});
|
|
@@ -1078,7 +1527,10 @@ async function acquireOwnerLock(input) {
|
|
|
1078
1527
|
}
|
|
1079
1528
|
}
|
|
1080
1529
|
async function readRegularFile(path) {
|
|
1081
|
-
const
|
|
1530
|
+
const beforeOpen = await lstat(path);
|
|
1531
|
+
if (beforeOpen.isSymbolicLink() || !beforeOpen.isFile())
|
|
1532
|
+
throw new Error(`${path} is not a bounded regular file`);
|
|
1533
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
1082
1534
|
try {
|
|
1083
1535
|
const stat = await handle.stat();
|
|
1084
1536
|
if (!stat.isFile() || stat.size > 5 * 1024 * 1024)
|
|
@@ -1089,6 +1541,24 @@ async function readRegularFile(path) {
|
|
|
1089
1541
|
await handle.close();
|
|
1090
1542
|
}
|
|
1091
1543
|
}
|
|
1544
|
+
async function writeExactDurableBytes(path, bytes, expectedSha256) {
|
|
1545
|
+
if (sha256(bytes) !== expectedSha256)
|
|
1546
|
+
throw new Error('durable result bytes do not match expected SHA-256');
|
|
1547
|
+
try {
|
|
1548
|
+
const existing = await readRegularFile(path);
|
|
1549
|
+
if (sha256(existing) !== expectedSha256)
|
|
1550
|
+
throw new Error('durable result path already contains different bytes');
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
catch (error) {
|
|
1554
|
+
if (!isErrorCode(error, 'ENOENT'))
|
|
1555
|
+
throw error;
|
|
1556
|
+
}
|
|
1557
|
+
await writeDurableAtomicFile(path, bytes, 0o600);
|
|
1558
|
+
const observed = await readRegularFile(path);
|
|
1559
|
+
if (sha256(observed) !== expectedSha256)
|
|
1560
|
+
throw new Error('durable result write postcondition failed');
|
|
1561
|
+
}
|
|
1092
1562
|
async function inspectRegularFile(path) {
|
|
1093
1563
|
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1094
1564
|
try {
|
|
@@ -1101,15 +1571,20 @@ async function inspectRegularFile(path) {
|
|
|
1101
1571
|
await handle.close();
|
|
1102
1572
|
}
|
|
1103
1573
|
}
|
|
1104
|
-
export async function runShellCheck(command, cwd, signal, timeoutMs) {
|
|
1105
|
-
return runSpawnCheck('/bin/sh', ['-lc', command], cwd, signal, timeoutMs);
|
|
1574
|
+
export async function runShellCheck(command, cwd, signal, timeoutMs, onLaunched) {
|
|
1575
|
+
return runSpawnCheck('/bin/sh', ['-lc', command], cwd, signal, timeoutMs, onLaunched);
|
|
1106
1576
|
}
|
|
1107
|
-
async function runProcessCheck(invocation, cwd, signal, timeoutMs) {
|
|
1108
|
-
return runSpawnCheck(invocation.file, invocation.args, cwd, signal, timeoutMs);
|
|
1577
|
+
async function runProcessCheck(invocation, cwd, signal, timeoutMs, onLaunched) {
|
|
1578
|
+
return runSpawnCheck(invocation.file, invocation.args, cwd, signal, timeoutMs, onLaunched);
|
|
1109
1579
|
}
|
|
1110
|
-
async function runSpawnCheck(file, args, cwd, signal, timeoutMs) {
|
|
1580
|
+
async function runSpawnCheck(file, args, cwd, signal, timeoutMs, onLaunched) {
|
|
1111
1581
|
return new Promise((resolveCheck, rejectCheck) => {
|
|
1112
|
-
const child = spawn(
|
|
1582
|
+
const child = spawn('/bin/sh', [
|
|
1583
|
+
'-c', 'IFS= read -r _ || exit 125; exec "$@"', 'codex-orchestrator-check-gate', file, ...args,
|
|
1584
|
+
], { cwd, detached: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1585
|
+
const launchOwnership = child.pid && onLaunched
|
|
1586
|
+
? onLaunched({ pid: child.pid, processGroupId: child.pid })
|
|
1587
|
+
: Promise.resolve();
|
|
1113
1588
|
const chunks = [];
|
|
1114
1589
|
let retained = 0;
|
|
1115
1590
|
const outputHash = createHash('sha256');
|
|
@@ -1150,6 +1625,7 @@ async function runSpawnCheck(file, args, cwd, signal, timeoutMs) {
|
|
|
1150
1625
|
}, 5_000);
|
|
1151
1626
|
killTimer.unref();
|
|
1152
1627
|
};
|
|
1628
|
+
void launchOwnership.then(() => child.stdin.end('\n')).catch(() => terminate());
|
|
1153
1629
|
signal.addEventListener('abort', terminate, { once: true });
|
|
1154
1630
|
if (signal.aborted)
|
|
1155
1631
|
terminate();
|
|
@@ -1189,10 +1665,15 @@ async function runSpawnCheck(file, args, cwd, signal, timeoutMs) {
|
|
|
1189
1665
|
rejectCheck(new Error(`Check exceeded ${timeoutMs}ms and was terminated.`));
|
|
1190
1666
|
return;
|
|
1191
1667
|
}
|
|
1668
|
+
await launchOwnership;
|
|
1192
1669
|
resolveCheck({
|
|
1193
1670
|
status: code === 0 ? 'passed' : 'failed',
|
|
1194
1671
|
output: Buffer.concat(chunks),
|
|
1195
1672
|
outputSha256: outputHash.digest('hex'),
|
|
1673
|
+
observation: {
|
|
1674
|
+
leader: 'absent',
|
|
1675
|
+
group: child.pid ? observeProcessGroup(child.pid) : 'unknown',
|
|
1676
|
+
},
|
|
1196
1677
|
});
|
|
1197
1678
|
})().catch(rejectCheck);
|
|
1198
1679
|
});
|
|
@@ -1303,8 +1784,7 @@ async function prepareContainedAttempt(input) {
|
|
|
1303
1784
|
operation: input.operationId,
|
|
1304
1785
|
bootId: input.bootId,
|
|
1305
1786
|
});
|
|
1306
|
-
const reportOnly = input.operationId === '
|
|
1307
|
-
|| input.operationId === 'code-review' || input.operationId === 'spec-review';
|
|
1787
|
+
const reportOnly = input.operationId === 'code-review';
|
|
1308
1788
|
if ((reportOnly
|
|
1309
1789
|
? snapshot.policy.sandboxMode !== 'read-only'
|
|
1310
1790
|
|| snapshot.policy.worktreeAccess !== 'read-only'
|
|
@@ -1340,6 +1820,8 @@ async function prepareContainedAttempt(input) {
|
|
|
1340
1820
|
tmpDir,
|
|
1341
1821
|
policy: structuredClone(snapshot.policy),
|
|
1342
1822
|
profile,
|
|
1823
|
+
reviewers: Object.keys(snapshot.reviewerProfiles).sort(),
|
|
1824
|
+
reviewerProfiles: structuredClone(snapshot.reviewerProfiles),
|
|
1343
1825
|
};
|
|
1344
1826
|
}
|
|
1345
1827
|
async function artifactInventory(root, logicalRoot) {
|
|
@@ -1355,7 +1837,9 @@ async function artifactInventory(root, logicalRoot) {
|
|
|
1355
1837
|
}
|
|
1356
1838
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
|
|
1357
1839
|
throw new Error('proof artifact root must be a direct directory');
|
|
1840
|
+
const rootIdentity = await inspectSafeArtifactRoot(root);
|
|
1358
1841
|
const visit = async (directory, relative) => {
|
|
1842
|
+
await assertSafeArtifactRoot(rootIdentity);
|
|
1359
1843
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
1360
1844
|
const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
1361
1845
|
const childPath = join(directory, entry.name);
|
|
@@ -1364,7 +1848,7 @@ async function artifactInventory(root, logicalRoot) {
|
|
|
1364
1848
|
if (entry.isDirectory())
|
|
1365
1849
|
await visit(childPath, childRelative);
|
|
1366
1850
|
else if (entry.isFile())
|
|
1367
|
-
output.set(`${logicalRoot}/${childRelative}`, sha256(await
|
|
1851
|
+
output.set(`${logicalRoot}/${childRelative}`, sha256(await readRegularFileWithoutSymlinkAncestors(rootIdentity, childPath)));
|
|
1368
1852
|
else
|
|
1369
1853
|
throw new Error('proof artifact tree contains a special file');
|
|
1370
1854
|
}
|
|
@@ -1413,20 +1897,6 @@ async function fingerprintDeniedEntry(path, existingInfo) {
|
|
|
1413
1897
|
}
|
|
1414
1898
|
return { kind: 'directory', entries };
|
|
1415
1899
|
}
|
|
1416
|
-
async function waitForProcessGroupAbsent(processGroupId) {
|
|
1417
|
-
while (true) {
|
|
1418
|
-
try {
|
|
1419
|
-
process.kill(-processGroupId, 0);
|
|
1420
|
-
}
|
|
1421
|
-
catch (error) {
|
|
1422
|
-
if (isErrorCode(error, 'ESRCH'))
|
|
1423
|
-
return;
|
|
1424
|
-
if (!isErrorCode(error, 'EPERM'))
|
|
1425
|
-
throw error;
|
|
1426
|
-
}
|
|
1427
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 1_000));
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
1900
|
export async function materializeReportReadView(input) {
|
|
1431
1901
|
await assertDisjointReadViewPaths(input.worktreePath, input.destination);
|
|
1432
1902
|
const deniedPaths = normalizeReadViewDeniedPaths(input.deniedPaths);
|