codex-orchestrator 2.0.2 → 2.0.3

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 (211) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +12 -9
  3. package/dist/src/index.d.ts +10 -0
  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 +63 -7
  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/candidate-cli.d.ts +4 -0
  24. package/dist/src/v2/candidate-cli.d.ts.map +1 -1
  25. package/dist/src/v2/candidate-cli.js +26 -11
  26. package/dist/src/v2/candidate-cli.js.map +1 -1
  27. package/dist/src/v2/cli-contract.d.ts +1 -1
  28. package/dist/src/v2/cli-contract.d.ts.map +1 -1
  29. package/dist/src/v2/cli-contract.js +10 -0
  30. package/dist/src/v2/cli-contract.js.map +1 -1
  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 +11 -0
  38. package/dist/src/v2/codex-process.js.map +1 -1
  39. package/dist/src/v2/config.d.ts +2 -1
  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 +6 -0
  48. package/dist/src/v2/containment.d.ts.map +1 -1
  49. package/dist/src/v2/containment.js +40 -1
  50. package/dist/src/v2/containment.js.map +1 -1
  51. package/dist/src/v2/direct-delivery.d.ts +101 -0
  52. package/dist/src/v2/direct-delivery.d.ts.map +1 -0
  53. package/dist/src/v2/direct-delivery.js +547 -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/route-continuations.d.ts +32 -0
  68. package/dist/src/v2/route-continuations.d.ts.map +1 -0
  69. package/dist/src/v2/route-continuations.js +2 -0
  70. package/dist/src/v2/route-continuations.js.map +1 -0
  71. package/dist/src/v2/route-coordinator.d.ts +77 -0
  72. package/dist/src/v2/route-coordinator.d.ts.map +1 -0
  73. package/dist/src/v2/route-coordinator.js +370 -0
  74. package/dist/src/v2/route-coordinator.js.map +1 -0
  75. package/dist/src/v2/route-decision.d.ts +129 -0
  76. package/dist/src/v2/route-decision.d.ts.map +1 -0
  77. package/dist/src/v2/route-decision.js +400 -0
  78. package/dist/src/v2/route-decision.js.map +1 -0
  79. package/dist/src/v2/run-issue.d.ts +63 -2
  80. package/dist/src/v2/run-issue.d.ts.map +1 -1
  81. package/dist/src/v2/run-issue.js +906 -91
  82. package/dist/src/v2/run-issue.js.map +1 -1
  83. package/dist/src/v2/run-store.d.ts +25 -1
  84. package/dist/src/v2/run-store.d.ts.map +1 -1
  85. package/dist/src/v2/run-store.js +143 -3
  86. package/dist/src/v2/run-store.js.map +1 -1
  87. package/dist/src/v2/runtime-assets.d.ts +15 -13
  88. package/dist/src/v2/runtime-assets.d.ts.map +1 -1
  89. package/dist/src/v2/runtime-assets.js +263 -416
  90. package/dist/src/v2/runtime-assets.js.map +1 -1
  91. package/dist/src/v2/runtime.d.ts +14 -6
  92. package/dist/src/v2/runtime.d.ts.map +1 -1
  93. package/dist/src/v2/runtime.js +478 -56
  94. package/dist/src/v2/runtime.js.map +1 -1
  95. package/dist/src/v2/setup-cli.d.ts.map +1 -1
  96. package/dist/src/v2/setup-cli.js +1 -0
  97. package/dist/src/v2/setup-cli.js.map +1 -1
  98. package/dist/src/v2/setup-runtime.d.ts.map +1 -1
  99. package/dist/src/v2/setup-runtime.js +20 -72
  100. package/dist/src/v2/setup-runtime.js.map +1 -1
  101. package/dist/src/v2/setup.d.ts +4 -1
  102. package/dist/src/v2/setup.d.ts.map +1 -1
  103. package/dist/src/v2/setup.js +104 -1
  104. package/dist/src/v2/setup.js.map +1 -1
  105. package/dist/src/v2/spec-coordinator.d.ts +85 -0
  106. package/dist/src/v2/spec-coordinator.d.ts.map +1 -0
  107. package/dist/src/v2/spec-coordinator.js +88 -0
  108. package/dist/src/v2/spec-coordinator.js.map +1 -0
  109. package/dist/src/v2/spec-delivery.d.ts +143 -0
  110. package/dist/src/v2/spec-delivery.d.ts.map +1 -0
  111. package/dist/src/v2/spec-delivery.js +401 -0
  112. package/dist/src/v2/spec-delivery.js.map +1 -0
  113. package/dist/src/v2/triage-route.d.ts +68 -0
  114. package/dist/src/v2/triage-route.d.ts.map +1 -0
  115. package/dist/src/v2/triage-route.js +223 -0
  116. package/dist/src/v2/triage-route.js.map +1 -0
  117. package/dist/src/v2/waiting-human-coordinator.d.ts +49 -0
  118. package/dist/src/v2/waiting-human-coordinator.d.ts.map +1 -0
  119. package/dist/src/v2/waiting-human-coordinator.js +509 -0
  120. package/dist/src/v2/waiting-human-coordinator.js.map +1 -0
  121. package/dist/src/v2/waiting-human.d.ts +143 -0
  122. package/dist/src/v2/waiting-human.d.ts.map +1 -0
  123. package/dist/src/v2/waiting-human.js +408 -0
  124. package/dist/src/v2/waiting-human.js.map +1 -0
  125. package/dist/src/v2/workflow-assets.d.ts +90 -0
  126. package/dist/src/v2/workflow-assets.d.ts.map +1 -0
  127. package/dist/src/v2/workflow-assets.js +554 -0
  128. package/dist/src/v2/workflow-assets.js.map +1 -0
  129. package/docs/deep-dive.md +15 -8
  130. package/internal-workflow/docs/agents/artifact-review-loop.md +267 -0
  131. package/internal-workflow/docs/agents/bug-workflow-routing.md +24 -0
  132. package/internal-workflow/docs/agents/coding-skill-routing.md +203 -0
  133. package/internal-workflow/docs/agents/confidence-rubric.md +65 -0
  134. package/internal-workflow/docs/agents/contract-test-ledger.md +60 -0
  135. package/internal-workflow/docs/agents/implementation-review-loop.md +302 -0
  136. package/internal-workflow/docs/agents/review-gates.md +49 -0
  137. package/internal-workflow/docs/agents/review-protocol.md +170 -0
  138. package/internal-workflow/docs/agents/tool-usage.md +88 -0
  139. package/internal-workflow/manifest.json +1 -0
  140. package/internal-workflow/operations/acceptance-proof/SKILL.md +3 -0
  141. package/internal-workflow/operations/ambiguity-review/SKILL.md +3 -0
  142. package/internal-workflow/operations/cleanup-review/SKILL.md +3 -0
  143. package/internal-workflow/operations/code-review/SKILL.md +3 -0
  144. package/internal-workflow/operations/implementation/SKILL.md +3 -0
  145. package/internal-workflow/operations/spec-author/SKILL.md +3 -0
  146. package/internal-workflow/operations/spec-implementation/SKILL.md +3 -0
  147. package/internal-workflow/operations/spec-review/SKILL.md +3 -0
  148. package/internal-workflow/operations/triage/SKILL.md +3 -0
  149. package/internal-workflow/profiles/analyst_deep.toml +9 -0
  150. package/internal-workflow/profiles/implementer_deep.toml +9 -0
  151. package/internal-workflow/profiles/implementer_standard.toml +9 -0
  152. package/internal-workflow/profiles/proof_agent.toml +8 -0
  153. package/internal-workflow/profiles/researcher_standard.toml +9 -0
  154. package/internal-workflow/profiles/reviewer_deep.toml +9 -0
  155. package/internal-workflow/profiles/reviewer_fast.toml +9 -0
  156. package/internal-workflow/profiles/reviewer_standard.toml +9 -0
  157. package/internal-workflow/schemas/ambiguity-review-v1.json +1 -0
  158. package/internal-workflow/schemas/code-review-v1.json +1 -0
  159. package/internal-workflow/schemas/implementation-report-v1.json +1 -0
  160. package/internal-workflow/schemas/proof-report-v1.json +1 -0
  161. package/internal-workflow/schemas/spec-author-v1.json +1 -0
  162. package/internal-workflow/schemas/spec-review-v1.json +30 -0
  163. package/internal-workflow/schemas/triage-route-v1.json +1 -0
  164. package/internal-workflow/skills/acceptance-proof/agents/openai.yaml +6 -0
  165. package/internal-workflow/skills/agent-auto/agents/openai.yaml +6 -0
  166. package/internal-workflow/skills/cleanup-review/SKILL.md +84 -0
  167. package/internal-workflow/skills/cleanup-review/agents/openai.yaml +6 -0
  168. package/internal-workflow/skills/code-review/SKILL.md +257 -0
  169. package/internal-workflow/skills/code-review/agents/openai.yaml +4 -0
  170. package/internal-workflow/skills/code-review/references/bug-classes.md +56 -0
  171. package/internal-workflow/skills/code-review/references/framework-lenses.md +34 -0
  172. package/internal-workflow/skills/code-review/references/targeted-recipes.md +49 -0
  173. package/internal-workflow/skills/codebase-design/DEEPENING.md +35 -0
  174. package/internal-workflow/skills/codebase-design/DESIGN-IT-TWICE.md +50 -0
  175. package/internal-workflow/skills/codebase-design/SKILL.md +82 -0
  176. package/internal-workflow/skills/codebase-design/agents/openai.yaml +6 -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 +93 -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 +211 -0
  185. package/internal-workflow/skills/implementation-spec-review/agents/openai.yaml +6 -0
  186. package/internal-workflow/skills/research/SKILL.md +107 -0
  187. package/internal-workflow/skills/research/agents/openai.yaml +6 -0
  188. package/internal-workflow/skills/small-task-implementer/SKILL.md +97 -0
  189. package/internal-workflow/skills/small-task-implementer/agents/openai.yaml +6 -0
  190. package/internal-workflow/skills/spec-implementer/SKILL.md +197 -0
  191. package/internal-workflow/skills/spec-implementer/agents/openai.yaml +6 -0
  192. package/internal-workflow/skills/tdd/SKILL.md +59 -0
  193. package/internal-workflow/skills/tdd/agents/openai.yaml +6 -0
  194. package/internal-workflow/skills/tdd/interface-design.md +31 -0
  195. package/internal-workflow/skills/tdd/mocking.md +59 -0
  196. package/internal-workflow/skills/tdd/refactoring.md +10 -0
  197. package/internal-workflow/skills/tdd/tests.md +77 -0
  198. package/internal-workflow/skills/triage/AGENT-BRIEF.md +192 -0
  199. package/internal-workflow/skills/triage/OUT-OF-SCOPE.md +101 -0
  200. package/internal-workflow/skills/triage/SKILL.md +134 -0
  201. package/internal-workflow/skills/triage/agents/openai.yaml +6 -0
  202. package/internal-workflow/skills/ui-evidence-proof/SKILL.md +123 -0
  203. package/internal-workflow/skills/ui-evidence-proof/agents/openai.yaml +6 -0
  204. package/package.json +6 -3
  205. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/SKILL.md +0 -0
  206. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/android.md +0 -0
  207. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/browser.md +0 -0
  208. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/references/ios.md +0 -0
  209. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/android-lease.mjs +0 -0
  210. /package/{internal-skills → internal-workflow/skills}/acceptance-proof/tools/ios-lease.mjs +0 -0
  211. /package/{internal-skills → internal-workflow/skills}/agent-auto/SKILL.md +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, createLegacyBypassDirectReview, createInitialDirectReview, directReviewClosureRequestSha256, directReviewTargetFingerprint, launchDirectReviewInvocation, prepareDirectReviewClosure, prepareDirectReviewInvocation, projectTerminalDirectReview, } from './direct-delivery.js';
6
7
  import { ProofQuiescenceError } from './acceptance-proof.js';
8
+ import { RouteMigrationUnrecoverableError, 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,57 @@ 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.migratePreRouteRun(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
+ if (!process.purpose || !process.resumeLifecycle || process.resumeReviewStage === undefined) {
381
+ return await this.terminal(active, { status: 'internal-error', code: 'direct-review-migration-unrecoverable' });
382
+ }
383
+ active = await this.persist(active, { lifecycle: process.resumeLifecycle, process: undefined });
358
384
  }
359
- if (existing.cycle >= config.runner.maxCycles) {
360
- return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
385
+ if (existing.lifecycle === 'waiting-human') {
386
+ const waiting = await this.continueWaitingHuman(active);
387
+ if ('result' in waiting)
388
+ return waiting.result;
389
+ active = waiting.active;
390
+ issueSnapshot = structuredClone(active.record.issueSnapshot);
391
+ frozenCriteria = structuredClone(active.record.frozenCriteria);
392
+ }
393
+ if (existing.lifecycle === 'spec-authoring') {
394
+ if (!await this.authorized(input.issueNumber, runId, branchName, config))
395
+ return await this.revoked(active);
396
+ return await this.continueSpecRequired(active);
397
+ }
398
+ if (!['triaging', 'routed', 'implementing', 'reworking', 'checking', 'proving'].includes(active.record.lifecycle)) {
399
+ return await this.terminal(active, { status: 'internal-error', code: 'resume-phase-not-reconciled' });
361
400
  }
362
401
  if (!await this.authorized(input.issueNumber, runId, branchName, config))
363
402
  return await this.revoked(active);
364
- active = await this.startNextCycle(active, [`Recovered interrupted ${existing.lifecycle} phase.`]);
403
+ if (active.record.lifecycle !== 'triaging' && active.record.lifecycle !== 'routed') {
404
+ if (!active.record.routeExecution || !active.record.routeReceipt)
405
+ throw new RouteMigrationUnrecoverableError();
406
+ const reviewRecovery = active.record.lifecycle === 'implementing' && active.record.directReview?.status === 'active'
407
+ && (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure');
408
+ const checkRecovery = active.record.lifecycle === 'checking'
409
+ && (active.record.directReview?.status === 'clear' || active.record.directReview?.status === 'legacy-bypass');
410
+ if (!reviewRecovery && !checkRecovery) {
411
+ if (active.record.cycle >= config.runner.maxCycles) {
412
+ return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
413
+ }
414
+ active = await this.startNextCycle(active, [`Recovered interrupted ${active.record.lifecycle} phase.`]);
415
+ }
416
+ }
365
417
  }
366
418
  }
367
419
  else {
@@ -394,7 +446,29 @@ export class RunIssue {
394
446
  catch {
395
447
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
396
448
  }
397
- active = await this.persist(active, { lifecycle: 'implementing' });
449
+ active = await this.migratePreRouteRun(active, issue);
450
+ issueSnapshot = structuredClone(active.record.issueSnapshot);
451
+ }
452
+ if (active.record.lifecycle === 'triaging' || active.record.lifecycle === 'routed') {
453
+ const routed = await this.routeRun(active, issueSnapshot, frozenCriteria, worktreePath, config, input.issueNumber, branchName);
454
+ if ('result' in routed)
455
+ return routed.result;
456
+ active = routed.active;
457
+ }
458
+ if (active.record.lifecycle !== 'implementing' && active.record.lifecycle !== 'checking') {
459
+ return await this.terminal(active, { status: 'internal-error', code: 'route-dispatch-not-implementing' });
460
+ }
461
+ if (active.record.lifecycle === 'checking' && !active.record.directReview && active.record.routeReceipt?.route === 'direct') {
462
+ const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
463
+ const targetFingerprint = directReviewTargetFingerprint({
464
+ snapshot: await this.dependencies.git.snapshot(worktreePath),
465
+ changedFiles,
466
+ routeDecisionSha256: active.record.routeReceipt.decisionSha256,
467
+ workflowGenerationHash: active.record.workflowGeneration.generationHash,
468
+ cycle: active.record.cycle,
469
+ frozenCriteria,
470
+ });
471
+ active = await this.persist(active, { directReview: createLegacyBypassDirectReview(targetFingerprint) });
398
472
  }
399
473
  attemptLoop: while (true) {
400
474
  if (!await this.authorized(input.issueNumber, runId, branchName, config)) {
@@ -402,64 +476,35 @@ export class RunIssue {
402
476
  }
403
477
  if (this.signal.aborted)
404
478
  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);
479
+ let resumeAtChecks = active.record.lifecycle === 'checking'
480
+ && (active.record.directReview?.status === 'clear' || active.record.directReview?.status === 'legacy-bypass');
481
+ if (active.record.lifecycle === 'implementing'
482
+ && active.record.directReview?.status === 'active'
483
+ && (active.record.directReview.stage === 'review-full' || active.record.directReview.stage === 'review-closure')) {
484
+ const recovered = await this.recoverDirectReviewInvocation(active);
485
+ if ('status' in recovered)
486
+ return recovered;
487
+ active = recovered;
488
+ const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, `recovered-implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
489
+ if ('status' in reviewed)
490
+ return reviewed;
491
+ active = reviewed;
492
+ if (active.record.lifecycle === 'implementing')
493
+ continue attemptLoop;
494
+ resumeAtChecks = true;
448
495
  }
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({
496
+ if (!resumeAtChecks) {
497
+ const implementationBaseline = await this.dependencies.git.snapshot(worktreePath);
498
+ const deniedPathsBaseline = await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths);
499
+ let implementation = await this.runImplementation({
456
500
  runId,
457
501
  worktreePath,
458
- issue: issueSnapshot,
502
+ issue: publicIssueSnapshot(issueSnapshot),
459
503
  frozenCriteria,
460
504
  cycle: active.record.cycle,
461
- reworkFindings: ['The previous implementation report did not match the generated schema.'],
462
- repairOnly: true,
505
+ reworkFindings: active.record.reworkFindings,
506
+ repairOnly: false,
507
+ workflowGeneration: active.record.workflowGeneration,
463
508
  });
464
509
  if (implementation.kind === 'safe-halt') {
465
510
  active = await this.persist(active, { lifecycle: 'safe-halt', process: implementation.process });
@@ -472,33 +517,121 @@ export class RunIssue {
472
517
  await new Promise((resolveWait) => setTimeout(resolveWait, 25));
473
518
  }
474
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
+ }
475
523
  return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
476
524
  }
525
+ if (await this.dependencies.git.fingerprintDeniedPaths(worktreePath, config.deny.readPaths) !== deniedPathsBaseline) {
526
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'denied-path-modified');
527
+ }
528
+ if (implementation.kind === 'transport-failed' && implementation.resumable && active.record.transportRetries === 0) {
529
+ const afterTransport = await this.dependencies.git.snapshot(worktreePath);
530
+ if (!sameFreshness(implementationBaseline, afterTransport)) {
531
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'transport-baseline-changed');
532
+ }
533
+ active = await this.persist(active, { transportRetries: 1 });
534
+ continue attemptLoop;
535
+ }
477
536
  if (implementation.kind !== 'completed')
478
537
  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
- }
538
+ let report;
483
539
  try {
484
540
  report = validateImplementationReport(implementation.report);
485
541
  }
486
542
  catch {
487
- return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
543
+ if (active.record.reportRepairs >= 1) {
544
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
545
+ }
546
+ const repairBaseline = await this.dependencies.git.snapshot(worktreePath);
547
+ active = await this.persist(active, { reportRepairs: 1 });
548
+ implementation = await this.runImplementation({
549
+ runId,
550
+ worktreePath,
551
+ issue: publicIssueSnapshot(issueSnapshot),
552
+ frozenCriteria,
553
+ cycle: active.record.cycle,
554
+ reworkFindings: ['The previous implementation report did not match the generated schema.'],
555
+ repairOnly: true,
556
+ workflowGeneration: active.record.workflowGeneration,
557
+ });
558
+ if (implementation.kind === 'safe-halt') {
559
+ active = await this.persist(active, { lifecycle: 'safe-halt', process: implementation.process });
560
+ while (true) {
561
+ try {
562
+ await implementation.waitForAbsence();
563
+ break;
564
+ }
565
+ catch {
566
+ await new Promise((resolveWait) => setTimeout(resolveWait, 25));
567
+ }
568
+ }
569
+ return await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed');
570
+ }
571
+ if (implementation.kind !== 'completed')
572
+ return await this.mapImplementationFailure(active, implementation);
573
+ const afterRepair = await this.dependencies.git.snapshot(worktreePath);
574
+ if (!sameFreshness(repairBaseline, afterRepair)) {
575
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'report-repair-modified-worktree');
576
+ }
577
+ try {
578
+ report = validateImplementationReport(implementation.report);
579
+ }
580
+ catch {
581
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-report-malformed' });
582
+ }
583
+ }
584
+ if (report.status === 'external-block') {
585
+ return await this.terminal(active, { status: 'blocked', kind: 'external', resumable: true });
586
+ }
587
+ if (await this.dependencies.git.getHead(worktreePath) !== baseSha) {
588
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true });
589
+ }
590
+ const changedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
591
+ if (changedFiles.length === 0 || !sameStrings(changedFiles, report.changedFiles)) {
592
+ return await this.terminal(active, { status: 'internal-error', code: 'implementation-change-set-invalid' });
593
+ }
594
+ if (active.record.routeReceipt?.route === 'direct') {
595
+ const targetFingerprint = directReviewTargetFingerprint({
596
+ snapshot: await this.dependencies.git.snapshot(worktreePath),
597
+ changedFiles,
598
+ routeDecisionSha256: active.record.routeReceipt.decisionSha256,
599
+ workflowGenerationHash: active.record.workflowGeneration.generationHash,
600
+ cycle: active.record.cycle,
601
+ frozenCriteria,
602
+ });
603
+ if (active.record.directReview?.stage === 'review-repair') {
604
+ active = await this.persist(active, {
605
+ directReview: prepareDirectReviewClosure(active.record.directReview, targetFingerprint).state,
606
+ });
607
+ }
608
+ else {
609
+ const reviewerSessionId = this.dependencies.createReviewSessionId();
610
+ assertNonEmptyString(reviewerSessionId, 'reviewerSessionId');
611
+ active = await this.persist(active, {
612
+ directReview: createInitialDirectReview({
613
+ targetFingerprint,
614
+ cleanupRequired: false,
615
+ cleanupReason: 'bounded final review owns cleanup',
616
+ cleanupReviewerSessionId: null,
617
+ codeReviewerSessionId: reviewerSessionId,
618
+ }),
619
+ });
620
+ }
621
+ const reviewed = await this.runDirectReviewFull(active, publicIssueSnapshot(issueSnapshot), frozenCriteria, implementation.attemptId ?? `implementation-cycle-${active.record.cycle}`, config.runner.maxCycles);
622
+ if ('status' in reviewed)
623
+ return reviewed;
624
+ active = reviewed;
625
+ if (active.record.lifecycle === 'implementing')
626
+ continue attemptLoop;
627
+ }
628
+ else {
629
+ active = await this.persist(active, { lifecycle: 'checking' });
488
630
  }
489
631
  }
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
632
  for (const [id, command] of Object.entries(config.checks)) {
633
+ if (active.record.checks.some((check) => check.id === id && check.status === 'passed'))
634
+ continue;
502
635
  if (this.signal.aborted)
503
636
  return await this.terminal(active, { status: 'cancelled' });
504
637
  let check;
@@ -516,12 +649,38 @@ export class RunIssue {
516
649
  if (active.record.cycle >= config.runner.maxCycles) {
517
650
  return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
518
651
  }
519
- active = await this.startNextCycle(active, [`Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`]);
652
+ const summary = `Check ${id} failed:\n${check.output.toString('utf8').slice(0, 8 * 1024)}`;
653
+ active = await this.startNextCycle(active, [summary], [{
654
+ provenance: 'check', sourceId: `check:${id}:${row.outputSha256}`, summary,
655
+ affectedContracts: ['configured-checks'],
656
+ }]);
520
657
  continue attemptLoop;
521
658
  }
522
659
  }
660
+ let reviewedStageBinding;
523
661
  try {
662
+ const reviewedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
663
+ const reviewedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, reviewedFiles);
664
+ reviewedStageBinding = { files: reviewedFiles, contentSha256: reviewedContentSha256 };
665
+ if (active.record.directReview?.status === 'clear') {
666
+ const currentFingerprint = directReviewTargetFingerprint({
667
+ snapshot: await this.dependencies.git.snapshot(worktreePath),
668
+ changedFiles: await this.dependencies.git.listChangedFiles(worktreePath),
669
+ routeDecisionSha256: active.record.routeReceipt.decisionSha256,
670
+ workflowGenerationHash: active.record.workflowGeneration.generationHash,
671
+ cycle: active.record.cycle,
672
+ frozenCriteria,
673
+ });
674
+ if (currentFingerprint !== active.record.directReview.targetFingerprint) {
675
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'direct-review-target-drift');
676
+ }
677
+ }
524
678
  await this.dependencies.git.stageAll(worktreePath);
679
+ const stagedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
680
+ const stagedContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, stagedFiles);
681
+ if (!sameStrings(reviewedFiles, stagedFiles) || reviewedContentSha256 !== stagedContentSha256) {
682
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'post-stage-review-binding-drift');
683
+ }
525
684
  }
526
685
  catch {
527
686
  return await this.terminal(active, { status: 'internal-error', code: 'local-git-effect-failed' });
@@ -531,6 +690,11 @@ export class RunIssue {
531
690
  }
532
691
  const proofChangedFiles = await this.dependencies.git.listChangedFiles(worktreePath);
533
692
  const freshness = await this.dependencies.git.snapshot(worktreePath);
693
+ const proofContentSha256 = await this.dependencies.git.fingerprintChangedFiles(worktreePath, proofChangedFiles);
694
+ if (!reviewedStageBinding || !sameStrings(reviewedStageBinding.files, proofChangedFiles)
695
+ || reviewedStageBinding.contentSha256 !== proofContentSha256) {
696
+ return await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: true }, 'checked-change-review-binding-drift');
697
+ }
534
698
  const payload = {
535
699
  version: 1,
536
700
  canonicalRepository,
@@ -546,7 +710,7 @@ export class RunIssue {
546
710
  changedFiles: proofChangedFiles,
547
711
  checks: active.record.checks.map((check) => ({ ...check, status: 'passed' })),
548
712
  checkPolicySha256: sha256(canonicalJson(config.checks)),
549
- packageVersion: this.dependencies.packageVersion,
713
+ packageVersion: active.record.packageVersion,
550
714
  proofSchemaVersion: 1,
551
715
  };
552
716
  const checkedChange = this.dependencies.checkedChangeMint.mint(payload);
@@ -556,7 +720,13 @@ export class RunIssue {
556
720
  active = await this.persist(active, { lifecycle: 'proving', checkedChangeSha256, proofId });
557
721
  let proof;
558
722
  try {
559
- proof = await this.dependencies.proof.proveChange({ proofId, issue: issueSnapshot, frozenCriteria, checkedChange });
723
+ proof = await this.dependencies.proof.proveChange({
724
+ proofId,
725
+ issue: publicIssueSnapshot(issueSnapshot),
726
+ frozenCriteria,
727
+ checkedChange,
728
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
729
+ });
560
730
  }
561
731
  catch (error) {
562
732
  if (error instanceof ProofQuiescenceError) {
@@ -573,6 +743,9 @@ export class RunIssue {
573
743
  untrackedContentSha256: freshness.untrackedContentSha256,
574
744
  worktreeIdentity: freshness.worktreeIdentity,
575
745
  },
746
+ purpose: 'proof',
747
+ resumeLifecycle: 'proving',
748
+ resumeReviewStage: null,
576
749
  },
577
750
  });
578
751
  while (true) {
@@ -594,7 +767,12 @@ export class RunIssue {
594
767
  if (active.record.cycle >= config.runner.maxCycles) {
595
768
  return await this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true });
596
769
  }
597
- active = await this.startNextCycle(active, proof.findings);
770
+ active = await this.startNextCycle(active, proof.findings, proof.findings.map((summary) => ({
771
+ provenance: 'proof',
772
+ sourceId: `proof:${proofId}:${sha256(summary)}`,
773
+ summary,
774
+ affectedContracts: ['acceptance-proof'],
775
+ })));
598
776
  continue attemptLoop;
599
777
  }
600
778
  if (proof.status !== 'passed')
@@ -608,12 +786,23 @@ export class RunIssue {
608
786
  if (!active && error instanceof TransportReadError) {
609
787
  return await this.preClaimTransport(input.issueNumber);
610
788
  }
789
+ if (!active && error instanceof WorkflowGenerationUnrecoverableError) {
790
+ return await this.preClaimInternal('workflow-generation-unrecoverable', input.issueNumber);
791
+ }
611
792
  if (active && error instanceof PostEffectStateError) {
612
793
  return await this.invokedFailure(error.active, 'post-effect-state-write-failed');
613
794
  }
614
795
  if (active && error instanceof TransportReadError) {
615
796
  return await this.invokedFailure(active, 'authorization-read-failed');
616
797
  }
798
+ if (active && error instanceof RouteMigrationUnrecoverableError) {
799
+ const evidence = await this.dependencies.writeEvidence({
800
+ runId: active.record.runId,
801
+ code: 'route-migration-unrecoverable',
802
+ summary: 'The pre-route run cannot be safely migrated without product-state ambiguity.',
803
+ });
804
+ return { status: 'blocked', kind: 'safety', resumable: false, evidencePath: evidence.path };
805
+ }
617
806
  if (active) {
618
807
  try {
619
808
  return await this.terminal(active, { status: 'internal-error', code: 'state-write-failed' });
@@ -641,6 +830,312 @@ export class RunIssue {
641
830
  }
642
831
  }
643
832
  }
833
+ async migratePreRouteRun(active, issue) {
834
+ if (active.record.lifecycle !== 'claimed'
835
+ || active.record.process
836
+ || active.record.intent
837
+ || active.record.routeExecution
838
+ || active.record.routeReceipt) {
839
+ throw new RouteMigrationUnrecoverableError();
840
+ }
841
+ const [snapshot, changedFiles] = await Promise.all([
842
+ this.dependencies.git.snapshot(active.record.worktreePath),
843
+ this.dependencies.git.listChangedFiles(active.record.worktreePath),
844
+ ]);
845
+ if (snapshot.headSha !== active.record.baseSha || changedFiles.length !== 0) {
846
+ throw new RouteMigrationUnrecoverableError();
847
+ }
848
+ try {
849
+ await this.dependencies.verifyWorkflowGeneration(active.record.workflowGeneration);
850
+ }
851
+ catch {
852
+ throw new RouteMigrationUnrecoverableError();
853
+ }
854
+ const issueSnapshot = issue?.state === 'OPEN' ? snapshotIssue(issue) : active.record.issueSnapshot;
855
+ return this.persist(active, { lifecycle: 'triaging', routeExecution: initialRouteExecution(), issueSnapshot });
856
+ }
857
+ async routeRun(starting, issue, frozenCriteria, worktreePath, config, issueNumber, branchName) {
858
+ let active = starting;
859
+ while (active.record.lifecycle === 'triaging') {
860
+ const state = {
861
+ read: async () => requireRouteExecution(active.record.routeExecution),
862
+ compareAndSwap: async (expected, next) => {
863
+ if (!sameRouteExecution(active.record.routeExecution, expected))
864
+ return false;
865
+ active = await this.persist(active, { routeExecution: structuredClone(next) });
866
+ return true;
867
+ },
868
+ complete: async (expected, next, receipt) => {
869
+ if (!sameRouteExecution(active.record.routeExecution, expected))
870
+ return false;
871
+ active = await this.persist(active, {
872
+ lifecycle: 'routed',
873
+ routeExecution: structuredClone(next),
874
+ routeReceipt: structuredClone(receipt),
875
+ });
876
+ return true;
877
+ },
878
+ cancel: async (expected) => {
879
+ if (!sameRouteExecution(active.record.routeExecution, expected))
880
+ return false;
881
+ const evidence = await this.dependencies.writeEvidence({
882
+ runId: active.record.runId,
883
+ code: 'cancelled',
884
+ summary: 'Routing was cancelled.',
885
+ });
886
+ active = await this.persist(active, {
887
+ lifecycle: 'cancelled',
888
+ routeExecution: undefined,
889
+ routeReceipt: undefined,
890
+ outcomeEvidenceId: evidence.id,
891
+ terminalOutcome: { status: 'cancelled', evidencePath: evidence.path },
892
+ ...(active.record.waitingHuman ? { waitingHuman: terminalWaiting(active.record.waitingHuman, { status: 'cancelled' }) } : {}),
893
+ });
894
+ return true;
895
+ },
896
+ };
897
+ const result = await this.dependencies.routeCoordinator.run({
898
+ state,
899
+ runId: active.record.runId,
900
+ worktreePath,
901
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
902
+ promptFacts: [
903
+ `issue=${canonicalJson(issue)}`,
904
+ `frozenCriteria=${canonicalJson(frozenCriteria)}`,
905
+ `canonicalRepository=${active.record.canonicalRepository}`,
906
+ `baseSha=${active.record.baseSha}`,
907
+ ...(active.record.waitingHuman?.phase === 'resumed' ? [
908
+ `trustedAnswer=${canonicalJson(active.record.waitingHuman.trustedAnswer)}`,
909
+ `priorWaitingRoute=${active.record.waitingHuman.history.at(-1)?.routeReceipt.decisionSha256 ?? ''}`,
910
+ ] : []),
911
+ ],
912
+ signal: this.signal,
913
+ });
914
+ if (result.status === 'repairable' || result.status === 'retryable')
915
+ continue;
916
+ if (result.status === 'safe-halt') {
917
+ active = await this.persist(active, { lifecycle: 'safe-halt', process: result.process });
918
+ while (true) {
919
+ try {
920
+ await result.waitForAbsence();
921
+ break;
922
+ }
923
+ catch {
924
+ await new Promise((resolveWait) => setTimeout(resolveWait, 25));
925
+ }
926
+ }
927
+ const after = await this.dependencies.git.snapshot(worktreePath);
928
+ if (!sameFreshness(result.process.baseline, after)) {
929
+ return { result: await this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'report-operation-worktree-mutated') };
930
+ }
931
+ return { result: await this.terminal(active, { status: 'transport-failed', resumable: false }, 'process-quiescence-delayed') };
932
+ }
933
+ if (result.status === 'cancelled') {
934
+ if (!active.record.terminalOutcome)
935
+ throw new Error('route cancellation was not persisted');
936
+ return { result: publicOutcome(active.record.terminalOutcome) };
937
+ }
938
+ if (result.status === 'blocked') {
939
+ return { result: await this.terminal(active, {
940
+ status: 'blocked',
941
+ kind: result.kind,
942
+ resumable: result.kind !== 'exhausted',
943
+ }, result.code) };
944
+ }
945
+ if (active.record.lifecycle !== 'routed' || !active.record.routeReceipt) {
946
+ return { result: await this.terminal(active, { status: 'internal-error', code: 'route-completion-not-persisted' }) };
947
+ }
948
+ }
949
+ if (active.record.lifecycle !== 'routed' || !active.record.routeReceipt) {
950
+ return { result: await this.terminal(active, { status: 'internal-error', code: 'route-state-not-dispatchable' }) };
951
+ }
952
+ try {
953
+ if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
954
+ return { result: await this.revoked(active) };
955
+ }
956
+ }
957
+ catch (error) {
958
+ if (error instanceof TransportReadError)
959
+ return { result: await this.invokedFailure(active, 'authorization-read-failed') };
960
+ throw error;
961
+ }
962
+ const receipt = structuredClone(active.record.routeReceipt);
963
+ const lifecycle = downstreamLifecycleForRoute(receipt, active.record.workflowGeneration.generationHash);
964
+ validateRouteTransition({
965
+ lifecycle: 'routed',
966
+ routeExecution: active.record.routeExecution,
967
+ routeReceipt: receipt,
968
+ generationHash: active.record.workflowGeneration.generationHash,
969
+ }, {
970
+ lifecycle,
971
+ routeExecution: active.record.routeExecution,
972
+ routeReceipt: receipt,
973
+ generationHash: active.record.workflowGeneration.generationHash,
974
+ });
975
+ if (receipt.route !== 'awaiting-user')
976
+ active = await this.persist(active, { lifecycle });
977
+ try {
978
+ if (!await this.authorized(issueNumber, active.record.runId, branchName, config)) {
979
+ return { result: await this.revoked(active) };
980
+ }
981
+ }
982
+ catch (error) {
983
+ if (error instanceof TransportReadError)
984
+ return { result: await this.invokedFailure(active, 'authorization-read-failed') };
985
+ throw error;
986
+ }
987
+ const context = {
988
+ runId: active.record.runId,
989
+ issue: structuredClone(active.record.issueSnapshot),
990
+ frozenCriteria: structuredClone(active.record.frozenCriteria),
991
+ worktreePath,
992
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
993
+ receipt,
994
+ };
995
+ if (receipt.route === 'awaiting-user') {
996
+ const waiting = await this.dependencies.routeContinuations.awaitingUser(context, this.waitingState(() => active, (next) => { active = next; }), this.signal);
997
+ const mapped = await this.mapWaitingResult(active, waiting);
998
+ if ('result' in mapped)
999
+ return mapped;
1000
+ return this.routeRun(mapped.active, mapped.active.record.issueSnapshot, mapped.active.record.frozenCriteria, worktreePath, config, issueNumber, branchName);
1001
+ }
1002
+ const continuation = receipt.route === 'direct'
1003
+ ? await this.dependencies.routeContinuations.direct(context)
1004
+ : await this.dependencies.routeContinuations.specRequired(context, this.specState(() => active, (next) => { active = next; }), this.signal);
1005
+ if (continuation.status === 'cancelled')
1006
+ return { result: await this.terminal(active, { status: 'cancelled' }) };
1007
+ if (continuation.status === 'blocked') {
1008
+ return { result: await this.terminal(active, {
1009
+ status: 'blocked', kind: continuation.kind, resumable: continuation.kind !== 'exhausted',
1010
+ }, continuation.code) };
1011
+ }
1012
+ if (continuation.status === 'retryable') {
1013
+ return { result: await this.terminal(active, { status: 'transport-failed', resumable: true }, continuation.code) };
1014
+ }
1015
+ if (receipt.route !== 'direct') {
1016
+ const specContinuation = continuation;
1017
+ if (specContinuation.status !== 'completed')
1018
+ return { result: await this.terminal(active, { status: 'internal-error', code: 'spec-freeze-receipt-missing' }) };
1019
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'spec-frozen', summary: specContinuation.receipt.receiptSha256 });
1020
+ return { result: { status: 'spec-frozen', receipt: specContinuation.receipt, evidencePath: evidence.path } };
1021
+ }
1022
+ return { active };
1023
+ }
1024
+ waitingState(readActive, writeActive) {
1025
+ return {
1026
+ read: async () => structuredClone(readActive().record.waitingHuman),
1027
+ compareAndSwap: async (expected, next) => {
1028
+ const active = readActive();
1029
+ const observed = active.record.waitingHuman;
1030
+ if (observed === undefined || expected === undefined) {
1031
+ if (observed !== expected)
1032
+ return false;
1033
+ }
1034
+ else if (canonicalJson(observed) !== canonicalJson(expected))
1035
+ return false;
1036
+ const saved = await this.persist(active, {
1037
+ ...(active.record.lifecycle === 'routed'
1038
+ && (expected === undefined || (expected.phase === 'resumed' && next.phase !== 'resumed' && next.phase !== 'history-only'))
1039
+ ? { lifecycle: 'waiting-human' }
1040
+ : {}),
1041
+ waitingHuman: structuredClone(next),
1042
+ });
1043
+ writeActive(saved);
1044
+ return true;
1045
+ },
1046
+ };
1047
+ }
1048
+ specState(readActive, writeActive) {
1049
+ return {
1050
+ read: async () => structuredClone(readActive().record.specDelivery),
1051
+ compareAndSwap: async (expected, next) => {
1052
+ const active = readActive();
1053
+ const observed = active.record.specDelivery;
1054
+ if (observed === undefined || expected === undefined) {
1055
+ if (observed !== expected)
1056
+ return false;
1057
+ }
1058
+ else if (canonicalJson(observed) !== canonicalJson(expected))
1059
+ return false;
1060
+ const saved = await this.persist(active, { specDelivery: structuredClone(next) });
1061
+ writeActive(saved);
1062
+ return true;
1063
+ },
1064
+ };
1065
+ }
1066
+ async continueSpecRequired(active) {
1067
+ if (!active.record.routeReceipt || active.record.routeReceipt.route !== 'spec-required') {
1068
+ return await this.terminal(active, { status: 'internal-error', code: 'spec-route-missing' });
1069
+ }
1070
+ let current = active;
1071
+ const context = {
1072
+ runId: current.record.runId, issue: structuredClone(current.record.issueSnapshot),
1073
+ frozenCriteria: structuredClone(current.record.frozenCriteria), worktreePath: current.record.worktreePath,
1074
+ workflowGeneration: structuredClone(current.record.workflowGeneration), receipt: structuredClone(current.record.routeReceipt),
1075
+ };
1076
+ const result = await this.dependencies.routeContinuations.specRequired(context, this.specState(() => current, (next) => { current = next; }), this.signal);
1077
+ if (result.status === 'completed') {
1078
+ const evidence = await this.dependencies.writeEvidence({ runId: current.record.runId, code: 'spec-frozen', summary: result.receipt.receiptSha256 });
1079
+ return { status: 'spec-frozen', receipt: result.receipt, evidencePath: evidence.path };
1080
+ }
1081
+ if (result.status === 'cancelled')
1082
+ return await this.terminal(current, { status: 'cancelled' });
1083
+ if (result.status === 'retryable')
1084
+ return await this.terminal(current, { status: 'transport-failed', resumable: true }, result.code);
1085
+ return await this.terminal(current, { status: 'blocked', kind: result.kind, resumable: result.kind !== 'exhausted' }, result.code);
1086
+ }
1087
+ async continueWaitingHuman(active) {
1088
+ if (!active.record.routeReceipt || !active.record.workflowGeneration) {
1089
+ return { result: await this.terminal(active, { status: 'internal-error', code: 'waiting-route-missing' }) };
1090
+ }
1091
+ const context = {
1092
+ runId: active.record.runId,
1093
+ issue: structuredClone(active.record.issueSnapshot),
1094
+ frozenCriteria: structuredClone(active.record.frozenCriteria),
1095
+ worktreePath: active.record.worktreePath,
1096
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
1097
+ receipt: structuredClone(active.record.routeReceipt),
1098
+ };
1099
+ let current = active;
1100
+ const result = await this.dependencies.routeContinuations.awaitingUser(context, this.waitingState(() => current, (next) => { current = next; }), this.signal);
1101
+ return this.mapWaitingResult(current, result);
1102
+ }
1103
+ async mapWaitingResult(active, result) {
1104
+ if (result.status === 'awaiting-answer') {
1105
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: 'awaiting-user', summary: result.questionId });
1106
+ return { result: { status: 'awaiting-user', questionId: result.questionId, answerPrefix: result.answerPrefix, evidencePath: evidence.path } };
1107
+ }
1108
+ if (result.status === 'retryable') {
1109
+ const evidence = await this.dependencies.writeEvidence({ runId: active.record.runId, code: result.code, summary: result.owner });
1110
+ return { result: { status: 'transport-failed', resumable: true, evidencePath: evidence.path } };
1111
+ }
1112
+ if (result.status === 'cancelled')
1113
+ return { result: await this.terminal(active, { status: 'cancelled' }) };
1114
+ if (result.status === 'blocked') {
1115
+ return { result: await this.terminal(active, { status: 'blocked', kind: result.kind, resumable: result.resumable }, result.code) };
1116
+ }
1117
+ const waitingHuman = archiveWaiting(active.record, result.answer, {
1118
+ phase: 'resumed', trustedAnswer: structuredClone(result.answer),
1119
+ });
1120
+ const routeExecution = initialRouteExecution();
1121
+ validateTrustedAnswerResumeTransition({
1122
+ lifecycle: active.record.lifecycle,
1123
+ routeExecution: active.record.routeExecution,
1124
+ routeReceipt: active.record.routeReceipt,
1125
+ generationHash: active.record.workflowGeneration.generationHash,
1126
+ }, {
1127
+ lifecycle: 'triaging', routeExecution, routeReceipt: undefined,
1128
+ generationHash: active.record.workflowGeneration.generationHash,
1129
+ }, active.record.waitingHuman);
1130
+ const resumed = await this.persist(active, {
1131
+ lifecycle: 'triaging',
1132
+ issueSnapshot: structuredClone(active.record.issueSnapshot),
1133
+ routeExecution,
1134
+ routeReceipt: undefined,
1135
+ waitingHuman,
1136
+ });
1137
+ return { active: resumed };
1138
+ }
644
1139
  async readStrictConfig(targetRoot) {
645
1140
  const value = await this.dependencies.readConfig(targetRoot);
646
1141
  return { bytes: Buffer.from(value.bytes), config: parseAgentAutoConfig(structuredClone(value.config)) };
@@ -653,7 +1148,8 @@ export class RunIssue {
653
1148
  return 'Issue is not open.';
654
1149
  if (!labels.has(config.github.labels.auto.name))
655
1150
  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))) {
1151
+ if ([config.github.labels.running.name, config.github.labels.blocked.name, config.github.labels.review.name, config.github.labels.waitingHuman.name]
1152
+ .some((label) => labels.has(label))) {
657
1153
  return 'Issue already has a terminal or running label.';
658
1154
  }
659
1155
  const branchName = `codex/issue-${issue.number}`;
@@ -669,12 +1165,14 @@ export class RunIssue {
669
1165
  async createRun(input) {
670
1166
  const state = await this.dependencies.runRecords.read();
671
1167
  const now = this.timestamp();
1168
+ const workflow = await this.dependencies.createWorkflowGeneration();
672
1169
  const record = {
673
1170
  ...input,
674
1171
  lifecycle: 'claimed', cycle: 1, reportRepairs: 0, transportRetries: 0,
675
1172
  reworkFindings: [],
676
- packageVersion: this.dependencies.packageVersion,
677
- skillHashes: structuredClone(this.dependencies.skillHashes),
1173
+ packageVersion: workflow.receipt.packageVersion,
1174
+ workflowGeneration: structuredClone(workflow.receipt),
1175
+ skillHashes: structuredClone(workflow.skillHashes),
678
1176
  checks: [], createdAt: now, updatedAt: now,
679
1177
  };
680
1178
  const saved = await this.dependencies.runRecords.compareAndSwap(state.generation, {
@@ -688,7 +1186,7 @@ export class RunIssue {
688
1186
  delete record.intent;
689
1187
  if (Object.hasOwn(changes, 'process') && changes.process === undefined)
690
1188
  delete record.process;
691
- for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId']) {
1189
+ for (const key of ['checkedChangeSha256', 'proofId', 'proofReceipt', 'terminalOutcome', 'outcomeEvidenceId', 'routeExecution', 'routeReceipt']) {
692
1190
  if (Object.hasOwn(changes, key) && changes[key] === undefined)
693
1191
  delete record[key];
694
1192
  }
@@ -739,19 +1237,232 @@ export class RunIssue {
739
1237
  }
740
1238
  async runImplementation(input) {
741
1239
  try {
742
- return await this.dependencies.implementationAgent.run({ ...input, signal: this.signal });
1240
+ return await this.dependencies.implementationAgent.run({
1241
+ ...input,
1242
+ workflowGeneration: structuredClone(input.workflowGeneration),
1243
+ signal: this.signal,
1244
+ });
743
1245
  }
744
1246
  catch {
745
1247
  return { kind: 'internal-error' };
746
1248
  }
747
1249
  }
748
- async startNextCycle(active, findings) {
1250
+ async runDirectReviewFull(starting, issue, frozenCriteria, implementationAttemptId, maxCycles) {
1251
+ let active = starting;
1252
+ let reportRepair;
1253
+ while (true) {
1254
+ const directReview = active.record.directReview;
1255
+ const routeReceipt = active.record.routeReceipt;
1256
+ if (!directReview || !routeReceipt || routeReceipt.route !== 'direct'
1257
+ || (directReview.stage !== 'review-full' && directReview.stage !== 'review-closure')) {
1258
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-state-invalid' });
1259
+ }
1260
+ const reviewerSessionId = directReview.review.reviewerSessionId;
1261
+ if (!reviewerSessionId)
1262
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-session-missing' });
1263
+ const closureRequestSha256 = directReview.stage === 'review-closure'
1264
+ ? directReviewClosureRequestSha256(directReview)
1265
+ : null;
1266
+ const mode = directReview.stage === 'review-closure' ? 'closure' : 'full';
1267
+ const result = await this.dependencies.implementationReviewer.run({
1268
+ runId: active.record.runId,
1269
+ worktreePath: active.record.worktreePath,
1270
+ operation: 'code-review',
1271
+ mode,
1272
+ reviewerSessionId,
1273
+ implementationAttemptId,
1274
+ targetRevision: directReview.targetRevision,
1275
+ targetFingerprint: directReview.targetFingerprint,
1276
+ closureRequestSha256,
1277
+ issue,
1278
+ frozenCriteria,
1279
+ routeReceipt: structuredClone(routeReceipt),
1280
+ defects: structuredClone(directReview.review.defects),
1281
+ affectedDefectIds: [...directReview.review.affectedDefectIds],
1282
+ fixedRepairFindings: directReview.repairFindings.filter((finding) => finding.status === 'fixed')
1283
+ .map((finding) => ({ id: finding.id, affectedContracts: [...finding.affectedContracts] })),
1284
+ mandatoryCoverage: directReview.stage === 'review-closure'
1285
+ ? [...directReview.review.coverage]
1286
+ : ['acceptance-criteria', 'correctness', 'test-quality'],
1287
+ workflowGeneration: structuredClone(active.record.workflowGeneration),
1288
+ repairOnly: reportRepair !== undefined,
1289
+ originalReportSha256: reportRepair?.originalReportSha256 ?? null,
1290
+ validationDiagnostic: reportRepair?.diagnostic ?? null,
1291
+ originalReportBytes: reportRepair?.originalReportBytes ?? null,
1292
+ signal: this.signal,
1293
+ onPrepared: async (invocation) => {
1294
+ const current = active.record.directReview;
1295
+ if (!current)
1296
+ throw new Error('direct review disappeared before prepare');
1297
+ active = await this.persist(active, {
1298
+ directReview: prepareDirectReviewInvocation(current, invocation),
1299
+ });
1300
+ },
1301
+ onLaunched: async (invocation) => {
1302
+ const current = active.record.directReview;
1303
+ if (!current)
1304
+ throw new Error('direct review disappeared before launch');
1305
+ active = await this.persist(active, {
1306
+ directReview: launchDirectReviewInvocation(current, invocation),
1307
+ });
1308
+ },
1309
+ });
1310
+ if (result.kind === 'completed') {
1311
+ const current = active.record.directReview;
1312
+ if (!current)
1313
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
1314
+ if (result.report.verdict === 'needs-work') {
1315
+ if (active.record.cycle >= maxCycles) {
1316
+ return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-repair-exhausted');
1317
+ }
1318
+ const repaired = acceptNeedsWorkDirectReview(current, result.report, result.artifactSha256);
1319
+ const findings = [
1320
+ ...result.report.defects
1321
+ .filter((defect) => defect.status === 'open' || defect.status === 'reopened')
1322
+ .map((defect) => `${defect.id}: ${defect.failure}\nRepair: ${defect.repair}`),
1323
+ ...repaired.repairFindings
1324
+ .filter((finding) => finding.status === 'reopened')
1325
+ .map((finding) => `${finding.id}: ${finding.summary}`),
1326
+ ];
1327
+ return this.persist(active, {
1328
+ lifecycle: 'implementing',
1329
+ cycle: (active.record.cycle + 1),
1330
+ directReview: repaired,
1331
+ reworkFindings: findings,
1332
+ checks: [],
1333
+ checkedChangeSha256: undefined,
1334
+ proofId: undefined,
1335
+ proofReceipt: undefined,
1336
+ });
1337
+ }
1338
+ if (result.report.verdict !== 'approved')
1339
+ return this.terminal(active, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-rejected');
1340
+ return this.persist(active, {
1341
+ lifecycle: 'checking',
1342
+ directReview: acceptApprovedDirectReview(current, result.report, result.artifactSha256),
1343
+ });
1344
+ }
1345
+ if (result.kind === 'report-invalid') {
1346
+ const current = active.record.directReview;
1347
+ if (!current)
1348
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-result-orphaned' });
1349
+ const selected = current.stage?.startsWith('cleanup-') ? current.cleanup : current.review;
1350
+ if (selected.reportRepairs >= 1 || reportRepair) {
1351
+ return this.terminal(active, { status: 'internal-error', code: 'direct-review-report-malformed' });
1352
+ }
1353
+ const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
1354
+ const trackKey = current.stage?.startsWith('cleanup-') ? 'cleanup' : 'review';
1355
+ active = await this.persist(active, {
1356
+ directReview: {
1357
+ ...withoutInvocation,
1358
+ [trackKey]: { ...withoutInvocation[trackKey], reportRepairs: 1 },
1359
+ },
1360
+ });
1361
+ reportRepair = {
1362
+ originalReportSha256: result.originalReportSha256,
1363
+ originalReportBytes: Buffer.from(result.originalReportBytes),
1364
+ diagnostic: result.diagnostic,
1365
+ };
1366
+ continue;
1367
+ }
1368
+ if (result.kind === 'transport-failed') {
1369
+ const current = active.record.directReview;
1370
+ if (!current || current.review.transportRetries >= 1) {
1371
+ return this.terminal(active, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
1372
+ }
1373
+ const { invocation: _invocation, ...withoutInvocation } = structuredClone(current);
1374
+ active = await this.persist(active, {
1375
+ directReview: {
1376
+ ...withoutInvocation,
1377
+ review: { ...withoutInvocation.review, transportRetries: 1 },
1378
+ },
1379
+ });
1380
+ continue;
1381
+ }
1382
+ if (result.kind === 'safe-halt') {
1383
+ active = await this.persist(active, {
1384
+ lifecycle: 'safe-halt',
1385
+ process: {
1386
+ ...result.process,
1387
+ purpose: 'code-review',
1388
+ resumeLifecycle: 'implementing',
1389
+ resumeReviewStage: directReview.stage,
1390
+ },
1391
+ });
1392
+ try {
1393
+ await result.waitForAbsence();
1394
+ }
1395
+ catch {
1396
+ return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-unconfirmed');
1397
+ }
1398
+ return this.terminal(active, { status: 'transport-failed', resumable: false }, 'direct-review-quiescence-delayed');
1399
+ }
1400
+ if (result.kind === 'cancelled')
1401
+ return this.terminal(active, { status: 'cancelled' });
1402
+ return this.terminal(active, { status: 'internal-error', code: result.code });
1403
+ }
1404
+ }
1405
+ async recoverDirectReviewInvocation(starting) {
1406
+ const directReview = starting.record.directReview;
1407
+ const invocation = directReview?.invocation;
1408
+ if (!directReview || !invocation)
1409
+ return starting;
1410
+ const { invocation: _invocation, ...withoutInvocation } = structuredClone(directReview);
1411
+ if (invocation.status === 'prepared') {
1412
+ return this.persist(starting, { directReview: withoutInvocation });
1413
+ }
1414
+ if (invocation.status === 'launched') {
1415
+ try {
1416
+ await this.dependencies.waitForReviewProcessAbsence(invocation.processGroupId);
1417
+ }
1418
+ catch {
1419
+ return this.terminal(starting, { status: 'blocked', kind: 'safety', resumable: false }, 'direct-review-process-absence-unconfirmed');
1420
+ }
1421
+ if (directReview.review.transportRetries >= 1) {
1422
+ return this.terminal(starting, { status: 'blocked', kind: 'exhausted', resumable: true }, 'direct-review-transport-exhausted');
1423
+ }
1424
+ return this.persist(starting, {
1425
+ directReview: {
1426
+ ...withoutInvocation,
1427
+ review: { ...withoutInvocation.review, transportRetries: 1 },
1428
+ },
1429
+ });
1430
+ }
1431
+ return this.persist(starting, { directReview: withoutInvocation });
1432
+ }
1433
+ async startNextCycle(active, findings, sources) {
1434
+ if (active.record.directReview?.status === 'clear') {
1435
+ const fallbackProvenance = active.record.lifecycle === 'proving' ? 'proof' : 'check';
1436
+ const normalizedSources = sources ?? findings.map((summary) => ({
1437
+ provenance: fallbackProvenance,
1438
+ sourceId: `${fallbackProvenance}:recovery:${sha256(summary)}`,
1439
+ summary,
1440
+ affectedContracts: [fallbackProvenance === 'proof' ? 'acceptance-proof' : 'configured-checks'],
1441
+ }));
1442
+ const repairFindings = normalizedSources.map((source) => ({
1443
+ id: source.sourceId,
1444
+ provenance: source.provenance,
1445
+ sourceId: source.sourceId,
1446
+ targetRevision: active.record.directReview.targetRevision,
1447
+ summary: source.summary,
1448
+ affectedContracts: source.affectedContracts,
1449
+ status: 'open',
1450
+ }));
1451
+ return this.persist(active, {
1452
+ lifecycle: 'implementing',
1453
+ cycle: (active.record.cycle + 1),
1454
+ reworkFindings: [...findings],
1455
+ directReview: beginDirectReviewRepair(active.record.directReview, repairFindings),
1456
+ checks: [],
1457
+ checkedChangeSha256: undefined,
1458
+ proofId: undefined,
1459
+ proofReceipt: undefined,
1460
+ });
1461
+ }
749
1462
  active = await this.persist(active, { lifecycle: 'reworking', reworkFindings: [...findings] });
750
1463
  return this.persist(active, {
751
1464
  lifecycle: 'implementing',
752
1465
  cycle: (active.record.cycle + 1),
753
- packageVersion: this.dependencies.packageVersion,
754
- skillHashes: structuredClone(this.dependencies.skillHashes),
755
1466
  checks: [],
756
1467
  checkedChangeSha256: undefined,
757
1468
  proofId: undefined,
@@ -792,6 +1503,38 @@ export class RunIssue {
792
1503
  outcomeEvidenceId: evidence.id,
793
1504
  process: undefined,
794
1505
  };
1506
+ if (outcome.status !== 'review-ready' && active.record.directReview && active.record.directReview.status !== 'terminal') {
1507
+ changes.directReview = projectTerminalDirectReview(active.record.directReview, outcome.status === 'blocked'
1508
+ ? { status: 'blocked', kind: outcome.kind }
1509
+ : { status: outcome.status });
1510
+ }
1511
+ if (active.record.waitingHuman && (active.record.lifecycle === 'waiting-human' || active.record.waitingHuman.phase === 'resumed')) {
1512
+ if (active.record.waitingHuman.phase === 'resumed') {
1513
+ changes.waitingHuman = {
1514
+ version: 1,
1515
+ clarificationAttempts: active.record.waitingHuman.clarificationAttempts,
1516
+ permissionRetries: active.record.waitingHuman.permissionRetries,
1517
+ effectRetries: structuredClone(active.record.waitingHuman.effectRetries),
1518
+ history: structuredClone(active.record.waitingHuman.history),
1519
+ phase: 'history-only',
1520
+ terminalOutcome: outcome.status === 'blocked'
1521
+ ? { status: 'blocked', kind: outcome.kind }
1522
+ : { status: outcome.status },
1523
+ };
1524
+ }
1525
+ else if (outcome.status === 'blocked' || outcome.status === 'cancelled') {
1526
+ changes.waitingHuman = archiveWaiting(active.record, waitingAnswer(active.record.waitingHuman), {
1527
+ phase: 'history-only',
1528
+ terminalOutcome: outcome.status === 'cancelled'
1529
+ ? { status: 'cancelled' }
1530
+ : { status: 'blocked', kind: outcome.kind },
1531
+ });
1532
+ }
1533
+ }
1534
+ if (active.record.lifecycle === 'triaging' || (active.record.lifecycle === 'safe-halt' && !active.record.routeReceipt)) {
1535
+ changes.routeExecution = undefined;
1536
+ changes.routeReceipt = undefined;
1537
+ }
795
1538
  if (!retainIntent)
796
1539
  changes.intent = undefined;
797
1540
  await this.persist(active, changes);
@@ -835,6 +1578,17 @@ function snapshotIssue(issue) {
835
1578
  url: issue.url,
836
1579
  state: 'OPEN',
837
1580
  labels: sortedUnique(issue.labels),
1581
+ comments: structuredClone(issue.comments),
1582
+ };
1583
+ }
1584
+ function publicIssueSnapshot(issue) {
1585
+ return {
1586
+ number: issue.number,
1587
+ title: issue.title,
1588
+ body: issue.body,
1589
+ url: issue.url,
1590
+ state: issue.state,
1591
+ labels: [...issue.labels],
838
1592
  };
839
1593
  }
840
1594
  function freezeCriteria(issue) {
@@ -864,11 +1618,72 @@ function findRun(state, runId) {
864
1618
  throw new Error('persisted run is missing');
865
1619
  return record;
866
1620
  }
1621
+ function waitingAnswer(waiting) {
1622
+ if ('answerReceipt' in waiting)
1623
+ return structuredClone(waiting.answerReceipt);
1624
+ if (waiting.phase === 'resumed')
1625
+ return structuredClone(waiting.trustedAnswer);
1626
+ return null;
1627
+ }
1628
+ function archiveWaiting(record, answer, terminal) {
1629
+ const waiting = record.waitingHuman;
1630
+ const routeReceipt = record.routeReceipt;
1631
+ if (!waiting || !routeReceipt)
1632
+ throw new Error('waiting archive requires active route evidence');
1633
+ const question = 'question' in waiting ? waiting.question : 'questionReceipt' in waiting ? waiting.questionReceipt.question : undefined;
1634
+ if (!question)
1635
+ throw new Error('waiting archive requires current question');
1636
+ const questionReceipt = 'questionReceipt' in waiting ? waiting.questionReceipt : null;
1637
+ const entry = {
1638
+ routeReceipt: structuredClone(routeReceipt),
1639
+ question: structuredClone(question),
1640
+ questionReceipt: questionReceipt ? structuredClone(questionReceipt) : null,
1641
+ answerReceipt: answer ? structuredClone(answer) : null,
1642
+ conflictHashes: [...question.conflictHashes],
1643
+ };
1644
+ const history = [...waiting.history];
1645
+ if (history.at(-1)?.question.questionSha256 === question.questionSha256) {
1646
+ if (canonicalJson(history.at(-1)) !== canonicalJson(entry))
1647
+ throw new Error('waiting archive evidence mismatch');
1648
+ }
1649
+ else {
1650
+ history.push(entry);
1651
+ }
1652
+ return {
1653
+ version: 1,
1654
+ clarificationAttempts: waiting.clarificationAttempts,
1655
+ permissionRetries: waiting.permissionRetries,
1656
+ effectRetries: structuredClone(waiting.effectRetries),
1657
+ history,
1658
+ ...terminal,
1659
+ };
1660
+ }
1661
+ function terminalWaiting(waiting, terminalOutcome) {
1662
+ if (waiting.phase !== 'resumed' && waiting.phase !== 'history-only')
1663
+ throw new Error('terminal waiting projection requires archived history');
1664
+ return {
1665
+ version: 1,
1666
+ clarificationAttempts: waiting.clarificationAttempts,
1667
+ permissionRetries: waiting.permissionRetries,
1668
+ effectRetries: structuredClone(waiting.effectRetries),
1669
+ history: structuredClone(waiting.history),
1670
+ phase: 'history-only',
1671
+ terminalOutcome,
1672
+ };
1673
+ }
867
1674
  function publicOutcome(outcome) {
868
1675
  if (outcome.status === 'internal-error')
869
1676
  return { status: 'internal-error', evidencePath: outcome.evidencePath };
870
1677
  return structuredClone(outcome);
871
1678
  }
1679
+ function requireRouteExecution(value) {
1680
+ if (!value)
1681
+ throw new Error('route execution is missing');
1682
+ return structuredClone(value);
1683
+ }
1684
+ function sameRouteExecution(left, right) {
1685
+ return left !== undefined && canonicalJson(left) === canonicalJson(right);
1686
+ }
872
1687
  function sameStrings(left, right) {
873
1688
  const a = [...left].sort();
874
1689
  const b = [...right].sort();