@straiffi/archon 1.7.7 → 1.7.8

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 (122) hide show
  1. package/dist/client/assets/ActivityView-DwfDSF4q.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-3dMCYCSZ.js +1 -0
  3. package/dist/client/assets/VerificationDialog-CoTfXMyb.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-DVbmGv0b.js → architectureDiagram-3BPJPVTR-Bdnc97XK.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-BsxPuJKK.js → blockDiagram-GPEHLZMM-boxM3She.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-9Jjpu2R1.js → c4Diagram-AAUBKEIU-CJDdF7xd.js} +1 -1
  7. package/dist/client/assets/channel-BGqdAcIT.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-Dzljq_5a.js → chunk-2J33WTMH-ClpcO-Om.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-CiBHaLdV.js → chunk-3OPIFGDE-CcuL_65J.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-DZFrI-_H.js → chunk-5ZQYHXKU-CUGYZXkH.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-u519MeJ7.js → chunk-727SXJPM-CApeLDyK.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-DC91Nw9K.js → chunk-AQP2D5EJ-8FdSvF2D.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-CR7cwH-D.js → chunk-CSCIHK7Q-CZoIOF3a.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-DJwQEfa_.js → chunk-KSCS5N6A-BigFQI5y.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-w6TWSOTT.js → chunk-L5ZTLDWV-DIi3SHaT.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-Dfw_jEjR.js → chunk-LZXEDZCA-B-Z0BcQy.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-DHdJgCA7.js → chunk-ND2GUHAM-EBeba0mg.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-GvOuUiUy.js → chunk-NZK2D7GU-DJBI2y53.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O--r1VQgDc.js → chunk-O5CBEL6O-BqxdXhMd.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-Xgc6shf5.js → chunk-WU5MYG2G-Coe1_Cbb.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-D5VuKRcj.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B9GNqpRr.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-IcfTsQ6f.js → dagre-BM42HDAG-tuTmeA0Y.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-D8Z4_NSQ.js → diagram-2AECGRRQ-ILGCpG2F.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CIjC8QzH.js → diagram-5GNKFQAL-C8qBZsgK.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-BBbYFygn.js → diagram-KO2AKTUF-BXlcm_Zo.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-lZstJdXv.js → diagram-LMA3HP47-B8V5EVbD.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-Cnqij60i.js → diagram-OG6HWLK6-CWsdOqcP.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-1DAJwSbR.js → erDiagram-TEJ5UH35-DW8pgly2.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-DIQkR95e.js → flowDiagram-I6XJVG4X-Djn7fOVR.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-mDGbRM53.js → ganttDiagram-6RSMTGT7-DZ421MEy.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BQirGZuE.js → gitGraphDiagram-PVQCEYII-CCNzLPuP.js} +1 -1
  33. package/dist/client/assets/index-BRc02lbW.js +273 -0
  34. package/dist/client/assets/index-CZO9HL1Z.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-CljoXByt.js → infoDiagram-5YYISTIA-Bd1gYgMp.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-ouY7AiSo.js → ishikawaDiagram-YF4QCWOH-txyMoatJ.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BiI9xI0r.js → journeyDiagram-JHISSGLW-DhT3bmX6.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-CvuW7aYT.js → kanban-definition-UN3LZRKU-BBHt2hfC.js} +1 -1
  39. package/dist/client/assets/{line-CoE97hT8.js → line-KGRcT7Qz.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-BHswQsmF.js → mermaid-parser.core-CDzt4_zR.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-a9PzZlL1.js → mermaid.core-DD_-k1aB.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-CIkEQqLV.js → mindmap-definition-RKZ34NQL-DoctVTDl.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-BIvcv2M5.js → pieDiagram-4H26LBE5-DFlLEi70.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BWCVll7J.js → quadrantDiagram-W4KKPZXB-3YHOQgl7.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-w5l0CZn2.js → requirementDiagram-4Y6WPE33-X4fZVL9M.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BuKReL9w.js → sankeyDiagram-5OEKKPKP-BUPnMXeG.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Br5KpukA.js → sequenceDiagram-3UESZ5HK-D6QU3M64.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-BfFCsPCI.js → stateDiagram-AJRCARHV-c35bu4qZ.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ymy1_EHr.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BrDP2iO0.js → timeline-definition-PNZ67QCA-CX2y6e_b.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-C7lGYnr9.js → vennDiagram-CIIHVFJN-BDaWltjF.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BRKJRuEF.js → wardleyDiagram-YWT4CUSO-BpBRL2sb.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-BjaB0hQ8.js → xychartDiagram-2RQKCTM6-Bqw7X0UT.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +458 -2
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +324 -25
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/activeAutomationChild.js +31 -0
  60. package/dist/server/lib/activeAutomationChild.js.map +1 -0
  61. package/dist/server/lib/automationCheckGate.js +266 -0
  62. package/dist/server/lib/automationCheckGate.js.map +1 -0
  63. package/dist/server/lib/automationProgression.js +20 -21
  64. package/dist/server/lib/automationProgression.js.map +1 -1
  65. package/dist/server/lib/automationReconciliation.js +16 -2
  66. package/dist/server/lib/automationReconciliation.js.map +1 -1
  67. package/dist/server/lib/automationResumeDecision.js +4 -1
  68. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  69. package/dist/server/lib/automationStageDetails.js +173 -41
  70. package/dist/server/lib/automationStageDetails.js.map +1 -1
  71. package/dist/server/lib/checkDefinitions.js +278 -0
  72. package/dist/server/lib/checkDefinitions.js.map +1 -0
  73. package/dist/server/lib/checkGateEvaluations.js +294 -0
  74. package/dist/server/lib/checkGateEvaluations.js.map +1 -0
  75. package/dist/server/lib/checkRunner.js +674 -0
  76. package/dist/server/lib/checkRunner.js.map +1 -0
  77. package/dist/server/lib/checkRuns.js +736 -0
  78. package/dist/server/lib/checkRuns.js.map +1 -0
  79. package/dist/server/lib/checkWorkspaceIdentity.js +207 -0
  80. package/dist/server/lib/checkWorkspaceIdentity.js.map +1 -0
  81. package/dist/server/lib/executionReconciliation.js +79 -2
  82. package/dist/server/lib/executionReconciliation.js.map +1 -1
  83. package/dist/server/lib/executionRetry.js +14 -6
  84. package/dist/server/lib/executionRetry.js.map +1 -1
  85. package/dist/server/lib/executionRetryContext.js +3 -3
  86. package/dist/server/lib/executionRetryContext.js.map +1 -1
  87. package/dist/server/lib/executionRunRetention.js +41 -3
  88. package/dist/server/lib/executionRunRetention.js.map +1 -1
  89. package/dist/server/lib/executionRuns.js +135 -2
  90. package/dist/server/lib/executionRuns.js.map +1 -1
  91. package/dist/server/lib/executionTimeline.js +52 -1
  92. package/dist/server/lib/executionTimeline.js.map +1 -1
  93. package/dist/server/lib/projectAutoConfig.js +47 -65
  94. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  95. package/dist/server/lib/projects.js +87 -7
  96. package/dist/server/lib/projects.js.map +1 -1
  97. package/dist/server/lib/requiredCheckPolicy.js +68 -0
  98. package/dist/server/lib/requiredCheckPolicy.js.map +1 -0
  99. package/dist/server/lib/shutdown.js +4 -4
  100. package/dist/server/lib/shutdown.js.map +1 -1
  101. package/dist/server/lib/testCommandRunner.js +3 -2
  102. package/dist/server/lib/testCommandRunner.js.map +1 -1
  103. package/dist/server/lib/testSessions.js +2 -6
  104. package/dist/server/lib/testSessions.js.map +1 -1
  105. package/dist/server/lib/ticketAutomationCommit.js +185 -22
  106. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  107. package/dist/server/lib/tickets.js +21 -1
  108. package/dist/server/lib/tickets.js.map +1 -1
  109. package/dist/server/lib/types.js +2 -0
  110. package/dist/server/lib/types.js.map +1 -1
  111. package/dist/server/workers/review.js +2 -10
  112. package/dist/server/workers/review.js.map +1 -1
  113. package/package.json +1 -1
  114. package/dist/client/assets/ActivityView-Btpsv-5U.js +0 -1
  115. package/dist/client/assets/GitHubActionsDialog-DRlIMgSB.js +0 -1
  116. package/dist/client/assets/TestsDialog-DSWDkKPV.js +0 -4
  117. package/dist/client/assets/channel-Lic13vb5.js +0 -1
  118. package/dist/client/assets/classDiagram-4FO5ZUOK-DACLwQIJ.js +0 -1
  119. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CKi6c1d9.js +0 -1
  120. package/dist/client/assets/index-DQt4dcwc.css +0 -2
  121. package/dist/client/assets/index-W9OCqkMU.js +0 -273
  122. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-lhIQ8FTe.js +0 -1
@@ -52,7 +52,7 @@ import { reconcileAutomationStage } from './lib/automationReconciliation.js';
52
52
  import { pausedRunBelongsToProgression, resolveAutomationResumeDecision } from './lib/automationResumeDecision.js';
53
53
  import { getAutomationStageDetails, } from './lib/automationStageDetails.js';
54
54
  import { buildAutomationFixInstruction, clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, isAutomationDepth, recordAutomationPause, resolveAutomationConfig, upsertBundleAutomationState, } from './lib/ticketAutomation.js';
55
- import { runAutomationCommitChain } from './lib/ticketAutomationCommit.js';
55
+ import { resolveAutomationCheckGateResume, runAutomationCommitChain } from './lib/ticketAutomationCommit.js';
56
56
  import { getDiscoveredSkills, normalizeSkillNames } from './lib/skills.js';
57
57
  import { clearAllPreviewProxies } from './lib/previewProxy.js';
58
58
  import { createExecutionRun, addExecutionRunLink, addExecutionRunLinks, getExecutionRun, hasActiveBuildExecutionRunForTicket, getGitHubActionsExecutionDetails, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listLifecycleEvents, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
@@ -61,7 +61,7 @@ import { listExecutionTimeline } from './lib/executionTimeline.js';
61
61
  import { loadExecutionArtifactSummariesForRuns, openExecutionArtifactForRetrieval, } from './lib/executionArtifacts.js';
62
62
  import { cleanupAllExecutionEvidence, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS, resolveExecutionEvidenceRetentionDays, } from './lib/executionRunRetention.js';
63
63
  import { ExecutionArtifactCleanupError, reconcileExecutionArtifactRegistrationIntents, reconcileFinalizedExecutionArtifacts, reconcileProjectExecutionArtifacts, resolveArtifactCleanupIntervalMs, } from './lib/executionArtifactRetention.js';
64
- import { shutdownRealtimeServer } from './lib/shutdown.js';
64
+ import { closeHttpServer, shutdownRealtimeServer } from './lib/shutdown.js';
65
65
  import { resolveClientBuildPaths, shouldServeClientApp, STATIC_CLIENT_DIRECTORY_OPTIONS, STATIC_CLIENT_INDEX_OPTIONS, } from './lib/staticClient.js';
66
66
  import { deleteIntegrationConnection, getGitHubConnectionConfig, getJiraConnectionConfig, listIntegrationConnections, upsertIntegrationConnection, } from './lib/integrations/index.js';
67
67
  import { createGitHubPullRequest, createOrFindGitHubPullRequest, findOpenGitHubPullRequest, findOpenGitHubPullRequestByHeadBranch, getGitHubPullRequestComments, getGitHubPullRequestReviews, getGitHubPullRequest, GitHubApiError, hasGitHubRemoteBranch, resolveGitHubRepository, validateGitHubConnection, } from './lib/integrations/github.js';
@@ -75,9 +75,14 @@ import { PlanningReferenceError, resolveExternalPlanningDescription } from './li
75
75
  import { refreshIfNeeded as refreshAssignedIssues, clearCachedAssignedIssues } from './lib/integrations/assignedIssuesCache.js';
76
76
  import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib/commitMessage.js';
77
77
  import { BUNDLE_PULL_REQUEST_SYNC_TTL_MS, getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, syncTrackedBundlePullRequestIfNeeded, upsertBundlePullRequest, upsertBundlePullRequestFromGitHub, } from './lib/bundlePullRequests.js';
78
- import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listInterruptedProjectDeletions, listProjects, releaseInterruptedProjectDeletionClaims, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
78
+ import { countInboundProjectLinks, getEffectiveProject, getProjectCheckDefinitions, getProjectById, getProjectTestCommands, getProjectTicketStats, hasProjectRunIde, listInterruptedProjectDeletions, listProjects, releaseInterruptedProjectDeletionClaims, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
79
79
  import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
80
- import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
80
+ import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
81
+ import { closeCheckRunnerAdmissions, getCheckBatchAttempts, openCheckRunnerAdmissions, rerunCheckBatchAndSchedule, shutdownCheckRunner, startCheckBatch, stopCheckBatch } from './lib/checkRunner.js';
82
+ import { findActiveCheckBatchForTarget, getCheckAttemptDetails, getCheckBatchDetails, hasActiveProjectCheckBatch, isCheckAttemptCurrent, resolveCheckBatchRerunEligibility } from './lib/checkRuns.js';
83
+ import { getCheckDefinitionFingerprint } from './lib/checkDefinitions.js';
84
+ import { getCheckGateEvaluation } from './lib/checkGateEvaluations.js';
85
+ import { captureCheckWorkspaceIdentity } from './lib/checkWorkspaceIdentity.js';
81
86
  import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTicketState, } from './lib/ticketWorkflowOperations.js';
82
87
  import { submitPlanTicketFollowUp, submitTicketFollowUp, } from './lib/ticketFollowUpOperations.js';
83
88
  import { closePendingTicketPlanQuestionSet, listPendingTicketPlanQuestionSetMessageIds, resolveTicketPlanQuestionSet, restoreClosedTicketPlanQuestionSets, } from './lib/planQuestionOperations.js';
@@ -292,15 +297,90 @@ const serializeGitHubActionsExecutionDetail = (details) => {
292
297
  jobs_count: details.jobs_count,
293
298
  };
294
299
  };
295
- const buildExecutionRunDetailResponse = (project, run) => {
296
- const retryEligibility = getExecutionRetryEligibility(project.id, run.id);
300
+ const serializeCheckAttemptExecutionDetail = (projectId, runId) => {
301
+ const attempt = getCheckAttemptDetails(projectId, runId);
302
+ return {
303
+ kind: 'check_attempt',
304
+ available: true,
305
+ attempt,
306
+ batch: attempt ? getCheckBatchDetails(projectId, attempt.batch_run_id) : null,
307
+ };
308
+ };
309
+ const hydrateCheckAttemptCurrency = async (project, batch, attempts) => {
310
+ const terminalAttempts = attempts.filter(attempt => attempt.outcome !== null);
311
+ if (terminalAttempts.length === 0) {
312
+ return attempts.map(attempt => ({ ...attempt, currency: 'pending' }));
313
+ }
314
+ const workspace = resolveExplicitProjectTestWorkspace(project, batch.target_kind, batch.target_bundle_id);
315
+ if ('error' in workspace) {
316
+ return attempts.map(attempt => ({
317
+ ...attempt,
318
+ currency: attempt.outcome === null ? 'pending' : 'unavailable',
319
+ }));
320
+ }
321
+ const currentIdentity = await captureCheckWorkspaceIdentity(workspace.cwd);
322
+ const currentProject = getProjectById(project.id);
323
+ const definitionsById = new Map(getProjectCheckDefinitions(currentProject).map(definition => [definition.id, definition]));
324
+ return attempts.map(attempt => {
325
+ if (attempt.outcome === null) {
326
+ return { ...attempt, currency: 'pending' };
327
+ }
328
+ const definition = definitionsById.get(attempt.definition_id);
329
+ if (!definition || getCheckDefinitionFingerprint(definition) !== attempt.definition_fingerprint) {
330
+ return { ...attempt, currency: 'stale' };
331
+ }
332
+ const currency = currentIdentity.state === 'unavailable'
333
+ ? 'unavailable'
334
+ : isCheckAttemptCurrent(attempt, currentIdentity) ? 'current' : 'stale';
335
+ return { ...attempt, currency };
336
+ });
337
+ };
338
+ const getCheckBatchRerunEligibility = (project, batch, selection) => {
339
+ const workspace = resolveExplicitProjectTestWorkspace(project, batch.target_kind, batch.target_bundle_id);
340
+ return resolveCheckBatchRerunEligibility({
341
+ projectId: project.id,
342
+ batchRunId: batch.execution_run_id,
343
+ availableDefinitions: getProjectCheckDefinitions(project),
344
+ selection,
345
+ targetAvailable: !('error' in workspace && typeof workspace.error === 'string' && workspace.error.includes('no longer exists')),
346
+ workspaceAvailable: !('error' in workspace),
347
+ });
348
+ };
349
+ const buildExecutionRunDetailResponse = async (project, run) => {
350
+ const checkBatch = run.kind === 'check_batch' ? getCheckBatchDetails(project.id, run.id) : null;
351
+ const retryEligibility = checkBatch
352
+ ? getCheckBatchRerunEligibility(project, checkBatch)
353
+ : getExecutionRetryEligibility(project.id, run.id);
354
+ const checkBatchAttempts = checkBatch
355
+ ? await hydrateCheckAttemptCurrency(project, checkBatch, getCheckBatchAttempts(project.id, run.id))
356
+ : [];
357
+ const checkAttemptDetail = run.kind === 'check_attempt' ? serializeCheckAttemptExecutionDetail(project.id, run.id) : null;
358
+ const checkAttempt = checkAttemptDetail?.attempt && checkAttemptDetail.batch
359
+ ? (await hydrateCheckAttemptCurrency(project, checkAttemptDetail.batch, [checkAttemptDetail.attempt]))[0]
360
+ : checkAttemptDetail?.attempt ?? null;
297
361
  const details = run.kind === 'test'
298
362
  ? serializeTestExecutionDetail(getTestExecutionDetails(project.id, run.id), retryEligibility)
299
- : run.kind === 'github_actions'
300
- ? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
301
- : ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
302
- ? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
303
- : { kind: run.kind, available: true, retry_eligibility: retryEligibility };
363
+ : run.kind === 'check_batch'
364
+ ? {
365
+ kind: 'check_batch',
366
+ available: true,
367
+ batch: checkBatch,
368
+ attempts: checkBatchAttempts,
369
+ retry_eligibility: retryEligibility,
370
+ action_kind: retryEligibility.eligible ? 'rerun' : null,
371
+ }
372
+ : run.kind === 'check_attempt'
373
+ ? {
374
+ ...checkAttemptDetail,
375
+ attempt: checkAttempt,
376
+ retry_eligibility: retryEligibility,
377
+ action_kind: null,
378
+ }
379
+ : run.kind === 'github_actions'
380
+ ? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
381
+ : ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
382
+ ? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
383
+ : { kind: run.kind, available: true, retry_eligibility: retryEligibility };
304
384
  const artifacts = loadExecutionArtifactSummariesForRuns(project.id, [run.id], MAX_ARTIFACTS_PER_RUN).get(run.id)
305
385
  ?? { artifacts: [], artifact_count: 0 };
306
386
  return {
@@ -3628,6 +3708,9 @@ app.delete('/bundles/:id', (req, res) => {
3628
3708
  if (result.project.active_target_kind === 'bundle' && result.project.active_target_bundle_id === bundle.id) {
3629
3709
  return res.status(409).json({ error: 'Cannot delete the currently selected target bundle' });
3630
3710
  }
3711
+ if (findActiveCheckBatchForTarget(result.project.id, 'bundle', bundle.id)) {
3712
+ return res.status(409).json({ error: 'Cannot delete a bundle while checks are running' });
3713
+ }
3631
3714
  const affectedChatSessionIds = listChatSessions(result.project.id)
3632
3715
  .filter(chatSession => chatSession.target_bundle_id === bundle.id)
3633
3716
  .map(chatSession => chatSession.id);
@@ -3976,7 +4059,7 @@ app.post('/bundles/:id/follow-up', (req, res) => {
3976
4059
  return res.status(response.status).json(response.value);
3977
4060
  });
3978
4061
  const AUTOMATION_SIDE_EFFECT_KINDS = new Set(['automation_commit', 'automation_push', 'automation_pull_request']);
3979
- const AUTOMATION_WORKER_PAUSE_KINDS = new Set(['build', 'review', 'build_follow_up', 'automation_review_fix']);
4062
+ const AUTOMATION_WORKER_PAUSE_KINDS = new Set(['build', 'review', 'build_follow_up', 'automation_review_fix', 'check_batch']);
3980
4063
  app.post('/bundles/:id/resume-automation', async (req, res) => {
3981
4064
  const result = getRequiredProject(req);
3982
4065
  if ('error' in result) {
@@ -4272,7 +4355,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4272
4355
  return res.status(followUpResult.status).json({ error: followUpResult.error });
4273
4356
  }
4274
4357
  }
4275
- else if (decision.outcome === 'continue_commit_chain') {
4358
+ else if (decision.outcome === 'continue_commit_chain' || decision.outcome === 'rerun_required_checks') {
4276
4359
  try {
4277
4360
  if (!revalidateDecision()) {
4278
4361
  return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
@@ -4283,7 +4366,10 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
4283
4366
  progressionRunId,
4284
4367
  broadcaster: io,
4285
4368
  });
4286
- await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io, progressionRunId);
4369
+ const checkGateResume = decision.outcome === 'rerun_required_checks'
4370
+ ? resolveAutomationCheckGateResume(result.project.id, progressionRunId)
4371
+ : undefined;
4372
+ await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io, progressionRunId, checkGateResume);
4287
4373
  }
4288
4374
  catch (error) {
4289
4375
  return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
@@ -5495,7 +5581,7 @@ app.post('/projects', (req, res) => {
5495
5581
  return res.status(400).json({ error: validation.error });
5496
5582
  }
5497
5583
  const projectCount = Number(db.prepare('SELECT COUNT(*) AS count FROM projects').get()?.count ?? 0);
5498
- const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, 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;
5584
+ const { name, repo_path, linked_project_ids, run_setup, run_services, check_definitions, required_check_definition_ids, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, 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;
5499
5585
  if (!repo_path) {
5500
5586
  return res.status(400).json({ error: 'repo_path must be a non-empty string' });
5501
5587
  }
@@ -5515,6 +5601,8 @@ app.post('/projects', (req, res) => {
5515
5601
  repo_path,
5516
5602
  run_setup,
5517
5603
  run_services,
5604
+ check_definitions,
5605
+ required_check_definition_ids,
5518
5606
  test_commands,
5519
5607
  run_ide,
5520
5608
  preview_service_name,
@@ -5535,8 +5623,8 @@ app.post('/projects', (req, res) => {
5535
5623
  automation_review_severity,
5536
5624
  automation_review_max_iterations
5537
5625
  )
5538
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5539
- `).run(id, name, normalizedRepoPath, run_setup, run_services, test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, scan_include_globs_json ?? null, scan_exclude_globs_json ?? null, worktree_sync ?? null, spec_aware_code_review_enabled ?? 0, automation_depth ?? 'manual', automation_review_fix_enabled ?? 0, automation_pause_on_open_questions ?? 0, automation_review_severity ?? 'medium', automation_review_max_iterations ?? 3);
5626
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5627
+ `).run(id, name, normalizedRepoPath, run_setup, run_services, check_definitions, required_check_definition_ids ?? '[]', test_commands, run_ide ?? null, preview_service_name ?? null, preview_path ?? null, preview_capability_mode ?? null, helper_model ?? null, helper_variant ?? null, commit_message_rules ?? null, auto_park_stale_tickets ?? 0, memory_enabled ?? 0, scan_include_globs_json ?? null, scan_exclude_globs_json ?? null, worktree_sync ?? null, spec_aware_code_review_enabled ?? 0, automation_depth ?? 'manual', automation_review_fix_enabled ?? 0, automation_pause_on_open_questions ?? 0, automation_review_severity ?? 'medium', automation_review_max_iterations ?? 3);
5540
5628
  replaceProjectLinks(id, linked_project_ids ?? []);
5541
5629
  ensureProjectRootBundle(id);
5542
5630
  if (projectCount === 0) {
@@ -6702,13 +6790,15 @@ app.patch('/projects/:id', (req, res) => {
6702
6790
  });
6703
6791
  }
6704
6792
  }
6705
- const { name, repo_path, linked_project_ids, run_setup, run_services, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, 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;
6793
+ const { name, repo_path, linked_project_ids, run_setup, run_services, check_definitions, required_check_definition_ids, test_commands, run_ide, preview_service_name, preview_path, preview_capability_mode, helper_model, helper_variant, commit_message_rules, 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;
6706
6794
  db.prepare(`
6707
6795
  UPDATE projects SET
6708
6796
  name = ?,
6709
6797
  repo_path = ?,
6710
6798
  run_setup = ?,
6711
6799
  run_services = ?,
6800
+ check_definitions = ?,
6801
+ required_check_definition_ids = ?,
6712
6802
  test_commands = ?,
6713
6803
  run_ide = ?,
6714
6804
  preview_service_name = ?,
@@ -6730,7 +6820,7 @@ app.patch('/projects/:id', (req, res) => {
6730
6820
  automation_review_max_iterations = ?,
6731
6821
  updated_at = CURRENT_TIMESTAMP
6732
6822
  WHERE id = ?
6733
- `).run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, hasOwn(validation.values, 'test_commands') ? test_commands : project.test_commands, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'scan_include_globs_json') ? scan_include_globs_json : project.scan_include_globs_json, hasOwn(validation.values, 'scan_exclude_globs_json') ? scan_exclude_globs_json : project.scan_exclude_globs_json, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, hasOwn(validation.values, 'spec_aware_code_review_enabled') ? spec_aware_code_review_enabled : project.spec_aware_code_review_enabled, hasOwn(validation.values, 'automation_depth') ? automation_depth : project.automation_depth, hasOwn(validation.values, 'automation_review_fix_enabled') ? automation_review_fix_enabled : project.automation_review_fix_enabled, hasOwn(validation.values, 'automation_pause_on_open_questions') ? automation_pause_on_open_questions : project.automation_pause_on_open_questions, hasOwn(validation.values, 'automation_review_severity') ? automation_review_severity : project.automation_review_severity, hasOwn(validation.values, 'automation_review_max_iterations') ? automation_review_max_iterations : project.automation_review_max_iterations, req.params.id);
6823
+ `).run(name ?? project.name, repo_path ?? project.repo_path, hasOwn(validation.values, 'run_setup') ? run_setup : project.run_setup, hasOwn(validation.values, 'run_services') ? run_services : project.run_services, hasOwn(validation.values, 'check_definitions') ? check_definitions : project.check_definitions, hasOwn(validation.values, 'required_check_definition_ids') ? required_check_definition_ids : project.required_check_definition_ids, hasOwn(validation.values, 'test_commands') ? test_commands : project.test_commands, hasOwn(validation.values, 'run_ide') ? run_ide : project.run_ide, hasOwn(validation.values, 'preview_service_name') ? preview_service_name : project.preview_service_name, hasOwn(validation.values, 'preview_path') ? preview_path : project.preview_path, hasOwn(validation.values, 'preview_capability_mode') ? preview_capability_mode : project.preview_capability_mode, hasOwn(validation.values, 'helper_model') ? helper_model : project.helper_model, hasOwn(validation.values, 'helper_variant') ? helper_variant : project.helper_variant, hasOwn(validation.values, 'commit_message_rules') ? commit_message_rules : project.commit_message_rules, hasOwn(validation.values, 'auto_park_stale_tickets') ? auto_park_stale_tickets : project.auto_park_stale_tickets, hasOwn(validation.values, 'memory_enabled') ? memory_enabled : project.memory_enabled, hasOwn(validation.values, 'scan_include_globs_json') ? scan_include_globs_json : project.scan_include_globs_json, hasOwn(validation.values, 'scan_exclude_globs_json') ? scan_exclude_globs_json : project.scan_exclude_globs_json, hasOwn(validation.values, 'worktree_sync') ? worktree_sync : project.worktree_sync, hasOwn(validation.values, 'spec_aware_code_review_enabled') ? spec_aware_code_review_enabled : project.spec_aware_code_review_enabled, hasOwn(validation.values, 'automation_depth') ? automation_depth : project.automation_depth, hasOwn(validation.values, 'automation_review_fix_enabled') ? automation_review_fix_enabled : project.automation_review_fix_enabled, hasOwn(validation.values, 'automation_pause_on_open_questions') ? automation_pause_on_open_questions : project.automation_pause_on_open_questions, hasOwn(validation.values, 'automation_review_severity') ? automation_review_severity : project.automation_review_severity, hasOwn(validation.values, 'automation_review_max_iterations') ? automation_review_max_iterations : project.automation_review_max_iterations, req.params.id);
6734
6824
  if (hasOwn(validation.values, 'linked_project_ids')) {
6735
6825
  replaceProjectLinks(project.id, linked_project_ids ?? []);
6736
6826
  }
@@ -6759,6 +6849,9 @@ const assertProjectDeletionEligible = (projectId, claimId) => {
6759
6849
  if (stats.ticketCount > 0 || stats.worktreeCount > 0 || stats.bundleCount > 0) {
6760
6850
  throw new ProjectDeletionError(409, 'Cannot delete a project with existing tickets, bundles, or worktrees');
6761
6851
  }
6852
+ if (hasActiveProjectCheckBatch(projectId)) {
6853
+ throw new ProjectDeletionError(409, 'Cannot delete a project while checks are active');
6854
+ }
6762
6855
  };
6763
6856
  const completeClaimedProjectDeletion = async (projectId, claimId) => {
6764
6857
  await runExclusiveExecutionArtifactMaintenance(() => reconcileProjectExecutionArtifacts(projectId, {
@@ -6785,6 +6878,19 @@ const completeClaimedProjectDeletion = async (projectId, claimId) => {
6785
6878
  db.prepare("DELETE FROM execution_artifacts WHERE project_id = ? AND availability = 'deleted'").run(projectId);
6786
6879
  deleteProjectAttachments(projectId);
6787
6880
  db.prepare(`
6881
+ DELETE FROM check_gate_evaluation_attempts
6882
+ WHERE evaluation_id IN (SELECT id FROM check_gate_evaluations WHERE project_id = ?)
6883
+ `).run(projectId);
6884
+ db.prepare('DELETE FROM check_gate_evaluations WHERE project_id = ?').run(projectId);
6885
+ db.prepare(`
6886
+ DELETE FROM check_attempt_details
6887
+ WHERE execution_run_id IN (SELECT id FROM execution_runs WHERE project_id = ?)
6888
+ `).run(projectId);
6889
+ db.prepare(`
6890
+ DELETE FROM check_batch_details
6891
+ WHERE execution_run_id IN (SELECT id FROM execution_runs WHERE project_id = ?)
6892
+ `).run(projectId);
6893
+ db.prepare(`
6788
6894
  UPDATE execution_runs
6789
6895
  SET parent_run_id = NULL, retry_of_run_id = NULL
6790
6896
  WHERE project_id = ?
@@ -7671,6 +7777,186 @@ app.post('/projects/:id/test-sessions', (req, res) => {
7671
7777
  targetLabel: workspace.target_label,
7672
7778
  }));
7673
7779
  });
7780
+ app.post('/projects/:id/check-batches', async (req, res) => {
7781
+ const project = getProjectById(req.params.id);
7782
+ if (!project) {
7783
+ return res.status(404).json({ error: 'Not found' });
7784
+ }
7785
+ const body = req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : null;
7786
+ const targetKind = body?.target_kind === 'bundle'
7787
+ ? 'bundle'
7788
+ : body?.target_kind === 'repo_root'
7789
+ ? 'repo_root'
7790
+ : null;
7791
+ if (!targetKind) {
7792
+ return res.status(400).json({ error: 'target_kind must be repo_root or bundle' });
7793
+ }
7794
+ const definitionIds = parseCheckDefinitionIds(body?.definition_ids);
7795
+ if (!definitionIds) {
7796
+ return res.status(400).json({ error: 'definition_ids must contain at least one unique check definition ID' });
7797
+ }
7798
+ const definitions = getProjectCheckDefinitions(project);
7799
+ const definitionsById = new Map(definitions.map(definition => [definition.id, definition]));
7800
+ const selectedDefinitions = definitionIds.map(id => definitionsById.get(id)).filter((definition) => Boolean(definition));
7801
+ if (selectedDefinitions.length !== definitionIds.length) {
7802
+ return res.status(409).json({ error: 'One or more selected check definitions are stale or unavailable' });
7803
+ }
7804
+ const targetBundleId = typeof body?.target_bundle_id === 'string' ? body.target_bundle_id.trim() || null : null;
7805
+ const workspace = resolveExplicitProjectTestWorkspace(project, targetKind, targetBundleId);
7806
+ if ('error' in workspace) {
7807
+ return res.status(Number(workspace.status)).json({ error: workspace.error });
7808
+ }
7809
+ try {
7810
+ const batch = await startCheckBatch({
7811
+ projectId: project.id,
7812
+ cwd: workspace.cwd,
7813
+ targetKind: workspace.target_kind,
7814
+ targetBundleId: workspace.target_bundle_id,
7815
+ targetLabel: workspace.target_label,
7816
+ selectedDefinitions,
7817
+ availableDefinitions: definitions,
7818
+ sourceReference: { kind: 'project', id: project.id },
7819
+ }, io);
7820
+ return res.status(201).json({
7821
+ batch,
7822
+ attempts: getCheckBatchAttempts(project.id, batch.execution_run_id),
7823
+ });
7824
+ }
7825
+ catch (error) {
7826
+ return res.status(409).json({ error: getErrorMessage(error) });
7827
+ }
7828
+ });
7829
+ const parseCheckDefinitionIds = (value) => {
7830
+ if (!Array.isArray(value) || value.length === 0 || value.some(id => typeof id !== 'string' || id.trim() === '')) {
7831
+ return null;
7832
+ }
7833
+ const ids = value.map(id => id.trim());
7834
+ return ids.length === new Set(ids).size ? ids : null;
7835
+ };
7836
+ const parseCheckRerunSelection = (body) => {
7837
+ const payload = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
7838
+ if (payload.mode === undefined || payload.mode === 'all') {
7839
+ return { mode: 'all' };
7840
+ }
7841
+ if (payload.mode === 'failed_only') {
7842
+ return { mode: 'failed' };
7843
+ }
7844
+ if (payload.mode !== 'selected') {
7845
+ return { error: 'mode must be all, failed_only, or selected' };
7846
+ }
7847
+ const definitionIds = parseCheckDefinitionIds(payload.definition_ids);
7848
+ if (!definitionIds) {
7849
+ return { error: 'definition_ids must contain at least one unique source definition ID for selected reruns' };
7850
+ }
7851
+ return { mode: 'selected', definitionIds };
7852
+ };
7853
+ const rerunProjectCheckBatch = async (project, sourceBatchRunId, body) => {
7854
+ const sourceBatch = getCheckBatchDetails(project.id, sourceBatchRunId);
7855
+ if (!sourceBatch) {
7856
+ return { ok: false, status: 404, error: 'Check batch not found', reason: 'status_not_retryable' };
7857
+ }
7858
+ const selection = parseCheckRerunSelection(body);
7859
+ if ('error' in selection) {
7860
+ return { ok: false, status: 400, error: selection.error, reason: 'retry_input_unavailable' };
7861
+ }
7862
+ const eligibility = getCheckBatchRerunEligibility(project, sourceBatch, selection);
7863
+ if (!eligibility.eligible) {
7864
+ return {
7865
+ ok: false,
7866
+ status: 409,
7867
+ error: 'Check batch cannot be rerun with its current target and definitions',
7868
+ reason: eligibility.reason,
7869
+ };
7870
+ }
7871
+ const workspace = resolveExplicitProjectTestWorkspace(project, sourceBatch.target_kind, sourceBatch.target_bundle_id);
7872
+ if ('error' in workspace) {
7873
+ return { ok: false, status: Number(workspace.status), error: workspace.error, reason: 'retry_workspace_unavailable' };
7874
+ }
7875
+ try {
7876
+ const value = await rerunCheckBatchAndSchedule({
7877
+ projectId: project.id,
7878
+ sourceBatchRunId,
7879
+ cwd: workspace.cwd,
7880
+ availableDefinitions: getProjectCheckDefinitions(project),
7881
+ selection,
7882
+ }, io);
7883
+ return { ok: true, value };
7884
+ }
7885
+ catch (error) {
7886
+ return { ok: false, status: 409, error: getErrorMessage(error), reason: 'retry_input_unavailable' };
7887
+ }
7888
+ };
7889
+ app.get('/projects/:id/check-batches', (req, res) => {
7890
+ const project = getProjectById(req.params.id);
7891
+ if (!project) {
7892
+ return res.status(404).json({ error: 'Not found' });
7893
+ }
7894
+ const query = parseLifecycleListQuery(req.query);
7895
+ if ('error' in query) {
7896
+ return res.status(400).json({ error: query.error });
7897
+ }
7898
+ try {
7899
+ return res.json(listExecutionTimeline({
7900
+ project,
7901
+ kind: 'check_batch',
7902
+ statuses: query.statuses,
7903
+ cursor: query.cursor,
7904
+ limit: query.limit,
7905
+ }));
7906
+ }
7907
+ catch (error) {
7908
+ if (error instanceof Error && error.message === 'Invalid execution run cursor') {
7909
+ return res.status(400).json({ error: 'cursor must be a valid execution run cursor' });
7910
+ }
7911
+ throw error;
7912
+ }
7913
+ });
7914
+ app.get('/projects/:id/check-batches/:batchRunId', async (req, res) => {
7915
+ const project = getProjectById(req.params.id);
7916
+ if (!project) {
7917
+ return res.status(404).json({ error: 'Not found' });
7918
+ }
7919
+ const batch = getCheckBatchDetails(req.params.id, req.params.batchRunId);
7920
+ if (!batch) {
7921
+ return res.status(404).json({ error: 'Check batch not found' });
7922
+ }
7923
+ return res.json({
7924
+ batch,
7925
+ attempts: await hydrateCheckAttemptCurrency(project, batch, getCheckBatchAttempts(req.params.id, req.params.batchRunId)),
7926
+ });
7927
+ });
7928
+ app.get('/projects/:id/check-gate-evaluations/:evaluationId', (req, res) => {
7929
+ const evaluation = getCheckGateEvaluation(req.params.id, req.params.evaluationId);
7930
+ if (!evaluation) {
7931
+ return res.status(404).json({ error: 'Check gate evaluation not found' });
7932
+ }
7933
+ return res.json(evaluation);
7934
+ });
7935
+ app.post('/projects/:id/check-batches/:batchRunId/stop', async (req, res) => {
7936
+ if (!getCheckBatchDetails(req.params.id, req.params.batchRunId)) {
7937
+ return res.status(404).json({ error: 'Check batch not found' });
7938
+ }
7939
+ try {
7940
+ const batch = await stopCheckBatch(req.params.id, req.params.batchRunId, io);
7941
+ return res.json({
7942
+ batch,
7943
+ attempts: getCheckBatchAttempts(req.params.id, req.params.batchRunId),
7944
+ });
7945
+ }
7946
+ catch (error) {
7947
+ return res.status(409).json({ error: getErrorMessage(error) });
7948
+ }
7949
+ });
7950
+ app.post('/projects/:id/check-batches/:batchRunId/rerun', async (req, res) => {
7951
+ const project = getProjectById(req.params.id);
7952
+ if (!project) {
7953
+ return res.status(404).json({ error: 'Not found' });
7954
+ }
7955
+ const result = await rerunProjectCheckBatch(project, req.params.batchRunId, req.body);
7956
+ return result.ok
7957
+ ? res.status(201).json(result.value)
7958
+ : res.status(result.status).json({ error: result.error });
7959
+ });
7674
7960
  app.get('/projects/:id/execution-runs', (req, res) => {
7675
7961
  const project = getProjectById(req.params.id);
7676
7962
  if (!project) {
@@ -7755,7 +8041,7 @@ app.get('/projects/:id/execution-timeline', (req, res) => {
7755
8041
  throw error;
7756
8042
  }
7757
8043
  });
7758
- app.get('/projects/:id/execution-timeline/:runId', (req, res) => {
8044
+ app.get('/projects/:id/execution-timeline/:runId', async (req, res) => {
7759
8045
  const project = getProjectById(req.params.id);
7760
8046
  if (!project) {
7761
8047
  return res.status(404).json({ error: 'Not found' });
@@ -7772,9 +8058,9 @@ app.get('/projects/:id/execution-timeline/:runId', (req, res) => {
7772
8058
  if (scope.scopeEntity && !links.some(link => link.entity_type === scope.scopeEntity?.type && link.entity_id === scope.scopeEntity.id)) {
7773
8059
  return res.status(404).json({ error: 'Execution run not found' });
7774
8060
  }
7775
- return res.json(buildExecutionRunDetailResponse(project, run));
8061
+ return res.json(await buildExecutionRunDetailResponse(project, run));
7776
8062
  });
7777
- app.get('/projects/:id/execution-runs/:runId', (req, res) => {
8063
+ app.get('/projects/:id/execution-runs/:runId', async (req, res) => {
7778
8064
  const project = getProjectById(req.params.id);
7779
8065
  if (!project) {
7780
8066
  return res.status(404).json({ error: 'Not found' });
@@ -7783,7 +8069,7 @@ app.get('/projects/:id/execution-runs/:runId', (req, res) => {
7783
8069
  if (!run) {
7784
8070
  return res.status(404).json({ error: 'Execution run not found' });
7785
8071
  }
7786
- return res.json(buildExecutionRunDetailResponse(project, run));
8072
+ return res.json(await buildExecutionRunDetailResponse(project, run));
7787
8073
  });
7788
8074
  app.get('/projects/:id/execution-runs/:runId/artifacts/:artifactId', async (req, res) => {
7789
8075
  const project = getProjectById(req.params.id);
@@ -7827,6 +8113,13 @@ app.post('/projects/:id/execution-runs/:runId/retry', async (req, res) => {
7827
8113
  if (!project) {
7828
8114
  return res.status(404).json({ error: 'Not found' });
7829
8115
  }
8116
+ const source = getExecutionRun(project.id, req.params.runId);
8117
+ if (source?.kind === 'check_batch') {
8118
+ const result = await rerunProjectCheckBatch(project, source.id, req.body);
8119
+ return result.ok
8120
+ ? res.status(201).json(result.value)
8121
+ : res.status(result.status).json({ error: result.error, reason: result.reason });
8122
+ }
7830
8123
  const resolution = resolveExecutionRetry(project.id, req.params.runId);
7831
8124
  const result = await dispatchExecutionRetry(resolution, io, async (retry) => {
7832
8125
  const session = createTestSession({
@@ -7892,7 +8185,7 @@ app.get('/test-sessions/:id/discovery', (req, res) => {
7892
8185
  if (!project) {
7893
8186
  return res.status(404).json({ error: 'Project not found' });
7894
8187
  }
7895
- command = findProjectTestCommand(project, commandId);
8188
+ command = getProjectTestCommands(project).find(item => item.id === commandId) ?? null;
7896
8189
  if (!command) {
7897
8190
  return res.status(404).json({ error: 'Test command not found' });
7898
8191
  }
@@ -7928,7 +8221,7 @@ app.post('/test-sessions/:id/run', async (req, res) => {
7928
8221
  if (!selection) {
7929
8222
  return res.status(400).json({ error: 'selection must be all, file, or case' });
7930
8223
  }
7931
- const command = findProjectTestCommand(project, commandId);
8224
+ const command = getProjectTestCommands(project).find(item => item.id === commandId) ?? null;
7932
8225
  if (!command) {
7933
8226
  return res.status(404).json({ error: 'Test command not found' });
7934
8227
  }
@@ -8305,6 +8598,7 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
8305
8598
  };
8306
8599
  httpServer.once('error', handleError);
8307
8600
  const { bindHost } = resolveDesktopServerHost(host);
8601
+ openCheckRunnerAdmissions();
8308
8602
  httpServer.listen(port, bindHost, handleListening);
8309
8603
  });
8310
8604
  }
@@ -8328,6 +8622,9 @@ export const shutdownServer = async (signal) => {
8328
8622
  return shutdownServerPromise;
8329
8623
  }
8330
8624
  shutdownRequested = true;
8625
+ closeCheckRunnerAdmissions();
8626
+ const httpServerClosePromise = httpServer.listening ? closeHttpServer(httpServer) : null;
8627
+ void httpServerClosePromise?.catch(() => undefined);
8331
8628
  const pendingStartup = startServerPromise;
8332
8629
  shutdownServerPromise = (async () => {
8333
8630
  await Promise.all([
@@ -8344,6 +8641,7 @@ export const shutdownServer = async (signal) => {
8344
8641
  }
8345
8642
  await executionArtifactCleanupPromise;
8346
8643
  await stopMobileAccess();
8644
+ await shutdownCheckRunner();
8347
8645
  await clearAllTestSessions(io);
8348
8646
  await clearAllGitHubActionsSessions();
8349
8647
  await shutdownRealtimeServer({
@@ -8356,6 +8654,7 @@ export const shutdownServer = async (signal) => {
8356
8654
  stopAllReviewAgents,
8357
8655
  stopAllPlanAgents,
8358
8656
  stopAllTickets,
8657
+ httpServerClosePromise,
8359
8658
  });
8360
8659
  })().finally(() => {
8361
8660
  shutdownRequested = false;