@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,6 +1,7 @@
1
1
  import db from '../db.js';
2
2
  import { formatAgentErrorForTranscript, getAgentErrorTranscriptRuns, runAgent } from '../lib/agent.js';
3
- import { clearAndEmitAgentRunsForParent } from '../lib/agentRuns.js';
3
+ import { createAgentRun, emitAgentRunsUpdated, getAgentRun, terminalizeAgentRunsForExecutionRun } from '../lib/agentRuns.js';
4
+ import { getExecutionRun, withExecutionRunTransaction } from '../lib/executionRuns.js';
4
5
  import config from '../lib/config.js';
5
6
  import { failBuildOpenQuestionBatch, filterResolvedBuildOpenQuestionsForBatch, listBuildOpenQuestionsForBundle, reconcileBuildOpenQuestions, setBuildOpenQuestionBatchStatus, } from '../lib/buildOpenQuestions.js';
6
7
  import { PLAN_MODE_QUESTION_PROMPT_RULES } from '../lib/planModeQuestions.js';
@@ -21,11 +22,29 @@ import { autoParkTicketIfStale, getTicket, listTickets, resolveTicketBranch, res
21
22
  import { decideReviewFollowUpAutomation, clearAutomationPause, depthAtLeast, getBundleAutomationState, recordAutomationPause, resolveAutomationConfig, } from '../lib/ticketAutomation.js';
22
23
  import { startReview } from './review.js';
23
24
  import { transitionBundleToReview } from '../lib/bundleReviewOperations.js';
25
+ import { ensureAutomationProgression, finishAutomationReviewFix, completeAutomationProgression, resolveProgressionForChild, runAutomationProgression, waitAutomationProgression, } from '../lib/automationProgression.js';
24
26
  const getErrorMessage = (error) => {
25
27
  return error instanceof Error ? error.message : String(error);
26
28
  };
27
- const updateStatus = (ticketId, status, log, io) => {
28
- db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, log ?? null, ticketId);
29
+ const asGenericEmitter = (io) => io;
30
+ const waitReviewFixProgression = (input) => {
31
+ try {
32
+ waitAutomationProgression(input);
33
+ }
34
+ catch (error) {
35
+ const state = getBundleAutomationState(input.projectId, input.bundleId);
36
+ if (state?.automation_progression_run_id === input.progressionRunId) {
37
+ throw error;
38
+ }
39
+ }
40
+ };
41
+ const updateStatus = (ticketId, status, log, io, operation) => {
42
+ if (operation) {
43
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_operation = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, operation, log ?? null, ticketId);
44
+ }
45
+ else {
46
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, log ?? null, ticketId);
47
+ }
29
48
  const updatedTicket = getTicket(ticketId);
30
49
  if (updatedTicket?.worktree_bundle_id && updatedTicket.state !== 'plan') {
31
50
  markTicketDescriptionVisibleInBundle(ticketId);
@@ -153,266 +172,775 @@ Use the review findings as the source of truth unless the follow-up instruction
153
172
 
154
173
  ${getProjectMemorySuggestionPromptRules(memoryEnabled, 'follow_up')}` : ''}`;
155
174
  };
156
- const runReviewFollowUpCompletionAutomation = (ticket, project, io) => {
175
+ const runReviewFollowUpCompletionAutomation = (ticket, project, io, executionContext, initiation) => {
157
176
  if (!ticket.worktree_bundle_id || !ticket.project_id) {
158
177
  return false;
159
178
  }
160
179
  const projectId = ticket.project_id;
161
180
  const bundleId = ticket.worktree_bundle_id;
162
- const config = resolveAutomationConfig(ticket.bundle ?? null, project);
181
+ const liveTicket = getTicket(ticket.id) ?? ticket;
182
+ const liveProject = getEffectiveProject(liveTicket) ?? project;
183
+ const config = resolveAutomationConfig(liveTicket.bundle ?? null, liveProject);
163
184
  const bundleState = getBundleAutomationState(projectId, bundleId);
164
185
  const decision = decideReviewFollowUpAutomation(config, bundleState);
186
+ const followUpRun = getExecutionRun(projectId, executionContext.runId);
187
+ const reviewFixRun = followUpRun?.parent_run_id
188
+ ? getExecutionRun(projectId, followUpRun.parent_run_id)
189
+ : null;
190
+ const progression = reviewFixRun?.kind === 'automation_review_fix'
191
+ ? resolveProgressionForChild({
192
+ projectId,
193
+ bundleId,
194
+ childRunId: executionContext.runId,
195
+ allowReviewFixParent: true,
196
+ })
197
+ : null;
165
198
  switch (decision.kind) {
166
199
  case 'manual':
200
+ if (progression && reviewFixRun) {
201
+ finishAutomationReviewFix({
202
+ projectId,
203
+ reviewFixRunId: reviewFixRun.id,
204
+ broadcaster: asGenericEmitter(io),
205
+ });
206
+ completeAutomationProgression({
207
+ projectId,
208
+ bundleId,
209
+ progressionRunId: progression.id,
210
+ domainStatus: 'manual_boundary_reached',
211
+ broadcaster: asGenericEmitter(io),
212
+ });
213
+ return true;
214
+ }
167
215
  return false;
168
216
  case 'paused':
217
+ if (reviewFixRun) {
218
+ finishAutomationReviewFix({
219
+ projectId,
220
+ reviewFixRunId: reviewFixRun.id,
221
+ broadcaster: asGenericEmitter(io),
222
+ });
223
+ }
169
224
  return true;
170
225
  case 're_review': {
171
- startReview(ticket, io);
226
+ if (!progression || !reviewFixRun) {
227
+ const legacyProgression = ensureAutomationProgression({
228
+ projectId,
229
+ bundleId,
230
+ ticketId: ticket.id,
231
+ ticketTitle: ticket.title,
232
+ parentRunId: executionContext.runId,
233
+ workflowContext: initiation?.workflowContext ?? null,
234
+ broadcaster: asGenericEmitter(io),
235
+ });
236
+ const reviewResult = startReview(ticket, io, {
237
+ initiation: {
238
+ initiator: 'automation',
239
+ causalParentRunId: legacyProgression.runId,
240
+ workflowContext: initiation?.workflowContext ?? null,
241
+ },
242
+ });
243
+ if (!reviewResult.accepted) {
244
+ waitAutomationProgression({
245
+ projectId,
246
+ bundleId,
247
+ progressionRunId: legacyProgression.runId,
248
+ pauseCode: 'review',
249
+ pauseReason: `Automatic re-review could not start: ${reviewResult.error}`,
250
+ reasonCode: 'stage_failed',
251
+ pausedStageRunId: executionContext.runId,
252
+ broadcaster: asGenericEmitter(io),
253
+ });
254
+ const pausedTicket = getTicket(ticket.id);
255
+ if (pausedTicket) {
256
+ io.emit('ticket:updated', pausedTicket);
257
+ }
258
+ }
259
+ return true;
260
+ }
261
+ if (followUpRun?.parent_run_id) {
262
+ finishAutomationReviewFix({
263
+ projectId,
264
+ reviewFixRunId: followUpRun.parent_run_id,
265
+ broadcaster: asGenericEmitter(io),
266
+ });
267
+ }
268
+ const reviewResult = startReview(ticket, io, {
269
+ initiation: {
270
+ initiator: 'automation',
271
+ causalParentRunId: progression.id,
272
+ workflowContext: initiation?.workflowContext ?? null,
273
+ },
274
+ });
275
+ if (!reviewResult.accepted) {
276
+ waitReviewFixProgression({
277
+ projectId,
278
+ bundleId,
279
+ progressionRunId: progression.id,
280
+ pauseCode: 'review',
281
+ pauseReason: `Automatic re-review could not start: ${reviewResult.error}`,
282
+ reasonCode: 'stage_failed',
283
+ pausedStageRunId: executionContext.runId,
284
+ broadcaster: asGenericEmitter(io),
285
+ });
286
+ const pausedTicket = getTicket(ticket.id);
287
+ if (pausedTicket) {
288
+ io.emit('ticket:updated', pausedTicket);
289
+ }
290
+ }
172
291
  return true;
173
292
  }
174
293
  case 'fall_through':
175
294
  return false;
176
295
  }
177
296
  };
178
- export const startFollowUp = (ticket, io, instruction, mode = 'build', options = {}) => {
297
+ export const isFollowUpRunning = (ticket) => {
298
+ const jobKey = resolveTicketWorkerKey(ticket, 'follow_up');
299
+ return !jobKey || isRunning(jobKey);
300
+ };
301
+ export function startFollowUp(ticket, io, instruction, mode = 'build', options = {}) {
302
+ const structuredResult = options.executionContext !== undefined;
303
+ const reject = (reason, error) => (structuredResult ? { accepted: false, reason, error } : false);
179
304
  const jobKey = resolveTicketWorkerKey(ticket, 'follow_up');
180
305
  if (!jobKey || isRunning(jobKey)) {
181
- return false;
306
+ return reject('busy', 'Ticket follow-up is already scheduled or running');
182
307
  }
183
- beginTicketThinkingRun(ticket.id);
184
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
185
- updateStatus(ticket.id, 'running', null, io);
186
- const activityKind = ticket.state === 'build' ? 'build_follow_up' : 'review_follow_up';
187
- enqueue(jobKey, async () => {
188
- registerBundleActivity(ticket, activityKind);
189
- let buildOpenQuestionBatchCompleted = false;
308
+ const project = getEffectiveProject(ticket);
309
+ if (!project || !ticket.project_id || project.id !== ticket.project_id) {
310
+ return reject('project_unavailable', 'Ticket project is unavailable');
311
+ }
312
+ const runConfig = options.runConfig ?? {
313
+ tool: resolveTicketTool(ticket, config.tool ?? 'opencode'),
314
+ model: ticket.model,
315
+ variant: ticket.variant,
316
+ skills: ticket.skills,
317
+ };
318
+ let executionContext = options.executionContext;
319
+ if (!executionContext) {
320
+ const initiation = {
321
+ initiator: 'user',
322
+ causalParentRunId: null,
323
+ retrySourceRunId: null,
324
+ workflowContext: null,
325
+ ...options.initiation,
326
+ };
190
327
  try {
191
- const project = getEffectiveProject(ticket);
192
- if (!project) {
193
- if (options.buildOpenQuestionBatchId) {
194
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Ticket project no longer exists');
195
- if (ticket.project_id && ticket.worktree_bundle_id) {
196
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', 'Build open question follow-up failed: Ticket project no longer exists');
197
- emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
198
- }
199
- }
200
- updateStatus(ticket.id, 'error', 'Ticket project no longer exists', io);
201
- return;
328
+ executionContext = withExecutionRunTransaction(transaction => {
329
+ const executionRun = transaction.createExecutionRun({
330
+ projectId: project.id,
331
+ kind: 'build_follow_up',
332
+ domainStatus: 'queued',
333
+ sourceReference: { kind: 'ticket', id: ticket.id, label: ticket.title },
334
+ links: ticket.worktree_bundle_id
335
+ ? [{ entityType: 'bundle', entityId: ticket.worktree_bundle_id, relationship: 'context' }]
336
+ : [],
337
+ initiatedBy: initiation.initiator,
338
+ parentRunId: initiation.causalParentRunId,
339
+ retryOfRunId: initiation.retrySourceRunId,
340
+ metadata: {
341
+ ...(initiation.workflowContext ?? {}),
342
+ retry_tool: runConfig.tool,
343
+ retry_model: runConfig.model,
344
+ retry_variant: runConfig.variant,
345
+ retry_skills: runConfig.skills,
346
+ },
347
+ eventType: initiation.retrySourceRunId ? 'retry_started' : 'build_follow_up_created',
348
+ });
349
+ return {
350
+ runId: executionRun.id,
351
+ correlationId: executionRun.correlation_id,
352
+ projectId: executionRun.project_id,
353
+ kind: executionRun.kind,
354
+ };
355
+ }, asGenericEmitter(io));
356
+ }
357
+ catch (error) {
358
+ return reject('invalid_context', getErrorMessage(error));
359
+ }
360
+ }
361
+ let ownedProgressionRunId = null;
362
+ let ownedReviewFixRunId = null;
363
+ let ownedReviewProgressionRunId = null;
364
+ if (options.buildOpenQuestionBatchId) {
365
+ try {
366
+ const executionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
367
+ if (executionRun?.kind !== 'build_follow_up'
368
+ || executionRun.initiated_by !== 'user'
369
+ || executionRun.metadata?.build_open_question_batch_id !== options.buildOpenQuestionBatchId
370
+ || !ticket.worktree_bundle_id) {
371
+ throw new Error('Build open question follow-up ownership is invalid');
202
372
  }
203
- const branch = resolveTicketBranch(ticket);
204
- if (!branch) {
205
- if (options.buildOpenQuestionBatchId) {
206
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Worktree not ready yet');
207
- if (ticket.project_id && ticket.worktree_bundle_id) {
208
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', 'Build open question follow-up failed to start: Worktree not ready yet');
209
- emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
210
- }
373
+ ownedProgressionRunId = resolveProgressionForChild({
374
+ projectId: executionContext.projectId,
375
+ bundleId: ticket.worktree_bundle_id,
376
+ childRunId: executionContext.runId,
377
+ }).id;
378
+ }
379
+ catch (error) {
380
+ return reject('invalid_context', getErrorMessage(error));
381
+ }
382
+ }
383
+ if (!options.buildOpenQuestionBatchId && ticket.worktree_bundle_id) {
384
+ const followUpRun = getExecutionRun(executionContext.projectId, executionContext.runId);
385
+ const parent = followUpRun?.parent_run_id
386
+ ? getExecutionRun(executionContext.projectId, followUpRun.parent_run_id)
387
+ : null;
388
+ if (parent?.kind === 'automation_review_fix') {
389
+ try {
390
+ ownedReviewFixRunId = parent.id;
391
+ ownedReviewProgressionRunId = resolveProgressionForChild({
392
+ projectId: executionContext.projectId,
393
+ bundleId: ticket.worktree_bundle_id,
394
+ childRunId: executionContext.runId,
395
+ allowReviewFixParent: true,
396
+ }).id;
397
+ }
398
+ catch (error) {
399
+ return reject('invalid_context', getErrorMessage(error));
400
+ }
401
+ }
402
+ }
403
+ if (!structuredResult) {
404
+ beginTicketThinkingRun(ticket.id);
405
+ const operation = ticket.state === 'review' && mode !== 'plan' ? 'follow_up' : undefined;
406
+ updateStatus(ticket.id, 'running', null, io, operation);
407
+ }
408
+ const activityKind = ticket.state === 'build' ? 'build_follow_up' : 'review_follow_up';
409
+ let queuedJob;
410
+ try {
411
+ queuedJob = enqueue(jobKey, async () => {
412
+ const scheduledRun = getExecutionRun(executionContext.projectId, executionContext.runId);
413
+ if (!scheduledRun || !['queued', 'running', 'waiting'].includes(scheduledRun.status)) {
414
+ if (options.buildOpenQuestionBatchId
415
+ && scheduledRun
416
+ && ['failed', 'cancelled', 'interrupted'].includes(scheduledRun.status)) {
417
+ failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build open question follow-up was stopped');
211
418
  }
212
- updateStatus(ticket.id, 'error', 'Worktree not ready yet', io);
213
419
  return;
214
420
  }
215
- const cwd = ticket.bundle?.kind === 'project_root' ? project.repo_path : resolveWorktreePath(branch, project);
216
- const tool = resolveTicketTool(ticket, config.tool ?? 'opencode');
217
- const isBuildTicket = ticket.state === 'build';
218
- const isReviewTicket = ticket.state === 'review';
219
- const isPlanMode = mode === 'plan';
220
- const latestReviewFindings = isBuildTicket
221
- ? ''
222
- : getLatestReviewMarkdownForContext({
223
- ticketId: ticket.id,
224
- projectId: ticket.project_id,
225
- worktreeBundleId: ticket.worktree_bundle_id,
421
+ registerBundleActivity(ticket, activityKind);
422
+ let buildOpenQuestionBatchCompleted = false;
423
+ try {
424
+ withExecutionRunTransaction(transaction => {
425
+ const operation = ticket.state === 'review' && mode !== 'plan' ? 'follow_up' : null;
426
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_operation = ?, agent_log = NULL, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?').run('running', operation, ticket.id, executionContext.projectId);
427
+ transaction.transitionExecutionRun({
428
+ projectId: executionContext.projectId,
429
+ runId: executionContext.runId,
430
+ status: 'running',
431
+ domainStatus: 'running',
432
+ eventType: 'build_follow_up_started',
433
+ });
434
+ }, asGenericEmitter(io));
435
+ beginTicketThinkingRun(ticket.id);
436
+ updateStatus(ticket.id, 'running', null, io, ticket.state === 'review' && mode !== 'plan' ? 'follow_up' : undefined);
437
+ const project = getEffectiveProject(ticket);
438
+ if (!project) {
439
+ throw new Error('Ticket project no longer exists');
440
+ }
441
+ const branch = resolveTicketBranch(ticket);
442
+ if (!branch) {
443
+ throw new Error('Worktree not ready yet');
444
+ }
445
+ const cwd = ticket.bundle?.kind === 'project_root' ? project.repo_path : resolveWorktreePath(branch, project);
446
+ const runTicket = { ...ticket, ...runConfig };
447
+ const tool = resolveTicketTool(runTicket, config.tool ?? 'opencode');
448
+ const isBuildTicket = ticket.state === 'build';
449
+ const isReviewTicket = ticket.state === 'review';
450
+ const isPlanMode = mode === 'plan';
451
+ const latestReviewFindings = isBuildTicket
452
+ ? ''
453
+ : getLatestReviewMarkdownForContext({
454
+ ticketId: ticket.id,
455
+ projectId: ticket.project_id,
456
+ worktreeBundleId: ticket.worktree_bundle_id,
457
+ });
458
+ if (isBuildTicket && !isPlanMode) {
459
+ clearBuildCompletion(ticket.id);
460
+ }
461
+ const isReviewResume = isReviewTicket && !isPlanMode && Boolean(ticket.session_id) && hasPreviousReviewFollowUp(ticket);
462
+ const prompt = isPlanMode
463
+ ? questionFollowUpPrompt(runTicket, instruction, Boolean(project.memory_enabled))
464
+ : isBuildTicket
465
+ ? followUpPrompt(runTicket, instruction, Boolean(project.memory_enabled), !options.buildOpenQuestionBatchId)
466
+ : prependSkillPromptBlock(reviewFindingsFollowUpPrompt(runTicket, latestReviewFindings, instruction, Boolean(project.memory_enabled)), runTicket.skills);
467
+ const resumeSessionId = options.providerContinuation === 'fresh' || isPlanMode
468
+ ? null
469
+ : (isBuildTicket || isReviewResume ? ticket.session_id : null);
470
+ const mainRunId = createAgentRun({
471
+ executionRunId: executionContext.runId,
472
+ parentType: 'ticket',
473
+ parentId: ticket.id,
474
+ label: ticket.state === 'review' ? 'Review follow-up' : 'Build follow-up',
475
+ role: 'main',
226
476
  });
227
- if (isBuildTicket && !isPlanMode) {
228
- clearBuildCompletion(ticket.id);
229
- }
230
- const isReviewResume = isReviewTicket && !isPlanMode && Boolean(ticket.session_id) && hasPreviousReviewFollowUp(ticket);
231
- const prompt = isPlanMode
232
- ? questionFollowUpPrompt(ticket, instruction, Boolean(project.memory_enabled))
233
- : isBuildTicket
234
- ? followUpPrompt(ticket, instruction, Boolean(project.memory_enabled), !options.buildOpenQuestionBatchId)
235
- : prependSkillPromptBlock(reviewFindingsFollowUpPrompt(ticket, latestReviewFindings, instruction, Boolean(project.memory_enabled)), ticket.skills);
236
- const resumeSessionId = isPlanMode ? null : (isBuildTicket || isReviewResume ? ticket.session_id : null);
237
- const { response: finalResponse, responseTiming, transcriptRuns } = await runAgent({
238
- ticketId: ticket.id,
239
- tool,
240
- prompt,
241
- cwd,
242
- model: ticket.model,
243
- variant: ticket.variant,
244
- resumeSessionId,
245
- trackBuildProcess: !isPlanMode && (isBuildTicket || isReviewTicket),
246
- emitSuccessUpdate: false,
247
- io,
248
- opencodeAgentMode: isPlanMode ? mode : undefined,
249
- });
250
- const parsedResponse = typeof finalResponse === 'string'
251
- ? parseAgentResponseWithOpenQuestions(finalResponse)
252
- : { response: '', openQuestions: [], hasOpenQuestionsMetadata: false, suggestions: [] };
253
- const openQuestions = options.buildOpenQuestionBatchId
254
- ? filterResolvedBuildOpenQuestionsForBatch(options.buildOpenQuestionBatchId, parsedResponse.openQuestions)
255
- : parsedResponse.openQuestions;
256
- const assistantMessage = isBuildTicket
257
- ? formatAssistantMessageWithOpenQuestions({
258
- response: parsedResponse.response,
259
- openQuestions,
260
- })
261
- : parsedResponse.response.trim();
262
- let assistantMessageId = null;
263
- if (assistantMessage) {
264
- assistantMessageId = createTicketMessage({
477
+ const mainRun = getAgentRun(mainRunId);
478
+ if (!mainRun) {
479
+ throw new Error('Unable to create the follow-up agent run');
480
+ }
481
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
482
+ const { response: finalResponse, responseTiming, transcriptRuns } = await runAgent({
265
483
  ticketId: ticket.id,
266
- role: 'assistant',
267
- kind: 'final_response',
268
- content: assistantMessage,
269
- responseTiming,
270
- transcriptRuns,
484
+ tool,
485
+ prompt,
486
+ cwd,
487
+ model: runTicket.model,
488
+ variant: runTicket.variant,
489
+ resumeSessionId,
490
+ trackBuildProcess: !isPlanMode && (isBuildTicket || isReviewTicket),
491
+ emitSuccessUpdate: false,
492
+ persistSessionIdToTicket: !isPlanMode,
493
+ executionContext,
494
+ runStream: {
495
+ runId: mainRun.id,
496
+ label: mainRun.label,
497
+ role: mainRun.role,
498
+ runIndex: mainRun.run_index,
499
+ },
500
+ io,
501
+ opencodeAgentMode: isPlanMode ? mode : undefined,
271
502
  });
503
+ const parsedResponse = typeof finalResponse === 'string'
504
+ ? parseAgentResponseWithOpenQuestions(finalResponse)
505
+ : { response: '', openQuestions: [], hasOpenQuestionsMetadata: false, suggestions: [] };
506
+ const openQuestions = options.buildOpenQuestionBatchId
507
+ ? filterResolvedBuildOpenQuestionsForBatch(options.buildOpenQuestionBatchId, parsedResponse.openQuestions)
508
+ : parsedResponse.openQuestions;
509
+ const assistantMessage = isBuildTicket
510
+ ? formatAssistantMessageWithOpenQuestions({
511
+ response: parsedResponse.response,
512
+ openQuestions,
513
+ })
514
+ : parsedResponse.response.trim();
515
+ const completedMainRun = getAgentRun(mainRun.id);
516
+ const completionStatus = completedMainRun?.agent_status === 'stopped' ? 'stopped' : 'done';
517
+ const authoritativeResult = withExecutionRunTransaction(transaction => {
518
+ const assistantMessageId = assistantMessage
519
+ ? createTicketMessage({
520
+ ticketId: ticket.id,
521
+ role: 'assistant',
522
+ kind: 'final_response',
523
+ content: assistantMessage,
524
+ responseTiming,
525
+ transcriptRuns,
526
+ })
527
+ : null;
528
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?').run(completionStatus, ticket.id, executionContext.projectId);
529
+ const completedTicket = getTicket(ticket.id) ?? ticket;
530
+ if (isBuildTicket && !isPlanMode && completionStatus === 'done') {
531
+ markBuildCompleted(ticket.id);
532
+ if (parsedResponse.hasOpenQuestionsMetadata) {
533
+ db.prepare('UPDATE tickets SET planning_context_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(JSON.stringify(createTicketOpenQuestionsContext(completedTicket, openQuestions)), ticket.id);
534
+ if (ticket.project_id && ticket.worktree_bundle_id) {
535
+ reconcileBuildOpenQuestions({
536
+ projectId: ticket.project_id,
537
+ bundleId: ticket.worktree_bundle_id,
538
+ ticketId: ticket.id,
539
+ questions: openQuestions,
540
+ sourceKey: `follow-up:${ticket.id}:${assistantMessageId ?? completedTicket.updated_at}`,
541
+ });
542
+ }
543
+ }
544
+ }
545
+ if (assistantMessageId) {
546
+ transaction.addExecutionRunLink({
547
+ projectId: executionContext.projectId,
548
+ runId: executionContext.runId,
549
+ link: {
550
+ entityType: 'ticket_message',
551
+ entityId: assistantMessageId,
552
+ relationship: 'result',
553
+ expectedParent: { entityType: 'ticket', entityId: ticket.id },
554
+ },
555
+ });
556
+ }
557
+ let batchCompleted = false;
558
+ if (options.buildOpenQuestionBatchId) {
559
+ if (completionStatus === 'stopped') {
560
+ failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build open question follow-up was stopped');
561
+ }
562
+ else if (parsedResponse.hasOpenQuestionsMetadata) {
563
+ setBuildOpenQuestionBatchStatus(options.buildOpenQuestionBatchId, 'done');
564
+ batchCompleted = true;
565
+ }
566
+ else {
567
+ failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build follow-up did not return valid Open Questions metadata');
568
+ }
569
+ }
570
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, completionStatus, null);
571
+ transaction.transitionExecutionRun({
572
+ projectId: executionContext.projectId,
573
+ runId: executionContext.runId,
574
+ status: completionStatus === 'stopped' ? 'cancelled' : 'completed',
575
+ domainStatus: completionStatus,
576
+ eventType: completionStatus === 'stopped' ? 'build_follow_up_cancelled' : 'build_follow_up_completed',
577
+ });
578
+ return { assistantMessageId, batchCompleted };
579
+ }, asGenericEmitter(io));
580
+ const assistantMessageId = authoritativeResult.assistantMessageId;
581
+ buildOpenQuestionBatchCompleted = authoritativeResult.batchCompleted;
272
582
  if (assistantMessageId) {
273
583
  bindTicketThinkingMessage(ticket.id, assistantMessageId);
274
584
  }
275
- }
276
- if (transcriptRuns) {
277
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
278
- }
279
- const completedTicket = getTicket(ticket.id) ?? ticket;
280
- if (options.buildOpenQuestionBatchId && completedTicket.agent_status === 'stopped') {
281
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build open question follow-up was stopped');
282
- if (ticket.project_id && ticket.worktree_bundle_id) {
283
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', 'Build open question follow-up was stopped');
585
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
586
+ const completedTicket = getTicket(ticket.id) ?? ticket;
587
+ if (options.buildOpenQuestionBatchId && completedTicket.agent_status === 'stopped') {
588
+ if (ticket.project_id && ticket.worktree_bundle_id && ownedProgressionRunId) {
589
+ waitAutomationProgression({
590
+ projectId: ticket.project_id,
591
+ bundleId: ticket.worktree_bundle_id,
592
+ progressionRunId: ownedProgressionRunId,
593
+ pauseCode: 'open_questions',
594
+ pauseReason: 'Build open question follow-up was stopped',
595
+ reasonCode: 'stage_failed',
596
+ pausedStageRunId: executionContext.runId,
597
+ broadcaster: asGenericEmitter(io),
598
+ });
599
+ }
284
600
  }
285
- }
286
- if (isBuildTicket && !isPlanMode && completedTicket.agent_status === 'done' && parsedResponse.hasOpenQuestionsMetadata) {
287
- db.prepare('UPDATE tickets SET planning_context_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(JSON.stringify(createTicketOpenQuestionsContext(completedTicket, openQuestions)), ticket.id);
288
- if (ticket.project_id && ticket.worktree_bundle_id) {
289
- reconcileBuildOpenQuestions({
601
+ if (completedTicket.agent_status === 'stopped'
602
+ && ticket.project_id
603
+ && ticket.worktree_bundle_id
604
+ && ownedReviewFixRunId
605
+ && ownedReviewProgressionRunId) {
606
+ const reason = 'Automation review fix follow-up was stopped';
607
+ finishAutomationReviewFix({
608
+ projectId: ticket.project_id,
609
+ reviewFixRunId: ownedReviewFixRunId,
610
+ failedMessage: reason,
611
+ broadcaster: asGenericEmitter(io),
612
+ });
613
+ waitReviewFixProgression({
290
614
  projectId: ticket.project_id,
291
615
  bundleId: ticket.worktree_bundle_id,
292
- ticketId: ticket.id,
293
- questions: openQuestions,
294
- sourceKey: `follow-up:${ticket.id}:${assistantMessageId ?? completedTicket.updated_at}`,
616
+ progressionRunId: ownedReviewProgressionRunId,
617
+ pauseCode: 'review',
618
+ pauseReason: reason,
619
+ reasonCode: 'stage_failed',
620
+ pausedStageRunId: executionContext.runId,
621
+ broadcaster: asGenericEmitter(io),
295
622
  });
296
- const bundleOpenQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
297
- const automationConfig = resolveAutomationConfig(completedTicket.bundle ?? null, project);
298
- const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
299
- const shouldPauseForOpenQuestions = (bundleOpenQuestions.length > 0
300
- && automationConfig.pauseOnOpenQuestions
301
- && depthAtLeast(automationConfig.depth, 'auto_review'));
302
- if (shouldPauseForOpenQuestions) {
303
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `${bundleOpenQuestions.length} unresolved Build open question${bundleOpenQuestions.length === 1 ? '' : 's'}`);
304
- }
305
- else if (bundleState?.automation_paused_code === 'open_questions') {
306
- clearAutomationPause(ticket.project_id, ticket.worktree_bundle_id);
307
- }
308
- emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
309
623
  }
310
- }
311
- if (project.memory_enabled && parsedResponse.suggestions.length > 0) {
312
- persistProjectMemorySuggestions(project.id, ticket.id, 'follow_up', parsedResponse.suggestions);
313
- }
314
- clearTicketStreamingResponse(ticket.id);
315
- const completedWithMessage = getTicket(ticket.id);
316
- if (completedWithMessage) {
317
- io.emit('ticket:updated', completedWithMessage);
318
- }
319
- if (!isPlanMode) {
320
- const updated = getTicket(ticket.id);
321
- if (isBuildTicket && updated?.agent_status === 'done') {
322
- markBuildCompleted(ticket.id);
323
- captureStageSuccess(ticket.id, 'build');
324
- const completed = autoParkTicketIfStale(ticket.id);
325
- if (completed) {
326
- io.emit('ticket:updated', completed);
327
- }
624
+ if (isBuildTicket && !isPlanMode && completedTicket.agent_status === 'done' && parsedResponse.hasOpenQuestionsMetadata) {
328
625
  if (ticket.project_id && ticket.worktree_bundle_id) {
329
- if (options.buildOpenQuestionBatchId) {
330
- if (!parsedResponse.hasOpenQuestionsMetadata) {
331
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build follow-up did not return valid Open Questions metadata');
332
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', 'Build follow-up did not return valid Open Questions metadata');
626
+ const bundleOpenQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
627
+ const automationConfig = resolveAutomationConfig(completedTicket.bundle ?? null, project);
628
+ const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
629
+ const shouldPauseForOpenQuestions = (bundleOpenQuestions.length > 0
630
+ && automationConfig.pauseOnOpenQuestions
631
+ && depthAtLeast(automationConfig.depth, 'auto_review'));
632
+ if (shouldPauseForOpenQuestions) {
633
+ recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `${bundleOpenQuestions.length} unresolved Build open question${bundleOpenQuestions.length === 1 ? '' : 's'}`);
634
+ }
635
+ else if (bundleState?.automation_paused_code === 'open_questions') {
636
+ if (bundleState.automation_progression_run_id) {
637
+ runAutomationProgression({
638
+ projectId: ticket.project_id,
639
+ bundleId: ticket.worktree_bundle_id,
640
+ progressionRunId: bundleState.automation_progression_run_id,
641
+ broadcaster: asGenericEmitter(io),
642
+ });
333
643
  }
334
644
  else {
335
- setBuildOpenQuestionBatchStatus(options.buildOpenQuestionBatchId, 'done');
336
- buildOpenQuestionBatchCompleted = true;
337
- }
338
- }
339
- const automationConfig = resolveAutomationConfig(updated.bundle ?? null, project);
340
- const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
341
- const bundleOpenQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
342
- if (options.buildOpenQuestionBatchId) {
343
- const shouldPauseForOpenQuestions = parsedResponse.hasOpenQuestionsMetadata
344
- && bundleOpenQuestions.length > 0
345
- && automationConfig.pauseOnOpenQuestions
346
- && depthAtLeast(automationConfig.depth, 'auto_review');
347
- if (parsedResponse.hasOpenQuestionsMetadata && !shouldPauseForOpenQuestions) {
348
645
  clearAutomationPause(ticket.project_id, ticket.worktree_bundle_id);
349
- if (depthAtLeast(automationConfig.depth, 'auto_review')) {
350
- const reviewResult = transitionBundleToReview(ticket.project_id, ticket.worktree_bundle_id, io, { automatic: true });
351
- if (!reviewResult.ok) {
352
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'review', reviewResult.error);
353
- }
354
- }
355
646
  }
356
- else if (shouldPauseForOpenQuestions) {
357
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `${bundleOpenQuestions.length} unresolved Build open question${bundleOpenQuestions.length === 1 ? '' : 's'}`);
358
- }
359
- }
360
- else if (depthAtLeast(automationConfig.depth, 'auto_review')
361
- && (!bundleState?.automation_paused_reason || bundleState.automation_paused_code === 'open_questions')
362
- && !(bundleOpenQuestions.length > 0 && automationConfig.pauseOnOpenQuestions)) {
363
- transitionBundleToReview(ticket.project_id, ticket.worktree_bundle_id, io, { automatic: true });
364
647
  }
365
648
  emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
366
649
  }
367
650
  }
368
- else if (!isBuildTicket && updated?.agent_status === 'done') {
369
- captureStageSuccess(ticket.id, 'review');
370
- const handled = runReviewFollowUpCompletionAutomation(updated, project, io);
371
- if (!handled) {
651
+ if (project.memory_enabled && parsedResponse.suggestions.length > 0) {
652
+ persistProjectMemorySuggestions(project.id, ticket.id, 'follow_up', parsedResponse.suggestions);
653
+ }
654
+ clearTicketStreamingResponse(ticket.id);
655
+ const completedWithMessage = getTicket(ticket.id);
656
+ if (completedWithMessage) {
657
+ io.emit('ticket:updated', completedWithMessage);
658
+ }
659
+ if (!isPlanMode) {
660
+ const updated = getTicket(ticket.id);
661
+ if (isBuildTicket && updated?.agent_status === 'done') {
662
+ captureStageSuccess(ticket.id, 'build');
372
663
  const completed = autoParkTicketIfStale(ticket.id);
373
664
  if (completed) {
374
665
  io.emit('ticket:updated', completed);
375
666
  }
667
+ if (ticket.project_id && ticket.worktree_bundle_id) {
668
+ if (options.buildOpenQuestionBatchId) {
669
+ if (!parsedResponse.hasOpenQuestionsMetadata) {
670
+ if (ownedProgressionRunId) {
671
+ waitAutomationProgression({
672
+ projectId: ticket.project_id,
673
+ bundleId: ticket.worktree_bundle_id,
674
+ progressionRunId: ownedProgressionRunId,
675
+ pauseCode: 'open_questions',
676
+ pauseReason: 'Build follow-up did not return valid Open Questions metadata',
677
+ reasonCode: 'stage_failed',
678
+ pausedStageRunId: executionContext.runId,
679
+ broadcaster: asGenericEmitter(io),
680
+ });
681
+ }
682
+ }
683
+ }
684
+ const automationConfig = resolveAutomationConfig(updated.bundle ?? null, project);
685
+ const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
686
+ const bundleOpenQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
687
+ if (options.buildOpenQuestionBatchId) {
688
+ const shouldPauseForOpenQuestions = parsedResponse.hasOpenQuestionsMetadata
689
+ && bundleOpenQuestions.length > 0
690
+ && automationConfig.pauseOnOpenQuestions
691
+ && depthAtLeast(automationConfig.depth, 'auto_review');
692
+ if (parsedResponse.hasOpenQuestionsMetadata && !shouldPauseForOpenQuestions) {
693
+ if (!ownedProgressionRunId) {
694
+ clearAutomationPause(ticket.project_id, ticket.worktree_bundle_id);
695
+ }
696
+ if (depthAtLeast(automationConfig.depth, 'auto_review')) {
697
+ const reviewResult = transitionBundleToReview(ticket.project_id, ticket.worktree_bundle_id, io, {
698
+ automatic: true,
699
+ initiation: {
700
+ causalParentRunId: ownedProgressionRunId ?? executionContext.runId,
701
+ workflowContext: options.initiation?.workflowContext ?? null,
702
+ },
703
+ });
704
+ if (!reviewResult.ok) {
705
+ if (ownedProgressionRunId) {
706
+ waitAutomationProgression({
707
+ projectId: ticket.project_id,
708
+ bundleId: ticket.worktree_bundle_id,
709
+ progressionRunId: ownedProgressionRunId,
710
+ pauseCode: 'review',
711
+ pauseReason: reviewResult.error,
712
+ reasonCode: 'stage_failed',
713
+ pausedStageRunId: executionContext.runId,
714
+ broadcaster: asGenericEmitter(io),
715
+ });
716
+ }
717
+ else {
718
+ recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'review', reviewResult.error);
719
+ }
720
+ }
721
+ }
722
+ else if (ownedProgressionRunId) {
723
+ completeAutomationProgression({
724
+ projectId: ticket.project_id,
725
+ bundleId: ticket.worktree_bundle_id,
726
+ progressionRunId: ownedProgressionRunId,
727
+ broadcaster: asGenericEmitter(io),
728
+ });
729
+ }
730
+ }
731
+ else if (shouldPauseForOpenQuestions) {
732
+ if (ownedProgressionRunId) {
733
+ waitAutomationProgression({
734
+ projectId: ticket.project_id,
735
+ bundleId: ticket.worktree_bundle_id,
736
+ progressionRunId: ownedProgressionRunId,
737
+ pauseCode: 'open_questions',
738
+ pauseReason: `${bundleOpenQuestions.length} unresolved Build open question${bundleOpenQuestions.length === 1 ? '' : 's'}`,
739
+ reasonCode: 'precondition_failed',
740
+ pausedStageRunId: null,
741
+ broadcaster: asGenericEmitter(io),
742
+ });
743
+ }
744
+ }
745
+ }
746
+ else if (depthAtLeast(automationConfig.depth, 'auto_review')
747
+ && (!bundleState?.automation_paused_reason || bundleState.automation_paused_code === 'open_questions')
748
+ && !(bundleOpenQuestions.length > 0 && automationConfig.pauseOnOpenQuestions)) {
749
+ const progression = ensureAutomationProgression({
750
+ projectId: ticket.project_id,
751
+ bundleId: ticket.worktree_bundle_id,
752
+ ticketId: ticket.id,
753
+ ticketTitle: ticket.title,
754
+ parentRunId: executionContext.runId,
755
+ workflowContext: options.initiation?.workflowContext ?? null,
756
+ broadcaster: asGenericEmitter(io),
757
+ });
758
+ const reviewResult = transitionBundleToReview(ticket.project_id, ticket.worktree_bundle_id, io, {
759
+ automatic: true,
760
+ initiation: {
761
+ causalParentRunId: progression.runId,
762
+ workflowContext: options.initiation?.workflowContext ?? null,
763
+ },
764
+ });
765
+ if (!reviewResult.ok) {
766
+ waitAutomationProgression({
767
+ projectId: ticket.project_id,
768
+ bundleId: ticket.worktree_bundle_id,
769
+ progressionRunId: progression.runId,
770
+ pauseCode: 'review',
771
+ pauseReason: reviewResult.error,
772
+ reasonCode: 'stage_failed',
773
+ pausedStageRunId: executionContext.runId,
774
+ broadcaster: asGenericEmitter(io),
775
+ });
776
+ }
777
+ }
778
+ emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
779
+ }
780
+ }
781
+ else if (!isBuildTicket && updated?.agent_status === 'done') {
782
+ captureStageSuccess(ticket.id, 'review');
783
+ const handled = runReviewFollowUpCompletionAutomation(updated, project, io, executionContext, options.initiation);
784
+ if (!handled) {
785
+ const completed = autoParkTicketIfStale(ticket.id);
786
+ if (completed) {
787
+ io.emit('ticket:updated', completed);
788
+ }
789
+ }
376
790
  }
377
791
  }
378
792
  }
379
- }
380
- catch (error) {
381
- if (options.buildOpenQuestionBatchId) {
382
- if (ticket.project_id && ticket.worktree_bundle_id) {
383
- if (buildOpenQuestionBatchCompleted) {
384
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'review', `Build open question follow-up completed but automation could not continue: ${getErrorMessage(error)}`);
793
+ catch (error) {
794
+ const executionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
795
+ if (executionRun && !['queued', 'running', 'waiting'].includes(executionRun.status)) {
796
+ if (ticket.project_id && ticket.worktree_bundle_id) {
797
+ if (ownedProgressionRunId) {
798
+ waitAutomationProgression({
799
+ projectId: ticket.project_id,
800
+ bundleId: ticket.worktree_bundle_id,
801
+ progressionRunId: ownedProgressionRunId,
802
+ pauseCode: 'review',
803
+ pauseReason: `Follow-up completed but automation could not continue: ${getErrorMessage(error)}`,
804
+ reasonCode: 'stage_failed',
805
+ pausedStageRunId: executionContext.runId,
806
+ broadcaster: asGenericEmitter(io),
807
+ });
808
+ }
809
+ else if (ownedReviewProgressionRunId) {
810
+ waitReviewFixProgression({
811
+ projectId: ticket.project_id,
812
+ bundleId: ticket.worktree_bundle_id,
813
+ progressionRunId: ownedReviewProgressionRunId,
814
+ pauseCode: 'review',
815
+ pauseReason: `Follow-up completed but automation could not continue: ${getErrorMessage(error)}`,
816
+ reasonCode: 'stage_failed',
817
+ pausedStageRunId: executionContext.runId,
818
+ broadcaster: asGenericEmitter(io),
819
+ });
820
+ }
821
+ else {
822
+ recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'review', `Follow-up completed but automation could not continue: ${getErrorMessage(error)}`);
823
+ }
824
+ emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
385
825
  }
386
- else {
826
+ return;
827
+ }
828
+ if (options.buildOpenQuestionBatchId) {
829
+ if (ticket.project_id && ticket.worktree_bundle_id) {
830
+ if (buildOpenQuestionBatchCompleted) {
831
+ if (ownedProgressionRunId) {
832
+ waitAutomationProgression({
833
+ projectId: ticket.project_id,
834
+ bundleId: ticket.worktree_bundle_id,
835
+ progressionRunId: ownedProgressionRunId,
836
+ pauseCode: 'review',
837
+ pauseReason: `Build open question follow-up completed but automation could not continue: ${getErrorMessage(error)}`,
838
+ reasonCode: 'stage_failed',
839
+ pausedStageRunId: executionContext.runId,
840
+ broadcaster: asGenericEmitter(io),
841
+ });
842
+ }
843
+ }
844
+ else {
845
+ failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, getErrorMessage(error));
846
+ if (ownedProgressionRunId) {
847
+ waitAutomationProgression({
848
+ projectId: ticket.project_id,
849
+ bundleId: ticket.worktree_bundle_id,
850
+ progressionRunId: ownedProgressionRunId,
851
+ pauseCode: 'open_questions',
852
+ pauseReason: `Build open question follow-up failed: ${getErrorMessage(error)}`,
853
+ reasonCode: 'stage_failed',
854
+ pausedStageRunId: executionContext.runId,
855
+ broadcaster: asGenericEmitter(io),
856
+ });
857
+ }
858
+ }
859
+ emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
860
+ }
861
+ else if (!buildOpenQuestionBatchCompleted) {
387
862
  failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, getErrorMessage(error));
388
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `Build open question follow-up failed: ${getErrorMessage(error)}`);
389
863
  }
390
- emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
391
864
  }
392
- else if (!buildOpenQuestionBatchCompleted) {
393
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, getErrorMessage(error));
865
+ const transcriptRuns = getAgentErrorTranscriptRuns(error);
866
+ const errorContent = formatAgentErrorForTranscript(error);
867
+ const message = getErrorMessage(error);
868
+ withExecutionRunTransaction(transaction => {
869
+ if (errorContent) {
870
+ const messageId = createTicketMessage({
871
+ ticketId: ticket.id,
872
+ role: 'assistant',
873
+ kind: 'error',
874
+ content: errorContent,
875
+ transcriptRuns,
876
+ });
877
+ transaction.addExecutionRunLink({
878
+ projectId: executionContext.projectId,
879
+ runId: executionContext.runId,
880
+ link: {
881
+ entityType: 'ticket_message',
882
+ entityId: messageId,
883
+ relationship: 'result',
884
+ expectedParent: { entityType: 'ticket', entityId: ticket.id },
885
+ },
886
+ });
887
+ }
888
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
889
+ .run('error', message, ticket.id, executionContext.projectId);
890
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
891
+ transaction.transitionExecutionRun({
892
+ projectId: executionContext.projectId,
893
+ runId: executionContext.runId,
894
+ status: 'failed',
895
+ domainStatus: 'error',
896
+ failure: { code: 'build_follow_up_failed', message },
897
+ eventType: 'build_follow_up_failed',
898
+ });
899
+ }, asGenericEmitter(io));
900
+ if (ownedReviewFixRunId && ownedReviewProgressionRunId && ticket.project_id && ticket.worktree_bundle_id) {
901
+ finishAutomationReviewFix({
902
+ projectId: ticket.project_id,
903
+ reviewFixRunId: ownedReviewFixRunId,
904
+ failedMessage: message,
905
+ broadcaster: asGenericEmitter(io),
906
+ });
907
+ waitReviewFixProgression({
908
+ projectId: ticket.project_id,
909
+ bundleId: ticket.worktree_bundle_id,
910
+ progressionRunId: ownedReviewProgressionRunId,
911
+ pauseCode: 'review',
912
+ pauseReason: `Automation review fix follow-up failed: ${message}`,
913
+ reasonCode: 'stage_failed',
914
+ pausedStageRunId: executionContext.runId,
915
+ broadcaster: asGenericEmitter(io),
916
+ });
394
917
  }
918
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
919
+ updateStatus(ticket.id, 'error', message, io);
395
920
  }
396
- const transcriptRuns = getAgentErrorTranscriptRuns(error);
397
- const errorContent = formatAgentErrorForTranscript(error);
398
- if (errorContent) {
399
- createTicketMessage({
400
- ticketId: ticket.id,
401
- role: 'assistant',
402
- kind: 'error',
403
- content: errorContent,
404
- transcriptRuns,
405
- });
406
- }
407
- if (transcriptRuns) {
408
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
921
+ finally {
922
+ unregisterBundleActivity(ticket, activityKind);
409
923
  }
410
- updateStatus(ticket.id, 'error', getErrorMessage(error), io);
411
- }
412
- finally {
413
- unregisterBundleActivity(ticket, activityKind);
414
- }
924
+ });
925
+ }
926
+ catch (error) {
927
+ const message = getErrorMessage(error);
928
+ withExecutionRunTransaction(transaction => {
929
+ options.compensate?.();
930
+ transaction.transitionExecutionRun({
931
+ projectId: executionContext.projectId,
932
+ runId: executionContext.runId,
933
+ status: 'failed',
934
+ domainStatus: options.compensationDomainStatus ?? ticket.agent_status ?? 'idle',
935
+ failure: { code: 'queue_failed', message },
936
+ eventType: 'build_follow_up_failed',
937
+ });
938
+ }, asGenericEmitter(io));
939
+ return reject('queue_failed', message);
940
+ }
941
+ queuedJob.catch(() => {
942
+ // The queued callback owns domain and execution failure finalization.
415
943
  });
416
- return true;
417
- };
944
+ return structuredResult ? { accepted: true, executionContext } : true;
945
+ }
418
946
  //# sourceMappingURL=followUp.js.map