codex-orchestrator 2.0.13 → 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 +21 -0
- package/dist/src/v2/acceptance-proof.js +1 -4
- package/dist/src/v2/acceptance-proof.js.map +1 -1
- 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 +1 -0
- package/dist/src/v2/containment.d.ts.map +1 -1
- package/dist/src/v2/containment.js +8 -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 +3 -3
- 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 +629 -286
- 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 -15
- package/dist/src/v2/run-store.d.ts.map +1 -1
- package/dist/src/v2/run-store.js +111 -25
- 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 -16
- 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/internal-workflow/skills/tickets-orchestrator/SKILL.md +4 -0
- package/internal-workflow/skills/to-spec/SKILL.md +17 -8
- package/internal-workflow/skills/to-spec/evals/evals.json +6 -0
- package/internal-workflow/skills/to-tickets/SKILL.md +29 -2
- package/internal-workflow/skills/to-tickets/evals/evals.json +12 -0
- package/package.json +1 -1
package/dist/src/v2/run-issue.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, resolve } from 'node:path';
|
|
2
2
|
import { checkedChangePayloadSha256, } from './checked-change.js';
|
|
3
3
|
import { parseAgentAutoConfig } from './config.js';
|
|
4
|
-
import { canonicalJson, containsCredentialEvidence, parseJsonWithoutDuplicateKeys, sha256 } from './containment.js';
|
|
4
|
+
import { canonicalJson, containsCredentialEvidence, containsHostIdentityEvidence, parseJsonWithoutDuplicateKeys, sha256 } from './containment.js';
|
|
5
5
|
import { findDeniedPathMatch } from './adapters/path-policy.js';
|
|
6
6
|
import { adoptActiveAttempt, confirmActiveAttemptCleanup, createActiveAttempt, launchActiveAttempt, observeActiveAttempt, } from './active-attempt.js';
|
|
7
7
|
import { validateImplementationReport } from './implementation-report.js';
|
|
@@ -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
|
}
|
|
@@ -774,13 +772,16 @@ export class RunIssue {
|
|
|
774
772
|
baseBranch: config.github.baseBranch,
|
|
775
773
|
});
|
|
776
774
|
if (!pullRequest?.number || !pullRequest.nodeId || !pullRequest.headSha) {
|
|
777
|
-
return { result: await this.
|
|
775
|
+
return { result: await this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'review-feedback-pr-identity-missing') };
|
|
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') };
|
|
778
779
|
}
|
|
779
780
|
const expectedHeadSha = feedback.previousPublishedHeadSha === null
|
|
780
781
|
? pullRequest.headSha
|
|
781
782
|
: feedback.previousPublishedHeadSha;
|
|
782
783
|
if (!expectedHeadSha) {
|
|
783
|
-
return { result: await this.
|
|
784
|
+
return { result: await this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'review-feedback-head-missing') };
|
|
784
785
|
}
|
|
785
786
|
const observed = await this.dependencies.reviewFeedback.observeAndFreeze({
|
|
786
787
|
runId: starting.record.runId,
|
|
@@ -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
|
-
return { result: await this.
|
|
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,26 +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.
|
|
1031
|
+
return await this.terminal(active, seed, active.record.pendingEffect.code);
|
|
1006
1032
|
}
|
|
1007
|
-
if (active.record.
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
return await this.publishBlockedTerminal(active, {
|
|
1012
|
-
status: 'blocked',
|
|
1013
|
-
kind: active.record.pendingEffect.blockKind,
|
|
1014
|
-
resumable: active.record.pendingEffect.resumable,
|
|
1015
|
-
}, active.record.pendingEffect.evidenceCode);
|
|
1016
|
-
}
|
|
1017
|
-
if (existing.terminalOutcome?.status === 'blocked'
|
|
1018
|
-
&& issue?.state === 'OPEN'
|
|
1019
|
-
&& blockedLabelProjection(issue.labels, config).status !== 'settled') {
|
|
1020
|
-
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);
|
|
1021
1037
|
}
|
|
1022
1038
|
if (active.record.terminalOutcome?.status === 'transport-failed'
|
|
1023
1039
|
&& !active.record.terminalOutcome.resumable
|
|
@@ -1072,7 +1088,9 @@ export class RunIssue {
|
|
|
1072
1088
|
return reconciled;
|
|
1073
1089
|
active = reconciled.active;
|
|
1074
1090
|
}
|
|
1075
|
-
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')) {
|
|
1076
1094
|
const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
|
|
1077
1095
|
if ('result' in activation)
|
|
1078
1096
|
return activation.result;
|
|
@@ -1193,7 +1211,7 @@ export class RunIssue {
|
|
|
1193
1211
|
}
|
|
1194
1212
|
attemptLoop: while (true) {
|
|
1195
1213
|
if (!await this.authorized(active, config)) {
|
|
1196
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
1214
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'authorization-revoked');
|
|
1197
1215
|
}
|
|
1198
1216
|
if (this.signal.aborted)
|
|
1199
1217
|
return await this.terminal(active, { status: 'cancelled' });
|
|
@@ -1224,6 +1242,12 @@ export class RunIssue {
|
|
|
1224
1242
|
...(feedbackBatch ? {
|
|
1225
1243
|
reviewFeedbackRound: active.record.reviewFeedback.repairRound,
|
|
1226
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
|
+
},
|
|
1227
1251
|
} : {}),
|
|
1228
1252
|
onPrepared: async (prepared) => {
|
|
1229
1253
|
const currentActive = active;
|
|
@@ -1327,8 +1351,11 @@ export class RunIssue {
|
|
|
1327
1351
|
}
|
|
1328
1352
|
return await this.terminal(active, implementationBlockerOutcome(report.blocker));
|
|
1329
1353
|
}
|
|
1354
|
+
if (report.status === 'answer-only' || report.status === 'boundary') {
|
|
1355
|
+
return this.completeIssueFeedbackResponse(active, report, config);
|
|
1356
|
+
}
|
|
1330
1357
|
if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
|
|
1331
|
-
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
1358
|
+
return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'implementation-head-drift');
|
|
1332
1359
|
}
|
|
1333
1360
|
const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
|
|
1334
1361
|
if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
|
|
@@ -1348,6 +1375,7 @@ export class RunIssue {
|
|
|
1348
1375
|
active = await this.adoptAttempt(active, sha256(canonicalJson(report)), {
|
|
1349
1376
|
transportRetries: active.record.transportRetries,
|
|
1350
1377
|
reportRepairs: 0,
|
|
1378
|
+
implementationResult: { summary: report.summary, residualRisks: report.residualRisks },
|
|
1351
1379
|
});
|
|
1352
1380
|
active = await this.clearAttempt(active);
|
|
1353
1381
|
}
|
|
@@ -1880,7 +1908,10 @@ export class RunIssue {
|
|
|
1880
1908
|
const record = { ...active.record, ...normalized, updatedAt: this.timestamp() };
|
|
1881
1909
|
if (Object.hasOwn(changes, 'pendingEffect') && changes.pendingEffect === undefined)
|
|
1882
1910
|
delete record.pendingEffect;
|
|
1883
|
-
|
|
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']) {
|
|
1884
1915
|
if (Object.hasOwn(changes, key) && changes[key] === undefined)
|
|
1885
1916
|
delete record[key];
|
|
1886
1917
|
}
|
|
@@ -1974,7 +2005,7 @@ export class RunIssue {
|
|
|
1974
2005
|
}
|
|
1975
2006
|
async authorizedForExactLabels(active, expectedLabels) {
|
|
1976
2007
|
const issue = await this.readIssue(active.record.issueNumber);
|
|
1977
|
-
return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
|
|
2008
|
+
return !!issue && issue.state === 'OPEN' && sameStrings(managedLabelProjection(issue.labels, active.config), expectedLabels)
|
|
1978
2009
|
&& this.hasTrustedClaim(issue, active.record);
|
|
1979
2010
|
}
|
|
1980
2011
|
hasTrustedClaim(issue, record) {
|
|
@@ -2030,6 +2061,7 @@ export class RunIssue {
|
|
|
2030
2061
|
}
|
|
2031
2062
|
const validation = await this.dependencies.reviewFeedback.revalidate({
|
|
2032
2063
|
batch,
|
|
2064
|
+
issueNumber,
|
|
2033
2065
|
epoch: 'pre-update',
|
|
2034
2066
|
expectedHeadSha: batch.priorPublishedHeadSha,
|
|
2035
2067
|
});
|
|
@@ -2392,7 +2424,7 @@ export class RunIssue {
|
|
|
2392
2424
|
: this.persistCandidateEvidenceSafetyTerminal(active, 'candidate-execution-missing');
|
|
2393
2425
|
}
|
|
2394
2426
|
if (inspection.value === 'mutated') {
|
|
2395
|
-
return this.
|
|
2427
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'candidate-materialization-mutated');
|
|
2396
2428
|
}
|
|
2397
2429
|
if (active.record.activeAttempt?.stage === 'observed' && active.record.activeAttempt.result) {
|
|
2398
2430
|
return { active };
|
|
@@ -2484,7 +2516,7 @@ export class RunIssue {
|
|
|
2484
2516
|
reviewFeedback: blockReviewFeedback(active.record.reviewFeedback, 'safety', this.timestamp()),
|
|
2485
2517
|
});
|
|
2486
2518
|
}
|
|
2487
|
-
return this.
|
|
2519
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, evidenceCode);
|
|
2488
2520
|
}
|
|
2489
2521
|
mapCandidateFailure(active, code) {
|
|
2490
2522
|
if (code === 'candidate-unstable' || code === 'candidate-io-failed' || code === 'candidate-materialization-io-failed') {
|
|
@@ -2857,54 +2889,7 @@ export class RunIssue {
|
|
|
2857
2889
|
return transitioned;
|
|
2858
2890
|
}
|
|
2859
2891
|
async blockReviewFeedback(active, kind, evidenceCode, outcome = { status: 'blocked', kind, resumable: false }) {
|
|
2860
|
-
|
|
2861
|
-
if (!feedback?.activeBatch)
|
|
2862
|
-
return this.terminal(active, { status: 'blocked', kind, resumable: false }, evidenceCode);
|
|
2863
|
-
{
|
|
2864
|
-
const config = active.config;
|
|
2865
|
-
const blockedLabels = [config.github.labels.auto.name, config.github.labels.blocked.name].sort();
|
|
2866
|
-
const existingIntent = active.record.pendingEffect;
|
|
2867
|
-
if (existingIntent?.kind !== 'review-blocked-labels') {
|
|
2868
|
-
active = await this.persist(active, { pendingEffect: {
|
|
2869
|
-
kind: 'review-blocked-labels', issueNumber: active.record.issueNumber,
|
|
2870
|
-
batchId: feedback.activeBatch.batchId, expected: blockedLabels, blockKind: kind, evidenceCode,
|
|
2871
|
-
} });
|
|
2872
|
-
}
|
|
2873
|
-
else if (existingIntent.issueNumber !== active.record.issueNumber
|
|
2874
|
-
|| existingIntent.batchId !== feedback.activeBatch.batchId
|
|
2875
|
-
|| !sameStrings(existingIntent.expected, blockedLabels)
|
|
2876
|
-
|| existingIntent.blockKind !== kind
|
|
2877
|
-
|| existingIntent.evidenceCode !== evidenceCode) {
|
|
2878
|
-
return this.invokedFailure(active, 'review-feedback-blocked-labels-pendingEffect-diverged');
|
|
2879
|
-
}
|
|
2880
|
-
const ownedRunningLabels = [config.github.labels.auto.name, config.github.labels.running.name].sort();
|
|
2881
|
-
const ownedReviewLabels = [config.github.labels.review.name];
|
|
2882
|
-
const effect = active.record.pendingEffect;
|
|
2883
|
-
if (effect?.kind !== 'review-blocked-labels') {
|
|
2884
|
-
return this.invokedFailure(active, 'review-feedback-blocked-labels-pendingEffect-diverged');
|
|
2885
|
-
}
|
|
2886
|
-
const settlement = await settleLabelsEffect(effect, {
|
|
2887
|
-
observe: async () => {
|
|
2888
|
-
const issue = await this.readIssue(active.record.issueNumber);
|
|
2889
|
-
if (!issue || issue.state !== 'OPEN')
|
|
2890
|
-
return 'diverged';
|
|
2891
|
-
if (sameStrings(issue.labels, blockedLabels))
|
|
2892
|
-
return 'confirmed';
|
|
2893
|
-
return sameStrings(issue.labels, ownedRunningLabels) || sameStrings(issue.labels, ownedReviewLabels)
|
|
2894
|
-
? 'absent' : 'diverged';
|
|
2895
|
-
},
|
|
2896
|
-
invoke: () => this.dependencies.issues.setLabels(active.record.issueNumber, blockedLabels),
|
|
2897
|
-
});
|
|
2898
|
-
if (settlement.status === 'unknown') {
|
|
2899
|
-
return this.invokedFailure(active, 'review-feedback-blocked-labels-delivery-unknown');
|
|
2900
|
-
}
|
|
2901
|
-
if (settlement.status !== 'confirmed') {
|
|
2902
|
-
return this.invokedFailure(active, 'review-feedback-blocked-labels-observation-diverged');
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
return this.persistTerminal(active, outcome, evidenceCode, false, {
|
|
2906
|
-
reviewFeedback: blockReviewFeedback(feedback, kind, this.timestamp()),
|
|
2907
|
-
}, true);
|
|
2892
|
+
return this.terminal(active, outcome, evidenceCode);
|
|
2908
2893
|
}
|
|
2909
2894
|
async mapImplementationFailure(active, result) {
|
|
2910
2895
|
if (result.kind === 'transport-failed')
|
|
@@ -2937,7 +2922,7 @@ export class RunIssue {
|
|
|
2937
2922
|
async revoked(active) {
|
|
2938
2923
|
if (active.record.pendingEffect)
|
|
2939
2924
|
return this.invokedFailure(active, 'revoked-with-pending-effect');
|
|
2940
|
-
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
|
|
2925
|
+
return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'authorization-revoked');
|
|
2941
2926
|
}
|
|
2942
2927
|
async invokedFailure(active, code, summary = 'Publication delivery requires reconciliation.') {
|
|
2943
2928
|
const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary });
|
|
@@ -2951,8 +2936,8 @@ export class RunIssue {
|
|
|
2951
2936
|
});
|
|
2952
2937
|
return { status: 'repair-ready', source, blockerIds: [...new Set(blockerIds)].sort(), evidencePath: evidence.path };
|
|
2953
2938
|
}
|
|
2954
|
-
async observeOutcomeEvidenceEffect(active, code, summary
|
|
2955
|
-
if (!active.record.pendingEffect
|
|
2939
|
+
async observeOutcomeEvidenceEffect(active, code, summary) {
|
|
2940
|
+
if (!active.record.pendingEffect) {
|
|
2956
2941
|
const recordedAt = this.timestamp();
|
|
2957
2942
|
const path = this.dependencies.outcomeEvidencePath(active.record.runId, code, sha256(summary));
|
|
2958
2943
|
const bytes = outcomeEvidenceBytes({ runId: active.record.runId, code, summary, recordedAt });
|
|
@@ -2983,115 +2968,311 @@ export class RunIssue {
|
|
|
2983
2968
|
throw new PostEffectStateError(active);
|
|
2984
2969
|
return { active, path: effect.path, id: `evidence:${effect.runId}:${effect.code}` };
|
|
2985
2970
|
}
|
|
2986
|
-
async
|
|
2987
|
-
|
|
2988
|
-
|
|
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' });
|
|
2989
2977
|
}
|
|
2990
|
-
if (
|
|
2991
|
-
|
|
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' });
|
|
2992
2981
|
}
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
const
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
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 = [
|
|
2987
|
+
marker,
|
|
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)),
|
|
3001
|
+
};
|
|
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;
|
|
3010
3037
|
}
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
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';
|
|
3017
3071
|
}
|
|
3018
3072
|
else {
|
|
3019
|
-
expected = existing.expected;
|
|
3020
|
-
if (!sameStrings(expected, autoBlocked) && !sameStrings(expected, blockedOnly) && expected.length !== 0) {
|
|
3021
|
-
return this.invokedFailure(active, 'blocked-labels-pendingEffect-diverged');
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
let issue = await this.readIssue(active.record.issueNumber);
|
|
3025
|
-
if (!issue)
|
|
3026
|
-
return this.invokedFailure(active, 'blocked-labels-issue-missing');
|
|
3027
|
-
let projection = blockedLabelProjection(issue.labels, active.config);
|
|
3028
|
-
if (issue.state === 'OPEN' && projection.status === 'transition') {
|
|
3029
|
-
if (!this.dependencies.issues.transitionToBlocked) {
|
|
3030
|
-
return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
|
|
3031
|
-
}
|
|
3032
3073
|
try {
|
|
3033
|
-
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
|
+
}
|
|
3034
3089
|
}
|
|
3035
3090
|
catch {
|
|
3036
|
-
|
|
3091
|
+
diagnostic = 'issue-feedback-response-delivery-failed';
|
|
3037
3092
|
}
|
|
3038
|
-
issue = await this.readIssue(active.record.issueNumber);
|
|
3039
|
-
projection = issue ? blockedLabelProjection(issue.labels, active.config) : { status: 'diverged' };
|
|
3040
3093
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
}
|
|
3047
|
-
if (!sameStrings(expected, projection.expected)) {
|
|
3048
|
-
expected = projection.expected;
|
|
3049
|
-
const pendingEffect = active.record.pendingEffect;
|
|
3050
|
-
if (pendingEffect?.kind !== 'blocked-labels')
|
|
3051
|
-
return this.invokedFailure(active, 'blocked-labels-pendingEffect-diverged');
|
|
3052
|
-
try {
|
|
3053
|
-
const { effectId: _effectId, ...intent } = pendingEffect;
|
|
3054
|
-
active = await this.persist(active, { pendingEffect: { ...intent, expected } });
|
|
3055
|
-
}
|
|
3056
|
-
catch {
|
|
3057
|
-
throw new PostEffectStateError(active);
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
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';
|
|
3060
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
|
+
};
|
|
3061
3110
|
try {
|
|
3062
|
-
active = await this.
|
|
3063
|
-
|
|
3111
|
+
active = await this.persist(active, {
|
|
3112
|
+
reviewFeedback: settleReviewFeedbackResponse(active.record.reviewFeedback, receipt),
|
|
3113
|
+
});
|
|
3064
3114
|
}
|
|
3065
3115
|
catch {
|
|
3066
|
-
|
|
3116
|
+
return publicOutcome(terminalOutcome);
|
|
3117
|
+
}
|
|
3118
|
+
try {
|
|
3119
|
+
active = await this.clearAttempt(active);
|
|
3067
3120
|
}
|
|
3121
|
+
catch {
|
|
3122
|
+
return publicOutcome(terminalOutcome);
|
|
3123
|
+
}
|
|
3124
|
+
return publicOutcome(active.record.terminalOutcome);
|
|
3068
3125
|
}
|
|
3069
|
-
async
|
|
3070
|
-
if (
|
|
3071
|
-
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.');
|
|
3072
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;
|
|
3073
3144
|
try {
|
|
3074
|
-
await this.
|
|
3145
|
+
active = await this.settleTerminalComment(active);
|
|
3075
3146
|
}
|
|
3076
3147
|
catch {
|
|
3077
|
-
return
|
|
3148
|
+
return active;
|
|
3078
3149
|
}
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|| blockedLabelProjection(observation.labels, active.config).status !== 'settled') {
|
|
3082
|
-
return this.invokedFailure(active, 'blocked-labels-observation-diverged');
|
|
3150
|
+
try {
|
|
3151
|
+
active = await this.settleTerminalLabels(active);
|
|
3083
3152
|
}
|
|
3084
|
-
|
|
3153
|
+
catch {
|
|
3154
|
+
return active;
|
|
3155
|
+
}
|
|
3156
|
+
return active;
|
|
3085
3157
|
}
|
|
3086
|
-
async
|
|
3087
|
-
|
|
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
|
+
} });
|
|
3171
|
+
}
|
|
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
|
+
} });
|
|
3222
|
+
}
|
|
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
|
+
});
|
|
3255
|
+
}
|
|
3256
|
+
async persistTerminal(active, outcome, evidenceCode, additionalChanges = {}) {
|
|
3257
|
+
const evidence = await this.observeOutcomeEvidenceEffect(active, evidenceCode, canonicalJson(outcome));
|
|
3088
3258
|
active = evidence.active;
|
|
3089
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);
|
|
3090
3270
|
const changes = {
|
|
3091
3271
|
...additionalChanges,
|
|
3092
3272
|
lifecycle: outcome.status,
|
|
3093
3273
|
terminalOutcome,
|
|
3094
3274
|
outcomeEvidenceId: evidence.id,
|
|
3275
|
+
...(notifications ? { terminalNotifications: notifications } : {}),
|
|
3095
3276
|
};
|
|
3096
3277
|
if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
|
|
3097
3278
|
changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
|
|
@@ -3099,8 +3280,13 @@ export class RunIssue {
|
|
|
3099
3280
|
: { status: outcome.status }, outcome.status === 'internal-error' ? outcome.code : undefined);
|
|
3100
3281
|
}
|
|
3101
3282
|
active = await this.persist(active, changes);
|
|
3102
|
-
|
|
3283
|
+
try {
|
|
3103
3284
|
active = await this.confirmEffect(active);
|
|
3285
|
+
}
|
|
3286
|
+
catch {
|
|
3287
|
+
return publicOutcome(terminalOutcome);
|
|
3288
|
+
}
|
|
3289
|
+
active = await this.settleTerminalNotificationsBestEffort(active);
|
|
3104
3290
|
return publicOutcome(terminalOutcome);
|
|
3105
3291
|
}
|
|
3106
3292
|
async preClaimInternal(code, issueNumber) {
|
|
@@ -3138,6 +3324,142 @@ class PostEffectStateError extends Error {
|
|
|
3138
3324
|
this.active = active;
|
|
3139
3325
|
}
|
|
3140
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
|
+
}
|
|
3141
3463
|
function lifecycleForAttempt(operationId) {
|
|
3142
3464
|
if (operationId === 'configured-check')
|
|
3143
3465
|
return 'checking';
|
|
@@ -3277,6 +3599,27 @@ function pendingCandidateBoundary(record) {
|
|
|
3277
3599
|
function commentsWithMarker(issue, marker) {
|
|
3278
3600
|
return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
|
|
3279
3601
|
}
|
|
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();
|
|
3606
|
+
}
|
|
3607
|
+
function publicBlockedText(value, fallback, maxLength) {
|
|
3608
|
+
if (containsCredentialEvidence(value) || containsHostIdentityEvidence(value)
|
|
3609
|
+
|| /["']?token["']?\s*[:=]\s*["']?[^\s"']{8,}/iu.test(value))
|
|
3610
|
+
return fallback;
|
|
3611
|
+
const normalized = value.replace(/[\r\n]+/gu, ' ').trim();
|
|
3612
|
+
if (normalized.length === 0)
|
|
3613
|
+
return fallback;
|
|
3614
|
+
return normalized.length <= maxLength
|
|
3615
|
+
? normalized
|
|
3616
|
+
: `${normalized.slice(0, Math.max(0, maxLength - 12))} [truncated]`;
|
|
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
|
+
}
|
|
3280
3623
|
function sortedUnique(values) {
|
|
3281
3624
|
return [...new Set(values)].sort();
|
|
3282
3625
|
}
|