@straiffi/archon 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/client/assets/ActivityView-DTEenQas.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +1 -0
  3. package/dist/client/assets/TestsDialog-cBaoffCs.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-CJgwCv5O.js → architectureDiagram-3BPJPVTR-BzzGotac.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM--xjes8FA.js → blockDiagram-GPEHLZMM-DIcqLHKg.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-DHnp82NG.js → c4Diagram-AAUBKEIU-Hc1Sqmhs.js} +1 -1
  7. package/dist/client/assets/channel-Cym17waa.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-DAOyJCKy.js → chunk-2J33WTMH-CWSwjsgk.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-C9YI_tk4.js → chunk-3OPIFGDE-BnX3DljN.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-BW9khkin.js → chunk-5ZQYHXKU-EyyJlI-d.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-DL4t5Hpi.js → chunk-727SXJPM-Ci6CfxWj.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-C4VMKa_O.js → chunk-AQP2D5EJ-RKZNsJTm.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DHP7zrZK.js → chunk-CSCIHK7Q-DBf0Euoj.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-CqZWek3t.js → chunk-KSCS5N6A-BmLQI_2S.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-D3q55pCI.js → chunk-L5ZTLDWV-DKwVAb8G.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-UDY_Lo6v.js → chunk-LZXEDZCA-axotKkbz.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-ZCiOxkYV.js → chunk-ND2GUHAM-BBlJ4nXC.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CuryoBtd.js → chunk-NZK2D7GU-BKJGoZZt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-PnPsz2TB.js → chunk-O5CBEL6O-C-Ghigo1.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-BzOopX-U.js → chunk-WU5MYG2G-CCzVWYcd.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-5LBwQ83I.js → dagre-BM42HDAG-rwIdPcjI.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-_4pckiGR.js → diagram-2AECGRRQ-DJWxOQsg.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CqElv5rJ.js → diagram-5GNKFQAL-C-mQeFtd.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DKv4zS4F.js → diagram-KO2AKTUF-BAu6u4sR.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-iLmTH4UP.js → diagram-LMA3HP47-U6CbvBn0.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-DpFYIa0o.js → diagram-OG6HWLK6-DNTwFcs1.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DnfbvbKs.js → erDiagram-TEJ5UH35-DaHX6Xw5.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-C2MN70Qv.js → flowDiagram-I6XJVG4X-B9hKqL2l.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-yonpliOJ.js → ganttDiagram-6RSMTGT7-BQ1HPHVO.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CaKDWx7y.js → gitGraphDiagram-PVQCEYII-BBRcsmMK.js} +1 -1
  33. package/dist/client/assets/index-CEMaJCPS.js +273 -0
  34. package/dist/client/assets/index-lWftq65U.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-82-8njnx.js → infoDiagram-5YYISTIA-DUoNiSGC.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BMiyLWwK.js → ishikawaDiagram-YF4QCWOH-DJN1y2w8.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-C1qBnR37.js → journeyDiagram-JHISSGLW-BxLLMT70.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-zByEuCzn.js → kanban-definition-UN3LZRKU-BdCPistJ.js} +1 -1
  39. package/dist/client/assets/{line-EpLqyzw7.js → line-4mIgXsZd.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-o124REPN.js → mermaid-parser.core-Pu1QGWJ-.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-DgiAn2-k.js → mermaid.core-DnH9DrgZ.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-UK6V0Bxq.js → mindmap-definition-RKZ34NQL-DqE4ba3C.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-B2s9oGFl.js → pieDiagram-4H26LBE5-Crjy9ejf.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-d5H7-zcc.js → quadrantDiagram-W4KKPZXB-BUSZ2VB7.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DlwN9Vcv.js → requirementDiagram-4Y6WPE33-CBhFKEoV.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-9dGyAxu5.js → sankeyDiagram-5OEKKPKP-CgdbY41v.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DKwRjEym.js → sequenceDiagram-3UESZ5HK-hkp_H7AE.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-D_6cuqsX.js → stateDiagram-AJRCARHV-C6x45RWp.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-ChqD4j8I.js → timeline-definition-PNZ67QCA-CjnZWttQ.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CSAZk0IW.js → vennDiagram-CIIHVFJN-DU5ZyYju.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BUE6IF4q.js → wardleyDiagram-YWT4CUSO-CaCEnCB0.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-MOkC53m_.js → xychartDiagram-2RQKCTM6-Ce3xa37U.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +471 -39
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +1267 -213
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +281 -76
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentRuns.js +205 -38
  62. package/dist/server/lib/agentRuns.js.map +1 -1
  63. package/dist/server/lib/automationProgression.js +464 -0
  64. package/dist/server/lib/automationProgression.js.map +1 -0
  65. package/dist/server/lib/automationReconciliation.js +498 -0
  66. package/dist/server/lib/automationReconciliation.js.map +1 -0
  67. package/dist/server/lib/automationStageDetails.js +232 -0
  68. package/dist/server/lib/automationStageDetails.js.map +1 -0
  69. package/dist/server/lib/buildOpenQuestionOperations.js +223 -0
  70. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
  71. package/dist/server/lib/buildOpenQuestions.js +52 -0
  72. package/dist/server/lib/buildOpenQuestions.js.map +1 -1
  73. package/dist/server/lib/buildSequences.js +130 -46
  74. package/dist/server/lib/buildSequences.js.map +1 -1
  75. package/dist/server/lib/bundleReviewOperations.js +82 -9
  76. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  77. package/dist/server/lib/chatOperations.js +31 -0
  78. package/dist/server/lib/chatOperations.js.map +1 -1
  79. package/dist/server/lib/executionReconciliation.js +518 -0
  80. package/dist/server/lib/executionReconciliation.js.map +1 -0
  81. package/dist/server/lib/executionRunRetention.js +141 -0
  82. package/dist/server/lib/executionRunRetention.js.map +1 -0
  83. package/dist/server/lib/executionRuns.js +1264 -0
  84. package/dist/server/lib/executionRuns.js.map +1 -0
  85. package/dist/server/lib/git.js +385 -6
  86. package/dist/server/lib/git.js.map +1 -1
  87. package/dist/server/lib/integrations/github.js +43 -0
  88. package/dist/server/lib/integrations/github.js.map +1 -1
  89. package/dist/server/lib/integrations/githubActions.js +1 -0
  90. package/dist/server/lib/integrations/githubActions.js.map +1 -1
  91. package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
  92. package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
  93. package/dist/server/lib/projectMemorySelections.js +7 -3
  94. package/dist/server/lib/projectMemorySelections.js.map +1 -1
  95. package/dist/server/lib/shutdown.js +2 -2
  96. package/dist/server/lib/shutdown.js.map +1 -1
  97. package/dist/server/lib/specArtifact.js +3 -0
  98. package/dist/server/lib/specArtifact.js.map +1 -1
  99. package/dist/server/lib/specFormatting.js +1 -1
  100. package/dist/server/lib/specFormatting.js.map +1 -1
  101. package/dist/server/lib/specOperations.js +31 -0
  102. package/dist/server/lib/specOperations.js.map +1 -1
  103. package/dist/server/lib/specReviewContext.js +44 -14
  104. package/dist/server/lib/specReviewContext.js.map +1 -1
  105. package/dist/server/lib/specReviewDraft.js +3 -2
  106. package/dist/server/lib/specReviewDraft.js.map +1 -1
  107. package/dist/server/lib/specReviewOperations.js +44 -7
  108. package/dist/server/lib/specReviewOperations.js.map +1 -1
  109. package/dist/server/lib/specReviewValidation.js +217 -0
  110. package/dist/server/lib/specReviewValidation.js.map +1 -0
  111. package/dist/server/lib/specReviews.js +30 -2
  112. package/dist/server/lib/specReviews.js.map +1 -1
  113. package/dist/server/lib/specs.js +18 -1
  114. package/dist/server/lib/specs.js.map +1 -1
  115. package/dist/server/lib/stageSnapshots.js +197 -0
  116. package/dist/server/lib/stageSnapshots.js.map +1 -0
  117. package/dist/server/lib/testSessions.js +164 -19
  118. package/dist/server/lib/testSessions.js.map +1 -1
  119. package/dist/server/lib/ticketAutomation.js +80 -4
  120. package/dist/server/lib/ticketAutomation.js.map +1 -1
  121. package/dist/server/lib/ticketAutomationCommit.js +466 -91
  122. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  123. package/dist/server/lib/ticketFollowUpOperations.js +214 -20
  124. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  125. package/dist/server/lib/ticketOpenQuestions.js +30 -14
  126. package/dist/server/lib/ticketOpenQuestions.js.map +1 -1
  127. package/dist/server/lib/ticketRunOperations.js +75 -7
  128. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  129. package/dist/server/lib/ticketUndo.js +77 -168
  130. package/dist/server/lib/ticketUndo.js.map +1 -1
  131. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  132. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  133. package/dist/server/lib/tickets.js +46 -29
  134. package/dist/server/lib/tickets.js.map +1 -1
  135. package/dist/server/lib/transcriptRuns.js +3 -3
  136. package/dist/server/lib/transcriptRuns.js.map +1 -1
  137. package/dist/server/lib/turnExecutionErrors.js +12 -0
  138. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  139. package/dist/server/lib/types.js +57 -1
  140. package/dist/server/lib/types.js.map +1 -1
  141. package/dist/server/workers/build.js +596 -184
  142. package/dist/server/workers/build.js.map +1 -1
  143. package/dist/server/workers/chat.js +261 -130
  144. package/dist/server/workers/chat.js.map +1 -1
  145. package/dist/server/workers/followUp.js +720 -195
  146. package/dist/server/workers/followUp.js.map +1 -1
  147. package/dist/server/workers/plan.js +527 -188
  148. package/dist/server/workers/plan.js.map +1 -1
  149. package/dist/server/workers/planFollowUp.js +231 -69
  150. package/dist/server/workers/planFollowUp.js.map +1 -1
  151. package/dist/server/workers/review.js +796 -239
  152. package/dist/server/workers/review.js.map +1 -1
  153. package/dist/server/workers/specReview.js +211 -45
  154. package/dist/server/workers/specReview.js.map +1 -1
  155. package/dist/server/workers/specSession.js +425 -252
  156. package/dist/server/workers/specSession.js.map +1 -1
  157. package/package.json +1 -1
  158. package/dist/client/assets/GitHubActionsDialog-BrUq-zO1.js +0 -1
  159. package/dist/client/assets/TestsDialog-D9N4GD77.js +0 -4
  160. package/dist/client/assets/TicketChangesModal-bYHJSO6d.js +0 -1
  161. package/dist/client/assets/channel-B4kCvLCt.js +0 -1
  162. package/dist/client/assets/classDiagram-4FO5ZUOK-C-eRRNNj.js +0 -1
  163. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B3vxoc73.js +0 -1
  164. package/dist/client/assets/index-9q1GXkUw.js +0 -270
  165. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  166. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BkMKz90T.js +0 -1
@@ -1,8 +1,9 @@
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
- import { failBuildOpenQuestionBatch, listBuildOpenQuestionsForBundle, reconcileBuildOpenQuestions, setBuildOpenQuestionBatchStatus, } from '../lib/buildOpenQuestions.js';
6
+ import { failBuildOpenQuestionBatch, filterResolvedBuildOpenQuestionsForBatch, listBuildOpenQuestionsForBundle, reconcileBuildOpenQuestions, setBuildOpenQuestionBatchStatus, } from '../lib/buildOpenQuestions.js';
6
7
  import { PLAN_MODE_QUESTION_PROMPT_RULES } from '../lib/planModeQuestions.js';
7
8
  import { persistProjectMemorySuggestions } from '../lib/projectMemory.js';
8
9
  import { resolveWorktreePath } from '../lib/git.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,254 +172,760 @@ 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);
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
+ let executionContext = options.executionContext;
313
+ if (!executionContext) {
314
+ const initiation = {
315
+ initiator: 'user',
316
+ causalParentRunId: null,
317
+ retrySourceRunId: null,
318
+ workflowContext: null,
319
+ ...options.initiation,
320
+ };
189
321
  try {
190
- const project = getEffectiveProject(ticket);
191
- if (!project) {
192
- if (options.buildOpenQuestionBatchId) {
193
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Ticket project no longer exists');
194
- if (ticket.project_id && ticket.worktree_bundle_id) {
195
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', 'Build open question follow-up failed: Ticket project no longer exists');
196
- emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
197
- }
198
- }
199
- updateStatus(ticket.id, 'error', 'Ticket project no longer exists', io);
200
- return;
322
+ executionContext = withExecutionRunTransaction(transaction => {
323
+ const executionRun = transaction.createExecutionRun({
324
+ projectId: project.id,
325
+ kind: 'build_follow_up',
326
+ domainStatus: 'queued',
327
+ sourceReference: { kind: 'ticket', id: ticket.id, label: ticket.title },
328
+ links: ticket.worktree_bundle_id
329
+ ? [{ entityType: 'bundle', entityId: ticket.worktree_bundle_id, relationship: 'context' }]
330
+ : [],
331
+ initiatedBy: initiation.initiator,
332
+ parentRunId: initiation.causalParentRunId,
333
+ retryOfRunId: initiation.retrySourceRunId,
334
+ metadata: initiation.workflowContext,
335
+ eventType: 'build_follow_up_created',
336
+ });
337
+ return {
338
+ runId: executionRun.id,
339
+ correlationId: executionRun.correlation_id,
340
+ projectId: executionRun.project_id,
341
+ kind: executionRun.kind,
342
+ };
343
+ }, asGenericEmitter(io));
344
+ }
345
+ catch (error) {
346
+ return reject('invalid_context', getErrorMessage(error));
347
+ }
348
+ }
349
+ let ownedProgressionRunId = null;
350
+ let ownedReviewFixRunId = null;
351
+ let ownedReviewProgressionRunId = null;
352
+ if (options.buildOpenQuestionBatchId) {
353
+ try {
354
+ const executionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
355
+ if (executionRun?.kind !== 'build_follow_up'
356
+ || executionRun.initiated_by !== 'user'
357
+ || executionRun.metadata?.build_open_question_batch_id !== options.buildOpenQuestionBatchId
358
+ || !ticket.worktree_bundle_id) {
359
+ throw new Error('Build open question follow-up ownership is invalid');
201
360
  }
202
- const branch = resolveTicketBranch(ticket);
203
- if (!branch) {
204
- if (options.buildOpenQuestionBatchId) {
205
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Worktree not ready yet');
206
- if (ticket.project_id && ticket.worktree_bundle_id) {
207
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', 'Build open question follow-up failed to start: Worktree not ready yet');
208
- emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
209
- }
361
+ ownedProgressionRunId = resolveProgressionForChild({
362
+ projectId: executionContext.projectId,
363
+ bundleId: ticket.worktree_bundle_id,
364
+ childRunId: executionContext.runId,
365
+ }).id;
366
+ }
367
+ catch (error) {
368
+ return reject('invalid_context', getErrorMessage(error));
369
+ }
370
+ }
371
+ if (!options.buildOpenQuestionBatchId && ticket.worktree_bundle_id) {
372
+ const followUpRun = getExecutionRun(executionContext.projectId, executionContext.runId);
373
+ const parent = followUpRun?.parent_run_id
374
+ ? getExecutionRun(executionContext.projectId, followUpRun.parent_run_id)
375
+ : null;
376
+ if (parent?.kind === 'automation_review_fix') {
377
+ try {
378
+ ownedReviewFixRunId = parent.id;
379
+ ownedReviewProgressionRunId = resolveProgressionForChild({
380
+ projectId: executionContext.projectId,
381
+ bundleId: ticket.worktree_bundle_id,
382
+ childRunId: executionContext.runId,
383
+ allowReviewFixParent: true,
384
+ }).id;
385
+ }
386
+ catch (error) {
387
+ return reject('invalid_context', getErrorMessage(error));
388
+ }
389
+ }
390
+ }
391
+ if (!structuredResult) {
392
+ beginTicketThinkingRun(ticket.id);
393
+ const operation = ticket.state === 'review' && mode !== 'plan' ? 'follow_up' : undefined;
394
+ updateStatus(ticket.id, 'running', null, io, operation);
395
+ }
396
+ const activityKind = ticket.state === 'build' ? 'build_follow_up' : 'review_follow_up';
397
+ let queuedJob;
398
+ try {
399
+ queuedJob = enqueue(jobKey, async () => {
400
+ const scheduledRun = getExecutionRun(executionContext.projectId, executionContext.runId);
401
+ if (!scheduledRun || !['queued', 'running', 'waiting'].includes(scheduledRun.status)) {
402
+ if (options.buildOpenQuestionBatchId
403
+ && scheduledRun
404
+ && ['failed', 'cancelled', 'interrupted'].includes(scheduledRun.status)) {
405
+ failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build open question follow-up was stopped');
210
406
  }
211
- updateStatus(ticket.id, 'error', 'Worktree not ready yet', io);
212
407
  return;
213
408
  }
214
- const cwd = ticket.bundle?.kind === 'project_root' ? project.repo_path : resolveWorktreePath(branch, project);
215
- const tool = resolveTicketTool(ticket, config.tool ?? 'opencode');
216
- const isBuildTicket = ticket.state === 'build';
217
- const isReviewTicket = ticket.state === 'review';
218
- const isPlanMode = mode === 'plan';
219
- const latestReviewFindings = isBuildTicket
220
- ? ''
221
- : getLatestReviewMarkdownForContext({
222
- ticketId: ticket.id,
223
- projectId: ticket.project_id,
224
- worktreeBundleId: ticket.worktree_bundle_id,
409
+ registerBundleActivity(ticket, activityKind);
410
+ let buildOpenQuestionBatchCompleted = false;
411
+ try {
412
+ withExecutionRunTransaction(transaction => {
413
+ const operation = ticket.state === 'review' && mode !== 'plan' ? 'follow_up' : null;
414
+ 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);
415
+ transaction.transitionExecutionRun({
416
+ projectId: executionContext.projectId,
417
+ runId: executionContext.runId,
418
+ status: 'running',
419
+ domainStatus: 'running',
420
+ eventType: 'build_follow_up_started',
421
+ });
422
+ }, asGenericEmitter(io));
423
+ beginTicketThinkingRun(ticket.id);
424
+ updateStatus(ticket.id, 'running', null, io, ticket.state === 'review' && mode !== 'plan' ? 'follow_up' : undefined);
425
+ const project = getEffectiveProject(ticket);
426
+ if (!project) {
427
+ throw new Error('Ticket project no longer exists');
428
+ }
429
+ const branch = resolveTicketBranch(ticket);
430
+ if (!branch) {
431
+ throw new Error('Worktree not ready yet');
432
+ }
433
+ const cwd = ticket.bundle?.kind === 'project_root' ? project.repo_path : resolveWorktreePath(branch, project);
434
+ const tool = resolveTicketTool(ticket, config.tool ?? 'opencode');
435
+ const isBuildTicket = ticket.state === 'build';
436
+ const isReviewTicket = ticket.state === 'review';
437
+ const isPlanMode = mode === 'plan';
438
+ const latestReviewFindings = isBuildTicket
439
+ ? ''
440
+ : getLatestReviewMarkdownForContext({
441
+ ticketId: ticket.id,
442
+ projectId: ticket.project_id,
443
+ worktreeBundleId: ticket.worktree_bundle_id,
444
+ });
445
+ if (isBuildTicket && !isPlanMode) {
446
+ clearBuildCompletion(ticket.id);
447
+ }
448
+ const isReviewResume = isReviewTicket && !isPlanMode && Boolean(ticket.session_id) && hasPreviousReviewFollowUp(ticket);
449
+ const prompt = isPlanMode
450
+ ? questionFollowUpPrompt(ticket, instruction, Boolean(project.memory_enabled))
451
+ : isBuildTicket
452
+ ? followUpPrompt(ticket, instruction, Boolean(project.memory_enabled), !options.buildOpenQuestionBatchId)
453
+ : prependSkillPromptBlock(reviewFindingsFollowUpPrompt(ticket, latestReviewFindings, instruction, Boolean(project.memory_enabled)), ticket.skills);
454
+ const resumeSessionId = isPlanMode ? null : (isBuildTicket || isReviewResume ? ticket.session_id : null);
455
+ const mainRunId = createAgentRun({
456
+ executionRunId: executionContext.runId,
457
+ parentType: 'ticket',
458
+ parentId: ticket.id,
459
+ label: ticket.state === 'review' ? 'Review follow-up' : 'Build follow-up',
460
+ role: 'main',
225
461
  });
226
- if (isBuildTicket && !isPlanMode) {
227
- clearBuildCompletion(ticket.id);
228
- }
229
- const isReviewResume = isReviewTicket && !isPlanMode && Boolean(ticket.session_id) && hasPreviousReviewFollowUp(ticket);
230
- const prompt = isPlanMode
231
- ? questionFollowUpPrompt(ticket, instruction, Boolean(project.memory_enabled))
232
- : isBuildTicket
233
- ? followUpPrompt(ticket, instruction, Boolean(project.memory_enabled), !options.buildOpenQuestionBatchId)
234
- : prependSkillPromptBlock(reviewFindingsFollowUpPrompt(ticket, latestReviewFindings, instruction, Boolean(project.memory_enabled)), ticket.skills);
235
- const resumeSessionId = isPlanMode ? null : (isBuildTicket || isReviewResume ? ticket.session_id : null);
236
- const { response: finalResponse, responseTiming, transcriptRuns } = await runAgent({
237
- ticketId: ticket.id,
238
- tool,
239
- prompt,
240
- cwd,
241
- model: ticket.model,
242
- variant: ticket.variant,
243
- resumeSessionId,
244
- trackBuildProcess: !isPlanMode && (isBuildTicket || isReviewTicket),
245
- emitSuccessUpdate: false,
246
- io,
247
- opencodeAgentMode: isPlanMode ? mode : undefined,
248
- });
249
- const parsedResponse = typeof finalResponse === 'string'
250
- ? parseAgentResponseWithOpenQuestions(finalResponse)
251
- : { response: '', openQuestions: [], hasOpenQuestionsMetadata: false, suggestions: [] };
252
- const assistantMessage = isBuildTicket
253
- ? formatAssistantMessageWithOpenQuestions({
254
- response: parsedResponse.response,
255
- openQuestions: parsedResponse.openQuestions,
256
- })
257
- : parsedResponse.response.trim();
258
- let assistantMessageId = null;
259
- if (assistantMessage) {
260
- assistantMessageId = createTicketMessage({
462
+ const mainRun = getAgentRun(mainRunId);
463
+ if (!mainRun) {
464
+ throw new Error('Unable to create the follow-up agent run');
465
+ }
466
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
467
+ const { response: finalResponse, responseTiming, transcriptRuns } = await runAgent({
261
468
  ticketId: ticket.id,
262
- role: 'assistant',
263
- kind: 'final_response',
264
- content: assistantMessage,
265
- responseTiming,
266
- transcriptRuns,
469
+ tool,
470
+ prompt,
471
+ cwd,
472
+ model: ticket.model,
473
+ variant: ticket.variant,
474
+ resumeSessionId,
475
+ trackBuildProcess: !isPlanMode && (isBuildTicket || isReviewTicket),
476
+ emitSuccessUpdate: false,
477
+ persistSessionIdToTicket: !isPlanMode,
478
+ executionContext,
479
+ runStream: {
480
+ runId: mainRun.id,
481
+ label: mainRun.label,
482
+ role: mainRun.role,
483
+ runIndex: mainRun.run_index,
484
+ },
485
+ io,
486
+ opencodeAgentMode: isPlanMode ? mode : undefined,
267
487
  });
488
+ const parsedResponse = typeof finalResponse === 'string'
489
+ ? parseAgentResponseWithOpenQuestions(finalResponse)
490
+ : { response: '', openQuestions: [], hasOpenQuestionsMetadata: false, suggestions: [] };
491
+ const openQuestions = options.buildOpenQuestionBatchId
492
+ ? filterResolvedBuildOpenQuestionsForBatch(options.buildOpenQuestionBatchId, parsedResponse.openQuestions)
493
+ : parsedResponse.openQuestions;
494
+ const assistantMessage = isBuildTicket
495
+ ? formatAssistantMessageWithOpenQuestions({
496
+ response: parsedResponse.response,
497
+ openQuestions,
498
+ })
499
+ : parsedResponse.response.trim();
500
+ const completedMainRun = getAgentRun(mainRun.id);
501
+ const completionStatus = completedMainRun?.agent_status === 'stopped' ? 'stopped' : 'done';
502
+ const authoritativeResult = withExecutionRunTransaction(transaction => {
503
+ const assistantMessageId = assistantMessage
504
+ ? createTicketMessage({
505
+ ticketId: ticket.id,
506
+ role: 'assistant',
507
+ kind: 'final_response',
508
+ content: assistantMessage,
509
+ responseTiming,
510
+ transcriptRuns,
511
+ })
512
+ : null;
513
+ 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);
514
+ const completedTicket = getTicket(ticket.id) ?? ticket;
515
+ if (isBuildTicket && !isPlanMode && completionStatus === 'done') {
516
+ markBuildCompleted(ticket.id);
517
+ if (parsedResponse.hasOpenQuestionsMetadata) {
518
+ db.prepare('UPDATE tickets SET planning_context_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(JSON.stringify(createTicketOpenQuestionsContext(completedTicket, openQuestions)), ticket.id);
519
+ if (ticket.project_id && ticket.worktree_bundle_id) {
520
+ reconcileBuildOpenQuestions({
521
+ projectId: ticket.project_id,
522
+ bundleId: ticket.worktree_bundle_id,
523
+ ticketId: ticket.id,
524
+ questions: openQuestions,
525
+ sourceKey: `follow-up:${ticket.id}:${assistantMessageId ?? completedTicket.updated_at}`,
526
+ });
527
+ }
528
+ }
529
+ }
530
+ if (assistantMessageId) {
531
+ transaction.addExecutionRunLink({
532
+ projectId: executionContext.projectId,
533
+ runId: executionContext.runId,
534
+ link: {
535
+ entityType: 'ticket_message',
536
+ entityId: assistantMessageId,
537
+ relationship: 'result',
538
+ expectedParent: { entityType: 'ticket', entityId: ticket.id },
539
+ },
540
+ });
541
+ }
542
+ let batchCompleted = false;
543
+ if (options.buildOpenQuestionBatchId) {
544
+ if (completionStatus === 'stopped') {
545
+ failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build open question follow-up was stopped');
546
+ }
547
+ else if (parsedResponse.hasOpenQuestionsMetadata) {
548
+ setBuildOpenQuestionBatchStatus(options.buildOpenQuestionBatchId, 'done');
549
+ batchCompleted = true;
550
+ }
551
+ else {
552
+ failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build follow-up did not return valid Open Questions metadata');
553
+ }
554
+ }
555
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, completionStatus, null);
556
+ transaction.transitionExecutionRun({
557
+ projectId: executionContext.projectId,
558
+ runId: executionContext.runId,
559
+ status: completionStatus === 'stopped' ? 'cancelled' : 'completed',
560
+ domainStatus: completionStatus,
561
+ eventType: completionStatus === 'stopped' ? 'build_follow_up_cancelled' : 'build_follow_up_completed',
562
+ });
563
+ return { assistantMessageId, batchCompleted };
564
+ }, asGenericEmitter(io));
565
+ const assistantMessageId = authoritativeResult.assistantMessageId;
566
+ buildOpenQuestionBatchCompleted = authoritativeResult.batchCompleted;
268
567
  if (assistantMessageId) {
269
568
  bindTicketThinkingMessage(ticket.id, assistantMessageId);
270
569
  }
271
- }
272
- if (transcriptRuns) {
273
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
274
- }
275
- const completedTicket = getTicket(ticket.id) ?? ticket;
276
- if (options.buildOpenQuestionBatchId && completedTicket.agent_status === 'stopped') {
277
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build open question follow-up was stopped');
278
- if (ticket.project_id && ticket.worktree_bundle_id) {
279
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', 'Build open question follow-up was stopped');
570
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
571
+ const completedTicket = getTicket(ticket.id) ?? ticket;
572
+ if (options.buildOpenQuestionBatchId && completedTicket.agent_status === 'stopped') {
573
+ if (ticket.project_id && ticket.worktree_bundle_id && ownedProgressionRunId) {
574
+ waitAutomationProgression({
575
+ projectId: ticket.project_id,
576
+ bundleId: ticket.worktree_bundle_id,
577
+ progressionRunId: ownedProgressionRunId,
578
+ pauseCode: 'open_questions',
579
+ pauseReason: 'Build open question follow-up was stopped',
580
+ reasonCode: 'stage_failed',
581
+ pausedStageRunId: executionContext.runId,
582
+ broadcaster: asGenericEmitter(io),
583
+ });
584
+ }
280
585
  }
281
- }
282
- if (isBuildTicket && !isPlanMode && completedTicket.agent_status === 'done' && parsedResponse.hasOpenQuestionsMetadata) {
283
- db.prepare('UPDATE tickets SET planning_context_json = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(JSON.stringify(createTicketOpenQuestionsContext(completedTicket, parsedResponse.openQuestions)), ticket.id);
284
- if (ticket.project_id && ticket.worktree_bundle_id) {
285
- reconcileBuildOpenQuestions({
586
+ if (completedTicket.agent_status === 'stopped'
587
+ && ticket.project_id
588
+ && ticket.worktree_bundle_id
589
+ && ownedReviewFixRunId
590
+ && ownedReviewProgressionRunId) {
591
+ const reason = 'Automation review fix follow-up was stopped';
592
+ finishAutomationReviewFix({
593
+ projectId: ticket.project_id,
594
+ reviewFixRunId: ownedReviewFixRunId,
595
+ failedMessage: reason,
596
+ broadcaster: asGenericEmitter(io),
597
+ });
598
+ waitReviewFixProgression({
286
599
  projectId: ticket.project_id,
287
600
  bundleId: ticket.worktree_bundle_id,
288
- ticketId: ticket.id,
289
- questions: parsedResponse.openQuestions,
290
- sourceKey: `follow-up:${ticket.id}:${assistantMessageId ?? completedTicket.updated_at}`,
601
+ progressionRunId: ownedReviewProgressionRunId,
602
+ pauseCode: 'review',
603
+ pauseReason: reason,
604
+ reasonCode: 'stage_failed',
605
+ pausedStageRunId: executionContext.runId,
606
+ broadcaster: asGenericEmitter(io),
291
607
  });
292
- const openQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
293
- const automationConfig = resolveAutomationConfig(completedTicket.bundle ?? null, project);
294
- const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
295
- const shouldPauseForOpenQuestions = (openQuestions.length > 0
296
- && automationConfig.pauseOnOpenQuestions
297
- && depthAtLeast(automationConfig.depth, 'auto_review'));
298
- if (shouldPauseForOpenQuestions) {
299
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `${openQuestions.length} unresolved Build open question${openQuestions.length === 1 ? '' : 's'}`);
300
- }
301
- else if (bundleState?.automation_paused_code === 'open_questions') {
302
- clearAutomationPause(ticket.project_id, ticket.worktree_bundle_id);
303
- }
304
- emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
305
608
  }
306
- }
307
- if (project.memory_enabled && parsedResponse.suggestions.length > 0) {
308
- persistProjectMemorySuggestions(project.id, ticket.id, 'follow_up', parsedResponse.suggestions);
309
- }
310
- clearTicketStreamingResponse(ticket.id);
311
- const completedWithMessage = getTicket(ticket.id);
312
- if (completedWithMessage) {
313
- io.emit('ticket:updated', completedWithMessage);
314
- }
315
- if (!isPlanMode) {
316
- const updated = getTicket(ticket.id);
317
- if (isBuildTicket && updated?.agent_status === 'done') {
318
- markBuildCompleted(ticket.id);
319
- captureStageSuccess(ticket.id, 'build');
320
- const completed = autoParkTicketIfStale(ticket.id);
321
- if (completed) {
322
- io.emit('ticket:updated', completed);
323
- }
609
+ if (isBuildTicket && !isPlanMode && completedTicket.agent_status === 'done' && parsedResponse.hasOpenQuestionsMetadata) {
324
610
  if (ticket.project_id && ticket.worktree_bundle_id) {
325
- if (options.buildOpenQuestionBatchId) {
326
- if (!parsedResponse.hasOpenQuestionsMetadata) {
327
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, 'Build follow-up did not return valid Open Questions metadata');
328
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', 'Build follow-up did not return valid Open Questions metadata');
611
+ const bundleOpenQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
612
+ const automationConfig = resolveAutomationConfig(completedTicket.bundle ?? null, project);
613
+ const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
614
+ const shouldPauseForOpenQuestions = (bundleOpenQuestions.length > 0
615
+ && automationConfig.pauseOnOpenQuestions
616
+ && depthAtLeast(automationConfig.depth, 'auto_review'));
617
+ if (shouldPauseForOpenQuestions) {
618
+ recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `${bundleOpenQuestions.length} unresolved Build open question${bundleOpenQuestions.length === 1 ? '' : 's'}`);
619
+ }
620
+ else if (bundleState?.automation_paused_code === 'open_questions') {
621
+ if (bundleState.automation_progression_run_id) {
622
+ runAutomationProgression({
623
+ projectId: ticket.project_id,
624
+ bundleId: ticket.worktree_bundle_id,
625
+ progressionRunId: bundleState.automation_progression_run_id,
626
+ broadcaster: asGenericEmitter(io),
627
+ });
329
628
  }
330
629
  else {
331
- setBuildOpenQuestionBatchStatus(options.buildOpenQuestionBatchId, 'done');
630
+ clearAutomationPause(ticket.project_id, ticket.worktree_bundle_id);
332
631
  }
333
632
  }
334
- const automationConfig = resolveAutomationConfig(updated.bundle ?? null, project);
335
- const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
336
- const openQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
337
- if (options.buildOpenQuestionBatchId) {
338
- const shouldPauseForOpenQuestions = parsedResponse.hasOpenQuestionsMetadata
339
- && openQuestions.length > 0
340
- && automationConfig.pauseOnOpenQuestions
341
- && depthAtLeast(automationConfig.depth, 'auto_review');
342
- if (parsedResponse.hasOpenQuestionsMetadata && !shouldPauseForOpenQuestions) {
343
- clearAutomationPause(ticket.project_id, ticket.worktree_bundle_id);
344
- if (depthAtLeast(automationConfig.depth, 'auto_review')) {
345
- const reviewResult = transitionBundleToReview(ticket.project_id, ticket.worktree_bundle_id, io, { automatic: true });
346
- if (!reviewResult.ok) {
347
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, reviewResult.error);
348
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', reviewResult.error);
633
+ emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
634
+ }
635
+ }
636
+ if (project.memory_enabled && parsedResponse.suggestions.length > 0) {
637
+ persistProjectMemorySuggestions(project.id, ticket.id, 'follow_up', parsedResponse.suggestions);
638
+ }
639
+ clearTicketStreamingResponse(ticket.id);
640
+ const completedWithMessage = getTicket(ticket.id);
641
+ if (completedWithMessage) {
642
+ io.emit('ticket:updated', completedWithMessage);
643
+ }
644
+ if (!isPlanMode) {
645
+ const updated = getTicket(ticket.id);
646
+ if (isBuildTicket && updated?.agent_status === 'done') {
647
+ captureStageSuccess(ticket.id, 'build');
648
+ const completed = autoParkTicketIfStale(ticket.id);
649
+ if (completed) {
650
+ io.emit('ticket:updated', completed);
651
+ }
652
+ if (ticket.project_id && ticket.worktree_bundle_id) {
653
+ if (options.buildOpenQuestionBatchId) {
654
+ if (!parsedResponse.hasOpenQuestionsMetadata) {
655
+ if (ownedProgressionRunId) {
656
+ waitAutomationProgression({
657
+ projectId: ticket.project_id,
658
+ bundleId: ticket.worktree_bundle_id,
659
+ progressionRunId: ownedProgressionRunId,
660
+ pauseCode: 'open_questions',
661
+ pauseReason: 'Build follow-up did not return valid Open Questions metadata',
662
+ reasonCode: 'stage_failed',
663
+ pausedStageRunId: executionContext.runId,
664
+ broadcaster: asGenericEmitter(io),
665
+ });
349
666
  }
350
667
  }
351
668
  }
352
- else if (shouldPauseForOpenQuestions) {
353
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `${openQuestions.length} unresolved Build open question${openQuestions.length === 1 ? '' : 's'}`);
669
+ const automationConfig = resolveAutomationConfig(updated.bundle ?? null, project);
670
+ const bundleState = getBundleAutomationState(ticket.project_id, ticket.worktree_bundle_id);
671
+ const bundleOpenQuestions = listBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id);
672
+ if (options.buildOpenQuestionBatchId) {
673
+ const shouldPauseForOpenQuestions = parsedResponse.hasOpenQuestionsMetadata
674
+ && bundleOpenQuestions.length > 0
675
+ && automationConfig.pauseOnOpenQuestions
676
+ && depthAtLeast(automationConfig.depth, 'auto_review');
677
+ if (parsedResponse.hasOpenQuestionsMetadata && !shouldPauseForOpenQuestions) {
678
+ if (!ownedProgressionRunId) {
679
+ clearAutomationPause(ticket.project_id, ticket.worktree_bundle_id);
680
+ }
681
+ if (depthAtLeast(automationConfig.depth, 'auto_review')) {
682
+ const reviewResult = transitionBundleToReview(ticket.project_id, ticket.worktree_bundle_id, io, {
683
+ automatic: true,
684
+ initiation: {
685
+ causalParentRunId: ownedProgressionRunId ?? executionContext.runId,
686
+ workflowContext: options.initiation?.workflowContext ?? null,
687
+ },
688
+ });
689
+ if (!reviewResult.ok) {
690
+ if (ownedProgressionRunId) {
691
+ waitAutomationProgression({
692
+ projectId: ticket.project_id,
693
+ bundleId: ticket.worktree_bundle_id,
694
+ progressionRunId: ownedProgressionRunId,
695
+ pauseCode: 'review',
696
+ pauseReason: reviewResult.error,
697
+ reasonCode: 'stage_failed',
698
+ pausedStageRunId: executionContext.runId,
699
+ broadcaster: asGenericEmitter(io),
700
+ });
701
+ }
702
+ else {
703
+ recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'review', reviewResult.error);
704
+ }
705
+ }
706
+ }
707
+ else if (ownedProgressionRunId) {
708
+ completeAutomationProgression({
709
+ projectId: ticket.project_id,
710
+ bundleId: ticket.worktree_bundle_id,
711
+ progressionRunId: ownedProgressionRunId,
712
+ broadcaster: asGenericEmitter(io),
713
+ });
714
+ }
715
+ }
716
+ else if (shouldPauseForOpenQuestions) {
717
+ if (ownedProgressionRunId) {
718
+ waitAutomationProgression({
719
+ projectId: ticket.project_id,
720
+ bundleId: ticket.worktree_bundle_id,
721
+ progressionRunId: ownedProgressionRunId,
722
+ pauseCode: 'open_questions',
723
+ pauseReason: `${bundleOpenQuestions.length} unresolved Build open question${bundleOpenQuestions.length === 1 ? '' : 's'}`,
724
+ reasonCode: 'precondition_failed',
725
+ pausedStageRunId: null,
726
+ broadcaster: asGenericEmitter(io),
727
+ });
728
+ }
729
+ }
730
+ }
731
+ else if (depthAtLeast(automationConfig.depth, 'auto_review')
732
+ && (!bundleState?.automation_paused_reason || bundleState.automation_paused_code === 'open_questions')
733
+ && !(bundleOpenQuestions.length > 0 && automationConfig.pauseOnOpenQuestions)) {
734
+ const progression = ensureAutomationProgression({
735
+ projectId: ticket.project_id,
736
+ bundleId: ticket.worktree_bundle_id,
737
+ ticketId: ticket.id,
738
+ ticketTitle: ticket.title,
739
+ parentRunId: executionContext.runId,
740
+ workflowContext: options.initiation?.workflowContext ?? null,
741
+ broadcaster: asGenericEmitter(io),
742
+ });
743
+ const reviewResult = transitionBundleToReview(ticket.project_id, ticket.worktree_bundle_id, io, {
744
+ automatic: true,
745
+ initiation: {
746
+ causalParentRunId: progression.runId,
747
+ workflowContext: options.initiation?.workflowContext ?? null,
748
+ },
749
+ });
750
+ if (!reviewResult.ok) {
751
+ waitAutomationProgression({
752
+ projectId: ticket.project_id,
753
+ bundleId: ticket.worktree_bundle_id,
754
+ progressionRunId: progression.runId,
755
+ pauseCode: 'review',
756
+ pauseReason: reviewResult.error,
757
+ reasonCode: 'stage_failed',
758
+ pausedStageRunId: executionContext.runId,
759
+ broadcaster: asGenericEmitter(io),
760
+ });
761
+ }
762
+ }
763
+ emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
764
+ }
765
+ }
766
+ else if (!isBuildTicket && updated?.agent_status === 'done') {
767
+ captureStageSuccess(ticket.id, 'review');
768
+ const handled = runReviewFollowUpCompletionAutomation(updated, project, io, executionContext, options.initiation);
769
+ if (!handled) {
770
+ const completed = autoParkTicketIfStale(ticket.id);
771
+ if (completed) {
772
+ io.emit('ticket:updated', completed);
354
773
  }
355
774
  }
356
- else if (depthAtLeast(automationConfig.depth, 'auto_review')
357
- && (!bundleState?.automation_paused_reason || bundleState.automation_paused_code === 'open_questions')
358
- && !(openQuestions.length > 0 && automationConfig.pauseOnOpenQuestions)) {
359
- transitionBundleToReview(ticket.project_id, ticket.worktree_bundle_id, io, { automatic: true });
775
+ }
776
+ }
777
+ }
778
+ catch (error) {
779
+ const executionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
780
+ if (executionRun && !['queued', 'running', 'waiting'].includes(executionRun.status)) {
781
+ if (ticket.project_id && ticket.worktree_bundle_id) {
782
+ if (ownedProgressionRunId) {
783
+ waitAutomationProgression({
784
+ projectId: ticket.project_id,
785
+ bundleId: ticket.worktree_bundle_id,
786
+ progressionRunId: ownedProgressionRunId,
787
+ pauseCode: 'review',
788
+ pauseReason: `Follow-up completed but automation could not continue: ${getErrorMessage(error)}`,
789
+ reasonCode: 'stage_failed',
790
+ pausedStageRunId: executionContext.runId,
791
+ broadcaster: asGenericEmitter(io),
792
+ });
793
+ }
794
+ else if (ownedReviewProgressionRunId) {
795
+ waitReviewFixProgression({
796
+ projectId: ticket.project_id,
797
+ bundleId: ticket.worktree_bundle_id,
798
+ progressionRunId: ownedReviewProgressionRunId,
799
+ pauseCode: 'review',
800
+ pauseReason: `Follow-up completed but automation could not continue: ${getErrorMessage(error)}`,
801
+ reasonCode: 'stage_failed',
802
+ pausedStageRunId: executionContext.runId,
803
+ broadcaster: asGenericEmitter(io),
804
+ });
805
+ }
806
+ else {
807
+ recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'review', `Follow-up completed but automation could not continue: ${getErrorMessage(error)}`);
360
808
  }
361
809
  emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
362
810
  }
811
+ return;
363
812
  }
364
- else if (!isBuildTicket && updated?.agent_status === 'done') {
365
- captureStageSuccess(ticket.id, 'review');
366
- const handled = runReviewFollowUpCompletionAutomation(updated, project, io);
367
- if (!handled) {
368
- const completed = autoParkTicketIfStale(ticket.id);
369
- if (completed) {
370
- io.emit('ticket:updated', completed);
813
+ if (options.buildOpenQuestionBatchId) {
814
+ if (ticket.project_id && ticket.worktree_bundle_id) {
815
+ if (buildOpenQuestionBatchCompleted) {
816
+ if (ownedProgressionRunId) {
817
+ waitAutomationProgression({
818
+ projectId: ticket.project_id,
819
+ bundleId: ticket.worktree_bundle_id,
820
+ progressionRunId: ownedProgressionRunId,
821
+ pauseCode: 'review',
822
+ pauseReason: `Build open question follow-up completed but automation could not continue: ${getErrorMessage(error)}`,
823
+ reasonCode: 'stage_failed',
824
+ pausedStageRunId: executionContext.runId,
825
+ broadcaster: asGenericEmitter(io),
826
+ });
827
+ }
371
828
  }
829
+ else {
830
+ failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, getErrorMessage(error));
831
+ if (ownedProgressionRunId) {
832
+ waitAutomationProgression({
833
+ projectId: ticket.project_id,
834
+ bundleId: ticket.worktree_bundle_id,
835
+ progressionRunId: ownedProgressionRunId,
836
+ pauseCode: 'open_questions',
837
+ pauseReason: `Build open question follow-up failed: ${getErrorMessage(error)}`,
838
+ reasonCode: 'stage_failed',
839
+ pausedStageRunId: executionContext.runId,
840
+ broadcaster: asGenericEmitter(io),
841
+ });
842
+ }
843
+ }
844
+ emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
845
+ }
846
+ else if (!buildOpenQuestionBatchCompleted) {
847
+ failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, getErrorMessage(error));
372
848
  }
373
849
  }
374
- }
375
- }
376
- catch (error) {
377
- if (options.buildOpenQuestionBatchId) {
378
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, getErrorMessage(error));
379
- if (ticket.project_id && ticket.worktree_bundle_id) {
380
- recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'open_questions', `Build open question follow-up failed: ${getErrorMessage(error)}`);
381
- emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
850
+ const transcriptRuns = getAgentErrorTranscriptRuns(error);
851
+ const errorContent = formatAgentErrorForTranscript(error);
852
+ const message = getErrorMessage(error);
853
+ withExecutionRunTransaction(transaction => {
854
+ if (errorContent) {
855
+ const messageId = createTicketMessage({
856
+ ticketId: ticket.id,
857
+ role: 'assistant',
858
+ kind: 'error',
859
+ content: errorContent,
860
+ transcriptRuns,
861
+ });
862
+ transaction.addExecutionRunLink({
863
+ projectId: executionContext.projectId,
864
+ runId: executionContext.runId,
865
+ link: {
866
+ entityType: 'ticket_message',
867
+ entityId: messageId,
868
+ relationship: 'result',
869
+ expectedParent: { entityType: 'ticket', entityId: ticket.id },
870
+ },
871
+ });
872
+ }
873
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
874
+ .run('error', message, ticket.id, executionContext.projectId);
875
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
876
+ transaction.transitionExecutionRun({
877
+ projectId: executionContext.projectId,
878
+ runId: executionContext.runId,
879
+ status: 'failed',
880
+ domainStatus: 'error',
881
+ failure: { code: 'build_follow_up_failed', message },
882
+ eventType: 'build_follow_up_failed',
883
+ });
884
+ }, asGenericEmitter(io));
885
+ if (ownedReviewFixRunId && ownedReviewProgressionRunId && ticket.project_id && ticket.worktree_bundle_id) {
886
+ finishAutomationReviewFix({
887
+ projectId: ticket.project_id,
888
+ reviewFixRunId: ownedReviewFixRunId,
889
+ failedMessage: message,
890
+ broadcaster: asGenericEmitter(io),
891
+ });
892
+ waitReviewFixProgression({
893
+ projectId: ticket.project_id,
894
+ bundleId: ticket.worktree_bundle_id,
895
+ progressionRunId: ownedReviewProgressionRunId,
896
+ pauseCode: 'review',
897
+ pauseReason: `Automation review fix follow-up failed: ${message}`,
898
+ reasonCode: 'stage_failed',
899
+ pausedStageRunId: executionContext.runId,
900
+ broadcaster: asGenericEmitter(io),
901
+ });
382
902
  }
903
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
904
+ updateStatus(ticket.id, 'error', message, io);
383
905
  }
384
- const transcriptRuns = getAgentErrorTranscriptRuns(error);
385
- const errorContent = formatAgentErrorForTranscript(error);
386
- if (errorContent) {
387
- createTicketMessage({
388
- ticketId: ticket.id,
389
- role: 'assistant',
390
- kind: 'error',
391
- content: errorContent,
392
- transcriptRuns,
393
- });
394
- }
395
- if (transcriptRuns) {
396
- clearAndEmitAgentRunsForParent(io, 'ticket', ticket.id);
906
+ finally {
907
+ unregisterBundleActivity(ticket, activityKind);
397
908
  }
398
- updateStatus(ticket.id, 'error', getErrorMessage(error), io);
399
- }
400
- finally {
401
- unregisterBundleActivity(ticket, activityKind);
402
- }
909
+ });
910
+ }
911
+ catch (error) {
912
+ const message = getErrorMessage(error);
913
+ withExecutionRunTransaction(transaction => {
914
+ options.compensate?.();
915
+ transaction.transitionExecutionRun({
916
+ projectId: executionContext.projectId,
917
+ runId: executionContext.runId,
918
+ status: 'failed',
919
+ domainStatus: options.compensationDomainStatus ?? ticket.agent_status ?? 'idle',
920
+ failure: { code: 'queue_failed', message },
921
+ eventType: 'build_follow_up_failed',
922
+ });
923
+ }, asGenericEmitter(io));
924
+ return reject('queue_failed', message);
925
+ }
926
+ queuedJob.catch(() => {
927
+ // The queued callback owns domain and execution failure finalization.
403
928
  });
404
- return true;
405
- };
929
+ return structuredResult ? { accepted: true, executionContext } : true;
930
+ }
406
931
  //# sourceMappingURL=followUp.js.map