@straiffi/archon 1.7.4 → 1.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{ActivityView-BgMz3NNT.js → ActivityView-BrZY8vYf.js} +1 -1
- package/dist/client/assets/{GitHubActionsDialog-CNqWDS3E.js → GitHubActionsDialog-CUfdVw97.js} +1 -1
- package/dist/client/assets/{TestsDialog-Cll7xJe0.js → TestsDialog-FXcnLOyf.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BL5dYiul.js → architectureDiagram-3BPJPVTR-D0PXGvSL.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Bi5x2v80.js → blockDiagram-GPEHLZMM-BCLAxnp5.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-esEm54Ay.js → c4Diagram-AAUBKEIU-DVZ5yZWV.js} +1 -1
- package/dist/client/assets/channel-BCPCrThZ.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-DACfY8xM.js → chunk-2J33WTMH-BGJYj1Lu.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-DZNUiaUs.js → chunk-3OPIFGDE-DI5-yK3m.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-ClDfkMAp.js → chunk-5ZQYHXKU-Dnr5DDsb.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-CCwA3GWT.js → chunk-727SXJPM-8HdFwn-P.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Byl4DRZ9.js → chunk-AQP2D5EJ-C7x9t2uJ.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-CXymZQ7A.js → chunk-CSCIHK7Q-CrhwRPh5.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-VV3D8g6a.js → chunk-KSCS5N6A-CdD62r5U.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-BL9N5GRI.js → chunk-L5ZTLDWV-CqQVvwc_.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-DKNN-zno.js → chunk-LZXEDZCA-DLHrqoJ4.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-CHCpQ1yr.js → chunk-ND2GUHAM-B0yOdiPF.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-B0Dm74mt.js → chunk-NZK2D7GU-boM-MocY.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-DAa5z2xG.js → chunk-O5CBEL6O-B0jgJn0t.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-DrVJWKVR.js → chunk-WU5MYG2G-C_S_v-m2.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BdJlc_1i.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Bidw5Zc1.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-B0fsl8mE.js → dagre-BM42HDAG-DKl6Y8ya.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-Cx_VEbSS.js → diagram-2AECGRRQ-6Lyn9DBp.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-Cv7VVn5r.js → diagram-5GNKFQAL-wdS1vEkN.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C8Sl5czV.js → diagram-KO2AKTUF-CPih29jF.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-C3dZaLz2.js → diagram-LMA3HP47-Cr5Px74d.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BAU9DtTr.js → diagram-OG6HWLK6-i0PjSScd.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-DpNc-eBD.js → erDiagram-TEJ5UH35-Dt4yOC9J.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-DGwTAT1S.js → flowDiagram-I6XJVG4X-Bf6kvajC.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-MPZhEgzJ.js → ganttDiagram-6RSMTGT7-DgxePMk_.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CY0tHL5N.js → gitGraphDiagram-PVQCEYII-BZq2cmFS.js} +1 -1
- package/dist/client/assets/index-BJGHMaMz.css +2 -0
- package/dist/client/assets/{index-CJRgmWwY.js → index-CjRMYO1U.js} +106 -106
- package/dist/client/assets/{infoDiagram-5YYISTIA-BBZeKjf4.js → infoDiagram-5YYISTIA-Chij69KN.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CM_iFrES.js → ishikawaDiagram-YF4QCWOH-3qHCLFgD.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Do_lY94X.js → journeyDiagram-JHISSGLW-DvRW8tbA.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-brxg3j37.js → kanban-definition-UN3LZRKU-DjR2T5I1.js} +1 -1
- package/dist/client/assets/{line-PHHbm9rj.js → line-B9OzrQ1Y.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-CLxAkW69.js → mermaid-parser.core-DYcDor4d.js} +1 -1
- package/dist/client/assets/{mermaid.core-CebvSmx9.js → mermaid.core-DG8QYo2u.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-CX_-ZaJF.js → mindmap-definition-RKZ34NQL-f4QPF6b3.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BJuHvql_.js → pieDiagram-4H26LBE5-CMkl2jBu.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DiJll7l9.js → quadrantDiagram-W4KKPZXB-BXaUHzJi.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-Bg-tXgcj.js → requirementDiagram-4Y6WPE33-B-Inuj_6.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BoK2egfZ.js → sankeyDiagram-5OEKKPKP-BjsmZFYI.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DcrDWNzv.js → sequenceDiagram-3UESZ5HK-B1PAm6sX.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-B995qt1S.js → stateDiagram-AJRCARHV-pLONNEN8.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-kWPDF3wr.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-DqSVAhqL.js → timeline-definition-PNZ67QCA-BLMkSXo_.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-D9wVraVY.js → vennDiagram-CIIHVFJN-CEwGRegG.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CLff9zVd.js → wardleyDiagram-YWT4CUSO-_md2IBXC.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-BuHUMMH2.js → xychartDiagram-2RQKCTM6-DVzwfXHc.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +282 -0
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +332 -130
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/attachments.js +16 -0
- package/dist/server/lib/attachments.js.map +1 -1
- package/dist/server/lib/automationResumeDecision.js +141 -0
- package/dist/server/lib/automationResumeDecision.js.map +1 -0
- package/dist/server/lib/buildSequences.js +52 -0
- package/dist/server/lib/buildSequences.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 +9 -1
- package/dist/server/lib/executionRetry.js.map +1 -1
- package/dist/server/lib/executionRetryContext.js +21 -0
- package/dist/server/lib/executionRetryContext.js.map +1 -1
- package/dist/server/lib/executionRuns.js +62 -2
- package/dist/server/lib/executionRuns.js.map +1 -1
- package/dist/server/lib/executionTimeline.js +38 -10
- package/dist/server/lib/executionTimeline.js.map +1 -1
- 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/ticketAutomationCommit.js +11 -4
- package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
- package/dist/server/lib/ticketFollowUpOperations.js +19 -15
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketUndo.js +83 -31
- package/dist/server/lib/ticketUndo.js.map +1 -1
- package/dist/server/lib/tickets.js +67 -6
- package/dist/server/lib/tickets.js.map +1 -1
- package/dist/server/lib/types.js +11 -0
- package/dist/server/lib/types.js.map +1 -1
- package/dist/server/workers/followUp.js +94 -6
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/review.js +1 -0
- package/dist/server/workers/review.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/channel-CqNHF3FD.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BjEgQwKg.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-gGomH-F_.js +0 -1
- package/dist/client/assets/index-mRkJWu70.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Dn8P7Sa1.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';
|
|
@@ -49,15 +49,18 @@ import { formatBuildOpenQuestionAnswersPrompt, getLatestBuildOpenQuestionBatchSt
|
|
|
49
49
|
import { acceptBuildOpenQuestionResolution } from './lib/buildOpenQuestionOperations.js';
|
|
50
50
|
import { completeAutomationProgression, createAutomationReviewFix, finishAutomationReviewFix, getAutomationProgression, recoverLegacyAutomationProgression, recoverStoppedSideEffectProgression, runAutomationProgression, stopAutomationProgression, waitAutomationProgression } from './lib/automationProgression.js';
|
|
51
51
|
import { reconcileAutomationStage } from './lib/automationReconciliation.js';
|
|
52
|
+
import { pausedRunBelongsToProgression, resolveAutomationResumeDecision } from './lib/automationResumeDecision.js';
|
|
52
53
|
import { getAutomationStageDetails, } from './lib/automationStageDetails.js';
|
|
53
|
-
import { clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, isAutomationDepth, recordAutomationPause, resolveAutomationConfig, upsertBundleAutomationState, } from './lib/ticketAutomation.js';
|
|
54
|
+
import { buildAutomationFixInstruction, clearAutomationPause, countBlockingFindings, depthAtLeast, getBundleAutomationState, isAutomationDepth, recordAutomationPause, resolveAutomationConfig, upsertBundleAutomationState, } from './lib/ticketAutomation.js';
|
|
54
55
|
import { runAutomationCommitChain } from './lib/ticketAutomationCommit.js';
|
|
55
56
|
import { getDiscoveredSkills, normalizeSkillNames } from './lib/skills.js';
|
|
56
57
|
import { clearAllPreviewProxies } from './lib/previewProxy.js';
|
|
57
58
|
import { createExecutionRun, addExecutionRunLink, addExecutionRunLinks, getExecutionRun, hasActiveBuildExecutionRunForTicket, getGitHubActionsExecutionDetails, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listLifecycleEvents, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
|
|
58
59
|
import { dispatchExecutionRetry, getExecutionRetryEligibility, resolveExecutionRetry } from './lib/executionRetry.js';
|
|
59
60
|
import { listExecutionTimeline } from './lib/executionTimeline.js';
|
|
61
|
+
import { loadExecutionArtifactSummariesForRuns, openExecutionArtifactForRetrieval, } from './lib/executionArtifacts.js';
|
|
60
62
|
import { cleanupAllExecutionEvidence, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS, resolveExecutionEvidenceRetentionDays, } from './lib/executionRunRetention.js';
|
|
63
|
+
import { ExecutionArtifactCleanupError, reconcileExecutionArtifactRegistrationIntents, reconcileFinalizedExecutionArtifacts, reconcileProjectExecutionArtifacts, resolveArtifactCleanupIntervalMs, } from './lib/executionArtifactRetention.js';
|
|
61
64
|
import { shutdownRealtimeServer } from './lib/shutdown.js';
|
|
62
65
|
import { resolveClientBuildPaths, shouldServeClientApp, STATIC_CLIENT_DIRECTORY_OPTIONS, STATIC_CLIENT_INDEX_OPTIONS, } from './lib/staticClient.js';
|
|
63
66
|
import { deleteIntegrationConnection, getGitHubConnectionConfig, getJiraConnectionConfig, listIntegrationConnections, upsertIntegrationConnection, } from './lib/integrations/index.js';
|
|
@@ -72,7 +75,7 @@ import { PlanningReferenceError, resolveExternalPlanningDescription } from './li
|
|
|
72
75
|
import { refreshIfNeeded as refreshAssignedIssues, clearCachedAssignedIssues } from './lib/integrations/assignedIssuesCache.js';
|
|
73
76
|
import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib/commitMessage.js';
|
|
74
77
|
import { BUNDLE_PULL_REQUEST_SYNC_TTL_MS, getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, syncTrackedBundlePullRequestIfNeeded, upsertBundlePullRequest, upsertBundlePullRequestFromGitHub, } from './lib/bundlePullRequests.js';
|
|
75
|
-
import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
|
|
78
|
+
import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listInterruptedProjectDeletions, listProjects, releaseInterruptedProjectDeletionClaims, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
|
|
76
79
|
import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
|
|
77
80
|
import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
|
|
78
81
|
import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTicketState, } from './lib/ticketWorkflowOperations.js';
|
|
@@ -94,7 +97,30 @@ import { exportSpecToDocs, checkSpecExportCollision } from './lib/specExport.js'
|
|
|
94
97
|
import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
|
|
95
98
|
import { startBuild } from './workers/build.js';
|
|
96
99
|
import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, prepareLinkedPlanningResearch, startPlan, stopAllPlanAgents, } from './workers/plan.js';
|
|
97
|
-
import { EXECUTION_RUN_KINDS, validateWorkflowContext } from './lib/types.js';
|
|
100
|
+
import { EXECUTION_RUN_KINDS, MAX_ARTIFACTS_PER_RUN, validateWorkflowContext } from './lib/types.js';
|
|
101
|
+
class ProjectDeletionError extends Error {
|
|
102
|
+
status;
|
|
103
|
+
constructor(status, message) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = 'ProjectDeletionError';
|
|
106
|
+
this.status = status;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let executionArtifactMaintenanceTail = Promise.resolve();
|
|
110
|
+
const runExclusiveExecutionArtifactMaintenance = async (task) => {
|
|
111
|
+
const previous = executionArtifactMaintenanceTail;
|
|
112
|
+
let release = () => undefined;
|
|
113
|
+
executionArtifactMaintenanceTail = new Promise(resolve => {
|
|
114
|
+
release = resolve;
|
|
115
|
+
});
|
|
116
|
+
await previous;
|
|
117
|
+
try {
|
|
118
|
+
return await task();
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
release();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
98
124
|
const getErrorMessage = (error) => {
|
|
99
125
|
return error instanceof Error ? error.message : String(error);
|
|
100
126
|
};
|
|
@@ -112,6 +138,9 @@ const requireExistingProject = (projectId) => {
|
|
|
112
138
|
if (!project) {
|
|
113
139
|
return { status: 404, error: 'Not found' };
|
|
114
140
|
}
|
|
141
|
+
if (project.deletion_started_at) {
|
|
142
|
+
return { status: 409, error: 'Project deletion is in progress' };
|
|
143
|
+
}
|
|
115
144
|
return { project };
|
|
116
145
|
};
|
|
117
146
|
const requireProjectMemoryProject = (projectId) => {
|
|
@@ -261,6 +290,26 @@ const serializeGitHubActionsExecutionDetail = (details) => {
|
|
|
261
290
|
jobs_count: details.jobs_count,
|
|
262
291
|
};
|
|
263
292
|
};
|
|
293
|
+
const buildExecutionRunDetailResponse = (project, run) => {
|
|
294
|
+
const retryEligibility = getExecutionRetryEligibility(project.id, run.id);
|
|
295
|
+
const details = run.kind === 'test'
|
|
296
|
+
? serializeTestExecutionDetail(getTestExecutionDetails(project.id, run.id), retryEligibility)
|
|
297
|
+
: run.kind === 'github_actions'
|
|
298
|
+
? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
|
|
299
|
+
: ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
|
|
300
|
+
? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
|
|
301
|
+
: { kind: run.kind, available: true, retry_eligibility: retryEligibility };
|
|
302
|
+
const artifacts = loadExecutionArtifactSummariesForRuns(project.id, [run.id], MAX_ARTIFACTS_PER_RUN).get(run.id)
|
|
303
|
+
?? { artifacts: [], artifact_count: 0 };
|
|
304
|
+
return {
|
|
305
|
+
run,
|
|
306
|
+
links: listExecutionRunLinks(project.id, run.id),
|
|
307
|
+
events: listLifecycleEvents(project.id, run.id).map(toCanonicalLifecycleEvent),
|
|
308
|
+
details,
|
|
309
|
+
agent_runs: listAgentRunsForExecutionRun(run.id),
|
|
310
|
+
...artifacts,
|
|
311
|
+
};
|
|
312
|
+
};
|
|
264
313
|
const formatWorktreeDeleteError = (target, branch, error) => {
|
|
265
314
|
return `Unable to delete the ${target} worktree for branch "${branch}": ${getErrorMessage(error)}`;
|
|
266
315
|
};
|
|
@@ -2400,6 +2449,17 @@ app.post('/projects/:id/chat-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2400
2449
|
if (error) {
|
|
2401
2450
|
return res.status(400).json({ error: getChatAttachmentUploadErrorMessage(error) });
|
|
2402
2451
|
}
|
|
2452
|
+
const currentProject = getProjectById(req.params.id);
|
|
2453
|
+
if (!currentProject) {
|
|
2454
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2455
|
+
}
|
|
2456
|
+
const currentChatSession = getProjectChatSession(currentProject.id, req.params.sessionId);
|
|
2457
|
+
if (!currentChatSession) {
|
|
2458
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2459
|
+
}
|
|
2460
|
+
if (currentChatSession.agent_status === 'running') {
|
|
2461
|
+
return res.status(409).json({ error: 'Wait for the current chat response to finish before uploading images.' });
|
|
2462
|
+
}
|
|
2403
2463
|
const files = (req.files ?? []);
|
|
2404
2464
|
if (files.length === 0) {
|
|
2405
2465
|
return res.status(400).json({ error: 'At least one image is required.' });
|
|
@@ -2417,11 +2477,11 @@ app.post('/projects/:id/chat-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2417
2477
|
try {
|
|
2418
2478
|
const attachments = files.map(file => {
|
|
2419
2479
|
const attachment = createChatAttachment({
|
|
2420
|
-
chatSessionId:
|
|
2480
|
+
chatSessionId: currentChatSession.id,
|
|
2421
2481
|
originalName: file.originalname,
|
|
2422
2482
|
mimeType: file.mimetype,
|
|
2423
2483
|
buffer: file.buffer,
|
|
2424
|
-
projectId:
|
|
2484
|
+
projectId: currentProject.id,
|
|
2425
2485
|
});
|
|
2426
2486
|
createdAttachmentIds.push(attachment.id);
|
|
2427
2487
|
return attachment;
|
|
@@ -2692,6 +2752,17 @@ app.post('/projects/:id/spec-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2692
2752
|
if (error) {
|
|
2693
2753
|
return res.status(400).json({ error: getSpecAttachmentUploadErrorMessage(error) });
|
|
2694
2754
|
}
|
|
2755
|
+
const currentProject = getProjectById(req.params.id);
|
|
2756
|
+
if (!currentProject) {
|
|
2757
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2758
|
+
}
|
|
2759
|
+
const currentSpecSession = getProjectSpecSession(currentProject.id, req.params.sessionId);
|
|
2760
|
+
if (!currentSpecSession) {
|
|
2761
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2762
|
+
}
|
|
2763
|
+
if (currentSpecSession.agent_status === 'running') {
|
|
2764
|
+
return res.status(409).json({ error: 'Wait for the current spec response to finish before uploading images.' });
|
|
2765
|
+
}
|
|
2695
2766
|
const files = (req.files ?? []);
|
|
2696
2767
|
if (files.length === 0) {
|
|
2697
2768
|
return res.status(400).json({ error: 'At least one image is required.' });
|
|
@@ -2709,11 +2780,11 @@ app.post('/projects/:id/spec-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2709
2780
|
try {
|
|
2710
2781
|
const attachments = files.map(file => {
|
|
2711
2782
|
const attachment = createSpecAttachment({
|
|
2712
|
-
specSessionId:
|
|
2783
|
+
specSessionId: currentSpecSession.id,
|
|
2713
2784
|
originalName: file.originalname,
|
|
2714
2785
|
mimeType: file.mimetype,
|
|
2715
2786
|
buffer: file.buffer,
|
|
2716
|
-
projectId:
|
|
2787
|
+
projectId: currentProject.id,
|
|
2717
2788
|
});
|
|
2718
2789
|
createdAttachmentIds.push(attachment.id);
|
|
2719
2790
|
return attachment;
|
|
@@ -3884,25 +3955,6 @@ app.post('/bundles/:id/follow-up', (req, res) => {
|
|
|
3884
3955
|
});
|
|
3885
3956
|
const AUTOMATION_SIDE_EFFECT_KINDS = new Set(['automation_commit', 'automation_push', 'automation_pull_request']);
|
|
3886
3957
|
const AUTOMATION_WORKER_PAUSE_KINDS = new Set(['build', 'review', 'build_follow_up', 'automation_review_fix']);
|
|
3887
|
-
const pausedRunBelongsToProgression = (projectId, bundleId, progressionRunId, pausedRunId) => {
|
|
3888
|
-
const pausedRun = getExecutionRun(projectId, pausedRunId);
|
|
3889
|
-
const progression = getExecutionRun(projectId, progressionRunId);
|
|
3890
|
-
if (!pausedRun || !progression || progression.kind !== 'automation_progression') {
|
|
3891
|
-
return false;
|
|
3892
|
-
}
|
|
3893
|
-
const ownsBundle = listExecutionRunLinks(projectId, pausedRun.id).some(link => (link.entity_type === 'bundle'
|
|
3894
|
-
&& link.entity_id === bundleId
|
|
3895
|
-
&& (link.relationship === 'primary' || link.relationship === 'context')));
|
|
3896
|
-
if (!ownsBundle) {
|
|
3897
|
-
return false;
|
|
3898
|
-
}
|
|
3899
|
-
if (pausedRun.parent_run_id === progressionRunId) {
|
|
3900
|
-
return true;
|
|
3901
|
-
}
|
|
3902
|
-
const parent = pausedRun.parent_run_id ? getExecutionRun(projectId, pausedRun.parent_run_id) : null;
|
|
3903
|
-
return parent?.kind === 'automation_review_fix' && parent.parent_run_id === progressionRunId
|
|
3904
|
-
|| progression.parent_run_id === pausedRun.id;
|
|
3905
|
-
};
|
|
3906
3958
|
app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
3907
3959
|
const result = getRequiredProject(req);
|
|
3908
3960
|
if ('error' in result) {
|
|
@@ -3916,15 +3968,38 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3916
3968
|
if (!bundleState || !bundleState.automation_paused_reason) {
|
|
3917
3969
|
return res.status(409).json({ error: 'Bundle automation is not paused' });
|
|
3918
3970
|
}
|
|
3919
|
-
const
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3971
|
+
const resolveCurrentDecision = () => {
|
|
3972
|
+
const currentState = getBundleAutomationState(result.project.id, bundle.id);
|
|
3973
|
+
if (!currentState?.automation_paused_reason) {
|
|
3974
|
+
return null;
|
|
3975
|
+
}
|
|
3976
|
+
return resolveAutomationResumeDecision({
|
|
3977
|
+
projectId: result.project.id,
|
|
3978
|
+
bundleId: bundle.id,
|
|
3979
|
+
state: currentState,
|
|
3980
|
+
config: resolveAutomationConfig(getBundle(bundle.id, result.project.id), getProjectById(result.project.id)),
|
|
3981
|
+
tickets: listDetailedBundleTickets(result.project.id, bundle.id),
|
|
3982
|
+
openQuestionCount: listBuildOpenQuestionsForBundle(result.project.id, bundle.id).length,
|
|
3983
|
+
questionResolutionActive: hasActiveBuildOpenQuestionBatch(result.project.id, bundle.id),
|
|
3984
|
+
});
|
|
3985
|
+
};
|
|
3986
|
+
const decision = resolveCurrentDecision();
|
|
3987
|
+
if (!decision || decision.status !== 'enabled') {
|
|
3988
|
+
const error = decision?.status === 'hidden' && decision.reason === 'question_resolution_active'
|
|
3989
|
+
? 'Build open question resolution is still in progress'
|
|
3990
|
+
: decision?.status === 'hidden' && decision.reason === 'resolve_open_questions'
|
|
3991
|
+
? 'Resolve or dismiss all Build open questions before resuming automation'
|
|
3992
|
+
: decision?.status === 'disabled' && decision.reason === 'invalid_stage_lineage'
|
|
3993
|
+
? 'The paused automation stage does not belong to the active progression'
|
|
3994
|
+
: 'Bundle automation cannot be resumed in its current state';
|
|
3995
|
+
return res.status(409).json({ error, automation_resume_decision: decision });
|
|
3996
|
+
}
|
|
3997
|
+
const revalidateDecision = () => {
|
|
3998
|
+
const current = resolveCurrentDecision();
|
|
3999
|
+
return current?.status === 'enabled'
|
|
4000
|
+
&& current.outcome === decision.outcome
|
|
4001
|
+
&& current.owner_ticket_id === decision.owner_ticket_id;
|
|
4002
|
+
};
|
|
3928
4003
|
let progressionRunId = bundleState.automation_progression_run_id;
|
|
3929
4004
|
if (!progressionRunId) {
|
|
3930
4005
|
try {
|
|
@@ -3952,12 +4027,13 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
3952
4027
|
catch (error) {
|
|
3953
4028
|
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
3954
4029
|
}
|
|
4030
|
+
if (!revalidateDecision()) {
|
|
4031
|
+
return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
|
|
4032
|
+
}
|
|
3955
4033
|
let sideEffectResume = null;
|
|
3956
|
-
let pausedRunKind = null;
|
|
3957
4034
|
if (bundleState.automation_paused_stage_run_id) {
|
|
3958
4035
|
const stageRunId = bundleState.automation_paused_stage_run_id;
|
|
3959
4036
|
const stage = getExecutionRun(result.project.id, stageRunId);
|
|
3960
|
-
pausedRunKind = stage?.kind ?? null;
|
|
3961
4037
|
if (!stage
|
|
3962
4038
|
|| (!AUTOMATION_SIDE_EFFECT_KINDS.has(stage.kind) && !AUTOMATION_WORKER_PAUSE_KINDS.has(stage.kind))
|
|
3963
4039
|
|| !pausedRunBelongsToProgression(result.project.id, bundle.id, progressionRunId, stageRunId)) {
|
|
@@ -4021,33 +4097,7 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
4021
4097
|
}
|
|
4022
4098
|
}
|
|
4023
4099
|
}
|
|
4024
|
-
|
|
4025
|
-
completeAutomationProgression({
|
|
4026
|
-
projectId: result.project.id,
|
|
4027
|
-
bundleId: bundle.id,
|
|
4028
|
-
progressionRunId,
|
|
4029
|
-
domainStatus: 'manual_boundary_reached',
|
|
4030
|
-
broadcaster: io,
|
|
4031
|
-
});
|
|
4032
|
-
for (const ticket of listDetailedBundleTickets(result.project.id, bundle.id)) {
|
|
4033
|
-
io.emit('ticket:updated', ticket);
|
|
4034
|
-
}
|
|
4035
|
-
return res.json(serializeBundleDetails(result.project, bundle));
|
|
4036
|
-
}
|
|
4037
|
-
const reviewOwnerSource = bundleState.automation_paused_code === 'open_questions'
|
|
4038
|
-
? listBundleSliceTickets(result.project.id, bundle.id, 'build')
|
|
4039
|
-
.find(ticket => ticket.agent_status === 'done') ?? null
|
|
4040
|
-
: resolveBundleReviewOwner(result.project.id, bundle.id)
|
|
4041
|
-
?? listBundleSliceTickets(result.project.id, bundle.id, 'review')[0]
|
|
4042
|
-
?? (bundleState.automation_paused_code === 'review' && (pausedRunKind === 'build' || pausedRunKind === 'build_follow_up')
|
|
4043
|
-
? listBundleSliceTickets(result.project.id, bundle.id, 'build')
|
|
4044
|
-
.find(ticket => ticket.agent_status === 'done') ?? null
|
|
4045
|
-
: null)
|
|
4046
|
-
?? null;
|
|
4047
|
-
if (!reviewOwnerSource) {
|
|
4048
|
-
return res.status(409).json({ error: 'No review ticket to resume from' });
|
|
4049
|
-
}
|
|
4050
|
-
const owner = getTicket(reviewOwnerSource.id);
|
|
4100
|
+
const owner = getTicket(decision.owner_ticket_id);
|
|
4051
4101
|
if (!owner) {
|
|
4052
4102
|
return res.status(409).json({ error: 'No review ticket to resume from' });
|
|
4053
4103
|
}
|
|
@@ -4081,6 +4131,9 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
4081
4131
|
};
|
|
4082
4132
|
};
|
|
4083
4133
|
if (sideEffectResume) {
|
|
4134
|
+
if (!decision.outcome.startsWith('reconcile_') || !revalidateDecision()) {
|
|
4135
|
+
return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
|
|
4136
|
+
}
|
|
4084
4137
|
try {
|
|
4085
4138
|
runAutomationProgression({
|
|
4086
4139
|
projectId: result.project.id,
|
|
@@ -4099,27 +4152,17 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
4099
4152
|
}
|
|
4100
4153
|
return res.json(serializeBundleDetails(result.project, bundle));
|
|
4101
4154
|
}
|
|
4102
|
-
if (owner.state === 'review' && owner.agent_status === 'running') {
|
|
4103
|
-
return res.status(409).json({ error: 'Review already running' });
|
|
4104
|
-
}
|
|
4105
4155
|
if (owner.state === 'review' && owner.agent_status !== 'running') {
|
|
4106
|
-
const latestRun = getLatestReviewRunForContext({
|
|
4107
|
-
ticketId: owner.id,
|
|
4108
|
-
projectId: owner.project_id,
|
|
4109
|
-
worktreeBundleId: owner.worktree_bundle_id,
|
|
4110
|
-
});
|
|
4111
|
-
const findings = latestRun ? listReviewFindings(latestRun.id) : [];
|
|
4112
|
-
const blockingCount = countBlockingFindings(findings, automationConfig);
|
|
4113
4156
|
const iteration = bundleState.review_iteration_count ?? 0;
|
|
4114
|
-
if (
|
|
4115
|
-
&& automationConfig.reviewFixEnabled
|
|
4116
|
-
&& blockingCount > 0
|
|
4117
|
-
&& iteration < automationConfig.reviewMaxIterations) {
|
|
4157
|
+
if (decision.outcome === 'start_review_fix') {
|
|
4118
4158
|
let reviewFixRunId = null;
|
|
4119
4159
|
let followUpResult;
|
|
4120
4160
|
const previousIteration = bundleState.review_iteration_count ?? 0;
|
|
4121
4161
|
const previousFixLoopActive = bundleState.fix_loop_active;
|
|
4122
4162
|
try {
|
|
4163
|
+
if (!revalidateDecision()) {
|
|
4164
|
+
return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
|
|
4165
|
+
}
|
|
4123
4166
|
const progression = getAutomationProgression(result.project.id, bundle.id);
|
|
4124
4167
|
if (!progression || progression.id !== progressionRunId) {
|
|
4125
4168
|
return res.status(409).json({ error: 'Automation progression ownership is invalid' });
|
|
@@ -4149,10 +4192,11 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
4149
4192
|
ticketId: owner.id,
|
|
4150
4193
|
requestedProjectId: result.project.id,
|
|
4151
4194
|
payload: {
|
|
4152
|
-
instruction:
|
|
4195
|
+
instruction: buildAutomationFixInstruction(automationConfig.reviewSeverity),
|
|
4153
4196
|
mode: 'build',
|
|
4154
4197
|
},
|
|
4155
4198
|
broadcaster: io,
|
|
4199
|
+
persistTriggerMessage: false,
|
|
4156
4200
|
initiation: {
|
|
4157
4201
|
initiator: 'automation',
|
|
4158
4202
|
causalParentRunId: reviewFix.runId,
|
|
@@ -4206,8 +4250,11 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
4206
4250
|
return res.status(followUpResult.status).json({ error: followUpResult.error });
|
|
4207
4251
|
}
|
|
4208
4252
|
}
|
|
4209
|
-
else if (
|
|
4253
|
+
else if (decision.outcome === 'continue_commit_chain') {
|
|
4210
4254
|
try {
|
|
4255
|
+
if (!revalidateDecision()) {
|
|
4256
|
+
return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
|
|
4257
|
+
}
|
|
4211
4258
|
runAutomationProgression({
|
|
4212
4259
|
projectId: result.project.id,
|
|
4213
4260
|
bundleId: bundle.id,
|
|
@@ -4220,7 +4267,10 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
4220
4267
|
return res.status(409).json({ error: error instanceof Error ? error.message : String(error) });
|
|
4221
4268
|
}
|
|
4222
4269
|
}
|
|
4223
|
-
else {
|
|
4270
|
+
else if (decision.outcome === 'rerun_review') {
|
|
4271
|
+
if (!revalidateDecision()) {
|
|
4272
|
+
return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
|
|
4273
|
+
}
|
|
4224
4274
|
const reviewResult = rerunTicketReviewOperation(owner, io, {
|
|
4225
4275
|
automatic: true,
|
|
4226
4276
|
initiation: { causalParentRunId: progressionRunId },
|
|
@@ -4230,8 +4280,14 @@ app.post('/bundles/:id/resume-automation', async (req, res) => {
|
|
|
4230
4280
|
return res.status(reviewResult.status).json({ error: reviewResult.error });
|
|
4231
4281
|
}
|
|
4232
4282
|
}
|
|
4283
|
+
else {
|
|
4284
|
+
return res.status(409).json({ error: 'The selected Resume outcome no longer matches the bundle state' });
|
|
4285
|
+
}
|
|
4233
4286
|
}
|
|
4234
|
-
else if (owner.state === 'build' && owner.agent_status === 'done') {
|
|
4287
|
+
else if (owner.state === 'build' && owner.agent_status === 'done' && decision.outcome === 'start_review') {
|
|
4288
|
+
if (!revalidateDecision()) {
|
|
4289
|
+
return res.status(409).json({ error: 'Bundle automation state changed before Resume could start' });
|
|
4290
|
+
}
|
|
4235
4291
|
const reviewResult = transitionBundleToReview(result.project.id, bundle.id, io, {
|
|
4236
4292
|
automatic: true,
|
|
4237
4293
|
initiation: { causalParentRunId: progressionRunId },
|
|
@@ -6659,32 +6715,114 @@ app.patch('/projects/:id', (req, res) => {
|
|
|
6659
6715
|
const updated = db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id);
|
|
6660
6716
|
res.json(serializeProject(updated ?? null));
|
|
6661
6717
|
});
|
|
6662
|
-
|
|
6663
|
-
const project =
|
|
6718
|
+
const assertProjectDeletionEligible = (projectId, claimId) => {
|
|
6719
|
+
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
|
6664
6720
|
if (!project) {
|
|
6665
|
-
|
|
6721
|
+
throw new ProjectDeletionError(404, 'Not found');
|
|
6666
6722
|
}
|
|
6667
|
-
|
|
6723
|
+
if (claimId ? project.deletion_claim_id !== claimId : Boolean(project.deletion_started_at)) {
|
|
6724
|
+
throw new ProjectDeletionError(409, 'Project deletion is already in progress');
|
|
6725
|
+
}
|
|
6726
|
+
const projectCount = Number(db.prepare(`
|
|
6727
|
+
SELECT COUNT(*) AS count FROM projects
|
|
6728
|
+
WHERE deletion_started_at IS NULL OR id = ?
|
|
6729
|
+
`).get(projectId)?.count ?? 0);
|
|
6668
6730
|
if (projectCount <= 1) {
|
|
6669
|
-
|
|
6731
|
+
throw new ProjectDeletionError(409, 'Cannot delete the last remaining project');
|
|
6670
6732
|
}
|
|
6671
|
-
if (countInboundProjectLinks(
|
|
6672
|
-
|
|
6733
|
+
if (countInboundProjectLinks(projectId) > 0) {
|
|
6734
|
+
throw new ProjectDeletionError(409, 'Cannot delete a project that is linked from another project');
|
|
6673
6735
|
}
|
|
6674
|
-
const stats = getProjectTicketStats(
|
|
6736
|
+
const stats = getProjectTicketStats(projectId);
|
|
6675
6737
|
if (stats.ticketCount > 0 || stats.worktreeCount > 0 || stats.bundleCount > 0) {
|
|
6676
|
-
|
|
6677
|
-
error: 'Cannot delete a project with existing tickets, bundles, or worktrees',
|
|
6678
|
-
});
|
|
6738
|
+
throw new ProjectDeletionError(409, 'Cannot delete a project with existing tickets, bundles, or worktrees');
|
|
6679
6739
|
}
|
|
6680
|
-
|
|
6740
|
+
};
|
|
6741
|
+
const completeClaimedProjectDeletion = async (projectId, claimId) => {
|
|
6742
|
+
await runExclusiveExecutionArtifactMaintenance(() => reconcileProjectExecutionArtifacts(projectId, {
|
|
6743
|
+
emitter: io,
|
|
6744
|
+
beginCleanup: () => {
|
|
6745
|
+
const marked = db.prepare(`
|
|
6746
|
+
UPDATE projects
|
|
6747
|
+
SET deletion_cleanup_started_at = COALESCE(deletion_cleanup_started_at, CURRENT_TIMESTAMP)
|
|
6748
|
+
WHERE id = ? AND deletion_claim_id = ?
|
|
6749
|
+
`).run(projectId, claimId);
|
|
6750
|
+
if (marked.changes !== 1) {
|
|
6751
|
+
throw new ProjectDeletionError(409, 'Project deletion ownership was lost');
|
|
6752
|
+
}
|
|
6753
|
+
},
|
|
6754
|
+
finalize: () => {
|
|
6755
|
+
db.transaction(() => {
|
|
6756
|
+
assertProjectDeletionEligible(projectId, claimId);
|
|
6757
|
+
if (db.prepare('SELECT 1 FROM execution_artifact_registration_intents WHERE project_id = ? LIMIT 1').get(projectId)) {
|
|
6758
|
+
throw new ExecutionArtifactCleanupError('active_registration', 'Cannot delete project while an artifact registration is active');
|
|
6759
|
+
}
|
|
6760
|
+
if (db.prepare("SELECT 1 FROM execution_artifacts WHERE project_id = ? AND availability <> 'deleted' LIMIT 1").get(projectId)) {
|
|
6761
|
+
throw new ExecutionArtifactCleanupError('cleanup_failed', 'Cannot delete project because artifact file cleanup failed');
|
|
6762
|
+
}
|
|
6763
|
+
db.prepare("DELETE FROM execution_artifacts WHERE project_id = ? AND availability = 'deleted'").run(projectId);
|
|
6764
|
+
deleteProjectAttachments(projectId);
|
|
6765
|
+
db.prepare(`
|
|
6766
|
+
UPDATE execution_runs
|
|
6767
|
+
SET parent_run_id = NULL, retry_of_run_id = NULL
|
|
6768
|
+
WHERE project_id = ?
|
|
6769
|
+
`).run(projectId);
|
|
6770
|
+
const deleted = db.prepare('DELETE FROM projects WHERE id = ? AND deletion_claim_id = ?').run(projectId, claimId);
|
|
6771
|
+
if (deleted.changes !== 1) {
|
|
6772
|
+
throw new ProjectDeletionError(409, 'Project deletion ownership was lost');
|
|
6773
|
+
}
|
|
6774
|
+
})();
|
|
6775
|
+
},
|
|
6776
|
+
}));
|
|
6777
|
+
};
|
|
6778
|
+
export const resumeInterruptedProjectDeletions = async () => {
|
|
6779
|
+
for (const project of listInterruptedProjectDeletions()) {
|
|
6780
|
+
try {
|
|
6781
|
+
await completeClaimedProjectDeletion(project.id, project.deletion_claim_id);
|
|
6782
|
+
}
|
|
6783
|
+
catch (error) {
|
|
6784
|
+
console.error(`Failed to resume deletion for project ${project.id}`, error);
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
};
|
|
6788
|
+
app.delete('/projects/:id', async (req, res) => {
|
|
6789
|
+
const claimId = randomUUID();
|
|
6790
|
+
try {
|
|
6791
|
+
db.transaction(() => {
|
|
6792
|
+
assertProjectDeletionEligible(req.params.id);
|
|
6793
|
+
const claimed = db.prepare(`
|
|
6794
|
+
UPDATE projects
|
|
6795
|
+
SET deletion_started_at = CURRENT_TIMESTAMP, deletion_claim_id = ?
|
|
6796
|
+
WHERE id = ? AND deletion_started_at IS NULL
|
|
6797
|
+
`).run(claimId, req.params.id);
|
|
6798
|
+
if (claimed.changes !== 1) {
|
|
6799
|
+
throw new ProjectDeletionError(409, 'Project deletion is already in progress');
|
|
6800
|
+
}
|
|
6801
|
+
})();
|
|
6802
|
+
}
|
|
6803
|
+
catch (error) {
|
|
6804
|
+
if (error instanceof ProjectDeletionError) {
|
|
6805
|
+
return res.status(error.status).json({ error: error.message });
|
|
6806
|
+
}
|
|
6807
|
+
throw error;
|
|
6808
|
+
}
|
|
6809
|
+
try {
|
|
6810
|
+
await completeClaimedProjectDeletion(req.params.id, claimId);
|
|
6811
|
+
}
|
|
6812
|
+
catch (error) {
|
|
6681
6813
|
db.prepare(`
|
|
6682
|
-
UPDATE
|
|
6683
|
-
SET
|
|
6684
|
-
WHERE
|
|
6685
|
-
`).run(req.params.id);
|
|
6686
|
-
|
|
6687
|
-
|
|
6814
|
+
UPDATE projects
|
|
6815
|
+
SET deletion_started_at = NULL, deletion_claim_id = NULL, deletion_cleanup_started_at = NULL
|
|
6816
|
+
WHERE id = ? AND deletion_claim_id = ? AND deletion_cleanup_started_at IS NULL
|
|
6817
|
+
`).run(req.params.id, claimId);
|
|
6818
|
+
if (error instanceof ExecutionArtifactCleanupError) {
|
|
6819
|
+
return res.status(409).json({ error: error.message });
|
|
6820
|
+
}
|
|
6821
|
+
if (error instanceof ProjectDeletionError) {
|
|
6822
|
+
return res.status(error.status).json({ error: error.message });
|
|
6823
|
+
}
|
|
6824
|
+
throw error;
|
|
6825
|
+
}
|
|
6688
6826
|
res.status(204).end();
|
|
6689
6827
|
});
|
|
6690
6828
|
app.post('/tickets', (req, res) => {
|
|
@@ -7612,22 +7750,7 @@ app.get('/projects/:id/execution-timeline/:runId', (req, res) => {
|
|
|
7612
7750
|
if (scope.scopeEntity && !links.some(link => link.entity_type === scope.scopeEntity?.type && link.entity_id === scope.scopeEntity.id)) {
|
|
7613
7751
|
return res.status(404).json({ error: 'Execution run not found' });
|
|
7614
7752
|
}
|
|
7615
|
-
|
|
7616
|
-
const retryEligibility = getExecutionRetryEligibility(project.id, run.id);
|
|
7617
|
-
const details = run.kind === 'test'
|
|
7618
|
-
? serializeTestExecutionDetail(testDetails, retryEligibility)
|
|
7619
|
-
: run.kind === 'github_actions'
|
|
7620
|
-
? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
|
|
7621
|
-
: ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
|
|
7622
|
-
? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
|
|
7623
|
-
: { kind: run.kind, available: true, retry_eligibility: retryEligibility };
|
|
7624
|
-
return res.json({
|
|
7625
|
-
run,
|
|
7626
|
-
links,
|
|
7627
|
-
events: listLifecycleEvents(project.id, run.id).map(toCanonicalLifecycleEvent),
|
|
7628
|
-
details,
|
|
7629
|
-
agent_runs: listAgentRunsForExecutionRun(run.id),
|
|
7630
|
-
});
|
|
7753
|
+
return res.json(buildExecutionRunDetailResponse(project, run));
|
|
7631
7754
|
});
|
|
7632
7755
|
app.get('/projects/:id/execution-runs/:runId', (req, res) => {
|
|
7633
7756
|
const project = getProjectById(req.params.id);
|
|
@@ -7638,19 +7761,44 @@ app.get('/projects/:id/execution-runs/:runId', (req, res) => {
|
|
|
7638
7761
|
if (!run) {
|
|
7639
7762
|
return res.status(404).json({ error: 'Execution run not found' });
|
|
7640
7763
|
}
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7764
|
+
return res.json(buildExecutionRunDetailResponse(project, run));
|
|
7765
|
+
});
|
|
7766
|
+
app.get('/projects/:id/execution-runs/:runId/artifacts/:artifactId', async (req, res) => {
|
|
7767
|
+
const project = getProjectById(req.params.id);
|
|
7768
|
+
if (!project || !getExecutionRun(project.id, req.params.runId)) {
|
|
7769
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7770
|
+
}
|
|
7771
|
+
const retrieval = await openExecutionArtifactForRetrieval(project.id, req.params.runId, req.params.artifactId, io);
|
|
7772
|
+
if (retrieval.kind === 'not_found') {
|
|
7773
|
+
return res.status(404).json({ error: 'Not found' });
|
|
7774
|
+
}
|
|
7775
|
+
if (retrieval.kind === 'unavailable') {
|
|
7776
|
+
return res.status(410).json({ error: 'Artifact unavailable' });
|
|
7777
|
+
}
|
|
7778
|
+
if (retrieval.kind === 'external') {
|
|
7779
|
+
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
7780
|
+
res.setHeader('Location', retrieval.url);
|
|
7781
|
+
return res.status(303).end();
|
|
7782
|
+
}
|
|
7783
|
+
res.setHeader('Content-Type', retrieval.mediaType);
|
|
7784
|
+
res.setHeader('Content-Length', retrieval.byteSize);
|
|
7785
|
+
res.setHeader('Content-Disposition', `attachment; filename="${retrieval.filename}"`);
|
|
7786
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
7787
|
+
const stream = retrieval.handle.createReadStream({ autoClose: true });
|
|
7788
|
+
stream.on('error', () => {
|
|
7789
|
+
console.error('Execution artifact stream failed', {
|
|
7790
|
+
project_id: project.id,
|
|
7791
|
+
execution_run_id: req.params.runId,
|
|
7792
|
+
artifact_id: req.params.artifactId,
|
|
7793
|
+
});
|
|
7794
|
+
res.destroy();
|
|
7795
|
+
});
|
|
7796
|
+
res.on('close', () => {
|
|
7797
|
+
if (!stream.destroyed) {
|
|
7798
|
+
stream.destroy();
|
|
7799
|
+
}
|
|
7800
|
+
});
|
|
7801
|
+
return stream.pipe(res);
|
|
7654
7802
|
});
|
|
7655
7803
|
app.post('/projects/:id/execution-runs/:runId/retry', async (req, res) => {
|
|
7656
7804
|
const project = getProjectById(req.params.id);
|
|
@@ -7991,12 +8139,21 @@ if (clientBuild) {
|
|
|
7991
8139
|
});
|
|
7992
8140
|
});
|
|
7993
8141
|
}
|
|
8142
|
+
app.use((error, _req, res, next) => {
|
|
8143
|
+
if (error instanceof Error && error.message.includes('Project is being deleted')) {
|
|
8144
|
+
res.status(409).json({ error: 'Project deletion is in progress' });
|
|
8145
|
+
return;
|
|
8146
|
+
}
|
|
8147
|
+
next(error);
|
|
8148
|
+
});
|
|
7994
8149
|
io.on('connection', (socket) => {
|
|
7995
8150
|
console.log('client connected:', socket.id);
|
|
7996
8151
|
registerTerminalSocketHandlers(io, socket);
|
|
7997
8152
|
});
|
|
7998
8153
|
const DEFAULT_PORT = 3001;
|
|
7999
8154
|
let executionEvidenceCleanupTimer = null;
|
|
8155
|
+
let executionArtifactCleanupTimer = null;
|
|
8156
|
+
let executionArtifactCleanupPromise = null;
|
|
8000
8157
|
let startServerPromise = null;
|
|
8001
8158
|
let shutdownServerPromise = null;
|
|
8002
8159
|
let shutdownRequested = false;
|
|
@@ -8023,6 +8180,41 @@ const startExecutionEvidenceCleanupTimer = () => {
|
|
|
8023
8180
|
executionEvidenceCleanupTimer = setInterval(runExecutionEvidenceCleanup, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS);
|
|
8024
8181
|
executionEvidenceCleanupTimer.unref();
|
|
8025
8182
|
};
|
|
8183
|
+
export const runExecutionArtifactCleanup = (mode) => {
|
|
8184
|
+
if (executionArtifactCleanupPromise) {
|
|
8185
|
+
return executionArtifactCleanupPromise;
|
|
8186
|
+
}
|
|
8187
|
+
executionArtifactCleanupPromise = (async () => {
|
|
8188
|
+
if (mode === 'periodic') {
|
|
8189
|
+
await resumeInterruptedProjectDeletions();
|
|
8190
|
+
}
|
|
8191
|
+
await runExclusiveExecutionArtifactMaintenance(async () => {
|
|
8192
|
+
const intents = await reconcileExecutionArtifactRegistrationIntents({ mode });
|
|
8193
|
+
const artifacts = await reconcileFinalizedExecutionArtifacts({ emitter: io });
|
|
8194
|
+
const failures = intents.failed.length + artifacts.failed.length;
|
|
8195
|
+
if (failures > 0) {
|
|
8196
|
+
console.error(`Artifact reconciliation retained ${failures} item(s) for retry`);
|
|
8197
|
+
}
|
|
8198
|
+
if (intents.deleted + artifacts.deleted > 0) {
|
|
8199
|
+
console.log(`Reconciled ${intents.deleted} artifact registration intent(s) and expired ${artifacts.deleted} artifact(s)`);
|
|
8200
|
+
}
|
|
8201
|
+
});
|
|
8202
|
+
})().catch(error => {
|
|
8203
|
+
console.error('Failed to reconcile execution artifacts', error);
|
|
8204
|
+
}).finally(() => {
|
|
8205
|
+
executionArtifactCleanupPromise = null;
|
|
8206
|
+
});
|
|
8207
|
+
return executionArtifactCleanupPromise;
|
|
8208
|
+
};
|
|
8209
|
+
const startExecutionArtifactCleanupTimer = () => {
|
|
8210
|
+
if (executionArtifactCleanupTimer) {
|
|
8211
|
+
return;
|
|
8212
|
+
}
|
|
8213
|
+
executionArtifactCleanupTimer = setInterval(() => {
|
|
8214
|
+
void runExecutionArtifactCleanup('periodic');
|
|
8215
|
+
}, resolveArtifactCleanupIntervalMs());
|
|
8216
|
+
executionArtifactCleanupTimer.unref();
|
|
8217
|
+
};
|
|
8026
8218
|
const resolveStartResult = (host) => {
|
|
8027
8219
|
const address = httpServer.address();
|
|
8028
8220
|
const { displayHost } = resolveDesktopServerHost(host);
|
|
@@ -8050,8 +8242,12 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
|
8050
8242
|
return startServerPromise;
|
|
8051
8243
|
}
|
|
8052
8244
|
startServerPromise = (async () => {
|
|
8245
|
+
releaseInterruptedProjectDeletionClaims();
|
|
8246
|
+
await resumeInterruptedProjectDeletions();
|
|
8053
8247
|
await reconcileStartupExecutions({ getGitHubConfig: getGitHubConnectionConfig });
|
|
8054
8248
|
throwIfShutdownRequested();
|
|
8249
|
+
await runExecutionArtifactCleanup('startup');
|
|
8250
|
+
throwIfShutdownRequested();
|
|
8055
8251
|
startGitHubActionsPolling(io);
|
|
8056
8252
|
try {
|
|
8057
8253
|
throwIfShutdownRequested();
|
|
@@ -8077,6 +8273,7 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
|
8077
8273
|
runExecutionEvidenceCleanup();
|
|
8078
8274
|
throwIfShutdownRequested();
|
|
8079
8275
|
startExecutionEvidenceCleanupTimer();
|
|
8276
|
+
startExecutionArtifactCleanupTimer();
|
|
8080
8277
|
return resolveStartResult(host);
|
|
8081
8278
|
})().finally(() => {
|
|
8082
8279
|
startServerPromise = null;
|
|
@@ -8098,6 +8295,11 @@ export const shutdownServer = async (signal) => {
|
|
|
8098
8295
|
clearInterval(executionEvidenceCleanupTimer);
|
|
8099
8296
|
executionEvidenceCleanupTimer = null;
|
|
8100
8297
|
}
|
|
8298
|
+
if (executionArtifactCleanupTimer) {
|
|
8299
|
+
clearInterval(executionArtifactCleanupTimer);
|
|
8300
|
+
executionArtifactCleanupTimer = null;
|
|
8301
|
+
}
|
|
8302
|
+
await executionArtifactCleanupPromise;
|
|
8101
8303
|
await stopMobileAccess();
|
|
8102
8304
|
await clearAllTestSessions(io);
|
|
8103
8305
|
await clearAllGitHubActionsSessions();
|