codex-orchestrator 2.0.5 → 2.0.7

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 (72) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +6 -2
  3. package/dist/src/v2/acceptance-proof.d.ts +7 -0
  4. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  5. package/dist/src/v2/acceptance-proof.js +111 -5
  6. package/dist/src/v2/acceptance-proof.js.map +1 -1
  7. package/dist/src/v2/adapters/command.d.ts +2 -0
  8. package/dist/src/v2/adapters/command.d.ts.map +1 -1
  9. package/dist/src/v2/adapters/command.js +67 -8
  10. package/dist/src/v2/adapters/command.js.map +1 -1
  11. package/dist/src/v2/adapters/gh-issue-adapter.js +16 -5
  12. package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
  13. package/dist/src/v2/android-proof-runner.d.ts +93 -0
  14. package/dist/src/v2/android-proof-runner.d.ts.map +1 -0
  15. package/dist/src/v2/android-proof-runner.js +921 -0
  16. package/dist/src/v2/android-proof-runner.js.map +1 -0
  17. package/dist/src/v2/checked-change.d.ts +1 -1
  18. package/dist/src/v2/checked-change.d.ts.map +1 -1
  19. package/dist/src/v2/checked-change.js +3 -2
  20. package/dist/src/v2/checked-change.js.map +1 -1
  21. package/dist/src/v2/code-review-report.d.ts +1 -2
  22. package/dist/src/v2/code-review-report.d.ts.map +1 -1
  23. package/dist/src/v2/code-review-report.js +6 -11
  24. package/dist/src/v2/code-review-report.js.map +1 -1
  25. package/dist/src/v2/config.d.ts +13 -0
  26. package/dist/src/v2/config.d.ts.map +1 -1
  27. package/dist/src/v2/config.js +57 -1
  28. package/dist/src/v2/config.js.map +1 -1
  29. package/dist/src/v2/containment.d.ts +0 -52
  30. package/dist/src/v2/containment.d.ts.map +1 -1
  31. package/dist/src/v2/containment.js +1 -195
  32. package/dist/src/v2/containment.js.map +1 -1
  33. package/dist/src/v2/direct-delivery.d.ts +11 -2
  34. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  35. package/dist/src/v2/direct-delivery.js +62 -17
  36. package/dist/src/v2/direct-delivery.js.map +1 -1
  37. package/dist/src/v2/immutable-workflow-publisher.js +6 -1
  38. package/dist/src/v2/immutable-workflow-publisher.js.map +1 -1
  39. package/dist/src/v2/implementation-reviewer.d.ts +1 -1
  40. package/dist/src/v2/implementation-reviewer.d.ts.map +1 -1
  41. package/dist/src/v2/implementation-reviewer.js +1 -2
  42. package/dist/src/v2/implementation-reviewer.js.map +1 -1
  43. package/dist/src/v2/mobile-lease.d.ts +9 -0
  44. package/dist/src/v2/mobile-lease.d.ts.map +1 -1
  45. package/dist/src/v2/mobile-lease.js +27 -3
  46. package/dist/src/v2/mobile-lease.js.map +1 -1
  47. package/dist/src/v2/proof-report.d.ts +1 -1
  48. package/dist/src/v2/proof-report.d.ts.map +1 -1
  49. package/dist/src/v2/proof-report.js +6 -4
  50. package/dist/src/v2/proof-report.js.map +1 -1
  51. package/dist/src/v2/run-issue.d.ts +18 -1
  52. package/dist/src/v2/run-issue.d.ts.map +1 -1
  53. package/dist/src/v2/run-issue.js +504 -109
  54. package/dist/src/v2/run-issue.js.map +1 -1
  55. package/dist/src/v2/run-store.d.ts +17 -1
  56. package/dist/src/v2/run-store.d.ts.map +1 -1
  57. package/dist/src/v2/run-store.js +30 -4
  58. package/dist/src/v2/run-store.js.map +1 -1
  59. package/dist/src/v2/runtime.d.ts.map +1 -1
  60. package/dist/src/v2/runtime.js +84 -27
  61. package/dist/src/v2/runtime.js.map +1 -1
  62. package/docs/deep-dive.md +25 -11
  63. package/internal-workflow/manifest.json +1 -1
  64. package/internal-workflow/schemas/proof-report-v1.json +1 -1
  65. package/internal-workflow/skills/acceptance-proof/SKILL.md +1 -1
  66. package/internal-workflow/skills/acceptance-proof/references/android.md +6 -6
  67. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +10 -8
  68. package/internal-workflow/skills/implementation-spec-review/SKILL.md +17 -3
  69. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +30 -0
  70. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +30 -6
  71. package/package.json +1 -2
  72. package/internal-workflow/skills/acceptance-proof/tools/android-lease.mjs +0 -280
@@ -3,7 +3,7 @@ import { checkedChangePayloadSha256, } from './checked-change.js';
3
3
  import { parseAgentAutoConfig } from './config.js';
4
4
  import { canonicalJson, sha256 } from './containment.js';
5
5
  import { validateImplementationReport } from './implementation-report.js';
6
- import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
6
+ import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, canRecoverTerminalDirectReviewReport, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, MAX_DIRECT_REVIEW_REPORT_REPAIRS, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, recoverTerminalDirectReviewReport, } from './direct-delivery.js';
7
7
  import { ProofLaunchAuthorizationError, ProofQuiescenceError } from './acceptance-proof.js';
8
8
  import { RouteInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
9
9
  import { initialRouteExecution, } from './route-coordinator.js';
@@ -30,56 +30,88 @@ export class RunIssue {
30
30
  let observation = await this.readIssue(issueNumber);
31
31
  if (!observation || observation.state !== 'OPEN')
32
32
  return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
33
+ const body = claimComment(runId, issueNumber, branchName);
34
+ const marker = body.split('\n')[0];
35
+ const commentIntent = { kind: 'comment', issueNumber, marker, bodySha256: sha256(body) };
36
+ // Compatibility with runs persisted by the former label-first claim order.
37
+ // If the label effect is already visible, confirm it. Otherwise establish
38
+ // the trusted claim comment before granting the running status label.
33
39
  if (active.record.intent?.kind === 'claim-labels') {
34
- if (!sameStrings(observation.labels, expectedLabels)) {
40
+ if (sameStrings(observation.labels, expectedLabels)) {
41
+ active = await this.confirmEffect(active);
42
+ }
43
+ else if (!this.hasTrustedClaim(observation, active.record)) {
35
44
  const labels = new Set(observation.labels);
36
45
  if (!labels.has(config.github.labels.auto.name)
37
46
  || labels.has(config.github.labels.blocked.name)
38
- || labels.has(config.github.labels.review.name)) {
47
+ || labels.has(config.github.labels.review.name)
48
+ || labels.has(config.github.labels.waitingHuman.name)) {
39
49
  return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
40
50
  }
51
+ active = await this.persist(active, { intent: commentIntent });
52
+ }
53
+ }
54
+ if (!active.record.intent) {
55
+ observation = await this.readIssue(issueNumber);
56
+ if (!observation || observation.state !== 'OPEN')
57
+ return { result: await this.publicationDiverged(active, 'claim-issue-missing') };
58
+ if (!this.hasTrustedClaim(observation, active.record)) {
59
+ active = await this.persist(active, { intent: commentIntent });
60
+ }
61
+ }
62
+ if (active.record.intent?.kind === 'comment') {
63
+ if (active.record.intent.marker !== marker || active.record.intent.bodySha256 !== sha256(body)) {
64
+ return { result: await this.publicationDiverged(active, 'claim-comment-intent-diverged') };
65
+ }
66
+ observation = await this.readIssue(issueNumber);
67
+ let comments = observation ? commentsWithMarker(observation, marker) : [];
68
+ if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
69
+ return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
70
+ }
71
+ if (comments.length === 0) {
41
72
  try {
42
- await this.dependencies.issues.setLabels(issueNumber, expectedLabels);
73
+ await this.dependencies.issues.postComment(issueNumber, body);
43
74
  }
44
75
  catch {
45
- return { result: await this.invokedFailure(active, 'claim-labels-delivery-unknown') };
76
+ return { result: await this.invokedFailure(active, 'claim-comment-delivery-unknown') };
46
77
  }
47
78
  observation = await this.readIssue(issueNumber);
79
+ comments = observation ? commentsWithMarker(observation, marker) : [];
48
80
  }
49
- if (!observation || !sameStrings(observation.labels, expectedLabels)) {
50
- return { result: await this.publicationDiverged(active, 'claim-labels-observation-diverged') };
81
+ if (comments.length !== 1 || comments[0].body !== body
82
+ || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comments[0].authorAssociation)) {
83
+ return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
51
84
  }
52
85
  active = await this.confirmEffect(active);
53
86
  }
54
- else if (!sameStrings(observation.labels, expectedLabels)) {
55
- return { result: await this.publicationDiverged(active, 'claim-labels-missing-before-comment') };
87
+ observation = await this.readIssue(issueNumber);
88
+ if (!observation || observation.state !== 'OPEN' || !this.hasTrustedClaim(observation, active.record)) {
89
+ return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
90
+ }
91
+ if (sameStrings(observation.labels, expectedLabels))
92
+ return { active };
93
+ const labels = new Set(observation.labels);
94
+ if (!labels.has(config.github.labels.auto.name)
95
+ || labels.has(config.github.labels.blocked.name)
96
+ || labels.has(config.github.labels.review.name)
97
+ || labels.has(config.github.labels.waitingHuman.name)) {
98
+ return { result: await this.publicationDiverged(active, 'claim-labels-diverged') };
56
99
  }
57
- const body = claimComment(runId, issueNumber, branchName);
58
- const marker = body.split('\n')[0];
59
100
  if (!active.record.intent) {
60
- active = await this.persist(active, { intent: { kind: 'comment', issueNumber, marker, bodySha256: sha256(body) } });
101
+ active = await this.persist(active, { intent: { kind: 'claim-labels', issueNumber, expected: expectedLabels } });
61
102
  }
62
- if (active.record.intent?.kind !== 'comment' || active.record.intent.marker !== marker || active.record.intent.bodySha256 !== sha256(body)) {
63
- return { result: await this.publicationDiverged(active, 'claim-comment-intent-diverged') };
103
+ if (active.record.intent?.kind !== 'claim-labels' || !sameStrings(active.record.intent.expected, expectedLabels)) {
104
+ return { result: await this.publicationDiverged(active, 'claim-labels-intent-diverged') };
64
105
  }
65
- observation = await this.readIssue(issueNumber);
66
- let comments = observation ? commentsWithMarker(observation, marker) : [];
67
- if (comments.some((comment) => comment.body !== body) || comments.length > 1) {
68
- return { result: await this.publicationDiverged(active, 'claim-comment-diverged') };
106
+ try {
107
+ await this.dependencies.issues.setLabels(issueNumber, expectedLabels);
69
108
  }
70
- if (comments.length === 0) {
71
- try {
72
- await this.dependencies.issues.postComment(issueNumber, body);
73
- }
74
- catch {
75
- return { result: await this.invokedFailure(active, 'claim-comment-delivery-unknown') };
76
- }
77
- observation = await this.readIssue(issueNumber);
78
- comments = observation ? commentsWithMarker(observation, marker) : [];
109
+ catch {
110
+ return { result: await this.invokedFailure(active, 'claim-labels-delivery-unknown') };
79
111
  }
80
- if (comments.length !== 1 || comments[0].body !== body
81
- || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comments[0].authorAssociation)) {
82
- return { result: await this.publicationDiverged(active, 'claim-comment-observation-diverged') };
112
+ observation = await this.readIssue(issueNumber);
113
+ if (!observation || !sameStrings(observation.labels, expectedLabels) || !this.hasTrustedClaim(observation, active.record)) {
114
+ return { result: await this.publicationDiverged(active, 'claim-labels-observation-diverged') };
83
115
  }
84
116
  active = await this.confirmEffect(active);
85
117
  return { active };
@@ -104,7 +136,7 @@ export class RunIssue {
104
136
  return await this.publicationDiverged(active, 'commit-tree-diverged');
105
137
  if (this.signal.aborted)
106
138
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
107
- if (!await this.authorized(issueNumber, runId, branchName, config))
139
+ if (!await this.authorized(active, config))
108
140
  return await this.revoked(active);
109
141
  try {
110
142
  commitSha = await this.dependencies.git.commit({ worktreePath, message: intent.message });
@@ -136,7 +168,7 @@ export class RunIssue {
136
168
  if (!remoteSha) {
137
169
  if (this.signal.aborted)
138
170
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
139
- if (!await this.authorized(issueNumber, runId, branchName, config))
171
+ if (!await this.authorized(active, config))
140
172
  return await this.revoked(active);
141
173
  try {
142
174
  await this.dependencies.git.push({ worktreePath, branchName });
@@ -170,7 +202,7 @@ export class RunIssue {
170
202
  if (!observed) {
171
203
  if (this.signal.aborted)
172
204
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
173
- if (!await this.authorized(issueNumber, runId, branchName, config))
205
+ if (!await this.authorized(active, config))
174
206
  return await this.revoked(active);
175
207
  try {
176
208
  await this.dependencies.pullRequests.createDraft({
@@ -210,7 +242,7 @@ export class RunIssue {
210
242
  if (matching.length === 0) {
211
243
  if (this.signal.aborted)
212
244
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
213
- if (!await this.authorized(issueNumber, runId, branchName, config))
245
+ if (!await this.authorized(active, config))
214
246
  return await this.revoked(active);
215
247
  try {
216
248
  await this.dependencies.issues.postComment(issueNumber, handoffBody);
@@ -236,7 +268,7 @@ export class RunIssue {
236
268
  if (!sameStrings(observation.labels, terminalLabels)) {
237
269
  if (this.signal.aborted)
238
270
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
239
- if (!await this.authorized(issueNumber, runId, branchName, config))
271
+ if (!await this.authorized(active, config))
240
272
  return await this.revoked(active);
241
273
  try {
242
274
  await this.dependencies.issues.setLabels(issueNumber, terminalLabels);
@@ -304,7 +336,7 @@ export class RunIssue {
304
336
  }
305
337
  head = await this.dependencies.git.getHead(active.record.worktreePath);
306
338
  if (head === intent.parentSha) {
307
- if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
339
+ if (!await this.authorized(active, config)) {
308
340
  return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
309
341
  }
310
342
  const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
@@ -350,7 +382,7 @@ export class RunIssue {
350
382
  }
351
383
  remote = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
352
384
  if (remote === oldHead) {
353
- if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
385
+ if (!await this.authorized(active, config)) {
354
386
  return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
355
387
  }
356
388
  const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
@@ -410,7 +442,7 @@ export class RunIssue {
410
442
  return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-diverged');
411
443
  }
412
444
  if (matches.length === 0) {
413
- if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
445
+ if (!await this.authorized(active, config)) {
414
446
  return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
415
447
  }
416
448
  try {
@@ -459,7 +491,7 @@ export class RunIssue {
459
491
  return validationFailure;
460
492
  let issue = await this.readIssue(issueNumber);
461
493
  if (!issue || !sameStrings(issue.labels, finalLabels)) {
462
- if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
494
+ if (!await this.authorized(active, config)) {
463
495
  return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
464
496
  }
465
497
  try {
@@ -546,7 +578,7 @@ export class RunIssue {
546
578
  }
547
579
  if (feedback.phase !== 'idle' || observed.status === 'none')
548
580
  return { result: publicOutcome(terminal) };
549
- if (!await this.authorizedForExactLabels(starting.record.issueNumber, starting.record.runId, starting.record.branchName, [config.github.labels.review.name])) {
581
+ if (!await this.authorizedForExactLabels(starting, [config.github.labels.review.name])) {
550
582
  return { result: publicOutcome(terminal) };
551
583
  }
552
584
  const projected = projectReviewFeedbackBatch(observed.batch, starting.record.directReview.targetRevision);
@@ -595,9 +627,9 @@ export class RunIssue {
595
627
  }
596
628
  const issue = await this.readIssue(active.record.issueNumber);
597
629
  const labelsAreRunning = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, runningLabels)
598
- && this.hasTrustedClaim(issue, active.record.runId, active.record.issueNumber, active.record.branchName);
630
+ && this.hasTrustedClaim(issue, active.record);
599
631
  const labelsAreReview = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, reviewLabels)
600
- && this.hasTrustedClaim(issue, active.record.runId, active.record.issueNumber, active.record.branchName);
632
+ && this.hasTrustedClaim(issue, active.record);
601
633
  if (!labelsAreRunning && !labelsAreReview) {
602
634
  return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-authority-revoked') };
603
635
  }
@@ -616,7 +648,7 @@ export class RunIssue {
616
648
  }
617
649
  const observed = await this.readIssue(active.record.issueNumber);
618
650
  if (!observed || !sameStrings(observed.labels, runningLabels)
619
- || !this.hasTrustedClaim(observed, active.record.runId, active.record.issueNumber, active.record.branchName)) {
651
+ || !this.hasTrustedClaim(observed, active.record)) {
620
652
  return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-labels-diverged') };
621
653
  }
622
654
  }
@@ -706,17 +738,6 @@ export class RunIssue {
706
738
  return await this.preClaimInternal('config-changed-during-owner-acquire', input.issueNumber);
707
739
  }
708
740
  const config = confirmedConfig.config;
709
- try {
710
- await this.dependencies.validateContainment(config);
711
- }
712
- catch {
713
- const evidence = await this.dependencies.writeEvidence({
714
- runId: `issue-${input.issueNumber}`,
715
- code: 'containment-certificate-invalid',
716
- summary: 'Containment certification is unavailable or stale.',
717
- });
718
- return { status: 'blocked', kind: 'safety', resumable: true, evidencePath: evidence.path };
719
- }
720
741
  if (this.signal.aborted)
721
742
  return await this.preClaimCancelled(input.issueNumber);
722
743
  let issue;
@@ -753,15 +774,33 @@ export class RunIssue {
753
774
  if (active.record.intent?.kind === 'review-blocked-labels') {
754
775
  return await this.blockReviewFeedback(active, active.record.intent.blockKind, active.record.intent.evidenceCode);
755
776
  }
777
+ if (active.record.intent?.kind === 'blocked-labels') {
778
+ return await this.publishBlockedTerminal(active, {
779
+ status: 'blocked',
780
+ kind: active.record.intent.blockKind,
781
+ resumable: active.record.intent.resumable,
782
+ }, active.record.intent.evidenceCode);
783
+ }
784
+ if (existing.terminalOutcome?.status === 'blocked'
785
+ && issue?.state === 'OPEN'
786
+ && blockedLabelProjection(issue.labels, config).status !== 'settled') {
787
+ return await this.reconcilePersistedBlockedTerminal(active, issue, existing.terminalOutcome);
788
+ }
756
789
  if (existing.terminalOutcome) {
757
- if (existing.terminalOutcome.status !== 'review-ready')
758
- return publicOutcome(existing.terminalOutcome);
759
- const continuation = await this.continueReviewReady(active, targetRoot, config);
760
- if ('result' in continuation)
761
- return continuation.result;
762
- active = continuation.active;
763
- issueSnapshot = structuredClone(active.record.issueSnapshot);
764
- frozenCriteria = structuredClone(active.record.frozenCriteria);
790
+ if (existing.terminalOutcome.status !== 'review-ready') {
791
+ const recovered = await this.recoverTerminalReviewReport(active, issue, config, frozenCriteria);
792
+ if (!recovered)
793
+ return publicOutcome(existing.terminalOutcome);
794
+ active = recovered;
795
+ }
796
+ else {
797
+ const continuation = await this.continueReviewReady(active, targetRoot, config);
798
+ if ('result' in continuation)
799
+ return continuation.result;
800
+ active = continuation.active;
801
+ issueSnapshot = structuredClone(active.record.issueSnapshot);
802
+ frozenCriteria = structuredClone(active.record.frozenCriteria);
803
+ }
765
804
  }
766
805
  if (active.record.reviewFeedback?.phase === 'frozen') {
767
806
  const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
@@ -779,15 +818,22 @@ export class RunIssue {
779
818
  if ('result' in claim)
780
819
  return claim.result;
781
820
  active = claim.active;
782
- const worktree = await this.dependencies.git.inspectWorktree({ worktreePath, branchName, baseSha });
783
- if (worktree === 'diverged')
784
- return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'claim-worktree-diverged');
821
+ let worktree;
822
+ try {
823
+ worktree = await this.dependencies.git.inspectWorktree({ worktreePath, branchName, baseSha });
824
+ }
825
+ catch (error) {
826
+ return await this.invokedFailure(active, 'local-git-worktree-inspection-failed', claimedWorktreeInspectionFailureSummary(error));
827
+ }
828
+ if (worktree === 'diverged') {
829
+ return await this.invokedFailure(active, 'local-git-worktree-diverged', 'The claimed worktree path is present but does not match the expected branch and base. Correct or preserve the local artifact, then retry the same run.');
830
+ }
785
831
  if (worktree === 'absent') {
786
832
  try {
787
833
  await this.dependencies.git.createWorktree({ targetRoot, worktreePath, branchName, baseBranch: config.github.baseBranch, baseSha });
788
834
  }
789
- catch {
790
- return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
835
+ catch (error) {
836
+ return await this.invokedFailure(active, 'local-git-worktree-creation-failed', worktreeCreationFailureSummary(error));
791
837
  }
792
838
  }
793
839
  active = await this.initializeClaimedRun(active, issue);
@@ -815,7 +861,7 @@ export class RunIssue {
815
861
  frozenCriteria = structuredClone(active.record.frozenCriteria);
816
862
  }
817
863
  if (existing.lifecycle === 'spec-authoring') {
818
- if (!await this.authorized(input.issueNumber, runId, branchName, config))
864
+ if (!await this.authorized(active, config))
819
865
  return await this.revoked(active);
820
866
  return await this.continueSpecRequired(active);
821
867
  }
@@ -853,7 +899,7 @@ export class RunIssue {
853
899
  if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
854
900
  return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
855
901
  }
856
- if (!await this.authorized(input.issueNumber, runId, branchName, config))
902
+ if (!await this.authorized(active, config))
857
903
  return await this.revoked(active);
858
904
  if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
859
905
  if (!active.record.routeExecution || !active.record.routeReceipt)
@@ -889,11 +935,13 @@ export class RunIssue {
889
935
  worktreePath = resolve(targetRoot, config.runner.workspaceRoot, `issue-${input.issueNumber}`);
890
936
  baseSha = await this.dependencies.git.getBaseSha({ targetRoot, baseBranch: config.github.baseBranch });
891
937
  assertGitSha(baseSha, 'baseSha');
892
- const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
938
+ const claimBody = claimComment(runId, input.issueNumber, branchName);
893
939
  active = await this.createRun({
894
940
  runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
895
941
  issueSnapshot, frozenCriteria, config,
896
- intent: { kind: 'claim-labels', issueNumber: input.issueNumber, expected: runningLabels },
942
+ intent: {
943
+ kind: 'comment', issueNumber: input.issueNumber, marker: claimBody.split('\n')[0], bodySha256: sha256(claimBody),
944
+ },
897
945
  });
898
946
  const claim = await this.reconcileClaim(active, config);
899
947
  if ('result' in claim)
@@ -902,8 +950,8 @@ export class RunIssue {
902
950
  try {
903
951
  await this.dependencies.git.createWorktree({ targetRoot, worktreePath, branchName, baseBranch: config.github.baseBranch, baseSha });
904
952
  }
905
- catch {
906
- return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
953
+ catch (error) {
954
+ return await this.invokedFailure(active, 'local-git-worktree-creation-failed', worktreeCreationFailureSummary(error));
907
955
  }
908
956
  active = await this.initializeClaimedRun(active, issue);
909
957
  issueSnapshot = structuredClone(active.record.issueSnapshot);
@@ -920,8 +968,15 @@ export class RunIssue {
920
968
  if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
921
969
  return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
922
970
  }
971
+ if (active.record.lifecycle === 'implementing' && active.record.routeReceipt?.route === 'direct'
972
+ && !active.record.directReview) {
973
+ const baseline = await this.captureBaselineChecks(active, config);
974
+ if ('status' in baseline)
975
+ return baseline;
976
+ active = baseline.active;
977
+ }
923
978
  attemptLoop: while (true) {
924
- if (!await this.authorized(input.issueNumber, runId, branchName, config)) {
979
+ if (!await this.authorized(active, config)) {
925
980
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
926
981
  }
927
982
  if (this.signal.aborted)
@@ -1197,23 +1252,40 @@ export class RunIssue {
1197
1252
  });
1198
1253
  }
1199
1254
  }
1255
+ const configuredChecks = Object.entries(config.checks);
1256
+ const reusableChecks = active.record.checks.filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
1257
+ if (reusableChecks.length !== active.record.checks.length) {
1258
+ active = await this.persist(active, {
1259
+ checks: reusableChecks,
1260
+ checkedChangeSha256: undefined,
1261
+ proofId: undefined,
1262
+ proofReceipt: undefined,
1263
+ });
1264
+ }
1200
1265
  for (const [id, command] of Object.entries(config.checks)) {
1201
- if (active.record.checks.some((check) => check.id === id && check.status === 'passed'))
1266
+ if (active.record.checks.some((check) => check.id === id
1267
+ && check.command === command
1268
+ && (check.status === 'passed' || check.status === 'unchanged-failure')))
1202
1269
  continue;
1203
1270
  if (this.signal.aborted)
1204
1271
  return await this.terminal(active, { status: 'cancelled' });
1205
1272
  let check;
1206
1273
  try {
1207
- check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, signal: this.signal });
1274
+ check = await this.dependencies.checks.run({ id, command, cwd: worktreePath, phase: 'changed', signal: this.signal });
1208
1275
  }
1209
1276
  catch {
1210
1277
  return await this.terminal(active, { status: 'internal-error', code: 'configured-check-execution-failed' });
1211
1278
  }
1212
1279
  if (this.signal.aborted)
1213
1280
  return await this.terminal(active, { status: 'cancelled' });
1214
- const row = { id, command, status: check.status, outputSha256: sha256(check.output) };
1281
+ const outputSha256 = check.outputSha256;
1282
+ const baseline = active.record.baselineChecks?.find((candidate) => candidate.id === id && candidate.command === command);
1283
+ const status = check.status === 'failed' && baseline?.status === 'failed' && baseline.outputSha256 === outputSha256
1284
+ ? 'unchanged-failure'
1285
+ : check.status;
1286
+ const row = { id, command, status, outputSha256 };
1215
1287
  active = await this.persist(active, { checks: [...active.record.checks, row] });
1216
- if (check.status !== 'passed') {
1288
+ if (status === 'failed') {
1217
1289
  if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
1218
1290
  return active.record.reviewFeedback?.activeBatch
1219
1291
  ? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
@@ -1265,6 +1337,9 @@ export class RunIssue {
1265
1337
  || reviewedStageBinding.contentSha256 !== proofContentSha256) {
1266
1338
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
1267
1339
  }
1340
+ if (!sameCheckPolicy(active.record.checks, config.checks)) {
1341
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'configured-check-policy-drift');
1342
+ }
1268
1343
  const payload = {
1269
1344
  version: 1,
1270
1345
  canonicalRepository,
@@ -1278,7 +1353,10 @@ export class RunIssue {
1278
1353
  untrackedContentSha256: freshness.untrackedContentSha256,
1279
1354
  worktreeIdentity: freshness.worktreeIdentity,
1280
1355
  changedFiles: proofChangedFiles,
1281
- checks: active.record.checks.map((check) => ({ ...check, status: 'passed' })),
1356
+ checks: active.record.checks.map((check) => ({
1357
+ ...check,
1358
+ status: check.status === 'unchanged-failure' ? 'unchanged-failure' : 'passed',
1359
+ })),
1282
1360
  checkPolicySha256: sha256(canonicalJson(config.checks)),
1283
1361
  packageVersion: active.record.packageVersion,
1284
1362
  proofSchemaVersion: 1,
@@ -1443,12 +1521,31 @@ export class RunIssue {
1443
1521
  catch {
1444
1522
  throw new RouteInitializationUnrecoverableError();
1445
1523
  }
1446
- const issueSnapshot = issue?.state === 'OPEN' ? snapshotIssue(issue) : active.record.issueSnapshot;
1524
+ const issueSnapshot = issue?.state === 'OPEN'
1525
+ ? refreshClaimedIssueSnapshot(active.record.issueSnapshot, issue)
1526
+ : active.record.issueSnapshot;
1447
1527
  return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
1448
1528
  }
1449
1529
  async routeRun(starting, issue, frozenCriteria, worktreePath, config, issueNumber, branchName) {
1450
1530
  let active = starting;
1451
1531
  while (active.record.lifecycle === 'triaging') {
1532
+ let currentIssue;
1533
+ try {
1534
+ currentIssue = await this.readIssue(active.record.issueNumber);
1535
+ }
1536
+ catch (error) {
1537
+ if (error instanceof TransportReadError) {
1538
+ return { result: await this.invokedFailure(active, 'authorization-read-failed') };
1539
+ }
1540
+ throw error;
1541
+ }
1542
+ if (!currentIssue || !this.isAuthorizedIssue(currentIssue, active.record, config)) {
1543
+ return { result: await this.revoked(active) };
1544
+ }
1545
+ active = await this.persist(active, {
1546
+ issueSnapshot: refreshClaimedIssueSnapshot(active.record.issueSnapshot, currentIssue),
1547
+ });
1548
+ issue = structuredClone(active.record.issueSnapshot);
1452
1549
  const state = {
1453
1550
  read: async () => requireRouteExecution(active.record.routeExecution),
1454
1551
  compareAndSwap: async (expected, next) => {
@@ -1550,7 +1647,7 @@ export class RunIssue {
1550
1647
  return { result: await this.terminal(active, { status: 'internal-error', code: 'route-state-not-dispatchable' }) };
1551
1648
  }
1552
1649
  try {
1553
- if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
1650
+ if (!await this.authorized(active, config)) {
1554
1651
  return { result: await this.revoked(active) };
1555
1652
  }
1556
1653
  }
@@ -1575,7 +1672,7 @@ export class RunIssue {
1575
1672
  if (receipt.route !== 'awaiting-user')
1576
1673
  active = await this.persist(active, { lifecycle });
1577
1674
  try {
1578
- if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
1675
+ if (!await this.authorized(active, config)) {
1579
1676
  return { result: await this.revoked(active) };
1580
1677
  }
1581
1678
  }
@@ -1774,7 +1871,7 @@ export class RunIssue {
1774
1871
  packageVersion: workflow.receipt.packageVersion,
1775
1872
  workflowGeneration: structuredClone(workflow.receipt),
1776
1873
  skillHashes: structuredClone(workflow.skillHashes),
1777
- checks: [], createdAt: now, updatedAt: now,
1874
+ baselineChecks: [], checks: [], createdAt: now, updatedAt: now,
1778
1875
  };
1779
1876
  const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
1780
1877
  schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
@@ -1808,33 +1905,50 @@ export class RunIssue {
1808
1905
  throw new PostEffectStateError(active);
1809
1906
  }
1810
1907
  }
1811
- async authorized(issueNumber, runId, branchName, config) {
1812
- const issue = await this.readIssue(issueNumber);
1813
- if (!issue || issue.state !== 'OPEN')
1908
+ async authorized(active, config) {
1909
+ const issue = await this.readIssue(active.record.issueNumber);
1910
+ return !!issue && this.isAuthorizedIssue(issue, active.record, config);
1911
+ }
1912
+ isAuthorizedIssue(issue, record, config) {
1913
+ if (issue.state !== 'OPEN')
1814
1914
  return false;
1815
1915
  const labels = new Set(issue.labels);
1816
- if (!labels.has(config.github.labels.auto.name) || !labels.has(config.github.labels.running.name))
1916
+ if (!labels.has(config.github.labels.auto.name)
1917
+ || labels.has(config.github.labels.blocked.name)
1918
+ || labels.has(config.github.labels.review.name)
1919
+ || labels.has(config.github.labels.waitingHuman.name))
1817
1920
  return false;
1818
- return this.hasTrustedClaim(issue, runId, issueNumber, branchName);
1921
+ return this.hasTrustedClaim(issue, record);
1819
1922
  }
1820
- async authorizedForExactLabels(issueNumber, runId, branchName, expectedLabels) {
1821
- const issue = await this.readIssue(issueNumber);
1923
+ async authorizedForExactLabels(active, expectedLabels) {
1924
+ const issue = await this.readIssue(active.record.issueNumber);
1822
1925
  return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
1823
- && this.hasTrustedClaim(issue, runId, issueNumber, branchName);
1926
+ && this.hasTrustedClaim(issue, active.record);
1824
1927
  }
1825
- hasTrustedClaim(issue, runId, issueNumber, branchName) {
1826
- const exactBody = claimComment(runId, issueNumber, branchName);
1827
- const markerPattern = /^<!-- codex-orchestrator:run:([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):claim -->$/u;
1828
- const markers = issue.comments.filter((comment) => markerPattern.test(comment.body.split('\n')[0] ?? ''));
1928
+ hasTrustedClaim(issue, record) {
1929
+ const exactBody = claimComment(record.runId, record.issueNumber, record.branchName);
1930
+ const markers = issue.comments.filter(isClaimMarkerComment);
1829
1931
  if (issue.comments.some((comment) => {
1830
1932
  const firstLine = comment.body.split('\n')[0] ?? '';
1831
- return firstLine.startsWith(`<!-- codex-orchestrator:run:${runId}:claim`) && !markerPattern.test(firstLine);
1933
+ return firstLine.startsWith(`<!-- codex-orchestrator:run:${record.runId}:claim`) && !claimMarkerPattern.test(firstLine);
1832
1934
  }))
1833
1935
  return false;
1834
- if (markers.some((comment) => (comment.body.split('\n')[0] ?? '').match(markerPattern)?.[1] !== runId))
1936
+ const currentMarkers = markers.filter((comment) => claimRunId(comment) === record.runId);
1937
+ if (currentMarkers.some((comment) => comment.body !== exactBody
1938
+ || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.authorAssociation)))
1835
1939
  return false;
1836
- const current = markers.filter((comment) => comment.body === exactBody);
1837
- return current.length === 1 && ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(current[0].authorAssociation);
1940
+ const historicalClaims = historicalClaimCounts(record);
1941
+ for (const comment of markers) {
1942
+ const markerRunId = claimRunId(comment);
1943
+ if (markerRunId === record.runId)
1944
+ continue;
1945
+ const key = observedHistoricalClaimKeys(comment, record)
1946
+ .find((candidate) => (historicalClaims.get(candidate) ?? 0) > 0);
1947
+ if (!key)
1948
+ return false;
1949
+ historicalClaims.set(key, historicalClaims.get(key) - 1);
1950
+ }
1951
+ return currentMarkers.length === 1;
1838
1952
  }
1839
1953
  async readIssue(issueNumber) {
1840
1954
  try {
@@ -1848,7 +1962,7 @@ export class RunIssue {
1848
1962
  const batch = active.record.reviewFeedback?.activeBatch;
1849
1963
  if (!batch)
1850
1964
  return undefined;
1851
- if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
1965
+ if (!await this.authorized(active, config)) {
1852
1966
  return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-authority-revoked');
1853
1967
  }
1854
1968
  if (!this.dependencies.reviewFeedback) {
@@ -1917,9 +2031,7 @@ export class RunIssue {
1917
2031
  affectedDefectIds: [...directReview.review.affectedDefectIds],
1918
2032
  fixedRepairFindings: directReview.repairFindings.filter((finding) => finding.status === 'fixed')
1919
2033
  .map((finding) => ({ id: finding.id, affectedContracts: [...finding.affectedContracts] })),
1920
- mandatoryCoverage: directReview.stage === 'review-closure'
1921
- ? [...directReview.review.coverage]
1922
- : ['acceptance-criteria', 'correctness', 'test-quality'],
2034
+ reviewFocus: ['acceptance-criteria', 'correctness', 'test-quality'],
1923
2035
  workflowGeneration: structuredClone(active.record.workflowGeneration),
1924
2036
  repairOnly: reportRepair !== undefined,
1925
2037
  originalReportSha256: reportRepair?.originalReportSha256 ?? null,
@@ -1989,14 +2101,17 @@ export class RunIssue {
1989
2101
  const current = active.record.directReview;
1990
2102
  if (!current)
1991
2103
  return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
1992
- if (current.review.reportRepairs >= 1 || reportRepair) {
1993
- return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
2104
+ if (current.review.reportRepairs >= MAX_DIRECT_REVIEW_REPORT_REPAIRS) {
2105
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' }, 'direct-review-report-malformed');
1994
2106
  }
1995
2107
  const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
1996
2108
  active = await this.persist(active, {
1997
2109
  directReview: {
1998
2110
  ...withoutInvocation,
1999
- review: { ...withoutInvocation.review, reportRepairs: 1 },
2111
+ review: {
2112
+ ...withoutInvocation.review,
2113
+ reportRepairs: (withoutInvocation.review.reportRepairs + 1),
2114
+ },
2000
2115
  },
2001
2116
  });
2002
2117
  reportRepair = {
@@ -2071,6 +2186,89 @@ export class RunIssue {
2071
2186
  }
2072
2187
  return this.persist(starting, { directReview: withoutInvocation });
2073
2188
  }
2189
+ async captureBaselineChecks(starting, config) {
2190
+ let active = starting;
2191
+ if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0)
2192
+ return { active };
2193
+ const configuredChecks = Object.entries(config.checks);
2194
+ const reusableBaselineChecks = (active.record.baselineChecks ?? []).filter((check) => configuredChecks.some(([id, command]) => check.id === id && check.command === command));
2195
+ if (reusableBaselineChecks.length !== (active.record.baselineChecks ?? []).length) {
2196
+ active = await this.persist(active, { baselineChecks: reusableBaselineChecks });
2197
+ }
2198
+ for (const [id, command] of Object.entries(config.checks)) {
2199
+ if (active.record.baselineChecks?.some((check) => check.id === id && check.command === command))
2200
+ continue;
2201
+ if (this.signal.aborted)
2202
+ return this.terminal(active, { status: 'cancelled' });
2203
+ let check;
2204
+ try {
2205
+ check = await this.dependencies.checks.run({
2206
+ id,
2207
+ command,
2208
+ cwd: active.record.worktreePath,
2209
+ phase: 'baseline',
2210
+ signal: this.signal,
2211
+ });
2212
+ }
2213
+ catch {
2214
+ continue;
2215
+ }
2216
+ if ((await this.dependencies.git.listChangedFiles(active.record.worktreePath)).length !== 0) {
2217
+ return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'baseline-check-mutated-worktree');
2218
+ }
2219
+ active = await this.persist(active, {
2220
+ baselineChecks: [
2221
+ ...(active.record.baselineChecks ?? []),
2222
+ { id, command, status: check.status, outputSha256: check.outputSha256 },
2223
+ ],
2224
+ });
2225
+ }
2226
+ return { active };
2227
+ }
2228
+ async recoverTerminalReviewReport(active, issue, config, frozenCriteria) {
2229
+ const directReview = active.record.directReview;
2230
+ const allowLegacyMalformed = directReview?.terminalCode === undefined
2231
+ && active.record.outcomeEvidenceId === `evidence:${active.record.runId}:direct-review-report-malformed`;
2232
+ if (!issue || !directReview || active.record.intent || active.record.process
2233
+ || active.record.terminalOutcome?.status !== 'internal-error'
2234
+ || directReview.status !== 'terminal' || directReview.terminalOutcome?.status !== 'internal-error'
2235
+ || !canRecoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed })
2236
+ || !this.isAuthorizedIssue(issue, active.record, config))
2237
+ return undefined;
2238
+ let worktree;
2239
+ try {
2240
+ worktree = await this.dependencies.git.inspectWorktree({
2241
+ worktreePath: active.record.worktreePath,
2242
+ branchName: active.record.branchName,
2243
+ baseSha: active.record.baseSha,
2244
+ });
2245
+ }
2246
+ catch {
2247
+ return undefined;
2248
+ }
2249
+ if (worktree !== 'matching'
2250
+ || await this.dependencies.git.getHead(active.record.worktreePath) !== expectedImplementationHead(active.record))
2251
+ return undefined;
2252
+ const changedFiles = await this.dependencies.git.listChangedFiles(active.record.worktreePath);
2253
+ if (changedFiles.length === 0 || !active.record.routeReceipt)
2254
+ return undefined;
2255
+ const targetFingerprint = directReviewTargetFingerprint({
2256
+ snapshot: await this.dependencies.git.snapshot(active.record.worktreePath),
2257
+ changedFiles,
2258
+ routeDecisionSha256: active.record.routeReceipt.decisionSha256,
2259
+ workflowGenerationHash: active.record.workflowGeneration.generationHash,
2260
+ cycle: active.record.cycle,
2261
+ frozenCriteria,
2262
+ });
2263
+ if (targetFingerprint !== directReview.targetFingerprint)
2264
+ return undefined;
2265
+ return this.persist(active, {
2266
+ lifecycle: 'implementing',
2267
+ directReview: recoverTerminalDirectReviewReport(directReview, { allowLegacyMalformed }),
2268
+ terminalOutcome: undefined,
2269
+ outcomeEvidenceId: undefined,
2270
+ });
2271
+ }
2074
2272
  async startNextCycle(active, findings, sources) {
2075
2273
  if (active.record.directReview?.status === 'clear') {
2076
2274
  const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
@@ -2204,14 +2402,109 @@ export class RunIssue {
2204
2402
  async revoked(active) {
2205
2403
  return this.terminal(await this.clearIntent(active), { status: 'blocked', kind: 'safety', resumable: true });
2206
2404
  }
2207
- async invokedFailure(active, code) {
2208
- const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary: 'Publication delivery requires reconciliation.' });
2405
+ async invokedFailure(active, code, summary = 'Publication delivery requires reconciliation.') {
2406
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary });
2209
2407
  return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
2210
2408
  }
2211
2409
  async terminal(active, outcome, evidenceCode = outcome.status, retainIntent = false) {
2212
2410
  if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
2213
2411
  return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
2214
2412
  }
2413
+ if (outcome.status === 'blocked') {
2414
+ return this.publishBlockedTerminal(active, outcome, evidenceCode);
2415
+ }
2416
+ return this.persistTerminal(active, outcome, evidenceCode, retainIntent);
2417
+ }
2418
+ async publishBlockedTerminal(starting, outcome, evidenceCode) {
2419
+ let active = starting;
2420
+ const autoBlocked = [active.config.github.labels.auto.name, active.config.github.labels.blocked.name].sort();
2421
+ const blockedOnly = [active.config.github.labels.blocked.name];
2422
+ let expected = autoBlocked;
2423
+ const existing = active.record.intent;
2424
+ if (!existing) {
2425
+ active = await this.persist(active, { intent: {
2426
+ kind: 'blocked-labels',
2427
+ issueNumber: active.record.issueNumber,
2428
+ expected,
2429
+ blockKind: outcome.kind,
2430
+ resumable: outcome.resumable,
2431
+ evidenceCode,
2432
+ } });
2433
+ }
2434
+ else if (existing.kind !== 'blocked-labels'
2435
+ || existing.issueNumber !== active.record.issueNumber
2436
+ || existing.blockKind !== outcome.kind
2437
+ || existing.resumable !== outcome.resumable
2438
+ || existing.evidenceCode !== evidenceCode) {
2439
+ return this.invokedFailure(active, 'blocked-labels-intent-diverged');
2440
+ }
2441
+ else {
2442
+ expected = existing.expected;
2443
+ if (!sameStrings(expected, autoBlocked) && !sameStrings(expected, blockedOnly) && expected.length !== 0) {
2444
+ return this.invokedFailure(active, 'blocked-labels-intent-diverged');
2445
+ }
2446
+ }
2447
+ let issue = await this.readIssue(active.record.issueNumber);
2448
+ if (!issue)
2449
+ return this.invokedFailure(active, 'blocked-labels-issue-missing');
2450
+ let projection = blockedLabelProjection(issue.labels, active.config);
2451
+ if (issue.state === 'OPEN' && projection.status === 'transition') {
2452
+ if (!this.dependencies.issues.transitionToBlocked) {
2453
+ return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
2454
+ }
2455
+ try {
2456
+ await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
2457
+ }
2458
+ catch {
2459
+ return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
2460
+ }
2461
+ issue = await this.readIssue(active.record.issueNumber);
2462
+ projection = issue ? blockedLabelProjection(issue.labels, active.config) : { status: 'diverged' };
2463
+ }
2464
+ if (!issue)
2465
+ return this.invokedFailure(active, 'blocked-labels-observation-missing');
2466
+ if (issue.state === 'OPEN') {
2467
+ if (projection.status !== 'settled') {
2468
+ return this.invokedFailure(active, 'blocked-labels-observation-diverged');
2469
+ }
2470
+ if (!sameStrings(expected, projection.expected)) {
2471
+ expected = projection.expected;
2472
+ const intent = active.record.intent;
2473
+ if (intent?.kind !== 'blocked-labels')
2474
+ return this.invokedFailure(active, 'blocked-labels-intent-diverged');
2475
+ try {
2476
+ active = await this.persist(active, { intent: { ...intent, expected } });
2477
+ }
2478
+ catch {
2479
+ throw new PostEffectStateError(active);
2480
+ }
2481
+ }
2482
+ }
2483
+ try {
2484
+ return await this.persistTerminal(active, outcome, evidenceCode, false);
2485
+ }
2486
+ catch {
2487
+ throw new PostEffectStateError(active);
2488
+ }
2489
+ }
2490
+ async reconcilePersistedBlockedTerminal(active, issue, outcome) {
2491
+ if (!this.dependencies.issues.transitionToBlocked) {
2492
+ return this.invokedFailure(active, 'blocked-labels-transition-unavailable');
2493
+ }
2494
+ try {
2495
+ await this.dependencies.issues.transitionToBlocked(active.record.issueNumber, blockedLabelPolicy(active.config));
2496
+ }
2497
+ catch {
2498
+ return this.invokedFailure(active, 'blocked-labels-delivery-unknown');
2499
+ }
2500
+ const observation = await this.readIssue(active.record.issueNumber);
2501
+ if (!observation || observation.state !== 'OPEN'
2502
+ || blockedLabelProjection(observation.labels, active.config).status !== 'settled') {
2503
+ return this.invokedFailure(active, 'blocked-labels-observation-diverged');
2504
+ }
2505
+ return publicOutcome(outcome);
2506
+ }
2507
+ async persistTerminal(active, outcome, evidenceCode, retainIntent) {
2215
2508
  const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
2216
2509
  const terminalOutcome = { ...outcome, evidencePath: evidence.path };
2217
2510
  const changes = {
@@ -2223,7 +2516,7 @@ export class RunIssue {
2223
2516
  if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
2224
2517
  changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
2225
2518
  ? { status: 'blocked', kind: outcome.kind }
2226
- : { status: outcome.status });
2519
+ : { status: outcome.status }, outcome.status === 'internal-error' ? outcome.code : undefined);
2227
2520
  }
2228
2521
  if (active.record.waitingHuman && (active.record.lifecycle === 'waiting-human' || active.record.waitingHuman.phase === 'resumed')) {
2229
2522
  if (active.record.waitingHuman.phase === 'resumed') {
@@ -2285,6 +2578,63 @@ class PostEffectStateError extends Error {
2285
2578
  this.active = active;
2286
2579
  }
2287
2580
  }
2581
+ const claimMarkerPattern = /^<!-- codex-orchestrator:run:([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):claim -->$/u;
2582
+ function isClaimMarkerComment(comment) {
2583
+ return claimMarkerPattern.test(comment.body.split('\n')[0] ?? '');
2584
+ }
2585
+ function claimRunId(comment) {
2586
+ return (comment.body.split('\n')[0] ?? '').match(claimMarkerPattern)?.[1];
2587
+ }
2588
+ function trustedHistoricalClaimBodyKey(comment, issueNumber, branchName) {
2589
+ const runId = claimRunId(comment);
2590
+ if (!runId || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.authorAssociation))
2591
+ return undefined;
2592
+ if (comment.body !== claimComment(runId, issueNumber, branchName))
2593
+ return undefined;
2594
+ return `${comment.authorAssociation}\u0000${comment.body}`;
2595
+ }
2596
+ function frozenHistoricalClaimKey(comment, issueNumber, branchName) {
2597
+ const bodyKey = trustedHistoricalClaimBodyKey(comment, issueNumber, branchName);
2598
+ if (!bodyKey)
2599
+ return undefined;
2600
+ return comment.id ? `id\u0000${comment.id}\u0000${bodyKey}` : `legacy\u0000${bodyKey}`;
2601
+ }
2602
+ function observedHistoricalClaimKeys(comment, record) {
2603
+ const bodyKey = trustedHistoricalClaimBodyKey(comment, record.issueNumber, record.branchName);
2604
+ if (!bodyKey)
2605
+ return [];
2606
+ const keys = comment.id ? [`id\u0000${comment.id}\u0000${bodyKey}`] : [];
2607
+ if (timestampAtOrBefore(comment.createdAt, record.createdAt)
2608
+ && timestampAtOrBefore(comment.updatedAt, record.createdAt)) {
2609
+ keys.push(`legacy\u0000${bodyKey}`);
2610
+ }
2611
+ return keys;
2612
+ }
2613
+ function timestampAtOrBefore(value, cutoff) {
2614
+ if (!value)
2615
+ return false;
2616
+ const timestamp = Date.parse(value);
2617
+ return !Number.isNaN(timestamp) && timestamp <= Date.parse(cutoff);
2618
+ }
2619
+ function historicalClaimCounts(record) {
2620
+ const counts = new Map();
2621
+ for (const comment of record.issueSnapshot.comments ?? []) {
2622
+ const key = frozenHistoricalClaimKey(comment, record.issueNumber, record.branchName);
2623
+ if (!key || claimRunId(comment) === record.runId)
2624
+ continue;
2625
+ counts.set(key, (counts.get(key) ?? 0) + 1);
2626
+ }
2627
+ return counts;
2628
+ }
2629
+ function refreshClaimedIssueSnapshot(baseline, issue) {
2630
+ return {
2631
+ ...structuredClone(baseline),
2632
+ comments: [
2633
+ ...(baseline.comments ?? []).filter(isClaimMarkerComment),
2634
+ ...issue.comments.filter((comment) => !isClaimMarkerComment(comment)),
2635
+ ],
2636
+ };
2637
+ }
2288
2638
  function snapshotIssue(issue) {
2289
2639
  if (issue.state !== 'OPEN')
2290
2640
  throw new Error('cannot snapshot a closed issue');
@@ -2406,6 +2756,36 @@ function sameStrings(left, right) {
2406
2756
  const b = [...right].sort();
2407
2757
  return a.length === b.length && a.every((value, index) => value === b[index]);
2408
2758
  }
2759
+ function blockedLabelPolicy(config) {
2760
+ return {
2761
+ auto: config.github.labels.auto.name,
2762
+ running: config.github.labels.running.name,
2763
+ blocked: config.github.labels.blocked.name,
2764
+ review: config.github.labels.review.name,
2765
+ waitingHuman: config.github.labels.waitingHuman.name,
2766
+ };
2767
+ }
2768
+ function blockedLabelProjection(labels, config) {
2769
+ const policy = blockedLabelPolicy(config);
2770
+ const present = new Set(labels);
2771
+ if (present.has(policy.review) || present.has(policy.waitingHuman))
2772
+ return { status: 'diverged' };
2773
+ const auto = present.has(policy.auto);
2774
+ const running = present.has(policy.running);
2775
+ const blocked = present.has(policy.blocked);
2776
+ if (running || (auto && !blocked))
2777
+ return { status: 'transition' };
2778
+ if (blocked)
2779
+ return { status: 'settled', expected: auto ? [policy.auto, policy.blocked].sort() : [policy.blocked] };
2780
+ return { status: 'settled', expected: [] };
2781
+ }
2782
+ function sameCheckPolicy(checks, policy) {
2783
+ const expected = Object.entries(policy).sort(([left], [right]) => left.localeCompare(right));
2784
+ const actual = checks.map((check) => [check.id, check.command])
2785
+ .sort(([left], [right]) => left.localeCompare(right));
2786
+ return actual.length === expected.length
2787
+ && actual.every(([id, command], index) => id === expected[index]?.[0] && command === expected[index]?.[1]);
2788
+ }
2409
2789
  function sameFreshness(left, right) {
2410
2790
  return left.headSha === right.headSha
2411
2791
  && left.indexTreeSha === right.indexTreeSha
@@ -2422,6 +2802,21 @@ function commentsWithMarker(issue, marker) {
2422
2802
  function sortedUnique(values) {
2423
2803
  return [...new Set(values)].sort();
2424
2804
  }
2805
+ function worktreeCreationFailureSummary(error) {
2806
+ return boundedLocalGitFailureSummary('Local Git worktree creation failed; correct the local Git state and retry the same run.', error);
2807
+ }
2808
+ function claimedWorktreeInspectionFailureSummary(error) {
2809
+ return boundedLocalGitFailureSummary('The claimed worktree could not be inspected; correct or preserve the local artifact and retry the same run.', error);
2810
+ }
2811
+ function boundedLocalGitFailureSummary(prefix, error) {
2812
+ const raw = error instanceof Error ? error.message : String(error);
2813
+ const diagnostic = raw
2814
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, ' ')
2815
+ .trim();
2816
+ if (diagnostic.length === 0)
2817
+ return prefix;
2818
+ return `${prefix}\n${diagnostic.slice(0, 4 * 1024 - prefix.length - 1)}`;
2819
+ }
2425
2820
  function assertUuid(value) {
2426
2821
  if (typeof value !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value))
2427
2822
  throw new Error('runId is invalid');