codex-orchestrator 2.0.4 → 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 (69) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +29 -3
  3. package/dist/src/v2/acceptance-proof.d.ts +5 -0
  4. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  5. package/dist/src/v2/acceptance-proof.js +10 -2
  6. package/dist/src/v2/acceptance-proof.js.map +1 -1
  7. package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts +7 -1
  8. package/dist/src/v2/adapters/gh-pull-request-adapter.d.ts.map +1 -1
  9. package/dist/src/v2/adapters/gh-pull-request-adapter.js +288 -0
  10. package/dist/src/v2/adapters/gh-pull-request-adapter.js.map +1 -1
  11. package/dist/src/v2/adapters/pull-requests.d.ts +69 -0
  12. package/dist/src/v2/adapters/pull-requests.d.ts.map +1 -1
  13. package/dist/src/v2/adapters/pull-requests.js +48 -0
  14. package/dist/src/v2/adapters/pull-requests.js.map +1 -1
  15. package/dist/src/v2/adapters/worktree.d.ts +1 -0
  16. package/dist/src/v2/adapters/worktree.d.ts.map +1 -1
  17. package/dist/src/v2/adapters/worktree.js +10 -1
  18. package/dist/src/v2/adapters/worktree.js.map +1 -1
  19. package/dist/src/v2/cli.d.ts +9 -0
  20. package/dist/src/v2/cli.d.ts.map +1 -1
  21. package/dist/src/v2/cli.js +38 -11
  22. package/dist/src/v2/cli.js.map +1 -1
  23. package/dist/src/v2/config.d.ts +0 -1
  24. package/dist/src/v2/config.d.ts.map +1 -1
  25. package/dist/src/v2/config.js +0 -3
  26. package/dist/src/v2/config.js.map +1 -1
  27. package/dist/src/v2/containment.d.ts +11 -2
  28. package/dist/src/v2/containment.d.ts.map +1 -1
  29. package/dist/src/v2/containment.js +35 -7
  30. package/dist/src/v2/containment.js.map +1 -1
  31. package/dist/src/v2/direct-delivery.d.ts +1 -1
  32. package/dist/src/v2/direct-delivery.d.ts.map +1 -1
  33. package/dist/src/v2/direct-delivery.js +9 -3
  34. package/dist/src/v2/direct-delivery.js.map +1 -1
  35. package/dist/src/v2/review-feedback-coordinator.d.ts +54 -0
  36. package/dist/src/v2/review-feedback-coordinator.d.ts.map +1 -0
  37. package/dist/src/v2/review-feedback-coordinator.js +245 -0
  38. package/dist/src/v2/review-feedback-coordinator.js.map +1 -0
  39. package/dist/src/v2/review-feedback.d.ts +127 -0
  40. package/dist/src/v2/review-feedback.d.ts.map +1 -0
  41. package/dist/src/v2/review-feedback.js +436 -0
  42. package/dist/src/v2/review-feedback.js.map +1 -0
  43. package/dist/src/v2/run-issue.d.ts +57 -0
  44. package/dist/src/v2/run-issue.d.ts.map +1 -1
  45. package/dist/src/v2/run-issue.js +701 -36
  46. package/dist/src/v2/run-issue.js.map +1 -1
  47. package/dist/src/v2/run-store.d.ts +45 -1
  48. package/dist/src/v2/run-store.d.ts.map +1 -1
  49. package/dist/src/v2/run-store.js +113 -4
  50. package/dist/src/v2/run-store.js.map +1 -1
  51. package/dist/src/v2/runtime.d.ts +37 -0
  52. package/dist/src/v2/runtime.d.ts.map +1 -1
  53. package/dist/src/v2/runtime.js +120 -5
  54. package/dist/src/v2/runtime.js.map +1 -1
  55. package/dist/src/v2/setup.js +1 -1
  56. package/dist/src/v2/setup.js.map +1 -1
  57. package/docs/deep-dive.md +61 -5
  58. package/internal-workflow/docs/agents/contract-test-ledger.md +11 -1
  59. package/internal-workflow/evals/coding-skill-evals.json +18 -0
  60. package/internal-workflow/manifest.json +1 -1
  61. package/internal-workflow/skills/code-debugger/SKILL.md +3 -3
  62. package/internal-workflow/skills/code-review/SKILL.md +18 -6
  63. package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +1 -1
  64. package/internal-workflow/skills/spec-implementer/references/review-loop.md +7 -1
  65. package/internal-workflow/skills/tdd/SKILL.md +5 -4
  66. package/internal-workflow/skills/tdd/evals/evals.json +18 -0
  67. package/internal-workflow/skills/tdd/mocking.md +3 -42
  68. package/internal-workflow/skills/tdd/refactoring.md +6 -8
  69. package/package.json +1 -1
@@ -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 {
@@ -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);
@@ -392,6 +819,37 @@ export class RunIssue {
392
819
  return await this.revoked(active);
393
820
  return await this.continueSpecRequired(active);
394
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
+ }
395
853
  if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
396
854
  return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
397
855
  }
@@ -404,8 +862,12 @@ export class RunIssue {
404
862
  && (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
405
863
  const checkRecovery = active.record.lifecycle === 'checking'
406
864
  && active.record.directReview?.status === 'clear';
407
- if (!reviewRecovery && !checkRecovery) {
408
- if (active.record.cycle >= config.runner.maxCycles) {
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)) {
409
871
  return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
410
872
  }
411
873
  active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
@@ -430,7 +892,7 @@ export class RunIssue {
430
892
  const runningLabels = sortedUnique([config.github.labels.auto.name, config.github.labels.running.name]);
431
893
  active = await this.createRun({
432
894
  runId, issueNumber: input.issueNumber, canonicalRepository, baseSha, branchName, worktreePath,
433
- issueSnapshot, frozenCriteria,
895
+ issueSnapshot, frozenCriteria, config,
434
896
  intent: { kind: 'claim-labels', issueNumber: input.issueNumber, expected: runningLabels },
435
897
  });
436
898
  const claim = await this.reconcileClaim(active, config);
@@ -452,7 +914,7 @@ export class RunIssue {
452
914
  return routed.result;
453
915
  active = routed.active;
454
916
  }
455
- if (active.record.lifecycle !== 'implementing' && active.record.lifecycle !== 'checking') {
917
+ if (!['implementing', 'checking', 'proving'].includes(active.record.lifecycle)) {
456
918
  return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
457
919
  }
458
920
  if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
@@ -464,7 +926,7 @@ export class RunIssue {
464
926
  }
465
927
  if (this.signal.aborted)
466
928
  return await this.terminal(active, { status: 'cancelled' });
467
- let resumeAtChecks = active.record.lifecycle === 'checking'
929
+ let resumeAtChecks = (active.record.lifecycle === 'checking' || active.record.lifecycle === 'proving')
468
930
  && active.record.directReview?.status === 'clear';
469
931
  if (active.record.lifecycle === 'implementing'
470
932
  && active.record.directReview?.status === 'active'
@@ -473,7 +935,7 @@ export class RunIssue {
473
935
  if ('status' in recovered)
474
936
  return recovered;
475
937
  active = recovered;
476
- 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);
477
939
  if ('status' in reviewed)
478
940
  return reviewed;
479
941
  active = reviewed;
@@ -482,8 +944,40 @@ export class RunIssue {
482
944
  resumeAtChecks = true;
483
945
  }
484
946
  if (!resumeAtChecks) {
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
+ } : {};
485
979
  const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
486
- let implementation = await this.runImplementation({
980
+ let implementation = recoveredFeedbackImplementation ?? await this.runImplementation({
487
981
  runId,
488
982
  worktreePath,
489
983
  issue: publicIssueSnapshot(issueSnapshot),
@@ -492,7 +986,9 @@ export class RunIssue {
492
986
  reworkFindings: active.record.reworkFindings,
493
987
  repairOnly: false,
494
988
  workflowGeneration: active.record.workflowGeneration,
989
+ ...feedbackLaunch,
495
990
  });
991
+ recoveredFeedbackImplementation = undefined;
496
992
  if (implementation.kind === 'safe-halt') {
497
993
  active = await this.persist(active, {
498
994
  lifecycle: 'safe-halt',
@@ -521,6 +1017,10 @@ export class RunIssue {
521
1017
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
522
1018
  }
523
1019
  if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
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');
1023
+ }
524
1024
  active = await this.persist(active, { transportRetries: 1 });
525
1025
  continue attemptLoop;
526
1026
  }
@@ -536,6 +1036,9 @@ export class RunIssue {
536
1036
  }
537
1037
  const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
538
1038
  active = await this.persist(active, { reportRepairs: 1 });
1039
+ const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
1040
+ if (repairBlock)
1041
+ return repairBlock;
539
1042
  implementation = await this.runImplementation({
540
1043
  runId,
541
1044
  worktreePath,
@@ -545,6 +1048,7 @@ export class RunIssue {
545
1048
  reworkFindings: ['The previous implementation report did not match the generated schema.'],
546
1049
  repairOnly: true,
547
1050
  workflowGeneration: active.record.workflowGeneration,
1051
+ ...feedbackLaunch,
548
1052
  });
549
1053
  if (implementation.kind === 'safe-halt') {
550
1054
  active = await this.persist(active, {
@@ -583,7 +1087,7 @@ export class RunIssue {
583
1087
  if (report.status === 'external-block') {
584
1088
  return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
585
1089
  }
586
- if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
1090
+ if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
587
1091
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
588
1092
  }
589
1093
  const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
@@ -593,6 +1097,9 @@ export class RunIssue {
593
1097
  }
594
1098
  const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
595
1099
  active = await this.persist(active, { reportRepairs: 1 });
1100
+ const repairBlock = await this.revalidateFeedbackWorker(active, config, input.issueNumber);
1101
+ if (repairBlock)
1102
+ return repairBlock;
596
1103
  implementation = await this.runImplementation({
597
1104
  runId,
598
1105
  worktreePath,
@@ -602,6 +1109,7 @@ export class RunIssue {
602
1109
  reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
603
1110
  repairOnly: true,
604
1111
  workflowGeneration: active.record.workflowGeneration,
1112
+ ...feedbackLaunch,
605
1113
  });
606
1114
  if (implementation.kind === 'safe-halt') {
607
1115
  active = await this.persist(active, {
@@ -655,6 +1163,9 @@ export class RunIssue {
655
1163
  if (active.record.directReview?.stage === 'review-repair') {
656
1164
  active = await this.persist(active, {
657
1165
  directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
1166
+ ...(active.record.reviewFeedback ? {
1167
+ reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
1168
+ } : {}),
658
1169
  });
659
1170
  }
660
1171
  else {
@@ -665,9 +1176,12 @@ export class RunIssue {
665
1176
  targetFingerprint,
666
1177
  codeReviewerSessionId: reviewerSessionId,
667
1178
  }),
1179
+ ...(active.record.reviewFeedback ? {
1180
+ reviewFeedback: { ...structuredClone(active.record.reviewFeedback), implementationInvocation: null },
1181
+ } : {}),
668
1182
  });
669
1183
  }
670
- 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);
671
1185
  if ('status' in reviewed)
672
1186
  return reviewed;
673
1187
  active = reviewed;
@@ -675,7 +1189,12 @@ export class RunIssue {
675
1189
  continue attemptLoop;
676
1190
  }
677
1191
  else {
678
- 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
+ });
679
1198
  }
680
1199
  }
681
1200
  for (const [id, command] of Object.entries(config.checks)) {
@@ -695,8 +1214,10 @@ export class RunIssue {
695
1214
  const row = { id, command, status: check.status, outputSha256: sha256(check.output) };
696
1215
  active = await this.persist(active, { checks: [...active.record.checks, row] });
697
1216
  if (check.status !== 'passed') {
698
- if (active.record.cycle >= config.runner.maxCycles) {
699
- 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 });
700
1221
  }
701
1222
  const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
702
1223
  active = await this.startNextCycle(active, [summary], [{
@@ -734,7 +1255,7 @@ export class RunIssue {
734
1255
  catch {
735
1256
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
736
1257
  }
737
- if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
1258
+ if (await this.dependencies.git.getHead(worktreePath) !== expectedImplementationHead(active.record)) {
738
1259
  return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
739
1260
  }
740
1261
  const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
@@ -750,7 +1271,7 @@ export class RunIssue {
750
1271
  runId,
751
1272
  issueNumber: input.issueNumber,
752
1273
  cycle: active.record.cycle,
753
- baseSha,
1274
+ baseSha: expectedImplementationHead(active.record),
754
1275
  headSha: freshness.headSha,
755
1276
  indexTreeSha: freshness.indexTreeSha,
756
1277
  trackedContentSha256: freshness.trackedContentSha256,
@@ -775,9 +1296,16 @@ export class RunIssue {
775
1296
  frozenCriteria,
776
1297
  checkedChange,
777
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
+ },
778
1304
  });
779
1305
  }
780
1306
  catch (error) {
1307
+ if (error instanceof ProofLaunchAuthorizationError)
1308
+ return error.outcome;
781
1309
  if (error instanceof ProofQuiescenceError) {
782
1310
  active = await this.persist(active, {
783
1311
  lifecycle: 'safe-halt',
@@ -813,8 +1341,10 @@ export class RunIssue {
813
1341
  if (this.signal.aborted)
814
1342
  return await this.terminal(active, { status: 'cancelled' });
815
1343
  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 });
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 });
818
1348
  }
819
1349
  active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
820
1350
  provenance: 'proof',
@@ -826,10 +1356,23 @@ export class RunIssue {
826
1356
  }
827
1357
  if (proof.status !== 'passed')
828
1358
  return await this.mapProofFailure(active, proof);
829
- 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
+ });
830
1371
  break;
831
1372
  }
832
- 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);
833
1376
  }
834
1377
  catch (error) {
835
1378
  if (!active && error instanceof TransportReadError) {
@@ -1223,8 +1766,9 @@ export class RunIssue {
1223
1766
  const state = await this.dependencies.runRecords.read();
1224
1767
  const now = this.timestamp();
1225
1768
  const workflow = await this.dependencies.createWorkflowGeneration();
1769
+ const { config, ...persistedInput } = input;
1226
1770
  const record = {
1227
- ...input,
1771
+ ...persistedInput,
1228
1772
  lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
1229
1773
  reworkFindings: [],
1230
1774
  packageVersion: workflow.receipt.packageVersion,
@@ -1233,9 +1777,9 @@ export class RunIssue {
1233
1777
  checks: [], createdAt: now, updatedAt: now,
1234
1778
  };
1235
1779
  const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
1236
- 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],
1237
1781
  });
1238
- return { state: saved, record: findRun(saved, input.runId) };
1782
+ return { state: saved, record: findRun(saved, input.runId), config };
1239
1783
  }
1240
1784
  async persist(active, changes) {
1241
1785
  const record = { ...active.record, ...changes, updatedAt: this.timestamp() };
@@ -1243,15 +1787,15 @@ export class RunIssue {
1243
1787
  delete record.intent;
1244
1788
  if (Object.hasOwn(changes, 'process') && changes.process === undefined)
1245
1789
  delete record.process;
1246
- for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
1790
+ for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt', 'reviewFeedback']) {
1247
1791
  if (Object.hasOwn(changes, key) && changes[key] === undefined)
1248
1792
  delete record[key];
1249
1793
  }
1250
1794
  const runs = active.state.runs.map((candidate) => candidate.runId === record.runId ? record : candidate);
1251
1795
  const saved = await this.dependencies.runRecords.compareAndSwap(active.state.generation, {
1252
- schema: 'codex-orchestrator.agent-auto-state', version: 1, runs,
1796
+ schema: 'codex-orchestrator.agent-auto-state', version: 2, runs,
1253
1797
  });
1254
- return { state: saved, record: findRun(saved, record.runId) };
1798
+ return { state: saved, record: findRun(saved, record.runId), config: active.config };
1255
1799
  }
1256
1800
  clearIntent(active) {
1257
1801
  return this.persist(active, { intent: undefined });
@@ -1271,6 +1815,14 @@ export class RunIssue {
1271
1815
  const labels = new Set(issue.labels);
1272
1816
  if (!labels.has(config.github.labels.auto.name) || !labels.has(config.github.labels.running.name))
1273
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) {
1274
1826
  const exactBody = claimComment(runId, issueNumber, branchName);
1275
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;
1276
1828
  const markers = issue.comments.filter((comment) => markerPattern.test(comment.body.split('\n')[0] ?? ''));
@@ -1292,6 +1844,30 @@ export class RunIssue {
1292
1844
  throw new TransportReadError();
1293
1845
  }
1294
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
+ }
1295
1871
  async runImplementation(input) {
1296
1872
  try {
1297
1873
  return await this.dependencies.implementationAgent.run({
@@ -1304,7 +1880,7 @@ export class RunIssue {
1304
1880
  return { kind: 'internal-error' };
1305
1881
  }
1306
1882
  }
1307
- async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
1883
+ async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles, config) {
1308
1884
  let active = starting;
1309
1885
  let reportRepair;
1310
1886
  while (true) {
@@ -1321,6 +1897,9 @@ export class RunIssue {
1321
1897
  ? directReviewClosureRequestSha256(directReview)
1322
1898
  : null;
1323
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;
1324
1903
  const result = await this.dependencies.implementationReviewer.run({
1325
1904
  runId: active.record.runId,
1326
1905
  worktreePath: active.record.worktreePath,
@@ -1369,8 +1948,10 @@ export class RunIssue {
1369
1948
  if (!current)
1370
1949
  return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
1371
1950
  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');
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');
1374
1955
  }
1375
1956
  const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
1376
1957
  const findings = [
@@ -1383,7 +1964,12 @@ export class RunIssue {
1383
1964
  ];
1384
1965
  return this.persist(active, {
1385
1966
  lifecycle: 'implementing',
1386
- 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
+ } : {}),
1387
1973
  directReview: repaired,
1388
1974
  reworkFindings: findings,
1389
1975
  checks: [],
@@ -1505,7 +2091,12 @@ export class RunIssue {
1505
2091
  }));
1506
2092
  return this.persist(active, {
1507
2093
  lifecycle: 'implementing',
1508
- 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
+ } : {}),
1509
2100
  reworkFindings: [...findings],
1510
2101
  directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
1511
2102
  checks: [],
@@ -1517,13 +2108,81 @@ export class RunIssue {
1517
2108
  active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
1518
2109
  return this.persist(active, {
1519
2110
  lifecycle: 'implementing',
1520
- 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
+ } : {}),
1521
2117
  checks: [],
1522
2118
  checkedChangeSha256: undefined,
1523
2119
  proofId: undefined,
1524
2120
  proofReceipt: undefined,
1525
2121
  });
1526
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
+ }
1527
2186
  async mapImplementationFailure(active, result) {
1528
2187
  if (result.kind === 'transport-failed')
1529
2188
  return this.terminal(active, { status: 'transport-failed', resumable: result.resumable });
@@ -1550,6 +2209,9 @@ export class RunIssue {
1550
2209
  return { status: 'transport-failed', resumable: true, evidencePath: evidence.path };
1551
2210
  }
1552
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
+ }
1553
2215
  const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: evidenceCode, summary: outcome.status });
1554
2216
  const terminalOutcome = { ...outcome, evidencePath: evidence.path };
1555
2217
  const changes = {
@@ -1751,6 +2413,9 @@ function sameFreshness(left, right) {
1751
2413
  && left.untrackedContentSha256 === right.untrackedContentSha256
1752
2414
  && left.worktreeIdentity === right.worktreeIdentity;
1753
2415
  }
2416
+ function expectedImplementationHead(record) {
2417
+ return record.reviewFeedback?.activeBatch?.priorPublishedHeadSha ?? record.baseSha;
2418
+ }
1754
2419
  function commentsWithMarker(issue, marker) {
1755
2420
  return issue.comments.filter((comment) => comment.body.split('\n')[0] === marker);
1756
2421
  }