codex-orchestrator 2.0.3 → 2.0.5

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 (161) hide show
  1. package/CHANGELOG.md +51 -427
  2. package/README.md +161 -37
  3. package/dist/src/index.d.ts +1 -1
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/v2/acceptance-proof.d.ts +5 -0
  6. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  7. package/dist/src/v2/acceptance-proof.js +10 -2
  8. package/dist/src/v2/acceptance-proof.js.map +1 -1
  9. package/dist/src/v2/adapters/gh-issue-adapter.d.ts.map +1 -1
  10. package/dist/src/v2/adapters/gh-issue-adapter.js +6 -7
  11. package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
  12. package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts +7 -1
  13. package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts.map +1 -1
  14. package/dist/src/v2/adapters/gh-pull-request-adapter.js +288 -0
  15. package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
  16. package/dist/src/v2/adapters/pull-requests.d.ts +69 -0
  17. package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
  18. package/dist/src/v2/adapters/pull-requests.js +48 -0
  19. package/dist/src/v2/adapters/pull-requests.js.map +1 -1
  20. package/dist/src/v2/adapters/worktree.d.ts +1 -0
  21. package/dist/src/v2/adapters/worktree.d.ts.map +1 -1
  22. package/dist/src/v2/adapters/worktree.js +10 -1
  23. package/dist/src/v2/adapters/worktree.js.map +1 -1
  24. package/dist/src/v2/cli-contract.d.ts +3 -3
  25. package/dist/src/v2/cli-contract.d.ts.map +1 -1
  26. package/dist/src/v2/cli-contract.js +1 -3
  27. package/dist/src/v2/cli-contract.js.map +1 -1
  28. package/dist/src/v2/cli.d.ts +33 -0
  29. package/dist/src/v2/cli.d.ts.map +1 -0
  30. package/dist/src/v2/{candidate-cli.js → cli.js} +55 -39
  31. package/dist/src/v2/cli.js.map +1 -0
  32. package/dist/src/v2/code-review-report.d.ts +1 -1
  33. package/dist/src/v2/code-review-report.d.ts.map +1 -1
  34. package/dist/src/v2/code-review-report.js +2 -2
  35. package/dist/src/v2/code-review-report.js.map +1 -1
  36. package/dist/src/v2/codex-process.d.ts.map +1 -1
  37. package/dist/src/v2/codex-process.js +12 -1
  38. package/dist/src/v2/codex-process.js.map +1 -1
  39. package/dist/src/v2/config.d.ts +2 -3
  40. package/dist/src/v2/config.d.ts.map +1 -1
  41. package/dist/src/v2/config.js +0 -3
  42. package/dist/src/v2/config.js.map +1 -1
  43. package/dist/src/v2/contained-report-operation.d.ts +2 -2
  44. package/dist/src/v2/contained-report-operation.d.ts.map +1 -1
  45. package/dist/src/v2/contained-report-operation.js +1 -1
  46. package/dist/src/v2/contained-report-operation.js.map +1 -1
  47. package/dist/src/v2/containment.d.ts +15 -2
  48. package/dist/src/v2/containment.d.ts.map +1 -1
  49. package/dist/src/v2/containment.js +43 -6
  50. package/dist/src/v2/containment.js.map +1 -1
  51. package/dist/src/v2/direct-delivery.d.ts +5 -10
  52. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  53. package/dist/src/v2/direct-delivery.js +32 -91
  54. package/dist/src/v2/direct-delivery.js.map +1 -1
  55. package/dist/src/v2/proof-report.d.ts.map +1 -1
  56. package/dist/src/v2/proof-report.js +55 -29
  57. package/dist/src/v2/proof-report.js.map +1 -1
  58. package/dist/src/v2/review-feedback-coordinator.d.ts +54 -0
  59. package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -0
  60. package/dist/src/v2/review-feedback-coordinator.js +245 -0
  61. package/dist/src/v2/review-feedback-coordinator.js.map +1 -0
  62. package/dist/src/v2/review-feedback.d.ts +127 -0
  63. package/dist/src/v2/review-feedback.d.ts.map +1 -0
  64. package/dist/src/v2/review-feedback.js +436 -0
  65. package/dist/src/v2/review-feedback.js.map +1 -0
  66. package/dist/src/v2/run-issue.d.ts +63 -9
  67. package/dist/src/v2/run-issue.d.ts.map +1 -1
  68. package/dist/src/v2/run-issue.js +798 -78
  69. package/dist/src/v2/run-issue.js.map +1 -1
  70. package/dist/src/v2/run-store.d.ts +49 -5
  71. package/dist/src/v2/run-store.d.ts.map +1 -1
  72. package/dist/src/v2/run-store.js +138 -44
  73. package/dist/src/v2/run-store.js.map +1 -1
  74. package/dist/src/v2/runtime.d.ts +40 -3
  75. package/dist/src/v2/runtime.d.ts.map +1 -1
  76. package/dist/src/v2/runtime.js +245 -52
  77. package/dist/src/v2/runtime.js.map +1 -1
  78. package/dist/src/v2/setup-cli.d.ts.map +1 -1
  79. package/dist/src/v2/setup-cli.js +4 -11
  80. package/dist/src/v2/setup-cli.js.map +1 -1
  81. package/dist/src/v2/setup-runtime.d.ts.map +1 -1
  82. package/dist/src/v2/setup-runtime.js +1 -61
  83. package/dist/src/v2/setup-runtime.js.map +1 -1
  84. package/dist/src/v2/setup-store.d.ts +0 -5
  85. package/dist/src/v2/setup-store.d.ts.map +1 -1
  86. package/dist/src/v2/setup-store.js +3 -106
  87. package/dist/src/v2/setup-store.js.map +1 -1
  88. package/dist/src/v2/setup.d.ts +6 -46
  89. package/dist/src/v2/setup.d.ts.map +1 -1
  90. package/dist/src/v2/setup.js +12 -294
  91. package/dist/src/v2/setup.js.map +1 -1
  92. package/dist/src/v2/workflow-assets.d.ts +19 -11
  93. package/dist/src/v2/workflow-assets.d.ts.map +1 -1
  94. package/dist/src/v2/workflow-assets.js +132 -40
  95. package/dist/src/v2/workflow-assets.js.map +1 -1
  96. package/docs/deep-dive.md +328 -56
  97. package/internal-workflow/docs/agents/bugfix-quality-gate.md +11 -0
  98. package/internal-workflow/docs/agents/coding-skill-routing.md +116 -196
  99. package/internal-workflow/docs/agents/contract-test-ledger.md +11 -1
  100. package/internal-workflow/docs/agents/review-gates.md +32 -39
  101. package/internal-workflow/docs/agents/review-protocol.md +75 -147
  102. package/internal-workflow/evals/coding-skill-evals.json +84 -0
  103. package/internal-workflow/manifest.json +1 -1
  104. package/internal-workflow/operations/acceptance-proof/SKILL.md +7 -1
  105. package/internal-workflow/operations/ambiguity-review/SKILL.md +2 -0
  106. package/internal-workflow/operations/code-review/SKILL.md +21 -1
  107. package/internal-workflow/operations/implementation/SKILL.md +22 -1
  108. package/internal-workflow/operations/spec-author/SKILL.md +10 -1
  109. package/internal-workflow/operations/spec-review/SKILL.md +10 -1
  110. package/internal-workflow/operations/triage/SKILL.md +10 -1
  111. package/internal-workflow/schemas/code-review-v1.json +1 -1
  112. package/internal-workflow/schemas/proof-report-v1.json +1 -1
  113. package/internal-workflow/skills/agent-auto/SKILL.md +6 -1
  114. package/internal-workflow/skills/code-debugger/SKILL.md +122 -0
  115. package/internal-workflow/skills/code-debugger/agents/openai.yaml +7 -0
  116. package/internal-workflow/skills/code-review/SKILL.md +51 -17
  117. package/internal-workflow/skills/code-review/references/cleanup-lens.md +52 -0
  118. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +15 -6
  119. package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +1 -1
  120. package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +2 -2
  121. package/internal-workflow/skills/implementation-spec-review/SKILL.md +108 -204
  122. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
  123. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +93 -0
  124. package/internal-workflow/skills/small-task-implementer/SKILL.md +15 -8
  125. package/internal-workflow/skills/spec-implementer/SKILL.md +101 -172
  126. package/internal-workflow/skills/spec-implementer/evals/evals.json +30 -0
  127. package/internal-workflow/skills/spec-implementer/references/review-loop.md +100 -0
  128. package/internal-workflow/skills/tdd/SKILL.md +20 -6
  129. package/internal-workflow/skills/tdd/agents/openai.yaml +2 -2
  130. package/internal-workflow/skills/tdd/evals/evals.json +18 -0
  131. package/internal-workflow/skills/tdd/mocking.md +3 -42
  132. package/internal-workflow/skills/tdd/refactoring.md +6 -8
  133. package/package.json +9 -6
  134. package/dist/src/v2/adapters/target-activity-fence.d.ts +0 -23
  135. package/dist/src/v2/adapters/target-activity-fence.d.ts.map +0 -1
  136. package/dist/src/v2/adapters/target-activity-fence.js +0 -249
  137. package/dist/src/v2/adapters/target-activity-fence.js.map +0 -1
  138. package/dist/src/v2/candidate-cli.d.ts +0 -26
  139. package/dist/src/v2/candidate-cli.d.ts.map +0 -1
  140. package/dist/src/v2/candidate-cli.js.map +0 -1
  141. package/dist/src/v2/legacy-cutover.d.ts +0 -52
  142. package/dist/src/v2/legacy-cutover.d.ts.map +0 -1
  143. package/dist/src/v2/legacy-cutover.js +0 -87
  144. package/dist/src/v2/legacy-cutover.js.map +0 -1
  145. package/internal-workflow/docs/agents/artifact-review-loop.md +0 -267
  146. package/internal-workflow/docs/agents/implementation-review-loop.md +0 -302
  147. package/internal-workflow/operations/cleanup-review/SKILL.md +0 -3
  148. package/internal-workflow/operations/spec-implementation/SKILL.md +0 -3
  149. package/internal-workflow/profiles/implementer_deep.toml +0 -9
  150. package/internal-workflow/profiles/researcher_standard.toml +0 -9
  151. package/internal-workflow/profiles/reviewer_fast.toml +0 -9
  152. package/internal-workflow/skills/cleanup-review/SKILL.md +0 -84
  153. package/internal-workflow/skills/cleanup-review/agents/openai.yaml +0 -6
  154. package/internal-workflow/skills/codebase-design/DEEPENING.md +0 -35
  155. package/internal-workflow/skills/codebase-design/DESIGN-IT-TWICE.md +0 -50
  156. package/internal-workflow/skills/codebase-design/SKILL.md +0 -82
  157. package/internal-workflow/skills/codebase-design/agents/openai.yaml +0 -6
  158. package/internal-workflow/skills/research/SKILL.md +0 -107
  159. package/internal-workflow/skills/research/agents/openai.yaml +0 -6
  160. package/internal-workflow/skills/ui-evidence-proof/SKILL.md +0 -123
  161. package/internal-workflow/skills/ui-evidence-proof/agents/openai.yaml +0 -6
@@ -3,11 +3,12 @@ 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, createLegacyBypassDirectReview, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
7
- import { ProofQuiescenceError } from './acceptance-proof.js';
8
- import { RouteMigrationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
6
+ import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
7
+ import { ProofLaunchAuthorizationError, ProofQuiescenceError } from './acceptance-proof.js';
8
+ import { RouteInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
9
9
  import { initialRouteExecution, } from './route-coordinator.js';
10
10
  import { downstreamLifecycleForRoute, validateRouteTransition, validateTrustedAnswerResumeTransition, } from './route-decision.js';
11
+ import { activateReviewFeedback, blockReviewFeedback, bootstrapReviewFeedback, markReviewFeedbackPublishing, markReviewFeedbackRepairing, markReviewFeedbackVerified, projectReviewFeedbackBatch, publishReviewFeedback, reserveNextReviewFeedbackRound, } from './review-feedback.js';
11
12
  export class OwnerLockSafetyError extends Error {
12
13
  }
13
14
  export class OwnerLockContentionError extends Error {
@@ -249,10 +250,17 @@ export class RunIssue {
249
250
  return await this.publicationDiverged(active, 'terminal-labels-diverged');
250
251
  }
251
252
  const evidence = await this.dependencies.writeEvidence({ runId, code: 'review-ready', summary: pullRequest.url });
252
- const outcome = { status: 'review-ready', pullRequestUrl: pullRequest.url, evidencePath: evidence.path };
253
+ const outcome = { status: 'review-ready', pullRequestUrl: pullRequest.url, evidencePath: evidence.path, continuationEpoch: commitSha };
253
254
  try {
254
255
  await this.persist(active, {
255
256
  lifecycle: 'review-ready', intent: undefined, outcomeEvidenceId: evidence.id, terminalOutcome: outcome,
257
+ ...(active.record.routeReceipt?.route === 'direct' ? {
258
+ reviewFeedback: bootstrapReviewFeedback(active.record.reviewFeedback ?? {
259
+ version: 1, phase: 'bootstrap-required', consumedSourceIds: [], previousPublishedHeadSha: null,
260
+ repairRound: 0, activeBatch: null, implementationInvocation: null, history: [],
261
+ verifiedReceipt: null, terminal: null,
262
+ }, commitSha, []),
263
+ } : {}),
256
264
  ...(active.record.waitingHuman ? { waitingHuman: terminalWaiting(active.record.waitingHuman, { status: 'review-ready' }) } : {}),
257
265
  });
258
266
  }
@@ -264,9 +272,409 @@ export class RunIssue {
264
272
  publicationDiverged(active, code) {
265
273
  return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, code);
266
274
  }
275
+ async updateExistingPullRequest(starting, config, issueNumber) {
276
+ let active = starting;
277
+ const feedback = active.record.reviewFeedback;
278
+ const batch = feedback?.activeBatch;
279
+ const coordinator = this.dependencies.reviewFeedback;
280
+ if (!feedback || feedback.phase !== 'publishing' || !batch || !coordinator
281
+ || !this.dependencies.pullRequests.listConversationComments || !this.dependencies.pullRequests.postConversationComment) {
282
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-contract-missing');
283
+ }
284
+ const oldHead = batch.priorPublishedHeadSha;
285
+ const message = `fix: address review feedback for #${issueNumber}`;
286
+ let head = await this.dependencies.git.getHead(active.record.worktreePath);
287
+ let remote = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
288
+ if (!active.record.intent) {
289
+ if (head !== oldHead || remote !== oldHead) {
290
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-update-baseline-diverged');
291
+ }
292
+ }
293
+ if (!active.record.intent || active.record.intent.kind === 'review-update-commit') {
294
+ if (!active.record.intent) {
295
+ active = await this.persist(active, { intent: {
296
+ kind: 'review-update-commit', batchId: batch.batchId, parentSha: oldHead,
297
+ treeSha: await this.dependencies.git.getTreeSha(active.record.worktreePath), message,
298
+ } });
299
+ }
300
+ const intent = active.record.intent;
301
+ if (intent?.kind !== 'review-update-commit' || intent.batchId !== batch.batchId
302
+ || intent.parentSha !== oldHead || intent.message !== message) {
303
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-commit-intent-diverged');
304
+ }
305
+ head = await this.dependencies.git.getHead(active.record.worktreePath);
306
+ if (head === intent.parentSha) {
307
+ if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
308
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
309
+ }
310
+ const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
311
+ const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-precommit-revalidation-failed');
312
+ if (validationFailure)
313
+ return validationFailure;
314
+ if (await this.dependencies.git.getTreeSha(active.record.worktreePath) !== intent.treeSha) {
315
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-commit-tree-diverged');
316
+ }
317
+ try {
318
+ head = await this.dependencies.git.commit({ worktreePath: active.record.worktreePath, message: intent.message });
319
+ }
320
+ catch {
321
+ return this.invokedFailure(active, 'review-feedback-commit-delivery-unknown');
322
+ }
323
+ }
324
+ const commit = await this.dependencies.git.inspectHead(active.record.worktreePath);
325
+ if (commit.sha !== head || commit.parentSha !== intent.parentSha || commit.treeSha !== intent.treeSha || commit.message !== intent.message) {
326
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-commit-observation-diverged');
327
+ }
328
+ active = await this.persist(active, { intent: {
329
+ kind: 'review-update-push', batchId: batch.batchId, branch: active.record.branchName,
330
+ priorRemoteSha: oldHead, sha: commit.sha, treeSha: commit.treeSha,
331
+ } });
332
+ }
333
+ const commit = await this.dependencies.git.inspectHead(active.record.worktreePath);
334
+ const recordedCommit = active.record.intent?.kind === 'review-update-push' ? active.record.intent : undefined;
335
+ if (commit.parentSha !== oldHead || commit.message !== message
336
+ || (recordedCommit && (commit.sha !== recordedCommit.sha || commit.treeSha !== recordedCommit.treeSha))) {
337
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-update-commit-identity-diverged');
338
+ }
339
+ head = commit.sha;
340
+ if (!active.record.intent || active.record.intent.kind === 'review-update-push') {
341
+ if (!active.record.intent)
342
+ active = await this.persist(active, { intent: {
343
+ kind: 'review-update-push', batchId: batch.batchId, branch: active.record.branchName,
344
+ priorRemoteSha: oldHead, sha: head, treeSha: commit.treeSha,
345
+ } });
346
+ const intent = active.record.intent;
347
+ if (intent?.kind !== 'review-update-push' || intent.batchId !== batch.batchId || intent.sha !== head
348
+ || intent.treeSha !== commit.treeSha || intent.branch !== active.record.branchName || intent.priorRemoteSha !== oldHead) {
349
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-push-intent-diverged');
350
+ }
351
+ remote = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
352
+ if (remote === oldHead) {
353
+ if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
354
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
355
+ }
356
+ const validation = await coordinator.revalidate({ batch, epoch: 'pre-update', expectedHeadSha: oldHead });
357
+ const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-prepush-revalidation-failed');
358
+ if (validationFailure)
359
+ return validationFailure;
360
+ try {
361
+ await this.dependencies.git.push({ worktreePath: active.record.worktreePath, branchName: active.record.branchName });
362
+ }
363
+ catch {
364
+ return this.invokedFailure(active, 'review-feedback-push-delivery-unknown');
365
+ }
366
+ remote = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
367
+ }
368
+ if (remote !== head)
369
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-push-observation-diverged');
370
+ }
371
+ const postPush = await coordinator.revalidate({ batch, epoch: 'post-push', expectedHeadSha: head });
372
+ const postPushFailure = await this.mapFeedbackRevalidation(active, postPush, 'review-feedback-postpush-revalidation-failed');
373
+ if (postPushFailure)
374
+ return postPushFailure;
375
+ const marker = `<!-- codex-orchestrator:run:${active.record.runId}:review-feedback:${batch.batchId} -->`;
376
+ const body = [
377
+ marker,
378
+ '',
379
+ `Addressed frozen review feedback batch ${batch.batchId}.`,
380
+ `Updated head: ${head}`,
381
+ 'Affected Closure, configured checks, and Acceptance Proof passed.',
382
+ 'Review threads remain for human resolution.',
383
+ ].join('\n');
384
+ if (active.record.intent?.kind === 'review-update-push') {
385
+ active = await this.persist(active, { intent: {
386
+ kind: 'review-summary', batchId: batch.batchId, pullRequestNumber: batch.pullRequest.number,
387
+ pullRequestNodeId: batch.pullRequest.nodeId, marker, bodySha256: sha256(body), epochHeadSha: head,
388
+ } });
389
+ }
390
+ let summaryId = '';
391
+ if (!active.record.intent || active.record.intent.kind === 'review-summary') {
392
+ if (!active.record.intent)
393
+ active = await this.persist(active, { intent: {
394
+ kind: 'review-summary', batchId: batch.batchId, pullRequestNumber: batch.pullRequest.number,
395
+ pullRequestNodeId: batch.pullRequest.nodeId, marker, bodySha256: sha256(body), epochHeadSha: head,
396
+ } });
397
+ const intent = active.record.intent;
398
+ if (intent?.kind !== 'review-summary' || intent.batchId !== batch.batchId
399
+ || intent.pullRequestNumber !== batch.pullRequest.number || intent.pullRequestNodeId !== batch.pullRequest.nodeId
400
+ || intent.marker !== marker || intent.bodySha256 !== sha256(body) || intent.epochHeadSha !== head) {
401
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-intent-diverged');
402
+ }
403
+ const validation = await coordinator.revalidate({ batch, epoch: 'post-push', expectedHeadSha: head });
404
+ const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-summary-revalidation-failed');
405
+ if (validationFailure)
406
+ return validationFailure;
407
+ let matches = (await this.dependencies.pullRequests.listConversationComments(batch.pullRequest.number))
408
+ .filter((comment) => comment.body.split('\n')[0] === marker);
409
+ if (matches.some((comment) => comment.body !== body) || matches.length > 1) {
410
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-diverged');
411
+ }
412
+ if (matches.length === 0) {
413
+ if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
414
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
415
+ }
416
+ try {
417
+ await this.dependencies.pullRequests.postConversationComment(batch.pullRequest.number, body);
418
+ }
419
+ catch {
420
+ return this.invokedFailure(active, 'review-feedback-summary-delivery-unknown');
421
+ }
422
+ matches = (await this.dependencies.pullRequests.listConversationComments(batch.pullRequest.number))
423
+ .filter((comment) => comment.body.split('\n')[0] === marker);
424
+ }
425
+ if (matches.length !== 1 || matches[0].body !== body)
426
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-observation-diverged');
427
+ summaryId = matches[0].id;
428
+ active = await this.persist(active, { intent: {
429
+ kind: 'review-final-labels', issueNumber, batchId: batch.batchId,
430
+ pullRequestNumber: batch.pullRequest.number, pullRequestNodeId: batch.pullRequest.nodeId,
431
+ epochHeadSha: head, expected: [config.github.labels.review.name],
432
+ } });
433
+ }
434
+ if (!summaryId) {
435
+ const matches = (await this.dependencies.pullRequests.listConversationComments(batch.pullRequest.number))
436
+ .filter((comment) => comment.body.split('\n')[0] === marker && comment.body === body);
437
+ if (matches.length !== 1)
438
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-summary-missing');
439
+ summaryId = matches[0].id;
440
+ }
441
+ const finalLabels = [config.github.labels.review.name];
442
+ if (!active.record.intent || active.record.intent.kind === 'review-final-labels') {
443
+ if (!active.record.intent)
444
+ active = await this.persist(active, { intent: {
445
+ kind: 'review-final-labels', issueNumber, batchId: batch.batchId,
446
+ pullRequestNumber: batch.pullRequest.number, pullRequestNodeId: batch.pullRequest.nodeId,
447
+ epochHeadSha: head, expected: finalLabels,
448
+ } });
449
+ const intent = active.record.intent;
450
+ if (intent?.kind !== 'review-final-labels' || intent.issueNumber !== issueNumber
451
+ || intent.batchId !== batch.batchId || intent.pullRequestNumber !== batch.pullRequest.number
452
+ || intent.pullRequestNodeId !== batch.pullRequest.nodeId || intent.epochHeadSha !== head
453
+ || !sameStrings(intent.expected, finalLabels)) {
454
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-final-labels-intent-diverged');
455
+ }
456
+ const validation = await coordinator.revalidate({ batch, epoch: 'post-push', expectedHeadSha: head });
457
+ const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-final-labels-revalidation-failed');
458
+ if (validationFailure)
459
+ return validationFailure;
460
+ let issue = await this.readIssue(issueNumber);
461
+ if (!issue || !sameStrings(issue.labels, finalLabels)) {
462
+ if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
463
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-publication-authority-revoked');
464
+ }
465
+ try {
466
+ await this.dependencies.issues.setLabels(issueNumber, finalLabels);
467
+ }
468
+ catch {
469
+ return this.invokedFailure(active, 'review-feedback-final-labels-delivery-unknown');
470
+ }
471
+ issue = await this.readIssue(issueNumber);
472
+ }
473
+ if (!issue || !sameStrings(issue.labels, finalLabels))
474
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-final-labels-diverged');
475
+ }
476
+ const receipt = {
477
+ kind: 'published',
478
+ batchId: batch.batchId,
479
+ sourceIds: batch.sources.map((source) => source.sourceId),
480
+ priorHeadSha: oldHead,
481
+ publishedHeadSha: head,
482
+ pullRequestNumber: batch.pullRequest.number,
483
+ summaryCommentId: summaryId,
484
+ publishedAt: this.timestamp(),
485
+ };
486
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'review-ready', summary: active.record.terminalOutcome?.status === 'review-ready' ? active.record.terminalOutcome.pullRequestUrl : `PR #${batch.pullRequest.number}` });
487
+ const pullRequestUrl = `https://github.com/${active.record.canonicalRepository}/pull/${batch.pullRequest.number}`;
488
+ const outcome = { status: 'review-ready', pullRequestUrl, evidencePath: evidence.path, continuationEpoch: head };
489
+ try {
490
+ await this.persist(active, {
491
+ lifecycle: 'review-ready',
492
+ reviewFeedback: publishReviewFeedback(active.record.reviewFeedback, receipt),
493
+ terminalOutcome: outcome,
494
+ outcomeEvidenceId: evidence.id,
495
+ intent: undefined,
496
+ });
497
+ }
498
+ catch {
499
+ throw new PostEffectStateError(active);
500
+ }
501
+ return publicOutcome(outcome);
502
+ }
503
+ async continueReviewReady(starting, targetRoot, config) {
504
+ const terminal = starting.record.terminalOutcome;
505
+ const feedback = starting.record.reviewFeedback;
506
+ if (!terminal || terminal.status !== 'review-ready' || !feedback || !this.dependencies.reviewFeedback
507
+ || starting.record.routeReceipt?.route !== 'direct' || starting.record.directReview?.status !== 'clear') {
508
+ return { result: publicOutcome(terminal) };
509
+ }
510
+ const pullRequest = await this.dependencies.pullRequests.findOpen({
511
+ headBranch: starting.record.branchName,
512
+ baseBranch: config.github.baseBranch,
513
+ });
514
+ if (!pullRequest?.number || !pullRequest.nodeId || !pullRequest.headSha) {
515
+ return { result: await this.reviewReadyObservationBlocked(starting, 'review-feedback-pr-identity-missing') };
516
+ }
517
+ const expectedHeadSha = feedback.phase === 'bootstrap-required'
518
+ ? pullRequest.headSha
519
+ : feedback.previousPublishedHeadSha;
520
+ if (!expectedHeadSha) {
521
+ return { result: await this.reviewReadyObservationBlocked(starting, 'review-feedback-head-missing') };
522
+ }
523
+ const observed = await this.dependencies.reviewFeedback.observeAndFreeze({
524
+ runId: starting.record.runId,
525
+ canonicalRepository: starting.record.canonicalRepository,
526
+ pullRequestNumber: pullRequest.number,
527
+ expectedHeadSha,
528
+ expectedHeadRefName: starting.record.branchName,
529
+ expectedBaseRefName: config.github.baseBranch,
530
+ marker: `<!-- codex-orchestrator:run:${starting.record.runId}:pr -->`,
531
+ consumedSourceIds: feedback.consumedSourceIds,
532
+ });
533
+ if (observed.status === 'retryable')
534
+ return { result: await this.invokedFailure(starting, 'review-feedback-observation-retryable') };
535
+ if (observed.status === 'blocked') {
536
+ return { result: await this.reviewReadyObservationBlocked(starting, 'review-feedback-observation-blocked') };
537
+ }
538
+ if (feedback.phase === 'bootstrap-required') {
539
+ const sourceIds = observed.status === 'frozen'
540
+ ? observed.batch.sources.map((source) => source.sourceId)
541
+ : observed.eligibleSourceIds;
542
+ const active = await this.persist(starting, {
543
+ reviewFeedback: bootstrapReviewFeedback(feedback, expectedHeadSha, sourceIds),
544
+ });
545
+ return { result: publicOutcome(active.record.terminalOutcome) };
546
+ }
547
+ if (feedback.phase !== 'idle' || observed.status === 'none')
548
+ return { result: publicOutcome(terminal) };
549
+ if (!await this.authorizedForExactLabels(starting.record.issueNumber, starting.record.runId, starting.record.branchName, [config.github.labels.review.name])) {
550
+ return { result: publicOutcome(terminal) };
551
+ }
552
+ const projected = projectReviewFeedbackBatch(observed.batch, starting.record.directReview.targetRevision);
553
+ const repairReview = beginDirectReviewRepair(starting.record.directReview, projected.repairFindings);
554
+ let active = await this.persist(starting, {
555
+ lifecycle: 'implementing',
556
+ reviewFeedback: activateReviewFeedback(feedback, observed.batch),
557
+ directReview: {
558
+ ...repairReview,
559
+ review: { ...repairReview.review, reportRepairs: 0, transportRetries: 0 },
560
+ },
561
+ reworkFindings: projected.repairFindings.map((finding) => finding.summary),
562
+ reportRepairs: 0,
563
+ transportRetries: 0,
564
+ checks: [],
565
+ checkedChangeSha256: undefined,
566
+ proofId: undefined,
567
+ proofReceipt: undefined,
568
+ terminalOutcome: undefined,
569
+ outcomeEvidenceId: undefined,
570
+ intent: {
571
+ kind: 'review-activation-labels',
572
+ issueNumber: starting.record.issueNumber,
573
+ batchId: observed.batch.batchId,
574
+ expected: sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]),
575
+ },
576
+ });
577
+ return this.resumeActivatedReviewFeedback(active, targetRoot, config);
578
+ }
579
+ async resumeActivatedReviewFeedback(starting, targetRoot, config) {
580
+ let active = starting;
581
+ const feedback = active.record.reviewFeedback;
582
+ const batch = feedback?.activeBatch;
583
+ if (!feedback || feedback.phase !== 'frozen' || !batch || !this.dependencies.reviewFeedback) {
584
+ return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-state-invalid') };
585
+ }
586
+ const expectedHeadSha = batch.priorPublishedHeadSha;
587
+ const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
588
+ const reviewLabels = [config.github.labels.review.name];
589
+ const intent = active.record.intent;
590
+ if (intent && (intent.kind !== 'review-activation-labels'
591
+ || intent.issueNumber !== active.record.issueNumber
592
+ || intent.batchId !== batch.batchId
593
+ || !sameStrings(intent.expected, runningLabels))) {
594
+ return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-intent-diverged') };
595
+ }
596
+ const issue = await this.readIssue(active.record.issueNumber);
597
+ const labelsAreRunning = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, runningLabels)
598
+ && this.hasTrustedClaim(issue, active.record.runId, active.record.issueNumber, active.record.branchName);
599
+ const labelsAreReview = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, reviewLabels)
600
+ && this.hasTrustedClaim(issue, active.record.runId, active.record.issueNumber, active.record.branchName);
601
+ if (!labelsAreRunning && !labelsAreReview) {
602
+ return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-authority-revoked') };
603
+ }
604
+ const validation = await this.dependencies.reviewFeedback.revalidate({
605
+ batch, epoch: 'pre-update', expectedHeadSha,
606
+ });
607
+ const validationFailure = await this.mapFeedbackRevalidation(active, validation, 'review-feedback-activation-revalidation-failed');
608
+ if (validationFailure)
609
+ return { result: validationFailure };
610
+ if (labelsAreReview) {
611
+ try {
612
+ await this.dependencies.issues.setLabels(active.record.issueNumber, runningLabels);
613
+ }
614
+ catch {
615
+ return { result: await this.invokedFailure(active, 'review-feedback-activation-labels-delivery-unknown') };
616
+ }
617
+ const observed = await this.readIssue(active.record.issueNumber);
618
+ if (!observed || !sameStrings(observed.labels, runningLabels)
619
+ || !this.hasTrustedClaim(observed, active.record.runId, active.record.issueNumber, active.record.branchName)) {
620
+ return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-activation-labels-diverged') };
621
+ }
622
+ }
623
+ if (intent)
624
+ active = await this.confirmEffect(active);
625
+ let worktree = await this.dependencies.git.inspectWorktree({
626
+ worktreePath: active.record.worktreePath,
627
+ branchName: active.record.branchName,
628
+ baseSha: expectedHeadSha,
629
+ });
630
+ if (worktree === 'absent' && this.dependencies.git.ensureContinuationWorktree) {
631
+ try {
632
+ await this.dependencies.git.ensureContinuationWorktree({
633
+ targetRoot,
634
+ worktreePath: active.record.worktreePath,
635
+ branchName: active.record.branchName,
636
+ baseBranch: config.github.baseBranch,
637
+ publishedHeadSha: expectedHeadSha,
638
+ });
639
+ worktree = await this.dependencies.git.inspectWorktree({
640
+ worktreePath: active.record.worktreePath,
641
+ branchName: active.record.branchName,
642
+ baseSha: expectedHeadSha,
643
+ });
644
+ }
645
+ catch {
646
+ worktree = 'diverged';
647
+ }
648
+ }
649
+ const remoteSha = await this.dependencies.git.getRemoteBranchSha(active.record.worktreePath, active.record.branchName);
650
+ if (worktree !== 'matching' || remoteSha !== expectedHeadSha
651
+ || (await this.dependencies.git.listChangedFilesIgnoringUntrackedRoot(active.record.worktreePath, config.proof.artifactDir)).length !== 0) {
652
+ return { result: await this.blockReviewFeedback(active, 'safety', 'review-feedback-worktree-diverged') };
653
+ }
654
+ active = await this.persist(active, { reviewFeedback: markReviewFeedbackRepairing(active.record.reviewFeedback) });
655
+ return { active };
656
+ }
657
+ async reviewReadyObservationBlocked(active, code) {
658
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary: 'Review feedback observation failed closed before activation.' });
659
+ const terminalOutcome = {
660
+ status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path,
661
+ };
662
+ await this.persist(active, {
663
+ lifecycle: 'blocked',
664
+ reviewFeedback: active.record.reviewFeedback,
665
+ directReview: active.record.directReview && active.record.directReview.status !== 'terminal'
666
+ ? projectTerminalDirectReview(active.record.directReview, { status: 'blocked', kind: 'safety' })
667
+ : active.record.directReview,
668
+ terminalOutcome,
669
+ outcomeEvidenceId: evidence.id,
670
+ intent: undefined,
671
+ });
672
+ return publicOutcome(terminalOutcome);
673
+ }
267
674
  async runIssue(input) {
268
675
  let owner;
269
676
  let active;
677
+ let recoveredFeedbackImplementation;
270
678
  try {
271
679
  assertPositiveInteger(input.issueNumber, 'issueNumber');
272
680
  const targetRoot = resolve(input.targetRoot);
@@ -335,17 +743,36 @@ export class RunIssue {
335
743
  if (existing.branchName !== expectedBranch || existing.worktreePath !== expectedWorktree) {
336
744
  return await this.preClaimInternal('run-identity-mismatch', input.issueNumber);
337
745
  }
338
- if (existing.terminalOutcome)
339
- return publicOutcome(existing.terminalOutcome);
340
- active = { state: persisted, record: existing };
746
+ active = { state: persisted, record: existing, config };
341
747
  issueSnapshot = structuredClone(existing.issueSnapshot);
342
748
  frozenCriteria = structuredClone(existing.frozenCriteria);
343
749
  runId = existing.runId;
344
750
  branchName = existing.branchName;
345
751
  worktreePath = existing.worktreePath;
346
752
  baseSha = existing.baseSha;
753
+ if (active.record.intent?.kind === 'review-blocked-labels') {
754
+ return await this.blockReviewFeedback(active, active.record.intent.blockKind, active.record.intent.evidenceCode);
755
+ }
756
+ 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);
765
+ }
766
+ if (active.record.reviewFeedback?.phase === 'frozen') {
767
+ const activation = await this.resumeActivatedReviewFeedback(active, targetRoot, config);
768
+ if ('result' in activation)
769
+ return activation.result;
770
+ active = activation.active;
771
+ }
347
772
  if (existing.lifecycle === 'publishing') {
348
- return await this.publish(active, config, issueSnapshot, input.issueNumber);
773
+ return active.record.reviewFeedback?.activeBatch
774
+ ? await this.updateExistingPullRequest(active, config, input.issueNumber)
775
+ : await this.publish(active, config, issueSnapshot, input.issueNumber);
349
776
  }
350
777
  if (existing.lifecycle === 'claimed') {
351
778
  const claim = await this.reconcileClaim(active, config);
@@ -363,7 +790,7 @@ export class RunIssue {
363
790
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
364
791
  }
365
792
  }
366
- active = await this.migratePreRouteRun(active, issue);
793
+ active = await this.initializeClaimedRun(active, issue);
367
794
  issueSnapshot = structuredClone(active.record.issueSnapshot);
368
795
  }
369
796
  else {
@@ -377,9 +804,6 @@ export class RunIssue {
377
804
  catch {
378
805
  return await this.invokedFailure(active, 'safe-halt-process-absence-unconfirmed');
379
806
  }
380
- if (!process.purpose || !process.resumeLifecycle || process.resumeReviewStage === undefined) {
381
- return await this.terminal(active, { status: 'internal-error', code: 'direct-review-migration-unrecoverable' });
382
- }
383
807
  active = await this.persist(active, { lifecycle: process.resumeLifecycle, process: undefined });
384
808
  }
385
809
  if (existing.lifecycle === 'waiting-human') {
@@ -395,6 +819,37 @@ export class RunIssue {
395
819
  return await this.revoked(active);
396
820
  return await this.continueSpecRequired(active);
397
821
  }
822
+ const feedbackInvocation = active.record.reviewFeedback?.implementationInvocation;
823
+ if (feedbackInvocation?.phase === 'prepared') {
824
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-launch-ambiguous');
825
+ }
826
+ if (feedbackInvocation?.phase === 'launched') {
827
+ try {
828
+ await this.dependencies.waitForReviewProcessAbsence(feedbackInvocation.processGroupId);
829
+ }
830
+ catch {
831
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-process-absence-unconfirmed');
832
+ }
833
+ if (!this.dependencies.implementationAgent.recover) {
834
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-unavailable');
835
+ }
836
+ try {
837
+ recoveredFeedbackImplementation = await this.dependencies.implementationAgent.recover({
838
+ runId: active.record.runId,
839
+ canonicalRepository: active.record.canonicalRepository,
840
+ invocation: feedbackInvocation,
841
+ });
842
+ }
843
+ catch {
844
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-failed');
845
+ }
846
+ if (!recoveredFeedbackImplementation) {
847
+ const snapshot = await this.dependencies.git.snapshot(active.record.worktreePath);
848
+ if (!sameFreshness(snapshot, feedbackInvocation.baseline)) {
849
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
850
+ }
851
+ }
852
+ }
398
853
  if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
399
854
  return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
400
855
  }
@@ -402,13 +857,17 @@ export class RunIssue {
402
857
  return await this.revoked(active);
403
858
  if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
404
859
  if (!active.record.routeExecution || !active.record.routeReceipt)
405
- throw new RouteMigrationUnrecoverableError();
860
+ throw new RouteInitializationUnrecoverableError();
406
861
  const reviewRecovery = active.record.lifecycle === 'implementing' && active.record.directReview?.status === 'active'
407
862
  && (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
408
863
  const checkRecovery = active.record.lifecycle === 'checking'
409
- && (active.record.directReview?.status === 'clear' || active.record.directReview?.status === 'legacy-bypass');
410
- if (!reviewRecovery && !checkRecovery) {
411
- if (active.record.cycle >= config.runner.maxCycles) {
864
+ && active.record.directReview?.status === 'clear';
865
+ const proofRecovery = active.record.lifecycle === 'proving'
866
+ && active.record.directReview?.status === 'clear';
867
+ const feedbackRepair = active.record.reviewFeedback?.phase === 'repairing'
868
+ && active.record.directReview?.stage === 'review-repair';
869
+ if (!reviewRecovery && !checkRecovery && !proofRecovery && !feedbackRepair) {
870
+ if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
412
871
  return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
413
872
  }
414
873
  active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
@@ -433,7 +892,7 @@ export class RunIssue {
433
892
  const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
434
893
  active = await this.createRun({
435
894
  runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
436
- issueSnapshot, frozenCriteria,
895
+ issueSnapshot, frozenCriteria, config,
437
896
  intent: { kind: 'claim-labels', issueNumber: input.issueNumber, expected: runningLabels },
438
897
  });
439
898
  const claim = await this.reconcileClaim(active, config);
@@ -446,7 +905,7 @@ export class RunIssue {
446
905
  catch {
447
906
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
448
907
  }
449
- active = await this.migratePreRouteRun(active, issue);
908
+ active = await this.initializeClaimedRun(active, issue);
450
909
  issueSnapshot = structuredClone(active.record.issueSnapshot);
451
910
  }
452
911
  if (active.record.lifecycle === 'triaging' || active.record.lifecycle === 'routed') {
@@ -455,20 +914,11 @@ export class RunIssue {
455
914
  return routed.result;
456
915
  active = routed.active;
457
916
  }
458
- if (active.record.lifecycle !== 'implementing' && active.record.lifecycle !== 'checking') {
917
+ if (!['implementing', 'checking', 'proving'].includes(active.record.lifecycle)) {
459
918
  return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
460
919
  }
461
920
  if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
462
- const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
463
- const targetFingerprint = directReviewTargetFingerprint({
464
- snapshot: await this.dependencies.git.snapshot(worktreePath),
465
- changedFiles,
466
- routeDecisionSha256: active.record.routeReceipt.decisionSha256,
467
- workflowGenerationHash: active.record.workflowGeneration.generationHash,
468
- cycle: active.record.cycle,
469
- frozenCriteria,
470
- });
471
- active = await this.persist(active, { directReview: createLegacyBypassDirectReview(targetFingerprint) });
921
+ return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
472
922
  }
473
923
  attemptLoop: while (true) {
474
924
  if (!await this.authorized(input.issueNumber, runId, branchName, config)) {
@@ -476,8 +926,8 @@ export class RunIssue {
476
926
  }
477
927
  if (this.signal.aborted)
478
928
  return await this.terminal(active, { status: 'cancelled' });
479
- let resumeAtChecks = active.record.lifecycle === 'checking'
480
- && (active.record.directReview?.status === 'clear' || active.record.directReview?.status === 'legacy-bypass');
929
+ let resumeAtChecks = (active.record.lifecycle === 'checking' || active.record.lifecycle === 'proving')
930
+ && active.record.directReview?.status === 'clear';
481
931
  if (active.record.lifecycle === 'implementing'
482
932
  && active.record.directReview?.status === 'active'
483
933
  && (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure')) {
@@ -485,7 +935,7 @@ export class RunIssue {
485
935
  if ('status' in recovered)
486
936
  return recovered;
487
937
  active = recovered;
488
- const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
938
+ const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
489
939
  if ('status' in reviewed)
490
940
  return reviewed;
491
941
  active = reviewed;
@@ -494,9 +944,40 @@ export class RunIssue {
494
944
  resumeAtChecks = true;
495
945
  }
496
946
  if (!resumeAtChecks) {
497
- const implementationBaseline = await this.dependencies.git.snapshot(worktreePath);
947
+ const feedbackBatch = active.record.reviewFeedback?.activeBatch;
948
+ const workerBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
949
+ if (workerBlock)
950
+ return workerBlock;
951
+ const feedbackProjection = feedbackBatch
952
+ ? projectReviewFeedbackBatch(feedbackBatch, active.record.directReview.targetRevision)
953
+ : undefined;
954
+ const feedbackLaunch = feedbackBatch ? {
955
+ reviewFeedbackRound: active.record.reviewFeedback.repairRound,
956
+ reviewFeedback: feedbackProjection.workerFeedback,
957
+ onPrepared: async (invocation) => {
958
+ const currentActive = active;
959
+ active = await this.persist(currentActive, {
960
+ reviewFeedback: {
961
+ ...structuredClone(currentActive.record.reviewFeedback),
962
+ implementationInvocation: { ...structuredClone(invocation), phase: 'prepared', pid: null, processGroupId: null, launchedAt: null },
963
+ },
964
+ });
965
+ },
966
+ onLaunched: async (invocation) => {
967
+ const currentActive = active;
968
+ const current = currentActive.record.reviewFeedback?.implementationInvocation;
969
+ if (!current || current.phase !== 'prepared' || current.attemptId !== invocation.attemptId)
970
+ throw new Error('review feedback implementation launch mismatch');
971
+ active = await this.persist(currentActive, {
972
+ reviewFeedback: {
973
+ ...structuredClone(currentActive.record.reviewFeedback),
974
+ implementationInvocation: { ...current, phase: 'launched', pid: invocation.pid, processGroupId: invocation.processGroupId, launchedAt: invocation.launchedAt },
975
+ },
976
+ });
977
+ },
978
+ } : {};
498
979
  const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
499
- let implementation = await this.runImplementation({
980
+ let implementation = recoveredFeedbackImplementation ?? await this.runImplementation({
500
981
  runId,
501
982
  worktreePath,
502
983
  issue: publicIssueSnapshot(issueSnapshot),
@@ -505,9 +986,19 @@ export class RunIssue {
505
986
  reworkFindings: active.record.reworkFindings,
506
987
  repairOnly: false,
507
988
  workflowGeneration: active.record.workflowGeneration,
989
+ ...feedbackLaunch,
508
990
  });
991
+ recoveredFeedbackImplementation = undefined;
509
992
  if (implementation.kind === 'safe-halt') {
510
- active = await this.persist(active, { lifecycle: 'safe-halt', process: implementation.process });
993
+ active = await this.persist(active, {
994
+ lifecycle: 'safe-halt',
995
+ process: {
996
+ ...implementation.process,
997
+ purpose: 'implementation',
998
+ resumeLifecycle: 'implementing',
999
+ resumeReviewStage: null,
1000
+ },
1001
+ });
511
1002
  while (true) {
512
1003
  try {
513
1004
  await implementation.waitForAbsence();
@@ -526,9 +1017,9 @@ export class RunIssue {
526
1017
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
527
1018
  }
528
1019
  if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
529
- const afterTransport = await this.dependencies.git.snapshot(worktreePath);
530
- if (!sameFreshness(implementationBaseline, afterTransport)) {
531
- return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'transport-baseline-changed');
1020
+ const invocation = active.record.reviewFeedback?.implementationInvocation;
1021
+ if (invocation && !sameFreshness(await this.dependencies.git.snapshot(worktreePath), invocation.baseline)) {
1022
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
532
1023
  }
533
1024
  active = await this.persist(active, { transportRetries: 1 });
534
1025
  continue attemptLoop;
@@ -545,6 +1036,9 @@ export class RunIssue {
545
1036
  }
546
1037
  const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
547
1038
  active = await this.persist(active, { reportRepairs: 1 });
1039
+ const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
1040
+ if (repairBlock)
1041
+ return repairBlock;
548
1042
  implementation = await this.runImplementation({
549
1043
  runId,
550
1044
  worktreePath,
@@ -554,9 +1048,18 @@ export class RunIssue {
554
1048
  reworkFindings: ['The previous implementation report did not match the generated schema.'],
555
1049
  repairOnly: true,
556
1050
  workflowGeneration: active.record.workflowGeneration,
1051
+ ...feedbackLaunch,
557
1052
  });
558
1053
  if (implementation.kind === 'safe-halt') {
559
- active = await this.persist(active, { lifecycle: 'safe-halt', process: implementation.process });
1054
+ active = await this.persist(active, {
1055
+ lifecycle: 'safe-halt',
1056
+ process: {
1057
+ ...implementation.process,
1058
+ purpose: 'implementation',
1059
+ resumeLifecycle: 'implementing',
1060
+ resumeReviewStage: null,
1061
+ },
1062
+ });
560
1063
  while (true) {
561
1064
  try {
562
1065
  await implementation.waitForAbsence();
@@ -584,12 +1087,69 @@ export class RunIssue {
584
1087
  if (report.status === 'external-block') {
585
1088
  return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
586
1089
  }
587
- if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
1090
+ if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
588
1091
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
589
1092
  }
590
1093
  const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
591
1094
  if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
592
- return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
1095
+ if (changedFiles.length === 0 || active.record.reportRepairs >= 1) {
1096
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
1097
+ }
1098
+ const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
1099
+ active = await this.persist(active, { reportRepairs: 1 });
1100
+ const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
1101
+ if (repairBlock)
1102
+ return repairBlock;
1103
+ implementation = await this.runImplementation({
1104
+ runId,
1105
+ worktreePath,
1106
+ issue: publicIssueSnapshot(issueSnapshot),
1107
+ frozenCriteria,
1108
+ cycle: active.record.cycle,
1109
+ reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
1110
+ repairOnly: true,
1111
+ workflowGeneration: active.record.workflowGeneration,
1112
+ ...feedbackLaunch,
1113
+ });
1114
+ if (implementation.kind === 'safe-halt') {
1115
+ active = await this.persist(active, {
1116
+ lifecycle: 'safe-halt',
1117
+ process: {
1118
+ ...implementation.process,
1119
+ purpose: 'implementation',
1120
+ resumeLifecycle: 'implementing',
1121
+ resumeReviewStage: null,
1122
+ },
1123
+ });
1124
+ while (true) {
1125
+ try {
1126
+ await implementation.waitForAbsence();
1127
+ break;
1128
+ }
1129
+ catch {
1130
+ await new Promise((resolveWait) => setTimeout(resolveWait, 25));
1131
+ }
1132
+ }
1133
+ return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
1134
+ }
1135
+ if (implementation.kind !== 'completed')
1136
+ return await this.mapImplementationFailure(active, implementation);
1137
+ const afterRepair = await this.dependencies.git.snapshot(worktreePath);
1138
+ if (!sameFreshness(repairBaseline, afterRepair)) {
1139
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
1140
+ }
1141
+ try {
1142
+ report = validateImplementationReport(implementation.report);
1143
+ }
1144
+ catch {
1145
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
1146
+ }
1147
+ if (report.status === 'external-block') {
1148
+ return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
1149
+ }
1150
+ if (!sameStrings(changedFiles, report.changedFiles)) {
1151
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
1152
+ }
593
1153
  }
594
1154
  if (active.record.routeReceipt?.route === 'direct') {
595
1155
  const targetFingerprint = directReviewTargetFingerprint({
@@ -603,6 +1163,9 @@ export class RunIssue {
603
1163
  if (active.record.directReview?.stage === 'review-repair') {
604
1164
  active = await this.persist(active, {
605
1165
  directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
1166
+ ...(active.record.reviewFeedback ? {
1167
+ reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
1168
+ } : {}),
606
1169
  });
607
1170
  }
608
1171
  else {
@@ -611,14 +1174,14 @@ export class RunIssue {
611
1174
  active = await this.persist(active, {
612
1175
  directReview: createInitialDirectReview({
613
1176
  targetFingerprint,
614
- cleanupRequired: false,
615
- cleanupReason: 'bounded final review owns cleanup',
616
- cleanupReviewerSessionId: null,
617
1177
  codeReviewerSessionId: reviewerSessionId,
618
1178
  }),
1179
+ ...(active.record.reviewFeedback ? {
1180
+ reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
1181
+ } : {}),
619
1182
  });
620
1183
  }
621
- const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
1184
+ const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
622
1185
  if ('status' in reviewed)
623
1186
  return reviewed;
624
1187
  active = reviewed;
@@ -626,7 +1189,12 @@ export class RunIssue {
626
1189
  continue attemptLoop;
627
1190
  }
628
1191
  else {
629
- active = await this.persist(active, { lifecycle: 'checking' });
1192
+ active = await this.persist(active, {
1193
+ lifecycle: 'checking',
1194
+ ...(active.record.reviewFeedback ? {
1195
+ reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
1196
+ } : {}),
1197
+ });
630
1198
  }
631
1199
  }
632
1200
  for (const [id, command] of Object.entries(config.checks)) {
@@ -646,8 +1214,10 @@ export class RunIssue {
646
1214
  const row = { id, command, status: check.status, outputSha256: sha256(check.output) };
647
1215
  active = await this.persist(active, { checks: [...active.record.checks, row] });
648
1216
  if (check.status !== 'passed') {
649
- if (active.record.cycle >= config.runner.maxCycles) {
650
- return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
1217
+ if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
1218
+ return active.record.reviewFeedback?.activeBatch
1219
+ ? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
1220
+ : await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
651
1221
  }
652
1222
  const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
653
1223
  active = await this.startNextCycle(active, [summary], [{
@@ -685,7 +1255,7 @@ export class RunIssue {
685
1255
  catch {
686
1256
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
687
1257
  }
688
- if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
1258
+ if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
689
1259
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
690
1260
  }
691
1261
  const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
@@ -701,7 +1271,7 @@ export class RunIssue {
701
1271
  runId,
702
1272
  issueNumber: input.issueNumber,
703
1273
  cycle: active.record.cycle,
704
- baseSha,
1274
+ baseSha: expectedImplementationHead(active.record),
705
1275
  headSha: freshness.headSha,
706
1276
  indexTreeSha: freshness.indexTreeSha,
707
1277
  trackedContentSha256: freshness.trackedContentSha256,
@@ -726,9 +1296,16 @@ export class RunIssue {
726
1296
  frozenCriteria,
727
1297
  checkedChange,
728
1298
  workflowGeneration: structuredClone(active.record.workflowGeneration),
1299
+ beforeAgentLaunch: async () => {
1300
+ const failure = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
1301
+ if (failure)
1302
+ throw new ProofLaunchAuthorizationError(failure);
1303
+ },
729
1304
  });
730
1305
  }
731
1306
  catch (error) {
1307
+ if (error instanceof ProofLaunchAuthorizationError)
1308
+ return error.outcome;
732
1309
  if (error instanceof ProofQuiescenceError) {
733
1310
  active = await this.persist(active, {
734
1311
  lifecycle: 'safe-halt',
@@ -764,8 +1341,10 @@ export class RunIssue {
764
1341
  if (this.signal.aborted)
765
1342
  return await this.terminal(active, { status: 'cancelled' });
766
1343
  if (proof.status === 'needs-rework') {
767
- if (active.record.cycle >= config.runner.maxCycles) {
768
- return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
1344
+ if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
1345
+ return active.record.reviewFeedback?.activeBatch
1346
+ ? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-proof-repair-exhausted')
1347
+ : await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
769
1348
  }
770
1349
  active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
771
1350
  provenance: 'proof',
@@ -777,10 +1356,23 @@ export class RunIssue {
777
1356
  }
778
1357
  if (proof.status !== 'passed')
779
1358
  return await this.mapProofFailure(active, proof);
780
- active = await this.persist(active, { lifecycle: 'publishing', proofReceipt: proof.receipt, reworkFindings: [] });
1359
+ active = await this.persist(active, {
1360
+ lifecycle: 'publishing',
1361
+ proofReceipt: proof.receipt,
1362
+ reworkFindings: [],
1363
+ ...(active.record.reviewFeedback?.activeBatch ? {
1364
+ reviewFeedback: markReviewFeedbackPublishing(markReviewFeedbackVerified(active.record.reviewFeedback, {
1365
+ checkedChangeSha256,
1366
+ proofId,
1367
+ verifiedAt: this.timestamp(),
1368
+ })),
1369
+ } : {}),
1370
+ });
781
1371
  break;
782
1372
  }
783
- return await this.publish(active, config, issueSnapshot, input.issueNumber);
1373
+ return active.record.reviewFeedback?.activeBatch
1374
+ ? await this.updateExistingPullRequest(active, config, input.issueNumber)
1375
+ : await this.publish(active, config, issueSnapshot, input.issueNumber);
784
1376
  }
785
1377
  catch (error) {
786
1378
  if (!active && error instanceof TransportReadError) {
@@ -795,11 +1387,11 @@ export class RunIssue {
795
1387
  if (active && error instanceof TransportReadError) {
796
1388
  return await this.invokedFailure(active, 'authorization-read-failed');
797
1389
  }
798
- if (active && error instanceof RouteMigrationUnrecoverableError) {
1390
+ if (active && error instanceof RouteInitializationUnrecoverableError) {
799
1391
  const evidence = await this.dependencies.writeEvidence({
800
1392
  runId: active.record.runId,
801
- code: 'route-migration-unrecoverable',
802
- summary: 'The pre-route run cannot be safely migrated without product-state ambiguity.',
1393
+ code: 'route-initialization-unrecoverable',
1394
+ summary: 'The claimed run cannot be safely initialized without product-state ambiguity.',
803
1395
  });
804
1396
  return { status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path };
805
1397
  }
@@ -830,26 +1422,26 @@ export class RunIssue {
830
1422
  }
831
1423
  }
832
1424
  }
833
- async migratePreRouteRun(active, issue) {
1425
+ async initializeClaimedRun(active, issue) {
834
1426
  if (active.record.lifecycle !== 'claimed'
835
1427
  || active.record.process
836
1428
  || active.record.intent
837
1429
  || active.record.routeExecution
838
1430
  || active.record.routeReceipt) {
839
- throw new RouteMigrationUnrecoverableError();
1431
+ throw new RouteInitializationUnrecoverableError();
840
1432
  }
841
1433
  const [snapshot, changedFiles] = await Promise.all([
842
1434
  this.dependencies.git.snapshot(active.record.worktreePath),
843
1435
  this.dependencies.git.listChangedFiles(active.record.worktreePath),
844
1436
  ]);
845
1437
  if (snapshot.headSha !== active.record.baseSha || changedFiles.length !== 0) {
846
- throw new RouteMigrationUnrecoverableError();
1438
+ throw new RouteInitializationUnrecoverableError();
847
1439
  }
848
1440
  try {
849
1441
  await this.dependencies.verifyWorkflowGeneration(active.record.workflowGeneration);
850
1442
  }
851
1443
  catch {
852
- throw new RouteMigrationUnrecoverableError();
1444
+ throw new RouteInitializationUnrecoverableError();
853
1445
  }
854
1446
  const issueSnapshot = issue?.state === 'OPEN' ? snapshotIssue(issue) : active.record.issueSnapshot;
855
1447
  return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
@@ -914,7 +1506,15 @@ export class RunIssue {
914
1506
  if (result.status === 'repairable' || result.status === 'retryable')
915
1507
  continue;
916
1508
  if (result.status === 'safe-halt') {
917
- active = await this.persist(active, { lifecycle: 'safe-halt', process: result.process });
1509
+ active = await this.persist(active, {
1510
+ lifecycle: 'safe-halt',
1511
+ process: {
1512
+ ...result.process,
1513
+ purpose: 'route',
1514
+ resumeLifecycle: 'triaging',
1515
+ resumeReviewStage: null,
1516
+ },
1517
+ });
918
1518
  while (true) {
919
1519
  try {
920
1520
  await result.waitForAbsence();
@@ -1166,8 +1766,9 @@ export class RunIssue {
1166
1766
  const state = await this.dependencies.runRecords.read();
1167
1767
  const now = this.timestamp();
1168
1768
  const workflow = await this.dependencies.createWorkflowGeneration();
1769
+ const { config, ...persistedInput } = input;
1169
1770
  const record = {
1170
- ...input,
1771
+ ...persistedInput,
1171
1772
  lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
1172
1773
  reworkFindings: [],
1173
1774
  packageVersion: workflow.receipt.packageVersion,
@@ -1176,9 +1777,9 @@ export class RunIssue {
1176
1777
  checks: [], createdAt: now, updatedAt: now,
1177
1778
  };
1178
1779
  const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
1179
- schema: 'codex-orchestrator.agent-auto-state', version: 1, runs: [...state.runs, record],
1780
+ schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
1180
1781
  });
1181
- return { state: saved, record: findRun(saved, input.runId) };
1782
+ return { state: saved, record: findRun(saved, input.runId), config };
1182
1783
  }
1183
1784
  async persist(active, changes) {
1184
1785
  const record = { ...active.record, ...changes, updatedAt: this.timestamp() };
@@ -1186,15 +1787,15 @@ export class RunIssue {
1186
1787
  delete record.intent;
1187
1788
  if (Object.hasOwn(changes, 'process') && changes.process === undefined)
1188
1789
  delete record.process;
1189
- for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
1790
+ for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reviewFeedback']) {
1190
1791
  if (Object.hasOwn(changes, key) && changes[key] === undefined)
1191
1792
  delete record[key];
1192
1793
  }
1193
1794
  const runs = active.state.runs.map((candidate) => candidate.runId === record.runId ? record : candidate);
1194
1795
  const saved = await this.dependencies.runRecords.compareAndSwap(active.state.generation, {
1195
- schema: 'codex-orchestrator.agent-auto-state', version: 1, runs,
1796
+ schema: 'codex-orchestrator.agent-auto-state', version: 2, runs,
1196
1797
  });
1197
- return { state: saved, record: findRun(saved, record.runId) };
1798
+ return { state: saved, record: findRun(saved, record.runId), config: active.config };
1198
1799
  }
1199
1800
  clearIntent(active) {
1200
1801
  return this.persist(active, { intent: undefined });
@@ -1214,6 +1815,14 @@ export class RunIssue {
1214
1815
  const labels = new Set(issue.labels);
1215
1816
  if (!labels.has(config.github.labels.auto.name) || !labels.has(config.github.labels.running.name))
1216
1817
  return false;
1818
+ return this.hasTrustedClaim(issue, runId, issueNumber, branchName);
1819
+ }
1820
+ async authorizedForExactLabels(issueNumber, runId, branchName, expectedLabels) {
1821
+ const issue = await this.readIssue(issueNumber);
1822
+ return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
1823
+ && this.hasTrustedClaim(issue, runId, issueNumber, branchName);
1824
+ }
1825
+ hasTrustedClaim(issue, runId, issueNumber, branchName) {
1217
1826
  const exactBody = claimComment(runId, issueNumber, branchName);
1218
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;
1219
1828
  const markers = issue.comments.filter((comment) => markerPattern.test(comment.body.split('\n')[0] ?? ''));
@@ -1235,6 +1844,30 @@ export class RunIssue {
1235
1844
  throw new TransportReadError();
1236
1845
  }
1237
1846
  }
1847
+ async revalidateFeedbackWorker(active, config, issueNumber) {
1848
+ const batch = active.record.reviewFeedback?.activeBatch;
1849
+ if (!batch)
1850
+ return undefined;
1851
+ if (!await this.authorized(issueNumber, active.record.runId, active.record.branchName, config)) {
1852
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-authority-revoked');
1853
+ }
1854
+ if (!this.dependencies.reviewFeedback) {
1855
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-revalidation-unavailable');
1856
+ }
1857
+ const validation = await this.dependencies.reviewFeedback.revalidate({
1858
+ batch,
1859
+ epoch: 'pre-update',
1860
+ expectedHeadSha: batch.priorPublishedHeadSha,
1861
+ });
1862
+ return this.mapFeedbackRevalidation(active, validation, 'review-feedback-worker-revalidation-failed');
1863
+ }
1864
+ async mapFeedbackRevalidation(active, validation, safetyCode) {
1865
+ if (validation.status === 'valid')
1866
+ return undefined;
1867
+ if (validation.status === 'retryable')
1868
+ return this.invokedFailure(active, `${safetyCode}-retryable`);
1869
+ return this.blockReviewFeedback(active, 'safety', safetyCode);
1870
+ }
1238
1871
  async runImplementation(input) {
1239
1872
  try {
1240
1873
  return await this.dependencies.implementationAgent.run({
@@ -1247,7 +1880,7 @@ export class RunIssue {
1247
1880
  return { kind: 'internal-error' };
1248
1881
  }
1249
1882
  }
1250
- async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
1883
+ async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles, config) {
1251
1884
  let active = starting;
1252
1885
  let reportRepair;
1253
1886
  while (true) {
@@ -1264,6 +1897,9 @@ export class RunIssue {
1264
1897
  ? directReviewClosureRequestSha256(directReview)
1265
1898
  : null;
1266
1899
  const mode = directReview.stage === 'review-closure' ? 'closure' : 'full';
1900
+ const reviewerAuthorizationFailure = await this.revalidateFeedbackWorker(active, config, active.record.issueNumber);
1901
+ if (reviewerAuthorizationFailure)
1902
+ return reviewerAuthorizationFailure;
1267
1903
  const result = await this.dependencies.implementationReviewer.run({
1268
1904
  runId: active.record.runId,
1269
1905
  worktreePath: active.record.worktreePath,
@@ -1312,8 +1948,10 @@ export class RunIssue {
1312
1948
  if (!current)
1313
1949
  return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
1314
1950
  if (result.report.verdict === 'needs-work') {
1315
- if (active.record.cycle >= maxCycles) {
1316
- return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
1951
+ if (this.repairBudgetExhausted(active, maxCycles)) {
1952
+ return active.record.reviewFeedback?.activeBatch
1953
+ ? this.blockReviewFeedback(active, 'exhausted', 'direct-review-repair-exhausted')
1954
+ : this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
1317
1955
  }
1318
1956
  const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
1319
1957
  const findings = [
@@ -1326,7 +1964,12 @@ export class RunIssue {
1326
1964
  ];
1327
1965
  return this.persist(active, {
1328
1966
  lifecycle: 'implementing',
1329
- cycle: (active.record.cycle + 1),
1967
+ cycle: active.record.reviewFeedback?.activeBatch
1968
+ ? active.record.cycle
1969
+ : (active.record.cycle + 1),
1970
+ ...(active.record.reviewFeedback?.activeBatch ? {
1971
+ reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
1972
+ } : {}),
1330
1973
  directReview: repaired,
1331
1974
  reworkFindings: findings,
1332
1975
  checks: [],
@@ -1346,16 +1989,14 @@ export class RunIssue {
1346
1989
  const current = active.record.directReview;
1347
1990
  if (!current)
1348
1991
  return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
1349
- const selected = current.stage?.startsWith('cleanup-') ? current.cleanup : current.review;
1350
- if (selected.reportRepairs >= 1 || reportRepair) {
1992
+ if (current.review.reportRepairs >= 1 || reportRepair) {
1351
1993
  return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
1352
1994
  }
1353
1995
  const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
1354
- const trackKey = current.stage?.startsWith('cleanup-') ? 'cleanup' : 'review';
1355
1996
  active = await this.persist(active, {
1356
1997
  directReview: {
1357
1998
  ...withoutInvocation,
1358
- [trackKey]: { ...withoutInvocation[trackKey], reportRepairs: 1 },
1999
+ review: { ...withoutInvocation.review, reportRepairs: 1 },
1359
2000
  },
1360
2001
  });
1361
2002
  reportRepair = {
@@ -1450,7 +2091,12 @@ export class RunIssue {
1450
2091
  }));
1451
2092
  return this.persist(active, {
1452
2093
  lifecycle: 'implementing',
1453
- cycle: (active.record.cycle + 1),
2094
+ cycle: active.record.reviewFeedback?.activeBatch
2095
+ ? active.record.cycle
2096
+ : (active.record.cycle + 1),
2097
+ ...(active.record.reviewFeedback?.activeBatch ? {
2098
+ reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
2099
+ } : {}),
1454
2100
  reworkFindings: [...findings],
1455
2101
  directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
1456
2102
  checks: [],
@@ -1462,13 +2108,81 @@ export class RunIssue {
1462
2108
  active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
1463
2109
  return this.persist(active, {
1464
2110
  lifecycle: 'implementing',
1465
- cycle: (active.record.cycle + 1),
2111
+ cycle: active.record.reviewFeedback?.activeBatch
2112
+ ? active.record.cycle
2113
+ : (active.record.cycle + 1),
2114
+ ...(active.record.reviewFeedback?.activeBatch ? {
2115
+ reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
2116
+ } : {}),
1466
2117
  checks: [],
1467
2118
  checkedChangeSha256: undefined,
1468
2119
  proofId: undefined,
1469
2120
  proofReceipt: undefined,
1470
2121
  });
1471
2122
  }
2123
+ repairBudgetExhausted(active, maxCycles) {
2124
+ return active.record.reviewFeedback?.activeBatch
2125
+ ? active.record.reviewFeedback.repairRound >= 3
2126
+ : active.record.cycle >= maxCycles;
2127
+ }
2128
+ async blockReviewFeedback(active, kind, evidenceCode) {
2129
+ const feedback = active.record.reviewFeedback;
2130
+ if (!feedback?.activeBatch)
2131
+ return this.terminal(active, { status: 'blocked', kind, resumable: false }, evidenceCode);
2132
+ {
2133
+ const config = active.config;
2134
+ const blockedLabels = [config.github.labels.auto.name, config.github.labels.blocked.name].sort();
2135
+ const existingIntent = active.record.intent;
2136
+ if (existingIntent?.kind !== 'review-blocked-labels') {
2137
+ active = await this.persist(active, { intent: {
2138
+ kind: 'review-blocked-labels', issueNumber: active.record.issueNumber,
2139
+ batchId: feedback.activeBatch.batchId, expected: blockedLabels, blockKind: kind, evidenceCode,
2140
+ } });
2141
+ }
2142
+ else if (existingIntent.issueNumber !== active.record.issueNumber
2143
+ || existingIntent.batchId !== feedback.activeBatch.batchId
2144
+ || !sameStrings(existingIntent.expected, blockedLabels)
2145
+ || existingIntent.blockKind !== kind
2146
+ || existingIntent.evidenceCode !== evidenceCode) {
2147
+ return this.invokedFailure(active, 'review-feedback-blocked-labels-intent-diverged');
2148
+ }
2149
+ let issue = await this.readIssue(active.record.issueNumber);
2150
+ const ownedRunningLabels = [config.github.labels.auto.name, config.github.labels.running.name].sort();
2151
+ const ownedReviewLabels = [config.github.labels.review.name];
2152
+ const canReduceAuthority = !!issue && issue.state === 'OPEN'
2153
+ && (sameStrings(issue.labels, ownedRunningLabels) || sameStrings(issue.labels, ownedReviewLabels));
2154
+ if (issue?.state === 'OPEN' && !sameStrings(issue.labels, blockedLabels) && !canReduceAuthority) {
2155
+ return this.invokedFailure(active, 'review-feedback-blocked-labels-source-diverged');
2156
+ }
2157
+ if (canReduceAuthority && !sameStrings(issue.labels, blockedLabels)) {
2158
+ try {
2159
+ await this.dependencies.issues.setLabels(active.record.issueNumber, blockedLabels);
2160
+ }
2161
+ catch {
2162
+ return this.invokedFailure(active, 'review-feedback-blocked-labels-delivery-unknown');
2163
+ }
2164
+ issue = await this.readIssue(active.record.issueNumber);
2165
+ }
2166
+ if (canReduceAuthority && (!issue || !sameStrings(issue.labels, blockedLabels))) {
2167
+ return this.invokedFailure(active, 'review-feedback-blocked-labels-observation-diverged');
2168
+ }
2169
+ }
2170
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: `review feedback ${kind}` });
2171
+ const terminalOutcome = { status: 'blocked', kind, resumable: false, evidencePath: evidence.path };
2172
+ const directReview = active.record.directReview && active.record.directReview.status !== 'terminal'
2173
+ ? projectTerminalDirectReview(active.record.directReview, { status: 'blocked', kind })
2174
+ : active.record.directReview;
2175
+ await this.persist(active, {
2176
+ lifecycle: 'blocked',
2177
+ reviewFeedback: blockReviewFeedback(feedback, kind, this.timestamp()),
2178
+ ...(directReview ? { directReview } : {}),
2179
+ terminalOutcome,
2180
+ outcomeEvidenceId: evidence.id,
2181
+ process: undefined,
2182
+ intent: undefined,
2183
+ });
2184
+ return publicOutcome(terminalOutcome);
2185
+ }
1472
2186
  async mapImplementationFailure(active, result) {
1473
2187
  if (result.kind === 'transport-failed')
1474
2188
  return this.terminal(active, { status: 'transport-failed', resumable: result.resumable });
@@ -1495,6 +2209,9 @@ export class RunIssue {
1495
2209
  return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
1496
2210
  }
1497
2211
  async terminal(active, outcome, evidenceCode = outcome.status, retainIntent = false) {
2212
+ if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
2213
+ return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
2214
+ }
1498
2215
  const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
1499
2216
  const terminalOutcome = { ...outcome, evidencePath: evidence.path };
1500
2217
  const changes = {
@@ -1696,6 +2413,9 @@ function sameFreshness(left, right) {
1696
2413
  && left.untrackedContentSha256 === right.untrackedContentSha256
1697
2414
  && left.worktreeIdentity === right.worktreeIdentity;
1698
2415
  }
2416
+ function expectedImplementationHead(record) {
2417
+ return record.reviewFeedback?.activeBatch?.priorPublishedHeadSha ?? record.baseSha;
2418
+ }
1699
2419
  function commentsWithMarker(issue, marker) {
1700
2420
  return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
1701
2421
  }