codex-orchestrator 2.0.5 → 2.0.7
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 +57 -0
- package/README.md +6 -2
- package/dist/src/v2/acceptance-proof.d.ts +7 -0
- package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
- package/dist/src/v2/acceptance-proof.js +111 -5
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- package/dist/src/v2/adapters/command.d.ts +2 -0
- package/dist/src/v2/adapters/command.d.ts.map +1 -1
- package/dist/src/v2/adapters/command.js +67 -8
- package/dist/src/v2/adapters/command.js.map +1 -1
- package/dist/src/v2/adapters/gh-issue-adapter.js +16 -5
- package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
- package/dist/src/v2/android-proof-runner.d.ts +93 -0
- package/dist/src/v2/android-proof-runner.d.ts.map +1 -0
- package/dist/src/v2/android-proof-runner.js +921 -0
- package/dist/src/v2/android-proof-runner.js.map +1 -0
- package/dist/src/v2/checked-change.d.ts +1 -1
- package/dist/src/v2/checked-change.d.ts.map +1 -1
- package/dist/src/v2/checked-change.js +3 -2
- package/dist/src/v2/checked-change.js.map +1 -1
- package/dist/src/v2/code-review-report.d.ts +1 -2
- package/dist/src/v2/code-review-report.d.ts.map +1 -1
- package/dist/src/v2/code-review-report.js +6 -11
- package/dist/src/v2/code-review-report.js.map +1 -1
- package/dist/src/v2/config.d.ts +13 -0
- package/dist/src/v2/config.d.ts.map +1 -1
- package/dist/src/v2/config.js +57 -1
- package/dist/src/v2/config.js.map +1 -1
- package/dist/src/v2/containment.d.ts +0 -52
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +1 -195
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/direct-delivery.d.ts +11 -2
- package/dist/src/v2/direct-delivery.d.ts.map +1 -1
- package/dist/src/v2/direct-delivery.js +62 -17
- package/dist/src/v2/direct-delivery.js.map +1 -1
- package/dist/src/v2/immutable-workflow-publisher.js +6 -1
- package/dist/src/v2/immutable-workflow-publisher.js.map +1 -1
- package/dist/src/v2/implementation-reviewer.d.ts +1 -1
- package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
- package/dist/src/v2/implementation-reviewer.js +1 -2
- package/dist/src/v2/implementation-reviewer.js.map +1 -1
- package/dist/src/v2/mobile-lease.d.ts +9 -0
- package/dist/src/v2/mobile-lease.d.ts.map +1 -1
- package/dist/src/v2/mobile-lease.js +27 -3
- package/dist/src/v2/mobile-lease.js.map +1 -1
- package/dist/src/v2/proof-report.d.ts +1 -1
- package/dist/src/v2/proof-report.d.ts.map +1 -1
- package/dist/src/v2/proof-report.js +6 -4
- package/dist/src/v2/proof-report.js.map +1 -1
- package/dist/src/v2/run-issue.d.ts +18 -1
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +504 -109
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +17 -1
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +30 -4
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +84 -27
- package/dist/src/v2/runtime.js.map +1 -1
- package/docs/deep-dive.md +25 -11
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/schemas/proof-report-v1.json +1 -1
- package/internal-workflow/skills/acceptance-proof/SKILL.md +1 -1
- package/internal-workflow/skills/acceptance-proof/references/android.md +6 -6
- package/internal-workflow/skills/implementation-spec-maker/SKILL.md +10 -8
- package/internal-workflow/skills/implementation-spec-review/SKILL.md +17 -3
- package/internal-workflow/skills/implementation-spec-review/evals/evals.json +30 -0
- package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +30 -6
- package/package.json +1 -2
- package/internal-workflow/skills/acceptance-proof/tools/android-lease.mjs +0 -280
package/dist/src/v2/run-issue.js
CHANGED
|
@@ -3,7 +3,7 @@ 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, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
|
|
6
|
+
import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, canRecoverTerminalDirectReviewReport, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, MAX_DIRECT_REVIEW_REPORT_REPAIRS, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, recoverTerminalDirectReviewReport, } from './direct-delivery.js';
|
|
7
7
|
import { ProofLaunchAuthorizationError, ProofQuiescenceError } from './acceptance-proof.js';
|
|
8
8
|
import { RouteInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
|
|
9
9
|
import { initialRouteExecution, } from './route-coordinator.js';
|
|
@@ -30,56 +30,88 @@ export class RunIssue {
|
|
|
30
30
|
let observation = await this.readIssue(issueNumber);
|
|
31
31
|
if (!observation || observation.state !== 'OPEN')
|
|
32
32
|
return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
|
|
33
|
+
const body = claimComment(runId, issueNumber, branchName);
|
|
34
|
+
const marker = body.split('\n')[0];
|
|
35
|
+
const commentIntent = { kind: 'comment', issueNumber, marker, bodySha256: sha256(body) };
|
|
36
|
+
// Compatibility with runs persisted by the former label-first claim order.
|
|
37
|
+
// If the label effect is already visible, confirm it. Otherwise establish
|
|
38
|
+
// the trusted claim comment before granting the running status label.
|
|
33
39
|
if (active.record.intent?.kind === 'claim-labels') {
|
|
34
|
-
if (
|
|
40
|
+
if (sameStrings(observation.labels, expectedLabels)) {
|
|
41
|
+
active = await this.confirmEffect(active);
|
|
42
|
+
}
|
|
43
|
+
else if (!this.hasTrustedClaim(observation, active.record)) {
|
|
35
44
|
const labels = new Set(observation.labels);
|
|
36
45
|
if (!labels.has(config.github.labels.auto.name)
|
|
37
46
|
|| labels.has(config.github.labels.blocked.name)
|
|
38
|
-
|| labels.has(config.github.labels.review.name)
|
|
47
|
+
|| labels.has(config.github.labels.review.name)
|
|
48
|
+
|| labels.has(config.github.labels.waitingHuman.name)) {
|
|
39
49
|
return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
|
|
40
50
|
}
|
|
51
|
+
active = await this.persist(active, { intent: commentIntent });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!active.record.intent) {
|
|
55
|
+
observation = await this.readIssue(issueNumber);
|
|
56
|
+
if (!observation || observation.state !== 'OPEN')
|
|
57
|
+
return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
|
|
58
|
+
if (!this.hasTrustedClaim(observation, active.record)) {
|
|
59
|
+
active = await this.persist(active, { intent: commentIntent });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (active.record.intent?.kind === 'comment') {
|
|
63
|
+
if (active.record.intent.marker !== marker || active.record.intent.bodySha256 !== sha256(body)) {
|
|
64
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-intent-diverged') };
|
|
65
|
+
}
|
|
66
|
+
observation = await this.readIssue(issueNumber);
|
|
67
|
+
let comments = observation ? commentsWithMarker(observation, marker) : [];
|
|
68
|
+
if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
|
|
69
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
|
|
70
|
+
}
|
|
71
|
+
if (comments.length === 0) {
|
|
41
72
|
try {
|
|
42
|
-
await this.dependencies.issues.
|
|
73
|
+
await this.dependencies.issues.postComment(issueNumber, body);
|
|
43
74
|
}
|
|
44
75
|
catch {
|
|
45
|
-
return { result: await this.invokedFailure(active, 'claim-
|
|
76
|
+
return { result: await this.invokedFailure(active, 'claim-comment-delivery-unknown') };
|
|
46
77
|
}
|
|
47
78
|
observation = await this.readIssue(issueNumber);
|
|
79
|
+
comments = observation ? commentsWithMarker(observation, marker) : [];
|
|
48
80
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
81
|
+
if (comments.length !== 1 || comments[0].body !== body
|
|
82
|
+
|| !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comments[0].authorAssociation)) {
|
|
83
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
|
|
51
84
|
}
|
|
52
85
|
active = await this.confirmEffect(active);
|
|
53
86
|
}
|
|
54
|
-
|
|
55
|
-
|
|
87
|
+
observation = await this.readIssue(issueNumber);
|
|
88
|
+
if (!observation || observation.state !== 'OPEN' || !this.hasTrustedClaim(observation, active.record)) {
|
|
89
|
+
return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
|
|
90
|
+
}
|
|
91
|
+
if (sameStrings(observation.labels, expectedLabels))
|
|
92
|
+
return { active };
|
|
93
|
+
const labels = new Set(observation.labels);
|
|
94
|
+
if (!labels.has(config.github.labels.auto.name)
|
|
95
|
+
|| labels.has(config.github.labels.blocked.name)
|
|
96
|
+
|| labels.has(config.github.labels.review.name)
|
|
97
|
+
|| labels.has(config.github.labels.waitingHuman.name)) {
|
|
98
|
+
return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
|
|
56
99
|
}
|
|
57
|
-
const body = claimComment(runId, issueNumber, branchName);
|
|
58
|
-
const marker = body.split('\n')[0];
|
|
59
100
|
if (!active.record.intent) {
|
|
60
|
-
active = await this.persist(active, { intent: { kind: '
|
|
101
|
+
active = await this.persist(active, { intent: { kind: 'claim-labels', issueNumber, expected: expectedLabels } });
|
|
61
102
|
}
|
|
62
|
-
if (active.record.intent?.kind !== '
|
|
63
|
-
return { result: await this.publicationDiverged(active, 'claim-
|
|
103
|
+
if (active.record.intent?.kind !== 'claim-labels' || !sameStrings(active.record.intent.expected, expectedLabels)) {
|
|
104
|
+
return { result: await this.publicationDiverged(active, 'claim-labels-intent-diverged') };
|
|
64
105
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
|
|
68
|
-
return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
|
|
106
|
+
try {
|
|
107
|
+
await this.dependencies.issues.setLabels(issueNumber, expectedLabels);
|
|
69
108
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await this.dependencies.issues.postComment(issueNumber, body);
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return { result: await this.invokedFailure(active, 'claim-comment-delivery-unknown') };
|
|
76
|
-
}
|
|
77
|
-
observation = await this.readIssue(issueNumber);
|
|
78
|
-
comments = observation ? commentsWithMarker(observation, marker) : [];
|
|
109
|
+
catch {
|
|
110
|
+
return { result: await this.invokedFailure(active, 'claim-labels-delivery-unknown') };
|
|
79
111
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return { result: await this.publicationDiverged(active, 'claim-
|
|
112
|
+
observation = await this.readIssue(issueNumber);
|
|
113
|
+
if (!observation || !sameStrings(observation.labels, expectedLabels) || !this.hasTrustedClaim(observation, active.record)) {
|
|
114
|
+
return { result: await this.publicationDiverged(active, 'claim-labels-observation-diverged') };
|
|
83
115
|
}
|
|
84
116
|
active = await this.confirmEffect(active);
|
|
85
117
|
return { active };
|
|
@@ -104,7 +136,7 @@ export class RunIssue {
|
|
|
104
136
|
return await this.publicationDiverged(active, 'commit-tree-diverged');
|
|
105
137
|
if (this.signal.aborted)
|
|
106
138
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
107
|
-
if (!await this.authorized(
|
|
139
|
+
if (!await this.authorized(active, config))
|
|
108
140
|
return await this.revoked(active);
|
|
109
141
|
try {
|
|
110
142
|
commitSha = await this.dependencies.git.commit({ worktreePath, message: intent.message });
|
|
@@ -136,7 +168,7 @@ export class RunIssue {
|
|
|
136
168
|
if (!remoteSha) {
|
|
137
169
|
if (this.signal.aborted)
|
|
138
170
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
139
|
-
if (!await this.authorized(
|
|
171
|
+
if (!await this.authorized(active, config))
|
|
140
172
|
return await this.revoked(active);
|
|
141
173
|
try {
|
|
142
174
|
await this.dependencies.git.push({ worktreePath, branchName });
|
|
@@ -170,7 +202,7 @@ export class RunIssue {
|
|
|
170
202
|
if (!observed) {
|
|
171
203
|
if (this.signal.aborted)
|
|
172
204
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
173
|
-
if (!await this.authorized(
|
|
205
|
+
if (!await this.authorized(active, config))
|
|
174
206
|
return await this.revoked(active);
|
|
175
207
|
try {
|
|
176
208
|
await this.dependencies.pullRequests.createDraft({
|
|
@@ -210,7 +242,7 @@ export class RunIssue {
|
|
|
210
242
|
if (matching.length === 0) {
|
|
211
243
|
if (this.signal.aborted)
|
|
212
244
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
213
|
-
if (!await this.authorized(
|
|
245
|
+
if (!await this.authorized(active, config))
|
|
214
246
|
return await this.revoked(active);
|
|
215
247
|
try {
|
|
216
248
|
await this.dependencies.issues.postComment(issueNumber, handoffBody);
|
|
@@ -236,7 +268,7 @@ export class RunIssue {
|
|
|
236
268
|
if (!sameStrings(observation.labels, terminalLabels)) {
|
|
237
269
|
if (this.signal.aborted)
|
|
238
270
|
return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
|
|
239
|
-
if (!await this.authorized(
|
|
271
|
+
if (!await this.authorized(active, config))
|
|
240
272
|
return await this.revoked(active);
|
|
241
273
|
try {
|
|
242
274
|
await this.dependencies.issues.setLabels(issueNumber, terminalLabels);
|
|
@@ -304,7 +336,7 @@ export class RunIssue {
|
|
|
304
336
|
}
|
|
305
337
|
head = await this.dependencies.git.getHead(active.record.worktreePath);
|
|
306
338
|
if (head === intent.parentSha) {
|
|
307
|
-
if (!await this.authorized(
|
|
339
|
+
if (!await this.authorized(active, config)) {
|
|
308
340
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
309
341
|
}
|
|
310
342
|
const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
|
|
@@ -350,7 +382,7 @@ export class RunIssue {
|
|
|
350
382
|
}
|
|
351
383
|
remote = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
|
|
352
384
|
if (remote === oldHead) {
|
|
353
|
-
if (!await this.authorized(
|
|
385
|
+
if (!await this.authorized(active, config)) {
|
|
354
386
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
355
387
|
}
|
|
356
388
|
const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
|
|
@@ -410,7 +442,7 @@ export class RunIssue {
|
|
|
410
442
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-diverged');
|
|
411
443
|
}
|
|
412
444
|
if (matches.length === 0) {
|
|
413
|
-
if (!await this.authorized(
|
|
445
|
+
if (!await this.authorized(active, config)) {
|
|
414
446
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
415
447
|
}
|
|
416
448
|
try {
|
|
@@ -459,7 +491,7 @@ export class RunIssue {
|
|
|
459
491
|
return validationFailure;
|
|
460
492
|
let issue = await this.readIssue(issueNumber);
|
|
461
493
|
if (!issue || !sameStrings(issue.labels, finalLabels)) {
|
|
462
|
-
if (!await this.authorized(
|
|
494
|
+
if (!await this.authorized(active, config)) {
|
|
463
495
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
464
496
|
}
|
|
465
497
|
try {
|
|
@@ -546,7 +578,7 @@ export class RunIssue {
|
|
|
546
578
|
}
|
|
547
579
|
if (feedback.phase !== 'idle' || observed.status === 'none')
|
|
548
580
|
return { result: publicOutcome(terminal) };
|
|
549
|
-
if (!await this.authorizedForExactLabels(starting
|
|
581
|
+
if (!await this.authorizedForExactLabels(starting, [config.github.labels.review.name])) {
|
|
550
582
|
return { result: publicOutcome(terminal) };
|
|
551
583
|
}
|
|
552
584
|
const projected = projectReviewFeedbackBatch(observed.batch, starting.record.directReview.targetRevision);
|
|
@@ -595,9 +627,9 @@ export class RunIssue {
|
|
|
595
627
|
}
|
|
596
628
|
const issue = await this.readIssue(active.record.issueNumber);
|
|
597
629
|
const labelsAreRunning = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, runningLabels)
|
|
598
|
-
&& this.hasTrustedClaim(issue, active.record
|
|
630
|
+
&& this.hasTrustedClaim(issue, active.record);
|
|
599
631
|
const labelsAreReview = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, reviewLabels)
|
|
600
|
-
&& this.hasTrustedClaim(issue, active.record
|
|
632
|
+
&& this.hasTrustedClaim(issue, active.record);
|
|
601
633
|
if (!labelsAreRunning && !labelsAreReview) {
|
|
602
634
|
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-authority-revoked') };
|
|
603
635
|
}
|
|
@@ -616,7 +648,7 @@ export class RunIssue {
|
|
|
616
648
|
}
|
|
617
649
|
const observed = await this.readIssue(active.record.issueNumber);
|
|
618
650
|
if (!observed || !sameStrings(observed.labels, runningLabels)
|
|
619
|
-
|| !this.hasTrustedClaim(observed, active.record
|
|
651
|
+
|| !this.hasTrustedClaim(observed, active.record)) {
|
|
620
652
|
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-labels-diverged') };
|
|
621
653
|
}
|
|
622
654
|
}
|
|
@@ -706,17 +738,6 @@ export class RunIssue {
|
|
|
706
738
|
return await this.preClaimInternal('config-changed-during-owner-acquire', input.issueNumber);
|
|
707
739
|
}
|
|
708
740
|
const config = confirmedConfig.config;
|
|
709
|
-
try {
|
|
710
|
-
await this.dependencies.validateContainment(config);
|
|
711
|
-
}
|
|
712
|
-
catch {
|
|
713
|
-
const evidence = await this.dependencies.writeEvidence({
|
|
714
|
-
runId: `issue-${input.issueNumber}`,
|
|
715
|
-
code: 'containment-certificate-invalid',
|
|
716
|
-
summary: 'Containment certification is unavailable or stale.',
|
|
717
|
-
});
|
|
718
|
-
return { status: 'blocked', kind: 'safety', resumable: true, evidencePath: evidence.path };
|
|
719
|
-
}
|
|
720
741
|
if (this.signal.aborted)
|
|
721
742
|
return await this.preClaimCancelled(input.issueNumber);
|
|
722
743
|
let issue;
|
|
@@ -753,15 +774,33 @@ export class RunIssue {
|
|
|
753
774
|
if (active.record.intent?.kind === 'review-blocked-labels') {
|
|
754
775
|
return await this.blockReviewFeedback(active, active.record.intent.blockKind, active.record.intent.evidenceCode);
|
|
755
776
|
}
|
|
777
|
+
if (active.record.intent?.kind === 'blocked-labels') {
|
|
778
|
+
return await this.publishBlockedTerminal(active, {
|
|
779
|
+
status: 'blocked',
|
|
780
|
+
kind: active.record.intent.blockKind,
|
|
781
|
+
resumable: active.record.intent.resumable,
|
|
782
|
+
}, active.record.intent.evidenceCode);
|
|
783
|
+
}
|
|
784
|
+
if (existing.terminalOutcome?.status === 'blocked'
|
|
785
|
+
&& issue?.state === 'OPEN'
|
|
786
|
+
&& blockedLabelProjection(issue.labels, config).status !== 'settled') {
|
|
787
|
+
return await this.reconcilePersistedBlockedTerminal(active, issue, existing.terminalOutcome);
|
|
788
|
+
}
|
|
756
789
|
if (existing.terminalOutcome) {
|
|
757
|
-
if (existing.terminalOutcome.status !== 'review-ready')
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
790
|
+
if (existing.terminalOutcome.status !== 'review-ready') {
|
|
791
|
+
const recovered = await this.recoverTerminalReviewReport(active, issue, config, frozenCriteria);
|
|
792
|
+
if (!recovered)
|
|
793
|
+
return publicOutcome(existing.terminalOutcome);
|
|
794
|
+
active = recovered;
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
const continuation = await this.continueReviewReady(active, targetRoot, config);
|
|
798
|
+
if ('result' in continuation)
|
|
799
|
+
return continuation.result;
|
|
800
|
+
active = continuation.active;
|
|
801
|
+
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
802
|
+
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
803
|
+
}
|
|
765
804
|
}
|
|
766
805
|
if (active.record.reviewFeedback?.phase === 'frozen') {
|
|
767
806
|
const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
@@ -779,15 +818,22 @@ export class RunIssue {
|
|
|
779
818
|
if ('result' in claim)
|
|
780
819
|
return claim.result;
|
|
781
820
|
active = claim.active;
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
821
|
+
let worktree;
|
|
822
|
+
try {
|
|
823
|
+
worktree = await this.dependencies.git.inspectWorktree({ worktreePath, branchName, baseSha });
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
return await this.invokedFailure(active, 'local-git-worktree-inspection-failed', claimedWorktreeInspectionFailureSummary(error));
|
|
827
|
+
}
|
|
828
|
+
if (worktree === 'diverged') {
|
|
829
|
+
return await this.invokedFailure(active, 'local-git-worktree-diverged', 'The claimed worktree path is present but does not match the expected branch and base. Correct or preserve the local artifact, then retry the same run.');
|
|
830
|
+
}
|
|
785
831
|
if (worktree === 'absent') {
|
|
786
832
|
try {
|
|
787
833
|
await this.dependencies.git.createWorktree({ targetRoot, worktreePath, branchName, baseBranch: config.github.baseBranch, baseSha });
|
|
788
834
|
}
|
|
789
|
-
catch {
|
|
790
|
-
return await this.
|
|
835
|
+
catch (error) {
|
|
836
|
+
return await this.invokedFailure(active, 'local-git-worktree-creation-failed', worktreeCreationFailureSummary(error));
|
|
791
837
|
}
|
|
792
838
|
}
|
|
793
839
|
active = await this.initializeClaimedRun(active, issue);
|
|
@@ -815,7 +861,7 @@ export class RunIssue {
|
|
|
815
861
|
frozenCriteria = structuredClone(active.record.frozenCriteria);
|
|
816
862
|
}
|
|
817
863
|
if (existing.lifecycle === 'spec-authoring') {
|
|
818
|
-
if (!await this.authorized(
|
|
864
|
+
if (!await this.authorized(active, config))
|
|
819
865
|
return await this.revoked(active);
|
|
820
866
|
return await this.continueSpecRequired(active);
|
|
821
867
|
}
|
|
@@ -853,7 +899,7 @@ export class RunIssue {
|
|
|
853
899
|
if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
|
|
854
900
|
return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
|
|
855
901
|
}
|
|
856
|
-
if (!await this.authorized(
|
|
902
|
+
if (!await this.authorized(active, config))
|
|
857
903
|
return await this.revoked(active);
|
|
858
904
|
if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
|
|
859
905
|
if (!active.record.routeExecution || !active.record.routeReceipt)
|
|
@@ -889,11 +935,13 @@ export class RunIssue {
|
|
|
889
935
|
worktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${input.issueNumber}`);
|
|
890
936
|
baseSha = await this.dependencies.git.getBaseSha({ targetRoot, baseBranch: config.github.baseBranch });
|
|
891
937
|
assertGitSha(baseSha, 'baseSha');
|
|
892
|
-
const
|
|
938
|
+
const claimBody = claimComment(runId, input.issueNumber, branchName);
|
|
893
939
|
active = await this.createRun({
|
|
894
940
|
runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
|
|
895
941
|
issueSnapshot, frozenCriteria, config,
|
|
896
|
-
intent: {
|
|
942
|
+
intent: {
|
|
943
|
+
kind: 'comment', issueNumber: input.issueNumber, marker: claimBody.split('\n')[0], bodySha256: sha256(claimBody),
|
|
944
|
+
},
|
|
897
945
|
});
|
|
898
946
|
const claim = await this.reconcileClaim(active, config);
|
|
899
947
|
if ('result' in claim)
|
|
@@ -902,8 +950,8 @@ export class RunIssue {
|
|
|
902
950
|
try {
|
|
903
951
|
await this.dependencies.git.createWorktree({ targetRoot, worktreePath, branchName, baseBranch: config.github.baseBranch, baseSha });
|
|
904
952
|
}
|
|
905
|
-
catch {
|
|
906
|
-
return await this.
|
|
953
|
+
catch (error) {
|
|
954
|
+
return await this.invokedFailure(active, 'local-git-worktree-creation-failed', worktreeCreationFailureSummary(error));
|
|
907
955
|
}
|
|
908
956
|
active = await this.initializeClaimedRun(active, issue);
|
|
909
957
|
issueSnapshot = structuredClone(active.record.issueSnapshot);
|
|
@@ -920,8 +968,15 @@ export class RunIssue {
|
|
|
920
968
|
if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
|
|
921
969
|
return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
|
|
922
970
|
}
|
|
971
|
+
if (active.record.lifecycle === 'implementing' && active.record.routeReceipt?.route === 'direct'
|
|
972
|
+
&& !active.record.directReview) {
|
|
973
|
+
const baseline = await this.captureBaselineChecks(active, config);
|
|
974
|
+
if ('status' in baseline)
|
|
975
|
+
return baseline;
|
|
976
|
+
active = baseline.active;
|
|
977
|
+
}
|
|
923
978
|
attemptLoop: while (true) {
|
|
924
|
-
if (!await this.authorized(
|
|
979
|
+
if (!await this.authorized(active, config)) {
|
|
925
980
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
926
981
|
}
|
|
927
982
|
if (this.signal.aborted)
|
|
@@ -1197,23 +1252,40 @@ export class RunIssue {
|
|
|
1197
1252
|
});
|
|
1198
1253
|
}
|
|
1199
1254
|
}
|
|
1255
|
+
const configuredChecks = Object.entries(config.checks);
|
|
1256
|
+
const reusableChecks = active.record.checks.filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
|
|
1257
|
+
if (reusableChecks.length !== active.record.checks.length) {
|
|
1258
|
+
active = await this.persist(active, {
|
|
1259
|
+
checks: reusableChecks,
|
|
1260
|
+
checkedChangeSha256: undefined,
|
|
1261
|
+
proofId: undefined,
|
|
1262
|
+
proofReceipt: undefined,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1200
1265
|
for (const [id, command] of Object.entries(config.checks)) {
|
|
1201
|
-
if (active.record.checks.some((check) => check.id === id
|
|
1266
|
+
if (active.record.checks.some((check) => check.id === id
|
|
1267
|
+
&& check.command === command
|
|
1268
|
+
&& (check.status === 'passed' || check.status === 'unchanged-failure')))
|
|
1202
1269
|
continue;
|
|
1203
1270
|
if (this.signal.aborted)
|
|
1204
1271
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1205
1272
|
let check;
|
|
1206
1273
|
try {
|
|
1207
|
-
check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, signal: this.signal });
|
|
1274
|
+
check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, phase: 'changed', signal: this.signal });
|
|
1208
1275
|
}
|
|
1209
1276
|
catch {
|
|
1210
1277
|
return await this.terminal(active, { status: 'internal-error', code: 'configured-check-execution-failed' });
|
|
1211
1278
|
}
|
|
1212
1279
|
if (this.signal.aborted)
|
|
1213
1280
|
return await this.terminal(active, { status: 'cancelled' });
|
|
1214
|
-
const
|
|
1281
|
+
const outputSha256 = check.outputSha256;
|
|
1282
|
+
const baseline = active.record.baselineChecks?.find((candidate) => candidate.id === id && candidate.command === command);
|
|
1283
|
+
const status = check.status === 'failed' && baseline?.status === 'failed' && baseline.outputSha256 === outputSha256
|
|
1284
|
+
? 'unchanged-failure'
|
|
1285
|
+
: check.status;
|
|
1286
|
+
const row = { id, command, status, outputSha256 };
|
|
1215
1287
|
active = await this.persist(active, { checks: [...active.record.checks, row] });
|
|
1216
|
-
if (
|
|
1288
|
+
if (status === 'failed') {
|
|
1217
1289
|
if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
|
|
1218
1290
|
return active.record.reviewFeedback?.activeBatch
|
|
1219
1291
|
? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
|
|
@@ -1265,6 +1337,9 @@ export class RunIssue {
|
|
|
1265
1337
|
|| reviewedStageBinding.contentSha256 !== proofContentSha256) {
|
|
1266
1338
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
|
|
1267
1339
|
}
|
|
1340
|
+
if (!sameCheckPolicy(active.record.checks, config.checks)) {
|
|
1341
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'configured-check-policy-drift');
|
|
1342
|
+
}
|
|
1268
1343
|
const payload = {
|
|
1269
1344
|
version: 1,
|
|
1270
1345
|
canonicalRepository,
|
|
@@ -1278,7 +1353,10 @@ export class RunIssue {
|
|
|
1278
1353
|
untrackedContentSha256: freshness.untrackedContentSha256,
|
|
1279
1354
|
worktreeIdentity: freshness.worktreeIdentity,
|
|
1280
1355
|
changedFiles: proofChangedFiles,
|
|
1281
|
-
checks: active.record.checks.map((check) => ({
|
|
1356
|
+
checks: active.record.checks.map((check) => ({
|
|
1357
|
+
...check,
|
|
1358
|
+
status: check.status === 'unchanged-failure' ? 'unchanged-failure' : 'passed',
|
|
1359
|
+
})),
|
|
1282
1360
|
checkPolicySha256: sha256(canonicalJson(config.checks)),
|
|
1283
1361
|
packageVersion: active.record.packageVersion,
|
|
1284
1362
|
proofSchemaVersion: 1,
|
|
@@ -1443,12 +1521,31 @@ export class RunIssue {
|
|
|
1443
1521
|
catch {
|
|
1444
1522
|
throw new RouteInitializationUnrecoverableError();
|
|
1445
1523
|
}
|
|
1446
|
-
const issueSnapshot = issue?.state === 'OPEN'
|
|
1524
|
+
const issueSnapshot = issue?.state === 'OPEN'
|
|
1525
|
+
? refreshClaimedIssueSnapshot(active.record.issueSnapshot, issue)
|
|
1526
|
+
: active.record.issueSnapshot;
|
|
1447
1527
|
return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
|
|
1448
1528
|
}
|
|
1449
1529
|
async routeRun(starting, issue, frozenCriteria, worktreePath, config, issueNumber, branchName) {
|
|
1450
1530
|
let active = starting;
|
|
1451
1531
|
while (active.record.lifecycle === 'triaging') {
|
|
1532
|
+
let currentIssue;
|
|
1533
|
+
try {
|
|
1534
|
+
currentIssue = await this.readIssue(active.record.issueNumber);
|
|
1535
|
+
}
|
|
1536
|
+
catch (error) {
|
|
1537
|
+
if (error instanceof TransportReadError) {
|
|
1538
|
+
return { result: await this.invokedFailure(active, 'authorization-read-failed') };
|
|
1539
|
+
}
|
|
1540
|
+
throw error;
|
|
1541
|
+
}
|
|
1542
|
+
if (!currentIssue || !this.isAuthorizedIssue(currentIssue, active.record, config)) {
|
|
1543
|
+
return { result: await this.revoked(active) };
|
|
1544
|
+
}
|
|
1545
|
+
active = await this.persist(active, {
|
|
1546
|
+
issueSnapshot: refreshClaimedIssueSnapshot(active.record.issueSnapshot, currentIssue),
|
|
1547
|
+
});
|
|
1548
|
+
issue = structuredClone(active.record.issueSnapshot);
|
|
1452
1549
|
const state = {
|
|
1453
1550
|
read: async () => requireRouteExecution(active.record.routeExecution),
|
|
1454
1551
|
compareAndSwap: async (expected, next) => {
|
|
@@ -1550,7 +1647,7 @@ export class RunIssue {
|
|
|
1550
1647
|
return { result: await this.terminal(active, { status: 'internal-error', code: 'route-state-not-dispatchable' }) };
|
|
1551
1648
|
}
|
|
1552
1649
|
try {
|
|
1553
|
-
if (!await this.authorized(
|
|
1650
|
+
if (!await this.authorized(active, config)) {
|
|
1554
1651
|
return { result: await this.revoked(active) };
|
|
1555
1652
|
}
|
|
1556
1653
|
}
|
|
@@ -1575,7 +1672,7 @@ export class RunIssue {
|
|
|
1575
1672
|
if (receipt.route !== 'awaiting-user')
|
|
1576
1673
|
active = await this.persist(active, { lifecycle });
|
|
1577
1674
|
try {
|
|
1578
|
-
if (!await this.authorized(
|
|
1675
|
+
if (!await this.authorized(active, config)) {
|
|
1579
1676
|
return { result: await this.revoked(active) };
|
|
1580
1677
|
}
|
|
1581
1678
|
}
|
|
@@ -1774,7 +1871,7 @@ export class RunIssue {
|
|
|
1774
1871
|
packageVersion: workflow.receipt.packageVersion,
|
|
1775
1872
|
workflowGeneration: structuredClone(workflow.receipt),
|
|
1776
1873
|
skillHashes: structuredClone(workflow.skillHashes),
|
|
1777
|
-
checks: [], createdAt: now, updatedAt: now,
|
|
1874
|
+
baselineChecks: [], checks: [], createdAt: now, updatedAt: now,
|
|
1778
1875
|
};
|
|
1779
1876
|
const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
|
|
1780
1877
|
schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
|
|
@@ -1808,33 +1905,50 @@ export class RunIssue {
|
|
|
1808
1905
|
throw new PostEffectStateError(active);
|
|
1809
1906
|
}
|
|
1810
1907
|
}
|
|
1811
|
-
async authorized(
|
|
1812
|
-
const issue = await this.readIssue(issueNumber);
|
|
1813
|
-
|
|
1908
|
+
async authorized(active, config) {
|
|
1909
|
+
const issue = await this.readIssue(active.record.issueNumber);
|
|
1910
|
+
return !!issue && this.isAuthorizedIssue(issue, active.record, config);
|
|
1911
|
+
}
|
|
1912
|
+
isAuthorizedIssue(issue, record, config) {
|
|
1913
|
+
if (issue.state !== 'OPEN')
|
|
1814
1914
|
return false;
|
|
1815
1915
|
const labels = new Set(issue.labels);
|
|
1816
|
-
if (!labels.has(config.github.labels.auto.name)
|
|
1916
|
+
if (!labels.has(config.github.labels.auto.name)
|
|
1917
|
+
|| labels.has(config.github.labels.blocked.name)
|
|
1918
|
+
|| labels.has(config.github.labels.review.name)
|
|
1919
|
+
|| labels.has(config.github.labels.waitingHuman.name))
|
|
1817
1920
|
return false;
|
|
1818
|
-
return this.hasTrustedClaim(issue,
|
|
1921
|
+
return this.hasTrustedClaim(issue, record);
|
|
1819
1922
|
}
|
|
1820
|
-
async authorizedForExactLabels(
|
|
1821
|
-
const issue = await this.readIssue(issueNumber);
|
|
1923
|
+
async authorizedForExactLabels(active, expectedLabels) {
|
|
1924
|
+
const issue = await this.readIssue(active.record.issueNumber);
|
|
1822
1925
|
return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
|
|
1823
|
-
&& this.hasTrustedClaim(issue,
|
|
1926
|
+
&& this.hasTrustedClaim(issue, active.record);
|
|
1824
1927
|
}
|
|
1825
|
-
hasTrustedClaim(issue,
|
|
1826
|
-
const exactBody = claimComment(runId, issueNumber, branchName);
|
|
1827
|
-
const
|
|
1828
|
-
const markers = issue.comments.filter((comment) => markerPattern.test(comment.body.split('\n')[0] ?? ''));
|
|
1928
|
+
hasTrustedClaim(issue, record) {
|
|
1929
|
+
const exactBody = claimComment(record.runId, record.issueNumber, record.branchName);
|
|
1930
|
+
const markers = issue.comments.filter(isClaimMarkerComment);
|
|
1829
1931
|
if (issue.comments.some((comment) => {
|
|
1830
1932
|
const firstLine = comment.body.split('\n')[0] ?? '';
|
|
1831
|
-
return firstLine.startsWith(`<!-- codex-orchestrator:run:${runId}:claim`) && !
|
|
1933
|
+
return firstLine.startsWith(`<!-- codex-orchestrator:run:${record.runId}:claim`) && !claimMarkerPattern.test(firstLine);
|
|
1832
1934
|
}))
|
|
1833
1935
|
return false;
|
|
1834
|
-
|
|
1936
|
+
const currentMarkers = markers.filter((comment) => claimRunId(comment) === record.runId);
|
|
1937
|
+
if (currentMarkers.some((comment) => comment.body !== exactBody
|
|
1938
|
+
|| !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.authorAssociation)))
|
|
1835
1939
|
return false;
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1940
|
+
const historicalClaims = historicalClaimCounts(record);
|
|
1941
|
+
for (const comment of markers) {
|
|
1942
|
+
const markerRunId = claimRunId(comment);
|
|
1943
|
+
if (markerRunId === record.runId)
|
|
1944
|
+
continue;
|
|
1945
|
+
const key = observedHistoricalClaimKeys(comment, record)
|
|
1946
|
+
.find((candidate) => (historicalClaims.get(candidate) ?? 0) > 0);
|
|
1947
|
+
if (!key)
|
|
1948
|
+
return false;
|
|
1949
|
+
historicalClaims.set(key, historicalClaims.get(key) - 1);
|
|
1950
|
+
}
|
|
1951
|
+
return currentMarkers.length === 1;
|
|
1838
1952
|
}
|
|
1839
1953
|
async readIssue(issueNumber) {
|
|
1840
1954
|
try {
|
|
@@ -1848,7 +1962,7 @@ export class RunIssue {
|
|
|
1848
1962
|
const batch = active.record.reviewFeedback?.activeBatch;
|
|
1849
1963
|
if (!batch)
|
|
1850
1964
|
return undefined;
|
|
1851
|
-
if (!await this.authorized(
|
|
1965
|
+
if (!await this.authorized(active, config)) {
|
|
1852
1966
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-authority-revoked');
|
|
1853
1967
|
}
|
|
1854
1968
|
if (!this.dependencies.reviewFeedback) {
|
|
@@ -1917,9 +2031,7 @@ export class RunIssue {
|
|
|
1917
2031
|
affectedDefectIds: [...directReview.review.affectedDefectIds],
|
|
1918
2032
|
fixedRepairFindings: directReview.repairFindings.filter((finding) => finding.status === 'fixed')
|
|
1919
2033
|
.map((finding) => ({ id: finding.id, affectedContracts: [...finding.affectedContracts] })),
|
|
1920
|
-
|
|
1921
|
-
? [...directReview.review.coverage]
|
|
1922
|
-
: ['acceptance-criteria', 'correctness', 'test-quality'],
|
|
2034
|
+
reviewFocus: ['acceptance-criteria', 'correctness', 'test-quality'],
|
|
1923
2035
|
workflowGeneration: structuredClone(active.record.workflowGeneration),
|
|
1924
2036
|
repairOnly: reportRepair !== undefined,
|
|
1925
2037
|
originalReportSha256: reportRepair?.originalReportSha256 ?? null,
|
|
@@ -1989,14 +2101,17 @@ export class RunIssue {
|
|
|
1989
2101
|
const current = active.record.directReview;
|
|
1990
2102
|
if (!current)
|
|
1991
2103
|
return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
|
|
1992
|
-
if (current.review.reportRepairs >=
|
|
1993
|
-
return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
|
|
2104
|
+
if (current.review.reportRepairs >= MAX_DIRECT_REVIEW_REPORT_REPAIRS) {
|
|
2105
|
+
return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' }, 'direct-review-report-malformed');
|
|
1994
2106
|
}
|
|
1995
2107
|
const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
|
|
1996
2108
|
active = await this.persist(active, {
|
|
1997
2109
|
directReview: {
|
|
1998
2110
|
...withoutInvocation,
|
|
1999
|
-
review: {
|
|
2111
|
+
review: {
|
|
2112
|
+
...withoutInvocation.review,
|
|
2113
|
+
reportRepairs: (withoutInvocation.review.reportRepairs + 1),
|
|
2114
|
+
},
|
|
2000
2115
|
},
|
|
2001
2116
|
});
|
|
2002
2117
|
reportRepair = {
|
|
@@ -2071,6 +2186,89 @@ export class RunIssue {
|
|
|
2071
2186
|
}
|
|
2072
2187
|
return this.persist(starting, { directReview: withoutInvocation });
|
|
2073
2188
|
}
|
|
2189
|
+
async captureBaselineChecks(starting, config) {
|
|
2190
|
+
let active = starting;
|
|
2191
|
+
if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0)
|
|
2192
|
+
return { active };
|
|
2193
|
+
const configuredChecks = Object.entries(config.checks);
|
|
2194
|
+
const reusableBaselineChecks = (active.record.baselineChecks ?? []).filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
|
|
2195
|
+
if (reusableBaselineChecks.length !== (active.record.baselineChecks ?? []).length) {
|
|
2196
|
+
active = await this.persist(active, { baselineChecks: reusableBaselineChecks });
|
|
2197
|
+
}
|
|
2198
|
+
for (const [id, command] of Object.entries(config.checks)) {
|
|
2199
|
+
if (active.record.baselineChecks?.some((check) => check.id === id && check.command === command))
|
|
2200
|
+
continue;
|
|
2201
|
+
if (this.signal.aborted)
|
|
2202
|
+
return this.terminal(active, { status: 'cancelled' });
|
|
2203
|
+
let check;
|
|
2204
|
+
try {
|
|
2205
|
+
check = await this.dependencies.checks.run({
|
|
2206
|
+
id,
|
|
2207
|
+
command,
|
|
2208
|
+
cwd: active.record.worktreePath,
|
|
2209
|
+
phase: 'baseline',
|
|
2210
|
+
signal: this.signal,
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
catch {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0) {
|
|
2217
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'baseline-check-mutated-worktree');
|
|
2218
|
+
}
|
|
2219
|
+
active = await this.persist(active, {
|
|
2220
|
+
baselineChecks: [
|
|
2221
|
+
...(active.record.baselineChecks ?? []),
|
|
2222
|
+
{ id, command, status: check.status, outputSha256: check.outputSha256 },
|
|
2223
|
+
],
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
return { active };
|
|
2227
|
+
}
|
|
2228
|
+
async recoverTerminalReviewReport(active, issue, config, frozenCriteria) {
|
|
2229
|
+
const directReview = active.record.directReview;
|
|
2230
|
+
const allowLegacyMalformed = directReview?.terminalCode === undefined
|
|
2231
|
+
&& active.record.outcomeEvidenceId === `evidence:${active.record.runId}:direct-review-report-malformed`;
|
|
2232
|
+
if (!issue || !directReview || active.record.intent || active.record.process
|
|
2233
|
+
|| active.record.terminalOutcome?.status !== 'internal-error'
|
|
2234
|
+
|| directReview.status !== 'terminal' || directReview.terminalOutcome?.status !== 'internal-error'
|
|
2235
|
+
|| !canRecoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed })
|
|
2236
|
+
|| !this.isAuthorizedIssue(issue, active.record, config))
|
|
2237
|
+
return undefined;
|
|
2238
|
+
let worktree;
|
|
2239
|
+
try {
|
|
2240
|
+
worktree = await this.dependencies.git.inspectWorktree({
|
|
2241
|
+
worktreePath: active.record.worktreePath,
|
|
2242
|
+
branchName: active.record.branchName,
|
|
2243
|
+
baseSha: active.record.baseSha,
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
catch {
|
|
2247
|
+
return undefined;
|
|
2248
|
+
}
|
|
2249
|
+
if (worktree !== 'matching'
|
|
2250
|
+
|| await this.dependencies.git.getHead(active.record.worktreePath) !== expectedImplementationHead(active.record))
|
|
2251
|
+
return undefined;
|
|
2252
|
+
const changedFiles = await this.dependencies.git.listChangedFiles(active.record.worktreePath);
|
|
2253
|
+
if (changedFiles.length === 0 || !active.record.routeReceipt)
|
|
2254
|
+
return undefined;
|
|
2255
|
+
const targetFingerprint = directReviewTargetFingerprint({
|
|
2256
|
+
snapshot: await this.dependencies.git.snapshot(active.record.worktreePath),
|
|
2257
|
+
changedFiles,
|
|
2258
|
+
routeDecisionSha256: active.record.routeReceipt.decisionSha256,
|
|
2259
|
+
workflowGenerationHash: active.record.workflowGeneration.generationHash,
|
|
2260
|
+
cycle: active.record.cycle,
|
|
2261
|
+
frozenCriteria,
|
|
2262
|
+
});
|
|
2263
|
+
if (targetFingerprint !== directReview.targetFingerprint)
|
|
2264
|
+
return undefined;
|
|
2265
|
+
return this.persist(active, {
|
|
2266
|
+
lifecycle: 'implementing',
|
|
2267
|
+
directReview: recoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed }),
|
|
2268
|
+
terminalOutcome: undefined,
|
|
2269
|
+
outcomeEvidenceId: undefined,
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2074
2272
|
async startNextCycle(active, findings, sources) {
|
|
2075
2273
|
if (active.record.directReview?.status === 'clear') {
|
|
2076
2274
|
const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
|
|
@@ -2204,14 +2402,109 @@ export class RunIssue {
|
|
|
2204
2402
|
async revoked(active) {
|
|
2205
2403
|
return this.terminal(await this.clearIntent(active), { status: 'blocked', kind: 'safety', resumable: true });
|
|
2206
2404
|
}
|
|
2207
|
-
async invokedFailure(active, code) {
|
|
2208
|
-
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary
|
|
2405
|
+
async invokedFailure(active, code, summary = 'Publication delivery requires reconciliation.') {
|
|
2406
|
+
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary });
|
|
2209
2407
|
return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
|
|
2210
2408
|
}
|
|
2211
2409
|
async terminal(active, outcome, evidenceCode = outcome.status, retainIntent = false) {
|
|
2212
2410
|
if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
|
|
2213
2411
|
return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
|
|
2214
2412
|
}
|
|
2413
|
+
if (outcome.status === 'blocked') {
|
|
2414
|
+
return this.publishBlockedTerminal(active, outcome, evidenceCode);
|
|
2415
|
+
}
|
|
2416
|
+
return this.persistTerminal(active, outcome, evidenceCode, retainIntent);
|
|
2417
|
+
}
|
|
2418
|
+
async publishBlockedTerminal(starting, outcome, evidenceCode) {
|
|
2419
|
+
let active = starting;
|
|
2420
|
+
const autoBlocked = [active.config.github.labels.auto.name, active.config.github.labels.blocked.name].sort();
|
|
2421
|
+
const blockedOnly = [active.config.github.labels.blocked.name];
|
|
2422
|
+
let expected = autoBlocked;
|
|
2423
|
+
const existing = active.record.intent;
|
|
2424
|
+
if (!existing) {
|
|
2425
|
+
active = await this.persist(active, { intent: {
|
|
2426
|
+
kind: 'blocked-labels',
|
|
2427
|
+
issueNumber: active.record.issueNumber,
|
|
2428
|
+
expected,
|
|
2429
|
+
blockKind: outcome.kind,
|
|
2430
|
+
resumable: outcome.resumable,
|
|
2431
|
+
evidenceCode,
|
|
2432
|
+
} });
|
|
2433
|
+
}
|
|
2434
|
+
else if (existing.kind !== 'blocked-labels'
|
|
2435
|
+
|| existing.issueNumber !== active.record.issueNumber
|
|
2436
|
+
|| existing.blockKind !== outcome.kind
|
|
2437
|
+
|| existing.resumable !== outcome.resumable
|
|
2438
|
+
|| existing.evidenceCode !== evidenceCode) {
|
|
2439
|
+
return this.invokedFailure(active, 'blocked-labels-intent-diverged');
|
|
2440
|
+
}
|
|
2441
|
+
else {
|
|
2442
|
+
expected = existing.expected;
|
|
2443
|
+
if (!sameStrings(expected, autoBlocked) && !sameStrings(expected, blockedOnly) && expected.length !== 0) {
|
|
2444
|
+
return this.invokedFailure(active, 'blocked-labels-intent-diverged');
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
let issue = await this.readIssue(active.record.issueNumber);
|
|
2448
|
+
if (!issue)
|
|
2449
|
+
return this.invokedFailure(active, 'blocked-labels-issue-missing');
|
|
2450
|
+
let projection = blockedLabelProjection(issue.labels, active.config);
|
|
2451
|
+
if (issue.state === 'OPEN' && projection.status === 'transition') {
|
|
2452
|
+
if (!this.dependencies.issues.transitionToBlocked) {
|
|
2453
|
+
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
2454
|
+
}
|
|
2455
|
+
try {
|
|
2456
|
+
await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
|
|
2457
|
+
}
|
|
2458
|
+
catch {
|
|
2459
|
+
return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
|
|
2460
|
+
}
|
|
2461
|
+
issue = await this.readIssue(active.record.issueNumber);
|
|
2462
|
+
projection = issue ? blockedLabelProjection(issue.labels, active.config) : { status: 'diverged' };
|
|
2463
|
+
}
|
|
2464
|
+
if (!issue)
|
|
2465
|
+
return this.invokedFailure(active, 'blocked-labels-observation-missing');
|
|
2466
|
+
if (issue.state === 'OPEN') {
|
|
2467
|
+
if (projection.status !== 'settled') {
|
|
2468
|
+
return this.invokedFailure(active, 'blocked-labels-observation-diverged');
|
|
2469
|
+
}
|
|
2470
|
+
if (!sameStrings(expected, projection.expected)) {
|
|
2471
|
+
expected = projection.expected;
|
|
2472
|
+
const intent = active.record.intent;
|
|
2473
|
+
if (intent?.kind !== 'blocked-labels')
|
|
2474
|
+
return this.invokedFailure(active, 'blocked-labels-intent-diverged');
|
|
2475
|
+
try {
|
|
2476
|
+
active = await this.persist(active, { intent: { ...intent, expected } });
|
|
2477
|
+
}
|
|
2478
|
+
catch {
|
|
2479
|
+
throw new PostEffectStateError(active);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
try {
|
|
2484
|
+
return await this.persistTerminal(active, outcome, evidenceCode, false);
|
|
2485
|
+
}
|
|
2486
|
+
catch {
|
|
2487
|
+
throw new PostEffectStateError(active);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
async reconcilePersistedBlockedTerminal(active, issue, outcome) {
|
|
2491
|
+
if (!this.dependencies.issues.transitionToBlocked) {
|
|
2492
|
+
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
2493
|
+
}
|
|
2494
|
+
try {
|
|
2495
|
+
await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
|
|
2496
|
+
}
|
|
2497
|
+
catch {
|
|
2498
|
+
return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
|
|
2499
|
+
}
|
|
2500
|
+
const observation = await this.readIssue(active.record.issueNumber);
|
|
2501
|
+
if (!observation || observation.state !== 'OPEN'
|
|
2502
|
+
|| blockedLabelProjection(observation.labels, active.config).status !== 'settled') {
|
|
2503
|
+
return this.invokedFailure(active, 'blocked-labels-observation-diverged');
|
|
2504
|
+
}
|
|
2505
|
+
return publicOutcome(outcome);
|
|
2506
|
+
}
|
|
2507
|
+
async persistTerminal(active, outcome, evidenceCode, retainIntent) {
|
|
2215
2508
|
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
|
|
2216
2509
|
const terminalOutcome = { ...outcome, evidencePath: evidence.path };
|
|
2217
2510
|
const changes = {
|
|
@@ -2223,7 +2516,7 @@ export class RunIssue {
|
|
|
2223
2516
|
if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
|
|
2224
2517
|
changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
|
|
2225
2518
|
? { status: 'blocked', kind: outcome.kind }
|
|
2226
|
-
: { status: outcome.status });
|
|
2519
|
+
: { status: outcome.status }, outcome.status === 'internal-error' ? outcome.code : undefined);
|
|
2227
2520
|
}
|
|
2228
2521
|
if (active.record.waitingHuman && (active.record.lifecycle === 'waiting-human' || active.record.waitingHuman.phase === 'resumed')) {
|
|
2229
2522
|
if (active.record.waitingHuman.phase === 'resumed') {
|
|
@@ -2285,6 +2578,63 @@ class PostEffectStateError extends Error {
|
|
|
2285
2578
|
this.active = active;
|
|
2286
2579
|
}
|
|
2287
2580
|
}
|
|
2581
|
+
const claimMarkerPattern = /^<!-- codex-orchestrator:run:([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):claim -->$/u;
|
|
2582
|
+
function isClaimMarkerComment(comment) {
|
|
2583
|
+
return claimMarkerPattern.test(comment.body.split('\n')[0] ?? '');
|
|
2584
|
+
}
|
|
2585
|
+
function claimRunId(comment) {
|
|
2586
|
+
return (comment.body.split('\n')[0] ?? '').match(claimMarkerPattern)?.[1];
|
|
2587
|
+
}
|
|
2588
|
+
function trustedHistoricalClaimBodyKey(comment, issueNumber, branchName) {
|
|
2589
|
+
const runId = claimRunId(comment);
|
|
2590
|
+
if (!runId || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.authorAssociation))
|
|
2591
|
+
return undefined;
|
|
2592
|
+
if (comment.body !== claimComment(runId, issueNumber, branchName))
|
|
2593
|
+
return undefined;
|
|
2594
|
+
return `${comment.authorAssociation}\u0000${comment.body}`;
|
|
2595
|
+
}
|
|
2596
|
+
function frozenHistoricalClaimKey(comment, issueNumber, branchName) {
|
|
2597
|
+
const bodyKey = trustedHistoricalClaimBodyKey(comment, issueNumber, branchName);
|
|
2598
|
+
if (!bodyKey)
|
|
2599
|
+
return undefined;
|
|
2600
|
+
return comment.id ? `id\u0000${comment.id}\u0000${bodyKey}` : `legacy\u0000${bodyKey}`;
|
|
2601
|
+
}
|
|
2602
|
+
function observedHistoricalClaimKeys(comment, record) {
|
|
2603
|
+
const bodyKey = trustedHistoricalClaimBodyKey(comment, record.issueNumber, record.branchName);
|
|
2604
|
+
if (!bodyKey)
|
|
2605
|
+
return [];
|
|
2606
|
+
const keys = comment.id ? [`id\u0000${comment.id}\u0000${bodyKey}`] : [];
|
|
2607
|
+
if (timestampAtOrBefore(comment.createdAt, record.createdAt)
|
|
2608
|
+
&& timestampAtOrBefore(comment.updatedAt, record.createdAt)) {
|
|
2609
|
+
keys.push(`legacy\u0000${bodyKey}`);
|
|
2610
|
+
}
|
|
2611
|
+
return keys;
|
|
2612
|
+
}
|
|
2613
|
+
function timestampAtOrBefore(value, cutoff) {
|
|
2614
|
+
if (!value)
|
|
2615
|
+
return false;
|
|
2616
|
+
const timestamp = Date.parse(value);
|
|
2617
|
+
return !Number.isNaN(timestamp) && timestamp <= Date.parse(cutoff);
|
|
2618
|
+
}
|
|
2619
|
+
function historicalClaimCounts(record) {
|
|
2620
|
+
const counts = new Map();
|
|
2621
|
+
for (const comment of record.issueSnapshot.comments ?? []) {
|
|
2622
|
+
const key = frozenHistoricalClaimKey(comment, record.issueNumber, record.branchName);
|
|
2623
|
+
if (!key || claimRunId(comment) === record.runId)
|
|
2624
|
+
continue;
|
|
2625
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2626
|
+
}
|
|
2627
|
+
return counts;
|
|
2628
|
+
}
|
|
2629
|
+
function refreshClaimedIssueSnapshot(baseline, issue) {
|
|
2630
|
+
return {
|
|
2631
|
+
...structuredClone(baseline),
|
|
2632
|
+
comments: [
|
|
2633
|
+
...(baseline.comments ?? []).filter(isClaimMarkerComment),
|
|
2634
|
+
...issue.comments.filter((comment) => !isClaimMarkerComment(comment)),
|
|
2635
|
+
],
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2288
2638
|
function snapshotIssue(issue) {
|
|
2289
2639
|
if (issue.state !== 'OPEN')
|
|
2290
2640
|
throw new Error('cannot snapshot a closed issue');
|
|
@@ -2406,6 +2756,36 @@ function sameStrings(left, right) {
|
|
|
2406
2756
|
const b = [...right].sort();
|
|
2407
2757
|
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
2408
2758
|
}
|
|
2759
|
+
function blockedLabelPolicy(config) {
|
|
2760
|
+
return {
|
|
2761
|
+
auto: config.github.labels.auto.name,
|
|
2762
|
+
running: config.github.labels.running.name,
|
|
2763
|
+
blocked: config.github.labels.blocked.name,
|
|
2764
|
+
review: config.github.labels.review.name,
|
|
2765
|
+
waitingHuman: config.github.labels.waitingHuman.name,
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
function blockedLabelProjection(labels, config) {
|
|
2769
|
+
const policy = blockedLabelPolicy(config);
|
|
2770
|
+
const present = new Set(labels);
|
|
2771
|
+
if (present.has(policy.review) || present.has(policy.waitingHuman))
|
|
2772
|
+
return { status: 'diverged' };
|
|
2773
|
+
const auto = present.has(policy.auto);
|
|
2774
|
+
const running = present.has(policy.running);
|
|
2775
|
+
const blocked = present.has(policy.blocked);
|
|
2776
|
+
if (running || (auto && !blocked))
|
|
2777
|
+
return { status: 'transition' };
|
|
2778
|
+
if (blocked)
|
|
2779
|
+
return { status: 'settled', expected: auto ? [policy.auto, policy.blocked].sort() : [policy.blocked] };
|
|
2780
|
+
return { status: 'settled', expected: [] };
|
|
2781
|
+
}
|
|
2782
|
+
function sameCheckPolicy(checks, policy) {
|
|
2783
|
+
const expected = Object.entries(policy).sort(([left], [right]) => left.localeCompare(right));
|
|
2784
|
+
const actual = checks.map((check) => [check.id, check.command])
|
|
2785
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
2786
|
+
return actual.length === expected.length
|
|
2787
|
+
&& actual.every(([id, command], index) => id === expected[index]?.[0] && command === expected[index]?.[1]);
|
|
2788
|
+
}
|
|
2409
2789
|
function sameFreshness(left, right) {
|
|
2410
2790
|
return left.headSha === right.headSha
|
|
2411
2791
|
&& left.indexTreeSha === right.indexTreeSha
|
|
@@ -2422,6 +2802,21 @@ function commentsWithMarker(issue, marker) {
|
|
|
2422
2802
|
function sortedUnique(values) {
|
|
2423
2803
|
return [...new Set(values)].sort();
|
|
2424
2804
|
}
|
|
2805
|
+
function worktreeCreationFailureSummary(error) {
|
|
2806
|
+
return boundedLocalGitFailureSummary('Local Git worktree creation failed; correct the local Git state and retry the same run.', error);
|
|
2807
|
+
}
|
|
2808
|
+
function claimedWorktreeInspectionFailureSummary(error) {
|
|
2809
|
+
return boundedLocalGitFailureSummary('The claimed worktree could not be inspected; correct or preserve the local artifact and retry the same run.', error);
|
|
2810
|
+
}
|
|
2811
|
+
function boundedLocalGitFailureSummary(prefix, error) {
|
|
2812
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
2813
|
+
const diagnostic = raw
|
|
2814
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, ' ')
|
|
2815
|
+
.trim();
|
|
2816
|
+
if (diagnostic.length === 0)
|
|
2817
|
+
return prefix;
|
|
2818
|
+
return `${prefix}\n${diagnostic.slice(0, 4 * 1024 - prefix.length - 1)}`;
|
|
2819
|
+
}
|
|
2425
2820
|
function assertUuid(value) {
|
|
2426
2821
|
if (typeof value !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value))
|
|
2427
2822
|
throw new Error('runId is invalid');
|