@straiffi/archon 1.10.0 → 1.10.2

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 (184) hide show
  1. package/dist/client/assets/ActivityView-CdDT5tgr.js +1 -0
  2. package/dist/client/assets/{GitHubActionsDialog-GWVxWGPK.js → GitHubActionsDialog-NdYtgMhg.js} +1 -1
  3. package/dist/client/assets/{VerificationDialog-C4RMuI4y.js → VerificationDialog-BfIKtVjt.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-CeM6gbLN.js → architectureDiagram-3BPJPVTR-C4zoWnXM.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-BP4hta96.js → blockDiagram-GPEHLZMM-ZhRwV8ma.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-5aJScvhn.js → c4Diagram-AAUBKEIU-CA2USxCJ.js} +1 -1
  7. package/dist/client/assets/channel-C4I-VlJu.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-BKc2IelV.js → chunk-2J33WTMH-CY32Brnt.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DCc15la5.js → chunk-3OPIFGDE-H70UH7rH.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-DSD9HbTp.js → chunk-5ZQYHXKU-DC1dllSt.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-DrAjnKiA.js → chunk-727SXJPM-DVIgI7hM.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-DxKnpHfQ.js → chunk-AQP2D5EJ-gwSYEPou.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DLejrAB_.js → chunk-CSCIHK7Q-Z1cfABPc.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-BWRNmF8S.js → chunk-KSCS5N6A-DFoW6dtq.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-D48i966f.js → chunk-L5ZTLDWV-CmQbbP8b.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-DLhg6Bok.js → chunk-LZXEDZCA-Ba16nX-R.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-B4-7cOEq.js → chunk-ND2GUHAM-Bc7u1Zqm.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-D2ttXA_d.js → chunk-NZK2D7GU-DNXYx7JX.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-BrWrOVCq.js → chunk-O5CBEL6O-Ba00cuZ5.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-CLcPwxPe.js → chunk-WU5MYG2G-SJ_ny5dU.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-Ca3KxBnW.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CSpMessw.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DIXN8_VX.js → dagre-BM42HDAG-CL2dKbj9.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-D2wjdCij.js → diagram-2AECGRRQ-DQq1qZ0l.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-E1Og_PHE.js → diagram-5GNKFQAL-Zf0YQuRe.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-B6P1Ud8a.js → diagram-KO2AKTUF-DcUxL9SG.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-C3Jxq56v.js → diagram-LMA3HP47-BAIixEu7.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-Do9LVhdL.js → diagram-OG6HWLK6-Dhhjm84p.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DLb5E4-f.js → erDiagram-TEJ5UH35-BcQOacBP.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-CcTs_wU9.js → flowDiagram-I6XJVG4X-BZIJqLp3.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-q9ez5fri.js → ganttDiagram-6RSMTGT7-DFC6qgrY.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BcaKiz4c.js → gitGraphDiagram-PVQCEYII-Ds0nwSU0.js} +1 -1
  33. package/dist/client/assets/index-Bg3l17L3.css +2 -0
  34. package/dist/client/assets/index-D_KmViSu.js +276 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-Cu0w4frE.js → infoDiagram-5YYISTIA-DfDrR5Mz.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CJ47Z-ak.js → ishikawaDiagram-YF4QCWOH-B2v4Byji.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-fAL_Zcvs.js → journeyDiagram-JHISSGLW-CO6laklO.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-BTNIybMC.js → kanban-definition-UN3LZRKU-BziFO199.js} +1 -1
  39. package/dist/client/assets/{line-BvsadeZR.js → line-Vb0A8-Oe.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-C0EkH2y_.js → mermaid-parser.core-Bm3Xv0Zt.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-BU5QIIbY.js → mermaid.core-DXBRp0XG.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-CWbSYK8f.js → mindmap-definition-RKZ34NQL-4GueJvPT.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-t4w2cB-y.js → pieDiagram-4H26LBE5-Djy0sVJR.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Cqasvrpa.js → quadrantDiagram-W4KKPZXB-3rWAi4K5.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-_l-mY-tb.js → requirementDiagram-4Y6WPE33-D7-WCnjH.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Cr9YrZ-Y.js → sankeyDiagram-5OEKKPKP-B4-3BUQ2.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DvzZhQpb.js → sequenceDiagram-3UESZ5HK-DCWomuSc.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-cFjPpAO3.js → stateDiagram-AJRCARHV-DpsWWFaO.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BE_IAuYt.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BjRNrEJY.js → timeline-definition-PNZ67QCA-B0Mh4MHF.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-xs9afjrw.js → vennDiagram-CIIHVFJN-Dpxr8LTG.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DH68ZYti.js → wardleyDiagram-YWT4CUSO-BdAz5ERd.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DLW5ktTU.js → xychartDiagram-2RQKCTM6-BV9Ah-jk.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +640 -0
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +795 -125
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agentCatalog.js +35 -0
  60. package/dist/server/lib/agentCatalog.js.map +1 -0
  61. package/dist/server/lib/agentModelConfig.js +151 -0
  62. package/dist/server/lib/agentModelConfig.js.map +1 -0
  63. package/dist/server/lib/agentRuns.js +1 -0
  64. package/dist/server/lib/agentRuns.js.map +1 -1
  65. package/dist/server/lib/automationCheckGate.js +98 -9
  66. package/dist/server/lib/automationCheckGate.js.map +1 -1
  67. package/dist/server/lib/automationLoopAgentEvidence.js +42 -0
  68. package/dist/server/lib/automationLoopAgentEvidence.js.map +1 -0
  69. package/dist/server/lib/automationLoopProjection.js +339 -0
  70. package/dist/server/lib/automationLoopProjection.js.map +1 -0
  71. package/dist/server/lib/automationProgression.js +115 -6
  72. package/dist/server/lib/automationProgression.js.map +1 -1
  73. package/dist/server/lib/automationReconciliation.js +27 -11
  74. package/dist/server/lib/automationReconciliation.js.map +1 -1
  75. package/dist/server/lib/automationResumeDecision.js +55 -15
  76. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  77. package/dist/server/lib/automationReviewGate.js +352 -26
  78. package/dist/server/lib/automationReviewGate.js.map +1 -1
  79. package/dist/server/lib/buildExecutionEnvironment.js +7 -4
  80. package/dist/server/lib/buildExecutionEnvironment.js.map +1 -1
  81. package/dist/server/lib/buildFlow.js +12 -0
  82. package/dist/server/lib/buildFlow.js.map +1 -1
  83. package/dist/server/lib/bundleReviewOperations.js +40 -23
  84. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  85. package/dist/server/lib/chatOperations.js +26 -4
  86. package/dist/server/lib/chatOperations.js.map +1 -1
  87. package/dist/server/lib/checkAutoFix.js +5 -4
  88. package/dist/server/lib/checkAutoFix.js.map +1 -1
  89. package/dist/server/lib/checkAutoFixDetails.js +54 -9
  90. package/dist/server/lib/checkAutoFixDetails.js.map +1 -1
  91. package/dist/server/lib/checkGateEvaluations.js +6 -1
  92. package/dist/server/lib/checkGateEvaluations.js.map +1 -1
  93. package/dist/server/lib/checkRepair.js +30 -0
  94. package/dist/server/lib/checkRepair.js.map +1 -1
  95. package/dist/server/lib/claudeAgentProvider.js +19 -0
  96. package/dist/server/lib/claudeAgentProvider.js.map +1 -1
  97. package/dist/server/lib/claudeValidationOutput.js +4 -4
  98. package/dist/server/lib/claudeValidationOutput.js.map +1 -1
  99. package/dist/server/lib/dockerSandboxExecutionEnvironment.js +48 -7
  100. package/dist/server/lib/dockerSandboxExecutionEnvironment.js.map +1 -1
  101. package/dist/server/lib/executionEnvironmentValidation.js +19 -10
  102. package/dist/server/lib/executionEnvironmentValidation.js.map +1 -1
  103. package/dist/server/lib/executionRunAuthority.js +30 -0
  104. package/dist/server/lib/executionRunAuthority.js.map +1 -1
  105. package/dist/server/lib/executionRuns.js +17 -3
  106. package/dist/server/lib/executionRuns.js.map +1 -1
  107. package/dist/server/lib/executionTimeline.js +3 -0
  108. package/dist/server/lib/executionTimeline.js.map +1 -1
  109. package/dist/server/lib/lightweightPrompt.js +13 -40
  110. package/dist/server/lib/lightweightPrompt.js.map +1 -1
  111. package/dist/server/lib/mobileAccess.js +47 -3
  112. package/dist/server/lib/mobileAccess.js.map +1 -1
  113. package/dist/server/lib/modelResolutionErrors.js +24 -0
  114. package/dist/server/lib/modelResolutionErrors.js.map +1 -0
  115. package/dist/server/lib/models.js +9 -9
  116. package/dist/server/lib/models.js.map +1 -1
  117. package/dist/server/lib/postBuildCheckRuns.js +79 -2
  118. package/dist/server/lib/postBuildCheckRuns.js.map +1 -1
  119. package/dist/server/lib/projectAutoConfig.js +15 -52
  120. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  121. package/dist/server/lib/projectMemoryScan.js +15 -9
  122. package/dist/server/lib/projectMemoryScan.js.map +1 -1
  123. package/dist/server/lib/projects.js +59 -4
  124. package/dist/server/lib/projects.js.map +1 -1
  125. package/dist/server/lib/reviewRepairCompletion.js +126 -0
  126. package/dist/server/lib/reviewRepairCompletion.js.map +1 -0
  127. package/dist/server/lib/reviewRepairIterationRecords.js +149 -0
  128. package/dist/server/lib/reviewRepairIterationRecords.js.map +1 -0
  129. package/dist/server/lib/reviewRepairIterations.js +590 -0
  130. package/dist/server/lib/reviewRepairIterations.js.map +1 -0
  131. package/dist/server/lib/reviewRepairReconciliation.js +588 -0
  132. package/dist/server/lib/reviewRepairReconciliation.js.map +1 -0
  133. package/dist/server/lib/reviewerConfig.js +18 -7
  134. package/dist/server/lib/reviewerConfig.js.map +1 -1
  135. package/dist/server/lib/run.js +7 -2
  136. package/dist/server/lib/run.js.map +1 -1
  137. package/dist/server/lib/specArtifact.js +1 -1
  138. package/dist/server/lib/specArtifact.js.map +1 -1
  139. package/dist/server/lib/specGraphFeature.js +2 -2
  140. package/dist/server/lib/specGraphFeature.js.map +1 -1
  141. package/dist/server/lib/specOperations.js +26 -4
  142. package/dist/server/lib/specOperations.js.map +1 -1
  143. package/dist/server/lib/specRepositoryEvidenceFormatting.js +2 -2
  144. package/dist/server/lib/specRepositoryEvidenceFormatting.js.map +1 -1
  145. package/dist/server/lib/specRepositoryImpact.js +3 -3
  146. package/dist/server/lib/specRepositoryImpact.js.map +1 -1
  147. package/dist/server/lib/specReviewContext.js +7 -1
  148. package/dist/server/lib/specReviewContext.js.map +1 -1
  149. package/dist/server/lib/specReviewOperations.js +20 -7
  150. package/dist/server/lib/specReviewOperations.js.map +1 -1
  151. package/dist/server/lib/specSnapshotEvidence.js +3 -3
  152. package/dist/server/lib/specSnapshotEvidence.js.map +1 -1
  153. package/dist/server/lib/ticketAutomation.js +9 -11
  154. package/dist/server/lib/ticketAutomation.js.map +1 -1
  155. package/dist/server/lib/ticketAutomationCommit.js +21 -11
  156. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  157. package/dist/server/lib/ticketFollowUpOperations.js +25 -7
  158. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  159. package/dist/server/lib/ticketSettingsOperations.js +9 -0
  160. package/dist/server/lib/ticketSettingsOperations.js.map +1 -1
  161. package/dist/server/lib/ticketWorkflowOperations.js +2 -6
  162. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  163. package/dist/server/lib/tickets.js +16 -6
  164. package/dist/server/lib/tickets.js.map +1 -1
  165. package/dist/server/lib/types.js +45 -0
  166. package/dist/server/lib/types.js.map +1 -1
  167. package/dist/server/workers/build.js +111 -1
  168. package/dist/server/workers/build.js.map +1 -1
  169. package/dist/server/workers/followUp.js +109 -22
  170. package/dist/server/workers/followUp.js.map +1 -1
  171. package/dist/server/workers/review.js +292 -60
  172. package/dist/server/workers/review.js.map +1 -1
  173. package/dist/server/workers/specReview.js +17 -8
  174. package/dist/server/workers/specReview.js.map +1 -1
  175. package/dist/server/workers/specSession.js +33 -23
  176. package/dist/server/workers/specSession.js.map +1 -1
  177. package/package.json +1 -1
  178. package/dist/client/assets/ActivityView-Y5IaJiim.js +0 -1
  179. package/dist/client/assets/channel-B3DjZXbm.js +0 -1
  180. package/dist/client/assets/classDiagram-4FO5ZUOK-DproSkn2.js +0 -1
  181. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-JVMMzJxJ.js +0 -1
  182. package/dist/client/assets/index-1CokwSFm.css +0 -2
  183. package/dist/client/assets/index-CSOKbu3w.js +0 -276
  184. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-btHMH5Cg.js +0 -1
@@ -48,6 +48,8 @@ import { createExecutionProfileSelection, executionProfileSelectionMetadata, val
48
48
  import { prepareTicketForBuild } from './lib/buildFlow.js';
49
49
  import { getActiveBundleRepairBlocker, getBundlePlanMutationBlockerById, shouldBlockPlanForBundleMutationById, } from './lib/bundleActivity.js';
50
50
  import { getDiscoveredModels, normalizeModelId, normalizeTicketModelId } from './lib/models.js';
51
+ import { getSupportedAgentCatalog } from './lib/agentCatalog.js';
52
+ import { parseStoredAgentModelConfig, resolveHelperModelConfig, serializeAgentModelConfig, validateAgentModelConfigUpdate, validateDiscoveredAgentModelConfig, } from './lib/agentModelConfig.js';
51
53
  import { upgradeTool, UpgradeInProgressError } from './lib/toolUpgrade.js';
52
54
  import { acceptProjectMemorySuggestion, applyProjectMemorySuggestionToDecision, applyProjectMemorySuggestionToConvention, archiveProjectConvention, archiveProjectDecision, createProjectConvention, createProjectDecision, dismissProjectMemorySuggestion, getProjectMemoryById, getProjectMemorySuggestionById, isProjectMemoryEnabled, listProjectConventions, listLatestSuccessfulProjectContextArtifacts, listProjectDecisions, listProjectMemories, listProjectMemorySuggestions, restoreProjectConvention, restoreProjectDecision, serializeProjectMemoryFromConventionValue, serializeProjectMemoryFromDecisionValue, serializeProjectMemoryFromSuggestionValue, supersedeProjectDecision, supersedeProjectDecisionFromSuggestion, updateProjectConvention, updateProjectDecision, validateProjectConventionPayload, validateProjectDecisionPayload, } from './lib/projectMemory.js';
53
55
  import { selectRelevantProjectMemory } from './lib/projectMemoryPrompt.js';
@@ -64,20 +66,23 @@ import { buildReviewFindingsResponse, computeDiffSignature, getLatestCompletedRe
64
66
  import { formatBuildOpenQuestionAnswersPrompt, getLatestBuildOpenQuestionBatchStatus, hasActiveBuildOpenQuestionBatch, listBuildOpenQuestionsForBundle, loadBuildOpenQuestionsByBundleIds, } from './lib/buildOpenQuestions.js';
65
67
  import { acceptBuildOpenQuestionResolution } from './lib/buildOpenQuestionOperations.js';
66
68
  import { areBundleTicketsReadyForAutomaticReview } from './lib/bundleAutomationReadiness.js';
67
- import { abandonAutomationPause, completeAutomationProgression, createAutomationReviewFix, ensureAutomationProgression, finishAutomationReviewFix, getAutomationProgression, recoverLegacyAutomationProgression, recoverStoppedSideEffectProgression, runAutomationProgression, stopAutomationProgression, waitAutomationProgression } from './lib/automationProgression.js';
69
+ import { abandonAutomationPause, completeAutomationProgression, completeAutomationProgressionInTransaction, completeAutomationReviewFixActionInTransaction, createAutomationReviewFixInTransaction, ensureAutomationProgression, finishAutomationReviewFix, getAutomationProgression, recoverLegacyAutomationProgression, recoverStoppedSideEffectProgression, runAutomationProgression, stopAutomationProgression, waitAutomationProgression } from './lib/automationProgression.js';
68
70
  import { reconcileAutomationStage } from './lib/automationReconciliation.js';
69
71
  import { runAutomationReviewGateOrPause } from './lib/automationReviewGate.js';
70
72
  import { pausedRunBelongsToProgression, resolveAutomationResumeDecision } from './lib/automationResumeDecision.js';
73
+ import { isLegacyReviewRepairMigrationPause } from './lib/reviewRepairReconciliation.js';
71
74
  import { getAutomationStageDetails, } from './lib/automationStageDetails.js';
72
- import { buildAutomationFixInstruction, clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, isAutomationDepth, recordAutomationPause, resolveAutomationConfig, upsertBundleAutomationState, } from './lib/ticketAutomation.js';
75
+ import { buildAutomationFixInstruction, clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, isAutomationActive, isAutomationDepth, recordAutomationPause, resetBundleAutomationLoop, resolveAutomationConfig, } from './lib/ticketAutomation.js';
73
76
  import { resolveAutomationCheckGateResume, runAutomationCommitChain, runAutomationCommitOperation } from './lib/ticketAutomationCommit.js';
74
77
  import { resolveBuildTicketCommitCheckpointLineage, canMutateBuildTicketCommitCheckpoint } from './lib/buildTicketCommit.js';
75
78
  import { continueAfterBuildCheckpoint } from './lib/automationBuildContinuation.js';
76
79
  import { getDiscoveredSkills, normalizeSkillNames } from './lib/skills.js';
77
80
  import { clearAllPreviewProxies } from './lib/previewProxy.js';
78
81
  import { createExecutionRun, addExecutionRunLink, addExecutionRunLinks, getExecutionRun, getLifecycleEvent, hasActiveBuildExecutionRunForTicket, getGitHubActionsExecutionDetails, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listLifecycleEvents, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
82
+ import { appendReviewRepairIterationDecisionInTransaction, attachReviewRepairResultInTransaction, attachReviewRepairActionEnvelopeInTransaction, attachReviewRepairActionInTransaction, claimReviewRepairIterationInTransaction, assertReviewRepairIterationDeletionEligible, deleteTerminalReviewRepairIterationsForTicketInTransaction, getActiveReviewRepairIteration, getReviewRepairIteration, pauseReviewRepairIteration, rewindStaleReviewRepairVerification, replaceReviewRepairActionEnvelopeRetryInTransaction, replaceReviewRepairActionRetryInTransaction, replaceReviewRepairVerificationRetryInTransaction, resetReviewRepairGateForRetry, resumePausedReviewRepairIteration, terminalizeReviewRepairIteration, terminalizeReviewRepairIterationInTransaction, } from './lib/reviewRepairIterations.js';
79
83
  import { dispatchExecutionRetry, getExecutionRetryEligibility, resolveExecutionRetry } from './lib/executionRetry.js';
80
84
  import { listExecutionTimeline } from './lib/executionTimeline.js';
85
+ import { loadReviewAutomationLoopDetails } from './lib/automationLoopProjection.js';
81
86
  import { loadExecutionArtifactSummariesForRuns, openExecutionArtifactForRetrieval, } from './lib/executionArtifacts.js';
82
87
  import { cleanupAllExecutionEvidence, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS, resolveExecutionEvidenceRetentionDays, } from './lib/executionRunRetention.js';
83
88
  import { ExecutionArtifactCleanupError, reconcileExecutionArtifactRegistrationIntents, reconcileFinalizedExecutionArtifacts, reconcileProjectExecutionArtifacts, resolveArtifactCleanupIntervalMs, } from './lib/executionArtifactRetention.js';
@@ -134,7 +139,7 @@ import { buildSpecImplementationDraft } from './lib/specDraft.js';
134
139
  import { materializeSpecSnapshotTickets } from './lib/specTicketMaterialization.js';
135
140
  import { exportSpecToDocs, checkSpecExportCollision, SpecExportCollisionError } from './lib/specExport.js';
136
141
  import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketBuildComplete, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
137
- import { startBuild } from './workers/build.js';
142
+ import { startBuild, startPostBuildCheckRecovery } from './workers/build.js';
138
143
  import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, prepareLinkedPlanningResearch, startPlan, stopAllPlanAgents, } from './workers/plan.js';
139
144
  import { EXECUTION_RUN_KINDS, MAX_ARTIFACTS_PER_RUN, validateWorkflowContext } from './lib/types.js';
140
145
  class ProjectDeletionError extends Error {
@@ -483,6 +488,9 @@ const buildExecutionRunDetailResponse = async (project, run) => {
483
488
  agent_runs: listAgentRunsForExecutionRun(run.id),
484
489
  ...artifacts,
485
490
  environment_context: getExecutionEnvironmentContextForRun(project.id, run.id),
491
+ automation_loops: run.kind === 'automation_progression'
492
+ ? loadReviewAutomationLoopDetails(project.id, run.id)
493
+ : [],
486
494
  };
487
495
  };
488
496
  const formatWorktreeDeleteError = (target, branch, error) => {
@@ -568,6 +576,18 @@ const cleanupWorktreeExecutionTarget = async ({ project, targetKind, targetKey,
568
576
  return { branchWarning: buildBranchCleanupWarning(branch, deleteLocalBranch(branch, project)) };
569
577
  };
570
578
  const deleteTicketRecord = async (ticket) => {
579
+ if (ticket.project_id) {
580
+ try {
581
+ assertReviewRepairIterationDeletionEligible(ticket.project_id, ticket.id);
582
+ }
583
+ catch (error) {
584
+ return {
585
+ ok: false,
586
+ status: 409,
587
+ error: error instanceof Error ? error.message : 'Unable to validate Review repair history',
588
+ };
589
+ }
590
+ }
571
591
  if (ticket.project_id
572
592
  && ticket.worktree_bundle_id
573
593
  && hasActiveBundleCheckWork(ticket.project_id, ticket.worktree_bundle_id)) {
@@ -644,7 +664,21 @@ const deleteTicketRecord = async (ticket) => {
644
664
  affectedTicketIds = sequenceRemoval.affectedTicketIds;
645
665
  }
646
666
  }
647
- db.prepare('DELETE FROM tickets WHERE id = ?').run(ticket.id);
667
+ try {
668
+ withExecutionRunTransaction(transaction => {
669
+ if (ticket.project_id) {
670
+ deleteTerminalReviewRepairIterationsForTicketInTransaction(transaction, ticket.project_id, ticket.id);
671
+ }
672
+ db.prepare('DELETE FROM tickets WHERE id = ?').run(ticket.id);
673
+ }, io, 'immediate');
674
+ }
675
+ catch (error) {
676
+ return {
677
+ ok: false,
678
+ status: 409,
679
+ error: error instanceof Error ? error.message : 'Unable to delete Review repair history',
680
+ };
681
+ }
648
682
  try {
649
683
  clearTicketStageSnapshots(ticket.id);
650
684
  }
@@ -1002,6 +1036,34 @@ const asProjectPayload = (body) => {
1002
1036
  }
1003
1037
  return body;
1004
1038
  };
1039
+ const validateProjectRoleModelDiscovery = async (body, values, currentProject) => {
1040
+ for (const role of ['helper', 'reviewer']) {
1041
+ const mapField = `${role}_models`;
1042
+ const jsonField = `${role}_models_json`;
1043
+ const hasMapUpdate = Object.hasOwn(body, mapField);
1044
+ const hasLegacyUpdate = Object.hasOwn(body, `${role}_model`) || Object.hasOwn(body, `${role}_variant`);
1045
+ if ((!hasMapUpdate && !hasLegacyUpdate) || !values[jsonField]) {
1046
+ continue;
1047
+ }
1048
+ const submittedAgents = hasMapUpdate
1049
+ ? Object.keys(body[mapField])
1050
+ : ['opencode'];
1051
+ const nextConfig = parseStoredAgentModelConfig(values[jsonField]);
1052
+ const currentConfig = parseStoredAgentModelConfig(currentProject?.[jsonField]);
1053
+ const agents = currentProject
1054
+ ? submittedAgents.filter(agent => (nextConfig[agent]?.model !== currentConfig[agent]?.model
1055
+ || nextConfig[agent]?.variant !== currentConfig[agent]?.variant))
1056
+ : submittedAgents;
1057
+ if (agents.length === 0) {
1058
+ continue;
1059
+ }
1060
+ const validation = await validateDiscoveredAgentModelConfig(nextConfig, mapField, agents);
1061
+ if ('error' in validation) {
1062
+ return validation;
1063
+ }
1064
+ }
1065
+ return { ok: true };
1066
+ };
1005
1067
  const isWorktreeReady = (branch, project) => {
1006
1068
  return existsSync(resolveWorktreePath(branch, project));
1007
1069
  };
@@ -1651,7 +1713,7 @@ const startBundlePullRequestDiscoveryIfNeeded = (project, bundle) => {
1651
1713
  })();
1652
1714
  bundlePullRequestDiscoveryInFlight.set(key, task);
1653
1715
  };
1654
- const serializeBundleRow = (projectId, bundle, openQuestions = bundle ? loadBuildOpenQuestionsByBundleIds(projectId, [bundle.id]).get(bundle.id) ?? [] : [], resolvedProject) => {
1716
+ const serializeBundleRow = (projectId, bundle, agent, openQuestions = bundle ? loadBuildOpenQuestionsByBundleIds(projectId, [bundle.id]).get(bundle.id) ?? [] : [], resolvedProject) => {
1655
1717
  if (!bundle) {
1656
1718
  return null;
1657
1719
  }
@@ -1660,11 +1722,20 @@ const serializeBundleRow = (projectId, bundle, openQuestions = bundle ? loadBuil
1660
1722
  ? true
1661
1723
  : Boolean(project && resolveExistingWorktreePath(bundle.branch, project));
1662
1724
  const effectivePolicy = resolveEffectiveRequiredCheckPolicyMembership(project);
1725
+ const reviewerModels = bundle.kind === 'project_root'
1726
+ ? {}
1727
+ : parseStoredAgentModelConfig(bundle.reviewer_models_json);
1728
+ const reviewerOpenCode = reviewerModels.opencode;
1729
+ const { reviewer_models_json: _reviewerModelsJson, ...serializedRow } = bundle;
1730
+ void _reviewerModelsJson;
1663
1731
  return {
1664
- ...bundle,
1665
- reviewer_model: bundle.kind === 'project_root' ? null : bundle.reviewer_model ?? null,
1666
- reviewer_variant: bundle.kind === 'project_root' || !bundle.reviewer_model ? null : bundle.reviewer_variant ?? null,
1667
- reviewer_config: resolveReviewerConfig(bundle.kind === 'project_root' ? null : bundle, project),
1732
+ ...serializedRow,
1733
+ reviewer_model: reviewerOpenCode?.model ?? null,
1734
+ reviewer_variant: reviewerOpenCode?.model ? reviewerOpenCode.variant : null,
1735
+ reviewer_models: reviewerModels,
1736
+ reviewer_config: resolveReviewerConfig(bundle.kind === 'project_root' ? null : {
1737
+ reviewer_models: reviewerModels,
1738
+ }, project, undefined, agent),
1668
1739
  has_prepared_worktree: hasPreparedWorktree,
1669
1740
  pull_request: getGitHubConnectionConfig(projectId)
1670
1741
  ? serializeBundlePullRequest(getBundlePullRequestRecord(bundle.id, projectId))
@@ -1879,8 +1950,9 @@ const resolveBundleRenameEligibility = (project, bundle, tickets = listBundleTic
1879
1950
  return { eligible: true, reason: null };
1880
1951
  };
1881
1952
  const serializeBundleDetails = (project, bundle, extras = {}) => {
1953
+ const agent = config.tool ?? 'opencode';
1882
1954
  const tickets = listDetailedBundleTickets(project.id, bundle.id);
1883
- const serializedBundle = serializeBundleRow(project.id, bundle, tickets.flatMap(ticket => ticket.build_open_questions ?? []), project);
1955
+ const serializedBundle = serializeBundleRow(project.id, bundle, agent, tickets.flatMap(ticket => ticket.build_open_questions ?? []), project);
1884
1956
  if (!serializedBundle) {
1885
1957
  throw new Error(`Failed to serialize bundle ${bundle.id}`);
1886
1958
  }
@@ -1907,7 +1979,10 @@ const serializeBundleDetails = (project, bundle, extras = {}) => {
1907
1979
  review_recovery_available: getBundleAutomationState(project.id, bundle.id)?.automation_paused_reason
1908
1980
  ? false
1909
1981
  : getBundleReviewRecovery(project.id, bundle.id) !== null,
1910
- has_helper_model: Boolean(project.helper_model),
1982
+ has_helper_model: Boolean(resolveHelperModelConfig({
1983
+ helperModelsJson: project.helper_models_json,
1984
+ agent,
1985
+ }).model),
1911
1986
  recent_commit_subjects: getBundleRecentCommitSubjects(project, bundle),
1912
1987
  };
1913
1988
  };
@@ -3631,7 +3706,7 @@ app.get('/bundles', async (req, res) => {
3631
3706
  const bundles = listBundles(result.project.id);
3632
3707
  timing.mark('list_bundles');
3633
3708
  const openQuestionsByBundleId = loadBuildOpenQuestionsByBundleIds(result.project.id, bundles.map(bundle => bundle.id));
3634
- const serializedBundles = bundles.map(bundle => serializeBundleRow(result.project.id, bundle, openQuestionsByBundleId.get(bundle.id) ?? [], result.project));
3709
+ const serializedBundles = bundles.map(bundle => serializeBundleRow(result.project.id, bundle, config.tool ?? 'opencode', openQuestionsByBundleId.get(bundle.id) ?? [], result.project));
3635
3710
  timing.mark('serialize_bundles');
3636
3711
  timing.end({ count: serializedBundles.length });
3637
3712
  res.json(serializedBundles);
@@ -3755,7 +3830,12 @@ app.post('/bundles/:id/pull-request-description', async (req, res) => {
3755
3830
  if (isProjectRootBundle(bundle)) {
3756
3831
  return res.status(409).json({ error: 'Pull request descriptions are not available for the project root bundle.' });
3757
3832
  }
3758
- if (!result.project.helper_model) {
3833
+ const helperTool = config.tool ?? 'opencode';
3834
+ const helperConfig = resolveHelperModelConfig({
3835
+ helperModelsJson: result.project.helper_models_json,
3836
+ agent: helperTool,
3837
+ });
3838
+ if (!helperConfig.model) {
3759
3839
  return res.status(409).json({ error: 'Set a helper model in project settings before generating a PR description.' });
3760
3840
  }
3761
3841
  const title = asTrimmedString(req.body?.title);
@@ -3772,9 +3852,9 @@ app.post('/bundles/:id/pull-request-description', async (req, res) => {
3772
3852
  try {
3773
3853
  const worktreePath = resolveWorktreePath(bundle.branch, result.project);
3774
3854
  const rawDescription = await runLightweightPrompt({
3775
- tool: 'opencode',
3776
- model: result.project.helper_model,
3777
- variant: result.project.helper_variant,
3855
+ tool: helperTool,
3856
+ model: helperConfig.model,
3857
+ variant: helperConfig.variant,
3778
3858
  }, buildPullRequestDescriptionPrompt({
3779
3859
  title,
3780
3860
  ticketTitles,
@@ -4002,7 +4082,7 @@ app.post('/bundles', async (req, res) => {
4002
4082
  });
4003
4083
  }
4004
4084
  }
4005
- res.status(201).json(serializeBundleRow(result.project.id, bundle, [], result.project));
4085
+ res.status(201).json(serializeBundleRow(result.project.id, bundle, config.tool ?? 'opencode', [], result.project));
4006
4086
  });
4007
4087
  app.delete('/bundles/:id', async (req, res) => {
4008
4088
  const result = getRequiredProject(req);
@@ -4075,7 +4155,7 @@ app.delete('/bundles/:id', async (req, res) => {
4075
4155
  }
4076
4156
  res.status(204).end();
4077
4157
  });
4078
- app.patch('/bundles/:id', (req, res) => {
4158
+ app.patch('/bundles/:id', async (req, res) => {
4079
4159
  const result = getRequiredProject(req);
4080
4160
  if ('error' in result) {
4081
4161
  return res.status(400).json({ error: result.error });
@@ -4085,7 +4165,7 @@ app.patch('/bundles/:id', (req, res) => {
4085
4165
  return res.status(404).json({ error: 'Bundle not found' });
4086
4166
  }
4087
4167
  const hasNameUpdate = hasOwn(req.body, 'name');
4088
- const hasReviewerUpdate = hasOwn(req.body, 'reviewer_model') || hasOwn(req.body, 'reviewer_variant');
4168
+ const hasReviewerUpdate = hasOwn(req.body, 'reviewer_models') || hasOwn(req.body, 'reviewer_model') || hasOwn(req.body, 'reviewer_variant');
4089
4169
  if (hasOwn(req.body, 'required_check_policy_override')) {
4090
4170
  return res.status(400).json({ error: 'Bundle required-check policy overrides are no longer supported; configure required checks on the project' });
4091
4171
  }
@@ -4181,8 +4261,32 @@ app.patch('/bundles/:id', (req, res) => {
4181
4261
  return res.status(400).json({ error: 'automation_review_max_iterations must be an integer between 1 and 10' });
4182
4262
  }
4183
4263
  }
4184
- let reviewerModel = bundle.reviewer_model ?? null;
4185
- let reviewerVariant = reviewerModel ? bundle.reviewer_variant ?? null : null;
4264
+ let reviewerModels = parseStoredAgentModelConfig(bundle.reviewer_models_json);
4265
+ let reviewerModel = reviewerModels.opencode?.model ?? null;
4266
+ let reviewerVariant = reviewerModel ? reviewerModels.opencode?.variant ?? null : null;
4267
+ let reviewerModelsPatch = null;
4268
+ if (hasOwn(req.body, 'reviewer_models')) {
4269
+ if (hasOwn(req.body, 'reviewer_model') || hasOwn(req.body, 'reviewer_variant')) {
4270
+ return res.status(400).json({
4271
+ error: 'reviewer_models cannot be provided with legacy reviewer_model or reviewer_variant fields',
4272
+ });
4273
+ }
4274
+ const result = validateAgentModelConfigUpdate(req.body.reviewer_models, 'reviewer_models');
4275
+ if ('error' in result) {
4276
+ return res.status(400).json({ error: result.error });
4277
+ }
4278
+ const submittedAgents = Object.keys(req.body.reviewer_models);
4279
+ const changedAgents = submittedAgents.filter(agent => (result.value[agent]?.model !== reviewerModels[agent]?.model
4280
+ || result.value[agent]?.variant !== reviewerModels[agent]?.variant));
4281
+ const discoveryValidation = await validateDiscoveredAgentModelConfig(result.value, 'reviewer_models', changedAgents);
4282
+ if ('error' in discoveryValidation) {
4283
+ return res.status(400).json({ error: discoveryValidation.error });
4284
+ }
4285
+ reviewerModelsPatch = result.value;
4286
+ reviewerModels = { ...reviewerModels, ...result.value };
4287
+ reviewerModel = reviewerModels.opencode?.model ?? null;
4288
+ reviewerVariant = reviewerModel ? reviewerModels.opencode?.variant ?? null : null;
4289
+ }
4186
4290
  if (hasOwn(req.body, 'reviewer_model')) {
4187
4291
  const value = req.body.reviewer_model;
4188
4292
  if (value == null) {
@@ -4206,16 +4310,34 @@ app.patch('/bundles/:id', (req, res) => {
4206
4310
  reviewerVariant = null;
4207
4311
  }
4208
4312
  else if (typeof value === 'string' && value.trim()) {
4209
- reviewerVariant = reviewerModel ? value.trim() : null;
4313
+ reviewerVariant = value.trim();
4210
4314
  }
4211
4315
  else {
4212
4316
  return res.status(400).json({ error: 'reviewer_variant must be a non-empty string' });
4213
4317
  }
4214
4318
  }
4319
+ if (!hasOwn(req.body, 'reviewer_models') && (hasOwn(req.body, 'reviewer_model') || hasOwn(req.body, 'reviewer_variant'))) {
4320
+ const result = validateAgentModelConfigUpdate({
4321
+ opencode: { model: reviewerModel, variant: reviewerVariant },
4322
+ }, 'reviewer_models');
4323
+ if ('error' in result) {
4324
+ return res.status(400).json({ error: result.error });
4325
+ }
4326
+ const discoveryValidation = await validateDiscoveredAgentModelConfig(result.value, 'reviewer_models', ['opencode']);
4327
+ if ('error' in discoveryValidation) {
4328
+ return res.status(400).json({ error: discoveryValidation.error });
4329
+ }
4330
+ reviewerModelsPatch = result.value;
4331
+ reviewerModels = { ...reviewerModels, ...result.value };
4332
+ }
4215
4333
  let updated = null;
4216
4334
  try {
4217
4335
  updated = db.transaction(() => {
4218
- if (nextName !== bundle.name) {
4336
+ const currentBundle = getBundle(bundle.id, result.project.id);
4337
+ if (!currentBundle) {
4338
+ return null;
4339
+ }
4340
+ if (hasNameUpdate && nextName !== currentBundle.name) {
4219
4341
  updateBundleIdentity({
4220
4342
  bundleId: bundle.id,
4221
4343
  projectId: result.project.id,
@@ -4223,21 +4345,36 @@ app.patch('/bundles/:id', (req, res) => {
4223
4345
  branch: nextBranch,
4224
4346
  });
4225
4347
  }
4226
- updateBundleAutomationConfig({
4227
- bundleId: bundle.id,
4228
- projectId: result.project.id,
4229
- automationDepth,
4230
- automationReviewFixEnabled,
4231
- automationPauseOnOpenQuestions,
4232
- automationReviewMaxIterations,
4233
- });
4348
+ if (hasOwn(req.body, 'automation_depth')
4349
+ || hasOwn(req.body, 'automation_review_fix_enabled')
4350
+ || hasOwn(req.body, 'automation_pause_on_open_questions')
4351
+ || hasOwn(req.body, 'automation_review_max_iterations')) {
4352
+ updateBundleAutomationConfig({
4353
+ bundleId: bundle.id,
4354
+ projectId: result.project.id,
4355
+ automationDepth: hasOwn(req.body, 'automation_depth') ? automationDepth : currentBundle.automation_depth ?? null,
4356
+ automationReviewFixEnabled: hasOwn(req.body, 'automation_review_fix_enabled') ? automationReviewFixEnabled : currentBundle.automation_review_fix_enabled ?? null,
4357
+ automationPauseOnOpenQuestions: hasOwn(req.body, 'automation_pause_on_open_questions') ? automationPauseOnOpenQuestions : currentBundle.automation_pause_on_open_questions ?? null,
4358
+ automationReviewMaxIterations: hasOwn(req.body, 'automation_review_max_iterations') ? automationReviewMaxIterations : currentBundle.automation_review_max_iterations ?? null,
4359
+ });
4360
+ }
4361
+ if (!reviewerModelsPatch) {
4362
+ return getBundle(bundle.id, result.project.id);
4363
+ }
4364
+ const currentReviewerModels = parseStoredAgentModelConfig(currentBundle.reviewer_models_json);
4365
+ const mergedReviewerModels = validateAgentModelConfigUpdate(reviewerModelsPatch, 'reviewer_models', currentReviewerModels);
4366
+ if ('error' in mergedReviewerModels) {
4367
+ throw new Error(mergedReviewerModels.error);
4368
+ }
4369
+ const currentOpenCode = mergedReviewerModels.value.opencode;
4234
4370
  return updateBundleReviewerConfig({
4235
4371
  bundleId: bundle.id,
4236
4372
  projectId: result.project.id,
4237
- reviewerModel,
4238
- reviewerVariant,
4373
+ reviewerModel: currentOpenCode?.model ?? null,
4374
+ reviewerVariant: currentOpenCode?.model ? currentOpenCode.variant : null,
4375
+ reviewerModelsJson: serializeAgentModelConfig(mergedReviewerModels.value),
4239
4376
  });
4240
- })();
4377
+ }).immediate();
4241
4378
  }
4242
4379
  catch (error) {
4243
4380
  if (error instanceof Error && 'code' in error && String(error.code).startsWith('SQLITE_CONSTRAINT')) {
@@ -4248,7 +4385,7 @@ app.patch('/bundles/:id', (req, res) => {
4248
4385
  listBundleTickets(result.project.id, bundle.id).forEach(ticket => {
4249
4386
  io.emit('ticket:updated', ticket);
4250
4387
  });
4251
- res.json(serializeBundleRow(result.project.id, updated));
4388
+ res.json(serializeBundleRow(result.project.id, updated, config.tool ?? 'opencode'));
4252
4389
  });
4253
4390
  app.post('/bundles/:id/build', (req, res) => {
4254
4391
  const result = getRequiredProject(req);
@@ -4545,17 +4682,18 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4545
4682
  questionResolutionActive: hasActiveBuildOpenQuestionBatch(result.project.id, bundle.id),
4546
4683
  });
4547
4684
  };
4548
- const decision = resolveCurrentDecision();
4549
- if (!decision || decision.status !== 'enabled') {
4550
- const error = decision?.status === 'hidden' && decision.reason === 'question_resolution_active'
4685
+ const initialDecision = resolveCurrentDecision();
4686
+ if (!initialDecision || initialDecision.status !== 'enabled') {
4687
+ const error = initialDecision?.status === 'hidden' && initialDecision.reason === 'question_resolution_active'
4551
4688
  ? 'Build open question resolution is still in progress'
4552
- : decision?.status === 'hidden' && decision.reason === 'resolve_open_questions'
4689
+ : initialDecision?.status === 'hidden' && initialDecision.reason === 'resolve_open_questions'
4553
4690
  ? 'Resolve or dismiss all Build open questions before resuming automation'
4554
- : decision?.status === 'disabled' && decision.reason === 'invalid_stage_lineage'
4691
+ : initialDecision?.status === 'disabled' && initialDecision.reason === 'invalid_stage_lineage'
4555
4692
  ? 'The paused automation stage does not belong to the active progression'
4556
4693
  : 'Bundle automation cannot be resumed in its current state';
4557
- return res.status(409).json({ error, automation_resume_decision: decision });
4694
+ return res.status(409).json({ error, automation_resume_decision: initialDecision });
4558
4695
  }
4696
+ let decision = initialDecision;
4559
4697
  const revalidateDecision = () => {
4560
4698
  const current = resolveCurrentDecision();
4561
4699
  return current?.status === 'enabled'
@@ -4770,6 +4908,179 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4770
4908
  return res.status(400).json({ error: 'Ticket project no longer exists' });
4771
4909
  }
4772
4910
  const automationConfig = resolveAutomationConfig(owner.bundle ?? null, project);
4911
+ const legacyReviewRepairPause = isLegacyReviewRepairMigrationPause(result.project.id, bundle.id, progressionRunId);
4912
+ let completedVerificationIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
4913
+ let reconciledSourceReviewRunId = null;
4914
+ if (completedVerificationIteration?.phase === 'paused') {
4915
+ withExecutionRunTransaction(transaction => {
4916
+ appendReviewRepairIterationDecisionInTransaction(transaction, {
4917
+ projectId: result.project.id,
4918
+ iterationId: completedVerificationIteration.id,
4919
+ decisionPoint: 'resume',
4920
+ effectiveAutomationConfig: automationConfig,
4921
+ decisionCode: 'resume_iteration_requested',
4922
+ });
4923
+ }, io, 'immediate');
4924
+ }
4925
+ if (completedVerificationIteration?.phase === 'paused') {
4926
+ const verificationRunId = completedVerificationIteration.resulting_review_execution_run_id;
4927
+ const verificationRun = verificationRunId ? getExecutionRun(result.project.id, verificationRunId) : null;
4928
+ if (verificationRun?.status === 'completed') {
4929
+ try {
4930
+ completedVerificationIteration = resumePausedReviewRepairIteration({
4931
+ projectId: result.project.id,
4932
+ iterationId: completedVerificationIteration.id,
4933
+ expectedCurrentRunId: verificationRunId,
4934
+ broadcaster: io,
4935
+ });
4936
+ }
4937
+ catch (error) {
4938
+ const current = getReviewRepairIteration(result.project.id, completedVerificationIteration.id);
4939
+ if (current?.resulting_review_execution_run_id === verificationRunId
4940
+ && ['verification_active', 'terminal'].includes(current.phase)) {
4941
+ return res.json(serializeBundleDetails(result.project, bundle));
4942
+ }
4943
+ return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
4944
+ }
4945
+ }
4946
+ }
4947
+ if (completedVerificationIteration?.phase === 'verification_active'
4948
+ && completedVerificationIteration.resulting_review_execution_run_id) {
4949
+ const verificationRunId = completedVerificationIteration.resulting_review_execution_run_id;
4950
+ const verificationRun = getExecutionRun(result.project.id, verificationRunId);
4951
+ const resultingReview = verificationRun?.status === 'completed'
4952
+ ? db.prepare(`
4953
+ SELECT review.id
4954
+ FROM review_runs review
4955
+ JOIN execution_run_links result_link
4956
+ ON result_link.execution_run_id = ?
4957
+ AND result_link.project_id = review.project_id
4958
+ AND result_link.entity_type = 'review_run'
4959
+ AND result_link.entity_id = review.id
4960
+ AND result_link.relationship = 'result'
4961
+ WHERE review.project_id = ? AND review.ticket_id = ?
4962
+ AND review.worktree_bundle_id = ? AND review.status = 'completed'
4963
+ LIMIT 1
4964
+ `).get(verificationRunId, result.project.id, owner.id, bundle.id)
4965
+ : undefined;
4966
+ if (resultingReview) {
4967
+ reconciledSourceReviewRunId = resultingReview.id;
4968
+ const blockingCount = countBlockingFindings(listReviewFindings(resultingReview.id), automationConfig);
4969
+ const terminal = !isAutomationActive(automationConfig)
4970
+ ? { outcome: 'interrupted', stopReason: 'configuration_changed' }
4971
+ : blockingCount === 0
4972
+ ? { outcome: 'repaired', stopReason: 'no_blocking_findings' }
4973
+ : automationConfig.reviewFixEnabled
4974
+ ? completedVerificationIteration.ordinal < automationConfig.reviewMaxIterations
4975
+ ? { outcome: 'continuing', stopReason: null }
4976
+ : { outcome: 'exhausted', stopReason: 'max_iterations' }
4977
+ : { outcome: 'interrupted', stopReason: 'automation_disabled' };
4978
+ try {
4979
+ withExecutionRunTransaction(transaction => {
4980
+ if (!completedVerificationIteration.resulting_review_run_id) {
4981
+ attachReviewRepairResultInTransaction(transaction, {
4982
+ projectId: result.project.id,
4983
+ iterationId: completedVerificationIteration.id,
4984
+ expectedPhase: 'verification_active',
4985
+ expectedVerificationRunId: verificationRunId,
4986
+ resultingReviewRunId: resultingReview.id,
4987
+ });
4988
+ }
4989
+ appendReviewRepairIterationDecisionInTransaction(transaction, {
4990
+ projectId: result.project.id,
4991
+ iterationId: completedVerificationIteration.id,
4992
+ decisionPoint: 'verification_completion',
4993
+ effectiveAutomationConfig: automationConfig,
4994
+ decisionCode: 'verification_reconciled_on_resume',
4995
+ operationRunId: verificationRunId,
4996
+ });
4997
+ terminalizeReviewRepairIterationInTransaction(transaction, {
4998
+ projectId: result.project.id,
4999
+ iterationId: completedVerificationIteration.id,
5000
+ expectedPhase: 'verification_active',
5001
+ expectedCurrentRunId: verificationRunId,
5002
+ outcome: terminal.outcome,
5003
+ stopReason: terminal.stopReason,
5004
+ resultingBlockingCount: blockingCount,
5005
+ });
5006
+ if (automationConfig.depth === 'manual') {
5007
+ completeAutomationProgressionInTransaction(transaction, {
5008
+ projectId: result.project.id,
5009
+ bundleId: bundle.id,
5010
+ progressionRunId,
5011
+ domainStatus: 'manual_boundary_reached',
5012
+ });
5013
+ }
5014
+ if (terminal.outcome === 'continuing') {
5015
+ claimReviewRepairIterationInTransaction(transaction, {
5016
+ projectId: result.project.id,
5017
+ worktreeBundleId: bundle.id,
5018
+ ownerTicketId: owner.id,
5019
+ automationProgressionRunId: progressionRunId,
5020
+ triggeringReviewExecutionRunId: verificationRunId,
5021
+ triggeringReviewRunId: resultingReview.id,
5022
+ triggeringBlockingCount: blockingCount,
5023
+ effectiveAutomationConfig: automationConfig,
5024
+ decisionCode: 'resume_verification_repair_admitted',
5025
+ });
5026
+ }
5027
+ }, io, 'immediate');
5028
+ }
5029
+ catch (error) {
5030
+ const current = getReviewRepairIteration(result.project.id, completedVerificationIteration.id);
5031
+ if (current?.phase === 'terminal'
5032
+ && current.resulting_review_execution_run_id === verificationRunId
5033
+ && current.resulting_review_run_id === resultingReview.id) {
5034
+ return res.json(serializeBundleDetails(result.project, bundle));
5035
+ }
5036
+ return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
5037
+ }
5038
+ if (terminal.outcome === 'continuing') {
5039
+ const continuationDecision = resolveCurrentDecision();
5040
+ if (!continuationDecision || continuationDecision.status !== 'enabled') {
5041
+ return res.status(409).json({ error: 'Bundle automation state changed before repair continuation could start' });
5042
+ }
5043
+ decision = continuationDecision;
5044
+ }
5045
+ else if (terminal.outcome === 'repaired') {
5046
+ resetBundleAutomationLoop(result.project.id, bundle.id);
5047
+ runAutomationProgression({
5048
+ projectId: result.project.id,
5049
+ bundleId: bundle.id,
5050
+ progressionRunId,
5051
+ broadcaster: io,
5052
+ });
5053
+ if (depthAtLeast(automationConfig.depth, 'auto_commit')) {
5054
+ await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io, progressionRunId, undefined, verificationRunId);
5055
+ }
5056
+ else {
5057
+ completeAutomationProgression({
5058
+ projectId: result.project.id,
5059
+ bundleId: bundle.id,
5060
+ progressionRunId,
5061
+ broadcaster: io,
5062
+ });
5063
+ }
5064
+ return res.json(serializeBundleDetails(result.project, bundle));
5065
+ }
5066
+ else if (automationConfig.depth !== 'manual') {
5067
+ waitAutomationProgression({
5068
+ projectId: result.project.id,
5069
+ bundleId: bundle.id,
5070
+ progressionRunId,
5071
+ pauseCode: 'review',
5072
+ pauseReason: 'Blocking Review findings remain after repair verification.',
5073
+ reasonCode: 'precondition_failed',
5074
+ pausedStageRunId: verificationRunId,
5075
+ broadcaster: io,
5076
+ });
5077
+ return res.json(serializeBundleDetails(result.project, bundle));
5078
+ }
5079
+ else {
5080
+ return res.json(serializeBundleDetails(result.project, bundle));
5081
+ }
5082
+ }
5083
+ }
4773
5084
  const restoreWaitingProgression = () => {
4774
5085
  waitAutomationProgression({
4775
5086
  projectId: result.project.id,
@@ -4821,19 +5132,36 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4821
5132
  if (!revalidateDecision()) {
4822
5133
  return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
4823
5134
  }
4824
- completeAutomationProgression({
4825
- projectId: result.project.id,
4826
- bundleId: bundle.id,
4827
- progressionRunId,
4828
- domainStatus: 'manual_boundary_reached',
4829
- broadcaster: io,
4830
- });
5135
+ withExecutionRunTransaction(transaction => {
5136
+ const activeIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5137
+ if (activeIteration && activeIteration.phase !== 'terminal') {
5138
+ terminalizeReviewRepairIterationInTransaction(transaction, {
5139
+ projectId: result.project.id,
5140
+ iterationId: activeIteration.id,
5141
+ expectedPhase: activeIteration.phase,
5142
+ expectedCurrentRunId: activeIteration.resulting_review_execution_run_id
5143
+ ?? activeIteration.gate_batch_run_id
5144
+ ?? activeIteration.action_run_id
5145
+ ?? activeIteration.action_envelope_run_id,
5146
+ outcome: 'interrupted',
5147
+ stopReason: 'configuration_changed',
5148
+ });
5149
+ }
5150
+ completeAutomationProgressionInTransaction(transaction, {
5151
+ projectId: result.project.id,
5152
+ bundleId: bundle.id,
5153
+ progressionRunId,
5154
+ domainStatus: 'manual_boundary_reached',
5155
+ });
5156
+ }, io, 'immediate');
4831
5157
  return res.json(serializeBundleDetails(result.project, bundle));
4832
5158
  }
4833
5159
  if ((decision.outcome === 'start_review' || decision.outcome === 'rerun_review_checks')
4834
5160
  && owner.parked_at === null
4835
5161
  && isTicketBuildComplete(owner)
4836
- && (owner.state === 'build' || (decision.outcome === 'rerun_review_checks' && owner.state === 'review'))) {
5162
+ && (owner.state === 'build'
5163
+ || (decision.outcome === 'rerun_review_checks' && owner.state === 'review')
5164
+ || (legacyReviewRepairPause && owner.state === 'review'))) {
4837
5165
  if (!revalidateDecision()) {
4838
5166
  return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
4839
5167
  }
@@ -4842,6 +5170,36 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4842
5170
  return res.status(409).json({ error: 'Automation progression ownership is invalid' });
4843
5171
  }
4844
5172
  const workflowContext = validateWorkflowContext(progression.metadata);
5173
+ let repairIteration = decision.outcome === 'rerun_review_checks'
5174
+ ? getActiveReviewRepairIteration(result.project.id, progressionRunId)
5175
+ : null;
5176
+ if (repairIteration?.phase === 'paused') {
5177
+ repairIteration = repairIteration.stop_reason === 'stale_evidence'
5178
+ && repairIteration.resulting_review_execution_run_id
5179
+ ? rewindStaleReviewRepairVerification({
5180
+ projectId: result.project.id,
5181
+ iterationId: repairIteration.id,
5182
+ expectedVerificationRunId: repairIteration.resulting_review_execution_run_id,
5183
+ broadcaster: io,
5184
+ })
5185
+ : resumePausedReviewRepairIteration({
5186
+ projectId: result.project.id,
5187
+ iterationId: repairIteration.id,
5188
+ expectedCurrentRunId: repairIteration.resulting_review_execution_run_id
5189
+ ?? repairIteration.gate_batch_run_id
5190
+ ?? repairIteration.action_run_id
5191
+ ?? repairIteration.action_envelope_run_id,
5192
+ broadcaster: io,
5193
+ });
5194
+ }
5195
+ if (decision.outcome === 'rerun_review_checks' && repairIteration?.phase === 'gate_active') {
5196
+ repairIteration = resetReviewRepairGateForRetry({
5197
+ projectId: result.project.id,
5198
+ iterationId: repairIteration.id,
5199
+ expectedGateBatchRunId: repairIteration.gate_batch_run_id,
5200
+ broadcaster: io,
5201
+ });
5202
+ }
4845
5203
  let reviewStartFailureStatus = null;
4846
5204
  let reviewStartFailureError = null;
4847
5205
  try {
@@ -4858,6 +5216,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4858
5216
  broadcaster: io,
4859
5217
  pausedStageRunId: bundleState.automation_paused_stage_run_id,
4860
5218
  initiation: { workflowContext },
5219
+ iterationId: repairIteration?.id,
4861
5220
  onReviewStartFailure: failure => {
4862
5221
  reviewStartFailureStatus = failure.status;
4863
5222
  reviewStartFailureError = failure.error;
@@ -4866,6 +5225,31 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4866
5225
  });
4867
5226
  }
4868
5227
  catch (error) {
5228
+ const currentIteration = repairIteration
5229
+ ? getReviewRepairIteration(result.project.id, repairIteration.id)
5230
+ : null;
5231
+ const pausedStage = bundleState.automation_paused_stage_run_id
5232
+ ? getExecutionRun(result.project.id, bundleState.automation_paused_stage_run_id)
5233
+ : null;
5234
+ const retrySource = pausedStage?.retry_of_run_id
5235
+ ? getExecutionRun(result.project.id, pausedStage.retry_of_run_id)
5236
+ : null;
5237
+ console.error('Bundle automation Review resume failed', {
5238
+ project_id: result.project.id,
5239
+ bundle_id: bundle.id,
5240
+ progression_run_id: progressionRunId,
5241
+ decision_outcome: decision.outcome,
5242
+ iteration_id: currentIteration?.id ?? null,
5243
+ iteration_phase: currentIteration?.phase ?? null,
5244
+ iteration_stop_reason: currentIteration?.stop_reason ?? null,
5245
+ paused_stage_run_id: pausedStage?.id ?? null,
5246
+ paused_stage_parent_run_id: pausedStage?.parent_run_id ?? null,
5247
+ paused_stage_retry_of_run_id: pausedStage?.retry_of_run_id ?? null,
5248
+ retry_source_parent_run_id: retrySource?.parent_run_id ?? null,
5249
+ error_name: error instanceof Error ? error.name : null,
5250
+ error_message: error instanceof Error ? error.message : String(error),
5251
+ error_stack: error instanceof Error ? error.stack : null,
5252
+ });
4869
5253
  return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
4870
5254
  }
4871
5255
  if (reviewStartFailureStatus !== null && reviewStartFailureError !== null) {
@@ -4873,12 +5257,12 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4873
5257
  }
4874
5258
  }
4875
5259
  else if (owner.state === 'review' && owner.agent_status !== 'running') {
4876
- const iteration = bundleState.review_iteration_count ?? 0;
4877
5260
  if (decision.outcome === 'start_review_fix') {
4878
5261
  let reviewFixRunId = null;
5262
+ let repairIterationId = null;
5263
+ let followUpRunId = null;
5264
+ let retryActionRunId = null;
4879
5265
  let followUpResult;
4880
- const previousIteration = bundleState.review_iteration_count ?? 0;
4881
- const previousFixLoopActive = bundleState.fix_loop_active;
4882
5266
  try {
4883
5267
  if (!revalidateDecision()) {
4884
5268
  return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
@@ -4888,17 +5272,14 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4888
5272
  return res.status(409).json({ error: 'Automation progression ownership is invalid' });
4889
5273
  }
4890
5274
  const workflowContext = validateWorkflowContext(progression.metadata);
4891
- const sourceReviewRun = getLatestCompletedReviewRunForContext({
4892
- ticketId: owner.id,
4893
- projectId: owner.project_id,
4894
- worktreeBundleId: owner.worktree_bundle_id,
4895
- });
4896
- if (!sourceReviewRun || sourceReviewRun.status !== 'completed') {
5275
+ let existingIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5276
+ const sourceReviewRunId = reconciledSourceReviewRunId ?? existingIteration?.triggering_review_run_id ?? null;
5277
+ if (!sourceReviewRunId) {
4897
5278
  throw new Error('The completed review findings to resume are unavailable');
4898
5279
  }
4899
5280
  const reviewFixWorkflowContext = validateWorkflowContext({
4900
5281
  ...(workflowContext ?? {}),
4901
- review_finding_run_id: sourceReviewRun.id,
5282
+ review_finding_run_id: sourceReviewRunId,
4902
5283
  review_severity: automationConfig.reviewSeverity,
4903
5284
  });
4904
5285
  runAutomationProgression({
@@ -4907,22 +5288,144 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4907
5288
  progressionRunId,
4908
5289
  broadcaster: io,
4909
5290
  });
4910
- upsertBundleAutomationState(result.project.id, bundle.id, {
4911
- review_iteration_count: iteration + 1,
4912
- fix_loop_active: 1,
4913
- });
4914
- const reviewFix = createAutomationReviewFix({
4915
- projectId: result.project.id,
4916
- bundleId: bundle.id,
4917
- ticketId: owner.id,
4918
- ticketTitle: owner.title,
4919
- progressionRunId,
4920
- workflowContext: reviewFixWorkflowContext,
4921
- reviewFindingRunId: sourceReviewRun.id,
4922
- reviewSeverity: automationConfig.reviewSeverity,
4923
- broadcaster: io,
4924
- });
4925
- reviewFixRunId = reviewFix.runId;
5291
+ if (existingIteration?.phase === 'paused') {
5292
+ existingIteration = resumePausedReviewRepairIteration({
5293
+ projectId: result.project.id,
5294
+ iterationId: existingIteration.id,
5295
+ expectedCurrentRunId: existingIteration.resulting_review_execution_run_id
5296
+ ?? existingIteration.gate_batch_run_id
5297
+ ?? existingIteration.action_run_id
5298
+ ?? existingIteration.action_envelope_run_id,
5299
+ broadcaster: io,
5300
+ });
5301
+ }
5302
+ if (existingIteration && ['gate_pending', 'gate_active', 'verification_pending', 'verification_active'].includes(existingIteration.phase)) {
5303
+ await runAutomationReviewGateOrPause({
5304
+ projectId: result.project.id,
5305
+ bundleId: bundle.id,
5306
+ progressionRunId,
5307
+ broadcaster: io,
5308
+ pausedStageRunId: existingIteration.action_run_id,
5309
+ initiation: { workflowContext: reviewFixWorkflowContext },
5310
+ iterationId: existingIteration.id,
5311
+ });
5312
+ return res.json(serializeBundleDetails(result.project, bundle));
5313
+ }
5314
+ if (existingIteration && existingIteration.triggering_review_run_id !== sourceReviewRunId) {
5315
+ throw new Error('The active Review repair iteration does not match the Resume source Review');
5316
+ }
5317
+ if (existingIteration?.phase === 'action_active' && existingIteration.action_run_id) {
5318
+ const actionRun = getExecutionRun(result.project.id, existingIteration.action_run_id);
5319
+ if (actionRun?.status === 'completed') {
5320
+ await runAutomationReviewGateOrPause({
5321
+ projectId: result.project.id,
5322
+ bundleId: bundle.id,
5323
+ progressionRunId,
5324
+ broadcaster: io,
5325
+ pausedStageRunId: actionRun.id,
5326
+ initiation: { workflowContext: reviewFixWorkflowContext },
5327
+ iterationId: existingIteration.id,
5328
+ });
5329
+ return res.json(serializeBundleDetails(result.project, bundle));
5330
+ }
5331
+ if (!actionRun || ['queued', 'running', 'waiting'].includes(actionRun.status)) {
5332
+ throw new Error('Review repair action is still active or unavailable');
5333
+ }
5334
+ retryActionRunId = actionRun.id;
5335
+ }
5336
+ const sourceReviewExecution = existingIteration ? null : db.prepare(`
5337
+ SELECT execution.id
5338
+ FROM execution_runs execution
5339
+ JOIN execution_run_links result_link
5340
+ ON result_link.execution_run_id = execution.id
5341
+ AND result_link.project_id = execution.project_id
5342
+ AND result_link.entity_type = 'review_run'
5343
+ AND result_link.entity_id = ?
5344
+ AND result_link.relationship = 'result'
5345
+ WHERE execution.project_id = ? AND execution.kind = 'review'
5346
+ AND execution.status = 'completed'
5347
+ LIMIT 1
5348
+ `).get(sourceReviewRunId, result.project.id);
5349
+ if (!existingIteration && !sourceReviewExecution) {
5350
+ throw new Error('The completed Review execution to resume is unavailable');
5351
+ }
5352
+ const claimed = withExecutionRunTransaction(transaction => {
5353
+ const repairIteration = existingIteration ?? claimReviewRepairIterationInTransaction(transaction, {
5354
+ projectId: result.project.id,
5355
+ worktreeBundleId: bundle.id,
5356
+ ownerTicketId: owner.id,
5357
+ automationProgressionRunId: progressionRunId,
5358
+ triggeringReviewExecutionRunId: sourceReviewExecution.id,
5359
+ triggeringReviewRunId: sourceReviewRunId,
5360
+ triggeringBlockingCount: countBlockingFindings(listReviewFindings(sourceReviewRunId), automationConfig),
5361
+ effectiveAutomationConfig: automationConfig,
5362
+ decisionCode: 'resume_repair_admitted',
5363
+ });
5364
+ if (!existingIteration) {
5365
+ appendReviewRepairIterationDecisionInTransaction(transaction, {
5366
+ projectId: result.project.id,
5367
+ iterationId: repairIteration.id,
5368
+ decisionPoint: 'resume',
5369
+ effectiveAutomationConfig: automationConfig,
5370
+ decisionCode: 'resume_repair_claimed',
5371
+ });
5372
+ }
5373
+ if (repairIteration.phase === 'action_claimed' && repairIteration.action_envelope_run_id) {
5374
+ const existingEnvelope = getExecutionRun(result.project.id, repairIteration.action_envelope_run_id);
5375
+ if (existingEnvelope && ['queued', 'running', 'waiting'].includes(existingEnvelope.status)) {
5376
+ return { iterationId: repairIteration.id, reviewFixRunId: repairIteration.action_envelope_run_id };
5377
+ }
5378
+ if (!existingEnvelope) {
5379
+ throw new Error('Review repair action envelope is unavailable');
5380
+ }
5381
+ const replacement = createAutomationReviewFixInTransaction(transaction, {
5382
+ projectId: result.project.id,
5383
+ bundleId: bundle.id,
5384
+ ticketId: owner.id,
5385
+ ticketTitle: owner.title,
5386
+ progressionRunId,
5387
+ workflowContext: reviewFixWorkflowContext,
5388
+ triggeringReviewRunId: sourceReviewExecution?.id,
5389
+ reviewFindingRunId: sourceReviewRunId,
5390
+ reviewSeverity: automationConfig.reviewSeverity,
5391
+ retryOfRunId: existingEnvelope.id,
5392
+ });
5393
+ replaceReviewRepairActionEnvelopeRetryInTransaction(transaction, {
5394
+ projectId: result.project.id,
5395
+ iterationId: repairIteration.id,
5396
+ expectedPhase: 'action_claimed',
5397
+ expectedCurrentRunId: existingEnvelope.id,
5398
+ runId: replacement.runId,
5399
+ });
5400
+ return { iterationId: repairIteration.id, reviewFixRunId: replacement.runId };
5401
+ }
5402
+ if (repairIteration.phase === 'action_active' && repairIteration.action_envelope_run_id && retryActionRunId) {
5403
+ return { iterationId: repairIteration.id, reviewFixRunId: repairIteration.action_envelope_run_id };
5404
+ }
5405
+ if (repairIteration.phase !== 'observed') {
5406
+ throw new Error('Review repair iteration is not resumable at action dispatch');
5407
+ }
5408
+ const reviewFix = createAutomationReviewFixInTransaction(transaction, {
5409
+ projectId: result.project.id,
5410
+ bundleId: bundle.id,
5411
+ ticketId: owner.id,
5412
+ ticketTitle: owner.title,
5413
+ progressionRunId,
5414
+ workflowContext: reviewFixWorkflowContext,
5415
+ triggeringReviewRunId: sourceReviewExecution?.id,
5416
+ reviewFindingRunId: sourceReviewRunId,
5417
+ reviewSeverity: automationConfig.reviewSeverity,
5418
+ });
5419
+ attachReviewRepairActionEnvelopeInTransaction(transaction, {
5420
+ projectId: result.project.id,
5421
+ iterationId: repairIteration.id,
5422
+ expectedPhase: 'observed',
5423
+ runId: reviewFix.runId,
5424
+ });
5425
+ return { iterationId: repairIteration.id, reviewFixRunId: reviewFix.runId };
5426
+ }, io, 'immediate');
5427
+ repairIterationId = claimed.iterationId;
5428
+ reviewFixRunId = claimed.reviewFixRunId;
4926
5429
  followUpResult = submitTicketFollowUp({
4927
5430
  ticketId: owner.id,
4928
5431
  requestedProjectId: result.project.id,
@@ -4934,16 +5437,42 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4934
5437
  persistTriggerMessage: false,
4935
5438
  initiation: {
4936
5439
  initiator: 'automation',
4937
- causalParentRunId: reviewFix.runId,
5440
+ causalParentRunId: reviewFixRunId,
5441
+ ...(retryActionRunId ? { retrySourceRunId: retryActionRunId } : {}),
4938
5442
  workflowContext: reviewFixWorkflowContext,
4939
5443
  },
5444
+ onExecutionCreated: (executionContext, transaction) => {
5445
+ if (retryActionRunId) {
5446
+ replaceReviewRepairActionRetryInTransaction(transaction, {
5447
+ projectId: result.project.id,
5448
+ iterationId: repairIterationId,
5449
+ expectedPhase: 'action_active',
5450
+ expectedCurrentRunId: retryActionRunId,
5451
+ runId: executionContext.runId,
5452
+ });
5453
+ }
5454
+ else {
5455
+ attachReviewRepairActionInTransaction(transaction, {
5456
+ projectId: result.project.id,
5457
+ iterationId: repairIterationId,
5458
+ expectedPhase: 'action_claimed',
5459
+ runId: executionContext.runId,
5460
+ });
5461
+ }
5462
+ followUpRunId = executionContext.runId;
5463
+ },
5464
+ onExecutionCompleted: (executionContext, transaction) => {
5465
+ const liveProject = getProjectById(result.project.id) ?? result.project;
5466
+ const liveBundle = getBundle(bundle.id, result.project.id) ?? bundle;
5467
+ completeAutomationReviewFixActionInTransaction(transaction, {
5468
+ projectId: result.project.id,
5469
+ actionRunId: executionContext.runId,
5470
+ effectiveAutomationConfig: resolveAutomationConfig(liveBundle, liveProject),
5471
+ });
5472
+ },
4940
5473
  });
4941
5474
  }
4942
5475
  catch (error) {
4943
- upsertBundleAutomationState(result.project.id, bundle.id, {
4944
- review_iteration_count: previousIteration,
4945
- fix_loop_active: previousFixLoopActive,
4946
- });
4947
5476
  try {
4948
5477
  if (reviewFixRunId) {
4949
5478
  finishAutomationReviewFix({
@@ -4952,6 +5481,22 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4952
5481
  failedMessage: error instanceof Error ? error.message : String(error),
4953
5482
  broadcaster: io,
4954
5483
  });
5484
+ const iteration = repairIterationId
5485
+ ? getActiveReviewRepairIteration(result.project.id, progressionRunId)
5486
+ : null;
5487
+ if (iteration && !['paused', 'terminal'].includes(iteration.phase)) {
5488
+ pauseReviewRepairIteration({
5489
+ projectId: result.project.id,
5490
+ iterationId: iteration.id,
5491
+ expectedPhase: iteration.phase,
5492
+ expectedCurrentRunId: iteration.resulting_review_execution_run_id
5493
+ ?? iteration.gate_batch_run_id
5494
+ ?? iteration.action_run_id
5495
+ ?? iteration.action_envelope_run_id,
5496
+ stopReason: 'action_failed',
5497
+ broadcaster: io,
5498
+ });
5499
+ }
4955
5500
  }
4956
5501
  }
4957
5502
  finally {
@@ -4960,10 +5505,6 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4960
5505
  return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
4961
5506
  }
4962
5507
  if (!followUpResult.ok) {
4963
- upsertBundleAutomationState(result.project.id, bundle.id, {
4964
- review_iteration_count: previousIteration,
4965
- fix_loop_active: previousFixLoopActive,
4966
- });
4967
5508
  if (reviewFixRunId) {
4968
5509
  finishAutomationReviewFix({
4969
5510
  projectId: result.project.id,
@@ -4981,6 +5522,16 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4981
5522
  pausedStageRunId: reviewFixRunId,
4982
5523
  broadcaster: io,
4983
5524
  });
5525
+ if (repairIterationId && followUpRunId) {
5526
+ pauseReviewRepairIteration({
5527
+ projectId: result.project.id,
5528
+ iterationId: repairIterationId,
5529
+ expectedPhase: 'action_active',
5530
+ expectedCurrentRunId: followUpRunId,
5531
+ stopReason: 'action_failed',
5532
+ broadcaster: io,
5533
+ });
5534
+ }
4984
5535
  }
4985
5536
  return res.status(followUpResult.status).json({ error: followUpResult.error });
4986
5537
  }
@@ -4990,6 +5541,27 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4990
5541
  if (!revalidateDecision()) {
4991
5542
  return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
4992
5543
  }
5544
+ if (decision.outcome === 'continue_commit_chain') {
5545
+ const activeIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5546
+ if (activeIteration && activeIteration.phase !== 'terminal') {
5547
+ withExecutionRunTransaction(transaction => {
5548
+ terminalizeReviewRepairIterationInTransaction(transaction, {
5549
+ projectId: result.project.id,
5550
+ iterationId: activeIteration.id,
5551
+ expectedPhase: activeIteration.phase,
5552
+ expectedCurrentRunId: activeIteration.resulting_review_execution_run_id
5553
+ ?? activeIteration.gate_batch_run_id
5554
+ ?? activeIteration.action_run_id
5555
+ ?? activeIteration.action_envelope_run_id,
5556
+ outcome: 'interrupted',
5557
+ stopReason: 'configuration_changed',
5558
+ });
5559
+ }, io, 'immediate');
5560
+ }
5561
+ if (bundleState.fix_loop_active === 1) {
5562
+ resetBundleAutomationLoop(result.project.id, bundle.id);
5563
+ }
5564
+ }
4993
5565
  runAutomationProgression({
4994
5566
  projectId: result.project.id,
4995
5567
  bundleId: bundle.id,
@@ -5010,6 +5582,22 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5010
5582
  return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
5011
5583
  }
5012
5584
  const sourceRunId = bundleState.automation_paused_stage_run_id;
5585
+ let repairIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5586
+ if (repairIteration?.phase === 'paused') {
5587
+ repairIteration = resumePausedReviewRepairIteration({
5588
+ projectId: result.project.id,
5589
+ iterationId: repairIteration.id,
5590
+ expectedCurrentRunId: repairIteration.resulting_review_execution_run_id
5591
+ ?? repairIteration.gate_batch_run_id
5592
+ ?? repairIteration.action_run_id
5593
+ ?? repairIteration.action_envelope_run_id,
5594
+ broadcaster: io,
5595
+ });
5596
+ }
5597
+ const verificationRetryRunId = repairIteration?.phase === 'verification_active'
5598
+ ? repairIteration.resulting_review_execution_run_id
5599
+ : null;
5600
+ let replacementVerificationRunId = null;
5013
5601
  const reviewResult = rerunTicketReviewOperation(owner, io, {
5014
5602
  automatic: true,
5015
5603
  initiation: {
@@ -5018,8 +5606,30 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5018
5606
  },
5019
5607
  continuation: sourceRunId ? resolveReviewContinuation(sourceRunId) : undefined,
5020
5608
  accept: acceptResumedReview,
5609
+ onExecutionCreated: verificationRetryRunId && repairIteration
5610
+ ? (executionContext, transaction) => {
5611
+ replaceReviewRepairVerificationRetryInTransaction(transaction, {
5612
+ projectId: result.project.id,
5613
+ iterationId: repairIteration.id,
5614
+ expectedPhase: 'verification_active',
5615
+ expectedCurrentRunId: verificationRetryRunId,
5616
+ runId: executionContext.runId,
5617
+ });
5618
+ replacementVerificationRunId = executionContext.runId;
5619
+ }
5620
+ : undefined,
5021
5621
  });
5022
5622
  if (!reviewResult.ok) {
5623
+ if (repairIteration && replacementVerificationRunId) {
5624
+ pauseReviewRepairIteration({
5625
+ projectId: result.project.id,
5626
+ iterationId: repairIteration.id,
5627
+ expectedPhase: 'verification_active',
5628
+ expectedCurrentRunId: replacementVerificationRunId,
5629
+ stopReason: 'verification_failed',
5630
+ broadcaster: io,
5631
+ });
5632
+ }
5023
5633
  return res.status(reviewResult.status).json({ error: reviewResult.error });
5024
5634
  }
5025
5635
  }
@@ -5090,6 +5700,21 @@ app.post('/bundles/:id/dismiss-automation-pause', async (req, res) => {
5090
5700
  catch {
5091
5701
  // Explicit abandonment also repairs a stale progression pointer.
5092
5702
  }
5703
+ const repairIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5704
+ if (repairIteration) {
5705
+ terminalizeReviewRepairIteration({
5706
+ projectId: result.project.id,
5707
+ iterationId: repairIteration.id,
5708
+ expectedPhase: repairIteration.phase,
5709
+ expectedCurrentRunId: repairIteration.resulting_review_execution_run_id
5710
+ ?? repairIteration.gate_batch_run_id
5711
+ ?? repairIteration.action_run_id
5712
+ ?? repairIteration.action_envelope_run_id,
5713
+ outcome: 'cancelled',
5714
+ stopReason: 'cancelled',
5715
+ broadcaster: io,
5716
+ });
5717
+ }
5093
5718
  }
5094
5719
  abandonAutomationPause({
5095
5720
  projectId: result.project.id,
@@ -5114,6 +5739,7 @@ app.post('/tool', (req, res) => {
5114
5739
  return res.status(400).json({ error: 'Invalid tool' });
5115
5740
  }
5116
5741
  config.tool = tool;
5742
+ io.emit('tool:updated', { tool });
5117
5743
  res.json({ tool });
5118
5744
  });
5119
5745
  app.post('/tools/upgrade', async (req, res) => {
@@ -5141,15 +5767,21 @@ app.post('/projects/auto-config', async (req, res) => {
5141
5767
  return res.status(400).json({ error: 'repo_path must be a non-empty string' });
5142
5768
  }
5143
5769
  try {
5144
- const result = await autoConfigProject(validation.values.repo_path, validation.values.helper_model
5145
- ? {
5146
- tool: 'opencode',
5147
- model: validation.values.helper_model,
5148
- variant: validation.values.helper_variant ?? null,
5149
- }
5150
- : {
5151
- tool: config.tool ?? 'opencode',
5152
- });
5770
+ const requestedTool = req.body?.tool;
5771
+ if (requestedTool !== undefined && requestedTool !== 'claude' && requestedTool !== 'opencode') {
5772
+ return res.status(400).json({ error: 'tool must be a supported agent' });
5773
+ }
5774
+ const tool = requestedTool
5775
+ ?? (hasOwn(req.body ?? {}, 'helper_model') && validation.values.helper_model ? 'opencode' : null)
5776
+ ?? config.tool
5777
+ ?? 'opencode';
5778
+ const helperConfig = resolveHelperModelConfig({
5779
+ helperModelsJson: validation.values.helper_models_json,
5780
+ agent: tool,
5781
+ });
5782
+ const result = await autoConfigProject(validation.values.repo_path, helperConfig.model
5783
+ ? { tool, model: helperConfig.model, variant: helperConfig.variant }
5784
+ : { tool });
5153
5785
  return res.json(result);
5154
5786
  }
5155
5787
  catch (error) {
@@ -5229,6 +5861,10 @@ app.get('/models', async (req, res) => {
5229
5861
  timing.end({ count: models.length });
5230
5862
  return res.json(models);
5231
5863
  });
5864
+ app.get('/agents', async (_req, res) => {
5865
+ res.set('Cache-Control', 'no-store');
5866
+ return res.json(await getSupportedAgentCatalog());
5867
+ });
5232
5868
  app.get('/skills', async (req, res) => {
5233
5869
  const tool = typeof req.query.tool === 'string' ? req.query.tool : '';
5234
5870
  const projectId = typeof req.query.project_id === 'string' ? req.query.project_id : null;
@@ -6178,13 +6814,18 @@ app.get('/projects', (_req, res) => {
6178
6814
  const projects = listProjects();
6179
6815
  res.json(projects.map(serializeProject));
6180
6816
  });
6181
- app.post('/projects', (req, res) => {
6182
- const validation = validateProjectPayload(asProjectPayload(req.body), { allowRepoInitialization: true });
6817
+ app.post('/projects', async (req, res) => {
6818
+ const body = asProjectPayload(req.body);
6819
+ const validation = validateProjectPayload(body, { allowRepoInitialization: true });
6183
6820
  if ('error' in validation) {
6184
6821
  return res.status(400).json({ error: validation.error });
6185
6822
  }
6823
+ const roleModelValidation = await validateProjectRoleModelDiscovery(body, validation.values);
6824
+ if ('error' in roleModelValidation) {
6825
+ return res.status(400).json({ error: roleModelValidation.error });
6826
+ }
6186
6827
  const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
6187
- const { name, repo_path, build_execution_provider, linked_project_ids, run_setup, run_services, check_definitions, required_check_definition_ids, pre_review_check_definition_ids, post_build_check_definition_ids, auto_fix_check_definition_ids, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, reviewer_model, reviewer_variant, commit_message_rules, commit_after_each_ticket, auto_park_stale_tickets, memory_enabled, scan_include_globs_json, scan_exclude_globs_json, worktree_sync, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_pause_on_open_questions, automation_review_severity, automation_review_max_iterations, } = validation.values;
6828
+ const { name, repo_path, build_execution_provider, linked_project_ids, run_setup, run_services, check_definitions, required_check_definition_ids, pre_review_check_definition_ids, post_build_check_definition_ids, auto_fix_check_definition_ids, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, helper_models_json, reviewer_model, reviewer_variant, reviewer_models_json, commit_message_rules, commit_after_each_ticket, auto_park_stale_tickets, memory_enabled, scan_include_globs_json, scan_exclude_globs_json, worktree_sync, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_pause_on_open_questions, automation_review_severity, automation_review_max_iterations, } = validation.values;
6188
6829
  if (!repo_path) {
6189
6830
  return res.status(400).json({ error: 'repo_path must be a non-empty string' });
6190
6831
  }
@@ -6217,8 +6858,10 @@ app.post('/projects', (req, res) => {
6217
6858
  preview_capability_mode,
6218
6859
  helper_model,
6219
6860
  helper_variant,
6861
+ helper_models_json,
6220
6862
  reviewer_model,
6221
6863
  reviewer_variant,
6864
+ reviewer_models_json,
6222
6865
  commit_message_rules,
6223
6866
  commit_after_each_ticket,
6224
6867
  auto_park_stale_tickets,
@@ -6233,8 +6876,8 @@ app.post('/projects', (req, res) => {
6233
6876
  automation_review_severity,
6234
6877
  automation_review_max_iterations
6235
6878
  )
6236
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6237
- `).run(id, name, normalizedRepoPath, build_execution_provider ?? 'host', run_setup, run_services, check_definitions, required_check_definition_ids ?? '[]', pre_review_check_definition_ids ?? '[]', post_build_check_definition_ids ?? '[]', auto_fix_check_definition_ids ?? '[]', test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, reviewer_model ?? null, reviewer_model ? reviewer_variant ?? null : null, commit_message_rules ?? null, commit_after_each_ticket ?? 0, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, scan_include_globs_json ?? null, scan_exclude_globs_json ?? null, worktree_sync ?? null, spec_aware_code_review_enabled ?? 0, automation_depth ?? 'manual', automation_review_fix_enabled ?? 0, automation_pause_on_open_questions ?? 0, automation_review_severity ?? 'medium', automation_review_max_iterations ?? 3);
6879
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
6880
+ `).run(id, name, normalizedRepoPath, build_execution_provider ?? 'host', run_setup, run_services, check_definitions, required_check_definition_ids ?? '[]', pre_review_check_definition_ids ?? '[]', post_build_check_definition_ids ?? '[]', auto_fix_check_definition_ids ?? '[]', test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, helper_models_json ?? '{}', reviewer_model ?? null, reviewer_model ? reviewer_variant ?? null : null, reviewer_models_json ?? '{}', commit_message_rules ?? null, commit_after_each_ticket ?? 0, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, scan_include_globs_json ?? null, scan_exclude_globs_json ?? null, worktree_sync ?? null, spec_aware_code_review_enabled ?? 0, automation_depth ?? 'manual', automation_review_fix_enabled ?? 0, automation_pause_on_open_questions ?? 0, automation_review_severity ?? 'medium', automation_review_max_iterations ?? 3);
6238
6881
  replaceProjectLinks(id, linked_project_ids ?? []);
6239
6882
  ensureProjectRootBundle(id);
6240
6883
  if (projectCount === 0) {
@@ -7240,15 +7883,12 @@ app.post('/projects/:id/git-commit-message', async (req, res) => {
7240
7883
  diff: getSelectedGitUnifiedDiff(workspace.cwd, selectedPaths),
7241
7884
  });
7242
7885
  try {
7243
- const rawMessage = await runLightweightPrompt(workspace.project.helper_model
7244
- ? {
7245
- tool: 'opencode',
7246
- model: workspace.project.helper_model,
7247
- variant: workspace.project.helper_variant,
7248
- }
7249
- : {
7250
- tool: config.tool ?? 'opencode',
7251
- }, prompt, workspace.cwd);
7886
+ const helperTool = config.tool ?? 'opencode';
7887
+ const helperConfig = resolveHelperModelConfig({
7888
+ helperModelsJson: workspace.project.helper_models_json,
7889
+ agent: helperTool,
7890
+ });
7891
+ const rawMessage = await runLightweightPrompt({ tool: helperTool, ...helperConfig }, prompt, workspace.cwd);
7252
7892
  const message = normalizeGeneratedCommitMessage(rawMessage);
7253
7893
  if (!message) {
7254
7894
  return res.status(500).json({ error: 'Unable to generate a commit message right now.' });
@@ -7271,7 +7911,12 @@ app.post('/projects/:id/pull-request-description', async (req, res) => {
7271
7911
  if (workspace.target_kind !== 'repo_root') {
7272
7912
  return res.status(409).json({ error: 'Pull request descriptions for bundle targets should use the bundle route.' });
7273
7913
  }
7274
- if (!project.helper_model) {
7914
+ const helperTool = config.tool ?? 'opencode';
7915
+ const helperConfig = resolveHelperModelConfig({
7916
+ helperModelsJson: project.helper_models_json,
7917
+ agent: helperTool,
7918
+ });
7919
+ if (!helperConfig.model) {
7275
7920
  return res.status(409).json({ error: 'Set a helper model in project settings before generating a PR description.' });
7276
7921
  }
7277
7922
  const title = asTrimmedString(req.body?.title);
@@ -7283,9 +7928,9 @@ app.post('/projects/:id/pull-request-description', async (req, res) => {
7283
7928
  : [];
7284
7929
  try {
7285
7930
  const rawDescription = await runLightweightPrompt({
7286
- tool: 'opencode',
7287
- model: project.helper_model,
7288
- variant: project.helper_variant,
7931
+ tool: helperTool,
7932
+ model: helperConfig.model,
7933
+ variant: helperConfig.variant,
7289
7934
  }, buildPullRequestDescriptionPrompt({
7290
7935
  title,
7291
7936
  ticketTitles: [],
@@ -7444,7 +8089,7 @@ app.post('/projects/:id/git-squash', async (req, res) => {
7444
8089
  total_count: summary.totalCount,
7445
8090
  });
7446
8091
  });
7447
- app.patch('/projects/:id', (req, res) => {
8092
+ app.patch('/projects/:id', async (req, res) => {
7448
8093
  const project = getProjectById(req.params.id);
7449
8094
  if (!project) {
7450
8095
  return res.status(404).json({ error: 'Not found' });
@@ -7455,10 +8100,15 @@ app.patch('/projects/:id', (req, res) => {
7455
8100
  reason: 'execution_profile_required',
7456
8101
  });
7457
8102
  }
7458
- const validation = validateProjectPayload(asProjectPayload(req.body), { partial: true, currentProjectId: project.id });
8103
+ const body = asProjectPayload(req.body);
8104
+ const validation = validateProjectPayload(body, { partial: true, currentProjectId: project.id });
7459
8105
  if ('error' in validation) {
7460
8106
  return res.status(400).json({ error: validation.error });
7461
8107
  }
8108
+ const roleModelValidation = await validateProjectRoleModelDiscovery(body, validation.values, project);
8109
+ if ('error' in roleModelValidation) {
8110
+ return res.status(400).json({ error: roleModelValidation.error });
8111
+ }
7462
8112
  if (hasOwn(validation.values, 'repo_path') && validation.values.repo_path !== project.repo_path) {
7463
8113
  const stats = getProjectTicketStats(project.id);
7464
8114
  if (stats.ticketCount > 0 || stats.worktreeCount > 0 || stats.bundleCount > 0) {
@@ -7467,7 +8117,7 @@ app.patch('/projects/:id', (req, res) => {
7467
8117
  });
7468
8118
  }
7469
8119
  }
7470
- const { name, repo_path, linked_project_ids, run_setup, run_services, check_definitions, required_check_definition_ids, pre_review_check_definition_ids, post_build_check_definition_ids, auto_fix_check_definition_ids, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, reviewer_model, reviewer_variant, commit_message_rules, commit_after_each_ticket, auto_park_stale_tickets, memory_enabled, scan_include_globs_json, scan_exclude_globs_json, worktree_sync, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_pause_on_open_questions, automation_review_severity, automation_review_max_iterations, } = validation.values;
8120
+ const { name, repo_path, linked_project_ids, run_setup, run_services, check_definitions, required_check_definition_ids, pre_review_check_definition_ids, post_build_check_definition_ids, auto_fix_check_definition_ids, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, helper_models_json, reviewer_model, reviewer_variant, reviewer_models_json, commit_message_rules, commit_after_each_ticket, auto_park_stale_tickets, memory_enabled, scan_include_globs_json, scan_exclude_globs_json, worktree_sync, spec_aware_code_review_enabled, automation_depth, automation_review_fix_enabled, automation_pause_on_open_questions, automation_review_severity, automation_review_max_iterations, } = validation.values;
7471
8121
  const updateProject = db.prepare(`
7472
8122
  UPDATE projects SET
7473
8123
  name = ?,
@@ -7487,8 +8137,10 @@ app.patch('/projects/:id', (req, res) => {
7487
8137
  preview_capability_mode = ?,
7488
8138
  helper_model = ?,
7489
8139
  helper_variant = ?,
8140
+ helper_models_json = ?,
7490
8141
  reviewer_model = ?,
7491
8142
  reviewer_variant = ?,
8143
+ reviewer_models_json = ?,
7492
8144
  commit_message_rules = ?,
7493
8145
  commit_after_each_ticket = ?,
7494
8146
  auto_park_stale_tickets = ?,
@@ -7523,6 +8175,13 @@ app.patch('/projects/:id', (req, res) => {
7523
8175
  ? (hasOwn(validation.values, 'reviewer_variant') ? reviewer_variant ?? null : project.reviewer_variant)
7524
8176
  : null;
7525
8177
  const updateResult = db.transaction(() => {
8178
+ const currentProject = getProjectById(project.id);
8179
+ if (!currentProject || JSON.stringify(currentProject) !== JSON.stringify(project)) {
8180
+ return {
8181
+ error: 'Project changed while model configuration was being validated; retry the update',
8182
+ status: 409,
8183
+ };
8184
+ }
7526
8185
  const policyReferences = validateProjectRequiredCheckPolicyReferences({
7527
8186
  projectId: project.id,
7528
8187
  checkDefinitionsJson: nextCheckDefinitions,
@@ -7549,12 +8208,12 @@ app.patch('/projects/:id', (req, res) => {
7549
8208
  };
7550
8209
  }
7551
8210
  }
7552
- updateProject.run(name ?? project.name, repo_path ?? project.repo_path, project.build_execution_provider, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, nextCheckDefinitions, nextRequiredCheckDefinitionIds, nextPreReviewCheckDefinitionIds, nextPostBuildCheckDefinitionIds, nextAutoFixCheckDefinitionIds, hasOwn(validation.values, 'test_commands') ? test_commands : project.test_commands, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, nextReviewerModel, nextReviewerVariant, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'commit_after_each_ticket') ? commit_after_each_ticket : project.commit_after_each_ticket, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'scan_include_globs_json') ? scan_include_globs_json : project.scan_include_globs_json, hasOwn(validation.values, 'scan_exclude_globs_json') ? scan_exclude_globs_json : project.scan_exclude_globs_json, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, hasOwn(validation.values, 'spec_aware_code_review_enabled') ? spec_aware_code_review_enabled : project.spec_aware_code_review_enabled, hasOwn(validation.values, 'automation_depth') ? automation_depth : project.automation_depth, hasOwn(validation.values, 'automation_review_fix_enabled') ? automation_review_fix_enabled : project.automation_review_fix_enabled, hasOwn(validation.values, 'automation_pause_on_open_questions') ? automation_pause_on_open_questions : project.automation_pause_on_open_questions, hasOwn(validation.values, 'automation_review_severity') ? automation_review_severity : project.automation_review_severity, hasOwn(validation.values, 'automation_review_max_iterations') ? automation_review_max_iterations : project.automation_review_max_iterations, req.params.id);
8211
+ updateProject.run(name ?? project.name, repo_path ?? project.repo_path, project.build_execution_provider, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, nextCheckDefinitions, nextRequiredCheckDefinitionIds, nextPreReviewCheckDefinitionIds, nextPostBuildCheckDefinitionIds, nextAutoFixCheckDefinitionIds, hasOwn(validation.values, 'test_commands') ? test_commands : project.test_commands, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, hasOwn(validation.values, 'helper_models_json') ? helper_models_json : project.helper_models_json, nextReviewerModel, nextReviewerVariant, hasOwn(validation.values, 'reviewer_models_json') ? reviewer_models_json : project.reviewer_models_json, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'commit_after_each_ticket') ? commit_after_each_ticket : project.commit_after_each_ticket, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'scan_include_globs_json') ? scan_include_globs_json : project.scan_include_globs_json, hasOwn(validation.values, 'scan_exclude_globs_json') ? scan_exclude_globs_json : project.scan_exclude_globs_json, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, hasOwn(validation.values, 'spec_aware_code_review_enabled') ? spec_aware_code_review_enabled : project.spec_aware_code_review_enabled, hasOwn(validation.values, 'automation_depth') ? automation_depth : project.automation_depth, hasOwn(validation.values, 'automation_review_fix_enabled') ? automation_review_fix_enabled : project.automation_review_fix_enabled, hasOwn(validation.values, 'automation_pause_on_open_questions') ? automation_pause_on_open_questions : project.automation_pause_on_open_questions, hasOwn(validation.values, 'automation_review_severity') ? automation_review_severity : project.automation_review_severity, hasOwn(validation.values, 'automation_review_max_iterations') ? automation_review_max_iterations : project.automation_review_max_iterations, req.params.id);
7553
8212
  if (hasOwn(validation.values, 'linked_project_ids')) {
7554
8213
  replaceProjectLinks(project.id, linked_project_ids ?? []);
7555
8214
  }
7556
8215
  return { ok: true };
7557
- })();
8216
+ }).immediate();
7558
8217
  if ('error' in updateResult) {
7559
8218
  const status = 'status' in updateResult && typeof updateResult.status === 'number'
7560
8219
  ? updateResult.status
@@ -7643,6 +8302,13 @@ const completeClaimedProjectDeletion = async (projectId, claimId) => {
7643
8302
  }
7644
8303
  db.prepare("DELETE FROM execution_artifacts WHERE project_id = ? AND availability = 'deleted'").run(projectId);
7645
8304
  deleteProjectAttachments(projectId);
8305
+ if (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'review_repair_iterations'").get()) {
8306
+ db.prepare(`
8307
+ DELETE FROM review_repair_iteration_decisions
8308
+ WHERE iteration_id IN (SELECT id FROM review_repair_iterations WHERE project_id = ?)
8309
+ `).run(projectId);
8310
+ db.prepare('DELETE FROM review_repair_iterations WHERE project_id = ?').run(projectId);
8311
+ }
7646
8312
  db.prepare(`
7647
8313
  DELETE FROM check_gate_evaluation_attempts
7648
8314
  WHERE evaluation_id IN (SELECT id FROM check_gate_evaluations WHERE project_id = ?)
@@ -8147,6 +8813,13 @@ app.post('/tickets/:id/retry', (req, res) => {
8147
8813
  return res.status(409).json({ error: buildBlocker.error, blocker_ticket_id: buildBlocker.blocker_ticket_id });
8148
8814
  }
8149
8815
  }
8816
+ const postBuildRecovery = startPostBuildCheckRecovery(ticket, io);
8817
+ if (postBuildRecovery) {
8818
+ if (!postBuildRecovery.accepted) {
8819
+ return res.status(postBuildRecovery.reason === 'queue_failed' ? 503 : 409).json({ error: postBuildRecovery.error });
8820
+ }
8821
+ return res.json(getTicket(req.params.id));
8822
+ }
8150
8823
  const retrySource = ticket.project_id
8151
8824
  ? db.prepare(`
8152
8825
  SELECT er.id
@@ -8476,15 +9149,12 @@ app.post('/tickets/:id/git-commit-message', async (req, res) => {
8476
9149
  diff: getSelectedGitUnifiedDiff(workspace.cwd, selectedPaths),
8477
9150
  });
8478
9151
  try {
8479
- const rawMessage = await runLightweightPrompt(workspace.project.helper_model
8480
- ? {
8481
- tool: 'opencode',
8482
- model: workspace.project.helper_model,
8483
- variant: workspace.project.helper_variant,
8484
- }
8485
- : {
8486
- tool: config.tool ?? 'opencode',
8487
- }, prompt, workspace.cwd);
9152
+ const helperTool = resolveTicketTool(ticket, config.tool ?? 'opencode');
9153
+ const helperConfig = resolveHelperModelConfig({
9154
+ helperModelsJson: workspace.project.helper_models_json,
9155
+ agent: helperTool,
9156
+ });
9157
+ const rawMessage = await runLightweightPrompt({ tool: helperTool, ...helperConfig }, prompt, workspace.cwd);
8488
9158
  const message = normalizeGeneratedCommitMessage(rawMessage);
8489
9159
  if (!message) {
8490
9160
  return res.status(500).json({ error: 'Unable to generate a commit message right now.' });