@straiffi/archon 1.10.9 → 1.10.12

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 (165) hide show
  1. package/dist/client/assets/ActivityView-BFn_Nhj4.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-iZD86Yju.js +1 -0
  3. package/dist/client/assets/VerificationDialog-CqKE8t1d.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BAIGV9Mj.js → architectureDiagram-3BPJPVTR-C8KefY67.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-B-5v5jSZ.js → blockDiagram-GPEHLZMM-BO2Q_Osr.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-C6wU415o.js → c4Diagram-AAUBKEIU-DGPkhbrK.js} +1 -1
  7. package/dist/client/assets/channel-DXKn3TOv.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-COmvX2tP.js → chunk-2J33WTMH-Dtb7Xu8C.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-BlANptdY.js → chunk-3OPIFGDE-Bnf4GbOF.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-ETyA2_GU.js → chunk-5ZQYHXKU-BWGid3g7.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-D5-DudwF.js → chunk-727SXJPM-ScA8IRGP.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-k1AJ4sA3.js → chunk-AQP2D5EJ-Bygg04Ip.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-jfksXwY9.js → chunk-CSCIHK7Q-CxdDDtfN.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-CHj0FDj9.js → chunk-KSCS5N6A-BgpZFsB5.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-1ODGxXhG.js → chunk-L5ZTLDWV-CKSew0OL.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-Bgh9X2W0.js → chunk-LZXEDZCA-DVG9Rx0K.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-DDkb3sp4.js → chunk-ND2GUHAM-CHBqNmuF.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-B6B8PZ_-.js → chunk-NZK2D7GU-DvZYWn7V.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-DhiUIFgj.js → chunk-O5CBEL6O-BL7lRTSe.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-Dzd0fbP3.js → chunk-WU5MYG2G-C-il2wHZ.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-U0fU20hT.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C6bQNSxL.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DoNDMCXI.js → dagre-BM42HDAG-B00l5tLi.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-ETDXSa5q.js → diagram-2AECGRRQ-Tum6g1_5.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CgTUep7e.js → diagram-5GNKFQAL-BK1GX5jc.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-BKTx2iRe.js → diagram-KO2AKTUF-BrzmD5NB.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-9iEiScHT.js → diagram-LMA3HP47-BhrlgXiJ.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-UIZRh7Qa.js → diagram-OG6HWLK6-K-fWsJ3a.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-BpNJmJva.js → erDiagram-TEJ5UH35-Der6PjHE.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-C-rK0XZ1.js → flowDiagram-I6XJVG4X-BVWgGhXe.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DWyfPvv_.js → ganttDiagram-6RSMTGT7-DcoZ5ggj.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-x71bzdc_.js → gitGraphDiagram-PVQCEYII-BhhbMTBu.js} +1 -1
  33. package/dist/client/assets/index-B6BltHSr.css +2 -0
  34. package/dist/client/assets/index-CFOYm3j9.js +276 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-CbRus0Xu.js → infoDiagram-5YYISTIA-Lwc-agyc.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DvaMHrgN.js → ishikawaDiagram-YF4QCWOH-B95U1Xcl.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BQBu4YGn.js → journeyDiagram-JHISSGLW-xFpigy5D.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-8rU-U7L1.js → kanban-definition-UN3LZRKU-D3mudQnR.js} +1 -1
  39. package/dist/client/assets/{line-CSZnwjLA.js → line-DtTL3ZI2.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-CTOl2slE.js → mermaid-parser.core-Dau7K6zV.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-B9gr0Vqf.js → mermaid.core-B8qtvATu.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-But1_PWQ.js → mindmap-definition-RKZ34NQL-Cus04Yq3.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-CnlVmfSG.js → pieDiagram-4H26LBE5-BCMGpBnn.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Dww1ZGuR.js → quadrantDiagram-W4KKPZXB-CAmOr-PJ.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CNf_loe8.js → requirementDiagram-4Y6WPE33-kisD3jc4.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBhi37Jj.js → sankeyDiagram-5OEKKPKP-CsCedmM0.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B64hb4E4.js → sequenceDiagram-3UESZ5HK-B-dAi954.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DcS6oB-S.js → stateDiagram-AJRCARHV-X0BnedIW.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-6B8YH8Y2.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-Dq-4tXfh.js → timeline-definition-PNZ67QCA-DfFoSjhr.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CeDklSE_.js → vennDiagram-CIIHVFJN-CKSCBwhG.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-D2cJISMm.js → wardleyDiagram-YWT4CUSO-DNIBQmHO.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CBF-tTpF.js → xychartDiagram-2RQKCTM6-FvB55TQN.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +368 -5
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +194 -250
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agentRuns.js +14 -1
  60. package/dist/server/lib/agentRuns.js.map +1 -1
  61. package/dist/server/lib/automationLoopProjection.js +55 -12
  62. package/dist/server/lib/automationLoopProjection.js.map +1 -1
  63. package/dist/server/lib/automationProgression.js +2 -5
  64. package/dist/server/lib/automationProgression.js.map +1 -1
  65. package/dist/server/lib/automationResumeDecision.js +19 -1
  66. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  67. package/dist/server/lib/buildControlEligibility.js +4 -0
  68. package/dist/server/lib/buildControlEligibility.js.map +1 -0
  69. package/dist/server/lib/buildExecutionAdmission.js +76 -0
  70. package/dist/server/lib/buildExecutionAdmission.js.map +1 -0
  71. package/dist/server/lib/buildSequenceResume.js +79 -0
  72. package/dist/server/lib/buildSequenceResume.js.map +1 -0
  73. package/dist/server/lib/buildWorkload.js +11 -0
  74. package/dist/server/lib/buildWorkload.js.map +1 -0
  75. package/dist/server/lib/checkAutoFixDetails.js +36 -1
  76. package/dist/server/lib/checkAutoFixDetails.js.map +1 -1
  77. package/dist/server/lib/executionEnvironmentReconciliation.js +15 -2
  78. package/dist/server/lib/executionEnvironmentReconciliation.js.map +1 -1
  79. package/dist/server/lib/executionEnvironments.js +15 -0
  80. package/dist/server/lib/executionEnvironments.js.map +1 -1
  81. package/dist/server/lib/executionRetry.js +13 -1
  82. package/dist/server/lib/executionRetry.js.map +1 -1
  83. package/dist/server/lib/executionRunAuthority.js +2 -2
  84. package/dist/server/lib/executionRunAuthority.js.map +1 -1
  85. package/dist/server/lib/executionTargets.js +7 -4
  86. package/dist/server/lib/executionTargets.js.map +1 -1
  87. package/dist/server/lib/executionTimeline.js +8 -13
  88. package/dist/server/lib/executionTimeline.js.map +1 -1
  89. package/dist/server/lib/executionTimelineHierarchy.js +83 -0
  90. package/dist/server/lib/executionTimelineHierarchy.js.map +1 -0
  91. package/dist/server/lib/failedBuildSequenceRecovery.js +133 -0
  92. package/dist/server/lib/failedBuildSequenceRecovery.js.map +1 -0
  93. package/dist/server/lib/hostRunSession.js +62 -0
  94. package/dist/server/lib/hostRunSession.js.map +1 -0
  95. package/dist/server/lib/hostServiceRecovery.js +159 -0
  96. package/dist/server/lib/hostServiceRecovery.js.map +1 -0
  97. package/dist/server/lib/hostServiceRunProvider.js +32 -0
  98. package/dist/server/lib/hostServiceRunProvider.js.map +1 -0
  99. package/dist/server/lib/mobileAccess.js +6 -2
  100. package/dist/server/lib/mobileAccess.js.map +1 -1
  101. package/dist/server/lib/projectAutoConfig.js +5 -2
  102. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  103. package/dist/server/lib/projectTargets.js +16 -2
  104. package/dist/server/lib/projectTargets.js.map +1 -1
  105. package/dist/server/lib/projects.js +25 -1
  106. package/dist/server/lib/projects.js.map +1 -1
  107. package/dist/server/lib/reviewFindingReconciliation.js +9 -2
  108. package/dist/server/lib/reviewFindingReconciliation.js.map +1 -1
  109. package/dist/server/lib/reviewFindings.js +82 -21
  110. package/dist/server/lib/reviewFindings.js.map +1 -1
  111. package/dist/server/lib/reviewRepairAssessmentRecords.js +41 -0
  112. package/dist/server/lib/reviewRepairAssessmentRecords.js.map +1 -0
  113. package/dist/server/lib/reviewRepairCompletion.js +78 -18
  114. package/dist/server/lib/reviewRepairCompletion.js.map +1 -1
  115. package/dist/server/lib/reviewRepairIterationRecords.js +4 -2
  116. package/dist/server/lib/reviewRepairIterationRecords.js.map +1 -1
  117. package/dist/server/lib/reviewRepairIterations.js +8 -2
  118. package/dist/server/lib/reviewRepairIterations.js.map +1 -1
  119. package/dist/server/lib/reviewRepairProgress.js +177 -0
  120. package/dist/server/lib/reviewRepairProgress.js.map +1 -0
  121. package/dist/server/lib/reviewRepairReconciliation.js +119 -140
  122. package/dist/server/lib/reviewRepairReconciliation.js.map +1 -1
  123. package/dist/server/lib/reviewRepairResumeOverride.js +55 -0
  124. package/dist/server/lib/reviewRepairResumeOverride.js.map +1 -0
  125. package/dist/server/lib/run.js +541 -504
  126. package/dist/server/lib/run.js.map +1 -1
  127. package/dist/server/lib/runProcessIdentity.js +71 -0
  128. package/dist/server/lib/runProcessIdentity.js.map +1 -0
  129. package/dist/server/lib/runProcesses.js +273 -0
  130. package/dist/server/lib/runProcesses.js.map +1 -0
  131. package/dist/server/lib/runServices.js +223 -0
  132. package/dist/server/lib/runServices.js.map +1 -0
  133. package/dist/server/lib/runSessionLifecycle.js +77 -0
  134. package/dist/server/lib/runSessionLifecycle.js.map +1 -0
  135. package/dist/server/lib/runStatus.js +8 -0
  136. package/dist/server/lib/runStatus.js.map +1 -0
  137. package/dist/server/lib/serviceRunCleanup.js +70 -0
  138. package/dist/server/lib/serviceRunCleanup.js.map +1 -0
  139. package/dist/server/lib/serviceRuns.js +458 -19
  140. package/dist/server/lib/serviceRuns.js.map +1 -1
  141. package/dist/server/lib/ticketAutomation.js +4 -27
  142. package/dist/server/lib/ticketAutomation.js.map +1 -1
  143. package/dist/server/lib/ticketRunOperations.js +103 -34
  144. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  145. package/dist/server/lib/tickets.js +0 -13
  146. package/dist/server/lib/tickets.js.map +1 -1
  147. package/dist/server/lib/types.js.map +1 -1
  148. package/dist/server/workers/build.js +44 -69
  149. package/dist/server/workers/build.js.map +1 -1
  150. package/dist/server/workers/plan.js +0 -7
  151. package/dist/server/workers/plan.js.map +1 -1
  152. package/dist/server/workers/review.js +95 -146
  153. package/dist/server/workers/review.js.map +1 -1
  154. package/package.json +1 -1
  155. package/dist/client/assets/ActivityView-CKvbDXeA.js +0 -1
  156. package/dist/client/assets/GitHubActionsDialog-Dzo0vtak.js +0 -1
  157. package/dist/client/assets/VerificationDialog-BJnOitrJ.js +0 -4
  158. package/dist/client/assets/channel-5mIy7nm1.js +0 -1
  159. package/dist/client/assets/classDiagram-4FO5ZUOK-BiQ-X2Sg.js +0 -1
  160. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CF-wCYW-.js +0 -1
  161. package/dist/client/assets/index-CIAO5aE_.css +0 -2
  162. package/dist/client/assets/index-nCGyalrn.js +0 -276
  163. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-PWeeL0tR.js +0 -1
  164. package/dist/server/lib/automationDeliveryDecision.js +0 -34
  165. package/dist/server/lib/automationDeliveryDecision.js.map +0 -1
@@ -70,8 +70,8 @@ import { abandonAutomationPause, completeAutomationProgression, completeAutomati
70
70
  import { reconcileAutomationStage } from './lib/automationReconciliation.js';
71
71
  import { runAutomationReviewGateOrPause } from './lib/automationReviewGate.js';
72
72
  import { pausedRunBelongsToProgression, resolveAutomationResumeDecision } from './lib/automationResumeDecision.js';
73
- import { resolveAutomationDeliveryDecision } from './lib/automationDeliveryDecision.js';
74
- import { isLegacyReviewRepairMigrationPause } from './lib/reviewRepairReconciliation.js';
73
+ import { isLegacyReviewRepairMigrationPause, recoverCompletedReviewRepairVerification } from './lib/reviewRepairReconciliation.js';
74
+ import { overrideNoProgressReviewRepairInTransaction } from './lib/reviewRepairResumeOverride.js';
75
75
  import { getAutomationStageDetails, } from './lib/automationStageDetails.js';
76
76
  import { buildAutomationFixInstruction, clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, isAutomationActive, isAutomationDepth, recordAutomationPause, resetBundleAutomationLoop, resolveAutomationConfig, } from './lib/ticketAutomation.js';
77
77
  import { resolveAutomationCheckGateResume, runAutomationCommitChain, runAutomationCommitOperation } from './lib/ticketAutomationCommit.js';
@@ -80,7 +80,7 @@ import { continueAfterBuildCheckpoint } from './lib/automationBuildContinuation.
80
80
  import { getDiscoveredSkills, normalizeSkillNames } from './lib/skills.js';
81
81
  import { clearAllPreviewProxies } from './lib/previewProxy.js';
82
82
  import { createExecutionRun, addExecutionRunLink, addExecutionRunLinks, getExecutionRun, getLifecycleEvent, hasActiveBuildExecutionRunForTicket, getGitHubActionsExecutionDetails, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listLifecycleEvents, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
83
- import { appendReviewRepairIterationDecisionInTransaction, attachReviewRepairResultInTransaction, attachReviewRepairActionEnvelopeInTransaction, attachReviewRepairActionInTransaction, claimReviewRepairIterationInTransaction, assertReviewRepairIterationDeletionEligible, deleteTerminalReviewRepairIterationsForTicketInTransaction, getActiveReviewRepairIteration, getReviewRepairIteration, pauseReviewRepairIteration, rewindStaleReviewRepairVerification, replaceReviewRepairActionEnvelopeRetryInTransaction, replaceReviewRepairActionRetryInTransaction, replaceReviewRepairVerificationRetryInTransaction, resetReviewRepairGateForRetry, resumePausedReviewRepairIteration, terminalizeReviewRepairIteration, terminalizeReviewRepairIterationInTransaction, } from './lib/reviewRepairIterations.js';
83
+ import { appendReviewRepairIterationDecisionInTransaction, attachReviewRepairActionEnvelopeInTransaction, attachReviewRepairActionInTransaction, claimReviewRepairIterationInTransaction, assertReviewRepairIterationDeletionEligible, deleteTerminalReviewRepairIterationsForTicketInTransaction, getActiveReviewRepairIteration, getReviewRepairIteration, pauseReviewRepairIteration, rewindStaleReviewRepairVerification, replaceReviewRepairActionEnvelopeRetryInTransaction, replaceReviewRepairActionRetryInTransaction, replaceReviewRepairVerificationRetryInTransaction, resetReviewRepairGateForRetry, resumePausedReviewRepairIteration, terminalizeReviewRepairIteration, terminalizeReviewRepairIterationInTransaction, } from './lib/reviewRepairIterations.js';
84
84
  import { dispatchExecutionRetry, getExecutionRetryEligibility, resolveExecutionRetry } from './lib/executionRetry.js';
85
85
  import { listExecutionTimeline } from './lib/executionTimeline.js';
86
86
  import { loadReviewAutomationLoopDetails } from './lib/automationLoopProjection.js';
@@ -105,7 +105,10 @@ import { refreshIfNeeded as refreshAssignedIssues, clearCachedAssignedIssues } f
105
105
  import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib/commitMessage.js';
106
106
  import { BUNDLE_PULL_REQUEST_SYNC_TTL_MS, getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, syncTrackedBundlePullRequestIfNeeded, upsertBundlePullRequest, upsertBundlePullRequestFromGitHub, } from './lib/bundlePullRequests.js';
107
107
  import { countInboundProjectLinks, getEffectiveProject, getProjectCheckDefinitions, getProjectById, getProjectTestCommands, getProjectTicketStats, hasProjectRunIde, listInterruptedProjectDeletions, listProjects, releaseInterruptedProjectDeletionClaims, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectRequiredCheckPolicyReferences, validateProjectPayload, } from './lib/projects.js';
108
- import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
108
+ import { forceReleaseProjectRunCleanup, getActiveRunStatus, getActiveRunStatusForProject, getLastRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
109
+ import { reconcilePendingServiceCleanups } from './lib/serviceRunCleanup.js';
110
+ import { getProjectServiceCleanupStatus, hasActiveProjectServiceRun } from './lib/serviceRuns.js';
111
+ import { projectRunStatus } from './lib/runStatus.js';
109
112
  import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
110
113
  import { closeCheckRunnerAdmissions, finalizeProductionCheckApproval, getCheckBatchAttempts, openCheckRunnerAdmissions, rerunCheckBatchAndSchedule, resumeProductionCheckApproval, shutdownCheckRunner, startCheckBatch, stopCheckBatch } from './lib/checkRunner.js';
111
114
  import { CheckRepairError, getCheckRepairCandidateState, getCheckRepairDetails, getLatestCheckRepairForBatch, retryCheckRepair, startCheckRepair, stopCheckRepair } from './lib/checkRepair.js';
@@ -421,7 +424,7 @@ const getCheckBatchRerunEligibility = (project, batch, selection) => {
421
424
  workspaceAvailable: !('error' in workspace),
422
425
  });
423
426
  };
424
- const buildExecutionRunDetailResponse = async (project, run) => {
427
+ const buildExecutionRunDetailResponse = async (project, run, scope) => {
425
428
  const checkBatch = run.kind === 'check_batch' ? getCheckBatchDetails(project.id, run.id) : null;
426
429
  const retryEligibility = checkBatch
427
430
  ? getCheckBatchRerunEligibility(project, checkBatch)
@@ -463,7 +466,7 @@ const buildExecutionRunDetailResponse = async (project, run) => {
463
466
  ? {
464
467
  kind: 'check_auto_fix',
465
468
  available: true,
466
- check_auto_fix: getCheckAutoFixDetails(project.id, run.id),
469
+ check_auto_fix: getCheckAutoFixDetails(project.id, run.id, scope),
467
470
  retry_eligibility: retryEligibility,
468
471
  action_kind: null,
469
472
  }
@@ -490,7 +493,7 @@ const buildExecutionRunDetailResponse = async (project, run) => {
490
493
  ...artifacts,
491
494
  environment_context: getExecutionEnvironmentContextForRun(project.id, run.id),
492
495
  automation_loops: run.kind === 'automation_progression'
493
- ? loadReviewAutomationLoopDetails(project.id, run.id)
496
+ ? loadReviewAutomationLoopDetails(project.id, run.id, scope)
494
497
  : [],
495
498
  };
496
499
  };
@@ -576,7 +579,26 @@ const cleanupWorktreeExecutionTarget = async ({ project, targetKind, targetKey,
576
579
  }
577
580
  return { branchWarning: buildBranchCleanupWarning(branch, deleteLocalBranch(branch, project)) };
578
581
  };
582
+ const assertTicketServiceTargetMutable = (ticketId) => {
583
+ db.transaction(() => {
584
+ const authority = db.prepare(`SELECT 1 FROM active_project_service_authorities active
585
+ JOIN execution_runs run ON run.id = active.execution_run_id
586
+ WHERE run.source_kind = 'ticket' AND run.source_id = ? LIMIT 1`).get(ticketId);
587
+ if (authority) {
588
+ throw new HttpError(409, 'Ticket has active service authority. Stop the service session and verify cleanup before changing or deleting its target.');
589
+ }
590
+ }).immediate();
591
+ };
579
592
  const deleteTicketRecord = async (ticket) => {
593
+ try {
594
+ assertTicketServiceTargetMutable(ticket.id);
595
+ }
596
+ catch (error) {
597
+ if (error instanceof HttpError) {
598
+ return { ok: false, status: error.status, error: error.message };
599
+ }
600
+ throw error;
601
+ }
580
602
  if (ticket.project_id) {
581
603
  try {
582
604
  assertReviewRepairIterationDeletionEligible(ticket.project_id, ticket.id);
@@ -667,6 +689,7 @@ const deleteTicketRecord = async (ticket) => {
667
689
  }
668
690
  try {
669
691
  withExecutionRunTransaction(transaction => {
692
+ assertTicketServiceTargetMutable(ticket.id);
670
693
  if (ticket.project_id) {
671
694
  deleteTerminalReviewRepairIterationsForTicketInTransaction(transaction, ticket.project_id, ticket.id);
672
695
  }
@@ -2799,11 +2822,9 @@ app.get('/run-config', (req, res) => {
2799
2822
  });
2800
2823
  app.get('/run-status', (req, res) => {
2801
2824
  const requestedProjectId = getRequestedProjectId(req);
2802
- return res.json({
2803
- active_run: requestedProjectId
2804
- ? getActiveRunStatusForProject(requestedProjectId)
2805
- : getActiveRunStatus(),
2806
- });
2825
+ const activeRun = requestedProjectId ? getActiveRunStatusForProject(requestedProjectId) : getActiveRunStatus();
2826
+ const durableAuthority = hasActiveProjectServiceRun(requestedProjectId);
2827
+ return res.json(projectRunStatus(activeRun, requestedProjectId ? getLastRunStatusForProject(requestedProjectId) : null, durableAuthority, requestedProjectId && !activeRun && durableAuthority ? getProjectServiceCleanupStatus(requestedProjectId) : null));
2807
2828
  });
2808
2829
  app.get('/preview-status', (req, res) => {
2809
2830
  const projectId = getRequestedProjectId(req);
@@ -4592,7 +4613,25 @@ app.post('/bundles/:id/undo-review', (req, res) => {
4592
4613
  }
4593
4614
  return res.json(undoResult.details);
4594
4615
  });
4595
- const parseServiceExecutionProviderChoice = (body) => {
4616
+ const parseServiceRunControl = (body) => {
4617
+ if (body === undefined) {
4618
+ return {};
4619
+ }
4620
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
4621
+ return { error: 'Run control body must be an object' };
4622
+ }
4623
+ const input = body;
4624
+ for (const field of ['service_id', 'session_id']) {
4625
+ if (input[field] !== undefined && (typeof input[field] !== 'string' || !input[field].trim())) {
4626
+ return { error: `${field} must be a non-empty string` };
4627
+ }
4628
+ }
4629
+ return {
4630
+ serviceId: typeof input.service_id === 'string' ? input.service_id.trim() : undefined,
4631
+ sessionId: typeof input.session_id === 'string' ? input.session_id.trim() : undefined,
4632
+ };
4633
+ };
4634
+ const parseServiceExecutionProviderChoice = (body, allowServiceControl = false) => {
4596
4635
  if (body === undefined || body === null) {
4597
4636
  return 'project_default';
4598
4637
  }
@@ -4600,7 +4639,8 @@ const parseServiceExecutionProviderChoice = (body) => {
4600
4639
  return null;
4601
4640
  }
4602
4641
  const request = body;
4603
- if (Object.keys(request).some(key => key !== 'execution_provider')) {
4642
+ if (Object.keys(request).some(key => key !== 'execution_provider'
4643
+ && !(allowServiceControl && (key === 'service_id' || key === 'session_id')))) {
4604
4644
  return null;
4605
4645
  }
4606
4646
  if (request.execution_provider === undefined || request.execution_provider === 'project_default') {
@@ -4659,8 +4699,17 @@ app.post('/bundles/:id/run', (req, res) => {
4659
4699
  return res.status(409).json(activeRunConflict);
4660
4700
  }
4661
4701
  const profileSelection = createServiceExecutionProfileSelection(project.id, executionProvider);
4702
+ try {
4703
+ void runTicketInWorkspace(targetTicket, io, project, workspace.cwd, profileSelection)
4704
+ .catch(error => console.error('[run] admitted bundle service startup failed:', error));
4705
+ }
4706
+ catch (error) {
4707
+ return res.status(409).json({
4708
+ error: error instanceof Error ? error.message : String(error),
4709
+ code: error instanceof Error && 'code' in error ? error.code : 'service_start_rejected',
4710
+ });
4711
+ }
4662
4712
  emitRunStateForContext(targetTicket, true);
4663
- runTicketInWorkspace(targetTicket, io, project, workspace.cwd, profileSelection);
4664
4713
  return res.json(serializeBundleDetails(result.project, bundle));
4665
4714
  });
4666
4715
  app.post('/bundles/:id/stop', async (req, res) => {
@@ -4700,52 +4749,6 @@ app.post('/bundles/:id/follow-up', (req, res) => {
4700
4749
  });
4701
4750
  const AUTOMATION_SIDE_EFFECT_KINDS = new Set(['automation_commit', 'automation_push', 'automation_pull_request']);
4702
4751
  const AUTOMATION_WORKER_PAUSE_KINDS = new Set(['build', 'review', 'build_follow_up', 'automation_review_fix', 'check_auto_fix', 'check_repair', 'check_batch']);
4703
- app.post('/bundles/:id/continue-delivery', async (req, res) => {
4704
- const result = getRequiredProject(req);
4705
- if ('error' in result) {
4706
- return res.status(400).json({ error: result.error });
4707
- }
4708
- const bundle = getBundle(req.params.id, result.project.id);
4709
- if (!bundle) {
4710
- return res.status(404).json({ error: 'Bundle not found' });
4711
- }
4712
- const state = getBundleAutomationState(result.project.id, bundle.id);
4713
- const tickets = listDetailedBundleTickets(result.project.id, bundle.id);
4714
- if (!state) {
4715
- return res.status(409).json({ error: 'Bundle delivery is not ready to continue' });
4716
- }
4717
- const automationConfig = resolveAutomationConfig(bundle, result.project);
4718
- const decision = resolveAutomationDeliveryDecision({
4719
- projectId: result.project.id,
4720
- bundleId: bundle.id,
4721
- state,
4722
- config: automationConfig,
4723
- tickets,
4724
- });
4725
- if (!decision) {
4726
- return res.status(409).json({ error: 'Bundle delivery is not ready to continue' });
4727
- }
4728
- const owner = tickets.find(ticket => ticket.id === decision.owner_ticket_id);
4729
- const reviewStageRunId = state.automation_delivery_ready_stage_run_id;
4730
- if (!owner || !reviewStageRunId) {
4731
- return res.status(409).json({ error: 'Bundle delivery owner is no longer available' });
4732
- }
4733
- try {
4734
- const progression = ensureAutomationProgression({
4735
- projectId: result.project.id,
4736
- bundleId: bundle.id,
4737
- ticketId: owner.id,
4738
- ticketTitle: owner.title,
4739
- parentRunId: reviewStageRunId,
4740
- broadcaster: io,
4741
- });
4742
- await runAutomationCommitChain(owner, result.project, owner.bundle ?? bundle, automationConfig, io, progression.runId, undefined, reviewStageRunId);
4743
- }
4744
- catch (error) {
4745
- return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
4746
- }
4747
- return res.json(serializeBundleDetails(result.project, bundle));
4748
- });
4749
4752
  app.post('/bundles/:id/resume-automation', async (req, res) => {
4750
4753
  const result = getRequiredProject(req);
4751
4754
  if ('error' in result) {
@@ -4785,7 +4788,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4785
4788
  : 'Bundle automation cannot be resumed in its current state';
4786
4789
  return res.status(409).json({ error, automation_resume_decision: initialDecision });
4787
4790
  }
4788
- let decision = initialDecision;
4791
+ const decision = initialDecision;
4789
4792
  const revalidateDecision = () => {
4790
4793
  const current = resolveCurrentDecision();
4791
4794
  return current?.status === 'enabled'
@@ -5001,9 +5004,21 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5001
5004
  }
5002
5005
  const automationConfig = resolveAutomationConfig(owner.bundle ?? null, project);
5003
5006
  const legacyReviewRepairPause = isLegacyReviewRepairMigrationPause(result.project.id, bundle.id, progressionRunId);
5004
- let completedVerificationIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5005
- let reconciledSourceReviewRunId = null;
5006
- if (completedVerificationIteration?.phase === 'paused') {
5007
+ const completedVerificationIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5008
+ if (completedVerificationIteration
5009
+ && ['paused', 'verification_active'].includes(completedVerificationIteration.phase)
5010
+ && completedVerificationIteration.stop_reason !== 'no_progress'
5011
+ && completedVerificationIteration.resulting_review_execution_run_id
5012
+ && getExecutionRun(result.project.id, completedVerificationIteration.resulting_review_execution_run_id)?.status === 'completed') {
5013
+ try {
5014
+ await recoverCompletedReviewRepairVerification(result.project.id, completedVerificationIteration.id, io, { resume: true });
5015
+ return res.json(serializeBundleDetails(result.project, bundle));
5016
+ }
5017
+ catch (error) {
5018
+ return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
5019
+ }
5020
+ }
5021
+ if (completedVerificationIteration?.phase === 'paused' && completedVerificationIteration.stop_reason !== 'no_progress') {
5007
5022
  withExecutionRunTransaction(transaction => {
5008
5023
  appendReviewRepairIterationDecisionInTransaction(transaction, {
5009
5024
  projectId: result.project.id,
@@ -5014,165 +5029,6 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5014
5029
  });
5015
5030
  }, io, 'immediate');
5016
5031
  }
5017
- if (completedVerificationIteration?.phase === 'paused') {
5018
- const verificationRunId = completedVerificationIteration.resulting_review_execution_run_id;
5019
- const verificationRun = verificationRunId ? getExecutionRun(result.project.id, verificationRunId) : null;
5020
- if (verificationRun?.status === 'completed') {
5021
- try {
5022
- completedVerificationIteration = resumePausedReviewRepairIteration({
5023
- projectId: result.project.id,
5024
- iterationId: completedVerificationIteration.id,
5025
- expectedCurrentRunId: verificationRunId,
5026
- broadcaster: io,
5027
- });
5028
- }
5029
- catch (error) {
5030
- const current = getReviewRepairIteration(result.project.id, completedVerificationIteration.id);
5031
- if (current?.resulting_review_execution_run_id === verificationRunId
5032
- && ['verification_active', 'terminal'].includes(current.phase)) {
5033
- return res.json(serializeBundleDetails(result.project, bundle));
5034
- }
5035
- return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
5036
- }
5037
- }
5038
- }
5039
- if (completedVerificationIteration?.phase === 'verification_active'
5040
- && completedVerificationIteration.resulting_review_execution_run_id) {
5041
- const verificationRunId = completedVerificationIteration.resulting_review_execution_run_id;
5042
- const verificationRun = getExecutionRun(result.project.id, verificationRunId);
5043
- const resultingReview = verificationRun?.status === 'completed'
5044
- ? db.prepare(`
5045
- SELECT review.id
5046
- FROM review_runs review
5047
- JOIN execution_run_links result_link
5048
- ON result_link.execution_run_id = ?
5049
- AND result_link.project_id = review.project_id
5050
- AND result_link.entity_type = 'review_run'
5051
- AND result_link.entity_id = review.id
5052
- AND result_link.relationship = 'result'
5053
- WHERE review.project_id = ? AND review.ticket_id = ?
5054
- AND review.worktree_bundle_id = ? AND review.status = 'completed'
5055
- LIMIT 1
5056
- `).get(verificationRunId, result.project.id, owner.id, bundle.id)
5057
- : undefined;
5058
- if (resultingReview) {
5059
- reconciledSourceReviewRunId = resultingReview.id;
5060
- const blockingCount = countBlockingFindings(listReviewFindings(resultingReview.id), automationConfig);
5061
- const terminal = !isAutomationActive(automationConfig)
5062
- ? { outcome: 'interrupted', stopReason: 'configuration_changed' }
5063
- : blockingCount === 0
5064
- ? { outcome: 'repaired', stopReason: 'no_blocking_findings' }
5065
- : automationConfig.reviewFixEnabled
5066
- ? completedVerificationIteration.ordinal < automationConfig.reviewMaxIterations
5067
- ? { outcome: 'continuing', stopReason: null }
5068
- : { outcome: 'exhausted', stopReason: 'max_iterations' }
5069
- : { outcome: 'interrupted', stopReason: 'automation_disabled' };
5070
- try {
5071
- withExecutionRunTransaction(transaction => {
5072
- if (!completedVerificationIteration.resulting_review_run_id) {
5073
- attachReviewRepairResultInTransaction(transaction, {
5074
- projectId: result.project.id,
5075
- iterationId: completedVerificationIteration.id,
5076
- expectedPhase: 'verification_active',
5077
- expectedVerificationRunId: verificationRunId,
5078
- resultingReviewRunId: resultingReview.id,
5079
- });
5080
- }
5081
- appendReviewRepairIterationDecisionInTransaction(transaction, {
5082
- projectId: result.project.id,
5083
- iterationId: completedVerificationIteration.id,
5084
- decisionPoint: 'verification_completion',
5085
- effectiveAutomationConfig: automationConfig,
5086
- decisionCode: 'verification_reconciled_on_resume',
5087
- operationRunId: verificationRunId,
5088
- });
5089
- terminalizeReviewRepairIterationInTransaction(transaction, {
5090
- projectId: result.project.id,
5091
- iterationId: completedVerificationIteration.id,
5092
- expectedPhase: 'verification_active',
5093
- expectedCurrentRunId: verificationRunId,
5094
- outcome: terminal.outcome,
5095
- stopReason: terminal.stopReason,
5096
- resultingBlockingCount: blockingCount,
5097
- });
5098
- if (automationConfig.depth === 'manual') {
5099
- completeAutomationProgressionInTransaction(transaction, {
5100
- projectId: result.project.id,
5101
- bundleId: bundle.id,
5102
- progressionRunId,
5103
- domainStatus: 'manual_boundary_reached',
5104
- });
5105
- }
5106
- if (terminal.outcome === 'continuing') {
5107
- claimReviewRepairIterationInTransaction(transaction, {
5108
- projectId: result.project.id,
5109
- worktreeBundleId: bundle.id,
5110
- ownerTicketId: owner.id,
5111
- automationProgressionRunId: progressionRunId,
5112
- triggeringReviewExecutionRunId: verificationRunId,
5113
- triggeringReviewRunId: resultingReview.id,
5114
- triggeringBlockingCount: blockingCount,
5115
- effectiveAutomationConfig: automationConfig,
5116
- decisionCode: 'resume_verification_repair_admitted',
5117
- });
5118
- }
5119
- }, io, 'immediate');
5120
- }
5121
- catch (error) {
5122
- const current = getReviewRepairIteration(result.project.id, completedVerificationIteration.id);
5123
- if (current?.phase === 'terminal'
5124
- && current.resulting_review_execution_run_id === verificationRunId
5125
- && current.resulting_review_run_id === resultingReview.id) {
5126
- return res.json(serializeBundleDetails(result.project, bundle));
5127
- }
5128
- return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
5129
- }
5130
- if (terminal.outcome === 'continuing') {
5131
- const continuationDecision = resolveCurrentDecision();
5132
- if (!continuationDecision || continuationDecision.status !== 'enabled') {
5133
- return res.status(409).json({ error: 'Bundle automation state changed before repair continuation could start' });
5134
- }
5135
- decision = continuationDecision;
5136
- }
5137
- else if (terminal.outcome === 'repaired') {
5138
- resetBundleAutomationLoop(result.project.id, bundle.id);
5139
- runAutomationProgression({
5140
- projectId: result.project.id,
5141
- bundleId: bundle.id,
5142
- progressionRunId,
5143
- broadcaster: io,
5144
- });
5145
- if (depthAtLeast(automationConfig.depth, 'auto_commit')) {
5146
- await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io, progressionRunId, undefined, verificationRunId);
5147
- }
5148
- else {
5149
- completeAutomationProgression({
5150
- projectId: result.project.id,
5151
- bundleId: bundle.id,
5152
- progressionRunId,
5153
- broadcaster: io,
5154
- });
5155
- }
5156
- return res.json(serializeBundleDetails(result.project, bundle));
5157
- }
5158
- else if (automationConfig.depth !== 'manual') {
5159
- waitAutomationProgression({
5160
- projectId: result.project.id,
5161
- bundleId: bundle.id,
5162
- progressionRunId,
5163
- pauseCode: 'review',
5164
- pauseReason: 'Blocking Review findings remain after repair verification.',
5165
- reasonCode: 'precondition_failed',
5166
- pausedStageRunId: verificationRunId,
5167
- broadcaster: io,
5168
- });
5169
- return res.json(serializeBundleDetails(result.project, bundle));
5170
- }
5171
- else {
5172
- return res.json(serializeBundleDetails(result.project, bundle));
5173
- }
5174
- }
5175
- }
5176
5032
  const restoreWaitingProgression = () => {
5177
5033
  waitAutomationProgression({
5178
5034
  projectId: result.project.id,
@@ -5365,7 +5221,16 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5365
5221
  }
5366
5222
  const workflowContext = validateWorkflowContext(progression.metadata);
5367
5223
  let existingIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5368
- const sourceReviewRunId = reconciledSourceReviewRunId ?? existingIteration?.triggering_review_run_id ?? null;
5224
+ const overridingNoProgress = existingIteration?.phase === 'paused' && existingIteration.stop_reason === 'no_progress';
5225
+ if (overridingNoProgress) {
5226
+ existingIteration = withExecutionRunTransaction(transaction => {
5227
+ if (!revalidateDecision()) {
5228
+ throw new Error('Bundle automation state changed before Resume override could start');
5229
+ }
5230
+ return overrideNoProgressReviewRepairInTransaction(transaction, result.project.id, existingIteration.id, io);
5231
+ }, io, 'immediate');
5232
+ }
5233
+ const sourceReviewRunId = existingIteration?.triggering_review_run_id ?? null;
5369
5234
  if (!sourceReviewRunId) {
5370
5235
  throw new Error('The completed review findings to resume are unavailable');
5371
5236
  }
@@ -5374,12 +5239,14 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5374
5239
  review_finding_run_id: sourceReviewRunId,
5375
5240
  review_severity: automationConfig.reviewSeverity,
5376
5241
  });
5377
- runAutomationProgression({
5378
- projectId: result.project.id,
5379
- bundleId: bundle.id,
5380
- progressionRunId,
5381
- broadcaster: io,
5382
- });
5242
+ if (!overridingNoProgress) {
5243
+ runAutomationProgression({
5244
+ projectId: result.project.id,
5245
+ bundleId: bundle.id,
5246
+ progressionRunId,
5247
+ broadcaster: io,
5248
+ });
5249
+ }
5383
5250
  if (existingIteration?.phase === 'paused') {
5384
5251
  existingIteration = resumePausedReviewRepairIteration({
5385
5252
  projectId: result.project.id,
@@ -5650,9 +5517,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5650
5517
  });
5651
5518
  }, io, 'immediate');
5652
5519
  }
5653
- if (bundleState.fix_loop_active === 1) {
5654
- resetBundleAutomationLoop(result.project.id, bundle.id);
5655
- }
5520
+ resetBundleAutomationLoop(result.project.id, bundle.id);
5656
5521
  }
5657
5522
  runAutomationProgression({
5658
5523
  projectId: result.project.id,
@@ -8201,14 +8066,6 @@ app.patch('/projects/:id', async (req, res) => {
8201
8066
  if ('error' in roleModelValidation) {
8202
8067
  return res.status(400).json({ error: roleModelValidation.error });
8203
8068
  }
8204
- if (hasOwn(validation.values, 'repo_path') && validation.values.repo_path !== project.repo_path) {
8205
- const stats = getProjectTicketStats(project.id);
8206
- if (stats.ticketCount > 0 || stats.worktreeCount > 0 || stats.bundleCount > 0) {
8207
- return res.status(409).json({
8208
- error: 'repo_path cannot be changed after tickets, bundles, or worktrees exist for this project',
8209
- });
8210
- }
8211
- }
8212
8069
  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;
8213
8070
  const updateProject = db.prepare(`
8214
8071
  UPDATE projects SET
@@ -8266,7 +8123,7 @@ app.patch('/projects/:id', async (req, res) => {
8266
8123
  const nextReviewerVariant = nextReviewerModel
8267
8124
  ? (hasOwn(validation.values, 'reviewer_variant') ? reviewer_variant ?? null : project.reviewer_variant)
8268
8125
  : null;
8269
- const updateResult = db.transaction(() => {
8126
+ const applyUpdate = db.transaction(() => {
8270
8127
  const currentProject = getProjectById(project.id);
8271
8128
  if (!currentProject || JSON.stringify(currentProject) !== JSON.stringify(project)) {
8272
8129
  return {
@@ -8274,6 +8131,15 @@ app.patch('/projects/:id', async (req, res) => {
8274
8131
  status: 409,
8275
8132
  };
8276
8133
  }
8134
+ if (hasOwn(validation.values, 'repo_path') && repo_path !== currentProject.repo_path) {
8135
+ if (currentProject.deletion_started_at || hasActiveProjectServiceRun(project.id) || getActiveRunStatusForProject(project.id)) {
8136
+ return { error: 'Cannot change project repository while service authority or deletion is active', status: 409 };
8137
+ }
8138
+ const stats = getProjectTicketStats(project.id);
8139
+ if (stats.ticketCount > 0 || stats.worktreeCount > 0 || stats.bundleCount > 0) {
8140
+ return { error: 'repo_path cannot be changed after tickets, bundles, or worktrees exist for this project', status: 409 };
8141
+ }
8142
+ }
8277
8143
  const policyReferences = validateProjectRequiredCheckPolicyReferences({
8278
8144
  projectId: project.id,
8279
8145
  checkDefinitionsJson: nextCheckDefinitions,
@@ -8305,7 +8171,17 @@ app.patch('/projects/:id', async (req, res) => {
8305
8171
  replaceProjectLinks(project.id, linked_project_ids ?? []);
8306
8172
  }
8307
8173
  return { ok: true };
8308
- }).immediate();
8174
+ });
8175
+ let updateResult;
8176
+ try {
8177
+ updateResult = applyUpdate.immediate();
8178
+ }
8179
+ catch (error) {
8180
+ if (error instanceof Error && 'code' in error && error.code === 'SQLITE_CONSTRAINT_TRIGGER') {
8181
+ return res.status(409).json({ error: error.message });
8182
+ }
8183
+ throw error;
8184
+ }
8309
8185
  if ('error' in updateResult) {
8310
8186
  const status = 'status' in updateResult && typeof updateResult.status === 'number'
8311
8187
  ? updateResult.status
@@ -8316,6 +8192,9 @@ app.patch('/projects/:id', async (req, res) => {
8316
8192
  res.json(serializeProject(updated ?? null));
8317
8193
  });
8318
8194
  const assertProjectDeletionEligible = (projectId, claimId) => {
8195
+ if (hasActiveProjectServiceRun(projectId) || getActiveRunStatusForProject(projectId)) {
8196
+ throw new ProjectDeletionError(409, 'Cannot delete a project while services are active');
8197
+ }
8319
8198
  const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
8320
8199
  if (!project) {
8321
8200
  throw new ProjectDeletionError(404, 'Not found');
@@ -8363,6 +8242,7 @@ const assertProjectDeletionEligible = (projectId, claimId) => {
8363
8242
  }
8364
8243
  };
8365
8244
  const completeClaimedProjectDeletion = async (projectId, claimId) => {
8245
+ assertProjectDeletionEligible(projectId, claimId);
8366
8246
  const marked = db.prepare(`
8367
8247
  UPDATE projects
8368
8248
  SET deletion_cleanup_started_at = COALESCE(deletion_cleanup_started_at, CURRENT_TIMESTAMP)
@@ -8599,6 +8479,17 @@ app.patch('/tickets/:id', async (req, res) => {
8599
8479
  const nextProjectId = hasProjectField ? project_id : ticket.project_id;
8600
8480
  const isProjectChanging = nextProjectId !== ticket.project_id;
8601
8481
  const hasStandaloneWorktree = typeof ticket.branch === 'string' && ticket.branch.trim() !== '';
8482
+ if (isProjectChanging || (hasBundleField && req.body.worktree_bundle_id !== ticket.worktree_bundle_id)) {
8483
+ try {
8484
+ assertTicketServiceTargetMutable(ticket.id);
8485
+ }
8486
+ catch (error) {
8487
+ if (error instanceof HttpError) {
8488
+ return res.status(error.status).json({ error: error.message });
8489
+ }
8490
+ throw error;
8491
+ }
8492
+ }
8602
8493
  if (isProjectChanging && hasStandaloneWorktree) {
8603
8494
  return res.status(409).json({ error: 'project_id cannot be changed after a worktree has been created' });
8604
8495
  }
@@ -8708,10 +8599,23 @@ app.patch('/tickets/:id', async (req, res) => {
8708
8599
  }
8709
8600
  }
8710
8601
  }
8711
- if (isProjectChanging) {
8712
- deleteTicketDependenciesForTicket(ticket.id);
8602
+ try {
8603
+ db.transaction(() => {
8604
+ if (bundleChanged) {
8605
+ assertTicketServiceTargetMutable(ticket.id);
8606
+ }
8607
+ if (isProjectChanging) {
8608
+ deleteTicketDependenciesForTicket(ticket.id);
8609
+ }
8610
+ db.prepare('UPDATE tickets SET title = ?, description = ?, model = ?, variant = ?, skills_json = ?, worktree_bundle_id = ?, project_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(title?.trim() ?? ticket.title, description ?? ticket.description, resolvedModel, resolvedVariant, JSON.stringify(resolvedSkills), resolvedBundleId, nextProjectId, req.params.id);
8611
+ }).immediate();
8612
+ }
8613
+ catch (error) {
8614
+ if (error instanceof HttpError) {
8615
+ return res.status(error.status).json({ error: error.message });
8616
+ }
8617
+ throw error;
8713
8618
  }
8714
- db.prepare('UPDATE tickets SET title = ?, description = ?, model = ?, variant = ?, skills_json = ?, worktree_bundle_id = ?, project_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(title?.trim() ?? ticket.title, description ?? ticket.description, resolvedModel, resolvedVariant, JSON.stringify(resolvedSkills), resolvedBundleId, nextProjectId, req.params.id);
8715
8619
  upsertTicketDescriptionMessage(req.params.id, description ?? ticket.description);
8716
8620
  const updated = getTicket(req.params.id);
8717
8621
  if (bundleChanged && previousBundleId && previousProject) {
@@ -9133,7 +9037,12 @@ app.post('/tickets/:id/undo', (req, res) => {
9133
9037
  return res.json(result.ticket);
9134
9038
  });
9135
9039
  app.post('/tickets/:id/run', (req, res) => {
9040
+ const control = parseServiceRunControl(req.body);
9041
+ if ('error' in control) {
9042
+ return res.status(400).json({ error: control.error });
9043
+ }
9136
9044
  const result = runTicketRun({
9045
+ ...control,
9137
9046
  ticketId: req.params.id,
9138
9047
  requestedProjectId: getRequestedProjectId(req),
9139
9048
  broadcaster: io,
@@ -9394,11 +9303,16 @@ app.post('/projects/:id/open-ide', (req, res) => {
9394
9303
  return res.json({ ok: true });
9395
9304
  });
9396
9305
  app.post('/projects/:id/run', (req, res) => {
9397
- const executionProvider = parseServiceExecutionProviderChoice(req.body);
9306
+ const control = parseServiceRunControl(req.body);
9307
+ if ('error' in control) {
9308
+ return res.status(400).json({ error: control.error });
9309
+ }
9310
+ const executionProvider = parseServiceExecutionProviderChoice(req.body, true);
9398
9311
  if (!executionProvider) {
9399
9312
  return res.status(400).json({ error: 'execution_provider must be project_default or host' });
9400
9313
  }
9401
9314
  const result = runProjectTarget({
9315
+ ...control,
9402
9316
  projectId: req.params.id,
9403
9317
  executionProvider,
9404
9318
  });
@@ -9883,7 +9797,7 @@ app.get('/projects/:id/execution-timeline/:runId', async (req, res) => {
9883
9797
  if (scope.scopeEntity && !links.some(link => link.entity_type === scope.scopeEntity?.type && link.entity_id === scope.scopeEntity.id)) {
9884
9798
  return res.status(404).json({ error: 'Execution run not found' });
9885
9799
  }
9886
- return res.json(await buildExecutionRunDetailResponse(project, run));
9800
+ return res.json(await buildExecutionRunDetailResponse(project, run, scope.scopeEntity));
9887
9801
  });
9888
9802
  app.get('/projects/:id/execution-runs/:runId', async (req, res) => {
9889
9803
  const project = getProjectById(req.params.id);
@@ -10131,7 +10045,12 @@ app.post('/terminals/:sessionId/destroy', (req, res) => {
10131
10045
  return res.status(204).send();
10132
10046
  });
10133
10047
  app.post('/tickets/:id/stop', async (req, res) => {
10048
+ const control = parseServiceRunControl(req.body);
10049
+ if ('error' in control) {
10050
+ return res.status(400).json({ error: control.error });
10051
+ }
10134
10052
  const result = await stopTicketRun({
10053
+ ...control,
10135
10054
  ticketId: req.params.id,
10136
10055
  requestedProjectId: getRequestedProjectId(req),
10137
10056
  broadcaster: io,
@@ -10142,7 +10061,12 @@ app.post('/tickets/:id/stop', async (req, res) => {
10142
10061
  return res.status(result.status).json(result.value);
10143
10062
  });
10144
10063
  app.post('/projects/:id/stop-run', async (req, res) => {
10064
+ const control = parseServiceRunControl(req.body);
10065
+ if ('error' in control) {
10066
+ return res.status(400).json({ error: control.error });
10067
+ }
10145
10068
  const result = await stopProjectTargetRun({
10069
+ ...control,
10146
10070
  projectId: req.params.id,
10147
10071
  });
10148
10072
  if (!result.ok) {
@@ -10150,6 +10074,23 @@ app.post('/projects/:id/stop-run', async (req, res) => {
10150
10074
  }
10151
10075
  return res.status(result.status).json(result.value);
10152
10076
  });
10077
+ app.post('/projects/:id/service-cleanup/force-release', (req, res) => {
10078
+ if (req.body?.confirm !== true) {
10079
+ return res.status(400).json({ error: 'Force release requires explicit confirmation' });
10080
+ }
10081
+ if (!getProjectById(req.params.id)) {
10082
+ return res.status(404).json({ error: 'Not found' });
10083
+ }
10084
+ try {
10085
+ const released = forceReleaseProjectRunCleanup(req.params.id);
10086
+ return released
10087
+ ? res.json({ active_run: null })
10088
+ : res.status(409).json({ error: 'No pending service cleanup was found' });
10089
+ }
10090
+ catch (error) {
10091
+ return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
10092
+ }
10093
+ });
10153
10094
  app.post('/tickets/:id/stop-build', async (req, res) => {
10154
10095
  const result = await stopBuildTicketRun({
10155
10096
  ticketId: req.params.id,
@@ -10439,7 +10380,10 @@ const runExecutionEnvironmentLifecycleReconciliation = () => {
10439
10380
  if (executionEnvironmentReconciliationPromise) {
10440
10381
  return executionEnvironmentReconciliationPromise;
10441
10382
  }
10442
- executionEnvironmentReconciliationPromise = reconcileExecutionEnvironmentLifecycleOperations()
10383
+ executionEnvironmentReconciliationPromise = reconcilePendingServiceCleanups({
10384
+ isProjectOwnedInMemory: projectId => getActiveRunStatusForProject(projectId) !== null,
10385
+ })
10386
+ .then(() => reconcileExecutionEnvironmentLifecycleOperations())
10443
10387
  .then(() => undefined)
10444
10388
  .catch(error => {
10445
10389
  console.error('Failed to reconcile execution environment lifecycle operations', error);