@straiffi/archon 1.7.10 → 1.8.1

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 (162) hide show
  1. package/dist/client/assets/{ActivityView-IUWr92uj.js → ActivityView-CbdDNsdO.js} +1 -1
  2. package/dist/client/assets/{GitHubActionsDialog-0YtWvbFk.js → GitHubActionsDialog-Cmb9Yebr.js} +1 -1
  3. package/dist/client/assets/VerificationDialog-YUi8g2ou.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-LLMyOR-6.js → architectureDiagram-3BPJPVTR-BgoQ_BIc.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-aa1HitBJ.js → blockDiagram-GPEHLZMM-Xdp2x8BW.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-BpSt5sdj.js → c4Diagram-AAUBKEIU-B220zS5a.js} +1 -1
  7. package/dist/client/assets/channel-CZ1HsfsD.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-D8pf3RNr.js → chunk-2J33WTMH-tYM2mjFx.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-_yx1N2iT.js → chunk-3OPIFGDE-CTITOUvV.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-CYURGSbL.js → chunk-5ZQYHXKU-D45jJDNN.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-Ci2Mu1zz.js → chunk-727SXJPM-X90EdJX4.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CqVgrKze.js → chunk-AQP2D5EJ-PEoVUnQP.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-CT8aPk-o.js → chunk-CSCIHK7Q-CLWG2R5F.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-yM1i0J-p.js → chunk-KSCS5N6A-BamDWXh_.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-BKMGr31Z.js → chunk-L5ZTLDWV-05TaQDcG.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-A7oUDETP.js → chunk-LZXEDZCA-CjsbWN3c.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-Cagc-XAG.js → chunk-ND2GUHAM-BVmPBEeD.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-pe8tKLWI.js → chunk-NZK2D7GU-DNZ6WouC.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-BahwghWW.js → chunk-O5CBEL6O-Dfonikgz.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-BfSAFufr.js → chunk-WU5MYG2G-CyiTxcGh.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-D4DcLL7k.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Benwuous.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DCma6-hS.js → dagre-BM42HDAG-9jv2zUKk.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DUeggZrO.js → diagram-2AECGRRQ-DmF0Td45.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CdjOKGtl.js → diagram-5GNKFQAL-DJNMQ7tI.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-CYjXhgh_.js → diagram-KO2AKTUF-EfcoL9E4.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-DYBCh4cm.js → diagram-LMA3HP47-C77Lzbhk.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-D3HPQt3z.js → diagram-OG6HWLK6-7GPyDhpX.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-mG8Yztg-.js → erDiagram-TEJ5UH35-B-mOpWb2.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-4FvjuoSZ.js → flowDiagram-I6XJVG4X-Bbte_qNo.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-ByCQI8ft.js → ganttDiagram-6RSMTGT7-DC4ltCss.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CzEUA7nB.js → gitGraphDiagram-PVQCEYII-4F87Sxzr.js} +1 -1
  33. package/dist/client/assets/index-D4qr0sQx.css +2 -0
  34. package/dist/client/assets/index-DCCSprul.js +274 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-Q9CqnXO1.js → infoDiagram-5YYISTIA-CHQaSUgi.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CO8BToGf.js → ishikawaDiagram-YF4QCWOH-CuXLE_L1.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-DtOyWvDJ.js → journeyDiagram-JHISSGLW-Dkj3B7DF.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-DIn9bYpk.js → kanban-definition-UN3LZRKU-DAH_WodG.js} +1 -1
  39. package/dist/client/assets/{line-H-k0M9_s.js → line-CxewTz3a.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-CZOUS5zK.js → mermaid-parser.core-W88fOlNl.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-Ck71Qr7h.js → mermaid.core-CjJzzlTK.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BRFEcJw_.js → mindmap-definition-RKZ34NQL-BdCMvjnB.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-CwihXXcJ.js → pieDiagram-4H26LBE5-HeTRmhyt.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-moiRw4Cm.js → quadrantDiagram-W4KKPZXB-CLXv5YTL.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-tshvM_vV.js → requirementDiagram-4Y6WPE33-DB4DS6CS.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-rtKIuBEp.js → sankeyDiagram-5OEKKPKP-BjUVvwEU.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-lkTCo4wE.js → sequenceDiagram-3UESZ5HK-FH39XKuz.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-CFQhVUSd.js → stateDiagram-AJRCARHV-U8DIBjJj.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Cw8SkdDx.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BmlQ7Wiy.js → timeline-definition-PNZ67QCA-cuO_PRfp.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CZyZepcT.js → vennDiagram-CIIHVFJN-BR7Vk6hi.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-C2b52gni.js → wardleyDiagram-YWT4CUSO-C443pGVk.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-FqnROUHq.js → xychartDiagram-2RQKCTM6-BtjwBwmu.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +503 -9
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +374 -201
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/activeAutomationChild.js +6 -1
  60. package/dist/server/lib/activeAutomationChild.js.map +1 -1
  61. package/dist/server/lib/automationCheckGate.js +183 -50
  62. package/dist/server/lib/automationCheckGate.js.map +1 -1
  63. package/dist/server/lib/automationProgression.js +76 -9
  64. package/dist/server/lib/automationProgression.js.map +1 -1
  65. package/dist/server/lib/automationReconciliation.js +65 -0
  66. package/dist/server/lib/automationReconciliation.js.map +1 -1
  67. package/dist/server/lib/automationResumeDecision.js +69 -32
  68. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  69. package/dist/server/lib/automationReviewGate.js +6 -1
  70. package/dist/server/lib/automationReviewGate.js.map +1 -1
  71. package/dist/server/lib/bundleActivity.js +20 -0
  72. package/dist/server/lib/bundleActivity.js.map +1 -1
  73. package/dist/server/lib/bundleReviewOperations.js +36 -5
  74. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  75. package/dist/server/lib/chatOperations.js +9 -0
  76. package/dist/server/lib/chatOperations.js.map +1 -1
  77. package/dist/server/lib/chatTicketProposals.js +2 -1
  78. package/dist/server/lib/chatTicketProposals.js.map +1 -1
  79. package/dist/server/lib/chats.js +2 -0
  80. package/dist/server/lib/chats.js.map +1 -1
  81. package/dist/server/lib/checkDiagnosticParsers.js +301 -0
  82. package/dist/server/lib/checkDiagnosticParsers.js.map +1 -0
  83. package/dist/server/lib/checkDiagnostics.js +142 -0
  84. package/dist/server/lib/checkDiagnostics.js.map +1 -0
  85. package/dist/server/lib/checkGateEvaluations.js +89 -1
  86. package/dist/server/lib/checkGateEvaluations.js.map +1 -1
  87. package/dist/server/lib/checkRepair.js +1031 -0
  88. package/dist/server/lib/checkRepair.js.map +1 -0
  89. package/dist/server/lib/checkRepairLifecycle.js +56 -0
  90. package/dist/server/lib/checkRepairLifecycle.js.map +1 -0
  91. package/dist/server/lib/checkRepairReconciliation.js +145 -0
  92. package/dist/server/lib/checkRepairReconciliation.js.map +1 -0
  93. package/dist/server/lib/checkRunner.js +122 -23
  94. package/dist/server/lib/checkRunner.js.map +1 -1
  95. package/dist/server/lib/checkRuns.js +60 -6
  96. package/dist/server/lib/checkRuns.js.map +1 -1
  97. package/dist/server/lib/executionReconciliation.js +21 -11
  98. package/dist/server/lib/executionReconciliation.js.map +1 -1
  99. package/dist/server/lib/executionRetryPredicates.js +3 -1
  100. package/dist/server/lib/executionRetryPredicates.js.map +1 -1
  101. package/dist/server/lib/executionRunRetention.js +24 -3
  102. package/dist/server/lib/executionRunRetention.js.map +1 -1
  103. package/dist/server/lib/executionRuns.js +15 -3
  104. package/dist/server/lib/executionRuns.js.map +1 -1
  105. package/dist/server/lib/git.js +57 -7
  106. package/dist/server/lib/git.js.map +1 -1
  107. package/dist/server/lib/planModeQuestions.js +2 -0
  108. package/dist/server/lib/planModeQuestions.js.map +1 -1
  109. package/dist/server/lib/planQuestions.js +39 -7
  110. package/dist/server/lib/planQuestions.js.map +1 -1
  111. package/dist/server/lib/postBuildCheckRuns.js +43 -0
  112. package/dist/server/lib/postBuildCheckRuns.js.map +1 -0
  113. package/dist/server/lib/postBuildChecks.js +122 -0
  114. package/dist/server/lib/postBuildChecks.js.map +1 -0
  115. package/dist/server/lib/projectMemorySuggestions.js +2 -1
  116. package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
  117. package/dist/server/lib/projects.js +24 -25
  118. package/dist/server/lib/projects.js.map +1 -1
  119. package/dist/server/lib/pullRequestContent.js +14 -0
  120. package/dist/server/lib/pullRequestContent.js.map +1 -1
  121. package/dist/server/lib/requiredCheckPolicy.js +13 -49
  122. package/dist/server/lib/requiredCheckPolicy.js.map +1 -1
  123. package/dist/server/lib/requiredCheckPolicyPrompt.js +46 -0
  124. package/dist/server/lib/requiredCheckPolicyPrompt.js.map +1 -0
  125. package/dist/server/lib/specDraft.js +6 -0
  126. package/dist/server/lib/specDraft.js.map +1 -1
  127. package/dist/server/lib/specResources.js +42 -0
  128. package/dist/server/lib/specResources.js.map +1 -0
  129. package/dist/server/lib/specs.js +9 -5
  130. package/dist/server/lib/specs.js.map +1 -1
  131. package/dist/server/lib/ticketAutomation.js +10 -3
  132. package/dist/server/lib/ticketAutomation.js.map +1 -1
  133. package/dist/server/lib/ticketAutomationCommit.js +36 -11
  134. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  135. package/dist/server/lib/ticketFollowUpOperations.js +33 -9
  136. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  137. package/dist/server/lib/tickets.js +11 -32
  138. package/dist/server/lib/tickets.js.map +1 -1
  139. package/dist/server/lib/types.js +1 -0
  140. package/dist/server/lib/types.js.map +1 -1
  141. package/dist/server/workers/build.js +42 -12
  142. package/dist/server/workers/build.js.map +1 -1
  143. package/dist/server/workers/chat.js +18 -3
  144. package/dist/server/workers/chat.js.map +1 -1
  145. package/dist/server/workers/followUp.js +70 -17
  146. package/dist/server/workers/followUp.js.map +1 -1
  147. package/dist/server/workers/plan.js +5 -3
  148. package/dist/server/workers/plan.js.map +1 -1
  149. package/dist/server/workers/planFollowUp.js +5 -6
  150. package/dist/server/workers/planFollowUp.js.map +1 -1
  151. package/dist/server/workers/review.js +38 -6
  152. package/dist/server/workers/review.js.map +1 -1
  153. package/dist/server/workers/specSession.js +10 -4
  154. package/dist/server/workers/specSession.js.map +1 -1
  155. package/package.json +1 -1
  156. package/dist/client/assets/VerificationDialog-B4Pyz1LV.js +0 -4
  157. package/dist/client/assets/channel-DWPRC-jg.js +0 -1
  158. package/dist/client/assets/classDiagram-4FO5ZUOK-DBZaZLLc.js +0 -1
  159. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B7HsVFoJ.js +0 -1
  160. package/dist/client/assets/index-D-miHTTb.js +0 -273
  161. package/dist/client/assets/index-TAQIlNdJ.css +0 -2
  162. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-rrN4DgWp.js +0 -1
@@ -18,7 +18,7 @@ import { resolveChatSessionTarget } from './lib/chatTargets.js';
18
18
  import { createChatMessage, getChatMessage, updateChatMessageContent } from './lib/chatMessages.js';
19
19
  import { attachChatTicketProposalsToMessageContent } from './lib/chatTicketProposals.js';
20
20
  import { createCorsOriginResolver, parseCorsOrigins } from './lib/cors.js';
21
- import { abortGitOperation, branchExistsLocally, commitGitChanges, createWorktreeAsync, deleteWorktree, getBundleReviewDiffForCwd, getGitDiffData, getGitDiffFiles, getGitStatus, getGitUnifiedDiff, getLocalProjectBranch, getProjectBranch, getProjectBranchSuggestions, getSelectedGitUnifiedDiff, hasKnownBranchRef, listUnpushedCommitsForCwd, pullGitBranch, pushCurrentBranch, rebaseGitBranchOntoBase, resolveExistingWorktreePath, resolveWorktreePath, summarizeTicketDiffFiles, switchProjectBranch, syncWorktreeFilesAsync, undoLatestUnpushedCommitForCwd } from './lib/git.js';
21
+ import { abortGitOperation, branchExistsLocally, commitGitChanges, createWorktreeAsync, deleteWorktree, getBundleReviewDiffForCwd, getGitDiffData, getGitDiffFiles, getGitStatus, getGitUnifiedDiff, getLocalProjectBranch, getProjectBranch, getProjectBranchSuggestions, getSelectedGitUnifiedDiff, getUnpushedCommitDiffForCwd, hasKnownBranchRef, listUnpushedCommitsForCwd, pullGitBranch, pushCurrentBranch, rebaseGitBranchOntoBase, resolveExistingWorktreePath, resolveWorktreePath, summarizeTicketDiffFiles, switchProjectBranch, syncWorktreeFilesAsync, undoLatestUnpushedCommitForCwd, UnpushedCommitDiffSelectionError } from './lib/git.js';
22
22
  import config from './lib/config.js';
23
23
  import { autoConfigProject, ProjectAutoConfigError } from './lib/projectAutoConfig.js';
24
24
  import { analyzeRepoPath, createInitialCommit, ensureInitialCommitForWorktrees, initializeGitRepo, isInitialCommitRequiredError, } from './lib/projectRepos.js';
@@ -31,7 +31,7 @@ import { runLightweightPrompt } from './lib/lightweightPrompt.js';
31
31
  import { advanceBuildChain, startBuildChain, stopBuildChain } from './lib/buildChains.js';
32
32
  import { activateQueuedBuildSequenceTicket, compensateBuildSequenceAcceptance, enqueueBundledBuild, getDependencyOrderedBundleTicketIds, getBuildSequenceIdForTicket, hasActiveSequenceForBundle, removeTicketFromBuildSequence, stopBuildSequence, } from './lib/buildSequences.js';
33
33
  import { prepareTicketForBuild } from './lib/buildFlow.js';
34
- import { getBundlePlanMutationBlockerById, shouldBlockPlanForBundleMutationById, } from './lib/bundleActivity.js';
34
+ import { getActiveBundleRepairBlocker, getBundlePlanMutationBlockerById, shouldBlockPlanForBundleMutationById, } from './lib/bundleActivity.js';
35
35
  import { getDiscoveredModels, normalizeModelId, normalizeTicketModelId } from './lib/models.js';
36
36
  import { upgradeTool, UpgradeInProgressError } from './lib/toolUpgrade.js';
37
37
  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';
@@ -47,7 +47,8 @@ import { exportProjectMemoryArchive, importProjectMemoryArchive } from './lib/pr
47
47
  import { buildReviewFindingsResponse, computeDiffSignature, getLatestReviewRunForContext, listReviewFindings } from './lib/reviewFindings.js';
48
48
  import { formatBuildOpenQuestionAnswersPrompt, getLatestBuildOpenQuestionBatchStatus, hasActiveBuildOpenQuestionBatch, listBuildOpenQuestionsForBundle, loadBuildOpenQuestionsByBundleIds, } from './lib/buildOpenQuestions.js';
49
49
  import { acceptBuildOpenQuestionResolution } from './lib/buildOpenQuestionOperations.js';
50
- import { completeAutomationProgression, createAutomationReviewFix, ensureAutomationProgression, finishAutomationReviewFix, getAutomationProgression, recoverLegacyAutomationProgression, recoverStoppedSideEffectProgression, runAutomationProgression, stopAutomationProgression, waitAutomationProgression } from './lib/automationProgression.js';
50
+ import { areBundleTicketsReadyForAutomaticReview } from './lib/bundleAutomationReadiness.js';
51
+ import { abandonAutomationPause, completeAutomationProgression, createAutomationReviewFix, ensureAutomationProgression, finishAutomationReviewFix, getAutomationProgression, recoverLegacyAutomationProgression, recoverStoppedSideEffectProgression, runAutomationProgression, stopAutomationProgression, waitAutomationProgression } from './lib/automationProgression.js';
51
52
  import { reconcileAutomationStage } from './lib/automationReconciliation.js';
52
53
  import { runAutomationReviewGateOrPause } from './lib/automationReviewGate.js';
53
54
  import { pausedRunBelongsToProgression, resolveAutomationResumeDecision } from './lib/automationResumeDecision.js';
@@ -82,11 +83,13 @@ import { countInboundProjectLinks, getEffectiveProject, getProjectCheckDefinitio
82
83
  import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
83
84
  import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
84
85
  import { closeCheckRunnerAdmissions, getCheckBatchAttempts, openCheckRunnerAdmissions, rerunCheckBatchAndSchedule, shutdownCheckRunner, startCheckBatch, stopCheckBatch } from './lib/checkRunner.js';
86
+ import { CheckRepairError, getCheckRepairCandidateState, getCheckRepairDetails, getLatestCheckRepairForBatch, retryCheckRepair, startCheckRepair, stopCheckRepair } from './lib/checkRepair.js';
85
87
  import { findActiveCheckBatchForTarget, getCheckAttemptDetails, getCheckBatchDetails, hasActiveProjectCheckBatch, isCheckAttemptCurrent, resolveCheckBatchRerunEligibility } from './lib/checkRuns.js';
86
- import { getCheckDefinitionFingerprint, validateRequiredCheckPolicySnapshotSize } from './lib/checkDefinitions.js';
87
- import { getCheckGateEvaluation } from './lib/checkGateEvaluations.js';
88
+ import { getCheckAttemptDiagnosticResponse, getCheckBatchDiagnosticResponse } from './lib/checkDiagnostics.js';
89
+ import { getCheckDefinitionFingerprint } from './lib/checkDefinitions.js';
90
+ import { getCheckGateEvaluation, hydrateCheckGatePresentApplicability } from './lib/checkGateEvaluations.js';
88
91
  import { captureCheckWorkspaceIdentity } from './lib/checkWorkspaceIdentity.js';
89
- import { canonicalizeBundleRequiredCheckPolicyOverride, parseBundleRequiredCheckPolicyOverride, resolveEffectiveRequiredCheckPolicyMembership, } from './lib/requiredCheckPolicy.js';
92
+ import { resolveEffectiveRequiredCheckPolicyMembership, resolveRequiredCheckPolicy } from './lib/requiredCheckPolicy.js';
90
93
  import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTicketState, } from './lib/ticketWorkflowOperations.js';
91
94
  import { submitPlanTicketFollowUp, submitTicketFollowUp, } from './lib/ticketFollowUpOperations.js';
92
95
  import { closePendingTicketPlanQuestionSet, listPendingTicketPlanQuestionSetMessageIds, resolveTicketPlanQuestionSet, restoreClosedTicketPlanQuestionSets, } from './lib/planQuestionOperations.js';
@@ -103,9 +106,9 @@ import { HttpError } from './lib/httpError.js';
103
106
  import { parseIncomingExternalSource } from './lib/externalSource.js';
104
107
  import { materializeTicketsWithDependencies } from './lib/ticketBatch.js';
105
108
  import { IMPLEMENTATION_READY_DESCRIPTION_SECTION_HEADINGS } from './lib/ticketDescriptionContract.js';
106
- import { buildSpecImplementationDraft, buildVisualizationAppendix } from './lib/specDraft.js';
109
+ import { buildImplementationResourcesAppendix, buildSpecImplementationDraft, buildVisualizationAppendix } from './lib/specDraft.js';
107
110
  import { exportSpecToDocs, checkSpecExportCollision } from './lib/specExport.js';
108
- import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
111
+ import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketBuildComplete, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
109
112
  import { startBuild } from './workers/build.js';
110
113
  import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, prepareLinkedPlanningResearch, startPlan, stopAllPlanAgents, } from './workers/plan.js';
111
114
  import { EXECUTION_RUN_KINDS, MAX_ARTIFACTS_PER_RUN, validateWorkflowContext } from './lib/types.js';
@@ -135,6 +138,34 @@ const runExclusiveExecutionArtifactMaintenance = async (task) => {
135
138
  const getErrorMessage = (error) => {
136
139
  return error instanceof Error ? error.message : String(error);
137
140
  };
141
+ const getCommitHashQuery = (req) => {
142
+ if (req.query.commit === undefined) {
143
+ return null;
144
+ }
145
+ return typeof req.query.commit === 'string' ? req.query.commit : '';
146
+ };
147
+ const sendUnpushedCommitDiff = (res, workspace, commitHash) => {
148
+ try {
149
+ const diffData = getUnpushedCommitDiffForCwd({
150
+ cwd: workspace.cwd,
151
+ branch: workspace.branch,
152
+ project: workspace.project,
153
+ commitHash,
154
+ });
155
+ return res.json({
156
+ stats: summarizeTicketDiffFiles(diffData.files),
157
+ files: diffData.files,
158
+ diff: diffData.diff,
159
+ });
160
+ }
161
+ catch (error) {
162
+ if (error instanceof UnpushedCommitDiffSelectionError) {
163
+ return res.status(400).json({ error: error.message });
164
+ }
165
+ console.error(`Failed to load committed diff for ${workspace.cwd}:`, error);
166
+ return res.status(500).json({ error: getErrorMessage(error) || 'Unable to load the committed diff.' });
167
+ }
168
+ };
138
169
  const getProjectMemoryDisabledError = () => 'Project memory is disabled for this project';
139
170
  const sanitizeProjectMemoryExportFileName = (projectName) => {
140
171
  const normalized = projectName
@@ -380,11 +411,19 @@ const buildExecutionRunDetailResponse = async (project, run) => {
380
411
  retry_eligibility: retryEligibility,
381
412
  action_kind: null,
382
413
  }
383
- : run.kind === 'github_actions'
384
- ? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
385
- : ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
386
- ? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
387
- : { kind: run.kind, available: true, retry_eligibility: retryEligibility };
414
+ : run.kind === 'check_repair'
415
+ ? {
416
+ kind: 'check_repair',
417
+ available: true,
418
+ check_repair: getCheckRepairDetails(project.id, run.id),
419
+ retry_eligibility: retryEligibility,
420
+ action_kind: null,
421
+ }
422
+ : run.kind === 'github_actions'
423
+ ? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
424
+ : ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
425
+ ? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
426
+ : { kind: run.kind, available: true, retry_eligibility: retryEligibility };
388
427
  const artifacts = loadExecutionArtifactSummariesForRuns(project.id, [run.id], MAX_ARTIFACTS_PER_RUN).get(run.id)
389
428
  ?? { artifacts: [], artifact_count: 0 };
390
429
  return {
@@ -444,7 +483,14 @@ const getParkTicketError = (ticket) => {
444
483
  const getBundlePlanBlockerError = () => {
445
484
  return 'AI planning is blocked because this bundle worktree is currently being modified.';
446
485
  };
486
+ const hasActiveBundleCheckWork = (projectId, bundleId) => Boolean(getActiveBundleRepairBlocker(bundleId)
487
+ || findActiveCheckBatchForTarget(projectId, 'bundle', bundleId));
447
488
  const deleteTicketRecord = async (ticket) => {
489
+ if (ticket.project_id
490
+ && ticket.worktree_bundle_id
491
+ && hasActiveBundleCheckWork(ticket.project_id, ticket.worktree_bundle_id)) {
492
+ return { ok: false, status: 409, error: 'Cannot delete a ticket while its bundle has active check work' };
493
+ }
448
494
  if (ticket.agent_status === 'running'
449
495
  || (ticket.project_id && hasActiveBuildExecutionRunForTicket(ticket.project_id, ticket.id))) {
450
496
  return { ok: false, status: 409, error: 'Cannot delete a ticket while the agent is running' };
@@ -812,6 +858,9 @@ const cleanupBundleIfEmpty = (bundleId, project) => {
812
858
  if (isProjectRootBundle(bundle)) {
813
859
  return;
814
860
  }
861
+ if (hasActiveBundleCheckWork(project.id, bundle.id)) {
862
+ throw new HttpError(409, 'Cannot delete a bundle while check work is active');
863
+ }
815
864
  try {
816
865
  deleteWorktree(bundle.branch, project);
817
866
  }
@@ -1464,10 +1513,7 @@ const serializeBundleRow = (projectId, bundle, openQuestions = bundle ? loadBuil
1464
1513
  const hasPreparedWorktree = bundle.kind === 'project_root'
1465
1514
  ? true
1466
1515
  : Boolean(project && resolveExistingWorktreePath(bundle.branch, project));
1467
- const parsedOverride = bundle.kind === 'project_root'
1468
- ? { value: null }
1469
- : parseBundleRequiredCheckPolicyOverride(bundle.required_check_policy_override);
1470
- const effectivePolicy = resolveEffectiveRequiredCheckPolicyMembership(project, bundle);
1516
+ const effectivePolicy = resolveEffectiveRequiredCheckPolicyMembership(project);
1471
1517
  return {
1472
1518
  ...bundle,
1473
1519
  has_prepared_worktree: hasPreparedWorktree,
@@ -1475,7 +1521,6 @@ const serializeBundleRow = (projectId, bundle, openQuestions = bundle ? loadBuil
1475
1521
  ? serializeBundlePullRequest(getBundlePullRequestRecord(bundle.id, projectId))
1476
1522
  : null,
1477
1523
  automation_config: resolveAutomationConfig(bundle, project),
1478
- required_check_policy_override: 'error' in parsedOverride ? null : parsedOverride.value,
1479
1524
  required_check_policy: 'error' in effectivePolicy
1480
1525
  ? undefined
1481
1526
  : {
@@ -1483,7 +1528,6 @@ const serializeBundleRow = (projectId, bundle, openQuestions = bundle ? loadBuil
1483
1528
  required_check_definition_ids: effectivePolicy.required_check_definition_ids,
1484
1529
  pre_review_check_definition_ids: effectivePolicy.pre_review_check_definition_ids,
1485
1530
  },
1486
- required_check_policy_error: 'error' in parsedOverride ? parsedOverride.error : ('error' in effectivePolicy ? effectivePolicy.error : null),
1487
1531
  open_questions: openQuestions,
1488
1532
  build_open_question_batch_status: getLatestBuildOpenQuestionBatchStatus(projectId, bundle.id),
1489
1533
  };
@@ -3049,11 +3093,14 @@ app.post('/projects/:id/spec-sessions/:sessionId/snapshots/:snapshotId/create-ti
3049
3093
  ? createAutoBundleForName(bundleName, project.id)
3050
3094
  : createAutoPlanningBundle(project.id);
3051
3095
  const ticketsForMaterialization = snapshot.tickets.map(ticket => {
3052
- const appendix = buildVisualizationAppendix(ticket.relevant_visualizations ?? [], snapshot);
3096
+ const appendices = [
3097
+ buildVisualizationAppendix(ticket.relevant_visualizations ?? [], snapshot),
3098
+ buildImplementationResourcesAppendix(snapshot.resource_urls),
3099
+ ].filter(Boolean);
3053
3100
  return {
3054
3101
  key: ticket.key,
3055
3102
  title: ticket.title,
3056
- description: appendix ? `${ticket.description}\n\n${appendix}` : ticket.description,
3103
+ description: appendices.length > 0 ? `${ticket.description}\n\n${appendices.join('\n\n')}` : ticket.description,
3057
3104
  };
3058
3105
  });
3059
3106
  const { createdTickets, createdDependencies } = materializeTicketsWithDependencies({
@@ -3295,6 +3342,20 @@ app.get('/tickets/:id/diff', (req, res) => {
3295
3342
  diff: diffData.diff,
3296
3343
  });
3297
3344
  });
3345
+ app.get('/tickets/:id/committed-diff', (req, res) => {
3346
+ const ticket = getTicketRouteContext(req.params.id);
3347
+ if (!ticket) {
3348
+ return res.status(404).json({ error: 'Not found' });
3349
+ }
3350
+ if (!matchesTicketProjectContext(ticket, req)) {
3351
+ return res.status(404).json({ error: 'Not found' });
3352
+ }
3353
+ const workspace = resolveTicketDiffWorkspaceFromContext(ticket);
3354
+ if ('error' in workspace) {
3355
+ return res.status(workspace.status).json({ error: workspace.error });
3356
+ }
3357
+ return sendUnpushedCommitDiff(res, workspace, getCommitHashQuery(req));
3358
+ });
3298
3359
  app.get('/tickets/:id/git-status', (req, res) => {
3299
3360
  const ticket = getTicketRouteContext(req.params.id);
3300
3361
  if (!ticket) {
@@ -3719,15 +3780,15 @@ app.delete('/bundles/:id', (req, res) => {
3719
3780
  if (isProjectRootBundle(bundle)) {
3720
3781
  return res.status(409).json({ error: 'Project root cannot be deleted' });
3721
3782
  }
3783
+ if (hasActiveBundleCheckWork(result.project.id, bundle.id)) {
3784
+ return res.status(409).json({ error: 'Cannot delete a bundle while check work is active' });
3785
+ }
3722
3786
  if (countBundleTickets(bundle.id, result.project.id) > 0) {
3723
3787
  return res.status(409).json({ error: 'Cannot delete a bundle that is still used by tickets' });
3724
3788
  }
3725
3789
  if (result.project.active_target_kind === 'bundle' && result.project.active_target_bundle_id === bundle.id) {
3726
3790
  return res.status(409).json({ error: 'Cannot delete the currently selected target bundle' });
3727
3791
  }
3728
- if (findActiveCheckBatchForTarget(result.project.id, 'bundle', bundle.id)) {
3729
- return res.status(409).json({ error: 'Cannot delete a bundle while checks are running' });
3730
- }
3731
3792
  const affectedChatSessionIds = listChatSessions(result.project.id)
3732
3793
  .filter(chatSession => chatSession.target_bundle_id === bundle.id)
3733
3794
  .map(chatSession => chatSession.id);
@@ -3758,14 +3819,14 @@ app.patch('/bundles/:id', (req, res) => {
3758
3819
  return res.status(404).json({ error: 'Bundle not found' });
3759
3820
  }
3760
3821
  const hasNameUpdate = hasOwn(req.body, 'name');
3761
- const hasRequiredCheckPolicyOverrideUpdate = hasOwn(req.body, 'required_check_policy_override');
3822
+ if (hasOwn(req.body, 'required_check_policy_override')) {
3823
+ return res.status(400).json({ error: 'Bundle required-check policy overrides are no longer supported; configure required checks on the project' });
3824
+ }
3762
3825
  if (isProjectRootBundle(bundle)) {
3763
3826
  return res.status(409).json({
3764
3827
  error: hasNameUpdate
3765
3828
  ? 'Project root bundles cannot be renamed'
3766
- : hasRequiredCheckPolicyOverrideUpdate
3767
- ? 'Project root bundles cannot override required-check policy'
3768
- : 'Project root bundle automation cannot be overridden',
3829
+ : 'Project root bundle automation cannot be overridden',
3769
3830
  });
3770
3831
  }
3771
3832
  let nextName = bundle.name;
@@ -3838,39 +3899,6 @@ app.patch('/bundles/:id', (req, res) => {
3838
3899
  return res.status(400).json({ error: 'automation_pause_on_open_questions must be a boolean' });
3839
3900
  }
3840
3901
  }
3841
- let requiredCheckPolicyOverride;
3842
- let requiredCheckPolicyOverrideValue;
3843
- if (hasRequiredCheckPolicyOverrideUpdate) {
3844
- const value = req.body.required_check_policy_override;
3845
- if (value == null) {
3846
- requiredCheckPolicyOverride = null;
3847
- requiredCheckPolicyOverrideValue = null;
3848
- }
3849
- else {
3850
- const result = canonicalizeBundleRequiredCheckPolicyOverride(value);
3851
- if ('error' in result) {
3852
- return res.status(400).json({ error: result.error });
3853
- }
3854
- requiredCheckPolicyOverrideValue = result.value;
3855
- requiredCheckPolicyOverride = result.value ? JSON.stringify(result.value) : null;
3856
- }
3857
- }
3858
- if (requiredCheckPolicyOverrideValue) {
3859
- const definitions = getProjectCheckDefinitions(result.project);
3860
- const definitionIds = new Set(definitions.map(definition => definition.id));
3861
- const missingId = [
3862
- ...requiredCheckPolicyOverrideValue.required_check_definition_ids,
3863
- ...requiredCheckPolicyOverrideValue.pre_review_check_definition_ids,
3864
- ].find(id => !definitionIds.has(id));
3865
- if (missingId) {
3866
- return res.status(400).json({ error: `required_check_policy_override references missing check definition ${missingId}` });
3867
- }
3868
- const requiredDefinitions = definitions.filter(definition => requiredCheckPolicyOverrideValue.required_check_definition_ids.includes(definition.id));
3869
- const policySnapshotSizeError = validateRequiredCheckPolicySnapshotSize(requiredCheckPolicyOverrideValue.required_check_definition_ids, requiredDefinitions);
3870
- if (policySnapshotSizeError) {
3871
- return res.status(400).json({ error: policySnapshotSizeError });
3872
- }
3873
- }
3874
3902
  let updated = null;
3875
3903
  try {
3876
3904
  updated = db.transaction(() => {
@@ -3888,7 +3916,6 @@ app.patch('/bundles/:id', (req, res) => {
3888
3916
  automationDepth,
3889
3917
  automationReviewFixEnabled,
3890
3918
  automationPauseOnOpenQuestions,
3891
- requiredCheckPolicyOverride,
3892
3919
  });
3893
3920
  })();
3894
3921
  }
@@ -3928,7 +3955,7 @@ app.post('/bundles/:id/review', (req, res) => {
3928
3955
  }
3929
3956
  return res.json(reviewResult.details);
3930
3957
  });
3931
- app.post('/bundles/:id/build-open-questions/resolve', (req, res) => {
3958
+ app.post('/bundles/:id/build-open-questions/resolve', async (req, res) => {
3932
3959
  const result = getRequiredProject(req);
3933
3960
  if ('error' in result) {
3934
3961
  return res.status(400).json({ error: result.error });
@@ -3971,54 +3998,70 @@ app.post('/bundles/:id/build-open-questions/resolve', (req, res) => {
3971
3998
  }
3972
3999
  if (accepted.kind === 'dismissed') {
3973
4000
  const automationConfig = resolveAutomationConfig(bundle, result.project);
3974
- const reviewResult = depthAtLeast(automationConfig.depth, 'auto_review')
3975
- ? transitionBundleToReview(result.project.id, bundle.id, io, {
3976
- automatic: true,
3977
- initiation: {
3978
- causalParentRunId: accepted.progressionRunId,
3979
- workflowContext: validateWorkflowContext({ build_open_question_batch_id: accepted.batchId }),
3980
- },
3981
- })
3982
- : null;
3983
- if (!reviewResult) {
4001
+ if (!depthAtLeast(automationConfig.depth, 'auto_review')) {
3984
4002
  completeAutomationProgression({
3985
4003
  projectId: result.project.id,
3986
4004
  bundleId: bundle.id,
3987
4005
  progressionRunId: accepted.progressionRunId,
3988
4006
  broadcaster: io,
3989
4007
  });
4008
+ for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
4009
+ io.emit('ticket:updated', ticket);
4010
+ }
4011
+ return res.json(serializeBundleDetails(result.project, bundle));
3990
4012
  }
3991
- for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
3992
- io.emit('ticket:updated', ticket);
3993
- }
3994
- if (reviewResult && !reviewResult.ok) {
3995
- if (reviewResult.code === 'bundle_build_incomplete') {
3996
- completeAutomationProgression({
3997
- projectId: result.project.id,
3998
- bundleId: bundle.id,
3999
- progressionRunId: accepted.progressionRunId,
4000
- broadcaster: io,
4001
- });
4002
- for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
4003
- io.emit('ticket:updated', ticket);
4004
- }
4005
- return res.json(serializeBundleDetails(result.project, bundle));
4013
+ const ticketsBeforeReview = listDetailedBundleTickets(result.project.id, bundle.id);
4014
+ if (!areBundleTicketsReadyForAutomaticReview(ticketsBeforeReview, bundle.id)) {
4015
+ completeAutomationProgression({
4016
+ projectId: result.project.id,
4017
+ bundleId: bundle.id,
4018
+ progressionRunId: accepted.progressionRunId,
4019
+ broadcaster: io,
4020
+ });
4021
+ for (const ticket of ticketsBeforeReview) {
4022
+ io.emit('ticket:updated', ticket);
4006
4023
  }
4024
+ return res.json(serializeBundleDetails(result.project, bundle));
4025
+ }
4026
+ let reviewStartFailureStatus = null;
4027
+ let reviewStartFailureError = null;
4028
+ const reviewStarted = await runAutomationReviewGateOrPause({
4029
+ projectId: result.project.id,
4030
+ bundleId: bundle.id,
4031
+ progressionRunId: accepted.progressionRunId,
4032
+ broadcaster: io,
4033
+ pausedStageRunId: null,
4034
+ initiation: {
4035
+ workflowContext: validateWorkflowContext({ build_open_question_batch_id: accepted.batchId }),
4036
+ },
4037
+ onReviewStartFailure: failure => {
4038
+ reviewStartFailureStatus = failure.status;
4039
+ reviewStartFailureError = failure.error;
4040
+ },
4041
+ });
4042
+ if (reviewStartFailureStatus !== null && reviewStartFailureError !== null) {
4007
4043
  waitAutomationProgression({
4008
4044
  projectId: result.project.id,
4009
4045
  bundleId: bundle.id,
4010
4046
  progressionRunId: accepted.progressionRunId,
4011
4047
  pauseCode: 'review',
4012
- pauseReason: reviewResult.error,
4048
+ pauseReason: reviewStartFailureError,
4013
4049
  reasonCode: 'stage_failed',
4014
4050
  pausedStageRunId: null,
4015
4051
  broadcaster: io,
4016
4052
  });
4017
- return res.status(reviewResult.status).json({ error: reviewResult.error });
4053
+ return res.status(reviewStartFailureStatus).json({ error: reviewStartFailureError });
4054
+ }
4055
+ const updatedTickets = listDetailedBundleTickets(result.project.id, bundle.id);
4056
+ for (const ticket of updatedTickets) {
4057
+ io.emit('ticket:updated', ticket);
4018
4058
  }
4059
+ const reviewOwner = reviewStarted
4060
+ ? updatedTickets.find(ticket => ticket.state === 'review' && ticket.parked_at === null)
4061
+ : null;
4019
4062
  return res.json(serializeBundleDetails(result.project, bundle, {
4020
- review_owner_ticket_id: reviewResult?.ok ? reviewResult.value?.id : undefined,
4021
- target_ticket_id: reviewResult?.ok ? reviewResult.value?.id : undefined,
4063
+ review_owner_ticket_id: reviewOwner?.id,
4064
+ target_ticket_id: reviewOwner?.id,
4022
4065
  }));
4023
4066
  }
4024
4067
  for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
@@ -4125,7 +4168,7 @@ app.post('/bundles/:id/follow-up', (req, res) => {
4125
4168
  return res.status(response.status).json(response.value);
4126
4169
  });
4127
4170
  const AUTOMATION_SIDE_EFFECT_KINDS = new Set(['automation_commit', 'automation_push', 'automation_pull_request']);
4128
- const AUTOMATION_WORKER_PAUSE_KINDS = new Set(['build', 'review', 'build_follow_up', 'automation_review_fix', 'check_batch']);
4171
+ const AUTOMATION_WORKER_PAUSE_KINDS = new Set(['build', 'review', 'build_follow_up', 'automation_review_fix', 'check_repair', 'check_batch']);
4129
4172
  app.post('/bundles/:id/resume-automation', async (req, res) => {
4130
4173
  const result = getRequiredProject(req);
4131
4174
  if ('error' in result) {
@@ -4208,7 +4251,11 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4208
4251
  });
4209
4252
  if (checkpointLineage) {
4210
4253
  const owner = getTicket(decision.owner_ticket_id);
4211
- if (!owner || owner.id !== checkpointLineage.ticket.id || owner.agent_status !== 'done') {
4254
+ if (!owner
4255
+ || owner.id !== checkpointLineage.ticket.id
4256
+ || owner.state !== 'build'
4257
+ || owner.parked_at !== null
4258
+ || !isTicketBuildComplete(owner)) {
4212
4259
  return res.status(409).json({ error: 'Checkpoint progression owner is no longer the active Build ticket' });
4213
4260
  }
4214
4261
  let retryOfRunId;
@@ -4463,6 +4510,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4463
4510
  pauseReason: bundleState.automation_paused_reason ?? 'Automation paused',
4464
4511
  reasonCode: bundleState.automation_pause_reason_code ?? 'manual_transition',
4465
4512
  pausedStageRunId: bundleState.automation_paused_stage_run_id,
4513
+ pausedBoundaryPurpose: bundleState.automation_paused_boundary_purpose,
4466
4514
  broadcaster: io,
4467
4515
  });
4468
4516
  };
@@ -4500,7 +4548,62 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4500
4548
  }
4501
4549
  return res.json(serializeBundleDetails(result.project, bundle));
4502
4550
  }
4503
- if (owner.state === 'review' && owner.agent_status !== 'running') {
4551
+ if (decision.outcome === 'complete_manual_boundary') {
4552
+ if (!revalidateDecision()) {
4553
+ return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
4554
+ }
4555
+ completeAutomationProgression({
4556
+ projectId: result.project.id,
4557
+ bundleId: bundle.id,
4558
+ progressionRunId,
4559
+ domainStatus: 'manual_boundary_reached',
4560
+ broadcaster: io,
4561
+ });
4562
+ return res.json(serializeBundleDetails(result.project, bundle));
4563
+ }
4564
+ if ((decision.outcome === 'start_review' || decision.outcome === 'rerun_review_checks')
4565
+ && owner.parked_at === null
4566
+ && isTicketBuildComplete(owner)
4567
+ && (owner.state === 'build' || (decision.outcome === 'rerun_review_checks' && owner.state === 'review'))) {
4568
+ if (!revalidateDecision()) {
4569
+ return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
4570
+ }
4571
+ const progression = getAutomationProgression(result.project.id, bundle.id);
4572
+ if (!progression || progression.id !== progressionRunId) {
4573
+ return res.status(409).json({ error: 'Automation progression ownership is invalid' });
4574
+ }
4575
+ const workflowContext = validateWorkflowContext(progression.metadata);
4576
+ let reviewStartFailureStatus = null;
4577
+ let reviewStartFailureError = null;
4578
+ try {
4579
+ runAutomationProgression({
4580
+ projectId: result.project.id,
4581
+ bundleId: bundle.id,
4582
+ progressionRunId,
4583
+ broadcaster: io,
4584
+ });
4585
+ await runAutomationReviewGateOrPause({
4586
+ projectId: result.project.id,
4587
+ bundleId: bundle.id,
4588
+ progressionRunId,
4589
+ broadcaster: io,
4590
+ pausedStageRunId: bundleState.automation_paused_stage_run_id,
4591
+ initiation: { workflowContext },
4592
+ onReviewStartFailure: failure => {
4593
+ reviewStartFailureStatus = failure.status;
4594
+ reviewStartFailureError = failure.error;
4595
+ restoreWaitingProgression();
4596
+ },
4597
+ });
4598
+ }
4599
+ catch (error) {
4600
+ return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
4601
+ }
4602
+ if (reviewStartFailureStatus !== null && reviewStartFailureError !== null) {
4603
+ return res.status(reviewStartFailureStatus).json({ error: reviewStartFailureError });
4604
+ }
4605
+ }
4606
+ else if (owner.state === 'review' && owner.agent_status !== 'running') {
4504
4607
  const iteration = bundleState.review_iteration_count ?? 0;
4505
4608
  if (decision.outcome === 'start_review_fix') {
4506
4609
  let reviewFixRunId = null;
@@ -4610,7 +4713,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4610
4713
  broadcaster: io,
4611
4714
  });
4612
4715
  const checkGateResume = decision.outcome === 'rerun_required_checks'
4613
- ? resolveAutomationCheckGateResume(result.project.id, progressionRunId)
4716
+ ? resolveAutomationCheckGateResume(result.project.id, progressionRunId, owner.bundle?.kind === 'project_root' ? project.repo_path : resolveWorktreePath(bundle.branch, project))
4614
4717
  : undefined;
4615
4718
  await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io, progressionRunId, checkGateResume);
4616
4719
  }
@@ -4635,45 +4738,6 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4635
4738
  return res.status(409).json({ error: 'The selected Resume outcome no longer matches the bundle state' });
4636
4739
  }
4637
4740
  }
4638
- else if (owner.state === 'build' && owner.agent_status === 'done' && (decision.outcome === 'start_review' || decision.outcome === 'rerun_review_checks')) {
4639
- if (!revalidateDecision()) {
4640
- return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
4641
- }
4642
- const progression = getAutomationProgression(result.project.id, bundle.id);
4643
- if (!progression || progression.id !== progressionRunId) {
4644
- return res.status(409).json({ error: 'Automation progression ownership is invalid' });
4645
- }
4646
- const workflowContext = validateWorkflowContext(progression.metadata);
4647
- let reviewStartFailureStatus = null;
4648
- let reviewStartFailureError = null;
4649
- try {
4650
- runAutomationProgression({
4651
- projectId: result.project.id,
4652
- bundleId: bundle.id,
4653
- progressionRunId,
4654
- broadcaster: io,
4655
- });
4656
- await runAutomationReviewGateOrPause({
4657
- projectId: result.project.id,
4658
- bundleId: bundle.id,
4659
- progressionRunId,
4660
- broadcaster: io,
4661
- pausedStageRunId: bundleState.automation_paused_stage_run_id,
4662
- initiation: { workflowContext },
4663
- onReviewStartFailure: failure => {
4664
- reviewStartFailureStatus = failure.status;
4665
- reviewStartFailureError = failure.error;
4666
- restoreWaitingProgression();
4667
- },
4668
- });
4669
- }
4670
- catch (error) {
4671
- return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
4672
- }
4673
- if (reviewStartFailureStatus !== null && reviewStartFailureError !== null) {
4674
- return res.status(reviewStartFailureStatus).json({ error: reviewStartFailureError });
4675
- }
4676
- }
4677
4741
  else {
4678
4742
  return res.status(409).json({ error: 'Ticket is not in a resumable state' });
4679
4743
  }
@@ -4704,72 +4768,31 @@ app.post('/bundles/:id/dismiss-automation-pause', async (req, res) => {
4704
4768
  && listBuildOpenQuestionsForBundle(result.project.id, bundle.id).length > 0) {
4705
4769
  return res.status(409).json({ error: 'Resolve or dismiss all Build open questions before dismissing this pause' });
4706
4770
  }
4707
- let progressionRunId = bundleState.automation_progression_run_id;
4708
- if (!progressionRunId) {
4771
+ const progressionRunId = bundleState.automation_progression_run_id;
4772
+ const checkpointLineage = progressionRunId
4773
+ ? resolveBuildTicketCommitCheckpointLineage({
4774
+ projectId: result.project.id,
4775
+ bundleId: bundle.id,
4776
+ progressionRunId,
4777
+ })
4778
+ : null;
4779
+ if (progressionRunId) {
4709
4780
  try {
4710
- progressionRunId = (bundleState.automation_paused_stage_run_id
4711
- ? recoverStoppedSideEffectProgression({
4712
- projectId: result.project.id,
4713
- bundleId: bundle.id,
4714
- broadcaster: io,
4715
- })
4716
- : recoverLegacyAutomationProgression({
4717
- projectId: result.project.id,
4718
- bundleId: bundle.id,
4719
- broadcaster: io,
4720
- })).runId;
4721
- }
4722
- catch (error) {
4723
- return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
4724
- }
4725
- }
4726
- try {
4727
- if (getAutomationProgression(result.project.id, bundle.id)?.id !== progressionRunId) {
4728
- return res.status(409).json({ error: 'Automation progression ownership is invalid' });
4729
- }
4730
- }
4731
- catch (error) {
4732
- return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
4733
- }
4734
- const checkpointLineage = resolveBuildTicketCommitCheckpointLineage({
4735
- projectId: result.project.id,
4736
- bundleId: bundle.id,
4737
- progressionRunId,
4738
- });
4739
- if (bundleState.automation_paused_stage_run_id) {
4740
- const stage = getExecutionRun(result.project.id, bundleState.automation_paused_stage_run_id);
4741
- if (!stage
4742
- || (!AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind) && !AUTOMATION_WORKER_PAUSE_KINDS.has(stage.kind))
4743
- || !pausedRunBelongsToProgression(result.project.id, bundle.id, progressionRunId, stage.id)) {
4744
- return res.status(409).json({ error: 'The paused automation stage does not belong to the active progression' });
4781
+ await stopAutomationProgression({
4782
+ projectId: result.project.id,
4783
+ bundleId: bundle.id,
4784
+ broadcaster: io,
4785
+ stopLocalWorker: worker => (worker.kind === 'review' ? stopReviewAgent(worker.ticket_id) : stopBuildAgent(worker.ticket_id)),
4786
+ });
4745
4787
  }
4746
- if (AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind)) {
4747
- let reconciled;
4748
- try {
4749
- reconciled = await reconcileAutomationStage({
4750
- projectId: result.project.id,
4751
- bundleId: bundle.id,
4752
- progressionRunId,
4753
- stageRunId: stage.id,
4754
- });
4755
- }
4756
- catch (error) {
4757
- return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
4758
- }
4759
- if (reconciled.domainStatus === 'ambiguous') {
4760
- return res.status(409).json({ error: 'The paused automation side effect is ambiguous and cannot be dismissed safely.' });
4761
- }
4762
- if (!(reconciled.status === 'completed' && reconciled.domainStatus === 'reconciled_completed')
4763
- && !(reconciled.status === 'interrupted' && reconciled.domainStatus === 'reconciled_absent')) {
4764
- return res.status(409).json({ error: 'The paused automation side effect has no safely dismissible outcome.' });
4765
- }
4788
+ catch {
4789
+ // Explicit abandonment also repairs a stale progression pointer.
4766
4790
  }
4767
4791
  }
4768
- await stopAutomationProgression({
4792
+ abandonAutomationPause({
4769
4793
  projectId: result.project.id,
4770
4794
  bundleId: bundle.id,
4771
4795
  broadcaster: io,
4772
- stopLocalWorker: worker => (worker.kind === 'review' ? stopReviewAgent(worker.ticket_id) : stopBuildAgent(worker.ticket_id)),
4773
4796
  });
4774
4797
  if (checkpointLineage) {
4775
4798
  const stoppedSequence = stopBuildSequence(checkpointLineage.ticket.id);
@@ -5859,7 +5882,7 @@ app.post('/projects', (req, res) => {
5859
5882
  return res.status(400).json({ error: validation.error });
5860
5883
  }
5861
5884
  const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
5862
- const { name, repo_path, linked_project_ids, run_setup, run_services, check_definitions, required_check_definition_ids, pre_review_check_definition_ids, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_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;
5885
+ 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, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_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;
5863
5886
  if (!repo_path) {
5864
5887
  return res.status(400).json({ error: 'repo_path must be a non-empty string' });
5865
5888
  }
@@ -5882,6 +5905,7 @@ app.post('/projects', (req, res) => {
5882
5905
  check_definitions,
5883
5906
  required_check_definition_ids,
5884
5907
  pre_review_check_definition_ids,
5908
+ post_build_check_definition_ids,
5885
5909
  test_commands,
5886
5910
  run_ide,
5887
5911
  preview_service_name,
@@ -5903,8 +5927,8 @@ app.post('/projects', (req, res) => {
5903
5927
  automation_review_severity,
5904
5928
  automation_review_max_iterations
5905
5929
  )
5906
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5907
- `).run(id, name, normalizedRepoPath, run_setup, run_services, check_definitions, required_check_definition_ids ?? '[]', pre_review_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, 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);
5930
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5931
+ `).run(id, name, normalizedRepoPath, run_setup, run_services, check_definitions, required_check_definition_ids ?? '[]', pre_review_check_definition_ids ?? '[]', post_build_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, 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);
5908
5932
  replaceProjectLinks(id, linked_project_ids ?? []);
5909
5933
  ensureProjectRootBundle(id);
5910
5934
  if (projectCount === 0) {
@@ -6814,6 +6838,17 @@ app.get('/projects/:id/diff', (req, res) => {
6814
6838
  diff: diffData.diff,
6815
6839
  });
6816
6840
  });
6841
+ app.get('/projects/:id/committed-diff', (req, res) => {
6842
+ const project = getProjectById(req.params.id);
6843
+ if (!project) {
6844
+ return res.status(404).json({ error: 'Not found' });
6845
+ }
6846
+ const workspace = resolveProjectTargetGitWorkspace(project);
6847
+ if ('error' in workspace) {
6848
+ return res.status(workspace.status).json({ error: workspace.error });
6849
+ }
6850
+ return sendUnpushedCommitDiff(res, workspace, getCommitHashQuery(req));
6851
+ });
6817
6852
  app.get('/projects/:id/git-status', (req, res) => {
6818
6853
  const project = getProjectById(req.params.id);
6819
6854
  if (!project) {
@@ -7070,7 +7105,7 @@ app.patch('/projects/:id', (req, res) => {
7070
7105
  });
7071
7106
  }
7072
7107
  }
7073
- const { name, repo_path, linked_project_ids, run_setup, run_services, check_definitions, required_check_definition_ids, pre_review_check_definition_ids, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_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;
7108
+ 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, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_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;
7074
7109
  const updateProject = db.prepare(`
7075
7110
  UPDATE projects SET
7076
7111
  name = ?,
@@ -7080,6 +7115,7 @@ app.patch('/projects/:id', (req, res) => {
7080
7115
  check_definitions = ?,
7081
7116
  required_check_definition_ids = ?,
7082
7117
  pre_review_check_definition_ids = ?,
7118
+ post_build_check_definition_ids = ?,
7083
7119
  test_commands = ?,
7084
7120
  run_ide = ?,
7085
7121
  preview_service_name = ?,
@@ -7110,17 +7146,21 @@ app.patch('/projects/:id', (req, res) => {
7110
7146
  const nextPreReviewCheckDefinitionIds = hasOwn(validation.values, 'pre_review_check_definition_ids')
7111
7147
  ? pre_review_check_definition_ids ?? '[]'
7112
7148
  : project.pre_review_check_definition_ids;
7149
+ const nextPostBuildCheckDefinitionIds = hasOwn(validation.values, 'post_build_check_definition_ids')
7150
+ ? post_build_check_definition_ids ?? '[]'
7151
+ : project.post_build_check_definition_ids ?? '[]';
7113
7152
  const updateResult = db.transaction(() => {
7114
7153
  const policyReferences = validateProjectRequiredCheckPolicyReferences({
7115
7154
  projectId: project.id,
7116
7155
  checkDefinitionsJson: nextCheckDefinitions,
7117
7156
  requiredCheckDefinitionIdsJson: nextRequiredCheckDefinitionIds,
7118
7157
  preReviewCheckDefinitionIdsJson: nextPreReviewCheckDefinitionIds,
7158
+ postBuildCheckDefinitionIdsJson: nextPostBuildCheckDefinitionIds,
7119
7159
  });
7120
7160
  if ('error' in policyReferences) {
7121
7161
  return policyReferences;
7122
7162
  }
7123
- updateProject.run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, nextCheckDefinitions, nextRequiredCheckDefinitionIds, nextPreReviewCheckDefinitionIds, 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, '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);
7163
+ updateProject.run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, nextCheckDefinitions, nextRequiredCheckDefinitionIds, nextPreReviewCheckDefinitionIds, nextPostBuildCheckDefinitionIds, 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, '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);
7124
7164
  if (hasOwn(validation.values, 'linked_project_ids')) {
7125
7165
  replaceProjectLinks(project.id, linked_project_ids ?? []);
7126
7166
  }
@@ -7187,6 +7227,12 @@ const completeClaimedProjectDeletion = async (projectId, claimId) => {
7187
7227
  WHERE evaluation_id IN (SELECT id FROM check_gate_evaluations WHERE project_id = ?)
7188
7228
  `).run(projectId);
7189
7229
  db.prepare('DELETE FROM check_gate_evaluations WHERE project_id = ?').run(projectId);
7230
+ if (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'check_repair_details'").get()) {
7231
+ db.prepare(`
7232
+ DELETE FROM check_repair_details
7233
+ WHERE execution_run_id IN (SELECT id FROM execution_runs WHERE project_id = ?)
7234
+ `).run(projectId);
7235
+ }
7190
7236
  db.prepare(`
7191
7237
  DELETE FROM check_attempt_details
7192
7238
  WHERE execution_run_id IN (SELECT id FROM execution_runs WHERE project_id = ?)
@@ -8131,6 +8177,98 @@ app.post('/projects/:id/check-batches', async (req, res) => {
8131
8177
  return res.status(409).json({ error: getErrorMessage(error) });
8132
8178
  }
8133
8179
  });
8180
+ app.post('/projects/:id/check-repairs', async (req, res) => {
8181
+ const project = getProjectById(req.params.id);
8182
+ if (!project) {
8183
+ return res.status(404).json({ error: 'Not found' });
8184
+ }
8185
+ const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body)
8186
+ ? req.body
8187
+ : null;
8188
+ if (!body || Object.keys(body).some(key => !['diagnostic_ids', 'raw_output_attempt_run_ids', 'ticket_id'].includes(key))) {
8189
+ return res.status(400).json({ error: 'Only diagnostic_ids, raw_output_attempt_run_ids, and optional ticket_id are accepted' });
8190
+ }
8191
+ const diagnosticIds = body.diagnostic_ids === undefined
8192
+ ? []
8193
+ : Array.isArray(body.diagnostic_ids)
8194
+ && body.diagnostic_ids.length <= 32
8195
+ && body.diagnostic_ids.every(id => typeof id === 'string' && id.trim().length > 0 && id.trim().length <= 100)
8196
+ ? body.diagnostic_ids.map(id => id.trim())
8197
+ : null;
8198
+ const rawOutputAttemptRunIds = body.raw_output_attempt_run_ids === undefined
8199
+ ? []
8200
+ : Array.isArray(body.raw_output_attempt_run_ids)
8201
+ && body.raw_output_attempt_run_ids.length <= 8
8202
+ && body.raw_output_attempt_run_ids.every(id => typeof id === 'string' && id.trim().length > 0 && id.trim().length <= 100)
8203
+ ? body.raw_output_attempt_run_ids.map(id => id.trim())
8204
+ : null;
8205
+ if (!diagnosticIds
8206
+ || diagnosticIds.length !== new Set(diagnosticIds).size
8207
+ || !rawOutputAttemptRunIds
8208
+ || rawOutputAttemptRunIds.length !== new Set(rawOutputAttemptRunIds).size
8209
+ || diagnosticIds.length + rawOutputAttemptRunIds.length === 0) {
8210
+ return res.status(400).json({ error: 'At least one unique diagnostic_ids or raw_output_attempt_run_ids evidence reference is required' });
8211
+ }
8212
+ if (body.ticket_id !== undefined && (typeof body.ticket_id !== 'string'
8213
+ || body.ticket_id.trim().length === 0
8214
+ || body.ticket_id.trim().length > 100)) {
8215
+ return res.status(400).json({ error: 'ticket_id must be a non-empty opaque ticket ID' });
8216
+ }
8217
+ try {
8218
+ return res.status(202).json(await startCheckRepair({
8219
+ projectId: project.id,
8220
+ diagnosticIds,
8221
+ rawOutputAttemptRunIds,
8222
+ ticketId: typeof body.ticket_id === 'string' ? body.ticket_id.trim() : null,
8223
+ io,
8224
+ }));
8225
+ }
8226
+ catch (error) {
8227
+ if (error instanceof CheckRepairError) {
8228
+ return res.status(error.status).json({ error: error.message });
8229
+ }
8230
+ return res.status(409).json({ error: getErrorMessage(error) });
8231
+ }
8232
+ });
8233
+ app.get('/projects/:id/check-repairs/:repairRunId', (req, res) => {
8234
+ const project = getProjectById(req.params.id);
8235
+ if (!project) {
8236
+ return res.status(404).json({ error: 'Not found' });
8237
+ }
8238
+ const repair = getCheckRepairDetails(project.id, req.params.repairRunId);
8239
+ return repair ? res.json(repair) : res.status(404).json({ error: 'Check repair not found' });
8240
+ });
8241
+ app.post('/projects/:id/check-repairs/:repairRunId/stop', async (req, res) => {
8242
+ if (!getProjectById(req.params.id)) {
8243
+ return res.status(404).json({ error: 'Not found' });
8244
+ }
8245
+ try {
8246
+ return res.json(await stopCheckRepair(req.params.id, req.params.repairRunId, io));
8247
+ }
8248
+ catch (error) {
8249
+ if (error instanceof CheckRepairError) {
8250
+ return res.status(error.status).json({ error: error.message });
8251
+ }
8252
+ return res.status(409).json({ error: getErrorMessage(error) });
8253
+ }
8254
+ });
8255
+ app.post('/projects/:id/check-repairs/:repairRunId/retry', async (req, res) => {
8256
+ if (!getProjectById(req.params.id)) {
8257
+ return res.status(404).json({ error: 'Not found' });
8258
+ }
8259
+ if (req.body && typeof req.body === 'object' && Object.keys(req.body).length > 0) {
8260
+ return res.status(400).json({ error: 'Check repair retries do not accept client-supplied repair context' });
8261
+ }
8262
+ try {
8263
+ return res.status(202).json(await retryCheckRepair(req.params.id, req.params.repairRunId, io));
8264
+ }
8265
+ catch (error) {
8266
+ if (error instanceof CheckRepairError) {
8267
+ return res.status(error.status).json({ error: error.message });
8268
+ }
8269
+ return res.status(409).json({ error: getErrorMessage(error) });
8270
+ }
8271
+ });
8134
8272
  const parseCheckDefinitionIds = (value) => {
8135
8273
  if (!Array.isArray(value) || value.length === 0 || value.some(id => typeof id !== 'string' || id.trim() === '')) {
8136
8274
  return null;
@@ -8230,12 +8368,47 @@ app.get('/projects/:id/check-batches/:batchRunId', async (req, res) => {
8230
8368
  attempts: await hydrateCheckAttemptCurrency(project, batch, getCheckBatchAttempts(req.params.id, req.params.batchRunId)),
8231
8369
  });
8232
8370
  });
8233
- app.get('/projects/:id/check-gate-evaluations/:evaluationId', (req, res) => {
8371
+ app.get('/projects/:id/check-batches/:batchRunId/diagnostics', async (req, res) => {
8372
+ const project = getProjectById(req.params.id);
8373
+ if (!project) {
8374
+ return res.status(404).json({ error: 'Not found' });
8375
+ }
8376
+ const diagnostics = getCheckBatchDiagnosticResponse(project.id, req.params.batchRunId);
8377
+ if (!diagnostics) {
8378
+ return res.status(404).json({ error: 'Check batch not found' });
8379
+ }
8380
+ return res.json({
8381
+ ...diagnostics,
8382
+ repair: await getCheckRepairCandidateState(project.id, req.params.batchRunId),
8383
+ latest_repair: getLatestCheckRepairForBatch(project.id, req.params.batchRunId),
8384
+ });
8385
+ });
8386
+ app.get('/projects/:id/check-attempts/:attemptRunId/diagnostics', (req, res) => {
8387
+ const project = getProjectById(req.params.id);
8388
+ if (!project) {
8389
+ return res.status(404).json({ error: 'Not found' });
8390
+ }
8391
+ const diagnostics = getCheckAttemptDiagnosticResponse(project.id, req.params.attemptRunId);
8392
+ if (!diagnostics) {
8393
+ return res.status(404).json({ error: 'Check attempt diagnostics not found' });
8394
+ }
8395
+ return res.json(diagnostics);
8396
+ });
8397
+ app.get('/projects/:id/check-gate-evaluations/:evaluationId', async (req, res) => {
8398
+ const project = getProjectById(req.params.id);
8399
+ if (!project) {
8400
+ return res.status(404).json({ error: 'Not found' });
8401
+ }
8234
8402
  const evaluation = getCheckGateEvaluation(req.params.id, req.params.evaluationId);
8235
8403
  if (!evaluation) {
8236
8404
  return res.status(404).json({ error: 'Check gate evaluation not found' });
8237
8405
  }
8238
- return res.json(evaluation);
8406
+ const workspace = resolveExplicitProjectTestWorkspace(project, evaluation.target.targetKind, evaluation.target.targetBundleId);
8407
+ const currentWorkspaceIdentity = 'error' in workspace
8408
+ ? null
8409
+ : await captureCheckWorkspaceIdentity(workspace.cwd);
8410
+ const currentPolicyResolution = resolveRequiredCheckPolicy(project, { boundaryKind: evaluation.boundaryKind });
8411
+ return res.json(hydrateCheckGatePresentApplicability(evaluation, currentWorkspaceIdentity, currentPolicyResolution));
8239
8412
  });
8240
8413
  app.post('/projects/:id/check-batches/:batchRunId/stop', async (req, res) => {
8241
8414
  if (!getCheckBatchDetails(req.params.id, req.params.batchRunId)) {