codex-orchestrator 2.0.10 → 2.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +26 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/v2/acceptance-proof.d.ts +29 -16
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +159 -129
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/command.d.ts +1 -0
- package/dist/src/v2/adapters/command.d.ts.map +1 -1
- package/dist/src/v2/adapters/command.js +1 -1
- package/dist/src/v2/adapters/command.js.map +1 -1
- package/dist/src/v2/adapters/gh-pull-request-adapter.js +1 -0
- package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
- package/dist/src/v2/adapters/pull-requests.d.ts +1 -0
- package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
- package/dist/src/v2/adapters/pull-requests.js +1 -0
- package/dist/src/v2/adapters/pull-requests.js.map +1 -1
- package/dist/src/v2/adapters/worktree.js +3 -3
- package/dist/src/v2/adapters/worktree.js.map +1 -1
- package/dist/src/v2/atomic-store.d.ts +15 -0
- package/dist/src/v2/atomic-store.d.ts.map +1 -1
- package/dist/src/v2/atomic-store.js +55 -8
- package/dist/src/v2/atomic-store.js.map +1 -1
- package/dist/src/v2/candidate.d.ts +152 -0
- package/dist/src/v2/candidate.d.ts.map +1 -0
- package/dist/src/v2/candidate.js +129 -0
- package/dist/src/v2/candidate.js.map +1 -0
- package/dist/src/v2/checked-change.d.ts +37 -6
- package/dist/src/v2/checked-change.d.ts.map +1 -1
- package/dist/src/v2/checked-change.js +71 -0
- package/dist/src/v2/checked-change.js.map +1 -1
- package/dist/src/v2/cli.d.ts +6 -0
- package/dist/src/v2/cli.d.ts.map +1 -1
- package/dist/src/v2/cli.js +57 -21
- package/dist/src/v2/cli.js.map +1 -1
- package/dist/src/v2/codex-process.d.ts +1 -0
- package/dist/src/v2/codex-process.d.ts.map +1 -1
- package/dist/src/v2/codex-process.js +12 -1
- package/dist/src/v2/codex-process.js.map +1 -1
- package/dist/src/v2/contained-report-operation.d.ts +196 -30
- package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
- package/dist/src/v2/contained-report-operation.js +464 -150
- package/dist/src/v2/contained-report-operation.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +10 -28
- package/dist/src/v2/direct-delivery.d.ts.map +1 -1
- package/dist/src/v2/direct-delivery.js +44 -114
- package/dist/src/v2/direct-delivery.js.map +1 -1
- package/dist/src/v2/implementation-reviewer.d.ts +4 -24
- package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
- package/dist/src/v2/implementation-reviewer.js +29 -35
- package/dist/src/v2/implementation-reviewer.js.map +1 -1
- package/dist/src/v2/owner-control-lock.d.ts +13 -1
- package/dist/src/v2/owner-control-lock.d.ts.map +1 -1
- package/dist/src/v2/owner-control-lock.js +66 -7
- package/dist/src/v2/owner-control-lock.js.map +1 -1
- package/dist/src/v2/proof-report.d.ts +2 -1
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +6 -40
- package/dist/src/v2/proof-report.js.map +1 -1
- package/dist/src/v2/proof-store.d.ts +19 -7
- package/dist/src/v2/proof-store.d.ts.map +1 -1
- package/dist/src/v2/proof-store.js +162 -41
- package/dist/src/v2/proof-store.js.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.d.ts +7 -1
- package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -1
- package/dist/src/v2/review-feedback-coordinator.js +97 -70
- package/dist/src/v2/review-feedback-coordinator.js.map +1 -1
- package/dist/src/v2/review-feedback.d.ts +0 -18
- package/dist/src/v2/review-feedback.d.ts.map +1 -1
- package/dist/src/v2/review-feedback.js +9 -34
- package/dist/src/v2/review-feedback.js.map +1 -1
- package/dist/src/v2/route-coordinator.d.ts +7 -12
- package/dist/src/v2/route-coordinator.d.ts.map +1 -1
- package/dist/src/v2/route-coordinator.js +33 -128
- package/dist/src/v2/route-coordinator.js.map +1 -1
- package/dist/src/v2/route-decision.d.ts +0 -9
- package/dist/src/v2/route-decision.d.ts.map +1 -1
- package/dist/src/v2/route-decision.js +7 -27
- package/dist/src/v2/route-decision.js.map +1 -1
- package/dist/src/v2/run-issue.d.ts +64 -26
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +1298 -620
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +39 -25
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +469 -88
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +61 -36
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +992 -402
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/spec-coordinator.d.ts +16 -28
- package/dist/src/v2/spec-coordinator.d.ts.map +1 -1
- package/dist/src/v2/spec-coordinator.js +81 -43
- package/dist/src/v2/spec-coordinator.js.map +1 -1
- package/dist/src/v2/spec-delivery.d.ts +3 -34
- package/dist/src/v2/spec-delivery.d.ts.map +1 -1
- package/dist/src/v2/spec-delivery.js +42 -107
- package/dist/src/v2/spec-delivery.js.map +1 -1
- package/docs/deep-dive.md +52 -14
- package/internal-workflow/evals/coding-skill-evals.json +58 -17
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/schemas/proof-report-v1.json +1 -1
- package/internal-workflow/skills/code-review/SKILL.md +18 -0
- package/internal-workflow/skills/spec-implementer/SKILL.md +8 -1
- package/package.json +1 -1
package/dist/src/v2/run-issue.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
2
|
import { checkedChangePayloadSha256, } from './checked-change.js';
|
|
3
3
|
import { parseAgentAutoConfig } from './config.js';
|
|
4
4
|
import { canonicalJson, sha256 } from './containment.js';
|
|
5
5
|
import { validateImplementationReport } from './implementation-report.js';
|
|
6
|
-
import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair,
|
|
7
|
-
import {
|
|
6
|
+
import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, createInitialDirectReview, directReviewCandidateTargetFingerprint, directReviewClosureRequestSha256, MAX_DIRECT_REVIEW_REPORT_REPAIRS, prepareDirectReviewClosure, projectTerminalDirectReview, } from './direct-delivery.js';
|
|
7
|
+
import { CandidateProofInspectionError, ProofLaunchAuthorizationError } from './acceptance-proof.js';
|
|
8
8
|
import { CheckProcessQuiescenceError, resolveIssueCheckPolicy } from './issue-check-policy.js';
|
|
9
9
|
import { RouteInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
|
|
10
10
|
import { initialRouteExecution, } from './route-coordinator.js';
|
|
11
|
+
import { reserveSpecAuthorSession, reserveSpecReviewerSession } from './spec-delivery.js';
|
|
11
12
|
import { downstreamLifecycleForRoute, validateRouteTransition, validateTrustedAnswerResumeTransition, } from './route-decision.js';
|
|
12
13
|
import { activateReviewFeedback, blockReviewFeedback, bootstrapReviewFeedback, markReviewFeedbackPublishing, markReviewFeedbackRepairing, markReviewFeedbackVerified, projectReviewFeedbackBatch, publishReviewFeedback, reserveNextReviewFeedbackRound, } from './review-feedback.js';
|
|
13
14
|
export class OwnerLockSafetyError extends Error {
|
|
@@ -71,6 +72,7 @@ export class RunIssue {
|
|
|
71
72
|
}
|
|
72
73
|
if (comments.length === 0) {
|
|
73
74
|
try {
|
|
75
|
+
await this.markV3ExternalEffect(active);
|
|
74
76
|
await this.dependencies.issues.postComment(issueNumber, body);
|
|
75
77
|
}
|
|
76
78
|
catch {
|
|
@@ -105,6 +107,7 @@ export class RunIssue {
|
|
|
105
107
|
return { result: await this.publicationDiverged(active, 'claim-labels-intent-diverged') };
|
|
106
108
|
}
|
|
107
109
|
try {
|
|
110
|
+
await this.markV3ExternalEffect(active);
|
|
108
111
|
await this.dependencies.issues.setLabels(issueNumber, expectedLabels);
|
|
109
112
|
}
|
|
110
113
|
catch {
|
|
@@ -122,7 +125,99 @@ export class RunIssue {
|
|
|
122
125
|
const { runId, branchName, worktreePath, baseSha } = active.record;
|
|
123
126
|
const message = `feat: implement #${issueNumber}`;
|
|
124
127
|
let commitSha = await this.dependencies.git.getHead(worktreePath);
|
|
125
|
-
|
|
128
|
+
const candidateBinding = active.record.candidateBinding;
|
|
129
|
+
if (candidateBinding) {
|
|
130
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
131
|
+
if (!candidate)
|
|
132
|
+
return this.persistRetainedCommitIntentTerminal(active, 'candidate-git-v2-required');
|
|
133
|
+
if (!active.record.intent) {
|
|
134
|
+
if (commitSha !== candidateBinding.expectedHeadSha)
|
|
135
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'candidate-branch-diverged-without-intent');
|
|
136
|
+
const recaptured = await candidate.captureAndPin({
|
|
137
|
+
worktreePath,
|
|
138
|
+
expectedHeadSha: candidateBinding.expectedHeadSha,
|
|
139
|
+
runId,
|
|
140
|
+
boundary: { kind: 'implementation-cycle', cycle: active.record.cycle },
|
|
141
|
+
artifactDir: config.proof.artifactDir,
|
|
142
|
+
});
|
|
143
|
+
if (recaptured.kind === 'failed')
|
|
144
|
+
return this.mapCandidateFailure(active, recaptured.code);
|
|
145
|
+
if (recaptured.value.bindingId !== candidateBinding.bindingId
|
|
146
|
+
|| recaptured.value.candidateTreeSha !== candidateBinding.candidateTreeSha
|
|
147
|
+
|| !sameStrings(recaptured.value.canonicalChangedFiles, candidateBinding.canonicalChangedFiles)) {
|
|
148
|
+
if (recaptured.value.bindingId !== candidateBinding.bindingId) {
|
|
149
|
+
await candidate.releasePin({ binding: recaptured.value, expectedPinnedCommitSha: recaptured.value.candidateCommitSha });
|
|
150
|
+
}
|
|
151
|
+
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
152
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
153
|
+
return 'status' in released
|
|
154
|
+
? released
|
|
155
|
+
: this.terminal(released.active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'candidate-worktree-drift');
|
|
156
|
+
}
|
|
157
|
+
const reopened = await this.startNextCycleFromCandidate(active, ['The issue worktree changed after proof; review the new candidate.']);
|
|
158
|
+
if ('status' in reopened)
|
|
159
|
+
return reopened;
|
|
160
|
+
return this.invokedFailure(reopened, 'candidate-worktree-drift', 'The mutable issue worktree changed after proof; the next bounded repair cycle is ready.');
|
|
161
|
+
}
|
|
162
|
+
active = await this.persist(active, { intent: {
|
|
163
|
+
kind: 'commit',
|
|
164
|
+
parentSha: candidateBinding.expectedHeadSha,
|
|
165
|
+
treeSha: candidateBinding.candidateTreeSha,
|
|
166
|
+
message,
|
|
167
|
+
candidateRef: candidateBinding.candidateRef,
|
|
168
|
+
} });
|
|
169
|
+
}
|
|
170
|
+
const intent = active.record.intent;
|
|
171
|
+
if (intent?.kind !== 'commit' || intent.parentSha !== candidateBinding.expectedHeadSha
|
|
172
|
+
|| intent.treeSha !== candidateBinding.candidateTreeSha || intent.message !== message
|
|
173
|
+
|| intent.candidateRef !== candidateBinding.candidateRef) {
|
|
174
|
+
return this.persistRetainedCommitIntentTerminal(active, 'candidate-commit-intent-diverged');
|
|
175
|
+
}
|
|
176
|
+
if (this.signal.aborted)
|
|
177
|
+
return await this.persistRetainedCommitIntentTerminal(active, 'candidate-publication-cancelled');
|
|
178
|
+
if (!await this.authorized(active, config))
|
|
179
|
+
return await this.persistRetainedCommitIntentTerminal(active, 'candidate-publication-authority-revoked');
|
|
180
|
+
await this.markV3ExternalEffect(active);
|
|
181
|
+
const publication = await candidate.createOrObserveCommit({
|
|
182
|
+
worktreePath,
|
|
183
|
+
branchName,
|
|
184
|
+
parentSha: intent.parentSha,
|
|
185
|
+
treeSha: intent.treeSha,
|
|
186
|
+
message: intent.message,
|
|
187
|
+
candidateRef: intent.candidateRef,
|
|
188
|
+
});
|
|
189
|
+
if (publication.kind === 'failed') {
|
|
190
|
+
return publication.code === 'candidate-ref-update-unknown'
|
|
191
|
+
? this.persistTerminal(active, { status: 'transport-failed', resumable: false }, publication.code, true)
|
|
192
|
+
: this.mapCandidateFailure(active, publication.code);
|
|
193
|
+
}
|
|
194
|
+
if (publication.value.kind === 'parent-unchanged')
|
|
195
|
+
return this.invokedFailure(active, 'candidate-branch-update-not-observed');
|
|
196
|
+
if (publication.value.kind === 'branch-diverged')
|
|
197
|
+
return this.persistRetainedCommitIntentTerminal(active, 'candidate-branch-diverged');
|
|
198
|
+
commitSha = publication.value.sha;
|
|
199
|
+
const normalized = await candidate.normalizeSharedIndex({ worktreePath, expectedHeadSha: commitSha });
|
|
200
|
+
if (normalized.kind === 'failed')
|
|
201
|
+
return this.mapCandidateFailure(active, normalized.code);
|
|
202
|
+
const residual = await this.dependencies.git.listChangedFilesIgnoringUntrackedRoot(worktreePath, config.proof.artifactDir);
|
|
203
|
+
if (residual.length > 0)
|
|
204
|
+
return this.persistRetainedCommitIntentTerminal(active, 'candidate-residual-worktree-drift');
|
|
205
|
+
try {
|
|
206
|
+
active = await this.persist(active, {
|
|
207
|
+
intent: { kind: 'push', branch: branchName, sha: commitSha },
|
|
208
|
+
changeBindingVersion: undefined,
|
|
209
|
+
candidateBinding: undefined,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
throw new PostEffectStateError(active);
|
|
214
|
+
}
|
|
215
|
+
const released = await candidate.releasePin({ binding: candidateBinding, expectedPinnedCommitSha: candidateBinding.candidateCommitSha });
|
|
216
|
+
if (released.kind === 'failed') {
|
|
217
|
+
return this.invokedFailure(active, 'candidate-pin-release-unknown', 'The publication commit is confirmed; orphan reconciliation will retry exact pin cleanup.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (active.record.intent?.kind === 'commit' || !active.record.intent) {
|
|
126
221
|
if (!active.record.intent) {
|
|
127
222
|
if (commitSha === baseSha) {
|
|
128
223
|
active = await this.persist(active, {
|
|
@@ -206,6 +301,7 @@ export class RunIssue {
|
|
|
206
301
|
if (!await this.authorized(active, config))
|
|
207
302
|
return await this.revoked(active);
|
|
208
303
|
try {
|
|
304
|
+
await this.markV3ExternalEffect(active);
|
|
209
305
|
await this.dependencies.pullRequests.createDraft({
|
|
210
306
|
title: `Implement #${issueNumber}: ${issue.title}`,
|
|
211
307
|
body: prBody,
|
|
@@ -246,6 +342,7 @@ export class RunIssue {
|
|
|
246
342
|
if (!await this.authorized(active, config))
|
|
247
343
|
return await this.revoked(active);
|
|
248
344
|
try {
|
|
345
|
+
await this.markV3ExternalEffect(active);
|
|
249
346
|
await this.dependencies.issues.postComment(issueNumber, handoffBody);
|
|
250
347
|
}
|
|
251
348
|
catch {
|
|
@@ -272,6 +369,7 @@ export class RunIssue {
|
|
|
272
369
|
if (!await this.authorized(active, config))
|
|
273
370
|
return await this.revoked(active);
|
|
274
371
|
try {
|
|
372
|
+
await this.markV3ExternalEffect(active);
|
|
275
373
|
await this.dependencies.issues.setLabels(issueNumber, terminalLabels);
|
|
276
374
|
}
|
|
277
375
|
catch {
|
|
@@ -290,7 +388,7 @@ export class RunIssue {
|
|
|
290
388
|
...(active.record.routeReceipt?.route === 'direct' ? {
|
|
291
389
|
reviewFeedback: bootstrapReviewFeedback(active.record.reviewFeedback ?? {
|
|
292
390
|
version: 1, phase: 'bootstrap-required', consumedSourceIds: [], previousPublishedHeadSha: null,
|
|
293
|
-
repairRound: 0, activeBatch: null,
|
|
391
|
+
repairRound: 0, activeBatch: null, history: [],
|
|
294
392
|
verifiedReceipt: null, terminal: null,
|
|
295
393
|
}, commitSha, []),
|
|
296
394
|
} : {}),
|
|
@@ -323,7 +421,105 @@ export class RunIssue {
|
|
|
323
421
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-update-baseline-diverged');
|
|
324
422
|
}
|
|
325
423
|
}
|
|
326
|
-
|
|
424
|
+
const candidateBinding = active.record.candidateBinding;
|
|
425
|
+
if (candidateBinding) {
|
|
426
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
427
|
+
if (!candidate)
|
|
428
|
+
return this.persistRetainedCommitIntentTerminal(active, 'candidate-git-v2-required');
|
|
429
|
+
if (!active.record.intent) {
|
|
430
|
+
const recaptured = await candidate.captureAndPin({
|
|
431
|
+
worktreePath: active.record.worktreePath,
|
|
432
|
+
expectedHeadSha: oldHead,
|
|
433
|
+
runId: active.record.runId,
|
|
434
|
+
boundary: {
|
|
435
|
+
kind: 'review-feedback', batchId: batch.batchId,
|
|
436
|
+
repairRound: feedback.repairRound,
|
|
437
|
+
},
|
|
438
|
+
artifactDir: config.proof.artifactDir,
|
|
439
|
+
});
|
|
440
|
+
if (recaptured.kind === 'failed')
|
|
441
|
+
return this.mapCandidateFailure(active, recaptured.code);
|
|
442
|
+
if (recaptured.value.bindingId !== candidateBinding.bindingId
|
|
443
|
+
|| recaptured.value.candidateTreeSha !== candidateBinding.candidateTreeSha
|
|
444
|
+
|| !sameStrings(recaptured.value.canonicalChangedFiles, candidateBinding.canonicalChangedFiles)) {
|
|
445
|
+
if (recaptured.value.bindingId !== candidateBinding.bindingId) {
|
|
446
|
+
await candidate.releasePin({ binding: recaptured.value, expectedPinnedCommitSha: recaptured.value.candidateCommitSha });
|
|
447
|
+
}
|
|
448
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
449
|
+
if ('status' in released)
|
|
450
|
+
return released;
|
|
451
|
+
if (feedback.repairRound >= 3) {
|
|
452
|
+
return this.blockReviewFeedback(released.active, 'exhausted', 'review-feedback-candidate-worktree-drift');
|
|
453
|
+
}
|
|
454
|
+
const reopened = await this.persist(released.active, {
|
|
455
|
+
lifecycle: 'implementing',
|
|
456
|
+
reviewFeedback: reserveNextReviewFeedbackRound(feedback),
|
|
457
|
+
reworkFindings: ['The issue worktree changed after proof; rebuild and revalidate the review-feedback candidate.'],
|
|
458
|
+
checks: [], checkedChangeSha256: undefined, proofId: undefined, proofReceipt: undefined,
|
|
459
|
+
});
|
|
460
|
+
return this.invokedFailure(reopened, 'review-feedback-candidate-worktree-drift', 'The next review-feedback repair round is ready.');
|
|
461
|
+
}
|
|
462
|
+
active = await this.persist(active, { intent: {
|
|
463
|
+
kind: 'review-update-commit', batchId: batch.batchId, parentSha: oldHead,
|
|
464
|
+
treeSha: candidateBinding.candidateTreeSha, message, candidateRef: candidateBinding.candidateRef,
|
|
465
|
+
} });
|
|
466
|
+
}
|
|
467
|
+
const intent = active.record.intent;
|
|
468
|
+
if (intent?.kind !== 'review-update-commit' || intent.batchId !== batch.batchId
|
|
469
|
+
|| intent.parentSha !== oldHead || intent.treeSha !== candidateBinding.candidateTreeSha
|
|
470
|
+
|| intent.message !== message || intent.candidateRef !== candidateBinding.candidateRef) {
|
|
471
|
+
return this.persistRetainedCommitIntentTerminal(active, 'review-feedback-candidate-intent-diverged');
|
|
472
|
+
}
|
|
473
|
+
if (!await this.authorized(active, config))
|
|
474
|
+
return this.persistRetainedCommitIntentTerminal(active, 'review-feedback-publication-authority-revoked');
|
|
475
|
+
const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
|
|
476
|
+
const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-precommit-revalidation-failed');
|
|
477
|
+
if (validationFailure)
|
|
478
|
+
return validationFailure;
|
|
479
|
+
await this.markV3ExternalEffect(active);
|
|
480
|
+
const publication = await candidate.createOrObserveCommit({
|
|
481
|
+
worktreePath: active.record.worktreePath,
|
|
482
|
+
branchName: active.record.branchName,
|
|
483
|
+
parentSha: intent.parentSha,
|
|
484
|
+
treeSha: intent.treeSha,
|
|
485
|
+
message: intent.message,
|
|
486
|
+
candidateRef: intent.candidateRef,
|
|
487
|
+
});
|
|
488
|
+
if (publication.kind === 'failed') {
|
|
489
|
+
return publication.code === 'candidate-ref-update-unknown'
|
|
490
|
+
? this.persistTerminal(active, { status: 'transport-failed', resumable: false }, publication.code, true)
|
|
491
|
+
: this.mapCandidateFailure(active, publication.code);
|
|
492
|
+
}
|
|
493
|
+
if (publication.value.kind === 'parent-unchanged')
|
|
494
|
+
return this.invokedFailure(active, 'review-feedback-candidate-branch-update-not-observed');
|
|
495
|
+
if (publication.value.kind === 'branch-diverged')
|
|
496
|
+
return this.persistRetainedCommitIntentTerminal(active, 'candidate-branch-diverged');
|
|
497
|
+
head = publication.value.sha;
|
|
498
|
+
const normalized = await candidate.normalizeSharedIndex({ worktreePath: active.record.worktreePath, expectedHeadSha: head });
|
|
499
|
+
if (normalized.kind === 'failed')
|
|
500
|
+
return this.mapCandidateFailure(active, normalized.code);
|
|
501
|
+
if ((await this.dependencies.git.listChangedFilesIgnoringUntrackedRoot(active.record.worktreePath, config.proof.artifactDir)).length > 0) {
|
|
502
|
+
return this.persistRetainedCommitIntentTerminal(active, 'candidate-residual-worktree-drift');
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
active = await this.persist(active, {
|
|
506
|
+
intent: {
|
|
507
|
+
kind: 'review-update-push', batchId: batch.batchId, branch: active.record.branchName,
|
|
508
|
+
priorRemoteSha: oldHead, sha: head, treeSha: candidateBinding.candidateTreeSha,
|
|
509
|
+
},
|
|
510
|
+
changeBindingVersion: undefined,
|
|
511
|
+
candidateBinding: undefined,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
throw new PostEffectStateError(active);
|
|
516
|
+
}
|
|
517
|
+
const released = await candidate.releasePin({ binding: candidateBinding, expectedPinnedCommitSha: candidateBinding.candidateCommitSha });
|
|
518
|
+
if (released.kind === 'failed') {
|
|
519
|
+
return this.invokedFailure(active, 'candidate-pin-release-unknown', 'The review update commit is confirmed; orphan reconciliation will retry exact pin cleanup.');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
else if (!active.record.intent || active.record.intent.kind === 'review-update-commit') {
|
|
327
523
|
if (!active.record.intent) {
|
|
328
524
|
active = await this.persist(active, { intent: {
|
|
329
525
|
kind: 'review-update-commit', batchId: batch.batchId, parentSha: oldHead,
|
|
@@ -447,6 +643,7 @@ export class RunIssue {
|
|
|
447
643
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
448
644
|
}
|
|
449
645
|
try {
|
|
646
|
+
await this.markV3ExternalEffect(active);
|
|
450
647
|
await this.dependencies.pullRequests.postConversationComment(batch.pullRequest.number, body);
|
|
451
648
|
}
|
|
452
649
|
catch {
|
|
@@ -496,6 +693,7 @@ export class RunIssue {
|
|
|
496
693
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
497
694
|
}
|
|
498
695
|
try {
|
|
696
|
+
await this.markV3ExternalEffect(active);
|
|
499
697
|
await this.dependencies.issues.setLabels(issueNumber, finalLabels);
|
|
500
698
|
}
|
|
501
699
|
catch {
|
|
@@ -533,19 +731,31 @@ export class RunIssue {
|
|
|
533
731
|
}
|
|
534
732
|
return publicOutcome(outcome);
|
|
535
733
|
}
|
|
536
|
-
async continueReviewReady(starting, targetRoot, config) {
|
|
734
|
+
async continueReviewReady(starting, targetRoot, config, initialIssue) {
|
|
537
735
|
const terminal = starting.record.terminalOutcome;
|
|
538
736
|
const feedback = starting.record.reviewFeedback;
|
|
539
737
|
if (!terminal || terminal.status !== 'review-ready' || !feedback || !this.dependencies.reviewFeedback
|
|
540
738
|
|| starting.record.routeReceipt?.route !== 'direct' || starting.record.directReview?.status !== 'clear') {
|
|
541
739
|
return { result: publicOutcome(terminal) };
|
|
542
740
|
}
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
741
|
+
const reviewLabels = [config.github.labels.review.name];
|
|
742
|
+
const initialAuthority = this.reviewReadyIssueAuthority(initialIssue, starting.record);
|
|
743
|
+
if (!initialAuthority)
|
|
744
|
+
return { result: await this.invokedFailure(starting, 'review-feedback-issue-observation-incomplete') };
|
|
745
|
+
if (!sameStrings(initialAuthority.labels, reviewLabels))
|
|
746
|
+
return { result: publicOutcome(terminal) };
|
|
747
|
+
let pullRequest;
|
|
748
|
+
try {
|
|
749
|
+
pullRequest = await this.dependencies.pullRequests.findOpen({
|
|
750
|
+
headBranch: starting.record.branchName,
|
|
751
|
+
baseBranch: config.github.baseBranch,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
return { result: await this.invokedFailure(starting, 'review-feedback-pr-observation-incomplete') };
|
|
756
|
+
}
|
|
547
757
|
if (!pullRequest?.number || !pullRequest.nodeId || !pullRequest.headSha) {
|
|
548
|
-
return { result: await this.
|
|
758
|
+
return { result: await this.invokedFailure(starting, 'review-feedback-pr-observation-incomplete') };
|
|
549
759
|
}
|
|
550
760
|
const expectedHeadSha = feedback.phase === 'bootstrap-required'
|
|
551
761
|
? pullRequest.headSha
|
|
@@ -562,12 +772,33 @@ export class RunIssue {
|
|
|
562
772
|
expectedBaseRefName: config.github.baseBranch,
|
|
563
773
|
marker: `<!-- codex-orchestrator:run:${starting.record.runId}:pr -->`,
|
|
564
774
|
consumedSourceIds: feedback.consumedSourceIds,
|
|
775
|
+
restPullRequest: {
|
|
776
|
+
number: pullRequest.number, nodeId: pullRequest.nodeId, headSha: pullRequest.headSha, body: pullRequest.body,
|
|
777
|
+
},
|
|
565
778
|
});
|
|
566
779
|
if (observed.status === 'retryable')
|
|
567
780
|
return { result: await this.invokedFailure(starting, 'review-feedback-observation-retryable') };
|
|
568
781
|
if (observed.status === 'blocked') {
|
|
569
782
|
return { result: await this.reviewReadyObservationBlocked(starting, 'review-feedback-observation-blocked') };
|
|
570
783
|
}
|
|
784
|
+
const willPersistFeedback = feedback.phase === 'bootstrap-required'
|
|
785
|
+
|| (feedback.phase === 'idle' && observed.status === 'frozen');
|
|
786
|
+
if (willPersistFeedback) {
|
|
787
|
+
let currentIssue;
|
|
788
|
+
try {
|
|
789
|
+
currentIssue = await this.readIssue(starting.record.issueNumber);
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
return { result: await this.invokedFailure(starting, 'review-feedback-issue-observation-incomplete') };
|
|
793
|
+
}
|
|
794
|
+
const currentAuthority = this.reviewReadyIssueAuthority(currentIssue, starting.record);
|
|
795
|
+
if (!currentAuthority || canonicalJson(currentAuthority) !== canonicalJson(initialAuthority)) {
|
|
796
|
+
return { result: await this.invokedFailure(starting, 'review-feedback-issue-observation-torn') };
|
|
797
|
+
}
|
|
798
|
+
if (currentAuthority.state !== 'OPEN' || !currentAuthority.trustedClaim) {
|
|
799
|
+
return { result: await this.reviewReadyObservationBlocked(starting, 'review-feedback-claim-authority-diverged') };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
571
802
|
if (feedback.phase === 'bootstrap-required') {
|
|
572
803
|
const sourceIds = observed.status === 'frozen'
|
|
573
804
|
? observed.batch.sources.map((source) => source.sourceId)
|
|
@@ -579,9 +810,6 @@ export class RunIssue {
|
|
|
579
810
|
}
|
|
580
811
|
if (feedback.phase !== 'idle' || observed.status === 'none')
|
|
581
812
|
return { result: publicOutcome(terminal) };
|
|
582
|
-
if (!await this.authorizedForExactLabels(starting, [config.github.labels.review.name])) {
|
|
583
|
-
return { result: publicOutcome(terminal) };
|
|
584
|
-
}
|
|
585
813
|
const projected = projectReviewFeedbackBatch(observed.batch, starting.record.directReview.targetRevision);
|
|
586
814
|
const repairReview = beginDirectReviewRepair(starting.record.directReview, projected.repairFindings);
|
|
587
815
|
let active = await this.persist(starting, {
|
|
@@ -589,11 +817,10 @@ export class RunIssue {
|
|
|
589
817
|
reviewFeedback: activateReviewFeedback(feedback, observed.batch),
|
|
590
818
|
directReview: {
|
|
591
819
|
...repairReview,
|
|
592
|
-
review: { ...repairReview.review, reportRepairs: 0
|
|
820
|
+
review: { ...repairReview.review, reportRepairs: 0 },
|
|
593
821
|
},
|
|
594
822
|
reworkFindings: projected.repairFindings.map((finding) => finding.summary),
|
|
595
823
|
reportRepairs: 0,
|
|
596
|
-
transportRetries: 0,
|
|
597
824
|
checks: [],
|
|
598
825
|
checkedChangeSha256: undefined,
|
|
599
826
|
proofId: undefined,
|
|
@@ -609,6 +836,11 @@ export class RunIssue {
|
|
|
609
836
|
});
|
|
610
837
|
return this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
611
838
|
}
|
|
839
|
+
reviewReadyIssueAuthority(issue, record) {
|
|
840
|
+
if (!issue)
|
|
841
|
+
return undefined;
|
|
842
|
+
return { state: issue.state, labels: sortedUnique(issue.labels), trustedClaim: this.hasTrustedClaim(issue, record) };
|
|
843
|
+
}
|
|
612
844
|
async resumeActivatedReviewFeedback(starting, targetRoot, config) {
|
|
613
845
|
let active = starting;
|
|
614
846
|
const feedback = active.record.reviewFeedback;
|
|
@@ -642,6 +874,7 @@ export class RunIssue {
|
|
|
642
874
|
return { result: validationFailure };
|
|
643
875
|
if (labelsAreReview) {
|
|
644
876
|
try {
|
|
877
|
+
await this.markV3ExternalEffect(active);
|
|
645
878
|
await this.dependencies.issues.setLabels(active.record.issueNumber, runningLabels);
|
|
646
879
|
}
|
|
647
880
|
catch {
|
|
@@ -707,7 +940,6 @@ export class RunIssue {
|
|
|
707
940
|
async runIssue(input) {
|
|
708
941
|
let owner;
|
|
709
942
|
let active;
|
|
710
|
-
let recoveredFeedbackImplementation;
|
|
711
943
|
try {
|
|
712
944
|
assertPositiveInteger(input.issueNumber, 'issueNumber');
|
|
713
945
|
const targetRoot = resolve(input.targetRoot);
|
|
@@ -734,12 +966,16 @@ export class RunIssue {
|
|
|
734
966
|
return await this.preClaimInternal('state-write-failed', input.issueNumber);
|
|
735
967
|
}
|
|
736
968
|
const confirmedConfig = await this.readStrictConfig(targetRoot);
|
|
737
|
-
if (!confirmedConfig.bytes.equals(initialConfig.bytes)
|
|
738
|
-
|| canonicalRepository !== `${confirmedConfig.config.github.owner.toLowerCase()}/${confirmedConfig.config.github.repo.toLowerCase()}`) {
|
|
969
|
+
if (!confirmedConfig.bytes.equals(initialConfig.bytes) || canonicalRepository !== `${confirmedConfig.config.github.owner.toLowerCase()}/${confirmedConfig.config.github.repo.toLowerCase()}`) {
|
|
739
970
|
return await this.preClaimInternal('config-changed-during-owner-acquire', input.issueNumber);
|
|
740
971
|
}
|
|
741
972
|
const config = confirmedConfig.config;
|
|
742
|
-
|
|
973
|
+
const persisted = await this.dependencies.runRecords.read();
|
|
974
|
+
const matchingRuns = persisted.runs.filter((run) => run.issueNumber === input.issueNumber && run.canonicalRepository === canonicalRepository);
|
|
975
|
+
if (matchingRuns.length > 1)
|
|
976
|
+
return await this.preClaimInternal('ambiguous-run-state', input.issueNumber);
|
|
977
|
+
const existing = matchingRuns[0];
|
|
978
|
+
if (this.signal.aborted && existing?.lifecycle !== 'proving')
|
|
743
979
|
return await this.preClaimCancelled(input.issueNumber);
|
|
744
980
|
let issue;
|
|
745
981
|
try {
|
|
@@ -748,11 +984,24 @@ export class RunIssue {
|
|
|
748
984
|
catch {
|
|
749
985
|
return await this.preClaimTransport(input.issueNumber);
|
|
750
986
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
987
|
+
if (this.dependencies.git.candidateV2?.reconcileOrphans) {
|
|
988
|
+
const reconciled = await this.dependencies.git.candidateV2.reconcileOrphans({
|
|
989
|
+
repositoryRoot: targetRoot,
|
|
990
|
+
workspaceRoot: resolve(targetRoot, config.runner.workspaceRoot),
|
|
991
|
+
activeCandidateRefs: persisted.runs.flatMap((run) => run.candidateBinding ? [run.candidateBinding.candidateRef] : []),
|
|
992
|
+
pendingCandidates: persisted.runs.flatMap((run) => {
|
|
993
|
+
const boundary = pendingCandidateBoundary(run);
|
|
994
|
+
return !run.terminalOutcome && !run.candidateBinding && boundary
|
|
995
|
+
? [{ runId: run.runId, worktreePath: run.worktreePath, expectedHeadSha: expectedImplementationHead(run), boundary, artifactDir: config.proof.artifactDir }]
|
|
996
|
+
: [];
|
|
997
|
+
}),
|
|
998
|
+
activeExecutions: persisted.runs.flatMap((run) => run.executionLease
|
|
999
|
+
? [{ path: run.executionLease.path, candidateCommitSha: run.executionLease.candidateCommitSha }]
|
|
1000
|
+
: []),
|
|
1001
|
+
});
|
|
1002
|
+
if (reconciled.kind === 'failed')
|
|
1003
|
+
return this.preClaimTransport(input.issueNumber);
|
|
1004
|
+
}
|
|
756
1005
|
let issueSnapshot;
|
|
757
1006
|
let frozenCriteria;
|
|
758
1007
|
let runId;
|
|
@@ -787,15 +1036,21 @@ export class RunIssue {
|
|
|
787
1036
|
&& blockedLabelProjection(issue.labels, config).status !== 'settled') {
|
|
788
1037
|
return await this.reconcilePersistedBlockedTerminal(active, issue, existing.terminalOutcome);
|
|
789
1038
|
}
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1039
|
+
if (active.record.terminalOutcome?.status === 'transport-failed'
|
|
1040
|
+
&& !active.record.terminalOutcome.resumable
|
|
1041
|
+
&& active.record.candidateBinding
|
|
1042
|
+
&& (active.record.intent?.kind === 'commit' || active.record.intent?.kind === 'review-update-commit')) {
|
|
1043
|
+
const reconciled = await this.reconcileUnknownCandidatePublication(active);
|
|
1044
|
+
if ('status' in reconciled)
|
|
1045
|
+
return reconciled;
|
|
1046
|
+
active = reconciled.active;
|
|
1047
|
+
}
|
|
1048
|
+
if (active.record.terminalOutcome) {
|
|
1049
|
+
if (active.record.terminalOutcome.status !== 'review-ready') {
|
|
1050
|
+
return publicOutcome(active.record.terminalOutcome);
|
|
796
1051
|
}
|
|
797
1052
|
else {
|
|
798
|
-
const continuation = await this.continueReviewReady(active, targetRoot, config);
|
|
1053
|
+
const continuation = await this.continueReviewReady(active, targetRoot, config, issue);
|
|
799
1054
|
if ('result' in continuation)
|
|
800
1055
|
return continuation.result;
|
|
801
1056
|
active = continuation.active;
|
|
@@ -803,18 +1058,42 @@ export class RunIssue {
|
|
|
803
1058
|
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
804
1059
|
}
|
|
805
1060
|
}
|
|
1061
|
+
const canonicalReviewRecovery = active.record.executionLease?.operation === 'direct-review'
|
|
1062
|
+
&& active.record.reportInvocation?.operation === 'code-review';
|
|
1063
|
+
const canonicalProofRecovery = active.record.lifecycle === 'proving'
|
|
1064
|
+
&& active.record.executionLease?.operation === 'acceptance-proof';
|
|
1065
|
+
if (active.record.executionLease && !canonicalReviewRecovery && !canonicalProofRecovery) {
|
|
1066
|
+
const reconciled = await this.reconcilePersistedCandidateExecution(active, config);
|
|
1067
|
+
if ('status' in reconciled)
|
|
1068
|
+
return reconciled;
|
|
1069
|
+
active = reconciled.active;
|
|
1070
|
+
}
|
|
1071
|
+
if (active.state.version === 2 && active.record.lifecycle === 'proving'
|
|
1072
|
+
&& active.record.routeReceipt?.route === 'direct') {
|
|
1073
|
+
const cutover = await this.cutoverLegacyImmutableBoundary(active, config, frozenCriteria);
|
|
1074
|
+
if ('status' in cutover)
|
|
1075
|
+
return cutover;
|
|
1076
|
+
active = cutover.active;
|
|
1077
|
+
}
|
|
1078
|
+
else if (active.state.version === 2 && active.record.lifecycle === 'checking'
|
|
1079
|
+
&& active.record.routeReceipt?.route === 'direct') {
|
|
1080
|
+
const cutover = await this.cutoverLegacyImmutableBoundary(active, config, frozenCriteria);
|
|
1081
|
+
if ('status' in cutover)
|
|
1082
|
+
return cutover;
|
|
1083
|
+
active = cutover.active;
|
|
1084
|
+
}
|
|
806
1085
|
if (active.record.reviewFeedback?.phase === 'frozen') {
|
|
807
1086
|
const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
808
1087
|
if ('result' in activation)
|
|
809
1088
|
return activation.result;
|
|
810
1089
|
active = activation.active;
|
|
811
1090
|
}
|
|
812
|
-
if (
|
|
1091
|
+
if (active.record.lifecycle === 'publishing') {
|
|
813
1092
|
return active.record.reviewFeedback?.activeBatch
|
|
814
1093
|
? await this.updateExistingPullRequest(active, config, input.issueNumber)
|
|
815
1094
|
: await this.publish(active, config, issueSnapshot, input.issueNumber);
|
|
816
1095
|
}
|
|
817
|
-
if (
|
|
1096
|
+
if (active.record.lifecycle === 'claimed') {
|
|
818
1097
|
const claim = await this.reconcileClaim(active, config);
|
|
819
1098
|
if ('result' in claim)
|
|
820
1099
|
return claim.result;
|
|
@@ -841,19 +1120,7 @@ export class RunIssue {
|
|
|
841
1120
|
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
842
1121
|
}
|
|
843
1122
|
else {
|
|
844
|
-
if (
|
|
845
|
-
const process = existing.process;
|
|
846
|
-
if (!process)
|
|
847
|
-
return await this.publicationDiverged(active, 'safe-halt-process-missing');
|
|
848
|
-
try {
|
|
849
|
-
await this.dependencies.waitForReviewProcessAbsence(process.processGroupId);
|
|
850
|
-
}
|
|
851
|
-
catch {
|
|
852
|
-
return await this.invokedFailure(active, 'safe-halt-process-absence-unconfirmed');
|
|
853
|
-
}
|
|
854
|
-
active = await this.persist(active, { lifecycle: process.resumeLifecycle, process: undefined });
|
|
855
|
-
}
|
|
856
|
-
if (existing.lifecycle === 'waiting-human') {
|
|
1123
|
+
if (active.record.lifecycle === 'waiting-human') {
|
|
857
1124
|
const waiting = await this.continueWaitingHuman(active);
|
|
858
1125
|
if ('result' in waiting)
|
|
859
1126
|
return waiting.result;
|
|
@@ -861,46 +1128,15 @@ export class RunIssue {
|
|
|
861
1128
|
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
862
1129
|
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
863
1130
|
}
|
|
864
|
-
if (
|
|
1131
|
+
if (active.record.lifecycle === 'spec-authoring') {
|
|
865
1132
|
if (!await this.authorized(active, config))
|
|
866
1133
|
return await this.revoked(active);
|
|
867
1134
|
return await this.continueSpecRequired(active);
|
|
868
1135
|
}
|
|
869
|
-
const feedbackInvocation = active.record.reviewFeedback?.implementationInvocation;
|
|
870
|
-
if (feedbackInvocation?.phase === 'prepared') {
|
|
871
|
-
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-launch-ambiguous');
|
|
872
|
-
}
|
|
873
|
-
if (feedbackInvocation?.phase === 'launched') {
|
|
874
|
-
try {
|
|
875
|
-
await this.dependencies.waitForReviewProcessAbsence(feedbackInvocation.processGroupId);
|
|
876
|
-
}
|
|
877
|
-
catch {
|
|
878
|
-
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-process-absence-unconfirmed');
|
|
879
|
-
}
|
|
880
|
-
if (!this.dependencies.implementationAgent.recover) {
|
|
881
|
-
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-unavailable');
|
|
882
|
-
}
|
|
883
|
-
try {
|
|
884
|
-
recoveredFeedbackImplementation = await this.dependencies.implementationAgent.recover({
|
|
885
|
-
runId: active.record.runId,
|
|
886
|
-
canonicalRepository: active.record.canonicalRepository,
|
|
887
|
-
invocation: feedbackInvocation,
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
catch {
|
|
891
|
-
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-failed');
|
|
892
|
-
}
|
|
893
|
-
if (!recoveredFeedbackImplementation) {
|
|
894
|
-
const snapshot = await this.dependencies.git.snapshot(active.record.worktreePath);
|
|
895
|
-
if (!sameFreshness(snapshot, feedbackInvocation.baseline)) {
|
|
896
|
-
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
1136
|
if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
901
1137
|
return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
|
|
902
1138
|
}
|
|
903
|
-
if (!await this.authorized(active, config))
|
|
1139
|
+
if (active.record.lifecycle !== 'proving' && !await this.authorized(active, config))
|
|
904
1140
|
return await this.revoked(active);
|
|
905
1141
|
if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
|
|
906
1142
|
if (!active.record.routeExecution || !active.record.routeReceipt)
|
|
@@ -911,12 +1147,13 @@ export class RunIssue {
|
|
|
911
1147
|
&& active.record.directReview?.status === 'clear';
|
|
912
1148
|
const proofRecovery = active.record.lifecycle === 'proving'
|
|
913
1149
|
&& active.record.directReview?.status === 'clear';
|
|
914
|
-
const
|
|
915
|
-
&& active.record.directReview?.
|
|
1150
|
+
const directReviewRepair = active.record.lifecycle === 'implementing'
|
|
1151
|
+
&& active.record.directReview?.status === 'active'
|
|
1152
|
+
&& active.record.directReview.stage === 'review-repair';
|
|
1153
|
+
const mutableRecovery = active.record.lifecycle === 'implementing' && active.record.mutableInvocation !== undefined;
|
|
916
1154
|
const qualificationRecovery = active.record.lifecycle === 'implementing'
|
|
917
|
-
&& !active.record.directReview
|
|
918
|
-
|
|
919
|
-
if (!reviewRecovery && !checkRecovery && !proofRecovery && !feedbackRepair && !qualificationRecovery) {
|
|
1155
|
+
&& !active.record.directReview;
|
|
1156
|
+
if (!reviewRecovery && !checkRecovery && !proofRecovery && !directReviewRepair && !qualificationRecovery && !mutableRecovery) {
|
|
920
1157
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
921
1158
|
return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
922
1159
|
}
|
|
@@ -937,7 +1174,12 @@ export class RunIssue {
|
|
|
937
1174
|
assertUuid(runId);
|
|
938
1175
|
branchName = `codex/issue-${input.issueNumber}`;
|
|
939
1176
|
worktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${input.issueNumber}`);
|
|
940
|
-
|
|
1177
|
+
try {
|
|
1178
|
+
baseSha = await this.dependencies.git.getBaseSha({ targetRoot, baseBranch: config.github.baseBranch });
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
return await this.preClaimTransport(input.issueNumber, 'base-refresh-failed', 'The configured remote base branch could not be refreshed before claim.');
|
|
1182
|
+
}
|
|
941
1183
|
assertGitSha(baseSha, 'baseSha');
|
|
942
1184
|
const claimBody = claimComment(runId, input.issueNumber, branchName);
|
|
943
1185
|
active = await this.createRun({
|
|
@@ -972,28 +1214,51 @@ export class RunIssue {
|
|
|
972
1214
|
if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
|
|
973
1215
|
return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
|
|
974
1216
|
}
|
|
1217
|
+
if (active.record.routeReceipt?.route === 'direct' && !this.dependencies.git.candidateV2) {
|
|
1218
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'candidate-git-v2-required');
|
|
1219
|
+
}
|
|
975
1220
|
if (active.record.lifecycle === 'implementing' && active.record.routeReceipt?.route === 'direct'
|
|
976
|
-
&& !active.record.directReview
|
|
1221
|
+
&& !active.record.directReview
|
|
1222
|
+
&& (!active.record.mutableInvocation || active.record.mutableInvocation.operation === 'qualification-repair')) {
|
|
977
1223
|
const qualification = await this.qualifyChecks(active, config, issueSnapshot, frozenCriteria);
|
|
978
1224
|
if ('status' in qualification)
|
|
979
1225
|
return qualification;
|
|
980
1226
|
active = qualification.active;
|
|
981
1227
|
}
|
|
982
1228
|
attemptLoop: while (true) {
|
|
983
|
-
if (!await this.authorized(active, config))
|
|
1229
|
+
if (active.record.lifecycle !== 'proving' && !await this.authorized(active, config))
|
|
984
1230
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
985
|
-
|
|
986
|
-
if (this.signal.aborted)
|
|
1231
|
+
if (this.signal.aborted && active.record.lifecycle !== 'proving')
|
|
987
1232
|
return await this.terminal(active, { status: 'cancelled' });
|
|
988
1233
|
let resumeAtChecks = (active.record.lifecycle === 'checking' || active.record.lifecycle === 'proving')
|
|
989
1234
|
&& active.record.directReview?.status === 'clear';
|
|
990
1235
|
if (active.record.lifecycle === 'implementing'
|
|
991
1236
|
&& active.record.directReview?.status === 'active'
|
|
992
1237
|
&& (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure')) {
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1238
|
+
const recoveryBoundary = active.record.reviewFeedback?.activeBatch
|
|
1239
|
+
? {
|
|
1240
|
+
kind: 'review-feedback',
|
|
1241
|
+
batchId: active.record.reviewFeedback.activeBatch.batchId,
|
|
1242
|
+
repairRound: active.record.reviewFeedback.repairRound,
|
|
1243
|
+
}
|
|
1244
|
+
: { kind: 'implementation-cycle', cycle: active.record.cycle };
|
|
1245
|
+
const captured = await this.ensureCandidateBinding(active, config, recoveryBoundary);
|
|
1246
|
+
if ('status' in captured)
|
|
1247
|
+
return captured;
|
|
1248
|
+
active = captured.active;
|
|
1249
|
+
const binding = active.record.candidateBinding;
|
|
1250
|
+
const candidateTargetFingerprint = directReviewCandidateTargetFingerprint({
|
|
1251
|
+
binding,
|
|
1252
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
1253
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
1254
|
+
cycle: active.record.cycle,
|
|
1255
|
+
frozenCriteria,
|
|
1256
|
+
});
|
|
1257
|
+
if (active.record.directReview.targetFingerprint !== candidateTargetFingerprint) {
|
|
1258
|
+
active = await this.persist(active, {
|
|
1259
|
+
directReview: { ...active.record.directReview, targetFingerprint: candidateTargetFingerprint },
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
997
1262
|
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
|
|
998
1263
|
if ('status' in reviewed)
|
|
999
1264
|
return reviewed;
|
|
@@ -1010,90 +1275,54 @@ export class RunIssue {
|
|
|
1010
1275
|
const feedbackProjection = feedbackBatch
|
|
1011
1276
|
? projectReviewFeedbackBatch(feedbackBatch, active.record.directReview.targetRevision)
|
|
1012
1277
|
: undefined;
|
|
1278
|
+
let feedbackImplementationLaunchFailure;
|
|
1013
1279
|
const implementationLaunch = feedbackBatch ? {
|
|
1014
1280
|
reviewFeedbackRound: active.record.reviewFeedback.repairRound,
|
|
1015
1281
|
reviewFeedback: feedbackProjection.workerFeedback,
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
},
|
|
1023
|
-
});
|
|
1024
|
-
},
|
|
1025
|
-
onLaunched: async (invocation) => {
|
|
1026
|
-
const currentActive = active;
|
|
1027
|
-
const current = currentActive.record.reviewFeedback?.implementationInvocation;
|
|
1028
|
-
if (!current || current.phase !== 'prepared' || current.attemptId !== invocation.attemptId)
|
|
1029
|
-
throw new Error('review feedback implementation launch mismatch');
|
|
1030
|
-
active = await this.persist(currentActive, {
|
|
1031
|
-
reviewFeedback: {
|
|
1032
|
-
...structuredClone(currentActive.record.reviewFeedback),
|
|
1033
|
-
implementationInvocation: { ...current, phase: 'launched', pid: invocation.pid, processGroupId: invocation.processGroupId, launchedAt: invocation.launchedAt },
|
|
1034
|
-
},
|
|
1035
|
-
});
|
|
1036
|
-
},
|
|
1037
|
-
} : {
|
|
1038
|
-
onLaunched: async () => {
|
|
1039
|
-
const currentActive = active;
|
|
1040
|
-
const qualification = currentActive.record.checkQualification;
|
|
1041
|
-
if (!qualification)
|
|
1042
|
-
throw new Error('implementation launch requires check qualification');
|
|
1043
|
-
active = await this.persist(currentActive, {
|
|
1044
|
-
checkQualification: { ...qualification, implementationStarted: true },
|
|
1045
|
-
});
|
|
1282
|
+
beforeLaunch: async () => {
|
|
1283
|
+
const failure = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1284
|
+
if (failure) {
|
|
1285
|
+
feedbackImplementationLaunchFailure = failure;
|
|
1286
|
+
throw new Error('review feedback implementation launch authorization changed');
|
|
1287
|
+
}
|
|
1046
1288
|
},
|
|
1047
|
-
};
|
|
1289
|
+
} : {};
|
|
1048
1290
|
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
|
|
1049
|
-
|
|
1050
|
-
|
|
1291
|
+
const phaseFacts = [deniedPathsBaseline, sha256(canonicalJson(resolveIssueCheckPolicy(active.record.issueSnapshot.body, config.checks).checks)), canonicalJson(active.record.checkQualification?.checks ?? [])];
|
|
1292
|
+
const recoveredContext = active.record.mutableInvocation?.context;
|
|
1293
|
+
const invocationState = this.mutableInvocationState(() => active, (next) => { active = next; });
|
|
1294
|
+
let implementation = await this.runImplementation({
|
|
1295
|
+
operation: feedbackBatch ? 'review-feedback-implementation' : 'implementation',
|
|
1051
1296
|
runId,
|
|
1052
1297
|
worktreePath,
|
|
1053
1298
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
1054
1299
|
frozenCriteria,
|
|
1055
1300
|
cycle: active.record.cycle,
|
|
1056
|
-
reworkFindings: active.record.reworkFindings,
|
|
1057
|
-
repairOnly: false,
|
|
1301
|
+
reworkFindings: recoveredContext?.reworkFindings ?? active.record.reworkFindings,
|
|
1302
|
+
repairOnly: recoveredContext?.repairOnly ?? false,
|
|
1058
1303
|
workflowGeneration: active.record.workflowGeneration,
|
|
1304
|
+
phaseFacts,
|
|
1305
|
+
invocationState,
|
|
1059
1306
|
...implementationLaunch,
|
|
1060
1307
|
});
|
|
1061
|
-
|
|
1308
|
+
if (feedbackImplementationLaunchFailure)
|
|
1309
|
+
return feedbackImplementationLaunchFailure;
|
|
1062
1310
|
if (implementation.kind === 'safe-halt') {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
process: {
|
|
1066
|
-
...implementation.process,
|
|
1067
|
-
purpose: 'implementation',
|
|
1068
|
-
resumeLifecycle: 'implementing',
|
|
1069
|
-
resumeReviewStage: null,
|
|
1070
|
-
},
|
|
1071
|
-
});
|
|
1072
|
-
while (true) {
|
|
1073
|
-
try {
|
|
1074
|
-
await implementation.waitForAbsence();
|
|
1075
|
-
break;
|
|
1076
|
-
}
|
|
1077
|
-
catch {
|
|
1078
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
1079
|
-
}
|
|
1311
|
+
if (implementation.code === 'report-repair-modified-worktree') {
|
|
1312
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, implementation.code);
|
|
1080
1313
|
}
|
|
1081
|
-
if (
|
|
1082
|
-
|
|
1314
|
+
if (implementation.code === 'mutable-operation-result-worktree-mismatch') {
|
|
1315
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1316
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, implementation.code);
|
|
1083
1317
|
}
|
|
1084
|
-
return await this.
|
|
1318
|
+
return await this.invokedFailure(active, implementation.code, 'Mutable implementation recovery remains fenced and requires a later bounded observation.');
|
|
1085
1319
|
}
|
|
1086
1320
|
if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
1321
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1087
1322
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
1088
1323
|
}
|
|
1089
|
-
if (implementation.kind === 'transport-failed'
|
|
1090
|
-
|
|
1091
|
-
if (invocation && !sameFreshness(await this.dependencies.git.snapshot(worktreePath), invocation.baseline)) {
|
|
1092
|
-
return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
|
|
1093
|
-
}
|
|
1094
|
-
active = await this.persist(active, { transportRetries: 1 });
|
|
1095
|
-
continue attemptLoop;
|
|
1096
|
-
}
|
|
1324
|
+
if (implementation.kind === 'transport-failed')
|
|
1325
|
+
return this.invokedFailure(active, implementation.code ?? 'implementation-agent-transport-failed', 'Mutable implementation did not produce recoverable output; the durable invocation remains available for recovery.');
|
|
1097
1326
|
if (implementation.kind !== 'completed')
|
|
1098
1327
|
return await this.mapImplementationFailure(active, implementation);
|
|
1099
1328
|
let report;
|
|
@@ -1102,15 +1331,16 @@ export class RunIssue {
|
|
|
1102
1331
|
}
|
|
1103
1332
|
catch {
|
|
1104
1333
|
if (active.record.reportRepairs >= 1) {
|
|
1334
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1105
1335
|
return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
|
|
1106
1336
|
}
|
|
1107
1337
|
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
1108
|
-
active = await this.persist(active, { reportRepairs: 1 });
|
|
1338
|
+
active = await this.persist(active, { reportRepairs: 1, mutableInvocation: undefined });
|
|
1109
1339
|
const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1110
1340
|
if (repairBlock)
|
|
1111
1341
|
return repairBlock;
|
|
1112
1342
|
implementation = await this.runImplementation({
|
|
1113
|
-
operation: 'implementation',
|
|
1343
|
+
operation: feedbackBatch ? 'review-feedback-implementation' : 'implementation',
|
|
1114
1344
|
runId,
|
|
1115
1345
|
worktreePath,
|
|
1116
1346
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -1119,60 +1349,56 @@ export class RunIssue {
|
|
|
1119
1349
|
reworkFindings: ['The previous implementation report did not match the generated schema.'],
|
|
1120
1350
|
repairOnly: true,
|
|
1121
1351
|
workflowGeneration: active.record.workflowGeneration,
|
|
1352
|
+
phaseFacts,
|
|
1353
|
+
invocationState,
|
|
1122
1354
|
...implementationLaunch,
|
|
1123
1355
|
});
|
|
1356
|
+
if (feedbackImplementationLaunchFailure)
|
|
1357
|
+
return feedbackImplementationLaunchFailure;
|
|
1124
1358
|
if (implementation.kind === 'safe-halt') {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
process: {
|
|
1128
|
-
...implementation.process,
|
|
1129
|
-
purpose: 'implementation',
|
|
1130
|
-
resumeLifecycle: 'implementing',
|
|
1131
|
-
resumeReviewStage: null,
|
|
1132
|
-
},
|
|
1133
|
-
});
|
|
1134
|
-
while (true) {
|
|
1135
|
-
try {
|
|
1136
|
-
await implementation.waitForAbsence();
|
|
1137
|
-
break;
|
|
1138
|
-
}
|
|
1139
|
-
catch {
|
|
1140
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
1141
|
-
}
|
|
1359
|
+
if (implementation.code === 'report-repair-modified-worktree') {
|
|
1360
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, implementation.code);
|
|
1142
1361
|
}
|
|
1143
|
-
return
|
|
1362
|
+
return this.invokedFailure(active, implementation.code);
|
|
1144
1363
|
}
|
|
1364
|
+
if (implementation.kind === 'transport-failed')
|
|
1365
|
+
return this.invokedFailure(active, implementation.code ?? 'implementation-agent-transport-failed');
|
|
1145
1366
|
if (implementation.kind !== 'completed')
|
|
1146
1367
|
return await this.mapImplementationFailure(active, implementation);
|
|
1147
1368
|
const afterRepair = await this.dependencies.git.snapshot(worktreePath);
|
|
1148
1369
|
if (!sameFreshness(repairBaseline, afterRepair)) {
|
|
1370
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1149
1371
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
|
|
1150
1372
|
}
|
|
1151
1373
|
try {
|
|
1152
1374
|
report = validateImplementationReport(implementation.report);
|
|
1153
1375
|
}
|
|
1154
1376
|
catch {
|
|
1377
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1155
1378
|
return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
|
|
1156
1379
|
}
|
|
1157
1380
|
}
|
|
1158
1381
|
if (report.status === 'external-block') {
|
|
1382
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1159
1383
|
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
1160
1384
|
}
|
|
1161
1385
|
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
1386
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1162
1387
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
1163
1388
|
}
|
|
1164
1389
|
const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
1165
1390
|
if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
|
|
1166
1391
|
if (changedFiles.length === 0 || active.record.reportRepairs >= 1) {
|
|
1392
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1167
1393
|
return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
|
|
1168
1394
|
}
|
|
1169
1395
|
const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
|
|
1170
|
-
active = await this.persist(active, { reportRepairs: 1 });
|
|
1396
|
+
active = await this.persist(active, { reportRepairs: 1, mutableInvocation: undefined });
|
|
1171
1397
|
const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
|
|
1172
1398
|
if (repairBlock)
|
|
1173
1399
|
return repairBlock;
|
|
1174
1400
|
implementation = await this.runImplementation({
|
|
1175
|
-
operation: 'implementation',
|
|
1401
|
+
operation: feedbackBatch ? 'review-feedback-implementation' : 'implementation',
|
|
1176
1402
|
runId,
|
|
1177
1403
|
worktreePath,
|
|
1178
1404
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
@@ -1181,52 +1407,64 @@ export class RunIssue {
|
|
|
1181
1407
|
reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
|
|
1182
1408
|
repairOnly: true,
|
|
1183
1409
|
workflowGeneration: active.record.workflowGeneration,
|
|
1410
|
+
phaseFacts,
|
|
1411
|
+
invocationState,
|
|
1184
1412
|
...implementationLaunch,
|
|
1185
1413
|
});
|
|
1414
|
+
if (feedbackImplementationLaunchFailure)
|
|
1415
|
+
return feedbackImplementationLaunchFailure;
|
|
1186
1416
|
if (implementation.kind === 'safe-halt') {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
process: {
|
|
1190
|
-
...implementation.process,
|
|
1191
|
-
purpose: 'implementation',
|
|
1192
|
-
resumeLifecycle: 'implementing',
|
|
1193
|
-
resumeReviewStage: null,
|
|
1194
|
-
},
|
|
1195
|
-
});
|
|
1196
|
-
while (true) {
|
|
1197
|
-
try {
|
|
1198
|
-
await implementation.waitForAbsence();
|
|
1199
|
-
break;
|
|
1200
|
-
}
|
|
1201
|
-
catch {
|
|
1202
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
1203
|
-
}
|
|
1417
|
+
if (implementation.code === 'report-repair-modified-worktree') {
|
|
1418
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, implementation.code);
|
|
1204
1419
|
}
|
|
1205
|
-
return
|
|
1420
|
+
return this.invokedFailure(active, implementation.code);
|
|
1206
1421
|
}
|
|
1422
|
+
if (implementation.kind === 'transport-failed')
|
|
1423
|
+
return this.invokedFailure(active, implementation.code ?? 'implementation-agent-transport-failed');
|
|
1207
1424
|
if (implementation.kind !== 'completed')
|
|
1208
1425
|
return await this.mapImplementationFailure(active, implementation);
|
|
1209
1426
|
const afterRepair = await this.dependencies.git.snapshot(worktreePath);
|
|
1210
1427
|
if (!sameFreshness(repairBaseline, afterRepair)) {
|
|
1428
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1211
1429
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
|
|
1212
1430
|
}
|
|
1213
1431
|
try {
|
|
1214
1432
|
report = validateImplementationReport(implementation.report);
|
|
1215
1433
|
}
|
|
1216
1434
|
catch {
|
|
1435
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1217
1436
|
return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
|
|
1218
1437
|
}
|
|
1219
1438
|
if (report.status === 'external-block') {
|
|
1439
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1220
1440
|
return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
|
|
1221
1441
|
}
|
|
1222
1442
|
if (!sameStrings(changedFiles, report.changedFiles)) {
|
|
1443
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
1223
1444
|
return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
|
|
1224
1445
|
}
|
|
1225
1446
|
}
|
|
1226
1447
|
if (active.record.routeReceipt?.route === 'direct') {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1448
|
+
const boundary = active.record.reviewFeedback?.activeBatch
|
|
1449
|
+
? {
|
|
1450
|
+
kind: 'review-feedback',
|
|
1451
|
+
batchId: active.record.reviewFeedback.activeBatch.batchId,
|
|
1452
|
+
repairRound: active.record.reviewFeedback.repairRound,
|
|
1453
|
+
}
|
|
1454
|
+
: { kind: 'implementation-cycle', cycle: active.record.cycle };
|
|
1455
|
+
const captured = await this.ensureCandidateBinding(active, config, boundary);
|
|
1456
|
+
if ('status' in captured)
|
|
1457
|
+
return captured;
|
|
1458
|
+
active = captured.active;
|
|
1459
|
+
const binding = active.record.candidateBinding;
|
|
1460
|
+
if (!sameStrings(binding.canonicalChangedFiles, report.changedFiles)) {
|
|
1461
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
1462
|
+
if ('status' in released)
|
|
1463
|
+
return released;
|
|
1464
|
+
return this.terminal(released.active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
|
|
1465
|
+
}
|
|
1466
|
+
const targetFingerprint = directReviewCandidateTargetFingerprint({
|
|
1467
|
+
binding,
|
|
1230
1468
|
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
1231
1469
|
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
1232
1470
|
cycle: active.record.cycle,
|
|
@@ -1235,9 +1473,7 @@ export class RunIssue {
|
|
|
1235
1473
|
if (active.record.directReview?.stage === 'review-repair') {
|
|
1236
1474
|
active = await this.persist(active, {
|
|
1237
1475
|
directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
|
|
1238
|
-
|
|
1239
|
-
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1240
|
-
} : {}),
|
|
1476
|
+
mutableInvocation: undefined,
|
|
1241
1477
|
});
|
|
1242
1478
|
}
|
|
1243
1479
|
else {
|
|
@@ -1248,9 +1484,7 @@ export class RunIssue {
|
|
|
1248
1484
|
targetFingerprint,
|
|
1249
1485
|
codeReviewerSessionId: reviewerSessionId,
|
|
1250
1486
|
}),
|
|
1251
|
-
|
|
1252
|
-
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1253
|
-
} : {}),
|
|
1487
|
+
mutableInvocation: undefined,
|
|
1254
1488
|
});
|
|
1255
1489
|
}
|
|
1256
1490
|
const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
|
|
@@ -1263,21 +1497,32 @@ export class RunIssue {
|
|
|
1263
1497
|
else {
|
|
1264
1498
|
active = await this.persist(active, {
|
|
1265
1499
|
lifecycle: 'checking',
|
|
1266
|
-
|
|
1267
|
-
reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
|
|
1268
|
-
} : {}),
|
|
1500
|
+
mutableInvocation: undefined,
|
|
1269
1501
|
});
|
|
1270
1502
|
}
|
|
1271
1503
|
}
|
|
1272
|
-
let
|
|
1504
|
+
let currentCheckPolicy;
|
|
1273
1505
|
try {
|
|
1274
|
-
|
|
1506
|
+
currentCheckPolicy = resolveIssueCheckPolicy(active.record.issueSnapshot.body, config.checks);
|
|
1275
1507
|
}
|
|
1276
1508
|
catch (error) {
|
|
1277
1509
|
return await this.invokedFailure(active, 'issue-verification-invalid', error instanceof Error ? error.message : undefined);
|
|
1278
1510
|
}
|
|
1279
|
-
const
|
|
1511
|
+
const currentCheckPolicySha256 = sha256(canonicalJson(currentCheckPolicy.checks));
|
|
1512
|
+
const recoveringPersistedProof = active.record.lifecycle === 'proving' && active.record.executionLease?.operation === 'acceptance-proof'
|
|
1513
|
+
&& active.record.checkedChangeSha256 !== undefined && active.record.proofId !== undefined;
|
|
1514
|
+
const persistedProofCheckPolicySha256 = active.record.checks.find((check) => 'checkPolicySha256' in check)?.checkPolicySha256 ?? active.record.checkQualification?.checkPolicySha256;
|
|
1515
|
+
const configuredChecks = recoveringPersistedProof
|
|
1516
|
+
? active.record.checks.map((check) => [check.id, check.command]) : Object.entries(currentCheckPolicy.checks);
|
|
1517
|
+
const finalCheckPolicySha256 = recoveringPersistedProof && persistedProofCheckPolicySha256 ? persistedProofCheckPolicySha256 : currentCheckPolicySha256;
|
|
1518
|
+
const finalBinding = active.record.candidateBinding;
|
|
1519
|
+
if (!finalBinding)
|
|
1520
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'candidate-git-v2-required');
|
|
1280
1521
|
const reusableChecks = active.record.checks.filter((check) => check.status === 'passed'
|
|
1522
|
+
&& 'bindingId' in check
|
|
1523
|
+
&& check.bindingId === finalBinding.bindingId
|
|
1524
|
+
&& check.candidateTreeSha === finalBinding.candidateTreeSha
|
|
1525
|
+
&& check.checkPolicySha256 === finalCheckPolicySha256
|
|
1281
1526
|
&& configuredChecks.some(([id, command]) => check.id === id && check.command === command));
|
|
1282
1527
|
if (reusableChecks.length !== active.record.checks.length) {
|
|
1283
1528
|
active = await this.persist(active, {
|
|
@@ -1290,13 +1535,34 @@ export class RunIssue {
|
|
|
1290
1535
|
for (const [id, command] of configuredChecks) {
|
|
1291
1536
|
if (active.record.checks.some((check) => check.id === id
|
|
1292
1537
|
&& check.command === command
|
|
1293
|
-
&& check.status === 'passed'
|
|
1538
|
+
&& check.status === 'passed'
|
|
1539
|
+
&& 'bindingId' in check
|
|
1540
|
+
&& check.bindingId === finalBinding.bindingId
|
|
1541
|
+
&& check.candidateTreeSha === finalBinding.candidateTreeSha
|
|
1542
|
+
&& check.checkPolicySha256 === finalCheckPolicySha256))
|
|
1294
1543
|
continue;
|
|
1295
1544
|
if (this.signal.aborted)
|
|
1296
1545
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1546
|
+
if (this.dependencies.checks.supportsLaunchOwnership !== true) {
|
|
1547
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'candidate-check-launch-ownership-required');
|
|
1548
|
+
}
|
|
1549
|
+
const execution = await this.prepareCandidateExecution(active, config, 'final-check', `${id}:${finalCheckPolicySha256}`);
|
|
1550
|
+
if ('status' in execution)
|
|
1551
|
+
return execution;
|
|
1552
|
+
active = execution.active;
|
|
1553
|
+
const lease = execution.lease;
|
|
1297
1554
|
let check;
|
|
1298
1555
|
try {
|
|
1299
|
-
check = await this.dependencies.checks.run({
|
|
1556
|
+
check = await this.dependencies.checks.run({
|
|
1557
|
+
id, command, source: currentCheckPolicy.source, cwd: lease.path, phase: 'changed', signal: this.signal,
|
|
1558
|
+
onLaunched: async ({ pid, processGroupId }) => {
|
|
1559
|
+
active = await this.persist(active, {
|
|
1560
|
+
executionLease: this.dependencies.git.candidateV2.markExecutionLaunched({
|
|
1561
|
+
lease, pid, processGroupId, launchedAt: this.timestamp(),
|
|
1562
|
+
}),
|
|
1563
|
+
});
|
|
1564
|
+
},
|
|
1565
|
+
});
|
|
1300
1566
|
}
|
|
1301
1567
|
catch (error) {
|
|
1302
1568
|
if (error instanceof CheckProcessQuiescenceError) {
|
|
@@ -1306,160 +1572,162 @@ export class RunIssue {
|
|
|
1306
1572
|
}
|
|
1307
1573
|
if (this.signal.aborted)
|
|
1308
1574
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1309
|
-
const
|
|
1575
|
+
const settledExecution = await this.settleCandidateExecution(active, config);
|
|
1576
|
+
if ('status' in settledExecution)
|
|
1577
|
+
return settledExecution;
|
|
1578
|
+
active = settledExecution.active;
|
|
1579
|
+
const row = {
|
|
1580
|
+
id, command, status: check.status, outputSha256: check.outputSha256,
|
|
1581
|
+
bindingId: finalBinding.bindingId,
|
|
1582
|
+
candidateTreeSha: finalBinding.candidateTreeSha,
|
|
1583
|
+
checkPolicySha256: finalCheckPolicySha256,
|
|
1584
|
+
};
|
|
1310
1585
|
active = await this.persist(active, { checks: [...active.record.checks, row] });
|
|
1311
1586
|
if (check.status === 'failed') {
|
|
1312
1587
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1588
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
1589
|
+
if ('status' in released)
|
|
1590
|
+
return released;
|
|
1591
|
+
active = released.active;
|
|
1313
1592
|
return active.record.reviewFeedback?.activeBatch
|
|
1314
1593
|
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
|
|
1315
1594
|
: await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
1316
1595
|
}
|
|
1317
1596
|
const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
|
|
1318
|
-
|
|
1597
|
+
const reopened = await this.startNextCycleFromCandidate(active, [summary], [{
|
|
1319
1598
|
provenance: 'check', sourceId: `check:${id}:${row.outputSha256}`, summary,
|
|
1320
1599
|
affectedContracts: ['configured-checks'],
|
|
1321
1600
|
}]);
|
|
1601
|
+
if ('status' in reopened)
|
|
1602
|
+
return reopened;
|
|
1603
|
+
active = reopened;
|
|
1322
1604
|
continue attemptLoop;
|
|
1323
1605
|
}
|
|
1324
1606
|
}
|
|
1325
|
-
|
|
1326
|
-
try {
|
|
1327
|
-
const reviewedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
1328
|
-
const reviewedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, reviewedFiles);
|
|
1329
|
-
reviewedStageBinding = { files: reviewedFiles, contentSha256: reviewedContentSha256 };
|
|
1330
|
-
if (active.record.directReview?.status === 'clear') {
|
|
1331
|
-
const currentFingerprint = directReviewTargetFingerprint({
|
|
1332
|
-
snapshot: await this.dependencies.git.snapshot(worktreePath),
|
|
1333
|
-
changedFiles: await this.dependencies.git.listChangedFiles(worktreePath),
|
|
1334
|
-
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
1335
|
-
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
1336
|
-
cycle: active.record.cycle,
|
|
1337
|
-
frozenCriteria,
|
|
1338
|
-
});
|
|
1339
|
-
if (currentFingerprint !== active.record.directReview.targetFingerprint) {
|
|
1340
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'direct-review-target-drift');
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
await this.dependencies.git.stageAll(worktreePath);
|
|
1344
|
-
const stagedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
1345
|
-
const stagedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, stagedFiles);
|
|
1346
|
-
if (!sameStrings(reviewedFiles, stagedFiles) || reviewedContentSha256 !== stagedContentSha256) {
|
|
1347
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'post-stage-review-binding-drift');
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
catch {
|
|
1351
|
-
return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
|
|
1352
|
-
}
|
|
1353
|
-
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
1354
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
1355
|
-
}
|
|
1356
|
-
const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
1357
|
-
const freshness = await this.dependencies.git.snapshot(worktreePath);
|
|
1358
|
-
const proofContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, proofChangedFiles);
|
|
1359
|
-
if (!reviewedStageBinding || !sameStrings(reviewedStageBinding.files, proofChangedFiles)
|
|
1360
|
-
|| reviewedStageBinding.contentSha256 !== proofContentSha256) {
|
|
1361
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
|
|
1362
|
-
}
|
|
1363
|
-
if (!sameCheckPolicy(active.record.checks, checkPolicy.checks)) {
|
|
1607
|
+
if (!sameCheckPolicy(active.record.checks, recoveringPersistedProof ? Object.fromEntries(configuredChecks) : currentCheckPolicy.checks)) {
|
|
1364
1608
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'configured-check-policy-drift');
|
|
1365
1609
|
}
|
|
1610
|
+
const proofBinding = active.record.candidateBinding;
|
|
1611
|
+
if (!proofBinding || active.record.checks.some((check) => !('bindingId' in check)
|
|
1612
|
+
|| check.bindingId !== proofBinding.bindingId || check.candidateTreeSha !== proofBinding.candidateTreeSha
|
|
1613
|
+
|| check.checkPolicySha256 !== finalCheckPolicySha256)) {
|
|
1614
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'checked-change-candidate-binding-drift');
|
|
1615
|
+
}
|
|
1366
1616
|
const payload = {
|
|
1367
|
-
version:
|
|
1617
|
+
version: 2,
|
|
1368
1618
|
canonicalRepository,
|
|
1369
1619
|
runId,
|
|
1370
1620
|
issueNumber: input.issueNumber,
|
|
1371
1621
|
cycle: active.record.cycle,
|
|
1372
1622
|
baseSha: expectedImplementationHead(active.record),
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
worktreeIdentity: freshness.worktreeIdentity,
|
|
1378
|
-
changedFiles: proofChangedFiles,
|
|
1379
|
-
checks: active.record.checks.map((check) => ({
|
|
1380
|
-
...check,
|
|
1381
|
-
status: 'passed',
|
|
1382
|
-
})),
|
|
1383
|
-
checkPolicySha256: sha256(canonicalJson(checkPolicy.checks)),
|
|
1623
|
+
binding: structuredClone(proofBinding),
|
|
1624
|
+
changedFiles: [...proofBinding.canonicalChangedFiles],
|
|
1625
|
+
checks: active.record.checks.map((check) => ({ ...check, status: 'passed' })),
|
|
1626
|
+
checkPolicySha256: finalCheckPolicySha256,
|
|
1384
1627
|
packageVersion: active.record.packageVersion,
|
|
1385
1628
|
proofSchemaVersion: 1,
|
|
1386
1629
|
};
|
|
1387
1630
|
const checkedChange = this.dependencies.checkedChangeMint.mint(payload);
|
|
1388
1631
|
const checkedChangeSha256 = checkedChangePayloadSha256(payload);
|
|
1389
|
-
|
|
1632
|
+
if (active.record.checkedChangeSha256 && active.record.checkedChangeSha256 !== checkedChangeSha256) {
|
|
1633
|
+
return await this.persistCandidateEvidenceSafetyTerminal(active, 'checked-change-recovery-binding-drift');
|
|
1634
|
+
}
|
|
1635
|
+
const proofId = active.record.proofId ?? this.dependencies.createProofId();
|
|
1390
1636
|
assertNonEmptyString(proofId, 'proofId');
|
|
1391
1637
|
active = await this.persist(active, { lifecycle: 'proving', checkedChangeSha256, proofId });
|
|
1638
|
+
const retainedProofLease = active.record.executionLease?.operation === 'acceptance-proof'
|
|
1639
|
+
? active.record.executionLease
|
|
1640
|
+
: undefined;
|
|
1641
|
+
const preparedProofExecution = retainedProofLease
|
|
1642
|
+
? { active, lease: retainedProofLease }
|
|
1643
|
+
: await this.prepareCandidateExecution(active, config, 'acceptance-proof', proofId);
|
|
1644
|
+
if ('status' in preparedProofExecution)
|
|
1645
|
+
return preparedProofExecution;
|
|
1646
|
+
active = preparedProofExecution.active;
|
|
1647
|
+
const proofExecution = preparedProofExecution;
|
|
1392
1648
|
let proof;
|
|
1393
1649
|
try {
|
|
1650
|
+
if (active.record.proofReceipt) {
|
|
1651
|
+
const settled = await this.settleCandidateExecution(active, config);
|
|
1652
|
+
if ('status' in settled)
|
|
1653
|
+
return settled;
|
|
1654
|
+
active = await this.persist(settled.active, { lifecycle: 'publishing', reworkFindings: [] });
|
|
1655
|
+
break;
|
|
1656
|
+
}
|
|
1394
1657
|
proof = await this.dependencies.proof.proveChange({
|
|
1395
1658
|
proofId,
|
|
1396
1659
|
issue: publicIssueSnapshot(issueSnapshot),
|
|
1397
1660
|
frozenCriteria,
|
|
1398
1661
|
checkedChange,
|
|
1662
|
+
executionLease: proofExecution.lease,
|
|
1399
1663
|
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1400
1664
|
beforeAgentLaunch: async () => {
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1665
|
+
if (recoveringPersistedProof && currentCheckPolicySha256 !== finalCheckPolicySha256)
|
|
1666
|
+
throw new ProofLaunchAuthorizationError({ status: 'blocked', code: 'configured-check-policy-drift' });
|
|
1667
|
+
const observation = !await this.authorized(active, config) ? { status: 'blocked', code: 'proof-worker-authority-revoked' } : await this.observeFeedbackWorker(active, config, input.issueNumber);
|
|
1668
|
+
if (observation.status !== 'valid')
|
|
1669
|
+
throw new ProofLaunchAuthorizationError(observation);
|
|
1404
1670
|
},
|
|
1405
1671
|
});
|
|
1406
1672
|
}
|
|
1407
1673
|
catch (error) {
|
|
1408
|
-
if (error instanceof ProofLaunchAuthorizationError)
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
process: {
|
|
1414
|
-
pid: error.pid,
|
|
1415
|
-
processGroupId: error.processGroupId,
|
|
1416
|
-
startedAt: this.timestamp(),
|
|
1417
|
-
baseline: {
|
|
1418
|
-
headSha: freshness.headSha,
|
|
1419
|
-
indexTreeSha: freshness.indexTreeSha,
|
|
1420
|
-
trackedContentSha256: freshness.trackedContentSha256,
|
|
1421
|
-
untrackedContentSha256: freshness.untrackedContentSha256,
|
|
1422
|
-
worktreeIdentity: freshness.worktreeIdentity,
|
|
1423
|
-
},
|
|
1424
|
-
purpose: 'proof',
|
|
1425
|
-
resumeLifecycle: 'proving',
|
|
1426
|
-
resumeReviewStage: null,
|
|
1427
|
-
},
|
|
1428
|
-
});
|
|
1429
|
-
while (true) {
|
|
1430
|
-
try {
|
|
1431
|
-
await error.waitForAbsence();
|
|
1432
|
-
break;
|
|
1433
|
-
}
|
|
1434
|
-
catch {
|
|
1435
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
1436
|
-
}
|
|
1674
|
+
if (error instanceof ProofLaunchAuthorizationError) {
|
|
1675
|
+
const observation = error.outcome;
|
|
1676
|
+
if (observation.code === 'configured-check-policy-drift') {
|
|
1677
|
+
const settled = await this.settleCandidateExecution(active, config);
|
|
1678
|
+
return 'status' in settled ? settled : this.terminal(settled.active, { status: 'blocked', kind: 'safety', resumable: true }, observation.code);
|
|
1437
1679
|
}
|
|
1438
|
-
|
|
1680
|
+
if (observation.status === 'retryable')
|
|
1681
|
+
return this.invokedFailure(active, `${observation.code}-retryable`);
|
|
1682
|
+
return active.record.reviewFeedback?.activeBatch ? this.blockReviewFeedback(active, 'safety', observation.code) : this.revoked(active);
|
|
1683
|
+
}
|
|
1684
|
+
if (error instanceof CandidateProofInspectionError) {
|
|
1685
|
+
return error.code === 'candidate-artifact-conflict'
|
|
1686
|
+
? this.persistCandidateEvidenceSafetyTerminal(active, error.code)
|
|
1687
|
+
: this.mapCandidateFailure(active, error.code);
|
|
1439
1688
|
}
|
|
1440
1689
|
return await this.terminal(active, { status: 'internal-error', code: 'acceptance-proof-internal-failure' });
|
|
1441
1690
|
}
|
|
1691
|
+
if (proof.status === 'transport-failed' && proof.resumable) {
|
|
1692
|
+
return this.invokedFailure(active, 'proof-invocation-deferred', 'Acceptance proof ownership or output requires bounded reconciliation on the next tick.');
|
|
1693
|
+
}
|
|
1694
|
+
if (recoveringPersistedProof && currentCheckPolicySha256 !== finalCheckPolicySha256) {
|
|
1695
|
+
const settled = await this.settleCandidateExecution(active, config);
|
|
1696
|
+
return 'status' in settled ? settled : this.terminal(settled.active, { status: 'blocked', kind: 'safety', resumable: true }, 'configured-check-policy-drift');
|
|
1697
|
+
}
|
|
1698
|
+
if (proof.status === 'passed')
|
|
1699
|
+
active = await this.persist(active, { proofReceipt: proof.receipt });
|
|
1700
|
+
const settledProofExecution = await this.settleCandidateExecution(active, config);
|
|
1701
|
+
if ('status' in settledProofExecution)
|
|
1702
|
+
return settledProofExecution;
|
|
1703
|
+
active = settledProofExecution.active;
|
|
1442
1704
|
if (this.signal.aborted)
|
|
1443
1705
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1444
1706
|
if (proof.status === 'needs-rework') {
|
|
1445
1707
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1708
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
1709
|
+
if ('status' in released)
|
|
1710
|
+
return released;
|
|
1711
|
+
active = released.active;
|
|
1446
1712
|
return active.record.reviewFeedback?.activeBatch
|
|
1447
1713
|
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-proof-repair-exhausted')
|
|
1448
1714
|
: await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
|
|
1449
1715
|
}
|
|
1450
|
-
|
|
1716
|
+
const reopened = await this.startNextCycleFromCandidate(active, proof.findings, proof.findings.map((summary) => ({
|
|
1451
1717
|
provenance: 'proof',
|
|
1452
1718
|
sourceId: `proof:${proofId}:${sha256(summary)}`,
|
|
1453
1719
|
summary,
|
|
1454
1720
|
affectedContracts: ['acceptance-proof'],
|
|
1455
1721
|
})));
|
|
1722
|
+
if ('status' in reopened)
|
|
1723
|
+
return reopened;
|
|
1724
|
+
active = reopened;
|
|
1456
1725
|
continue attemptLoop;
|
|
1457
1726
|
}
|
|
1458
1727
|
if (proof.status !== 'passed')
|
|
1459
1728
|
return await this.mapProofFailure(active, proof);
|
|
1460
1729
|
active = await this.persist(active, {
|
|
1461
1730
|
lifecycle: 'publishing',
|
|
1462
|
-
proofReceipt: proof.receipt,
|
|
1463
1731
|
reworkFindings: [],
|
|
1464
1732
|
...(active.record.reviewFeedback?.activeBatch ? {
|
|
1465
1733
|
reviewFeedback: markReviewFeedbackPublishing(markReviewFeedbackVerified(active.record.reviewFeedback, {
|
|
@@ -1525,7 +1793,6 @@ export class RunIssue {
|
|
|
1525
1793
|
}
|
|
1526
1794
|
async initializeClaimedRun(active, issue) {
|
|
1527
1795
|
if (active.record.lifecycle !== 'claimed'
|
|
1528
|
-
|| active.record.process
|
|
1529
1796
|
|| active.record.intent
|
|
1530
1797
|
|| active.record.routeExecution
|
|
1531
1798
|
|| active.record.routeReceipt) {
|
|
@@ -1577,13 +1844,16 @@ export class RunIssue {
|
|
|
1577
1844
|
active = await this.persist(active, { routeExecution: structuredClone(next) });
|
|
1578
1845
|
return true;
|
|
1579
1846
|
},
|
|
1580
|
-
complete: async (expected, next, receipt) => {
|
|
1847
|
+
complete: async (expected, next, receipt, attemptId) => {
|
|
1581
1848
|
if (!sameRouteExecution(active.record.routeExecution, expected))
|
|
1582
1849
|
return false;
|
|
1850
|
+
if (active.record.reportInvocation?.attemptId !== attemptId)
|
|
1851
|
+
return false;
|
|
1583
1852
|
active = await this.persist(active, {
|
|
1584
1853
|
lifecycle: 'routed',
|
|
1585
1854
|
routeExecution: structuredClone(next),
|
|
1586
1855
|
routeReceipt: structuredClone(receipt),
|
|
1856
|
+
reportInvocation: undefined,
|
|
1587
1857
|
});
|
|
1588
1858
|
return true;
|
|
1589
1859
|
},
|
|
@@ -1601,10 +1871,19 @@ export class RunIssue {
|
|
|
1601
1871
|
routeReceipt: undefined,
|
|
1602
1872
|
outcomeEvidenceId: evidence.id,
|
|
1603
1873
|
terminalOutcome: { status: 'cancelled', evidencePath: evidence.path },
|
|
1874
|
+
reportInvocation: undefined,
|
|
1604
1875
|
...(active.record.waitingHuman ? { waitingHuman: terminalWaiting(active.record.waitingHuman, { status: 'cancelled' }) } : {}),
|
|
1605
1876
|
});
|
|
1606
1877
|
return true;
|
|
1607
1878
|
},
|
|
1879
|
+
invocation: this.reportInvocationState(() => active, (next) => { active = next; }),
|
|
1880
|
+
settle: async (expected, next, attemptId) => {
|
|
1881
|
+
if (!sameRouteExecution(active.record.routeExecution, expected)
|
|
1882
|
+
|| active.record.reportInvocation?.attemptId !== attemptId)
|
|
1883
|
+
return false;
|
|
1884
|
+
active = await this.persist(active, { routeExecution: structuredClone(next), reportInvocation: undefined });
|
|
1885
|
+
return true;
|
|
1886
|
+
},
|
|
1608
1887
|
};
|
|
1609
1888
|
const result = await this.dependencies.routeCoordinator.run({
|
|
1610
1889
|
state,
|
|
@@ -1623,32 +1902,13 @@ export class RunIssue {
|
|
|
1623
1902
|
],
|
|
1624
1903
|
signal: this.signal,
|
|
1625
1904
|
});
|
|
1626
|
-
if (result.status === 'repairable'
|
|
1905
|
+
if (result.status === 'repairable')
|
|
1627
1906
|
continue;
|
|
1907
|
+
if (result.status === 'retryable') {
|
|
1908
|
+
return { result: await this.invokedFailure(active, result.code, 'Report-only recovery is deferred to the next runner tick.') };
|
|
1909
|
+
}
|
|
1628
1910
|
if (result.status === 'safe-halt') {
|
|
1629
|
-
|
|
1630
|
-
lifecycle: 'safe-halt',
|
|
1631
|
-
process: {
|
|
1632
|
-
...result.process,
|
|
1633
|
-
purpose: 'route',
|
|
1634
|
-
resumeLifecycle: 'triaging',
|
|
1635
|
-
resumeReviewStage: null,
|
|
1636
|
-
},
|
|
1637
|
-
});
|
|
1638
|
-
while (true) {
|
|
1639
|
-
try {
|
|
1640
|
-
await result.waitForAbsence();
|
|
1641
|
-
break;
|
|
1642
|
-
}
|
|
1643
|
-
catch {
|
|
1644
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
const after = await this.dependencies.git.snapshot(worktreePath);
|
|
1648
|
-
if (!sameFreshness(result.process.baseline, after)) {
|
|
1649
|
-
return { result: await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'report-operation-worktree-mutated') };
|
|
1650
|
-
}
|
|
1651
|
-
return { result: await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed') };
|
|
1911
|
+
return { result: await this.invokedFailure(active, result.code, 'Report-only process ownership remains unresolved; no relaunch was attempted.') };
|
|
1652
1912
|
}
|
|
1653
1913
|
if (result.status === 'cancelled') {
|
|
1654
1914
|
if (!active.record.terminalOutcome)
|
|
@@ -1729,9 +1989,8 @@ export class RunIssue {
|
|
|
1729
1989
|
status: 'blocked', kind: continuation.kind, resumable: continuation.kind !== 'exhausted',
|
|
1730
1990
|
}, continuation.code) };
|
|
1731
1991
|
}
|
|
1732
|
-
if (continuation.status === 'retryable')
|
|
1733
|
-
return { result: await this.
|
|
1734
|
-
}
|
|
1992
|
+
if (continuation.status === 'retryable')
|
|
1993
|
+
return { result: await this.invokedFailure(active, continuation.code) };
|
|
1735
1994
|
if (receipt.route !== 'direct') {
|
|
1736
1995
|
const specContinuation = continuation;
|
|
1737
1996
|
if (specContinuation.status !== 'completed')
|
|
@@ -1766,6 +2025,29 @@ export class RunIssue {
|
|
|
1766
2025
|
};
|
|
1767
2026
|
}
|
|
1768
2027
|
specState(readActive, writeActive) {
|
|
2028
|
+
const invocation = (reserveSession) => ({
|
|
2029
|
+
read: async () => structuredClone(readActive().record.reportInvocation),
|
|
2030
|
+
compareAndSwap: async (expected, next) => {
|
|
2031
|
+
const active = readActive();
|
|
2032
|
+
const observed = active.record.reportInvocation;
|
|
2033
|
+
if (observed === undefined || expected === undefined ? observed !== expected
|
|
2034
|
+
: canonicalJson(observed) !== canonicalJson(expected))
|
|
2035
|
+
return false;
|
|
2036
|
+
const preparing = expected === undefined && next?.phase === 'prepared';
|
|
2037
|
+
const specDelivery = preparing ? reserveSession(active.record.specDelivery) : active.record.specDelivery;
|
|
2038
|
+
writeActive(await this.persist(active, { ...(specDelivery ? { specDelivery } : {}),
|
|
2039
|
+
reportInvocation: next ? structuredClone(next) : undefined }));
|
|
2040
|
+
return true;
|
|
2041
|
+
},
|
|
2042
|
+
});
|
|
2043
|
+
const settle = async (expected, next, attemptId) => {
|
|
2044
|
+
const active = readActive();
|
|
2045
|
+
if (canonicalJson(active.record.specDelivery) !== canonicalJson(expected)
|
|
2046
|
+
|| active.record.reportInvocation?.attemptId !== attemptId)
|
|
2047
|
+
return false;
|
|
2048
|
+
writeActive(await this.persist(active, { specDelivery: structuredClone(next), reportInvocation: undefined }));
|
|
2049
|
+
return true;
|
|
2050
|
+
};
|
|
1769
2051
|
return {
|
|
1770
2052
|
read: async () => structuredClone(readActive().record.specDelivery),
|
|
1771
2053
|
compareAndSwap: async (expected, next) => {
|
|
@@ -1781,6 +2063,34 @@ export class RunIssue {
|
|
|
1781
2063
|
writeActive(saved);
|
|
1782
2064
|
return true;
|
|
1783
2065
|
},
|
|
2066
|
+
authorInvocation: (authorSessionId) => invocation((state) => reserveSpecAuthorSession(state, authorSessionId)),
|
|
2067
|
+
reviewInvocation: (reviewerSessionId) => invocation((state) => reserveSpecReviewerSession(state, reviewerSessionId)),
|
|
2068
|
+
settleAuthor: settle,
|
|
2069
|
+
settleReview: settle,
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
reportInvocationState(readActive, writeActive, launch) {
|
|
2073
|
+
return {
|
|
2074
|
+
read: async () => structuredClone(readActive().record.reportInvocation),
|
|
2075
|
+
compareAndSwap: async (expected, next) => {
|
|
2076
|
+
const active = readActive();
|
|
2077
|
+
const observed = active.record.reportInvocation;
|
|
2078
|
+
if (observed === undefined || expected === undefined) {
|
|
2079
|
+
if (observed !== expected)
|
|
2080
|
+
return false;
|
|
2081
|
+
}
|
|
2082
|
+
else if (canonicalJson(observed) !== canonicalJson(expected))
|
|
2083
|
+
return false;
|
|
2084
|
+
const launching = expected?.phase === 'prepared' && next?.phase === 'launched';
|
|
2085
|
+
if (launching)
|
|
2086
|
+
await launch?.beforeLaunch();
|
|
2087
|
+
const launchChanges = launching && launch ? launch.launchChanges(next) : {};
|
|
2088
|
+
writeActive(await this.persist(active, {
|
|
2089
|
+
...launchChanges,
|
|
2090
|
+
reportInvocation: next ? structuredClone(next) : undefined,
|
|
2091
|
+
}));
|
|
2092
|
+
return true;
|
|
2093
|
+
},
|
|
1784
2094
|
};
|
|
1785
2095
|
}
|
|
1786
2096
|
async continueSpecRequired(active) {
|
|
@@ -1801,7 +2111,7 @@ export class RunIssue {
|
|
|
1801
2111
|
if (result.status === 'cancelled')
|
|
1802
2112
|
return await this.terminal(current, { status: 'cancelled' });
|
|
1803
2113
|
if (result.status === 'retryable')
|
|
1804
|
-
return await this.
|
|
2114
|
+
return await this.invokedFailure(current, result.code);
|
|
1805
2115
|
return await this.terminal(current, { status: 'blocked', kind: result.kind, resumable: result.kind !== 'exhausted' }, result.code);
|
|
1806
2116
|
}
|
|
1807
2117
|
async continueWaitingHuman(active) {
|
|
@@ -1889,7 +2199,7 @@ export class RunIssue {
|
|
|
1889
2199
|
const { config, ...persistedInput } = input;
|
|
1890
2200
|
const record = {
|
|
1891
2201
|
...persistedInput,
|
|
1892
|
-
lifecycle: 'claimed', cycle: 1, reportRepairs: 0,
|
|
2202
|
+
lifecycle: 'claimed', cycle: 1, reportRepairs: 0,
|
|
1893
2203
|
reworkFindings: [],
|
|
1894
2204
|
packageVersion: workflow.receipt.packageVersion,
|
|
1895
2205
|
workflowGeneration: structuredClone(workflow.receipt),
|
|
@@ -1897,23 +2207,21 @@ export class RunIssue {
|
|
|
1897
2207
|
checks: [], createdAt: now, updatedAt: now,
|
|
1898
2208
|
};
|
|
1899
2209
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
1900
|
-
schema: 'codex-orchestrator.agent-auto-state', version:
|
|
2210
|
+
schema: 'codex-orchestrator.agent-auto-state', version: state.version, runs: [...state.runs, record],
|
|
1901
2211
|
});
|
|
1902
2212
|
return { state: saved, record: findRun(saved, input.runId), config };
|
|
1903
2213
|
}
|
|
1904
|
-
async persist(active, changes) {
|
|
2214
|
+
async persist(active, changes, stateVersion = active.state.version) {
|
|
1905
2215
|
const record = { ...active.record, ...changes, updatedAt: this.timestamp() };
|
|
1906
2216
|
if (Object.hasOwn(changes, 'intent') && changes.intent === undefined)
|
|
1907
2217
|
delete record.intent;
|
|
1908
|
-
|
|
1909
|
-
delete record.process;
|
|
1910
|
-
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reviewFeedback', 'checkQualification', 'baselineChecks']) {
|
|
2218
|
+
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reportInvocation', 'mutableInvocation', 'reviewFeedback', 'checkQualification', 'baselineChecks', 'changeBindingVersion', 'candidateBinding', 'executionLease']) {
|
|
1911
2219
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
1912
2220
|
delete record[key];
|
|
1913
2221
|
}
|
|
1914
2222
|
const runs = active.state.runs.map((candidate) => candidate.runId === record.runId ? record : candidate);
|
|
1915
2223
|
const saved = await this.dependencies.runRecords.compareAndSwap(active.state.generation, {
|
|
1916
|
-
schema: 'codex-orchestrator.agent-auto-state', version:
|
|
2224
|
+
schema: 'codex-orchestrator.agent-auto-state', version: stateVersion, runs,
|
|
1917
2225
|
});
|
|
1918
2226
|
return { state: saved, record: findRun(saved, record.runId), config: active.config };
|
|
1919
2227
|
}
|
|
@@ -1943,11 +2251,6 @@ export class RunIssue {
|
|
|
1943
2251
|
return false;
|
|
1944
2252
|
return this.hasTrustedClaim(issue, record);
|
|
1945
2253
|
}
|
|
1946
|
-
async authorizedForExactLabels(active, expectedLabels) {
|
|
1947
|
-
const issue = await this.readIssue(active.record.issueNumber);
|
|
1948
|
-
return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
|
|
1949
|
-
&& this.hasTrustedClaim(issue, active.record);
|
|
1950
|
-
}
|
|
1951
2254
|
hasTrustedClaim(issue, record) {
|
|
1952
2255
|
const exactBody = claimComment(record.runId, record.issueNumber, record.branchName);
|
|
1953
2256
|
const markers = issue.comments.filter(isClaimMarkerComment);
|
|
@@ -1982,21 +2285,31 @@ export class RunIssue {
|
|
|
1982
2285
|
}
|
|
1983
2286
|
}
|
|
1984
2287
|
async revalidateFeedbackWorker(active, config, issueNumber) {
|
|
2288
|
+
const validation = await this.observeFeedbackWorker(active, config, issueNumber);
|
|
2289
|
+
if (validation.status === 'valid')
|
|
2290
|
+
return undefined;
|
|
2291
|
+
if (validation.status === 'retryable')
|
|
2292
|
+
return this.invokedFailure(active, `${validation.code}-retryable`);
|
|
2293
|
+
return this.blockReviewFeedback(active, 'safety', validation.code);
|
|
2294
|
+
}
|
|
2295
|
+
async observeFeedbackWorker(active, config, issueNumber) {
|
|
1985
2296
|
const batch = active.record.reviewFeedback?.activeBatch;
|
|
1986
2297
|
if (!batch)
|
|
1987
|
-
return
|
|
2298
|
+
return { status: 'valid' };
|
|
1988
2299
|
if (!await this.authorized(active, config)) {
|
|
1989
|
-
return
|
|
2300
|
+
return { status: 'blocked', code: 'review-feedback-worker-authority-revoked' };
|
|
1990
2301
|
}
|
|
1991
2302
|
if (!this.dependencies.reviewFeedback) {
|
|
1992
|
-
return
|
|
2303
|
+
return { status: 'blocked', code: 'review-feedback-worker-revalidation-unavailable' };
|
|
1993
2304
|
}
|
|
1994
2305
|
const validation = await this.dependencies.reviewFeedback.revalidate({
|
|
1995
2306
|
batch,
|
|
1996
2307
|
epoch: 'pre-update',
|
|
1997
2308
|
expectedHeadSha: batch.priorPublishedHeadSha,
|
|
1998
2309
|
});
|
|
1999
|
-
return
|
|
2310
|
+
return validation.status === 'valid'
|
|
2311
|
+
? { status: 'valid' }
|
|
2312
|
+
: { status: validation.status, code: 'review-feedback-worker-revalidation-failed' };
|
|
2000
2313
|
}
|
|
2001
2314
|
async mapFeedbackRevalidation(active, validation, safetyCode) {
|
|
2002
2315
|
if (validation.status === 'valid')
|
|
@@ -2005,6 +2318,277 @@ export class RunIssue {
|
|
|
2005
2318
|
return this.invokedFailure(active, `${safetyCode}-retryable`);
|
|
2006
2319
|
return this.blockReviewFeedback(active, 'safety', safetyCode);
|
|
2007
2320
|
}
|
|
2321
|
+
async ensureCandidateBinding(active, config, boundary) {
|
|
2322
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
2323
|
+
if (!candidate)
|
|
2324
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'candidate-git-v2-required');
|
|
2325
|
+
if (active.record.candidateBinding) {
|
|
2326
|
+
const inspection = await candidate.inspectPin(active.record.candidateBinding);
|
|
2327
|
+
if (inspection.kind === 'failed')
|
|
2328
|
+
return this.mapCandidateFailure(active, inspection.code);
|
|
2329
|
+
if (inspection.value === 'missing')
|
|
2330
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'candidate-pin-missing');
|
|
2331
|
+
if (inspection.value === 'diverged')
|
|
2332
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'candidate-pin-diverged');
|
|
2333
|
+
return { active };
|
|
2334
|
+
}
|
|
2335
|
+
const captured = await candidate.captureAndPin({
|
|
2336
|
+
worktreePath: active.record.worktreePath,
|
|
2337
|
+
expectedHeadSha: expectedImplementationHead(active.record),
|
|
2338
|
+
runId: active.record.runId,
|
|
2339
|
+
boundary,
|
|
2340
|
+
artifactDir: config.proof.artifactDir,
|
|
2341
|
+
});
|
|
2342
|
+
if (captured.kind === 'failed')
|
|
2343
|
+
return this.mapCandidateFailure(active, captured.code);
|
|
2344
|
+
return { active: await this.persist(active, {
|
|
2345
|
+
changeBindingVersion: 2,
|
|
2346
|
+
candidateBinding: captured.value,
|
|
2347
|
+
}, 3) };
|
|
2348
|
+
}
|
|
2349
|
+
async cutoverLegacyImmutableBoundary(active, config, frozenCriteria) {
|
|
2350
|
+
const captured = await this.ensureCandidateBinding(active, config, {
|
|
2351
|
+
kind: 'implementation-cycle', cycle: active.record.cycle,
|
|
2352
|
+
});
|
|
2353
|
+
if ('status' in captured)
|
|
2354
|
+
return captured;
|
|
2355
|
+
active = captured.active;
|
|
2356
|
+
const binding = active.record.candidateBinding;
|
|
2357
|
+
const targetFingerprint = directReviewCandidateTargetFingerprint({
|
|
2358
|
+
binding,
|
|
2359
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
2360
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
2361
|
+
cycle: active.record.cycle,
|
|
2362
|
+
frozenCriteria,
|
|
2363
|
+
});
|
|
2364
|
+
return { active: await this.persist(active, {
|
|
2365
|
+
lifecycle: 'implementing',
|
|
2366
|
+
directReview: createInitialDirectReview({
|
|
2367
|
+
targetFingerprint,
|
|
2368
|
+
codeReviewerSessionId: this.dependencies.createReviewSessionId(),
|
|
2369
|
+
}),
|
|
2370
|
+
checks: [], checkedChangeSha256: undefined, proofId: undefined, proofReceipt: undefined,
|
|
2371
|
+
checkQualification: undefined, baselineChecks: undefined,
|
|
2372
|
+
terminalOutcome: undefined, outcomeEvidenceId: undefined,
|
|
2373
|
+
}) };
|
|
2374
|
+
}
|
|
2375
|
+
async prepareCandidateExecution(active, config, operation, operationSourceId) {
|
|
2376
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
2377
|
+
const binding = active.record.candidateBinding;
|
|
2378
|
+
if (!candidate || !binding)
|
|
2379
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'candidate-git-v2-required');
|
|
2380
|
+
const attemptId = sha256(canonicalJson({ bindingId: binding.bindingId, operation, operationSourceId }));
|
|
2381
|
+
const prepared = await candidate.prepareExecution({
|
|
2382
|
+
binding,
|
|
2383
|
+
runId: active.record.runId,
|
|
2384
|
+
workspaceRoot: resolve(dirname(active.record.worktreePath)),
|
|
2385
|
+
operation,
|
|
2386
|
+
attemptId,
|
|
2387
|
+
});
|
|
2388
|
+
if (prepared.kind === 'failed')
|
|
2389
|
+
return this.mapCandidateFailure(active, prepared.code);
|
|
2390
|
+
if (prepared.value.kind === 'path-diverged') {
|
|
2391
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'path-diverged');
|
|
2392
|
+
}
|
|
2393
|
+
const next = await this.persist(active, { executionLease: prepared.value.lease });
|
|
2394
|
+
return { active: next, lease: prepared.value.lease };
|
|
2395
|
+
}
|
|
2396
|
+
async settleCandidateExecution(active, config) {
|
|
2397
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
2398
|
+
const binding = active.record.candidateBinding;
|
|
2399
|
+
const lease = active.record.executionLease;
|
|
2400
|
+
if (!candidate || !binding || !lease)
|
|
2401
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'candidate-execution-state-missing');
|
|
2402
|
+
const inspection = await candidate.inspectExecution({ binding, lease, artifactDir: config.proof.artifactDir });
|
|
2403
|
+
if (inspection.kind === 'failed')
|
|
2404
|
+
return this.mapCandidateFailure(active, inspection.code);
|
|
2405
|
+
if (inspection.value !== 'matching' && inspection.value !== 'missing') {
|
|
2406
|
+
return this.reconcilePersistedCandidateExecution(active, config);
|
|
2407
|
+
}
|
|
2408
|
+
if (inspection.value === 'matching') {
|
|
2409
|
+
const removed = await candidate.removeExecution({ lease, requireProcessAbsent: true });
|
|
2410
|
+
if (removed.kind === 'failed')
|
|
2411
|
+
return this.mapCandidateFailure(active, removed.code);
|
|
2412
|
+
}
|
|
2413
|
+
return { active: await this.persist(active, { executionLease: undefined }) };
|
|
2414
|
+
}
|
|
2415
|
+
async inspectCompletedReviewExecution(active, config) {
|
|
2416
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
2417
|
+
const binding = active.record.candidateBinding;
|
|
2418
|
+
const lease = active.record.executionLease;
|
|
2419
|
+
if (!candidate || !binding || !lease || lease.operation !== 'direct-review') {
|
|
2420
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'candidate-execution-state-missing');
|
|
2421
|
+
}
|
|
2422
|
+
const inspection = await candidate.inspectExecution({ binding, lease, artifactDir: config.proof.artifactDir });
|
|
2423
|
+
if (inspection.kind === 'failed')
|
|
2424
|
+
return this.mapCandidateFailure(active, inspection.code);
|
|
2425
|
+
if (inspection.value !== 'matching')
|
|
2426
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, `candidate-execution-${inspection.value}`);
|
|
2427
|
+
return { lease };
|
|
2428
|
+
}
|
|
2429
|
+
async removeCompletedReviewExecution(active, lease) {
|
|
2430
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
2431
|
+
if (!candidate)
|
|
2432
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'candidate-execution-state-missing');
|
|
2433
|
+
const removed = await candidate.removeExecution({ lease, requireProcessAbsent: true });
|
|
2434
|
+
if (removed.kind === 'failed')
|
|
2435
|
+
return this.mapCandidateFailure(active, removed.code);
|
|
2436
|
+
return { active };
|
|
2437
|
+
}
|
|
2438
|
+
async reconcilePersistedCandidateExecution(active, config) {
|
|
2439
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
2440
|
+
const binding = active.record.candidateBinding;
|
|
2441
|
+
const lease = active.record.executionLease;
|
|
2442
|
+
if (!candidate || !binding || !lease) {
|
|
2443
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'candidate-execution-state-missing');
|
|
2444
|
+
}
|
|
2445
|
+
if (lease.operation === 'direct-review' && lease.phase === 'launched') {
|
|
2446
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'direct-review-canonical-invocation-missing');
|
|
2447
|
+
}
|
|
2448
|
+
if (lease.phase === 'launched') {
|
|
2449
|
+
try {
|
|
2450
|
+
await this.dependencies.waitForReviewProcessAbsence(lease.processGroupId);
|
|
2451
|
+
}
|
|
2452
|
+
catch {
|
|
2453
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'candidate-execution-process-absence-unconfirmed');
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const inspection = await candidate.inspectExecution({ binding, lease, artifactDir: config.proof.artifactDir });
|
|
2457
|
+
if (inspection.kind === 'failed')
|
|
2458
|
+
return this.mapCandidateFailure(active, inspection.code);
|
|
2459
|
+
if (inspection.value === 'missing')
|
|
2460
|
+
return this.persistCandidateEvidenceSafetyTerminal(active, 'candidate-execution-missing');
|
|
2461
|
+
if (inspection.value === 'mutated') {
|
|
2462
|
+
const cleared = await this.persist(active, {
|
|
2463
|
+
executionLease: undefined, changeBindingVersion: undefined, candidateBinding: undefined,
|
|
2464
|
+
});
|
|
2465
|
+
const removed = await candidate.removeExecution({ lease, requireProcessAbsent: true });
|
|
2466
|
+
if (removed.kind === 'failed')
|
|
2467
|
+
return this.mapCandidateFailure(cleared, removed.code);
|
|
2468
|
+
const released = await candidate.releasePin({ binding, expectedPinnedCommitSha: binding.candidateCommitSha });
|
|
2469
|
+
if (released.kind === 'failed')
|
|
2470
|
+
return this.mapCandidateFailure(cleared, released.code);
|
|
2471
|
+
return this.persistTerminal(cleared, { status: 'blocked', kind: 'safety', resumable: false }, 'candidate-materialization-mutated', false);
|
|
2472
|
+
}
|
|
2473
|
+
if (lease.operation === 'acceptance-proof') {
|
|
2474
|
+
return { active };
|
|
2475
|
+
}
|
|
2476
|
+
const cleared = await this.persist(active, { executionLease: undefined });
|
|
2477
|
+
const removed = await candidate.removeExecution({ lease, requireProcessAbsent: true });
|
|
2478
|
+
if (removed.kind === 'failed')
|
|
2479
|
+
return this.mapCandidateFailure(cleared, removed.code);
|
|
2480
|
+
return { active: cleared };
|
|
2481
|
+
}
|
|
2482
|
+
async clearAndReleaseCandidate(active) {
|
|
2483
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
2484
|
+
const binding = active.record.candidateBinding;
|
|
2485
|
+
if (!candidate || !binding)
|
|
2486
|
+
return { active };
|
|
2487
|
+
if (active.record.executionLease)
|
|
2488
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'candidate-execution-not-quiescent');
|
|
2489
|
+
const normalized = await candidate.normalizeSharedIndex({
|
|
2490
|
+
worktreePath: active.record.worktreePath,
|
|
2491
|
+
expectedHeadSha: expectedImplementationHead(active.record),
|
|
2492
|
+
});
|
|
2493
|
+
if (normalized.kind === 'failed')
|
|
2494
|
+
return this.mapCandidateFailure(active, normalized.code);
|
|
2495
|
+
const cleared = await this.persist(active, { changeBindingVersion: undefined, candidateBinding: undefined });
|
|
2496
|
+
const released = await candidate.releasePin({ binding, expectedPinnedCommitSha: binding.candidateCommitSha });
|
|
2497
|
+
if (released.kind === 'failed') {
|
|
2498
|
+
return this.invokedFailure(cleared, 'candidate-pin-release-pending', 'The candidate state transition is durable; orphan reconciliation will retry exact pin cleanup.');
|
|
2499
|
+
}
|
|
2500
|
+
return { active: cleared };
|
|
2501
|
+
}
|
|
2502
|
+
async persistRetainedCommitIntentTerminal(active, evidenceCode) {
|
|
2503
|
+
const intent = active.record.intent;
|
|
2504
|
+
if (!active.record.candidateBinding || (intent?.kind !== 'commit' && intent?.kind !== 'review-update-commit')) {
|
|
2505
|
+
return this.invokedFailure(active, 'retained-candidate-intent-state-invalid');
|
|
2506
|
+
}
|
|
2507
|
+
const evidence = await this.dependencies.writeEvidence({
|
|
2508
|
+
runId: active.record.runId,
|
|
2509
|
+
code: evidenceCode,
|
|
2510
|
+
summary: 'Candidate publication stopped locally with the exact commit intent and pin retained.',
|
|
2511
|
+
});
|
|
2512
|
+
const terminalOutcome = {
|
|
2513
|
+
status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path,
|
|
2514
|
+
};
|
|
2515
|
+
const directReview = active.record.directReview && active.record.directReview.status !== 'terminal'
|
|
2516
|
+
? projectTerminalDirectReview(active.record.directReview, { status: 'blocked', kind: 'safety' })
|
|
2517
|
+
: active.record.directReview;
|
|
2518
|
+
await this.persist(active, {
|
|
2519
|
+
lifecycle: 'blocked',
|
|
2520
|
+
terminalOutcome,
|
|
2521
|
+
outcomeEvidenceId: evidence.id,
|
|
2522
|
+
...(directReview ? { directReview } : {}),
|
|
2523
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
2524
|
+
reviewFeedback: blockReviewFeedback(active.record.reviewFeedback, 'safety', this.timestamp()),
|
|
2525
|
+
} : {}),
|
|
2526
|
+
reportInvocation: undefined,
|
|
2527
|
+
});
|
|
2528
|
+
return publicOutcome(terminalOutcome);
|
|
2529
|
+
}
|
|
2530
|
+
async reconcileUnknownCandidatePublication(active) {
|
|
2531
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
2532
|
+
const binding = active.record.candidateBinding;
|
|
2533
|
+
const intent = active.record.intent;
|
|
2534
|
+
if (!candidate || !binding || (intent?.kind !== 'commit' && intent?.kind !== 'review-update-commit')) {
|
|
2535
|
+
return publicOutcome(active.record.terminalOutcome);
|
|
2536
|
+
}
|
|
2537
|
+
const observation = await candidate.createOrObserveCommit({
|
|
2538
|
+
worktreePath: active.record.worktreePath,
|
|
2539
|
+
branchName: active.record.branchName,
|
|
2540
|
+
parentSha: intent.parentSha,
|
|
2541
|
+
treeSha: intent.treeSha,
|
|
2542
|
+
message: intent.message,
|
|
2543
|
+
candidateRef: intent.candidateRef,
|
|
2544
|
+
observeOnly: true,
|
|
2545
|
+
});
|
|
2546
|
+
if (observation.kind === 'failed') {
|
|
2547
|
+
return publicOutcome(active.record.terminalOutcome);
|
|
2548
|
+
}
|
|
2549
|
+
if (observation.value.kind === 'branch-diverged') {
|
|
2550
|
+
return this.persistRetainedCommitIntentTerminal(active, 'candidate-branch-diverged');
|
|
2551
|
+
}
|
|
2552
|
+
let directReview = active.record.directReview;
|
|
2553
|
+
if (directReview?.status === 'terminal') {
|
|
2554
|
+
const { terminalCode: _terminalCode, terminalOutcome: _terminalOutcome, ...preserved } = structuredClone(directReview);
|
|
2555
|
+
directReview = { ...preserved, status: 'clear' };
|
|
2556
|
+
}
|
|
2557
|
+
return { active: await this.persist(active, {
|
|
2558
|
+
lifecycle: 'publishing', terminalOutcome: undefined, outcomeEvidenceId: undefined,
|
|
2559
|
+
...(directReview ? { directReview } : {}),
|
|
2560
|
+
}) };
|
|
2561
|
+
}
|
|
2562
|
+
async persistCandidateEvidenceSafetyTerminal(active, evidenceCode) {
|
|
2563
|
+
if (!active.record.candidateBinding)
|
|
2564
|
+
return this.invokedFailure(active, 'candidate-evidence-state-invalid');
|
|
2565
|
+
const evidence = await this.dependencies.writeEvidence({
|
|
2566
|
+
runId: active.record.runId,
|
|
2567
|
+
code: evidenceCode,
|
|
2568
|
+
summary: 'Candidate evidence was retained without a Git or GitHub effect.',
|
|
2569
|
+
});
|
|
2570
|
+
const terminalOutcome = {
|
|
2571
|
+
status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path,
|
|
2572
|
+
};
|
|
2573
|
+
const directReview = active.record.directReview && active.record.directReview.status !== 'terminal'
|
|
2574
|
+
? projectTerminalDirectReview(active.record.directReview, { status: 'blocked', kind: 'safety' })
|
|
2575
|
+
: active.record.directReview;
|
|
2576
|
+
await this.persist(active, {
|
|
2577
|
+
lifecycle: 'blocked', terminalOutcome, outcomeEvidenceId: evidence.id,
|
|
2578
|
+
...(directReview ? { directReview } : {}),
|
|
2579
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
2580
|
+
reviewFeedback: blockReviewFeedback(active.record.reviewFeedback, 'safety', this.timestamp()),
|
|
2581
|
+
} : {}),
|
|
2582
|
+
reportInvocation: undefined,
|
|
2583
|
+
});
|
|
2584
|
+
return publicOutcome(terminalOutcome);
|
|
2585
|
+
}
|
|
2586
|
+
mapCandidateFailure(active, code) {
|
|
2587
|
+
if (code === 'candidate-unstable' || code === 'candidate-io-failed' || code === 'candidate-materialization-io-failed') {
|
|
2588
|
+
return this.invokedFailure(active, code, 'Candidate operation failed before an effect and may be retried without consuming a repair budget.');
|
|
2589
|
+
}
|
|
2590
|
+
return this.terminal(active, { status: 'transport-failed', resumable: false }, code);
|
|
2591
|
+
}
|
|
2008
2592
|
async runImplementation(input) {
|
|
2009
2593
|
try {
|
|
2010
2594
|
return await this.dependencies.implementationAgent.run({
|
|
@@ -2017,6 +2601,23 @@ export class RunIssue {
|
|
|
2017
2601
|
return { kind: 'internal-error' };
|
|
2018
2602
|
}
|
|
2019
2603
|
}
|
|
2604
|
+
mutableInvocationState(current, replace) {
|
|
2605
|
+
return {
|
|
2606
|
+
read: async () => structuredClone(current().record.mutableInvocation),
|
|
2607
|
+
compareAndSwap: async (expected, next) => {
|
|
2608
|
+
const active = current();
|
|
2609
|
+
if (canonicalJson(active.record.mutableInvocation ?? null) !== canonicalJson(expected ?? null))
|
|
2610
|
+
return false;
|
|
2611
|
+
try {
|
|
2612
|
+
replace(await this.persist(active, { mutableInvocation: next }));
|
|
2613
|
+
return true;
|
|
2614
|
+
}
|
|
2615
|
+
catch {
|
|
2616
|
+
return false;
|
|
2617
|
+
}
|
|
2618
|
+
},
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2020
2621
|
async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles, config) {
|
|
2021
2622
|
let active = starting;
|
|
2022
2623
|
let reportRepair;
|
|
@@ -2034,12 +2635,52 @@ export class RunIssue {
|
|
|
2034
2635
|
? directReviewClosureRequestSha256(directReview)
|
|
2035
2636
|
: null;
|
|
2036
2637
|
const mode = directReview.stage === 'review-closure' ? 'closure' : 'full';
|
|
2037
|
-
const
|
|
2038
|
-
if (
|
|
2039
|
-
|
|
2638
|
+
const reviewerAuthorization = await this.observeFeedbackWorker(active, config, active.record.issueNumber);
|
|
2639
|
+
if (reviewerAuthorization.status !== 'valid') {
|
|
2640
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
2641
|
+
if ('status' in released)
|
|
2642
|
+
return released;
|
|
2643
|
+
active = released.active;
|
|
2644
|
+
return reviewerAuthorization.status === 'retryable'
|
|
2645
|
+
? this.invokedFailure(active, `${reviewerAuthorization.code}-retryable`)
|
|
2646
|
+
: this.blockReviewFeedback(active, 'safety', reviewerAuthorization.code);
|
|
2647
|
+
}
|
|
2648
|
+
let executionLease;
|
|
2649
|
+
if (active.record.reportInvocation?.operation === 'code-review') {
|
|
2650
|
+
if (active.record.executionLease?.operation !== 'direct-review') {
|
|
2651
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-candidate-execution-missing');
|
|
2652
|
+
}
|
|
2653
|
+
executionLease = active.record.executionLease;
|
|
2654
|
+
}
|
|
2655
|
+
else {
|
|
2656
|
+
const execution = await this.prepareCandidateExecution(active, config, 'direct-review', `${reviewerSessionId}:${directReview.targetRevision}:${mode}:${implementationAttemptId}`);
|
|
2657
|
+
if ('status' in execution)
|
|
2658
|
+
return execution;
|
|
2659
|
+
active = execution.active;
|
|
2660
|
+
executionLease = execution.lease;
|
|
2661
|
+
}
|
|
2662
|
+
const abortPreparedReview = async (validation) => {
|
|
2663
|
+
const settledExecution = await this.settleCandidateExecution(active, config);
|
|
2664
|
+
if ('status' in settledExecution)
|
|
2665
|
+
return settledExecution;
|
|
2666
|
+
active = settledExecution.active;
|
|
2667
|
+
if (active.record.reportInvocation)
|
|
2668
|
+
return this.invokedFailure(active, 'direct-review-authorization-changed-after-prepare');
|
|
2669
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
2670
|
+
if ('status' in released)
|
|
2671
|
+
return released;
|
|
2672
|
+
active = released.active;
|
|
2673
|
+
return validation.status === 'retryable'
|
|
2674
|
+
? this.invokedFailure(active, `${validation.code}-retryable`)
|
|
2675
|
+
: this.blockReviewFeedback(active, 'safety', validation.code);
|
|
2676
|
+
};
|
|
2677
|
+
const launchAuthorization = await this.observeFeedbackWorker(active, config, active.record.issueNumber);
|
|
2678
|
+
if (launchAuthorization.status !== 'valid')
|
|
2679
|
+
return abortPreparedReview(launchAuthorization);
|
|
2680
|
+
let launchAuthorizationFailure;
|
|
2040
2681
|
const result = await this.dependencies.implementationReviewer.run({
|
|
2041
2682
|
runId: active.record.runId,
|
|
2042
|
-
worktreePath:
|
|
2683
|
+
worktreePath: executionLease.path,
|
|
2043
2684
|
operation: 'code-review',
|
|
2044
2685
|
mode,
|
|
2045
2686
|
reviewerSessionId,
|
|
@@ -2061,33 +2702,48 @@ export class RunIssue {
|
|
|
2061
2702
|
validationDiagnostic: reportRepair?.diagnostic ?? null,
|
|
2062
2703
|
originalReportBytes: reportRepair?.originalReportBytes ?? null,
|
|
2063
2704
|
signal: this.signal,
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2705
|
+
invocationState: this.reportInvocationState(() => active, (next) => { active = next; }, {
|
|
2706
|
+
beforeLaunch: async () => {
|
|
2707
|
+
const authorization = await this.observeFeedbackWorker(active, config, active.record.issueNumber);
|
|
2708
|
+
if (authorization.status !== 'valid') {
|
|
2709
|
+
launchAuthorizationFailure = authorization;
|
|
2710
|
+
throw new Error('direct review launch authorization changed');
|
|
2711
|
+
}
|
|
2712
|
+
},
|
|
2713
|
+
launchChanges: (invocation) => {
|
|
2714
|
+
const lease = active.record.executionLease;
|
|
2715
|
+
if (!lease || invocation.pid === null || invocation.processGroupId === null || invocation.launchedAt === null) {
|
|
2716
|
+
throw new Error('direct review candidate execution is missing at launch');
|
|
2717
|
+
}
|
|
2718
|
+
return {
|
|
2719
|
+
executionLease: this.dependencies.git.candidateV2.markExecutionLaunched({
|
|
2720
|
+
lease, pid: invocation.pid, processGroupId: invocation.processGroupId, launchedAt: invocation.launchedAt,
|
|
2721
|
+
}),
|
|
2722
|
+
};
|
|
2723
|
+
},
|
|
2724
|
+
}),
|
|
2080
2725
|
});
|
|
2726
|
+
if (launchAuthorizationFailure)
|
|
2727
|
+
return abortPreparedReview(launchAuthorizationFailure);
|
|
2728
|
+
let adoptedExecution;
|
|
2729
|
+
if (result.kind === 'completed' || result.kind === 'report-invalid') {
|
|
2730
|
+
const inspected = await this.inspectCompletedReviewExecution(active, config);
|
|
2731
|
+
if ('status' in inspected)
|
|
2732
|
+
return inspected;
|
|
2733
|
+
adoptedExecution = inspected.lease;
|
|
2734
|
+
}
|
|
2735
|
+
else if (result.kind !== 'safe-halt') {
|
|
2736
|
+
const settledExecution = await this.settleCandidateExecution(active, config);
|
|
2737
|
+
if ('status' in settledExecution)
|
|
2738
|
+
return settledExecution;
|
|
2739
|
+
active = settledExecution.active;
|
|
2740
|
+
}
|
|
2081
2741
|
if (result.kind === 'completed') {
|
|
2082
2742
|
const current = active.record.directReview;
|
|
2083
2743
|
if (!current)
|
|
2084
2744
|
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
2085
2745
|
if (result.report.verdict === 'needs-work') {
|
|
2086
|
-
|
|
2087
|
-
return active.record.reviewFeedback?.activeBatch
|
|
2088
|
-
? this.blockReviewFeedback(active, 'exhausted', 'direct-review-repair-exhausted')
|
|
2089
|
-
: this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
|
|
2090
|
-
}
|
|
2746
|
+
const exhausted = this.repairBudgetExhausted(active, maxCycles);
|
|
2091
2747
|
const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
|
|
2092
2748
|
const findings = [
|
|
2093
2749
|
...result.report.defects
|
|
@@ -2097,13 +2753,15 @@ export class RunIssue {
|
|
|
2097
2753
|
.filter((finding) => finding.status === 'reopened')
|
|
2098
2754
|
.map((finding) => `${finding.id}: ${finding.summary}`),
|
|
2099
2755
|
];
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2756
|
+
active = await this.persist(active, {
|
|
2757
|
+
...(!exhausted ? {
|
|
2758
|
+
lifecycle: 'implementing',
|
|
2759
|
+
cycle: active.record.reviewFeedback?.activeBatch
|
|
2760
|
+
? active.record.cycle
|
|
2761
|
+
: (active.record.cycle + 1),
|
|
2762
|
+
...(active.record.reviewFeedback?.activeBatch ? {
|
|
2763
|
+
reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
|
|
2764
|
+
} : {}),
|
|
2107
2765
|
} : {}),
|
|
2108
2766
|
directReview: repaired,
|
|
2109
2767
|
reworkFindings: findings,
|
|
@@ -2111,23 +2769,48 @@ export class RunIssue {
|
|
|
2111
2769
|
checkedChangeSha256: undefined,
|
|
2112
2770
|
proofId: undefined,
|
|
2113
2771
|
proofReceipt: undefined,
|
|
2772
|
+
reportInvocation: undefined,
|
|
2773
|
+
executionLease: undefined,
|
|
2114
2774
|
});
|
|
2775
|
+
const removed = await this.removeCompletedReviewExecution(active, adoptedExecution);
|
|
2776
|
+
if ('status' in removed)
|
|
2777
|
+
return removed;
|
|
2778
|
+
active = removed.active;
|
|
2779
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
2780
|
+
if ('status' in released)
|
|
2781
|
+
return released;
|
|
2782
|
+
active = released.active;
|
|
2783
|
+
if (exhausted) {
|
|
2784
|
+
return active.record.reviewFeedback?.activeBatch
|
|
2785
|
+
? this.blockReviewFeedback(active, 'exhausted', 'direct-review-repair-exhausted')
|
|
2786
|
+
: this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
|
|
2787
|
+
}
|
|
2788
|
+
return active;
|
|
2115
2789
|
}
|
|
2116
|
-
if (result.report.verdict !== 'approved')
|
|
2117
|
-
|
|
2118
|
-
|
|
2790
|
+
if (result.report.verdict !== 'approved') {
|
|
2791
|
+
const terminal = await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-rejected');
|
|
2792
|
+
await this.dependencies.git.candidateV2.removeExecution({ lease: adoptedExecution, requireProcessAbsent: true });
|
|
2793
|
+
return terminal;
|
|
2794
|
+
}
|
|
2795
|
+
active = await this.persist(active, {
|
|
2119
2796
|
lifecycle: 'checking',
|
|
2120
2797
|
directReview: acceptApprovedDirectReview(current, result.report, result.artifactSha256),
|
|
2798
|
+
reportInvocation: undefined,
|
|
2799
|
+
executionLease: undefined,
|
|
2121
2800
|
});
|
|
2801
|
+
const removed = await this.removeCompletedReviewExecution(active, adoptedExecution);
|
|
2802
|
+
return 'status' in removed ? removed : removed.active;
|
|
2122
2803
|
}
|
|
2123
2804
|
if (result.kind === 'report-invalid') {
|
|
2124
2805
|
const current = active.record.directReview;
|
|
2125
2806
|
if (!current)
|
|
2126
2807
|
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
2127
2808
|
if (current.review.reportRepairs >= MAX_DIRECT_REVIEW_REPORT_REPAIRS) {
|
|
2128
|
-
|
|
2809
|
+
const terminal = await this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' }, 'direct-review-report-malformed');
|
|
2810
|
+
await this.dependencies.git.candidateV2.removeExecution({ lease: adoptedExecution, requireProcessAbsent: true });
|
|
2811
|
+
return terminal;
|
|
2129
2812
|
}
|
|
2130
|
-
const
|
|
2813
|
+
const withoutInvocation = structuredClone(current);
|
|
2131
2814
|
active = await this.persist(active, {
|
|
2132
2815
|
directReview: {
|
|
2133
2816
|
...withoutInvocation,
|
|
@@ -2136,7 +2819,13 @@ export class RunIssue {
|
|
|
2136
2819
|
reportRepairs: (withoutInvocation.review.reportRepairs + 1),
|
|
2137
2820
|
},
|
|
2138
2821
|
},
|
|
2822
|
+
reportInvocation: undefined,
|
|
2823
|
+
executionLease: undefined,
|
|
2139
2824
|
});
|
|
2825
|
+
const removed = await this.removeCompletedReviewExecution(active, adoptedExecution);
|
|
2826
|
+
if ('status' in removed)
|
|
2827
|
+
return removed;
|
|
2828
|
+
active = removed.active;
|
|
2140
2829
|
reportRepair = {
|
|
2141
2830
|
originalReportSha256: result.originalReportSha256,
|
|
2142
2831
|
originalReportBytes: Buffer.from(result.originalReportBytes),
|
|
@@ -2145,70 +2834,16 @@ export class RunIssue {
|
|
|
2145
2834
|
continue;
|
|
2146
2835
|
}
|
|
2147
2836
|
if (result.kind === 'transport-failed') {
|
|
2148
|
-
|
|
2149
|
-
if (!current || current.review.transportRetries >= 1) {
|
|
2150
|
-
return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
|
|
2151
|
-
}
|
|
2152
|
-
const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
|
|
2153
|
-
active = await this.persist(active, {
|
|
2154
|
-
directReview: {
|
|
2155
|
-
...withoutInvocation,
|
|
2156
|
-
review: { ...withoutInvocation.review, transportRetries: 1 },
|
|
2157
|
-
},
|
|
2158
|
-
});
|
|
2159
|
-
continue;
|
|
2837
|
+
return this.invokedFailure(active, 'direct-review-transport-failed');
|
|
2160
2838
|
}
|
|
2161
2839
|
if (result.kind === 'safe-halt') {
|
|
2162
|
-
|
|
2163
|
-
lifecycle: 'safe-halt',
|
|
2164
|
-
process: {
|
|
2165
|
-
...result.process,
|
|
2166
|
-
purpose: 'code-review',
|
|
2167
|
-
resumeLifecycle: 'implementing',
|
|
2168
|
-
resumeReviewStage: directReview.stage,
|
|
2169
|
-
},
|
|
2170
|
-
});
|
|
2171
|
-
try {
|
|
2172
|
-
await result.waitForAbsence();
|
|
2173
|
-
}
|
|
2174
|
-
catch {
|
|
2175
|
-
return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-unconfirmed');
|
|
2176
|
-
}
|
|
2177
|
-
return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-delayed');
|
|
2840
|
+
return this.invokedFailure(active, result.code, 'Code-review process ownership remains unresolved; no relaunch was attempted.');
|
|
2178
2841
|
}
|
|
2179
2842
|
if (result.kind === 'cancelled')
|
|
2180
2843
|
return this.terminal(active, { status: 'cancelled' });
|
|
2181
2844
|
return this.terminal(active, { status: 'internal-error', code: result.code });
|
|
2182
2845
|
}
|
|
2183
2846
|
}
|
|
2184
|
-
async recoverDirectReviewInvocation(starting) {
|
|
2185
|
-
const directReview = starting.record.directReview;
|
|
2186
|
-
const invocation = directReview?.invocation;
|
|
2187
|
-
if (!directReview || !invocation)
|
|
2188
|
-
return starting;
|
|
2189
|
-
const { invocation: _invocation, ...withoutInvocation } = structuredClone(directReview);
|
|
2190
|
-
if (invocation.status === 'prepared') {
|
|
2191
|
-
return this.persist(starting, { directReview: withoutInvocation });
|
|
2192
|
-
}
|
|
2193
|
-
if (invocation.status === 'launched') {
|
|
2194
|
-
try {
|
|
2195
|
-
await this.dependencies.waitForReviewProcessAbsence(invocation.processGroupId);
|
|
2196
|
-
}
|
|
2197
|
-
catch {
|
|
2198
|
-
return this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-process-absence-unconfirmed');
|
|
2199
|
-
}
|
|
2200
|
-
if (directReview.review.transportRetries >= 1) {
|
|
2201
|
-
return this.terminal(starting, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
|
|
2202
|
-
}
|
|
2203
|
-
return this.persist(starting, {
|
|
2204
|
-
directReview: {
|
|
2205
|
-
...withoutInvocation,
|
|
2206
|
-
review: { ...withoutInvocation.review, transportRetries: 1 },
|
|
2207
|
-
},
|
|
2208
|
-
});
|
|
2209
|
-
}
|
|
2210
|
-
return this.persist(starting, { directReview: withoutInvocation });
|
|
2211
|
-
}
|
|
2212
2847
|
async qualifyChecks(starting, config, issue, frozenCriteria) {
|
|
2213
2848
|
let active = starting;
|
|
2214
2849
|
let checkPolicy;
|
|
@@ -2226,70 +2861,74 @@ export class RunIssue {
|
|
|
2226
2861
|
? existingQualification
|
|
2227
2862
|
: undefined;
|
|
2228
2863
|
let repairAttempts = prior?.repairAttempts ?? 0;
|
|
2229
|
-
const pendingInvocation = existingQualification?.repairInvocation ?? null;
|
|
2230
|
-
if (pendingInvocation) {
|
|
2231
|
-
if (pendingInvocation.phase === 'launched') {
|
|
2232
|
-
try {
|
|
2233
|
-
await this.dependencies.waitForReviewProcessAbsence(pendingInvocation.processGroupId);
|
|
2234
|
-
}
|
|
2235
|
-
catch {
|
|
2236
|
-
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-process-absence-unconfirmed');
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
if (!this.dependencies.implementationAgent.recover) {
|
|
2240
|
-
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-report-recovery-unavailable');
|
|
2241
|
-
}
|
|
2242
|
-
let recovered;
|
|
2243
|
-
try {
|
|
2244
|
-
recovered = await this.dependencies.implementationAgent.recover({
|
|
2245
|
-
runId: active.record.runId,
|
|
2246
|
-
canonicalRepository: active.record.canonicalRepository,
|
|
2247
|
-
invocation: pendingInvocation,
|
|
2248
|
-
});
|
|
2249
|
-
}
|
|
2250
|
-
catch {
|
|
2251
|
-
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-report-recovery-failed');
|
|
2252
|
-
}
|
|
2253
|
-
if (recovered) {
|
|
2254
|
-
const settled = await this.settleQualificationRepair(active, config, recovered);
|
|
2255
|
-
if ('status' in settled)
|
|
2256
|
-
return settled;
|
|
2257
|
-
active = settled.active;
|
|
2258
|
-
}
|
|
2259
|
-
else {
|
|
2260
|
-
const deniedPathsBaseline = active.record.checkQualification?.deniedPathsBaseline;
|
|
2261
|
-
if (!deniedPathsBaseline
|
|
2262
|
-
|| await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
2263
|
-
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'denied-path-modified');
|
|
2264
|
-
}
|
|
2265
|
-
if (!sameFreshness(await this.dependencies.git.snapshot(active.record.worktreePath), pendingInvocation.baseline)) {
|
|
2266
|
-
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'qualification-repair-worktree-ambiguous');
|
|
2267
|
-
}
|
|
2268
|
-
active = await this.clearQualificationRepairInvocation(active);
|
|
2269
|
-
}
|
|
2270
|
-
}
|
|
2271
2864
|
if (!prior) {
|
|
2272
2865
|
active = await this.persist(active, {
|
|
2273
2866
|
checkQualification: {
|
|
2274
2867
|
version: 1, checkPolicySha256, repairAttempts, checks: [],
|
|
2275
|
-
implementationStarted: false, repairInvocation: null,
|
|
2276
2868
|
},
|
|
2277
2869
|
baselineChecks: undefined,
|
|
2278
2870
|
});
|
|
2279
2871
|
}
|
|
2872
|
+
if (active.record.mutableInvocation?.operation === 'qualification-repair') {
|
|
2873
|
+
const findings = active.record.checkQualification?.repairFindings;
|
|
2874
|
+
if (!findings?.length)
|
|
2875
|
+
return this.invokedFailure(active, 'qualification-repair-correlation-missing');
|
|
2876
|
+
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths);
|
|
2877
|
+
const invocationState = this.mutableInvocationState(() => active, (next) => { active = next; });
|
|
2878
|
+
const recovered = await this.runImplementation({
|
|
2879
|
+
operation: 'qualification-repair', runId: active.record.runId, worktreePath: active.record.worktreePath,
|
|
2880
|
+
issue, frozenCriteria, cycle: repairAttempts + 1, reworkFindings: findings, repairOnly: false,
|
|
2881
|
+
workflowGeneration: active.record.workflowGeneration, phaseFacts: [deniedPathsBaseline], invocationState,
|
|
2882
|
+
});
|
|
2883
|
+
if (recovered.kind === 'safe-halt')
|
|
2884
|
+
return this.invokedFailure(active, recovered.code);
|
|
2885
|
+
if (recovered.kind === 'transport-failed')
|
|
2886
|
+
return this.invokedFailure(active, recovered.code ?? 'qualification-repair-agent-failed');
|
|
2887
|
+
if (recovered.kind === 'cancelled')
|
|
2888
|
+
return this.terminal(active, { status: 'cancelled' });
|
|
2889
|
+
if (recovered.kind !== 'completed')
|
|
2890
|
+
return this.invokedFailure(active, 'qualification-repair-agent-failed');
|
|
2891
|
+
const settled = await this.settleQualificationRepair(active, config, recovered, deniedPathsBaseline);
|
|
2892
|
+
if ('status' in settled)
|
|
2893
|
+
return settled;
|
|
2894
|
+
active = settled.active;
|
|
2895
|
+
repairAttempts = active.record.checkQualification.repairAttempts;
|
|
2896
|
+
}
|
|
2280
2897
|
while (true) {
|
|
2898
|
+
const captured = await this.ensureCandidateBinding(active, config, {
|
|
2899
|
+
kind: 'qualification', repairAttempt: repairAttempts,
|
|
2900
|
+
});
|
|
2901
|
+
if ('status' in captured)
|
|
2902
|
+
return captured;
|
|
2903
|
+
active = captured.active;
|
|
2904
|
+
const qualificationBinding = active.record.candidateBinding;
|
|
2281
2905
|
const checks = [];
|
|
2282
2906
|
const failures = [];
|
|
2283
2907
|
for (const [id, command] of configuredChecks) {
|
|
2284
2908
|
if (this.signal.aborted)
|
|
2285
2909
|
return this.terminal(active, { status: 'cancelled' });
|
|
2910
|
+
if (this.dependencies.checks.supportsLaunchOwnership !== true) {
|
|
2911
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'candidate-check-launch-ownership-required');
|
|
2912
|
+
}
|
|
2913
|
+
const execution = await this.prepareCandidateExecution(active, config, 'qualification-check', `${id}:${checkPolicySha256}`);
|
|
2914
|
+
if ('status' in execution)
|
|
2915
|
+
return execution;
|
|
2916
|
+
active = execution.active;
|
|
2917
|
+
const lease = execution.lease;
|
|
2286
2918
|
let check;
|
|
2287
2919
|
try {
|
|
2288
2920
|
check = await this.dependencies.checks.run({
|
|
2289
2921
|
id, command, source: checkPolicy.source,
|
|
2290
|
-
cwd:
|
|
2922
|
+
cwd: lease.path,
|
|
2291
2923
|
phase: 'qualification',
|
|
2292
2924
|
signal: this.signal,
|
|
2925
|
+
onLaunched: async ({ pid, processGroupId }) => {
|
|
2926
|
+
active = await this.persist(active, {
|
|
2927
|
+
executionLease: this.dependencies.git.candidateV2.markExecutionLaunched({
|
|
2928
|
+
lease, pid, processGroupId, launchedAt: this.timestamp(),
|
|
2929
|
+
}),
|
|
2930
|
+
});
|
|
2931
|
+
},
|
|
2293
2932
|
});
|
|
2294
2933
|
}
|
|
2295
2934
|
catch (error) {
|
|
@@ -2298,9 +2937,18 @@ export class RunIssue {
|
|
|
2298
2937
|
}
|
|
2299
2938
|
return this.invokedFailure(active, 'qualification-check-execution-failed', error instanceof Error ? error.message : 'The qualification check process did not start or settle. Retry the same run.');
|
|
2300
2939
|
}
|
|
2940
|
+
const settledExecution = await this.settleCandidateExecution(active, config);
|
|
2941
|
+
if ('status' in settledExecution)
|
|
2942
|
+
return settledExecution;
|
|
2943
|
+
active = settledExecution.active;
|
|
2301
2944
|
if (this.signal.aborted)
|
|
2302
2945
|
return this.terminal(active, { status: 'cancelled' });
|
|
2303
|
-
checks.push({
|
|
2946
|
+
checks.push({
|
|
2947
|
+
id, command, status: check.status, outputSha256: check.outputSha256,
|
|
2948
|
+
bindingId: qualificationBinding.bindingId,
|
|
2949
|
+
candidateTreeSha: qualificationBinding.candidateTreeSha,
|
|
2950
|
+
checkPolicySha256,
|
|
2951
|
+
});
|
|
2304
2952
|
if (check.status === 'failed') {
|
|
2305
2953
|
failures.push(`Pre-implementation scoped check ${id} failed (${command}):\n${check.output.toString('utf8').slice(0, 8 * 1024)}`);
|
|
2306
2954
|
}
|
|
@@ -2308,19 +2956,28 @@ export class RunIssue {
|
|
|
2308
2956
|
active = await this.persist(active, {
|
|
2309
2957
|
checkQualification: {
|
|
2310
2958
|
version: 1, checkPolicySha256, repairAttempts, checks,
|
|
2311
|
-
implementationStarted: false, repairInvocation: null,
|
|
2312
2959
|
},
|
|
2313
2960
|
baselineChecks: undefined,
|
|
2314
2961
|
});
|
|
2315
|
-
if (failures.length === 0)
|
|
2316
|
-
|
|
2962
|
+
if (failures.length === 0) {
|
|
2963
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
2964
|
+
return 'status' in released ? released : { active: released.active };
|
|
2965
|
+
}
|
|
2317
2966
|
if (repairAttempts >= config.runner.maxCycles) {
|
|
2318
2967
|
return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'qualification-repair-exhausted');
|
|
2319
2968
|
}
|
|
2320
2969
|
if (!await this.authorized(active, config)) {
|
|
2321
2970
|
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
2322
2971
|
}
|
|
2972
|
+
const released = await this.clearAndReleaseCandidate(active);
|
|
2973
|
+
if ('status' in released)
|
|
2974
|
+
return released;
|
|
2975
|
+
active = released.active;
|
|
2323
2976
|
const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths);
|
|
2977
|
+
active = await this.persist(active, {
|
|
2978
|
+
checkQualification: { ...active.record.checkQualification, repairFindings: [...failures] },
|
|
2979
|
+
});
|
|
2980
|
+
const invocationState = this.mutableInvocationState(() => active, (next) => { active = next; });
|
|
2324
2981
|
const repair = await this.runImplementation({
|
|
2325
2982
|
operation: 'qualification-repair',
|
|
2326
2983
|
runId: active.record.runId,
|
|
@@ -2331,82 +2988,37 @@ export class RunIssue {
|
|
|
2331
2988
|
reworkFindings: failures,
|
|
2332
2989
|
repairOnly: false,
|
|
2333
2990
|
workflowGeneration: active.record.workflowGeneration,
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
checkQualification: {
|
|
2337
|
-
...active.record.checkQualification,
|
|
2338
|
-
implementationStarted: false,
|
|
2339
|
-
deniedPathsBaseline,
|
|
2340
|
-
repairInvocation: {
|
|
2341
|
-
...structuredClone(invocation), phase: 'prepared',
|
|
2342
|
-
pid: null, processGroupId: null, launchedAt: null,
|
|
2343
|
-
},
|
|
2344
|
-
},
|
|
2345
|
-
});
|
|
2346
|
-
},
|
|
2347
|
-
onLaunched: async (invocation) => {
|
|
2348
|
-
const current = active.record.checkQualification?.repairInvocation;
|
|
2349
|
-
if (!current || current.phase !== 'prepared' || current.attemptId !== invocation.attemptId) {
|
|
2350
|
-
throw new Error('qualification repair launch mismatch');
|
|
2351
|
-
}
|
|
2352
|
-
repairAttempts = (repairAttempts + 1);
|
|
2353
|
-
active = await this.persist(active, {
|
|
2354
|
-
checkQualification: {
|
|
2355
|
-
...active.record.checkQualification, repairAttempts,
|
|
2356
|
-
repairInvocation: {
|
|
2357
|
-
...current, phase: 'launched', pid: invocation.pid,
|
|
2358
|
-
processGroupId: invocation.processGroupId, launchedAt: invocation.launchedAt,
|
|
2359
|
-
},
|
|
2360
|
-
},
|
|
2361
|
-
});
|
|
2362
|
-
},
|
|
2991
|
+
phaseFacts: [deniedPathsBaseline],
|
|
2992
|
+
invocationState,
|
|
2363
2993
|
});
|
|
2364
2994
|
if (repair.kind === 'safe-halt') {
|
|
2365
|
-
|
|
2366
|
-
lifecycle: 'safe-halt',
|
|
2367
|
-
process: {
|
|
2368
|
-
...repair.process,
|
|
2369
|
-
purpose: 'implementation',
|
|
2370
|
-
resumeLifecycle: 'implementing',
|
|
2371
|
-
resumeReviewStage: null,
|
|
2372
|
-
},
|
|
2373
|
-
});
|
|
2374
|
-
while (true) {
|
|
2375
|
-
try {
|
|
2376
|
-
await repair.waitForAbsence();
|
|
2377
|
-
break;
|
|
2378
|
-
}
|
|
2379
|
-
catch {
|
|
2380
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
if (await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
2384
|
-
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
|
|
2385
|
-
}
|
|
2386
|
-
return this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
|
|
2995
|
+
return this.invokedFailure(active, repair.code);
|
|
2387
2996
|
}
|
|
2388
2997
|
if (repair.kind !== 'completed'
|
|
2389
2998
|
&& await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
2390
|
-
return this.
|
|
2999
|
+
return this.invokedFailure(active, 'qualification-repair-effect-unresolved');
|
|
2391
3000
|
}
|
|
2392
3001
|
if (repair.kind === 'cancelled')
|
|
2393
3002
|
return this.terminal(active, { status: 'cancelled' });
|
|
2394
3003
|
if (repair.kind !== 'completed') {
|
|
2395
3004
|
return this.invokedFailure(active, 'qualification-repair-agent-failed', 'Qualification repair did not complete. Retry the same run.');
|
|
2396
3005
|
}
|
|
2397
|
-
const settled = await this.settleQualificationRepair(active, config, repair);
|
|
3006
|
+
const settled = await this.settleQualificationRepair(active, config, repair, deniedPathsBaseline);
|
|
2398
3007
|
if ('status' in settled)
|
|
2399
3008
|
return settled;
|
|
2400
3009
|
active = settled.active;
|
|
3010
|
+
repairAttempts = active.record.checkQualification.repairAttempts;
|
|
2401
3011
|
}
|
|
2402
3012
|
}
|
|
2403
|
-
async settleQualificationRepair(active, config, repair) {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|| await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
3013
|
+
async settleQualificationRepair(active, config, repair, deniedPathsBaseline) {
|
|
3014
|
+
if (await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
|
|
3015
|
+
active = await this.persist(active, { mutableInvocation: undefined });
|
|
2407
3016
|
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'denied-path-modified');
|
|
2408
3017
|
}
|
|
2409
|
-
|
|
3018
|
+
const qualification = { ...active.record.checkQualification };
|
|
3019
|
+
delete qualification.repairFindings;
|
|
3020
|
+
qualification.repairAttempts = (qualification.repairAttempts + 1);
|
|
3021
|
+
active = await this.persist(active, { checkQualification: qualification, mutableInvocation: undefined });
|
|
2410
3022
|
let report;
|
|
2411
3023
|
try {
|
|
2412
3024
|
report = validateImplementationReport(repair.report);
|
|
@@ -2426,71 +3038,6 @@ export class RunIssue {
|
|
|
2426
3038
|
}
|
|
2427
3039
|
return { active };
|
|
2428
3040
|
}
|
|
2429
|
-
async clearQualificationRepairInvocation(active) {
|
|
2430
|
-
const qualification = { ...active.record.checkQualification, implementationStarted: false, repairInvocation: null };
|
|
2431
|
-
delete qualification.deniedPathsBaseline;
|
|
2432
|
-
return this.persist(active, { checkQualification: qualification });
|
|
2433
|
-
}
|
|
2434
|
-
hasRecordedImplementationStart(record, config) {
|
|
2435
|
-
const qualification = record.checkQualification;
|
|
2436
|
-
if (!qualification)
|
|
2437
|
-
return false;
|
|
2438
|
-
let policy;
|
|
2439
|
-
try {
|
|
2440
|
-
policy = resolveIssueCheckPolicy(record.issueSnapshot.body, config.checks);
|
|
2441
|
-
}
|
|
2442
|
-
catch {
|
|
2443
|
-
return false;
|
|
2444
|
-
}
|
|
2445
|
-
return qualification.implementationStarted === true
|
|
2446
|
-
&& qualification.checkPolicySha256 === sha256(canonicalJson(policy.checks))
|
|
2447
|
-
&& sameCheckPolicy(qualification.checks, policy.checks)
|
|
2448
|
-
&& qualification.checks.every((check) => check.status === 'passed');
|
|
2449
|
-
}
|
|
2450
|
-
async recoverTerminalReviewReport(active, issue, config, frozenCriteria) {
|
|
2451
|
-
const directReview = active.record.directReview;
|
|
2452
|
-
const allowLegacyMalformed = directReview?.terminalCode === undefined
|
|
2453
|
-
&& active.record.outcomeEvidenceId === `evidence:${active.record.runId}:direct-review-report-malformed`;
|
|
2454
|
-
if (!issue || !directReview || active.record.intent || active.record.process
|
|
2455
|
-
|| active.record.terminalOutcome?.status !== 'internal-error'
|
|
2456
|
-
|| directReview.status !== 'terminal' || directReview.terminalOutcome?.status !== 'internal-error'
|
|
2457
|
-
|| !canRecoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed })
|
|
2458
|
-
|| !this.isAuthorizedIssue(issue, active.record, config))
|
|
2459
|
-
return undefined;
|
|
2460
|
-
let worktree;
|
|
2461
|
-
try {
|
|
2462
|
-
worktree = await this.dependencies.git.inspectWorktree({
|
|
2463
|
-
worktreePath: active.record.worktreePath,
|
|
2464
|
-
branchName: active.record.branchName,
|
|
2465
|
-
baseSha: active.record.baseSha,
|
|
2466
|
-
});
|
|
2467
|
-
}
|
|
2468
|
-
catch {
|
|
2469
|
-
return undefined;
|
|
2470
|
-
}
|
|
2471
|
-
if (worktree !== 'matching'
|
|
2472
|
-
|| await this.dependencies.git.getHead(active.record.worktreePath) !== expectedImplementationHead(active.record))
|
|
2473
|
-
return undefined;
|
|
2474
|
-
const changedFiles = await this.dependencies.git.listChangedFiles(active.record.worktreePath);
|
|
2475
|
-
if (changedFiles.length === 0 || !active.record.routeReceipt)
|
|
2476
|
-
return undefined;
|
|
2477
|
-
const targetFingerprint = directReviewTargetFingerprint({
|
|
2478
|
-
snapshot: await this.dependencies.git.snapshot(active.record.worktreePath),
|
|
2479
|
-
changedFiles,
|
|
2480
|
-
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
2481
|
-
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
2482
|
-
cycle: active.record.cycle,
|
|
2483
|
-
frozenCriteria,
|
|
2484
|
-
});
|
|
2485
|
-
if (targetFingerprint !== directReview.targetFingerprint)
|
|
2486
|
-
return undefined;
|
|
2487
|
-
return this.persist(active, {
|
|
2488
|
-
lifecycle: 'implementing',
|
|
2489
|
-
directReview: recoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed }),
|
|
2490
|
-
terminalOutcome: undefined,
|
|
2491
|
-
outcomeEvidenceId: undefined,
|
|
2492
|
-
});
|
|
2493
|
-
}
|
|
2494
3041
|
async startNextCycle(active, findings, sources) {
|
|
2495
3042
|
if (active.record.directReview?.status === 'clear') {
|
|
2496
3043
|
const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
|
|
@@ -2511,6 +3058,8 @@ export class RunIssue {
|
|
|
2511
3058
|
}));
|
|
2512
3059
|
return this.persist(active, {
|
|
2513
3060
|
lifecycle: 'implementing',
|
|
3061
|
+
changeBindingVersion: undefined,
|
|
3062
|
+
candidateBinding: undefined,
|
|
2514
3063
|
cycle: active.record.reviewFeedback?.activeBatch
|
|
2515
3064
|
? active.record.cycle
|
|
2516
3065
|
: (active.record.cycle + 1),
|
|
@@ -2528,6 +3077,8 @@ export class RunIssue {
|
|
|
2528
3077
|
active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
|
|
2529
3078
|
return this.persist(active, {
|
|
2530
3079
|
lifecycle: 'implementing',
|
|
3080
|
+
changeBindingVersion: undefined,
|
|
3081
|
+
candidateBinding: undefined,
|
|
2531
3082
|
cycle: active.record.reviewFeedback?.activeBatch
|
|
2532
3083
|
? active.record.cycle
|
|
2533
3084
|
: (active.record.cycle + 1),
|
|
@@ -2540,12 +3091,39 @@ export class RunIssue {
|
|
|
2540
3091
|
proofReceipt: undefined,
|
|
2541
3092
|
});
|
|
2542
3093
|
}
|
|
3094
|
+
async startNextCycleFromCandidate(active, findings, sources) {
|
|
3095
|
+
const binding = active.record.candidateBinding;
|
|
3096
|
+
const candidate = this.dependencies.git.candidateV2;
|
|
3097
|
+
if (!binding || !candidate)
|
|
3098
|
+
return this.startNextCycle(active, findings, sources);
|
|
3099
|
+
if (active.record.executionLease) {
|
|
3100
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'candidate-execution-not-quiescent');
|
|
3101
|
+
}
|
|
3102
|
+
const normalized = await candidate.normalizeSharedIndex({
|
|
3103
|
+
worktreePath: active.record.worktreePath,
|
|
3104
|
+
expectedHeadSha: expectedImplementationHead(active.record),
|
|
3105
|
+
});
|
|
3106
|
+
if (normalized.kind === 'failed')
|
|
3107
|
+
return this.mapCandidateFailure(active, normalized.code);
|
|
3108
|
+
const transitioned = await this.startNextCycle(active, findings, sources);
|
|
3109
|
+
const released = await candidate.releasePin({ binding, expectedPinnedCommitSha: binding.candidateCommitSha });
|
|
3110
|
+
if (released.kind === 'failed') {
|
|
3111
|
+
return this.invokedFailure(transitioned, 'candidate-pin-release-pending', 'The next repair cycle is durable; orphan reconciliation will retry exact pin cleanup.');
|
|
3112
|
+
}
|
|
3113
|
+
return transitioned;
|
|
3114
|
+
}
|
|
2543
3115
|
repairBudgetExhausted(active, maxCycles) {
|
|
2544
3116
|
return active.record.reviewFeedback?.activeBatch
|
|
2545
3117
|
? active.record.reviewFeedback.repairRound >= 3
|
|
2546
3118
|
: active.record.cycle >= maxCycles;
|
|
2547
3119
|
}
|
|
2548
3120
|
async blockReviewFeedback(active, kind, evidenceCode) {
|
|
3121
|
+
if (active.record.mutableInvocation) {
|
|
3122
|
+
const reconciled = await this.reconcileMutableBeforeTerminal(active);
|
|
3123
|
+
if ('status' in reconciled)
|
|
3124
|
+
return reconciled;
|
|
3125
|
+
active = reconciled.active;
|
|
3126
|
+
}
|
|
2549
3127
|
const feedback = active.record.reviewFeedback;
|
|
2550
3128
|
if (!feedback?.activeBatch)
|
|
2551
3129
|
return this.terminal(active, { status: 'blocked', kind, resumable: false }, evidenceCode);
|
|
@@ -2576,6 +3154,7 @@ export class RunIssue {
|
|
|
2576
3154
|
}
|
|
2577
3155
|
if (canReduceAuthority && !sameStrings(issue.labels, blockedLabels)) {
|
|
2578
3156
|
try {
|
|
3157
|
+
await this.markV3ExternalEffect(active);
|
|
2579
3158
|
await this.dependencies.issues.setLabels(active.record.issueNumber, blockedLabels);
|
|
2580
3159
|
}
|
|
2581
3160
|
catch {
|
|
@@ -2598,7 +3177,7 @@ export class RunIssue {
|
|
|
2598
3177
|
...(directReview ? { directReview } : {}),
|
|
2599
3178
|
terminalOutcome,
|
|
2600
3179
|
outcomeEvidenceId: evidence.id,
|
|
2601
|
-
|
|
3180
|
+
reportInvocation: undefined,
|
|
2602
3181
|
intent: undefined,
|
|
2603
3182
|
});
|
|
2604
3183
|
return publicOutcome(terminalOutcome);
|
|
@@ -2628,7 +3207,21 @@ export class RunIssue {
|
|
|
2628
3207
|
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary });
|
|
2629
3208
|
return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
|
|
2630
3209
|
}
|
|
3210
|
+
async markV3ExternalEffect(active) {
|
|
3211
|
+
if (active.state.version !== 3)
|
|
3212
|
+
return;
|
|
3213
|
+
if (!this.dependencies.runRecords.markPublicationEffectPossible) {
|
|
3214
|
+
throw new Error('run-state publication watermark is unavailable');
|
|
3215
|
+
}
|
|
3216
|
+
await this.dependencies.runRecords.markPublicationEffectPossible();
|
|
3217
|
+
}
|
|
2631
3218
|
async terminal(active, outcome, evidenceCode = outcome.status, retainIntent = false) {
|
|
3219
|
+
if (active.record.mutableInvocation) {
|
|
3220
|
+
const reconciled = await this.reconcileMutableBeforeTerminal(active);
|
|
3221
|
+
if ('status' in reconciled)
|
|
3222
|
+
return reconciled;
|
|
3223
|
+
active = reconciled.active;
|
|
3224
|
+
}
|
|
2632
3225
|
if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
|
|
2633
3226
|
return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
|
|
2634
3227
|
}
|
|
@@ -2637,6 +3230,68 @@ export class RunIssue {
|
|
|
2637
3230
|
}
|
|
2638
3231
|
return this.persistTerminal(active, outcome, evidenceCode, retainIntent);
|
|
2639
3232
|
}
|
|
3233
|
+
async reconcileMutableBeforeTerminal(starting) {
|
|
3234
|
+
let active = starting;
|
|
3235
|
+
const invocation = active.record.mutableInvocation;
|
|
3236
|
+
if (!invocation)
|
|
3237
|
+
return { active };
|
|
3238
|
+
let deniedPathsBaseline;
|
|
3239
|
+
try {
|
|
3240
|
+
deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(active.record.worktreePath, active.config.deny.readPaths);
|
|
3241
|
+
}
|
|
3242
|
+
catch {
|
|
3243
|
+
return this.invokedFailure(active, 'mutable-operation-terminal-baseline-unavailable');
|
|
3244
|
+
}
|
|
3245
|
+
const qualification = invocation.operation === 'qualification-repair';
|
|
3246
|
+
const feedback = invocation.operation === 'review-feedback-implementation';
|
|
3247
|
+
const repairFindings = invocation.context.reworkFindings;
|
|
3248
|
+
if (qualification && !repairFindings?.length) {
|
|
3249
|
+
return this.invokedFailure(active, 'qualification-repair-correlation-missing');
|
|
3250
|
+
}
|
|
3251
|
+
const activeBatch = feedback ? active.record.reviewFeedback?.activeBatch : undefined;
|
|
3252
|
+
const directReview = feedback ? active.record.directReview : undefined;
|
|
3253
|
+
if (feedback && (!activeBatch || !directReview)) {
|
|
3254
|
+
return this.invokedFailure(active, 'review-feedback-implementation-correlation-missing');
|
|
3255
|
+
}
|
|
3256
|
+
const feedbackProjection = activeBatch && directReview
|
|
3257
|
+
? projectReviewFeedbackBatch(activeBatch, directReview.targetRevision)
|
|
3258
|
+
: undefined;
|
|
3259
|
+
const phaseFacts = qualification
|
|
3260
|
+
? [deniedPathsBaseline]
|
|
3261
|
+
: [
|
|
3262
|
+
deniedPathsBaseline,
|
|
3263
|
+
sha256(canonicalJson(resolveIssueCheckPolicy(active.record.issueSnapshot.body, active.config.checks).checks)),
|
|
3264
|
+
canonicalJson(active.record.checkQualification?.checks ?? []),
|
|
3265
|
+
];
|
|
3266
|
+
const invocationState = this.mutableInvocationState(() => active, (next) => { active = next; });
|
|
3267
|
+
let settled;
|
|
3268
|
+
try {
|
|
3269
|
+
settled = await this.dependencies.implementationAgent.settle({
|
|
3270
|
+
operation: invocation.operation,
|
|
3271
|
+
runId: active.record.runId,
|
|
3272
|
+
worktreePath: active.record.worktreePath,
|
|
3273
|
+
issue: publicIssueSnapshot(active.record.issueSnapshot),
|
|
3274
|
+
frozenCriteria: active.record.frozenCriteria,
|
|
3275
|
+
cycle: qualification ? active.record.checkQualification.repairAttempts + 1 : active.record.cycle,
|
|
3276
|
+
reworkFindings: repairFindings ?? [],
|
|
3277
|
+
repairOnly: invocation.context.repairOnly,
|
|
3278
|
+
workflowGeneration: active.record.workflowGeneration,
|
|
3279
|
+
...(feedbackProjection ? {
|
|
3280
|
+
reviewFeedbackRound: active.record.reviewFeedback.repairRound,
|
|
3281
|
+
reviewFeedback: feedbackProjection.workerFeedback,
|
|
3282
|
+
} : {}),
|
|
3283
|
+
phaseFacts,
|
|
3284
|
+
invocationState,
|
|
3285
|
+
signal: this.signal,
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
catch {
|
|
3289
|
+
return this.invokedFailure(active, 'mutable-operation-terminal-settlement-failed');
|
|
3290
|
+
}
|
|
3291
|
+
return settled.kind === 'settled'
|
|
3292
|
+
? { active }
|
|
3293
|
+
: this.invokedFailure(active, settled.code, 'Mutable invocation remains fenced before terminal reconciliation.');
|
|
3294
|
+
}
|
|
2640
3295
|
async publishBlockedTerminal(starting, outcome, evidenceCode) {
|
|
2641
3296
|
let active = starting;
|
|
2642
3297
|
const autoBlocked = [active.config.github.labels.auto.name, active.config.github.labels.blocked.name].sort();
|
|
@@ -2675,6 +3330,7 @@ export class RunIssue {
|
|
|
2675
3330
|
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
2676
3331
|
}
|
|
2677
3332
|
try {
|
|
3333
|
+
await this.markV3ExternalEffect(active);
|
|
2678
3334
|
await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
|
|
2679
3335
|
}
|
|
2680
3336
|
catch {
|
|
@@ -2714,6 +3370,7 @@ export class RunIssue {
|
|
|
2714
3370
|
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
2715
3371
|
}
|
|
2716
3372
|
try {
|
|
3373
|
+
await this.markV3ExternalEffect(active);
|
|
2717
3374
|
await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
|
|
2718
3375
|
}
|
|
2719
3376
|
catch {
|
|
@@ -2733,8 +3390,10 @@ export class RunIssue {
|
|
|
2733
3390
|
lifecycle: outcome.status,
|
|
2734
3391
|
terminalOutcome,
|
|
2735
3392
|
outcomeEvidenceId: evidence.id,
|
|
2736
|
-
|
|
3393
|
+
reportInvocation: undefined,
|
|
2737
3394
|
};
|
|
3395
|
+
if (active.record.reportInvocation?.operation === 'code-review')
|
|
3396
|
+
changes.executionLease = undefined;
|
|
2738
3397
|
if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
|
|
2739
3398
|
changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
|
|
2740
3399
|
? { status: 'blocked', kind: outcome.kind }
|
|
@@ -2763,7 +3422,7 @@ export class RunIssue {
|
|
|
2763
3422
|
});
|
|
2764
3423
|
}
|
|
2765
3424
|
}
|
|
2766
|
-
if (active.record.lifecycle === 'triaging'
|
|
3425
|
+
if (active.record.lifecycle === 'triaging') {
|
|
2767
3426
|
changes.routeExecution = undefined;
|
|
2768
3427
|
changes.routeReceipt = undefined;
|
|
2769
3428
|
}
|
|
@@ -2780,8 +3439,8 @@ export class RunIssue {
|
|
|
2780
3439
|
const evidence = await this.dependencies.writeEvidence({ runId: `issue-${issueNumber}`, code: 'cancelled', summary: 'Cancelled.' });
|
|
2781
3440
|
return { status: 'cancelled', evidencePath: evidence.path };
|
|
2782
3441
|
}
|
|
2783
|
-
async preClaimTransport(issueNumber) {
|
|
2784
|
-
const evidence = await this.dependencies.writeEvidence({ runId: `issue-${issueNumber}`, code
|
|
3442
|
+
async preClaimTransport(issueNumber, code = 'issue-read-failed', summary = 'Issue read failed.') {
|
|
3443
|
+
const evidence = await this.dependencies.writeEvidence({ runId: `issue-${issueNumber}`, code, summary });
|
|
2785
3444
|
return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
|
|
2786
3445
|
}
|
|
2787
3446
|
timestamp() {
|
|
@@ -3018,6 +3677,25 @@ function sameFreshness(left, right) {
|
|
|
3018
3677
|
function expectedImplementationHead(record) {
|
|
3019
3678
|
return record.reviewFeedback?.activeBatch?.priorPublishedHeadSha ?? record.baseSha;
|
|
3020
3679
|
}
|
|
3680
|
+
function pendingCandidateBoundary(record) {
|
|
3681
|
+
if (record.lifecycle !== 'implementing')
|
|
3682
|
+
return undefined;
|
|
3683
|
+
const feedback = record.reviewFeedback;
|
|
3684
|
+
if (feedback?.activeBatch && feedback.repairRound >= 1 && feedback.repairRound <= 3) {
|
|
3685
|
+
return {
|
|
3686
|
+
kind: 'review-feedback',
|
|
3687
|
+
batchId: feedback.activeBatch.batchId,
|
|
3688
|
+
repairRound: feedback.repairRound,
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
if (record.mutableInvocation?.operation === 'qualification-repair' && record.checkQualification) {
|
|
3692
|
+
return {
|
|
3693
|
+
kind: 'qualification',
|
|
3694
|
+
repairAttempt: record.checkQualification.repairAttempts,
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
return { kind: 'implementation-cycle', cycle: record.cycle };
|
|
3698
|
+
}
|
|
3021
3699
|
function commentsWithMarker(issue, marker) {
|
|
3022
3700
|
return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
|
|
3023
3701
|
}
|