@straiffi/archon 1.7.4 → 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-BgMz3NNT.js → ActivityView-CT9iCPyc.js} +1 -1
- package/dist/client/assets/{GitHubActionsDialog-CNqWDS3E.js → GitHubActionsDialog-BA6xyQeX.js} +1 -1
- package/dist/client/assets/{TestsDialog-Cll7xJe0.js → TestsDialog-D2qrW5Ug.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BL5dYiul.js → architectureDiagram-3BPJPVTR-R6v7w0Nl.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Bi5x2v80.js → blockDiagram-GPEHLZMM-CHlBlnJq.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-esEm54Ay.js → c4Diagram-AAUBKEIU-BvZKqUc6.js} +1 -1
- package/dist/client/assets/channel-Ckv8cPSf.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-DACfY8xM.js → chunk-2J33WTMH-2gsIkBeO.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-DZNUiaUs.js → chunk-3OPIFGDE-CcqPBjqx.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-ClDfkMAp.js → chunk-5ZQYHXKU-DX_wK5Rp.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-CCwA3GWT.js → chunk-727SXJPM-B9WUxzJL.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Byl4DRZ9.js → chunk-AQP2D5EJ-BG0s75_H.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-CXymZQ7A.js → chunk-CSCIHK7Q-CyNcvvqW.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-VV3D8g6a.js → chunk-KSCS5N6A-CWVy_Wi0.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-BL9N5GRI.js → chunk-L5ZTLDWV-3dsXRB0X.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-DKNN-zno.js → chunk-LZXEDZCA-B09QSB9z.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-CHCpQ1yr.js → chunk-ND2GUHAM-BxzAdxTN.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-B0Dm74mt.js → chunk-NZK2D7GU-D4ZfFwbB.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-DAa5z2xG.js → chunk-O5CBEL6O-BxQULuKS.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-DrVJWKVR.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-B0fsl8mE.js → dagre-BM42HDAG-Dx_V7OMy.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-Cx_VEbSS.js → diagram-2AECGRRQ-Cc3VTg9P.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-Cv7VVn5r.js → diagram-5GNKFQAL-mJ1xhs2G.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C8Sl5czV.js → diagram-KO2AKTUF-CLyT9ac8.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-C3dZaLz2.js → diagram-LMA3HP47-CVr47yij.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BAU9DtTr.js → diagram-OG6HWLK6-Dp5TJ8lM.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-DpNc-eBD.js → erDiagram-TEJ5UH35-BBxwyu0r.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-DGwTAT1S.js → flowDiagram-I6XJVG4X-CuFYzVQ-.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-MPZhEgzJ.js → ganttDiagram-6RSMTGT7-aRQarcQY.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CY0tHL5N.js → gitGraphDiagram-PVQCEYII-B3mHSkBv.js} +1 -1
- package/dist/client/assets/index-BJGHMaMz.css +2 -0
- package/dist/client/assets/{index-CJRgmWwY.js → index-Oz0S7bcT.js} +106 -106
- package/dist/client/assets/{infoDiagram-5YYISTIA-BBZeKjf4.js → infoDiagram-5YYISTIA-BSyryQJ9.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CM_iFrES.js → ishikawaDiagram-YF4QCWOH-BAGbrtSG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Do_lY94X.js → journeyDiagram-JHISSGLW-CDTht6k6.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-brxg3j37.js → kanban-definition-UN3LZRKU-BDtuMp0H.js} +1 -1
- package/dist/client/assets/{line-PHHbm9rj.js → line-n0aNicmA.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-CLxAkW69.js → mermaid-parser.core-CdE4vZ4k.js} +1 -1
- package/dist/client/assets/{mermaid.core-CebvSmx9.js → mermaid.core-C546CQj1.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-CX_-ZaJF.js → mindmap-definition-RKZ34NQL-BwO779am.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BJuHvql_.js → pieDiagram-4H26LBE5-DU1k71kn.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-DiJll7l9.js → quadrantDiagram-W4KKPZXB-CvM5lyQg.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-Bg-tXgcj.js → requirementDiagram-4Y6WPE33-DDBdSq0B.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BoK2egfZ.js → sankeyDiagram-5OEKKPKP-ELM2mu81.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DcrDWNzv.js → sequenceDiagram-3UESZ5HK-D7uF-nRR.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-B995qt1S.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-DqSVAhqL.js → timeline-definition-PNZ67QCA-zHJGFvZ8.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-D9wVraVY.js → vennDiagram-CIIHVFJN-EG1HFfD7.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CLff9zVd.js → wardleyDiagram-YWT4CUSO-DdQevxXg.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-BuHUMMH2.js → xychartDiagram-2RQKCTM6-BjihNG0E.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 +270 -54
- 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/buildSequences.js +25 -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/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/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';
|
|
@@ -57,7 +57,9 @@ import { clearAllPreviewProxies } from './lib/previewProxy.js';
|
|
|
57
57
|
import { createExecutionRun, addExecutionRunLink, addExecutionRunLinks, getExecutionRun, hasActiveBuildExecutionRunForTicket, getGitHubActionsExecutionDetails, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listLifecycleEvents, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
|
|
58
58
|
import { dispatchExecutionRetry, getExecutionRetryEligibility, resolveExecutionRetry } from './lib/executionRetry.js';
|
|
59
59
|
import { listExecutionTimeline } from './lib/executionTimeline.js';
|
|
60
|
+
import { loadExecutionArtifactSummariesForRuns, openExecutionArtifactForRetrieval, } from './lib/executionArtifacts.js';
|
|
60
61
|
import { cleanupAllExecutionEvidence, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS, resolveExecutionEvidenceRetentionDays, } from './lib/executionRunRetention.js';
|
|
62
|
+
import { ExecutionArtifactCleanupError, reconcileExecutionArtifactRegistrationIntents, reconcileFinalizedExecutionArtifacts, reconcileProjectExecutionArtifacts, resolveArtifactCleanupIntervalMs, } from './lib/executionArtifactRetention.js';
|
|
61
63
|
import { shutdownRealtimeServer } from './lib/shutdown.js';
|
|
62
64
|
import { resolveClientBuildPaths, shouldServeClientApp, STATIC_CLIENT_DIRECTORY_OPTIONS, STATIC_CLIENT_INDEX_OPTIONS, } from './lib/staticClient.js';
|
|
63
65
|
import { deleteIntegrationConnection, getGitHubConnectionConfig, getJiraConnectionConfig, listIntegrationConnections, upsertIntegrationConnection, } from './lib/integrations/index.js';
|
|
@@ -72,7 +74,7 @@ import { PlanningReferenceError, resolveExternalPlanningDescription } from './li
|
|
|
72
74
|
import { refreshIfNeeded as refreshAssignedIssues, clearCachedAssignedIssues } from './lib/integrations/assignedIssuesCache.js';
|
|
73
75
|
import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib/commitMessage.js';
|
|
74
76
|
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';
|
|
77
|
+
import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listInterruptedProjectDeletions, listProjects, releaseInterruptedProjectDeletionClaims, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
|
|
76
78
|
import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
|
|
77
79
|
import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
|
|
78
80
|
import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTicketState, } from './lib/ticketWorkflowOperations.js';
|
|
@@ -94,7 +96,30 @@ import { exportSpecToDocs, checkSpecExportCollision } from './lib/specExport.js'
|
|
|
94
96
|
import { createTicketDependency, deleteTicketDependenciesForTicket, deleteTicketDependency, getTicketDependency, isTicketRunSetupReady, listTicketDependencies, validateCreateTicketDependency, } from './lib/ticketDependencies.js';
|
|
95
97
|
import { startBuild } from './workers/build.js';
|
|
96
98
|
import { expandPlanningTicketOutline, generatePlanningTicketOutline, generatePlanText, prepareLinkedPlanningResearch, startPlan, stopAllPlanAgents, } from './workers/plan.js';
|
|
97
|
-
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
|
+
};
|
|
98
123
|
const getErrorMessage = (error) => {
|
|
99
124
|
return error instanceof Error ? error.message : String(error);
|
|
100
125
|
};
|
|
@@ -112,6 +137,9 @@ const requireExistingProject = (projectId) => {
|
|
|
112
137
|
if (!project) {
|
|
113
138
|
return { status: 404, error: 'Not found' };
|
|
114
139
|
}
|
|
140
|
+
if (project.deletion_started_at) {
|
|
141
|
+
return { status: 409, error: 'Project deletion is in progress' };
|
|
142
|
+
}
|
|
115
143
|
return { project };
|
|
116
144
|
};
|
|
117
145
|
const requireProjectMemoryProject = (projectId) => {
|
|
@@ -261,6 +289,26 @@ const serializeGitHubActionsExecutionDetail = (details) => {
|
|
|
261
289
|
jobs_count: details.jobs_count,
|
|
262
290
|
};
|
|
263
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
|
+
};
|
|
264
312
|
const formatWorktreeDeleteError = (target, branch, error) => {
|
|
265
313
|
return `Unable to delete the ${target} worktree for branch "${branch}": ${getErrorMessage(error)}`;
|
|
266
314
|
};
|
|
@@ -2400,6 +2448,17 @@ app.post('/projects/:id/chat-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2400
2448
|
if (error) {
|
|
2401
2449
|
return res.status(400).json({ error: getChatAttachmentUploadErrorMessage(error) });
|
|
2402
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
|
+
}
|
|
2403
2462
|
const files = (req.files ?? []);
|
|
2404
2463
|
if (files.length === 0) {
|
|
2405
2464
|
return res.status(400).json({ error: 'At least one image is required.' });
|
|
@@ -2417,11 +2476,11 @@ app.post('/projects/:id/chat-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2417
2476
|
try {
|
|
2418
2477
|
const attachments = files.map(file => {
|
|
2419
2478
|
const attachment = createChatAttachment({
|
|
2420
|
-
chatSessionId:
|
|
2479
|
+
chatSessionId: currentChatSession.id,
|
|
2421
2480
|
originalName: file.originalname,
|
|
2422
2481
|
mimeType: file.mimetype,
|
|
2423
2482
|
buffer: file.buffer,
|
|
2424
|
-
projectId:
|
|
2483
|
+
projectId: currentProject.id,
|
|
2425
2484
|
});
|
|
2426
2485
|
createdAttachmentIds.push(attachment.id);
|
|
2427
2486
|
return attachment;
|
|
@@ -2692,6 +2751,17 @@ app.post('/projects/:id/spec-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2692
2751
|
if (error) {
|
|
2693
2752
|
return res.status(400).json({ error: getSpecAttachmentUploadErrorMessage(error) });
|
|
2694
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
|
+
}
|
|
2695
2765
|
const files = (req.files ?? []);
|
|
2696
2766
|
if (files.length === 0) {
|
|
2697
2767
|
return res.status(400).json({ error: 'At least one image is required.' });
|
|
@@ -2709,11 +2779,11 @@ app.post('/projects/:id/spec-sessions/:sessionId/attachments', (req, res) => {
|
|
|
2709
2779
|
try {
|
|
2710
2780
|
const attachments = files.map(file => {
|
|
2711
2781
|
const attachment = createSpecAttachment({
|
|
2712
|
-
specSessionId:
|
|
2782
|
+
specSessionId: currentSpecSession.id,
|
|
2713
2783
|
originalName: file.originalname,
|
|
2714
2784
|
mimeType: file.mimetype,
|
|
2715
2785
|
buffer: file.buffer,
|
|
2716
|
-
projectId:
|
|
2786
|
+
projectId: currentProject.id,
|
|
2717
2787
|
});
|
|
2718
2788
|
createdAttachmentIds.push(attachment.id);
|
|
2719
2789
|
return attachment;
|
|
@@ -6659,32 +6729,114 @@ app.patch('/projects/:id', (req, res) => {
|
|
|
6659
6729
|
const updated = db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id);
|
|
6660
6730
|
res.json(serializeProject(updated ?? null));
|
|
6661
6731
|
});
|
|
6662
|
-
|
|
6663
|
-
const project =
|
|
6732
|
+
const assertProjectDeletionEligible = (projectId, claimId) => {
|
|
6733
|
+
const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(projectId);
|
|
6664
6734
|
if (!project) {
|
|
6665
|
-
|
|
6735
|
+
throw new ProjectDeletionError(404, 'Not found');
|
|
6666
6736
|
}
|
|
6667
|
-
|
|
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);
|
|
6668
6744
|
if (projectCount <= 1) {
|
|
6669
|
-
|
|
6745
|
+
throw new ProjectDeletionError(409, 'Cannot delete the last remaining project');
|
|
6670
6746
|
}
|
|
6671
|
-
if (countInboundProjectLinks(
|
|
6672
|
-
|
|
6747
|
+
if (countInboundProjectLinks(projectId) > 0) {
|
|
6748
|
+
throw new ProjectDeletionError(409, 'Cannot delete a project that is linked from another project');
|
|
6673
6749
|
}
|
|
6674
|
-
const stats = getProjectTicketStats(
|
|
6750
|
+
const stats = getProjectTicketStats(projectId);
|
|
6675
6751
|
if (stats.ticketCount > 0 || stats.worktreeCount > 0 || stats.bundleCount > 0) {
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6752
|
+
throw new ProjectDeletionError(409, 'Cannot delete a project with existing tickets, bundles, or worktrees');
|
|
6753
|
+
}
|
|
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
|
+
})();
|
|
6679
6816
|
}
|
|
6680
|
-
|
|
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) {
|
|
6681
6827
|
db.prepare(`
|
|
6682
|
-
UPDATE
|
|
6683
|
-
SET
|
|
6684
|
-
WHERE
|
|
6685
|
-
`).run(req.params.id);
|
|
6686
|
-
|
|
6687
|
-
|
|
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
|
+
}
|
|
6688
6840
|
res.status(204).end();
|
|
6689
6841
|
});
|
|
6690
6842
|
app.post('/tickets', (req, res) => {
|
|
@@ -7612,22 +7764,7 @@ app.get('/projects/:id/execution-timeline/:runId', (req, res) => {
|
|
|
7612
7764
|
if (scope.scopeEntity && !links.some(link => link.entity_type === scope.scopeEntity?.type && link.entity_id === scope.scopeEntity.id)) {
|
|
7613
7765
|
return res.status(404).json({ error: 'Execution run not found' });
|
|
7614
7766
|
}
|
|
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
|
-
});
|
|
7767
|
+
return res.json(buildExecutionRunDetailResponse(project, run));
|
|
7631
7768
|
});
|
|
7632
7769
|
app.get('/projects/:id/execution-runs/:runId', (req, res) => {
|
|
7633
7770
|
const project = getProjectById(req.params.id);
|
|
@@ -7638,19 +7775,44 @@ app.get('/projects/:id/execution-runs/:runId', (req, res) => {
|
|
|
7638
7775
|
if (!run) {
|
|
7639
7776
|
return res.status(404).json({ error: 'Execution run not found' });
|
|
7640
7777
|
}
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
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();
|
|
7809
|
+
});
|
|
7810
|
+
res.on('close', () => {
|
|
7811
|
+
if (!stream.destroyed) {
|
|
7812
|
+
stream.destroy();
|
|
7813
|
+
}
|
|
7814
|
+
});
|
|
7815
|
+
return stream.pipe(res);
|
|
7654
7816
|
});
|
|
7655
7817
|
app.post('/projects/:id/execution-runs/:runId/retry', async (req, res) => {
|
|
7656
7818
|
const project = getProjectById(req.params.id);
|
|
@@ -7991,12 +8153,21 @@ if (clientBuild) {
|
|
|
7991
8153
|
});
|
|
7992
8154
|
});
|
|
7993
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
|
+
});
|
|
7994
8163
|
io.on('connection', (socket) => {
|
|
7995
8164
|
console.log('client connected:', socket.id);
|
|
7996
8165
|
registerTerminalSocketHandlers(io, socket);
|
|
7997
8166
|
});
|
|
7998
8167
|
const DEFAULT_PORT = 3001;
|
|
7999
8168
|
let executionEvidenceCleanupTimer = null;
|
|
8169
|
+
let executionArtifactCleanupTimer = null;
|
|
8170
|
+
let executionArtifactCleanupPromise = null;
|
|
8000
8171
|
let startServerPromise = null;
|
|
8001
8172
|
let shutdownServerPromise = null;
|
|
8002
8173
|
let shutdownRequested = false;
|
|
@@ -8023,6 +8194,41 @@ const startExecutionEvidenceCleanupTimer = () => {
|
|
|
8023
8194
|
executionEvidenceCleanupTimer = setInterval(runExecutionEvidenceCleanup, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS);
|
|
8024
8195
|
executionEvidenceCleanupTimer.unref();
|
|
8025
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
|
+
};
|
|
8026
8232
|
const resolveStartResult = (host) => {
|
|
8027
8233
|
const address = httpServer.address();
|
|
8028
8234
|
const { displayHost } = resolveDesktopServerHost(host);
|
|
@@ -8050,8 +8256,12 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
|
8050
8256
|
return startServerPromise;
|
|
8051
8257
|
}
|
|
8052
8258
|
startServerPromise = (async () => {
|
|
8259
|
+
releaseInterruptedProjectDeletionClaims();
|
|
8260
|
+
await resumeInterruptedProjectDeletions();
|
|
8053
8261
|
await reconcileStartupExecutions({ getGitHubConfig: getGitHubConnectionConfig });
|
|
8054
8262
|
throwIfShutdownRequested();
|
|
8263
|
+
await runExecutionArtifactCleanup('startup');
|
|
8264
|
+
throwIfShutdownRequested();
|
|
8055
8265
|
startGitHubActionsPolling(io);
|
|
8056
8266
|
try {
|
|
8057
8267
|
throwIfShutdownRequested();
|
|
@@ -8077,6 +8287,7 @@ export const startServer = ({ port = DEFAULT_PORT, host } = {}) => {
|
|
|
8077
8287
|
runExecutionEvidenceCleanup();
|
|
8078
8288
|
throwIfShutdownRequested();
|
|
8079
8289
|
startExecutionEvidenceCleanupTimer();
|
|
8290
|
+
startExecutionArtifactCleanupTimer();
|
|
8080
8291
|
return resolveStartResult(host);
|
|
8081
8292
|
})().finally(() => {
|
|
8082
8293
|
startServerPromise = null;
|
|
@@ -8098,6 +8309,11 @@ export const shutdownServer = async (signal) => {
|
|
|
8098
8309
|
clearInterval(executionEvidenceCleanupTimer);
|
|
8099
8310
|
executionEvidenceCleanupTimer = null;
|
|
8100
8311
|
}
|
|
8312
|
+
if (executionArtifactCleanupTimer) {
|
|
8313
|
+
clearInterval(executionArtifactCleanupTimer);
|
|
8314
|
+
executionArtifactCleanupTimer = null;
|
|
8315
|
+
}
|
|
8316
|
+
await executionArtifactCleanupPromise;
|
|
8101
8317
|
await stopMobileAccess();
|
|
8102
8318
|
await clearAllTestSessions(io);
|
|
8103
8319
|
await clearAllGitHubActionsSessions();
|