codex-orchestrator 2.0.4 → 2.0.6

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 (92) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +30 -3
  3. package/dist/src/v2/acceptance-proof.d.ts +12 -0
  4. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  5. package/dist/src/v2/acceptance-proof.js +120 -6
  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/adapters/gh-pull-request-adapter.d.ts +7 -1
  14. package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts.map +1 -1
  15. package/dist/src/v2/adapters/gh-pull-request-adapter.js +288 -0
  16. package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
  17. package/dist/src/v2/adapters/pull-requests.d.ts +69 -0
  18. package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
  19. package/dist/src/v2/adapters/pull-requests.js +48 -0
  20. package/dist/src/v2/adapters/pull-requests.js.map +1 -1
  21. package/dist/src/v2/adapters/worktree.d.ts +1 -0
  22. package/dist/src/v2/adapters/worktree.d.ts.map +1 -1
  23. package/dist/src/v2/adapters/worktree.js +10 -1
  24. package/dist/src/v2/adapters/worktree.js.map +1 -1
  25. package/dist/src/v2/android-proof-runner.d.ts +93 -0
  26. package/dist/src/v2/android-proof-runner.d.ts.map +1 -0
  27. package/dist/src/v2/android-proof-runner.js +921 -0
  28. package/dist/src/v2/android-proof-runner.js.map +1 -0
  29. package/dist/src/v2/cli.d.ts +9 -0
  30. package/dist/src/v2/cli.d.ts.map +1 -1
  31. package/dist/src/v2/cli.js +38 -11
  32. package/dist/src/v2/cli.js.map +1 -1
  33. package/dist/src/v2/config.d.ts +13 -1
  34. package/dist/src/v2/config.d.ts.map +1 -1
  35. package/dist/src/v2/config.js +57 -4
  36. package/dist/src/v2/config.js.map +1 -1
  37. package/dist/src/v2/containment.d.ts +11 -2
  38. package/dist/src/v2/containment.d.ts.map +1 -1
  39. package/dist/src/v2/containment.js +35 -7
  40. package/dist/src/v2/containment.js.map +1 -1
  41. package/dist/src/v2/direct-delivery.d.ts +1 -1
  42. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  43. package/dist/src/v2/direct-delivery.js +9 -3
  44. package/dist/src/v2/direct-delivery.js.map +1 -1
  45. package/dist/src/v2/immutable-workflow-publisher.js +6 -1
  46. package/dist/src/v2/immutable-workflow-publisher.js.map +1 -1
  47. package/dist/src/v2/mobile-lease.d.ts +9 -0
  48. package/dist/src/v2/mobile-lease.d.ts.map +1 -1
  49. package/dist/src/v2/mobile-lease.js +27 -3
  50. package/dist/src/v2/mobile-lease.js.map +1 -1
  51. package/dist/src/v2/review-feedback-coordinator.d.ts +54 -0
  52. package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -0
  53. package/dist/src/v2/review-feedback-coordinator.js +245 -0
  54. package/dist/src/v2/review-feedback-coordinator.js.map +1 -0
  55. package/dist/src/v2/review-feedback.d.ts +127 -0
  56. package/dist/src/v2/review-feedback.d.ts.map +1 -0
  57. package/dist/src/v2/review-feedback.js +436 -0
  58. package/dist/src/v2/review-feedback.js.map +1 -0
  59. package/dist/src/v2/run-issue.d.ts +61 -0
  60. package/dist/src/v2/run-issue.d.ts.map +1 -1
  61. package/dist/src/v2/run-issue.js +843 -66
  62. package/dist/src/v2/run-issue.js.map +1 -1
  63. package/dist/src/v2/run-store.d.ts +48 -1
  64. package/dist/src/v2/run-store.d.ts.map +1 -1
  65. package/dist/src/v2/run-store.js +125 -5
  66. package/dist/src/v2/run-store.js.map +1 -1
  67. package/dist/src/v2/runtime.d.ts +37 -0
  68. package/dist/src/v2/runtime.d.ts.map +1 -1
  69. package/dist/src/v2/runtime.js +183 -15
  70. package/dist/src/v2/runtime.js.map +1 -1
  71. package/dist/src/v2/setup.js +1 -1
  72. package/dist/src/v2/setup.js.map +1 -1
  73. package/docs/deep-dive.md +71 -7
  74. package/internal-workflow/docs/agents/contract-test-ledger.md +11 -1
  75. package/internal-workflow/evals/coding-skill-evals.json +18 -0
  76. package/internal-workflow/manifest.json +1 -1
  77. package/internal-workflow/skills/acceptance-proof/SKILL.md +1 -1
  78. package/internal-workflow/skills/acceptance-proof/references/android.md +6 -6
  79. package/internal-workflow/skills/code-debugger/SKILL.md +3 -3
  80. package/internal-workflow/skills/code-review/SKILL.md +18 -6
  81. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +10 -8
  82. package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +1 -1
  83. package/internal-workflow/skills/implementation-spec-review/SKILL.md +17 -3
  84. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +30 -0
  85. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +30 -6
  86. package/internal-workflow/skills/spec-implementer/references/review-loop.md +7 -1
  87. package/internal-workflow/skills/tdd/SKILL.md +5 -4
  88. package/internal-workflow/skills/tdd/evals/evals.json +18 -0
  89. package/internal-workflow/skills/tdd/mocking.md +3 -42
  90. package/internal-workflow/skills/tdd/refactoring.md +6 -8
  91. package/package.json +1 -1
  92. package/internal-workflow/skills/acceptance-proof/tools/android-lease.mjs +0 -280
@@ -4,10 +4,11 @@ import { parseAgentAutoConfig } from './config.js';
4
4
  import { canonicalJson, sha256 } from './containment.js';
5
5
  import { validateImplementationReport } from './implementation-report.js';
6
6
  import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
7
- import { ProofQuiescenceError } from './acceptance-proof.js';
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';
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 {
@@ -103,7 +104,7 @@ export class RunIssue {
103
104
  return await this.publicationDiverged(active, 'commit-tree-diverged');
104
105
  if (this.signal.aborted)
105
106
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
106
- if (!await this.authorized(issueNumber, runId, branchName, config))
107
+ if (!await this.authorized(active, config))
107
108
  return await this.revoked(active);
108
109
  try {
109
110
  commitSha = await this.dependencies.git.commit({ worktreePath, message: intent.message });
@@ -135,7 +136,7 @@ export class RunIssue {
135
136
  if (!remoteSha) {
136
137
  if (this.signal.aborted)
137
138
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
138
- if (!await this.authorized(issueNumber, runId, branchName, config))
139
+ if (!await this.authorized(active, config))
139
140
  return await this.revoked(active);
140
141
  try {
141
142
  await this.dependencies.git.push({ worktreePath, branchName });
@@ -169,7 +170,7 @@ export class RunIssue {
169
170
  if (!observed) {
170
171
  if (this.signal.aborted)
171
172
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
172
- if (!await this.authorized(issueNumber, runId, branchName, config))
173
+ if (!await this.authorized(active, config))
173
174
  return await this.revoked(active);
174
175
  try {
175
176
  await this.dependencies.pullRequests.createDraft({
@@ -209,7 +210,7 @@ export class RunIssue {
209
210
  if (matching.length === 0) {
210
211
  if (this.signal.aborted)
211
212
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
212
- if (!await this.authorized(issueNumber, runId, branchName, config))
213
+ if (!await this.authorized(active, config))
213
214
  return await this.revoked(active);
214
215
  try {
215
216
  await this.dependencies.issues.postComment(issueNumber, handoffBody);
@@ -235,7 +236,7 @@ export class RunIssue {
235
236
  if (!sameStrings(observation.labels, terminalLabels)) {
236
237
  if (this.signal.aborted)
237
238
  return await this.terminal(await this.clearIntent(active), { status: 'cancelled' });
238
- if (!await this.authorized(issueNumber, runId, branchName, config))
239
+ if (!await this.authorized(active, config))
239
240
  return await this.revoked(active);
240
241
  try {
241
242
  await this.dependencies.issues.setLabels(issueNumber, terminalLabels);
@@ -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(active, 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(active, 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(active, 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(active, 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, [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);
599
+ const labelsAreReview = !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, reviewLabels)
600
+ && this.hasTrustedClaim(issue, active.record);
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)) {
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,32 +743,58 @@ 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);
352
779
  if ('result' in claim)
353
780
  return claim.result;
354
781
  active = claim.active;
355
- const worktree = await this.dependencies.git.inspectWorktree({ worktreePath, branchName, baseSha });
356
- if (worktree === 'diverged')
357
- return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'claim-worktree-diverged');
782
+ let worktree;
783
+ try {
784
+ worktree = await this.dependencies.git.inspectWorktree({ worktreePath, branchName, baseSha });
785
+ }
786
+ catch (error) {
787
+ return await this.invokedFailure(active, 'local-git-worktree-inspection-failed', claimedWorktreeInspectionFailureSummary(error));
788
+ }
789
+ if (worktree === 'diverged') {
790
+ 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.');
791
+ }
358
792
  if (worktree === 'absent') {
359
793
  try {
360
794
  await this.dependencies.git.createWorktree({ targetRoot, worktreePath, branchName, baseBranch: config.github.baseBranch, baseSha });
361
795
  }
362
- catch {
363
- return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
796
+ catch (error) {
797
+ return await this.invokedFailure(active, 'local-git-worktree-creation-failed', worktreeCreationFailureSummary(error));
364
798
  }
365
799
  }
366
800
  active = await this.initializeClaimedRun(active, issue);
@@ -388,14 +822,45 @@ export class RunIssue {
388
822
  frozenCriteria = structuredClone(active.record.frozenCriteria);
389
823
  }
390
824
  if (existing.lifecycle === 'spec-authoring') {
391
- if (!await this.authorized(input.issueNumber, runId, branchName, config))
825
+ if (!await this.authorized(active, config))
392
826
  return await this.revoked(active);
393
827
  return await this.continueSpecRequired(active);
394
828
  }
829
+ const feedbackInvocation = active.record.reviewFeedback?.implementationInvocation;
830
+ if (feedbackInvocation?.phase === 'prepared') {
831
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-launch-ambiguous');
832
+ }
833
+ if (feedbackInvocation?.phase === 'launched') {
834
+ try {
835
+ await this.dependencies.waitForReviewProcessAbsence(feedbackInvocation.processGroupId);
836
+ }
837
+ catch {
838
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-process-absence-unconfirmed');
839
+ }
840
+ if (!this.dependencies.implementationAgent.recover) {
841
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-unavailable');
842
+ }
843
+ try {
844
+ recoveredFeedbackImplementation = await this.dependencies.implementationAgent.recover({
845
+ runId: active.record.runId,
846
+ canonicalRepository: active.record.canonicalRepository,
847
+ invocation: feedbackInvocation,
848
+ });
849
+ }
850
+ catch {
851
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-report-recovery-failed');
852
+ }
853
+ if (!recoveredFeedbackImplementation) {
854
+ const snapshot = await this.dependencies.git.snapshot(active.record.worktreePath);
855
+ if (!sameFreshness(snapshot, feedbackInvocation.baseline)) {
856
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
857
+ }
858
+ }
859
+ }
395
860
  if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
396
861
  return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
397
862
  }
398
- if (!await this.authorized(input.issueNumber, runId, branchName, config))
863
+ if (!await this.authorized(active, config))
399
864
  return await this.revoked(active);
400
865
  if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
401
866
  if (!active.record.routeExecution || !active.record.routeReceipt)
@@ -404,8 +869,12 @@ export class RunIssue {
404
869
  && (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
405
870
  const checkRecovery = active.record.lifecycle === 'checking'
406
871
  && active.record.directReview?.status === 'clear';
407
- if (!reviewRecovery && !checkRecovery) {
408
- if (active.record.cycle >= config.runner.maxCycles) {
872
+ const proofRecovery = active.record.lifecycle === 'proving'
873
+ && active.record.directReview?.status === 'clear';
874
+ const feedbackRepair = active.record.reviewFeedback?.phase === 'repairing'
875
+ && active.record.directReview?.stage === 'review-repair';
876
+ if (!reviewRecovery && !checkRecovery && !proofRecovery && !feedbackRepair) {
877
+ if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
409
878
  return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
410
879
  }
411
880
  active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
@@ -430,7 +899,7 @@ export class RunIssue {
430
899
  const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
431
900
  active = await this.createRun({
432
901
  runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
433
- issueSnapshot, frozenCriteria,
902
+ issueSnapshot, frozenCriteria, config,
434
903
  intent: { kind: 'claim-labels', issueNumber: input.issueNumber, expected: runningLabels },
435
904
  });
436
905
  const claim = await this.reconcileClaim(active, config);
@@ -440,8 +909,8 @@ export class RunIssue {
440
909
  try {
441
910
  await this.dependencies.git.createWorktree({ targetRoot, worktreePath, branchName, baseBranch: config.github.baseBranch, baseSha });
442
911
  }
443
- catch {
444
- return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
912
+ catch (error) {
913
+ return await this.invokedFailure(active, 'local-git-worktree-creation-failed', worktreeCreationFailureSummary(error));
445
914
  }
446
915
  active = await this.initializeClaimedRun(active, issue);
447
916
  issueSnapshot = structuredClone(active.record.issueSnapshot);
@@ -452,19 +921,19 @@ export class RunIssue {
452
921
  return routed.result;
453
922
  active = routed.active;
454
923
  }
455
- if (active.record.lifecycle !== 'implementing' && active.record.lifecycle !== 'checking') {
924
+ if (!['implementing', 'checking', 'proving'].includes(active.record.lifecycle)) {
456
925
  return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
457
926
  }
458
927
  if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
459
928
  return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
460
929
  }
461
930
  attemptLoop: while (true) {
462
- if (!await this.authorized(input.issueNumber, runId, branchName, config)) {
931
+ if (!await this.authorized(active, config)) {
463
932
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
464
933
  }
465
934
  if (this.signal.aborted)
466
935
  return await this.terminal(active, { status: 'cancelled' });
467
- let resumeAtChecks = active.record.lifecycle === 'checking'
936
+ let resumeAtChecks = (active.record.lifecycle === 'checking' || active.record.lifecycle === 'proving')
468
937
  && active.record.directReview?.status === 'clear';
469
938
  if (active.record.lifecycle === 'implementing'
470
939
  && active.record.directReview?.status === 'active'
@@ -473,7 +942,7 @@ export class RunIssue {
473
942
  if ('status' in recovered)
474
943
  return recovered;
475
944
  active = recovered;
476
- const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
945
+ const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
477
946
  if ('status' in reviewed)
478
947
  return reviewed;
479
948
  active = reviewed;
@@ -482,8 +951,40 @@ export class RunIssue {
482
951
  resumeAtChecks = true;
483
952
  }
484
953
  if (!resumeAtChecks) {
954
+ const feedbackBatch = active.record.reviewFeedback?.activeBatch;
955
+ const workerBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
956
+ if (workerBlock)
957
+ return workerBlock;
958
+ const feedbackProjection = feedbackBatch
959
+ ? projectReviewFeedbackBatch(feedbackBatch, active.record.directReview.targetRevision)
960
+ : undefined;
961
+ const feedbackLaunch = feedbackBatch ? {
962
+ reviewFeedbackRound: active.record.reviewFeedback.repairRound,
963
+ reviewFeedback: feedbackProjection.workerFeedback,
964
+ onPrepared: async (invocation) => {
965
+ const currentActive = active;
966
+ active = await this.persist(currentActive, {
967
+ reviewFeedback: {
968
+ ...structuredClone(currentActive.record.reviewFeedback),
969
+ implementationInvocation: { ...structuredClone(invocation), phase: 'prepared', pid: null, processGroupId: null, launchedAt: null },
970
+ },
971
+ });
972
+ },
973
+ onLaunched: async (invocation) => {
974
+ const currentActive = active;
975
+ const current = currentActive.record.reviewFeedback?.implementationInvocation;
976
+ if (!current || current.phase !== 'prepared' || current.attemptId !== invocation.attemptId)
977
+ throw new Error('review feedback implementation launch mismatch');
978
+ active = await this.persist(currentActive, {
979
+ reviewFeedback: {
980
+ ...structuredClone(currentActive.record.reviewFeedback),
981
+ implementationInvocation: { ...current, phase: 'launched', pid: invocation.pid, processGroupId: invocation.processGroupId, launchedAt: invocation.launchedAt },
982
+ },
983
+ });
984
+ },
985
+ } : {};
485
986
  const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
486
- let implementation = await this.runImplementation({
987
+ let implementation = recoveredFeedbackImplementation ?? await this.runImplementation({
487
988
  runId,
488
989
  worktreePath,
489
990
  issue: publicIssueSnapshot(issueSnapshot),
@@ -492,7 +993,9 @@ export class RunIssue {
492
993
  reworkFindings: active.record.reworkFindings,
493
994
  repairOnly: false,
494
995
  workflowGeneration: active.record.workflowGeneration,
996
+ ...feedbackLaunch,
495
997
  });
998
+ recoveredFeedbackImplementation = undefined;
496
999
  if (implementation.kind === 'safe-halt') {
497
1000
  active = await this.persist(active, {
498
1001
  lifecycle: 'safe-halt',
@@ -521,6 +1024,10 @@ export class RunIssue {
521
1024
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
522
1025
  }
523
1026
  if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
1027
+ const invocation = active.record.reviewFeedback?.implementationInvocation;
1028
+ if (invocation && !sameFreshness(await this.dependencies.git.snapshot(worktreePath), invocation.baseline)) {
1029
+ return await this.blockReviewFeedback(active, 'safety', 'review-feedback-implementation-worktree-ambiguous');
1030
+ }
524
1031
  active = await this.persist(active, { transportRetries: 1 });
525
1032
  continue attemptLoop;
526
1033
  }
@@ -536,6 +1043,9 @@ export class RunIssue {
536
1043
  }
537
1044
  const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
538
1045
  active = await this.persist(active, { reportRepairs: 1 });
1046
+ const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
1047
+ if (repairBlock)
1048
+ return repairBlock;
539
1049
  implementation = await this.runImplementation({
540
1050
  runId,
541
1051
  worktreePath,
@@ -545,6 +1055,7 @@ export class RunIssue {
545
1055
  reworkFindings: ['The previous implementation report did not match the generated schema.'],
546
1056
  repairOnly: true,
547
1057
  workflowGeneration: active.record.workflowGeneration,
1058
+ ...feedbackLaunch,
548
1059
  });
549
1060
  if (implementation.kind === 'safe-halt') {
550
1061
  active = await this.persist(active, {
@@ -583,7 +1094,7 @@ export class RunIssue {
583
1094
  if (report.status === 'external-block') {
584
1095
  return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
585
1096
  }
586
- if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
1097
+ if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
587
1098
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
588
1099
  }
589
1100
  const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
@@ -593,6 +1104,9 @@ export class RunIssue {
593
1104
  }
594
1105
  const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
595
1106
  active = await this.persist(active, { reportRepairs: 1 });
1107
+ const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
1108
+ if (repairBlock)
1109
+ return repairBlock;
596
1110
  implementation = await this.runImplementation({
597
1111
  runId,
598
1112
  worktreePath,
@@ -602,6 +1116,7 @@ export class RunIssue {
602
1116
  reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
603
1117
  repairOnly: true,
604
1118
  workflowGeneration: active.record.workflowGeneration,
1119
+ ...feedbackLaunch,
605
1120
  });
606
1121
  if (implementation.kind === 'safe-halt') {
607
1122
  active = await this.persist(active, {
@@ -655,6 +1170,9 @@ export class RunIssue {
655
1170
  if (active.record.directReview?.stage === 'review-repair') {
656
1171
  active = await this.persist(active, {
657
1172
  directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
1173
+ ...(active.record.reviewFeedback ? {
1174
+ reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
1175
+ } : {}),
658
1176
  });
659
1177
  }
660
1178
  else {
@@ -665,9 +1183,12 @@ export class RunIssue {
665
1183
  targetFingerprint,
666
1184
  codeReviewerSessionId: reviewerSessionId,
667
1185
  }),
1186
+ ...(active.record.reviewFeedback ? {
1187
+ reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
1188
+ } : {}),
668
1189
  });
669
1190
  }
670
- const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
1191
+ const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles, config);
671
1192
  if ('status' in reviewed)
672
1193
  return reviewed;
673
1194
  active = reviewed;
@@ -675,7 +1196,12 @@ export class RunIssue {
675
1196
  continue attemptLoop;
676
1197
  }
677
1198
  else {
678
- active = await this.persist(active, { lifecycle: 'checking' });
1199
+ active = await this.persist(active, {
1200
+ lifecycle: 'checking',
1201
+ ...(active.record.reviewFeedback ? {
1202
+ reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
1203
+ } : {}),
1204
+ });
679
1205
  }
680
1206
  }
681
1207
  for (const [id, command] of Object.entries(config.checks)) {
@@ -695,8 +1221,10 @@ export class RunIssue {
695
1221
  const row = { id, command, status: check.status, outputSha256: sha256(check.output) };
696
1222
  active = await this.persist(active, { checks: [...active.record.checks, row] });
697
1223
  if (check.status !== 'passed') {
698
- if (active.record.cycle >= config.runner.maxCycles) {
699
- return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
1224
+ if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
1225
+ return active.record.reviewFeedback?.activeBatch
1226
+ ? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-check-repair-exhausted')
1227
+ : await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
700
1228
  }
701
1229
  const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
702
1230
  active = await this.startNextCycle(active, [summary], [{
@@ -734,7 +1262,7 @@ export class RunIssue {
734
1262
  catch {
735
1263
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
736
1264
  }
737
- if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
1265
+ if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
738
1266
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
739
1267
  }
740
1268
  const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
@@ -750,7 +1278,7 @@ export class RunIssue {
750
1278
  runId,
751
1279
  issueNumber: input.issueNumber,
752
1280
  cycle: active.record.cycle,
753
- baseSha,
1281
+ baseSha: expectedImplementationHead(active.record),
754
1282
  headSha: freshness.headSha,
755
1283
  indexTreeSha: freshness.indexTreeSha,
756
1284
  trackedContentSha256: freshness.trackedContentSha256,
@@ -775,9 +1303,16 @@ export class RunIssue {
775
1303
  frozenCriteria,
776
1304
  checkedChange,
777
1305
  workflowGeneration: structuredClone(active.record.workflowGeneration),
1306
+ beforeAgentLaunch: async () => {
1307
+ const failure = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
1308
+ if (failure)
1309
+ throw new ProofLaunchAuthorizationError(failure);
1310
+ },
778
1311
  });
779
1312
  }
780
1313
  catch (error) {
1314
+ if (error instanceof ProofLaunchAuthorizationError)
1315
+ return error.outcome;
781
1316
  if (error instanceof ProofQuiescenceError) {
782
1317
  active = await this.persist(active, {
783
1318
  lifecycle: 'safe-halt',
@@ -813,8 +1348,10 @@ export class RunIssue {
813
1348
  if (this.signal.aborted)
814
1349
  return await this.terminal(active, { status: 'cancelled' });
815
1350
  if (proof.status === 'needs-rework') {
816
- if (active.record.cycle >= config.runner.maxCycles) {
817
- return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
1351
+ if (this.repairBudgetExhausted(active, config.runner.maxCycles)) {
1352
+ return active.record.reviewFeedback?.activeBatch
1353
+ ? await this.blockReviewFeedback(active, 'exhausted', 'review-feedback-proof-repair-exhausted')
1354
+ : await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
818
1355
  }
819
1356
  active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
820
1357
  provenance: 'proof',
@@ -826,10 +1363,23 @@ export class RunIssue {
826
1363
  }
827
1364
  if (proof.status !== 'passed')
828
1365
  return await this.mapProofFailure(active, proof);
829
- active = await this.persist(active, { lifecycle: 'publishing', proofReceipt: proof.receipt, reworkFindings: [] });
1366
+ active = await this.persist(active, {
1367
+ lifecycle: 'publishing',
1368
+ proofReceipt: proof.receipt,
1369
+ reworkFindings: [],
1370
+ ...(active.record.reviewFeedback?.activeBatch ? {
1371
+ reviewFeedback: markReviewFeedbackPublishing(markReviewFeedbackVerified(active.record.reviewFeedback, {
1372
+ checkedChangeSha256,
1373
+ proofId,
1374
+ verifiedAt: this.timestamp(),
1375
+ })),
1376
+ } : {}),
1377
+ });
830
1378
  break;
831
1379
  }
832
- return await this.publish(active, config, issueSnapshot, input.issueNumber);
1380
+ return active.record.reviewFeedback?.activeBatch
1381
+ ? await this.updateExistingPullRequest(active, config, input.issueNumber)
1382
+ : await this.publish(active, config, issueSnapshot, input.issueNumber);
833
1383
  }
834
1384
  catch (error) {
835
1385
  if (!active && error instanceof TransportReadError) {
@@ -900,12 +1450,31 @@ export class RunIssue {
900
1450
  catch {
901
1451
  throw new RouteInitializationUnrecoverableError();
902
1452
  }
903
- const issueSnapshot = issue?.state === 'OPEN' ? snapshotIssue(issue) : active.record.issueSnapshot;
1453
+ const issueSnapshot = issue?.state === 'OPEN'
1454
+ ? refreshClaimedIssueSnapshot(active.record.issueSnapshot, issue)
1455
+ : active.record.issueSnapshot;
904
1456
  return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
905
1457
  }
906
1458
  async routeRun(starting, issue, frozenCriteria, worktreePath, config, issueNumber, branchName) {
907
1459
  let active = starting;
908
1460
  while (active.record.lifecycle === 'triaging') {
1461
+ let currentIssue;
1462
+ try {
1463
+ currentIssue = await this.readIssue(active.record.issueNumber);
1464
+ }
1465
+ catch (error) {
1466
+ if (error instanceof TransportReadError) {
1467
+ return { result: await this.invokedFailure(active, 'authorization-read-failed') };
1468
+ }
1469
+ throw error;
1470
+ }
1471
+ if (!currentIssue || !this.isAuthorizedIssue(currentIssue, active.record, config)) {
1472
+ return { result: await this.revoked(active) };
1473
+ }
1474
+ active = await this.persist(active, {
1475
+ issueSnapshot: refreshClaimedIssueSnapshot(active.record.issueSnapshot, currentIssue),
1476
+ });
1477
+ issue = structuredClone(active.record.issueSnapshot);
909
1478
  const state = {
910
1479
  read: async () => requireRouteExecution(active.record.routeExecution),
911
1480
  compareAndSwap: async (expected, next) => {
@@ -1007,7 +1576,7 @@ export class RunIssue {
1007
1576
  return { result: await this.terminal(active, { status: 'internal-error', code: 'route-state-not-dispatchable' }) };
1008
1577
  }
1009
1578
  try {
1010
- if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
1579
+ if (!await this.authorized(active, config)) {
1011
1580
  return { result: await this.revoked(active) };
1012
1581
  }
1013
1582
  }
@@ -1032,7 +1601,7 @@ export class RunIssue {
1032
1601
  if (receipt.route !== 'awaiting-user')
1033
1602
  active = await this.persist(active, { lifecycle });
1034
1603
  try {
1035
- if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
1604
+ if (!await this.authorized(active, config)) {
1036
1605
  return { result: await this.revoked(active) };
1037
1606
  }
1038
1607
  }
@@ -1223,8 +1792,9 @@ export class RunIssue {
1223
1792
  const state = await this.dependencies.runRecords.read();
1224
1793
  const now = this.timestamp();
1225
1794
  const workflow = await this.dependencies.createWorkflowGeneration();
1795
+ const { config, ...persistedInput } = input;
1226
1796
  const record = {
1227
- ...input,
1797
+ ...persistedInput,
1228
1798
  lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
1229
1799
  reworkFindings: [],
1230
1800
  packageVersion: workflow.receipt.packageVersion,
@@ -1233,9 +1803,9 @@ export class RunIssue {
1233
1803
  checks: [], createdAt: now, updatedAt: now,
1234
1804
  };
1235
1805
  const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
1236
- schema: 'codex-orchestrator.agent-auto-state', version: 1, runs: [...state.runs, record],
1806
+ schema: 'codex-orchestrator.agent-auto-state', version: 2, runs: [...state.runs, record],
1237
1807
  });
1238
- return { state: saved, record: findRun(saved, input.runId) };
1808
+ return { state: saved, record: findRun(saved, input.runId), config };
1239
1809
  }
1240
1810
  async persist(active, changes) {
1241
1811
  const record = { ...active.record, ...changes, updatedAt: this.timestamp() };
@@ -1243,15 +1813,15 @@ export class RunIssue {
1243
1813
  delete record.intent;
1244
1814
  if (Object.hasOwn(changes, 'process') && changes.process === undefined)
1245
1815
  delete record.process;
1246
- for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
1816
+ for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reviewFeedback']) {
1247
1817
  if (Object.hasOwn(changes, key) && changes[key] === undefined)
1248
1818
  delete record[key];
1249
1819
  }
1250
1820
  const runs = active.state.runs.map((candidate) => candidate.runId === record.runId ? record : candidate);
1251
1821
  const saved = await this.dependencies.runRecords.compareAndSwap(active.state.generation, {
1252
- schema: 'codex-orchestrator.agent-auto-state', version: 1, runs,
1822
+ schema: 'codex-orchestrator.agent-auto-state', version: 2, runs,
1253
1823
  });
1254
- return { state: saved, record: findRun(saved, record.runId) };
1824
+ return { state: saved, record: findRun(saved, record.runId), config: active.config };
1255
1825
  }
1256
1826
  clearIntent(active) {
1257
1827
  return this.persist(active, { intent: undefined });
@@ -1264,25 +1834,47 @@ export class RunIssue {
1264
1834
  throw new PostEffectStateError(active);
1265
1835
  }
1266
1836
  }
1267
- async authorized(issueNumber, runId, branchName, config) {
1268
- const issue = await this.readIssue(issueNumber);
1269
- if (!issue || issue.state !== 'OPEN')
1837
+ async authorized(active, config) {
1838
+ const issue = await this.readIssue(active.record.issueNumber);
1839
+ return !!issue && this.isAuthorizedIssue(issue, active.record, config);
1840
+ }
1841
+ isAuthorizedIssue(issue, record, config) {
1842
+ if (issue.state !== 'OPEN')
1270
1843
  return false;
1271
1844
  const labels = new Set(issue.labels);
1272
1845
  if (!labels.has(config.github.labels.auto.name) || !labels.has(config.github.labels.running.name))
1273
1846
  return false;
1274
- const exactBody = claimComment(runId, issueNumber, branchName);
1275
- 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;
1276
- const markers = issue.comments.filter((comment) => markerPattern.test(comment.body.split('\n')[0] ?? ''));
1847
+ return this.hasTrustedClaim(issue, record);
1848
+ }
1849
+ async authorizedForExactLabels(active, expectedLabels) {
1850
+ const issue = await this.readIssue(active.record.issueNumber);
1851
+ return !!issue && issue.state === 'OPEN' && sameStrings(issue.labels, expectedLabels)
1852
+ && this.hasTrustedClaim(issue, active.record);
1853
+ }
1854
+ hasTrustedClaim(issue, record) {
1855
+ const exactBody = claimComment(record.runId, record.issueNumber, record.branchName);
1856
+ const markers = issue.comments.filter(isClaimMarkerComment);
1277
1857
  if (issue.comments.some((comment) => {
1278
1858
  const firstLine = comment.body.split('\n')[0] ?? '';
1279
- return firstLine.startsWith(`<!-- codex-orchestrator:run:${runId}:claim`) && !markerPattern.test(firstLine);
1859
+ return firstLine.startsWith(`<!-- codex-orchestrator:run:${record.runId}:claim`) && !claimMarkerPattern.test(firstLine);
1280
1860
  }))
1281
1861
  return false;
1282
- if (markers.some((comment) => (comment.body.split('\n')[0] ?? '').match(markerPattern)?.[1] !== runId))
1862
+ const currentMarkers = markers.filter((comment) => claimRunId(comment) === record.runId);
1863
+ if (currentMarkers.some((comment) => comment.body !== exactBody
1864
+ || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.authorAssociation)))
1283
1865
  return false;
1284
- const current = markers.filter((comment) => comment.body === exactBody);
1285
- return current.length === 1 && ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(current[0].authorAssociation);
1866
+ const historicalClaims = historicalClaimCounts(record);
1867
+ for (const comment of markers) {
1868
+ const markerRunId = claimRunId(comment);
1869
+ if (markerRunId === record.runId)
1870
+ continue;
1871
+ const key = observedHistoricalClaimKeys(comment, record)
1872
+ .find((candidate) => (historicalClaims.get(candidate) ?? 0) > 0);
1873
+ if (!key)
1874
+ return false;
1875
+ historicalClaims.set(key, historicalClaims.get(key) - 1);
1876
+ }
1877
+ return currentMarkers.length === 1;
1286
1878
  }
1287
1879
  async readIssue(issueNumber) {
1288
1880
  try {
@@ -1292,6 +1884,30 @@ export class RunIssue {
1292
1884
  throw new TransportReadError();
1293
1885
  }
1294
1886
  }
1887
+ async revalidateFeedbackWorker(active, config, issueNumber) {
1888
+ const batch = active.record.reviewFeedback?.activeBatch;
1889
+ if (!batch)
1890
+ return undefined;
1891
+ if (!await this.authorized(active, config)) {
1892
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-authority-revoked');
1893
+ }
1894
+ if (!this.dependencies.reviewFeedback) {
1895
+ return this.blockReviewFeedback(active, 'safety', 'review-feedback-worker-revalidation-unavailable');
1896
+ }
1897
+ const validation = await this.dependencies.reviewFeedback.revalidate({
1898
+ batch,
1899
+ epoch: 'pre-update',
1900
+ expectedHeadSha: batch.priorPublishedHeadSha,
1901
+ });
1902
+ return this.mapFeedbackRevalidation(active, validation, 'review-feedback-worker-revalidation-failed');
1903
+ }
1904
+ async mapFeedbackRevalidation(active, validation, safetyCode) {
1905
+ if (validation.status === 'valid')
1906
+ return undefined;
1907
+ if (validation.status === 'retryable')
1908
+ return this.invokedFailure(active, `${safetyCode}-retryable`);
1909
+ return this.blockReviewFeedback(active, 'safety', safetyCode);
1910
+ }
1295
1911
  async runImplementation(input) {
1296
1912
  try {
1297
1913
  return await this.dependencies.implementationAgent.run({
@@ -1304,7 +1920,7 @@ export class RunIssue {
1304
1920
  return { kind: 'internal-error' };
1305
1921
  }
1306
1922
  }
1307
- async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
1923
+ async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles, config) {
1308
1924
  let active = starting;
1309
1925
  let reportRepair;
1310
1926
  while (true) {
@@ -1321,6 +1937,9 @@ export class RunIssue {
1321
1937
  ? directReviewClosureRequestSha256(directReview)
1322
1938
  : null;
1323
1939
  const mode = directReview.stage === 'review-closure' ? 'closure' : 'full';
1940
+ const reviewerAuthorizationFailure = await this.revalidateFeedbackWorker(active, config, active.record.issueNumber);
1941
+ if (reviewerAuthorizationFailure)
1942
+ return reviewerAuthorizationFailure;
1324
1943
  const result = await this.dependencies.implementationReviewer.run({
1325
1944
  runId: active.record.runId,
1326
1945
  worktreePath: active.record.worktreePath,
@@ -1369,8 +1988,10 @@ export class RunIssue {
1369
1988
  if (!current)
1370
1989
  return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
1371
1990
  if (result.report.verdict === 'needs-work') {
1372
- if (active.record.cycle >= maxCycles) {
1373
- return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
1991
+ if (this.repairBudgetExhausted(active, maxCycles)) {
1992
+ return active.record.reviewFeedback?.activeBatch
1993
+ ? this.blockReviewFeedback(active, 'exhausted', 'direct-review-repair-exhausted')
1994
+ : this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
1374
1995
  }
1375
1996
  const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
1376
1997
  const findings = [
@@ -1383,7 +2004,12 @@ export class RunIssue {
1383
2004
  ];
1384
2005
  return this.persist(active, {
1385
2006
  lifecycle: 'implementing',
1386
- cycle: (active.record.cycle + 1),
2007
+ cycle: active.record.reviewFeedback?.activeBatch
2008
+ ? active.record.cycle
2009
+ : (active.record.cycle + 1),
2010
+ ...(active.record.reviewFeedback?.activeBatch ? {
2011
+ reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
2012
+ } : {}),
1387
2013
  directReview: repaired,
1388
2014
  reworkFindings: findings,
1389
2015
  checks: [],
@@ -1505,7 +2131,12 @@ export class RunIssue {
1505
2131
  }));
1506
2132
  return this.persist(active, {
1507
2133
  lifecycle: 'implementing',
1508
- cycle: (active.record.cycle + 1),
2134
+ cycle: active.record.reviewFeedback?.activeBatch
2135
+ ? active.record.cycle
2136
+ : (active.record.cycle + 1),
2137
+ ...(active.record.reviewFeedback?.activeBatch ? {
2138
+ reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
2139
+ } : {}),
1509
2140
  reworkFindings: [...findings],
1510
2141
  directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
1511
2142
  checks: [],
@@ -1517,13 +2148,81 @@ export class RunIssue {
1517
2148
  active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
1518
2149
  return this.persist(active, {
1519
2150
  lifecycle: 'implementing',
1520
- cycle: (active.record.cycle + 1),
2151
+ cycle: active.record.reviewFeedback?.activeBatch
2152
+ ? active.record.cycle
2153
+ : (active.record.cycle + 1),
2154
+ ...(active.record.reviewFeedback?.activeBatch ? {
2155
+ reviewFeedback: reserveNextReviewFeedbackRound(active.record.reviewFeedback),
2156
+ } : {}),
1521
2157
  checks: [],
1522
2158
  checkedChangeSha256: undefined,
1523
2159
  proofId: undefined,
1524
2160
  proofReceipt: undefined,
1525
2161
  });
1526
2162
  }
2163
+ repairBudgetExhausted(active, maxCycles) {
2164
+ return active.record.reviewFeedback?.activeBatch
2165
+ ? active.record.reviewFeedback.repairRound >= 3
2166
+ : active.record.cycle >= maxCycles;
2167
+ }
2168
+ async blockReviewFeedback(active, kind, evidenceCode) {
2169
+ const feedback = active.record.reviewFeedback;
2170
+ if (!feedback?.activeBatch)
2171
+ return this.terminal(active, { status: 'blocked', kind, resumable: false }, evidenceCode);
2172
+ {
2173
+ const config = active.config;
2174
+ const blockedLabels = [config.github.labels.auto.name, config.github.labels.blocked.name].sort();
2175
+ const existingIntent = active.record.intent;
2176
+ if (existingIntent?.kind !== 'review-blocked-labels') {
2177
+ active = await this.persist(active, { intent: {
2178
+ kind: 'review-blocked-labels', issueNumber: active.record.issueNumber,
2179
+ batchId: feedback.activeBatch.batchId, expected: blockedLabels, blockKind: kind, evidenceCode,
2180
+ } });
2181
+ }
2182
+ else if (existingIntent.issueNumber !== active.record.issueNumber
2183
+ || existingIntent.batchId !== feedback.activeBatch.batchId
2184
+ || !sameStrings(existingIntent.expected, blockedLabels)
2185
+ || existingIntent.blockKind !== kind
2186
+ || existingIntent.evidenceCode !== evidenceCode) {
2187
+ return this.invokedFailure(active, 'review-feedback-blocked-labels-intent-diverged');
2188
+ }
2189
+ let issue = await this.readIssue(active.record.issueNumber);
2190
+ const ownedRunningLabels = [config.github.labels.auto.name, config.github.labels.running.name].sort();
2191
+ const ownedReviewLabels = [config.github.labels.review.name];
2192
+ const canReduceAuthority = !!issue && issue.state === 'OPEN'
2193
+ && (sameStrings(issue.labels, ownedRunningLabels) || sameStrings(issue.labels, ownedReviewLabels));
2194
+ if (issue?.state === 'OPEN' && !sameStrings(issue.labels, blockedLabels) && !canReduceAuthority) {
2195
+ return this.invokedFailure(active, 'review-feedback-blocked-labels-source-diverged');
2196
+ }
2197
+ if (canReduceAuthority && !sameStrings(issue.labels, blockedLabels)) {
2198
+ try {
2199
+ await this.dependencies.issues.setLabels(active.record.issueNumber, blockedLabels);
2200
+ }
2201
+ catch {
2202
+ return this.invokedFailure(active, 'review-feedback-blocked-labels-delivery-unknown');
2203
+ }
2204
+ issue = await this.readIssue(active.record.issueNumber);
2205
+ }
2206
+ if (canReduceAuthority && (!issue || !sameStrings(issue.labels, blockedLabels))) {
2207
+ return this.invokedFailure(active, 'review-feedback-blocked-labels-observation-diverged');
2208
+ }
2209
+ }
2210
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: `review feedback ${kind}` });
2211
+ const terminalOutcome = { status: 'blocked', kind, resumable: false, evidencePath: evidence.path };
2212
+ const directReview = active.record.directReview && active.record.directReview.status !== 'terminal'
2213
+ ? projectTerminalDirectReview(active.record.directReview, { status: 'blocked', kind })
2214
+ : active.record.directReview;
2215
+ await this.persist(active, {
2216
+ lifecycle: 'blocked',
2217
+ reviewFeedback: blockReviewFeedback(feedback, kind, this.timestamp()),
2218
+ ...(directReview ? { directReview } : {}),
2219
+ terminalOutcome,
2220
+ outcomeEvidenceId: evidence.id,
2221
+ process: undefined,
2222
+ intent: undefined,
2223
+ });
2224
+ return publicOutcome(terminalOutcome);
2225
+ }
1527
2226
  async mapImplementationFailure(active, result) {
1528
2227
  if (result.kind === 'transport-failed')
1529
2228
  return this.terminal(active, { status: 'transport-failed', resumable: result.resumable });
@@ -1545,11 +2244,14 @@ export class RunIssue {
1545
2244
  async revoked(active) {
1546
2245
  return this.terminal(await this.clearIntent(active), { status: 'blocked', kind: 'safety', resumable: true });
1547
2246
  }
1548
- async invokedFailure(active, code) {
1549
- const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary: 'Publication delivery requires reconciliation.' });
2247
+ async invokedFailure(active, code, summary = 'Publication delivery requires reconciliation.') {
2248
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code, summary });
1550
2249
  return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
1551
2250
  }
1552
2251
  async terminal(active, outcome, evidenceCode = outcome.status, retainIntent = false) {
2252
+ if (active.record.reviewFeedback?.activeBatch && outcome.status !== 'review-ready') {
2253
+ return this.blockReviewFeedback(active, outcome.status === 'blocked' && outcome.kind === 'exhausted' ? 'exhausted' : 'safety', evidenceCode);
2254
+ }
1553
2255
  const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
1554
2256
  const terminalOutcome = { ...outcome, evidencePath: evidence.path };
1555
2257
  const changes = {
@@ -1623,6 +2325,63 @@ class PostEffectStateError extends Error {
1623
2325
  this.active = active;
1624
2326
  }
1625
2327
  }
2328
+ 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;
2329
+ function isClaimMarkerComment(comment) {
2330
+ return claimMarkerPattern.test(comment.body.split('\n')[0] ?? '');
2331
+ }
2332
+ function claimRunId(comment) {
2333
+ return (comment.body.split('\n')[0] ?? '').match(claimMarkerPattern)?.[1];
2334
+ }
2335
+ function trustedHistoricalClaimBodyKey(comment, issueNumber, branchName) {
2336
+ const runId = claimRunId(comment);
2337
+ if (!runId || !['OWNER', 'MEMBER', 'COLLABORATOR'].includes(comment.authorAssociation))
2338
+ return undefined;
2339
+ if (comment.body !== claimComment(runId, issueNumber, branchName))
2340
+ return undefined;
2341
+ return `${comment.authorAssociation}\u0000${comment.body}`;
2342
+ }
2343
+ function frozenHistoricalClaimKey(comment, issueNumber, branchName) {
2344
+ const bodyKey = trustedHistoricalClaimBodyKey(comment, issueNumber, branchName);
2345
+ if (!bodyKey)
2346
+ return undefined;
2347
+ return comment.id ? `id\u0000${comment.id}\u0000${bodyKey}` : `legacy\u0000${bodyKey}`;
2348
+ }
2349
+ function observedHistoricalClaimKeys(comment, record) {
2350
+ const bodyKey = trustedHistoricalClaimBodyKey(comment, record.issueNumber, record.branchName);
2351
+ if (!bodyKey)
2352
+ return [];
2353
+ const keys = comment.id ? [`id\u0000${comment.id}\u0000${bodyKey}`] : [];
2354
+ if (timestampAtOrBefore(comment.createdAt, record.createdAt)
2355
+ && timestampAtOrBefore(comment.updatedAt, record.createdAt)) {
2356
+ keys.push(`legacy\u0000${bodyKey}`);
2357
+ }
2358
+ return keys;
2359
+ }
2360
+ function timestampAtOrBefore(value, cutoff) {
2361
+ if (!value)
2362
+ return false;
2363
+ const timestamp = Date.parse(value);
2364
+ return !Number.isNaN(timestamp) && timestamp <= Date.parse(cutoff);
2365
+ }
2366
+ function historicalClaimCounts(record) {
2367
+ const counts = new Map();
2368
+ for (const comment of record.issueSnapshot.comments ?? []) {
2369
+ const key = frozenHistoricalClaimKey(comment, record.issueNumber, record.branchName);
2370
+ if (!key || claimRunId(comment) === record.runId)
2371
+ continue;
2372
+ counts.set(key, (counts.get(key) ?? 0) + 1);
2373
+ }
2374
+ return counts;
2375
+ }
2376
+ function refreshClaimedIssueSnapshot(baseline, issue) {
2377
+ return {
2378
+ ...structuredClone(baseline),
2379
+ comments: [
2380
+ ...(baseline.comments ?? []).filter(isClaimMarkerComment),
2381
+ ...issue.comments.filter((comment) => !isClaimMarkerComment(comment)),
2382
+ ],
2383
+ };
2384
+ }
1626
2385
  function snapshotIssue(issue) {
1627
2386
  if (issue.state !== 'OPEN')
1628
2387
  throw new Error('cannot snapshot a closed issue');
@@ -1751,12 +2510,30 @@ function sameFreshness(left, right) {
1751
2510
  && left.untrackedContentSha256 === right.untrackedContentSha256
1752
2511
  && left.worktreeIdentity === right.worktreeIdentity;
1753
2512
  }
2513
+ function expectedImplementationHead(record) {
2514
+ return record.reviewFeedback?.activeBatch?.priorPublishedHeadSha ?? record.baseSha;
2515
+ }
1754
2516
  function commentsWithMarker(issue, marker) {
1755
2517
  return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
1756
2518
  }
1757
2519
  function sortedUnique(values) {
1758
2520
  return [...new Set(values)].sort();
1759
2521
  }
2522
+ function worktreeCreationFailureSummary(error) {
2523
+ return boundedLocalGitFailureSummary('Local Git worktree creation failed; correct the local Git state and retry the same run.', error);
2524
+ }
2525
+ function claimedWorktreeInspectionFailureSummary(error) {
2526
+ return boundedLocalGitFailureSummary('The claimed worktree could not be inspected; correct or preserve the local artifact and retry the same run.', error);
2527
+ }
2528
+ function boundedLocalGitFailureSummary(prefix, error) {
2529
+ const raw = error instanceof Error ? error.message : String(error);
2530
+ const diagnostic = raw
2531
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, ' ')
2532
+ .trim();
2533
+ if (diagnostic.length === 0)
2534
+ return prefix;
2535
+ return `${prefix}\n${diagnostic.slice(0, 4 * 1024 - prefix.length - 1)}`;
2536
+ }
1760
2537
  function assertUuid(value) {
1761
2538
  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))
1762
2539
  throw new Error('runId is invalid');