@straiffi/archon 1.7.3 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/client/assets/ActivityView-BgMz3NNT.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-CNqWDS3E.js +1 -0
  3. package/dist/client/assets/TestsDialog-Cll7xJe0.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BzzGotac.js → architectureDiagram-3BPJPVTR-BL5dYiul.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DIcqLHKg.js → blockDiagram-GPEHLZMM-Bi5x2v80.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-Hc1Sqmhs.js → c4Diagram-AAUBKEIU-esEm54Ay.js} +1 -1
  7. package/dist/client/assets/channel-CqNHF3FD.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-CWSwjsgk.js → chunk-2J33WTMH-DACfY8xM.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-BnX3DljN.js → chunk-3OPIFGDE-DZNUiaUs.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-EyyJlI-d.js → chunk-5ZQYHXKU-ClDfkMAp.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-Ci6CfxWj.js → chunk-727SXJPM-CCwA3GWT.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-RKZNsJTm.js → chunk-AQP2D5EJ-Byl4DRZ9.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DBf0Euoj.js → chunk-CSCIHK7Q-CXymZQ7A.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-BmLQI_2S.js → chunk-KSCS5N6A-VV3D8g6a.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-DKwVAb8G.js → chunk-L5ZTLDWV-BL9N5GRI.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-axotKkbz.js → chunk-LZXEDZCA-DKNN-zno.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-BBlJ4nXC.js → chunk-ND2GUHAM-CHCpQ1yr.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-BKJGoZZt.js → chunk-NZK2D7GU-B0Dm74mt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-C-Ghigo1.js → chunk-O5CBEL6O-DAa5z2xG.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-CCzVWYcd.js → chunk-WU5MYG2G-DrVJWKVR.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BjEgQwKg.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-gGomH-F_.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-rwIdPcjI.js → dagre-BM42HDAG-B0fsl8mE.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DJWxOQsg.js → diagram-2AECGRRQ-Cx_VEbSS.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-C-mQeFtd.js → diagram-5GNKFQAL-Cv7VVn5r.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-BAu6u4sR.js → diagram-KO2AKTUF-C8Sl5czV.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-U6CbvBn0.js → diagram-LMA3HP47-C3dZaLz2.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-DNTwFcs1.js → diagram-OG6HWLK6-BAU9DtTr.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DaHX6Xw5.js → erDiagram-TEJ5UH35-DpNc-eBD.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-B9hKqL2l.js → flowDiagram-I6XJVG4X-DGwTAT1S.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-BQ1HPHVO.js → ganttDiagram-6RSMTGT7-MPZhEgzJ.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-BBRcsmMK.js → gitGraphDiagram-PVQCEYII-CY0tHL5N.js} +1 -1
  33. package/dist/client/assets/index-CJRgmWwY.js +273 -0
  34. package/dist/client/assets/index-mRkJWu70.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-DUoNiSGC.js → infoDiagram-5YYISTIA-BBZeKjf4.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-DJN1y2w8.js → ishikawaDiagram-YF4QCWOH-CM_iFrES.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-BxLLMT70.js → journeyDiagram-JHISSGLW-Do_lY94X.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-BdCPistJ.js → kanban-definition-UN3LZRKU-brxg3j37.js} +1 -1
  39. package/dist/client/assets/{line-4mIgXsZd.js → line-PHHbm9rj.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-Pu1QGWJ-.js → mermaid-parser.core-CLxAkW69.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-DnH9DrgZ.js → mermaid.core-CebvSmx9.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-DqE4ba3C.js → mindmap-definition-RKZ34NQL-CX_-ZaJF.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-Crjy9ejf.js → pieDiagram-4H26LBE5-BJuHvql_.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-BUSZ2VB7.js → quadrantDiagram-W4KKPZXB-DiJll7l9.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-CBhFKEoV.js → requirementDiagram-4Y6WPE33-Bg-tXgcj.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-CgdbY41v.js → sankeyDiagram-5OEKKPKP-BoK2egfZ.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-hkp_H7AE.js → sequenceDiagram-3UESZ5HK-DcrDWNzv.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-C6x45RWp.js → stateDiagram-AJRCARHV-B995qt1S.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Dn8P7Sa1.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-CjnZWttQ.js → timeline-definition-PNZ67QCA-DqSVAhqL.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-DU5ZyYju.js → vennDiagram-CIIHVFJN-D9wVraVY.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-CaCEnCB0.js → wardleyDiagram-YWT4CUSO-CLff9zVd.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-Ce3xa37U.js → xychartDiagram-2RQKCTM6-BuHUMMH2.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +1 -0
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +145 -88
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/attachments.js +24 -0
  60. package/dist/server/lib/attachments.js.map +1 -1
  61. package/dist/server/lib/buildOpenQuestionOperations.js +17 -2
  62. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -1
  63. package/dist/server/lib/bundleReviewOperations.js +9 -0
  64. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  65. package/dist/server/lib/chatOperations.js +40 -16
  66. package/dist/server/lib/chatOperations.js.map +1 -1
  67. package/dist/server/lib/executionRetry.js +694 -0
  68. package/dist/server/lib/executionRetry.js.map +1 -0
  69. package/dist/server/lib/executionRetryContext.js +489 -0
  70. package/dist/server/lib/executionRetryContext.js.map +1 -0
  71. package/dist/server/lib/executionRetryPredicates.js +74 -0
  72. package/dist/server/lib/executionRetryPredicates.js.map +1 -0
  73. package/dist/server/lib/executionRuns.js +141 -38
  74. package/dist/server/lib/executionRuns.js.map +1 -1
  75. package/dist/server/lib/executionTimeline.js +45 -0
  76. package/dist/server/lib/executionTimeline.js.map +1 -0
  77. package/dist/server/lib/specOperations.js +51 -16
  78. package/dist/server/lib/specOperations.js.map +1 -1
  79. package/dist/server/lib/specReviewContext.js +10 -4
  80. package/dist/server/lib/specReviewContext.js.map +1 -1
  81. package/dist/server/lib/specReviewOperations.js +33 -5
  82. package/dist/server/lib/specReviewOperations.js.map +1 -1
  83. package/dist/server/lib/testSessions.js +2 -5
  84. package/dist/server/lib/testSessions.js.map +1 -1
  85. package/dist/server/lib/ticketFollowUpOperations.js +74 -27
  86. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  87. package/dist/server/lib/ticketMessages.js +16 -0
  88. package/dist/server/lib/ticketMessages.js.map +1 -1
  89. package/dist/server/lib/types.js +1 -0
  90. package/dist/server/lib/types.js.map +1 -1
  91. package/dist/server/workers/build.js +1 -1
  92. package/dist/server/workers/build.js.map +1 -1
  93. package/dist/server/workers/chat.js +26 -20
  94. package/dist/server/workers/chat.js.map +1 -1
  95. package/dist/server/workers/followUp.js +24 -9
  96. package/dist/server/workers/followUp.js.map +1 -1
  97. package/dist/server/workers/plan.js +23 -4
  98. package/dist/server/workers/plan.js.map +1 -1
  99. package/dist/server/workers/planFollowUp.js +12 -9
  100. package/dist/server/workers/planFollowUp.js.map +1 -1
  101. package/dist/server/workers/review.js +2 -2
  102. package/dist/server/workers/review.js.map +1 -1
  103. package/dist/server/workers/specReview.js +2 -2
  104. package/dist/server/workers/specReview.js.map +1 -1
  105. package/dist/server/workers/specSession.js +29 -22
  106. package/dist/server/workers/specSession.js.map +1 -1
  107. package/package.json +1 -1
  108. package/dist/client/assets/ActivityView-DTEenQas.js +0 -1
  109. package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +0 -1
  110. package/dist/client/assets/TestsDialog-cBaoffCs.js +0 -4
  111. package/dist/client/assets/channel-Cym17waa.js +0 -1
  112. package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +0 -1
  113. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +0 -1
  114. package/dist/client/assets/index-CEMaJCPS.js +0 -273
  115. package/dist/client/assets/index-lWftq65U.css +0 -2
  116. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +0 -1
@@ -54,7 +54,9 @@ 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, hasDirectRetryChild, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
57
+ import { createExecutionRun, addExecutionRunLink, addExecutionRunLinks, getExecutionRun, hasActiveBuildExecutionRunForTicket, getGitHubActionsExecutionDetails, getTestExecutionDetails, GitHubActionsDispatchConflictError, listExecutionRunLinks, listExecutionRuns, listLifecycleEvents, listProjectLifecycleEvents, toCanonicalLifecycleEvent, transitionExecutionRun, withExecutionRunTransaction, } from './lib/executionRuns.js';
58
+ import { dispatchExecutionRetry, getExecutionRetryEligibility, resolveExecutionRetry } from './lib/executionRetry.js';
59
+ import { listExecutionTimeline } from './lib/executionTimeline.js';
58
60
  import { cleanupAllExecutionEvidence, EXECUTION_EVIDENCE_CLEANUP_INTERVAL_MS, resolveExecutionEvidenceRetentionDays, } from './lib/executionRunRetention.js';
59
61
  import { shutdownRealtimeServer } from './lib/shutdown.js';
60
62
  import { resolveClientBuildPaths, shouldServeClientApp, STATIC_CLIENT_DIRECTORY_OPTIONS, STATIC_CLIENT_INDEX_OPTIONS, } from './lib/staticClient.js';
@@ -72,7 +74,7 @@ import { buildCommitMessagePrompt, normalizeGeneratedCommitMessage } from './lib
72
74
  import { BUNDLE_PULL_REQUEST_SYNC_TTL_MS, getBundlePullRequestRecord, hasOpenBundlePullRequest, serializeBundlePullRequest, shouldSyncBundlePullRequest, syncTrackedBundlePullRequestIfNeeded, upsertBundlePullRequest, upsertBundlePullRequestFromGitHub, } from './lib/bundlePullRequests.js';
73
75
  import { countInboundProjectLinks, getEffectiveProject, getProjectById, getProjectTicketStats, hasProjectRunIde, listProjects, replaceProjectLinks, serializeProject, updateProjectActiveTarget, validateProjectPayload, } from './lib/projects.js';
74
76
  import { getActiveRunStatus, getActiveRunStatusForProject, getPreviewStatusForProject, hasRunConfig, isRunning, openIde, openTicketIdeInWorkspace, resolveProjectRunContextKey, runSetupCommandsInWorkspace, runTicketInWorkspace, stopAllTickets, stopTicket } from './lib/run.js';
75
- import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExist, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
77
+ import { clearAllTestSessions, createTestSession, deleteTestSession, discoverTestsForSession, doesTestFileExistForSession, findProjectTestCommand, getTestSession, isTestSelectionSupported, startTestSessionRun, stopTestSessionRun } from './lib/testSessions.js';
76
78
  import { getBundleBuildBlockerResponse, shouldStartBundleBuildNow, transitionTicketState, } from './lib/ticketWorkflowOperations.js';
77
79
  import { submitPlanTicketFollowUp, submitTicketFollowUp, } from './lib/ticketFollowUpOperations.js';
78
80
  import { canUseBuildControls, formatActiveRunConflict, getTicketRunReadinessError, resumeBuildTicketRun, runProjectTarget, runTicketRun, stopBuildTicketRun, stopReviewTicketRun, stopProjectTargetRun, stopTicketRun, } from './lib/ticketRunOperations.js';
@@ -167,6 +169,44 @@ const parseLifecycleListQuery = (query) => {
167
169
  limit,
168
170
  };
169
171
  };
172
+ const parseExecutionTimelineScope = (projectId, query) => {
173
+ const hasScopeType = query.scope_type != null && query.scope_type !== '';
174
+ const hasScopeId = query.scope_id != null && query.scope_id !== '';
175
+ if (hasScopeType !== hasScopeId) {
176
+ return { error: 'scope_type and scope_id must be provided together' };
177
+ }
178
+ if (!hasScopeType) {
179
+ return { scopeEntity: undefined };
180
+ }
181
+ if (typeof query.scope_type !== 'string' || typeof query.scope_id !== 'string') {
182
+ return { error: 'scope_type and scope_id must be strings' };
183
+ }
184
+ const scopeType = query.scope_type;
185
+ const scopeId = query.scope_id.trim();
186
+ if (!['ticket', 'bundle', 'chat_session', 'spec_session'].includes(scopeType)) {
187
+ return { error: 'scope_type must be ticket, bundle, chat_session, or spec_session' };
188
+ }
189
+ if (!scopeId) {
190
+ return { error: 'scope_id must not be empty' };
191
+ }
192
+ const table = scopeType === 'ticket'
193
+ ? 'tickets'
194
+ : scopeType === 'bundle'
195
+ ? 'worktree_bundles'
196
+ : scopeType === 'chat_session'
197
+ ? 'chat_sessions'
198
+ : 'spec_sessions';
199
+ const exists = db.prepare(`SELECT 1 FROM ${table} WHERE id = ? AND project_id = ?`).get(scopeId, projectId);
200
+ if (!exists) {
201
+ return { error: 'Scope not found', status: 404 };
202
+ }
203
+ return {
204
+ scopeEntity: {
205
+ type: scopeType,
206
+ id: scopeId,
207
+ },
208
+ };
209
+ };
170
210
  const serializeTestExecutionDetail = (details, retryEligibility) => {
171
211
  if (!details) {
172
212
  return { kind: 'test', available: false, retry_eligibility: retryEligibility };
@@ -947,63 +987,6 @@ const parseTestTargetSelection = (value) => {
947
987
  }
948
988
  return null;
949
989
  };
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
990
  const resolveProjectFileSuggestionWorkspace = (project, options = {}) => {
1008
991
  const ticketId = options.ticketId?.trim() || null;
1009
992
  if (ticketId) {
@@ -7582,6 +7565,70 @@ app.get('/projects/:id/lifecycle-events', (req, res) => {
7582
7565
  throw error;
7583
7566
  }
7584
7567
  });
7568
+ app.get('/projects/:id/execution-timeline', (req, res) => {
7569
+ const project = getProjectById(req.params.id);
7570
+ if (!project) {
7571
+ return res.status(404).json({ error: 'Not found' });
7572
+ }
7573
+ const query = parseLifecycleListQuery(req.query);
7574
+ if ('error' in query) {
7575
+ return res.status(400).json({ error: query.error });
7576
+ }
7577
+ const scope = parseExecutionTimelineScope(project.id, req.query);
7578
+ if ('error' in scope) {
7579
+ return res.status(scope.status ?? 400).json({ error: scope.error });
7580
+ }
7581
+ try {
7582
+ return res.json(listExecutionTimeline({
7583
+ project,
7584
+ kind: query.kind,
7585
+ statuses: query.statuses,
7586
+ cursor: query.cursor,
7587
+ limit: query.limit,
7588
+ scope: scope.scopeEntity,
7589
+ }));
7590
+ }
7591
+ catch (error) {
7592
+ if (error instanceof Error && error.message === 'Invalid execution run cursor') {
7593
+ return res.status(400).json({ error: 'cursor must be a valid execution run cursor' });
7594
+ }
7595
+ throw error;
7596
+ }
7597
+ });
7598
+ app.get('/projects/:id/execution-timeline/:runId', (req, res) => {
7599
+ const project = getProjectById(req.params.id);
7600
+ if (!project) {
7601
+ return res.status(404).json({ error: 'Not found' });
7602
+ }
7603
+ const scope = parseExecutionTimelineScope(project.id, req.query);
7604
+ if ('error' in scope) {
7605
+ return res.status(scope.status ?? 400).json({ error: scope.error });
7606
+ }
7607
+ const run = getExecutionRun(project.id, req.params.runId);
7608
+ if (!run) {
7609
+ return res.status(404).json({ error: 'Execution run not found' });
7610
+ }
7611
+ const links = listExecutionRunLinks(project.id, run.id);
7612
+ if (scope.scopeEntity && !links.some(link => link.entity_type === scope.scopeEntity?.type && link.entity_id === scope.scopeEntity.id)) {
7613
+ return res.status(404).json({ error: 'Execution run not found' });
7614
+ }
7615
+ const testDetails = run.kind === 'test' ? getTestExecutionDetails(project.id, run.id) : null;
7616
+ const retryEligibility = getExecutionRetryEligibility(project.id, run.id);
7617
+ const details = run.kind === 'test'
7618
+ ? serializeTestExecutionDetail(testDetails, retryEligibility)
7619
+ : run.kind === 'github_actions'
7620
+ ? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
7621
+ : ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
7622
+ ? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
7623
+ : { kind: run.kind, available: true, retry_eligibility: retryEligibility };
7624
+ return res.json({
7625
+ run,
7626
+ links,
7627
+ events: listLifecycleEvents(project.id, run.id).map(toCanonicalLifecycleEvent),
7628
+ details,
7629
+ agent_runs: listAgentRunsForExecutionRun(run.id),
7630
+ });
7631
+ });
7585
7632
  app.get('/projects/:id/execution-runs/:runId', (req, res) => {
7586
7633
  const project = getProjectById(req.params.id);
7587
7634
  if (!project) {
@@ -7592,46 +7639,55 @@ app.get('/projects/:id/execution-runs/:runId', (req, res) => {
7592
7639
  return res.status(404).json({ error: 'Execution run not found' });
7593
7640
  }
7594
7641
  const testDetails = run.kind === 'test' ? getTestExecutionDetails(project.id, run.id) : null;
7642
+ const retryEligibility = getExecutionRetryEligibility(project.id, run.id);
7595
7643
  const details = run.kind === 'test'
7596
- ? serializeTestExecutionDetail(testDetails, resolveTestRetry(project, run, testDetails).eligibility)
7644
+ ? serializeTestExecutionDetail(testDetails, retryEligibility)
7597
7645
  : run.kind === 'github_actions'
7598
- ? serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id))
7599
- : { kind: run.kind, available: true };
7646
+ ? { ...serializeGitHubActionsExecutionDetail(getGitHubActionsExecutionDetails(project.id, run.id)), retry_eligibility: retryEligibility }
7647
+ : ['automation_commit', 'automation_push', 'automation_pull_request'].includes(run.kind)
7648
+ ? { kind: run.kind, available: true, automation_stage: getAutomationStageDetails(project.id, run.id), retry_eligibility: retryEligibility }
7649
+ : { kind: run.kind, available: true, retry_eligibility: retryEligibility };
7600
7650
  const links = listExecutionRunLinks(project.id, run.id);
7601
7651
  const agentRuns = listAgentRunsForExecutionRun(run.id);
7602
- return res.json({ run, links, details, agent_runs: agentRuns });
7652
+ const events = listLifecycleEvents(project.id, run.id).map(toCanonicalLifecycleEvent);
7653
+ return res.json({ run, links, events, details, agent_runs: agentRuns });
7603
7654
  });
7604
7655
  app.post('/projects/:id/execution-runs/:runId/retry', async (req, res) => {
7605
7656
  const project = getProjectById(req.params.id);
7606
7657
  if (!project) {
7607
7658
  return res.status(404).json({ error: 'Not found' });
7608
7659
  }
7609
- const executionRun = getExecutionRun(project.id, req.params.runId);
7610
- const details = getTestExecutionDetails(project.id, req.params.runId);
7611
- const retry = resolveTestRetry(project, executionRun, details);
7612
- if ('error' in retry) {
7613
- return res.status(retry.error.status).json({ error: retry.error.message });
7614
- }
7615
- const session = createTestSession({
7616
- projectId: project.id,
7617
- ticketId: retry.details.ticket_id,
7618
- targetKind: retry.workspace.target_kind,
7619
- targetBundleId: retry.workspace.target_bundle_id,
7620
- cwd: retry.workspace.cwd,
7621
- targetLabel: retry.workspace.target_label,
7660
+ const resolution = resolveExecutionRetry(project.id, req.params.runId);
7661
+ const result = await dispatchExecutionRetry(resolution, io, async (retry) => {
7662
+ const session = createTestSession({
7663
+ projectId: project.id,
7664
+ ticketId: retry.ticketId,
7665
+ targetKind: retry.workspace.target_kind,
7666
+ targetBundleId: retry.workspace.target_bundle_id,
7667
+ cwd: retry.workspace.cwd,
7668
+ targetLabel: retry.workspace.target_label,
7669
+ });
7670
+ try {
7671
+ const started = await startTestSessionRun(session.id, project, {
7672
+ command: retry.command,
7673
+ selection: retry.selection,
7674
+ retryOfRunId: req.params.runId,
7675
+ deleteOnComplete: true,
7676
+ }, io);
7677
+ if (!started.run_id) {
7678
+ throw new Error('Test retry did not create a replacement execution');
7679
+ }
7680
+ return started.run_id;
7681
+ }
7682
+ catch (error) {
7683
+ await deleteTestSession(session.id, io).catch(() => undefined);
7684
+ throw error;
7685
+ }
7622
7686
  });
7623
- try {
7624
- return res.status(201).json(await startTestSessionRun(session.id, project, {
7625
- command: retry.command,
7626
- selection: retry.selection,
7627
- retryOfRunId: retry.executionRun.id,
7628
- deleteOnComplete: true,
7629
- }, io));
7630
- }
7631
- catch (error) {
7632
- await deleteTestSession(session.id, io).catch(() => undefined);
7633
- return res.status(409).json({ error: getErrorMessage(error) });
7687
+ if (!result.ok) {
7688
+ return res.status(result.status).json({ error: result.error, reason: result.reason });
7634
7689
  }
7690
+ return res.status(201).json(result.value);
7635
7691
  });
7636
7692
  app.post('/tickets/:id/test-sessions', (req, res) => {
7637
7693
  const ticket = getTicket(req.params.id);
@@ -7735,7 +7791,7 @@ app.post('/test-sessions/:id/stop', async (req, res) => {
7735
7791
  }
7736
7792
  });
7737
7793
  app.delete('/test-sessions/:id', async (req, res) => {
7738
- const deleted = await deleteTestSession(req.params.id, io, { purgeExecutionRun: true });
7794
+ const deleted = await deleteTestSession(req.params.id, io);
7739
7795
  if (!deleted) {
7740
7796
  return res.status(404).json({ error: 'Not found' });
7741
7797
  }
@@ -7890,6 +7946,7 @@ app.post('/tickets/:id/plan', (req, res) => {
7890
7946
  }
7891
7947
  const scheduling = startPlan(ticket, io, {
7892
7948
  branch,
7949
+ targetBundleId,
7893
7950
  });
7894
7951
  if (!scheduling.accepted) {
7895
7952
  const status = scheduling.reason === 'queue_failed' ? 503 : 409;