@straiffi/archon 1.7.3 → 1.7.5
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-CT9iCPyc.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-BA6xyQeX.js +1 -0
- package/dist/client/assets/TestsDialog-D2qrW5Ug.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BzzGotac.js → architectureDiagram-3BPJPVTR-R6v7w0Nl.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-DIcqLHKg.js → blockDiagram-GPEHLZMM-CHlBlnJq.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-Hc1Sqmhs.js → c4Diagram-AAUBKEIU-BvZKqUc6.js} +1 -1
- package/dist/client/assets/channel-Ckv8cPSf.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-CWSwjsgk.js → chunk-2J33WTMH-2gsIkBeO.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-BnX3DljN.js → chunk-3OPIFGDE-CcqPBjqx.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-EyyJlI-d.js → chunk-5ZQYHXKU-DX_wK5Rp.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-Ci6CfxWj.js → chunk-727SXJPM-B9WUxzJL.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-RKZNsJTm.js → chunk-AQP2D5EJ-BG0s75_H.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-DBf0Euoj.js → chunk-CSCIHK7Q-CyNcvvqW.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-BmLQI_2S.js → chunk-KSCS5N6A-CWVy_Wi0.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-DKwVAb8G.js → chunk-L5ZTLDWV-3dsXRB0X.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-axotKkbz.js → chunk-LZXEDZCA-B09QSB9z.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-BBlJ4nXC.js → chunk-ND2GUHAM-BxzAdxTN.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-BKJGoZZt.js → chunk-NZK2D7GU-D4ZfFwbB.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-C-Ghigo1.js → chunk-O5CBEL6O-BxQULuKS.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-CCzVWYcd.js → chunk-WU5MYG2G-CEJwp0Ry.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-B-N-arsi.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-PTX7YrlL.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-rwIdPcjI.js → dagre-BM42HDAG-Dx_V7OMy.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-DJWxOQsg.js → diagram-2AECGRRQ-Cc3VTg9P.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-C-mQeFtd.js → diagram-5GNKFQAL-mJ1xhs2G.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BAu6u4sR.js → diagram-KO2AKTUF-CLyT9ac8.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-U6CbvBn0.js → diagram-LMA3HP47-CVr47yij.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-DNTwFcs1.js → diagram-OG6HWLK6-Dp5TJ8lM.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-DaHX6Xw5.js → erDiagram-TEJ5UH35-BBxwyu0r.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-B9hKqL2l.js → flowDiagram-I6XJVG4X-CuFYzVQ-.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-BQ1HPHVO.js → ganttDiagram-6RSMTGT7-aRQarcQY.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BBRcsmMK.js → gitGraphDiagram-PVQCEYII-B3mHSkBv.js} +1 -1
- package/dist/client/assets/index-BJGHMaMz.css +2 -0
- package/dist/client/assets/index-Oz0S7bcT.js +273 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-DUoNiSGC.js → infoDiagram-5YYISTIA-BSyryQJ9.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DJN1y2w8.js → ishikawaDiagram-YF4QCWOH-BAGbrtSG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-BxLLMT70.js → journeyDiagram-JHISSGLW-CDTht6k6.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BdCPistJ.js → kanban-definition-UN3LZRKU-BDtuMp0H.js} +1 -1
- package/dist/client/assets/{line-4mIgXsZd.js → line-n0aNicmA.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-Pu1QGWJ-.js → mermaid-parser.core-CdE4vZ4k.js} +1 -1
- package/dist/client/assets/{mermaid.core-DnH9DrgZ.js → mermaid.core-C546CQj1.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-DqE4ba3C.js → mindmap-definition-RKZ34NQL-BwO779am.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-Crjy9ejf.js → pieDiagram-4H26LBE5-DU1k71kn.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BUSZ2VB7.js → quadrantDiagram-W4KKPZXB-CvM5lyQg.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-CBhFKEoV.js → requirementDiagram-4Y6WPE33-DDBdSq0B.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CgdbY41v.js → sankeyDiagram-5OEKKPKP-ELM2mu81.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-hkp_H7AE.js → sequenceDiagram-3UESZ5HK-D7uF-nRR.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-C6x45RWp.js → stateDiagram-AJRCARHV-IhaGRCbG.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-ceHFQMIT.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-CjnZWttQ.js → timeline-definition-PNZ67QCA-zHJGFvZ8.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-DU5ZyYju.js → vennDiagram-CIIHVFJN-EG1HFfD7.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CaCEnCB0.js → wardleyDiagram-YWT4CUSO-DdQevxXg.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-Ce3xa37U.js → xychartDiagram-2RQKCTM6-BjihNG0E.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +283 -0
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +390 -117
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/attachments.js +40 -0
- package/dist/server/lib/attachments.js.map +1 -1
- package/dist/server/lib/buildOpenQuestionOperations.js +17 -2
- package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -1
- package/dist/server/lib/buildSequences.js +25 -0
- package/dist/server/lib/buildSequences.js.map +1 -1
- package/dist/server/lib/bundleReviewOperations.js +9 -0
- package/dist/server/lib/bundleReviewOperations.js.map +1 -1
- package/dist/server/lib/chatOperations.js +40 -16
- package/dist/server/lib/chatOperations.js.map +1 -1
- package/dist/server/lib/executionArtifactCleanupState.js +12 -0
- package/dist/server/lib/executionArtifactCleanupState.js.map +1 -0
- package/dist/server/lib/executionArtifactRetention.js +405 -0
- package/dist/server/lib/executionArtifactRetention.js.map +1 -0
- package/dist/server/lib/executionArtifacts.js +705 -0
- package/dist/server/lib/executionArtifacts.js.map +1 -0
- package/dist/server/lib/executionRetry.js +702 -0
- package/dist/server/lib/executionRetry.js.map +1 -0
- package/dist/server/lib/executionRetryContext.js +510 -0
- package/dist/server/lib/executionRetryContext.js.map +1 -0
- package/dist/server/lib/executionRetryPredicates.js +74 -0
- package/dist/server/lib/executionRetryPredicates.js.map +1 -0
- package/dist/server/lib/executionRuns.js +203 -40
- package/dist/server/lib/executionRuns.js.map +1 -1
- package/dist/server/lib/executionTimeline.js +73 -0
- package/dist/server/lib/executionTimeline.js.map +1 -0
- package/dist/server/lib/lightweightPrompt.js +2 -2
- package/dist/server/lib/lightweightPrompt.js.map +1 -1
- package/dist/server/lib/projects.js +16 -3
- package/dist/server/lib/projects.js.map +1 -1
- package/dist/server/lib/pullRequestContent.js +148 -25
- package/dist/server/lib/pullRequestContent.js.map +1 -1
- package/dist/server/lib/queue.js +1 -0
- package/dist/server/lib/queue.js.map +1 -1
- package/dist/server/lib/runtimePaths.js +7 -0
- package/dist/server/lib/runtimePaths.js.map +1 -1
- package/dist/server/lib/specOperations.js +51 -16
- package/dist/server/lib/specOperations.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +10 -4
- package/dist/server/lib/specReviewContext.js.map +1 -1
- package/dist/server/lib/specReviewOperations.js +33 -5
- package/dist/server/lib/specReviewOperations.js.map +1 -1
- package/dist/server/lib/testSessions.js +2 -5
- package/dist/server/lib/testSessions.js.map +1 -1
- package/dist/server/lib/ticketAutomationCommit.js +11 -4
- package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
- package/dist/server/lib/ticketFollowUpOperations.js +74 -27
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketMessages.js +16 -0
- package/dist/server/lib/ticketMessages.js.map +1 -1
- package/dist/server/lib/types.js +12 -0
- package/dist/server/lib/types.js.map +1 -1
- package/dist/server/workers/build.js +1 -1
- package/dist/server/workers/build.js.map +1 -1
- package/dist/server/workers/chat.js +26 -20
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +118 -15
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +23 -4
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +12 -9
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +2 -2
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +2 -2
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +29 -22
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/ActivityView-DTEenQas.js +0 -1
- package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +0 -1
- package/dist/client/assets/TestsDialog-cBaoffCs.js +0 -4
- package/dist/client/assets/channel-Cym17waa.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +0 -1
- package/dist/client/assets/index-CEMaJCPS.js +0 -273
- package/dist/client/assets/index-lWftq65U.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generato
|
|
|
10
10
|
import db from './db.js';
|
|
11
11
|
import { deleteChatSession, getChatSession, listChatSessions, updateChatSessionMode } from './lib/chats.js';
|
|
12
12
|
import { deleteSpecSession, getCreatedTicketIdsForSnapshot, getSpecSession, getSpecSnapshot, listSpecSessions } from './lib/specs.js';
|
|
13
|
-
import { createChatAttachment, createSpecAttachment, getAttachmentById, deleteChatAttachment, deleteSpecSessionAttachment, getChatAttachmentForSession, getChatAttachmentLinkedMessageId, getSpecAttachmentForSession, getSpecAttachmentLinkedMessageId, MAX_CHAT_ATTACHMENTS_PER_TURN, MAX_CHAT_ATTACHMENT_SIZE_BYTES, MAX_SPEC_ATTACHMENTS_PER_TURN, MAX_SPEC_ATTACHMENT_SIZE_BYTES, readChatAttachmentContent, readSpecAttachmentContent, validateImageAttachment, } from './lib/attachments.js';
|
|
13
|
+
import { createChatAttachment, createSpecAttachment, getAttachmentById, deleteChatAttachment, deleteProjectAttachments, deleteSpecSessionAttachment, getChatAttachmentForSession, getChatAttachmentLinkedMessageId, getSpecAttachmentForSession, getSpecAttachmentLinkedMessageId, MAX_CHAT_ATTACHMENTS_PER_TURN, MAX_CHAT_ATTACHMENT_SIZE_BYTES, MAX_SPEC_ATTACHMENTS_PER_TURN, MAX_SPEC_ATTACHMENT_SIZE_BYTES, readChatAttachmentContent, readSpecAttachmentContent, validateImageAttachment, } from './lib/attachments.js';
|
|
14
14
|
import { createProjectChatSession, resolveChatMode, resolveChatTool, stopProjectChatSessionResponse, submitProjectChatSessionMessage, } from './lib/chatOperations.js';
|
|
15
15
|
import { createProjectSpecSession, deleteProjectSpecSession, stopProjectSpecSessionResponse, submitProjectSpecSessionMessage, updateProjectSpecSessionMode, } from './lib/specOperations.js';
|
|
16
16
|
import { buildProjectSpecReviewRevisionDraft, getProjectSpecReviewFindings, startProjectSpecReview, stopProjectSpecReview, } from './lib/specReviewOperations.js';
|
|
@@ -54,8 +54,12 @@ import { clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAut
|
|
|
54
54
|
import { runAutomationCommitChain } from './lib/ticketAutomationCommit.js';
|
|
55
55
|
import { getDiscoveredSkills, normalizeSkillNames } from './lib/skills.js';
|
|
56
56
|
import { clearAllPreviewProxies } from './lib/previewProxy.js';
|
|
57
|
-
import { createExecutionRun, addExecutionRunLink, addExecutionRunLinks, getExecutionRun, hasActiveBuildExecutionRunForTicket, getGitHubActionsExecutionDetails,
|
|
57
|
+
import { createExecutionRun, addExecutionRunLink, addExecutionRunLinks, getExecutionRun, hasActiveBuildExecutionRunForTicket, getGitHubActionsExecutionDetails, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listLifecycleEvents, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
|
|
58
|
+
import { dispatchExecutionRetry, getExecutionRetryEligibility, resolveExecutionRetry } from './lib/executionRetry.js';
|
|
59
|
+
import { listExecutionTimeline } from './lib/executionTimeline.js';
|
|
60
|
+
import { loadExecutionArtifactSummariesForRuns, openExecutionArtifactForRetrieval, } from './lib/executionArtifacts.js';
|
|
58
61
|
import { cleanupAllExecutionEvidence, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS, resolveExecutionEvidenceRetentionDays, } from './lib/executionRunRetention.js';
|
|
62
|
+
import { ExecutionArtifactCleanupError, reconcileExecutionArtifactRegistrationIntents, reconcileFinalizedExecutionArtifacts, reconcileProjectExecutionArtifacts, resolveArtifactCleanupIntervalMs, } from './lib/executionArtifactRetention.js';
|
|
59
63
|
import { shutdownRealtimeServer } from './lib/shutdown.js';
|
|
60
64
|
import { resolveClientBuildPaths, shouldServeClientApp, STATIC_CLIENT_DIRECTORY_OPTIONS, STATIC_CLIENT_INDEX_OPTIONS, } from './lib/staticClient.js';
|
|
61
65
|
import { deleteIntegrationConnection, getGitHubConnectionConfig, getJiraConnectionConfig, listIntegrationConnections, upsertIntegrationConnection, } from './lib/integrations/index.js';
|
|
@@ -70,9 +74,9 @@ import { PlanningReferenceError, resolveExternalPlanningDescription } from './li
|
|
|
70
74
|
import { refreshIfNeeded as refreshAssignedIssues, clearCachedAssignedIssues } from './lib/integrations/assignedIssuesCache.js';
|
|
71
75
|
import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib/commitMessage.js';
|
|
72
76
|
import { BUNDLE_PULL_REQUEST_SYNC_TTL_MS, getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, syncTrackedBundlePullRequestIfNeeded, upsertBundlePullRequest, upsertBundlePullRequestFromGitHub, } from './lib/bundlePullRequests.js';
|
|
73
|
-
import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
|
|
77
|
+
import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listInterruptedProjectDeletions, listProjects, releaseInterruptedProjectDeletionClaims, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
|
|
74
78
|
import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
|
|
75
|
-
import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession,
|
|
79
|
+
import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
|
|
76
80
|
import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTicketState, } from './lib/ticketWorkflowOperations.js';
|
|
77
81
|
import { submitPlanTicketFollowUp, submitTicketFollowUp, } from './lib/ticketFollowUpOperations.js';
|
|
78
82
|
import { canUseBuildControls, formatActiveRunConflict, getTicketRunReadinessError, resumeBuildTicketRun, runProjectTarget, runTicketRun, stopBuildTicketRun, stopReviewTicketRun, stopProjectTargetRun, stopTicketRun, } from './lib/ticketRunOperations.js';
|
|
@@ -92,7 +96,30 @@ import { exportSpecToDocs, checkSpecExportCollision } from './lib/specExport.js'
|
|
|
92
96
|
import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
|
|
93
97
|
import { startBuild } from './workers/build.js';
|
|
94
98
|
import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, prepareLinkedPlanningResearch, startPlan, stopAllPlanAgents, } from './workers/plan.js';
|
|
95
|
-
import { EXECUTION_RUN_KINDS, validateWorkflowContext } from './lib/types.js';
|
|
99
|
+
import { EXECUTION_RUN_KINDS, MAX_ARTIFACTS_PER_RUN, validateWorkflowContext } from './lib/types.js';
|
|
100
|
+
class ProjectDeletionError extends Error {
|
|
101
|
+
status;
|
|
102
|
+
constructor(status, message) {
|
|
103
|
+
super(message);
|
|
104
|
+
this.name = 'ProjectDeletionError';
|
|
105
|
+
this.status = status;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
let executionArtifactMaintenanceTail = Promise.resolve();
|
|
109
|
+
const runExclusiveExecutionArtifactMaintenance = async (task) => {
|
|
110
|
+
const previous = executionArtifactMaintenanceTail;
|
|
111
|
+
let release = () => undefined;
|
|
112
|
+
executionArtifactMaintenanceTail = new Promise(resolve => {
|
|
113
|
+
release = resolve;
|
|
114
|
+
});
|
|
115
|
+
await previous;
|
|
116
|
+
try {
|
|
117
|
+
return await task();
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
release();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
96
123
|
const getErrorMessage = (error) => {
|
|
97
124
|
return error instanceof Error ? error.message : String(error);
|
|
98
125
|
};
|
|
@@ -110,6 +137,9 @@ const requireExistingProject = (projectId) => {
|
|
|
110
137
|
if (!project) {
|
|
111
138
|
return { status: 404, error: 'Not found' };
|
|
112
139
|
}
|
|
140
|
+
if (project.deletion_started_at) {
|
|
141
|
+
return { status: 409, error: 'Project deletion is in progress' };
|
|
142
|
+
}
|
|
113
143
|
return { project };
|
|
114
144
|
};
|
|
115
145
|
const requireProjectMemoryProject = (projectId) => {
|
|
@@ -167,6 +197,44 @@ const parseLifecycleListQuery = (query) => {
|
|
|
167
197
|
limit,
|
|
168
198
|
};
|
|
169
199
|
};
|
|
200
|
+
const parseExecutionTimelineScope = (projectId, query) => {
|
|
201
|
+
const hasScopeType = query.scope_type != null && query.scope_type !== '';
|
|
202
|
+
const hasScopeId = query.scope_id != null && query.scope_id !== '';
|
|
203
|
+
if (hasScopeType !== hasScopeId) {
|
|
204
|
+
return { error: 'scope_type and scope_id must be provided together' };
|
|
205
|
+
}
|
|
206
|
+
if (!hasScopeType) {
|
|
207
|
+
return { scopeEntity: undefined };
|
|
208
|
+
}
|
|
209
|
+
if (typeof query.scope_type !== 'string' || typeof query.scope_id !== 'string') {
|
|
210
|
+
return { error: 'scope_type and scope_id must be strings' };
|
|
211
|
+
}
|
|
212
|
+
const scopeType = query.scope_type;
|
|
213
|
+
const scopeId = query.scope_id.trim();
|
|
214
|
+
if (!['ticket', 'bundle', 'chat_session', 'spec_session'].includes(scopeType)) {
|
|
215
|
+
return { error: 'scope_type must be ticket, bundle, chat_session, or spec_session' };
|
|
216
|
+
}
|
|
217
|
+
if (!scopeId) {
|
|
218
|
+
return { error: 'scope_id must not be empty' };
|
|
219
|
+
}
|
|
220
|
+
const table = scopeType === 'ticket'
|
|
221
|
+
? 'tickets'
|
|
222
|
+
: scopeType === 'bundle'
|
|
223
|
+
? 'worktree_bundles'
|
|
224
|
+
: scopeType === 'chat_session'
|
|
225
|
+
? 'chat_sessions'
|
|
226
|
+
: 'spec_sessions';
|
|
227
|
+
const exists = db.prepare(`SELECT 1 FROM ${table} WHERE id = ? AND project_id = ?`).get(scopeId, projectId);
|
|
228
|
+
if (!exists) {
|
|
229
|
+
return { error: 'Scope not found', status: 404 };
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
scopeEntity: {
|
|
233
|
+
type: scopeType,
|
|
234
|
+
id: scopeId,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
};
|
|
170
238
|
const serializeTestExecutionDetail = (details, retryEligibility) => {
|
|
171
239
|
if (!details) {
|
|
172
240
|
return { kind: 'test', available: false, retry_eligibility: retryEligibility };
|
|
@@ -221,6 +289,26 @@ const serializeGitHubActionsExecutionDetail = (details) => {
|
|
|
221
289
|
jobs_count: details.jobs_count,
|
|
222
290
|
};
|
|
223
291
|
};
|
|
292
|
+
const buildExecutionRunDetailResponse = (project, run) => {
|
|
293
|
+
const retryEligibility = getExecutionRetryEligibility(project.id, run.id);
|
|
294
|
+
const details = run.kind === 'test'
|
|
295
|
+
? serializeTestExecutionDetail(getTestExecutionDetails(project.id, run.id), retryEligibility)
|
|
296
|
+
: run.kind === 'github_actions'
|
|
297
|
+
? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
|
|
298
|
+
: ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
|
|
299
|
+
? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
|
|
300
|
+
: { kind: run.kind, available: true, retry_eligibility: retryEligibility };
|
|
301
|
+
const artifacts = loadExecutionArtifactSummariesForRuns(project.id, [run.id], MAX_ARTIFACTS_PER_RUN).get(run.id)
|
|
302
|
+
?? { artifacts: [], artifact_count: 0 };
|
|
303
|
+
return {
|
|
304
|
+
run,
|
|
305
|
+
links: listExecutionRunLinks(project.id, run.id),
|
|
306
|
+
events: listLifecycleEvents(project.id, run.id).map(toCanonicalLifecycleEvent),
|
|
307
|
+
details,
|
|
308
|
+
agent_runs: listAgentRunsForExecutionRun(run.id),
|
|
309
|
+
...artifacts,
|
|
310
|
+
};
|
|
311
|
+
};
|
|
224
312
|
const formatWorktreeDeleteError = (target, branch, error) => {
|
|
225
313
|
return `Unable to delete the ${target} worktree for branch "${branch}": ${getErrorMessage(error)}`;
|
|
226
314
|
};
|
|
@@ -947,63 +1035,6 @@ const parseTestTargetSelection = (value) => {
|
|
|
947
1035
|
}
|
|
948
1036
|
return null;
|
|
949
1037
|
};
|
|
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
|
-
};
|
|
1007
1038
|
const resolveProjectFileSuggestionWorkspace = (project, options = {}) => {
|
|
1008
1039
|
const ticketId = options.ticketId?.trim() || null;
|
|
1009
1040
|
if (ticketId) {
|
|
@@ -2417,6 +2448,17 @@ app.post('/projects/:id/chat-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2417
2448
|
if (error) {
|
|
2418
2449
|
return res.status(400).json({ error: getChatAttachmentUploadErrorMessage(error) });
|
|
2419
2450
|
}
|
|
2451
|
+
const currentProject = getProjectById(req.params.id);
|
|
2452
|
+
if (!currentProject) {
|
|
2453
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2454
|
+
}
|
|
2455
|
+
const currentChatSession = getProjectChatSession(currentProject.id, req.params.sessionId);
|
|
2456
|
+
if (!currentChatSession) {
|
|
2457
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2458
|
+
}
|
|
2459
|
+
if (currentChatSession.agent_status === 'running') {
|
|
2460
|
+
return res.status(409).json({ error: 'Wait for the current chat response to finish before uploading images.' });
|
|
2461
|
+
}
|
|
2420
2462
|
const files = (req.files ?? []);
|
|
2421
2463
|
if (files.length === 0) {
|
|
2422
2464
|
return res.status(400).json({ error: 'At least one image is required.' });
|
|
@@ -2434,11 +2476,11 @@ app.post('/projects/:id/chat-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2434
2476
|
try {
|
|
2435
2477
|
const attachments = files.map(file => {
|
|
2436
2478
|
const attachment = createChatAttachment({
|
|
2437
|
-
chatSessionId:
|
|
2479
|
+
chatSessionId: currentChatSession.id,
|
|
2438
2480
|
originalName: file.originalname,
|
|
2439
2481
|
mimeType: file.mimetype,
|
|
2440
2482
|
buffer: file.buffer,
|
|
2441
|
-
projectId:
|
|
2483
|
+
projectId: currentProject.id,
|
|
2442
2484
|
});
|
|
2443
2485
|
createdAttachmentIds.push(attachment.id);
|
|
2444
2486
|
return attachment;
|
|
@@ -2709,6 +2751,17 @@ app.post('/projects/:id/spec-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2709
2751
|
if (error) {
|
|
2710
2752
|
return res.status(400).json({ error: getSpecAttachmentUploadErrorMessage(error) });
|
|
2711
2753
|
}
|
|
2754
|
+
const currentProject = getProjectById(req.params.id);
|
|
2755
|
+
if (!currentProject) {
|
|
2756
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2757
|
+
}
|
|
2758
|
+
const currentSpecSession = getProjectSpecSession(currentProject.id, req.params.sessionId);
|
|
2759
|
+
if (!currentSpecSession) {
|
|
2760
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2761
|
+
}
|
|
2762
|
+
if (currentSpecSession.agent_status === 'running') {
|
|
2763
|
+
return res.status(409).json({ error: 'Wait for the current spec response to finish before uploading images.' });
|
|
2764
|
+
}
|
|
2712
2765
|
const files = (req.files ?? []);
|
|
2713
2766
|
if (files.length === 0) {
|
|
2714
2767
|
return res.status(400).json({ error: 'At least one image is required.' });
|
|
@@ -2726,11 +2779,11 @@ app.post('/projects/:id/spec-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2726
2779
|
try {
|
|
2727
2780
|
const attachments = files.map(file => {
|
|
2728
2781
|
const attachment = createSpecAttachment({
|
|
2729
|
-
specSessionId:
|
|
2782
|
+
specSessionId: currentSpecSession.id,
|
|
2730
2783
|
originalName: file.originalname,
|
|
2731
2784
|
mimeType: file.mimetype,
|
|
2732
2785
|
buffer: file.buffer,
|
|
2733
|
-
projectId:
|
|
2786
|
+
projectId: currentProject.id,
|
|
2734
2787
|
});
|
|
2735
2788
|
createdAttachmentIds.push(attachment.id);
|
|
2736
2789
|
return attachment;
|
|
@@ -6676,32 +6729,114 @@ app.patch('/projects/:id', (req, res) => {
|
|
|
6676
6729
|
const updated = db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id);
|
|
6677
6730
|
res.json(serializeProject(updated ?? null));
|
|
6678
6731
|
});
|
|
6679
|
-
|
|
6680
|
-
const project =
|
|
6732
|
+
const assertProjectDeletionEligible = (projectId, claimId) => {
|
|
6733
|
+
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
|
6681
6734
|
if (!project) {
|
|
6682
|
-
|
|
6735
|
+
throw new ProjectDeletionError(404, 'Not found');
|
|
6683
6736
|
}
|
|
6684
|
-
|
|
6737
|
+
if (claimId ? project.deletion_claim_id !== claimId : Boolean(project.deletion_started_at)) {
|
|
6738
|
+
throw new ProjectDeletionError(409, 'Project deletion is already in progress');
|
|
6739
|
+
}
|
|
6740
|
+
const projectCount = Number(db.prepare(`
|
|
6741
|
+
SELECT COUNT(*) AS count FROM projects
|
|
6742
|
+
WHERE deletion_started_at IS NULL OR id = ?
|
|
6743
|
+
`).get(projectId)?.count ?? 0);
|
|
6685
6744
|
if (projectCount <= 1) {
|
|
6686
|
-
|
|
6745
|
+
throw new ProjectDeletionError(409, 'Cannot delete the last remaining project');
|
|
6687
6746
|
}
|
|
6688
|
-
if (countInboundProjectLinks(
|
|
6689
|
-
|
|
6747
|
+
if (countInboundProjectLinks(projectId) > 0) {
|
|
6748
|
+
throw new ProjectDeletionError(409, 'Cannot delete a project that is linked from another project');
|
|
6690
6749
|
}
|
|
6691
|
-
const stats = getProjectTicketStats(
|
|
6750
|
+
const stats = getProjectTicketStats(projectId);
|
|
6692
6751
|
if (stats.ticketCount > 0 || stats.worktreeCount > 0 || stats.bundleCount > 0) {
|
|
6693
|
-
|
|
6694
|
-
error: 'Cannot delete a project with existing tickets, bundles, or worktrees',
|
|
6695
|
-
});
|
|
6752
|
+
throw new ProjectDeletionError(409, 'Cannot delete a project with existing tickets, bundles, or worktrees');
|
|
6696
6753
|
}
|
|
6697
|
-
|
|
6754
|
+
};
|
|
6755
|
+
const completeClaimedProjectDeletion = async (projectId, claimId) => {
|
|
6756
|
+
await runExclusiveExecutionArtifactMaintenance(() => reconcileProjectExecutionArtifacts(projectId, {
|
|
6757
|
+
emitter: io,
|
|
6758
|
+
beginCleanup: () => {
|
|
6759
|
+
const marked = db.prepare(`
|
|
6760
|
+
UPDATE projects
|
|
6761
|
+
SET deletion_cleanup_started_at = COALESCE(deletion_cleanup_started_at, CURRENT_TIMESTAMP)
|
|
6762
|
+
WHERE id = ? AND deletion_claim_id = ?
|
|
6763
|
+
`).run(projectId, claimId);
|
|
6764
|
+
if (marked.changes !== 1) {
|
|
6765
|
+
throw new ProjectDeletionError(409, 'Project deletion ownership was lost');
|
|
6766
|
+
}
|
|
6767
|
+
},
|
|
6768
|
+
finalize: () => {
|
|
6769
|
+
db.transaction(() => {
|
|
6770
|
+
assertProjectDeletionEligible(projectId, claimId);
|
|
6771
|
+
if (db.prepare('SELECT 1 FROM execution_artifact_registration_intents WHERE project_id = ? LIMIT 1').get(projectId)) {
|
|
6772
|
+
throw new ExecutionArtifactCleanupError('active_registration', 'Cannot delete project while an artifact registration is active');
|
|
6773
|
+
}
|
|
6774
|
+
if (db.prepare("SELECT 1 FROM execution_artifacts WHERE project_id = ? AND availability <> 'deleted' LIMIT 1").get(projectId)) {
|
|
6775
|
+
throw new ExecutionArtifactCleanupError('cleanup_failed', 'Cannot delete project because artifact file cleanup failed');
|
|
6776
|
+
}
|
|
6777
|
+
db.prepare("DELETE FROM execution_artifacts WHERE project_id = ? AND availability = 'deleted'").run(projectId);
|
|
6778
|
+
deleteProjectAttachments(projectId);
|
|
6779
|
+
db.prepare(`
|
|
6780
|
+
UPDATE execution_runs
|
|
6781
|
+
SET parent_run_id = NULL, retry_of_run_id = NULL
|
|
6782
|
+
WHERE project_id = ?
|
|
6783
|
+
`).run(projectId);
|
|
6784
|
+
const deleted = db.prepare('DELETE FROM projects WHERE id = ? AND deletion_claim_id = ?').run(projectId, claimId);
|
|
6785
|
+
if (deleted.changes !== 1) {
|
|
6786
|
+
throw new ProjectDeletionError(409, 'Project deletion ownership was lost');
|
|
6787
|
+
}
|
|
6788
|
+
})();
|
|
6789
|
+
},
|
|
6790
|
+
}));
|
|
6791
|
+
};
|
|
6792
|
+
export const resumeInterruptedProjectDeletions = async () => {
|
|
6793
|
+
for (const project of listInterruptedProjectDeletions()) {
|
|
6794
|
+
try {
|
|
6795
|
+
await completeClaimedProjectDeletion(project.id, project.deletion_claim_id);
|
|
6796
|
+
}
|
|
6797
|
+
catch (error) {
|
|
6798
|
+
console.error(`Failed to resume deletion for project ${project.id}`, error);
|
|
6799
|
+
}
|
|
6800
|
+
}
|
|
6801
|
+
};
|
|
6802
|
+
app.delete('/projects/:id', async (req, res) => {
|
|
6803
|
+
const claimId = randomUUID();
|
|
6804
|
+
try {
|
|
6805
|
+
db.transaction(() => {
|
|
6806
|
+
assertProjectDeletionEligible(req.params.id);
|
|
6807
|
+
const claimed = db.prepare(`
|
|
6808
|
+
UPDATE projects
|
|
6809
|
+
SET deletion_started_at = CURRENT_TIMESTAMP, deletion_claim_id = ?
|
|
6810
|
+
WHERE id = ? AND deletion_started_at IS NULL
|
|
6811
|
+
`).run(claimId, req.params.id);
|
|
6812
|
+
if (claimed.changes !== 1) {
|
|
6813
|
+
throw new ProjectDeletionError(409, 'Project deletion is already in progress');
|
|
6814
|
+
}
|
|
6815
|
+
})();
|
|
6816
|
+
}
|
|
6817
|
+
catch (error) {
|
|
6818
|
+
if (error instanceof ProjectDeletionError) {
|
|
6819
|
+
return res.status(error.status).json({ error: error.message });
|
|
6820
|
+
}
|
|
6821
|
+
throw error;
|
|
6822
|
+
}
|
|
6823
|
+
try {
|
|
6824
|
+
await completeClaimedProjectDeletion(req.params.id, claimId);
|
|
6825
|
+
}
|
|
6826
|
+
catch (error) {
|
|
6698
6827
|
db.prepare(`
|
|
6699
|
-
UPDATE
|
|
6700
|
-
SET
|
|
6701
|
-
WHERE
|
|
6702
|
-
`).run(req.params.id);
|
|
6703
|
-
|
|
6704
|
-
|
|
6828
|
+
UPDATE projects
|
|
6829
|
+
SET deletion_started_at = NULL, deletion_claim_id = NULL, deletion_cleanup_started_at = NULL
|
|
6830
|
+
WHERE id = ? AND deletion_claim_id = ? AND deletion_cleanup_started_at IS NULL
|
|
6831
|
+
`).run(req.params.id, claimId);
|
|
6832
|
+
if (error instanceof ExecutionArtifactCleanupError) {
|
|
6833
|
+
return res.status(409).json({ error: error.message });
|
|
6834
|
+
}
|
|
6835
|
+
if (error instanceof ProjectDeletionError) {
|
|
6836
|
+
return res.status(error.status).json({ error: error.message });
|
|
6837
|
+
}
|
|
6838
|
+
throw error;
|
|
6839
|
+
}
|
|
6705
6840
|
res.status(204).end();
|
|
6706
6841
|
});
|
|
6707
6842
|
app.post('/tickets', (req, res) => {
|
|
@@ -7582,56 +7717,139 @@ app.get('/projects/:id/lifecycle-events', (req, res) => {
|
|
|
7582
7717
|
throw error;
|
|
7583
7718
|
}
|
|
7584
7719
|
});
|
|
7585
|
-
app.get('/projects/:id/execution-
|
|
7720
|
+
app.get('/projects/:id/execution-timeline', (req, res) => {
|
|
7586
7721
|
const project = getProjectById(req.params.id);
|
|
7587
7722
|
if (!project) {
|
|
7588
7723
|
return res.status(404).json({ error: 'Not found' });
|
|
7589
7724
|
}
|
|
7725
|
+
const query = parseLifecycleListQuery(req.query);
|
|
7726
|
+
if ('error' in query) {
|
|
7727
|
+
return res.status(400).json({ error: query.error });
|
|
7728
|
+
}
|
|
7729
|
+
const scope = parseExecutionTimelineScope(project.id, req.query);
|
|
7730
|
+
if ('error' in scope) {
|
|
7731
|
+
return res.status(scope.status ?? 400).json({ error: scope.error });
|
|
7732
|
+
}
|
|
7733
|
+
try {
|
|
7734
|
+
return res.json(listExecutionTimeline({
|
|
7735
|
+
project,
|
|
7736
|
+
kind: query.kind,
|
|
7737
|
+
statuses: query.statuses,
|
|
7738
|
+
cursor: query.cursor,
|
|
7739
|
+
limit: query.limit,
|
|
7740
|
+
scope: scope.scopeEntity,
|
|
7741
|
+
}));
|
|
7742
|
+
}
|
|
7743
|
+
catch (error) {
|
|
7744
|
+
if (error instanceof Error && error.message === 'Invalid execution run cursor') {
|
|
7745
|
+
return res.status(400).json({ error: 'cursor must be a valid execution run cursor' });
|
|
7746
|
+
}
|
|
7747
|
+
throw error;
|
|
7748
|
+
}
|
|
7749
|
+
});
|
|
7750
|
+
app.get('/projects/:id/execution-timeline/:runId', (req, res) => {
|
|
7751
|
+
const project = getProjectById(req.params.id);
|
|
7752
|
+
if (!project) {
|
|
7753
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7754
|
+
}
|
|
7755
|
+
const scope = parseExecutionTimelineScope(project.id, req.query);
|
|
7756
|
+
if ('error' in scope) {
|
|
7757
|
+
return res.status(scope.status ?? 400).json({ error: scope.error });
|
|
7758
|
+
}
|
|
7590
7759
|
const run = getExecutionRun(project.id, req.params.runId);
|
|
7591
7760
|
if (!run) {
|
|
7592
7761
|
return res.status(404).json({ error: 'Execution run not found' });
|
|
7593
7762
|
}
|
|
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
7763
|
const links = listExecutionRunLinks(project.id, run.id);
|
|
7601
|
-
|
|
7602
|
-
|
|
7764
|
+
if (scope.scopeEntity && !links.some(link => link.entity_type === scope.scopeEntity?.type && link.entity_id === scope.scopeEntity.id)) {
|
|
7765
|
+
return res.status(404).json({ error: 'Execution run not found' });
|
|
7766
|
+
}
|
|
7767
|
+
return res.json(buildExecutionRunDetailResponse(project, run));
|
|
7603
7768
|
});
|
|
7604
|
-
app.
|
|
7769
|
+
app.get('/projects/:id/execution-runs/:runId', (req, res) => {
|
|
7605
7770
|
const project = getProjectById(req.params.id);
|
|
7606
7771
|
if (!project) {
|
|
7607
7772
|
return res.status(404).json({ error: 'Not found' });
|
|
7608
7773
|
}
|
|
7609
|
-
const
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
if ('error' in retry) {
|
|
7613
|
-
return res.status(retry.error.status).json({ error: retry.error.message });
|
|
7774
|
+
const run = getExecutionRun(project.id, req.params.runId);
|
|
7775
|
+
if (!run) {
|
|
7776
|
+
return res.status(404).json({ error: 'Execution run not found' });
|
|
7614
7777
|
}
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7778
|
+
return res.json(buildExecutionRunDetailResponse(project, run));
|
|
7779
|
+
});
|
|
7780
|
+
app.get('/projects/:id/execution-runs/:runId/artifacts/:artifactId', async (req, res) => {
|
|
7781
|
+
const project = getProjectById(req.params.id);
|
|
7782
|
+
if (!project || !getExecutionRun(project.id, req.params.runId)) {
|
|
7783
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7784
|
+
}
|
|
7785
|
+
const retrieval = await openExecutionArtifactForRetrieval(project.id, req.params.runId, req.params.artifactId, io);
|
|
7786
|
+
if (retrieval.kind === 'not_found') {
|
|
7787
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7788
|
+
}
|
|
7789
|
+
if (retrieval.kind === 'unavailable') {
|
|
7790
|
+
return res.status(410).json({ error: 'Artifact unavailable' });
|
|
7791
|
+
}
|
|
7792
|
+
if (retrieval.kind === 'external') {
|
|
7793
|
+
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
7794
|
+
res.setHeader('Location', retrieval.url);
|
|
7795
|
+
return res.status(303).end();
|
|
7796
|
+
}
|
|
7797
|
+
res.setHeader('Content-Type', retrieval.mediaType);
|
|
7798
|
+
res.setHeader('Content-Length', retrieval.byteSize);
|
|
7799
|
+
res.setHeader('Content-Disposition', `attachment; filename="${retrieval.filename}"`);
|
|
7800
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
7801
|
+
const stream = retrieval.handle.createReadStream({ autoClose: true });
|
|
7802
|
+
stream.on('error', () => {
|
|
7803
|
+
console.error('Execution artifact stream failed', {
|
|
7804
|
+
project_id: project.id,
|
|
7805
|
+
execution_run_id: req.params.runId,
|
|
7806
|
+
artifact_id: req.params.artifactId,
|
|
7807
|
+
});
|
|
7808
|
+
res.destroy();
|
|
7622
7809
|
});
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7810
|
+
res.on('close', () => {
|
|
7811
|
+
if (!stream.destroyed) {
|
|
7812
|
+
stream.destroy();
|
|
7813
|
+
}
|
|
7814
|
+
});
|
|
7815
|
+
return stream.pipe(res);
|
|
7816
|
+
});
|
|
7817
|
+
app.post('/projects/:id/execution-runs/:runId/retry', async (req, res) => {
|
|
7818
|
+
const project = getProjectById(req.params.id);
|
|
7819
|
+
if (!project) {
|
|
7820
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7630
7821
|
}
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7822
|
+
const resolution = resolveExecutionRetry(project.id, req.params.runId);
|
|
7823
|
+
const result = await dispatchExecutionRetry(resolution, io, async (retry) => {
|
|
7824
|
+
const session = createTestSession({
|
|
7825
|
+
projectId: project.id,
|
|
7826
|
+
ticketId: retry.ticketId,
|
|
7827
|
+
targetKind: retry.workspace.target_kind,
|
|
7828
|
+
targetBundleId: retry.workspace.target_bundle_id,
|
|
7829
|
+
cwd: retry.workspace.cwd,
|
|
7830
|
+
targetLabel: retry.workspace.target_label,
|
|
7831
|
+
});
|
|
7832
|
+
try {
|
|
7833
|
+
const started = await startTestSessionRun(session.id, project, {
|
|
7834
|
+
command: retry.command,
|
|
7835
|
+
selection: retry.selection,
|
|
7836
|
+
retryOfRunId: req.params.runId,
|
|
7837
|
+
deleteOnComplete: true,
|
|
7838
|
+
}, io);
|
|
7839
|
+
if (!started.run_id) {
|
|
7840
|
+
throw new Error('Test retry did not create a replacement execution');
|
|
7841
|
+
}
|
|
7842
|
+
return started.run_id;
|
|
7843
|
+
}
|
|
7844
|
+
catch (error) {
|
|
7845
|
+
await deleteTestSession(session.id, io).catch(() => undefined);
|
|
7846
|
+
throw error;
|
|
7847
|
+
}
|
|
7848
|
+
});
|
|
7849
|
+
if (!result.ok) {
|
|
7850
|
+
return res.status(result.status).json({ error: result.error, reason: result.reason });
|
|
7634
7851
|
}
|
|
7852
|
+
return res.status(201).json(result.value);
|
|
7635
7853
|
});
|
|
7636
7854
|
app.post('/tickets/:id/test-sessions', (req, res) => {
|
|
7637
7855
|
const ticket = getTicket(req.params.id);
|
|
@@ -7735,7 +7953,7 @@ app.post('/test-sessions/:id/stop', async (req, res) => {
|
|
|
7735
7953
|
}
|
|
7736
7954
|
});
|
|
7737
7955
|
app.delete('/test-sessions/:id', async (req, res) => {
|
|
7738
|
-
const deleted = await deleteTestSession(req.params.id, io
|
|
7956
|
+
const deleted = await deleteTestSession(req.params.id, io);
|
|
7739
7957
|
if (!deleted) {
|
|
7740
7958
|
return res.status(404).json({ error: 'Not found' });
|
|
7741
7959
|
}
|
|
@@ -7890,6 +8108,7 @@ app.post('/tickets/:id/plan', (req, res) => {
|
|
|
7890
8108
|
}
|
|
7891
8109
|
const scheduling = startPlan(ticket, io, {
|
|
7892
8110
|
branch,
|
|
8111
|
+
targetBundleId,
|
|
7893
8112
|
});
|
|
7894
8113
|
if (!scheduling.accepted) {
|
|
7895
8114
|
const status = scheduling.reason === 'queue_failed' ? 503 : 409;
|
|
@@ -7934,12 +8153,21 @@ if (clientBuild) {
|
|
|
7934
8153
|
});
|
|
7935
8154
|
});
|
|
7936
8155
|
}
|
|
8156
|
+
app.use((error, _req, res, next) => {
|
|
8157
|
+
if (error instanceof Error && error.message.includes('Project is being deleted')) {
|
|
8158
|
+
res.status(409).json({ error: 'Project deletion is in progress' });
|
|
8159
|
+
return;
|
|
8160
|
+
}
|
|
8161
|
+
next(error);
|
|
8162
|
+
});
|
|
7937
8163
|
io.on('connection', (socket) => {
|
|
7938
8164
|
console.log('client connected:', socket.id);
|
|
7939
8165
|
registerTerminalSocketHandlers(io, socket);
|
|
7940
8166
|
});
|
|
7941
8167
|
const DEFAULT_PORT = 3001;
|
|
7942
8168
|
let executionEvidenceCleanupTimer = null;
|
|
8169
|
+
let executionArtifactCleanupTimer = null;
|
|
8170
|
+
let executionArtifactCleanupPromise = null;
|
|
7943
8171
|
let startServerPromise = null;
|
|
7944
8172
|
let shutdownServerPromise = null;
|
|
7945
8173
|
let shutdownRequested = false;
|
|
@@ -7966,6 +8194,41 @@ const startExecutionEvidenceCleanupTimer = () => {
|
|
|
7966
8194
|
executionEvidenceCleanupTimer = setInterval(runExecutionEvidenceCleanup, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS);
|
|
7967
8195
|
executionEvidenceCleanupTimer.unref();
|
|
7968
8196
|
};
|
|
8197
|
+
export const runExecutionArtifactCleanup = (mode) => {
|
|
8198
|
+
if (executionArtifactCleanupPromise) {
|
|
8199
|
+
return executionArtifactCleanupPromise;
|
|
8200
|
+
}
|
|
8201
|
+
executionArtifactCleanupPromise = (async () => {
|
|
8202
|
+
if (mode === 'periodic') {
|
|
8203
|
+
await resumeInterruptedProjectDeletions();
|
|
8204
|
+
}
|
|
8205
|
+
await runExclusiveExecutionArtifactMaintenance(async () => {
|
|
8206
|
+
const intents = await reconcileExecutionArtifactRegistrationIntents({ mode });
|
|
8207
|
+
const artifacts = await reconcileFinalizedExecutionArtifacts({ emitter: io });
|
|
8208
|
+
const failures = intents.failed.length + artifacts.failed.length;
|
|
8209
|
+
if (failures > 0) {
|
|
8210
|
+
console.error(`Artifact reconciliation retained ${failures} item(s) for retry`);
|
|
8211
|
+
}
|
|
8212
|
+
if (intents.deleted + artifacts.deleted > 0) {
|
|
8213
|
+
console.log(`Reconciled ${intents.deleted} artifact registration intent(s) and expired ${artifacts.deleted} artifact(s)`);
|
|
8214
|
+
}
|
|
8215
|
+
});
|
|
8216
|
+
})().catch(error => {
|
|
8217
|
+
console.error('Failed to reconcile execution artifacts', error);
|
|
8218
|
+
}).finally(() => {
|
|
8219
|
+
executionArtifactCleanupPromise = null;
|
|
8220
|
+
});
|
|
8221
|
+
return executionArtifactCleanupPromise;
|
|
8222
|
+
};
|
|
8223
|
+
const startExecutionArtifactCleanupTimer = () => {
|
|
8224
|
+
if (executionArtifactCleanupTimer) {
|
|
8225
|
+
return;
|
|
8226
|
+
}
|
|
8227
|
+
executionArtifactCleanupTimer = setInterval(() => {
|
|
8228
|
+
void runExecutionArtifactCleanup('periodic');
|
|
8229
|
+
}, resolveArtifactCleanupIntervalMs());
|
|
8230
|
+
executionArtifactCleanupTimer.unref();
|
|
8231
|
+
};
|
|
7969
8232
|
const resolveStartResult = (host) => {
|
|
7970
8233
|
const address = httpServer.address();
|
|
7971
8234
|
const { displayHost } = resolveDesktopServerHost(host);
|
|
@@ -7993,8 +8256,12 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
|
7993
8256
|
return startServerPromise;
|
|
7994
8257
|
}
|
|
7995
8258
|
startServerPromise = (async () => {
|
|
8259
|
+
releaseInterruptedProjectDeletionClaims();
|
|
8260
|
+
await resumeInterruptedProjectDeletions();
|
|
7996
8261
|
await reconcileStartupExecutions({ getGitHubConfig: getGitHubConnectionConfig });
|
|
7997
8262
|
throwIfShutdownRequested();
|
|
8263
|
+
await runExecutionArtifactCleanup('startup');
|
|
8264
|
+
throwIfShutdownRequested();
|
|
7998
8265
|
startGitHubActionsPolling(io);
|
|
7999
8266
|
try {
|
|
8000
8267
|
throwIfShutdownRequested();
|
|
@@ -8020,6 +8287,7 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
|
8020
8287
|
runExecutionEvidenceCleanup();
|
|
8021
8288
|
throwIfShutdownRequested();
|
|
8022
8289
|
startExecutionEvidenceCleanupTimer();
|
|
8290
|
+
startExecutionArtifactCleanupTimer();
|
|
8023
8291
|
return resolveStartResult(host);
|
|
8024
8292
|
})().finally(() => {
|
|
8025
8293
|
startServerPromise = null;
|
|
@@ -8041,6 +8309,11 @@ export const shutdownServer = async (signal) => {
|
|
|
8041
8309
|
clearInterval(executionEvidenceCleanupTimer);
|
|
8042
8310
|
executionEvidenceCleanupTimer = null;
|
|
8043
8311
|
}
|
|
8312
|
+
if (executionArtifactCleanupTimer) {
|
|
8313
|
+
clearInterval(executionArtifactCleanupTimer);
|
|
8314
|
+
executionArtifactCleanupTimer = null;
|
|
8315
|
+
}
|
|
8316
|
+
await executionArtifactCleanupPromise;
|
|
8044
8317
|
await stopMobileAccess();
|
|
8045
8318
|
await clearAllTestSessions(io);
|
|
8046
8319
|
await clearAllGitHubActionsSessions();
|