codex-orchestrator 2.0.14 → 2.0.16

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 (51) hide show
  1. package/CHANGELOG.md +25 -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 +61 -6
  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 -8
  24. package/dist/src/v2/run-issue.d.ts.map +1 -1
  25. package/dist/src/v2/run-issue.js +609 -327
  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 -1
  36. package/dist/src/v2/runtime.d.ts.map +1 -1
  37. package/dist/src/v2/runtime.js +23 -15
  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/internal-workflow/skills/plan/SKILL.md +2 -2
  46. package/internal-workflow/skills/plan/evals/evals.json +1 -1
  47. package/internal-workflow/skills/to-tickets/SKILL.md +37 -14
  48. package/internal-workflow/skills/to-tickets/agents/openai.yaml +1 -1
  49. package/internal-workflow/skills/to-tickets/evals/evals.json +38 -2
  50. package/internal-workflow/skills/to-tickets/references/publishing-details.md +14 -2
  51. 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;
@@ -1251,8 +1269,6 @@ export class RunIssue {
1251
1269
  },
1252
1270
  };
1253
1271
  const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
1254
- const reportOnlyRecovery = active.record.reportRepairs > 0;
1255
- const reportOnlyBaseline = reportOnlyRecovery ? await this.dependencies.git.snapshot(worktreePath) : null;
1256
1272
  if (!active.record.activeAttempt) {
1257
1273
  active = await this.prepareAttempt(active, 'implementation', feedbackBatch
1258
1274
  ? `${feedbackBatch.batchId}:${active.record.reviewFeedback.repairRound}`
@@ -1272,10 +1288,7 @@ export class RunIssue {
1272
1288
  frozenCriteria,
1273
1289
  deliveryAuthority: active.record.deliveryAuthority,
1274
1290
  cycle: active.record.cycle,
1275
- reworkFindings: reportOnlyRecovery
1276
- ? ['Emit a schema-valid implementation report for the existing exact product result.']
1277
- : active.record.reworkFindings,
1278
- repairOnly: reportOnlyRecovery,
1291
+ reworkFindings: active.record.reworkFindings,
1279
1292
  workflowGeneration: active.record.workflowGeneration,
1280
1293
  signal: this.signal,
1281
1294
  ...implementationLaunch,
@@ -1304,9 +1317,6 @@ export class RunIssue {
1304
1317
  }
1305
1318
  if (implementation.kind !== 'completed')
1306
1319
  return await this.mapImplementationFailure(active, implementation);
1307
- if (reportOnlyBaseline && !sameFreshness(reportOnlyBaseline, await this.dependencies.git.snapshot(worktreePath))) {
1308
- return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
1309
- }
1310
1320
  let report;
1311
1321
  try {
1312
1322
  report = validateImplementationReport(implementation.report);
@@ -1319,7 +1329,7 @@ export class RunIssue {
1319
1329
  else {
1320
1330
  active = await this.persist(active, { reportRepairs: active.record.reportRepairs + 1 });
1321
1331
  }
1322
- return this.invokedFailure(active, 'implementation-report-retryable', 'The implementation report was malformed; a later bounded invocation will retry report-only.');
1332
+ return this.invokedFailure(active, 'implementation-report-retryable', 'The implementation report was malformed; a later bounded invocation will retry implementation.');
1323
1333
  }
1324
1334
  if (report.status === 'external-block') {
1325
1335
  if (report.blocker.resumable) {
@@ -1333,6 +1343,9 @@ export class RunIssue {
1333
1343
  }
1334
1344
  return await this.terminal(active, implementationBlockerOutcome(report.blocker));
1335
1345
  }
1346
+ if (report.status === 'answer-only' || report.status === 'boundary') {
1347
+ return this.completeIssueFeedbackResponse(active, report, config);
1348
+ }
1336
1349
  if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
1337
1350
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'implementation-head-drift');
1338
1351
  }
@@ -1348,12 +1361,13 @@ export class RunIssue {
1348
1361
  else {
1349
1362
  active = await this.persist(active, { reportRepairs: active.record.reportRepairs + 1 });
1350
1363
  }
1351
- return this.invokedFailure(active, 'implementation-change-set-report-retryable', `The implementation report must describe the current exact change set ${canonicalJson(changedFiles)}; a later bounded invocation will retry report-only.`);
1364
+ return this.invokedFailure(active, 'implementation-change-set-report-retryable', `The implementation report must describe the current exact change set ${canonicalJson(changedFiles)}; a later bounded invocation will retry implementation.`);
1352
1365
  }
1353
1366
  if (isAdoptableAttempt(active.record.activeAttempt)) {
1354
1367
  active = await this.adoptAttempt(active, sha256(canonicalJson(report)), {
1355
1368
  transportRetries: active.record.transportRetries,
1356
1369
  reportRepairs: 0,
1370
+ implementationResult: { summary: report.summary, residualRisks: report.residualRisks },
1357
1371
  });
1358
1372
  active = await this.clearAttempt(active);
1359
1373
  }
@@ -1886,7 +1900,10 @@ export class RunIssue {
1886
1900
  const record = { ...active.record, ...normalized, updatedAt: this.timestamp() };
1887
1901
  if (Object.hasOwn(changes, 'pendingEffect') && changes.pendingEffect === undefined)
1888
1902
  delete record.pendingEffect;
1889
- for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'reviewFeedback', 'changeBindingVersion', 'candidateBinding', 'candidateMaterialization', 'activeAttempt']) {
1903
+ if (Object.hasOwn(changes, 'terminalOutcome') && changes.terminalOutcome === undefined
1904
+ && !Object.hasOwn(changes, 'terminalNotifications'))
1905
+ delete record.terminalNotifications;
1906
+ for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'reviewFeedback', 'changeBindingVersion', 'candidateBinding', 'candidateMaterialization', 'activeAttempt', 'terminalNotifications']) {
1890
1907
  if (Object.hasOwn(changes, key) && changes[key] === undefined)
1891
1908
  delete record[key];
1892
1909
  }
@@ -1980,7 +1997,7 @@ export class RunIssue {
1980
1997
  }
1981
1998
  async authorizedForExactLabels(active, expectedLabels) {
1982
1999
  const issue = await this.readIssue(active.record.issueNumber);
1983
- return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
2000
+ return !!issue && issue.state === 'OPEN' && sameStrings(managedLabelProjection(issue.labels, active.config), expectedLabels)
1984
2001
  && this.hasTrustedClaim(issue, active.record);
1985
2002
  }
1986
2003
  hasTrustedClaim(issue, record) {
@@ -2036,6 +2053,7 @@ export class RunIssue {
2036
2053
  }
2037
2054
  const validation = await this.dependencies.reviewFeedback.revalidate({
2038
2055
  batch,
2056
+ issueNumber,
2039
2057
  epoch: 'pre-update',
2040
2058
  expectedHeadSha: batch.priorPublishedHeadSha,
2041
2059
  });
@@ -2863,12 +2881,7 @@ export class RunIssue {
2863
2881
  return transitioned;
2864
2882
  }
2865
2883
  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);
2884
+ return this.terminal(active, outcome, evidenceCode);
2872
2885
  }
2873
2886
  async mapImplementationFailure(active, result) {
2874
2887
  if (result.kind === 'transport-failed')
@@ -2915,8 +2928,8 @@ export class RunIssue {
2915
2928
  });
2916
2929
  return { status: 'repair-ready', source, blockerIds: [...new Set(blockerIds)].sort(), evidencePath: evidence.path };
2917
2930
  }
2918
- async observeOutcomeEvidenceEffect(active, code, summary, replaceConfirmedEffect = false) {
2919
- if (!active.record.pendingEffect || replaceConfirmedEffect) {
2931
+ async observeOutcomeEvidenceEffect(active, code, summary) {
2932
+ if (!active.record.pendingEffect) {
2920
2933
  const recordedAt = this.timestamp();
2921
2934
  const path = this.dependencies.outcomeEvidencePath(active.record.runId, code, sha256(summary));
2922
2935
  const bytes = outcomeEvidenceBytes({ runId: active.record.runId, code, summary, recordedAt });
@@ -2947,159 +2960,311 @@ export class RunIssue {
2947
2960
  throw new PostEffectStateError(active);
2948
2961
  return { active, path: effect.path, id: `evidence:${effect.runId}:${effect.code}` };
2949
2962
  }
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);
2963
+ async completeIssueFeedbackResponse(starting, report, config) {
2964
+ let active = starting;
2965
+ const feedback = active.record.reviewFeedback;
2966
+ const batch = feedback?.activeBatch;
2967
+ if (!feedback || !batch || batch.sources.some((source) => source.kind !== 'issue-comment')) {
2968
+ return this.terminal(active, { status: 'internal-error', code: 'issue-feedback-response-source-invalid' });
2953
2969
  }
2954
- if (outcome.status === 'blocked') {
2955
- return this.publishBlockedTerminal(active, outcome, evidenceCode);
2970
+ if (await this.dependencies.git.getHead(active.record.worktreePath) !== batch.priorPublishedHeadSha
2971
+ || (await this.dependencies.git.listChangedFilesIgnoringUntrackedRoot(active.record.worktreePath, config.proof.artifactDir)).length !== 0) {
2972
+ return this.terminal(active, { status: 'internal-error', code: 'issue-feedback-response-mutated-worktree' });
2956
2973
  }
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,
2974
+ const marker = `<!-- codex-orchestrator:run:${active.record.runId}:issue-feedback:${batch.batchId} -->`;
2975
+ const response = publicIssueFeedbackText(report.response, report.status === 'boundary'
2976
+ ? 'This request cannot be acted on without a new issue-local decision or authority.'
2977
+ : 'The requested answer could not be published safely.');
2978
+ const body = [
2966
2979
  marker,
2967
- bodySha256: sha256(body),
2968
- blockKind: outcome.kind,
2969
- resumable: outcome.resumable,
2970
- evidenceCode,
2971
- ...(outcome.blocker ? { blocker: outcome.blocker } : {}),
2980
+ report.status === 'boundary' ? 'codex-orchestrator reached an authority boundary.' : 'codex-orchestrator answered this follow-up.',
2981
+ '',
2982
+ response,
2983
+ ...(report.status === 'boundary' ? ['', `Boundary: ${report.boundary.kind}`] : []),
2984
+ ].join('\n');
2985
+ const directReview = active.record.directReview;
2986
+ const sourceIds = batch.sources.map((source) => source.sourceId);
2987
+ const restoredDirectReview = {
2988
+ ...structuredClone(directReview),
2989
+ status: 'clear',
2990
+ stage: 'review',
2991
+ review: { ...structuredClone(directReview.review), disposition: 'clear' },
2992
+ repairFindings: directReview.repairFindings.filter((finding) => !sourceIds.includes(finding.sourceId)),
2972
2993
  };
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;
2994
+ const pullRequest = await this.dependencies.pullRequests.findOpen({
2995
+ headBranch: active.record.branchName,
2996
+ baseBranch: config.github.baseBranch,
2997
+ });
2998
+ if (!pullRequest?.url || pullRequest.number !== batch.pullRequest.number
2999
+ || pullRequest.nodeId !== batch.pullRequest.nodeId || pullRequest.headSha !== batch.pullRequest.headSha
3000
+ || pullRequest.headRefName !== batch.pullRequest.headRefName
3001
+ || pullRequest.baseRefName !== batch.pullRequest.baseRefName) {
3002
+ return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'issue-feedback-prior-handoff-identity-mismatch', { reviewFeedback: blockReviewFeedback({
3003
+ ...feedback,
3004
+ consumedSourceIds: feedback.consumedSourceIds.filter((id) => !sourceIds.includes(id)),
3005
+ }, 'safety', this.timestamp()) });
3006
+ }
3007
+ const terminalSeed = {
3008
+ status: 'review-ready',
3009
+ pullRequestUrl: pullRequest.url,
3010
+ continuationEpoch: batch.priorPublishedHeadSha,
3011
+ };
3012
+ const terminalOutcome = {
3013
+ ...terminalSeed,
3014
+ evidencePath: this.dependencies.outcomeEvidencePath(active.record.runId, 'review-ready', sha256(canonicalJson(terminalSeed))),
3015
+ };
3016
+ const respondedAt = this.timestamp();
3017
+ const initialReceipt = {
3018
+ kind: 'responded',
3019
+ batchId: batch.batchId,
3020
+ sourceIds,
3021
+ responseKind: report.status,
3022
+ publication: 'failed',
3023
+ diagnostic: 'issue-feedback-response-publication-not-settled',
3024
+ respondedAt,
3025
+ };
3026
+ let comments = active.record.issueSnapshot.comments ?? [];
3027
+ try {
3028
+ comments = (await this.readIssue(active.record.issueNumber))?.comments ?? comments;
3019
3029
  }
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');
3030
+ catch { /* bounded fallback */ }
3031
+ const freshNotifications = terminalNotificationState(active.record, terminalSeed, this.timestamp(), comments);
3032
+ const consumedChanges = {
3033
+ lifecycle: 'review-ready',
3034
+ terminalOutcome,
3035
+ outcomeEvidenceId: `evidence:${active.record.runId}:review-ready`,
3036
+ reviewFeedback: respondReviewFeedback(feedback, initialReceipt),
3037
+ directReview: restoredDirectReview,
3038
+ reworkFindings: [],
3039
+ reportRepairs: 0,
3040
+ transportRetries: 0,
3041
+ terminalNotifications: {
3042
+ ...freshNotifications,
3043
+ comment: { status: 'delivered', attempts: 0 },
3044
+ labels: { status: 'delivered', attempts: 0 },
3045
+ },
3046
+ };
3047
+ active = isAdoptableAttempt(active.record.activeAttempt)
3048
+ ? await this.adoptAttempt(active, sha256(canonicalJson(report)), consumedChanges)
3049
+ : await this.persist(active, consumedChanges);
3050
+ let publication = 'failed';
3051
+ let commentId;
3052
+ let diagnostic;
3053
+ const validation = this.dependencies.reviewFeedback
3054
+ ? await this.dependencies.reviewFeedback.revalidate({
3055
+ batch, issueNumber: active.record.issueNumber, epoch: 'pre-update', expectedHeadSha: batch.priorPublishedHeadSha,
3056
+ })
3057
+ : { status: 'blocked', reason: 'review feedback revalidation unavailable' };
3058
+ if (validation.status !== 'valid') {
3059
+ publication = 'suppressed';
3060
+ diagnostic = validation.status === 'retryable'
3061
+ ? 'issue-feedback-response-revalidation-retryable'
3062
+ : 'issue-feedback-response-revalidation-blocked';
3026
3063
  }
3027
3064
  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
3065
  try {
3042
- await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
3066
+ let issue = await this.readIssue(active.record.issueNumber);
3067
+ let matches = issue ? commentsWithMarker(issue, marker) : [];
3068
+ if (matches.length === 0) {
3069
+ await this.dependencies.issues.postComment(active.record.issueNumber, body);
3070
+ issue = await this.readIssue(active.record.issueNumber);
3071
+ matches = issue ? commentsWithMarker(issue, marker) : [];
3072
+ }
3073
+ const match = matches.length === 1 && sha256(matches[0].body) === sha256(body) ? matches[0] : undefined;
3074
+ if (match) {
3075
+ publication = 'delivered';
3076
+ commentId = match.id ?? `marker:${batch.batchId}`;
3077
+ }
3078
+ else {
3079
+ diagnostic = 'issue-feedback-response-observation-diverged';
3080
+ }
3043
3081
  }
3044
3082
  catch {
3045
- return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
3083
+ diagnostic = 'issue-feedback-response-delivery-failed';
3046
3084
  }
3047
- issue = await this.readIssue(active.record.issueNumber);
3048
- projection = issue ? blockedLabelProjection(issue.labels, active.config) : { status: 'diverged' };
3049
3085
  }
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
- }
3086
+ try {
3087
+ await this.dependencies.issues.setLabels(active.record.issueNumber, [config.github.labels.review.name]);
3088
+ }
3089
+ catch {
3090
+ diagnostic = diagnostic ?? 'issue-feedback-review-label-reconciliation-failed';
3069
3091
  }
3092
+ const receipt = {
3093
+ kind: 'responded',
3094
+ batchId: batch.batchId,
3095
+ sourceIds,
3096
+ responseKind: report.status,
3097
+ publication,
3098
+ ...(commentId ? { commentId } : {}),
3099
+ ...(diagnostic ? { diagnostic } : {}),
3100
+ respondedAt,
3101
+ };
3070
3102
  try {
3071
- return await this.persistTerminal(active, outcome, evidenceCode, false, {}, true);
3103
+ active = await this.persist(active, {
3104
+ reviewFeedback: settleReviewFeedbackResponse(active.record.reviewFeedback, receipt),
3105
+ });
3072
3106
  }
3073
3107
  catch {
3074
- throw new PostEffectStateError(active);
3108
+ return publicOutcome(terminalOutcome);
3109
+ }
3110
+ try {
3111
+ active = await this.clearAttempt(active);
3075
3112
  }
3113
+ catch {
3114
+ return publicOutcome(terminalOutcome);
3115
+ }
3116
+ return publicOutcome(active.record.terminalOutcome);
3076
3117
  }
3077
- async reconcilePersistedBlockedTerminal(active, issue, outcome) {
3078
- if (!this.dependencies.issues.transitionToBlocked) {
3079
- return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
3118
+ async terminal(active, outcome, evidenceCode = outcome.status, additionalChanges = {}) {
3119
+ if (active.record.pendingEffect && active.record.pendingEffect.kind !== 'outcome-evidence') {
3120
+ return this.invokedFailure(active, 'terminal-pending-effect-unsettled', 'Terminal state requires the existing effect postcondition to settle first.');
3080
3121
  }
3122
+ const feedback = active.record.reviewFeedback;
3123
+ const terminalChanges = feedback?.activeBatch && !additionalChanges.reviewFeedback ? {
3124
+ ...additionalChanges,
3125
+ reviewFeedback: blockReviewFeedback(feedback, outcome.status === 'blocked' ? outcome.kind : 'safety', this.timestamp()),
3126
+ } : additionalChanges;
3127
+ return this.persistTerminal(active, outcome, evidenceCode, terminalChanges);
3128
+ }
3129
+ async settleTerminalNotificationsBestEffort(starting) {
3130
+ let active = starting;
3131
+ if (!active.record.terminalOutcome || !active.record.terminalNotifications)
3132
+ return active;
3133
+ if (active.record.pendingEffect && active.record.pendingEffect.kind !== 'terminal-comment'
3134
+ && active.record.pendingEffect.kind !== 'terminal-labels')
3135
+ return active;
3081
3136
  try {
3082
- await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
3137
+ active = await this.settleTerminalComment(active);
3083
3138
  }
3084
3139
  catch {
3085
- return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
3140
+ return active;
3141
+ }
3142
+ try {
3143
+ active = await this.settleTerminalLabels(active);
3086
3144
  }
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');
3145
+ catch {
3146
+ return active;
3091
3147
  }
3092
- return publicOutcome(outcome);
3148
+ return active;
3149
+ }
3150
+ async settleTerminalComment(starting) {
3151
+ let active = starting;
3152
+ const notifications = active.record.terminalNotifications;
3153
+ if (notifications.comment.status !== 'pending' || notifications.comment.attempts >= TERMINAL_NOTIFICATION_ATTEMPTS)
3154
+ return active;
3155
+ const body = terminalComment(active.record.runId, active.record.cycle, notifications.report);
3156
+ const marker = body.split('\n')[0];
3157
+ const attempt = notifications.comment.attempts + 1;
3158
+ if (!active.record.pendingEffect) {
3159
+ active = await this.persist(active, { pendingEffect: {
3160
+ kind: 'terminal-comment', issueNumber: active.record.issueNumber, marker, bodySha256: sha256(body),
3161
+ outcome: notifications.report.outcome, attempt,
3162
+ } });
3163
+ }
3164
+ const effect = active.record.pendingEffect;
3165
+ if (effect?.kind !== 'terminal-comment' || effect.issueNumber !== active.record.issueNumber
3166
+ || effect.marker !== marker || effect.bodySha256 !== sha256(body)
3167
+ || effect.outcome !== notifications.report.outcome || effect.attempt !== attempt)
3168
+ return active;
3169
+ const settlement = await settleCommentEffect(effect, {
3170
+ observe: async () => {
3171
+ const issue = await this.readIssue(effect.issueNumber);
3172
+ if (!issue || issue.state !== 'OPEN')
3173
+ return 'diverged';
3174
+ const matching = commentsWithMarker(issue, marker);
3175
+ if (matching.length === 0)
3176
+ return 'absent';
3177
+ return matching.length === 1 && sha256(matching[0].body) === effect.bodySha256 ? 'confirmed' : 'diverged';
3178
+ },
3179
+ invoke: () => this.dependencies.issues.postComment(effect.issueNumber, body),
3180
+ });
3181
+ const delivered = settlement.status === 'confirmed';
3182
+ const status = delivered ? 'delivered'
3183
+ : attempt >= TERMINAL_NOTIFICATION_ATTEMPTS ? 'exhausted' : 'pending';
3184
+ const diagnostic = delivered ? {} : { diagnostic: terminalNotificationDiagnostic('comment', settlement.status) };
3185
+ return this.persist(active, {
3186
+ terminalNotifications: {
3187
+ ...notifications,
3188
+ comment: { status, attempts: attempt, ...diagnostic },
3189
+ },
3190
+ pendingEffect: undefined,
3191
+ });
3093
3192
  }
3094
- async persistTerminal(active, outcome, evidenceCode, retainIntent, additionalChanges = {}, replaceConfirmedEffect = false) {
3095
- const evidence = await this.observeOutcomeEvidenceEffect(active, evidenceCode, canonicalJson(outcome), replaceConfirmedEffect);
3193
+ async settleTerminalLabels(starting) {
3194
+ let active = starting;
3195
+ let notifications = active.record.terminalNotifications;
3196
+ const policy = terminalLabelChanges(active.config, notifications.report.outcome);
3197
+ if (notifications.labels.status === 'delivered') {
3198
+ const issue = await this.readIssue(active.record.issueNumber);
3199
+ const settled = issue && policy.add.every((label) => issue.labels.includes(label))
3200
+ && policy.remove.every((label) => !issue.labels.includes(label));
3201
+ if (settled)
3202
+ return active;
3203
+ notifications = { ...notifications, labels: { status: 'pending', attempts: 0 } };
3204
+ active = await this.persist(active, { terminalNotifications: notifications });
3205
+ }
3206
+ if (notifications.labels.status !== 'pending' || notifications.labels.attempts >= TERMINAL_NOTIFICATION_ATTEMPTS)
3207
+ return active;
3208
+ const attempt = notifications.labels.attempts + 1;
3209
+ if (!active.record.pendingEffect) {
3210
+ active = await this.persist(active, { pendingEffect: {
3211
+ kind: 'terminal-labels', issueNumber: active.record.issueNumber, ...policy,
3212
+ outcome: notifications.report.outcome, attempt,
3213
+ } });
3214
+ }
3215
+ const effect = active.record.pendingEffect;
3216
+ if (effect?.kind !== 'terminal-labels' || effect.issueNumber !== active.record.issueNumber
3217
+ || effect.outcome !== notifications.report.outcome || effect.attempt !== attempt
3218
+ || !sameStrings(effect.add, policy.add) || !sameStrings(effect.remove, policy.remove))
3219
+ return active;
3220
+ const settlement = await settleLabelsEffect(effect, {
3221
+ observe: async () => {
3222
+ const issue = await this.readIssue(effect.issueNumber);
3223
+ if (!issue)
3224
+ return 'diverged';
3225
+ return effect.add.every((label) => issue.labels.includes(label))
3226
+ && effect.remove.every((label) => !issue.labels.includes(label)) ? 'confirmed' : 'absent';
3227
+ },
3228
+ invoke: async () => {
3229
+ if (!this.dependencies.issues.reconcileTerminalLabels)
3230
+ throw new Error('terminal label reconciliation unavailable');
3231
+ await this.dependencies.issues.reconcileTerminalLabels(effect.issueNumber, {
3232
+ outcome: effect.outcome, add: effect.add, remove: effect.remove,
3233
+ });
3234
+ },
3235
+ });
3236
+ const delivered = settlement.status === 'confirmed';
3237
+ const status = delivered ? 'delivered'
3238
+ : attempt >= TERMINAL_NOTIFICATION_ATTEMPTS ? 'exhausted' : 'pending';
3239
+ const diagnostic = delivered ? {} : { diagnostic: terminalNotificationDiagnostic('labels', settlement.status) };
3240
+ return this.persist(active, {
3241
+ terminalNotifications: {
3242
+ ...notifications,
3243
+ labels: { status, attempts: attempt, ...diagnostic },
3244
+ },
3245
+ pendingEffect: undefined,
3246
+ });
3247
+ }
3248
+ async persistTerminal(active, outcome, evidenceCode, additionalChanges = {}) {
3249
+ const evidence = await this.observeOutcomeEvidenceEffect(active, evidenceCode, canonicalJson(outcome));
3096
3250
  active = evidence.active;
3097
3251
  const terminalOutcome = { ...outcome, evidencePath: evidence.path };
3252
+ let terminalComments = active.record.issueSnapshot.comments ?? [];
3253
+ if (outcome.status !== 'transport-failed') {
3254
+ try {
3255
+ terminalComments = (await this.readIssue(active.record.issueNumber))?.comments ?? terminalComments;
3256
+ }
3257
+ catch {
3258
+ // Terminal notification observation is best-effort and cannot change the Run outcome.
3259
+ }
3260
+ }
3261
+ const notifications = terminalNotificationState(active.record, outcome, this.timestamp(), terminalComments);
3098
3262
  const changes = {
3099
3263
  ...additionalChanges,
3100
3264
  lifecycle: outcome.status,
3101
3265
  terminalOutcome,
3102
3266
  outcomeEvidenceId: evidence.id,
3267
+ ...(notifications ? { terminalNotifications: notifications } : {}),
3103
3268
  };
3104
3269
  if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
3105
3270
  changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
@@ -3107,8 +3272,13 @@ export class RunIssue {
3107
3272
  : { status: outcome.status }, outcome.status === 'internal-error' ? outcome.code : undefined);
3108
3273
  }
3109
3274
  active = await this.persist(active, changes);
3110
- if (!retainIntent)
3275
+ try {
3111
3276
  active = await this.confirmEffect(active);
3277
+ }
3278
+ catch {
3279
+ return publicOutcome(terminalOutcome);
3280
+ }
3281
+ active = await this.settleTerminalNotificationsBestEffort(active);
3112
3282
  return publicOutcome(terminalOutcome);
3113
3283
  }
3114
3284
  async preClaimInternal(code, issueNumber) {
@@ -3146,6 +3316,142 @@ class PostEffectStateError extends Error {
3146
3316
  this.active = active;
3147
3317
  }
3148
3318
  }
3319
+ const TERMINAL_NOTIFICATION_ATTEMPTS = 3;
3320
+ function terminalNotificationState(record, outcome, observedAt, comments) {
3321
+ if (outcome.status === 'transport-failed')
3322
+ return undefined;
3323
+ const commentId = issueCommentHighWaterMark(comments);
3324
+ return {
3325
+ version: 1,
3326
+ commentCutoff: { commentId, observedAt },
3327
+ report: terminalReportSnapshot(record, outcome),
3328
+ comment: { status: 'pending', attempts: 0 },
3329
+ labels: { status: 'pending', attempts: 0 },
3330
+ };
3331
+ }
3332
+ function terminalReportSnapshot(record, outcome) {
3333
+ const summary = outcome.status === 'blocked'
3334
+ ? outcome.blocker?.summary ?? `The run stopped at ${outcome.kind}.`
3335
+ : outcome.status === 'internal-error'
3336
+ ? internalErrorSummary(outcome.code)
3337
+ : outcome.status === 'cancelled'
3338
+ ? 'The run was cancelled before completion.'
3339
+ : record.implementationResult?.summary ?? record.proofReceipt?.summary ?? 'The authorized implementation is ready for review.';
3340
+ const nextAction = outcome.status === 'review-ready'
3341
+ ? 'Review the draft PR. To request an in-scope follow-up, comment on this issue.'
3342
+ : outcome.status === 'blocked'
3343
+ ? outcome.resumable
3344
+ ? 'Resolve the stated blocker, then rerun the same issue.'
3345
+ : 'Provide the missing decision or authority in this issue before continuing.'
3346
+ : outcome.status === 'internal-error'
3347
+ ? 'Inspect the package-owned evidence for the code above, correct the runner condition, and retry the same issue.'
3348
+ : 'Re-add the authorization label and rerun the same issue if the work should continue.';
3349
+ return {
3350
+ version: 1,
3351
+ outcome: outcome.status,
3352
+ summary: publicTerminalText(summary, 'Terminal summary is unavailable.'),
3353
+ ...(outcome.status === 'review-ready' ? { pullRequestUrl: outcome.pullRequestUrl } : {}),
3354
+ passedChecks: record.checks.filter((check) => check.status === 'passed').slice(0, 16)
3355
+ .map((check) => publicTerminalText(check.id, 'Unnamed check.', 200)),
3356
+ publishableProof: (record.proofReceipt?.publishableEvidence ?? []).slice(0, 4).map((proof) => publicTerminalText(`${proof.ref}: ${proof.description}`, 'Publishable proof detail is unavailable.', 500)),
3357
+ unverified: record.proofReceipt ? [] : ['No publishable Acceptance Proof receipt was retained.'],
3358
+ risks: (record.implementationResult?.residualRisks ?? []).slice(0, 4)
3359
+ .map((risk) => publicTerminalText(risk, 'Risk detail is unavailable.', 500)),
3360
+ reviewFocus: (record.directReview?.review.coverage ?? []).slice(0, 8)
3361
+ .map((focus) => publicTerminalText(focus, 'Review focus is unavailable.', 250)),
3362
+ nextAction,
3363
+ ...(outcome.status === 'blocked' ? { blocker: {
3364
+ kind: outcome.kind,
3365
+ resumable: outcome.resumable,
3366
+ attempted: (outcome.blocker?.attempted ?? []).slice(0, 8)
3367
+ .map((attempt) => publicTerminalText(attempt, 'Attempt detail is unavailable.', 500)),
3368
+ } } : {}),
3369
+ };
3370
+ }
3371
+ function internalErrorSummary(code) {
3372
+ return code === 'implementation-change-set-invalid'
3373
+ ? 'The implementation report declared changed files, but the runner found no matching worktree changes.'
3374
+ : `The runner encountered an internal failure (${code}) and could not complete this run.`;
3375
+ }
3376
+ function terminalComment(runId, cycle, report) {
3377
+ const markerKind = report.outcome === 'review-ready' ? 'handoff' : report.outcome;
3378
+ const marker = `<!-- codex-orchestrator:run:${runId}:cycle:${cycle}:${markerKind} -->`;
3379
+ if (report.outcome === 'review-ready') {
3380
+ return [
3381
+ marker,
3382
+ 'codex-orchestrator completed this run.',
3383
+ '',
3384
+ '## Summary', report.summary,
3385
+ '',
3386
+ '## Pull request', report.pullRequestUrl ?? 'Draft PR URL is unavailable.',
3387
+ '',
3388
+ '## Passed checks', ...terminalList(report.passedChecks),
3389
+ '',
3390
+ '## Publishable proof', ...terminalList(report.publishableProof),
3391
+ '',
3392
+ '## Not verified', ...terminalList(report.unverified),
3393
+ '',
3394
+ '## Known risks', ...terminalList(report.risks),
3395
+ '',
3396
+ '## Review focus', ...terminalList(report.reviewFocus),
3397
+ '',
3398
+ '## Follow-up', report.nextAction,
3399
+ ].join('\n');
3400
+ }
3401
+ const title = report.outcome === 'blocked' ? 'blocked' : report.outcome === 'internal-error' ? 'stopped after an internal error' : 'was cancelled';
3402
+ return [
3403
+ marker,
3404
+ `codex-orchestrator ${title} this run.`,
3405
+ '',
3406
+ ...(report.blocker ? [`Kind: ${report.blocker.kind}`, `Resumable: ${report.blocker.resumable ? 'yes' : 'no'}`, ''] : []),
3407
+ '## Reason', `Reason: ${report.summary}`,
3408
+ ...(report.blocker ? ['', 'Attempted:', ...terminalList(report.blocker.attempted)] : []),
3409
+ '',
3410
+ '## Retained work and state',
3411
+ ...terminalList([
3412
+ ...report.passedChecks.map((check) => `Passed check: ${check}`),
3413
+ ...report.publishableProof.map((proof) => `Proof: ${proof}`),
3414
+ ]),
3415
+ '',
3416
+ '## Next action', report.nextAction,
3417
+ ].join('\n');
3418
+ }
3419
+ function terminalList(values) {
3420
+ return values.length > 0 ? values.map((value) => `- ${value}`) : ['- None recorded.'];
3421
+ }
3422
+ function terminalNotificationDiagnostic(kind, status) {
3423
+ return status === 'unknown'
3424
+ ? `terminal-${kind}-delivery-unknown`
3425
+ : `terminal-${kind}-observation-${status}`;
3426
+ }
3427
+ function terminalLabelChanges(config, outcome) {
3428
+ const labels = config.github.labels;
3429
+ if (outcome === 'review-ready')
3430
+ return {
3431
+ add: [labels.review.name], remove: [labels.auto.name, labels.running.name, labels.blocked.name].sort(),
3432
+ };
3433
+ if (outcome === 'blocked')
3434
+ return { add: [labels.blocked.name], remove: [labels.running.name, labels.review.name].sort() };
3435
+ return {
3436
+ add: [],
3437
+ remove: [labels.running.name, labels.blocked.name, labels.review.name, ...(outcome === 'cancelled' ? [labels.auto.name] : [])].sort(),
3438
+ };
3439
+ }
3440
+ function claimLabelProjectionSettled(labels, config) {
3441
+ const policy = config.github.labels;
3442
+ return labels.includes(policy.auto.name) && labels.includes(policy.running.name)
3443
+ && !labels.includes(policy.blocked.name) && !labels.includes(policy.review.name);
3444
+ }
3445
+ function issueCommentHighWaterMark(comments) {
3446
+ const ids = comments.map((comment) => comment.id).filter((id) => Boolean(id));
3447
+ const decimal = ids.filter((id) => /^\d+$/u.test(id));
3448
+ if (decimal.length > 0)
3449
+ return decimal.reduce((highest, id) => BigInt(id) > BigInt(highest) ? id : highest);
3450
+ return ids.at(-1) ?? null;
3451
+ }
3452
+ function publicTerminalText(value, fallback, maxLength = 2_000) {
3453
+ return publicBlockedText(value, fallback, maxLength);
3454
+ }
3149
3455
  function lifecycleForAttempt(operationId) {
3150
3456
  if (operationId === 'configured-check')
3151
3457
  return 'checking';
@@ -3285,39 +3591,10 @@ function pendingCandidateBoundary(record) {
3285
3591
  function commentsWithMarker(issue, marker) {
3286
3592
  return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
3287
3593
  }
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');
3594
+ function managedLabelProjection(labels, config) {
3595
+ const policy = config.github.labels;
3596
+ const managed = new Set([policy.auto.name, policy.running.name, policy.blocked.name, policy.review.name]);
3597
+ return labels.filter((label) => managed.has(label)).sort();
3321
3598
  }
3322
3599
  function publicBlockedText(value, fallback, maxLength) {
3323
3600
  if (containsCredentialEvidence(value) || containsHostIdentityEvidence(value)
@@ -3330,6 +3607,11 @@ function publicBlockedText(value, fallback, maxLength) {
3330
3607
  ? normalized
3331
3608
  : `${normalized.slice(0, Math.max(0, maxLength - 12))} [truncated]`;
3332
3609
  }
3610
+ function publicIssueFeedbackText(value, fallback) {
3611
+ if (/\b(?:chain[- ]of[- ]thought|reasoning artifact|local-only log|private reasoning|debug log)\b/iu.test(value))
3612
+ return fallback;
3613
+ return publicBlockedText(value, fallback, 4_000);
3614
+ }
3333
3615
  function sortedUnique(values) {
3334
3616
  return [...new Set(values)].sort();
3335
3617
  }