@straiffi/archon 1.7.2 → 1.7.4
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.
- package/dist/client/assets/ActivityView-BgMz3NNT.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-CNqWDS3E.js +1 -0
- package/dist/client/assets/TestsDialog-Cll7xJe0.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-Do9nbC8v.js → architectureDiagram-3BPJPVTR-BL5dYiul.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-DGGOLHF8.js → blockDiagram-GPEHLZMM-Bi5x2v80.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-Bz7tQybs.js → c4Diagram-AAUBKEIU-esEm54Ay.js} +1 -1
- package/dist/client/assets/channel-CqNHF3FD.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-CJO-GmWJ.js → chunk-2J33WTMH-DACfY8xM.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-DPAVagpC.js → chunk-3OPIFGDE-DZNUiaUs.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-Cof75Mu7.js → chunk-5ZQYHXKU-ClDfkMAp.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-COuPqQ8_.js → chunk-727SXJPM-CCwA3GWT.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-CFj5KabW.js → chunk-AQP2D5EJ-Byl4DRZ9.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-C4M6w5cT.js → chunk-CSCIHK7Q-CXymZQ7A.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-ByaiwVue.js → chunk-KSCS5N6A-VV3D8g6a.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-Cx5jeot9.js → chunk-L5ZTLDWV-BL9N5GRI.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-DmGtztud.js → chunk-LZXEDZCA-DKNN-zno.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-i3yuspqL.js → chunk-ND2GUHAM-CHCpQ1yr.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-CQ4E_jP4.js → chunk-NZK2D7GU-B0Dm74mt.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-D2g4VDCd.js → chunk-O5CBEL6O-DAa5z2xG.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-exIu-zQ2.js → chunk-WU5MYG2G-DrVJWKVR.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BjEgQwKg.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-gGomH-F_.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-DvF75yLZ.js → dagre-BM42HDAG-B0fsl8mE.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-DFv0HxLL.js → diagram-2AECGRRQ-Cx_VEbSS.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-C_LRir0-.js → diagram-5GNKFQAL-Cv7VVn5r.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-DGZxRDRy.js → diagram-KO2AKTUF-C8Sl5czV.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-UXt0uti4.js → diagram-LMA3HP47-C3dZaLz2.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-CFbZS4n5.js → diagram-OG6HWLK6-BAU9DtTr.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-CfAwZDdf.js → erDiagram-TEJ5UH35-DpNc-eBD.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Cdgi5z3n.js → flowDiagram-I6XJVG4X-DGwTAT1S.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-DOrkFkhu.js → ganttDiagram-6RSMTGT7-MPZhEgzJ.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-cEox0peD.js → gitGraphDiagram-PVQCEYII-CY0tHL5N.js} +1 -1
- package/dist/client/assets/index-CJRgmWwY.js +273 -0
- package/dist/client/assets/index-mRkJWu70.css +2 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-gm51u2Nn.js → infoDiagram-5YYISTIA-BBZeKjf4.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CjnoOIja.js → ishikawaDiagram-YF4QCWOH-CM_iFrES.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Dlg4DsiO.js → journeyDiagram-JHISSGLW-Do_lY94X.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BU0XZaSd.js → kanban-definition-UN3LZRKU-brxg3j37.js} +1 -1
- package/dist/client/assets/{line-Bert22-_.js → line-PHHbm9rj.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-BvQSGDmL.js → mermaid-parser.core-CLxAkW69.js} +1 -1
- package/dist/client/assets/{mermaid.core--qSAHJRj.js → mermaid.core-CebvSmx9.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgFIvWWD.js → mindmap-definition-RKZ34NQL-CX_-ZaJF.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DGcbXD0M.js → pieDiagram-4H26LBE5-BJuHvql_.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-ClkBUeia.js → quadrantDiagram-W4KKPZXB-DiJll7l9.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-B8CwGToU.js → requirementDiagram-4Y6WPE33-Bg-tXgcj.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DTk3jL6B.js → sankeyDiagram-5OEKKPKP-BoK2egfZ.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQVrcnKf.js → sequenceDiagram-3UESZ5HK-DcrDWNzv.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-DD4yMZPB.js → stateDiagram-AJRCARHV-B995qt1S.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Dn8P7Sa1.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-BnCBVqcd.js → timeline-definition-PNZ67QCA-DqSVAhqL.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-BDmLMNXS.js → vennDiagram-CIIHVFJN-D9wVraVY.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DSLuQiEv.js → wardleyDiagram-YWT4CUSO-CLff9zVd.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-DAq_LA4r.js → xychartDiagram-2RQKCTM6-BuHUMMH2.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +472 -39
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +1311 -212
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/agent.js +228 -71
- package/dist/server/lib/agent.js.map +1 -1
- package/dist/server/lib/agentRuns.js +205 -38
- package/dist/server/lib/agentRuns.js.map +1 -1
- package/dist/server/lib/attachments.js +24 -0
- package/dist/server/lib/attachments.js.map +1 -1
- package/dist/server/lib/automationProgression.js +464 -0
- package/dist/server/lib/automationProgression.js.map +1 -0
- package/dist/server/lib/automationReconciliation.js +498 -0
- package/dist/server/lib/automationReconciliation.js.map +1 -0
- package/dist/server/lib/automationStageDetails.js +232 -0
- package/dist/server/lib/automationStageDetails.js.map +1 -0
- package/dist/server/lib/buildOpenQuestionOperations.js +238 -0
- package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
- package/dist/server/lib/buildOpenQuestions.js +44 -0
- package/dist/server/lib/buildOpenQuestions.js.map +1 -1
- package/dist/server/lib/buildSequences.js +130 -46
- package/dist/server/lib/buildSequences.js.map +1 -1
- package/dist/server/lib/bundleReviewOperations.js +91 -9
- package/dist/server/lib/bundleReviewOperations.js.map +1 -1
- package/dist/server/lib/chatOperations.js +67 -12
- package/dist/server/lib/chatOperations.js.map +1 -1
- package/dist/server/lib/executionReconciliation.js +518 -0
- package/dist/server/lib/executionReconciliation.js.map +1 -0
- package/dist/server/lib/executionRetry.js +694 -0
- package/dist/server/lib/executionRetry.js.map +1 -0
- package/dist/server/lib/executionRetryContext.js +489 -0
- package/dist/server/lib/executionRetryContext.js.map +1 -0
- package/dist/server/lib/executionRetryPredicates.js +74 -0
- package/dist/server/lib/executionRetryPredicates.js.map +1 -0
- package/dist/server/lib/executionRunRetention.js +141 -0
- package/dist/server/lib/executionRunRetention.js.map +1 -0
- package/dist/server/lib/executionRuns.js +1367 -0
- package/dist/server/lib/executionRuns.js.map +1 -0
- package/dist/server/lib/executionTimeline.js +45 -0
- package/dist/server/lib/executionTimeline.js.map +1 -0
- package/dist/server/lib/git.js +385 -6
- package/dist/server/lib/git.js.map +1 -1
- package/dist/server/lib/integrations/github.js +43 -0
- package/dist/server/lib/integrations/github.js.map +1 -1
- package/dist/server/lib/integrations/githubActions.js +1 -0
- package/dist/server/lib/integrations/githubActions.js.map +1 -1
- package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
- package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
- package/dist/server/lib/projectMemorySelections.js +7 -3
- package/dist/server/lib/projectMemorySelections.js.map +1 -1
- package/dist/server/lib/shutdown.js +2 -2
- package/dist/server/lib/shutdown.js.map +1 -1
- package/dist/server/lib/specArtifact.js +3 -0
- package/dist/server/lib/specArtifact.js.map +1 -1
- package/dist/server/lib/specFormatting.js +1 -1
- package/dist/server/lib/specFormatting.js.map +1 -1
- package/dist/server/lib/specOperations.js +78 -12
- package/dist/server/lib/specOperations.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +54 -18
- package/dist/server/lib/specReviewContext.js.map +1 -1
- package/dist/server/lib/specReviewDraft.js +3 -2
- package/dist/server/lib/specReviewDraft.js.map +1 -1
- package/dist/server/lib/specReviewOperations.js +74 -9
- package/dist/server/lib/specReviewOperations.js.map +1 -1
- package/dist/server/lib/specReviewValidation.js +217 -0
- package/dist/server/lib/specReviewValidation.js.map +1 -0
- package/dist/server/lib/specReviews.js +30 -2
- package/dist/server/lib/specReviews.js.map +1 -1
- package/dist/server/lib/specs.js +18 -1
- package/dist/server/lib/specs.js.map +1 -1
- package/dist/server/lib/stageSnapshots.js +197 -0
- package/dist/server/lib/stageSnapshots.js.map +1 -0
- package/dist/server/lib/testSessions.js +160 -18
- package/dist/server/lib/testSessions.js.map +1 -1
- package/dist/server/lib/ticketAutomation.js +80 -4
- package/dist/server/lib/ticketAutomation.js.map +1 -1
- package/dist/server/lib/ticketAutomationCommit.js +466 -91
- package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
- package/dist/server/lib/ticketFollowUpOperations.js +270 -29
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketMessages.js +16 -0
- package/dist/server/lib/ticketMessages.js.map +1 -1
- package/dist/server/lib/ticketRunOperations.js +51 -6
- package/dist/server/lib/ticketRunOperations.js.map +1 -1
- package/dist/server/lib/ticketUndo.js +77 -168
- package/dist/server/lib/ticketUndo.js.map +1 -1
- package/dist/server/lib/ticketWorkflowOperations.js +132 -28
- package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
- package/dist/server/lib/tickets.js +41 -27
- package/dist/server/lib/tickets.js.map +1 -1
- package/dist/server/lib/transcriptRuns.js +3 -3
- package/dist/server/lib/transcriptRuns.js.map +1 -1
- package/dist/server/lib/turnExecutionErrors.js +12 -0
- package/dist/server/lib/turnExecutionErrors.js.map +1 -0
- package/dist/server/lib/types.js +58 -1
- package/dist/server/lib/types.js.map +1 -1
- package/dist/server/workers/build.js +596 -184
- package/dist/server/workers/build.js.map +1 -1
- package/dist/server/workers/chat.js +269 -132
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +731 -203
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +546 -188
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +240 -75
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +790 -297
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +212 -46
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +434 -254
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/GitHubActionsDialog-DAD5REmY.js +0 -1
- package/dist/client/assets/TestsDialog-B5NVrroq.js +0 -4
- package/dist/client/assets/TicketChangesModal-By-_gTGd.js +0 -1
- package/dist/client/assets/channel-Dd5Len0X.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-B7KUg6r8.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CQm-z5c4.js +0 -1
- package/dist/client/assets/index-BHq6-Eir.js +0 -270
- package/dist/client/assets/index-BxTc5qQT.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C8yO6POT.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from 'fs';
|
|
|
4
4
|
import { createServer } from 'http';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { Server } from 'socket.io';
|
|
7
|
-
import { randomUUID } from 'crypto';
|
|
7
|
+
import { createHash, randomUUID } from 'crypto';
|
|
8
8
|
import cors from 'cors';
|
|
9
9
|
import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator';
|
|
10
10
|
import db from './db.js';
|
|
@@ -26,10 +26,10 @@ import { browseRepoPath } from './lib/directoryPicker.js';
|
|
|
26
26
|
import { resolveDesktopServerHost } from './lib/desktopServerHost.js';
|
|
27
27
|
import { emitMobileRealtime, getMobileAccessStatus, regenerateMobilePairingChallenge, revokeMobileSession, setMobileAccessStatusChangeListener, startMobileAccess, stopMobileAccess, } from './lib/mobileAccess.js';
|
|
28
28
|
import { validateDesktopMobileAccessRequest } from './lib/mobileAccessSecurity.js';
|
|
29
|
-
import { refreshChatSessionUsageSnapshot, refreshSpecSessionUsageSnapshot, stopAllBuildAgents, stopAllReviewAgents } from './lib/agent.js';
|
|
29
|
+
import { refreshChatSessionUsageSnapshot, refreshSpecSessionUsageSnapshot, stopAllBuildAgents, stopAllReviewAgents, stopBuildAgent, stopReviewAgent } from './lib/agent.js';
|
|
30
30
|
import { runLightweightPrompt } from './lib/lightweightPrompt.js';
|
|
31
31
|
import { startBuildChain, stopBuildChain } from './lib/buildChains.js';
|
|
32
|
-
import { enqueueBundledBuild, getDependencyOrderedBundleTicketIds, removeTicketFromBuildSequence,
|
|
32
|
+
import { activateQueuedBuildSequenceTicket, compensateBuildSequenceAcceptance, enqueueBundledBuild, getDependencyOrderedBundleTicketIds, hasActiveSequenceForBundle, removeTicketFromBuildSequence, stopBuildSequence, } from './lib/buildSequences.js';
|
|
33
33
|
import { prepareTicketForBuild } from './lib/buildFlow.js';
|
|
34
34
|
import { getBundlePlanMutationBlockerById, shouldBlockPlanForBundleMutationById, } from './lib/bundleActivity.js';
|
|
35
35
|
import { getDiscoveredModels, normalizeModelId, normalizeTicketModelId } from './lib/models.js';
|
|
@@ -45,11 +45,19 @@ import { getProjectFileManifest, ProjectFileContentError, ProjectFileTargetUnava
|
|
|
45
45
|
import { updateProjectTargetSelection } from './lib/projectTargets.js';
|
|
46
46
|
import { exportProjectMemoryArchive, importProjectMemoryArchive } from './lib/projectMemoryTransfer.js';
|
|
47
47
|
import { buildReviewFindingsResponse, computeDiffSignature, getLatestReviewRunForContext, listReviewFindings } from './lib/reviewFindings.js';
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
48
|
+
import { formatBuildOpenQuestionAnswersPrompt, getLatestBuildOpenQuestionBatchStatus, hasActiveBuildOpenQuestionBatch, listBuildOpenQuestionsForBundle, loadBuildOpenQuestionsByBundleIds, } from './lib/buildOpenQuestions.js';
|
|
49
|
+
import { acceptBuildOpenQuestionResolution } from './lib/buildOpenQuestionOperations.js';
|
|
50
|
+
import { completeAutomationProgression, createAutomationReviewFix, finishAutomationReviewFix, getAutomationProgression, recoverLegacyAutomationProgression, recoverStoppedSideEffectProgression, runAutomationProgression, stopAutomationProgression, waitAutomationProgression } from './lib/automationProgression.js';
|
|
51
|
+
import { reconcileAutomationStage } from './lib/automationReconciliation.js';
|
|
52
|
+
import { getAutomationStageDetails, } from './lib/automationStageDetails.js';
|
|
53
|
+
import { clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, isAutomationDepth, recordAutomationPause, resolveAutomationConfig, upsertBundleAutomationState, } from './lib/ticketAutomation.js';
|
|
50
54
|
import { runAutomationCommitChain } from './lib/ticketAutomationCommit.js';
|
|
51
55
|
import { getDiscoveredSkills, normalizeSkillNames } from './lib/skills.js';
|
|
52
56
|
import { clearAllPreviewProxies } from './lib/previewProxy.js';
|
|
57
|
+
import { createExecutionRun, addExecutionRunLink, addExecutionRunLinks, getExecutionRun, hasActiveBuildExecutionRunForTicket, getGitHubActionsExecutionDetails, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listLifecycleEvents, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
|
|
58
|
+
import { dispatchExecutionRetry, getExecutionRetryEligibility, resolveExecutionRetry } from './lib/executionRetry.js';
|
|
59
|
+
import { listExecutionTimeline } from './lib/executionTimeline.js';
|
|
60
|
+
import { cleanupAllExecutionEvidence, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS, resolveExecutionEvidenceRetentionDays, } from './lib/executionRunRetention.js';
|
|
53
61
|
import { shutdownRealtimeServer } from './lib/shutdown.js';
|
|
54
62
|
import { resolveClientBuildPaths, shouldServeClientApp, STATIC_CLIENT_DIRECTORY_OPTIONS, STATIC_CLIENT_INDEX_OPTIONS, } from './lib/staticClient.js';
|
|
55
63
|
import { deleteIntegrationConnection, getGitHubConnectionConfig, getJiraConnectionConfig, listIntegrationConnections, upsertIntegrationConnection, } from './lib/integrations/index.js';
|
|
@@ -58,7 +66,8 @@ import { importJiraIssue, JiraApiError, prepareJiraIssueImport, validateJiraConn
|
|
|
58
66
|
import { importGitHubIssue, prepareGitHubIssueImport } from './lib/integrations/github-issues.js';
|
|
59
67
|
import { buildImportedIssueSessionTitle, importProviderHostedIssueImages } from './lib/integrations/importedIssueImages.js';
|
|
60
68
|
import { dispatchGitHubWorkflow, getGitHubWorkflowDispatchConfig, getGitHubWorkflowRun, getGitHubWorkflowRunJobs, listGitHubWorkflows, listGitHubWorkflowRuns, } from './lib/integrations/githubActions.js';
|
|
61
|
-
import { clearAllGitHubActionsSessions, getActiveTrackedRunCount, getTrackedGitHubActionsRuns, trackGitHubActionsRun, } from './lib/integrations/githubActionsSessions.js';
|
|
69
|
+
import { clearAllGitHubActionsSessions, getActiveTrackedRunCount, getTrackedGitHubActionsRuns, startGitHubActionsPolling, trackGitHubActionsRun, } from './lib/integrations/githubActionsSessions.js';
|
|
70
|
+
import { reconcileStartupExecutions } from './lib/executionReconciliation.js';
|
|
62
71
|
import { PlanningReferenceError, resolveExternalPlanningDescription } from './lib/integrations/planning.js';
|
|
63
72
|
import { refreshIfNeeded as refreshAssignedIssues, clearCachedAssignedIssues } from './lib/integrations/assignedIssuesCache.js';
|
|
64
73
|
import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib/commitMessage.js';
|
|
@@ -70,12 +79,12 @@ import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTic
|
|
|
70
79
|
import { submitPlanTicketFollowUp, submitTicketFollowUp, } from './lib/ticketFollowUpOperations.js';
|
|
71
80
|
import { canUseBuildControls, formatActiveRunConflict, getTicketRunReadinessError, resumeBuildTicketRun, runProjectTarget, runTicketRun, stopBuildTicketRun, stopReviewTicketRun, stopProjectTargetRun, stopTicketRun, } from './lib/ticketRunOperations.js';
|
|
72
81
|
import { isTicketAgentSettingsPayload, updateTicketAgentSettings, } from './lib/ticketSettingsOperations.js';
|
|
73
|
-
import { resolveBundleReviewOwner, transitionBundleToReview } from './lib/bundleReviewOperations.js';
|
|
74
|
-
import { getActiveBundleStageChangeHead, undoTicketStage } from './lib/ticketUndo.js';
|
|
82
|
+
import { rerunTicketReviewOperation, resolveBundleReviewOwner, transitionBundleToReview } from './lib/bundleReviewOperations.js';
|
|
83
|
+
import { clearTicketStageSnapshots, getActiveBundleStageChangeHead, reconcileStageSnapshots, undoTicketStage } from './lib/ticketUndo.js';
|
|
75
84
|
import { closeAllTerminalSessions, createProjectTerminalSession, createTerminalSessionForWorkspace, destroyTerminalSessionById, registerTerminalSocketHandlers, } from './lib/terminal.js';
|
|
76
85
|
import { countBundleTickets, createTicketRecord, createBundle, deleteBundle, ensureProjectRootBundle, autoParkTicketIfStale, autoParkSettledBundleTicket, isBundleSettledForAutoPark, clearAutoParkDismissalIfNeeded, assignTicketDoneOrder, assignTicketLaneOrder, getBundle, getBundleByName, getBundleByBranch, getBundleRepresentativeTicketContext, isProjectRootBundle, getTicket, isBundledTicket, listTicketsInRunContext, listBundles, listBoardTicketEnrichment, listTickets, resolveTicketBranch, resolveTicketTool, updateBundleAutomationConfig, updateBundleIdentity, } from './lib/tickets.js';
|
|
77
86
|
import { createTicketMessage, upsertTicketDescriptionMessage } from './lib/ticketMessages.js';
|
|
78
|
-
import { listAgentRunsForParent } from './lib/agentRuns.js';
|
|
87
|
+
import { listAgentRunsForExecutionRun, listAgentRunsForParent, terminalizeAgentRunsForExecutionRun } from './lib/agentRuns.js';
|
|
79
88
|
import { HttpError } from './lib/httpError.js';
|
|
80
89
|
import { parseIncomingExternalSource } from './lib/externalSource.js';
|
|
81
90
|
import { materializeTicketsWithDependencies } from './lib/ticketBatch.js';
|
|
@@ -84,9 +93,8 @@ import { buildSpecImplementationDraft, buildVisualizationAppendix } from './lib/
|
|
|
84
93
|
import { exportSpecToDocs, checkSpecExportCollision } from './lib/specExport.js';
|
|
85
94
|
import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
|
|
86
95
|
import { startBuild } from './workers/build.js';
|
|
87
|
-
import {
|
|
88
|
-
import {
|
|
89
|
-
import { startReview } from './workers/review.js';
|
|
96
|
+
import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, prepareLinkedPlanningResearch, startPlan, stopAllPlanAgents, } from './workers/plan.js';
|
|
97
|
+
import { EXECUTION_RUN_KINDS, validateWorkflowContext } from './lib/types.js';
|
|
90
98
|
const getErrorMessage = (error) => {
|
|
91
99
|
return error instanceof Error ? error.message : String(error);
|
|
92
100
|
};
|
|
@@ -134,6 +142,125 @@ const parseMultiValueQuery = (value, allowedValues, fieldName) => {
|
|
|
134
142
|
values: entries.filter((entry, index, values) => values.indexOf(entry) === index),
|
|
135
143
|
};
|
|
136
144
|
};
|
|
145
|
+
const EXECUTION_RUN_STATUSES = ['queued', 'running', 'waiting', 'completed', 'failed', 'cancelled', 'interrupted'];
|
|
146
|
+
const parseLifecycleListQuery = (query) => {
|
|
147
|
+
const kind = parseMultiValueQuery(query.kind, EXECUTION_RUN_KINDS, 'kind');
|
|
148
|
+
if ('error' in kind) {
|
|
149
|
+
return kind;
|
|
150
|
+
}
|
|
151
|
+
if (kind.values.length > 1) {
|
|
152
|
+
return { error: 'kind must include at most one value' };
|
|
153
|
+
}
|
|
154
|
+
const status = parseMultiValueQuery(query.status, EXECUTION_RUN_STATUSES, 'status');
|
|
155
|
+
if ('error' in status) {
|
|
156
|
+
return status;
|
|
157
|
+
}
|
|
158
|
+
if (query.cursor != null && typeof query.cursor !== 'string') {
|
|
159
|
+
return { error: 'cursor must be a string' };
|
|
160
|
+
}
|
|
161
|
+
const limit = query.limit == null || query.limit === '' ? 50 : Number(query.limit);
|
|
162
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
|
|
163
|
+
return { error: 'limit must be an integer between 1 and 200' };
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
kind: kind.values[0],
|
|
167
|
+
statuses: status.values,
|
|
168
|
+
cursor: typeof query.cursor === 'string' && query.cursor !== '' ? query.cursor : null,
|
|
169
|
+
limit,
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
const parseExecutionTimelineScope = (projectId, query) => {
|
|
173
|
+
const hasScopeType = query.scope_type != null && query.scope_type !== '';
|
|
174
|
+
const hasScopeId = query.scope_id != null && query.scope_id !== '';
|
|
175
|
+
if (hasScopeType !== hasScopeId) {
|
|
176
|
+
return { error: 'scope_type and scope_id must be provided together' };
|
|
177
|
+
}
|
|
178
|
+
if (!hasScopeType) {
|
|
179
|
+
return { scopeEntity: undefined };
|
|
180
|
+
}
|
|
181
|
+
if (typeof query.scope_type !== 'string' || typeof query.scope_id !== 'string') {
|
|
182
|
+
return { error: 'scope_type and scope_id must be strings' };
|
|
183
|
+
}
|
|
184
|
+
const scopeType = query.scope_type;
|
|
185
|
+
const scopeId = query.scope_id.trim();
|
|
186
|
+
if (!['ticket', 'bundle', 'chat_session', 'spec_session'].includes(scopeType)) {
|
|
187
|
+
return { error: 'scope_type must be ticket, bundle, chat_session, or spec_session' };
|
|
188
|
+
}
|
|
189
|
+
if (!scopeId) {
|
|
190
|
+
return { error: 'scope_id must not be empty' };
|
|
191
|
+
}
|
|
192
|
+
const table = scopeType === 'ticket'
|
|
193
|
+
? 'tickets'
|
|
194
|
+
: scopeType === 'bundle'
|
|
195
|
+
? 'worktree_bundles'
|
|
196
|
+
: scopeType === 'chat_session'
|
|
197
|
+
? 'chat_sessions'
|
|
198
|
+
: 'spec_sessions';
|
|
199
|
+
const exists = db.prepare(`SELECT 1 FROM ${table} WHERE id = ? AND project_id = ?`).get(scopeId, projectId);
|
|
200
|
+
if (!exists) {
|
|
201
|
+
return { error: 'Scope not found', status: 404 };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
scopeEntity: {
|
|
205
|
+
type: scopeType,
|
|
206
|
+
id: scopeId,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
const serializeTestExecutionDetail = (details, retryEligibility) => {
|
|
211
|
+
if (!details) {
|
|
212
|
+
return { kind: 'test', available: false, retry_eligibility: retryEligibility };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
kind: 'test',
|
|
216
|
+
available: true,
|
|
217
|
+
command_id: details.command_id,
|
|
218
|
+
command_name: details.command_name,
|
|
219
|
+
command_runner: details.command_runner,
|
|
220
|
+
target_kind: details.target_kind,
|
|
221
|
+
target_bundle_id: details.target_bundle_id,
|
|
222
|
+
target_label: details.target_label,
|
|
223
|
+
ticket_id: details.ticket_id,
|
|
224
|
+
selection: details.selection,
|
|
225
|
+
summary: details.summary,
|
|
226
|
+
exit_code: details.exit_code,
|
|
227
|
+
duration_ms: details.duration_ms,
|
|
228
|
+
output: details.output,
|
|
229
|
+
output_truncated: details.output_truncated,
|
|
230
|
+
output_retention_state: details.output_retention_state,
|
|
231
|
+
output_expired_at: details.output_expired_at,
|
|
232
|
+
output_byte_count: details.output_byte_count,
|
|
233
|
+
retry_eligibility: retryEligibility,
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const serializeGitHubActionsExecutionDetail = (details) => {
|
|
237
|
+
if (!details) {
|
|
238
|
+
return { kind: 'github_actions', available: false };
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
kind: 'github_actions',
|
|
242
|
+
available: true,
|
|
243
|
+
workflow_id: details.workflow_id,
|
|
244
|
+
workflow_name: details.workflow_name,
|
|
245
|
+
ref: details.ref,
|
|
246
|
+
input_names: details.input_names,
|
|
247
|
+
provider_run_id: details.provider_run_id,
|
|
248
|
+
provider_status: details.provider_status,
|
|
249
|
+
conclusion: details.conclusion,
|
|
250
|
+
html_url: details.html_url,
|
|
251
|
+
head_sha: details.head_sha,
|
|
252
|
+
head_branch: details.head_branch,
|
|
253
|
+
provider_created_at: details.provider_created_at,
|
|
254
|
+
provider_updated_at: details.provider_updated_at,
|
|
255
|
+
provider_started_at: details.provider_started_at,
|
|
256
|
+
dispatched_at: details.dispatched_at,
|
|
257
|
+
last_synced_at: details.last_synced_at,
|
|
258
|
+
jobs: details.jobs,
|
|
259
|
+
jobs_retention_state: details.jobs_retention_state,
|
|
260
|
+
jobs_expired_at: details.jobs_expired_at,
|
|
261
|
+
jobs_count: details.jobs_count,
|
|
262
|
+
};
|
|
263
|
+
};
|
|
137
264
|
const formatWorktreeDeleteError = (target, branch, error) => {
|
|
138
265
|
return `Unable to delete the ${target} worktree for branch "${branch}": ${getErrorMessage(error)}`;
|
|
139
266
|
};
|
|
@@ -183,21 +310,13 @@ const getBundlePlanBlockerError = () => {
|
|
|
183
310
|
return 'AI planning is blocked because this bundle worktree is currently being modified.';
|
|
184
311
|
};
|
|
185
312
|
const deleteTicketRecord = async (ticket) => {
|
|
186
|
-
if (ticket.agent_status === 'running'
|
|
313
|
+
if (ticket.agent_status === 'running'
|
|
314
|
+
|| (ticket.project_id && hasActiveBuildExecutionRunForTicket(ticket.project_id, ticket.id))) {
|
|
187
315
|
return { ok: false, status: 409, error: 'Cannot delete a ticket while the agent is running' };
|
|
188
316
|
}
|
|
189
317
|
if (isRunning(ticket)) {
|
|
190
318
|
await stopTicket(ticket);
|
|
191
319
|
}
|
|
192
|
-
let nextBuildTicketId = null;
|
|
193
|
-
let affectedTicketIds = [];
|
|
194
|
-
if (ticket.state === 'build') {
|
|
195
|
-
const sequenceRemoval = removeTicketFromBuildSequence(ticket.id);
|
|
196
|
-
if (sequenceRemoval.ok) {
|
|
197
|
-
nextBuildTicketId = sequenceRemoval.nextTicketId;
|
|
198
|
-
affectedTicketIds = sequenceRemoval.affectedTicketIds;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
320
|
const branch = resolveTicketBranch(ticket);
|
|
202
321
|
if (!isBundledTicket(ticket) && branch) {
|
|
203
322
|
const project = getEffectiveProject(ticket);
|
|
@@ -216,7 +335,24 @@ const deleteTicketRecord = async (ticket) => {
|
|
|
216
335
|
}
|
|
217
336
|
const bundledProject = isBundledTicket(ticket) ? getEffectiveProject(ticket) : null;
|
|
218
337
|
const deletedBundleId = ticket.worktree_bundle_id;
|
|
338
|
+
let nextBuildTicketId = null;
|
|
339
|
+
let nextBuildSequenceId = null;
|
|
340
|
+
let affectedTicketIds = [];
|
|
341
|
+
if (ticket.state === 'build') {
|
|
342
|
+
const sequenceRemoval = removeTicketFromBuildSequence(ticket.id);
|
|
343
|
+
if (sequenceRemoval.ok) {
|
|
344
|
+
nextBuildTicketId = sequenceRemoval.nextTicketId;
|
|
345
|
+
nextBuildSequenceId = sequenceRemoval.sequenceId;
|
|
346
|
+
affectedTicketIds = sequenceRemoval.affectedTicketIds;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
219
349
|
db.prepare('DELETE FROM tickets WHERE id = ?').run(ticket.id);
|
|
350
|
+
try {
|
|
351
|
+
clearTicketStageSnapshots(ticket.id);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
console.warn(`[ticket-delete] Unable to remove stage snapshots for ticket ${ticket.id}`, error);
|
|
355
|
+
}
|
|
220
356
|
if (bundledProject && deletedBundleId) {
|
|
221
357
|
try {
|
|
222
358
|
cleanupBundleIfEmpty(deletedBundleId, bundledProject);
|
|
@@ -230,7 +366,7 @@ const deleteTicketRecord = async (ticket) => {
|
|
|
230
366
|
}
|
|
231
367
|
}
|
|
232
368
|
io.emit('ticket:deleted', { id: ticket.id });
|
|
233
|
-
return { ok: true, nextBuildTicketId, affectedTicketIds };
|
|
369
|
+
return { ok: true, nextBuildTicketId, nextBuildSequenceId, affectedTicketIds };
|
|
234
370
|
};
|
|
235
371
|
const emitUpdatedTickets = (ticketIds) => {
|
|
236
372
|
for (const ticketId of ticketIds) {
|
|
@@ -240,6 +376,41 @@ const emitUpdatedTickets = (ticketIds) => {
|
|
|
240
376
|
}
|
|
241
377
|
}
|
|
242
378
|
};
|
|
379
|
+
const scheduleBuildAfterTicketDeletion = (result) => {
|
|
380
|
+
if (!result.nextBuildTicketId || !result.nextBuildSequenceId) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const nextTicket = getTicket(result.nextBuildTicketId);
|
|
384
|
+
if (!nextTicket) {
|
|
385
|
+
stopBuildSequence(result.nextBuildTicketId);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const scheduling = startBuild(nextTicket, io, {
|
|
389
|
+
sequenceId: result.nextBuildSequenceId,
|
|
390
|
+
initiation: { initiator: 'automation' },
|
|
391
|
+
accept: () => {
|
|
392
|
+
const transition = prepareTicketForBuild(nextTicket.id);
|
|
393
|
+
if (!transition.ok) {
|
|
394
|
+
throw new Error(transition.error);
|
|
395
|
+
}
|
|
396
|
+
activateQueuedBuildSequenceTicket(result.nextBuildSequenceId, transition.ticket.id);
|
|
397
|
+
return {
|
|
398
|
+
ticket: transition.ticket,
|
|
399
|
+
sequenceId: result.nextBuildSequenceId,
|
|
400
|
+
compensate: () => {
|
|
401
|
+
db.prepare(`
|
|
402
|
+
UPDATE build_sequence_tickets SET status = 'queued', started_at = NULL
|
|
403
|
+
WHERE build_sequence_id = ? AND ticket_id = ? AND status = 'active'
|
|
404
|
+
`).run(result.nextBuildSequenceId, transition.ticket.id);
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
if (!scheduling.accepted) {
|
|
410
|
+
const stopped = stopBuildSequence(nextTicket.id);
|
|
411
|
+
emitUpdatedTickets(stopped.affectedTicketIds);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
243
414
|
const CORS_ORIGINS = parseCorsOrigins(process.env.CORS_ORIGIN);
|
|
244
415
|
const CORS_ORIGIN_RESOLVER = createCorsOriginResolver(CORS_ORIGINS);
|
|
245
416
|
const app = express();
|
|
@@ -523,16 +694,15 @@ const asProjectPayload = (body) => {
|
|
|
523
694
|
const isWorktreeReady = (branch, project) => {
|
|
524
695
|
return existsSync(resolveWorktreePath(branch, project));
|
|
525
696
|
};
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
stopAllActiveBuildSequences();
|
|
697
|
+
try {
|
|
698
|
+
const reconciliation = reconcileStageSnapshots();
|
|
699
|
+
if (reconciliation.removedRecords > 0 || reconciliation.removedDirectories > 0) {
|
|
700
|
+
console.info('[stage-snapshots] Reconciled stale undo data', reconciliation);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
console.warn('[stage-snapshots] Unable to reconcile stale undo data', error);
|
|
705
|
+
}
|
|
536
706
|
const getRequestedProjectId = (req) => {
|
|
537
707
|
if (typeof req.query.project_id === 'string' && req.query.project_id !== '') {
|
|
538
708
|
return req.query.project_id;
|
|
@@ -586,6 +756,9 @@ const resolveTicketDiffWorkspace = (ticket) => {
|
|
|
586
756
|
return { error: 'Ticket project no longer exists', status: 400 };
|
|
587
757
|
}
|
|
588
758
|
const bundle = ticket.worktree_bundle_id ? getBundle(ticket.worktree_bundle_id, project.id) : null;
|
|
759
|
+
if (ticket.worktree_bundle_id && !bundle) {
|
|
760
|
+
return { error: 'Selected bundle target no longer exists', status: 409 };
|
|
761
|
+
}
|
|
589
762
|
if (bundle && isProjectRootBundle(bundle)) {
|
|
590
763
|
const branchState = getProjectBranch(project, { includeDiffStats: false });
|
|
591
764
|
if (branchState.status !== 'ready' || !branchState.branch) {
|
|
@@ -598,7 +771,7 @@ const resolveTicketDiffWorkspace = (ticket) => {
|
|
|
598
771
|
kind: 'project_root',
|
|
599
772
|
};
|
|
600
773
|
}
|
|
601
|
-
const branch = resolveTicketBranch(ticket);
|
|
774
|
+
const branch = bundle?.branch ?? resolveTicketBranch(ticket);
|
|
602
775
|
if (!branch || !isWorktreeReady(branch, project)) {
|
|
603
776
|
return { error: 'Worktree not ready yet', status: 400 };
|
|
604
777
|
}
|
|
@@ -620,6 +793,9 @@ const resolveTicketDiffWorkspaceFromContext = (ticket) => {
|
|
|
620
793
|
if (!project) {
|
|
621
794
|
return { error: 'Ticket project no longer exists', status: 400 };
|
|
622
795
|
}
|
|
796
|
+
if (ticket.worktree_bundle_id && !ticket.bundle_kind) {
|
|
797
|
+
return { error: 'Selected bundle target no longer exists', status: 409 };
|
|
798
|
+
}
|
|
623
799
|
if (ticket.bundle_kind === 'project_root') {
|
|
624
800
|
const branchState = getProjectBranch(project, { includeDiffStats: false });
|
|
625
801
|
if (branchState.status !== 'ready' || !branchState.branch) {
|
|
@@ -632,7 +808,7 @@ const resolveTicketDiffWorkspaceFromContext = (ticket) => {
|
|
|
632
808
|
kind: 'project_root',
|
|
633
809
|
};
|
|
634
810
|
}
|
|
635
|
-
const branch =
|
|
811
|
+
const branch = ticket.worktree_bundle_id ? ticket.bundle_branch : ticket.branch;
|
|
636
812
|
if (!branch || !isWorktreeReady(branch, project)) {
|
|
637
813
|
return { error: 'Worktree not ready yet', status: 400 };
|
|
638
814
|
}
|
|
@@ -1421,25 +1597,15 @@ const submitBundleFollowUpResponse = (projectId, bundleId, payload, broadcaster)
|
|
|
1421
1597
|
if (!targetTicket) {
|
|
1422
1598
|
return { ok: false, status: 409, error: 'No non-plan tickets are available for follow-up in this bundle' };
|
|
1423
1599
|
}
|
|
1424
|
-
const
|
|
1425
|
-
if (!effectiveProject) {
|
|
1426
|
-
return { ok: false, status: 400, error: 'Ticket project no longer exists' };
|
|
1427
|
-
}
|
|
1428
|
-
const branch = resolveTicketBranch(targetTicket);
|
|
1429
|
-
if (!branch || targetTicket.has_worktree === false || !isWorktreeReady(branch, effectiveProject)) {
|
|
1430
|
-
return { ok: false, status: 400, error: 'Worktree not ready yet' };
|
|
1431
|
-
}
|
|
1432
|
-
const started = startFollowUp(targetTicket, broadcaster, instruction, mode);
|
|
1433
|
-
if (!started) {
|
|
1434
|
-
return { ok: false, status: 409, error: 'Ticket is already running' };
|
|
1435
|
-
}
|
|
1436
|
-
createTicketMessage({
|
|
1600
|
+
const submitted = submitTicketFollowUp({
|
|
1437
1601
|
ticketId: targetTicket.id,
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
origin: payload?.origin === 'changes' ? 'changes' : null,
|
|
1602
|
+
requestedProjectId: project.id,
|
|
1603
|
+
payload: { ...payload, instruction, mode },
|
|
1604
|
+
broadcaster: broadcaster,
|
|
1442
1605
|
});
|
|
1606
|
+
if (!submitted.ok) {
|
|
1607
|
+
return submitted;
|
|
1608
|
+
}
|
|
1443
1609
|
return {
|
|
1444
1610
|
ok: true,
|
|
1445
1611
|
status: 200,
|
|
@@ -1462,8 +1628,11 @@ const rerunTicketReview = (ticketId) => {
|
|
|
1462
1628
|
if (!ticket.can_re_review) {
|
|
1463
1629
|
return { ok: false, status: 409, error: 'Review can only be re-run after a follow-up finishes' };
|
|
1464
1630
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1631
|
+
const scheduling = rerunTicketReviewOperation(ticket, io);
|
|
1632
|
+
if (!scheduling.ok) {
|
|
1633
|
+
return { ok: false, status: scheduling.status, error: scheduling.error };
|
|
1634
|
+
}
|
|
1635
|
+
return { ok: true, ticket: scheduling.value ?? ticket };
|
|
1467
1636
|
};
|
|
1468
1637
|
const reviewBundleTickets = (projectId, bundleId) => {
|
|
1469
1638
|
const project = getProjectById(projectId);
|
|
@@ -1534,19 +1703,81 @@ const buildBundleTickets = (projectId, bundleId, broadcaster) => {
|
|
|
1534
1703
|
}
|
|
1535
1704
|
}
|
|
1536
1705
|
const movedTickets = [];
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1706
|
+
const ticketSnapshots = orderedPlanTickets.map(ticket => db.prepare(`
|
|
1707
|
+
SELECT id, project_id, state, tool, agent_status, agent_log, streaming_response, branch,
|
|
1708
|
+
build_completed_at, run_setup_status, auto_park_dismissed_at, planning_context_json, lane_order
|
|
1709
|
+
FROM tickets
|
|
1710
|
+
WHERE id = ? AND project_id = ?
|
|
1711
|
+
`).get(ticket.id, project.id));
|
|
1712
|
+
const firstPlanTicket = orderedPlanTickets[0];
|
|
1713
|
+
if (!firstPlanTicket) {
|
|
1714
|
+
return { ok: false, status: 409, error: 'No plan tickets are available to build in this bundle' };
|
|
1715
|
+
}
|
|
1716
|
+
const acceptBundleBuild = () => {
|
|
1717
|
+
let activeSequenceId = null;
|
|
1718
|
+
for (const [index, planTicket] of orderedPlanTickets.entries()) {
|
|
1719
|
+
const buildTransition = prepareTicketForBuild(planTicket.id, {
|
|
1720
|
+
ignoredBlockerTicketIds: bundleTicketIds,
|
|
1721
|
+
});
|
|
1722
|
+
if (!buildTransition.ok) {
|
|
1723
|
+
throw new Error(buildTransition.error);
|
|
1724
|
+
}
|
|
1725
|
+
movedTickets.push(buildTransition.ticket);
|
|
1726
|
+
const queuedBuild = enqueueBundledBuild({
|
|
1727
|
+
ticketId: buildTransition.ticket.id,
|
|
1728
|
+
projectId: project.id,
|
|
1729
|
+
worktreeBundleId: bundle.id,
|
|
1730
|
+
activate: index === 0,
|
|
1731
|
+
});
|
|
1732
|
+
if (index === 0) {
|
|
1733
|
+
activeSequenceId = queuedBuild.sequenceId;
|
|
1734
|
+
}
|
|
1548
1735
|
}
|
|
1549
|
-
|
|
1736
|
+
return {
|
|
1737
|
+
ticket: movedTickets[0],
|
|
1738
|
+
sequenceId: activeSequenceId,
|
|
1739
|
+
...(activeSequenceId
|
|
1740
|
+
? {
|
|
1741
|
+
compensate: () => {
|
|
1742
|
+
compensateBuildSequenceAcceptance(activeSequenceId, movedTickets.map(ticket => ticket.id));
|
|
1743
|
+
for (const snapshot of ticketSnapshots) {
|
|
1744
|
+
db.prepare(`
|
|
1745
|
+
UPDATE tickets
|
|
1746
|
+
SET state = ?, tool = ?, agent_status = ?, agent_log = ?, streaming_response = ?, branch = ?,
|
|
1747
|
+
build_completed_at = ?, run_setup_status = ?, auto_park_dismissed_at = ?,
|
|
1748
|
+
planning_context_json = ?, lane_order = ?, updated_at = CURRENT_TIMESTAMP
|
|
1749
|
+
WHERE id = ? AND project_id = ?
|
|
1750
|
+
`).run(snapshot.state, snapshot.tool, snapshot.agent_status, snapshot.agent_log, snapshot.streaming_response, snapshot.branch, snapshot.build_completed_at, snapshot.run_setup_status, snapshot.auto_park_dismissed_at, snapshot.planning_context_json, snapshot.lane_order, snapshot.id, snapshot.project_id);
|
|
1751
|
+
}
|
|
1752
|
+
},
|
|
1753
|
+
}
|
|
1754
|
+
: {}),
|
|
1755
|
+
};
|
|
1756
|
+
};
|
|
1757
|
+
if (hasActiveSequenceForBundle(project.id, bundle.id)) {
|
|
1758
|
+
try {
|
|
1759
|
+
db.transaction(acceptBundleBuild)();
|
|
1760
|
+
}
|
|
1761
|
+
catch (error) {
|
|
1762
|
+
return { ok: false, status: 409, error: error instanceof Error ? error.message : String(error) };
|
|
1763
|
+
}
|
|
1764
|
+
emitUpdatedTickets(movedTickets.map(ticket => ticket.id));
|
|
1765
|
+
return {
|
|
1766
|
+
ok: true,
|
|
1767
|
+
status: 200,
|
|
1768
|
+
value: serializeBundleDetails(project, bundle, { target_ticket_id: movedTickets[0]?.id ?? null }),
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const scheduling = startBuild(firstPlanTicket, broadcaster, { accept: acceptBundleBuild });
|
|
1772
|
+
if (scheduling === undefined) {
|
|
1773
|
+
acceptBundleBuild();
|
|
1774
|
+
}
|
|
1775
|
+
else if (!scheduling.accepted) {
|
|
1776
|
+
return {
|
|
1777
|
+
ok: false,
|
|
1778
|
+
status: scheduling.reason === 'queue_failed' ? 503 : 409,
|
|
1779
|
+
error: scheduling.error,
|
|
1780
|
+
};
|
|
1550
1781
|
}
|
|
1551
1782
|
emitUpdatedTickets(movedTickets.map(ticket => ticket.id));
|
|
1552
1783
|
const details = serializeBundleDetails(project, bundle, {
|
|
@@ -1556,22 +1787,6 @@ const buildBundleTickets = (projectId, bundleId, broadcaster) => {
|
|
|
1556
1787
|
ok: true,
|
|
1557
1788
|
status: 200,
|
|
1558
1789
|
value: details,
|
|
1559
|
-
afterResponse: () => {
|
|
1560
|
-
for (const [index, movedTicket] of movedTickets.entries()) {
|
|
1561
|
-
if (!movedTicket.project_id || !movedTicket.worktree_bundle_id) {
|
|
1562
|
-
continue;
|
|
1563
|
-
}
|
|
1564
|
-
const queuedBuild = enqueueBundledBuild({
|
|
1565
|
-
ticketId: movedTicket.id,
|
|
1566
|
-
projectId: movedTicket.project_id,
|
|
1567
|
-
worktreeBundleId: movedTicket.worktree_bundle_id,
|
|
1568
|
-
activate: index === 0 && shouldStartBundleBuildNow(movedTicket),
|
|
1569
|
-
});
|
|
1570
|
-
if (queuedBuild.shouldStartNow) {
|
|
1571
|
-
startBuild(movedTicket, broadcaster);
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
},
|
|
1575
1790
|
};
|
|
1576
1791
|
};
|
|
1577
1792
|
const getCurrentReviewWorkspaceSignature = (ticket) => {
|
|
@@ -1912,14 +2127,14 @@ const validateExpandedPlanningAgentDraft = (outline, draft) => {
|
|
|
1912
2127
|
}
|
|
1913
2128
|
return draft;
|
|
1914
2129
|
};
|
|
1915
|
-
const createPlanningAgentTickets = db.transaction(({ draft, initiativePrompt, projectId, model, variant, skills,
|
|
2130
|
+
const createPlanningAgentTickets = db.transaction(({ draft, initiativePrompt, projectId, model, variant, skills, worktreeBundleId, }) => {
|
|
1916
2131
|
const { createdTickets, createdDependencies, ticketIdsByKey } = materializeTicketsWithDependencies({
|
|
1917
2132
|
options: {
|
|
1918
2133
|
projectId,
|
|
1919
2134
|
model,
|
|
1920
2135
|
variant,
|
|
1921
2136
|
skills,
|
|
1922
|
-
tool:
|
|
2137
|
+
tool: null,
|
|
1923
2138
|
worktreeBundleId,
|
|
1924
2139
|
agentStatus: 'done',
|
|
1925
2140
|
},
|
|
@@ -2761,6 +2976,7 @@ app.post('/projects/:id/spec-sessions/:sessionId/snapshots/:snapshotId/review/st
|
|
|
2761
2976
|
projectId: req.params.id,
|
|
2762
2977
|
specSessionId: req.params.sessionId,
|
|
2763
2978
|
snapshotId: req.params.snapshotId,
|
|
2979
|
+
broadcaster: io,
|
|
2764
2980
|
});
|
|
2765
2981
|
if (!result.ok) {
|
|
2766
2982
|
return res.status(result.status).json({ error: result.error });
|
|
@@ -3470,7 +3686,6 @@ app.post('/bundles/:id/build', (req, res) => {
|
|
|
3470
3686
|
});
|
|
3471
3687
|
}
|
|
3472
3688
|
res.status(buildResult.status).json(buildResult.value);
|
|
3473
|
-
buildResult.afterResponse?.();
|
|
3474
3689
|
});
|
|
3475
3690
|
app.post('/bundles/:id/review', (req, res) => {
|
|
3476
3691
|
const result = getRequiredProject(req);
|
|
@@ -3512,33 +3727,51 @@ app.post('/bundles/:id/build-open-questions/resolve', (req, res) => {
|
|
|
3512
3727
|
if (resolutions.some(resolution => resolution === null || !resolution.questionId)) {
|
|
3513
3728
|
return res.status(400).json({ error: 'Each resolution requires a question_id' });
|
|
3514
3729
|
}
|
|
3515
|
-
const
|
|
3516
|
-
if (buildTickets.length === 0
|
|
3517
|
-
|| buildTickets.some(ticket => ticket.agent_status !== 'done' && ticket.agent_status !== 'stopped')) {
|
|
3518
|
-
return res.status(409).json({ error: 'All build tickets in the bundle must be done or stopped before resolving questions' });
|
|
3519
|
-
}
|
|
3520
|
-
const created = createBuildOpenQuestionResolutionBatch({
|
|
3730
|
+
const accepted = acceptBuildOpenQuestionResolution({
|
|
3521
3731
|
projectId: result.project.id,
|
|
3522
3732
|
bundleId: bundle.id,
|
|
3523
3733
|
resolutions: resolutions.filter((resolution) => resolution !== null),
|
|
3734
|
+
broadcaster: io,
|
|
3524
3735
|
});
|
|
3525
|
-
if (!
|
|
3526
|
-
return res.status(
|
|
3527
|
-
error:
|
|
3528
|
-
...(
|
|
3736
|
+
if (!accepted.ok) {
|
|
3737
|
+
return res.status(accepted.status).json({
|
|
3738
|
+
error: accepted.error,
|
|
3739
|
+
...(accepted.code ? { code: accepted.code } : {}),
|
|
3529
3740
|
});
|
|
3530
3741
|
}
|
|
3531
|
-
|
|
3532
|
-
if (answeredQuestions.length === 0) {
|
|
3533
|
-
clearAutomationPause(result.project.id, bundle.id);
|
|
3742
|
+
if (accepted.kind === 'dismissed') {
|
|
3534
3743
|
const automationConfig = resolveAutomationConfig(bundle, result.project);
|
|
3535
3744
|
const reviewResult = depthAtLeast(automationConfig.depth, 'auto_review')
|
|
3536
|
-
? transitionBundleToReview(result.project.id, bundle.id, io, {
|
|
3745
|
+
? transitionBundleToReview(result.project.id, bundle.id, io, {
|
|
3746
|
+
automatic: true,
|
|
3747
|
+
initiation: {
|
|
3748
|
+
causalParentRunId: accepted.progressionRunId,
|
|
3749
|
+
workflowContext: validateWorkflowContext({ build_open_question_batch_id: accepted.batchId }),
|
|
3750
|
+
},
|
|
3751
|
+
})
|
|
3537
3752
|
: null;
|
|
3753
|
+
if (!reviewResult) {
|
|
3754
|
+
completeAutomationProgression({
|
|
3755
|
+
projectId: result.project.id,
|
|
3756
|
+
bundleId: bundle.id,
|
|
3757
|
+
progressionRunId: accepted.progressionRunId,
|
|
3758
|
+
broadcaster: io,
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3538
3761
|
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
3539
3762
|
io.emit('ticket:updated', ticket);
|
|
3540
3763
|
}
|
|
3541
3764
|
if (reviewResult && !reviewResult.ok) {
|
|
3765
|
+
waitAutomationProgression({
|
|
3766
|
+
projectId: result.project.id,
|
|
3767
|
+
bundleId: bundle.id,
|
|
3768
|
+
progressionRunId: accepted.progressionRunId,
|
|
3769
|
+
pauseCode: 'review',
|
|
3770
|
+
pauseReason: reviewResult.error,
|
|
3771
|
+
reasonCode: 'stage_failed',
|
|
3772
|
+
pausedStageRunId: null,
|
|
3773
|
+
broadcaster: io,
|
|
3774
|
+
});
|
|
3542
3775
|
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
3543
3776
|
}
|
|
3544
3777
|
return res.json(serializeBundleDetails(result.project, bundle, {
|
|
@@ -3546,35 +3779,11 @@ app.post('/bundles/:id/build-open-questions/resolve', (req, res) => {
|
|
|
3546
3779
|
target_ticket_id: reviewResult?.value?.id,
|
|
3547
3780
|
}));
|
|
3548
3781
|
}
|
|
3549
|
-
const targetTicketSource = resolveBundleTargetTicket(buildTickets);
|
|
3550
|
-
const targetTicket = targetTicketSource ? getTicket(targetTicketSource.id) : null;
|
|
3551
|
-
if (!targetTicket) {
|
|
3552
|
-
failBuildOpenQuestionBatch(created.batch.id, 'No completed Build ticket is available for the follow-up');
|
|
3553
|
-
recordAutomationPause(result.project.id, bundle.id, 'open_questions', 'Build open question follow-up could not be started');
|
|
3554
|
-
return res.status(409).json({ error: 'No completed Build ticket is available for the follow-up' });
|
|
3555
|
-
}
|
|
3556
|
-
const instruction = [
|
|
3557
|
-
'Apply this complete batch of answered Build requirements in one bundle-scoped pass. Preserve the grouping by originating ticket and do not work on dismissed questions. Treat every submitted decision as authoritative and do not return an answered or dismissed batch question as open.',
|
|
3558
|
-
formatBuildOpenQuestionAnswersPrompt(answeredQuestions),
|
|
3559
|
-
].join('\n\n');
|
|
3560
|
-
setBuildOpenQuestionBatchStatus(created.batch.id, 'running');
|
|
3561
|
-
clearAutomationPause(result.project.id, bundle.id);
|
|
3562
|
-
const started = startFollowUp(targetTicket, io, instruction, 'build', {
|
|
3563
|
-
buildOpenQuestionBatchId: created.batch.id,
|
|
3564
|
-
});
|
|
3565
|
-
if (!started) {
|
|
3566
|
-
failBuildOpenQuestionBatch(created.batch.id, 'Build follow-up could not be started');
|
|
3567
|
-
recordAutomationPause(result.project.id, bundle.id, 'open_questions', 'Build open question follow-up could not be started');
|
|
3568
|
-
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
3569
|
-
io.emit('ticket:updated', ticket);
|
|
3570
|
-
}
|
|
3571
|
-
return res.status(409).json({ error: 'Build follow-up could not be started' });
|
|
3572
|
-
}
|
|
3573
3782
|
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
3574
3783
|
io.emit('ticket:updated', ticket);
|
|
3575
3784
|
}
|
|
3576
|
-
return res.status(
|
|
3577
|
-
target_ticket_id: targetTicket.id,
|
|
3785
|
+
return res.status(accepted.status).json(serializeBundleDetails(result.project, bundle, {
|
|
3786
|
+
target_ticket_id: accepted.targetTicket.id,
|
|
3578
3787
|
}));
|
|
3579
3788
|
});
|
|
3580
3789
|
app.post('/bundles/:id/undo-review', (req, res) => {
|
|
@@ -3652,6 +3861,12 @@ app.post('/bundles/:id/stop', async (req, res) => {
|
|
|
3652
3861
|
if (!targetTicket) {
|
|
3653
3862
|
return res.status(404).json({ error: 'Bundle tickets not found' });
|
|
3654
3863
|
}
|
|
3864
|
+
await stopAutomationProgression({
|
|
3865
|
+
projectId: result.project.id,
|
|
3866
|
+
bundleId: bundle.id,
|
|
3867
|
+
broadcaster: io,
|
|
3868
|
+
stopLocalWorker: worker => (worker.kind === 'review' ? stopReviewAgent(worker.ticket_id) : stopBuildAgent(worker.ticket_id)),
|
|
3869
|
+
});
|
|
3655
3870
|
await stopTicket(targetTicket);
|
|
3656
3871
|
emitRunStateForContext(targetTicket, false);
|
|
3657
3872
|
return res.json(serializeBundleDetails(result.project, bundle));
|
|
@@ -3667,6 +3882,27 @@ app.post('/bundles/:id/follow-up', (req, res) => {
|
|
|
3667
3882
|
}
|
|
3668
3883
|
return res.status(response.status).json(response.value);
|
|
3669
3884
|
});
|
|
3885
|
+
const AUTOMATION_SIDE_EFFECT_KINDS = new Set(['automation_commit', 'automation_push', 'automation_pull_request']);
|
|
3886
|
+
const AUTOMATION_WORKER_PAUSE_KINDS = new Set(['build', 'review', 'build_follow_up', 'automation_review_fix']);
|
|
3887
|
+
const pausedRunBelongsToProgression = (projectId, bundleId, progressionRunId, pausedRunId) => {
|
|
3888
|
+
const pausedRun = getExecutionRun(projectId, pausedRunId);
|
|
3889
|
+
const progression = getExecutionRun(projectId, progressionRunId);
|
|
3890
|
+
if (!pausedRun || !progression || progression.kind !== 'automation_progression') {
|
|
3891
|
+
return false;
|
|
3892
|
+
}
|
|
3893
|
+
const ownsBundle = listExecutionRunLinks(projectId, pausedRun.id).some(link => (link.entity_type === 'bundle'
|
|
3894
|
+
&& link.entity_id === bundleId
|
|
3895
|
+
&& (link.relationship === 'primary' || link.relationship === 'context')));
|
|
3896
|
+
if (!ownsBundle) {
|
|
3897
|
+
return false;
|
|
3898
|
+
}
|
|
3899
|
+
if (pausedRun.parent_run_id === progressionRunId) {
|
|
3900
|
+
return true;
|
|
3901
|
+
}
|
|
3902
|
+
const parent = pausedRun.parent_run_id ? getExecutionRun(projectId, pausedRun.parent_run_id) : null;
|
|
3903
|
+
return parent?.kind === 'automation_review_fix' && parent.parent_run_id === progressionRunId
|
|
3904
|
+
|| progression.parent_run_id === pausedRun.id;
|
|
3905
|
+
};
|
|
3670
3906
|
app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
3671
3907
|
const result = getRequiredProject(req);
|
|
3672
3908
|
if ('error' in result) {
|
|
@@ -3689,11 +3925,124 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3689
3925
|
&& listBuildOpenQuestionsForBundle(result.project.id, bundle.id).length > 0) {
|
|
3690
3926
|
return res.status(409).json({ error: 'Resolve or dismiss all Build open questions before resuming automation' });
|
|
3691
3927
|
}
|
|
3928
|
+
let progressionRunId = bundleState.automation_progression_run_id;
|
|
3929
|
+
if (!progressionRunId) {
|
|
3930
|
+
try {
|
|
3931
|
+
progressionRunId = (bundleState.automation_paused_stage_run_id
|
|
3932
|
+
? recoverStoppedSideEffectProgression({
|
|
3933
|
+
projectId: result.project.id,
|
|
3934
|
+
bundleId: bundle.id,
|
|
3935
|
+
broadcaster: io,
|
|
3936
|
+
})
|
|
3937
|
+
: recoverLegacyAutomationProgression({
|
|
3938
|
+
projectId: result.project.id,
|
|
3939
|
+
bundleId: bundle.id,
|
|
3940
|
+
broadcaster: io,
|
|
3941
|
+
})).runId;
|
|
3942
|
+
}
|
|
3943
|
+
catch (error) {
|
|
3944
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
try {
|
|
3948
|
+
if (getAutomationProgression(result.project.id, bundle.id)?.id !== progressionRunId) {
|
|
3949
|
+
return res.status(409).json({ error: 'Automation progression ownership is invalid' });
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
catch (error) {
|
|
3953
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
3954
|
+
}
|
|
3955
|
+
let sideEffectResume = null;
|
|
3956
|
+
let pausedRunKind = null;
|
|
3957
|
+
if (bundleState.automation_paused_stage_run_id) {
|
|
3958
|
+
const stageRunId = bundleState.automation_paused_stage_run_id;
|
|
3959
|
+
const stage = getExecutionRun(result.project.id, stageRunId);
|
|
3960
|
+
pausedRunKind = stage?.kind ?? null;
|
|
3961
|
+
if (!stage
|
|
3962
|
+
|| (!AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind) && !AUTOMATION_WORKER_PAUSE_KINDS.has(stage.kind))
|
|
3963
|
+
|| !pausedRunBelongsToProgression(result.project.id, bundle.id, progressionRunId, stageRunId)) {
|
|
3964
|
+
return res.status(409).json({ error: 'The paused automation stage does not belong to the active progression' });
|
|
3965
|
+
}
|
|
3966
|
+
if (AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind)) {
|
|
3967
|
+
let reconciled;
|
|
3968
|
+
try {
|
|
3969
|
+
reconciled = await reconcileAutomationStage({
|
|
3970
|
+
projectId: result.project.id,
|
|
3971
|
+
bundleId: bundle.id,
|
|
3972
|
+
progressionRunId,
|
|
3973
|
+
stageRunId,
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3976
|
+
catch (error) {
|
|
3977
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
3978
|
+
}
|
|
3979
|
+
if (reconciled.domainStatus === 'ambiguous') {
|
|
3980
|
+
return res.status(409).json({ error: 'The paused automation side effect is ambiguous and cannot be resumed safely.' });
|
|
3981
|
+
}
|
|
3982
|
+
const details = getAutomationStageDetails(result.project.id, stage.id);
|
|
3983
|
+
if (!details || details.stage_kind !== stage.kind) {
|
|
3984
|
+
return res.status(409).json({ error: 'The paused automation side effect has no complete persisted identity.' });
|
|
3985
|
+
}
|
|
3986
|
+
if (reconciled.status === 'completed' && reconciled.domainStatus === 'reconciled_completed') {
|
|
3987
|
+
if (stage.kind === 'automation_commit') {
|
|
3988
|
+
const commitResult = details.result;
|
|
3989
|
+
const expectedHeadSha = commitResult && ('commitSha' in commitResult ? commitResult.commitSha : commitResult.headSha);
|
|
3990
|
+
if (!expectedHeadSha) {
|
|
3991
|
+
return res.status(409).json({ error: 'The reconciled commit has no persisted HEAD identity.' });
|
|
3992
|
+
}
|
|
3993
|
+
sideEffectResume = { stageKind: stage.kind, expectedHeadSha };
|
|
3994
|
+
}
|
|
3995
|
+
else if (stage.kind === 'automation_push') {
|
|
3996
|
+
const pushResult = details.result;
|
|
3997
|
+
if (!pushResult?.remoteSha) {
|
|
3998
|
+
return res.status(409).json({ error: 'The reconciled push has no persisted remote identity.' });
|
|
3999
|
+
}
|
|
4000
|
+
sideEffectResume = { stageKind: stage.kind, expectedHeadSha: pushResult.remoteSha };
|
|
4001
|
+
}
|
|
4002
|
+
else {
|
|
4003
|
+
sideEffectResume = { stageKind: stage.kind };
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
else if (reconciled.status === 'interrupted' && reconciled.domainStatus === 'reconciled_absent') {
|
|
4007
|
+
if (stage.kind === 'automation_push') {
|
|
4008
|
+
const pushIntent = details.intent;
|
|
4009
|
+
sideEffectResume = { stageKind: stage.kind, retryOfRunId: stage.id, expectedHeadSha: pushIntent.targetLocalSha };
|
|
4010
|
+
}
|
|
4011
|
+
else if (stage.kind === 'automation_pull_request') {
|
|
4012
|
+
const pullRequestIntent = details.intent;
|
|
4013
|
+
sideEffectResume = { stageKind: stage.kind, retryOfRunId: stage.id, expectedHeadSha: pullRequestIntent.headSha };
|
|
4014
|
+
}
|
|
4015
|
+
else {
|
|
4016
|
+
sideEffectResume = { stageKind: stage.kind, retryOfRunId: stage.id };
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
else {
|
|
4020
|
+
return res.status(409).json({ error: 'The paused automation side effect has no safely resumable outcome.' });
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
if (liveAutomationConfig.depth === 'manual') {
|
|
4025
|
+
completeAutomationProgression({
|
|
4026
|
+
projectId: result.project.id,
|
|
4027
|
+
bundleId: bundle.id,
|
|
4028
|
+
progressionRunId,
|
|
4029
|
+
domainStatus: 'manual_boundary_reached',
|
|
4030
|
+
broadcaster: io,
|
|
4031
|
+
});
|
|
4032
|
+
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
4033
|
+
io.emit('ticket:updated', ticket);
|
|
4034
|
+
}
|
|
4035
|
+
return res.json(serializeBundleDetails(result.project, bundle));
|
|
4036
|
+
}
|
|
3692
4037
|
const reviewOwnerSource = bundleState.automation_paused_code === 'open_questions'
|
|
3693
4038
|
? listBundleSliceTickets(result.project.id, bundle.id, 'build')
|
|
3694
4039
|
.find(ticket => ticket.agent_status === 'done') ?? null
|
|
3695
4040
|
: resolveBundleReviewOwner(result.project.id, bundle.id)
|
|
3696
4041
|
?? listBundleSliceTickets(result.project.id, bundle.id, 'review')[0]
|
|
4042
|
+
?? (bundleState.automation_paused_code === 'review' && (pausedRunKind === 'build' || pausedRunKind === 'build_follow_up')
|
|
4043
|
+
? listBundleSliceTickets(result.project.id, bundle.id, 'build')
|
|
4044
|
+
.find(ticket => ticket.agent_status === 'done') ?? null
|
|
4045
|
+
: null)
|
|
3697
4046
|
?? null;
|
|
3698
4047
|
if (!reviewOwnerSource) {
|
|
3699
4048
|
return res.status(409).json({ error: 'No review ticket to resume from' });
|
|
@@ -3707,6 +4056,49 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3707
4056
|
return res.status(400).json({ error: 'Ticket project no longer exists' });
|
|
3708
4057
|
}
|
|
3709
4058
|
const automationConfig = resolveAutomationConfig(owner.bundle ?? null, project);
|
|
4059
|
+
const restoreWaitingProgression = () => {
|
|
4060
|
+
waitAutomationProgression({
|
|
4061
|
+
projectId: result.project.id,
|
|
4062
|
+
bundleId: bundle.id,
|
|
4063
|
+
progressionRunId,
|
|
4064
|
+
pauseCode: bundleState.automation_paused_code ?? 'review',
|
|
4065
|
+
pauseReason: bundleState.automation_paused_reason ?? 'Automation paused',
|
|
4066
|
+
reasonCode: bundleState.automation_pause_reason_code ?? 'manual_transition',
|
|
4067
|
+
pausedStageRunId: bundleState.automation_paused_stage_run_id,
|
|
4068
|
+
broadcaster: io,
|
|
4069
|
+
});
|
|
4070
|
+
};
|
|
4071
|
+
const acceptResumedReview = () => {
|
|
4072
|
+
runAutomationProgression({
|
|
4073
|
+
projectId: result.project.id,
|
|
4074
|
+
bundleId: bundle.id,
|
|
4075
|
+
progressionRunId,
|
|
4076
|
+
broadcaster: io,
|
|
4077
|
+
});
|
|
4078
|
+
return {
|
|
4079
|
+
ticket: owner,
|
|
4080
|
+
compensate: restoreWaitingProgression,
|
|
4081
|
+
};
|
|
4082
|
+
};
|
|
4083
|
+
if (sideEffectResume) {
|
|
4084
|
+
try {
|
|
4085
|
+
runAutomationProgression({
|
|
4086
|
+
projectId: result.project.id,
|
|
4087
|
+
bundleId: bundle.id,
|
|
4088
|
+
progressionRunId,
|
|
4089
|
+
broadcaster: io,
|
|
4090
|
+
});
|
|
4091
|
+
await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io, progressionRunId, sideEffectResume);
|
|
4092
|
+
}
|
|
4093
|
+
catch (error) {
|
|
4094
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4095
|
+
}
|
|
4096
|
+
const resumedOwner = getTicket(owner.id);
|
|
4097
|
+
if (resumedOwner) {
|
|
4098
|
+
io.emit('ticket:updated', resumedOwner);
|
|
4099
|
+
}
|
|
4100
|
+
return res.json(serializeBundleDetails(result.project, bundle));
|
|
4101
|
+
}
|
|
3710
4102
|
if (owner.state === 'review' && owner.agent_status === 'running') {
|
|
3711
4103
|
return res.status(409).json({ error: 'Review already running' });
|
|
3712
4104
|
}
|
|
@@ -3723,24 +4115,131 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3723
4115
|
&& automationConfig.reviewFixEnabled
|
|
3724
4116
|
&& blockingCount > 0
|
|
3725
4117
|
&& iteration < automationConfig.reviewMaxIterations) {
|
|
3726
|
-
|
|
3727
|
-
|
|
4118
|
+
let reviewFixRunId = null;
|
|
4119
|
+
let followUpResult;
|
|
4120
|
+
const previousIteration = bundleState.review_iteration_count ?? 0;
|
|
4121
|
+
const previousFixLoopActive = bundleState.fix_loop_active;
|
|
4122
|
+
try {
|
|
4123
|
+
const progression = getAutomationProgression(result.project.id, bundle.id);
|
|
4124
|
+
if (!progression || progression.id !== progressionRunId) {
|
|
4125
|
+
return res.status(409).json({ error: 'Automation progression ownership is invalid' });
|
|
4126
|
+
}
|
|
4127
|
+
const workflowContext = validateWorkflowContext(progression.metadata);
|
|
4128
|
+
runAutomationProgression({
|
|
4129
|
+
projectId: result.project.id,
|
|
4130
|
+
bundleId: bundle.id,
|
|
4131
|
+
progressionRunId,
|
|
4132
|
+
broadcaster: io,
|
|
4133
|
+
});
|
|
4134
|
+
upsertBundleAutomationState(result.project.id, bundle.id, {
|
|
4135
|
+
review_iteration_count: iteration + 1,
|
|
4136
|
+
fix_loop_active: 1,
|
|
4137
|
+
});
|
|
4138
|
+
const reviewFix = createAutomationReviewFix({
|
|
4139
|
+
projectId: result.project.id,
|
|
4140
|
+
bundleId: bundle.id,
|
|
4141
|
+
ticketId: owner.id,
|
|
4142
|
+
ticketTitle: owner.title,
|
|
4143
|
+
progressionRunId,
|
|
4144
|
+
workflowContext,
|
|
4145
|
+
broadcaster: io,
|
|
4146
|
+
});
|
|
4147
|
+
reviewFixRunId = reviewFix.runId;
|
|
4148
|
+
followUpResult = submitTicketFollowUp({
|
|
4149
|
+
ticketId: owner.id,
|
|
4150
|
+
requestedProjectId: result.project.id,
|
|
4151
|
+
payload: {
|
|
4152
|
+
instruction: `Fix the code review findings at or above "${automationConfig.reviewSeverity}" severity. Address each blocking finding directly and minimally. Do not introduce unrelated changes, reformatting, or refactors. Do not commit.`,
|
|
4153
|
+
mode: 'build',
|
|
4154
|
+
},
|
|
4155
|
+
broadcaster: io,
|
|
4156
|
+
initiation: {
|
|
4157
|
+
initiator: 'automation',
|
|
4158
|
+
causalParentRunId: reviewFix.runId,
|
|
4159
|
+
workflowContext,
|
|
4160
|
+
},
|
|
4161
|
+
});
|
|
4162
|
+
}
|
|
4163
|
+
catch (error) {
|
|
4164
|
+
upsertBundleAutomationState(result.project.id, bundle.id, {
|
|
4165
|
+
review_iteration_count: previousIteration,
|
|
4166
|
+
fix_loop_active: previousFixLoopActive,
|
|
4167
|
+
});
|
|
4168
|
+
try {
|
|
4169
|
+
if (reviewFixRunId) {
|
|
4170
|
+
finishAutomationReviewFix({
|
|
4171
|
+
projectId: result.project.id,
|
|
4172
|
+
reviewFixRunId,
|
|
4173
|
+
failedMessage: error instanceof Error ? error.message : String(error),
|
|
4174
|
+
broadcaster: io,
|
|
4175
|
+
});
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
finally {
|
|
4179
|
+
restoreWaitingProgression();
|
|
4180
|
+
}
|
|
4181
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4182
|
+
}
|
|
4183
|
+
if (!followUpResult.ok) {
|
|
4184
|
+
upsertBundleAutomationState(result.project.id, bundle.id, {
|
|
4185
|
+
review_iteration_count: previousIteration,
|
|
4186
|
+
fix_loop_active: previousFixLoopActive,
|
|
4187
|
+
});
|
|
4188
|
+
if (reviewFixRunId) {
|
|
4189
|
+
finishAutomationReviewFix({
|
|
4190
|
+
projectId: result.project.id,
|
|
4191
|
+
reviewFixRunId,
|
|
4192
|
+
failedMessage: followUpResult.error,
|
|
4193
|
+
broadcaster: io,
|
|
4194
|
+
});
|
|
4195
|
+
waitAutomationProgression({
|
|
4196
|
+
projectId: result.project.id,
|
|
4197
|
+
bundleId: bundle.id,
|
|
4198
|
+
progressionRunId,
|
|
4199
|
+
pauseCode: 'review',
|
|
4200
|
+
pauseReason: `fix follow-up could not start: ${followUpResult.error}`,
|
|
4201
|
+
reasonCode: 'stage_failed',
|
|
4202
|
+
pausedStageRunId: reviewFixRunId,
|
|
4203
|
+
broadcaster: io,
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
return res.status(followUpResult.status).json({ error: followUpResult.error });
|
|
4207
|
+
}
|
|
3728
4208
|
}
|
|
3729
4209
|
else if (depthAtLeast(automationConfig.depth, 'auto_commit')) {
|
|
3730
|
-
|
|
3731
|
-
|
|
4210
|
+
try {
|
|
4211
|
+
runAutomationProgression({
|
|
4212
|
+
projectId: result.project.id,
|
|
4213
|
+
bundleId: bundle.id,
|
|
4214
|
+
progressionRunId,
|
|
4215
|
+
broadcaster: io,
|
|
4216
|
+
});
|
|
4217
|
+
await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io, progressionRunId);
|
|
4218
|
+
}
|
|
4219
|
+
catch (error) {
|
|
4220
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4221
|
+
}
|
|
3732
4222
|
}
|
|
3733
4223
|
else {
|
|
3734
|
-
|
|
3735
|
-
|
|
4224
|
+
const reviewResult = rerunTicketReviewOperation(owner, io, {
|
|
4225
|
+
automatic: true,
|
|
4226
|
+
initiation: { causalParentRunId: progressionRunId },
|
|
4227
|
+
accept: acceptResumedReview,
|
|
4228
|
+
});
|
|
4229
|
+
if (!reviewResult.ok) {
|
|
4230
|
+
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
4231
|
+
}
|
|
3736
4232
|
}
|
|
3737
4233
|
}
|
|
3738
4234
|
else if (owner.state === 'build' && owner.agent_status === 'done') {
|
|
3739
|
-
const reviewResult =
|
|
4235
|
+
const reviewResult = transitionBundleToReview(result.project.id, bundle.id, io, {
|
|
4236
|
+
automatic: true,
|
|
4237
|
+
initiation: { causalParentRunId: progressionRunId },
|
|
4238
|
+
accept: acceptResumedReview,
|
|
4239
|
+
});
|
|
3740
4240
|
if (!reviewResult.ok) {
|
|
3741
4241
|
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
3742
4242
|
}
|
|
3743
|
-
clearAutomationPause(result.project.id, bundle.id);
|
|
3744
4243
|
}
|
|
3745
4244
|
else {
|
|
3746
4245
|
return res.status(409).json({ error: 'Ticket is not in a resumable state' });
|
|
@@ -3751,7 +4250,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3751
4250
|
}
|
|
3752
4251
|
return res.json(serializeBundleDetails(result.project, bundle));
|
|
3753
4252
|
});
|
|
3754
|
-
app.post('/bundles/:id/dismiss-automation-pause', (req, res) => {
|
|
4253
|
+
app.post('/bundles/:id/dismiss-automation-pause', async (req, res) => {
|
|
3755
4254
|
const result = getRequiredProject(req);
|
|
3756
4255
|
if ('error' in result) {
|
|
3757
4256
|
return res.status(400).json({ error: result.error });
|
|
@@ -3772,7 +4271,68 @@ app.post('/bundles/:id/dismiss-automation-pause', (req, res) => {
|
|
|
3772
4271
|
&& listBuildOpenQuestionsForBundle(result.project.id, bundle.id).length > 0) {
|
|
3773
4272
|
return res.status(409).json({ error: 'Resolve or dismiss all Build open questions before dismissing this pause' });
|
|
3774
4273
|
}
|
|
3775
|
-
|
|
4274
|
+
let progressionRunId = bundleState.automation_progression_run_id;
|
|
4275
|
+
if (!progressionRunId) {
|
|
4276
|
+
try {
|
|
4277
|
+
progressionRunId = (bundleState.automation_paused_stage_run_id
|
|
4278
|
+
? recoverStoppedSideEffectProgression({
|
|
4279
|
+
projectId: result.project.id,
|
|
4280
|
+
bundleId: bundle.id,
|
|
4281
|
+
broadcaster: io,
|
|
4282
|
+
})
|
|
4283
|
+
: recoverLegacyAutomationProgression({
|
|
4284
|
+
projectId: result.project.id,
|
|
4285
|
+
bundleId: bundle.id,
|
|
4286
|
+
broadcaster: io,
|
|
4287
|
+
})).runId;
|
|
4288
|
+
}
|
|
4289
|
+
catch (error) {
|
|
4290
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
try {
|
|
4294
|
+
if (getAutomationProgression(result.project.id, bundle.id)?.id !== progressionRunId) {
|
|
4295
|
+
return res.status(409).json({ error: 'Automation progression ownership is invalid' });
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
catch (error) {
|
|
4299
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4300
|
+
}
|
|
4301
|
+
if (bundleState.automation_paused_stage_run_id) {
|
|
4302
|
+
const stage = getExecutionRun(result.project.id, bundleState.automation_paused_stage_run_id);
|
|
4303
|
+
if (!stage
|
|
4304
|
+
|| (!AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind) && !AUTOMATION_WORKER_PAUSE_KINDS.has(stage.kind))
|
|
4305
|
+
|| !pausedRunBelongsToProgression(result.project.id, bundle.id, progressionRunId, stage.id)) {
|
|
4306
|
+
return res.status(409).json({ error: 'The paused automation stage does not belong to the active progression' });
|
|
4307
|
+
}
|
|
4308
|
+
if (AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind)) {
|
|
4309
|
+
let reconciled;
|
|
4310
|
+
try {
|
|
4311
|
+
reconciled = await reconcileAutomationStage({
|
|
4312
|
+
projectId: result.project.id,
|
|
4313
|
+
bundleId: bundle.id,
|
|
4314
|
+
progressionRunId,
|
|
4315
|
+
stageRunId: stage.id,
|
|
4316
|
+
});
|
|
4317
|
+
}
|
|
4318
|
+
catch (error) {
|
|
4319
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4320
|
+
}
|
|
4321
|
+
if (reconciled.domainStatus === 'ambiguous') {
|
|
4322
|
+
return res.status(409).json({ error: 'The paused automation side effect is ambiguous and cannot be dismissed safely.' });
|
|
4323
|
+
}
|
|
4324
|
+
if (!(reconciled.status === 'completed' && reconciled.domainStatus === 'reconciled_completed')
|
|
4325
|
+
&& !(reconciled.status === 'interrupted' && reconciled.domainStatus === 'reconciled_absent')) {
|
|
4326
|
+
return res.status(409).json({ error: 'The paused automation side effect has no safely dismissible outcome.' });
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
await stopAutomationProgression({
|
|
4331
|
+
projectId: result.project.id,
|
|
4332
|
+
bundleId: bundle.id,
|
|
4333
|
+
broadcaster: io,
|
|
4334
|
+
stopLocalWorker: worker => (worker.kind === 'review' ? stopReviewAgent(worker.ticket_id) : stopBuildAgent(worker.ticket_id)),
|
|
4335
|
+
});
|
|
3776
4336
|
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
3777
4337
|
io.emit('ticket:updated', ticket);
|
|
3778
4338
|
}
|
|
@@ -4415,20 +4975,84 @@ app.post('/integrations/github/actions/workflows/:id/dispatch', async (req, res)
|
|
|
4415
4975
|
}
|
|
4416
4976
|
}
|
|
4417
4977
|
console.info(`[integrations] github actions dispatch project=${result.project.id} workflow=${workflowId} ref=${ref}`);
|
|
4978
|
+
const inputNames = Object.keys(inputs).sort();
|
|
4979
|
+
const inputFingerprint = inputNames.length > 0
|
|
4980
|
+
? createHash('sha256').update(JSON.stringify(inputNames.map(name => [name, inputs[name]]))).digest('hex')
|
|
4981
|
+
: null;
|
|
4982
|
+
const dispatchedAt = new Date().toISOString();
|
|
4983
|
+
const workflowName = asTrimmedString(req.body?.workflowName)?.slice(0, 500) || `Workflow ${workflowId}`;
|
|
4984
|
+
const workflowUrl = asTrimmedString(req.body?.workflowUrl)?.slice(0, 2_000) || null;
|
|
4985
|
+
let executionRun;
|
|
4986
|
+
try {
|
|
4987
|
+
executionRun = createExecutionRun({
|
|
4988
|
+
projectId: result.project.id,
|
|
4989
|
+
kind: 'github_actions',
|
|
4990
|
+
domainStatus: 'dispatching',
|
|
4991
|
+
sourceReference: {
|
|
4992
|
+
kind: 'github_workflow',
|
|
4993
|
+
id: String(workflowId),
|
|
4994
|
+
label: workflowName,
|
|
4995
|
+
url: workflowUrl,
|
|
4996
|
+
},
|
|
4997
|
+
eventType: 'github_actions_dispatch_requested',
|
|
4998
|
+
eventMessage: 'GitHub Actions workflow dispatch was requested.',
|
|
4999
|
+
eventMetadata: { workflow_id: workflowId, ref, input_names: inputNames },
|
|
5000
|
+
githubActionsDetails: {
|
|
5001
|
+
workflowId,
|
|
5002
|
+
workflowName,
|
|
5003
|
+
ref,
|
|
5004
|
+
inputNames,
|
|
5005
|
+
inputFingerprint,
|
|
5006
|
+
dispatchedAt,
|
|
5007
|
+
},
|
|
5008
|
+
}, io);
|
|
5009
|
+
}
|
|
5010
|
+
catch (error) {
|
|
5011
|
+
if (error instanceof GitHubActionsDispatchConflictError) {
|
|
5012
|
+
return res.status(409).json({
|
|
5013
|
+
error: error.message,
|
|
5014
|
+
execution_run_id: error.executionRunId,
|
|
5015
|
+
});
|
|
5016
|
+
}
|
|
5017
|
+
throw error;
|
|
5018
|
+
}
|
|
4418
5019
|
try {
|
|
4419
|
-
const dispatchedAt = Date.now();
|
|
4420
5020
|
await dispatchGitHubWorkflow(githubConfig, workflowId, { ref, inputs });
|
|
5021
|
+
transitionExecutionRun({
|
|
5022
|
+
projectId: result.project.id,
|
|
5023
|
+
runId: executionRun.id,
|
|
5024
|
+
status: 'waiting',
|
|
5025
|
+
domainStatus: 'resolving',
|
|
5026
|
+
eventType: 'github_actions_dispatch_accepted',
|
|
5027
|
+
eventMessage: 'GitHub accepted the workflow dispatch request.',
|
|
5028
|
+
eventMetadata: { workflow_id: workflowId, ref },
|
|
5029
|
+
}, io);
|
|
4421
5030
|
const trackedRun = trackGitHubActionsRun({
|
|
4422
5031
|
io,
|
|
5032
|
+
executionRunId: executionRun.id,
|
|
4423
5033
|
projectId: result.project.id,
|
|
4424
|
-
workflowId,
|
|
4425
5034
|
config: githubConfig,
|
|
4426
|
-
dispatchedAt,
|
|
4427
5035
|
});
|
|
4428
|
-
return res.status(202).json({
|
|
5036
|
+
return res.status(202).json({
|
|
5037
|
+
tracking_key: trackedRun.tracking_key,
|
|
5038
|
+
execution_run_id: executionRun.id,
|
|
5039
|
+
dispatched: true,
|
|
5040
|
+
});
|
|
4429
5041
|
}
|
|
4430
5042
|
catch (error) {
|
|
4431
5043
|
console.warn(`[integrations] github actions dispatch failed project=${result.project.id} workflow=${workflowId}`, error);
|
|
5044
|
+
transitionExecutionRun({
|
|
5045
|
+
projectId: result.project.id,
|
|
5046
|
+
runId: executionRun.id,
|
|
5047
|
+
status: 'failed',
|
|
5048
|
+
domainStatus: 'dispatch_failed',
|
|
5049
|
+
failure: {
|
|
5050
|
+
code: error instanceof GitHubApiError ? `github_actions_dispatch_http_${error.status}` : 'github_actions_dispatch_failed',
|
|
5051
|
+
message: error instanceof GitHubApiError ? error.publicMessage : 'Unable to dispatch the workflow right now.',
|
|
5052
|
+
},
|
|
5053
|
+
eventType: 'github_actions_dispatch_failed',
|
|
5054
|
+
eventMessage: error instanceof GitHubApiError ? error.publicMessage : 'Unable to dispatch the workflow right now.',
|
|
5055
|
+
}, io);
|
|
4432
5056
|
if (error instanceof GitHubApiError) {
|
|
4433
5057
|
return res.status(error.status).json({ error: error.publicMessage });
|
|
4434
5058
|
}
|
|
@@ -4636,6 +5260,8 @@ app.post('/planning-agent', async (req, res) => {
|
|
|
4636
5260
|
ensureProjectRootBundle(project.id);
|
|
4637
5261
|
const trimmedDescription = description.trim();
|
|
4638
5262
|
let autoCreatedBundle = null;
|
|
5263
|
+
let planningExecutionRun = null;
|
|
5264
|
+
let materializationCommitted = false;
|
|
4639
5265
|
try {
|
|
4640
5266
|
const resolvedDescription = await resolveExternalPlanningDescription(project.id, trimmedDescription);
|
|
4641
5267
|
const requestedBundleSelection = resolveRequestedBundleSelection(req.body, project.id);
|
|
@@ -4670,13 +5296,38 @@ app.post('/planning-agent', async (req, res) => {
|
|
|
4670
5296
|
});
|
|
4671
5297
|
}
|
|
4672
5298
|
}
|
|
4673
|
-
|
|
4674
|
-
|
|
5299
|
+
planningExecutionRun = createExecutionRun({
|
|
5300
|
+
projectId: project.id,
|
|
5301
|
+
kind: 'planning_agent',
|
|
5302
|
+
domainStatus: 'queued',
|
|
5303
|
+
sourceReference: { kind: 'project', id: project.id, label: project.name },
|
|
5304
|
+
eventType: 'planning_agent_created',
|
|
5305
|
+
}, io);
|
|
5306
|
+
const executionContext = {
|
|
5307
|
+
runId: planningExecutionRun.id,
|
|
5308
|
+
correlationId: planningExecutionRun.correlation_id,
|
|
5309
|
+
projectId: planningExecutionRun.project_id,
|
|
5310
|
+
kind: planningExecutionRun.kind,
|
|
5311
|
+
};
|
|
5312
|
+
const executionPlannerOptions = {
|
|
4675
5313
|
...plannerOptions,
|
|
5314
|
+
executionContext,
|
|
5315
|
+
agentRunParent: { parentType: 'planning', parentId: project.id, io },
|
|
5316
|
+
};
|
|
5317
|
+
transitionExecutionRun({
|
|
5318
|
+
projectId: project.id,
|
|
5319
|
+
runId: planningExecutionRun.id,
|
|
5320
|
+
status: 'running',
|
|
5321
|
+
domainStatus: 'running',
|
|
5322
|
+
eventType: 'planning_agent_started',
|
|
5323
|
+
}, io);
|
|
5324
|
+
const linkedResearch = await prepareLinkedPlanningResearch(resolvedDescription, executionPlannerOptions);
|
|
5325
|
+
const outline = normalizePlanningAgentOutline(await generatePlanningTicketOutline(resolvedDescription, {
|
|
5326
|
+
...executionPlannerOptions,
|
|
4676
5327
|
linkedResearch,
|
|
4677
5328
|
}));
|
|
4678
5329
|
const draft = validateExpandedPlanningAgentDraft(outline, normalizePlanningAgentDraft(await expandPlanningTicketOutline(resolvedDescription, outline, {
|
|
4679
|
-
...
|
|
5330
|
+
...executionPlannerOptions,
|
|
4680
5331
|
linkedResearch,
|
|
4681
5332
|
})));
|
|
4682
5333
|
const created = createPlanningAgentTickets({
|
|
@@ -4686,18 +5337,57 @@ app.post('/planning-agent', async (req, res) => {
|
|
|
4686
5337
|
model: tool === 'opencode' && typeof model === 'string' ? normalizeModelId(model) || null : null,
|
|
4687
5338
|
variant: tool === 'opencode' && typeof variant === 'string' && variant.trim() ? variant.trim() : null,
|
|
4688
5339
|
skills: resolveRequestedSkills(req.body?.skills),
|
|
4689
|
-
tool,
|
|
4690
5340
|
worktreeBundleId: bundleSelection.bundleId,
|
|
4691
5341
|
});
|
|
5342
|
+
materializationCommitted = true;
|
|
5343
|
+
addExecutionRunLinks({
|
|
5344
|
+
projectId: project.id,
|
|
5345
|
+
runId: planningExecutionRun.id,
|
|
5346
|
+
links: created.tickets.map(ticket => ({
|
|
5347
|
+
entityType: 'ticket',
|
|
5348
|
+
entityId: ticket.id,
|
|
5349
|
+
relationship: 'result',
|
|
5350
|
+
})),
|
|
5351
|
+
});
|
|
4692
5352
|
for (const ticket of created.tickets) {
|
|
4693
5353
|
io.emit('ticket:created', ticket);
|
|
4694
5354
|
}
|
|
4695
5355
|
for (const dependency of created.dependencies) {
|
|
4696
5356
|
io.emit('ticket-dependency:created', dependency);
|
|
4697
5357
|
}
|
|
5358
|
+
transitionExecutionRun({
|
|
5359
|
+
projectId: project.id,
|
|
5360
|
+
runId: planningExecutionRun.id,
|
|
5361
|
+
status: 'completed',
|
|
5362
|
+
domainStatus: 'done',
|
|
5363
|
+
eventType: 'planning_agent_completed',
|
|
5364
|
+
}, io);
|
|
4698
5365
|
return res.status(201).json(created);
|
|
4699
5366
|
}
|
|
4700
5367
|
catch (error) {
|
|
5368
|
+
if (planningExecutionRun) {
|
|
5369
|
+
const rawFailureMessage = getErrorMessage(error) || 'Planning agent failed';
|
|
5370
|
+
const failureMessage = (materializationCommitted
|
|
5371
|
+
? `Planning tickets were created, but execution result reconciliation failed. Reconcile run ${planningExecutionRun.id}: ${rawFailureMessage}`
|
|
5372
|
+
: rawFailureMessage).slice(0, 4_000);
|
|
5373
|
+
try {
|
|
5374
|
+
terminalizeAgentRunsForExecutionRun(planningExecutionRun.id, 'error', failureMessage);
|
|
5375
|
+
}
|
|
5376
|
+
catch {
|
|
5377
|
+
// Preserve the top-level execution failure when child detail finalization fails.
|
|
5378
|
+
}
|
|
5379
|
+
transitionExecutionRun({
|
|
5380
|
+
projectId: project.id,
|
|
5381
|
+
runId: planningExecutionRun.id,
|
|
5382
|
+
status: 'failed',
|
|
5383
|
+
domainStatus: 'error',
|
|
5384
|
+
failure: {
|
|
5385
|
+
code: materializationCommitted ? 'planning_agent_result_reconciliation_failed' : 'planning_agent_failed',
|
|
5386
|
+
message: failureMessage,
|
|
5387
|
+
},
|
|
5388
|
+
eventType: 'planning_agent_failed',
|
|
5389
|
+
}, io);
|
|
5390
|
+
}
|
|
4701
5391
|
if (autoCreatedBundle && countBundleTickets(autoCreatedBundle.id, project.id) === 0) {
|
|
4702
5392
|
deleteBundle(autoCreatedBundle.id, project.id);
|
|
4703
5393
|
}
|
|
@@ -5987,7 +6677,14 @@ app.delete('/projects/:id', (req, res) => {
|
|
|
5987
6677
|
error: 'Cannot delete a project with existing tickets, bundles, or worktrees',
|
|
5988
6678
|
});
|
|
5989
6679
|
}
|
|
5990
|
-
db.
|
|
6680
|
+
db.transaction(() => {
|
|
6681
|
+
db.prepare(`
|
|
6682
|
+
UPDATE execution_runs
|
|
6683
|
+
SET parent_run_id = NULL, retry_of_run_id = NULL
|
|
6684
|
+
WHERE project_id = ?
|
|
6685
|
+
`).run(req.params.id);
|
|
6686
|
+
db.prepare('DELETE FROM projects WHERE id = ?').run(req.params.id);
|
|
6687
|
+
})();
|
|
5991
6688
|
res.status(204).end();
|
|
5992
6689
|
});
|
|
5993
6690
|
app.post('/tickets', (req, res) => {
|
|
@@ -6260,11 +6957,14 @@ app.post('/tickets/delete-parked', async (req, res) => {
|
|
|
6260
6957
|
deleted_ticket_ids: [],
|
|
6261
6958
|
failures: [],
|
|
6262
6959
|
};
|
|
6960
|
+
const sequenceContinuations = [];
|
|
6263
6961
|
for (const ticket of parkedTickets) {
|
|
6264
6962
|
const deleteResult = await deleteTicketRecord(ticket);
|
|
6265
6963
|
if (deleteResult.ok) {
|
|
6266
6964
|
summary.deleted_count += 1;
|
|
6267
6965
|
summary.deleted_ticket_ids.push(ticket.id);
|
|
6966
|
+
emitUpdatedTickets(deleteResult.affectedTicketIds ?? []);
|
|
6967
|
+
sequenceContinuations.push(deleteResult);
|
|
6268
6968
|
continue;
|
|
6269
6969
|
}
|
|
6270
6970
|
summary.failed_count += 1;
|
|
@@ -6274,6 +6974,11 @@ app.post('/tickets/delete-parked', async (req, res) => {
|
|
|
6274
6974
|
error: deleteResult.error,
|
|
6275
6975
|
});
|
|
6276
6976
|
}
|
|
6977
|
+
for (const continuation of sequenceContinuations) {
|
|
6978
|
+
if (continuation.nextBuildTicketId && getTicket(continuation.nextBuildTicketId)) {
|
|
6979
|
+
scheduleBuildAfterTicketDeletion(continuation);
|
|
6980
|
+
}
|
|
6981
|
+
}
|
|
6277
6982
|
return res.json(summary);
|
|
6278
6983
|
});
|
|
6279
6984
|
app.post('/tickets/:id/build-chain', (req, res) => {
|
|
@@ -6287,22 +6992,43 @@ app.post('/tickets/:id/build-chain', (req, res) => {
|
|
|
6287
6992
|
if (!existing.project_id) {
|
|
6288
6993
|
return res.status(400).json({ error: 'Ticket project no longer exists' });
|
|
6289
6994
|
}
|
|
6290
|
-
const
|
|
6291
|
-
if (!
|
|
6292
|
-
return res.status(
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
if (!buildTransition.ok) {
|
|
6296
|
-
stopBuildChain(chainStart.activeTicketId);
|
|
6297
|
-
return res.status(buildTransition.status).json({
|
|
6298
|
-
error: buildTransition.error,
|
|
6299
|
-
...(buildTransition.blockers ? { blockers: buildTransition.blockers } : {}),
|
|
6995
|
+
const validation = prepareTicketForBuild(existing.id, { updateState: false });
|
|
6996
|
+
if (!validation.ok) {
|
|
6997
|
+
return res.status(validation.status).json({
|
|
6998
|
+
error: validation.error,
|
|
6999
|
+
...(validation.blockers ? { blockers: validation.blockers } : {}),
|
|
6300
7000
|
});
|
|
6301
7001
|
}
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
7002
|
+
let affectedTicketIds = [];
|
|
7003
|
+
let activeTicketId = existing.id;
|
|
7004
|
+
const acceptChain = () => {
|
|
7005
|
+
const chainStart = startBuildChain(existing.id, existing.project_id);
|
|
7006
|
+
if (!chainStart.ok) {
|
|
7007
|
+
throw new Error(chainStart.error);
|
|
7008
|
+
}
|
|
7009
|
+
affectedTicketIds = chainStart.affectedTicketIds;
|
|
7010
|
+
activeTicketId = chainStart.activeTicketId;
|
|
7011
|
+
const buildTransition = prepareTicketForBuild(chainStart.activeTicketId);
|
|
7012
|
+
if (!buildTransition.ok) {
|
|
7013
|
+
throw new Error(buildTransition.error);
|
|
7014
|
+
}
|
|
7015
|
+
return {
|
|
7016
|
+
ticket: buildTransition.ticket,
|
|
7017
|
+
sequenceId: chainStart.sequenceId,
|
|
7018
|
+
compensate: () => compensateBuildSequenceAcceptance(chainStart.sequenceId, chainStart.affectedTicketIds),
|
|
7019
|
+
};
|
|
7020
|
+
};
|
|
7021
|
+
const scheduling = startBuild(existing, io, { accept: acceptChain });
|
|
7022
|
+
if (scheduling === undefined) {
|
|
7023
|
+
acceptChain();
|
|
7024
|
+
}
|
|
7025
|
+
else if (!scheduling.accepted) {
|
|
7026
|
+
return res.status(scheduling.reason === 'queue_failed' ? 503 : 409).json({ error: scheduling.error });
|
|
7027
|
+
}
|
|
7028
|
+
emitUpdatedTickets(affectedTicketIds);
|
|
7029
|
+
const updated = getTicket(activeTicketId);
|
|
7030
|
+
io.emit('ticket:updated', updated);
|
|
7031
|
+
return res.json(updated);
|
|
6306
7032
|
});
|
|
6307
7033
|
app.post('/tickets/:id/stop-build-chain', (req, res) => {
|
|
6308
7034
|
const existing = getTicket(req.params.id);
|
|
@@ -6336,31 +7062,152 @@ app.post('/tickets/:id/retry', (req, res) => {
|
|
|
6336
7062
|
&& hasActiveBuildOpenQuestionBatch(ticket.project_id, ticket.worktree_bundle_id)) {
|
|
6337
7063
|
return res.status(409).json({ error: 'Build open question resolution is still in progress' });
|
|
6338
7064
|
}
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
res.json(updated);
|
|
6343
|
-
if (ticket.state === 'build' && updated) {
|
|
6344
|
-
if (updated.project_id && updated.worktree_bundle_id) {
|
|
6345
|
-
const buildBlocker = getBundleBuildBlockerResponse(updated);
|
|
7065
|
+
if (ticket.state === 'build') {
|
|
7066
|
+
if (ticket.project_id && ticket.worktree_bundle_id) {
|
|
7067
|
+
const buildBlocker = getBundleBuildBlockerResponse(ticket);
|
|
6346
7068
|
if (buildBlocker) {
|
|
6347
|
-
return;
|
|
7069
|
+
return res.status(409).json({ error: buildBlocker.error, blocker_ticket_id: buildBlocker.blocker_ticket_id });
|
|
6348
7070
|
}
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
7071
|
+
}
|
|
7072
|
+
const retrySource = ticket.project_id
|
|
7073
|
+
? db.prepare(`
|
|
7074
|
+
SELECT er.id
|
|
7075
|
+
FROM execution_runs er
|
|
7076
|
+
INNER JOIN execution_run_links link
|
|
7077
|
+
ON link.execution_run_id = er.id
|
|
7078
|
+
AND link.project_id = er.project_id
|
|
7079
|
+
AND link.relationship = 'primary'
|
|
7080
|
+
AND link.entity_type = 'ticket'
|
|
7081
|
+
WHERE er.project_id = ?
|
|
7082
|
+
AND er.kind = 'build'
|
|
7083
|
+
AND er.status IN ('failed', 'interrupted')
|
|
7084
|
+
AND link.entity_id = ?
|
|
7085
|
+
ORDER BY er.created_at DESC, er.id DESC
|
|
7086
|
+
LIMIT 1
|
|
7087
|
+
`).get(ticket.project_id, ticket.id)
|
|
7088
|
+
: undefined;
|
|
7089
|
+
const acceptRetry = () => {
|
|
7090
|
+
db.prepare('UPDATE tickets SET agent_status = NULL, agent_log = NULL, agent_message_id = NULL, previous_agent_log = NULL, previous_agent_message_id = NULL, streaming_response = NULL, build_completed_at = NULL, auto_park_dismissed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(req.params.id);
|
|
7091
|
+
const updated = getTicket(req.params.id);
|
|
7092
|
+
if (!updated) {
|
|
7093
|
+
throw new Error('Not found');
|
|
7094
|
+
}
|
|
7095
|
+
let sequenceId = null;
|
|
7096
|
+
if (updated.project_id && updated.worktree_bundle_id) {
|
|
7097
|
+
sequenceId = enqueueBundledBuild({
|
|
7098
|
+
ticketId: updated.id,
|
|
7099
|
+
projectId: updated.project_id,
|
|
7100
|
+
worktreeBundleId: updated.worktree_bundle_id,
|
|
7101
|
+
activate: shouldStartBundleBuildNow(updated),
|
|
7102
|
+
}).sequenceId;
|
|
7103
|
+
}
|
|
7104
|
+
return {
|
|
7105
|
+
ticket: updated,
|
|
7106
|
+
sequenceId,
|
|
7107
|
+
...(sequenceId ? { compensate: () => compensateBuildSequenceAcceptance(sequenceId, [updated.id]) } : {}),
|
|
7108
|
+
};
|
|
7109
|
+
};
|
|
7110
|
+
if (ticket.project_id && ticket.worktree_bundle_id && hasActiveSequenceForBundle(ticket.project_id, ticket.worktree_bundle_id)) {
|
|
7111
|
+
try {
|
|
7112
|
+
withExecutionRunTransaction(transaction => {
|
|
7113
|
+
const acceptance = acceptRetry();
|
|
7114
|
+
if (!acceptance.sequenceId) {
|
|
7115
|
+
throw new Error('Build sequence no longer exists');
|
|
7116
|
+
}
|
|
7117
|
+
transaction.createExecutionRun({
|
|
7118
|
+
projectId: ticket.project_id,
|
|
7119
|
+
kind: 'build',
|
|
7120
|
+
domainStatus: 'queued',
|
|
7121
|
+
sourceReference: { kind: 'ticket', id: acceptance.ticket.id, label: acceptance.ticket.title },
|
|
7122
|
+
links: [
|
|
7123
|
+
{ entityType: 'bundle', entityId: ticket.worktree_bundle_id, relationship: 'context' },
|
|
7124
|
+
{ entityType: 'build_sequence', entityId: acceptance.sequenceId, relationship: 'context' },
|
|
7125
|
+
],
|
|
7126
|
+
initiatedBy: 'user',
|
|
7127
|
+
retryOfRunId: retrySource?.id ?? null,
|
|
7128
|
+
eventType: 'build_created',
|
|
7129
|
+
});
|
|
7130
|
+
}, io);
|
|
6357
7131
|
}
|
|
7132
|
+
catch (error) {
|
|
7133
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
7134
|
+
}
|
|
7135
|
+
const updated = getTicket(req.params.id);
|
|
7136
|
+
io.emit('ticket:updated', updated);
|
|
7137
|
+
return res.json(updated);
|
|
7138
|
+
}
|
|
7139
|
+
const scheduling = startBuild(ticket, io, {
|
|
7140
|
+
initiation: { initiator: 'user', retrySourceRunId: retrySource?.id ?? null },
|
|
7141
|
+
accept: acceptRetry,
|
|
7142
|
+
});
|
|
7143
|
+
if (scheduling === undefined) {
|
|
7144
|
+
acceptRetry();
|
|
7145
|
+
}
|
|
7146
|
+
else if (!scheduling.accepted) {
|
|
7147
|
+
return res.status(scheduling.reason === 'queue_failed' ? 503 : 409).json({ error: scheduling.error });
|
|
6358
7148
|
}
|
|
6359
|
-
|
|
7149
|
+
const updated = getTicket(req.params.id);
|
|
7150
|
+
io.emit('ticket:updated', updated);
|
|
7151
|
+
return res.json(updated);
|
|
6360
7152
|
}
|
|
6361
|
-
|
|
6362
|
-
|
|
7153
|
+
const reviewRetrySource = ticket.state === 'review' && ticket.project_id
|
|
7154
|
+
? db.prepare(`
|
|
7155
|
+
SELECT er.id
|
|
7156
|
+
FROM execution_runs er
|
|
7157
|
+
JOIN execution_run_links link
|
|
7158
|
+
ON link.execution_run_id = er.id
|
|
7159
|
+
AND link.relationship = 'primary'
|
|
7160
|
+
AND link.entity_type = 'ticket'
|
|
7161
|
+
WHERE er.project_id = ?
|
|
7162
|
+
AND er.kind = 'review'
|
|
7163
|
+
AND er.status IN ('failed', 'interrupted')
|
|
7164
|
+
AND link.entity_id = ?
|
|
7165
|
+
ORDER BY er.created_at DESC, er.id DESC
|
|
7166
|
+
LIMIT 1
|
|
7167
|
+
`).get(ticket.project_id, ticket.id)
|
|
7168
|
+
: undefined;
|
|
7169
|
+
if (ticket.state === 'review') {
|
|
7170
|
+
const reviewSnapshot = {
|
|
7171
|
+
agent_status: ticket.agent_status,
|
|
7172
|
+
agent_log: ticket.agent_log,
|
|
7173
|
+
agent_message_id: ticket.agent_message_id,
|
|
7174
|
+
previous_agent_log: ticket.previous_agent_log,
|
|
7175
|
+
previous_agent_message_id: ticket.previous_agent_message_id,
|
|
7176
|
+
streaming_response: ticket.streaming_response,
|
|
7177
|
+
build_completed_at: ticket.build_completed_at,
|
|
7178
|
+
auto_park_dismissed_at: ticket.auto_park_dismissed_at,
|
|
7179
|
+
};
|
|
7180
|
+
const reviewResult = rerunTicketReviewOperation(ticket, io, {
|
|
7181
|
+
initiation: { retrySourceRunId: reviewRetrySource?.id ?? null },
|
|
7182
|
+
accept: () => {
|
|
7183
|
+
db.prepare('UPDATE tickets SET agent_status = NULL, agent_log = NULL, agent_message_id = NULL, previous_agent_log = NULL, previous_agent_message_id = NULL, streaming_response = NULL, build_completed_at = NULL, auto_park_dismissed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(req.params.id);
|
|
7184
|
+
const acceptedTicket = getTicket(req.params.id);
|
|
7185
|
+
if (!acceptedTicket) {
|
|
7186
|
+
throw new Error('Not found');
|
|
7187
|
+
}
|
|
7188
|
+
return {
|
|
7189
|
+
ticket: acceptedTicket,
|
|
7190
|
+
compensate: () => {
|
|
7191
|
+
db.prepare(`
|
|
7192
|
+
UPDATE tickets SET agent_status = ?, agent_log = ?, agent_message_id = ?, previous_agent_log = ?,
|
|
7193
|
+
previous_agent_message_id = ?, streaming_response = ?, build_completed_at = ?, auto_park_dismissed_at = ?,
|
|
7194
|
+
updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
|
7195
|
+
`).run(reviewSnapshot.agent_status, reviewSnapshot.agent_log, reviewSnapshot.agent_message_id, reviewSnapshot.previous_agent_log, reviewSnapshot.previous_agent_message_id, reviewSnapshot.streaming_response, reviewSnapshot.build_completed_at, reviewSnapshot.auto_park_dismissed_at, ticket.id);
|
|
7196
|
+
},
|
|
7197
|
+
};
|
|
7198
|
+
},
|
|
7199
|
+
});
|
|
7200
|
+
if (!reviewResult.ok) {
|
|
7201
|
+
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
7202
|
+
}
|
|
7203
|
+
const updated = getTicket(req.params.id);
|
|
7204
|
+
io.emit('ticket:updated', updated);
|
|
7205
|
+
return res.json(updated);
|
|
6363
7206
|
}
|
|
7207
|
+
db.prepare('UPDATE tickets SET agent_status = NULL, agent_log = NULL, agent_message_id = NULL, previous_agent_log = NULL, previous_agent_message_id = NULL, streaming_response = NULL, auto_park_dismissed_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(req.params.id);
|
|
7208
|
+
const updated = getTicket(req.params.id);
|
|
7209
|
+
io.emit('ticket:updated', updated);
|
|
7210
|
+
return res.json(updated);
|
|
6364
7211
|
});
|
|
6365
7212
|
app.post('/tickets/:id/review', (req, res) => {
|
|
6366
7213
|
const ticket = getTicket(req.params.id);
|
|
@@ -6664,6 +7511,184 @@ app.post('/projects/:id/test-sessions', (req, res) => {
|
|
|
6664
7511
|
targetLabel: workspace.target_label,
|
|
6665
7512
|
}));
|
|
6666
7513
|
});
|
|
7514
|
+
app.get('/projects/:id/execution-runs', (req, res) => {
|
|
7515
|
+
const project = getProjectById(req.params.id);
|
|
7516
|
+
if (!project) {
|
|
7517
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7518
|
+
}
|
|
7519
|
+
const query = parseLifecycleListQuery(req.query);
|
|
7520
|
+
if ('error' in query) {
|
|
7521
|
+
return res.status(400).json({ error: query.error });
|
|
7522
|
+
}
|
|
7523
|
+
try {
|
|
7524
|
+
return res.json(listExecutionRuns({
|
|
7525
|
+
projectId: project.id,
|
|
7526
|
+
kind: query.kind,
|
|
7527
|
+
statuses: query.statuses,
|
|
7528
|
+
cursor: query.cursor,
|
|
7529
|
+
limit: query.limit,
|
|
7530
|
+
}));
|
|
7531
|
+
}
|
|
7532
|
+
catch (error) {
|
|
7533
|
+
if (error instanceof Error && error.message === 'Invalid execution run cursor') {
|
|
7534
|
+
return res.status(400).json({ error: 'cursor must be a valid execution run cursor' });
|
|
7535
|
+
}
|
|
7536
|
+
throw error;
|
|
7537
|
+
}
|
|
7538
|
+
});
|
|
7539
|
+
app.get('/projects/:id/lifecycle-events', (req, res) => {
|
|
7540
|
+
const project = getProjectById(req.params.id);
|
|
7541
|
+
if (!project) {
|
|
7542
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7543
|
+
}
|
|
7544
|
+
const query = parseLifecycleListQuery(req.query);
|
|
7545
|
+
if ('error' in query) {
|
|
7546
|
+
return res.status(400).json({ error: query.error });
|
|
7547
|
+
}
|
|
7548
|
+
try {
|
|
7549
|
+
const page = listProjectLifecycleEvents({
|
|
7550
|
+
projectId: project.id,
|
|
7551
|
+
kind: query.kind,
|
|
7552
|
+
statuses: query.statuses,
|
|
7553
|
+
cursor: query.cursor,
|
|
7554
|
+
limit: query.limit,
|
|
7555
|
+
});
|
|
7556
|
+
return res.json({
|
|
7557
|
+
...page,
|
|
7558
|
+
events: page.events.map(toCanonicalLifecycleEvent),
|
|
7559
|
+
});
|
|
7560
|
+
}
|
|
7561
|
+
catch (error) {
|
|
7562
|
+
if (error instanceof Error && error.message === 'Invalid lifecycle event cursor') {
|
|
7563
|
+
return res.status(400).json({ error: 'cursor must be a valid lifecycle event cursor' });
|
|
7564
|
+
}
|
|
7565
|
+
throw error;
|
|
7566
|
+
}
|
|
7567
|
+
});
|
|
7568
|
+
app.get('/projects/:id/execution-timeline', (req, res) => {
|
|
7569
|
+
const project = getProjectById(req.params.id);
|
|
7570
|
+
if (!project) {
|
|
7571
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7572
|
+
}
|
|
7573
|
+
const query = parseLifecycleListQuery(req.query);
|
|
7574
|
+
if ('error' in query) {
|
|
7575
|
+
return res.status(400).json({ error: query.error });
|
|
7576
|
+
}
|
|
7577
|
+
const scope = parseExecutionTimelineScope(project.id, req.query);
|
|
7578
|
+
if ('error' in scope) {
|
|
7579
|
+
return res.status(scope.status ?? 400).json({ error: scope.error });
|
|
7580
|
+
}
|
|
7581
|
+
try {
|
|
7582
|
+
return res.json(listExecutionTimeline({
|
|
7583
|
+
project,
|
|
7584
|
+
kind: query.kind,
|
|
7585
|
+
statuses: query.statuses,
|
|
7586
|
+
cursor: query.cursor,
|
|
7587
|
+
limit: query.limit,
|
|
7588
|
+
scope: scope.scopeEntity,
|
|
7589
|
+
}));
|
|
7590
|
+
}
|
|
7591
|
+
catch (error) {
|
|
7592
|
+
if (error instanceof Error && error.message === 'Invalid execution run cursor') {
|
|
7593
|
+
return res.status(400).json({ error: 'cursor must be a valid execution run cursor' });
|
|
7594
|
+
}
|
|
7595
|
+
throw error;
|
|
7596
|
+
}
|
|
7597
|
+
});
|
|
7598
|
+
app.get('/projects/:id/execution-timeline/:runId', (req, res) => {
|
|
7599
|
+
const project = getProjectById(req.params.id);
|
|
7600
|
+
if (!project) {
|
|
7601
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7602
|
+
}
|
|
7603
|
+
const scope = parseExecutionTimelineScope(project.id, req.query);
|
|
7604
|
+
if ('error' in scope) {
|
|
7605
|
+
return res.status(scope.status ?? 400).json({ error: scope.error });
|
|
7606
|
+
}
|
|
7607
|
+
const run = getExecutionRun(project.id, req.params.runId);
|
|
7608
|
+
if (!run) {
|
|
7609
|
+
return res.status(404).json({ error: 'Execution run not found' });
|
|
7610
|
+
}
|
|
7611
|
+
const links = listExecutionRunLinks(project.id, run.id);
|
|
7612
|
+
if (scope.scopeEntity && !links.some(link => link.entity_type === scope.scopeEntity?.type && link.entity_id === scope.scopeEntity.id)) {
|
|
7613
|
+
return res.status(404).json({ error: 'Execution run not found' });
|
|
7614
|
+
}
|
|
7615
|
+
const testDetails = run.kind === 'test' ? getTestExecutionDetails(project.id, run.id) : null;
|
|
7616
|
+
const retryEligibility = getExecutionRetryEligibility(project.id, run.id);
|
|
7617
|
+
const details = run.kind === 'test'
|
|
7618
|
+
? serializeTestExecutionDetail(testDetails, retryEligibility)
|
|
7619
|
+
: run.kind === 'github_actions'
|
|
7620
|
+
? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
|
|
7621
|
+
: ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
|
|
7622
|
+
? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
|
|
7623
|
+
: { kind: run.kind, available: true, retry_eligibility: retryEligibility };
|
|
7624
|
+
return res.json({
|
|
7625
|
+
run,
|
|
7626
|
+
links,
|
|
7627
|
+
events: listLifecycleEvents(project.id, run.id).map(toCanonicalLifecycleEvent),
|
|
7628
|
+
details,
|
|
7629
|
+
agent_runs: listAgentRunsForExecutionRun(run.id),
|
|
7630
|
+
});
|
|
7631
|
+
});
|
|
7632
|
+
app.get('/projects/:id/execution-runs/:runId', (req, res) => {
|
|
7633
|
+
const project = getProjectById(req.params.id);
|
|
7634
|
+
if (!project) {
|
|
7635
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7636
|
+
}
|
|
7637
|
+
const run = getExecutionRun(project.id, req.params.runId);
|
|
7638
|
+
if (!run) {
|
|
7639
|
+
return res.status(404).json({ error: 'Execution run not found' });
|
|
7640
|
+
}
|
|
7641
|
+
const testDetails = run.kind === 'test' ? getTestExecutionDetails(project.id, run.id) : null;
|
|
7642
|
+
const retryEligibility = getExecutionRetryEligibility(project.id, run.id);
|
|
7643
|
+
const details = run.kind === 'test'
|
|
7644
|
+
? serializeTestExecutionDetail(testDetails, retryEligibility)
|
|
7645
|
+
: run.kind === 'github_actions'
|
|
7646
|
+
? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
|
|
7647
|
+
: ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
|
|
7648
|
+
? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
|
|
7649
|
+
: { kind: run.kind, available: true, retry_eligibility: retryEligibility };
|
|
7650
|
+
const links = listExecutionRunLinks(project.id, run.id);
|
|
7651
|
+
const agentRuns = listAgentRunsForExecutionRun(run.id);
|
|
7652
|
+
const events = listLifecycleEvents(project.id, run.id).map(toCanonicalLifecycleEvent);
|
|
7653
|
+
return res.json({ run, links, events, details, agent_runs: agentRuns });
|
|
7654
|
+
});
|
|
7655
|
+
app.post('/projects/:id/execution-runs/:runId/retry', async (req, res) => {
|
|
7656
|
+
const project = getProjectById(req.params.id);
|
|
7657
|
+
if (!project) {
|
|
7658
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7659
|
+
}
|
|
7660
|
+
const resolution = resolveExecutionRetry(project.id, req.params.runId);
|
|
7661
|
+
const result = await dispatchExecutionRetry(resolution, io, async (retry) => {
|
|
7662
|
+
const session = createTestSession({
|
|
7663
|
+
projectId: project.id,
|
|
7664
|
+
ticketId: retry.ticketId,
|
|
7665
|
+
targetKind: retry.workspace.target_kind,
|
|
7666
|
+
targetBundleId: retry.workspace.target_bundle_id,
|
|
7667
|
+
cwd: retry.workspace.cwd,
|
|
7668
|
+
targetLabel: retry.workspace.target_label,
|
|
7669
|
+
});
|
|
7670
|
+
try {
|
|
7671
|
+
const started = await startTestSessionRun(session.id, project, {
|
|
7672
|
+
command: retry.command,
|
|
7673
|
+
selection: retry.selection,
|
|
7674
|
+
retryOfRunId: req.params.runId,
|
|
7675
|
+
deleteOnComplete: true,
|
|
7676
|
+
}, io);
|
|
7677
|
+
if (!started.run_id) {
|
|
7678
|
+
throw new Error('Test retry did not create a replacement execution');
|
|
7679
|
+
}
|
|
7680
|
+
return started.run_id;
|
|
7681
|
+
}
|
|
7682
|
+
catch (error) {
|
|
7683
|
+
await deleteTestSession(session.id, io).catch(() => undefined);
|
|
7684
|
+
throw error;
|
|
7685
|
+
}
|
|
7686
|
+
});
|
|
7687
|
+
if (!result.ok) {
|
|
7688
|
+
return res.status(result.status).json({ error: result.error, reason: result.reason });
|
|
7689
|
+
}
|
|
7690
|
+
return res.status(201).json(result.value);
|
|
7691
|
+
});
|
|
6667
7692
|
app.post('/tickets/:id/test-sessions', (req, res) => {
|
|
6668
7693
|
const ticket = getTicket(req.params.id);
|
|
6669
7694
|
if (!ticket) {
|
|
@@ -6919,8 +7944,15 @@ app.post('/tickets/:id/plan', (req, res) => {
|
|
|
6919
7944
|
});
|
|
6920
7945
|
}
|
|
6921
7946
|
}
|
|
6922
|
-
startPlan(ticket, io, {
|
|
6923
|
-
|
|
7947
|
+
const scheduling = startPlan(ticket, io, {
|
|
7948
|
+
branch,
|
|
7949
|
+
targetBundleId,
|
|
7950
|
+
});
|
|
7951
|
+
if (!scheduling.accepted) {
|
|
7952
|
+
const status = scheduling.reason === 'queue_failed' ? 503 : 409;
|
|
7953
|
+
return res.status(status).json({ error: scheduling.error });
|
|
7954
|
+
}
|
|
7955
|
+
return res.status(202).json({ ok: true, execution_run_id: scheduling.executionContext.runId });
|
|
6924
7956
|
});
|
|
6925
7957
|
app.delete('/tickets/:id', async (req, res) => {
|
|
6926
7958
|
const ticket = getTicket(req.params.id);
|
|
@@ -6937,12 +7969,7 @@ app.delete('/tickets/:id', async (req, res) => {
|
|
|
6937
7969
|
if (result.affectedTicketIds) {
|
|
6938
7970
|
emitUpdatedTickets(result.affectedTicketIds);
|
|
6939
7971
|
}
|
|
6940
|
-
|
|
6941
|
-
const nextTicket = getTicket(result.nextBuildTicketId);
|
|
6942
|
-
if (nextTicket) {
|
|
6943
|
-
startBuild(nextTicket, io);
|
|
6944
|
-
}
|
|
6945
|
-
}
|
|
7972
|
+
scheduleBuildAfterTicketDeletion(result);
|
|
6946
7973
|
res.status(204).end();
|
|
6947
7974
|
});
|
|
6948
7975
|
const clientBuild = resolveClientBuildPaths(import.meta.url);
|
|
@@ -6969,6 +7996,33 @@ io.on('connection', (socket) => {
|
|
|
6969
7996
|
registerTerminalSocketHandlers(io, socket);
|
|
6970
7997
|
});
|
|
6971
7998
|
const DEFAULT_PORT = 3001;
|
|
7999
|
+
let executionEvidenceCleanupTimer = null;
|
|
8000
|
+
let startServerPromise = null;
|
|
8001
|
+
let shutdownServerPromise = null;
|
|
8002
|
+
let shutdownRequested = false;
|
|
8003
|
+
const throwIfShutdownRequested = () => {
|
|
8004
|
+
if (shutdownRequested) {
|
|
8005
|
+
throw new Error('Server startup was cancelled by shutdown');
|
|
8006
|
+
}
|
|
8007
|
+
};
|
|
8008
|
+
const runExecutionEvidenceCleanup = () => {
|
|
8009
|
+
try {
|
|
8010
|
+
const result = cleanupAllExecutionEvidence({ retentionDays: resolveExecutionEvidenceRetentionDays() });
|
|
8011
|
+
if (result.total > 0) {
|
|
8012
|
+
console.log(`Expired execution evidence for ${result.total} run(s): ${result.test_output} test output, ${result.github_jobs} GitHub job snapshot(s)`);
|
|
8013
|
+
}
|
|
8014
|
+
}
|
|
8015
|
+
catch (error) {
|
|
8016
|
+
console.error('Failed to clean up expired execution evidence', error);
|
|
8017
|
+
}
|
|
8018
|
+
};
|
|
8019
|
+
const startExecutionEvidenceCleanupTimer = () => {
|
|
8020
|
+
if (executionEvidenceCleanupTimer) {
|
|
8021
|
+
return;
|
|
8022
|
+
}
|
|
8023
|
+
executionEvidenceCleanupTimer = setInterval(runExecutionEvidenceCleanup, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS);
|
|
8024
|
+
executionEvidenceCleanupTimer.unref();
|
|
8025
|
+
};
|
|
6972
8026
|
const resolveStartResult = (host) => {
|
|
6973
8027
|
const address = httpServer.address();
|
|
6974
8028
|
const { displayHost } = resolveDesktopServerHost(host);
|
|
@@ -6986,38 +8040,83 @@ const resolveStartResult = (host) => {
|
|
|
6986
8040
|
};
|
|
6987
8041
|
};
|
|
6988
8042
|
export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
8043
|
+
if (shutdownRequested) {
|
|
8044
|
+
return Promise.reject(new Error('Server startup was cancelled by shutdown'));
|
|
8045
|
+
}
|
|
8046
|
+
if (httpServer.listening) {
|
|
8047
|
+
return Promise.resolve(resolveStartResult(host));
|
|
8048
|
+
}
|
|
8049
|
+
if (startServerPromise) {
|
|
8050
|
+
return startServerPromise;
|
|
8051
|
+
}
|
|
8052
|
+
startServerPromise = (async () => {
|
|
8053
|
+
await reconcileStartupExecutions({ getGitHubConfig: getGitHubConnectionConfig });
|
|
8054
|
+
throwIfShutdownRequested();
|
|
8055
|
+
startGitHubActionsPolling(io);
|
|
8056
|
+
try {
|
|
8057
|
+
throwIfShutdownRequested();
|
|
8058
|
+
await new Promise((resolve, reject) => {
|
|
8059
|
+
const handleError = (error) => {
|
|
8060
|
+
httpServer.off('error', handleError);
|
|
8061
|
+
reject(error);
|
|
8062
|
+
};
|
|
8063
|
+
const handleListening = () => {
|
|
8064
|
+
httpServer.off('error', handleError);
|
|
8065
|
+
resolve();
|
|
8066
|
+
};
|
|
8067
|
+
httpServer.once('error', handleError);
|
|
8068
|
+
const { bindHost } = resolveDesktopServerHost(host);
|
|
8069
|
+
httpServer.listen(port, bindHost, handleListening);
|
|
8070
|
+
});
|
|
6993
8071
|
}
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
}
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
8072
|
+
catch (error) {
|
|
8073
|
+
await clearAllGitHubActionsSessions();
|
|
8074
|
+
throw error;
|
|
8075
|
+
}
|
|
8076
|
+
throwIfShutdownRequested();
|
|
8077
|
+
runExecutionEvidenceCleanup();
|
|
8078
|
+
throwIfShutdownRequested();
|
|
8079
|
+
startExecutionEvidenceCleanupTimer();
|
|
8080
|
+
return resolveStartResult(host);
|
|
8081
|
+
})().finally(() => {
|
|
8082
|
+
startServerPromise = null;
|
|
7005
8083
|
});
|
|
8084
|
+
return startServerPromise;
|
|
7006
8085
|
};
|
|
7007
8086
|
export const shutdownServer = async (signal) => {
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
8087
|
+
if (shutdownServerPromise) {
|
|
8088
|
+
return shutdownServerPromise;
|
|
8089
|
+
}
|
|
8090
|
+
shutdownRequested = true;
|
|
8091
|
+
const pendingStartup = startServerPromise;
|
|
8092
|
+
shutdownServerPromise = (async () => {
|
|
8093
|
+
await Promise.all([
|
|
8094
|
+
pendingStartup?.catch(() => undefined),
|
|
8095
|
+
stopAllPlanAgents(),
|
|
8096
|
+
]);
|
|
8097
|
+
if (executionEvidenceCleanupTimer) {
|
|
8098
|
+
clearInterval(executionEvidenceCleanupTimer);
|
|
8099
|
+
executionEvidenceCleanupTimer = null;
|
|
8100
|
+
}
|
|
8101
|
+
await stopMobileAccess();
|
|
8102
|
+
await clearAllTestSessions(io);
|
|
8103
|
+
await clearAllGitHubActionsSessions();
|
|
8104
|
+
await shutdownRealtimeServer({
|
|
8105
|
+
signal,
|
|
8106
|
+
io,
|
|
8107
|
+
httpServer,
|
|
8108
|
+
closeAllTerminalSessions,
|
|
8109
|
+
clearAllPreviewProxies,
|
|
8110
|
+
stopAllBuildAgents,
|
|
8111
|
+
stopAllReviewAgents,
|
|
8112
|
+
stopAllPlanAgents,
|
|
8113
|
+
stopAllTickets,
|
|
8114
|
+
});
|
|
8115
|
+
})().finally(() => {
|
|
8116
|
+
shutdownRequested = false;
|
|
8117
|
+
shutdownServerPromise = null;
|
|
7020
8118
|
});
|
|
8119
|
+
return shutdownServerPromise;
|
|
7021
8120
|
};
|
|
7022
8121
|
export const registerShutdownHandlers = () => {
|
|
7023
8122
|
const createHandler = (signal) => {
|