codex-orchestrator 0.1.41 → 0.1.42

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 (52) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/src/github/gh-issue-adapter.d.ts +2 -0
  3. package/dist/src/github/gh-issue-adapter.d.ts.map +1 -1
  4. package/dist/src/github/gh-issue-adapter.js +7 -1
  5. package/dist/src/github/gh-issue-adapter.js.map +1 -1
  6. package/dist/src/github/issues.d.ts +3 -0
  7. package/dist/src/github/issues.d.ts.map +1 -1
  8. package/dist/src/github/issues.js +7 -1
  9. package/dist/src/github/issues.js.map +1 -1
  10. package/dist/src/runner/acceptance-proof-loop.d.ts +115 -0
  11. package/dist/src/runner/acceptance-proof-loop.d.ts.map +1 -0
  12. package/dist/src/runner/acceptance-proof-loop.js +401 -0
  13. package/dist/src/runner/acceptance-proof-loop.js.map +1 -0
  14. package/dist/src/runner/acceptance-proof-runner.d.ts +2 -20
  15. package/dist/src/runner/acceptance-proof-runner.d.ts.map +1 -1
  16. package/dist/src/runner/acceptance-proof-runner.js +9 -103
  17. package/dist/src/runner/acceptance-proof-runner.js.map +1 -1
  18. package/dist/src/runner/agent-attempt.d.ts +105 -0
  19. package/dist/src/runner/agent-attempt.d.ts.map +1 -0
  20. package/dist/src/runner/agent-attempt.js +235 -0
  21. package/dist/src/runner/agent-attempt.js.map +1 -0
  22. package/dist/src/runner/daemon-command.d.ts +1 -1
  23. package/dist/src/runner/daemon-command.d.ts.map +1 -1
  24. package/dist/src/runner/daemon-command.js +24 -0
  25. package/dist/src/runner/daemon-command.js.map +1 -1
  26. package/dist/src/runner/durable-run-summary.js +10 -1
  27. package/dist/src/runner/durable-run-summary.js.map +1 -1
  28. package/dist/src/runner/local-execution-session.d.ts.map +1 -1
  29. package/dist/src/runner/local-execution-session.js +83 -126
  30. package/dist/src/runner/local-execution-session.js.map +1 -1
  31. package/dist/src/runner/plan-auto-command.d.ts.map +1 -1
  32. package/dist/src/runner/plan-auto-command.js +181 -295
  33. package/dist/src/runner/plan-auto-command.js.map +1 -1
  34. package/dist/src/runner/review-gate-policy.d.ts +3 -11
  35. package/dist/src/runner/review-gate-policy.d.ts.map +1 -1
  36. package/dist/src/runner/review-gate-policy.js +6 -178
  37. package/dist/src/runner/review-gate-policy.js.map +1 -1
  38. package/dist/src/runner/runner-handoff-decision.d.ts.map +1 -1
  39. package/dist/src/runner/runner-handoff-decision.js +22 -4
  40. package/dist/src/runner/runner-handoff-decision.js.map +1 -1
  41. package/dist/src/runner/scoped-auto-command.d.ts.map +1 -1
  42. package/dist/src/runner/scoped-auto-command.js +104 -221
  43. package/dist/src/runner/scoped-auto-command.js.map +1 -1
  44. package/dist/src/runner/terminal-outcome.d.ts +58 -0
  45. package/dist/src/runner/terminal-outcome.d.ts.map +1 -0
  46. package/dist/src/runner/terminal-outcome.js +73 -0
  47. package/dist/src/runner/terminal-outcome.js.map +1 -0
  48. package/dist/src/runner/visual-proof-runner.d.ts +2 -0
  49. package/dist/src/runner/visual-proof-runner.d.ts.map +1 -1
  50. package/dist/src/runner/visual-proof-runner.js +30 -8
  51. package/dist/src/runner/visual-proof-runner.js.map +1 -1
  52. package/package.json +1 -1
@@ -6,7 +6,6 @@ import { resolveBaseBranch } from '../git/base-branch.js';
6
6
  import { GitMergeConflictError, GitWorktreeManager, renderBranchTemplate } from '../git/worktree.js';
7
7
  import { GhCliIssueAdapter } from '../github/gh-issue-adapter.js';
8
8
  import { GhCliPullRequestAdapter } from '../github/gh-pull-request-adapter.js';
9
- import { verifyPullRequestRefs } from '../github/pull-requests.js';
10
9
  import { defaultShellCommandExecutor } from '../process/command.js';
11
10
  import { formatSessionTimestamp, readRunnerConfig, runConfiguredChecks, } from './command-utils.js';
12
11
  import { buildChildBlockedReport, buildChildReviewReport, buildIssueTreePullRequestBody, buildIssueTreeReviewReport, buildParentBlockedReport, } from './handoff-evidence.js';
@@ -16,16 +15,16 @@ import { writeContextSnapshot } from './context-snapshot.js';
16
15
  import { readPlanAutoCompletionReport, } from './completion-report.js';
17
16
  import { claimIssue, discoverIssueWork } from './issue-state-machine.js';
18
17
  import { collectExecutableChildBatches, persistAutonomousChildNode, readAutonomousChildNodes, topologicalPlanNodes, } from './issue-tree.js';
19
- import { runImplementationPublishabilityCheck } from './local-execution-session.js';
20
18
  import { RunnerLifecycleEventStore } from './lifecycle-events.js';
21
19
  import { RunnerStateStore } from './local-state.js';
22
20
  import { classifyPlanAutoBlockedChildRecovery, classifyPlanAutoCompletedChildRecovery, classifyPlanAutoParentRecovery, } from './plan-auto-recovery.js';
23
21
  import { buildIssueTreeChildPrompt, buildPlanAutoPrompt, sessionPromptPath, sessionReportPath, writeDurablePrompt, } from './prompt.js';
24
- import { decideImplementationRework } from './rework-policy.js';
22
+ import { runAgentAttemptLoop } from './agent-attempt.js';
25
23
  import { evaluateParentRiskRoutingGate } from './review-gates.js';
26
24
  import { buildBlockedHandoffEvidence, buildPromotionAsBlockedHandoffEvidence, buildReviewReadyHandoffEvidence, } from './runner-handoff-decision.js';
27
25
  import { sessionLogPath } from './run-log.js';
28
26
  import { cleanupSessionCodexHome, sessionCodexHomePath } from './session-home.js';
27
+ import { finishBlockedTerminalOutcome, finishReviewReadyCommentTerminalOutcome, finishReviewReadyTerminalOutcome, } from './terminal-outcome.js';
29
28
  class ChildExecutionBlockedError extends Error {
30
29
  durableRunSummary;
31
30
  freshContextReview;
@@ -375,37 +374,44 @@ export async function runPlanAutoCommand(options) {
375
374
  ...failedFinalValidation.map((line) => `${line.command}: ${line.status} - ${line.summary}`),
376
375
  ], childIssues);
377
376
  }
378
- await git.pushBranch({ worktreePath, branchName });
379
- pullRequest = await pullRequestAdapter.createDraftPullRequest({
380
- title: renderParentTemplate(config.pullRequests.issueTreeTitle, options.issueNumber),
381
- body: buildIssueTreePullRequestBody({
377
+ const handoff = await finishReviewReadyTerminalOutcome({
378
+ issueNumber: options.issueNumber,
379
+ config,
380
+ branchName,
381
+ baseBranch: resolvedBase.prBaseBranch,
382
+ worktreePath,
383
+ git,
384
+ pullRequestAdapter,
385
+ issueAdapter,
386
+ pullRequest: {
387
+ title: renderParentTemplate(config.pullRequests.issueTreeTitle, options.issueNumber),
388
+ body: buildIssueTreePullRequestBody({
389
+ parentIssueNumber: options.issueNumber,
390
+ childIssues,
391
+ childResults: executionResults,
392
+ finalValidation,
393
+ sizeRisk: report.sizeRisk,
394
+ parentReviewHandoff: report.parentReviewHandoff,
395
+ riskRoutingWarnings,
396
+ }),
397
+ },
398
+ findExistingPullRequest: false,
399
+ reportComment: (verifiedPullRequest) => buildIssueTreeReviewReport({
382
400
  parentIssueNumber: options.issueNumber,
383
- childIssues,
401
+ pullRequest: verifiedPullRequest,
402
+ batches: batches.batches,
384
403
  childResults: executionResults,
385
404
  finalValidation,
386
405
  sizeRisk: report.sizeRisk,
387
406
  parentReviewHandoff: report.parentReviewHandoff,
388
407
  riskRoutingWarnings,
389
408
  }),
390
- headBranch: branchName,
391
- baseBranch: resolvedBase.prBaseBranch,
392
- });
393
- pullRequest = await verifyPullRequestRefs(pullRequestAdapter, pullRequest, branchName, resolvedBase.prBaseBranch);
394
- const reportComment = buildIssueTreeReviewReport({
395
- parentIssueNumber: options.issueNumber,
396
- pullRequest,
397
- batches: batches.batches,
398
- childResults: executionResults,
399
- finalValidation,
400
- sizeRisk: report.sizeRisk,
401
- parentReviewHandoff: report.parentReviewHandoff,
402
- riskRoutingWarnings,
409
+ onPullRequestReady: (verifiedPullRequest) => {
410
+ pullRequest = verifiedPullRequest;
411
+ },
412
+ beforeIssueMutation: () => markCompletedChildrenReviewReady(issueAdapter, config, store, options.issueNumber, executionResults),
413
+ afterComment: () => store.removeRun(options.issueNumber),
403
414
  });
404
- await markCompletedChildrenReviewReady(issueAdapter, config, store, options.issueNumber, executionResults);
405
- await issueAdapter.removeLabels(options.issueNumber, [config.github.labels.running.name]);
406
- await issueAdapter.addLabels(options.issueNumber, [config.github.labels.review.name]);
407
- await issueAdapter.postComment(options.issueNumber, reportComment);
408
- await store.removeRun(options.issueNumber);
409
415
  await safeAppendEvent(events, {
410
416
  issueNumber: options.issueNumber,
411
417
  mode: 'plan-parent',
@@ -415,14 +421,14 @@ export async function runPlanAutoCommand(options) {
415
421
  summary: 'Issue-tree execution completed draft PR handoff.',
416
422
  artifacts: [
417
423
  ...sessionArtifacts(promptPath, reportPath, logPath, parentSnapshotPath),
418
- { kind: 'pr', url: pullRequest.url, description: 'Draft pull request' },
424
+ { kind: 'pr', url: handoff.pullRequest.url, description: 'Draft pull request' },
419
425
  ],
420
426
  });
421
427
  return {
422
428
  ...buildBase(),
423
429
  status: 'review-ready',
424
- pullRequest,
425
- reportComment,
430
+ pullRequest: handoff.pullRequest,
431
+ reportComment: handoff.reportComment,
426
432
  riskRoutingWarnings,
427
433
  };
428
434
  }
@@ -479,7 +485,6 @@ async function executeChild(input) {
479
485
  const childIssueNumber = input.child.issue.number;
480
486
  const branchName = `codex/tree-${input.parentIssue.number}-issue-${childIssueNumber}`;
481
487
  const worktreePath = join(input.targetRoot, input.config.runner.workspaceRoot, `tree-${input.parentIssue.number}-issue-${childIssueNumber}`);
482
- const store = new RunnerStateStore(input.targetRoot, input.config);
483
488
  let sessionId = '';
484
489
  let promptPath = '';
485
490
  let reportPath = '';
@@ -506,204 +511,69 @@ async function executeChild(input) {
506
511
  await input.issueAdapter.addLabels(childIssueNumber, [input.config.github.labels.running.name]);
507
512
  await input.issueAdapter.postComment(childIssueNumber, `codex-orchestrator: claimed #${childIssueNumber} for tree-child autonomous work under #${input.parentIssue.number} at ${input.now.toISOString()}.`);
508
513
  }
509
- let publishability;
510
- let rework = input.recovery?.rework;
511
- const reworkAttempts = [];
512
- const maxReworkAttempts = Math.max(input.config.loopPolicy.rework.maxAttempts, input.config.reviewGates.acceptanceProof.maxIterations - 1);
513
- const firstAttempt = input.recovery ? input.recovery.rework.attempt : 0;
514
- for (let attempt = firstAttempt; attempt <= maxReworkAttempts; attempt++) {
515
- const attemptNow = new Date(input.now.getTime() + attempt);
516
- sessionId = `tree-${input.parentIssue.number}-issue-${childIssueNumber}-${formatSessionTimestamp(attemptNow)}${attempt === 0 ? '' : `-attempt-${attempt}`}`;
517
- promptPath = sessionPromptPath({
518
- targetRoot: input.targetRoot,
519
- config: input.config,
520
- issueNumber: childIssueNumber,
521
- sessionId,
522
- });
523
- reportPath = sessionReportPath({
524
- targetRoot: input.targetRoot,
525
- config: input.config,
526
- issueNumber: childIssueNumber,
527
- sessionId,
528
- });
529
- logPath = sessionLogPath({
530
- targetRoot: input.targetRoot,
531
- config: input.config,
532
- issueNumber: childIssueNumber,
533
- sessionId,
534
- });
535
- const isolatedHomePath = sessionCodexHomePath({ targetRoot: input.targetRoot, sessionId });
536
- await mkdir(dirname(reportPath), { recursive: true });
537
- await mkdir(isolatedHomePath, { recursive: true });
538
- const promptText = buildIssueTreeChildPrompt({
539
- parentIssue: input.parentIssue,
540
- childIssue: input.child.issue,
541
- config: input.config,
542
- workflowPromptText: input.issueTreeWorkflowPrompt,
543
- childMetadata: input.child.metadata,
544
- dependencyIssues: input.dependencyIssues,
545
- promptPath,
546
- reportPath,
547
- branchName,
548
- worktreePath,
549
- });
550
- const attemptPromptText = rework
551
- ? `${promptText}\n\n## Rework Request\nThis is an automatic rework attempt (#${rework.attempt}). Continue from the current worktree state; do not start over.\nThe previous attempt was blocked for these reasons:\n${rework.blockedReasons.map((reason) => `- ${reason}`).join('\n')}\n${rework.disableOptionalFigmaMcp ? 'Optional Figma MCP failed in the previous attempt. Continue without optional Figma MCP access unless the issue explicitly requires Figma.\n' : ''}Address the blockers, then produce a fresh completion report JSON for the runner.`
552
- : promptText;
553
- await writeDurablePrompt({
554
- targetRoot: input.targetRoot,
555
- config: input.config,
556
- issueNumber: childIssueNumber,
557
- sessionId,
558
- promptText: attemptPromptText,
559
- });
560
- const snapshot = await writeContextSnapshot({
561
- targetRoot: input.targetRoot,
562
- config: input.config,
563
- issue: input.child.issue,
564
- mode: 'tree-child',
565
- phase: 'tree-child',
566
- decision: rework
567
- ? `tree-child rework attempt #${rework.attempt} under parent #${input.parentIssue.number}`
568
- : `child issue execution under parent #${input.parentIssue.number}`,
569
- sessionId,
570
- worktreePath,
571
- promptPath,
572
- reportPath,
573
- logPath,
574
- branchName,
575
- baseBranch: input.parentBranchName,
576
- parentIssueNumber: input.parentIssue.number,
577
- blockedBy: [],
578
- createdAt: attemptNow,
579
- });
580
- snapshotPath = snapshot.path;
581
- await store.upsertRun({
582
- issueNumber: childIssueNumber,
583
- parentIssueNumber: input.parentIssue.number,
584
- mode: 'tree-child',
585
- workspacePath: worktreePath,
586
- sessionId,
587
- branchName,
588
- promptPath,
589
- reportPath,
590
- logPath,
591
- retryCount: attempt,
592
- createdAt: input.now.toISOString(),
593
- updatedAt: attemptNow.toISOString(),
594
- attemptStartedAt: attemptNow.toISOString(),
595
- snapshotPath,
596
- });
597
- await safeAppendEvent(input.events, {
598
- timestamp: attemptNow,
599
- issueNumber: childIssueNumber,
600
- parentIssueNumber: input.parentIssue.number,
601
- mode: 'tree-child',
602
- sessionId,
603
- phase: 'tree-child',
604
- status: 'started',
605
- summary: rework ? `Starting tree-child rework attempt #${rework.attempt}.` : 'Starting tree-child Codex session.',
606
- artifacts: sessionArtifacts(promptPath, reportPath, logPath, snapshotPath),
607
- });
608
- const beforeHead = await input.git.getHead(worktreePath);
609
- let codexResult;
610
- try {
611
- codexResult = await input.codexAdapter.run({
612
- targetRoot: input.targetRoot,
514
+ const attemptLoop = await runAgentAttemptLoop({
515
+ targetRoot: input.targetRoot,
516
+ config: input.config,
517
+ issue: input.child.issue,
518
+ issueNumber: childIssueNumber,
519
+ parentIssueNumber: input.parentIssue.number,
520
+ mode: 'tree-child',
521
+ phase: 'tree-child',
522
+ branchName,
523
+ worktreePath,
524
+ baseBranch: input.parentBranchName,
525
+ createdAt: input.now,
526
+ firstAttempt: input.recovery ? input.recovery.rework.attempt : 0,
527
+ initialRework: input.recovery?.rework,
528
+ buildSessionId: ({ attempt, attemptNow }) => `tree-${input.parentIssue.number}-issue-${childIssueNumber}-${formatSessionTimestamp(attemptNow)}${attempt === 0 ? '' : `-attempt-${attempt}`}`,
529
+ buildPrompt: ({ promptPath: attemptPromptPath, reportPath: attemptReportPath, rework }) => {
530
+ const promptText = buildIssueTreeChildPrompt({
531
+ parentIssue: input.parentIssue,
532
+ childIssue: input.child.issue,
613
533
  config: input.config,
614
- worktreePath,
615
- promptPath,
616
- promptText: attemptPromptText,
617
- reportPath,
618
- isolatedHomePath,
619
- issueNumber: childIssueNumber,
620
- sessionId,
534
+ workflowPromptText: input.issueTreeWorkflowPrompt,
535
+ childMetadata: input.child.metadata,
536
+ dependencyIssues: input.dependencyIssues,
537
+ promptPath: attemptPromptPath,
538
+ reportPath: attemptReportPath,
621
539
  branchName,
622
- phase: 'tree-child',
623
- logPath,
624
- disableOptionalFigmaMcp: rework?.disableOptionalFigmaMcp,
540
+ worktreePath,
625
541
  });
626
- }
627
- finally {
628
- await cleanupSessionCodexHome(isolatedHomePath);
629
- }
630
- const afterHead = await input.git.getHead(worktreePath);
631
- publishability = await runImplementationPublishabilityCheck({
632
- config: input.config,
633
- issue: input.child.issue,
542
+ return rework
543
+ ? `${promptText}\n\n## Rework Request\nThis is an automatic rework attempt (#${rework.attempt}). Continue from the current worktree state; do not start over.\nThe previous attempt was blocked for these reasons:\n${rework.blockedReasons.map((reason) => `- ${reason}`).join('\n')}\n${rework.disableOptionalFigmaMcp ? 'Optional Figma MCP failed in the previous attempt. Continue without optional Figma MCP access unless the issue explicitly requires Figma.\n' : ''}Address the blockers, then produce a fresh completion report JSON for the runner.`
544
+ : promptText;
545
+ },
546
+ buildSnapshotDecision: ({ rework }) => rework
547
+ ? `tree-child rework attempt #${rework.attempt} under parent #${input.parentIssue.number}`
548
+ : `child issue execution under parent #${input.parentIssue.number}`,
549
+ startedSummary: ({ rework }) => rework ? `Starting tree-child rework attempt #${rework.attempt}.` : 'Starting tree-child Codex session.',
550
+ reworkScheduledSummary: ({ nextAttempt }) => `Runner scheduled tree-child rework attempt #${nextAttempt}.`,
551
+ missingPublishabilityMessage: 'Runner internal error: missing child publishability result',
552
+ codexAdapter: input.codexAdapter,
553
+ git: input.git,
554
+ shellExecutor: input.shellExecutor,
555
+ commitMessage: `Codex: implement issue #${childIssueNumber} for parent #${input.parentIssue.number}`,
556
+ events: input.events,
557
+ acceptanceProof: {
634
558
  targetRoot: input.targetRoot,
635
- worktreePath,
636
- reportPath,
637
- beforeHead,
638
- afterHead,
639
- codexResult,
640
- git: input.git,
641
- shellExecutor: input.shellExecutor,
642
- commitMessage: `Codex: implement issue #${childIssueNumber} for parent #${input.parentIssue.number}`,
643
- acceptanceProof: {
644
- targetRoot: input.targetRoot,
645
- sessionId,
646
- branchName,
647
- workflowPromptText: input.acceptanceProofWorkflowPrompt,
648
- codexAdapter: input.codexAdapter,
649
- onAttemptEvent: (event) => appendAcceptanceProofEvent({
650
- events: input.events,
651
- issueNumber: childIssueNumber,
652
- parentIssueNumber: input.parentIssue.number,
653
- sessionId,
654
- event,
655
- }),
656
- },
657
- });
658
- if (publishability.status === 'blocked') {
659
- const reworkDecision = decideImplementationRework({
660
- reasons: publishability.reasons,
661
- config: input.config,
662
- attempt,
663
- });
664
- reworkAttempts.push({
665
- attempt,
666
- maxAttempts: 'maxAttempts' in reworkDecision ? reworkDecision.maxAttempts : undefined,
667
- decisionKind: reworkDecision.kind,
668
- reasons: publishability.reasons,
669
- promptPath,
670
- reportPath,
671
- logPath,
672
- snapshotPath,
673
- });
674
- if (reworkDecision.kind === 'retry') {
675
- rework = reworkDecision.rework;
676
- await safeAppendEvent(input.events, {
677
- timestamp: attemptNow,
678
- issueNumber: childIssueNumber,
679
- parentIssueNumber: input.parentIssue.number,
680
- mode: 'tree-child',
681
- sessionId,
682
- phase: 'tree-child',
683
- status: 'needs-rework',
684
- summary: `Runner scheduled tree-child rework attempt #${reworkDecision.nextAttempt}.`,
685
- artifacts: sessionArtifacts(promptPath, reportPath, logPath, snapshotPath),
686
- });
687
- await mkdir(isolatedHomePath, { recursive: true });
688
- continue;
689
- }
690
- await safeAppendEvent(input.events, {
691
- timestamp: attemptNow,
692
- issueNumber: childIssueNumber,
693
- parentIssueNumber: input.parentIssue.number,
694
- mode: 'tree-child',
695
- sessionId,
696
- phase: 'tree-child',
697
- status: 'blocked',
698
- summary: `Runner rework decision: ${reworkDecision.kind}.`,
699
- artifacts: sessionArtifacts(promptPath, reportPath, logPath, snapshotPath),
700
- });
701
- }
702
- break;
703
- }
704
- if (!publishability) {
705
- throw new Error('Runner internal error: missing child publishability result');
706
- }
559
+ workflowPromptText: input.acceptanceProofWorkflowPrompt,
560
+ codexAdapter: input.codexAdapter,
561
+ },
562
+ onAcceptanceProofAttemptEvent: ({ sessionId: attemptSessionId, event }) => appendAcceptanceProofEvent({
563
+ events: input.events,
564
+ issueNumber: childIssueNumber,
565
+ parentIssueNumber: input.parentIssue.number,
566
+ sessionId: attemptSessionId,
567
+ event,
568
+ }),
569
+ });
570
+ const publishability = attemptLoop.publishability;
571
+ sessionId = attemptLoop.sessionId;
572
+ promptPath = attemptLoop.promptPath;
573
+ reportPath = attemptLoop.reportPath;
574
+ logPath = attemptLoop.logPath;
575
+ snapshotPath = attemptLoop.snapshotPath;
576
+ const reworkAttempts = attemptLoop.reworkAttempts;
707
577
  if (publishability.status === 'promotion-requested') {
708
578
  const promotion = publishability.report.promotion;
709
579
  const evidence = buildPromotionAsBlockedHandoffEvidence({
@@ -842,31 +712,37 @@ async function blockFailedBatch(issueAdapter, config, parentIssueNumber, failure
842
712
  if (!failure.child) {
843
713
  continue;
844
714
  }
845
- await issueAdapter.removeLabels(failure.child.issue.number, [config.github.labels.running.name]);
846
- await issueAdapter.addLabels(failure.child.issue.number, [config.github.labels.blocked.name]);
847
- await issueAdapter.postComment(failure.child.issue.number, buildChildBlockedReport({
848
- parentIssueNumber,
849
- childIssueNumber: failure.child.issue.number,
850
- reasons: [failure.message],
851
- details: ['Worktree preserved for maintainer inspection.'],
852
- freshContextReview: failure.freshContextReview,
853
- durableRunSummary: failure.durableRunSummary,
854
- reworkAttempts: failure.reworkAttempts,
855
- acceptanceProof: failure.acceptanceProof,
856
- }));
715
+ await finishBlockedTerminalOutcome({
716
+ issueNumber: failure.child.issue.number,
717
+ config,
718
+ issueAdapter,
719
+ reportComment: buildChildBlockedReport({
720
+ parentIssueNumber,
721
+ childIssueNumber: failure.child.issue.number,
722
+ reasons: [failure.message],
723
+ details: ['Worktree preserved for maintainer inspection.'],
724
+ freshContextReview: failure.freshContextReview,
725
+ durableRunSummary: failure.durableRunSummary,
726
+ reworkAttempts: failure.reworkAttempts,
727
+ acceptanceProof: failure.acceptanceProof,
728
+ }),
729
+ });
857
730
  }
858
731
  for (const result of successfulUnmerged) {
859
- await issueAdapter.removeLabels(result.child.issue.number, [config.github.labels.running.name]);
860
- await issueAdapter.addLabels(result.child.issue.number, [config.github.labels.blocked.name]);
861
- await issueAdapter.postComment(result.child.issue.number, buildChildBlockedReport({
862
- parentIssueNumber,
863
- childIssueNumber: result.child.issue.number,
864
- reasons: ['A sibling child failed before the batch merge; this child branch was not merged.'],
865
- branchName: result.branchName,
866
- worktreePath: result.worktreePath,
867
- durableRunSummary: result.durableRunSummary,
868
- acceptanceProof: result.acceptanceProof,
869
- }));
732
+ await finishBlockedTerminalOutcome({
733
+ issueNumber: result.child.issue.number,
734
+ config,
735
+ issueAdapter,
736
+ reportComment: buildChildBlockedReport({
737
+ parentIssueNumber,
738
+ childIssueNumber: result.child.issue.number,
739
+ reasons: ['A sibling child failed before the batch merge; this child branch was not merged.'],
740
+ branchName: result.branchName,
741
+ worktreePath: result.worktreePath,
742
+ durableRunSummary: result.durableRunSummary,
743
+ acceptanceProof: result.acceptanceProof,
744
+ }),
745
+ });
870
746
  }
871
747
  }
872
748
  async function handleMergeConflict(git, issueAdapter, config, parentIssueNumber, error, childResult, batchResults) {
@@ -878,30 +754,33 @@ async function handleMergeConflict(git, issueAdapter, config, parentIssueNumber,
878
754
  abortMessage = abortError instanceof Error ? abortError.message : 'merge abort failed';
879
755
  }
880
756
  for (const result of batchResults) {
881
- await issueAdapter.removeLabels(result.child.issue.number, [config.github.labels.running.name]);
882
- await issueAdapter.addLabels(result.child.issue.number, [config.github.labels.blocked.name]);
883
- await issueAdapter.postComment(result.child.issue.number, buildChildBlockedReport({
884
- parentIssueNumber,
885
- childIssueNumber: result.child.issue.number,
886
- reasons: [
887
- result.child.issue.number === childResult.child.issue.number
888
- ? `Merge conflict from branch ${childResult.branchName}`
889
- : `A sibling merge conflict stopped the batch before publication: ${childResult.branchName}`,
890
- ],
891
- details: [
892
- `- Parent worktree: ${error.worktreePath}`,
893
- `- Child worktree: ${result.worktreePath}`,
894
- `- Child branch preserved: ${result.branchName}`,
895
- abortMessage ? `- Merge abort also failed: ${abortMessage}` : '- Merge abort completed.',
896
- ],
897
- batchChildren: batchResults.map((batchResult) => ({
898
- issueNumber: batchResult.child.issue.number,
899
- branchName: batchResult.branchName,
900
- })),
901
- gitOutput: error.stderr || error.stdout || 'no git output',
902
- durableRunSummary: result.durableRunSummary,
903
- acceptanceProof: result.acceptanceProof,
904
- }));
757
+ await finishBlockedTerminalOutcome({
758
+ issueNumber: result.child.issue.number,
759
+ config,
760
+ issueAdapter,
761
+ reportComment: buildChildBlockedReport({
762
+ parentIssueNumber,
763
+ childIssueNumber: result.child.issue.number,
764
+ reasons: [
765
+ result.child.issue.number === childResult.child.issue.number
766
+ ? `Merge conflict from branch ${childResult.branchName}`
767
+ : `A sibling merge conflict stopped the batch before publication: ${childResult.branchName}`,
768
+ ],
769
+ details: [
770
+ `- Parent worktree: ${error.worktreePath}`,
771
+ `- Child worktree: ${result.worktreePath}`,
772
+ `- Child branch preserved: ${result.branchName}`,
773
+ abortMessage ? `- Merge abort also failed: ${abortMessage}` : '- Merge abort completed.',
774
+ ],
775
+ batchChildren: batchResults.map((batchResult) => ({
776
+ issueNumber: batchResult.child.issue.number,
777
+ branchName: batchResult.branchName,
778
+ })),
779
+ gitOutput: error.stderr || error.stdout || 'no git output',
780
+ durableRunSummary: result.durableRunSummary,
781
+ acceptanceProof: result.acceptanceProof,
782
+ }),
783
+ });
905
784
  }
906
785
  }
907
786
  async function markCompletedChildrenReviewReady(issueAdapter, config, store, parentIssueNumber, childResults) {
@@ -910,10 +789,13 @@ async function markCompletedChildrenReviewReady(issueAdapter, config, store, par
910
789
  await store.removeRun(childResult.child.issue.number);
911
790
  continue;
912
791
  }
913
- await issueAdapter.removeLabels(childResult.child.issue.number, [config.github.labels.running.name]);
914
- await issueAdapter.addLabels(childResult.child.issue.number, [config.github.labels.review.name]);
915
- await issueAdapter.postComment(childResult.child.issue.number, buildChildReviewReport({ parentIssueNumber, result: childResult }));
916
- await store.removeRun(childResult.child.issue.number);
792
+ await finishReviewReadyCommentTerminalOutcome({
793
+ issueNumber: childResult.child.issue.number,
794
+ config,
795
+ issueAdapter,
796
+ reportComment: buildChildReviewReport({ parentIssueNumber, result: childResult }),
797
+ afterTerminalMutation: () => store.removeRun(childResult.child.issue.number),
798
+ });
917
799
  }
918
800
  }
919
801
  async function blockCompletedChildren(issueAdapter, config, store, parentIssueNumber, childResults, reason, parentWorktreePath) {
@@ -921,20 +803,23 @@ async function blockCompletedChildren(issueAdapter, config, store, parentIssueNu
921
803
  if (result.recovered) {
922
804
  continue;
923
805
  }
924
- await issueAdapter.removeLabels(result.child.issue.number, [config.github.labels.running.name]);
925
- await issueAdapter.addLabels(result.child.issue.number, [config.github.labels.blocked.name]);
926
- await issueAdapter.postComment(result.child.issue.number, buildChildBlockedReport({
927
- parentIssueNumber,
928
- childIssueNumber: result.child.issue.number,
929
- reasons: [reason],
930
- details: [
931
- `- Merged child branch was not published: ${result.branchName}`,
932
- `- Parent worktree preserved: ${parentWorktreePath}`,
933
- ],
934
- durableRunSummary: result.durableRunSummary,
935
- acceptanceProof: result.acceptanceProof,
936
- }));
937
- await store.removeRun(result.child.issue.number);
806
+ await finishBlockedTerminalOutcome({
807
+ issueNumber: result.child.issue.number,
808
+ config,
809
+ issueAdapter,
810
+ reportComment: buildChildBlockedReport({
811
+ parentIssueNumber,
812
+ childIssueNumber: result.child.issue.number,
813
+ reasons: [reason],
814
+ details: [
815
+ `- Merged child branch was not published: ${result.branchName}`,
816
+ `- Parent worktree preserved: ${parentWorktreePath}`,
817
+ ],
818
+ durableRunSummary: result.durableRunSummary,
819
+ acceptanceProof: result.acceptanceProof,
820
+ }),
821
+ afterTerminalMutation: () => store.removeRun(result.child.issue.number),
822
+ });
938
823
  }
939
824
  }
940
825
  async function finishPlanBlocked(issueAdapter, config, events, result, reasons, mutatedChildren, commentMarker) {
@@ -945,12 +830,13 @@ async function finishPlanBlocked(issueAdapter, config, events, result, reasons,
945
830
  mutatedChildren,
946
831
  });
947
832
  const reportComment = commentMarker ? `<!-- codex-orchestrator:${commentMarker} -->\n${reportBody}` : reportBody;
948
- await issueAdapter.removeLabels(result.parentIssueNumber, [config.github.labels.running.name]);
949
- await issueAdapter.addLabels(result.parentIssueNumber, [config.github.labels.blocked.name]);
950
- const currentIssue = commentMarker ? await issueAdapter.getIssue(result.parentIssueNumber) : undefined;
951
- if (!commentMarker || !currentIssue?.comments.some((comment) => comment.body.includes(commentMarker))) {
952
- await issueAdapter.postComment(result.parentIssueNumber, reportComment);
953
- }
833
+ await finishBlockedTerminalOutcome({
834
+ issueNumber: result.parentIssueNumber,
835
+ config,
836
+ issueAdapter,
837
+ reportComment,
838
+ skipCommentIfIncludes: commentMarker,
839
+ });
954
840
  await safeAppendEvent(events, {
955
841
  issueNumber: result.parentIssueNumber,
956
842
  mode: 'plan-parent',