codex-orchestrator 2.0.2 → 2.0.4

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 (221) hide show
  1. package/CHANGELOG.md +44 -426
  2. package/README.md +135 -34
  3. package/dist/src/index.d.ts +11 -1
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +5 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/v2/acceptance-proof.d.ts +3 -0
  8. package/dist/src/v2/acceptance-proof.d.ts.map +1 -1
  9. package/dist/src/v2/acceptance-proof.js +2 -8
  10. package/dist/src/v2/acceptance-proof.js.map +1 -1
  11. package/dist/src/v2/adapters/gh-issue-adapter.d.ts +5 -3
  12. package/dist/src/v2/adapters/gh-issue-adapter.d.ts.map +1 -1
  13. package/dist/src/v2/adapters/gh-issue-adapter.js +67 -12
  14. package/dist/src/v2/adapters/gh-issue-adapter.js.map +1 -1
  15. package/dist/src/v2/adapters/issues.d.ts +16 -2
  16. package/dist/src/v2/adapters/issues.d.ts.map +1 -1
  17. package/dist/src/v2/adapters/issues.js +15 -5
  18. package/dist/src/v2/adapters/issues.js.map +1 -1
  19. package/dist/src/v2/adapters/mission-coordinator-lock.d.ts +1 -0
  20. package/dist/src/v2/adapters/mission-coordinator-lock.d.ts.map +1 -1
  21. package/dist/src/v2/adapters/mission-coordinator-lock.js +5 -1
  22. package/dist/src/v2/adapters/mission-coordinator-lock.js.map +1 -1
  23. package/dist/src/v2/cli-contract.d.ts +3 -3
  24. package/dist/src/v2/cli-contract.d.ts.map +1 -1
  25. package/dist/src/v2/cli-contract.js +9 -1
  26. package/dist/src/v2/cli-contract.js.map +1 -1
  27. package/dist/src/v2/cli.d.ts +24 -0
  28. package/dist/src/v2/cli.d.ts.map +1 -0
  29. package/dist/src/v2/{candidate-cli.js → cli.js} +30 -26
  30. package/dist/src/v2/cli.js.map +1 -0
  31. package/dist/src/v2/code-review-report.d.ts +66 -0
  32. package/dist/src/v2/code-review-report.d.ts.map +1 -0
  33. package/dist/src/v2/code-review-report.js +259 -0
  34. package/dist/src/v2/code-review-report.js.map +1 -0
  35. package/dist/src/v2/codex-process.d.ts +8 -1
  36. package/dist/src/v2/codex-process.d.ts.map +1 -1
  37. package/dist/src/v2/codex-process.js +22 -0
  38. package/dist/src/v2/codex-process.js.map +1 -1
  39. package/dist/src/v2/config.d.ts +4 -3
  40. package/dist/src/v2/config.d.ts.map +1 -1
  41. package/dist/src/v2/config.js +8 -3
  42. package/dist/src/v2/config.js.map +1 -1
  43. package/dist/src/v2/contained-report-operation.d.ts +100 -0
  44. package/dist/src/v2/contained-report-operation.d.ts.map +1 -0
  45. package/dist/src/v2/contained-report-operation.js +200 -0
  46. package/dist/src/v2/contained-report-operation.js.map +1 -0
  47. package/dist/src/v2/containment.d.ts +10 -0
  48. package/dist/src/v2/containment.d.ts.map +1 -1
  49. package/dist/src/v2/containment.js +49 -1
  50. package/dist/src/v2/containment.js.map +1 -1
  51. package/dist/src/v2/direct-delivery.d.ts +96 -0
  52. package/dist/src/v2/direct-delivery.d.ts.map +1 -0
  53. package/dist/src/v2/direct-delivery.js +482 -0
  54. package/dist/src/v2/direct-delivery.js.map +1 -0
  55. package/dist/src/v2/immutable-workflow-publisher.d.ts +40 -0
  56. package/dist/src/v2/immutable-workflow-publisher.d.ts.map +1 -0
  57. package/dist/src/v2/immutable-workflow-publisher.js +218 -0
  58. package/dist/src/v2/immutable-workflow-publisher.js.map +1 -0
  59. package/dist/src/v2/implementation-reviewer.d.ts +81 -0
  60. package/dist/src/v2/implementation-reviewer.d.ts.map +1 -0
  61. package/dist/src/v2/implementation-reviewer.js +157 -0
  62. package/dist/src/v2/implementation-reviewer.js.map +1 -0
  63. package/dist/src/v2/owner-control-lock.d.ts +41 -0
  64. package/dist/src/v2/owner-control-lock.d.ts.map +1 -0
  65. package/dist/src/v2/owner-control-lock.js +174 -0
  66. package/dist/src/v2/owner-control-lock.js.map +1 -0
  67. package/dist/src/v2/proof-report.d.ts.map +1 -1
  68. package/dist/src/v2/proof-report.js +55 -29
  69. package/dist/src/v2/proof-report.js.map +1 -1
  70. package/dist/src/v2/route-continuations.d.ts +32 -0
  71. package/dist/src/v2/route-continuations.d.ts.map +1 -0
  72. package/dist/src/v2/route-continuations.js +2 -0
  73. package/dist/src/v2/route-continuations.js.map +1 -0
  74. package/dist/src/v2/route-coordinator.d.ts +77 -0
  75. package/dist/src/v2/route-coordinator.d.ts.map +1 -0
  76. package/dist/src/v2/route-coordinator.js +370 -0
  77. package/dist/src/v2/route-coordinator.js.map +1 -0
  78. package/dist/src/v2/route-decision.d.ts +129 -0
  79. package/dist/src/v2/route-decision.d.ts.map +1 -0
  80. package/dist/src/v2/route-decision.js +400 -0
  81. package/dist/src/v2/route-decision.js.map +1 -0
  82. package/dist/src/v2/run-issue.d.ts +64 -6
  83. package/dist/src/v2/run-issue.d.ts.map +1 -1
  84. package/dist/src/v2/run-issue.js +962 -92
  85. package/dist/src/v2/run-issue.js.map +1 -1
  86. package/dist/src/v2/run-store.d.ts +25 -1
  87. package/dist/src/v2/run-store.d.ts.map +1 -1
  88. package/dist/src/v2/run-store.js +129 -4
  89. package/dist/src/v2/run-store.js.map +1 -1
  90. package/dist/src/v2/runtime-assets.d.ts +15 -13
  91. package/dist/src/v2/runtime-assets.d.ts.map +1 -1
  92. package/dist/src/v2/runtime-assets.js +263 -416
  93. package/dist/src/v2/runtime-assets.js.map +1 -1
  94. package/dist/src/v2/runtime.d.ts +17 -9
  95. package/dist/src/v2/runtime.d.ts.map +1 -1
  96. package/dist/src/v2/runtime.js +564 -64
  97. package/dist/src/v2/runtime.js.map +1 -1
  98. package/dist/src/v2/setup-cli.d.ts.map +1 -1
  99. package/dist/src/v2/setup-cli.js +4 -10
  100. package/dist/src/v2/setup-cli.js.map +1 -1
  101. package/dist/src/v2/setup-runtime.d.ts.map +1 -1
  102. package/dist/src/v2/setup-runtime.js +19 -131
  103. package/dist/src/v2/setup-runtime.js.map +1 -1
  104. package/dist/src/v2/setup-store.d.ts +0 -5
  105. package/dist/src/v2/setup-store.d.ts.map +1 -1
  106. package/dist/src/v2/setup-store.js +3 -106
  107. package/dist/src/v2/setup-store.js.map +1 -1
  108. package/dist/src/v2/setup.d.ts +6 -43
  109. package/dist/src/v2/setup.d.ts.map +1 -1
  110. package/dist/src/v2/setup.js +13 -192
  111. package/dist/src/v2/setup.js.map +1 -1
  112. package/dist/src/v2/spec-coordinator.d.ts +85 -0
  113. package/dist/src/v2/spec-coordinator.d.ts.map +1 -0
  114. package/dist/src/v2/spec-coordinator.js +88 -0
  115. package/dist/src/v2/spec-coordinator.js.map +1 -0
  116. package/dist/src/v2/spec-delivery.d.ts +143 -0
  117. package/dist/src/v2/spec-delivery.d.ts.map +1 -0
  118. package/dist/src/v2/spec-delivery.js +401 -0
  119. package/dist/src/v2/spec-delivery.js.map +1 -0
  120. package/dist/src/v2/triage-route.d.ts +68 -0
  121. package/dist/src/v2/triage-route.d.ts.map +1 -0
  122. package/dist/src/v2/triage-route.js +223 -0
  123. package/dist/src/v2/triage-route.js.map +1 -0
  124. package/dist/src/v2/waiting-human-coordinator.d.ts +49 -0
  125. package/dist/src/v2/waiting-human-coordinator.d.ts.map +1 -0
  126. package/dist/src/v2/waiting-human-coordinator.js +509 -0
  127. package/dist/src/v2/waiting-human-coordinator.js.map +1 -0
  128. package/dist/src/v2/waiting-human.d.ts +143 -0
  129. package/dist/src/v2/waiting-human.d.ts.map +1 -0
  130. package/dist/src/v2/waiting-human.js +408 -0
  131. package/dist/src/v2/waiting-human.js.map +1 -0
  132. package/dist/src/v2/workflow-assets.d.ts +98 -0
  133. package/dist/src/v2/workflow-assets.d.ts.map +1 -0
  134. package/dist/src/v2/workflow-assets.js +646 -0
  135. package/dist/src/v2/workflow-assets.js.map +1 -0
  136. package/docs/deep-dive.md +275 -52
  137. package/internal-workflow/docs/agents/bug-workflow-routing.md +24 -0
  138. package/internal-workflow/docs/agents/bugfix-quality-gate.md +11 -0
  139. package/internal-workflow/docs/agents/coding-skill-routing.md +123 -0
  140. package/internal-workflow/docs/agents/confidence-rubric.md +65 -0
  141. package/internal-workflow/docs/agents/contract-test-ledger.md +60 -0
  142. package/internal-workflow/docs/agents/review-gates.md +42 -0
  143. package/internal-workflow/docs/agents/review-protocol.md +98 -0
  144. package/internal-workflow/docs/agents/tool-usage.md +88 -0
  145. package/internal-workflow/evals/coding-skill-evals.json +66 -0
  146. package/internal-workflow/manifest.json +1 -0
  147. package/internal-workflow/operations/acceptance-proof/SKILL.md +9 -0
  148. package/internal-workflow/operations/ambiguity-review/SKILL.md +5 -0
  149. package/internal-workflow/operations/code-review/SKILL.md +23 -0
  150. package/internal-workflow/operations/implementation/SKILL.md +24 -0
  151. package/internal-workflow/operations/spec-author/SKILL.md +12 -0
  152. package/internal-workflow/operations/spec-review/SKILL.md +12 -0
  153. package/internal-workflow/operations/triage/SKILL.md +12 -0
  154. package/internal-workflow/profiles/analyst_deep.toml +9 -0
  155. package/internal-workflow/profiles/implementer_standard.toml +9 -0
  156. package/internal-workflow/profiles/proof_agent.toml +8 -0
  157. package/internal-workflow/profiles/reviewer_deep.toml +9 -0
  158. package/internal-workflow/profiles/reviewer_standard.toml +9 -0
  159. package/internal-workflow/schemas/ambiguity-review-v1.json +1 -0
  160. package/internal-workflow/schemas/code-review-v1.json +1 -0
  161. package/internal-workflow/schemas/implementation-report-v1.json +1 -0
  162. package/internal-workflow/schemas/proof-report-v1.json +1 -0
  163. package/internal-workflow/schemas/spec-author-v1.json +1 -0
  164. package/internal-workflow/schemas/spec-review-v1.json +30 -0
  165. package/internal-workflow/schemas/triage-route-v1.json +1 -0
  166. package/internal-workflow/skills/acceptance-proof/agents/openai.yaml +6 -0
  167. package/{internal-skills → internal-workflow/skills}/agent-auto/SKILL.md +6 -1
  168. package/internal-workflow/skills/agent-auto/agents/openai.yaml +6 -0
  169. package/internal-workflow/skills/code-debugger/SKILL.md +122 -0
  170. package/internal-workflow/skills/code-debugger/agents/openai.yaml +7 -0
  171. package/internal-workflow/skills/code-review/SKILL.md +279 -0
  172. package/internal-workflow/skills/code-review/agents/openai.yaml +4 -0
  173. package/internal-workflow/skills/code-review/references/bug-classes.md +56 -0
  174. package/internal-workflow/skills/code-review/references/cleanup-lens.md +52 -0
  175. package/internal-workflow/skills/code-review/references/framework-lenses.md +34 -0
  176. package/internal-workflow/skills/code-review/references/targeted-recipes.md +49 -0
  177. package/internal-workflow/skills/diagnosing-bugs/SKILL.md +138 -0
  178. package/internal-workflow/skills/diagnosing-bugs/agents/openai.yaml +6 -0
  179. package/internal-workflow/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  180. package/internal-workflow/skills/implementation-spec-maker/SKILL.md +102 -0
  181. package/internal-workflow/skills/implementation-spec-maker/agents/openai.yaml +6 -0
  182. package/internal-workflow/skills/implementation-spec-maker/references/source-modes.md +31 -0
  183. package/internal-workflow/skills/implementation-spec-maker/references/spec-template.md +146 -0
  184. package/internal-workflow/skills/implementation-spec-review/SKILL.md +115 -0
  185. package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +6 -0
  186. package/internal-workflow/skills/implementation-spec-review/evals/evals.json +24 -0
  187. package/internal-workflow/skills/implementation-spec-review/references/review-loop.md +93 -0
  188. package/internal-workflow/skills/small-task-implementer/SKILL.md +104 -0
  189. package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +6 -0
  190. package/internal-workflow/skills/spec-implementer/SKILL.md +126 -0
  191. package/internal-workflow/skills/spec-implementer/agents/openai.yaml +6 -0
  192. package/internal-workflow/skills/spec-implementer/evals/evals.json +30 -0
  193. package/internal-workflow/skills/spec-implementer/references/review-loop.md +94 -0
  194. package/internal-workflow/skills/tdd/SKILL.md +72 -0
  195. package/internal-workflow/skills/tdd/agents/openai.yaml +6 -0
  196. package/internal-workflow/skills/tdd/interface-design.md +31 -0
  197. package/internal-workflow/skills/tdd/mocking.md +59 -0
  198. package/internal-workflow/skills/tdd/refactoring.md +10 -0
  199. package/internal-workflow/skills/tdd/tests.md +77 -0
  200. package/internal-workflow/skills/triage/AGENT-BRIEF.md +192 -0
  201. package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +101 -0
  202. package/internal-workflow/skills/triage/SKILL.md +134 -0
  203. package/internal-workflow/skills/triage/agents/openai.yaml +6 -0
  204. package/package.json +14 -8
  205. package/dist/src/v2/adapters/target-activity-fence.d.ts +0 -23
  206. package/dist/src/v2/adapters/target-activity-fence.d.ts.map +0 -1
  207. package/dist/src/v2/adapters/target-activity-fence.js +0 -249
  208. package/dist/src/v2/adapters/target-activity-fence.js.map +0 -1
  209. package/dist/src/v2/candidate-cli.d.ts +0 -22
  210. package/dist/src/v2/candidate-cli.d.ts.map +0 -1
  211. package/dist/src/v2/candidate-cli.js.map +0 -1
  212. package/dist/src/v2/legacy-cutover.d.ts +0 -52
  213. package/dist/src/v2/legacy-cutover.d.ts.map +0 -1
  214. package/dist/src/v2/legacy-cutover.js +0 -87
  215. package/dist/src/v2/legacy-cutover.js.map +0 -1
  216. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/SKILL.md +0 -0
  217. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/android.md +0 -0
  218. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/browser.md +0 -0
  219. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/ios.md +0 -0
  220. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/android-lease.mjs +0 -0
  221. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/ios-lease.mjs +0 -0
@@ -3,9 +3,15 @@ import { checkedChangePayloadSha256, } from './checked-change.js';
3
3
  import { parseAgentAutoConfig } from './config.js';
4
4
  import { canonicalJson, sha256 } from './containment.js';
5
5
  import { validateImplementationReport } from './implementation-report.js';
6
+ import { acceptApprovedDirectReview, acceptNeedsWorkDirectReview, beginDirectReviewRepair, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
6
7
  import { ProofQuiescenceError } from './acceptance-proof.js';
8
+ import { RouteInitializationUnrecoverableError, WorkflowGenerationUnrecoverableError, } from './run-store.js';
9
+ import { initialRouteExecution, } from './route-coordinator.js';
10
+ import { downstreamLifecycleForRoute, validateRouteTransition, validateTrustedAnswerResumeTransition, } from './route-decision.js';
7
11
  export class OwnerLockSafetyError extends Error {
8
12
  }
13
+ export class OwnerLockContentionError extends Error {
14
+ }
9
15
  export class RunIssue {
10
16
  dependencies;
11
17
  signal;
@@ -245,7 +251,10 @@ export class RunIssue {
245
251
  const evidence = await this.dependencies.writeEvidence({ runId, code: 'review-ready', summary: pullRequest.url });
246
252
  const outcome = { status: 'review-ready', pullRequestUrl: pullRequest.url, evidencePath: evidence.path };
247
253
  try {
248
- await this.persist(active, { lifecycle: 'review-ready', intent: undefined, outcomeEvidenceId: evidence.id, terminalOutcome: outcome });
254
+ await this.persist(active, {
255
+ lifecycle: 'review-ready', intent: undefined, outcomeEvidenceId: evidence.id, terminalOutcome: outcome,
256
+ ...(active.record.waitingHuman ? { waitingHuman: terminalWaiting(active.record.waitingHuman, { status: 'review-ready' }) } : {}),
257
+ });
249
258
  }
250
259
  catch {
251
260
  throw new PostEffectStateError(active);
@@ -267,6 +276,12 @@ export class RunIssue {
267
276
  owner = await this.dependencies.ownerLock.acquire({ canonicalRepository, targetRoot });
268
277
  }
269
278
  catch (error) {
279
+ if (error instanceof OwnerLockContentionError) {
280
+ const evidence = await this.dependencies.writeEvidence({
281
+ runId: `issue-${input.issueNumber}`, code: 'owner-contention', summary: 'A known live owner is still running.',
282
+ });
283
+ return { status: 'requeued', reason: 'owner-contention', evidencePath: evidence.path };
284
+ }
270
285
  if (error instanceof OwnerLockSafetyError) {
271
286
  const evidence = await this.dependencies.writeEvidence({
272
287
  runId: `issue-${input.issueNumber}`,
@@ -348,20 +363,54 @@ export class RunIssue {
348
363
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
349
364
  }
350
365
  }
351
- active = await this.persist(active, { lifecycle: 'implementing' });
366
+ active = await this.initializeClaimedRun(active, issue);
367
+ issueSnapshot = structuredClone(active.record.issueSnapshot);
352
368
  }
353
369
  else {
354
- if (existing.lifecycle === 'safe-halt')
355
- return await this.publicationDiverged(active, 'safe-halt-requires-process-absence');
356
- if (!['implementing', 'reworking', 'checking', 'proving'].includes(existing.lifecycle)) {
357
- return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
370
+ if (existing.lifecycle === 'safe-halt') {
371
+ const process = existing.process;
372
+ if (!process)
373
+ return await this.publicationDiverged(active, 'safe-halt-process-missing');
374
+ try {
375
+ await this.dependencies.waitForReviewProcessAbsence(process.processGroupId);
376
+ }
377
+ catch {
378
+ return await this.invokedFailure(active, 'safe-halt-process-absence-unconfirmed');
379
+ }
380
+ active = await this.persist(active, { lifecycle: process.resumeLifecycle, process: undefined });
358
381
  }
359
- if (existing.cycle >= config.runner.maxCycles) {
360
- return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
382
+ if (existing.lifecycle === 'waiting-human') {
383
+ const waiting = await this.continueWaitingHuman(active);
384
+ if ('result' in waiting)
385
+ return waiting.result;
386
+ active = waiting.active;
387
+ issueSnapshot = structuredClone(active.record.issueSnapshot);
388
+ frozenCriteria = structuredClone(active.record.frozenCriteria);
389
+ }
390
+ if (existing.lifecycle === 'spec-authoring') {
391
+ if (!await this.authorized(input.issueNumber, runId, branchName, config))
392
+ return await this.revoked(active);
393
+ return await this.continueSpecRequired(active);
394
+ }
395
+ if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
396
+ return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
361
397
  }
362
398
  if (!await this.authorized(input.issueNumber, runId, branchName, config))
363
399
  return await this.revoked(active);
364
- active = await this.startNextCycle(active, [`Recovered interrupted ${existing.lifecycle} phase.`]);
400
+ if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
401
+ if (!active.record.routeExecution || !active.record.routeReceipt)
402
+ throw new RouteInitializationUnrecoverableError();
403
+ const reviewRecovery = active.record.lifecycle === 'implementing' && active.record.directReview?.status === 'active'
404
+ && (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
405
+ const checkRecovery = active.record.lifecycle === 'checking'
406
+ && active.record.directReview?.status === 'clear';
407
+ if (!reviewRecovery && !checkRecovery) {
408
+ if (active.record.cycle >= config.runner.maxCycles) {
409
+ return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
410
+ }
411
+ active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
412
+ }
413
+ }
365
414
  }
366
415
  }
367
416
  else {
@@ -394,7 +443,20 @@ export class RunIssue {
394
443
  catch {
395
444
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
396
445
  }
397
- active = await this.persist(active, { lifecycle: 'implementing' });
446
+ active = await this.initializeClaimedRun(active, issue);
447
+ issueSnapshot = structuredClone(active.record.issueSnapshot);
448
+ }
449
+ if (active.record.lifecycle === 'triaging' || active.record.lifecycle === 'routed') {
450
+ const routed = await this.routeRun(active, issueSnapshot, frozenCriteria, worktreePath, config, input.issueNumber, branchName);
451
+ if ('result' in routed)
452
+ return routed.result;
453
+ active = routed.active;
454
+ }
455
+ if (active.record.lifecycle !== 'implementing' && active.record.lifecycle !== 'checking') {
456
+ return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
457
+ }
458
+ if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
459
+ return await this.terminal(active, { status: 'internal-error', code: 'direct-review-state-missing' });
398
460
  }
399
461
  attemptLoop: while (true) {
400
462
  if (!await this.authorized(input.issueNumber, runId, branchName, config)) {
@@ -402,67 +464,45 @@ export class RunIssue {
402
464
  }
403
465
  if (this.signal.aborted)
404
466
  return await this.terminal(active, { status: 'cancelled' });
405
- const implementationBaseline = await this.dependencies.git.snapshot(worktreePath);
406
- const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
407
- let implementation = await this.runImplementation({
408
- runId,
409
- worktreePath,
410
- issue: issueSnapshot,
411
- frozenCriteria,
412
- cycle: active.record.cycle,
413
- reworkFindings: active.record.reworkFindings,
414
- repairOnly: false,
415
- });
416
- if (implementation.kind === 'safe-halt') {
417
- active = await this.persist(active, { lifecycle: 'safe-halt', process: implementation.process });
418
- while (true) {
419
- try {
420
- await implementation.waitForAbsence();
421
- break;
422
- }
423
- catch {
424
- await new Promise((resolveWait) => setTimeout(resolveWait, 25));
425
- }
426
- }
427
- if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
428
- return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
429
- }
430
- return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
431
- }
432
- if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
433
- return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
434
- }
435
- if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
436
- const afterTransport = await this.dependencies.git.snapshot(worktreePath);
437
- if (!sameFreshness(implementationBaseline, afterTransport)) {
438
- return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'transport-baseline-changed');
439
- }
440
- active = await this.persist(active, { transportRetries: 1 });
441
- continue attemptLoop;
442
- }
443
- if (implementation.kind !== 'completed')
444
- return await this.mapImplementationFailure(active, implementation);
445
- let report;
446
- try {
447
- report = validateImplementationReport(implementation.report);
467
+ let resumeAtChecks = active.record.lifecycle === 'checking'
468
+ && active.record.directReview?.status === 'clear';
469
+ if (active.record.lifecycle === 'implementing'
470
+ && active.record.directReview?.status === 'active'
471
+ && (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure')) {
472
+ const recovered = await this.recoverDirectReviewInvocation(active);
473
+ if ('status' in recovered)
474
+ return recovered;
475
+ active = recovered;
476
+ const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
477
+ if ('status' in reviewed)
478
+ return reviewed;
479
+ active = reviewed;
480
+ if (active.record.lifecycle === 'implementing')
481
+ continue attemptLoop;
482
+ resumeAtChecks = true;
448
483
  }
449
- catch {
450
- if (active.record.reportRepairs >= 1) {
451
- return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
452
- }
453
- const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
454
- active = await this.persist(active, { reportRepairs: 1 });
455
- implementation = await this.runImplementation({
484
+ if (!resumeAtChecks) {
485
+ const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
486
+ let implementation = await this.runImplementation({
456
487
  runId,
457
488
  worktreePath,
458
- issue: issueSnapshot,
489
+ issue: publicIssueSnapshot(issueSnapshot),
459
490
  frozenCriteria,
460
491
  cycle: active.record.cycle,
461
- reworkFindings: ['The previous implementation report did not match the generated schema.'],
462
- repairOnly: true,
492
+ reworkFindings: active.record.reworkFindings,
493
+ repairOnly: false,
494
+ workflowGeneration: active.record.workflowGeneration,
463
495
  });
464
496
  if (implementation.kind === 'safe-halt') {
465
- active = await this.persist(active, { lifecycle: 'safe-halt', process: implementation.process });
497
+ active = await this.persist(active, {
498
+ lifecycle: 'safe-halt',
499
+ process: {
500
+ ...implementation.process,
501
+ purpose: 'implementation',
502
+ resumeLifecycle: 'implementing',
503
+ resumeReviewStage: null,
504
+ },
505
+ });
466
506
  while (true) {
467
507
  try {
468
508
  await implementation.waitForAbsence();
@@ -472,33 +512,175 @@ export class RunIssue {
472
512
  await new Promise((resolveWait) => setTimeout(resolveWait, 25));
473
513
  }
474
514
  }
515
+ if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
516
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
517
+ }
475
518
  return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
476
519
  }
520
+ if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
521
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
522
+ }
523
+ if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
524
+ active = await this.persist(active, { transportRetries: 1 });
525
+ continue attemptLoop;
526
+ }
477
527
  if (implementation.kind !== 'completed')
478
528
  return await this.mapImplementationFailure(active, implementation);
479
- const afterRepair = await this.dependencies.git.snapshot(worktreePath);
480
- if (!sameFreshness(repairBaseline, afterRepair)) {
481
- return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
482
- }
529
+ let report;
483
530
  try {
484
531
  report = validateImplementationReport(implementation.report);
485
532
  }
486
533
  catch {
487
- return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
534
+ if (active.record.reportRepairs >= 1) {
535
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
536
+ }
537
+ const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
538
+ active = await this.persist(active, { reportRepairs: 1 });
539
+ implementation = await this.runImplementation({
540
+ runId,
541
+ worktreePath,
542
+ issue: publicIssueSnapshot(issueSnapshot),
543
+ frozenCriteria,
544
+ cycle: active.record.cycle,
545
+ reworkFindings: ['The previous implementation report did not match the generated schema.'],
546
+ repairOnly: true,
547
+ workflowGeneration: active.record.workflowGeneration,
548
+ });
549
+ if (implementation.kind === 'safe-halt') {
550
+ active = await this.persist(active, {
551
+ lifecycle: 'safe-halt',
552
+ process: {
553
+ ...implementation.process,
554
+ purpose: 'implementation',
555
+ resumeLifecycle: 'implementing',
556
+ resumeReviewStage: null,
557
+ },
558
+ });
559
+ while (true) {
560
+ try {
561
+ await implementation.waitForAbsence();
562
+ break;
563
+ }
564
+ catch {
565
+ await new Promise((resolveWait) => setTimeout(resolveWait, 25));
566
+ }
567
+ }
568
+ return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
569
+ }
570
+ if (implementation.kind !== 'completed')
571
+ return await this.mapImplementationFailure(active, implementation);
572
+ const afterRepair = await this.dependencies.git.snapshot(worktreePath);
573
+ if (!sameFreshness(repairBaseline, afterRepair)) {
574
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
575
+ }
576
+ try {
577
+ report = validateImplementationReport(implementation.report);
578
+ }
579
+ catch {
580
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
581
+ }
582
+ }
583
+ if (report.status === 'external-block') {
584
+ return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
585
+ }
586
+ if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
587
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
588
+ }
589
+ const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
590
+ if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
591
+ if (changedFiles.length === 0 || active.record.reportRepairs >= 1) {
592
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
593
+ }
594
+ const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
595
+ active = await this.persist(active, { reportRepairs: 1 });
596
+ implementation = await this.runImplementation({
597
+ runId,
598
+ worktreePath,
599
+ issue: publicIssueSnapshot(issueSnapshot),
600
+ frozenCriteria,
601
+ cycle: active.record.cycle,
602
+ reworkFindings: [`The report changedFiles must equal the complete current product change set: ${canonicalJson(changedFiles)}.`],
603
+ repairOnly: true,
604
+ workflowGeneration: active.record.workflowGeneration,
605
+ });
606
+ if (implementation.kind === 'safe-halt') {
607
+ active = await this.persist(active, {
608
+ lifecycle: 'safe-halt',
609
+ process: {
610
+ ...implementation.process,
611
+ purpose: 'implementation',
612
+ resumeLifecycle: 'implementing',
613
+ resumeReviewStage: null,
614
+ },
615
+ });
616
+ while (true) {
617
+ try {
618
+ await implementation.waitForAbsence();
619
+ break;
620
+ }
621
+ catch {
622
+ await new Promise((resolveWait) => setTimeout(resolveWait, 25));
623
+ }
624
+ }
625
+ return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
626
+ }
627
+ if (implementation.kind !== 'completed')
628
+ return await this.mapImplementationFailure(active, implementation);
629
+ const afterRepair = await this.dependencies.git.snapshot(worktreePath);
630
+ if (!sameFreshness(repairBaseline, afterRepair)) {
631
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
632
+ }
633
+ try {
634
+ report = validateImplementationReport(implementation.report);
635
+ }
636
+ catch {
637
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
638
+ }
639
+ if (report.status === 'external-block') {
640
+ return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
641
+ }
642
+ if (!sameStrings(changedFiles, report.changedFiles)) {
643
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
644
+ }
645
+ }
646
+ if (active.record.routeReceipt?.route === 'direct') {
647
+ const targetFingerprint = directReviewTargetFingerprint({
648
+ snapshot: await this.dependencies.git.snapshot(worktreePath),
649
+ changedFiles,
650
+ routeDecisionSha256: active.record.routeReceipt.decisionSha256,
651
+ workflowGenerationHash: active.record.workflowGeneration.generationHash,
652
+ cycle: active.record.cycle,
653
+ frozenCriteria,
654
+ });
655
+ if (active.record.directReview?.stage === 'review-repair') {
656
+ active = await this.persist(active, {
657
+ directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
658
+ });
659
+ }
660
+ else {
661
+ const reviewerSessionId = this.dependencies.createReviewSessionId();
662
+ assertNonEmptyString(reviewerSessionId, 'reviewerSessionId');
663
+ active = await this.persist(active, {
664
+ directReview: createInitialDirectReview({
665
+ targetFingerprint,
666
+ codeReviewerSessionId: reviewerSessionId,
667
+ }),
668
+ });
669
+ }
670
+ const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
671
+ if ('status' in reviewed)
672
+ return reviewed;
673
+ active = reviewed;
674
+ if (active.record.lifecycle === 'implementing')
675
+ continue attemptLoop;
676
+ }
677
+ else {
678
+ active = await this.persist(active, { lifecycle: 'checking' });
488
679
  }
489
680
  }
490
- if (report.status === 'external-block') {
491
- return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
492
- }
493
- if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
494
- return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
495
- }
496
- const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
497
- if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
498
- return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
499
- }
500
- active = await this.persist(active, { lifecycle: 'checking' });
501
681
  for (const [id, command] of Object.entries(config.checks)) {
682
+ if (active.record.checks.some((check) => check.id === id && check.status === 'passed'))
683
+ continue;
502
684
  if (this.signal.aborted)
503
685
  return await this.terminal(active, { status: 'cancelled' });
504
686
  let check;
@@ -516,12 +698,38 @@ export class RunIssue {
516
698
  if (active.record.cycle >= config.runner.maxCycles) {
517
699
  return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
518
700
  }
519
- active = await this.startNextCycle(active, [`Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`]);
701
+ const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
702
+ active = await this.startNextCycle(active, [summary], [{
703
+ provenance: 'check', sourceId: `check:${id}:${row.outputSha256}`, summary,
704
+ affectedContracts: ['configured-checks'],
705
+ }]);
520
706
  continue attemptLoop;
521
707
  }
522
708
  }
709
+ let reviewedStageBinding;
523
710
  try {
711
+ const reviewedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
712
+ const reviewedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, reviewedFiles);
713
+ reviewedStageBinding = { files: reviewedFiles, contentSha256: reviewedContentSha256 };
714
+ if (active.record.directReview?.status === 'clear') {
715
+ const currentFingerprint = directReviewTargetFingerprint({
716
+ snapshot: await this.dependencies.git.snapshot(worktreePath),
717
+ changedFiles: await this.dependencies.git.listChangedFiles(worktreePath),
718
+ routeDecisionSha256: active.record.routeReceipt.decisionSha256,
719
+ workflowGenerationHash: active.record.workflowGeneration.generationHash,
720
+ cycle: active.record.cycle,
721
+ frozenCriteria,
722
+ });
723
+ if (currentFingerprint !== active.record.directReview.targetFingerprint) {
724
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'direct-review-target-drift');
725
+ }
726
+ }
524
727
  await this.dependencies.git.stageAll(worktreePath);
728
+ const stagedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
729
+ const stagedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, stagedFiles);
730
+ if (!sameStrings(reviewedFiles, stagedFiles) || reviewedContentSha256 !== stagedContentSha256) {
731
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'post-stage-review-binding-drift');
732
+ }
525
733
  }
526
734
  catch {
527
735
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
@@ -531,6 +739,11 @@ export class RunIssue {
531
739
  }
532
740
  const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
533
741
  const freshness = await this.dependencies.git.snapshot(worktreePath);
742
+ const proofContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, proofChangedFiles);
743
+ if (!reviewedStageBinding || !sameStrings(reviewedStageBinding.files, proofChangedFiles)
744
+ || reviewedStageBinding.contentSha256 !== proofContentSha256) {
745
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
746
+ }
534
747
  const payload = {
535
748
  version: 1,
536
749
  canonicalRepository,
@@ -546,7 +759,7 @@ export class RunIssue {
546
759
  changedFiles: proofChangedFiles,
547
760
  checks: active.record.checks.map((check) => ({ ...check, status: 'passed' })),
548
761
  checkPolicySha256: sha256(canonicalJson(config.checks)),
549
- packageVersion: this.dependencies.packageVersion,
762
+ packageVersion: active.record.packageVersion,
550
763
  proofSchemaVersion: 1,
551
764
  };
552
765
  const checkedChange = this.dependencies.checkedChangeMint.mint(payload);
@@ -556,7 +769,13 @@ export class RunIssue {
556
769
  active = await this.persist(active, { lifecycle: 'proving', checkedChangeSha256, proofId });
557
770
  let proof;
558
771
  try {
559
- proof = await this.dependencies.proof.proveChange({ proofId, issue: issueSnapshot, frozenCriteria, checkedChange });
772
+ proof = await this.dependencies.proof.proveChange({
773
+ proofId,
774
+ issue: publicIssueSnapshot(issueSnapshot),
775
+ frozenCriteria,
776
+ checkedChange,
777
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
778
+ });
560
779
  }
561
780
  catch (error) {
562
781
  if (error instanceof ProofQuiescenceError) {
@@ -573,6 +792,9 @@ export class RunIssue {
573
792
  untrackedContentSha256: freshness.untrackedContentSha256,
574
793
  worktreeIdentity: freshness.worktreeIdentity,
575
794
  },
795
+ purpose: 'proof',
796
+ resumeLifecycle: 'proving',
797
+ resumeReviewStage: null,
576
798
  },
577
799
  });
578
800
  while (true) {
@@ -594,7 +816,12 @@ export class RunIssue {
594
816
  if (active.record.cycle >= config.runner.maxCycles) {
595
817
  return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
596
818
  }
597
- active = await this.startNextCycle(active, proof.findings);
819
+ active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
820
+ provenance: 'proof',
821
+ sourceId: `proof:${proofId}:${sha256(summary)}`,
822
+ summary,
823
+ affectedContracts: ['acceptance-proof'],
824
+ })));
598
825
  continue attemptLoop;
599
826
  }
600
827
  if (proof.status !== 'passed')
@@ -608,12 +835,23 @@ export class RunIssue {
608
835
  if (!active && error instanceof TransportReadError) {
609
836
  return await this.preClaimTransport(input.issueNumber);
610
837
  }
838
+ if (!active && error instanceof WorkflowGenerationUnrecoverableError) {
839
+ return await this.preClaimInternal('workflow-generation-unrecoverable', input.issueNumber);
840
+ }
611
841
  if (active && error instanceof PostEffectStateError) {
612
842
  return await this.invokedFailure(error.active, 'post-effect-state-write-failed');
613
843
  }
614
844
  if (active && error instanceof TransportReadError) {
615
845
  return await this.invokedFailure(active, 'authorization-read-failed');
616
846
  }
847
+ if (active && error instanceof RouteInitializationUnrecoverableError) {
848
+ const evidence = await this.dependencies.writeEvidence({
849
+ runId: active.record.runId,
850
+ code: 'route-initialization-unrecoverable',
851
+ summary: 'The claimed run cannot be safely initialized without product-state ambiguity.',
852
+ });
853
+ return { status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path };
854
+ }
617
855
  if (active) {
618
856
  try {
619
857
  return await this.terminal(active, { status: 'internal-error', code: 'state-write-failed' });
@@ -641,6 +879,320 @@ export class RunIssue {
641
879
  }
642
880
  }
643
881
  }
882
+ async initializeClaimedRun(active, issue) {
883
+ if (active.record.lifecycle !== 'claimed'
884
+ || active.record.process
885
+ || active.record.intent
886
+ || active.record.routeExecution
887
+ || active.record.routeReceipt) {
888
+ throw new RouteInitializationUnrecoverableError();
889
+ }
890
+ const [snapshot, changedFiles] = await Promise.all([
891
+ this.dependencies.git.snapshot(active.record.worktreePath),
892
+ this.dependencies.git.listChangedFiles(active.record.worktreePath),
893
+ ]);
894
+ if (snapshot.headSha !== active.record.baseSha || changedFiles.length !== 0) {
895
+ throw new RouteInitializationUnrecoverableError();
896
+ }
897
+ try {
898
+ await this.dependencies.verifyWorkflowGeneration(active.record.workflowGeneration);
899
+ }
900
+ catch {
901
+ throw new RouteInitializationUnrecoverableError();
902
+ }
903
+ const issueSnapshot = issue?.state === 'OPEN' ? snapshotIssue(issue) : active.record.issueSnapshot;
904
+ return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
905
+ }
906
+ async routeRun(starting, issue, frozenCriteria, worktreePath, config, issueNumber, branchName) {
907
+ let active = starting;
908
+ while (active.record.lifecycle === 'triaging') {
909
+ const state = {
910
+ read: async () => requireRouteExecution(active.record.routeExecution),
911
+ compareAndSwap: async (expected, next) => {
912
+ if (!sameRouteExecution(active.record.routeExecution, expected))
913
+ return false;
914
+ active = await this.persist(active, { routeExecution: structuredClone(next) });
915
+ return true;
916
+ },
917
+ complete: async (expected, next, receipt) => {
918
+ if (!sameRouteExecution(active.record.routeExecution, expected))
919
+ return false;
920
+ active = await this.persist(active, {
921
+ lifecycle: 'routed',
922
+ routeExecution: structuredClone(next),
923
+ routeReceipt: structuredClone(receipt),
924
+ });
925
+ return true;
926
+ },
927
+ cancel: async (expected) => {
928
+ if (!sameRouteExecution(active.record.routeExecution, expected))
929
+ return false;
930
+ const evidence = await this.dependencies.writeEvidence({
931
+ runId: active.record.runId,
932
+ code: 'cancelled',
933
+ summary: 'Routing was cancelled.',
934
+ });
935
+ active = await this.persist(active, {
936
+ lifecycle: 'cancelled',
937
+ routeExecution: undefined,
938
+ routeReceipt: undefined,
939
+ outcomeEvidenceId: evidence.id,
940
+ terminalOutcome: { status: 'cancelled', evidencePath: evidence.path },
941
+ ...(active.record.waitingHuman ? { waitingHuman: terminalWaiting(active.record.waitingHuman, { status: 'cancelled' }) } : {}),
942
+ });
943
+ return true;
944
+ },
945
+ };
946
+ const result = await this.dependencies.routeCoordinator.run({
947
+ state,
948
+ runId: active.record.runId,
949
+ worktreePath,
950
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
951
+ promptFacts: [
952
+ `issue=${canonicalJson(issue)}`,
953
+ `frozenCriteria=${canonicalJson(frozenCriteria)}`,
954
+ `canonicalRepository=${active.record.canonicalRepository}`,
955
+ `baseSha=${active.record.baseSha}`,
956
+ ...(active.record.waitingHuman?.phase === 'resumed' ? [
957
+ `trustedAnswer=${canonicalJson(active.record.waitingHuman.trustedAnswer)}`,
958
+ `priorWaitingRoute=${active.record.waitingHuman.history.at(-1)?.routeReceipt.decisionSha256 ?? ''}`,
959
+ ] : []),
960
+ ],
961
+ signal: this.signal,
962
+ });
963
+ if (result.status === 'repairable' || result.status === 'retryable')
964
+ continue;
965
+ if (result.status === 'safe-halt') {
966
+ active = await this.persist(active, {
967
+ lifecycle: 'safe-halt',
968
+ process: {
969
+ ...result.process,
970
+ purpose: 'route',
971
+ resumeLifecycle: 'triaging',
972
+ resumeReviewStage: null,
973
+ },
974
+ });
975
+ while (true) {
976
+ try {
977
+ await result.waitForAbsence();
978
+ break;
979
+ }
980
+ catch {
981
+ await new Promise((resolveWait) => setTimeout(resolveWait, 25));
982
+ }
983
+ }
984
+ const after = await this.dependencies.git.snapshot(worktreePath);
985
+ if (!sameFreshness(result.process.baseline, after)) {
986
+ return { result: await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'report-operation-worktree-mutated') };
987
+ }
988
+ return { result: await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed') };
989
+ }
990
+ if (result.status === 'cancelled') {
991
+ if (!active.record.terminalOutcome)
992
+ throw new Error('route cancellation was not persisted');
993
+ return { result: publicOutcome(active.record.terminalOutcome) };
994
+ }
995
+ if (result.status === 'blocked') {
996
+ return { result: await this.terminal(active, {
997
+ status: 'blocked',
998
+ kind: result.kind,
999
+ resumable: result.kind !== 'exhausted',
1000
+ }, result.code) };
1001
+ }
1002
+ if (active.record.lifecycle !== 'routed' || !active.record.routeReceipt) {
1003
+ return { result: await this.terminal(active, { status: 'internal-error', code: 'route-completion-not-persisted' }) };
1004
+ }
1005
+ }
1006
+ if (active.record.lifecycle !== 'routed' || !active.record.routeReceipt) {
1007
+ return { result: await this.terminal(active, { status: 'internal-error', code: 'route-state-not-dispatchable' }) };
1008
+ }
1009
+ try {
1010
+ if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
1011
+ return { result: await this.revoked(active) };
1012
+ }
1013
+ }
1014
+ catch (error) {
1015
+ if (error instanceof TransportReadError)
1016
+ return { result: await this.invokedFailure(active, 'authorization-read-failed') };
1017
+ throw error;
1018
+ }
1019
+ const receipt = structuredClone(active.record.routeReceipt);
1020
+ const lifecycle = downstreamLifecycleForRoute(receipt, active.record.workflowGeneration.generationHash);
1021
+ validateRouteTransition({
1022
+ lifecycle: 'routed',
1023
+ routeExecution: active.record.routeExecution,
1024
+ routeReceipt: receipt,
1025
+ generationHash: active.record.workflowGeneration.generationHash,
1026
+ }, {
1027
+ lifecycle,
1028
+ routeExecution: active.record.routeExecution,
1029
+ routeReceipt: receipt,
1030
+ generationHash: active.record.workflowGeneration.generationHash,
1031
+ });
1032
+ if (receipt.route !== 'awaiting-user')
1033
+ active = await this.persist(active, { lifecycle });
1034
+ try {
1035
+ if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
1036
+ return { result: await this.revoked(active) };
1037
+ }
1038
+ }
1039
+ catch (error) {
1040
+ if (error instanceof TransportReadError)
1041
+ return { result: await this.invokedFailure(active, 'authorization-read-failed') };
1042
+ throw error;
1043
+ }
1044
+ const context = {
1045
+ runId: active.record.runId,
1046
+ issue: structuredClone(active.record.issueSnapshot),
1047
+ frozenCriteria: structuredClone(active.record.frozenCriteria),
1048
+ worktreePath,
1049
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
1050
+ receipt,
1051
+ };
1052
+ if (receipt.route === 'awaiting-user') {
1053
+ const waiting = await this.dependencies.routeContinuations.awaitingUser(context, this.waitingState(() => active, (next) => { active = next; }), this.signal);
1054
+ const mapped = await this.mapWaitingResult(active, waiting);
1055
+ if ('result' in mapped)
1056
+ return mapped;
1057
+ return this.routeRun(mapped.active, mapped.active.record.issueSnapshot, mapped.active.record.frozenCriteria, worktreePath, config, issueNumber, branchName);
1058
+ }
1059
+ const continuation = receipt.route === 'direct'
1060
+ ? await this.dependencies.routeContinuations.direct(context)
1061
+ : await this.dependencies.routeContinuations.specRequired(context, this.specState(() => active, (next) => { active = next; }), this.signal);
1062
+ if (continuation.status === 'cancelled')
1063
+ return { result: await this.terminal(active, { status: 'cancelled' }) };
1064
+ if (continuation.status === 'blocked') {
1065
+ return { result: await this.terminal(active, {
1066
+ status: 'blocked', kind: continuation.kind, resumable: continuation.kind !== 'exhausted',
1067
+ }, continuation.code) };
1068
+ }
1069
+ if (continuation.status === 'retryable') {
1070
+ return { result: await this.terminal(active, { status: 'transport-failed', resumable: true }, continuation.code) };
1071
+ }
1072
+ if (receipt.route !== 'direct') {
1073
+ const specContinuation = continuation;
1074
+ if (specContinuation.status !== 'completed')
1075
+ return { result: await this.terminal(active, { status: 'internal-error', code: 'spec-freeze-receipt-missing' }) };
1076
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'spec-frozen', summary: specContinuation.receipt.receiptSha256 });
1077
+ return { result: { status: 'spec-frozen', receipt: specContinuation.receipt, evidencePath: evidence.path } };
1078
+ }
1079
+ return { active };
1080
+ }
1081
+ waitingState(readActive, writeActive) {
1082
+ return {
1083
+ read: async () => structuredClone(readActive().record.waitingHuman),
1084
+ compareAndSwap: async (expected, next) => {
1085
+ const active = readActive();
1086
+ const observed = active.record.waitingHuman;
1087
+ if (observed === undefined || expected === undefined) {
1088
+ if (observed !== expected)
1089
+ return false;
1090
+ }
1091
+ else if (canonicalJson(observed) !== canonicalJson(expected))
1092
+ return false;
1093
+ const saved = await this.persist(active, {
1094
+ ...(active.record.lifecycle === 'routed'
1095
+ && (expected === undefined || (expected.phase === 'resumed' && next.phase !== 'resumed' && next.phase !== 'history-only'))
1096
+ ? { lifecycle: 'waiting-human' }
1097
+ : {}),
1098
+ waitingHuman: structuredClone(next),
1099
+ });
1100
+ writeActive(saved);
1101
+ return true;
1102
+ },
1103
+ };
1104
+ }
1105
+ specState(readActive, writeActive) {
1106
+ return {
1107
+ read: async () => structuredClone(readActive().record.specDelivery),
1108
+ compareAndSwap: async (expected, next) => {
1109
+ const active = readActive();
1110
+ const observed = active.record.specDelivery;
1111
+ if (observed === undefined || expected === undefined) {
1112
+ if (observed !== expected)
1113
+ return false;
1114
+ }
1115
+ else if (canonicalJson(observed) !== canonicalJson(expected))
1116
+ return false;
1117
+ const saved = await this.persist(active, { specDelivery: structuredClone(next) });
1118
+ writeActive(saved);
1119
+ return true;
1120
+ },
1121
+ };
1122
+ }
1123
+ async continueSpecRequired(active) {
1124
+ if (!active.record.routeReceipt || active.record.routeReceipt.route !== 'spec-required') {
1125
+ return await this.terminal(active, { status: 'internal-error', code: 'spec-route-missing' });
1126
+ }
1127
+ let current = active;
1128
+ const context = {
1129
+ runId: current.record.runId, issue: structuredClone(current.record.issueSnapshot),
1130
+ frozenCriteria: structuredClone(current.record.frozenCriteria), worktreePath: current.record.worktreePath,
1131
+ workflowGeneration: structuredClone(current.record.workflowGeneration), receipt: structuredClone(current.record.routeReceipt),
1132
+ };
1133
+ const result = await this.dependencies.routeContinuations.specRequired(context, this.specState(() => current, (next) => { current = next; }), this.signal);
1134
+ if (result.status === 'completed') {
1135
+ const evidence = await this.dependencies.writeEvidence({ runId: current.record.runId, code: 'spec-frozen', summary: result.receipt.receiptSha256 });
1136
+ return { status: 'spec-frozen', receipt: result.receipt, evidencePath: evidence.path };
1137
+ }
1138
+ if (result.status === 'cancelled')
1139
+ return await this.terminal(current, { status: 'cancelled' });
1140
+ if (result.status === 'retryable')
1141
+ return await this.terminal(current, { status: 'transport-failed', resumable: true }, result.code);
1142
+ return await this.terminal(current, { status: 'blocked', kind: result.kind, resumable: result.kind !== 'exhausted' }, result.code);
1143
+ }
1144
+ async continueWaitingHuman(active) {
1145
+ if (!active.record.routeReceipt || !active.record.workflowGeneration) {
1146
+ return { result: await this.terminal(active, { status: 'internal-error', code: 'waiting-route-missing' }) };
1147
+ }
1148
+ const context = {
1149
+ runId: active.record.runId,
1150
+ issue: structuredClone(active.record.issueSnapshot),
1151
+ frozenCriteria: structuredClone(active.record.frozenCriteria),
1152
+ worktreePath: active.record.worktreePath,
1153
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
1154
+ receipt: structuredClone(active.record.routeReceipt),
1155
+ };
1156
+ let current = active;
1157
+ const result = await this.dependencies.routeContinuations.awaitingUser(context, this.waitingState(() => current, (next) => { current = next; }), this.signal);
1158
+ return this.mapWaitingResult(current, result);
1159
+ }
1160
+ async mapWaitingResult(active, result) {
1161
+ if (result.status === 'awaiting-answer') {
1162
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'awaiting-user', summary: result.questionId });
1163
+ return { result: { status: 'awaiting-user', questionId: result.questionId, answerPrefix: result.answerPrefix, evidencePath: evidence.path } };
1164
+ }
1165
+ if (result.status === 'retryable') {
1166
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: result.code, summary: result.owner });
1167
+ return { result: { status: 'transport-failed', resumable: true, evidencePath: evidence.path } };
1168
+ }
1169
+ if (result.status === 'cancelled')
1170
+ return { result: await this.terminal(active, { status: 'cancelled' }) };
1171
+ if (result.status === 'blocked') {
1172
+ return { result: await this.terminal(active, { status: 'blocked', kind: result.kind, resumable: result.resumable }, result.code) };
1173
+ }
1174
+ const waitingHuman = archiveWaiting(active.record, result.answer, {
1175
+ phase: 'resumed', trustedAnswer: structuredClone(result.answer),
1176
+ });
1177
+ const routeExecution = initialRouteExecution();
1178
+ validateTrustedAnswerResumeTransition({
1179
+ lifecycle: active.record.lifecycle,
1180
+ routeExecution: active.record.routeExecution,
1181
+ routeReceipt: active.record.routeReceipt,
1182
+ generationHash: active.record.workflowGeneration.generationHash,
1183
+ }, {
1184
+ lifecycle: 'triaging', routeExecution, routeReceipt: undefined,
1185
+ generationHash: active.record.workflowGeneration.generationHash,
1186
+ }, active.record.waitingHuman);
1187
+ const resumed = await this.persist(active, {
1188
+ lifecycle: 'triaging',
1189
+ issueSnapshot: structuredClone(active.record.issueSnapshot),
1190
+ routeExecution,
1191
+ routeReceipt: undefined,
1192
+ waitingHuman,
1193
+ });
1194
+ return { active: resumed };
1195
+ }
644
1196
  async readStrictConfig(targetRoot) {
645
1197
  const value = await this.dependencies.readConfig(targetRoot);
646
1198
  return { bytes: Buffer.from(value.bytes), config: parseAgentAutoConfig(structuredClone(value.config)) };
@@ -653,7 +1205,8 @@ export class RunIssue {
653
1205
  return 'Issue is not open.';
654
1206
  if (!labels.has(config.github.labels.auto.name))
655
1207
  return 'Issue lacks the auto label.';
656
- if ([config.github.labels.running.name, config.github.labels.blocked.name, config.github.labels.review.name].some((label) => labels.has(label))) {
1208
+ if ([config.github.labels.running.name, config.github.labels.blocked.name, config.github.labels.review.name, config.github.labels.waitingHuman.name]
1209
+ .some((label) => labels.has(label))) {
657
1210
  return 'Issue already has a terminal or running label.';
658
1211
  }
659
1212
  const branchName = `codex/issue-${issue.number}`;
@@ -669,12 +1222,14 @@ export class RunIssue {
669
1222
  async createRun(input) {
670
1223
  const state = await this.dependencies.runRecords.read();
671
1224
  const now = this.timestamp();
1225
+ const workflow = await this.dependencies.createWorkflowGeneration();
672
1226
  const record = {
673
1227
  ...input,
674
1228
  lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
675
1229
  reworkFindings: [],
676
- packageVersion: this.dependencies.packageVersion,
677
- skillHashes: structuredClone(this.dependencies.skillHashes),
1230
+ packageVersion: workflow.receipt.packageVersion,
1231
+ workflowGeneration: structuredClone(workflow.receipt),
1232
+ skillHashes: structuredClone(workflow.skillHashes),
678
1233
  checks: [], createdAt: now, updatedAt: now,
679
1234
  };
680
1235
  const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
@@ -688,7 +1243,7 @@ export class RunIssue {
688
1243
  delete record.intent;
689
1244
  if (Object.hasOwn(changes, 'process') && changes.process === undefined)
690
1245
  delete record.process;
691
- for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId']) {
1246
+ for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
692
1247
  if (Object.hasOwn(changes, key) && changes[key] === undefined)
693
1248
  delete record[key];
694
1249
  }
@@ -739,19 +1294,230 @@ export class RunIssue {
739
1294
  }
740
1295
  async runImplementation(input) {
741
1296
  try {
742
- return await this.dependencies.implementationAgent.run({ ...input, signal: this.signal });
1297
+ return await this.dependencies.implementationAgent.run({
1298
+ ...input,
1299
+ workflowGeneration: structuredClone(input.workflowGeneration),
1300
+ signal: this.signal,
1301
+ });
743
1302
  }
744
1303
  catch {
745
1304
  return { kind: 'internal-error' };
746
1305
  }
747
1306
  }
748
- async startNextCycle(active, findings) {
1307
+ async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
1308
+ let active = starting;
1309
+ let reportRepair;
1310
+ while (true) {
1311
+ const directReview = active.record.directReview;
1312
+ const routeReceipt = active.record.routeReceipt;
1313
+ if (!directReview || !routeReceipt || routeReceipt.route !== 'direct'
1314
+ || (directReview.stage !== 'review-full' && directReview.stage !== 'review-closure')) {
1315
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-state-invalid' });
1316
+ }
1317
+ const reviewerSessionId = directReview.review.reviewerSessionId;
1318
+ if (!reviewerSessionId)
1319
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-session-missing' });
1320
+ const closureRequestSha256 = directReview.stage === 'review-closure'
1321
+ ? directReviewClosureRequestSha256(directReview)
1322
+ : null;
1323
+ const mode = directReview.stage === 'review-closure' ? 'closure' : 'full';
1324
+ const result = await this.dependencies.implementationReviewer.run({
1325
+ runId: active.record.runId,
1326
+ worktreePath: active.record.worktreePath,
1327
+ operation: 'code-review',
1328
+ mode,
1329
+ reviewerSessionId,
1330
+ implementationAttemptId,
1331
+ targetRevision: directReview.targetRevision,
1332
+ targetFingerprint: directReview.targetFingerprint,
1333
+ closureRequestSha256,
1334
+ issue,
1335
+ frozenCriteria,
1336
+ routeReceipt: structuredClone(routeReceipt),
1337
+ defects: structuredClone(directReview.review.defects),
1338
+ affectedDefectIds: [...directReview.review.affectedDefectIds],
1339
+ fixedRepairFindings: directReview.repairFindings.filter((finding) => finding.status === 'fixed')
1340
+ .map((finding) => ({ id: finding.id, affectedContracts: [...finding.affectedContracts] })),
1341
+ mandatoryCoverage: directReview.stage === 'review-closure'
1342
+ ? [...directReview.review.coverage]
1343
+ : ['acceptance-criteria', 'correctness', 'test-quality'],
1344
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
1345
+ repairOnly: reportRepair !== undefined,
1346
+ originalReportSha256: reportRepair?.originalReportSha256 ?? null,
1347
+ validationDiagnostic: reportRepair?.diagnostic ?? null,
1348
+ originalReportBytes: reportRepair?.originalReportBytes ?? null,
1349
+ signal: this.signal,
1350
+ onPrepared: async (invocation) => {
1351
+ const current = active.record.directReview;
1352
+ if (!current)
1353
+ throw new Error('direct review disappeared before prepare');
1354
+ active = await this.persist(active, {
1355
+ directReview: prepareDirectReviewInvocation(current, invocation),
1356
+ });
1357
+ },
1358
+ onLaunched: async (invocation) => {
1359
+ const current = active.record.directReview;
1360
+ if (!current)
1361
+ throw new Error('direct review disappeared before launch');
1362
+ active = await this.persist(active, {
1363
+ directReview: launchDirectReviewInvocation(current, invocation),
1364
+ });
1365
+ },
1366
+ });
1367
+ if (result.kind === 'completed') {
1368
+ const current = active.record.directReview;
1369
+ if (!current)
1370
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
1371
+ 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');
1374
+ }
1375
+ const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
1376
+ const findings = [
1377
+ ...result.report.defects
1378
+ .filter((defect) => defect.status === 'open' || defect.status === 'reopened')
1379
+ .map((defect) => `${defect.id}: ${defect.failure}\nRepair: ${defect.repair}`),
1380
+ ...repaired.repairFindings
1381
+ .filter((finding) => finding.status === 'reopened')
1382
+ .map((finding) => `${finding.id}: ${finding.summary}`),
1383
+ ];
1384
+ return this.persist(active, {
1385
+ lifecycle: 'implementing',
1386
+ cycle: (active.record.cycle + 1),
1387
+ directReview: repaired,
1388
+ reworkFindings: findings,
1389
+ checks: [],
1390
+ checkedChangeSha256: undefined,
1391
+ proofId: undefined,
1392
+ proofReceipt: undefined,
1393
+ });
1394
+ }
1395
+ if (result.report.verdict !== 'approved')
1396
+ return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-rejected');
1397
+ return this.persist(active, {
1398
+ lifecycle: 'checking',
1399
+ directReview: acceptApprovedDirectReview(current, result.report, result.artifactSha256),
1400
+ });
1401
+ }
1402
+ if (result.kind === 'report-invalid') {
1403
+ const current = active.record.directReview;
1404
+ if (!current)
1405
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
1406
+ if (current.review.reportRepairs >= 1 || reportRepair) {
1407
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
1408
+ }
1409
+ const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
1410
+ active = await this.persist(active, {
1411
+ directReview: {
1412
+ ...withoutInvocation,
1413
+ review: { ...withoutInvocation.review, reportRepairs: 1 },
1414
+ },
1415
+ });
1416
+ reportRepair = {
1417
+ originalReportSha256: result.originalReportSha256,
1418
+ originalReportBytes: Buffer.from(result.originalReportBytes),
1419
+ diagnostic: result.diagnostic,
1420
+ };
1421
+ continue;
1422
+ }
1423
+ if (result.kind === 'transport-failed') {
1424
+ const current = active.record.directReview;
1425
+ if (!current || current.review.transportRetries >= 1) {
1426
+ return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
1427
+ }
1428
+ const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
1429
+ active = await this.persist(active, {
1430
+ directReview: {
1431
+ ...withoutInvocation,
1432
+ review: { ...withoutInvocation.review, transportRetries: 1 },
1433
+ },
1434
+ });
1435
+ continue;
1436
+ }
1437
+ if (result.kind === 'safe-halt') {
1438
+ active = await this.persist(active, {
1439
+ lifecycle: 'safe-halt',
1440
+ process: {
1441
+ ...result.process,
1442
+ purpose: 'code-review',
1443
+ resumeLifecycle: 'implementing',
1444
+ resumeReviewStage: directReview.stage,
1445
+ },
1446
+ });
1447
+ try {
1448
+ await result.waitForAbsence();
1449
+ }
1450
+ catch {
1451
+ return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-unconfirmed');
1452
+ }
1453
+ return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-delayed');
1454
+ }
1455
+ if (result.kind === 'cancelled')
1456
+ return this.terminal(active, { status: 'cancelled' });
1457
+ return this.terminal(active, { status: 'internal-error', code: result.code });
1458
+ }
1459
+ }
1460
+ async recoverDirectReviewInvocation(starting) {
1461
+ const directReview = starting.record.directReview;
1462
+ const invocation = directReview?.invocation;
1463
+ if (!directReview || !invocation)
1464
+ return starting;
1465
+ const { invocation: _invocation, ...withoutInvocation } = structuredClone(directReview);
1466
+ if (invocation.status === 'prepared') {
1467
+ return this.persist(starting, { directReview: withoutInvocation });
1468
+ }
1469
+ if (invocation.status === 'launched') {
1470
+ try {
1471
+ await this.dependencies.waitForReviewProcessAbsence(invocation.processGroupId);
1472
+ }
1473
+ catch {
1474
+ return this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-process-absence-unconfirmed');
1475
+ }
1476
+ if (directReview.review.transportRetries >= 1) {
1477
+ return this.terminal(starting, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
1478
+ }
1479
+ return this.persist(starting, {
1480
+ directReview: {
1481
+ ...withoutInvocation,
1482
+ review: { ...withoutInvocation.review, transportRetries: 1 },
1483
+ },
1484
+ });
1485
+ }
1486
+ return this.persist(starting, { directReview: withoutInvocation });
1487
+ }
1488
+ async startNextCycle(active, findings, sources) {
1489
+ if (active.record.directReview?.status === 'clear') {
1490
+ const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
1491
+ const normalizedSources = sources ?? findings.map((summary) => ({
1492
+ provenance: fallbackProvenance,
1493
+ sourceId: `${fallbackProvenance}:recovery:${sha256(summary)}`,
1494
+ summary,
1495
+ affectedContracts: [fallbackProvenance === 'proof' ? 'acceptance-proof' : 'configured-checks'],
1496
+ }));
1497
+ const repairFindings = normalizedSources.map((source) => ({
1498
+ id: source.sourceId,
1499
+ provenance: source.provenance,
1500
+ sourceId: source.sourceId,
1501
+ targetRevision: active.record.directReview.targetRevision,
1502
+ summary: source.summary,
1503
+ affectedContracts: source.affectedContracts,
1504
+ status: 'open',
1505
+ }));
1506
+ return this.persist(active, {
1507
+ lifecycle: 'implementing',
1508
+ cycle: (active.record.cycle + 1),
1509
+ reworkFindings: [...findings],
1510
+ directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
1511
+ checks: [],
1512
+ checkedChangeSha256: undefined,
1513
+ proofId: undefined,
1514
+ proofReceipt: undefined,
1515
+ });
1516
+ }
749
1517
  active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
750
1518
  return this.persist(active, {
751
1519
  lifecycle: 'implementing',
752
1520
  cycle: (active.record.cycle + 1),
753
- packageVersion: this.dependencies.packageVersion,
754
- skillHashes: structuredClone(this.dependencies.skillHashes),
755
1521
  checks: [],
756
1522
  checkedChangeSha256: undefined,
757
1523
  proofId: undefined,
@@ -792,6 +1558,38 @@ export class RunIssue {
792
1558
  outcomeEvidenceId: evidence.id,
793
1559
  process: undefined,
794
1560
  };
1561
+ if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
1562
+ changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
1563
+ ? { status: 'blocked', kind: outcome.kind }
1564
+ : { status: outcome.status });
1565
+ }
1566
+ if (active.record.waitingHuman && (active.record.lifecycle === 'waiting-human' || active.record.waitingHuman.phase === 'resumed')) {
1567
+ if (active.record.waitingHuman.phase === 'resumed') {
1568
+ changes.waitingHuman = {
1569
+ version: 1,
1570
+ clarificationAttempts: active.record.waitingHuman.clarificationAttempts,
1571
+ permissionRetries: active.record.waitingHuman.permissionRetries,
1572
+ effectRetries: structuredClone(active.record.waitingHuman.effectRetries),
1573
+ history: structuredClone(active.record.waitingHuman.history),
1574
+ phase: 'history-only',
1575
+ terminalOutcome: outcome.status === 'blocked'
1576
+ ? { status: 'blocked', kind: outcome.kind }
1577
+ : { status: outcome.status },
1578
+ };
1579
+ }
1580
+ else if (outcome.status === 'blocked' || outcome.status === 'cancelled') {
1581
+ changes.waitingHuman = archiveWaiting(active.record, waitingAnswer(active.record.waitingHuman), {
1582
+ phase: 'history-only',
1583
+ terminalOutcome: outcome.status === 'cancelled'
1584
+ ? { status: 'cancelled' }
1585
+ : { status: 'blocked', kind: outcome.kind },
1586
+ });
1587
+ }
1588
+ }
1589
+ if (active.record.lifecycle === 'triaging' || (active.record.lifecycle === 'safe-halt' && !active.record.routeReceipt)) {
1590
+ changes.routeExecution = undefined;
1591
+ changes.routeReceipt = undefined;
1592
+ }
795
1593
  if (!retainIntent)
796
1594
  changes.intent = undefined;
797
1595
  await this.persist(active, changes);
@@ -835,6 +1633,17 @@ function snapshotIssue(issue) {
835
1633
  url: issue.url,
836
1634
  state: 'OPEN',
837
1635
  labels: sortedUnique(issue.labels),
1636
+ comments: structuredClone(issue.comments),
1637
+ };
1638
+ }
1639
+ function publicIssueSnapshot(issue) {
1640
+ return {
1641
+ number: issue.number,
1642
+ title: issue.title,
1643
+ body: issue.body,
1644
+ url: issue.url,
1645
+ state: issue.state,
1646
+ labels: [...issue.labels],
838
1647
  };
839
1648
  }
840
1649
  function freezeCriteria(issue) {
@@ -864,11 +1673,72 @@ function findRun(state, runId) {
864
1673
  throw new Error('persisted run is missing');
865
1674
  return record;
866
1675
  }
1676
+ function waitingAnswer(waiting) {
1677
+ if ('answerReceipt' in waiting)
1678
+ return structuredClone(waiting.answerReceipt);
1679
+ if (waiting.phase === 'resumed')
1680
+ return structuredClone(waiting.trustedAnswer);
1681
+ return null;
1682
+ }
1683
+ function archiveWaiting(record, answer, terminal) {
1684
+ const waiting = record.waitingHuman;
1685
+ const routeReceipt = record.routeReceipt;
1686
+ if (!waiting || !routeReceipt)
1687
+ throw new Error('waiting archive requires active route evidence');
1688
+ const question = 'question' in waiting ? waiting.question : 'questionReceipt' in waiting ? waiting.questionReceipt.question : undefined;
1689
+ if (!question)
1690
+ throw new Error('waiting archive requires current question');
1691
+ const questionReceipt = 'questionReceipt' in waiting ? waiting.questionReceipt : null;
1692
+ const entry = {
1693
+ routeReceipt: structuredClone(routeReceipt),
1694
+ question: structuredClone(question),
1695
+ questionReceipt: questionReceipt ? structuredClone(questionReceipt) : null,
1696
+ answerReceipt: answer ? structuredClone(answer) : null,
1697
+ conflictHashes: [...question.conflictHashes],
1698
+ };
1699
+ const history = [...waiting.history];
1700
+ if (history.at(-1)?.question.questionSha256 === question.questionSha256) {
1701
+ if (canonicalJson(history.at(-1)) !== canonicalJson(entry))
1702
+ throw new Error('waiting archive evidence mismatch');
1703
+ }
1704
+ else {
1705
+ history.push(entry);
1706
+ }
1707
+ return {
1708
+ version: 1,
1709
+ clarificationAttempts: waiting.clarificationAttempts,
1710
+ permissionRetries: waiting.permissionRetries,
1711
+ effectRetries: structuredClone(waiting.effectRetries),
1712
+ history,
1713
+ ...terminal,
1714
+ };
1715
+ }
1716
+ function terminalWaiting(waiting, terminalOutcome) {
1717
+ if (waiting.phase !== 'resumed' && waiting.phase !== 'history-only')
1718
+ throw new Error('terminal waiting projection requires archived history');
1719
+ return {
1720
+ version: 1,
1721
+ clarificationAttempts: waiting.clarificationAttempts,
1722
+ permissionRetries: waiting.permissionRetries,
1723
+ effectRetries: structuredClone(waiting.effectRetries),
1724
+ history: structuredClone(waiting.history),
1725
+ phase: 'history-only',
1726
+ terminalOutcome,
1727
+ };
1728
+ }
867
1729
  function publicOutcome(outcome) {
868
1730
  if (outcome.status === 'internal-error')
869
1731
  return { status: 'internal-error', evidencePath: outcome.evidencePath };
870
1732
  return structuredClone(outcome);
871
1733
  }
1734
+ function requireRouteExecution(value) {
1735
+ if (!value)
1736
+ throw new Error('route execution is missing');
1737
+ return structuredClone(value);
1738
+ }
1739
+ function sameRouteExecution(left, right) {
1740
+ return left !== undefined && canonicalJson(left) === canonicalJson(right);
1741
+ }
872
1742
  function sameStrings(left, right) {
873
1743
  const a = [...left].sort();
874
1744
  const b = [...right].sort();