@straiffi/archon 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/client/assets/ActivityView-DTEenQas.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +1 -0
  3. package/dist/client/assets/TestsDialog-cBaoffCs.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-CJgwCv5O.js → architectureDiagram-3BPJPVTR-BzzGotac.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM--xjes8FA.js → blockDiagram-GPEHLZMM-DIcqLHKg.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-DHnp82NG.js → c4Diagram-AAUBKEIU-Hc1Sqmhs.js} +1 -1
  7. package/dist/client/assets/channel-Cym17waa.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-DAOyJCKy.js → chunk-2J33WTMH-CWSwjsgk.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-C9YI_tk4.js → chunk-3OPIFGDE-BnX3DljN.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-BW9khkin.js → chunk-5ZQYHXKU-EyyJlI-d.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-DL4t5Hpi.js → chunk-727SXJPM-Ci6CfxWj.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-C4VMKa_O.js → chunk-AQP2D5EJ-RKZNsJTm.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DHP7zrZK.js → chunk-CSCIHK7Q-DBf0Euoj.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-CqZWek3t.js → chunk-KSCS5N6A-BmLQI_2S.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-D3q55pCI.js → chunk-L5ZTLDWV-DKwVAb8G.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-UDY_Lo6v.js → chunk-LZXEDZCA-axotKkbz.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-ZCiOxkYV.js → chunk-ND2GUHAM-BBlJ4nXC.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CuryoBtd.js → chunk-NZK2D7GU-BKJGoZZt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-PnPsz2TB.js → chunk-O5CBEL6O-C-Ghigo1.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-BzOopX-U.js → chunk-WU5MYG2G-CCzVWYcd.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-5LBwQ83I.js → dagre-BM42HDAG-rwIdPcjI.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-_4pckiGR.js → diagram-2AECGRRQ-DJWxOQsg.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CqElv5rJ.js → diagram-5GNKFQAL-C-mQeFtd.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DKv4zS4F.js → diagram-KO2AKTUF-BAu6u4sR.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-iLmTH4UP.js → diagram-LMA3HP47-U6CbvBn0.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-DpFYIa0o.js → diagram-OG6HWLK6-DNTwFcs1.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DnfbvbKs.js → erDiagram-TEJ5UH35-DaHX6Xw5.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-C2MN70Qv.js → flowDiagram-I6XJVG4X-B9hKqL2l.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-yonpliOJ.js → ganttDiagram-6RSMTGT7-BQ1HPHVO.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CaKDWx7y.js → gitGraphDiagram-PVQCEYII-BBRcsmMK.js} +1 -1
  33. package/dist/client/assets/index-CEMaJCPS.js +273 -0
  34. package/dist/client/assets/index-lWftq65U.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-82-8njnx.js → infoDiagram-5YYISTIA-DUoNiSGC.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BMiyLWwK.js → ishikawaDiagram-YF4QCWOH-DJN1y2w8.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-C1qBnR37.js → journeyDiagram-JHISSGLW-BxLLMT70.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-zByEuCzn.js → kanban-definition-UN3LZRKU-BdCPistJ.js} +1 -1
  39. package/dist/client/assets/{line-EpLqyzw7.js → line-4mIgXsZd.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-o124REPN.js → mermaid-parser.core-Pu1QGWJ-.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-DgiAn2-k.js → mermaid.core-DnH9DrgZ.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-UK6V0Bxq.js → mindmap-definition-RKZ34NQL-DqE4ba3C.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-B2s9oGFl.js → pieDiagram-4H26LBE5-Crjy9ejf.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-d5H7-zcc.js → quadrantDiagram-W4KKPZXB-BUSZ2VB7.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DlwN9Vcv.js → requirementDiagram-4Y6WPE33-CBhFKEoV.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-9dGyAxu5.js → sankeyDiagram-5OEKKPKP-CgdbY41v.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DKwRjEym.js → sequenceDiagram-3UESZ5HK-hkp_H7AE.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-D_6cuqsX.js → stateDiagram-AJRCARHV-C6x45RWp.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-ChqD4j8I.js → timeline-definition-PNZ67QCA-CjnZWttQ.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CSAZk0IW.js → vennDiagram-CIIHVFJN-DU5ZyYju.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BUE6IF4q.js → wardleyDiagram-YWT4CUSO-CaCEnCB0.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-MOkC53m_.js → xychartDiagram-2RQKCTM6-Ce3xa37U.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +471 -39
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +1267 -213
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +281 -76
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentRuns.js +205 -38
  62. package/dist/server/lib/agentRuns.js.map +1 -1
  63. package/dist/server/lib/automationProgression.js +464 -0
  64. package/dist/server/lib/automationProgression.js.map +1 -0
  65. package/dist/server/lib/automationReconciliation.js +498 -0
  66. package/dist/server/lib/automationReconciliation.js.map +1 -0
  67. package/dist/server/lib/automationStageDetails.js +232 -0
  68. package/dist/server/lib/automationStageDetails.js.map +1 -0
  69. package/dist/server/lib/buildOpenQuestionOperations.js +223 -0
  70. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
  71. package/dist/server/lib/buildOpenQuestions.js +52 -0
  72. package/dist/server/lib/buildOpenQuestions.js.map +1 -1
  73. package/dist/server/lib/buildSequences.js +130 -46
  74. package/dist/server/lib/buildSequences.js.map +1 -1
  75. package/dist/server/lib/bundleReviewOperations.js +82 -9
  76. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  77. package/dist/server/lib/chatOperations.js +31 -0
  78. package/dist/server/lib/chatOperations.js.map +1 -1
  79. package/dist/server/lib/executionReconciliation.js +518 -0
  80. package/dist/server/lib/executionReconciliation.js.map +1 -0
  81. package/dist/server/lib/executionRunRetention.js +141 -0
  82. package/dist/server/lib/executionRunRetention.js.map +1 -0
  83. package/dist/server/lib/executionRuns.js +1264 -0
  84. package/dist/server/lib/executionRuns.js.map +1 -0
  85. package/dist/server/lib/git.js +385 -6
  86. package/dist/server/lib/git.js.map +1 -1
  87. package/dist/server/lib/integrations/github.js +43 -0
  88. package/dist/server/lib/integrations/github.js.map +1 -1
  89. package/dist/server/lib/integrations/githubActions.js +1 -0
  90. package/dist/server/lib/integrations/githubActions.js.map +1 -1
  91. package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
  92. package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
  93. package/dist/server/lib/projectMemorySelections.js +7 -3
  94. package/dist/server/lib/projectMemorySelections.js.map +1 -1
  95. package/dist/server/lib/shutdown.js +2 -2
  96. package/dist/server/lib/shutdown.js.map +1 -1
  97. package/dist/server/lib/specArtifact.js +3 -0
  98. package/dist/server/lib/specArtifact.js.map +1 -1
  99. package/dist/server/lib/specFormatting.js +1 -1
  100. package/dist/server/lib/specFormatting.js.map +1 -1
  101. package/dist/server/lib/specOperations.js +31 -0
  102. package/dist/server/lib/specOperations.js.map +1 -1
  103. package/dist/server/lib/specReviewContext.js +44 -14
  104. package/dist/server/lib/specReviewContext.js.map +1 -1
  105. package/dist/server/lib/specReviewDraft.js +3 -2
  106. package/dist/server/lib/specReviewDraft.js.map +1 -1
  107. package/dist/server/lib/specReviewOperations.js +44 -7
  108. package/dist/server/lib/specReviewOperations.js.map +1 -1
  109. package/dist/server/lib/specReviewValidation.js +217 -0
  110. package/dist/server/lib/specReviewValidation.js.map +1 -0
  111. package/dist/server/lib/specReviews.js +30 -2
  112. package/dist/server/lib/specReviews.js.map +1 -1
  113. package/dist/server/lib/specs.js +18 -1
  114. package/dist/server/lib/specs.js.map +1 -1
  115. package/dist/server/lib/stageSnapshots.js +197 -0
  116. package/dist/server/lib/stageSnapshots.js.map +1 -0
  117. package/dist/server/lib/testSessions.js +164 -19
  118. package/dist/server/lib/testSessions.js.map +1 -1
  119. package/dist/server/lib/ticketAutomation.js +80 -4
  120. package/dist/server/lib/ticketAutomation.js.map +1 -1
  121. package/dist/server/lib/ticketAutomationCommit.js +466 -91
  122. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  123. package/dist/server/lib/ticketFollowUpOperations.js +214 -20
  124. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  125. package/dist/server/lib/ticketOpenQuestions.js +30 -14
  126. package/dist/server/lib/ticketOpenQuestions.js.map +1 -1
  127. package/dist/server/lib/ticketRunOperations.js +75 -7
  128. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  129. package/dist/server/lib/ticketUndo.js +77 -168
  130. package/dist/server/lib/ticketUndo.js.map +1 -1
  131. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  132. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  133. package/dist/server/lib/tickets.js +46 -29
  134. package/dist/server/lib/tickets.js.map +1 -1
  135. package/dist/server/lib/transcriptRuns.js +3 -3
  136. package/dist/server/lib/transcriptRuns.js.map +1 -1
  137. package/dist/server/lib/turnExecutionErrors.js +12 -0
  138. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  139. package/dist/server/lib/types.js +57 -1
  140. package/dist/server/lib/types.js.map +1 -1
  141. package/dist/server/workers/build.js +596 -184
  142. package/dist/server/workers/build.js.map +1 -1
  143. package/dist/server/workers/chat.js +261 -130
  144. package/dist/server/workers/chat.js.map +1 -1
  145. package/dist/server/workers/followUp.js +720 -195
  146. package/dist/server/workers/followUp.js.map +1 -1
  147. package/dist/server/workers/plan.js +527 -188
  148. package/dist/server/workers/plan.js.map +1 -1
  149. package/dist/server/workers/planFollowUp.js +231 -69
  150. package/dist/server/workers/planFollowUp.js.map +1 -1
  151. package/dist/server/workers/review.js +796 -239
  152. package/dist/server/workers/review.js.map +1 -1
  153. package/dist/server/workers/specReview.js +211 -45
  154. package/dist/server/workers/specReview.js.map +1 -1
  155. package/dist/server/workers/specSession.js +425 -252
  156. package/dist/server/workers/specSession.js.map +1 -1
  157. package/package.json +1 -1
  158. package/dist/client/assets/GitHubActionsDialog-BrUq-zO1.js +0 -1
  159. package/dist/client/assets/TestsDialog-D9N4GD77.js +0 -4
  160. package/dist/client/assets/TicketChangesModal-bYHJSO6d.js +0 -1
  161. package/dist/client/assets/channel-B4kCvLCt.js +0 -1
  162. package/dist/client/assets/classDiagram-4FO5ZUOK-C-eRRNNj.js +0 -1
  163. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B3vxoc73.js +0 -1
  164. package/dist/client/assets/index-9q1GXkUw.js +0 -270
  165. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  166. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BkMKz90T.js +0 -1
@@ -0,0 +1,1264 @@
1
+ import { randomUUID } from 'crypto';
2
+ import db from '../db.js';
3
+ import { EXECUTION_RUN_KINDS, EXECUTION_RUN_LINK_ENTITY_TYPES } from './types.js';
4
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
5
+ const ACTIVE_STATUSES = new Set(['queued', 'running', 'waiting']);
6
+ const MAX_METADATA_BYTES = 16 * 1024;
7
+ const MAX_EVENT_MESSAGE_LENGTH = 4_000;
8
+ const MAX_FAILURE_MESSAGE_LENGTH = 4_000;
9
+ const MAX_JOBS_SNAPSHOT_BYTES = 64 * 1024;
10
+ const MAX_TEST_OUTPUT_LENGTH = 256 * 1024;
11
+ const APPROVED_EXECUTION_RUN_KINDS = new Set(EXECUTION_RUN_KINDS);
12
+ const EXECUTION_RUN_INITIATORS = new Set(['user', 'automation', 'recovery', 'system']);
13
+ const EXECUTION_RUN_LINK_RELATIONSHIPS = new Set(['primary', 'context', 'result', 'trigger']);
14
+ const EXECUTION_RUN_LINK_ENTITY_TYPE_SET = new Set(EXECUTION_RUN_LINK_ENTITY_TYPES);
15
+ const GENERIC_RETRY_DENIED_KINDS = new Set([
16
+ 'automation_commit',
17
+ 'automation_push',
18
+ 'automation_pull_request',
19
+ ]);
20
+ export class GitHubActionsDispatchConflictError extends Error {
21
+ executionRunId;
22
+ constructor(executionRunId) {
23
+ super('A dispatch for this project, workflow, and ref is still being resolved.');
24
+ this.name = 'GitHubActionsDispatchConflictError';
25
+ this.executionRunId = executionRunId;
26
+ }
27
+ }
28
+ const getRunStatement = db.prepare('SELECT * FROM execution_runs WHERE id = ? AND project_id = ?');
29
+ const getRunByIdStatement = db.prepare('SELECT * FROM execution_runs WHERE id = ?');
30
+ const projectExistsStatement = db.prepare('SELECT 1 FROM projects WHERE id = ?');
31
+ const insertRunStatement = db.prepare(`
32
+ INSERT INTO execution_runs (
33
+ id, project_id, kind, status, domain_status, source_kind, source_id, source_label, source_url,
34
+ correlation_id, initiated_by, metadata_schema_version, metadata_json, parent_run_id,
35
+ retry_of_run_id, attempt, created_at, updated_at
36
+ ) VALUES (?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
37
+ `);
38
+ const insertEventStatement = db.prepare(`
39
+ INSERT INTO lifecycle_events (
40
+ id, project_id, execution_run_id, correlation_id, sequence, event_type, from_status,
41
+ to_status, domain_status, message, metadata_schema_version, metadata_json, created_at
42
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
43
+ `);
44
+ const nextEventSequenceStatement = db.prepare(`
45
+ SELECT COALESCE(MAX(sequence), 0) + 1 AS sequence
46
+ FROM lifecycle_events
47
+ WHERE execution_run_id = ?
48
+ `);
49
+ const insertRunLinkStatement = db.prepare(`
50
+ INSERT INTO execution_run_links (
51
+ execution_run_id, project_id, entity_type, entity_id, relationship,
52
+ label_snapshot, url_snapshot, created_at
53
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
54
+ `);
55
+ const getRunLinksStatement = db.prepare(`
56
+ SELECT * FROM execution_run_links
57
+ WHERE execution_run_id = ? AND project_id = ?
58
+ ORDER BY created_at ASC, entity_type ASC, entity_id ASC
59
+ `);
60
+ const getRunLinkStatement = db.prepare(`
61
+ SELECT * FROM execution_run_links
62
+ WHERE execution_run_id = ? AND project_id = ? AND entity_type = ? AND entity_id = ? AND relationship = ?
63
+ `);
64
+ const getDirectRetryChildStatement = db.prepare('SELECT id FROM execution_runs WHERE retry_of_run_id = ? LIMIT 1');
65
+ const getEventStatement = db.prepare('SELECT * FROM lifecycle_events WHERE id = ? AND project_id = ?');
66
+ const insertTestDetailsStatement = db.prepare(`
67
+ INSERT INTO test_execution_details (
68
+ execution_run_id, command_id, command_name, command_runner, command_snapshot,
69
+ active_command, target_kind, target_bundle_id, target_label, ticket_id,
70
+ selection_json, summary_json, exit_code, duration_ms, output, output_truncated,
71
+ output_retention_state, output_expired_at, output_byte_count
72
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'retained', NULL, ?)
73
+ `);
74
+ const insertGitHubActionsDetailsStatement = db.prepare(`
75
+ INSERT INTO github_actions_run_details (
76
+ execution_run_id, workflow_id, workflow_name, ref, input_names_json, input_fingerprint,
77
+ resolution_attempts, provider_error_count, provider_run_id, provider_status,
78
+ conclusion, html_url, head_sha, head_branch, provider_created_at, provider_updated_at,
79
+ provider_started_at, dispatched_at, last_synced_at, jobs_json,
80
+ jobs_retention_state, jobs_expired_at, jobs_count
81
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'retained', NULL, ?)
82
+ `);
83
+ const getTestTargetBundleStatement = db.prepare(`
84
+ SELECT id, kind
85
+ FROM worktree_bundles
86
+ WHERE id = ? AND project_id = ?
87
+ `);
88
+ const getTestTargetTicketStatement = db.prepare(`
89
+ SELECT id, worktree_bundle_id
90
+ FROM tickets
91
+ WHERE id = ? AND project_id = ?
92
+ `);
93
+ const updateRunStatement = db.prepare(`
94
+ UPDATE execution_runs
95
+ SET status = ?, domain_status = ?, failure_code = ?, failure_message = ?,
96
+ failure_details_json = ?, started_at = ?, completed_at = ?, updated_at = ?
97
+ WHERE id = ? AND project_id = ?
98
+ `);
99
+ const assertBoundedString = (value, name, maxLength) => {
100
+ if (value !== null && value !== undefined && value.length > maxLength) {
101
+ throw new Error(`${name} exceeds ${maxLength} characters`);
102
+ }
103
+ };
104
+ const normalizeBoundedMessage = (value, maxLength) => {
105
+ if (value === null || value === undefined) {
106
+ return null;
107
+ }
108
+ if (value.length <= maxLength) {
109
+ return value;
110
+ }
111
+ const truncated = value.slice(0, maxLength);
112
+ const lastCodeUnit = truncated.charCodeAt(truncated.length - 1);
113
+ return lastCodeUnit >= 0xD800 && lastCodeUnit <= 0xDBFF
114
+ ? truncated.slice(0, -1)
115
+ : truncated;
116
+ };
117
+ const serializeBoundedObject = (value, name) => {
118
+ if (value === null || value === undefined) {
119
+ return null;
120
+ }
121
+ let serialized;
122
+ try {
123
+ serialized = JSON.stringify(value);
124
+ }
125
+ catch {
126
+ throw new Error(`${name} must be JSON serializable`);
127
+ }
128
+ if (Buffer.byteLength(serialized, 'utf8') > MAX_METADATA_BYTES) {
129
+ throw new Error(`${name} exceeds ${MAX_METADATA_BYTES} bytes`);
130
+ }
131
+ return serialized;
132
+ };
133
+ const parseStoredObject = (value) => {
134
+ if (!value) {
135
+ return null;
136
+ }
137
+ try {
138
+ const parsed = JSON.parse(value);
139
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
140
+ return parsed;
141
+ }
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ return null;
147
+ };
148
+ const parseStoredArray = (value) => {
149
+ if (!value) {
150
+ return [];
151
+ }
152
+ try {
153
+ const parsed = JSON.parse(value);
154
+ return Array.isArray(parsed) ? parsed : [];
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ };
160
+ const serializeJobsSnapshot = (jobs) => {
161
+ if (jobs === null || jobs === undefined) {
162
+ return null;
163
+ }
164
+ const bounded = jobs.slice(0, 50).map(job => ({
165
+ ...job,
166
+ steps: job.steps.slice(0, 50),
167
+ }));
168
+ while (bounded.length > 0) {
169
+ const serialized = JSON.stringify(bounded);
170
+ if (Buffer.byteLength(serialized, 'utf8') <= MAX_JOBS_SNAPSHOT_BYTES) {
171
+ return serialized;
172
+ }
173
+ bounded.pop();
174
+ }
175
+ return '[]';
176
+ };
177
+ const boundTestOutput = (output, alreadyTruncated = false) => {
178
+ if (output.length <= MAX_TEST_OUTPUT_LENGTH) {
179
+ return { output, outputTruncated: alreadyTruncated };
180
+ }
181
+ return {
182
+ output: output.slice(-MAX_TEST_OUTPUT_LENGTH),
183
+ outputTruncated: true,
184
+ };
185
+ };
186
+ const isSourceAvailable = (row) => {
187
+ if (!row.source_kind || !row.source_id) {
188
+ return false;
189
+ }
190
+ if (row.source_kind === 'ticket') {
191
+ return Boolean(db.prepare('SELECT 1 FROM tickets WHERE id = ? AND project_id = ?').get(row.source_id, row.project_id));
192
+ }
193
+ if (row.source_kind === 'project') {
194
+ return Boolean(projectExistsStatement.get(row.source_id));
195
+ }
196
+ if (row.source_kind === 'bundle') {
197
+ return Boolean(db.prepare('SELECT 1 FROM worktree_bundles WHERE id = ? AND project_id = ?').get(row.source_id, row.project_id));
198
+ }
199
+ if (row.source_kind === 'chat_session') {
200
+ return Boolean(db.prepare('SELECT 1 FROM chat_sessions WHERE id = ? AND project_id = ?').get(row.source_id, row.project_id));
201
+ }
202
+ if (row.source_kind === 'spec_session') {
203
+ return Boolean(db.prepare('SELECT 1 FROM spec_sessions WHERE id = ? AND project_id = ?').get(row.source_id, row.project_id));
204
+ }
205
+ return Boolean(safeSnapshotUrl(row.source_url));
206
+ };
207
+ const safeSnapshotUrl = (value) => {
208
+ if (!value) {
209
+ return null;
210
+ }
211
+ try {
212
+ const url = new URL(value);
213
+ return url.protocol === 'http:' || url.protocol === 'https:' ? value : null;
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ };
219
+ const serializeRun = (row) => ({
220
+ id: row.id,
221
+ project_id: row.project_id,
222
+ kind: row.kind,
223
+ status: row.status,
224
+ domain_status: row.domain_status,
225
+ source_reference: row.source_kind && row.source_id
226
+ ? {
227
+ kind: row.source_kind,
228
+ id: row.source_id,
229
+ label: row.source_label,
230
+ available: isSourceAvailable(row),
231
+ url: safeSnapshotUrl(row.source_url),
232
+ }
233
+ : null,
234
+ correlation_id: row.correlation_id,
235
+ initiated_by: row.initiated_by,
236
+ metadata_schema_version: row.metadata_schema_version,
237
+ metadata: parseStoredObject(row.metadata_json),
238
+ parent_run_id: row.parent_run_id,
239
+ retry_of_run_id: row.retry_of_run_id,
240
+ attempt: row.attempt,
241
+ failure: row.failure_message
242
+ ? {
243
+ code: row.failure_code,
244
+ message: row.failure_message,
245
+ details: parseStoredObject(row.failure_details_json),
246
+ }
247
+ : null,
248
+ created_at: row.created_at,
249
+ started_at: row.started_at,
250
+ completed_at: row.completed_at,
251
+ updated_at: row.updated_at,
252
+ });
253
+ const serializeEvent = (row) => ({
254
+ ...row,
255
+ metadata: parseStoredObject(row.metadata_json),
256
+ });
257
+ const PUBLIC_EVENT_METADATA_KEYS = new Set([
258
+ 'candidate_run_ids',
259
+ 'conclusion',
260
+ 'input_names',
261
+ 'provider_run_id',
262
+ 'ref',
263
+ 'resolution_attempts',
264
+ 'trigger',
265
+ 'workflow_id',
266
+ ]);
267
+ export const toCanonicalLifecycleEvent = (event) => {
268
+ const metadata = event.metadata
269
+ ? Object.fromEntries(Object.entries(event.metadata).filter(([key]) => PUBLIC_EVENT_METADATA_KEYS.has(key)))
270
+ : null;
271
+ return {
272
+ ...event,
273
+ metadata: metadata && Object.keys(metadata).length > 0 ? metadata : null,
274
+ };
275
+ };
276
+ const encodeCursor = (cursor) => Buffer.from(JSON.stringify({
277
+ version: 1,
278
+ created_at: cursor.created_at,
279
+ id: cursor.id,
280
+ }), 'utf8').toString('base64url');
281
+ const decodeCursor = (value) => {
282
+ try {
283
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
284
+ if (parsed.version !== 1
285
+ || typeof parsed.created_at !== 'string'
286
+ || parsed.created_at.length === 0
287
+ || parsed.created_at.length > 64
288
+ || typeof parsed.id !== 'string'
289
+ || parsed.id.length === 0
290
+ || parsed.id.length > 128) {
291
+ throw new Error('Invalid execution run cursor');
292
+ }
293
+ return { created_at: parsed.created_at, id: parsed.id };
294
+ }
295
+ catch (error) {
296
+ if (error instanceof Error && error.message === 'Invalid execution run cursor') {
297
+ throw error;
298
+ }
299
+ throw new Error('Invalid execution run cursor');
300
+ }
301
+ };
302
+ const encodeLifecycleEventCursor = (cursor) => Buffer.from(JSON.stringify({
303
+ version: 1,
304
+ created_at: cursor.created_at,
305
+ execution_run_id: cursor.execution_run_id,
306
+ sequence: cursor.sequence,
307
+ id: cursor.id,
308
+ }), 'utf8').toString('base64url');
309
+ const decodeLifecycleEventCursor = (value) => {
310
+ try {
311
+ const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
312
+ if (parsed.version !== 1
313
+ || typeof parsed.created_at !== 'string'
314
+ || parsed.created_at.length === 0
315
+ || parsed.created_at.length > 64
316
+ || typeof parsed.execution_run_id !== 'string'
317
+ || parsed.execution_run_id.length === 0
318
+ || parsed.execution_run_id.length > 128
319
+ || !Number.isSafeInteger(parsed.sequence)
320
+ || parsed.sequence < 1
321
+ || typeof parsed.id !== 'string'
322
+ || parsed.id.length === 0
323
+ || parsed.id.length > 128) {
324
+ throw new Error('Invalid lifecycle event cursor');
325
+ }
326
+ return {
327
+ created_at: parsed.created_at,
328
+ execution_run_id: parsed.execution_run_id,
329
+ sequence: parsed.sequence,
330
+ id: parsed.id,
331
+ };
332
+ }
333
+ catch (error) {
334
+ if (error instanceof Error && error.message === 'Invalid lifecycle event cursor') {
335
+ throw error;
336
+ }
337
+ throw new Error('Invalid lifecycle event cursor');
338
+ }
339
+ };
340
+ const resolveLinkLabel = (projectId, link) => {
341
+ if (link.label) {
342
+ return link.label;
343
+ }
344
+ if (link.entityType === 'ticket') {
345
+ const row = db.prepare('SELECT title FROM tickets WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
346
+ return row?.title ?? null;
347
+ }
348
+ if (link.entityType === 'project') {
349
+ const row = db.prepare('SELECT name FROM projects WHERE id = ?').get(link.entityId);
350
+ return row?.name ?? null;
351
+ }
352
+ if (link.entityType === 'bundle') {
353
+ const row = db.prepare('SELECT name FROM worktree_bundles WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
354
+ return row?.name ?? null;
355
+ }
356
+ if (link.entityType === 'chat_session') {
357
+ const row = db.prepare('SELECT title FROM chat_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
358
+ return row?.title ?? null;
359
+ }
360
+ if (link.entityType === 'spec_session') {
361
+ const row = db.prepare('SELECT title FROM spec_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId);
362
+ return row?.title ?? null;
363
+ }
364
+ if (link.entityType === 'bundle_pull_request') {
365
+ const row = db.prepare(`
366
+ SELECT pr_number FROM bundle_pull_requests
367
+ WHERE id = ? AND project_id = ?
368
+ `).get(link.entityId, projectId);
369
+ return row ? `Pull request #${row.pr_number}` : null;
370
+ }
371
+ return null;
372
+ };
373
+ const resolveLinkOwner = (projectId, link) => {
374
+ switch (link.entityType) {
375
+ case 'project':
376
+ return link.entityId === projectId && projectExistsStatement.get(link.entityId)
377
+ ? { parentEntityType: null, parentEntityId: null }
378
+ : null;
379
+ case 'ticket':
380
+ return db.prepare('SELECT 1 FROM tickets WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
381
+ ? { parentEntityType: null, parentEntityId: null }
382
+ : null;
383
+ case 'bundle':
384
+ return db.prepare('SELECT 1 FROM worktree_bundles WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
385
+ ? { parentEntityType: null, parentEntityId: null }
386
+ : null;
387
+ case 'chat_session':
388
+ return db.prepare('SELECT 1 FROM chat_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
389
+ ? { parentEntityType: null, parentEntityId: null }
390
+ : null;
391
+ case 'spec_session':
392
+ return db.prepare('SELECT 1 FROM spec_sessions WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
393
+ ? { parentEntityType: null, parentEntityId: null }
394
+ : null;
395
+ case 'spec_snapshot': {
396
+ const row = db.prepare(`
397
+ SELECT snapshot.spec_session_id AS parent_id FROM spec_snapshots snapshot
398
+ JOIN spec_sessions session ON session.id = snapshot.spec_session_id
399
+ WHERE snapshot.id = ? AND session.project_id = ?
400
+ `).get(link.entityId, projectId);
401
+ return row ? { parentEntityType: 'spec_session', parentEntityId: row.parent_id } : null;
402
+ }
403
+ case 'review_run': {
404
+ const row = db.prepare(`
405
+ SELECT review.ticket_id AS parent_id FROM review_runs review
406
+ JOIN tickets ticket ON ticket.id = review.ticket_id
407
+ WHERE review.id = ? AND ticket.project_id = ?
408
+ `).get(link.entityId, projectId);
409
+ return row ? { parentEntityType: 'ticket', parentEntityId: row.parent_id } : null;
410
+ }
411
+ case 'spec_review_run': {
412
+ const row = db.prepare(`
413
+ SELECT review.spec_session_id AS parent_id FROM spec_review_runs review
414
+ JOIN spec_sessions session ON session.id = review.spec_session_id
415
+ JOIN spec_snapshots snapshot
416
+ ON snapshot.id = review.spec_snapshot_id AND snapshot.spec_session_id = review.spec_session_id
417
+ WHERE review.id = ? AND review.project_id = ? AND session.project_id = ?
418
+ `).get(link.entityId, projectId, projectId);
419
+ return row ? { parentEntityType: 'spec_session', parentEntityId: row.parent_id } : null;
420
+ }
421
+ case 'build_sequence':
422
+ return db.prepare('SELECT 1 FROM build_sequences WHERE id = ? AND project_id = ?').get(link.entityId, projectId)
423
+ ? { parentEntityType: null, parentEntityId: null }
424
+ : null;
425
+ case 'github_workflow':
426
+ return projectExistsStatement.get(projectId)
427
+ ? { parentEntityType: null, parentEntityId: null }
428
+ : null;
429
+ case 'ticket_message': {
430
+ const row = db.prepare(`
431
+ SELECT message.ticket_id AS parent_id
432
+ FROM ticket_messages message
433
+ JOIN tickets ticket ON ticket.id = message.ticket_id
434
+ WHERE message.id = ? AND ticket.project_id = ?
435
+ `).get(link.entityId, projectId);
436
+ return row ? { parentEntityType: 'ticket', parentEntityId: row.parent_id } : null;
437
+ }
438
+ case 'chat_message': {
439
+ const row = db.prepare(`
440
+ SELECT message.chat_session_id AS parent_id
441
+ FROM chat_messages message
442
+ JOIN chat_sessions session ON session.id = message.chat_session_id
443
+ WHERE message.id = ? AND session.project_id = ?
444
+ `).get(link.entityId, projectId);
445
+ return row ? { parentEntityType: 'chat_session', parentEntityId: row.parent_id } : null;
446
+ }
447
+ case 'spec_message': {
448
+ const row = db.prepare(`
449
+ SELECT message.spec_session_id AS parent_id
450
+ FROM spec_messages message
451
+ JOIN spec_sessions session ON session.id = message.spec_session_id
452
+ WHERE message.id = ? AND session.project_id = ?
453
+ `).get(link.entityId, projectId);
454
+ return row ? { parentEntityType: 'spec_session', parentEntityId: row.parent_id } : null;
455
+ }
456
+ case 'bundle_pull_request': {
457
+ const row = db.prepare(`
458
+ SELECT pull_request.bundle_id AS parent_id
459
+ FROM bundle_pull_requests pull_request
460
+ JOIN worktree_bundles bundle
461
+ ON bundle.id = pull_request.bundle_id AND bundle.project_id = pull_request.project_id
462
+ WHERE pull_request.id = ? AND pull_request.project_id = ?
463
+ `).get(link.entityId, projectId);
464
+ return row ? { parentEntityType: 'bundle', parentEntityId: row.parent_id } : null;
465
+ }
466
+ }
467
+ };
468
+ const assertLinkOwnership = (projectId, link) => {
469
+ if (!EXECUTION_RUN_LINK_ENTITY_TYPE_SET.has(link.entityType)) {
470
+ throw new Error(`Unsupported execution run link entity type: ${link.entityType}`);
471
+ }
472
+ const owner = resolveLinkOwner(projectId, link);
473
+ if (!owner) {
474
+ throw new Error(`${link.entityType} ${link.entityId} does not belong to project ${projectId}`);
475
+ }
476
+ if (link.expectedParent && (owner.parentEntityType !== link.expectedParent.entityType
477
+ || owner.parentEntityId !== link.expectedParent.entityId)) {
478
+ throw new Error(`${link.entityType} ${link.entityId} does not belong to expected ${link.expectedParent.entityType} ${link.expectedParent.entityId}`);
479
+ }
480
+ };
481
+ const validateExecutionRunLink = (projectId, link) => {
482
+ if (!EXECUTION_RUN_LINK_RELATIONSHIPS.has(link.relationship)) {
483
+ throw new Error(`Unsupported execution run link relationship: ${link.relationship}`);
484
+ }
485
+ if (!EXECUTION_RUN_LINK_ENTITY_TYPE_SET.has(link.entityType)) {
486
+ throw new Error(`Unsupported execution run link entity type: ${link.entityType}`);
487
+ }
488
+ if (link.entityType.trim().length === 0 || link.entityId.trim().length === 0) {
489
+ throw new Error('Execution run links require an entity type and ID');
490
+ }
491
+ assertBoundedString(link.entityType, 'Link entity type', 100);
492
+ assertBoundedString(link.entityId, 'Link entity ID', 500);
493
+ assertBoundedString(link.url, 'Link URL', 2_000);
494
+ assertLinkOwnership(projectId, link);
495
+ const label = resolveLinkLabel(projectId, link);
496
+ assertBoundedString(label, 'Link label', 500);
497
+ return { ...link, label };
498
+ };
499
+ const normalizeInitialLinks = (input) => {
500
+ const links = [...(input.links ?? [])];
501
+ if (input.sourceReference) {
502
+ links.push({
503
+ entityType: input.sourceReference.kind,
504
+ entityId: input.sourceReference.id,
505
+ relationship: 'primary',
506
+ label: input.sourceReference.label,
507
+ url: input.sourceReference.url,
508
+ });
509
+ }
510
+ if (links.filter(link => link.relationship === 'primary').length > 1) {
511
+ throw new Error('Execution runs may have only one primary link');
512
+ }
513
+ return links;
514
+ };
515
+ const resolveCorrelationId = (parent) => {
516
+ if (parent) {
517
+ return parent.correlation_id;
518
+ }
519
+ return randomUUID();
520
+ };
521
+ const assertRetryEligible = (kind, retrySource, allowAutomationSideEffect) => {
522
+ if (!TERMINAL_STATUSES.has(retrySource.status)) {
523
+ throw new Error('Retry source execution run must be terminal');
524
+ }
525
+ if (kind === 'github_actions') {
526
+ throw new Error('GitHub Actions execution runs cannot be retried directly');
527
+ }
528
+ if (GENERIC_RETRY_DENIED_KINDS.has(kind) && !allowAutomationSideEffect) {
529
+ throw new Error(`${kind} execution runs cannot be retried through the generic retry API`);
530
+ }
531
+ if (kind !== 'test' && retrySource.status === 'completed') {
532
+ throw new Error('Completed execution runs are not eligible for retry');
533
+ }
534
+ if (hasDirectRetryChild(retrySource.id)) {
535
+ throw new Error(`Execution run ${retrySource.id} already has a direct retry child`);
536
+ }
537
+ };
538
+ const assertTestTarget = (projectId, details) => {
539
+ let expectedTargetKind = 'repo_root';
540
+ let expectedBundleId = null;
541
+ if (details.ticketId) {
542
+ const ticket = getTestTargetTicketStatement.get(details.ticketId, projectId);
543
+ if (!ticket) {
544
+ throw new Error(`Ticket ${details.ticketId} does not belong to project ${projectId}`);
545
+ }
546
+ if (ticket.worktree_bundle_id) {
547
+ const bundle = getTestTargetBundleStatement.get(ticket.worktree_bundle_id, projectId);
548
+ if (!bundle) {
549
+ throw new Error(`Ticket ${details.ticketId} bundle target does not belong to project ${projectId}`);
550
+ }
551
+ if (bundle.kind !== 'project_root') {
552
+ expectedTargetKind = 'bundle';
553
+ expectedBundleId = bundle.id;
554
+ }
555
+ }
556
+ }
557
+ else if (details.targetKind === 'bundle') {
558
+ const bundle = getTestTargetBundleStatement.get(details.targetBundleId, projectId);
559
+ if (!bundle || bundle.kind === 'project_root') {
560
+ throw new Error(`Bundle ${details.targetBundleId} does not belong to project ${projectId}`);
561
+ }
562
+ expectedTargetKind = 'bundle';
563
+ expectedBundleId = bundle.id;
564
+ }
565
+ if (details.targetKind !== expectedTargetKind || (details.targetBundleId ?? null) !== expectedBundleId) {
566
+ throw new Error('Test execution target does not match its ticket or bundle context');
567
+ }
568
+ };
569
+ export const hasDirectRetryChild = (runId) => Boolean(getDirectRetryChildStatement.get(runId));
570
+ const insertTestDetails = (runId, details) => {
571
+ const boundedOutput = boundTestOutput(details.output ?? '', details.outputTruncated);
572
+ insertTestDetailsStatement.run(runId, details.commandId ?? null, details.commandName ?? null, details.commandRunner ?? null, details.commandSnapshot ?? null, details.activeCommand ?? null, details.targetKind ?? null, details.targetBundleId ?? null, details.targetLabel ?? null, details.ticketId ?? null, serializeBoundedObject(details.selection, 'Test selection'), serializeBoundedObject(details.summary, 'Test summary'), details.exitCode ?? null, details.durationMs ?? null, boundedOutput.output, boundedOutput.outputTruncated ? 1 : 0, Buffer.byteLength(boundedOutput.output, 'utf8'));
573
+ };
574
+ const insertGitHubActionsDetails = (runId, details) => {
575
+ const jobsJson = serializeJobsSnapshot(details.jobs);
576
+ insertGitHubActionsDetailsStatement.run(runId, details.workflowId, details.workflowName ?? null, details.ref ?? null, details.inputNames ? JSON.stringify([...new Set(details.inputNames)].sort()) : null, details.inputFingerprint ?? null, details.resolutionAttempts ?? 0, details.providerErrorCount ?? 0, details.providerRunId ?? null, details.providerStatus ?? null, details.conclusion ?? null, details.htmlUrl ?? null, details.headSha ?? null, details.headBranch ?? null, details.providerCreatedAt ?? null, details.providerUpdatedAt ?? null, details.providerStartedAt ?? null, details.dispatchedAt ?? null, details.lastSyncedAt ?? null, jobsJson, details.jobs?.length ?? 0);
577
+ };
578
+ const getRunRow = (projectId, runId) => {
579
+ const row = getRunStatement.get(runId, projectId);
580
+ if (!row) {
581
+ throw new Error(`Execution run ${runId} was not found in project ${projectId}`);
582
+ }
583
+ return row;
584
+ };
585
+ const getRelatedRun = (projectId, runId, relation) => {
586
+ const row = getRunByIdStatement.get(runId);
587
+ if (!row) {
588
+ throw new Error(`${relation} execution run ${runId} was not found`);
589
+ }
590
+ if (row.project_id !== projectId) {
591
+ throw new Error(`${relation} execution run must belong to project ${projectId}`);
592
+ }
593
+ return row;
594
+ };
595
+ const assertNoLineageCycle = (runId, relatedRun, column) => {
596
+ const visited = new Set();
597
+ let current = relatedRun;
598
+ while (current) {
599
+ if (current.id === runId) {
600
+ throw new Error(`Execution run ${runId} would create a lineage cycle`);
601
+ }
602
+ if (visited.has(current.id)) {
603
+ throw new Error(`Existing execution run lineage contains a cycle at ${current.id}`);
604
+ }
605
+ visited.add(current.id);
606
+ const nextId = current[column];
607
+ current = nextId ? getRunByIdStatement.get(nextId) : undefined;
608
+ }
609
+ };
610
+ const createEvent = (input) => {
611
+ assertBoundedString(input.eventType, 'Lifecycle event type', 100);
612
+ const message = normalizeBoundedMessage(input.message, MAX_EVENT_MESSAGE_LENGTH);
613
+ const schemaVersion = input.metadataSchemaVersion ?? 1;
614
+ if (!Number.isSafeInteger(schemaVersion) || schemaVersion < 1) {
615
+ throw new Error('Lifecycle event metadata schema version must be a positive integer');
616
+ }
617
+ const id = input.id ?? randomUUID();
618
+ const run = getRunRow(input.projectId, input.runId);
619
+ const sequence = nextEventSequenceStatement.get(input.runId).sequence;
620
+ insertEventStatement.run(id, input.projectId, input.runId, run.correlation_id, sequence, input.eventType, input.fromStatus, input.toStatus, input.domainStatus, message, schemaVersion, input.metadataJson, input.createdAt);
621
+ const row = getEventStatement.get(id, input.projectId);
622
+ return serializeEvent(row);
623
+ };
624
+ const emitCommittedNotifications = (notifications) => {
625
+ for (const notification of notifications) {
626
+ try {
627
+ if (notification.event === 'execution-run:updated') {
628
+ notification.emitter?.emit(notification.event, notification.payload);
629
+ }
630
+ else {
631
+ notification.emitter?.emit(notification.event, toCanonicalLifecycleEvent(notification.payload));
632
+ }
633
+ }
634
+ catch {
635
+ // Realtime delivery is best-effort after the database transaction commits.
636
+ }
637
+ }
638
+ };
639
+ const enqueueRunAndEvent = (notifications, run, event, emitter) => {
640
+ notifications.push({ event: 'execution-run:updated', payload: run, emitter }, { event: 'lifecycle-event:created', payload: event, emitter });
641
+ };
642
+ const createExecutionRunInTransaction = (input, allowAutomationSideEffectRetry = false) => {
643
+ if (!projectExistsStatement.get(input.projectId)) {
644
+ throw new Error(`Project ${input.projectId} was not found`);
645
+ }
646
+ const id = input.id ?? randomUUID();
647
+ assertBoundedString(id, 'Execution run ID', 128);
648
+ if (!APPROVED_EXECUTION_RUN_KINDS.has(input.kind)) {
649
+ throw new Error(`Unsupported execution run kind: ${input.kind}`);
650
+ }
651
+ const initiatedBy = input.initiatedBy ?? 'user';
652
+ if (!EXECUTION_RUN_INITIATORS.has(initiatedBy)) {
653
+ throw new Error(`Unsupported execution run initiator: ${initiatedBy}`);
654
+ }
655
+ assertBoundedString(input.domainStatus, 'Domain status', 200);
656
+ const links = normalizeInitialLinks(input).map(link => validateExecutionRunLink(input.projectId, link));
657
+ const primaryLink = links.find(link => link.relationship === 'primary') ?? null;
658
+ const metadataSchemaVersion = input.metadataSchemaVersion ?? 1;
659
+ if (!Number.isSafeInteger(metadataSchemaVersion) || metadataSchemaVersion < 1) {
660
+ throw new Error('Execution run metadata schema version must be a positive integer');
661
+ }
662
+ const metadataJson = serializeBoundedObject(input.metadata, 'Execution run metadata');
663
+ let retrySource = null;
664
+ if (input.retryOfRunId) {
665
+ retrySource = getRelatedRun(input.projectId, input.retryOfRunId, 'Retry source');
666
+ if (retrySource.kind !== input.kind) {
667
+ throw new Error('Retry source execution run must have the same kind');
668
+ }
669
+ assertNoLineageCycle(id, retrySource, 'retry_of_run_id');
670
+ if (allowAutomationSideEffectRetry && !GENERIC_RETRY_DENIED_KINDS.has(input.kind)) {
671
+ throw new Error('Dedicated automation retry creation only supports automation side-effect runs');
672
+ }
673
+ assertRetryEligible(input.kind, retrySource, allowAutomationSideEffectRetry);
674
+ }
675
+ let expectedParentId = retrySource?.parent_run_id ?? null;
676
+ if (retrySource && input.parentRunId !== undefined && input.parentRunId !== expectedParentId) {
677
+ const recoveryParent = allowAutomationSideEffectRetry && typeof input.parentRunId === 'string'
678
+ ? getRelatedRun(input.projectId, input.parentRunId, 'Recovery parent')
679
+ : null;
680
+ if (recoveryParent?.kind !== 'automation_progression'
681
+ || recoveryParent.initiated_by !== 'recovery'
682
+ || recoveryParent.parent_run_id !== retrySource.id
683
+ || !ACTIVE_STATUSES.has(recoveryParent.status)) {
684
+ throw new Error('Retry parent execution run must match the retry source parent');
685
+ }
686
+ expectedParentId = recoveryParent.id;
687
+ }
688
+ const parentRunId = retrySource ? expectedParentId : input.parentRunId ?? null;
689
+ let parent = null;
690
+ if (parentRunId) {
691
+ parent = getRelatedRun(input.projectId, parentRunId, 'Parent');
692
+ assertNoLineageCycle(id, parent, 'parent_run_id');
693
+ }
694
+ const correlationId = resolveCorrelationId(parent);
695
+ if (input.kind !== 'test' && input.testDetails) {
696
+ throw new Error('Test execution details require a test execution run');
697
+ }
698
+ if (input.kind !== 'github_actions' && input.githubActionsDetails) {
699
+ throw new Error('GitHub Actions details require a github_actions execution run');
700
+ }
701
+ if (input.kind === 'github_actions' && !input.githubActionsDetails) {
702
+ throw new Error('GitHub Actions execution runs require domain details');
703
+ }
704
+ if (input.kind === 'test') {
705
+ assertTestTarget(input.projectId, input.testDetails);
706
+ }
707
+ if (input.kind === 'github_actions') {
708
+ const details = input.githubActionsDetails;
709
+ if (!Number.isSafeInteger(details.workflowId) || details.workflowId < 1) {
710
+ throw new Error('GitHub Actions execution runs require a positive workflow ID');
711
+ }
712
+ const conflict = db.prepare(`
713
+ SELECT er.id
714
+ FROM execution_runs er
715
+ JOIN github_actions_run_details ga ON ga.execution_run_id = er.id
716
+ WHERE er.project_id = ?
717
+ AND er.kind = 'github_actions'
718
+ AND er.status IN ('queued', 'waiting')
719
+ AND ga.provider_run_id IS NULL
720
+ AND ga.workflow_id = ?
721
+ AND ga.ref = ?
722
+ LIMIT 1
723
+ `).get(input.projectId, details.workflowId, details.ref ?? null);
724
+ if (conflict) {
725
+ throw new GitHubActionsDispatchConflictError(conflict.id);
726
+ }
727
+ }
728
+ const now = new Date().toISOString();
729
+ insertRunStatement.run(id, input.projectId, input.kind, input.domainStatus ?? null, primaryLink?.entityType ?? null, primaryLink?.entityId ?? null, primaryLink?.label ?? null, primaryLink?.url ?? null, correlationId, initiatedBy, metadataSchemaVersion, metadataJson, parent?.id ?? null, retrySource?.id ?? null, retrySource ? retrySource.attempt + 1 : 1, now, now);
730
+ for (const link of links) {
731
+ insertRunLinkStatement.run(id, input.projectId, link.entityType, link.entityId, link.relationship, link.label, link.url ?? null, now);
732
+ }
733
+ if (input.kind === 'test') {
734
+ insertTestDetails(id, input.testDetails);
735
+ }
736
+ if (input.kind === 'github_actions') {
737
+ insertGitHubActionsDetails(id, input.githubActionsDetails);
738
+ }
739
+ const event = createEvent({
740
+ projectId: input.projectId,
741
+ runId: id,
742
+ eventType: input.eventType ?? 'created',
743
+ fromStatus: null,
744
+ toStatus: 'queued',
745
+ domainStatus: input.domainStatus ?? null,
746
+ message: input.eventMessage,
747
+ metadataJson: serializeBoundedObject(input.eventMetadata, 'Lifecycle event metadata'),
748
+ createdAt: now,
749
+ });
750
+ return { run: serializeRun(getRunRow(input.projectId, id)), event };
751
+ };
752
+ const insertRunLink = (input) => {
753
+ if (input.link.relationship === 'primary') {
754
+ throw new Error('Primary execution run links can only be set at run creation');
755
+ }
756
+ const link = validateExecutionRunLink(input.projectId, input.link);
757
+ const existing = getRunLinkStatement.get(input.runId, input.projectId, link.entityType, link.entityId, link.relationship);
758
+ if (existing) {
759
+ throw new Error(`Execution run link already exists for ${link.entityType} ${link.entityId} as ${link.relationship}`);
760
+ }
761
+ const now = new Date().toISOString();
762
+ insertRunLinkStatement.run(input.runId, input.projectId, link.entityType, link.entityId, link.relationship, link.label, link.url ?? null, now);
763
+ return getRunLinkStatement.get(input.runId, input.projectId, link.entityType, link.entityId, link.relationship);
764
+ };
765
+ const addExecutionRunLinkInTransaction = (input) => {
766
+ getRunRow(input.projectId, input.runId);
767
+ return insertRunLink(input);
768
+ };
769
+ const addExecutionRunLinksInTransaction = (input) => {
770
+ getRunRow(input.projectId, input.runId);
771
+ return input.links.map(link => insertRunLink({
772
+ projectId: input.projectId,
773
+ runId: input.runId,
774
+ link,
775
+ }));
776
+ };
777
+ const transitionExecutionRunInTransaction = (input, allowAmbiguousAutomationReconciliation = false) => {
778
+ const current = getRunRow(input.projectId, input.runId);
779
+ const domainStatus = input.domainStatus === undefined ? current.domain_status : input.domainStatus;
780
+ const failure = input.failure
781
+ ? { ...input.failure, message: normalizeBoundedMessage(input.failure.message, MAX_FAILURE_MESSAGE_LENGTH) ?? '' }
782
+ : null;
783
+ assertBoundedString(domainStatus, 'Domain status', 200);
784
+ assertBoundedString(failure?.code, 'Failure code', 200);
785
+ const failureDetailsJson = serializeBoundedObject(failure?.details, 'Failure details');
786
+ const isAmbiguousAutomationReconciliation = allowAmbiguousAutomationReconciliation
787
+ && current.status === 'failed'
788
+ && current.domain_status === 'ambiguous'
789
+ && GENERIC_RETRY_DENIED_KINDS.has(current.kind)
790
+ && (input.status === 'completed' || input.status === 'interrupted');
791
+ if (TERMINAL_STATUSES.has(current.status) && !isAmbiguousAutomationReconciliation) {
792
+ const terminalDataAgrees = current.status === input.status
793
+ && current.domain_status === domainStatus
794
+ && current.failure_code === (failure?.code ?? null)
795
+ && current.failure_message === (failure?.message ?? null)
796
+ && current.failure_details_json === failureDetailsJson
797
+ && (input.completedAt === undefined || current.completed_at === input.completedAt);
798
+ if (!terminalDataAgrees) {
799
+ throw new Error(`Execution run ${input.runId} is already terminal with conflicting data`);
800
+ }
801
+ return { run: serializeRun(current), event: null };
802
+ }
803
+ if (current.status === input.status) {
804
+ throw new Error(`Execution run ${input.runId} is already ${input.status}`);
805
+ }
806
+ if (ACTIVE_STATUSES.has(input.status)) {
807
+ const activeTransitionAllowed = (current.status === 'queued' && (input.status === 'running' || input.status === 'waiting'))
808
+ || (current.status === 'running' && input.status === 'waiting')
809
+ || (current.status === 'waiting' && input.status === 'running');
810
+ if (!activeTransitionAllowed) {
811
+ throw new Error(`Illegal execution run transition from ${current.status} to ${input.status}`);
812
+ }
813
+ if (failure) {
814
+ throw new Error('Active execution runs cannot have failure information');
815
+ }
816
+ }
817
+ const now = new Date().toISOString();
818
+ const completedAt = TERMINAL_STATUSES.has(input.status) ? input.completedAt ?? now : null;
819
+ const startedAt = input.status === 'running' ? current.started_at ?? now : current.started_at;
820
+ updateRunStatement.run(input.status, domainStatus, failure?.code ?? null, failure?.message ?? null, failureDetailsJson, startedAt, completedAt, now, input.runId, input.projectId);
821
+ const event = createEvent({
822
+ projectId: input.projectId,
823
+ runId: input.runId,
824
+ eventType: input.eventType ?? 'status_changed',
825
+ fromStatus: current.status,
826
+ toStatus: input.status,
827
+ domainStatus,
828
+ message: input.eventMessage,
829
+ metadataJson: serializeBoundedObject(input.eventMetadata, 'Lifecycle event metadata'),
830
+ createdAt: now,
831
+ });
832
+ return { run: serializeRun(getRunRow(input.projectId, input.runId)), event };
833
+ };
834
+ const appendLifecycleEventInTransaction = (input) => {
835
+ const run = getRunRow(input.projectId, input.runId);
836
+ const event = createEvent({
837
+ projectId: input.projectId,
838
+ runId: input.runId,
839
+ eventType: input.eventType,
840
+ fromStatus: run.status,
841
+ toStatus: run.status,
842
+ domainStatus: input.domainStatus === undefined ? run.domain_status : input.domainStatus,
843
+ message: input.message,
844
+ metadataSchemaVersion: input.metadataSchemaVersion,
845
+ metadataJson: serializeBoundedObject(input.metadata, 'Lifecycle event metadata'),
846
+ createdAt: new Date().toISOString(),
847
+ });
848
+ return { run: serializeRun(run), event };
849
+ };
850
+ const isPromiseLike = (value) => {
851
+ if ((typeof value !== 'object' || value === null) && typeof value !== 'function') {
852
+ return false;
853
+ }
854
+ return typeof value.then === 'function';
855
+ };
856
+ const executionTransactionNotificationScopes = [];
857
+ export const withExecutionRunTransaction = (callback, emitter) => {
858
+ const notifications = [];
859
+ const runTransaction = db.transaction(() => {
860
+ const transaction = {
861
+ createExecutionRun: input => {
862
+ const result = createExecutionRunInTransaction(input);
863
+ enqueueRunAndEvent(notifications, result.run, result.event, emitter);
864
+ return result.run;
865
+ },
866
+ createAutomationRetryExecutionRun: input => {
867
+ const result = createExecutionRunInTransaction(input, true);
868
+ enqueueRunAndEvent(notifications, result.run, result.event, emitter);
869
+ return result.run;
870
+ },
871
+ transitionExecutionRun: input => {
872
+ const result = transitionExecutionRunInTransaction(input);
873
+ if (result.event) {
874
+ enqueueRunAndEvent(notifications, result.run, result.event, emitter);
875
+ }
876
+ return result.run;
877
+ },
878
+ reconcileAmbiguousAutomationExecutionRun: input => {
879
+ const result = transitionExecutionRunInTransaction(input, true);
880
+ if (result.event) {
881
+ enqueueRunAndEvent(notifications, result.run, result.event, emitter);
882
+ }
883
+ return result.run;
884
+ },
885
+ appendLifecycleEvent: input => {
886
+ const result = appendLifecycleEventInTransaction(input);
887
+ notifications.push({ event: 'lifecycle-event:created', payload: result.event, emitter });
888
+ return result.event;
889
+ },
890
+ addExecutionRunLink: input => {
891
+ const link = addExecutionRunLinkInTransaction(input);
892
+ return { ...link, url_snapshot: safeSnapshotUrl(link.url_snapshot) };
893
+ },
894
+ addExecutionRunLinks: input => addExecutionRunLinksInTransaction(input).map(link => ({
895
+ ...link,
896
+ url_snapshot: safeSnapshotUrl(link.url_snapshot),
897
+ })),
898
+ };
899
+ const result = callback(transaction);
900
+ if (isPromiseLike(result)) {
901
+ throw new Error('Execution run transactions require a synchronous callback');
902
+ }
903
+ return result;
904
+ });
905
+ const parentNotifications = executionTransactionNotificationScopes.at(-1);
906
+ executionTransactionNotificationScopes.push(notifications);
907
+ try {
908
+ const result = runTransaction();
909
+ executionTransactionNotificationScopes.pop();
910
+ if (parentNotifications) {
911
+ parentNotifications.push(...notifications);
912
+ }
913
+ else {
914
+ emitCommittedNotifications(notifications);
915
+ }
916
+ return result;
917
+ }
918
+ catch (error) {
919
+ executionTransactionNotificationScopes.pop();
920
+ throw error;
921
+ }
922
+ };
923
+ export const createExecutionRun = (input, emitter) => (withExecutionRunTransaction(transaction => transaction.createExecutionRun(input), emitter));
924
+ export const addExecutionRunLink = (input) => (withExecutionRunTransaction(transaction => transaction.addExecutionRunLink(input)));
925
+ export const addExecutionRunLinks = (input) => (withExecutionRunTransaction(transaction => transaction.addExecutionRunLinks(input)));
926
+ export const transitionExecutionRun = (input, emitter) => (withExecutionRunTransaction(transaction => transaction.transitionExecutionRun(input), emitter));
927
+ export const appendLifecycleEvent = (input, emitter) => {
928
+ return withExecutionRunTransaction(transaction => transaction.appendLifecycleEvent(input), emitter);
929
+ };
930
+ export const getExecutionRun = (projectId, runId) => {
931
+ const row = getRunStatement.get(runId, projectId);
932
+ return row ? serializeRun(row) : null;
933
+ };
934
+ export const getQueuedSpecReviewExecutionRun = (projectId, snapshotId) => {
935
+ const row = db.prepare(`
936
+ SELECT execution_runs.id
937
+ FROM execution_runs
938
+ INNER JOIN execution_run_links snapshot_link
939
+ ON snapshot_link.execution_run_id = execution_runs.id
940
+ AND snapshot_link.project_id = execution_runs.project_id
941
+ AND snapshot_link.entity_type = 'spec_snapshot'
942
+ AND snapshot_link.relationship = 'context'
943
+ WHERE execution_runs.project_id = ?
944
+ AND execution_runs.kind = 'spec_review'
945
+ AND execution_runs.status = 'queued'
946
+ AND snapshot_link.entity_id = ?
947
+ ORDER BY execution_runs.created_at DESC, execution_runs.id DESC
948
+ LIMIT 1
949
+ `).get(projectId, snapshotId);
950
+ return row ? getExecutionRun(projectId, row.id) : null;
951
+ };
952
+ export const getQueuedBuildExecutionRun = (projectId, ticketId, sequenceId) => {
953
+ const row = db.prepare(`
954
+ SELECT execution_runs.id
955
+ FROM execution_runs
956
+ INNER JOIN execution_run_links ticket_link
957
+ ON ticket_link.execution_run_id = execution_runs.id
958
+ AND ticket_link.project_id = execution_runs.project_id
959
+ AND ticket_link.entity_type = 'ticket'
960
+ AND ticket_link.relationship = 'primary'
961
+ INNER JOIN execution_run_links sequence_link
962
+ ON sequence_link.execution_run_id = execution_runs.id
963
+ AND sequence_link.project_id = execution_runs.project_id
964
+ AND sequence_link.entity_type = 'build_sequence'
965
+ AND sequence_link.relationship = 'context'
966
+ WHERE execution_runs.project_id = ?
967
+ AND execution_runs.kind = 'build'
968
+ AND execution_runs.status = 'queued'
969
+ AND ticket_link.entity_id = ?
970
+ AND sequence_link.entity_id = ?
971
+ ORDER BY execution_runs.created_at ASC, execution_runs.id ASC
972
+ LIMIT 1
973
+ `).get(projectId, ticketId, sequenceId);
974
+ return row ? getExecutionRun(projectId, row.id) : null;
975
+ };
976
+ export const hasActiveBuildExecutionRunForTicket = (projectId, ticketId) => {
977
+ return Boolean(db.prepare(`
978
+ SELECT 1
979
+ FROM execution_runs
980
+ INNER JOIN execution_run_links ticket_link
981
+ ON ticket_link.execution_run_id = execution_runs.id
982
+ AND ticket_link.project_id = execution_runs.project_id
983
+ AND ticket_link.entity_type = 'ticket'
984
+ AND ticket_link.relationship = 'primary'
985
+ WHERE execution_runs.project_id = ?
986
+ AND execution_runs.kind = 'build'
987
+ AND execution_runs.status IN ('queued', 'running', 'waiting')
988
+ AND ticket_link.entity_id = ?
989
+ LIMIT 1
990
+ `).get(projectId, ticketId));
991
+ };
992
+ const purgeTestExecutionRunTransaction = db.transaction((projectId, runId) => {
993
+ return db.prepare(`
994
+ DELETE FROM execution_runs
995
+ WHERE id = ? AND project_id = ? AND kind = 'test'
996
+ `).run(runId, projectId).changes > 0;
997
+ });
998
+ export const purgeTestExecutionRun = (projectId, runId) => {
999
+ return purgeTestExecutionRunTransaction(projectId, runId);
1000
+ };
1001
+ export const listExecutionRunLinks = (projectId, runId) => {
1002
+ getRunRow(projectId, runId);
1003
+ const links = getRunLinksStatement.all(runId, projectId);
1004
+ return links.map(link => ({ ...link, url_snapshot: safeSnapshotUrl(link.url_snapshot) }));
1005
+ };
1006
+ const testDetailsSelect = `
1007
+ SELECT td.*, er.project_id, er.status AS execution_status
1008
+ FROM test_execution_details td
1009
+ JOIN execution_runs er ON er.id = td.execution_run_id
1010
+ `;
1011
+ const serializeTestDetails = (row) => ({
1012
+ execution_run_id: row.execution_run_id,
1013
+ project_id: row.project_id,
1014
+ execution_status: row.execution_status,
1015
+ command_id: row.command_id,
1016
+ command_name: row.command_name,
1017
+ command_runner: row.command_runner,
1018
+ command_snapshot: row.command_snapshot,
1019
+ active_command: row.active_command,
1020
+ target_kind: row.target_kind,
1021
+ target_bundle_id: row.target_bundle_id,
1022
+ target_label: row.target_label,
1023
+ ticket_id: row.ticket_id,
1024
+ selection: parseStoredObject(row.selection_json),
1025
+ summary: parseStoredObject(row.summary_json),
1026
+ exit_code: row.exit_code,
1027
+ duration_ms: row.duration_ms,
1028
+ output: row.output,
1029
+ output_truncated: row.output_truncated === 1,
1030
+ output_retention_state: row.output_retention_state,
1031
+ output_expired_at: row.output_expired_at,
1032
+ output_byte_count: row.output_byte_count ?? Buffer.byteLength(row.output, 'utf8'),
1033
+ });
1034
+ export const getTestExecutionDetails = (projectId, runId) => {
1035
+ const row = db.prepare(`${testDetailsSelect} WHERE er.project_id = ? AND er.id = ?`)
1036
+ .get(projectId, runId);
1037
+ return row ? serializeTestDetails(row) : null;
1038
+ };
1039
+ export const updateTestExecutionDetails = (input) => {
1040
+ const current = getTestExecutionDetails(input.projectId, input.runId);
1041
+ if (!current) {
1042
+ throw new Error(`Test execution run ${input.runId} was not found in project ${input.projectId}`);
1043
+ }
1044
+ const boundedOutput = input.output === undefined
1045
+ ? { output: current.output, outputTruncated: current.output_truncated }
1046
+ : boundTestOutput(input.output, input.outputTruncated ?? current.output_truncated);
1047
+ db.prepare(`
1048
+ UPDATE test_execution_details
1049
+ SET summary_json = ?, exit_code = ?, duration_ms = ?, output = ?, output_truncated = ?,
1050
+ output_retention_state = ?, output_expired_at = ?, output_byte_count = ?
1051
+ WHERE execution_run_id = ?
1052
+ `).run(input.summary === undefined ? serializeBoundedObject(current.summary, 'Test summary') : serializeBoundedObject(input.summary, 'Test summary'), input.exitCode === undefined ? current.exit_code : input.exitCode, input.durationMs === undefined ? current.duration_ms : input.durationMs, boundedOutput.output, boundedOutput.outputTruncated ? 1 : 0, input.output === undefined ? current.output_retention_state : 'retained', input.output === undefined ? current.output_expired_at : null, input.output === undefined ? current.output_byte_count : Buffer.byteLength(boundedOutput.output, 'utf8'), input.runId);
1053
+ return getTestExecutionDetails(input.projectId, input.runId);
1054
+ };
1055
+ export const reconcileInterruptedTestRuns = (emitter) => {
1056
+ const rows = db.prepare(`
1057
+ SELECT project_id, id
1058
+ FROM execution_runs
1059
+ WHERE kind = 'test' AND status IN ('queued', 'running', 'waiting')
1060
+ `).all();
1061
+ for (const row of rows) {
1062
+ transitionExecutionRun({
1063
+ projectId: row.project_id,
1064
+ runId: row.id,
1065
+ status: 'interrupted',
1066
+ domainStatus: 'interrupted',
1067
+ failure: {
1068
+ code: 'server_restarted',
1069
+ message: 'The server restarted while this local test run was active.',
1070
+ },
1071
+ eventType: 'test_run_interrupted',
1072
+ }, emitter);
1073
+ }
1074
+ return rows.length;
1075
+ };
1076
+ const serializeGitHubActionsDetails = (row) => ({
1077
+ execution_run_id: row.execution_run_id,
1078
+ project_id: row.project_id,
1079
+ execution_status: row.execution_status,
1080
+ domain_status: row.domain_status,
1081
+ failure: row.failure_message ? {
1082
+ code: row.failure_code,
1083
+ message: row.failure_message,
1084
+ details: parseStoredObject(row.failure_details_json),
1085
+ } : null,
1086
+ workflow_id: row.workflow_id,
1087
+ workflow_name: row.workflow_name,
1088
+ ref: row.ref,
1089
+ input_names: parseStoredArray(row.input_names_json).filter(value => typeof value === 'string'),
1090
+ input_fingerprint: row.input_fingerprint,
1091
+ resolution_attempts: row.resolution_attempts,
1092
+ provider_error_count: row.provider_error_count,
1093
+ provider_run_id: row.provider_run_id,
1094
+ provider_status: row.provider_status,
1095
+ conclusion: row.conclusion,
1096
+ html_url: row.html_url,
1097
+ head_sha: row.head_sha,
1098
+ head_branch: row.head_branch,
1099
+ provider_created_at: row.provider_created_at,
1100
+ provider_updated_at: row.provider_updated_at,
1101
+ provider_started_at: row.provider_started_at,
1102
+ dispatched_at: row.dispatched_at,
1103
+ last_synced_at: row.last_synced_at,
1104
+ jobs: parseStoredArray(row.jobs_json),
1105
+ jobs_retention_state: row.jobs_retention_state,
1106
+ jobs_expired_at: row.jobs_expired_at,
1107
+ jobs_count: row.jobs_count ?? parseStoredArray(row.jobs_json).length,
1108
+ });
1109
+ const githubActionsDetailsSelect = `
1110
+ SELECT ga.*, er.project_id, er.status AS execution_status, er.domain_status,
1111
+ er.failure_code, er.failure_message, er.failure_details_json
1112
+ FROM github_actions_run_details ga
1113
+ JOIN execution_runs er ON er.id = ga.execution_run_id
1114
+ `;
1115
+ export const getGitHubActionsExecutionDetails = (projectId, runId) => {
1116
+ const row = db.prepare(`${githubActionsDetailsSelect} WHERE er.project_id = ? AND er.id = ?`)
1117
+ .get(projectId, runId);
1118
+ return row ? serializeGitHubActionsDetails(row) : null;
1119
+ };
1120
+ export const listGitHubActionsExecutionDetails = (projectId, statuses) => {
1121
+ const clauses = ["er.kind = 'github_actions'"];
1122
+ const parameters = [];
1123
+ if (projectId) {
1124
+ clauses.push('er.project_id = ?');
1125
+ parameters.push(projectId);
1126
+ }
1127
+ if (statuses && statuses.length > 0) {
1128
+ clauses.push(`er.status IN (${statuses.map(() => '?').join(', ')})`);
1129
+ parameters.push(...statuses);
1130
+ }
1131
+ const rows = db.prepare(`${githubActionsDetailsSelect} WHERE ${clauses.join(' AND ')} ORDER BY er.created_at DESC, er.id DESC`)
1132
+ .all(...parameters);
1133
+ return rows.map(serializeGitHubActionsDetails);
1134
+ };
1135
+ export const updateGitHubActionsRunDetails = (input) => {
1136
+ const current = getGitHubActionsExecutionDetails(input.projectId, input.runId);
1137
+ if (!current) {
1138
+ throw new Error(`GitHub Actions execution run ${input.runId} was not found in project ${input.projectId}`);
1139
+ }
1140
+ const jobsJson = input.jobs === undefined ? serializeJobsSnapshot(current.jobs) : serializeJobsSnapshot(input.jobs);
1141
+ db.prepare(`
1142
+ UPDATE github_actions_run_details
1143
+ SET workflow_name = ?, resolution_attempts = ?, provider_error_count = ?, provider_run_id = ?, provider_status = ?,
1144
+ conclusion = ?, html_url = ?, head_sha = ?, head_branch = ?, provider_created_at = ?,
1145
+ provider_updated_at = ?, provider_started_at = ?, last_synced_at = ?, jobs_json = ?,
1146
+ jobs_retention_state = ?, jobs_expired_at = ?, jobs_count = ?
1147
+ WHERE execution_run_id = ?
1148
+ `).run(input.workflowName === undefined ? current.workflow_name : input.workflowName, input.resolutionAttempts ?? current.resolution_attempts, input.providerErrorCount ?? current.provider_error_count, input.providerRunId === undefined ? current.provider_run_id : input.providerRunId, input.providerStatus === undefined ? current.provider_status : input.providerStatus, input.conclusion === undefined ? current.conclusion : input.conclusion, input.htmlUrl === undefined ? current.html_url : input.htmlUrl, input.headSha === undefined ? current.head_sha : input.headSha, input.headBranch === undefined ? current.head_branch : input.headBranch, input.providerCreatedAt === undefined ? current.provider_created_at : input.providerCreatedAt, input.providerUpdatedAt === undefined ? current.provider_updated_at : input.providerUpdatedAt, input.providerStartedAt === undefined ? current.provider_started_at : input.providerStartedAt, input.lastSyncedAt === undefined ? current.last_synced_at : input.lastSyncedAt, jobsJson, input.jobs === undefined ? current.jobs_retention_state : 'retained', input.jobs === undefined ? current.jobs_expired_at : null, input.jobs === undefined ? current.jobs_count : input.jobs?.length ?? 0, input.runId);
1149
+ return getGitHubActionsExecutionDetails(input.projectId, input.runId);
1150
+ };
1151
+ export const toGitHubActionsRun = (details) => {
1152
+ if (details.provider_run_id === null) {
1153
+ return null;
1154
+ }
1155
+ return {
1156
+ id: details.provider_run_id,
1157
+ name: details.workflow_name,
1158
+ workflow_id: details.workflow_id,
1159
+ status: details.provider_status === 'queued' || details.provider_status === 'in_progress' || details.provider_status === 'completed'
1160
+ ? details.provider_status
1161
+ : 'unknown',
1162
+ conclusion: details.conclusion,
1163
+ html_url: details.html_url,
1164
+ head_branch: details.head_branch,
1165
+ created_at: details.provider_created_at,
1166
+ updated_at: details.provider_updated_at,
1167
+ run_started_at: details.provider_started_at,
1168
+ jobs: details.jobs,
1169
+ };
1170
+ };
1171
+ export const listExecutionRuns = (input) => {
1172
+ const limit = input.limit ?? 50;
1173
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
1174
+ throw new Error('Execution run page limit must be between 1 and 200');
1175
+ }
1176
+ const statuses = [...new Set(input.statuses ?? [])];
1177
+ const cursor = input.cursor ? decodeCursor(input.cursor) : null;
1178
+ const clauses = ['project_id = ?'];
1179
+ const parameters = [input.projectId];
1180
+ if (input.kind) {
1181
+ clauses.push('kind = ?');
1182
+ parameters.push(input.kind);
1183
+ }
1184
+ if (statuses.length > 0) {
1185
+ clauses.push(`status IN (${statuses.map(() => '?').join(', ')})`);
1186
+ parameters.push(...statuses);
1187
+ }
1188
+ if (cursor) {
1189
+ clauses.push('(created_at < ? OR (created_at = ? AND id < ?))');
1190
+ parameters.push(cursor.created_at, cursor.created_at, cursor.id);
1191
+ }
1192
+ const rows = db.prepare(`
1193
+ SELECT * FROM execution_runs
1194
+ WHERE ${clauses.join(' AND ')}
1195
+ ORDER BY created_at DESC, id DESC
1196
+ LIMIT ?
1197
+ `).all(...parameters, limit + 1);
1198
+ const pageRows = rows.slice(0, limit);
1199
+ const lastRow = pageRows.at(-1);
1200
+ return {
1201
+ runs: pageRows.map(serializeRun),
1202
+ next_cursor: rows.length > limit && lastRow
1203
+ ? encodeCursor({ created_at: lastRow.created_at, id: lastRow.id })
1204
+ : null,
1205
+ };
1206
+ };
1207
+ export const listLifecycleEvents = (projectId, runId) => {
1208
+ getRunRow(projectId, runId);
1209
+ const rows = db.prepare(`
1210
+ SELECT * FROM lifecycle_events
1211
+ WHERE project_id = ? AND execution_run_id = ?
1212
+ ORDER BY sequence ASC
1213
+ `).all(projectId, runId);
1214
+ return rows.map(serializeEvent);
1215
+ };
1216
+ export const listProjectLifecycleEvents = (input) => {
1217
+ const limit = input.limit ?? 50;
1218
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 200) {
1219
+ throw new Error('Lifecycle event page limit must be between 1 and 200');
1220
+ }
1221
+ const statuses = [...new Set(input.statuses ?? [])];
1222
+ const cursor = input.cursor ? decodeLifecycleEventCursor(input.cursor) : null;
1223
+ const clauses = ['le.project_id = ?'];
1224
+ const parameters = [input.projectId];
1225
+ if (input.kind) {
1226
+ clauses.push('er.kind = ?');
1227
+ parameters.push(input.kind);
1228
+ }
1229
+ if (statuses.length > 0) {
1230
+ clauses.push(`er.status IN (${statuses.map(() => '?').join(', ')})`);
1231
+ parameters.push(...statuses);
1232
+ }
1233
+ if (cursor) {
1234
+ clauses.push(`(
1235
+ le.created_at < ?
1236
+ OR (le.created_at = ? AND le.execution_run_id < ?)
1237
+ OR (le.created_at = ? AND le.execution_run_id = ? AND le.sequence < ?)
1238
+ OR (le.created_at = ? AND le.execution_run_id = ? AND le.sequence = ? AND le.id < ?)
1239
+ )`);
1240
+ parameters.push(cursor.created_at, cursor.created_at, cursor.execution_run_id, cursor.created_at, cursor.execution_run_id, cursor.sequence, cursor.created_at, cursor.execution_run_id, cursor.sequence, cursor.id);
1241
+ }
1242
+ const rows = db.prepare(`
1243
+ SELECT le.*
1244
+ FROM lifecycle_events le
1245
+ JOIN execution_runs er ON er.id = le.execution_run_id AND er.project_id = le.project_id
1246
+ WHERE ${clauses.join(' AND ')}
1247
+ ORDER BY le.created_at DESC, le.execution_run_id DESC, le.sequence DESC, le.id DESC
1248
+ LIMIT ?
1249
+ `).all(...parameters, limit + 1);
1250
+ const pageRows = rows.slice(0, limit);
1251
+ const lastRow = pageRows.at(-1);
1252
+ return {
1253
+ events: pageRows.map(serializeEvent),
1254
+ next_cursor: rows.length > limit && lastRow
1255
+ ? encodeLifecycleEventCursor({
1256
+ created_at: lastRow.created_at,
1257
+ execution_run_id: lastRow.execution_run_id,
1258
+ sequence: lastRow.sequence,
1259
+ id: lastRow.id,
1260
+ })
1261
+ : null,
1262
+ };
1263
+ };
1264
+ //# sourceMappingURL=executionRuns.js.map