codex-orchestrator 2.0.14 → 2.0.15
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 +14 -0
- package/dist/src/v2/adapters/gh-issue-adapter.js +2 -0
- package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +2 -1
- package/dist/src/v2/containment.js.map +1 -1
- package/dist/src/v2/implementation-report.d.ts +5 -1
- package/dist/src/v2/implementation-report.d.ts.map +1 -1
- package/dist/src/v2/implementation-report.js +46 -1
- package/dist/src/v2/implementation-report.js.map +1 -1
- package/dist/src/v2/pending-effect-settlement.d.ts +2 -2
- package/dist/src/v2/pending-effect-settlement.d.ts.map +1 -1
- package/dist/src/v2/pending-effect-settlement.js +2 -2
- package/dist/src/v2/pending-effect-settlement.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 +84 -4
- package/dist/src/v2/review-feedback-coordinator.js.map +1 -1
- package/dist/src/v2/review-feedback.d.ts +14 -2
- package/dist/src/v2/review-feedback.d.ts.map +1 -1
- package/dist/src/v2/review-feedback.js +74 -6
- package/dist/src/v2/review-feedback.js.map +1 -1
- package/dist/src/v2/run-issue.d.ts +16 -7
- package/dist/src/v2/run-issue.d.ts.map +1 -1
- package/dist/src/v2/run-issue.js +606 -316
- package/dist/src/v2/run-issue.js.map +1 -1
- package/dist/src/v2/run-state-projections.d.ts +0 -13
- package/dist/src/v2/run-state-projections.d.ts.map +1 -1
- package/dist/src/v2/run-state-projections.js +0 -20
- package/dist/src/v2/run-state-projections.js.map +1 -1
- package/dist/src/v2/run-store.d.ts +51 -30
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +106 -34
- package/dist/src/v2/run-store.js.map +1 -1
- package/dist/src/v2/runtime.d.ts +10 -0
- package/dist/src/v2/runtime.d.ts.map +1 -1
- package/dist/src/v2/runtime.js +23 -14
- package/dist/src/v2/runtime.js.map +1 -1
- package/dist/src/v2/validation-progression.d.ts +1 -1
- package/dist/src/v2/validation-progression.d.ts.map +1 -1
- package/dist/src/v2/validation-progression.js.map +1 -1
- package/internal-workflow/manifest.json +1 -1
- package/internal-workflow/operations/implementation/SKILL.md +6 -1
- package/internal-workflow/schemas/implementation-report-v1.json +1 -1
- package/package.json +1 -1
package/dist/src/v2/run-issue.js
CHANGED
|
@@ -11,9 +11,9 @@ import { CandidateProofInspectionError, ProofLaunchAuthorizationError } from './
|
|
|
11
11
|
import { CheckProcessQuiescenceError, resolveIssueCheckPolicy } from './issue-check-policy.js';
|
|
12
12
|
import { IssueInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
|
|
13
13
|
import { createPendingEffect } from './run-store.js';
|
|
14
|
-
import { blockReviewFeedback, initializeReviewFeedback, projectReviewFeedbackBatch, publishReviewFeedback, reserveNextReviewFeedbackRound, } from './review-feedback.js';
|
|
14
|
+
import { blockReviewFeedback, initializeReviewFeedback, projectReviewFeedbackBatch, publishReviewFeedback, respondReviewFeedback, reserveNextReviewFeedbackRound, settleReviewFeedbackResponse, } from './review-feedback.js';
|
|
15
15
|
import { createIssueDeliveryAuthority } from './delivery-authority.js';
|
|
16
|
-
import {
|
|
16
|
+
import { claimComment, claimRunId, freezeCriteria, historicalClaimCounts, isClaimMarkerComment, isExactClaimMarkerLine, isAdoptableAttempt, observedHistoricalClaimKeys, outcomeEvidenceBytes, publicOutcome, publicIssueSnapshot, refreshClaimedIssueSnapshot, snapshotIssue, sameInspectionIdentity, sameStrings, semanticChangesMatch, } from './run-state-projections.js';
|
|
17
17
|
import { applyValidationTransition, nextValidationTransition, projectValidationFeedbackActivation, projectValidationProofPassed, projectValidationProofStart, projectValidationRepair, projectValidationReviewApproved, projectValidationReviewNeedsWork, projectValidationReviewReportRepair, projectValidationReviewTransportRetry, } from './validation-progression.js';
|
|
18
18
|
import { settleCommentEffect, settleCommitEffect, settleCleanupEffect, settleDraftPullRequestEffect, settleLabelsEffect, settlePushEffect, } from './pending-effect-settlement.js';
|
|
19
19
|
export class OwnerLockSafetyError extends Error {
|
|
@@ -77,7 +77,7 @@ export class RunIssue {
|
|
|
77
77
|
if (!observation || observation.state !== 'OPEN' || !this.hasTrustedClaim(observation, active.record)) {
|
|
78
78
|
return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
|
|
79
79
|
}
|
|
80
|
-
if (
|
|
80
|
+
if (claimLabelProjectionSettled(observation.labels, config))
|
|
81
81
|
return { active };
|
|
82
82
|
const labels = new Set(observation.labels);
|
|
83
83
|
if (!labels.has(config.github.labels.auto.name)
|
|
@@ -98,7 +98,7 @@ export class RunIssue {
|
|
|
98
98
|
observation = await this.readIssue(issueNumber);
|
|
99
99
|
if (!observation || !this.hasTrustedClaim(observation, active.record))
|
|
100
100
|
return 'diverged';
|
|
101
|
-
return
|
|
101
|
+
return claimLabelProjectionSettled(observation.labels, config) ? 'confirmed' : 'absent';
|
|
102
102
|
},
|
|
103
103
|
invoke: () => this.dependencies.issues.setLabels(issueNumber, expectedLabels),
|
|
104
104
|
});
|
|
@@ -241,7 +241,7 @@ export class RunIssue {
|
|
|
241
241
|
});
|
|
242
242
|
if (settlement.status === 'unauthorized') {
|
|
243
243
|
return this.signal.aborted
|
|
244
|
-
? await this.terminal(await this.
|
|
244
|
+
? await this.terminal(await this.confirmEffect(active), { status: 'cancelled' })
|
|
245
245
|
: await this.revoked(active);
|
|
246
246
|
}
|
|
247
247
|
if (settlement.status === 'unknown')
|
|
@@ -273,7 +273,7 @@ export class RunIssue {
|
|
|
273
273
|
});
|
|
274
274
|
if (settlement.status === 'unauthorized') {
|
|
275
275
|
return this.signal.aborted
|
|
276
|
-
? await this.terminal(await this.
|
|
276
|
+
? await this.terminal(await this.confirmEffect(active), { status: 'cancelled' })
|
|
277
277
|
: await this.revoked(active);
|
|
278
278
|
}
|
|
279
279
|
if (settlement.status === 'unknown')
|
|
@@ -320,7 +320,7 @@ export class RunIssue {
|
|
|
320
320
|
});
|
|
321
321
|
if (settlement.status === 'unauthorized') {
|
|
322
322
|
return this.signal.aborted
|
|
323
|
-
? await this.terminal(await this.
|
|
323
|
+
? await this.terminal(await this.confirmEffect(active), { status: 'cancelled' })
|
|
324
324
|
: await this.revoked(active);
|
|
325
325
|
}
|
|
326
326
|
if (settlement.status === 'unknown')
|
|
@@ -332,74 +332,9 @@ export class RunIssue {
|
|
|
332
332
|
const pullRequest = await this.dependencies.pullRequests.findOpen({ headBranch: branchName, baseBranch: config.github.baseBranch });
|
|
333
333
|
if (!pullRequest || pullRequest.body !== prBody)
|
|
334
334
|
return await this.publicationDiverged(active, 'pr-missing-before-handoff');
|
|
335
|
-
|
|
336
|
-
const handoffBody = `${handoffMarker}\nReview-ready draft PR: ${pullRequest.url}`;
|
|
337
|
-
if (active.record.pendingEffect?.kind === 'handoff-comment' || !active.record.pendingEffect) {
|
|
338
|
-
if (!active.record.pendingEffect) {
|
|
339
|
-
active = await this.persist(active, {
|
|
340
|
-
pendingEffect: { kind: 'handoff-comment', issueNumber, marker: handoffMarker, bodySha256: sha256(handoffBody) },
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
const pendingEffect = active.record.pendingEffect;
|
|
344
|
-
if (pendingEffect?.kind !== 'handoff-comment' || pendingEffect.issueNumber !== issueNumber
|
|
345
|
-
|| pendingEffect.marker !== handoffMarker || pendingEffect.bodySha256 !== sha256(handoffBody)) {
|
|
346
|
-
return this.publicationDiverged(active, 'handoff-comment-pendingEffect-diverged');
|
|
347
|
-
}
|
|
348
|
-
const settlement = await settleCommentEffect(pendingEffect, {
|
|
349
|
-
observe: async () => {
|
|
350
|
-
const observation = await this.readIssue(issueNumber);
|
|
351
|
-
if (!observation)
|
|
352
|
-
return 'diverged';
|
|
353
|
-
const matching = commentsWithMarker(observation, handoffMarker);
|
|
354
|
-
if (matching.length === 0)
|
|
355
|
-
return 'absent';
|
|
356
|
-
return matching.length === 1 && sha256(matching[0].body) === sha256(handoffBody) ? 'confirmed' : 'diverged';
|
|
357
|
-
},
|
|
358
|
-
authorize: async () => !this.signal.aborted && await this.authorized(active, config),
|
|
359
|
-
invoke: () => this.dependencies.issues.postComment(issueNumber, handoffBody),
|
|
360
|
-
});
|
|
361
|
-
if (settlement.status === 'unauthorized') {
|
|
362
|
-
return this.signal.aborted
|
|
363
|
-
? await this.terminal(await this.clearEffect(active), { status: 'cancelled' })
|
|
364
|
-
: await this.revoked(active);
|
|
365
|
-
}
|
|
366
|
-
if (settlement.status === 'unknown')
|
|
367
|
-
return await this.invokedFailure(active, 'handoff-comment-delivery-unknown');
|
|
368
|
-
if (settlement.status !== 'confirmed')
|
|
369
|
-
return await this.publicationDiverged(active, 'handoff-comment-observation-diverged');
|
|
370
|
-
active = await this.confirmEffect(active);
|
|
371
|
-
}
|
|
372
|
-
const terminalLabels = [config.github.labels.review.name];
|
|
373
|
-
if (active.record.pendingEffect?.kind === 'final-labels' || !active.record.pendingEffect) {
|
|
374
|
-
if (!active.record.pendingEffect)
|
|
375
|
-
active = await this.persist(active, { pendingEffect: { kind: 'final-labels', issueNumber, expected: terminalLabels } });
|
|
376
|
-
const pendingEffect = active.record.pendingEffect;
|
|
377
|
-
if (pendingEffect?.kind !== 'final-labels' || pendingEffect.issueNumber !== issueNumber
|
|
378
|
-
|| !sameStrings(pendingEffect.expected, terminalLabels)) {
|
|
379
|
-
return this.publicationDiverged(active, 'terminal-labels-pendingEffect-diverged');
|
|
380
|
-
}
|
|
381
|
-
const settlement = await settleLabelsEffect(pendingEffect, {
|
|
382
|
-
observe: async () => {
|
|
383
|
-
const observation = await this.readIssue(issueNumber);
|
|
384
|
-
return !observation ? 'diverged' : sameStrings(observation.labels, terminalLabels) ? 'confirmed' : 'absent';
|
|
385
|
-
},
|
|
386
|
-
authorize: async () => !this.signal.aborted && await this.authorized(active, config),
|
|
387
|
-
invoke: () => this.dependencies.issues.setLabels(issueNumber, terminalLabels),
|
|
388
|
-
});
|
|
389
|
-
if (settlement.status === 'unauthorized') {
|
|
390
|
-
return this.signal.aborted
|
|
391
|
-
? await this.terminal(await this.clearEffect(active), { status: 'cancelled' })
|
|
392
|
-
: await this.revoked(active);
|
|
393
|
-
}
|
|
394
|
-
if (settlement.status === 'unknown')
|
|
395
|
-
return await this.invokedFailure(active, 'terminal-labels-delivery-unknown');
|
|
396
|
-
if (settlement.status !== 'confirmed')
|
|
397
|
-
return await this.publicationDiverged(active, 'terminal-labels-diverged');
|
|
398
|
-
active = await this.confirmEffect(active);
|
|
399
|
-
}
|
|
400
|
-
return this.persistTerminal(active, {
|
|
335
|
+
return this.terminal(active, {
|
|
401
336
|
status: 'review-ready', pullRequestUrl: pullRequest.url, continuationEpoch: commitSha,
|
|
402
|
-
}, 'review-ready',
|
|
337
|
+
}, 'review-ready', {
|
|
403
338
|
reviewFeedback: initializeReviewFeedback(active.record.reviewFeedback ?? {
|
|
404
339
|
version: 1, updateEpoch: 0, consumedSourceIds: [], previousPublishedHeadSha: null,
|
|
405
340
|
repairRound: 0, activeBatch: null, history: [], verifiedReceipt: null,
|
|
@@ -484,7 +419,7 @@ export class RunIssue {
|
|
|
484
419
|
|| pendingEffect.message !== message || pendingEffect.candidateRef !== candidateBinding.candidateRef) {
|
|
485
420
|
return this.persistRetainedCommitIntentTerminal(active, 'review-feedback-candidate-pendingEffect-diverged');
|
|
486
421
|
}
|
|
487
|
-
let
|
|
422
|
+
let authorizationValidation;
|
|
488
423
|
const settlement = await settleCommitEffect(pendingEffect, {
|
|
489
424
|
observe: async () => {
|
|
490
425
|
const observed = await candidate.createOrObserveCommit({
|
|
@@ -500,9 +435,8 @@ export class RunIssue {
|
|
|
500
435
|
authorize: async () => {
|
|
501
436
|
if (!await this.authorized(active, config))
|
|
502
437
|
return false;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return !authorizationFailure;
|
|
438
|
+
authorizationValidation = await coordinator.revalidate({ batch, issueNumber, epoch: 'pre-update', expectedHeadSha: oldHead });
|
|
439
|
+
return authorizationValidation.status === 'valid';
|
|
506
440
|
},
|
|
507
441
|
invoke: async () => {
|
|
508
442
|
const invoked = await candidate.createOrObserveCommit({
|
|
@@ -515,8 +449,16 @@ export class RunIssue {
|
|
|
515
449
|
},
|
|
516
450
|
});
|
|
517
451
|
if (settlement.status === 'unauthorized') {
|
|
518
|
-
|
|
519
|
-
|
|
452
|
+
if (authorizationValidation?.status === 'retryable') {
|
|
453
|
+
return this.invokedFailure(active, 'review-feedback-precommit-revalidation-failed-retryable');
|
|
454
|
+
}
|
|
455
|
+
active = await this.confirmEffect(active);
|
|
456
|
+
if (authorizationValidation) {
|
|
457
|
+
const failure = await this.mapFeedbackRevalidation(active, authorizationValidation, 'review-feedback-precommit-revalidation-failed');
|
|
458
|
+
if (failure)
|
|
459
|
+
return failure;
|
|
460
|
+
}
|
|
461
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
520
462
|
}
|
|
521
463
|
if (settlement.status === 'unknown') {
|
|
522
464
|
return this.invokedFailure(active, 'candidate-ref-update-unknown', 'Candidate update outcome is unknown; retain and observe the exact commit effect.');
|
|
@@ -551,7 +493,7 @@ export class RunIssue {
|
|
|
551
493
|
|| pendingEffect.parentSha !== oldHead || pendingEffect.message !== message) {
|
|
552
494
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-commit-pendingEffect-diverged');
|
|
553
495
|
}
|
|
554
|
-
let
|
|
496
|
+
let authorizationValidation;
|
|
555
497
|
const settlement = await settleCommitEffect(pendingEffect, {
|
|
556
498
|
observe: async () => {
|
|
557
499
|
const observed = await this.dependencies.git.inspectHead(active.record.worktreePath);
|
|
@@ -566,20 +508,30 @@ export class RunIssue {
|
|
|
566
508
|
authorize: async () => {
|
|
567
509
|
if (!await this.authorized(active, config))
|
|
568
510
|
return false;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
return !authorizationFailure;
|
|
511
|
+
authorizationValidation = await coordinator.revalidate({ batch, issueNumber, epoch: 'pre-update', expectedHeadSha: oldHead });
|
|
512
|
+
return authorizationValidation.status === 'valid';
|
|
572
513
|
},
|
|
573
514
|
invoke: async () => {
|
|
574
515
|
await this.dependencies.git.commit({ worktreePath: active.record.worktreePath, message: pendingEffect.message });
|
|
575
516
|
},
|
|
576
517
|
});
|
|
577
518
|
if (settlement.status === 'unauthorized') {
|
|
578
|
-
|
|
579
|
-
|
|
519
|
+
if (authorizationValidation?.status === 'retryable') {
|
|
520
|
+
return this.invokedFailure(active, 'review-feedback-precommit-revalidation-failed-retryable');
|
|
521
|
+
}
|
|
522
|
+
active = await this.confirmEffect(active);
|
|
523
|
+
if (authorizationValidation) {
|
|
524
|
+
const failure = await this.mapFeedbackRevalidation(active, authorizationValidation, 'review-feedback-precommit-revalidation-failed');
|
|
525
|
+
if (failure)
|
|
526
|
+
return failure;
|
|
527
|
+
}
|
|
528
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
580
529
|
}
|
|
581
530
|
if (settlement.status === 'unknown')
|
|
582
531
|
return this.invokedFailure(active, 'review-feedback-commit-delivery-unknown');
|
|
532
|
+
if (settlement.status === 'diverged') {
|
|
533
|
+
return this.blockReviewFeedback(await this.confirmEffect(active), 'safety', 'review-feedback-commit-observation-diverged');
|
|
534
|
+
}
|
|
583
535
|
if (settlement.status !== 'confirmed') {
|
|
584
536
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-commit-observation-diverged');
|
|
585
537
|
}
|
|
@@ -608,7 +560,7 @@ export class RunIssue {
|
|
|
608
560
|
|| pendingEffect.treeSha !== commit.treeSha || pendingEffect.branch !== active.record.branchName || pendingEffect.priorRemoteSha !== oldHead) {
|
|
609
561
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-push-pendingEffect-diverged');
|
|
610
562
|
}
|
|
611
|
-
let
|
|
563
|
+
let authorizationValidation;
|
|
612
564
|
const settlement = await settlePushEffect(pendingEffect, {
|
|
613
565
|
observe: async () => {
|
|
614
566
|
remote = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
|
|
@@ -617,9 +569,8 @@ export class RunIssue {
|
|
|
617
569
|
authorize: async () => {
|
|
618
570
|
if (!await this.authorized(active, config))
|
|
619
571
|
return false;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return !authorizationFailure;
|
|
572
|
+
authorizationValidation = await coordinator.revalidate({ batch, issueNumber, epoch: 'pre-update', expectedHeadSha: oldHead });
|
|
573
|
+
return authorizationValidation.status === 'valid';
|
|
623
574
|
},
|
|
624
575
|
invoke: () => this.dependencies.git.push({
|
|
625
576
|
worktreePath: active.record.worktreePath,
|
|
@@ -627,16 +578,28 @@ export class RunIssue {
|
|
|
627
578
|
}),
|
|
628
579
|
});
|
|
629
580
|
if (settlement.status === 'unauthorized') {
|
|
630
|
-
|
|
631
|
-
|
|
581
|
+
if (authorizationValidation?.status === 'retryable') {
|
|
582
|
+
return this.invokedFailure(active, 'review-feedback-prepush-revalidation-failed-retryable');
|
|
583
|
+
}
|
|
584
|
+
active = await this.confirmEffect(active);
|
|
585
|
+
if (authorizationValidation) {
|
|
586
|
+
const failure = await this.mapFeedbackRevalidation(active, authorizationValidation, 'review-feedback-prepush-revalidation-failed');
|
|
587
|
+
if (failure)
|
|
588
|
+
return failure;
|
|
589
|
+
}
|
|
590
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
632
591
|
}
|
|
633
592
|
if (settlement.status === 'unknown')
|
|
634
593
|
return this.invokedFailure(active, 'review-feedback-push-delivery-unknown');
|
|
594
|
+
if (settlement.status === 'diverged') {
|
|
595
|
+
return this.blockReviewFeedback(await this.confirmEffect(active), 'safety', 'review-feedback-push-observation-diverged');
|
|
596
|
+
}
|
|
635
597
|
if (settlement.status !== 'confirmed') {
|
|
636
598
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-push-observation-diverged');
|
|
637
599
|
}
|
|
600
|
+
active = await this.confirmEffect(active);
|
|
638
601
|
}
|
|
639
|
-
const postPush = await coordinator.revalidate({ batch, epoch: 'post-push', expectedHeadSha: head });
|
|
602
|
+
const postPush = await coordinator.revalidate({ batch, issueNumber, epoch: 'post-push', expectedHeadSha: head });
|
|
640
603
|
const postPushFailure = await this.mapFeedbackRevalidation(active, postPush, 'review-feedback-postpush-revalidation-failed');
|
|
641
604
|
if (postPushFailure)
|
|
642
605
|
return postPushFailure;
|
|
@@ -668,10 +631,7 @@ export class RunIssue {
|
|
|
668
631
|
|| pendingEffect.marker !== marker || pendingEffect.bodySha256 !== sha256(body) || pendingEffect.epochHeadSha !== head) {
|
|
669
632
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-pendingEffect-diverged');
|
|
670
633
|
}
|
|
671
|
-
|
|
672
|
-
const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-summary-revalidation-failed');
|
|
673
|
-
if (validationFailure)
|
|
674
|
-
return validationFailure;
|
|
634
|
+
let authorizationValidation;
|
|
675
635
|
let matches = [];
|
|
676
636
|
const settlement = await settleCommentEffect(pendingEffect, {
|
|
677
637
|
observe: async () => {
|
|
@@ -681,16 +641,33 @@ export class RunIssue {
|
|
|
681
641
|
return 'absent';
|
|
682
642
|
return matches.length === 1 && matches[0].body === body ? 'confirmed' : 'diverged';
|
|
683
643
|
},
|
|
684
|
-
authorize: () =>
|
|
644
|
+
authorize: async () => {
|
|
645
|
+
if (!await this.authorized(active, config))
|
|
646
|
+
return false;
|
|
647
|
+
authorizationValidation = await coordinator.revalidate({ batch, issueNumber, epoch: 'post-push', expectedHeadSha: head });
|
|
648
|
+
return authorizationValidation.status === 'valid';
|
|
649
|
+
},
|
|
685
650
|
invoke: async () => {
|
|
686
651
|
await this.dependencies.pullRequests.postConversationComment(batch.pullRequest.number, body);
|
|
687
652
|
},
|
|
688
653
|
});
|
|
689
654
|
if (settlement.status === 'unauthorized') {
|
|
655
|
+
if (authorizationValidation?.status === 'retryable') {
|
|
656
|
+
return this.invokedFailure(active, 'review-feedback-summary-revalidation-failed-retryable');
|
|
657
|
+
}
|
|
658
|
+
active = await this.confirmEffect(active);
|
|
659
|
+
if (authorizationValidation) {
|
|
660
|
+
const failure = await this.mapFeedbackRevalidation(active, authorizationValidation, 'review-feedback-summary-revalidation-failed');
|
|
661
|
+
if (failure)
|
|
662
|
+
return failure;
|
|
663
|
+
}
|
|
690
664
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
691
665
|
}
|
|
692
666
|
if (settlement.status === 'unknown')
|
|
693
667
|
return this.invokedFailure(active, 'review-feedback-summary-delivery-unknown');
|
|
668
|
+
if (settlement.status === 'diverged') {
|
|
669
|
+
return this.blockReviewFeedback(await this.confirmEffect(active), 'safety', 'review-feedback-summary-observation-diverged');
|
|
670
|
+
}
|
|
694
671
|
if (settlement.status !== 'confirmed') {
|
|
695
672
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-observation-diverged');
|
|
696
673
|
}
|
|
@@ -723,23 +700,37 @@ export class RunIssue {
|
|
|
723
700
|
|| !sameStrings(pendingEffect.expected, finalLabels)) {
|
|
724
701
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-final-labels-pendingEffect-diverged');
|
|
725
702
|
}
|
|
726
|
-
|
|
727
|
-
const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-final-labels-revalidation-failed');
|
|
728
|
-
if (validationFailure)
|
|
729
|
-
return validationFailure;
|
|
703
|
+
let authorizationValidation;
|
|
730
704
|
const settlement = await settleLabelsEffect(pendingEffect, {
|
|
731
705
|
observe: async () => {
|
|
732
706
|
const issue = await this.readIssue(issueNumber);
|
|
733
|
-
return !issue ? 'diverged' : sameStrings(issue.labels, finalLabels) ? 'confirmed' : 'absent';
|
|
707
|
+
return !issue ? 'diverged' : sameStrings(managedLabelProjection(issue.labels, config), finalLabels) ? 'confirmed' : 'absent';
|
|
708
|
+
},
|
|
709
|
+
authorize: async () => {
|
|
710
|
+
if (!await this.authorized(active, config))
|
|
711
|
+
return false;
|
|
712
|
+
authorizationValidation = await coordinator.revalidate({ batch, issueNumber, epoch: 'post-push', expectedHeadSha: head });
|
|
713
|
+
return authorizationValidation.status === 'valid';
|
|
734
714
|
},
|
|
735
|
-
authorize: () => this.authorized(active, config),
|
|
736
715
|
invoke: () => this.dependencies.issues.setLabels(issueNumber, finalLabels),
|
|
737
716
|
});
|
|
738
717
|
if (settlement.status === 'unauthorized') {
|
|
718
|
+
if (authorizationValidation?.status === 'retryable') {
|
|
719
|
+
return this.invokedFailure(active, 'review-feedback-final-labels-revalidation-failed-retryable');
|
|
720
|
+
}
|
|
721
|
+
active = await this.confirmEffect(active);
|
|
722
|
+
if (authorizationValidation) {
|
|
723
|
+
const failure = await this.mapFeedbackRevalidation(active, authorizationValidation, 'review-feedback-final-labels-revalidation-failed');
|
|
724
|
+
if (failure)
|
|
725
|
+
return failure;
|
|
726
|
+
}
|
|
739
727
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
|
|
740
728
|
}
|
|
741
729
|
if (settlement.status === 'unknown')
|
|
742
730
|
return this.invokedFailure(active, 'review-feedback-final-labels-delivery-unknown');
|
|
731
|
+
if (settlement.status === 'diverged') {
|
|
732
|
+
return this.blockReviewFeedback(await this.confirmEffect(active), 'safety', 'review-feedback-final-labels-diverged');
|
|
733
|
+
}
|
|
743
734
|
if (settlement.status !== 'confirmed') {
|
|
744
735
|
return this.blockReviewFeedback(active, 'safety', 'review-feedback-final-labels-diverged');
|
|
745
736
|
}
|
|
@@ -755,10 +746,17 @@ export class RunIssue {
|
|
|
755
746
|
summaryCommentId: summaryId,
|
|
756
747
|
publishedAt: this.timestamp(),
|
|
757
748
|
};
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
749
|
+
const pullRequest = await this.dependencies.pullRequests.findOpen({
|
|
750
|
+
headBranch: active.record.branchName,
|
|
751
|
+
baseBranch: config.github.baseBranch,
|
|
752
|
+
});
|
|
753
|
+
if (!pullRequest || pullRequest.number !== batch.pullRequest.number || pullRequest.nodeId !== batch.pullRequest.nodeId
|
|
754
|
+
|| pullRequest.headSha !== head) {
|
|
755
|
+
return this.blockReviewFeedback(active, 'safety', 'review-feedback-published-pr-identity-diverged');
|
|
756
|
+
}
|
|
757
|
+
return this.terminal(active, {
|
|
758
|
+
status: 'review-ready', pullRequestUrl: pullRequest.url, continuationEpoch: head,
|
|
759
|
+
}, 'review-ready', {
|
|
762
760
|
reviewFeedback: publishReviewFeedback(active.record.reviewFeedback, receipt),
|
|
763
761
|
});
|
|
764
762
|
}
|
|
@@ -776,6 +774,9 @@ export class RunIssue {
|
|
|
776
774
|
if (!pullRequest?.number || !pullRequest.nodeId || !pullRequest.headSha) {
|
|
777
775
|
return { result: await this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'review-feedback-pr-identity-missing') };
|
|
778
776
|
}
|
|
777
|
+
if (pullRequest.url !== terminal.pullRequestUrl) {
|
|
778
|
+
return { result: await this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'review-feedback-prior-handoff-identity-mismatch') };
|
|
779
|
+
}
|
|
779
780
|
const expectedHeadSha = feedback.previousPublishedHeadSha === null
|
|
780
781
|
? pullRequest.headSha
|
|
781
782
|
: feedback.previousPublishedHeadSha;
|
|
@@ -797,29 +798,40 @@ export class RunIssue {
|
|
|
797
798
|
headSha: pullRequest.headSha,
|
|
798
799
|
body: pullRequest.body,
|
|
799
800
|
},
|
|
801
|
+
...(starting.record.terminalNotifications ? {
|
|
802
|
+
issueCommentCutoff: {
|
|
803
|
+
issueNumber: starting.record.issueNumber,
|
|
804
|
+
...starting.record.terminalNotifications.commentCutoff,
|
|
805
|
+
},
|
|
806
|
+
} : {}),
|
|
800
807
|
});
|
|
801
808
|
if (observed.status === 'retryable')
|
|
802
809
|
return { result: await this.invokedFailure(starting, 'review-feedback-observation-retryable') };
|
|
803
810
|
if (observed.status === 'blocked') {
|
|
804
811
|
return { result: await this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'review-feedback-observation-blocked') };
|
|
805
812
|
}
|
|
813
|
+
let ready = starting;
|
|
806
814
|
if (feedback.previousPublishedHeadSha === null) {
|
|
807
|
-
const
|
|
815
|
+
const issueCommentBatch = observed.status === 'frozen'
|
|
816
|
+
&& observed.batch.sources.every((source) => source.kind === 'issue-comment');
|
|
817
|
+
const sourceIds = observed.status === 'frozen' && !issueCommentBatch
|
|
808
818
|
? observed.batch.sources.map((source) => source.sourceId)
|
|
809
|
-
: observed.eligibleSourceIds;
|
|
810
|
-
|
|
819
|
+
: observed.status === 'none' ? observed.eligibleSourceIds : [];
|
|
820
|
+
ready = await this.persist(starting, {
|
|
811
821
|
reviewFeedback: initializeReviewFeedback(feedback, expectedHeadSha, sourceIds),
|
|
812
822
|
});
|
|
813
|
-
|
|
823
|
+
if (!issueCommentBatch)
|
|
824
|
+
return { result: publicOutcome(ready.record.terminalOutcome) };
|
|
814
825
|
}
|
|
815
|
-
|
|
826
|
+
const readyFeedback = ready.record.reviewFeedback;
|
|
827
|
+
if (readyFeedback.activeBatch || observed.status === 'none')
|
|
816
828
|
return { result: publicOutcome(terminal) };
|
|
817
829
|
if (!await this.authorizedForExactLabels(starting, [config.github.labels.review.name])) {
|
|
818
830
|
return { result: publicOutcome(terminal) };
|
|
819
831
|
}
|
|
820
|
-
const projected = projectReviewFeedbackBatch(observed.batch,
|
|
821
|
-
const candidateTreeSha = await this.dependencies.git.getTreeSha(
|
|
822
|
-
const activation = projectValidationFeedbackActivation(
|
|
832
|
+
const projected = projectReviewFeedbackBatch(observed.batch, ready.record.directReview.targetRevision);
|
|
833
|
+
const candidateTreeSha = await this.dependencies.git.getTreeSha(ready.record.worktreePath);
|
|
834
|
+
const activation = projectValidationFeedbackActivation(ready.record, {
|
|
823
835
|
batch: observed.batch,
|
|
824
836
|
repairFindings: projected.repairFindings,
|
|
825
837
|
candidateTreeSha,
|
|
@@ -830,7 +842,16 @@ export class RunIssue {
|
|
|
830
842
|
expected: sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]),
|
|
831
843
|
},
|
|
832
844
|
});
|
|
833
|
-
|
|
845
|
+
if (observed.batch.sources.every((source) => source.kind === 'issue-comment')) {
|
|
846
|
+
activation.changes.cycle = ready.record.cycle + 1;
|
|
847
|
+
activation.changes.checks = structuredClone(ready.record.checks);
|
|
848
|
+
activation.changes.checkedChangeSha256 = ready.record.checkedChangeSha256;
|
|
849
|
+
activation.changes.proofId = ready.record.proofId;
|
|
850
|
+
activation.changes.proofReceipt = structuredClone(ready.record.proofReceipt);
|
|
851
|
+
activation.changes.implementationResult = structuredClone(ready.record.implementationResult);
|
|
852
|
+
activation.changes.terminalNotifications = undefined;
|
|
853
|
+
}
|
|
854
|
+
const active = await this.persistValidationTransition(ready, activation);
|
|
834
855
|
return this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
835
856
|
}
|
|
836
857
|
async resumeActivatedReviewFeedback(starting, targetRoot, config) {
|
|
@@ -844,41 +865,45 @@ export class RunIssue {
|
|
|
844
865
|
const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
|
|
845
866
|
const reviewLabels = [config.github.labels.review.name];
|
|
846
867
|
const pendingEffect = active.record.pendingEffect;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
||
|
|
850
|
-
||
|
|
868
|
+
const activationEffect = pendingEffect?.kind === 'review-activation-labels' ? pendingEffect : undefined;
|
|
869
|
+
if (pendingEffect && pendingEffect.kind !== 'continuation-worktree-create' && (!activationEffect
|
|
870
|
+
|| activationEffect.issueNumber !== active.record.issueNumber
|
|
871
|
+
|| activationEffect.batchId !== batch.batchId
|
|
872
|
+
|| !sameStrings(activationEffect.expected, runningLabels))) {
|
|
851
873
|
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-pendingEffect-diverged') };
|
|
852
874
|
}
|
|
853
875
|
const issue = await this.readIssue(active.record.issueNumber);
|
|
854
|
-
const labelsAreRunning = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, runningLabels)
|
|
876
|
+
const labelsAreRunning = !!issue && issue.state === 'OPEN' && sameStrings(managedLabelProjection(issue.labels, config), runningLabels)
|
|
855
877
|
&& this.hasTrustedClaim(issue, active.record);
|
|
856
|
-
const labelsAreReview = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, reviewLabels)
|
|
878
|
+
const labelsAreReview = !!issue && issue.state === 'OPEN' && sameStrings(managedLabelProjection(issue.labels, config), reviewLabels)
|
|
857
879
|
&& this.hasTrustedClaim(issue, active.record);
|
|
858
880
|
if (!labelsAreRunning && !labelsAreReview) {
|
|
859
881
|
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-authority-revoked') };
|
|
860
882
|
}
|
|
861
883
|
const validation = await this.dependencies.reviewFeedback.revalidate({
|
|
862
|
-
batch, epoch: 'pre-update', expectedHeadSha,
|
|
884
|
+
batch, issueNumber: active.record.issueNumber, epoch: 'pre-update', expectedHeadSha,
|
|
863
885
|
});
|
|
864
886
|
const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-activation-revalidation-failed');
|
|
865
887
|
if (validationFailure)
|
|
866
888
|
return { result: validationFailure };
|
|
867
|
-
if (
|
|
868
|
-
const settlement = await settleLabelsEffect(
|
|
889
|
+
if (activationEffect) {
|
|
890
|
+
const settlement = await settleLabelsEffect(activationEffect, {
|
|
869
891
|
observe: async () => {
|
|
870
892
|
const observed = await this.readIssue(active.record.issueNumber);
|
|
871
893
|
if (!observed || !this.hasTrustedClaim(observed, active.record))
|
|
872
894
|
return 'diverged';
|
|
873
|
-
if (sameStrings(observed.labels, runningLabels))
|
|
895
|
+
if (sameStrings(managedLabelProjection(observed.labels, config), runningLabels))
|
|
874
896
|
return 'confirmed';
|
|
875
|
-
return sameStrings(observed.labels, reviewLabels) ? 'absent' : 'diverged';
|
|
897
|
+
return sameStrings(managedLabelProjection(observed.labels, config), reviewLabels) ? 'absent' : 'diverged';
|
|
876
898
|
},
|
|
877
899
|
invoke: () => this.dependencies.issues.setLabels(active.record.issueNumber, runningLabels),
|
|
878
900
|
});
|
|
879
901
|
if (settlement.status === 'unknown') {
|
|
880
902
|
return { result: await this.invokedFailure(active, 'review-feedback-activation-labels-delivery-unknown') };
|
|
881
903
|
}
|
|
904
|
+
if (settlement.status === 'diverged') {
|
|
905
|
+
return { result: await this.blockReviewFeedback(await this.confirmEffect(active), 'safety', 'review-feedback-activation-labels-diverged') };
|
|
906
|
+
}
|
|
882
907
|
if (settlement.status !== 'confirmed') {
|
|
883
908
|
return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-labels-diverged') };
|
|
884
909
|
}
|
|
@@ -998,32 +1023,17 @@ export class RunIssue {
|
|
|
998
1023
|
if (active.record.pendingEffect?.kind === 'outcome-evidence') {
|
|
999
1024
|
if (active.record.terminalOutcome) {
|
|
1000
1025
|
const observed = await this.observeOutcomeEvidenceEffect(active, active.record.pendingEffect.code, active.record.pendingEffect.summary);
|
|
1001
|
-
await this.confirmEffect(observed.active);
|
|
1026
|
+
active = await this.confirmEffect(observed.active);
|
|
1027
|
+
active = await this.settleTerminalNotificationsBestEffort(active);
|
|
1002
1028
|
return publicOutcome(active.record.terminalOutcome);
|
|
1003
1029
|
}
|
|
1004
1030
|
const seed = parseTerminalSeedSummary(active.record.pendingEffect.summary);
|
|
1005
|
-
return await this.
|
|
1006
|
-
}
|
|
1007
|
-
if (active.record.pendingEffect?.kind === 'blocked-comment') {
|
|
1008
|
-
return await this.publishBlockedTerminal(active, {
|
|
1009
|
-
status: 'blocked',
|
|
1010
|
-
kind: active.record.pendingEffect.blockKind,
|
|
1011
|
-
resumable: active.record.pendingEffect.resumable,
|
|
1012
|
-
...(active.record.pendingEffect.blocker ? { blocker: active.record.pendingEffect.blocker } : {}),
|
|
1013
|
-
}, active.record.pendingEffect.evidenceCode);
|
|
1031
|
+
return await this.terminal(active, seed, active.record.pendingEffect.code);
|
|
1014
1032
|
}
|
|
1015
|
-
if (active.record.
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
resumable: active.record.pendingEffect.resumable,
|
|
1020
|
-
...(active.record.pendingEffect.blocker ? { blocker: active.record.pendingEffect.blocker } : {}),
|
|
1021
|
-
}, active.record.pendingEffect.evidenceCode);
|
|
1022
|
-
}
|
|
1023
|
-
if (existing.terminalOutcome?.status === 'blocked'
|
|
1024
|
-
&& issue?.state === 'OPEN'
|
|
1025
|
-
&& blockedLabelProjection(issue.labels, config).status !== 'settled') {
|
|
1026
|
-
return await this.reconcilePersistedBlockedTerminal(active, issue, existing.terminalOutcome);
|
|
1033
|
+
if (active.record.terminalOutcome && (active.record.terminalNotifications
|
|
1034
|
+
|| active.record.pendingEffect?.kind === 'terminal-comment'
|
|
1035
|
+
|| active.record.pendingEffect?.kind === 'terminal-labels')) {
|
|
1036
|
+
active = await this.settleTerminalNotificationsBestEffort(active);
|
|
1027
1037
|
}
|
|
1028
1038
|
if (active.record.terminalOutcome?.status === 'transport-failed'
|
|
1029
1039
|
&& !active.record.terminalOutcome.resumable
|
|
@@ -1078,7 +1088,9 @@ export class RunIssue {
|
|
|
1078
1088
|
return reconciled;
|
|
1079
1089
|
active = reconciled.active;
|
|
1080
1090
|
}
|
|
1081
|
-
if (active.record.reviewFeedback?.activeBatch
|
|
1091
|
+
if (active.record.reviewFeedback?.activeBatch
|
|
1092
|
+
&& (active.record.pendingEffect?.kind === 'review-activation-labels'
|
|
1093
|
+
|| active.record.pendingEffect?.kind === 'continuation-worktree-create')) {
|
|
1082
1094
|
const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
1083
1095
|
if ('result' in activation)
|
|
1084
1096
|
return activation.result;
|
|
@@ -1230,6 +1242,12 @@ export class RunIssue {
|
|
|
1230
1242
|
...(feedbackBatch ? {
|
|
1231
1243
|
reviewFeedbackRound: active.record.reviewFeedback.repairRound,
|
|
1232
1244
|
reviewFeedback: feedbackProjection.workerFeedback,
|
|
1245
|
+
reviewFeedbackPullRequest: {
|
|
1246
|
+
number: feedbackBatch.pullRequest.number,
|
|
1247
|
+
headSha: feedbackBatch.pullRequest.headSha,
|
|
1248
|
+
headRefName: feedbackBatch.pullRequest.headRefName,
|
|
1249
|
+
url: `https://github.com/${active.record.canonicalRepository}/pull/${feedbackBatch.pullRequest.number}`,
|
|
1250
|
+
},
|
|
1233
1251
|
} : {}),
|
|
1234
1252
|
onPrepared: async (prepared) => {
|
|
1235
1253
|
const currentActive = active;
|
|
@@ -1333,6 +1351,9 @@ export class RunIssue {
|
|
|
1333
1351
|
}
|
|
1334
1352
|
return await this.terminal(active, implementationBlockerOutcome(report.blocker));
|
|
1335
1353
|
}
|
|
1354
|
+
if (report.status === 'answer-only' || report.status === 'boundary') {
|
|
1355
|
+
return this.completeIssueFeedbackResponse(active, report, config);
|
|
1356
|
+
}
|
|
1336
1357
|
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
1337
1358
|
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'implementation-head-drift');
|
|
1338
1359
|
}
|
|
@@ -1354,6 +1375,7 @@ export class RunIssue {
|
|
|
1354
1375
|
active = await this.adoptAttempt(active, sha256(canonicalJson(report)), {
|
|
1355
1376
|
transportRetries: active.record.transportRetries,
|
|
1356
1377
|
reportRepairs: 0,
|
|
1378
|
+
implementationResult: { summary: report.summary, residualRisks: report.residualRisks },
|
|
1357
1379
|
});
|
|
1358
1380
|
active = await this.clearAttempt(active);
|
|
1359
1381
|
}
|
|
@@ -1886,7 +1908,10 @@ export class RunIssue {
|
|
|
1886
1908
|
const record = { ...active.record, ...normalized, updatedAt: this.timestamp() };
|
|
1887
1909
|
if (Object.hasOwn(changes, 'pendingEffect') && changes.pendingEffect === undefined)
|
|
1888
1910
|
delete record.pendingEffect;
|
|
1889
|
-
|
|
1911
|
+
if (Object.hasOwn(changes, 'terminalOutcome') && changes.terminalOutcome === undefined
|
|
1912
|
+
&& !Object.hasOwn(changes, 'terminalNotifications'))
|
|
1913
|
+
delete record.terminalNotifications;
|
|
1914
|
+
for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'reviewFeedback', 'changeBindingVersion', 'candidateBinding', 'candidateMaterialization', 'activeAttempt', 'terminalNotifications']) {
|
|
1890
1915
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
1891
1916
|
delete record[key];
|
|
1892
1917
|
}
|
|
@@ -1980,7 +2005,7 @@ export class RunIssue {
|
|
|
1980
2005
|
}
|
|
1981
2006
|
async authorizedForExactLabels(active, expectedLabels) {
|
|
1982
2007
|
const issue = await this.readIssue(active.record.issueNumber);
|
|
1983
|
-
return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
|
|
2008
|
+
return !!issue && issue.state === 'OPEN' && sameStrings(managedLabelProjection(issue.labels, active.config), expectedLabels)
|
|
1984
2009
|
&& this.hasTrustedClaim(issue, active.record);
|
|
1985
2010
|
}
|
|
1986
2011
|
hasTrustedClaim(issue, record) {
|
|
@@ -2036,6 +2061,7 @@ export class RunIssue {
|
|
|
2036
2061
|
}
|
|
2037
2062
|
const validation = await this.dependencies.reviewFeedback.revalidate({
|
|
2038
2063
|
batch,
|
|
2064
|
+
issueNumber,
|
|
2039
2065
|
epoch: 'pre-update',
|
|
2040
2066
|
expectedHeadSha: batch.priorPublishedHeadSha,
|
|
2041
2067
|
});
|
|
@@ -2863,12 +2889,7 @@ export class RunIssue {
|
|
|
2863
2889
|
return transitioned;
|
|
2864
2890
|
}
|
|
2865
2891
|
async blockReviewFeedback(active, kind, evidenceCode, outcome = { status: 'blocked', kind, resumable: false }) {
|
|
2866
|
-
|
|
2867
|
-
if (!feedback?.activeBatch)
|
|
2868
|
-
return this.terminal(active, { status: 'blocked', kind, resumable: false }, evidenceCode);
|
|
2869
|
-
return this.publishBlockedTerminal(active, outcome, evidenceCode, {
|
|
2870
|
-
reviewFeedback: blockReviewFeedback(feedback, kind, this.timestamp()),
|
|
2871
|
-
}, true);
|
|
2892
|
+
return this.terminal(active, outcome, evidenceCode);
|
|
2872
2893
|
}
|
|
2873
2894
|
async mapImplementationFailure(active, result) {
|
|
2874
2895
|
if (result.kind === 'transport-failed')
|
|
@@ -2915,8 +2936,8 @@ export class RunIssue {
|
|
|
2915
2936
|
});
|
|
2916
2937
|
return { status: 'repair-ready', source, blockerIds: [...new Set(blockerIds)].sort(), evidencePath: evidence.path };
|
|
2917
2938
|
}
|
|
2918
|
-
async observeOutcomeEvidenceEffect(active, code, summary
|
|
2919
|
-
if (!active.record.pendingEffect
|
|
2939
|
+
async observeOutcomeEvidenceEffect(active, code, summary) {
|
|
2940
|
+
if (!active.record.pendingEffect) {
|
|
2920
2941
|
const recordedAt = this.timestamp();
|
|
2921
2942
|
const path = this.dependencies.outcomeEvidencePath(active.record.runId, code, sha256(summary));
|
|
2922
2943
|
const bytes = outcomeEvidenceBytes({ runId: active.record.runId, code, summary, recordedAt });
|
|
@@ -2947,159 +2968,311 @@ export class RunIssue {
|
|
|
2947
2968
|
throw new PostEffectStateError(active);
|
|
2948
2969
|
return { active, path: effect.path, id: `evidence:${effect.runId}:${effect.code}` };
|
|
2949
2970
|
}
|
|
2950
|
-
async
|
|
2951
|
-
|
|
2952
|
-
|
|
2971
|
+
async completeIssueFeedbackResponse(starting, report, config) {
|
|
2972
|
+
let active = starting;
|
|
2973
|
+
const feedback = active.record.reviewFeedback;
|
|
2974
|
+
const batch = feedback?.activeBatch;
|
|
2975
|
+
if (!feedback || !batch || batch.sources.some((source) => source.kind !== 'issue-comment')) {
|
|
2976
|
+
return this.terminal(active, { status: 'internal-error', code: 'issue-feedback-response-source-invalid' });
|
|
2953
2977
|
}
|
|
2954
|
-
if (
|
|
2955
|
-
|
|
2978
|
+
if (await this.dependencies.git.getHead(active.record.worktreePath) !== batch.priorPublishedHeadSha
|
|
2979
|
+
|| (await this.dependencies.git.listChangedFilesIgnoringUntrackedRoot(active.record.worktreePath, config.proof.artifactDir)).length !== 0) {
|
|
2980
|
+
return this.terminal(active, { status: 'internal-error', code: 'issue-feedback-response-mutated-worktree' });
|
|
2956
2981
|
}
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
const body =
|
|
2962
|
-
const marker = body.split('\n')[0];
|
|
2963
|
-
const commentIntent = {
|
|
2964
|
-
kind: 'blocked-comment',
|
|
2965
|
-
issueNumber: active.record.issueNumber,
|
|
2982
|
+
const marker = `<!-- codex-orchestrator:run:${active.record.runId}:issue-feedback:${batch.batchId} -->`;
|
|
2983
|
+
const response = publicIssueFeedbackText(report.response, report.status === 'boundary'
|
|
2984
|
+
? 'This request cannot be acted on without a new issue-local decision or authority.'
|
|
2985
|
+
: 'The requested answer could not be published safely.');
|
|
2986
|
+
const body = [
|
|
2966
2987
|
marker,
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2988
|
+
report.status === 'boundary' ? 'codex-orchestrator reached an authority boundary.' : 'codex-orchestrator answered this follow-up.',
|
|
2989
|
+
'',
|
|
2990
|
+
response,
|
|
2991
|
+
...(report.status === 'boundary' ? ['', `Boundary: ${report.boundary.kind}`] : []),
|
|
2992
|
+
].join('\n');
|
|
2993
|
+
const directReview = active.record.directReview;
|
|
2994
|
+
const sourceIds = batch.sources.map((source) => source.sourceId);
|
|
2995
|
+
const restoredDirectReview = {
|
|
2996
|
+
...structuredClone(directReview),
|
|
2997
|
+
status: 'clear',
|
|
2998
|
+
stage: 'review',
|
|
2999
|
+
review: { ...structuredClone(directReview.review), disposition: 'clear' },
|
|
3000
|
+
repairFindings: directReview.repairFindings.filter((finding) => !sourceIds.includes(finding.sourceId)),
|
|
2972
3001
|
};
|
|
2973
|
-
const
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
if (!
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
expected,
|
|
3009
|
-
blockKind: outcome.kind,
|
|
3010
|
-
resumable: outcome.resumable,
|
|
3011
|
-
evidenceCode,
|
|
3012
|
-
...(outcome.blocker ? { blocker: outcome.blocker } : {}),
|
|
3013
|
-
} });
|
|
3014
|
-
}
|
|
3015
|
-
catch {
|
|
3016
|
-
throw new PostEffectStateError(active);
|
|
3017
|
-
}
|
|
3018
|
-
existing = active.record.pendingEffect;
|
|
3002
|
+
const pullRequest = await this.dependencies.pullRequests.findOpen({
|
|
3003
|
+
headBranch: active.record.branchName,
|
|
3004
|
+
baseBranch: config.github.baseBranch,
|
|
3005
|
+
});
|
|
3006
|
+
if (!pullRequest?.url || pullRequest.number !== batch.pullRequest.number
|
|
3007
|
+
|| pullRequest.nodeId !== batch.pullRequest.nodeId || pullRequest.headSha !== batch.pullRequest.headSha
|
|
3008
|
+
|| pullRequest.headRefName !== batch.pullRequest.headRefName
|
|
3009
|
+
|| pullRequest.baseRefName !== batch.pullRequest.baseRefName) {
|
|
3010
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'issue-feedback-prior-handoff-identity-mismatch', { reviewFeedback: blockReviewFeedback({
|
|
3011
|
+
...feedback,
|
|
3012
|
+
consumedSourceIds: feedback.consumedSourceIds.filter((id) => !sourceIds.includes(id)),
|
|
3013
|
+
}, 'safety', this.timestamp()) });
|
|
3014
|
+
}
|
|
3015
|
+
const terminalSeed = {
|
|
3016
|
+
status: 'review-ready',
|
|
3017
|
+
pullRequestUrl: pullRequest.url,
|
|
3018
|
+
continuationEpoch: batch.priorPublishedHeadSha,
|
|
3019
|
+
};
|
|
3020
|
+
const terminalOutcome = {
|
|
3021
|
+
...terminalSeed,
|
|
3022
|
+
evidencePath: this.dependencies.outcomeEvidencePath(active.record.runId, 'review-ready', sha256(canonicalJson(terminalSeed))),
|
|
3023
|
+
};
|
|
3024
|
+
const respondedAt = this.timestamp();
|
|
3025
|
+
const initialReceipt = {
|
|
3026
|
+
kind: 'responded',
|
|
3027
|
+
batchId: batch.batchId,
|
|
3028
|
+
sourceIds,
|
|
3029
|
+
responseKind: report.status,
|
|
3030
|
+
publication: 'failed',
|
|
3031
|
+
diagnostic: 'issue-feedback-response-publication-not-settled',
|
|
3032
|
+
respondedAt,
|
|
3033
|
+
};
|
|
3034
|
+
let comments = active.record.issueSnapshot.comments ?? [];
|
|
3035
|
+
try {
|
|
3036
|
+
comments = (await this.readIssue(active.record.issueNumber))?.comments ?? comments;
|
|
3019
3037
|
}
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3038
|
+
catch { /* bounded fallback */ }
|
|
3039
|
+
const freshNotifications = terminalNotificationState(active.record, terminalSeed, this.timestamp(), comments);
|
|
3040
|
+
const consumedChanges = {
|
|
3041
|
+
lifecycle: 'review-ready',
|
|
3042
|
+
terminalOutcome,
|
|
3043
|
+
outcomeEvidenceId: `evidence:${active.record.runId}:review-ready`,
|
|
3044
|
+
reviewFeedback: respondReviewFeedback(feedback, initialReceipt),
|
|
3045
|
+
directReview: restoredDirectReview,
|
|
3046
|
+
reworkFindings: [],
|
|
3047
|
+
reportRepairs: 0,
|
|
3048
|
+
transportRetries: 0,
|
|
3049
|
+
terminalNotifications: {
|
|
3050
|
+
...freshNotifications,
|
|
3051
|
+
comment: { status: 'delivered', attempts: 0 },
|
|
3052
|
+
labels: { status: 'delivered', attempts: 0 },
|
|
3053
|
+
},
|
|
3054
|
+
};
|
|
3055
|
+
active = isAdoptableAttempt(active.record.activeAttempt)
|
|
3056
|
+
? await this.adoptAttempt(active, sha256(canonicalJson(report)), consumedChanges)
|
|
3057
|
+
: await this.persist(active, consumedChanges);
|
|
3058
|
+
let publication = 'failed';
|
|
3059
|
+
let commentId;
|
|
3060
|
+
let diagnostic;
|
|
3061
|
+
const validation = this.dependencies.reviewFeedback
|
|
3062
|
+
? await this.dependencies.reviewFeedback.revalidate({
|
|
3063
|
+
batch, issueNumber: active.record.issueNumber, epoch: 'pre-update', expectedHeadSha: batch.priorPublishedHeadSha,
|
|
3064
|
+
})
|
|
3065
|
+
: { status: 'blocked', reason: 'review feedback revalidation unavailable' };
|
|
3066
|
+
if (validation.status !== 'valid') {
|
|
3067
|
+
publication = 'suppressed';
|
|
3068
|
+
diagnostic = validation.status === 'retryable'
|
|
3069
|
+
? 'issue-feedback-response-revalidation-retryable'
|
|
3070
|
+
: 'issue-feedback-response-revalidation-blocked';
|
|
3026
3071
|
}
|
|
3027
3072
|
else {
|
|
3028
|
-
expected = existing.expected;
|
|
3029
|
-
if (!sameStrings(expected, autoBlocked) && !sameStrings(expected, blockedOnly) && expected.length !== 0) {
|
|
3030
|
-
return this.invokedFailure(active, 'blocked-labels-pendingEffect-diverged');
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
|
-
let issue = await this.readIssue(active.record.issueNumber);
|
|
3034
|
-
if (!issue)
|
|
3035
|
-
return this.invokedFailure(active, 'blocked-labels-issue-missing');
|
|
3036
|
-
let projection = blockedLabelProjection(issue.labels, active.config);
|
|
3037
|
-
if (issue.state === 'OPEN' && projection.status === 'transition') {
|
|
3038
|
-
if (!this.dependencies.issues.transitionToBlocked) {
|
|
3039
|
-
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
3040
|
-
}
|
|
3041
3073
|
try {
|
|
3042
|
-
await this.
|
|
3074
|
+
let issue = await this.readIssue(active.record.issueNumber);
|
|
3075
|
+
let matches = issue ? commentsWithMarker(issue, marker) : [];
|
|
3076
|
+
if (matches.length === 0) {
|
|
3077
|
+
await this.dependencies.issues.postComment(active.record.issueNumber, body);
|
|
3078
|
+
issue = await this.readIssue(active.record.issueNumber);
|
|
3079
|
+
matches = issue ? commentsWithMarker(issue, marker) : [];
|
|
3080
|
+
}
|
|
3081
|
+
const match = matches.length === 1 && sha256(matches[0].body) === sha256(body) ? matches[0] : undefined;
|
|
3082
|
+
if (match) {
|
|
3083
|
+
publication = 'delivered';
|
|
3084
|
+
commentId = match.id ?? `marker:${batch.batchId}`;
|
|
3085
|
+
}
|
|
3086
|
+
else {
|
|
3087
|
+
diagnostic = 'issue-feedback-response-observation-diverged';
|
|
3088
|
+
}
|
|
3043
3089
|
}
|
|
3044
3090
|
catch {
|
|
3045
|
-
|
|
3091
|
+
diagnostic = 'issue-feedback-response-delivery-failed';
|
|
3046
3092
|
}
|
|
3047
|
-
issue = await this.readIssue(active.record.issueNumber);
|
|
3048
|
-
projection = issue ? blockedLabelProjection(issue.labels, active.config) : { status: 'diverged' };
|
|
3049
3093
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3094
|
+
try {
|
|
3095
|
+
await this.dependencies.issues.setLabels(active.record.issueNumber, [config.github.labels.review.name]);
|
|
3096
|
+
}
|
|
3097
|
+
catch {
|
|
3098
|
+
diagnostic = diagnostic ?? 'issue-feedback-review-label-reconciliation-failed';
|
|
3099
|
+
}
|
|
3100
|
+
const receipt = {
|
|
3101
|
+
kind: 'responded',
|
|
3102
|
+
batchId: batch.batchId,
|
|
3103
|
+
sourceIds,
|
|
3104
|
+
responseKind: report.status,
|
|
3105
|
+
publication,
|
|
3106
|
+
...(commentId ? { commentId } : {}),
|
|
3107
|
+
...(diagnostic ? { diagnostic } : {}),
|
|
3108
|
+
respondedAt,
|
|
3109
|
+
};
|
|
3110
|
+
try {
|
|
3111
|
+
active = await this.persist(active, {
|
|
3112
|
+
reviewFeedback: settleReviewFeedbackResponse(active.record.reviewFeedback, receipt),
|
|
3113
|
+
});
|
|
3114
|
+
}
|
|
3115
|
+
catch {
|
|
3116
|
+
return publicOutcome(terminalOutcome);
|
|
3069
3117
|
}
|
|
3070
3118
|
try {
|
|
3071
|
-
|
|
3119
|
+
active = await this.clearAttempt(active);
|
|
3072
3120
|
}
|
|
3073
3121
|
catch {
|
|
3074
|
-
|
|
3122
|
+
return publicOutcome(terminalOutcome);
|
|
3075
3123
|
}
|
|
3124
|
+
return publicOutcome(active.record.terminalOutcome);
|
|
3076
3125
|
}
|
|
3077
|
-
async
|
|
3078
|
-
if (
|
|
3079
|
-
return this.invokedFailure(active, '
|
|
3126
|
+
async terminal(active, outcome, evidenceCode = outcome.status, additionalChanges = {}) {
|
|
3127
|
+
if (active.record.pendingEffect && active.record.pendingEffect.kind !== 'outcome-evidence') {
|
|
3128
|
+
return this.invokedFailure(active, 'terminal-pending-effect-unsettled', 'Terminal state requires the existing effect postcondition to settle first.');
|
|
3129
|
+
}
|
|
3130
|
+
const feedback = active.record.reviewFeedback;
|
|
3131
|
+
const terminalChanges = feedback?.activeBatch && !additionalChanges.reviewFeedback ? {
|
|
3132
|
+
...additionalChanges,
|
|
3133
|
+
reviewFeedback: blockReviewFeedback(feedback, outcome.status === 'blocked' ? outcome.kind : 'safety', this.timestamp()),
|
|
3134
|
+
} : additionalChanges;
|
|
3135
|
+
return this.persistTerminal(active, outcome, evidenceCode, terminalChanges);
|
|
3136
|
+
}
|
|
3137
|
+
async settleTerminalNotificationsBestEffort(starting) {
|
|
3138
|
+
let active = starting;
|
|
3139
|
+
if (!active.record.terminalOutcome || !active.record.terminalNotifications)
|
|
3140
|
+
return active;
|
|
3141
|
+
if (active.record.pendingEffect && active.record.pendingEffect.kind !== 'terminal-comment'
|
|
3142
|
+
&& active.record.pendingEffect.kind !== 'terminal-labels')
|
|
3143
|
+
return active;
|
|
3144
|
+
try {
|
|
3145
|
+
active = await this.settleTerminalComment(active);
|
|
3146
|
+
}
|
|
3147
|
+
catch {
|
|
3148
|
+
return active;
|
|
3080
3149
|
}
|
|
3081
3150
|
try {
|
|
3082
|
-
await this.
|
|
3151
|
+
active = await this.settleTerminalLabels(active);
|
|
3083
3152
|
}
|
|
3084
3153
|
catch {
|
|
3085
|
-
return
|
|
3154
|
+
return active;
|
|
3155
|
+
}
|
|
3156
|
+
return active;
|
|
3157
|
+
}
|
|
3158
|
+
async settleTerminalComment(starting) {
|
|
3159
|
+
let active = starting;
|
|
3160
|
+
const notifications = active.record.terminalNotifications;
|
|
3161
|
+
if (notifications.comment.status !== 'pending' || notifications.comment.attempts >= TERMINAL_NOTIFICATION_ATTEMPTS)
|
|
3162
|
+
return active;
|
|
3163
|
+
const body = terminalComment(active.record.runId, active.record.cycle, notifications.report);
|
|
3164
|
+
const marker = body.split('\n')[0];
|
|
3165
|
+
const attempt = notifications.comment.attempts + 1;
|
|
3166
|
+
if (!active.record.pendingEffect) {
|
|
3167
|
+
active = await this.persist(active, { pendingEffect: {
|
|
3168
|
+
kind: 'terminal-comment', issueNumber: active.record.issueNumber, marker, bodySha256: sha256(body),
|
|
3169
|
+
outcome: notifications.report.outcome, attempt,
|
|
3170
|
+
} });
|
|
3086
3171
|
}
|
|
3087
|
-
const
|
|
3088
|
-
if (
|
|
3089
|
-
||
|
|
3090
|
-
|
|
3172
|
+
const effect = active.record.pendingEffect;
|
|
3173
|
+
if (effect?.kind !== 'terminal-comment' || effect.issueNumber !== active.record.issueNumber
|
|
3174
|
+
|| effect.marker !== marker || effect.bodySha256 !== sha256(body)
|
|
3175
|
+
|| effect.outcome !== notifications.report.outcome || effect.attempt !== attempt)
|
|
3176
|
+
return active;
|
|
3177
|
+
const settlement = await settleCommentEffect(effect, {
|
|
3178
|
+
observe: async () => {
|
|
3179
|
+
const issue = await this.readIssue(effect.issueNumber);
|
|
3180
|
+
if (!issue || issue.state !== 'OPEN')
|
|
3181
|
+
return 'diverged';
|
|
3182
|
+
const matching = commentsWithMarker(issue, marker);
|
|
3183
|
+
if (matching.length === 0)
|
|
3184
|
+
return 'absent';
|
|
3185
|
+
return matching.length === 1 && sha256(matching[0].body) === effect.bodySha256 ? 'confirmed' : 'diverged';
|
|
3186
|
+
},
|
|
3187
|
+
invoke: () => this.dependencies.issues.postComment(effect.issueNumber, body),
|
|
3188
|
+
});
|
|
3189
|
+
const delivered = settlement.status === 'confirmed';
|
|
3190
|
+
const status = delivered ? 'delivered'
|
|
3191
|
+
: attempt >= TERMINAL_NOTIFICATION_ATTEMPTS ? 'exhausted' : 'pending';
|
|
3192
|
+
const diagnostic = delivered ? {} : { diagnostic: terminalNotificationDiagnostic('comment', settlement.status) };
|
|
3193
|
+
return this.persist(active, {
|
|
3194
|
+
terminalNotifications: {
|
|
3195
|
+
...notifications,
|
|
3196
|
+
comment: { status, attempts: attempt, ...diagnostic },
|
|
3197
|
+
},
|
|
3198
|
+
pendingEffect: undefined,
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
async settleTerminalLabels(starting) {
|
|
3202
|
+
let active = starting;
|
|
3203
|
+
let notifications = active.record.terminalNotifications;
|
|
3204
|
+
const policy = terminalLabelChanges(active.config, notifications.report.outcome);
|
|
3205
|
+
if (notifications.labels.status === 'delivered') {
|
|
3206
|
+
const issue = await this.readIssue(active.record.issueNumber);
|
|
3207
|
+
const settled = issue && policy.add.every((label) => issue.labels.includes(label))
|
|
3208
|
+
&& policy.remove.every((label) => !issue.labels.includes(label));
|
|
3209
|
+
if (settled)
|
|
3210
|
+
return active;
|
|
3211
|
+
notifications = { ...notifications, labels: { status: 'pending', attempts: 0 } };
|
|
3212
|
+
active = await this.persist(active, { terminalNotifications: notifications });
|
|
3213
|
+
}
|
|
3214
|
+
if (notifications.labels.status !== 'pending' || notifications.labels.attempts >= TERMINAL_NOTIFICATION_ATTEMPTS)
|
|
3215
|
+
return active;
|
|
3216
|
+
const attempt = notifications.labels.attempts + 1;
|
|
3217
|
+
if (!active.record.pendingEffect) {
|
|
3218
|
+
active = await this.persist(active, { pendingEffect: {
|
|
3219
|
+
kind: 'terminal-labels', issueNumber: active.record.issueNumber, ...policy,
|
|
3220
|
+
outcome: notifications.report.outcome, attempt,
|
|
3221
|
+
} });
|
|
3091
3222
|
}
|
|
3092
|
-
|
|
3223
|
+
const effect = active.record.pendingEffect;
|
|
3224
|
+
if (effect?.kind !== 'terminal-labels' || effect.issueNumber !== active.record.issueNumber
|
|
3225
|
+
|| effect.outcome !== notifications.report.outcome || effect.attempt !== attempt
|
|
3226
|
+
|| !sameStrings(effect.add, policy.add) || !sameStrings(effect.remove, policy.remove))
|
|
3227
|
+
return active;
|
|
3228
|
+
const settlement = await settleLabelsEffect(effect, {
|
|
3229
|
+
observe: async () => {
|
|
3230
|
+
const issue = await this.readIssue(effect.issueNumber);
|
|
3231
|
+
if (!issue)
|
|
3232
|
+
return 'diverged';
|
|
3233
|
+
return effect.add.every((label) => issue.labels.includes(label))
|
|
3234
|
+
&& effect.remove.every((label) => !issue.labels.includes(label)) ? 'confirmed' : 'absent';
|
|
3235
|
+
},
|
|
3236
|
+
invoke: async () => {
|
|
3237
|
+
if (!this.dependencies.issues.reconcileTerminalLabels)
|
|
3238
|
+
throw new Error('terminal label reconciliation unavailable');
|
|
3239
|
+
await this.dependencies.issues.reconcileTerminalLabels(effect.issueNumber, {
|
|
3240
|
+
outcome: effect.outcome, add: effect.add, remove: effect.remove,
|
|
3241
|
+
});
|
|
3242
|
+
},
|
|
3243
|
+
});
|
|
3244
|
+
const delivered = settlement.status === 'confirmed';
|
|
3245
|
+
const status = delivered ? 'delivered'
|
|
3246
|
+
: attempt >= TERMINAL_NOTIFICATION_ATTEMPTS ? 'exhausted' : 'pending';
|
|
3247
|
+
const diagnostic = delivered ? {} : { diagnostic: terminalNotificationDiagnostic('labels', settlement.status) };
|
|
3248
|
+
return this.persist(active, {
|
|
3249
|
+
terminalNotifications: {
|
|
3250
|
+
...notifications,
|
|
3251
|
+
labels: { status, attempts: attempt, ...diagnostic },
|
|
3252
|
+
},
|
|
3253
|
+
pendingEffect: undefined,
|
|
3254
|
+
});
|
|
3093
3255
|
}
|
|
3094
|
-
async persistTerminal(active, outcome, evidenceCode,
|
|
3095
|
-
const evidence = await this.observeOutcomeEvidenceEffect(active, evidenceCode, canonicalJson(outcome)
|
|
3256
|
+
async persistTerminal(active, outcome, evidenceCode, additionalChanges = {}) {
|
|
3257
|
+
const evidence = await this.observeOutcomeEvidenceEffect(active, evidenceCode, canonicalJson(outcome));
|
|
3096
3258
|
active = evidence.active;
|
|
3097
3259
|
const terminalOutcome = { ...outcome, evidencePath: evidence.path };
|
|
3260
|
+
let terminalComments = active.record.issueSnapshot.comments ?? [];
|
|
3261
|
+
if (outcome.status !== 'transport-failed') {
|
|
3262
|
+
try {
|
|
3263
|
+
terminalComments = (await this.readIssue(active.record.issueNumber))?.comments ?? terminalComments;
|
|
3264
|
+
}
|
|
3265
|
+
catch {
|
|
3266
|
+
// Terminal notification observation is best-effort and cannot change the Run outcome.
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
const notifications = terminalNotificationState(active.record, outcome, this.timestamp(), terminalComments);
|
|
3098
3270
|
const changes = {
|
|
3099
3271
|
...additionalChanges,
|
|
3100
3272
|
lifecycle: outcome.status,
|
|
3101
3273
|
terminalOutcome,
|
|
3102
3274
|
outcomeEvidenceId: evidence.id,
|
|
3275
|
+
...(notifications ? { terminalNotifications: notifications } : {}),
|
|
3103
3276
|
};
|
|
3104
3277
|
if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
|
|
3105
3278
|
changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
|
|
@@ -3107,8 +3280,13 @@ export class RunIssue {
|
|
|
3107
3280
|
: { status: outcome.status }, outcome.status === 'internal-error' ? outcome.code : undefined);
|
|
3108
3281
|
}
|
|
3109
3282
|
active = await this.persist(active, changes);
|
|
3110
|
-
|
|
3283
|
+
try {
|
|
3111
3284
|
active = await this.confirmEffect(active);
|
|
3285
|
+
}
|
|
3286
|
+
catch {
|
|
3287
|
+
return publicOutcome(terminalOutcome);
|
|
3288
|
+
}
|
|
3289
|
+
active = await this.settleTerminalNotificationsBestEffort(active);
|
|
3112
3290
|
return publicOutcome(terminalOutcome);
|
|
3113
3291
|
}
|
|
3114
3292
|
async preClaimInternal(code, issueNumber) {
|
|
@@ -3146,6 +3324,142 @@ class PostEffectStateError extends Error {
|
|
|
3146
3324
|
this.active = active;
|
|
3147
3325
|
}
|
|
3148
3326
|
}
|
|
3327
|
+
const TERMINAL_NOTIFICATION_ATTEMPTS = 3;
|
|
3328
|
+
function terminalNotificationState(record, outcome, observedAt, comments) {
|
|
3329
|
+
if (outcome.status === 'transport-failed')
|
|
3330
|
+
return undefined;
|
|
3331
|
+
const commentId = issueCommentHighWaterMark(comments);
|
|
3332
|
+
return {
|
|
3333
|
+
version: 1,
|
|
3334
|
+
commentCutoff: { commentId, observedAt },
|
|
3335
|
+
report: terminalReportSnapshot(record, outcome),
|
|
3336
|
+
comment: { status: 'pending', attempts: 0 },
|
|
3337
|
+
labels: { status: 'pending', attempts: 0 },
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
function terminalReportSnapshot(record, outcome) {
|
|
3341
|
+
const summary = outcome.status === 'blocked'
|
|
3342
|
+
? outcome.blocker?.summary ?? `The run stopped at ${outcome.kind}.`
|
|
3343
|
+
: outcome.status === 'internal-error'
|
|
3344
|
+
? internalErrorSummary(outcome.code)
|
|
3345
|
+
: outcome.status === 'cancelled'
|
|
3346
|
+
? 'The run was cancelled before completion.'
|
|
3347
|
+
: record.implementationResult?.summary ?? record.proofReceipt?.summary ?? 'The authorized implementation is ready for review.';
|
|
3348
|
+
const nextAction = outcome.status === 'review-ready'
|
|
3349
|
+
? 'Review the draft PR. To request an in-scope follow-up, comment on this issue.'
|
|
3350
|
+
: outcome.status === 'blocked'
|
|
3351
|
+
? outcome.resumable
|
|
3352
|
+
? 'Resolve the stated blocker, then rerun the same issue.'
|
|
3353
|
+
: 'Provide the missing decision or authority in this issue before continuing.'
|
|
3354
|
+
: outcome.status === 'internal-error'
|
|
3355
|
+
? 'Inspect the package-owned evidence for the code above, correct the runner condition, and retry the same issue.'
|
|
3356
|
+
: 'Re-add the authorization label and rerun the same issue if the work should continue.';
|
|
3357
|
+
return {
|
|
3358
|
+
version: 1,
|
|
3359
|
+
outcome: outcome.status,
|
|
3360
|
+
summary: publicTerminalText(summary, 'Terminal summary is unavailable.'),
|
|
3361
|
+
...(outcome.status === 'review-ready' ? { pullRequestUrl: outcome.pullRequestUrl } : {}),
|
|
3362
|
+
passedChecks: record.checks.filter((check) => check.status === 'passed').slice(0, 16)
|
|
3363
|
+
.map((check) => publicTerminalText(check.id, 'Unnamed check.', 200)),
|
|
3364
|
+
publishableProof: (record.proofReceipt?.publishableEvidence ?? []).slice(0, 4).map((proof) => publicTerminalText(`${proof.ref}: ${proof.description}`, 'Publishable proof detail is unavailable.', 500)),
|
|
3365
|
+
unverified: record.proofReceipt ? [] : ['No publishable Acceptance Proof receipt was retained.'],
|
|
3366
|
+
risks: (record.implementationResult?.residualRisks ?? []).slice(0, 4)
|
|
3367
|
+
.map((risk) => publicTerminalText(risk, 'Risk detail is unavailable.', 500)),
|
|
3368
|
+
reviewFocus: (record.directReview?.review.coverage ?? []).slice(0, 8)
|
|
3369
|
+
.map((focus) => publicTerminalText(focus, 'Review focus is unavailable.', 250)),
|
|
3370
|
+
nextAction,
|
|
3371
|
+
...(outcome.status === 'blocked' ? { blocker: {
|
|
3372
|
+
kind: outcome.kind,
|
|
3373
|
+
resumable: outcome.resumable,
|
|
3374
|
+
attempted: (outcome.blocker?.attempted ?? []).slice(0, 8)
|
|
3375
|
+
.map((attempt) => publicTerminalText(attempt, 'Attempt detail is unavailable.', 500)),
|
|
3376
|
+
} } : {}),
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
function internalErrorSummary(code) {
|
|
3380
|
+
return code === 'implementation-change-set-invalid'
|
|
3381
|
+
? 'The implementation report declared changed files, but the runner found no matching worktree changes.'
|
|
3382
|
+
: `The runner encountered an internal failure (${code}) and could not complete this run.`;
|
|
3383
|
+
}
|
|
3384
|
+
function terminalComment(runId, cycle, report) {
|
|
3385
|
+
const markerKind = report.outcome === 'review-ready' ? 'handoff' : report.outcome;
|
|
3386
|
+
const marker = `<!-- codex-orchestrator:run:${runId}:cycle:${cycle}:${markerKind} -->`;
|
|
3387
|
+
if (report.outcome === 'review-ready') {
|
|
3388
|
+
return [
|
|
3389
|
+
marker,
|
|
3390
|
+
'codex-orchestrator completed this run.',
|
|
3391
|
+
'',
|
|
3392
|
+
'## Summary', report.summary,
|
|
3393
|
+
'',
|
|
3394
|
+
'## Pull request', report.pullRequestUrl ?? 'Draft PR URL is unavailable.',
|
|
3395
|
+
'',
|
|
3396
|
+
'## Passed checks', ...terminalList(report.passedChecks),
|
|
3397
|
+
'',
|
|
3398
|
+
'## Publishable proof', ...terminalList(report.publishableProof),
|
|
3399
|
+
'',
|
|
3400
|
+
'## Not verified', ...terminalList(report.unverified),
|
|
3401
|
+
'',
|
|
3402
|
+
'## Known risks', ...terminalList(report.risks),
|
|
3403
|
+
'',
|
|
3404
|
+
'## Review focus', ...terminalList(report.reviewFocus),
|
|
3405
|
+
'',
|
|
3406
|
+
'## Follow-up', report.nextAction,
|
|
3407
|
+
].join('\n');
|
|
3408
|
+
}
|
|
3409
|
+
const title = report.outcome === 'blocked' ? 'blocked' : report.outcome === 'internal-error' ? 'stopped after an internal error' : 'was cancelled';
|
|
3410
|
+
return [
|
|
3411
|
+
marker,
|
|
3412
|
+
`codex-orchestrator ${title} this run.`,
|
|
3413
|
+
'',
|
|
3414
|
+
...(report.blocker ? [`Kind: ${report.blocker.kind}`, `Resumable: ${report.blocker.resumable ? 'yes' : 'no'}`, ''] : []),
|
|
3415
|
+
'## Reason', `Reason: ${report.summary}`,
|
|
3416
|
+
...(report.blocker ? ['', 'Attempted:', ...terminalList(report.blocker.attempted)] : []),
|
|
3417
|
+
'',
|
|
3418
|
+
'## Retained work and state',
|
|
3419
|
+
...terminalList([
|
|
3420
|
+
...report.passedChecks.map((check) => `Passed check: ${check}`),
|
|
3421
|
+
...report.publishableProof.map((proof) => `Proof: ${proof}`),
|
|
3422
|
+
]),
|
|
3423
|
+
'',
|
|
3424
|
+
'## Next action', report.nextAction,
|
|
3425
|
+
].join('\n');
|
|
3426
|
+
}
|
|
3427
|
+
function terminalList(values) {
|
|
3428
|
+
return values.length > 0 ? values.map((value) => `- ${value}`) : ['- None recorded.'];
|
|
3429
|
+
}
|
|
3430
|
+
function terminalNotificationDiagnostic(kind, status) {
|
|
3431
|
+
return status === 'unknown'
|
|
3432
|
+
? `terminal-${kind}-delivery-unknown`
|
|
3433
|
+
: `terminal-${kind}-observation-${status}`;
|
|
3434
|
+
}
|
|
3435
|
+
function terminalLabelChanges(config, outcome) {
|
|
3436
|
+
const labels = config.github.labels;
|
|
3437
|
+
if (outcome === 'review-ready')
|
|
3438
|
+
return {
|
|
3439
|
+
add: [labels.review.name], remove: [labels.auto.name, labels.running.name, labels.blocked.name].sort(),
|
|
3440
|
+
};
|
|
3441
|
+
if (outcome === 'blocked')
|
|
3442
|
+
return { add: [labels.blocked.name], remove: [labels.running.name, labels.review.name].sort() };
|
|
3443
|
+
return {
|
|
3444
|
+
add: [],
|
|
3445
|
+
remove: [labels.running.name, labels.blocked.name, labels.review.name, ...(outcome === 'cancelled' ? [labels.auto.name] : [])].sort(),
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
function claimLabelProjectionSettled(labels, config) {
|
|
3449
|
+
const policy = config.github.labels;
|
|
3450
|
+
return labels.includes(policy.auto.name) && labels.includes(policy.running.name)
|
|
3451
|
+
&& !labels.includes(policy.blocked.name) && !labels.includes(policy.review.name);
|
|
3452
|
+
}
|
|
3453
|
+
function issueCommentHighWaterMark(comments) {
|
|
3454
|
+
const ids = comments.map((comment) => comment.id).filter((id) => Boolean(id));
|
|
3455
|
+
const decimal = ids.filter((id) => /^\d+$/u.test(id));
|
|
3456
|
+
if (decimal.length > 0)
|
|
3457
|
+
return decimal.reduce((highest, id) => BigInt(id) > BigInt(highest) ? id : highest);
|
|
3458
|
+
return ids.at(-1) ?? null;
|
|
3459
|
+
}
|
|
3460
|
+
function publicTerminalText(value, fallback, maxLength = 2_000) {
|
|
3461
|
+
return publicBlockedText(value, fallback, maxLength);
|
|
3462
|
+
}
|
|
3149
3463
|
function lifecycleForAttempt(operationId) {
|
|
3150
3464
|
if (operationId === 'configured-check')
|
|
3151
3465
|
return 'checking';
|
|
@@ -3285,39 +3599,10 @@ function pendingCandidateBoundary(record) {
|
|
|
3285
3599
|
function commentsWithMarker(issue, marker) {
|
|
3286
3600
|
return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
|
|
3287
3601
|
}
|
|
3288
|
-
function
|
|
3289
|
-
const
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
'',
|
|
3293
|
-
`Kind: ${outcome.kind}`,
|
|
3294
|
-
`Resumable: ${outcome.resumable ? 'yes' : 'no'}`,
|
|
3295
|
-
];
|
|
3296
|
-
if (outcome.blocker) {
|
|
3297
|
-
lines.push('', `Reason: ${publicBlockedText(outcome.blocker.summary, evidenceCode, 2_000)}`);
|
|
3298
|
-
if (outcome.blocker.reviewerRejectionDetail) {
|
|
3299
|
-
lines.push(`Detail: ${publicBlockedText(outcome.blocker.reviewerRejectionDetail, 'Review detail was unsafe or unavailable.', 2_000)}`);
|
|
3300
|
-
}
|
|
3301
|
-
if (outcome.blocker.attempted.length > 0) {
|
|
3302
|
-
const attempted = [];
|
|
3303
|
-
let remaining = 8_000;
|
|
3304
|
-
for (const attempt of outcome.blocker.attempted.slice(0, 16)) {
|
|
3305
|
-
if (remaining <= 0)
|
|
3306
|
-
break;
|
|
3307
|
-
const text = publicBlockedText(attempt, 'Unsafe or unavailable attempted action.', Math.min(2_000, remaining));
|
|
3308
|
-
attempted.push(`- ${text}`);
|
|
3309
|
-
remaining -= text.length;
|
|
3310
|
-
}
|
|
3311
|
-
lines.push('', 'Attempted:', ...attempted);
|
|
3312
|
-
}
|
|
3313
|
-
else {
|
|
3314
|
-
lines.push('', 'Attempted:', '- No attempted actions were recorded.');
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
else {
|
|
3318
|
-
lines.push('', `Reason: ${evidenceCode}`, '', 'Attempted:', '- No attempted actions were recorded.');
|
|
3319
|
-
}
|
|
3320
|
-
return lines.join('\n');
|
|
3602
|
+
function managedLabelProjection(labels, config) {
|
|
3603
|
+
const policy = config.github.labels;
|
|
3604
|
+
const managed = new Set([policy.auto.name, policy.running.name, policy.blocked.name, policy.review.name]);
|
|
3605
|
+
return labels.filter((label) => managed.has(label)).sort();
|
|
3321
3606
|
}
|
|
3322
3607
|
function publicBlockedText(value, fallback, maxLength) {
|
|
3323
3608
|
if (containsCredentialEvidence(value) || containsHostIdentityEvidence(value)
|
|
@@ -3330,6 +3615,11 @@ function publicBlockedText(value, fallback, maxLength) {
|
|
|
3330
3615
|
? normalized
|
|
3331
3616
|
: `${normalized.slice(0, Math.max(0, maxLength - 12))} [truncated]`;
|
|
3332
3617
|
}
|
|
3618
|
+
function publicIssueFeedbackText(value, fallback) {
|
|
3619
|
+
if (/\b(?:chain[- ]of[- ]thought|reasoning artifact|local-only log|private reasoning|debug log)\b/iu.test(value))
|
|
3620
|
+
return fallback;
|
|
3621
|
+
return publicBlockedText(value, fallback, 4_000);
|
|
3622
|
+
}
|
|
3333
3623
|
function sortedUnique(values) {
|
|
3334
3624
|
return [...new Set(values)].sort();
|
|
3335
3625
|
}
|