@straiffi/archon 1.7.2 → 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 (176) 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-Do9nbC8v.js → architectureDiagram-3BPJPVTR-BL5dYiul.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DGGOLHF8.js → blockDiagram-GPEHLZMM-Bi5x2v80.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-Bz7tQybs.js → c4Diagram-AAUBKEIU-esEm54Ay.js} +1 -1
  7. package/dist/client/assets/channel-CqNHF3FD.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-CJO-GmWJ.js → chunk-2J33WTMH-DACfY8xM.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DPAVagpC.js → chunk-3OPIFGDE-DZNUiaUs.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-Cof75Mu7.js → chunk-5ZQYHXKU-ClDfkMAp.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-COuPqQ8_.js → chunk-727SXJPM-CCwA3GWT.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CFj5KabW.js → chunk-AQP2D5EJ-Byl4DRZ9.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-C4M6w5cT.js → chunk-CSCIHK7Q-CXymZQ7A.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-ByaiwVue.js → chunk-KSCS5N6A-VV3D8g6a.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-Cx5jeot9.js → chunk-L5ZTLDWV-BL9N5GRI.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-DmGtztud.js → chunk-LZXEDZCA-DKNN-zno.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-i3yuspqL.js → chunk-ND2GUHAM-CHCpQ1yr.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CQ4E_jP4.js → chunk-NZK2D7GU-B0Dm74mt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-D2g4VDCd.js → chunk-O5CBEL6O-DAa5z2xG.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-exIu-zQ2.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-DvF75yLZ.js → dagre-BM42HDAG-B0fsl8mE.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DFv0HxLL.js → diagram-2AECGRRQ-Cx_VEbSS.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-C_LRir0-.js → diagram-5GNKFQAL-Cv7VVn5r.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DGZxRDRy.js → diagram-KO2AKTUF-C8Sl5czV.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-UXt0uti4.js → diagram-LMA3HP47-C3dZaLz2.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-CFbZS4n5.js → diagram-OG6HWLK6-BAU9DtTr.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-CfAwZDdf.js → erDiagram-TEJ5UH35-DpNc-eBD.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cdgi5z3n.js → flowDiagram-I6XJVG4X-DGwTAT1S.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DOrkFkhu.js → ganttDiagram-6RSMTGT7-MPZhEgzJ.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-cEox0peD.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-gm51u2Nn.js → infoDiagram-5YYISTIA-BBZeKjf4.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CjnoOIja.js → ishikawaDiagram-YF4QCWOH-CM_iFrES.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-Dlg4DsiO.js → journeyDiagram-JHISSGLW-Do_lY94X.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-BU0XZaSd.js → kanban-definition-UN3LZRKU-brxg3j37.js} +1 -1
  39. package/dist/client/assets/{line-Bert22-_.js → line-PHHbm9rj.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-BvQSGDmL.js → mermaid-parser.core-CLxAkW69.js} +1 -1
  41. package/dist/client/assets/{mermaid.core--qSAHJRj.js → mermaid.core-CebvSmx9.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgFIvWWD.js → mindmap-definition-RKZ34NQL-CX_-ZaJF.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-DGcbXD0M.js → pieDiagram-4H26LBE5-BJuHvql_.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-ClkBUeia.js → quadrantDiagram-W4KKPZXB-DiJll7l9.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-B8CwGToU.js → requirementDiagram-4Y6WPE33-Bg-tXgcj.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DTk3jL6B.js → sankeyDiagram-5OEKKPKP-BoK2egfZ.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQVrcnKf.js → sequenceDiagram-3UESZ5HK-DcrDWNzv.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DD4yMZPB.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-BnCBVqcd.js → timeline-definition-PNZ67QCA-DqSVAhqL.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-BDmLMNXS.js → vennDiagram-CIIHVFJN-D9wVraVY.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DSLuQiEv.js → wardleyDiagram-YWT4CUSO-CLff9zVd.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DAq_LA4r.js → xychartDiagram-2RQKCTM6-BuHUMMH2.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +472 -39
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +1311 -212
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +228 -71
  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/attachments.js +24 -0
  64. package/dist/server/lib/attachments.js.map +1 -1
  65. package/dist/server/lib/automationProgression.js +464 -0
  66. package/dist/server/lib/automationProgression.js.map +1 -0
  67. package/dist/server/lib/automationReconciliation.js +498 -0
  68. package/dist/server/lib/automationReconciliation.js.map +1 -0
  69. package/dist/server/lib/automationStageDetails.js +232 -0
  70. package/dist/server/lib/automationStageDetails.js.map +1 -0
  71. package/dist/server/lib/buildOpenQuestionOperations.js +238 -0
  72. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
  73. package/dist/server/lib/buildOpenQuestions.js +44 -0
  74. package/dist/server/lib/buildOpenQuestions.js.map +1 -1
  75. package/dist/server/lib/buildSequences.js +130 -46
  76. package/dist/server/lib/buildSequences.js.map +1 -1
  77. package/dist/server/lib/bundleReviewOperations.js +91 -9
  78. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  79. package/dist/server/lib/chatOperations.js +67 -12
  80. package/dist/server/lib/chatOperations.js.map +1 -1
  81. package/dist/server/lib/executionReconciliation.js +518 -0
  82. package/dist/server/lib/executionReconciliation.js.map +1 -0
  83. package/dist/server/lib/executionRetry.js +694 -0
  84. package/dist/server/lib/executionRetry.js.map +1 -0
  85. package/dist/server/lib/executionRetryContext.js +489 -0
  86. package/dist/server/lib/executionRetryContext.js.map +1 -0
  87. package/dist/server/lib/executionRetryPredicates.js +74 -0
  88. package/dist/server/lib/executionRetryPredicates.js.map +1 -0
  89. package/dist/server/lib/executionRunRetention.js +141 -0
  90. package/dist/server/lib/executionRunRetention.js.map +1 -0
  91. package/dist/server/lib/executionRuns.js +1367 -0
  92. package/dist/server/lib/executionRuns.js.map +1 -0
  93. package/dist/server/lib/executionTimeline.js +45 -0
  94. package/dist/server/lib/executionTimeline.js.map +1 -0
  95. package/dist/server/lib/git.js +385 -6
  96. package/dist/server/lib/git.js.map +1 -1
  97. package/dist/server/lib/integrations/github.js +43 -0
  98. package/dist/server/lib/integrations/github.js.map +1 -1
  99. package/dist/server/lib/integrations/githubActions.js +1 -0
  100. package/dist/server/lib/integrations/githubActions.js.map +1 -1
  101. package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
  102. package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
  103. package/dist/server/lib/projectMemorySelections.js +7 -3
  104. package/dist/server/lib/projectMemorySelections.js.map +1 -1
  105. package/dist/server/lib/shutdown.js +2 -2
  106. package/dist/server/lib/shutdown.js.map +1 -1
  107. package/dist/server/lib/specArtifact.js +3 -0
  108. package/dist/server/lib/specArtifact.js.map +1 -1
  109. package/dist/server/lib/specFormatting.js +1 -1
  110. package/dist/server/lib/specFormatting.js.map +1 -1
  111. package/dist/server/lib/specOperations.js +78 -12
  112. package/dist/server/lib/specOperations.js.map +1 -1
  113. package/dist/server/lib/specReviewContext.js +54 -18
  114. package/dist/server/lib/specReviewContext.js.map +1 -1
  115. package/dist/server/lib/specReviewDraft.js +3 -2
  116. package/dist/server/lib/specReviewDraft.js.map +1 -1
  117. package/dist/server/lib/specReviewOperations.js +74 -9
  118. package/dist/server/lib/specReviewOperations.js.map +1 -1
  119. package/dist/server/lib/specReviewValidation.js +217 -0
  120. package/dist/server/lib/specReviewValidation.js.map +1 -0
  121. package/dist/server/lib/specReviews.js +30 -2
  122. package/dist/server/lib/specReviews.js.map +1 -1
  123. package/dist/server/lib/specs.js +18 -1
  124. package/dist/server/lib/specs.js.map +1 -1
  125. package/dist/server/lib/stageSnapshots.js +197 -0
  126. package/dist/server/lib/stageSnapshots.js.map +1 -0
  127. package/dist/server/lib/testSessions.js +160 -18
  128. package/dist/server/lib/testSessions.js.map +1 -1
  129. package/dist/server/lib/ticketAutomation.js +80 -4
  130. package/dist/server/lib/ticketAutomation.js.map +1 -1
  131. package/dist/server/lib/ticketAutomationCommit.js +466 -91
  132. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  133. package/dist/server/lib/ticketFollowUpOperations.js +270 -29
  134. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  135. package/dist/server/lib/ticketMessages.js +16 -0
  136. package/dist/server/lib/ticketMessages.js.map +1 -1
  137. package/dist/server/lib/ticketRunOperations.js +51 -6
  138. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  139. package/dist/server/lib/ticketUndo.js +77 -168
  140. package/dist/server/lib/ticketUndo.js.map +1 -1
  141. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  142. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  143. package/dist/server/lib/tickets.js +41 -27
  144. package/dist/server/lib/tickets.js.map +1 -1
  145. package/dist/server/lib/transcriptRuns.js +3 -3
  146. package/dist/server/lib/transcriptRuns.js.map +1 -1
  147. package/dist/server/lib/turnExecutionErrors.js +12 -0
  148. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  149. package/dist/server/lib/types.js +58 -1
  150. package/dist/server/lib/types.js.map +1 -1
  151. package/dist/server/workers/build.js +596 -184
  152. package/dist/server/workers/build.js.map +1 -1
  153. package/dist/server/workers/chat.js +269 -132
  154. package/dist/server/workers/chat.js.map +1 -1
  155. package/dist/server/workers/followUp.js +731 -203
  156. package/dist/server/workers/followUp.js.map +1 -1
  157. package/dist/server/workers/plan.js +546 -188
  158. package/dist/server/workers/plan.js.map +1 -1
  159. package/dist/server/workers/planFollowUp.js +240 -75
  160. package/dist/server/workers/planFollowUp.js.map +1 -1
  161. package/dist/server/workers/review.js +790 -297
  162. package/dist/server/workers/review.js.map +1 -1
  163. package/dist/server/workers/specReview.js +212 -46
  164. package/dist/server/workers/specReview.js.map +1 -1
  165. package/dist/server/workers/specSession.js +434 -254
  166. package/dist/server/workers/specSession.js.map +1 -1
  167. package/package.json +1 -1
  168. package/dist/client/assets/GitHubActionsDialog-DAD5REmY.js +0 -1
  169. package/dist/client/assets/TestsDialog-B5NVrroq.js +0 -4
  170. package/dist/client/assets/TicketChangesModal-By-_gTGd.js +0 -1
  171. package/dist/client/assets/channel-Dd5Len0X.js +0 -1
  172. package/dist/client/assets/classDiagram-4FO5ZUOK-B7KUg6r8.js +0 -1
  173. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CQm-z5c4.js +0 -1
  174. package/dist/client/assets/index-BHq6-Eir.js +0 -270
  175. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  176. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C8yO6POT.js +0 -1
@@ -1,9 +1,9 @@
1
1
  import db from '../db.js';
2
2
  import { formatAgentErrorForTranscript, getAgentErrorTranscriptRuns, runAgent, storeTicketUsageSnapshot, storeTicketUsageSnapshotDirect } from '../lib/agent.js';
3
- import { clearAndEmitAgentRunsForParent } from '../lib/agentRuns.js';
3
+ import { createAgentRun, emitAgentRunsUpdated, getAgentRun, terminalizeAgentRunsForExecutionRun } from '../lib/agentRuns.js';
4
4
  import { advanceBuildChain } from '../lib/buildChains.js';
5
5
  import { listBuildOpenQuestionsForBundle, reconcileBuildOpenQuestions } from '../lib/buildOpenQuestions.js';
6
- import { stopBuildSequence } from '../lib/buildSequences.js';
6
+ import { activateQueuedBuildSequenceTicket, getActiveBuildSequenceId, getBuildSequenceIdForTicket, stopBuildSequence } from '../lib/buildSequences.js';
7
7
  import { prepareTicketForBuild } from '../lib/buildFlow.js';
8
8
  import { createWorktreeAsync, deleteWorktree, resolveWorktreePath, syncWorktreeFilesAsync, worktreeExists } from '../lib/git.js';
9
9
  import config from '../lib/config.js';
@@ -13,6 +13,7 @@ import { getProjectMemorySuggestionPromptRules } from '../lib/projectMemorySugge
13
13
  import { ensureInitialCommitForWorktrees } from '../lib/projectRepos.js';
14
14
  import { getEffectiveProject } from '../lib/projects.js';
15
15
  import { enqueue, isRunning } from '../lib/queue.js';
16
+ import { getExecutionRun, getQueuedBuildExecutionRun, transitionExecutionRun, withExecutionRunTransaction } from '../lib/executionRuns.js';
16
17
  import { runSetupCommandsInWorkspace } from '../lib/run.js';
17
18
  import { registerBundleActivity, unregisterBundleActivity } from '../lib/bundleActivity.js';
18
19
  import { beginTicketThinkingRun, bindTicketThinkingMessage } from '../lib/thinkingState.js';
@@ -23,9 +24,23 @@ import { captureStageBaseline, captureStageSuccess, clearStageSnapshot } from '.
23
24
  import { autoParkTicketIfStale, getTicket, listTickets, resolveTicketBranch, resolveTicketTool, resolveTicketWorkerKey } from '../lib/tickets.js';
24
25
  import { clearAutomationPause, depthAtLeast, getBundleAutomationState, recordAutomationPause, resolveAutomationConfig } from '../lib/ticketAutomation.js';
25
26
  import { transitionBundleToReview } from '../lib/bundleReviewOperations.js';
27
+ import { completeAutomationProgression, ensureAutomationProgression, runAutomationProgression, waitAutomationProgression } from '../lib/automationProgression.js';
26
28
  const getErrorMessage = (error) => {
27
29
  return error instanceof Error ? error.message : String(error);
28
30
  };
31
+ const asGenericEmitter = (io) => io;
32
+ const defaultInitiation = {
33
+ initiator: 'user',
34
+ causalParentRunId: null,
35
+ retrySourceRunId: null,
36
+ workflowContext: null,
37
+ };
38
+ const emitTicketUpdate = (ticketId, io) => {
39
+ const updated = getTicket(ticketId);
40
+ if (updated) {
41
+ io.emit('ticket:updated', updated);
42
+ }
43
+ };
29
44
  const slugify = (text) => text.replace(/[^a-zA-Z0-9\s-]/g, '').trim().replace(/\s+/g, '-').slice(0, 60);
30
45
  const branchName = (ticket) => slugify(ticket.title);
31
46
  const clearBuildCompletion = (ticketId) => {
@@ -88,11 +103,6 @@ const resumeBuildPrompt = (ticket, projectPath, memoryEnabled) => {
88
103
  const openQuestionsBlock = getBuildOpenQuestionsBlock(ticket);
89
104
  return prependSkillPromptBlock(`Working directory: ${projectPath}\n\nContinue implementing this build-stage ticket from the current session and worktree state. Do not restart from scratch, and keep any existing partial work unless you intentionally need to change it.\n\n<ticket>\n${ticket.description || ticket.title}\n</ticket>\n\n${projectMemory ? `${projectMemory}\n\n` : ''}${openQuestionsBlock ? `${openQuestionsBlock}\n\n` : ''}Continue the build from where it stopped in the current project worktree only. If the ticket references linked projects or other repositories, treat them as read-only context and do not modify them. Do NOT commit your changes.\n\n${OPEN_QUESTIONS_RESPONSE_RULES}${getProjectMemorySuggestionPromptRules(memoryEnabled, 'build') ? `\n\n${getProjectMemorySuggestionPromptRules(memoryEnabled, 'build')}` : ''}`, ticket.skills);
90
105
  };
91
- const scheduleBuildStart = (ticket, io) => {
92
- setTimeout(() => {
93
- startBuild(ticket, io);
94
- }, 0);
95
- };
96
106
  const resolveTicketWorkspace = (ticket, project, branch) => {
97
107
  const bundle = ticket.worktree_bundle_id ? getTicket(ticket.id)?.bundle ?? null : null;
98
108
  if (bundle?.kind === 'project_root') {
@@ -107,215 +117,617 @@ const resolveTicketWorkspace = (ticket, project, branch) => {
107
117
  };
108
118
  };
109
119
  const startBuildJob = (ticket, io, options = {}) => {
110
- const jobKey = resolveTicketWorkerKey(ticket, 'build');
111
- if (!jobKey || isRunning(jobKey)) {
112
- return;
120
+ const initiation = { ...defaultInitiation, ...options.initiation };
121
+ let acceptedTicket = ticket;
122
+ let sequenceId = options.sequenceId ?? null;
123
+ let compensateAcceptance;
124
+ let executionContext;
125
+ let jobKey = '';
126
+ let project;
127
+ let branch = '';
128
+ let tool;
129
+ let rejectionReason = 'invalid_context';
130
+ let ticketSnapshot;
131
+ try {
132
+ executionContext = withExecutionRunTransaction(transaction => {
133
+ const originalTicketSnapshot = ticket.project_id
134
+ ? db.prepare(`
135
+ SELECT state, tool, agent_status, agent_log, streaming_response, branch,
136
+ build_completed_at, run_setup_status, auto_park_dismissed_at,
137
+ planning_context_json, lane_order
138
+ FROM tickets
139
+ WHERE id = ? AND project_id = ?
140
+ `).get(ticket.id, ticket.project_id)
141
+ : undefined;
142
+ const acceptance = options.accept?.();
143
+ if (acceptance) {
144
+ acceptedTicket = acceptance.ticket;
145
+ sequenceId = acceptance.sequenceId ?? options.sequenceId ?? getActiveBuildSequenceId(acceptedTicket.id);
146
+ compensateAcceptance = acceptance.compensate;
147
+ }
148
+ sequenceId ??= getActiveBuildSequenceId(acceptedTicket.id);
149
+ jobKey = resolveTicketWorkerKey(acceptedTicket, 'build') ?? '';
150
+ if (!jobKey || isRunning(jobKey)) {
151
+ rejectionReason = 'busy';
152
+ throw new Error('Ticket build is already scheduled or running');
153
+ }
154
+ const acceptedProject = getEffectiveProject(acceptedTicket);
155
+ if (!acceptedProject || !acceptedTicket.project_id || acceptedProject.id !== acceptedTicket.project_id) {
156
+ rejectionReason = 'project_unavailable';
157
+ throw new Error('Ticket project is unavailable');
158
+ }
159
+ project = acceptedProject;
160
+ branch = resolveTicketBranch(acceptedTicket) ?? branchName(acceptedTicket);
161
+ ticketSnapshot = acceptedTicket.id === ticket.id
162
+ ? originalTicketSnapshot
163
+ : db.prepare(`
164
+ SELECT state, tool, agent_status, agent_log, streaming_response, branch,
165
+ build_completed_at, run_setup_status, auto_park_dismissed_at,
166
+ planning_context_json, lane_order
167
+ FROM tickets
168
+ WHERE id = ? AND project_id = ?
169
+ `).get(acceptedTicket.id, project.id);
170
+ tool = resolveTicketTool(acceptedTicket, config.tool ?? 'opencode');
171
+ clearBuildCompletion(acceptedTicket.id);
172
+ if (!acceptedTicket.worktree_bundle_id) {
173
+ db.prepare('UPDATE tickets SET branch = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(branch, acceptedTicket.id);
174
+ }
175
+ const queuedExecutionRun = sequenceId
176
+ ? getQueuedBuildExecutionRun(project.id, acceptedTicket.id, sequenceId)
177
+ : null;
178
+ const executionRun = queuedExecutionRun ?? transaction.createExecutionRun({
179
+ projectId: project.id,
180
+ kind: 'build',
181
+ domainStatus: 'queued',
182
+ sourceReference: { kind: 'ticket', id: acceptedTicket.id, label: acceptedTicket.title },
183
+ links: [
184
+ ...(acceptedTicket.worktree_bundle_id
185
+ ? [{ entityType: 'bundle', entityId: acceptedTicket.worktree_bundle_id, relationship: 'context' }]
186
+ : []),
187
+ ...(sequenceId
188
+ ? [{ entityType: 'build_sequence', entityId: sequenceId, relationship: 'context' }]
189
+ : []),
190
+ ],
191
+ initiatedBy: initiation.initiator,
192
+ parentRunId: initiation.causalParentRunId,
193
+ retryOfRunId: initiation.retrySourceRunId,
194
+ metadata: initiation.workflowContext,
195
+ eventType: initiation.retrySourceRunId ? 'retry_started' : 'build_created',
196
+ });
197
+ return {
198
+ runId: executionRun.id,
199
+ correlationId: executionRun.correlation_id,
200
+ projectId: executionRun.project_id,
201
+ kind: executionRun.kind,
202
+ };
203
+ }, io);
113
204
  }
114
- clearBuildCompletion(ticket.id);
115
- const branch = resolveTicketBranch(ticket) ?? branchName(ticket);
116
- const tool = resolveTicketTool(ticket, config.tool ?? 'opencode');
117
- if (!ticket.worktree_bundle_id) {
118
- db.prepare('UPDATE tickets SET branch = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(branch, ticket.id);
205
+ catch (error) {
206
+ return { accepted: false, reason: rejectionReason, error: getErrorMessage(error) };
119
207
  }
120
- enqueue(jobKey, async () => {
121
- registerBundleActivity(ticket, 'build');
122
- try {
123
- beginTicketThinkingRun(ticket.id);
124
- const project = getEffectiveProject(ticket);
125
- if (!project) {
126
- throw new Error('Ticket project no longer exists');
127
- }
128
- const workspace = resolveTicketWorkspace(ticket, project, branch);
129
- const shouldCreateWorktree = workspace.shouldCreateWorktree;
130
- updateStatus(ticket.id, 'running', shouldCreateWorktree ? 'Preparing worktree...' : 'Preparing build workspace...', io);
131
- if (shouldCreateWorktree) {
132
- let setupCompleted = false;
133
- updateRunSetupStatus(ticket.id, 'preparing', io);
134
- try {
135
- ensureInitialCommitForWorktrees(project.repo_path);
136
- await createWorktreeAsync(branch, project);
137
- updateStatus(ticket.id, 'running', 'Syncing worktree files...', io);
138
- await syncWorktreeFilesAsync(branch, project);
139
- updateStatus(ticket.id, 'running', 'Running setup commands...', io);
140
- await runSetupCommandsInWorkspace(project, workspace.cwd);
141
- updateRunSetupStatus(ticket.id, 'ready', io);
142
- setupCompleted = true;
208
+ let queuedJob;
209
+ try {
210
+ queuedJob = enqueue(jobKey, async () => {
211
+ let activityRegistered = false;
212
+ try {
213
+ registerBundleActivity(acceptedTicket, 'build');
214
+ activityRegistered = true;
215
+ withExecutionRunTransaction(transaction => {
216
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?').run('running', 'Preparing build workspace...', acceptedTicket.id, executionContext.projectId);
217
+ const currentTicket = getTicket(acceptedTicket.id);
218
+ if (!currentTicket || currentTicket.project_id !== executionContext.projectId) {
219
+ throw new Error('Ticket no longer exists in the execution project');
220
+ }
221
+ transaction.transitionExecutionRun({
222
+ projectId: executionContext.projectId,
223
+ runId: executionContext.runId,
224
+ status: 'running',
225
+ domainStatus: 'running',
226
+ eventType: 'build_started',
227
+ });
228
+ }, io);
229
+ if (acceptedTicket.worktree_bundle_id) {
230
+ markTicketDescriptionVisibleInBundle(acceptedTicket.id);
143
231
  }
144
- finally {
145
- if (!setupCompleted) {
146
- updateRunSetupStatus(ticket.id, null, io);
147
- deleteWorktree(branch, project);
232
+ emitTicketUpdate(acceptedTicket.id, io);
233
+ beginTicketThinkingRun(acceptedTicket.id);
234
+ const mainRunId = createAgentRun({
235
+ executionRunId: executionContext.runId,
236
+ parentType: 'ticket',
237
+ parentId: acceptedTicket.id,
238
+ label: options.resume ? 'Build continuation' : 'Build',
239
+ role: 'main',
240
+ });
241
+ const mainRun = getAgentRun(mainRunId);
242
+ if (!mainRun) {
243
+ throw new Error('Unable to create the Build agent run');
244
+ }
245
+ emitAgentRunsUpdated(io, 'ticket', acceptedTicket.id);
246
+ const currentProject = getEffectiveProject(acceptedTicket);
247
+ if (!currentProject || currentProject.id !== executionContext.projectId) {
248
+ throw new Error('Ticket project no longer exists');
249
+ }
250
+ const workspace = resolveTicketWorkspace(acceptedTicket, currentProject, branch);
251
+ const shouldCreateWorktree = workspace.shouldCreateWorktree;
252
+ updateStatus(acceptedTicket.id, 'running', shouldCreateWorktree ? 'Preparing worktree...' : 'Preparing build workspace...', io);
253
+ if (shouldCreateWorktree) {
254
+ let setupCompleted = false;
255
+ updateRunSetupStatus(acceptedTicket.id, 'preparing', io);
256
+ try {
257
+ ensureInitialCommitForWorktrees(currentProject.repo_path);
258
+ await createWorktreeAsync(branch, currentProject);
259
+ updateStatus(acceptedTicket.id, 'running', 'Syncing worktree files...', io);
260
+ await syncWorktreeFilesAsync(branch, currentProject);
261
+ updateStatus(acceptedTicket.id, 'running', 'Running setup commands...', io);
262
+ await runSetupCommandsInWorkspace(currentProject, workspace.cwd);
263
+ updateRunSetupStatus(acceptedTicket.id, 'ready', io);
264
+ setupCompleted = true;
265
+ }
266
+ finally {
267
+ if (!setupCompleted) {
268
+ updateRunSetupStatus(acceptedTicket.id, null, io);
269
+ deleteWorktree(branch, currentProject);
270
+ }
148
271
  }
149
272
  }
150
- }
151
- else {
152
- updateRunSetupStatus(ticket.id, 'ready', io);
153
- }
154
- updateStatus(ticket.id, 'running', 'Saving build baseline...', io);
155
- await captureStageBaseline(ticket.id, 'build');
156
- const cwd = workspace.cwd;
157
- const { response: finalResponse, responseTiming, usageSnapshot, transcriptRuns } = await runAgent({
158
- ticketId: ticket.id,
159
- tool,
160
- prompt: options.resume ? resumeBuildPrompt(ticket, cwd, Boolean(project.memory_enabled)) : buildPrompt(ticket, cwd, Boolean(project.memory_enabled)),
161
- cwd,
162
- model: ticket.model,
163
- variant: ticket.variant,
164
- resumeSessionId: options.resume ? ticket.session_id : null,
165
- trackBuildProcess: true,
166
- emitSuccessUpdate: false,
167
- io,
168
- });
169
- const parsedBuildOutput = typeof finalResponse === 'string'
170
- ? parseAgentResponseWithOpenQuestions(finalResponse)
171
- : { response: '', openQuestions: [], hasOpenQuestionsMetadata: false, suggestions: [] };
172
- const assistantMessage = formatAssistantMessageWithOpenQuestions({
173
- response: parsedBuildOutput.response,
174
- openQuestions: parsedBuildOutput.openQuestions,
175
- });
176
- let assistantMessageId = null;
177
- if (assistantMessage) {
178
- assistantMessageId = createTicketMessage({
179
- ticketId: ticket.id,
180
- role: 'assistant',
181
- kind: 'final_response',
182
- content: assistantMessage,
183
- responseTiming,
184
- transcriptRuns,
273
+ else {
274
+ updateRunSetupStatus(acceptedTicket.id, 'ready', io);
275
+ }
276
+ updateStatus(acceptedTicket.id, 'running', 'Saving build baseline...', io);
277
+ await captureStageBaseline(acceptedTicket.id, 'build');
278
+ const cwd = workspace.cwd;
279
+ const { response: finalResponse, responseTiming, usageSnapshot, transcriptRuns } = await runAgent({
280
+ ticketId: acceptedTicket.id,
281
+ tool,
282
+ prompt: options.resume ? resumeBuildPrompt(acceptedTicket, cwd, Boolean(currentProject.memory_enabled)) : buildPrompt(acceptedTicket, cwd, Boolean(currentProject.memory_enabled)),
283
+ cwd,
284
+ model: acceptedTicket.model,
285
+ variant: acceptedTicket.variant,
286
+ resumeSessionId: options.resume ? acceptedTicket.session_id : null,
287
+ trackBuildProcess: true,
288
+ emitSuccessUpdate: false,
289
+ executionContext,
290
+ persistSessionIdToTicket: true,
291
+ runStream: {
292
+ runId: mainRun.id,
293
+ label: mainRun.label,
294
+ role: mainRun.role,
295
+ runIndex: mainRun.run_index,
296
+ },
297
+ io,
298
+ });
299
+ const completedMainRun = getAgentRun(mainRun.id);
300
+ const completionStatus = completedMainRun?.agent_status === 'stopped' ? 'stopped' : 'done';
301
+ const parsedBuildOutput = typeof finalResponse === 'string'
302
+ ? parseAgentResponseWithOpenQuestions(finalResponse)
303
+ : { response: '', openQuestions: [], hasOpenQuestionsMetadata: false, suggestions: [] };
304
+ const assistantMessage = formatAssistantMessageWithOpenQuestions({
305
+ response: parsedBuildOutput.response,
306
+ openQuestions: parsedBuildOutput.openQuestions,
185
307
  });
308
+ const assistantMessageId = withExecutionRunTransaction(transaction => {
309
+ const messageId = assistantMessage
310
+ ? createTicketMessage({
311
+ ticketId: acceptedTicket.id,
312
+ role: 'assistant',
313
+ kind: 'final_response',
314
+ content: assistantMessage,
315
+ responseTiming,
316
+ transcriptRuns,
317
+ })
318
+ : null;
319
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?').run(completionStatus, acceptedTicket.id, executionContext.projectId);
320
+ const completedTicket = getTicket(acceptedTicket.id) ?? acceptedTicket;
321
+ if (completionStatus === 'done') {
322
+ markBuildCompleted(acceptedTicket.id);
323
+ if (parsedBuildOutput.hasOpenQuestionsMetadata) {
324
+ db.prepare('UPDATE tickets SET planning_context_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(JSON.stringify(createTicketOpenQuestionsContext(completedTicket, parsedBuildOutput.openQuestions)), acceptedTicket.id);
325
+ if (acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
326
+ reconcileBuildOpenQuestions({
327
+ projectId: acceptedTicket.project_id,
328
+ bundleId: acceptedTicket.worktree_bundle_id,
329
+ ticketId: acceptedTicket.id,
330
+ questions: parsedBuildOutput.openQuestions,
331
+ sourceKey: `build:${acceptedTicket.id}:${messageId ?? completedTicket.updated_at}`,
332
+ });
333
+ }
334
+ }
335
+ }
336
+ if (messageId) {
337
+ transaction.addExecutionRunLink({
338
+ projectId: executionContext.projectId,
339
+ runId: executionContext.runId,
340
+ link: {
341
+ entityType: 'ticket_message',
342
+ entityId: messageId,
343
+ relationship: 'result',
344
+ expectedParent: { entityType: 'ticket', entityId: acceptedTicket.id },
345
+ },
346
+ });
347
+ }
348
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, completionStatus, null);
349
+ transaction.transitionExecutionRun({
350
+ projectId: executionContext.projectId,
351
+ runId: executionContext.runId,
352
+ status: completionStatus === 'stopped' ? 'cancelled' : 'completed',
353
+ domainStatus: completionStatus,
354
+ eventType: completionStatus === 'stopped' ? 'build_cancelled' : 'build_completed',
355
+ });
356
+ return messageId;
357
+ }, io);
186
358
  if (assistantMessageId) {
187
- bindTicketThinkingMessage(ticket.id, assistantMessageId);
359
+ bindTicketThinkingMessage(acceptedTicket.id, assistantMessageId);
188
360
  }
189
- }
190
- if (transcriptRuns) {
191
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
192
- }
193
- const completedTicket = getTicket(ticket.id) ?? ticket;
194
- if (completedTicket.agent_status === 'done' && parsedBuildOutput.hasOpenQuestionsMetadata) {
195
- db.prepare('UPDATE tickets SET planning_context_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(JSON.stringify(createTicketOpenQuestionsContext(completedTicket, parsedBuildOutput.openQuestions)), ticket.id);
196
- if (ticket.project_id && ticket.worktree_bundle_id) {
197
- reconcileBuildOpenQuestions({
198
- projectId: ticket.project_id,
199
- bundleId: ticket.worktree_bundle_id,
200
- ticketId: ticket.id,
201
- questions: parsedBuildOutput.openQuestions,
202
- sourceKey: `build:${ticket.id}:${assistantMessageId ?? completedTicket.updated_at}`,
203
- });
204
- const openQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
205
- const automationConfig = resolveAutomationConfig(completedTicket.bundle ?? null, project);
206
- const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
361
+ emitTicketUpdate(acceptedTicket.id, io);
362
+ const completedTicket = getTicket(acceptedTicket.id) ?? acceptedTicket;
363
+ if (completionStatus === 'done'
364
+ && parsedBuildOutput.hasOpenQuestionsMetadata
365
+ && acceptedTicket.project_id
366
+ && acceptedTicket.worktree_bundle_id) {
367
+ const openQuestions = listBuildOpenQuestionsForBundle(acceptedTicket.project_id, acceptedTicket.worktree_bundle_id);
368
+ const liveProject = getEffectiveProject(completedTicket) ?? project;
369
+ const automationConfig = resolveAutomationConfig(completedTicket.bundle ?? null, liveProject);
370
+ const bundleState = getBundleAutomationState(acceptedTicket.project_id, acceptedTicket.worktree_bundle_id);
207
371
  const shouldPauseForOpenQuestions = (openQuestions.length > 0
208
372
  && automationConfig.pauseOnOpenQuestions
209
373
  && depthAtLeast(automationConfig.depth, 'auto_review'));
210
374
  if (shouldPauseForOpenQuestions) {
211
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `${openQuestions.length} unresolved Build open question${openQuestions.length === 1 ? '' : 's'}`);
375
+ const progression = ensureAutomationProgression({
376
+ projectId: acceptedTicket.project_id,
377
+ bundleId: acceptedTicket.worktree_bundle_id,
378
+ ticketId: acceptedTicket.id,
379
+ ticketTitle: acceptedTicket.title,
380
+ parentRunId: executionContext.runId,
381
+ workflowContext: initiation.workflowContext,
382
+ broadcaster: asGenericEmitter(io),
383
+ });
384
+ waitAutomationProgression({
385
+ projectId: acceptedTicket.project_id,
386
+ bundleId: acceptedTicket.worktree_bundle_id,
387
+ progressionRunId: progression.runId,
388
+ pauseCode: 'open_questions',
389
+ pauseReason: `${openQuestions.length} unresolved Build open question${openQuestions.length === 1 ? '' : 's'}`,
390
+ reasonCode: 'precondition_failed',
391
+ pausedStageRunId: executionContext.runId,
392
+ broadcaster: asGenericEmitter(io),
393
+ });
212
394
  }
213
395
  else if (bundleState?.automation_paused_code === 'open_questions') {
214
- clearAutomationPause(ticket.project_id, ticket.worktree_bundle_id);
396
+ if (!depthAtLeast(automationConfig.depth, 'auto_review') && bundleState.automation_progression_run_id) {
397
+ completeAutomationProgression({
398
+ projectId: acceptedTicket.project_id,
399
+ bundleId: acceptedTicket.worktree_bundle_id,
400
+ progressionRunId: bundleState.automation_progression_run_id,
401
+ domainStatus: 'manual_boundary_reached',
402
+ broadcaster: asGenericEmitter(io),
403
+ });
404
+ }
405
+ else if (bundleState.automation_progression_run_id) {
406
+ runAutomationProgression({
407
+ projectId: acceptedTicket.project_id,
408
+ bundleId: acceptedTicket.worktree_bundle_id,
409
+ progressionRunId: bundleState.automation_progression_run_id,
410
+ broadcaster: asGenericEmitter(io),
411
+ });
412
+ }
413
+ else {
414
+ clearAutomationPause(acceptedTicket.project_id, acceptedTicket.worktree_bundle_id);
415
+ }
215
416
  }
216
- emitUpdatedTickets(listTickets(ticket.project_id)
217
- .filter(bundleTicket => bundleTicket.worktree_bundle_id === ticket.worktree_bundle_id)
417
+ emitUpdatedTickets(listTickets(acceptedTicket.project_id)
418
+ .filter(bundleTicket => bundleTicket.worktree_bundle_id === acceptedTicket.worktree_bundle_id)
218
419
  .map(bundleTicket => bundleTicket.id), io);
219
420
  }
220
- }
221
- clearTicketStreamingResponse(ticket.id);
222
- const freshTicket = getTicket(ticket.id);
223
- if (freshTicket?.tool === 'opencode' && freshTicket.session_id) {
224
- storeTicketUsageSnapshot(freshTicket.id, freshTicket.session_id);
225
- }
226
- else if (freshTicket?.tool === 'claude' && usageSnapshot) {
227
- storeTicketUsageSnapshotDirect(freshTicket.id, usageSnapshot);
228
- }
229
- const completedWithMessage = getTicket(ticket.id);
230
- if (completedWithMessage) {
231
- io.emit('ticket:updated', completedWithMessage);
232
- }
233
- const updated = getTicket(ticket.id);
234
- if (updated?.agent_status === 'done') {
235
- if (project.memory_enabled && parsedBuildOutput.suggestions.length > 0) {
236
- persistProjectMemorySuggestions(project.id, ticket.id, 'build', parsedBuildOutput.suggestions);
421
+ const freshTicket = getTicket(acceptedTicket.id);
422
+ if (freshTicket?.tool === 'opencode' && freshTicket.session_id) {
423
+ storeTicketUsageSnapshot(freshTicket.id, freshTicket.session_id);
237
424
  }
238
- markBuildCompleted(ticket.id);
239
- clearStageSnapshot(ticket.id, 'review');
240
- captureStageSuccess(ticket.id, 'build');
241
- const completed = autoParkTicketIfStale(ticket.id);
242
- if (completed) {
243
- io.emit('ticket:updated', completed);
425
+ else if (freshTicket?.tool === 'claude' && usageSnapshot) {
426
+ storeTicketUsageSnapshotDirect(freshTicket.id, usageSnapshot);
244
427
  }
245
- }
246
- const nextAgentStatus = getTicket(ticket.id)?.agent_status ?? updated?.agent_status ?? null;
247
- if (nextAgentStatus !== 'stopped') {
248
- const buildChainAdvance = advanceBuildChain(ticket.id, nextAgentStatus);
249
- emitUpdatedTickets(buildChainAdvance.affectedTicketIds, io);
250
- if (buildChainAdvance.nextTicketId) {
251
- const nextBuild = prepareTicketForBuild(buildChainAdvance.nextTicketId, {
252
- ignoredBlockerTicketIds: buildChainAdvance.ignoredBlockerTicketIds,
253
- });
254
- if (!nextBuild.ok) {
255
- const stoppedChain = stopBuildSequence(buildChainAdvance.nextTicketId);
256
- emitUpdatedTickets(stoppedChain.affectedTicketIds, io);
257
- return;
428
+ const completedWithMessage = getTicket(acceptedTicket.id);
429
+ if (completedWithMessage) {
430
+ io.emit('ticket:updated', completedWithMessage);
431
+ }
432
+ const updated = getTicket(acceptedTicket.id);
433
+ if (updated?.agent_status === 'done') {
434
+ if (project.memory_enabled && parsedBuildOutput.suggestions.length > 0) {
435
+ persistProjectMemorySuggestions(project.id, acceptedTicket.id, 'build', parsedBuildOutput.suggestions);
258
436
  }
259
- io.emit('ticket:updated', nextBuild.ticket);
260
- if (resolveTicketWorkerKey(nextBuild.ticket, 'build') === jobKey) {
261
- scheduleBuildStart(nextBuild.ticket, io);
262
- return;
437
+ clearStageSnapshot(acceptedTicket.id, 'review');
438
+ captureStageSuccess(acceptedTicket.id, 'build');
439
+ const completed = autoParkTicketIfStale(acceptedTicket.id);
440
+ if (completed) {
441
+ io.emit('ticket:updated', completed);
263
442
  }
264
- startBuild(nextBuild.ticket, io);
265
443
  }
266
- else if (updated?.agent_status === 'done' && ticket.project_id && ticket.worktree_bundle_id) {
267
- const automationConfig = resolveAutomationConfig(updated?.bundle ?? null, project);
268
- const autoReviewEnabled = depthAtLeast(automationConfig.depth, 'auto_review');
269
- const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
270
- const openQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
271
- const shouldPauseForOpenQuestions = autoReviewEnabled
272
- && automationConfig.pauseOnOpenQuestions
273
- && openQuestions.length > 0;
274
- if (shouldPauseForOpenQuestions) {
275
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `${openQuestions.length} unresolved Build open question${openQuestions.length === 1 ? '' : 's'}`);
276
- emitUpdatedTickets(listTickets(ticket.project_id)
277
- .filter(bundleTicket => bundleTicket.worktree_bundle_id === ticket.worktree_bundle_id)
278
- .map(bundleTicket => bundleTicket.id), io);
444
+ const nextAgentStatus = getTicket(acceptedTicket.id)?.agent_status ?? updated?.agent_status ?? null;
445
+ let nextBuild = null;
446
+ let nextBuildIgnoredBlockerTicketIds = [];
447
+ if (nextAgentStatus !== 'stopped') {
448
+ const buildChainAdvance = advanceBuildChain(acceptedTicket.id, nextAgentStatus);
449
+ emitUpdatedTickets(buildChainAdvance.affectedTicketIds, io);
450
+ if (buildChainAdvance.nextTicketId) {
451
+ const nextBuildValidation = prepareTicketForBuild(buildChainAdvance.nextTicketId, {
452
+ ignoredBlockerTicketIds: buildChainAdvance.ignoredBlockerTicketIds,
453
+ updateState: false,
454
+ });
455
+ if (!nextBuildValidation.ok) {
456
+ const stoppedChain = stopBuildSequence(buildChainAdvance.nextTicketId);
457
+ emitUpdatedTickets(stoppedChain.affectedTicketIds, io);
458
+ }
459
+ else {
460
+ nextBuild = nextBuildValidation.ticket;
461
+ nextBuildIgnoredBlockerTicketIds = buildChainAdvance.ignoredBlockerTicketIds;
462
+ }
279
463
  }
280
- else {
281
- if (bundleState?.automation_paused_code === 'open_questions') {
282
- clearAutomationPause(ticket.project_id, ticket.worktree_bundle_id);
464
+ else if (updated?.agent_status === 'done' && acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
465
+ const liveProject = getEffectiveProject(updated) ?? project;
466
+ const automationConfig = resolveAutomationConfig(updated?.bundle ?? null, liveProject);
467
+ const autoReviewEnabled = depthAtLeast(automationConfig.depth, 'auto_review');
468
+ const bundleState = getBundleAutomationState(acceptedTicket.project_id, acceptedTicket.worktree_bundle_id);
469
+ const openQuestions = listBuildOpenQuestionsForBundle(acceptedTicket.project_id, acceptedTicket.worktree_bundle_id);
470
+ const shouldPauseForOpenQuestions = autoReviewEnabled
471
+ && automationConfig.pauseOnOpenQuestions
472
+ && openQuestions.length > 0;
473
+ if (shouldPauseForOpenQuestions) {
474
+ const progression = ensureAutomationProgression({
475
+ projectId: acceptedTicket.project_id,
476
+ bundleId: acceptedTicket.worktree_bundle_id,
477
+ ticketId: acceptedTicket.id,
478
+ ticketTitle: acceptedTicket.title,
479
+ parentRunId: executionContext.runId,
480
+ workflowContext: initiation.workflowContext,
481
+ broadcaster: asGenericEmitter(io),
482
+ });
483
+ waitAutomationProgression({
484
+ projectId: acceptedTicket.project_id,
485
+ bundleId: acceptedTicket.worktree_bundle_id,
486
+ progressionRunId: progression.runId,
487
+ pauseCode: 'open_questions',
488
+ pauseReason: `${openQuestions.length} unresolved Build open question${openQuestions.length === 1 ? '' : 's'}`,
489
+ reasonCode: 'precondition_failed',
490
+ pausedStageRunId: executionContext.runId,
491
+ broadcaster: asGenericEmitter(io),
492
+ });
493
+ emitUpdatedTickets(listTickets(acceptedTicket.project_id)
494
+ .filter(bundleTicket => bundleTicket.worktree_bundle_id === acceptedTicket.worktree_bundle_id)
495
+ .map(bundleTicket => bundleTicket.id), io);
283
496
  }
284
- if (autoReviewEnabled && (!bundleState?.automation_paused_reason || bundleState.automation_paused_code === 'open_questions')) {
285
- transitionBundleToReview(ticket.project_id, ticket.worktree_bundle_id, io, { automatic: true });
497
+ else {
498
+ if (bundleState?.automation_paused_code === 'open_questions') {
499
+ if (!autoReviewEnabled && bundleState.automation_progression_run_id) {
500
+ completeAutomationProgression({
501
+ projectId: acceptedTicket.project_id,
502
+ bundleId: acceptedTicket.worktree_bundle_id,
503
+ progressionRunId: bundleState.automation_progression_run_id,
504
+ domainStatus: 'manual_boundary_reached',
505
+ broadcaster: asGenericEmitter(io),
506
+ });
507
+ }
508
+ else if (bundleState.automation_progression_run_id) {
509
+ runAutomationProgression({
510
+ projectId: acceptedTicket.project_id,
511
+ bundleId: acceptedTicket.worktree_bundle_id,
512
+ progressionRunId: bundleState.automation_progression_run_id,
513
+ broadcaster: asGenericEmitter(io),
514
+ });
515
+ }
516
+ else {
517
+ clearAutomationPause(acceptedTicket.project_id, acceptedTicket.worktree_bundle_id);
518
+ }
519
+ }
520
+ if (autoReviewEnabled && (!bundleState?.automation_paused_reason || bundleState.automation_paused_code === 'open_questions')) {
521
+ const progression = ensureAutomationProgression({
522
+ projectId: acceptedTicket.project_id,
523
+ bundleId: acceptedTicket.worktree_bundle_id,
524
+ ticketId: acceptedTicket.id,
525
+ ticketTitle: acceptedTicket.title,
526
+ parentRunId: executionContext.runId,
527
+ workflowContext: initiation.workflowContext,
528
+ broadcaster: asGenericEmitter(io),
529
+ });
530
+ if (getBundleAutomationState(acceptedTicket.project_id, acceptedTicket.worktree_bundle_id)?.automation_paused_code === 'open_questions') {
531
+ runAutomationProgression({
532
+ projectId: acceptedTicket.project_id,
533
+ bundleId: acceptedTicket.worktree_bundle_id,
534
+ progressionRunId: progression.runId,
535
+ broadcaster: asGenericEmitter(io),
536
+ });
537
+ }
538
+ const reviewResult = transitionBundleToReview(acceptedTicket.project_id, acceptedTicket.worktree_bundle_id, io, {
539
+ automatic: true,
540
+ initiation: {
541
+ causalParentRunId: progression.runId,
542
+ workflowContext: initiation.workflowContext,
543
+ },
544
+ });
545
+ if (!reviewResult.ok) {
546
+ waitAutomationProgression({
547
+ projectId: acceptedTicket.project_id,
548
+ bundleId: acceptedTicket.worktree_bundle_id,
549
+ progressionRunId: progression.runId,
550
+ pauseCode: 'review',
551
+ pauseReason: reviewResult.error,
552
+ reasonCode: 'stage_failed',
553
+ pausedStageRunId: executionContext.runId,
554
+ broadcaster: asGenericEmitter(io),
555
+ });
556
+ }
557
+ }
286
558
  }
287
559
  }
288
560
  }
561
+ if (nextBuild) {
562
+ const nextSequenceId = sequenceId ?? getBuildSequenceIdForTicket(nextBuild.id);
563
+ const nextOptions = {
564
+ initiation: {
565
+ initiator: 'automation',
566
+ causalParentRunId: executionContext.runId,
567
+ workflowContext: initiation.workflowContext,
568
+ },
569
+ sequenceId: nextSequenceId,
570
+ accept: () => {
571
+ const transition = prepareTicketForBuild(nextBuild.id, {
572
+ ignoredBlockerTicketIds: nextBuildIgnoredBlockerTicketIds,
573
+ });
574
+ if (!transition.ok) {
575
+ throw new Error(transition.error);
576
+ }
577
+ if (!nextSequenceId) {
578
+ throw new Error('Build sequence no longer exists');
579
+ }
580
+ activateQueuedBuildSequenceTicket(nextSequenceId, transition.ticket.id);
581
+ return {
582
+ ticket: transition.ticket,
583
+ sequenceId: nextSequenceId,
584
+ compensate: () => {
585
+ db.prepare(`
586
+ UPDATE build_sequence_tickets SET status = 'queued', started_at = NULL
587
+ WHERE build_sequence_id = ? AND ticket_id = ? AND status = 'active'
588
+ `).run(nextSequenceId, transition.ticket.id);
589
+ },
590
+ };
591
+ },
592
+ };
593
+ const scheduleNextBuild = () => {
594
+ const scheduling = startBuild(nextBuild, io, nextOptions);
595
+ if (!scheduling.accepted) {
596
+ if (scheduling.reason === 'busy') {
597
+ setTimeout(scheduleNextBuild, 0);
598
+ return;
599
+ }
600
+ const stoppedSequence = stopBuildSequence(nextBuild.id);
601
+ emitUpdatedTickets(stoppedSequence.affectedTicketIds, io);
602
+ }
603
+ };
604
+ if (resolveTicketWorkerKey(nextBuild, 'build') === jobKey) {
605
+ queuedJob.finally(scheduleNextBuild).catch(() => { });
606
+ }
607
+ else {
608
+ scheduleNextBuild();
609
+ }
610
+ }
289
611
  }
290
- }
291
- catch (err) {
292
- const transcriptRuns = getAgentErrorTranscriptRuns(err);
293
- const errorContent = formatAgentErrorForTranscript(err);
294
- if (errorContent) {
295
- createTicketMessage({
296
- ticketId: ticket.id,
297
- role: 'assistant',
298
- kind: 'error',
299
- content: errorContent,
300
- transcriptRuns,
301
- });
302
- }
303
- if (transcriptRuns) {
304
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
612
+ catch (err) {
613
+ const executionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
614
+ if (executionRun && !['queued', 'running', 'waiting'].includes(executionRun.status)) {
615
+ if (acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
616
+ recordAutomationPause(acceptedTicket.project_id, acceptedTicket.worktree_bundle_id, 'review', `Build completed but automation could not continue: ${getErrorMessage(err)}`);
617
+ emitUpdatedTickets(listTickets(acceptedTicket.project_id)
618
+ .filter(bundleTicket => bundleTicket.worktree_bundle_id === acceptedTicket.worktree_bundle_id)
619
+ .map(bundleTicket => bundleTicket.id), io);
620
+ }
621
+ return;
622
+ }
623
+ const transcriptRuns = getAgentErrorTranscriptRuns(err);
624
+ const errorContent = formatAgentErrorForTranscript(err);
625
+ const message = getErrorMessage(err);
626
+ let affectedTicketIds = [];
627
+ try {
628
+ withExecutionRunTransaction(transaction => {
629
+ if (errorContent) {
630
+ const messageId = createTicketMessage({
631
+ ticketId: acceptedTicket.id,
632
+ role: 'assistant',
633
+ kind: 'error',
634
+ content: errorContent,
635
+ transcriptRuns,
636
+ });
637
+ if (messageId) {
638
+ transaction.addExecutionRunLink({
639
+ projectId: executionContext.projectId,
640
+ runId: executionContext.runId,
641
+ link: {
642
+ entityType: 'ticket_message',
643
+ entityId: messageId,
644
+ relationship: 'result',
645
+ expectedParent: { entityType: 'ticket', entityId: acceptedTicket.id },
646
+ },
647
+ });
648
+ }
649
+ }
650
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?').run('error', message, acceptedTicket.id, executionContext.projectId);
651
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
652
+ const buildChainAdvance = advanceBuildChain(acceptedTicket.id, 'error');
653
+ affectedTicketIds = buildChainAdvance.affectedTicketIds;
654
+ transaction.transitionExecutionRun({
655
+ projectId: executionContext.projectId,
656
+ runId: executionContext.runId,
657
+ status: 'failed',
658
+ domainStatus: 'error',
659
+ failure: { code: 'build_failed', message },
660
+ eventType: 'build_failed',
661
+ });
662
+ }, io);
663
+ }
664
+ catch {
665
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
666
+ transitionExecutionRun({
667
+ projectId: executionContext.projectId,
668
+ runId: executionContext.runId,
669
+ status: 'failed',
670
+ domainStatus: 'error',
671
+ failure: { code: 'build_failed', message },
672
+ eventType: 'build_failed',
673
+ }, io);
674
+ }
675
+ const updated = getTicket(acceptedTicket.id);
676
+ if (updated) {
677
+ io.emit('ticket:updated', updated);
678
+ }
679
+ emitUpdatedTickets(affectedTicketIds, io);
305
680
  }
306
- db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run('error', getErrorMessage(err), ticket.id);
307
- const updated = getTicket(ticket.id);
308
- if (updated) {
309
- io.emit('ticket:updated', updated);
681
+ finally {
682
+ if (activityRegistered) {
683
+ unregisterBundleActivity(acceptedTicket, 'build');
684
+ }
310
685
  }
311
- const buildChainAdvance = advanceBuildChain(ticket.id, 'error');
312
- emitUpdatedTickets(buildChainAdvance.affectedTicketIds, io);
686
+ });
687
+ }
688
+ catch (error) {
689
+ const message = getErrorMessage(error);
690
+ try {
691
+ withExecutionRunTransaction(transaction => {
692
+ if (ticketSnapshot) {
693
+ db.prepare(`
694
+ UPDATE tickets
695
+ SET state = ?, tool = ?, agent_status = ?, agent_log = ?, streaming_response = ?, branch = ?,
696
+ build_completed_at = ?, run_setup_status = ?, auto_park_dismissed_at = ?,
697
+ planning_context_json = ?, lane_order = ?, updated_at = CURRENT_TIMESTAMP
698
+ WHERE id = ? AND project_id = ?
699
+ `).run(ticketSnapshot.state, ticketSnapshot.tool, ticketSnapshot.agent_status, ticketSnapshot.agent_log, ticketSnapshot.streaming_response, ticketSnapshot.branch, ticketSnapshot.build_completed_at, ticketSnapshot.run_setup_status, ticketSnapshot.auto_park_dismissed_at, ticketSnapshot.planning_context_json, ticketSnapshot.lane_order, acceptedTicket.id, project.id);
700
+ }
701
+ compensateAcceptance?.();
702
+ options.compensate?.();
703
+ transaction.transitionExecutionRun({
704
+ projectId: executionContext.projectId,
705
+ runId: executionContext.runId,
706
+ status: 'failed',
707
+ domainStatus: acceptedTicket.agent_status ?? 'idle',
708
+ failure: { code: 'queue_failed', message },
709
+ eventType: 'build_failed',
710
+ });
711
+ }, io);
313
712
  }
314
- finally {
315
- unregisterBundleActivity(ticket, 'build');
713
+ catch {
714
+ // Keep the durable failed attempt even when domain compensation can no longer apply.
715
+ transitionExecutionRun({
716
+ projectId: executionContext.projectId,
717
+ runId: executionContext.runId,
718
+ status: 'failed',
719
+ domainStatus: acceptedTicket.agent_status ?? 'idle',
720
+ failure: { code: 'queue_failed', message },
721
+ eventType: 'build_failed',
722
+ }, io);
316
723
  }
724
+ return { accepted: false, reason: 'queue_failed', error: message };
725
+ }
726
+ queuedJob.catch(() => {
727
+ // The queued callback owns domain and execution failure finalization.
317
728
  });
729
+ return { accepted: true, executionContext };
318
730
  };
319
- export const startBuild = (ticket, io) => startBuildJob(ticket, io);
320
- export const resumeBuild = (ticket, io) => startBuildJob(ticket, io, { resume: true });
731
+ export const startBuild = (ticket, io, options = {}) => startBuildJob(ticket, io, options);
732
+ export const resumeBuild = (ticket, io, options = {}) => startBuildJob(ticket, io, { ...options, resume: true });
321
733
  //# sourceMappingURL=build.js.map