@straiffi/archon 1.7.1 → 1.7.3
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-DTEenQas.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +1 -0
- package/dist/client/assets/TestsDialog-cBaoffCs.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-CJgwCv5O.js → architectureDiagram-3BPJPVTR-BzzGotac.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM--xjes8FA.js → blockDiagram-GPEHLZMM-DIcqLHKg.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-DHnp82NG.js → c4Diagram-AAUBKEIU-Hc1Sqmhs.js} +1 -1
- package/dist/client/assets/channel-Cym17waa.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-DAOyJCKy.js → chunk-2J33WTMH-CWSwjsgk.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-C9YI_tk4.js → chunk-3OPIFGDE-BnX3DljN.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-BW9khkin.js → chunk-5ZQYHXKU-EyyJlI-d.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-DL4t5Hpi.js → chunk-727SXJPM-Ci6CfxWj.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-C4VMKa_O.js → chunk-AQP2D5EJ-RKZNsJTm.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-DHP7zrZK.js → chunk-CSCIHK7Q-DBf0Euoj.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-CqZWek3t.js → chunk-KSCS5N6A-BmLQI_2S.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-D3q55pCI.js → chunk-L5ZTLDWV-DKwVAb8G.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-UDY_Lo6v.js → chunk-LZXEDZCA-axotKkbz.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-ZCiOxkYV.js → chunk-ND2GUHAM-BBlJ4nXC.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-CuryoBtd.js → chunk-NZK2D7GU-BKJGoZZt.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-PnPsz2TB.js → chunk-O5CBEL6O-C-Ghigo1.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-BzOopX-U.js → chunk-WU5MYG2G-CCzVWYcd.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-5LBwQ83I.js → dagre-BM42HDAG-rwIdPcjI.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-_4pckiGR.js → diagram-2AECGRRQ-DJWxOQsg.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CqElv5rJ.js → diagram-5GNKFQAL-C-mQeFtd.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-DKv4zS4F.js → diagram-KO2AKTUF-BAu6u4sR.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-iLmTH4UP.js → diagram-LMA3HP47-U6CbvBn0.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-DpFYIa0o.js → diagram-OG6HWLK6-DNTwFcs1.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-DnfbvbKs.js → erDiagram-TEJ5UH35-DaHX6Xw5.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-C2MN70Qv.js → flowDiagram-I6XJVG4X-B9hKqL2l.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-yonpliOJ.js → ganttDiagram-6RSMTGT7-BQ1HPHVO.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CaKDWx7y.js → gitGraphDiagram-PVQCEYII-BBRcsmMK.js} +1 -1
- package/dist/client/assets/index-CEMaJCPS.js +273 -0
- package/dist/client/assets/index-lWftq65U.css +2 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-82-8njnx.js → infoDiagram-5YYISTIA-DUoNiSGC.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BMiyLWwK.js → ishikawaDiagram-YF4QCWOH-DJN1y2w8.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-C1qBnR37.js → journeyDiagram-JHISSGLW-BxLLMT70.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-zByEuCzn.js → kanban-definition-UN3LZRKU-BdCPistJ.js} +1 -1
- package/dist/client/assets/{line-EpLqyzw7.js → line-4mIgXsZd.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-o124REPN.js → mermaid-parser.core-Pu1QGWJ-.js} +1 -1
- package/dist/client/assets/{mermaid.core-DgiAn2-k.js → mermaid.core-DnH9DrgZ.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-UK6V0Bxq.js → mindmap-definition-RKZ34NQL-DqE4ba3C.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-B2s9oGFl.js → pieDiagram-4H26LBE5-Crjy9ejf.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-d5H7-zcc.js → quadrantDiagram-W4KKPZXB-BUSZ2VB7.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DlwN9Vcv.js → requirementDiagram-4Y6WPE33-CBhFKEoV.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-9dGyAxu5.js → sankeyDiagram-5OEKKPKP-CgdbY41v.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DKwRjEym.js → sequenceDiagram-3UESZ5HK-hkp_H7AE.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-D_6cuqsX.js → stateDiagram-AJRCARHV-C6x45RWp.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ChqD4j8I.js → timeline-definition-PNZ67QCA-CjnZWttQ.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-CSAZk0IW.js → vennDiagram-CIIHVFJN-DU5ZyYju.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BUE6IF4q.js → wardleyDiagram-YWT4CUSO-CaCEnCB0.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-MOkC53m_.js → xychartDiagram-2RQKCTM6-Ce3xa37U.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +471 -39
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +1267 -213
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/agent.js +281 -76
- 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/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 +223 -0
- package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
- package/dist/server/lib/buildOpenQuestions.js +52 -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 +82 -9
- package/dist/server/lib/bundleReviewOperations.js.map +1 -1
- package/dist/server/lib/chatOperations.js +31 -0
- 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/executionRunRetention.js +141 -0
- package/dist/server/lib/executionRunRetention.js.map +1 -0
- package/dist/server/lib/executionRuns.js +1264 -0
- package/dist/server/lib/executionRuns.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 +31 -0
- package/dist/server/lib/specOperations.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +44 -14
- 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 +44 -7
- 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 +164 -19
- 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 +214 -20
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketOpenQuestions.js +30 -14
- package/dist/server/lib/ticketOpenQuestions.js.map +1 -1
- package/dist/server/lib/ticketRunOperations.js +75 -7
- 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 +46 -29
- 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 +57 -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 +261 -130
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +720 -195
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +527 -188
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +231 -69
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +796 -239
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +211 -45
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +425 -252
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/GitHubActionsDialog-BrUq-zO1.js +0 -1
- package/dist/client/assets/TestsDialog-D9N4GD77.js +0 -4
- package/dist/client/assets/TicketChangesModal-bYHJSO6d.js +0 -1
- package/dist/client/assets/channel-B4kCvLCt.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C-eRRNNj.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B3vxoc73.js +0 -1
- package/dist/client/assets/index-9q1GXkUw.js +0 -270
- package/dist/client/assets/index-BxTc5qQT.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BkMKz90T.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 } 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,17 @@ 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, hasDirectRetryChild, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
|
|
58
|
+
import { cleanupAllExecutionEvidence, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS, resolveExecutionEvidenceRetentionDays, } from './lib/executionRunRetention.js';
|
|
53
59
|
import { shutdownRealtimeServer } from './lib/shutdown.js';
|
|
54
60
|
import { resolveClientBuildPaths, shouldServeClientApp, STATIC_CLIENT_DIRECTORY_OPTIONS, STATIC_CLIENT_INDEX_OPTIONS, } from './lib/staticClient.js';
|
|
55
61
|
import { deleteIntegrationConnection, getGitHubConnectionConfig, getJiraConnectionConfig, listIntegrationConnections, upsertIntegrationConnection, } from './lib/integrations/index.js';
|
|
@@ -58,24 +64,25 @@ import { importJiraIssue, JiraApiError, prepareJiraIssueImport, validateJiraConn
|
|
|
58
64
|
import { importGitHubIssue, prepareGitHubIssueImport } from './lib/integrations/github-issues.js';
|
|
59
65
|
import { buildImportedIssueSessionTitle, importProviderHostedIssueImages } from './lib/integrations/importedIssueImages.js';
|
|
60
66
|
import { dispatchGitHubWorkflow, getGitHubWorkflowDispatchConfig, getGitHubWorkflowRun, getGitHubWorkflowRunJobs, listGitHubWorkflows, listGitHubWorkflowRuns, } from './lib/integrations/githubActions.js';
|
|
61
|
-
import { clearAllGitHubActionsSessions, getActiveTrackedRunCount, getTrackedGitHubActionsRuns, trackGitHubActionsRun, } from './lib/integrations/githubActionsSessions.js';
|
|
67
|
+
import { clearAllGitHubActionsSessions, getActiveTrackedRunCount, getTrackedGitHubActionsRuns, startGitHubActionsPolling, trackGitHubActionsRun, } from './lib/integrations/githubActionsSessions.js';
|
|
68
|
+
import { reconcileStartupExecutions } from './lib/executionReconciliation.js';
|
|
62
69
|
import { PlanningReferenceError, resolveExternalPlanningDescription } from './lib/integrations/planning.js';
|
|
63
70
|
import { refreshIfNeeded as refreshAssignedIssues, clearCachedAssignedIssues } from './lib/integrations/assignedIssuesCache.js';
|
|
64
71
|
import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib/commitMessage.js';
|
|
65
72
|
import { BUNDLE_PULL_REQUEST_SYNC_TTL_MS, getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, syncTrackedBundlePullRequestIfNeeded, upsertBundlePullRequest, upsertBundlePullRequestFromGitHub, } from './lib/bundlePullRequests.js';
|
|
66
73
|
import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
|
|
67
74
|
import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
|
|
68
|
-
import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
|
|
75
|
+
import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExist, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
|
|
69
76
|
import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTicketState, } from './lib/ticketWorkflowOperations.js';
|
|
70
77
|
import { submitPlanTicketFollowUp, submitTicketFollowUp, } from './lib/ticketFollowUpOperations.js';
|
|
71
|
-
import { canUseBuildControls, formatActiveRunConflict, getTicketRunReadinessError, resumeBuildTicketRun, runProjectTarget, runTicketRun, stopBuildTicketRun, stopProjectTargetRun, stopTicketRun, } from './lib/ticketRunOperations.js';
|
|
78
|
+
import { canUseBuildControls, formatActiveRunConflict, getTicketRunReadinessError, resumeBuildTicketRun, runProjectTarget, runTicketRun, stopBuildTicketRun, stopReviewTicketRun, stopProjectTargetRun, stopTicketRun, } from './lib/ticketRunOperations.js';
|
|
72
79
|
import { isTicketAgentSettingsPayload, updateTicketAgentSettings, } from './lib/ticketSettingsOperations.js';
|
|
73
|
-
import { resolveBundleReviewOwner, transitionBundleToReview } from './lib/bundleReviewOperations.js';
|
|
74
|
-
import { getActiveBundleStageChangeHead, undoTicketStage } from './lib/ticketUndo.js';
|
|
80
|
+
import { rerunTicketReviewOperation, resolveBundleReviewOwner, transitionBundleToReview } from './lib/bundleReviewOperations.js';
|
|
81
|
+
import { clearTicketStageSnapshots, getActiveBundleStageChangeHead, reconcileStageSnapshots, undoTicketStage } from './lib/ticketUndo.js';
|
|
75
82
|
import { closeAllTerminalSessions, createProjectTerminalSession, createTerminalSessionForWorkspace, destroyTerminalSessionById, registerTerminalSocketHandlers, } from './lib/terminal.js';
|
|
76
83
|
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
84
|
import { createTicketMessage, upsertTicketDescriptionMessage } from './lib/ticketMessages.js';
|
|
78
|
-
import { listAgentRunsForParent } from './lib/agentRuns.js';
|
|
85
|
+
import { listAgentRunsForExecutionRun, listAgentRunsForParent, terminalizeAgentRunsForExecutionRun } from './lib/agentRuns.js';
|
|
79
86
|
import { HttpError } from './lib/httpError.js';
|
|
80
87
|
import { parseIncomingExternalSource } from './lib/externalSource.js';
|
|
81
88
|
import { materializeTicketsWithDependencies } from './lib/ticketBatch.js';
|
|
@@ -84,9 +91,8 @@ import { buildSpecImplementationDraft, buildVisualizationAppendix } from './lib/
|
|
|
84
91
|
import { exportSpecToDocs, checkSpecExportCollision } from './lib/specExport.js';
|
|
85
92
|
import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
|
|
86
93
|
import { startBuild } from './workers/build.js';
|
|
87
|
-
import {
|
|
88
|
-
import {
|
|
89
|
-
import { startReview } from './workers/review.js';
|
|
94
|
+
import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, prepareLinkedPlanningResearch, startPlan, stopAllPlanAgents, } from './workers/plan.js';
|
|
95
|
+
import { EXECUTION_RUN_KINDS, validateWorkflowContext } from './lib/types.js';
|
|
90
96
|
const getErrorMessage = (error) => {
|
|
91
97
|
return error instanceof Error ? error.message : String(error);
|
|
92
98
|
};
|
|
@@ -134,6 +140,87 @@ const parseMultiValueQuery = (value, allowedValues, fieldName) => {
|
|
|
134
140
|
values: entries.filter((entry, index, values) => values.indexOf(entry) === index),
|
|
135
141
|
};
|
|
136
142
|
};
|
|
143
|
+
const EXECUTION_RUN_STATUSES = ['queued', 'running', 'waiting', 'completed', 'failed', 'cancelled', 'interrupted'];
|
|
144
|
+
const parseLifecycleListQuery = (query) => {
|
|
145
|
+
const kind = parseMultiValueQuery(query.kind, EXECUTION_RUN_KINDS, 'kind');
|
|
146
|
+
if ('error' in kind) {
|
|
147
|
+
return kind;
|
|
148
|
+
}
|
|
149
|
+
if (kind.values.length > 1) {
|
|
150
|
+
return { error: 'kind must include at most one value' };
|
|
151
|
+
}
|
|
152
|
+
const status = parseMultiValueQuery(query.status, EXECUTION_RUN_STATUSES, 'status');
|
|
153
|
+
if ('error' in status) {
|
|
154
|
+
return status;
|
|
155
|
+
}
|
|
156
|
+
if (query.cursor != null && typeof query.cursor !== 'string') {
|
|
157
|
+
return { error: 'cursor must be a string' };
|
|
158
|
+
}
|
|
159
|
+
const limit = query.limit == null || query.limit === '' ? 50 : Number(query.limit);
|
|
160
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
|
|
161
|
+
return { error: 'limit must be an integer between 1 and 200' };
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
kind: kind.values[0],
|
|
165
|
+
statuses: status.values,
|
|
166
|
+
cursor: typeof query.cursor === 'string' && query.cursor !== '' ? query.cursor : null,
|
|
167
|
+
limit,
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
const serializeTestExecutionDetail = (details, retryEligibility) => {
|
|
171
|
+
if (!details) {
|
|
172
|
+
return { kind: 'test', available: false, retry_eligibility: retryEligibility };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
kind: 'test',
|
|
176
|
+
available: true,
|
|
177
|
+
command_id: details.command_id,
|
|
178
|
+
command_name: details.command_name,
|
|
179
|
+
command_runner: details.command_runner,
|
|
180
|
+
target_kind: details.target_kind,
|
|
181
|
+
target_bundle_id: details.target_bundle_id,
|
|
182
|
+
target_label: details.target_label,
|
|
183
|
+
ticket_id: details.ticket_id,
|
|
184
|
+
selection: details.selection,
|
|
185
|
+
summary: details.summary,
|
|
186
|
+
exit_code: details.exit_code,
|
|
187
|
+
duration_ms: details.duration_ms,
|
|
188
|
+
output: details.output,
|
|
189
|
+
output_truncated: details.output_truncated,
|
|
190
|
+
output_retention_state: details.output_retention_state,
|
|
191
|
+
output_expired_at: details.output_expired_at,
|
|
192
|
+
output_byte_count: details.output_byte_count,
|
|
193
|
+
retry_eligibility: retryEligibility,
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const serializeGitHubActionsExecutionDetail = (details) => {
|
|
197
|
+
if (!details) {
|
|
198
|
+
return { kind: 'github_actions', available: false };
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
kind: 'github_actions',
|
|
202
|
+
available: true,
|
|
203
|
+
workflow_id: details.workflow_id,
|
|
204
|
+
workflow_name: details.workflow_name,
|
|
205
|
+
ref: details.ref,
|
|
206
|
+
input_names: details.input_names,
|
|
207
|
+
provider_run_id: details.provider_run_id,
|
|
208
|
+
provider_status: details.provider_status,
|
|
209
|
+
conclusion: details.conclusion,
|
|
210
|
+
html_url: details.html_url,
|
|
211
|
+
head_sha: details.head_sha,
|
|
212
|
+
head_branch: details.head_branch,
|
|
213
|
+
provider_created_at: details.provider_created_at,
|
|
214
|
+
provider_updated_at: details.provider_updated_at,
|
|
215
|
+
provider_started_at: details.provider_started_at,
|
|
216
|
+
dispatched_at: details.dispatched_at,
|
|
217
|
+
last_synced_at: details.last_synced_at,
|
|
218
|
+
jobs: details.jobs,
|
|
219
|
+
jobs_retention_state: details.jobs_retention_state,
|
|
220
|
+
jobs_expired_at: details.jobs_expired_at,
|
|
221
|
+
jobs_count: details.jobs_count,
|
|
222
|
+
};
|
|
223
|
+
};
|
|
137
224
|
const formatWorktreeDeleteError = (target, branch, error) => {
|
|
138
225
|
return `Unable to delete the ${target} worktree for branch "${branch}": ${getErrorMessage(error)}`;
|
|
139
226
|
};
|
|
@@ -183,21 +270,13 @@ const getBundlePlanBlockerError = () => {
|
|
|
183
270
|
return 'AI planning is blocked because this bundle worktree is currently being modified.';
|
|
184
271
|
};
|
|
185
272
|
const deleteTicketRecord = async (ticket) => {
|
|
186
|
-
if (ticket.agent_status === 'running'
|
|
273
|
+
if (ticket.agent_status === 'running'
|
|
274
|
+
|| (ticket.project_id && hasActiveBuildExecutionRunForTicket(ticket.project_id, ticket.id))) {
|
|
187
275
|
return { ok: false, status: 409, error: 'Cannot delete a ticket while the agent is running' };
|
|
188
276
|
}
|
|
189
277
|
if (isRunning(ticket)) {
|
|
190
278
|
await stopTicket(ticket);
|
|
191
279
|
}
|
|
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
280
|
const branch = resolveTicketBranch(ticket);
|
|
202
281
|
if (!isBundledTicket(ticket) && branch) {
|
|
203
282
|
const project = getEffectiveProject(ticket);
|
|
@@ -216,7 +295,24 @@ const deleteTicketRecord = async (ticket) => {
|
|
|
216
295
|
}
|
|
217
296
|
const bundledProject = isBundledTicket(ticket) ? getEffectiveProject(ticket) : null;
|
|
218
297
|
const deletedBundleId = ticket.worktree_bundle_id;
|
|
298
|
+
let nextBuildTicketId = null;
|
|
299
|
+
let nextBuildSequenceId = null;
|
|
300
|
+
let affectedTicketIds = [];
|
|
301
|
+
if (ticket.state === 'build') {
|
|
302
|
+
const sequenceRemoval = removeTicketFromBuildSequence(ticket.id);
|
|
303
|
+
if (sequenceRemoval.ok) {
|
|
304
|
+
nextBuildTicketId = sequenceRemoval.nextTicketId;
|
|
305
|
+
nextBuildSequenceId = sequenceRemoval.sequenceId;
|
|
306
|
+
affectedTicketIds = sequenceRemoval.affectedTicketIds;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
219
309
|
db.prepare('DELETE FROM tickets WHERE id = ?').run(ticket.id);
|
|
310
|
+
try {
|
|
311
|
+
clearTicketStageSnapshots(ticket.id);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
console.warn(`[ticket-delete] Unable to remove stage snapshots for ticket ${ticket.id}`, error);
|
|
315
|
+
}
|
|
220
316
|
if (bundledProject && deletedBundleId) {
|
|
221
317
|
try {
|
|
222
318
|
cleanupBundleIfEmpty(deletedBundleId, bundledProject);
|
|
@@ -230,7 +326,7 @@ const deleteTicketRecord = async (ticket) => {
|
|
|
230
326
|
}
|
|
231
327
|
}
|
|
232
328
|
io.emit('ticket:deleted', { id: ticket.id });
|
|
233
|
-
return { ok: true, nextBuildTicketId, affectedTicketIds };
|
|
329
|
+
return { ok: true, nextBuildTicketId, nextBuildSequenceId, affectedTicketIds };
|
|
234
330
|
};
|
|
235
331
|
const emitUpdatedTickets = (ticketIds) => {
|
|
236
332
|
for (const ticketId of ticketIds) {
|
|
@@ -240,6 +336,41 @@ const emitUpdatedTickets = (ticketIds) => {
|
|
|
240
336
|
}
|
|
241
337
|
}
|
|
242
338
|
};
|
|
339
|
+
const scheduleBuildAfterTicketDeletion = (result) => {
|
|
340
|
+
if (!result.nextBuildTicketId || !result.nextBuildSequenceId) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const nextTicket = getTicket(result.nextBuildTicketId);
|
|
344
|
+
if (!nextTicket) {
|
|
345
|
+
stopBuildSequence(result.nextBuildTicketId);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const scheduling = startBuild(nextTicket, io, {
|
|
349
|
+
sequenceId: result.nextBuildSequenceId,
|
|
350
|
+
initiation: { initiator: 'automation' },
|
|
351
|
+
accept: () => {
|
|
352
|
+
const transition = prepareTicketForBuild(nextTicket.id);
|
|
353
|
+
if (!transition.ok) {
|
|
354
|
+
throw new Error(transition.error);
|
|
355
|
+
}
|
|
356
|
+
activateQueuedBuildSequenceTicket(result.nextBuildSequenceId, transition.ticket.id);
|
|
357
|
+
return {
|
|
358
|
+
ticket: transition.ticket,
|
|
359
|
+
sequenceId: result.nextBuildSequenceId,
|
|
360
|
+
compensate: () => {
|
|
361
|
+
db.prepare(`
|
|
362
|
+
UPDATE build_sequence_tickets SET status = 'queued', started_at = NULL
|
|
363
|
+
WHERE build_sequence_id = ? AND ticket_id = ? AND status = 'active'
|
|
364
|
+
`).run(result.nextBuildSequenceId, transition.ticket.id);
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
if (!scheduling.accepted) {
|
|
370
|
+
const stopped = stopBuildSequence(nextTicket.id);
|
|
371
|
+
emitUpdatedTickets(stopped.affectedTicketIds);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
243
374
|
const CORS_ORIGINS = parseCorsOrigins(process.env.CORS_ORIGIN);
|
|
244
375
|
const CORS_ORIGIN_RESOLVER = createCorsOriginResolver(CORS_ORIGINS);
|
|
245
376
|
const app = express();
|
|
@@ -523,16 +654,15 @@ const asProjectPayload = (body) => {
|
|
|
523
654
|
const isWorktreeReady = (branch, project) => {
|
|
524
655
|
return existsSync(resolveWorktreePath(branch, project));
|
|
525
656
|
};
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
stopAllActiveBuildSequences();
|
|
657
|
+
try {
|
|
658
|
+
const reconciliation = reconcileStageSnapshots();
|
|
659
|
+
if (reconciliation.removedRecords > 0 || reconciliation.removedDirectories > 0) {
|
|
660
|
+
console.info('[stage-snapshots] Reconciled stale undo data', reconciliation);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
console.warn('[stage-snapshots] Unable to reconcile stale undo data', error);
|
|
665
|
+
}
|
|
536
666
|
const getRequestedProjectId = (req) => {
|
|
537
667
|
if (typeof req.query.project_id === 'string' && req.query.project_id !== '') {
|
|
538
668
|
return req.query.project_id;
|
|
@@ -586,6 +716,9 @@ const resolveTicketDiffWorkspace = (ticket) => {
|
|
|
586
716
|
return { error: 'Ticket project no longer exists', status: 400 };
|
|
587
717
|
}
|
|
588
718
|
const bundle = ticket.worktree_bundle_id ? getBundle(ticket.worktree_bundle_id, project.id) : null;
|
|
719
|
+
if (ticket.worktree_bundle_id && !bundle) {
|
|
720
|
+
return { error: 'Selected bundle target no longer exists', status: 409 };
|
|
721
|
+
}
|
|
589
722
|
if (bundle && isProjectRootBundle(bundle)) {
|
|
590
723
|
const branchState = getProjectBranch(project, { includeDiffStats: false });
|
|
591
724
|
if (branchState.status !== 'ready' || !branchState.branch) {
|
|
@@ -598,7 +731,7 @@ const resolveTicketDiffWorkspace = (ticket) => {
|
|
|
598
731
|
kind: 'project_root',
|
|
599
732
|
};
|
|
600
733
|
}
|
|
601
|
-
const branch = resolveTicketBranch(ticket);
|
|
734
|
+
const branch = bundle?.branch ?? resolveTicketBranch(ticket);
|
|
602
735
|
if (!branch || !isWorktreeReady(branch, project)) {
|
|
603
736
|
return { error: 'Worktree not ready yet', status: 400 };
|
|
604
737
|
}
|
|
@@ -620,6 +753,9 @@ const resolveTicketDiffWorkspaceFromContext = (ticket) => {
|
|
|
620
753
|
if (!project) {
|
|
621
754
|
return { error: 'Ticket project no longer exists', status: 400 };
|
|
622
755
|
}
|
|
756
|
+
if (ticket.worktree_bundle_id && !ticket.bundle_kind) {
|
|
757
|
+
return { error: 'Selected bundle target no longer exists', status: 409 };
|
|
758
|
+
}
|
|
623
759
|
if (ticket.bundle_kind === 'project_root') {
|
|
624
760
|
const branchState = getProjectBranch(project, { includeDiffStats: false });
|
|
625
761
|
if (branchState.status !== 'ready' || !branchState.branch) {
|
|
@@ -632,7 +768,7 @@ const resolveTicketDiffWorkspaceFromContext = (ticket) => {
|
|
|
632
768
|
kind: 'project_root',
|
|
633
769
|
};
|
|
634
770
|
}
|
|
635
|
-
const branch =
|
|
771
|
+
const branch = ticket.worktree_bundle_id ? ticket.bundle_branch : ticket.branch;
|
|
636
772
|
if (!branch || !isWorktreeReady(branch, project)) {
|
|
637
773
|
return { error: 'Worktree not ready yet', status: 400 };
|
|
638
774
|
}
|
|
@@ -811,6 +947,63 @@ const parseTestTargetSelection = (value) => {
|
|
|
811
947
|
}
|
|
812
948
|
return null;
|
|
813
949
|
};
|
|
950
|
+
const unavailableTestRetry = (reason, status, message) => ({
|
|
951
|
+
eligibility: { eligible: false, reason },
|
|
952
|
+
error: { status, message },
|
|
953
|
+
});
|
|
954
|
+
const resolveTestRetry = (project, executionRun, details) => {
|
|
955
|
+
if (!executionRun || executionRun.kind !== 'test' || !details) {
|
|
956
|
+
return unavailableTestRetry('details_unavailable', 404, 'Test execution run not found');
|
|
957
|
+
}
|
|
958
|
+
if (!['failed', 'cancelled', 'interrupted'].includes(executionRun.status)) {
|
|
959
|
+
return unavailableTestRetry('run_not_terminal', 409, 'Only failed, cancelled, or interrupted test runs can be retried.');
|
|
960
|
+
}
|
|
961
|
+
if (hasDirectRetryChild(executionRun.id)) {
|
|
962
|
+
return unavailableTestRetry('retry_already_created', 409, 'A retry has already been created for this test run.');
|
|
963
|
+
}
|
|
964
|
+
if (!details.command_id) {
|
|
965
|
+
return unavailableTestRetry('command_unidentifiable', 409, 'The original test command is no longer identifiable.');
|
|
966
|
+
}
|
|
967
|
+
const command = findProjectTestCommand(project, details.command_id);
|
|
968
|
+
if (!command) {
|
|
969
|
+
return unavailableTestRetry('command_missing', 404, 'Test command not found');
|
|
970
|
+
}
|
|
971
|
+
const selection = parseTestTargetSelection(details.selection);
|
|
972
|
+
if (!selection) {
|
|
973
|
+
return unavailableTestRetry('selection_invalid', 409, 'The original test selection is no longer valid.');
|
|
974
|
+
}
|
|
975
|
+
let workspace;
|
|
976
|
+
if (details.ticket_id) {
|
|
977
|
+
const ticket = getTicket(details.ticket_id);
|
|
978
|
+
if (!ticket || ticket.project_id !== project.id) {
|
|
979
|
+
return unavailableTestRetry('target_missing', 404, 'Ticket not found');
|
|
980
|
+
}
|
|
981
|
+
workspace = resolveTicketTestWorkspace(ticket);
|
|
982
|
+
}
|
|
983
|
+
else if (details.target_kind === 'repo_root' || details.target_kind === 'bundle') {
|
|
984
|
+
workspace = resolveExplicitProjectTestWorkspace(project, details.target_kind, details.target_bundle_id);
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
return unavailableTestRetry('target_invalid', 409, 'The original test target is no longer valid.');
|
|
988
|
+
}
|
|
989
|
+
if ('error' in workspace) {
|
|
990
|
+
return unavailableTestRetry('target_unavailable', workspace.status, workspace.error);
|
|
991
|
+
}
|
|
992
|
+
if (!isTestSelectionSupported(command, selection, workspace.cwd)) {
|
|
993
|
+
return unavailableTestRetry('selection_unsupported', 409, 'The selected test command only supports running the full suite.');
|
|
994
|
+
}
|
|
995
|
+
if (selection.kind !== 'all' && !doesTestFileExist(workspace.cwd, selection.path, command)) {
|
|
996
|
+
return unavailableTestRetry('test_file_missing', 404, 'Selected test file no longer exists in this target workspace.');
|
|
997
|
+
}
|
|
998
|
+
return {
|
|
999
|
+
eligibility: { eligible: true, reason: null },
|
|
1000
|
+
details,
|
|
1001
|
+
executionRun,
|
|
1002
|
+
command,
|
|
1003
|
+
selection,
|
|
1004
|
+
workspace,
|
|
1005
|
+
};
|
|
1006
|
+
};
|
|
814
1007
|
const resolveProjectFileSuggestionWorkspace = (project, options = {}) => {
|
|
815
1008
|
const ticketId = options.ticketId?.trim() || null;
|
|
816
1009
|
if (ticketId) {
|
|
@@ -1421,25 +1614,15 @@ const submitBundleFollowUpResponse = (projectId, bundleId, payload, broadcaster)
|
|
|
1421
1614
|
if (!targetTicket) {
|
|
1422
1615
|
return { ok: false, status: 409, error: 'No non-plan tickets are available for follow-up in this bundle' };
|
|
1423
1616
|
}
|
|
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({
|
|
1617
|
+
const submitted = submitTicketFollowUp({
|
|
1437
1618
|
ticketId: targetTicket.id,
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
origin: payload?.origin === 'changes' ? 'changes' : null,
|
|
1619
|
+
requestedProjectId: project.id,
|
|
1620
|
+
payload: { ...payload, instruction, mode },
|
|
1621
|
+
broadcaster: broadcaster,
|
|
1442
1622
|
});
|
|
1623
|
+
if (!submitted.ok) {
|
|
1624
|
+
return submitted;
|
|
1625
|
+
}
|
|
1443
1626
|
return {
|
|
1444
1627
|
ok: true,
|
|
1445
1628
|
status: 200,
|
|
@@ -1462,8 +1645,11 @@ const rerunTicketReview = (ticketId) => {
|
|
|
1462
1645
|
if (!ticket.can_re_review) {
|
|
1463
1646
|
return { ok: false, status: 409, error: 'Review can only be re-run after a follow-up finishes' };
|
|
1464
1647
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1648
|
+
const scheduling = rerunTicketReviewOperation(ticket, io);
|
|
1649
|
+
if (!scheduling.ok) {
|
|
1650
|
+
return { ok: false, status: scheduling.status, error: scheduling.error };
|
|
1651
|
+
}
|
|
1652
|
+
return { ok: true, ticket: scheduling.value ?? ticket };
|
|
1467
1653
|
};
|
|
1468
1654
|
const reviewBundleTickets = (projectId, bundleId) => {
|
|
1469
1655
|
const project = getProjectById(projectId);
|
|
@@ -1534,19 +1720,81 @@ const buildBundleTickets = (projectId, bundleId, broadcaster) => {
|
|
|
1534
1720
|
}
|
|
1535
1721
|
}
|
|
1536
1722
|
const movedTickets = [];
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1723
|
+
const ticketSnapshots = orderedPlanTickets.map(ticket => db.prepare(`
|
|
1724
|
+
SELECT id, project_id, state, tool, agent_status, agent_log, streaming_response, branch,
|
|
1725
|
+
build_completed_at, run_setup_status, auto_park_dismissed_at, planning_context_json, lane_order
|
|
1726
|
+
FROM tickets
|
|
1727
|
+
WHERE id = ? AND project_id = ?
|
|
1728
|
+
`).get(ticket.id, project.id));
|
|
1729
|
+
const firstPlanTicket = orderedPlanTickets[0];
|
|
1730
|
+
if (!firstPlanTicket) {
|
|
1731
|
+
return { ok: false, status: 409, error: 'No plan tickets are available to build in this bundle' };
|
|
1732
|
+
}
|
|
1733
|
+
const acceptBundleBuild = () => {
|
|
1734
|
+
let activeSequenceId = null;
|
|
1735
|
+
for (const [index, planTicket] of orderedPlanTickets.entries()) {
|
|
1736
|
+
const buildTransition = prepareTicketForBuild(planTicket.id, {
|
|
1737
|
+
ignoredBlockerTicketIds: bundleTicketIds,
|
|
1738
|
+
});
|
|
1739
|
+
if (!buildTransition.ok) {
|
|
1740
|
+
throw new Error(buildTransition.error);
|
|
1741
|
+
}
|
|
1742
|
+
movedTickets.push(buildTransition.ticket);
|
|
1743
|
+
const queuedBuild = enqueueBundledBuild({
|
|
1744
|
+
ticketId: buildTransition.ticket.id,
|
|
1745
|
+
projectId: project.id,
|
|
1746
|
+
worktreeBundleId: bundle.id,
|
|
1747
|
+
activate: index === 0,
|
|
1748
|
+
});
|
|
1749
|
+
if (index === 0) {
|
|
1750
|
+
activeSequenceId = queuedBuild.sequenceId;
|
|
1751
|
+
}
|
|
1548
1752
|
}
|
|
1549
|
-
|
|
1753
|
+
return {
|
|
1754
|
+
ticket: movedTickets[0],
|
|
1755
|
+
sequenceId: activeSequenceId,
|
|
1756
|
+
...(activeSequenceId
|
|
1757
|
+
? {
|
|
1758
|
+
compensate: () => {
|
|
1759
|
+
compensateBuildSequenceAcceptance(activeSequenceId, movedTickets.map(ticket => ticket.id));
|
|
1760
|
+
for (const snapshot of ticketSnapshots) {
|
|
1761
|
+
db.prepare(`
|
|
1762
|
+
UPDATE tickets
|
|
1763
|
+
SET state = ?, tool = ?, agent_status = ?, agent_log = ?, streaming_response = ?, branch = ?,
|
|
1764
|
+
build_completed_at = ?, run_setup_status = ?, auto_park_dismissed_at = ?,
|
|
1765
|
+
planning_context_json = ?, lane_order = ?, updated_at = CURRENT_TIMESTAMP
|
|
1766
|
+
WHERE id = ? AND project_id = ?
|
|
1767
|
+
`).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);
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
}
|
|
1771
|
+
: {}),
|
|
1772
|
+
};
|
|
1773
|
+
};
|
|
1774
|
+
if (hasActiveSequenceForBundle(project.id, bundle.id)) {
|
|
1775
|
+
try {
|
|
1776
|
+
db.transaction(acceptBundleBuild)();
|
|
1777
|
+
}
|
|
1778
|
+
catch (error) {
|
|
1779
|
+
return { ok: false, status: 409, error: error instanceof Error ? error.message : String(error) };
|
|
1780
|
+
}
|
|
1781
|
+
emitUpdatedTickets(movedTickets.map(ticket => ticket.id));
|
|
1782
|
+
return {
|
|
1783
|
+
ok: true,
|
|
1784
|
+
status: 200,
|
|
1785
|
+
value: serializeBundleDetails(project, bundle, { target_ticket_id: movedTickets[0]?.id ?? null }),
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
const scheduling = startBuild(firstPlanTicket, broadcaster, { accept: acceptBundleBuild });
|
|
1789
|
+
if (scheduling === undefined) {
|
|
1790
|
+
acceptBundleBuild();
|
|
1791
|
+
}
|
|
1792
|
+
else if (!scheduling.accepted) {
|
|
1793
|
+
return {
|
|
1794
|
+
ok: false,
|
|
1795
|
+
status: scheduling.reason === 'queue_failed' ? 503 : 409,
|
|
1796
|
+
error: scheduling.error,
|
|
1797
|
+
};
|
|
1550
1798
|
}
|
|
1551
1799
|
emitUpdatedTickets(movedTickets.map(ticket => ticket.id));
|
|
1552
1800
|
const details = serializeBundleDetails(project, bundle, {
|
|
@@ -1556,22 +1804,6 @@ const buildBundleTickets = (projectId, bundleId, broadcaster) => {
|
|
|
1556
1804
|
ok: true,
|
|
1557
1805
|
status: 200,
|
|
1558
1806
|
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
1807
|
};
|
|
1576
1808
|
};
|
|
1577
1809
|
const getCurrentReviewWorkspaceSignature = (ticket) => {
|
|
@@ -1912,14 +2144,14 @@ const validateExpandedPlanningAgentDraft = (outline, draft) => {
|
|
|
1912
2144
|
}
|
|
1913
2145
|
return draft;
|
|
1914
2146
|
};
|
|
1915
|
-
const createPlanningAgentTickets = db.transaction(({ draft, initiativePrompt, projectId, model, variant, skills,
|
|
2147
|
+
const createPlanningAgentTickets = db.transaction(({ draft, initiativePrompt, projectId, model, variant, skills, worktreeBundleId, }) => {
|
|
1916
2148
|
const { createdTickets, createdDependencies, ticketIdsByKey } = materializeTicketsWithDependencies({
|
|
1917
2149
|
options: {
|
|
1918
2150
|
projectId,
|
|
1919
2151
|
model,
|
|
1920
2152
|
variant,
|
|
1921
2153
|
skills,
|
|
1922
|
-
tool:
|
|
2154
|
+
tool: null,
|
|
1923
2155
|
worktreeBundleId,
|
|
1924
2156
|
agentStatus: 'done',
|
|
1925
2157
|
},
|
|
@@ -2761,6 +2993,7 @@ app.post('/projects/:id/spec-sessions/:sessionId/snapshots/:snapshotId/review/st
|
|
|
2761
2993
|
projectId: req.params.id,
|
|
2762
2994
|
specSessionId: req.params.sessionId,
|
|
2763
2995
|
snapshotId: req.params.snapshotId,
|
|
2996
|
+
broadcaster: io,
|
|
2764
2997
|
});
|
|
2765
2998
|
if (!result.ok) {
|
|
2766
2999
|
return res.status(result.status).json({ error: result.error });
|
|
@@ -3470,7 +3703,6 @@ app.post('/bundles/:id/build', (req, res) => {
|
|
|
3470
3703
|
});
|
|
3471
3704
|
}
|
|
3472
3705
|
res.status(buildResult.status).json(buildResult.value);
|
|
3473
|
-
buildResult.afterResponse?.();
|
|
3474
3706
|
});
|
|
3475
3707
|
app.post('/bundles/:id/review', (req, res) => {
|
|
3476
3708
|
const result = getRequiredProject(req);
|
|
@@ -3512,33 +3744,51 @@ app.post('/bundles/:id/build-open-questions/resolve', (req, res) => {
|
|
|
3512
3744
|
if (resolutions.some(resolution => resolution === null || !resolution.questionId)) {
|
|
3513
3745
|
return res.status(400).json({ error: 'Each resolution requires a question_id' });
|
|
3514
3746
|
}
|
|
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({
|
|
3747
|
+
const accepted = acceptBuildOpenQuestionResolution({
|
|
3521
3748
|
projectId: result.project.id,
|
|
3522
3749
|
bundleId: bundle.id,
|
|
3523
3750
|
resolutions: resolutions.filter((resolution) => resolution !== null),
|
|
3751
|
+
broadcaster: io,
|
|
3524
3752
|
});
|
|
3525
|
-
if (!
|
|
3526
|
-
return res.status(
|
|
3527
|
-
error:
|
|
3528
|
-
...(
|
|
3753
|
+
if (!accepted.ok) {
|
|
3754
|
+
return res.status(accepted.status).json({
|
|
3755
|
+
error: accepted.error,
|
|
3756
|
+
...(accepted.code ? { code: accepted.code } : {}),
|
|
3529
3757
|
});
|
|
3530
3758
|
}
|
|
3531
|
-
|
|
3532
|
-
if (answeredQuestions.length === 0) {
|
|
3533
|
-
clearAutomationPause(result.project.id, bundle.id);
|
|
3759
|
+
if (accepted.kind === 'dismissed') {
|
|
3534
3760
|
const automationConfig = resolveAutomationConfig(bundle, result.project);
|
|
3535
3761
|
const reviewResult = depthAtLeast(automationConfig.depth, 'auto_review')
|
|
3536
|
-
? transitionBundleToReview(result.project.id, bundle.id, io, {
|
|
3762
|
+
? transitionBundleToReview(result.project.id, bundle.id, io, {
|
|
3763
|
+
automatic: true,
|
|
3764
|
+
initiation: {
|
|
3765
|
+
causalParentRunId: accepted.progressionRunId,
|
|
3766
|
+
workflowContext: validateWorkflowContext({ build_open_question_batch_id: accepted.batchId }),
|
|
3767
|
+
},
|
|
3768
|
+
})
|
|
3537
3769
|
: null;
|
|
3770
|
+
if (!reviewResult) {
|
|
3771
|
+
completeAutomationProgression({
|
|
3772
|
+
projectId: result.project.id,
|
|
3773
|
+
bundleId: bundle.id,
|
|
3774
|
+
progressionRunId: accepted.progressionRunId,
|
|
3775
|
+
broadcaster: io,
|
|
3776
|
+
});
|
|
3777
|
+
}
|
|
3538
3778
|
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
3539
3779
|
io.emit('ticket:updated', ticket);
|
|
3540
3780
|
}
|
|
3541
3781
|
if (reviewResult && !reviewResult.ok) {
|
|
3782
|
+
waitAutomationProgression({
|
|
3783
|
+
projectId: result.project.id,
|
|
3784
|
+
bundleId: bundle.id,
|
|
3785
|
+
progressionRunId: accepted.progressionRunId,
|
|
3786
|
+
pauseCode: 'review',
|
|
3787
|
+
pauseReason: reviewResult.error,
|
|
3788
|
+
reasonCode: 'stage_failed',
|
|
3789
|
+
pausedStageRunId: null,
|
|
3790
|
+
broadcaster: io,
|
|
3791
|
+
});
|
|
3542
3792
|
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
3543
3793
|
}
|
|
3544
3794
|
return res.json(serializeBundleDetails(result.project, bundle, {
|
|
@@ -3546,34 +3796,11 @@ app.post('/bundles/:id/build-open-questions/resolve', (req, res) => {
|
|
|
3546
3796
|
target_ticket_id: reviewResult?.value?.id,
|
|
3547
3797
|
}));
|
|
3548
3798
|
}
|
|
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.',
|
|
3558
|
-
formatBuildOpenQuestionAnswersPrompt(answeredQuestions),
|
|
3559
|
-
].join('\n\n');
|
|
3560
|
-
setBuildOpenQuestionBatchStatus(created.batch.id, 'running');
|
|
3561
|
-
const started = startFollowUp(targetTicket, io, instruction, 'build', {
|
|
3562
|
-
buildOpenQuestionBatchId: created.batch.id,
|
|
3563
|
-
});
|
|
3564
|
-
if (!started) {
|
|
3565
|
-
failBuildOpenQuestionBatch(created.batch.id, 'Build follow-up could not be started');
|
|
3566
|
-
recordAutomationPause(result.project.id, bundle.id, 'open_questions', 'Build open question follow-up could not be started');
|
|
3567
|
-
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
3568
|
-
io.emit('ticket:updated', ticket);
|
|
3569
|
-
}
|
|
3570
|
-
return res.status(409).json({ error: 'Build follow-up could not be started' });
|
|
3571
|
-
}
|
|
3572
3799
|
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
3573
3800
|
io.emit('ticket:updated', ticket);
|
|
3574
3801
|
}
|
|
3575
|
-
return res.status(
|
|
3576
|
-
target_ticket_id: targetTicket.id,
|
|
3802
|
+
return res.status(accepted.status).json(serializeBundleDetails(result.project, bundle, {
|
|
3803
|
+
target_ticket_id: accepted.targetTicket.id,
|
|
3577
3804
|
}));
|
|
3578
3805
|
});
|
|
3579
3806
|
app.post('/bundles/:id/undo-review', (req, res) => {
|
|
@@ -3651,6 +3878,12 @@ app.post('/bundles/:id/stop', async (req, res) => {
|
|
|
3651
3878
|
if (!targetTicket) {
|
|
3652
3879
|
return res.status(404).json({ error: 'Bundle tickets not found' });
|
|
3653
3880
|
}
|
|
3881
|
+
await stopAutomationProgression({
|
|
3882
|
+
projectId: result.project.id,
|
|
3883
|
+
bundleId: bundle.id,
|
|
3884
|
+
broadcaster: io,
|
|
3885
|
+
stopLocalWorker: worker => (worker.kind === 'review' ? stopReviewAgent(worker.ticket_id) : stopBuildAgent(worker.ticket_id)),
|
|
3886
|
+
});
|
|
3654
3887
|
await stopTicket(targetTicket);
|
|
3655
3888
|
emitRunStateForContext(targetTicket, false);
|
|
3656
3889
|
return res.json(serializeBundleDetails(result.project, bundle));
|
|
@@ -3666,6 +3899,27 @@ app.post('/bundles/:id/follow-up', (req, res) => {
|
|
|
3666
3899
|
}
|
|
3667
3900
|
return res.status(response.status).json(response.value);
|
|
3668
3901
|
});
|
|
3902
|
+
const AUTOMATION_SIDE_EFFECT_KINDS = new Set(['automation_commit', 'automation_push', 'automation_pull_request']);
|
|
3903
|
+
const AUTOMATION_WORKER_PAUSE_KINDS = new Set(['build', 'review', 'build_follow_up', 'automation_review_fix']);
|
|
3904
|
+
const pausedRunBelongsToProgression = (projectId, bundleId, progressionRunId, pausedRunId) => {
|
|
3905
|
+
const pausedRun = getExecutionRun(projectId, pausedRunId);
|
|
3906
|
+
const progression = getExecutionRun(projectId, progressionRunId);
|
|
3907
|
+
if (!pausedRun || !progression || progression.kind !== 'automation_progression') {
|
|
3908
|
+
return false;
|
|
3909
|
+
}
|
|
3910
|
+
const ownsBundle = listExecutionRunLinks(projectId, pausedRun.id).some(link => (link.entity_type === 'bundle'
|
|
3911
|
+
&& link.entity_id === bundleId
|
|
3912
|
+
&& (link.relationship === 'primary' || link.relationship === 'context')));
|
|
3913
|
+
if (!ownsBundle) {
|
|
3914
|
+
return false;
|
|
3915
|
+
}
|
|
3916
|
+
if (pausedRun.parent_run_id === progressionRunId) {
|
|
3917
|
+
return true;
|
|
3918
|
+
}
|
|
3919
|
+
const parent = pausedRun.parent_run_id ? getExecutionRun(projectId, pausedRun.parent_run_id) : null;
|
|
3920
|
+
return parent?.kind === 'automation_review_fix' && parent.parent_run_id === progressionRunId
|
|
3921
|
+
|| progression.parent_run_id === pausedRun.id;
|
|
3922
|
+
};
|
|
3669
3923
|
app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
3670
3924
|
const result = getRequiredProject(req);
|
|
3671
3925
|
if ('error' in result) {
|
|
@@ -3688,11 +3942,124 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3688
3942
|
&& listBuildOpenQuestionsForBundle(result.project.id, bundle.id).length > 0) {
|
|
3689
3943
|
return res.status(409).json({ error: 'Resolve or dismiss all Build open questions before resuming automation' });
|
|
3690
3944
|
}
|
|
3945
|
+
let progressionRunId = bundleState.automation_progression_run_id;
|
|
3946
|
+
if (!progressionRunId) {
|
|
3947
|
+
try {
|
|
3948
|
+
progressionRunId = (bundleState.automation_paused_stage_run_id
|
|
3949
|
+
? recoverStoppedSideEffectProgression({
|
|
3950
|
+
projectId: result.project.id,
|
|
3951
|
+
bundleId: bundle.id,
|
|
3952
|
+
broadcaster: io,
|
|
3953
|
+
})
|
|
3954
|
+
: recoverLegacyAutomationProgression({
|
|
3955
|
+
projectId: result.project.id,
|
|
3956
|
+
bundleId: bundle.id,
|
|
3957
|
+
broadcaster: io,
|
|
3958
|
+
})).runId;
|
|
3959
|
+
}
|
|
3960
|
+
catch (error) {
|
|
3961
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
try {
|
|
3965
|
+
if (getAutomationProgression(result.project.id, bundle.id)?.id !== progressionRunId) {
|
|
3966
|
+
return res.status(409).json({ error: 'Automation progression ownership is invalid' });
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
catch (error) {
|
|
3970
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
3971
|
+
}
|
|
3972
|
+
let sideEffectResume = null;
|
|
3973
|
+
let pausedRunKind = null;
|
|
3974
|
+
if (bundleState.automation_paused_stage_run_id) {
|
|
3975
|
+
const stageRunId = bundleState.automation_paused_stage_run_id;
|
|
3976
|
+
const stage = getExecutionRun(result.project.id, stageRunId);
|
|
3977
|
+
pausedRunKind = stage?.kind ?? null;
|
|
3978
|
+
if (!stage
|
|
3979
|
+
|| (!AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind) && !AUTOMATION_WORKER_PAUSE_KINDS.has(stage.kind))
|
|
3980
|
+
|| !pausedRunBelongsToProgression(result.project.id, bundle.id, progressionRunId, stageRunId)) {
|
|
3981
|
+
return res.status(409).json({ error: 'The paused automation stage does not belong to the active progression' });
|
|
3982
|
+
}
|
|
3983
|
+
if (AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind)) {
|
|
3984
|
+
let reconciled;
|
|
3985
|
+
try {
|
|
3986
|
+
reconciled = await reconcileAutomationStage({
|
|
3987
|
+
projectId: result.project.id,
|
|
3988
|
+
bundleId: bundle.id,
|
|
3989
|
+
progressionRunId,
|
|
3990
|
+
stageRunId,
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
3993
|
+
catch (error) {
|
|
3994
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
3995
|
+
}
|
|
3996
|
+
if (reconciled.domainStatus === 'ambiguous') {
|
|
3997
|
+
return res.status(409).json({ error: 'The paused automation side effect is ambiguous and cannot be resumed safely.' });
|
|
3998
|
+
}
|
|
3999
|
+
const details = getAutomationStageDetails(result.project.id, stage.id);
|
|
4000
|
+
if (!details || details.stage_kind !== stage.kind) {
|
|
4001
|
+
return res.status(409).json({ error: 'The paused automation side effect has no complete persisted identity.' });
|
|
4002
|
+
}
|
|
4003
|
+
if (reconciled.status === 'completed' && reconciled.domainStatus === 'reconciled_completed') {
|
|
4004
|
+
if (stage.kind === 'automation_commit') {
|
|
4005
|
+
const commitResult = details.result;
|
|
4006
|
+
const expectedHeadSha = commitResult && ('commitSha' in commitResult ? commitResult.commitSha : commitResult.headSha);
|
|
4007
|
+
if (!expectedHeadSha) {
|
|
4008
|
+
return res.status(409).json({ error: 'The reconciled commit has no persisted HEAD identity.' });
|
|
4009
|
+
}
|
|
4010
|
+
sideEffectResume = { stageKind: stage.kind, expectedHeadSha };
|
|
4011
|
+
}
|
|
4012
|
+
else if (stage.kind === 'automation_push') {
|
|
4013
|
+
const pushResult = details.result;
|
|
4014
|
+
if (!pushResult?.remoteSha) {
|
|
4015
|
+
return res.status(409).json({ error: 'The reconciled push has no persisted remote identity.' });
|
|
4016
|
+
}
|
|
4017
|
+
sideEffectResume = { stageKind: stage.kind, expectedHeadSha: pushResult.remoteSha };
|
|
4018
|
+
}
|
|
4019
|
+
else {
|
|
4020
|
+
sideEffectResume = { stageKind: stage.kind };
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
else if (reconciled.status === 'interrupted' && reconciled.domainStatus === 'reconciled_absent') {
|
|
4024
|
+
if (stage.kind === 'automation_push') {
|
|
4025
|
+
const pushIntent = details.intent;
|
|
4026
|
+
sideEffectResume = { stageKind: stage.kind, retryOfRunId: stage.id, expectedHeadSha: pushIntent.targetLocalSha };
|
|
4027
|
+
}
|
|
4028
|
+
else if (stage.kind === 'automation_pull_request') {
|
|
4029
|
+
const pullRequestIntent = details.intent;
|
|
4030
|
+
sideEffectResume = { stageKind: stage.kind, retryOfRunId: stage.id, expectedHeadSha: pullRequestIntent.headSha };
|
|
4031
|
+
}
|
|
4032
|
+
else {
|
|
4033
|
+
sideEffectResume = { stageKind: stage.kind, retryOfRunId: stage.id };
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
else {
|
|
4037
|
+
return res.status(409).json({ error: 'The paused automation side effect has no safely resumable outcome.' });
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
if (liveAutomationConfig.depth === 'manual') {
|
|
4042
|
+
completeAutomationProgression({
|
|
4043
|
+
projectId: result.project.id,
|
|
4044
|
+
bundleId: bundle.id,
|
|
4045
|
+
progressionRunId,
|
|
4046
|
+
domainStatus: 'manual_boundary_reached',
|
|
4047
|
+
broadcaster: io,
|
|
4048
|
+
});
|
|
4049
|
+
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
4050
|
+
io.emit('ticket:updated', ticket);
|
|
4051
|
+
}
|
|
4052
|
+
return res.json(serializeBundleDetails(result.project, bundle));
|
|
4053
|
+
}
|
|
3691
4054
|
const reviewOwnerSource = bundleState.automation_paused_code === 'open_questions'
|
|
3692
4055
|
? listBundleSliceTickets(result.project.id, bundle.id, 'build')
|
|
3693
4056
|
.find(ticket => ticket.agent_status === 'done') ?? null
|
|
3694
4057
|
: resolveBundleReviewOwner(result.project.id, bundle.id)
|
|
3695
4058
|
?? listBundleSliceTickets(result.project.id, bundle.id, 'review')[0]
|
|
4059
|
+
?? (bundleState.automation_paused_code === 'review' && (pausedRunKind === 'build' || pausedRunKind === 'build_follow_up')
|
|
4060
|
+
? listBundleSliceTickets(result.project.id, bundle.id, 'build')
|
|
4061
|
+
.find(ticket => ticket.agent_status === 'done') ?? null
|
|
4062
|
+
: null)
|
|
3696
4063
|
?? null;
|
|
3697
4064
|
if (!reviewOwnerSource) {
|
|
3698
4065
|
return res.status(409).json({ error: 'No review ticket to resume from' });
|
|
@@ -3706,6 +4073,49 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3706
4073
|
return res.status(400).json({ error: 'Ticket project no longer exists' });
|
|
3707
4074
|
}
|
|
3708
4075
|
const automationConfig = resolveAutomationConfig(owner.bundle ?? null, project);
|
|
4076
|
+
const restoreWaitingProgression = () => {
|
|
4077
|
+
waitAutomationProgression({
|
|
4078
|
+
projectId: result.project.id,
|
|
4079
|
+
bundleId: bundle.id,
|
|
4080
|
+
progressionRunId,
|
|
4081
|
+
pauseCode: bundleState.automation_paused_code ?? 'review',
|
|
4082
|
+
pauseReason: bundleState.automation_paused_reason ?? 'Automation paused',
|
|
4083
|
+
reasonCode: bundleState.automation_pause_reason_code ?? 'manual_transition',
|
|
4084
|
+
pausedStageRunId: bundleState.automation_paused_stage_run_id,
|
|
4085
|
+
broadcaster: io,
|
|
4086
|
+
});
|
|
4087
|
+
};
|
|
4088
|
+
const acceptResumedReview = () => {
|
|
4089
|
+
runAutomationProgression({
|
|
4090
|
+
projectId: result.project.id,
|
|
4091
|
+
bundleId: bundle.id,
|
|
4092
|
+
progressionRunId,
|
|
4093
|
+
broadcaster: io,
|
|
4094
|
+
});
|
|
4095
|
+
return {
|
|
4096
|
+
ticket: owner,
|
|
4097
|
+
compensate: restoreWaitingProgression,
|
|
4098
|
+
};
|
|
4099
|
+
};
|
|
4100
|
+
if (sideEffectResume) {
|
|
4101
|
+
try {
|
|
4102
|
+
runAutomationProgression({
|
|
4103
|
+
projectId: result.project.id,
|
|
4104
|
+
bundleId: bundle.id,
|
|
4105
|
+
progressionRunId,
|
|
4106
|
+
broadcaster: io,
|
|
4107
|
+
});
|
|
4108
|
+
await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io, progressionRunId, sideEffectResume);
|
|
4109
|
+
}
|
|
4110
|
+
catch (error) {
|
|
4111
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4112
|
+
}
|
|
4113
|
+
const resumedOwner = getTicket(owner.id);
|
|
4114
|
+
if (resumedOwner) {
|
|
4115
|
+
io.emit('ticket:updated', resumedOwner);
|
|
4116
|
+
}
|
|
4117
|
+
return res.json(serializeBundleDetails(result.project, bundle));
|
|
4118
|
+
}
|
|
3709
4119
|
if (owner.state === 'review' && owner.agent_status === 'running') {
|
|
3710
4120
|
return res.status(409).json({ error: 'Review already running' });
|
|
3711
4121
|
}
|
|
@@ -3722,24 +4132,131 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3722
4132
|
&& automationConfig.reviewFixEnabled
|
|
3723
4133
|
&& blockingCount > 0
|
|
3724
4134
|
&& iteration < automationConfig.reviewMaxIterations) {
|
|
3725
|
-
|
|
3726
|
-
|
|
4135
|
+
let reviewFixRunId = null;
|
|
4136
|
+
let followUpResult;
|
|
4137
|
+
const previousIteration = bundleState.review_iteration_count ?? 0;
|
|
4138
|
+
const previousFixLoopActive = bundleState.fix_loop_active;
|
|
4139
|
+
try {
|
|
4140
|
+
const progression = getAutomationProgression(result.project.id, bundle.id);
|
|
4141
|
+
if (!progression || progression.id !== progressionRunId) {
|
|
4142
|
+
return res.status(409).json({ error: 'Automation progression ownership is invalid' });
|
|
4143
|
+
}
|
|
4144
|
+
const workflowContext = validateWorkflowContext(progression.metadata);
|
|
4145
|
+
runAutomationProgression({
|
|
4146
|
+
projectId: result.project.id,
|
|
4147
|
+
bundleId: bundle.id,
|
|
4148
|
+
progressionRunId,
|
|
4149
|
+
broadcaster: io,
|
|
4150
|
+
});
|
|
4151
|
+
upsertBundleAutomationState(result.project.id, bundle.id, {
|
|
4152
|
+
review_iteration_count: iteration + 1,
|
|
4153
|
+
fix_loop_active: 1,
|
|
4154
|
+
});
|
|
4155
|
+
const reviewFix = createAutomationReviewFix({
|
|
4156
|
+
projectId: result.project.id,
|
|
4157
|
+
bundleId: bundle.id,
|
|
4158
|
+
ticketId: owner.id,
|
|
4159
|
+
ticketTitle: owner.title,
|
|
4160
|
+
progressionRunId,
|
|
4161
|
+
workflowContext,
|
|
4162
|
+
broadcaster: io,
|
|
4163
|
+
});
|
|
4164
|
+
reviewFixRunId = reviewFix.runId;
|
|
4165
|
+
followUpResult = submitTicketFollowUp({
|
|
4166
|
+
ticketId: owner.id,
|
|
4167
|
+
requestedProjectId: result.project.id,
|
|
4168
|
+
payload: {
|
|
4169
|
+
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.`,
|
|
4170
|
+
mode: 'build',
|
|
4171
|
+
},
|
|
4172
|
+
broadcaster: io,
|
|
4173
|
+
initiation: {
|
|
4174
|
+
initiator: 'automation',
|
|
4175
|
+
causalParentRunId: reviewFix.runId,
|
|
4176
|
+
workflowContext,
|
|
4177
|
+
},
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
catch (error) {
|
|
4181
|
+
upsertBundleAutomationState(result.project.id, bundle.id, {
|
|
4182
|
+
review_iteration_count: previousIteration,
|
|
4183
|
+
fix_loop_active: previousFixLoopActive,
|
|
4184
|
+
});
|
|
4185
|
+
try {
|
|
4186
|
+
if (reviewFixRunId) {
|
|
4187
|
+
finishAutomationReviewFix({
|
|
4188
|
+
projectId: result.project.id,
|
|
4189
|
+
reviewFixRunId,
|
|
4190
|
+
failedMessage: error instanceof Error ? error.message : String(error),
|
|
4191
|
+
broadcaster: io,
|
|
4192
|
+
});
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
finally {
|
|
4196
|
+
restoreWaitingProgression();
|
|
4197
|
+
}
|
|
4198
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4199
|
+
}
|
|
4200
|
+
if (!followUpResult.ok) {
|
|
4201
|
+
upsertBundleAutomationState(result.project.id, bundle.id, {
|
|
4202
|
+
review_iteration_count: previousIteration,
|
|
4203
|
+
fix_loop_active: previousFixLoopActive,
|
|
4204
|
+
});
|
|
4205
|
+
if (reviewFixRunId) {
|
|
4206
|
+
finishAutomationReviewFix({
|
|
4207
|
+
projectId: result.project.id,
|
|
4208
|
+
reviewFixRunId,
|
|
4209
|
+
failedMessage: followUpResult.error,
|
|
4210
|
+
broadcaster: io,
|
|
4211
|
+
});
|
|
4212
|
+
waitAutomationProgression({
|
|
4213
|
+
projectId: result.project.id,
|
|
4214
|
+
bundleId: bundle.id,
|
|
4215
|
+
progressionRunId,
|
|
4216
|
+
pauseCode: 'review',
|
|
4217
|
+
pauseReason: `fix follow-up could not start: ${followUpResult.error}`,
|
|
4218
|
+
reasonCode: 'stage_failed',
|
|
4219
|
+
pausedStageRunId: reviewFixRunId,
|
|
4220
|
+
broadcaster: io,
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
return res.status(followUpResult.status).json({ error: followUpResult.error });
|
|
4224
|
+
}
|
|
3727
4225
|
}
|
|
3728
4226
|
else if (depthAtLeast(automationConfig.depth, 'auto_commit')) {
|
|
3729
|
-
|
|
3730
|
-
|
|
4227
|
+
try {
|
|
4228
|
+
runAutomationProgression({
|
|
4229
|
+
projectId: result.project.id,
|
|
4230
|
+
bundleId: bundle.id,
|
|
4231
|
+
progressionRunId,
|
|
4232
|
+
broadcaster: io,
|
|
4233
|
+
});
|
|
4234
|
+
await runAutomationCommitChain(owner, project, owner.bundle ?? null, automationConfig, io, progressionRunId);
|
|
4235
|
+
}
|
|
4236
|
+
catch (error) {
|
|
4237
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4238
|
+
}
|
|
3731
4239
|
}
|
|
3732
4240
|
else {
|
|
3733
|
-
|
|
3734
|
-
|
|
4241
|
+
const reviewResult = rerunTicketReviewOperation(owner, io, {
|
|
4242
|
+
automatic: true,
|
|
4243
|
+
initiation: { causalParentRunId: progressionRunId },
|
|
4244
|
+
accept: acceptResumedReview,
|
|
4245
|
+
});
|
|
4246
|
+
if (!reviewResult.ok) {
|
|
4247
|
+
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
4248
|
+
}
|
|
3735
4249
|
}
|
|
3736
4250
|
}
|
|
3737
4251
|
else if (owner.state === 'build' && owner.agent_status === 'done') {
|
|
3738
|
-
const reviewResult =
|
|
4252
|
+
const reviewResult = transitionBundleToReview(result.project.id, bundle.id, io, {
|
|
4253
|
+
automatic: true,
|
|
4254
|
+
initiation: { causalParentRunId: progressionRunId },
|
|
4255
|
+
accept: acceptResumedReview,
|
|
4256
|
+
});
|
|
3739
4257
|
if (!reviewResult.ok) {
|
|
3740
4258
|
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
3741
4259
|
}
|
|
3742
|
-
clearAutomationPause(result.project.id, bundle.id);
|
|
3743
4260
|
}
|
|
3744
4261
|
else {
|
|
3745
4262
|
return res.status(409).json({ error: 'Ticket is not in a resumable state' });
|
|
@@ -3750,7 +4267,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3750
4267
|
}
|
|
3751
4268
|
return res.json(serializeBundleDetails(result.project, bundle));
|
|
3752
4269
|
});
|
|
3753
|
-
app.post('/bundles/:id/dismiss-automation-pause', (req, res) => {
|
|
4270
|
+
app.post('/bundles/:id/dismiss-automation-pause', async (req, res) => {
|
|
3754
4271
|
const result = getRequiredProject(req);
|
|
3755
4272
|
if ('error' in result) {
|
|
3756
4273
|
return res.status(400).json({ error: result.error });
|
|
@@ -3771,7 +4288,68 @@ app.post('/bundles/:id/dismiss-automation-pause', (req, res) => {
|
|
|
3771
4288
|
&& listBuildOpenQuestionsForBundle(result.project.id, bundle.id).length > 0) {
|
|
3772
4289
|
return res.status(409).json({ error: 'Resolve or dismiss all Build open questions before dismissing this pause' });
|
|
3773
4290
|
}
|
|
3774
|
-
|
|
4291
|
+
let progressionRunId = bundleState.automation_progression_run_id;
|
|
4292
|
+
if (!progressionRunId) {
|
|
4293
|
+
try {
|
|
4294
|
+
progressionRunId = (bundleState.automation_paused_stage_run_id
|
|
4295
|
+
? recoverStoppedSideEffectProgression({
|
|
4296
|
+
projectId: result.project.id,
|
|
4297
|
+
bundleId: bundle.id,
|
|
4298
|
+
broadcaster: io,
|
|
4299
|
+
})
|
|
4300
|
+
: recoverLegacyAutomationProgression({
|
|
4301
|
+
projectId: result.project.id,
|
|
4302
|
+
bundleId: bundle.id,
|
|
4303
|
+
broadcaster: io,
|
|
4304
|
+
})).runId;
|
|
4305
|
+
}
|
|
4306
|
+
catch (error) {
|
|
4307
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
try {
|
|
4311
|
+
if (getAutomationProgression(result.project.id, bundle.id)?.id !== progressionRunId) {
|
|
4312
|
+
return res.status(409).json({ error: 'Automation progression ownership is invalid' });
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
catch (error) {
|
|
4316
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4317
|
+
}
|
|
4318
|
+
if (bundleState.automation_paused_stage_run_id) {
|
|
4319
|
+
const stage = getExecutionRun(result.project.id, bundleState.automation_paused_stage_run_id);
|
|
4320
|
+
if (!stage
|
|
4321
|
+
|| (!AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind) && !AUTOMATION_WORKER_PAUSE_KINDS.has(stage.kind))
|
|
4322
|
+
|| !pausedRunBelongsToProgression(result.project.id, bundle.id, progressionRunId, stage.id)) {
|
|
4323
|
+
return res.status(409).json({ error: 'The paused automation stage does not belong to the active progression' });
|
|
4324
|
+
}
|
|
4325
|
+
if (AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind)) {
|
|
4326
|
+
let reconciled;
|
|
4327
|
+
try {
|
|
4328
|
+
reconciled = await reconcileAutomationStage({
|
|
4329
|
+
projectId: result.project.id,
|
|
4330
|
+
bundleId: bundle.id,
|
|
4331
|
+
progressionRunId,
|
|
4332
|
+
stageRunId: stage.id,
|
|
4333
|
+
});
|
|
4334
|
+
}
|
|
4335
|
+
catch (error) {
|
|
4336
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4337
|
+
}
|
|
4338
|
+
if (reconciled.domainStatus === 'ambiguous') {
|
|
4339
|
+
return res.status(409).json({ error: 'The paused automation side effect is ambiguous and cannot be dismissed safely.' });
|
|
4340
|
+
}
|
|
4341
|
+
if (!(reconciled.status === 'completed' && reconciled.domainStatus === 'reconciled_completed')
|
|
4342
|
+
&& !(reconciled.status === 'interrupted' && reconciled.domainStatus === 'reconciled_absent')) {
|
|
4343
|
+
return res.status(409).json({ error: 'The paused automation side effect has no safely dismissible outcome.' });
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
await stopAutomationProgression({
|
|
4348
|
+
projectId: result.project.id,
|
|
4349
|
+
bundleId: bundle.id,
|
|
4350
|
+
broadcaster: io,
|
|
4351
|
+
stopLocalWorker: worker => (worker.kind === 'review' ? stopReviewAgent(worker.ticket_id) : stopBuildAgent(worker.ticket_id)),
|
|
4352
|
+
});
|
|
3775
4353
|
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
3776
4354
|
io.emit('ticket:updated', ticket);
|
|
3777
4355
|
}
|
|
@@ -4414,20 +4992,84 @@ app.post('/integrations/github/actions/workflows/:id/dispatch', async (req, res)
|
|
|
4414
4992
|
}
|
|
4415
4993
|
}
|
|
4416
4994
|
console.info(`[integrations] github actions dispatch project=${result.project.id} workflow=${workflowId} ref=${ref}`);
|
|
4995
|
+
const inputNames = Object.keys(inputs).sort();
|
|
4996
|
+
const inputFingerprint = inputNames.length > 0
|
|
4997
|
+
? createHash('sha256').update(JSON.stringify(inputNames.map(name => [name, inputs[name]]))).digest('hex')
|
|
4998
|
+
: null;
|
|
4999
|
+
const dispatchedAt = new Date().toISOString();
|
|
5000
|
+
const workflowName = asTrimmedString(req.body?.workflowName)?.slice(0, 500) || `Workflow ${workflowId}`;
|
|
5001
|
+
const workflowUrl = asTrimmedString(req.body?.workflowUrl)?.slice(0, 2_000) || null;
|
|
5002
|
+
let executionRun;
|
|
5003
|
+
try {
|
|
5004
|
+
executionRun = createExecutionRun({
|
|
5005
|
+
projectId: result.project.id,
|
|
5006
|
+
kind: 'github_actions',
|
|
5007
|
+
domainStatus: 'dispatching',
|
|
5008
|
+
sourceReference: {
|
|
5009
|
+
kind: 'github_workflow',
|
|
5010
|
+
id: String(workflowId),
|
|
5011
|
+
label: workflowName,
|
|
5012
|
+
url: workflowUrl,
|
|
5013
|
+
},
|
|
5014
|
+
eventType: 'github_actions_dispatch_requested',
|
|
5015
|
+
eventMessage: 'GitHub Actions workflow dispatch was requested.',
|
|
5016
|
+
eventMetadata: { workflow_id: workflowId, ref, input_names: inputNames },
|
|
5017
|
+
githubActionsDetails: {
|
|
5018
|
+
workflowId,
|
|
5019
|
+
workflowName,
|
|
5020
|
+
ref,
|
|
5021
|
+
inputNames,
|
|
5022
|
+
inputFingerprint,
|
|
5023
|
+
dispatchedAt,
|
|
5024
|
+
},
|
|
5025
|
+
}, io);
|
|
5026
|
+
}
|
|
5027
|
+
catch (error) {
|
|
5028
|
+
if (error instanceof GitHubActionsDispatchConflictError) {
|
|
5029
|
+
return res.status(409).json({
|
|
5030
|
+
error: error.message,
|
|
5031
|
+
execution_run_id: error.executionRunId,
|
|
5032
|
+
});
|
|
5033
|
+
}
|
|
5034
|
+
throw error;
|
|
5035
|
+
}
|
|
4417
5036
|
try {
|
|
4418
|
-
const dispatchedAt = Date.now();
|
|
4419
5037
|
await dispatchGitHubWorkflow(githubConfig, workflowId, { ref, inputs });
|
|
5038
|
+
transitionExecutionRun({
|
|
5039
|
+
projectId: result.project.id,
|
|
5040
|
+
runId: executionRun.id,
|
|
5041
|
+
status: 'waiting',
|
|
5042
|
+
domainStatus: 'resolving',
|
|
5043
|
+
eventType: 'github_actions_dispatch_accepted',
|
|
5044
|
+
eventMessage: 'GitHub accepted the workflow dispatch request.',
|
|
5045
|
+
eventMetadata: { workflow_id: workflowId, ref },
|
|
5046
|
+
}, io);
|
|
4420
5047
|
const trackedRun = trackGitHubActionsRun({
|
|
4421
5048
|
io,
|
|
5049
|
+
executionRunId: executionRun.id,
|
|
4422
5050
|
projectId: result.project.id,
|
|
4423
|
-
workflowId,
|
|
4424
5051
|
config: githubConfig,
|
|
4425
|
-
dispatchedAt,
|
|
4426
5052
|
});
|
|
4427
|
-
return res.status(202).json({
|
|
5053
|
+
return res.status(202).json({
|
|
5054
|
+
tracking_key: trackedRun.tracking_key,
|
|
5055
|
+
execution_run_id: executionRun.id,
|
|
5056
|
+
dispatched: true,
|
|
5057
|
+
});
|
|
4428
5058
|
}
|
|
4429
5059
|
catch (error) {
|
|
4430
5060
|
console.warn(`[integrations] github actions dispatch failed project=${result.project.id} workflow=${workflowId}`, error);
|
|
5061
|
+
transitionExecutionRun({
|
|
5062
|
+
projectId: result.project.id,
|
|
5063
|
+
runId: executionRun.id,
|
|
5064
|
+
status: 'failed',
|
|
5065
|
+
domainStatus: 'dispatch_failed',
|
|
5066
|
+
failure: {
|
|
5067
|
+
code: error instanceof GitHubApiError ? `github_actions_dispatch_http_${error.status}` : 'github_actions_dispatch_failed',
|
|
5068
|
+
message: error instanceof GitHubApiError ? error.publicMessage : 'Unable to dispatch the workflow right now.',
|
|
5069
|
+
},
|
|
5070
|
+
eventType: 'github_actions_dispatch_failed',
|
|
5071
|
+
eventMessage: error instanceof GitHubApiError ? error.publicMessage : 'Unable to dispatch the workflow right now.',
|
|
5072
|
+
}, io);
|
|
4431
5073
|
if (error instanceof GitHubApiError) {
|
|
4432
5074
|
return res.status(error.status).json({ error: error.publicMessage });
|
|
4433
5075
|
}
|
|
@@ -4635,6 +5277,8 @@ app.post('/planning-agent', async (req, res) => {
|
|
|
4635
5277
|
ensureProjectRootBundle(project.id);
|
|
4636
5278
|
const trimmedDescription = description.trim();
|
|
4637
5279
|
let autoCreatedBundle = null;
|
|
5280
|
+
let planningExecutionRun = null;
|
|
5281
|
+
let materializationCommitted = false;
|
|
4638
5282
|
try {
|
|
4639
5283
|
const resolvedDescription = await resolveExternalPlanningDescription(project.id, trimmedDescription);
|
|
4640
5284
|
const requestedBundleSelection = resolveRequestedBundleSelection(req.body, project.id);
|
|
@@ -4669,13 +5313,38 @@ app.post('/planning-agent', async (req, res) => {
|
|
|
4669
5313
|
});
|
|
4670
5314
|
}
|
|
4671
5315
|
}
|
|
4672
|
-
|
|
4673
|
-
|
|
5316
|
+
planningExecutionRun = createExecutionRun({
|
|
5317
|
+
projectId: project.id,
|
|
5318
|
+
kind: 'planning_agent',
|
|
5319
|
+
domainStatus: 'queued',
|
|
5320
|
+
sourceReference: { kind: 'project', id: project.id, label: project.name },
|
|
5321
|
+
eventType: 'planning_agent_created',
|
|
5322
|
+
}, io);
|
|
5323
|
+
const executionContext = {
|
|
5324
|
+
runId: planningExecutionRun.id,
|
|
5325
|
+
correlationId: planningExecutionRun.correlation_id,
|
|
5326
|
+
projectId: planningExecutionRun.project_id,
|
|
5327
|
+
kind: planningExecutionRun.kind,
|
|
5328
|
+
};
|
|
5329
|
+
const executionPlannerOptions = {
|
|
4674
5330
|
...plannerOptions,
|
|
5331
|
+
executionContext,
|
|
5332
|
+
agentRunParent: { parentType: 'planning', parentId: project.id, io },
|
|
5333
|
+
};
|
|
5334
|
+
transitionExecutionRun({
|
|
5335
|
+
projectId: project.id,
|
|
5336
|
+
runId: planningExecutionRun.id,
|
|
5337
|
+
status: 'running',
|
|
5338
|
+
domainStatus: 'running',
|
|
5339
|
+
eventType: 'planning_agent_started',
|
|
5340
|
+
}, io);
|
|
5341
|
+
const linkedResearch = await prepareLinkedPlanningResearch(resolvedDescription, executionPlannerOptions);
|
|
5342
|
+
const outline = normalizePlanningAgentOutline(await generatePlanningTicketOutline(resolvedDescription, {
|
|
5343
|
+
...executionPlannerOptions,
|
|
4675
5344
|
linkedResearch,
|
|
4676
5345
|
}));
|
|
4677
5346
|
const draft = validateExpandedPlanningAgentDraft(outline, normalizePlanningAgentDraft(await expandPlanningTicketOutline(resolvedDescription, outline, {
|
|
4678
|
-
...
|
|
5347
|
+
...executionPlannerOptions,
|
|
4679
5348
|
linkedResearch,
|
|
4680
5349
|
})));
|
|
4681
5350
|
const created = createPlanningAgentTickets({
|
|
@@ -4685,18 +5354,57 @@ app.post('/planning-agent', async (req, res) => {
|
|
|
4685
5354
|
model: tool === 'opencode' && typeof model === 'string' ? normalizeModelId(model) || null : null,
|
|
4686
5355
|
variant: tool === 'opencode' && typeof variant === 'string' && variant.trim() ? variant.trim() : null,
|
|
4687
5356
|
skills: resolveRequestedSkills(req.body?.skills),
|
|
4688
|
-
tool,
|
|
4689
5357
|
worktreeBundleId: bundleSelection.bundleId,
|
|
4690
5358
|
});
|
|
5359
|
+
materializationCommitted = true;
|
|
5360
|
+
addExecutionRunLinks({
|
|
5361
|
+
projectId: project.id,
|
|
5362
|
+
runId: planningExecutionRun.id,
|
|
5363
|
+
links: created.tickets.map(ticket => ({
|
|
5364
|
+
entityType: 'ticket',
|
|
5365
|
+
entityId: ticket.id,
|
|
5366
|
+
relationship: 'result',
|
|
5367
|
+
})),
|
|
5368
|
+
});
|
|
4691
5369
|
for (const ticket of created.tickets) {
|
|
4692
5370
|
io.emit('ticket:created', ticket);
|
|
4693
5371
|
}
|
|
4694
5372
|
for (const dependency of created.dependencies) {
|
|
4695
5373
|
io.emit('ticket-dependency:created', dependency);
|
|
4696
5374
|
}
|
|
5375
|
+
transitionExecutionRun({
|
|
5376
|
+
projectId: project.id,
|
|
5377
|
+
runId: planningExecutionRun.id,
|
|
5378
|
+
status: 'completed',
|
|
5379
|
+
domainStatus: 'done',
|
|
5380
|
+
eventType: 'planning_agent_completed',
|
|
5381
|
+
}, io);
|
|
4697
5382
|
return res.status(201).json(created);
|
|
4698
5383
|
}
|
|
4699
5384
|
catch (error) {
|
|
5385
|
+
if (planningExecutionRun) {
|
|
5386
|
+
const rawFailureMessage = getErrorMessage(error) || 'Planning agent failed';
|
|
5387
|
+
const failureMessage = (materializationCommitted
|
|
5388
|
+
? `Planning tickets were created, but execution result reconciliation failed. Reconcile run ${planningExecutionRun.id}: ${rawFailureMessage}`
|
|
5389
|
+
: rawFailureMessage).slice(0, 4_000);
|
|
5390
|
+
try {
|
|
5391
|
+
terminalizeAgentRunsForExecutionRun(planningExecutionRun.id, 'error', failureMessage);
|
|
5392
|
+
}
|
|
5393
|
+
catch {
|
|
5394
|
+
// Preserve the top-level execution failure when child detail finalization fails.
|
|
5395
|
+
}
|
|
5396
|
+
transitionExecutionRun({
|
|
5397
|
+
projectId: project.id,
|
|
5398
|
+
runId: planningExecutionRun.id,
|
|
5399
|
+
status: 'failed',
|
|
5400
|
+
domainStatus: 'error',
|
|
5401
|
+
failure: {
|
|
5402
|
+
code: materializationCommitted ? 'planning_agent_result_reconciliation_failed' : 'planning_agent_failed',
|
|
5403
|
+
message: failureMessage,
|
|
5404
|
+
},
|
|
5405
|
+
eventType: 'planning_agent_failed',
|
|
5406
|
+
}, io);
|
|
5407
|
+
}
|
|
4700
5408
|
if (autoCreatedBundle && countBundleTickets(autoCreatedBundle.id, project.id) === 0) {
|
|
4701
5409
|
deleteBundle(autoCreatedBundle.id, project.id);
|
|
4702
5410
|
}
|
|
@@ -5986,7 +6694,14 @@ app.delete('/projects/:id', (req, res) => {
|
|
|
5986
6694
|
error: 'Cannot delete a project with existing tickets, bundles, or worktrees',
|
|
5987
6695
|
});
|
|
5988
6696
|
}
|
|
5989
|
-
db.
|
|
6697
|
+
db.transaction(() => {
|
|
6698
|
+
db.prepare(`
|
|
6699
|
+
UPDATE execution_runs
|
|
6700
|
+
SET parent_run_id = NULL, retry_of_run_id = NULL
|
|
6701
|
+
WHERE project_id = ?
|
|
6702
|
+
`).run(req.params.id);
|
|
6703
|
+
db.prepare('DELETE FROM projects WHERE id = ?').run(req.params.id);
|
|
6704
|
+
})();
|
|
5990
6705
|
res.status(204).end();
|
|
5991
6706
|
});
|
|
5992
6707
|
app.post('/tickets', (req, res) => {
|
|
@@ -6259,11 +6974,14 @@ app.post('/tickets/delete-parked', async (req, res) => {
|
|
|
6259
6974
|
deleted_ticket_ids: [],
|
|
6260
6975
|
failures: [],
|
|
6261
6976
|
};
|
|
6977
|
+
const sequenceContinuations = [];
|
|
6262
6978
|
for (const ticket of parkedTickets) {
|
|
6263
6979
|
const deleteResult = await deleteTicketRecord(ticket);
|
|
6264
6980
|
if (deleteResult.ok) {
|
|
6265
6981
|
summary.deleted_count += 1;
|
|
6266
6982
|
summary.deleted_ticket_ids.push(ticket.id);
|
|
6983
|
+
emitUpdatedTickets(deleteResult.affectedTicketIds ?? []);
|
|
6984
|
+
sequenceContinuations.push(deleteResult);
|
|
6267
6985
|
continue;
|
|
6268
6986
|
}
|
|
6269
6987
|
summary.failed_count += 1;
|
|
@@ -6273,6 +6991,11 @@ app.post('/tickets/delete-parked', async (req, res) => {
|
|
|
6273
6991
|
error: deleteResult.error,
|
|
6274
6992
|
});
|
|
6275
6993
|
}
|
|
6994
|
+
for (const continuation of sequenceContinuations) {
|
|
6995
|
+
if (continuation.nextBuildTicketId && getTicket(continuation.nextBuildTicketId)) {
|
|
6996
|
+
scheduleBuildAfterTicketDeletion(continuation);
|
|
6997
|
+
}
|
|
6998
|
+
}
|
|
6276
6999
|
return res.json(summary);
|
|
6277
7000
|
});
|
|
6278
7001
|
app.post('/tickets/:id/build-chain', (req, res) => {
|
|
@@ -6286,22 +7009,43 @@ app.post('/tickets/:id/build-chain', (req, res) => {
|
|
|
6286
7009
|
if (!existing.project_id) {
|
|
6287
7010
|
return res.status(400).json({ error: 'Ticket project no longer exists' });
|
|
6288
7011
|
}
|
|
6289
|
-
const
|
|
6290
|
-
if (!
|
|
6291
|
-
return res.status(
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
if (!buildTransition.ok) {
|
|
6295
|
-
stopBuildChain(chainStart.activeTicketId);
|
|
6296
|
-
return res.status(buildTransition.status).json({
|
|
6297
|
-
error: buildTransition.error,
|
|
6298
|
-
...(buildTransition.blockers ? { blockers: buildTransition.blockers } : {}),
|
|
7012
|
+
const validation = prepareTicketForBuild(existing.id, { updateState: false });
|
|
7013
|
+
if (!validation.ok) {
|
|
7014
|
+
return res.status(validation.status).json({
|
|
7015
|
+
error: validation.error,
|
|
7016
|
+
...(validation.blockers ? { blockers: validation.blockers } : {}),
|
|
6299
7017
|
});
|
|
6300
7018
|
}
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
7019
|
+
let affectedTicketIds = [];
|
|
7020
|
+
let activeTicketId = existing.id;
|
|
7021
|
+
const acceptChain = () => {
|
|
7022
|
+
const chainStart = startBuildChain(existing.id, existing.project_id);
|
|
7023
|
+
if (!chainStart.ok) {
|
|
7024
|
+
throw new Error(chainStart.error);
|
|
7025
|
+
}
|
|
7026
|
+
affectedTicketIds = chainStart.affectedTicketIds;
|
|
7027
|
+
activeTicketId = chainStart.activeTicketId;
|
|
7028
|
+
const buildTransition = prepareTicketForBuild(chainStart.activeTicketId);
|
|
7029
|
+
if (!buildTransition.ok) {
|
|
7030
|
+
throw new Error(buildTransition.error);
|
|
7031
|
+
}
|
|
7032
|
+
return {
|
|
7033
|
+
ticket: buildTransition.ticket,
|
|
7034
|
+
sequenceId: chainStart.sequenceId,
|
|
7035
|
+
compensate: () => compensateBuildSequenceAcceptance(chainStart.sequenceId, chainStart.affectedTicketIds),
|
|
7036
|
+
};
|
|
7037
|
+
};
|
|
7038
|
+
const scheduling = startBuild(existing, io, { accept: acceptChain });
|
|
7039
|
+
if (scheduling === undefined) {
|
|
7040
|
+
acceptChain();
|
|
7041
|
+
}
|
|
7042
|
+
else if (!scheduling.accepted) {
|
|
7043
|
+
return res.status(scheduling.reason === 'queue_failed' ? 503 : 409).json({ error: scheduling.error });
|
|
7044
|
+
}
|
|
7045
|
+
emitUpdatedTickets(affectedTicketIds);
|
|
7046
|
+
const updated = getTicket(activeTicketId);
|
|
7047
|
+
io.emit('ticket:updated', updated);
|
|
7048
|
+
return res.json(updated);
|
|
6305
7049
|
});
|
|
6306
7050
|
app.post('/tickets/:id/stop-build-chain', (req, res) => {
|
|
6307
7051
|
const existing = getTicket(req.params.id);
|
|
@@ -6335,31 +7079,152 @@ app.post('/tickets/:id/retry', (req, res) => {
|
|
|
6335
7079
|
&& hasActiveBuildOpenQuestionBatch(ticket.project_id, ticket.worktree_bundle_id)) {
|
|
6336
7080
|
return res.status(409).json({ error: 'Build open question resolution is still in progress' });
|
|
6337
7081
|
}
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
res.json(updated);
|
|
6342
|
-
if (ticket.state === 'build' && updated) {
|
|
6343
|
-
if (updated.project_id && updated.worktree_bundle_id) {
|
|
6344
|
-
const buildBlocker = getBundleBuildBlockerResponse(updated);
|
|
7082
|
+
if (ticket.state === 'build') {
|
|
7083
|
+
if (ticket.project_id && ticket.worktree_bundle_id) {
|
|
7084
|
+
const buildBlocker = getBundleBuildBlockerResponse(ticket);
|
|
6345
7085
|
if (buildBlocker) {
|
|
6346
|
-
return;
|
|
7086
|
+
return res.status(409).json({ error: buildBlocker.error, blocker_ticket_id: buildBlocker.blocker_ticket_id });
|
|
6347
7087
|
}
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
7088
|
+
}
|
|
7089
|
+
const retrySource = ticket.project_id
|
|
7090
|
+
? db.prepare(`
|
|
7091
|
+
SELECT er.id
|
|
7092
|
+
FROM execution_runs er
|
|
7093
|
+
INNER JOIN execution_run_links link
|
|
7094
|
+
ON link.execution_run_id = er.id
|
|
7095
|
+
AND link.project_id = er.project_id
|
|
7096
|
+
AND link.relationship = 'primary'
|
|
7097
|
+
AND link.entity_type = 'ticket'
|
|
7098
|
+
WHERE er.project_id = ?
|
|
7099
|
+
AND er.kind = 'build'
|
|
7100
|
+
AND er.status IN ('failed', 'interrupted')
|
|
7101
|
+
AND link.entity_id = ?
|
|
7102
|
+
ORDER BY er.created_at DESC, er.id DESC
|
|
7103
|
+
LIMIT 1
|
|
7104
|
+
`).get(ticket.project_id, ticket.id)
|
|
7105
|
+
: undefined;
|
|
7106
|
+
const acceptRetry = () => {
|
|
7107
|
+
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);
|
|
7108
|
+
const updated = getTicket(req.params.id);
|
|
7109
|
+
if (!updated) {
|
|
7110
|
+
throw new Error('Not found');
|
|
7111
|
+
}
|
|
7112
|
+
let sequenceId = null;
|
|
7113
|
+
if (updated.project_id && updated.worktree_bundle_id) {
|
|
7114
|
+
sequenceId = enqueueBundledBuild({
|
|
7115
|
+
ticketId: updated.id,
|
|
7116
|
+
projectId: updated.project_id,
|
|
7117
|
+
worktreeBundleId: updated.worktree_bundle_id,
|
|
7118
|
+
activate: shouldStartBundleBuildNow(updated),
|
|
7119
|
+
}).sequenceId;
|
|
7120
|
+
}
|
|
7121
|
+
return {
|
|
7122
|
+
ticket: updated,
|
|
7123
|
+
sequenceId,
|
|
7124
|
+
...(sequenceId ? { compensate: () => compensateBuildSequenceAcceptance(sequenceId, [updated.id]) } : {}),
|
|
7125
|
+
};
|
|
7126
|
+
};
|
|
7127
|
+
if (ticket.project_id && ticket.worktree_bundle_id && hasActiveSequenceForBundle(ticket.project_id, ticket.worktree_bundle_id)) {
|
|
7128
|
+
try {
|
|
7129
|
+
withExecutionRunTransaction(transaction => {
|
|
7130
|
+
const acceptance = acceptRetry();
|
|
7131
|
+
if (!acceptance.sequenceId) {
|
|
7132
|
+
throw new Error('Build sequence no longer exists');
|
|
7133
|
+
}
|
|
7134
|
+
transaction.createExecutionRun({
|
|
7135
|
+
projectId: ticket.project_id,
|
|
7136
|
+
kind: 'build',
|
|
7137
|
+
domainStatus: 'queued',
|
|
7138
|
+
sourceReference: { kind: 'ticket', id: acceptance.ticket.id, label: acceptance.ticket.title },
|
|
7139
|
+
links: [
|
|
7140
|
+
{ entityType: 'bundle', entityId: ticket.worktree_bundle_id, relationship: 'context' },
|
|
7141
|
+
{ entityType: 'build_sequence', entityId: acceptance.sequenceId, relationship: 'context' },
|
|
7142
|
+
],
|
|
7143
|
+
initiatedBy: 'user',
|
|
7144
|
+
retryOfRunId: retrySource?.id ?? null,
|
|
7145
|
+
eventType: 'build_created',
|
|
7146
|
+
});
|
|
7147
|
+
}, io);
|
|
6356
7148
|
}
|
|
7149
|
+
catch (error) {
|
|
7150
|
+
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
7151
|
+
}
|
|
7152
|
+
const updated = getTicket(req.params.id);
|
|
7153
|
+
io.emit('ticket:updated', updated);
|
|
7154
|
+
return res.json(updated);
|
|
6357
7155
|
}
|
|
6358
|
-
startBuild(
|
|
7156
|
+
const scheduling = startBuild(ticket, io, {
|
|
7157
|
+
initiation: { initiator: 'user', retrySourceRunId: retrySource?.id ?? null },
|
|
7158
|
+
accept: acceptRetry,
|
|
7159
|
+
});
|
|
7160
|
+
if (scheduling === undefined) {
|
|
7161
|
+
acceptRetry();
|
|
7162
|
+
}
|
|
7163
|
+
else if (!scheduling.accepted) {
|
|
7164
|
+
return res.status(scheduling.reason === 'queue_failed' ? 503 : 409).json({ error: scheduling.error });
|
|
7165
|
+
}
|
|
7166
|
+
const updated = getTicket(req.params.id);
|
|
7167
|
+
io.emit('ticket:updated', updated);
|
|
7168
|
+
return res.json(updated);
|
|
6359
7169
|
}
|
|
6360
|
-
|
|
6361
|
-
|
|
7170
|
+
const reviewRetrySource = ticket.state === 'review' && ticket.project_id
|
|
7171
|
+
? db.prepare(`
|
|
7172
|
+
SELECT er.id
|
|
7173
|
+
FROM execution_runs er
|
|
7174
|
+
JOIN execution_run_links link
|
|
7175
|
+
ON link.execution_run_id = er.id
|
|
7176
|
+
AND link.relationship = 'primary'
|
|
7177
|
+
AND link.entity_type = 'ticket'
|
|
7178
|
+
WHERE er.project_id = ?
|
|
7179
|
+
AND er.kind = 'review'
|
|
7180
|
+
AND er.status IN ('failed', 'interrupted')
|
|
7181
|
+
AND link.entity_id = ?
|
|
7182
|
+
ORDER BY er.created_at DESC, er.id DESC
|
|
7183
|
+
LIMIT 1
|
|
7184
|
+
`).get(ticket.project_id, ticket.id)
|
|
7185
|
+
: undefined;
|
|
7186
|
+
if (ticket.state === 'review') {
|
|
7187
|
+
const reviewSnapshot = {
|
|
7188
|
+
agent_status: ticket.agent_status,
|
|
7189
|
+
agent_log: ticket.agent_log,
|
|
7190
|
+
agent_message_id: ticket.agent_message_id,
|
|
7191
|
+
previous_agent_log: ticket.previous_agent_log,
|
|
7192
|
+
previous_agent_message_id: ticket.previous_agent_message_id,
|
|
7193
|
+
streaming_response: ticket.streaming_response,
|
|
7194
|
+
build_completed_at: ticket.build_completed_at,
|
|
7195
|
+
auto_park_dismissed_at: ticket.auto_park_dismissed_at,
|
|
7196
|
+
};
|
|
7197
|
+
const reviewResult = rerunTicketReviewOperation(ticket, io, {
|
|
7198
|
+
initiation: { retrySourceRunId: reviewRetrySource?.id ?? null },
|
|
7199
|
+
accept: () => {
|
|
7200
|
+
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);
|
|
7201
|
+
const acceptedTicket = getTicket(req.params.id);
|
|
7202
|
+
if (!acceptedTicket) {
|
|
7203
|
+
throw new Error('Not found');
|
|
7204
|
+
}
|
|
7205
|
+
return {
|
|
7206
|
+
ticket: acceptedTicket,
|
|
7207
|
+
compensate: () => {
|
|
7208
|
+
db.prepare(`
|
|
7209
|
+
UPDATE tickets SET agent_status = ?, agent_log = ?, agent_message_id = ?, previous_agent_log = ?,
|
|
7210
|
+
previous_agent_message_id = ?, streaming_response = ?, build_completed_at = ?, auto_park_dismissed_at = ?,
|
|
7211
|
+
updated_at = CURRENT_TIMESTAMP WHERE id = ?
|
|
7212
|
+
`).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);
|
|
7213
|
+
},
|
|
7214
|
+
};
|
|
7215
|
+
},
|
|
7216
|
+
});
|
|
7217
|
+
if (!reviewResult.ok) {
|
|
7218
|
+
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
7219
|
+
}
|
|
7220
|
+
const updated = getTicket(req.params.id);
|
|
7221
|
+
io.emit('ticket:updated', updated);
|
|
7222
|
+
return res.json(updated);
|
|
6362
7223
|
}
|
|
7224
|
+
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);
|
|
7225
|
+
const updated = getTicket(req.params.id);
|
|
7226
|
+
io.emit('ticket:updated', updated);
|
|
7227
|
+
return res.json(updated);
|
|
6363
7228
|
});
|
|
6364
7229
|
app.post('/tickets/:id/review', (req, res) => {
|
|
6365
7230
|
const ticket = getTicket(req.params.id);
|
|
@@ -6663,6 +7528,111 @@ app.post('/projects/:id/test-sessions', (req, res) => {
|
|
|
6663
7528
|
targetLabel: workspace.target_label,
|
|
6664
7529
|
}));
|
|
6665
7530
|
});
|
|
7531
|
+
app.get('/projects/:id/execution-runs', (req, res) => {
|
|
7532
|
+
const project = getProjectById(req.params.id);
|
|
7533
|
+
if (!project) {
|
|
7534
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7535
|
+
}
|
|
7536
|
+
const query = parseLifecycleListQuery(req.query);
|
|
7537
|
+
if ('error' in query) {
|
|
7538
|
+
return res.status(400).json({ error: query.error });
|
|
7539
|
+
}
|
|
7540
|
+
try {
|
|
7541
|
+
return res.json(listExecutionRuns({
|
|
7542
|
+
projectId: project.id,
|
|
7543
|
+
kind: query.kind,
|
|
7544
|
+
statuses: query.statuses,
|
|
7545
|
+
cursor: query.cursor,
|
|
7546
|
+
limit: query.limit,
|
|
7547
|
+
}));
|
|
7548
|
+
}
|
|
7549
|
+
catch (error) {
|
|
7550
|
+
if (error instanceof Error && error.message === 'Invalid execution run cursor') {
|
|
7551
|
+
return res.status(400).json({ error: 'cursor must be a valid execution run cursor' });
|
|
7552
|
+
}
|
|
7553
|
+
throw error;
|
|
7554
|
+
}
|
|
7555
|
+
});
|
|
7556
|
+
app.get('/projects/:id/lifecycle-events', (req, res) => {
|
|
7557
|
+
const project = getProjectById(req.params.id);
|
|
7558
|
+
if (!project) {
|
|
7559
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7560
|
+
}
|
|
7561
|
+
const query = parseLifecycleListQuery(req.query);
|
|
7562
|
+
if ('error' in query) {
|
|
7563
|
+
return res.status(400).json({ error: query.error });
|
|
7564
|
+
}
|
|
7565
|
+
try {
|
|
7566
|
+
const page = listProjectLifecycleEvents({
|
|
7567
|
+
projectId: project.id,
|
|
7568
|
+
kind: query.kind,
|
|
7569
|
+
statuses: query.statuses,
|
|
7570
|
+
cursor: query.cursor,
|
|
7571
|
+
limit: query.limit,
|
|
7572
|
+
});
|
|
7573
|
+
return res.json({
|
|
7574
|
+
...page,
|
|
7575
|
+
events: page.events.map(toCanonicalLifecycleEvent),
|
|
7576
|
+
});
|
|
7577
|
+
}
|
|
7578
|
+
catch (error) {
|
|
7579
|
+
if (error instanceof Error && error.message === 'Invalid lifecycle event cursor') {
|
|
7580
|
+
return res.status(400).json({ error: 'cursor must be a valid lifecycle event cursor' });
|
|
7581
|
+
}
|
|
7582
|
+
throw error;
|
|
7583
|
+
}
|
|
7584
|
+
});
|
|
7585
|
+
app.get('/projects/:id/execution-runs/:runId', (req, res) => {
|
|
7586
|
+
const project = getProjectById(req.params.id);
|
|
7587
|
+
if (!project) {
|
|
7588
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7589
|
+
}
|
|
7590
|
+
const run = getExecutionRun(project.id, req.params.runId);
|
|
7591
|
+
if (!run) {
|
|
7592
|
+
return res.status(404).json({ error: 'Execution run not found' });
|
|
7593
|
+
}
|
|
7594
|
+
const testDetails = run.kind === 'test' ? getTestExecutionDetails(project.id, run.id) : null;
|
|
7595
|
+
const details = run.kind === 'test'
|
|
7596
|
+
? serializeTestExecutionDetail(testDetails, resolveTestRetry(project, run, testDetails).eligibility)
|
|
7597
|
+
: run.kind === 'github_actions'
|
|
7598
|
+
? serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id))
|
|
7599
|
+
: { kind: run.kind, available: true };
|
|
7600
|
+
const links = listExecutionRunLinks(project.id, run.id);
|
|
7601
|
+
const agentRuns = listAgentRunsForExecutionRun(run.id);
|
|
7602
|
+
return res.json({ run, links, details, agent_runs: agentRuns });
|
|
7603
|
+
});
|
|
7604
|
+
app.post('/projects/:id/execution-runs/:runId/retry', async (req, res) => {
|
|
7605
|
+
const project = getProjectById(req.params.id);
|
|
7606
|
+
if (!project) {
|
|
7607
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7608
|
+
}
|
|
7609
|
+
const executionRun = getExecutionRun(project.id, req.params.runId);
|
|
7610
|
+
const details = getTestExecutionDetails(project.id, req.params.runId);
|
|
7611
|
+
const retry = resolveTestRetry(project, executionRun, details);
|
|
7612
|
+
if ('error' in retry) {
|
|
7613
|
+
return res.status(retry.error.status).json({ error: retry.error.message });
|
|
7614
|
+
}
|
|
7615
|
+
const session = createTestSession({
|
|
7616
|
+
projectId: project.id,
|
|
7617
|
+
ticketId: retry.details.ticket_id,
|
|
7618
|
+
targetKind: retry.workspace.target_kind,
|
|
7619
|
+
targetBundleId: retry.workspace.target_bundle_id,
|
|
7620
|
+
cwd: retry.workspace.cwd,
|
|
7621
|
+
targetLabel: retry.workspace.target_label,
|
|
7622
|
+
});
|
|
7623
|
+
try {
|
|
7624
|
+
return res.status(201).json(await startTestSessionRun(session.id, project, {
|
|
7625
|
+
command: retry.command,
|
|
7626
|
+
selection: retry.selection,
|
|
7627
|
+
retryOfRunId: retry.executionRun.id,
|
|
7628
|
+
deleteOnComplete: true,
|
|
7629
|
+
}, io));
|
|
7630
|
+
}
|
|
7631
|
+
catch (error) {
|
|
7632
|
+
await deleteTestSession(session.id, io).catch(() => undefined);
|
|
7633
|
+
return res.status(409).json({ error: getErrorMessage(error) });
|
|
7634
|
+
}
|
|
7635
|
+
});
|
|
6666
7636
|
app.post('/tickets/:id/test-sessions', (req, res) => {
|
|
6667
7637
|
const ticket = getTicket(req.params.id);
|
|
6668
7638
|
if (!ticket) {
|
|
@@ -6765,7 +7735,7 @@ app.post('/test-sessions/:id/stop', async (req, res) => {
|
|
|
6765
7735
|
}
|
|
6766
7736
|
});
|
|
6767
7737
|
app.delete('/test-sessions/:id', async (req, res) => {
|
|
6768
|
-
const deleted = await deleteTestSession(req.params.id, io);
|
|
7738
|
+
const deleted = await deleteTestSession(req.params.id, io, { purgeExecutionRun: true });
|
|
6769
7739
|
if (!deleted) {
|
|
6770
7740
|
return res.status(404).json({ error: 'Not found' });
|
|
6771
7741
|
}
|
|
@@ -6846,6 +7816,16 @@ app.post('/tickets/:id/stop-build', async (req, res) => {
|
|
|
6846
7816
|
}
|
|
6847
7817
|
return res.status(result.status).json(result.value);
|
|
6848
7818
|
});
|
|
7819
|
+
app.post('/tickets/:id/stop-review', async (req, res) => {
|
|
7820
|
+
const result = await stopReviewTicketRun({
|
|
7821
|
+
ticketId: req.params.id,
|
|
7822
|
+
requestedProjectId: getRequestedProjectId(req),
|
|
7823
|
+
});
|
|
7824
|
+
if (!result.ok) {
|
|
7825
|
+
return res.status(result.status).json({ error: result.error });
|
|
7826
|
+
}
|
|
7827
|
+
return res.status(result.status).json(result.value);
|
|
7828
|
+
});
|
|
6849
7829
|
app.post('/tickets/:id/resume-build', (req, res) => {
|
|
6850
7830
|
const result = resumeBuildTicketRun({
|
|
6851
7831
|
ticketId: req.params.id,
|
|
@@ -6908,8 +7888,14 @@ app.post('/tickets/:id/plan', (req, res) => {
|
|
|
6908
7888
|
});
|
|
6909
7889
|
}
|
|
6910
7890
|
}
|
|
6911
|
-
startPlan(ticket, io, {
|
|
6912
|
-
|
|
7891
|
+
const scheduling = startPlan(ticket, io, {
|
|
7892
|
+
branch,
|
|
7893
|
+
});
|
|
7894
|
+
if (!scheduling.accepted) {
|
|
7895
|
+
const status = scheduling.reason === 'queue_failed' ? 503 : 409;
|
|
7896
|
+
return res.status(status).json({ error: scheduling.error });
|
|
7897
|
+
}
|
|
7898
|
+
return res.status(202).json({ ok: true, execution_run_id: scheduling.executionContext.runId });
|
|
6913
7899
|
});
|
|
6914
7900
|
app.delete('/tickets/:id', async (req, res) => {
|
|
6915
7901
|
const ticket = getTicket(req.params.id);
|
|
@@ -6926,12 +7912,7 @@ app.delete('/tickets/:id', async (req, res) => {
|
|
|
6926
7912
|
if (result.affectedTicketIds) {
|
|
6927
7913
|
emitUpdatedTickets(result.affectedTicketIds);
|
|
6928
7914
|
}
|
|
6929
|
-
|
|
6930
|
-
const nextTicket = getTicket(result.nextBuildTicketId);
|
|
6931
|
-
if (nextTicket) {
|
|
6932
|
-
startBuild(nextTicket, io);
|
|
6933
|
-
}
|
|
6934
|
-
}
|
|
7915
|
+
scheduleBuildAfterTicketDeletion(result);
|
|
6935
7916
|
res.status(204).end();
|
|
6936
7917
|
});
|
|
6937
7918
|
const clientBuild = resolveClientBuildPaths(import.meta.url);
|
|
@@ -6958,6 +7939,33 @@ io.on('connection', (socket) => {
|
|
|
6958
7939
|
registerTerminalSocketHandlers(io, socket);
|
|
6959
7940
|
});
|
|
6960
7941
|
const DEFAULT_PORT = 3001;
|
|
7942
|
+
let executionEvidenceCleanupTimer = null;
|
|
7943
|
+
let startServerPromise = null;
|
|
7944
|
+
let shutdownServerPromise = null;
|
|
7945
|
+
let shutdownRequested = false;
|
|
7946
|
+
const throwIfShutdownRequested = () => {
|
|
7947
|
+
if (shutdownRequested) {
|
|
7948
|
+
throw new Error('Server startup was cancelled by shutdown');
|
|
7949
|
+
}
|
|
7950
|
+
};
|
|
7951
|
+
const runExecutionEvidenceCleanup = () => {
|
|
7952
|
+
try {
|
|
7953
|
+
const result = cleanupAllExecutionEvidence({ retentionDays: resolveExecutionEvidenceRetentionDays() });
|
|
7954
|
+
if (result.total > 0) {
|
|
7955
|
+
console.log(`Expired execution evidence for ${result.total} run(s): ${result.test_output} test output, ${result.github_jobs} GitHub job snapshot(s)`);
|
|
7956
|
+
}
|
|
7957
|
+
}
|
|
7958
|
+
catch (error) {
|
|
7959
|
+
console.error('Failed to clean up expired execution evidence', error);
|
|
7960
|
+
}
|
|
7961
|
+
};
|
|
7962
|
+
const startExecutionEvidenceCleanupTimer = () => {
|
|
7963
|
+
if (executionEvidenceCleanupTimer) {
|
|
7964
|
+
return;
|
|
7965
|
+
}
|
|
7966
|
+
executionEvidenceCleanupTimer = setInterval(runExecutionEvidenceCleanup, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS);
|
|
7967
|
+
executionEvidenceCleanupTimer.unref();
|
|
7968
|
+
};
|
|
6961
7969
|
const resolveStartResult = (host) => {
|
|
6962
7970
|
const address = httpServer.address();
|
|
6963
7971
|
const { displayHost } = resolveDesktopServerHost(host);
|
|
@@ -6975,37 +7983,83 @@ const resolveStartResult = (host) => {
|
|
|
6975
7983
|
};
|
|
6976
7984
|
};
|
|
6977
7985
|
export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
7986
|
+
if (shutdownRequested) {
|
|
7987
|
+
return Promise.reject(new Error('Server startup was cancelled by shutdown'));
|
|
7988
|
+
}
|
|
7989
|
+
if (httpServer.listening) {
|
|
7990
|
+
return Promise.resolve(resolveStartResult(host));
|
|
7991
|
+
}
|
|
7992
|
+
if (startServerPromise) {
|
|
7993
|
+
return startServerPromise;
|
|
7994
|
+
}
|
|
7995
|
+
startServerPromise = (async () => {
|
|
7996
|
+
await reconcileStartupExecutions({ getGitHubConfig: getGitHubConnectionConfig });
|
|
7997
|
+
throwIfShutdownRequested();
|
|
7998
|
+
startGitHubActionsPolling(io);
|
|
7999
|
+
try {
|
|
8000
|
+
throwIfShutdownRequested();
|
|
8001
|
+
await new Promise((resolve, reject) => {
|
|
8002
|
+
const handleError = (error) => {
|
|
8003
|
+
httpServer.off('error', handleError);
|
|
8004
|
+
reject(error);
|
|
8005
|
+
};
|
|
8006
|
+
const handleListening = () => {
|
|
8007
|
+
httpServer.off('error', handleError);
|
|
8008
|
+
resolve();
|
|
8009
|
+
};
|
|
8010
|
+
httpServer.once('error', handleError);
|
|
8011
|
+
const { bindHost } = resolveDesktopServerHost(host);
|
|
8012
|
+
httpServer.listen(port, bindHost, handleListening);
|
|
8013
|
+
});
|
|
6982
8014
|
}
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
}
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
8015
|
+
catch (error) {
|
|
8016
|
+
await clearAllGitHubActionsSessions();
|
|
8017
|
+
throw error;
|
|
8018
|
+
}
|
|
8019
|
+
throwIfShutdownRequested();
|
|
8020
|
+
runExecutionEvidenceCleanup();
|
|
8021
|
+
throwIfShutdownRequested();
|
|
8022
|
+
startExecutionEvidenceCleanupTimer();
|
|
8023
|
+
return resolveStartResult(host);
|
|
8024
|
+
})().finally(() => {
|
|
8025
|
+
startServerPromise = null;
|
|
6994
8026
|
});
|
|
8027
|
+
return startServerPromise;
|
|
6995
8028
|
};
|
|
6996
8029
|
export const shutdownServer = async (signal) => {
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
8030
|
+
if (shutdownServerPromise) {
|
|
8031
|
+
return shutdownServerPromise;
|
|
8032
|
+
}
|
|
8033
|
+
shutdownRequested = true;
|
|
8034
|
+
const pendingStartup = startServerPromise;
|
|
8035
|
+
shutdownServerPromise = (async () => {
|
|
8036
|
+
await Promise.all([
|
|
8037
|
+
pendingStartup?.catch(() => undefined),
|
|
8038
|
+
stopAllPlanAgents(),
|
|
8039
|
+
]);
|
|
8040
|
+
if (executionEvidenceCleanupTimer) {
|
|
8041
|
+
clearInterval(executionEvidenceCleanupTimer);
|
|
8042
|
+
executionEvidenceCleanupTimer = null;
|
|
8043
|
+
}
|
|
8044
|
+
await stopMobileAccess();
|
|
8045
|
+
await clearAllTestSessions(io);
|
|
8046
|
+
await clearAllGitHubActionsSessions();
|
|
8047
|
+
await shutdownRealtimeServer({
|
|
8048
|
+
signal,
|
|
8049
|
+
io,
|
|
8050
|
+
httpServer,
|
|
8051
|
+
closeAllTerminalSessions,
|
|
8052
|
+
clearAllPreviewProxies,
|
|
8053
|
+
stopAllBuildAgents,
|
|
8054
|
+
stopAllReviewAgents,
|
|
8055
|
+
stopAllPlanAgents,
|
|
8056
|
+
stopAllTickets,
|
|
8057
|
+
});
|
|
8058
|
+
})().finally(() => {
|
|
8059
|
+
shutdownRequested = false;
|
|
8060
|
+
shutdownServerPromise = null;
|
|
7008
8061
|
});
|
|
8062
|
+
return shutdownServerPromise;
|
|
7009
8063
|
};
|
|
7010
8064
|
export const registerShutdownHandlers = () => {
|
|
7011
8065
|
const createHandler = (signal) => {
|