@straiffi/archon 1.10.9 → 1.10.10

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 (159) hide show
  1. package/dist/client/assets/ActivityView-CRkM5TeN.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-KFUiR00H.js +1 -0
  3. package/dist/client/assets/VerificationDialog-BIDttgiW.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BAIGV9Mj.js → architectureDiagram-3BPJPVTR-u0PSZlNP.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-B-5v5jSZ.js → blockDiagram-GPEHLZMM-DFXLHRIk.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-C6wU415o.js → c4Diagram-AAUBKEIU-Dvic8tCt.js} +1 -1
  7. package/dist/client/assets/channel-Dhv_uSKx.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-COmvX2tP.js → chunk-2J33WTMH-t5CxhRXJ.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-BlANptdY.js → chunk-3OPIFGDE-YHT1dP8Q.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-ETyA2_GU.js → chunk-5ZQYHXKU-DcxuI7lb.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-D5-DudwF.js → chunk-727SXJPM-D1F6DmrT.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-k1AJ4sA3.js → chunk-AQP2D5EJ-BIx6Ohbk.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-jfksXwY9.js → chunk-CSCIHK7Q-B355h6HA.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-CHj0FDj9.js → chunk-KSCS5N6A-CLHGYOLQ.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-1ODGxXhG.js → chunk-L5ZTLDWV-DFLYcMx8.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-Bgh9X2W0.js → chunk-LZXEDZCA-DtljFujX.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-DDkb3sp4.js → chunk-ND2GUHAM-CIuUQ86K.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-B6B8PZ_-.js → chunk-NZK2D7GU-CIwqlxek.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-DhiUIFgj.js → chunk-O5CBEL6O-W42BEPu4.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-Dzd0fbP3.js → chunk-WU5MYG2G-C_zuaNxV.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-9XKvSSLq.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Dbr5_49Y.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-DoNDMCXI.js → dagre-BM42HDAG-UKfIUJTP.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-ETDXSa5q.js → diagram-2AECGRRQ-Xn6yojqq.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CgTUep7e.js → diagram-5GNKFQAL-BzpY1ZEl.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-BKTx2iRe.js → diagram-KO2AKTUF-Dyrvzvsg.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-9iEiScHT.js → diagram-LMA3HP47-AqImKwAr.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-UIZRh7Qa.js → diagram-OG6HWLK6-CaQQ46mi.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-BpNJmJva.js → erDiagram-TEJ5UH35-Dkw4Aa5u.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-C-rK0XZ1.js → flowDiagram-I6XJVG4X-CJTuGX9h.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DWyfPvv_.js → ganttDiagram-6RSMTGT7-CidgOdS-.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-x71bzdc_.js → gitGraphDiagram-PVQCEYII-Bj02mIQj.js} +1 -1
  33. package/dist/client/assets/index-ClRB9YWJ.css +2 -0
  34. package/dist/client/assets/index-DBMdsTvy.js +276 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-CbRus0Xu.js → infoDiagram-5YYISTIA-GwJWfo_r.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DvaMHrgN.js → ishikawaDiagram-YF4QCWOH-rbrlWbUn.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BQBu4YGn.js → journeyDiagram-JHISSGLW-BLi_uzQM.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-8rU-U7L1.js → kanban-definition-UN3LZRKU-ynlywm-_.js} +1 -1
  39. package/dist/client/assets/{line-CSZnwjLA.js → line-BNyTZBB4.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-CTOl2slE.js → mermaid-parser.core-CzUOsZAD.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-B9gr0Vqf.js → mermaid.core-K0QR6SzN.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-But1_PWQ.js → mindmap-definition-RKZ34NQL-BKcgqXm0.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-CnlVmfSG.js → pieDiagram-4H26LBE5-Bjx0lvDI.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Dww1ZGuR.js → quadrantDiagram-W4KKPZXB-CmUlgyUv.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CNf_loe8.js → requirementDiagram-4Y6WPE33-Dj2mzg1y.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBhi37Jj.js → sankeyDiagram-5OEKKPKP-DC--H6qf.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-B64hb4E4.js → sequenceDiagram-3UESZ5HK-BkNrt6Ra.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DcS6oB-S.js → stateDiagram-AJRCARHV-BuxxRu1Y.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Cyo85GPN.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-Dq-4tXfh.js → timeline-definition-PNZ67QCA-Db3pCVSp.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CeDklSE_.js → vennDiagram-CIIHVFJN-BgBSGThQ.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-D2cJISMm.js → wardleyDiagram-YWT4CUSO-BJhPdjYS.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-CBF-tTpF.js → xychartDiagram-2RQKCTM6-BY4Ra0kX.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +341 -4
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +172 -249
  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/executionRetry.js +13 -1
  78. package/dist/server/lib/executionRetry.js.map +1 -1
  79. package/dist/server/lib/executionRunAuthority.js +2 -2
  80. package/dist/server/lib/executionRunAuthority.js.map +1 -1
  81. package/dist/server/lib/executionTargets.js +7 -4
  82. package/dist/server/lib/executionTargets.js.map +1 -1
  83. package/dist/server/lib/executionTimeline.js +8 -13
  84. package/dist/server/lib/executionTimeline.js.map +1 -1
  85. package/dist/server/lib/executionTimelineHierarchy.js +83 -0
  86. package/dist/server/lib/executionTimelineHierarchy.js.map +1 -0
  87. package/dist/server/lib/failedBuildSequenceRecovery.js +133 -0
  88. package/dist/server/lib/failedBuildSequenceRecovery.js.map +1 -0
  89. package/dist/server/lib/hostRunSession.js +62 -0
  90. package/dist/server/lib/hostRunSession.js.map +1 -0
  91. package/dist/server/lib/hostServiceRecovery.js +159 -0
  92. package/dist/server/lib/hostServiceRecovery.js.map +1 -0
  93. package/dist/server/lib/hostServiceRunProvider.js +32 -0
  94. package/dist/server/lib/hostServiceRunProvider.js.map +1 -0
  95. package/dist/server/lib/mobileAccess.js +6 -2
  96. package/dist/server/lib/mobileAccess.js.map +1 -1
  97. package/dist/server/lib/projectAutoConfig.js +5 -2
  98. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  99. package/dist/server/lib/projectTargets.js +8 -2
  100. package/dist/server/lib/projectTargets.js.map +1 -1
  101. package/dist/server/lib/projects.js +25 -1
  102. package/dist/server/lib/projects.js.map +1 -1
  103. package/dist/server/lib/reviewFindingReconciliation.js +9 -2
  104. package/dist/server/lib/reviewFindingReconciliation.js.map +1 -1
  105. package/dist/server/lib/reviewFindings.js +82 -21
  106. package/dist/server/lib/reviewFindings.js.map +1 -1
  107. package/dist/server/lib/reviewRepairAssessmentRecords.js +41 -0
  108. package/dist/server/lib/reviewRepairAssessmentRecords.js.map +1 -0
  109. package/dist/server/lib/reviewRepairCompletion.js +78 -18
  110. package/dist/server/lib/reviewRepairCompletion.js.map +1 -1
  111. package/dist/server/lib/reviewRepairIterationRecords.js +4 -2
  112. package/dist/server/lib/reviewRepairIterationRecords.js.map +1 -1
  113. package/dist/server/lib/reviewRepairIterations.js +8 -2
  114. package/dist/server/lib/reviewRepairIterations.js.map +1 -1
  115. package/dist/server/lib/reviewRepairProgress.js +177 -0
  116. package/dist/server/lib/reviewRepairProgress.js.map +1 -0
  117. package/dist/server/lib/reviewRepairReconciliation.js +119 -140
  118. package/dist/server/lib/reviewRepairReconciliation.js.map +1 -1
  119. package/dist/server/lib/reviewRepairResumeOverride.js +55 -0
  120. package/dist/server/lib/reviewRepairResumeOverride.js.map +1 -0
  121. package/dist/server/lib/run.js +523 -504
  122. package/dist/server/lib/run.js.map +1 -1
  123. package/dist/server/lib/runProcessIdentity.js +71 -0
  124. package/dist/server/lib/runProcessIdentity.js.map +1 -0
  125. package/dist/server/lib/runProcesses.js +261 -0
  126. package/dist/server/lib/runProcesses.js.map +1 -0
  127. package/dist/server/lib/runServices.js +221 -0
  128. package/dist/server/lib/runServices.js.map +1 -0
  129. package/dist/server/lib/runSessionLifecycle.js +68 -0
  130. package/dist/server/lib/runSessionLifecycle.js.map +1 -0
  131. package/dist/server/lib/runStatus.js +7 -0
  132. package/dist/server/lib/runStatus.js.map +1 -0
  133. package/dist/server/lib/serviceRuns.js +273 -16
  134. package/dist/server/lib/serviceRuns.js.map +1 -1
  135. package/dist/server/lib/ticketAutomation.js +4 -27
  136. package/dist/server/lib/ticketAutomation.js.map +1 -1
  137. package/dist/server/lib/ticketRunOperations.js +103 -34
  138. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  139. package/dist/server/lib/tickets.js +0 -13
  140. package/dist/server/lib/tickets.js.map +1 -1
  141. package/dist/server/lib/types.js.map +1 -1
  142. package/dist/server/workers/build.js +44 -69
  143. package/dist/server/workers/build.js.map +1 -1
  144. package/dist/server/workers/plan.js +0 -7
  145. package/dist/server/workers/plan.js.map +1 -1
  146. package/dist/server/workers/review.js +95 -146
  147. package/dist/server/workers/review.js.map +1 -1
  148. package/package.json +1 -1
  149. package/dist/client/assets/ActivityView-CKvbDXeA.js +0 -1
  150. package/dist/client/assets/GitHubActionsDialog-Dzo0vtak.js +0 -1
  151. package/dist/client/assets/VerificationDialog-BJnOitrJ.js +0 -4
  152. package/dist/client/assets/channel-5mIy7nm1.js +0 -1
  153. package/dist/client/assets/classDiagram-4FO5ZUOK-BiQ-X2Sg.js +0 -1
  154. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CF-wCYW-.js +0 -1
  155. package/dist/client/assets/index-CIAO5aE_.css +0 -2
  156. package/dist/client/assets/index-nCGyalrn.js +0 -276
  157. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-PWeeL0tR.js +0 -1
  158. package/dist/server/lib/automationDeliveryDecision.js +0 -34
  159. 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,9 @@ 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 { getActiveRunStatus, getActiveRunStatusForProject, getLastRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
109
+ import { hasActiveProjectServiceRun } from './lib/serviceRuns.js';
110
+ import { projectRunStatus } from './lib/runStatus.js';
109
111
  import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
110
112
  import { closeCheckRunnerAdmissions, finalizeProductionCheckApproval, getCheckBatchAttempts, openCheckRunnerAdmissions, rerunCheckBatchAndSchedule, resumeProductionCheckApproval, shutdownCheckRunner, startCheckBatch, stopCheckBatch } from './lib/checkRunner.js';
111
113
  import { CheckRepairError, getCheckRepairCandidateState, getCheckRepairDetails, getLatestCheckRepairForBatch, retryCheckRepair, startCheckRepair, stopCheckRepair } from './lib/checkRepair.js';
@@ -421,7 +423,7 @@ const getCheckBatchRerunEligibility = (project, batch, selection) => {
421
423
  workspaceAvailable: !('error' in workspace),
422
424
  });
423
425
  };
424
- const buildExecutionRunDetailResponse = async (project, run) => {
426
+ const buildExecutionRunDetailResponse = async (project, run, scope) => {
425
427
  const checkBatch = run.kind === 'check_batch' ? getCheckBatchDetails(project.id, run.id) : null;
426
428
  const retryEligibility = checkBatch
427
429
  ? getCheckBatchRerunEligibility(project, checkBatch)
@@ -463,7 +465,7 @@ const buildExecutionRunDetailResponse = async (project, run) => {
463
465
  ? {
464
466
  kind: 'check_auto_fix',
465
467
  available: true,
466
- check_auto_fix: getCheckAutoFixDetails(project.id, run.id),
468
+ check_auto_fix: getCheckAutoFixDetails(project.id, run.id, scope),
467
469
  retry_eligibility: retryEligibility,
468
470
  action_kind: null,
469
471
  }
@@ -490,7 +492,7 @@ const buildExecutionRunDetailResponse = async (project, run) => {
490
492
  ...artifacts,
491
493
  environment_context: getExecutionEnvironmentContextForRun(project.id, run.id),
492
494
  automation_loops: run.kind === 'automation_progression'
493
- ? loadReviewAutomationLoopDetails(project.id, run.id)
495
+ ? loadReviewAutomationLoopDetails(project.id, run.id, scope)
494
496
  : [],
495
497
  };
496
498
  };
@@ -576,7 +578,26 @@ const cleanupWorktreeExecutionTarget = async ({ project, targetKind, targetKey,
576
578
  }
577
579
  return { branchWarning: buildBranchCleanupWarning(branch, deleteLocalBranch(branch, project)) };
578
580
  };
581
+ const assertTicketServiceTargetMutable = (ticketId) => {
582
+ db.transaction(() => {
583
+ const authority = db.prepare(`SELECT 1 FROM active_project_service_authorities active
584
+ JOIN execution_runs run ON run.id = active.execution_run_id
585
+ WHERE run.source_kind = 'ticket' AND run.source_id = ? LIMIT 1`).get(ticketId);
586
+ if (authority) {
587
+ throw new HttpError(409, 'Ticket has active service authority. Stop the service session and verify cleanup before changing or deleting its target.');
588
+ }
589
+ }).immediate();
590
+ };
579
591
  const deleteTicketRecord = async (ticket) => {
592
+ try {
593
+ assertTicketServiceTargetMutable(ticket.id);
594
+ }
595
+ catch (error) {
596
+ if (error instanceof HttpError) {
597
+ return { ok: false, status: error.status, error: error.message };
598
+ }
599
+ throw error;
600
+ }
580
601
  if (ticket.project_id) {
581
602
  try {
582
603
  assertReviewRepairIterationDeletionEligible(ticket.project_id, ticket.id);
@@ -667,6 +688,7 @@ const deleteTicketRecord = async (ticket) => {
667
688
  }
668
689
  try {
669
690
  withExecutionRunTransaction(transaction => {
691
+ assertTicketServiceTargetMutable(ticket.id);
670
692
  if (ticket.project_id) {
671
693
  deleteTerminalReviewRepairIterationsForTicketInTransaction(transaction, ticket.project_id, ticket.id);
672
694
  }
@@ -2799,11 +2821,9 @@ app.get('/run-config', (req, res) => {
2799
2821
  });
2800
2822
  app.get('/run-status', (req, res) => {
2801
2823
  const requestedProjectId = getRequestedProjectId(req);
2802
- return res.json({
2803
- active_run: requestedProjectId
2804
- ? getActiveRunStatusForProject(requestedProjectId)
2805
- : getActiveRunStatus(),
2806
- });
2824
+ const activeRun = requestedProjectId ? getActiveRunStatusForProject(requestedProjectId) : getActiveRunStatus();
2825
+ const durableAuthority = hasActiveProjectServiceRun(requestedProjectId);
2826
+ return res.json(projectRunStatus(activeRun, requestedProjectId ? getLastRunStatusForProject(requestedProjectId) : null, durableAuthority));
2807
2827
  });
2808
2828
  app.get('/preview-status', (req, res) => {
2809
2829
  const projectId = getRequestedProjectId(req);
@@ -4592,7 +4612,25 @@ app.post('/bundles/:id/undo-review', (req, res) => {
4592
4612
  }
4593
4613
  return res.json(undoResult.details);
4594
4614
  });
4595
- const parseServiceExecutionProviderChoice = (body) => {
4615
+ const parseServiceRunControl = (body) => {
4616
+ if (body === undefined) {
4617
+ return {};
4618
+ }
4619
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
4620
+ return { error: 'Run control body must be an object' };
4621
+ }
4622
+ const input = body;
4623
+ for (const field of ['service_id', 'session_id']) {
4624
+ if (input[field] !== undefined && (typeof input[field] !== 'string' || !input[field].trim())) {
4625
+ return { error: `${field} must be a non-empty string` };
4626
+ }
4627
+ }
4628
+ return {
4629
+ serviceId: typeof input.service_id === 'string' ? input.service_id.trim() : undefined,
4630
+ sessionId: typeof input.session_id === 'string' ? input.session_id.trim() : undefined,
4631
+ };
4632
+ };
4633
+ const parseServiceExecutionProviderChoice = (body, allowServiceControl = false) => {
4596
4634
  if (body === undefined || body === null) {
4597
4635
  return 'project_default';
4598
4636
  }
@@ -4600,7 +4638,8 @@ const parseServiceExecutionProviderChoice = (body) => {
4600
4638
  return null;
4601
4639
  }
4602
4640
  const request = body;
4603
- if (Object.keys(request).some(key => key !== 'execution_provider')) {
4641
+ if (Object.keys(request).some(key => key !== 'execution_provider'
4642
+ && !(allowServiceControl && (key === 'service_id' || key === 'session_id')))) {
4604
4643
  return null;
4605
4644
  }
4606
4645
  if (request.execution_provider === undefined || request.execution_provider === 'project_default') {
@@ -4659,8 +4698,17 @@ app.post('/bundles/:id/run', (req, res) => {
4659
4698
  return res.status(409).json(activeRunConflict);
4660
4699
  }
4661
4700
  const profileSelection = createServiceExecutionProfileSelection(project.id, executionProvider);
4701
+ try {
4702
+ void runTicketInWorkspace(targetTicket, io, project, workspace.cwd, profileSelection)
4703
+ .catch(error => console.error('[run] admitted bundle service startup failed:', error));
4704
+ }
4705
+ catch (error) {
4706
+ return res.status(409).json({
4707
+ error: error instanceof Error ? error.message : String(error),
4708
+ code: error instanceof Error && 'code' in error ? error.code : 'service_start_rejected',
4709
+ });
4710
+ }
4662
4711
  emitRunStateForContext(targetTicket, true);
4663
- runTicketInWorkspace(targetTicket, io, project, workspace.cwd, profileSelection);
4664
4712
  return res.json(serializeBundleDetails(result.project, bundle));
4665
4713
  });
4666
4714
  app.post('/bundles/:id/stop', async (req, res) => {
@@ -4700,52 +4748,6 @@ app.post('/bundles/:id/follow-up', (req, res) => {
4700
4748
  });
4701
4749
  const AUTOMATION_SIDE_EFFECT_KINDS = new Set(['automation_commit', 'automation_push', 'automation_pull_request']);
4702
4750
  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
4751
  app.post('/bundles/:id/resume-automation', async (req, res) => {
4750
4752
  const result = getRequiredProject(req);
4751
4753
  if ('error' in result) {
@@ -4785,7 +4787,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4785
4787
  : 'Bundle automation cannot be resumed in its current state';
4786
4788
  return res.status(409).json({ error, automation_resume_decision: initialDecision });
4787
4789
  }
4788
- let decision = initialDecision;
4790
+ const decision = initialDecision;
4789
4791
  const revalidateDecision = () => {
4790
4792
  const current = resolveCurrentDecision();
4791
4793
  return current?.status === 'enabled'
@@ -5001,9 +5003,21 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5001
5003
  }
5002
5004
  const automationConfig = resolveAutomationConfig(owner.bundle ?? null, project);
5003
5005
  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') {
5006
+ const completedVerificationIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5007
+ if (completedVerificationIteration
5008
+ && ['paused', 'verification_active'].includes(completedVerificationIteration.phase)
5009
+ && completedVerificationIteration.stop_reason !== 'no_progress'
5010
+ && completedVerificationIteration.resulting_review_execution_run_id
5011
+ && getExecutionRun(result.project.id, completedVerificationIteration.resulting_review_execution_run_id)?.status === 'completed') {
5012
+ try {
5013
+ await recoverCompletedReviewRepairVerification(result.project.id, completedVerificationIteration.id, io, { resume: true });
5014
+ return res.json(serializeBundleDetails(result.project, bundle));
5015
+ }
5016
+ catch (error) {
5017
+ return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
5018
+ }
5019
+ }
5020
+ if (completedVerificationIteration?.phase === 'paused' && completedVerificationIteration.stop_reason !== 'no_progress') {
5007
5021
  withExecutionRunTransaction(transaction => {
5008
5022
  appendReviewRepairIterationDecisionInTransaction(transaction, {
5009
5023
  projectId: result.project.id,
@@ -5014,165 +5028,6 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5014
5028
  });
5015
5029
  }, io, 'immediate');
5016
5030
  }
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
5031
  const restoreWaitingProgression = () => {
5177
5032
  waitAutomationProgression({
5178
5033
  projectId: result.project.id,
@@ -5365,7 +5220,16 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5365
5220
  }
5366
5221
  const workflowContext = validateWorkflowContext(progression.metadata);
5367
5222
  let existingIteration = getActiveReviewRepairIteration(result.project.id, progressionRunId);
5368
- const sourceReviewRunId = reconciledSourceReviewRunId ?? existingIteration?.triggering_review_run_id ?? null;
5223
+ const overridingNoProgress = existingIteration?.phase === 'paused' && existingIteration.stop_reason === 'no_progress';
5224
+ if (overridingNoProgress) {
5225
+ existingIteration = withExecutionRunTransaction(transaction => {
5226
+ if (!revalidateDecision()) {
5227
+ throw new Error('Bundle automation state changed before Resume override could start');
5228
+ }
5229
+ return overrideNoProgressReviewRepairInTransaction(transaction, result.project.id, existingIteration.id, io);
5230
+ }, io, 'immediate');
5231
+ }
5232
+ const sourceReviewRunId = existingIteration?.triggering_review_run_id ?? null;
5369
5233
  if (!sourceReviewRunId) {
5370
5234
  throw new Error('The completed review findings to resume are unavailable');
5371
5235
  }
@@ -5374,12 +5238,14 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5374
5238
  review_finding_run_id: sourceReviewRunId,
5375
5239
  review_severity: automationConfig.reviewSeverity,
5376
5240
  });
5377
- runAutomationProgression({
5378
- projectId: result.project.id,
5379
- bundleId: bundle.id,
5380
- progressionRunId,
5381
- broadcaster: io,
5382
- });
5241
+ if (!overridingNoProgress) {
5242
+ runAutomationProgression({
5243
+ projectId: result.project.id,
5244
+ bundleId: bundle.id,
5245
+ progressionRunId,
5246
+ broadcaster: io,
5247
+ });
5248
+ }
5383
5249
  if (existingIteration?.phase === 'paused') {
5384
5250
  existingIteration = resumePausedReviewRepairIteration({
5385
5251
  projectId: result.project.id,
@@ -5650,9 +5516,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
5650
5516
  });
5651
5517
  }, io, 'immediate');
5652
5518
  }
5653
- if (bundleState.fix_loop_active === 1) {
5654
- resetBundleAutomationLoop(result.project.id, bundle.id);
5655
- }
5519
+ resetBundleAutomationLoop(result.project.id, bundle.id);
5656
5520
  }
5657
5521
  runAutomationProgression({
5658
5522
  projectId: result.project.id,
@@ -8201,14 +8065,6 @@ app.patch('/projects/:id', async (req, res) => {
8201
8065
  if ('error' in roleModelValidation) {
8202
8066
  return res.status(400).json({ error: roleModelValidation.error });
8203
8067
  }
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
8068
  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
8069
  const updateProject = db.prepare(`
8214
8070
  UPDATE projects SET
@@ -8266,7 +8122,7 @@ app.patch('/projects/:id', async (req, res) => {
8266
8122
  const nextReviewerVariant = nextReviewerModel
8267
8123
  ? (hasOwn(validation.values, 'reviewer_variant') ? reviewer_variant ?? null : project.reviewer_variant)
8268
8124
  : null;
8269
- const updateResult = db.transaction(() => {
8125
+ const applyUpdate = db.transaction(() => {
8270
8126
  const currentProject = getProjectById(project.id);
8271
8127
  if (!currentProject || JSON.stringify(currentProject) !== JSON.stringify(project)) {
8272
8128
  return {
@@ -8274,6 +8130,15 @@ app.patch('/projects/:id', async (req, res) => {
8274
8130
  status: 409,
8275
8131
  };
8276
8132
  }
8133
+ if (hasOwn(validation.values, 'repo_path') && repo_path !== currentProject.repo_path) {
8134
+ if (currentProject.deletion_started_at || hasActiveProjectServiceRun(project.id) || getActiveRunStatusForProject(project.id)) {
8135
+ return { error: 'Cannot change project repository while service authority or deletion is active', status: 409 };
8136
+ }
8137
+ const stats = getProjectTicketStats(project.id);
8138
+ if (stats.ticketCount > 0 || stats.worktreeCount > 0 || stats.bundleCount > 0) {
8139
+ return { error: 'repo_path cannot be changed after tickets, bundles, or worktrees exist for this project', status: 409 };
8140
+ }
8141
+ }
8277
8142
  const policyReferences = validateProjectRequiredCheckPolicyReferences({
8278
8143
  projectId: project.id,
8279
8144
  checkDefinitionsJson: nextCheckDefinitions,
@@ -8305,7 +8170,17 @@ app.patch('/projects/:id', async (req, res) => {
8305
8170
  replaceProjectLinks(project.id, linked_project_ids ?? []);
8306
8171
  }
8307
8172
  return { ok: true };
8308
- }).immediate();
8173
+ });
8174
+ let updateResult;
8175
+ try {
8176
+ updateResult = applyUpdate.immediate();
8177
+ }
8178
+ catch (error) {
8179
+ if (error instanceof Error && 'code' in error && error.code === 'SQLITE_CONSTRAINT_TRIGGER') {
8180
+ return res.status(409).json({ error: error.message });
8181
+ }
8182
+ throw error;
8183
+ }
8309
8184
  if ('error' in updateResult) {
8310
8185
  const status = 'status' in updateResult && typeof updateResult.status === 'number'
8311
8186
  ? updateResult.status
@@ -8316,6 +8191,9 @@ app.patch('/projects/:id', async (req, res) => {
8316
8191
  res.json(serializeProject(updated ?? null));
8317
8192
  });
8318
8193
  const assertProjectDeletionEligible = (projectId, claimId) => {
8194
+ if (hasActiveProjectServiceRun(projectId) || getActiveRunStatusForProject(projectId)) {
8195
+ throw new ProjectDeletionError(409, 'Cannot delete a project while services are active');
8196
+ }
8319
8197
  const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
8320
8198
  if (!project) {
8321
8199
  throw new ProjectDeletionError(404, 'Not found');
@@ -8363,6 +8241,7 @@ const assertProjectDeletionEligible = (projectId, claimId) => {
8363
8241
  }
8364
8242
  };
8365
8243
  const completeClaimedProjectDeletion = async (projectId, claimId) => {
8244
+ assertProjectDeletionEligible(projectId, claimId);
8366
8245
  const marked = db.prepare(`
8367
8246
  UPDATE projects
8368
8247
  SET deletion_cleanup_started_at = COALESCE(deletion_cleanup_started_at, CURRENT_TIMESTAMP)
@@ -8599,6 +8478,17 @@ app.patch('/tickets/:id', async (req, res) => {
8599
8478
  const nextProjectId = hasProjectField ? project_id : ticket.project_id;
8600
8479
  const isProjectChanging = nextProjectId !== ticket.project_id;
8601
8480
  const hasStandaloneWorktree = typeof ticket.branch === 'string' && ticket.branch.trim() !== '';
8481
+ if (isProjectChanging || (hasBundleField && req.body.worktree_bundle_id !== ticket.worktree_bundle_id)) {
8482
+ try {
8483
+ assertTicketServiceTargetMutable(ticket.id);
8484
+ }
8485
+ catch (error) {
8486
+ if (error instanceof HttpError) {
8487
+ return res.status(error.status).json({ error: error.message });
8488
+ }
8489
+ throw error;
8490
+ }
8491
+ }
8602
8492
  if (isProjectChanging && hasStandaloneWorktree) {
8603
8493
  return res.status(409).json({ error: 'project_id cannot be changed after a worktree has been created' });
8604
8494
  }
@@ -8708,10 +8598,23 @@ app.patch('/tickets/:id', async (req, res) => {
8708
8598
  }
8709
8599
  }
8710
8600
  }
8711
- if (isProjectChanging) {
8712
- deleteTicketDependenciesForTicket(ticket.id);
8601
+ try {
8602
+ db.transaction(() => {
8603
+ if (bundleChanged) {
8604
+ assertTicketServiceTargetMutable(ticket.id);
8605
+ }
8606
+ if (isProjectChanging) {
8607
+ deleteTicketDependenciesForTicket(ticket.id);
8608
+ }
8609
+ 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);
8610
+ }).immediate();
8611
+ }
8612
+ catch (error) {
8613
+ if (error instanceof HttpError) {
8614
+ return res.status(error.status).json({ error: error.message });
8615
+ }
8616
+ throw error;
8713
8617
  }
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
8618
  upsertTicketDescriptionMessage(req.params.id, description ?? ticket.description);
8716
8619
  const updated = getTicket(req.params.id);
8717
8620
  if (bundleChanged && previousBundleId && previousProject) {
@@ -9133,7 +9036,12 @@ app.post('/tickets/:id/undo', (req, res) => {
9133
9036
  return res.json(result.ticket);
9134
9037
  });
9135
9038
  app.post('/tickets/:id/run', (req, res) => {
9039
+ const control = parseServiceRunControl(req.body);
9040
+ if ('error' in control) {
9041
+ return res.status(400).json({ error: control.error });
9042
+ }
9136
9043
  const result = runTicketRun({
9044
+ ...control,
9137
9045
  ticketId: req.params.id,
9138
9046
  requestedProjectId: getRequestedProjectId(req),
9139
9047
  broadcaster: io,
@@ -9394,11 +9302,16 @@ app.post('/projects/:id/open-ide', (req, res) => {
9394
9302
  return res.json({ ok: true });
9395
9303
  });
9396
9304
  app.post('/projects/:id/run', (req, res) => {
9397
- const executionProvider = parseServiceExecutionProviderChoice(req.body);
9305
+ const control = parseServiceRunControl(req.body);
9306
+ if ('error' in control) {
9307
+ return res.status(400).json({ error: control.error });
9308
+ }
9309
+ const executionProvider = parseServiceExecutionProviderChoice(req.body, true);
9398
9310
  if (!executionProvider) {
9399
9311
  return res.status(400).json({ error: 'execution_provider must be project_default or host' });
9400
9312
  }
9401
9313
  const result = runProjectTarget({
9314
+ ...control,
9402
9315
  projectId: req.params.id,
9403
9316
  executionProvider,
9404
9317
  });
@@ -9883,7 +9796,7 @@ app.get('/projects/:id/execution-timeline/:runId', async (req, res) => {
9883
9796
  if (scope.scopeEntity && !links.some(link => link.entity_type === scope.scopeEntity?.type && link.entity_id === scope.scopeEntity.id)) {
9884
9797
  return res.status(404).json({ error: 'Execution run not found' });
9885
9798
  }
9886
- return res.json(await buildExecutionRunDetailResponse(project, run));
9799
+ return res.json(await buildExecutionRunDetailResponse(project, run, scope.scopeEntity));
9887
9800
  });
9888
9801
  app.get('/projects/:id/execution-runs/:runId', async (req, res) => {
9889
9802
  const project = getProjectById(req.params.id);
@@ -10131,7 +10044,12 @@ app.post('/terminals/:sessionId/destroy', (req, res) => {
10131
10044
  return res.status(204).send();
10132
10045
  });
10133
10046
  app.post('/tickets/:id/stop', async (req, res) => {
10047
+ const control = parseServiceRunControl(req.body);
10048
+ if ('error' in control) {
10049
+ return res.status(400).json({ error: control.error });
10050
+ }
10134
10051
  const result = await stopTicketRun({
10052
+ ...control,
10135
10053
  ticketId: req.params.id,
10136
10054
  requestedProjectId: getRequestedProjectId(req),
10137
10055
  broadcaster: io,
@@ -10142,7 +10060,12 @@ app.post('/tickets/:id/stop', async (req, res) => {
10142
10060
  return res.status(result.status).json(result.value);
10143
10061
  });
10144
10062
  app.post('/projects/:id/stop-run', async (req, res) => {
10063
+ const control = parseServiceRunControl(req.body);
10064
+ if ('error' in control) {
10065
+ return res.status(400).json({ error: control.error });
10066
+ }
10145
10067
  const result = await stopProjectTargetRun({
10068
+ ...control,
10146
10069
  projectId: req.params.id,
10147
10070
  });
10148
10071
  if (!result.ok) {