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.
Files changed (45) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/src/v2/adapters/gh-issue-adapter.js +2 -0
  3. package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
  4. package/dist/src/v2/containment.d.ts.map +1 -1
  5. package/dist/src/v2/containment.js +2 -1
  6. package/dist/src/v2/containment.js.map +1 -1
  7. package/dist/src/v2/implementation-report.d.ts +5 -1
  8. package/dist/src/v2/implementation-report.d.ts.map +1 -1
  9. package/dist/src/v2/implementation-report.js +46 -1
  10. package/dist/src/v2/implementation-report.js.map +1 -1
  11. package/dist/src/v2/pending-effect-settlement.d.ts +2 -2
  12. package/dist/src/v2/pending-effect-settlement.d.ts.map +1 -1
  13. package/dist/src/v2/pending-effect-settlement.js +2 -2
  14. package/dist/src/v2/pending-effect-settlement.js.map +1 -1
  15. package/dist/src/v2/review-feedback-coordinator.d.ts +7 -1
  16. package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -1
  17. package/dist/src/v2/review-feedback-coordinator.js +84 -4
  18. package/dist/src/v2/review-feedback-coordinator.js.map +1 -1
  19. package/dist/src/v2/review-feedback.d.ts +14 -2
  20. package/dist/src/v2/review-feedback.d.ts.map +1 -1
  21. package/dist/src/v2/review-feedback.js +74 -6
  22. package/dist/src/v2/review-feedback.js.map +1 -1
  23. package/dist/src/v2/run-issue.d.ts +16 -7
  24. package/dist/src/v2/run-issue.d.ts.map +1 -1
  25. package/dist/src/v2/run-issue.js +606 -316
  26. package/dist/src/v2/run-issue.js.map +1 -1
  27. package/dist/src/v2/run-state-projections.d.ts +0 -13
  28. package/dist/src/v2/run-state-projections.d.ts.map +1 -1
  29. package/dist/src/v2/run-state-projections.js +0 -20
  30. package/dist/src/v2/run-state-projections.js.map +1 -1
  31. package/dist/src/v2/run-store.d.ts +51 -30
  32. package/dist/src/v2/run-store.d.ts.map +1 -1
  33. package/dist/src/v2/run-store.js +106 -34
  34. package/dist/src/v2/run-store.js.map +1 -1
  35. package/dist/src/v2/runtime.d.ts +10 -0
  36. package/dist/src/v2/runtime.d.ts.map +1 -1
  37. package/dist/src/v2/runtime.js +23 -14
  38. package/dist/src/v2/runtime.js.map +1 -1
  39. package/dist/src/v2/validation-progression.d.ts +1 -1
  40. package/dist/src/v2/validation-progression.d.ts.map +1 -1
  41. package/dist/src/v2/validation-progression.js.map +1 -1
  42. package/internal-workflow/manifest.json +1 -1
  43. package/internal-workflow/operations/implementation/SKILL.md +6 -1
  44. package/internal-workflow/schemas/implementation-report-v1.json +1 -1
  45. package/package.json +1 -1
@@ -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 { blockedLabelPolicy, blockedLabelProjection, claimComment, claimRunId, freezeCriteria, historicalClaimCounts, isClaimMarkerComment, isExactClaimMarkerLine, isAdoptableAttempt, observedHistoricalClaimKeys, outcomeEvidenceBytes, publicOutcome, publicIssueSnapshot, refreshClaimedIssueSnapshot, snapshotIssue, sameInspectionIdentity, sameStrings, semanticChangesMatch, } from './run-state-projections.js';
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 (sameStrings(observation.labels, expectedLabels))
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 sameStrings(observation.labels, expectedLabels) ? 'confirmed' : 'absent';
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.clearEffect(active), { status: 'cancelled' })
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.clearEffect(active), { status: 'cancelled' })
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.clearEffect(active), { status: 'cancelled' })
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
- const handoffMarker = `<!-- codex-orchestrator:run:${runId}:handoff -->`;
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', false, {
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 authorizationFailure;
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
- const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
504
- authorizationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-precommit-revalidation-failed');
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
- return authorizationFailure
519
- ?? this.persistRetainedCommitIntentTerminal(active, 'review-feedback-publication-authority-revoked');
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 authorizationFailure;
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
- const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
570
- authorizationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-precommit-revalidation-failed');
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
- return authorizationFailure
579
- ?? this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
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 authorizationFailure;
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
- const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
621
- authorizationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-prepush-revalidation-failed');
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
- return authorizationFailure
631
- ?? this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
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
- const validation = await coordinator.revalidate({ batch, epoch: 'post-push', expectedHeadSha: head });
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: () => this.authorized(active, config),
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
- const validation = await coordinator.revalidate({ batch, epoch: 'post-push', expectedHeadSha: head });
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 pullRequestUrl = `https://github.com/${active.record.canonicalRepository}/pull/${batch.pullRequest.number}`;
759
- return this.persistTerminal(active, {
760
- status: 'review-ready', pullRequestUrl, continuationEpoch: head,
761
- }, 'review-ready', false, {
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 sourceIds = observed.status === 'frozen'
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
- const active = await this.persist(starting, {
819
+ : observed.status === 'none' ? observed.eligibleSourceIds : [];
820
+ ready = await this.persist(starting, {
811
821
  reviewFeedback: initializeReviewFeedback(feedback, expectedHeadSha, sourceIds),
812
822
  });
813
- return { result: publicOutcome(active.record.terminalOutcome) };
823
+ if (!issueCommentBatch)
824
+ return { result: publicOutcome(ready.record.terminalOutcome) };
814
825
  }
815
- if (feedback.activeBatch || observed.status === 'none')
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, starting.record.directReview.targetRevision);
821
- const candidateTreeSha = await this.dependencies.git.getTreeSha(starting.record.worktreePath);
822
- const activation = projectValidationFeedbackActivation(starting.record, {
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
- const active = await this.persistValidationTransition(starting, activation);
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
- if (pendingEffect && (pendingEffect.kind !== 'review-activation-labels'
848
- || pendingEffect.issueNumber !== active.record.issueNumber
849
- || pendingEffect.batchId !== batch.batchId
850
- || !sameStrings(pendingEffect.expected, runningLabels))) {
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 (pendingEffect) {
868
- const settlement = await settleLabelsEffect(pendingEffect, {
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.persistTerminal(active, seed, active.record.pendingEffect.code, false);
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.pendingEffect?.kind === 'blocked-labels') {
1016
- return await this.publishBlockedTerminal(active, {
1017
- status: 'blocked',
1018
- kind: active.record.pendingEffect.blockKind,
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 && active.record.pendingEffect?.kind === 'review-activation-labels') {
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
- for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'reviewFeedback', 'changeBindingVersion', 'candidateBinding', 'candidateMaterialization', 'activeAttempt']) {
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
- const feedback = active.record.reviewFeedback;
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, replaceConfirmedEffect = false) {
2919
- if (!active.record.pendingEffect || replaceConfirmedEffect) {
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 terminal(active, outcome, evidenceCode = outcome.status, retainIntent = false) {
2951
- if (active.record.reviewFeedback?.activeBatch && outcome.status === 'blocked') {
2952
- return this.blockReviewFeedback(active, outcome.kind, evidenceCode, outcome);
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 (outcome.status === 'blocked') {
2955
- return this.publishBlockedTerminal(active, outcome, evidenceCode);
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
- return this.persistTerminal(active, outcome, evidenceCode, retainIntent);
2958
- }
2959
- async publishBlockedTerminal(starting, outcome, evidenceCode, additionalChanges = {}, replaceExistingIntent = false) {
2960
- let active = starting;
2961
- const body = blockedComment(active.record.runId, outcome, evidenceCode);
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
- bodySha256: sha256(body),
2968
- blockKind: outcome.kind,
2969
- resumable: outcome.resumable,
2970
- evidenceCode,
2971
- ...(outcome.blocker ? { blocker: outcome.blocker } : {}),
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 autoBlocked = [active.config.github.labels.auto.name, active.config.github.labels.blocked.name].sort();
2974
- const blockedOnly = [active.config.github.labels.blocked.name];
2975
- let expected = autoBlocked;
2976
- let existing = active.record.pendingEffect;
2977
- if (!existing || replaceExistingIntent) {
2978
- active = await this.persist(active, { ...additionalChanges, pendingEffect: commentIntent });
2979
- existing = active.record.pendingEffect;
2980
- }
2981
- if (existing?.kind === 'blocked-comment') {
2982
- if (existing.issueNumber !== commentIntent.issueNumber || existing.marker !== marker
2983
- || existing.bodySha256 !== sha256(body) || existing.blockKind !== outcome.kind
2984
- || existing.resumable !== outcome.resumable || existing.evidenceCode !== evidenceCode) {
2985
- return this.invokedFailure(active, 'blocked-comment-pendingEffect-diverged');
2986
- }
2987
- const settlement = await settleCommentEffect(existing, {
2988
- observe: async () => {
2989
- const observation = await this.readIssue(active.record.issueNumber);
2990
- if (!observation || observation.state !== 'OPEN')
2991
- return 'diverged';
2992
- const matching = observation ? commentsWithMarker(observation, marker) : [];
2993
- if (matching.length === 0)
2994
- return 'absent';
2995
- return matching.length === 1 && sha256(matching[0].body) === sha256(body)
2996
- ? 'confirmed' : 'diverged';
2997
- },
2998
- invoke: () => this.dependencies.issues.postComment(active.record.issueNumber, body),
2999
- });
3000
- if (settlement.status === 'unknown')
3001
- return this.invokedFailure(active, 'blocked-comment-delivery-unknown');
3002
- if (settlement.status !== 'confirmed')
3003
- return this.invokedFailure(active, 'blocked-comment-observation-diverged');
3004
- try {
3005
- active = await this.persist(active, { pendingEffect: {
3006
- kind: 'blocked-labels',
3007
- issueNumber: active.record.issueNumber,
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
- if (existing?.kind !== 'blocked-labels'
3021
- || existing.issueNumber !== active.record.issueNumber
3022
- || existing.blockKind !== outcome.kind
3023
- || existing.resumable !== outcome.resumable
3024
- || existing.evidenceCode !== evidenceCode) {
3025
- return this.invokedFailure(active, 'blocked-labels-pendingEffect-diverged');
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.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
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
- return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
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
- if (!issue)
3051
- return this.invokedFailure(active, 'blocked-labels-observation-missing');
3052
- if (issue.state === 'OPEN') {
3053
- if (projection.status !== 'settled') {
3054
- return this.invokedFailure(active, 'blocked-labels-observation-diverged');
3055
- }
3056
- if (!sameStrings(expected, projection.expected)) {
3057
- expected = projection.expected;
3058
- const pendingEffect = active.record.pendingEffect;
3059
- if (pendingEffect?.kind !== 'blocked-labels')
3060
- return this.invokedFailure(active, 'blocked-labels-pendingEffect-diverged');
3061
- try {
3062
- const { effectId: _effectId, ...intent } = pendingEffect;
3063
- active = await this.persist(active, { pendingEffect: { ...intent, expected } });
3064
- }
3065
- catch {
3066
- throw new PostEffectStateError(active);
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
- return await this.persistTerminal(active, outcome, evidenceCode, false, {}, true);
3119
+ active = await this.clearAttempt(active);
3072
3120
  }
3073
3121
  catch {
3074
- throw new PostEffectStateError(active);
3122
+ return publicOutcome(terminalOutcome);
3075
3123
  }
3124
+ return publicOutcome(active.record.terminalOutcome);
3076
3125
  }
3077
- async reconcilePersistedBlockedTerminal(active, issue, outcome) {
3078
- if (!this.dependencies.issues.transitionToBlocked) {
3079
- return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
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.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
3151
+ active = await this.settleTerminalLabels(active);
3083
3152
  }
3084
3153
  catch {
3085
- return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
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 observation = await this.readIssue(active.record.issueNumber);
3088
- if (!observation || observation.state !== 'OPEN'
3089
- || blockedLabelProjection(observation.labels, active.config).status !== 'settled') {
3090
- return this.invokedFailure(active, 'blocked-labels-observation-diverged');
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
- return publicOutcome(outcome);
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, retainIntent, additionalChanges = {}, replaceConfirmedEffect = false) {
3095
- const evidence = await this.observeOutcomeEvidenceEffect(active, evidenceCode, canonicalJson(outcome), replaceConfirmedEffect);
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
- if (!retainIntent)
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 blockedComment(runId, outcome, evidenceCode) {
3289
- const lines = [
3290
- `<!-- codex-orchestrator:run:${runId}:blocked -->`,
3291
- 'codex-orchestrator blocked this run.',
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
  }