@straiffi/archon 1.7.2 → 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 (164) 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-Do9nbC8v.js → architectureDiagram-3BPJPVTR-BzzGotac.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-DGGOLHF8.js → blockDiagram-GPEHLZMM-DIcqLHKg.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-Bz7tQybs.js → c4Diagram-AAUBKEIU-Hc1Sqmhs.js} +1 -1
  7. package/dist/client/assets/channel-Cym17waa.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-CJO-GmWJ.js → chunk-2J33WTMH-CWSwjsgk.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-DPAVagpC.js → chunk-3OPIFGDE-BnX3DljN.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-Cof75Mu7.js → chunk-5ZQYHXKU-EyyJlI-d.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-COuPqQ8_.js → chunk-727SXJPM-Ci6CfxWj.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-CFj5KabW.js → chunk-AQP2D5EJ-RKZNsJTm.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-C4M6w5cT.js → chunk-CSCIHK7Q-DBf0Euoj.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-ByaiwVue.js → chunk-KSCS5N6A-BmLQI_2S.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-Cx5jeot9.js → chunk-L5ZTLDWV-DKwVAb8G.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-DmGtztud.js → chunk-LZXEDZCA-axotKkbz.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-i3yuspqL.js → chunk-ND2GUHAM-BBlJ4nXC.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CQ4E_jP4.js → chunk-NZK2D7GU-BKJGoZZt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-D2g4VDCd.js → chunk-O5CBEL6O-C-Ghigo1.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-exIu-zQ2.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-DvF75yLZ.js → dagre-BM42HDAG-rwIdPcjI.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-DFv0HxLL.js → diagram-2AECGRRQ-DJWxOQsg.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-C_LRir0-.js → diagram-5GNKFQAL-C-mQeFtd.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DGZxRDRy.js → diagram-KO2AKTUF-BAu6u4sR.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-UXt0uti4.js → diagram-LMA3HP47-U6CbvBn0.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-CFbZS4n5.js → diagram-OG6HWLK6-DNTwFcs1.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-CfAwZDdf.js → erDiagram-TEJ5UH35-DaHX6Xw5.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-Cdgi5z3n.js → flowDiagram-I6XJVG4X-B9hKqL2l.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-DOrkFkhu.js → ganttDiagram-6RSMTGT7-BQ1HPHVO.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-cEox0peD.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-gm51u2Nn.js → infoDiagram-5YYISTIA-DUoNiSGC.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CjnoOIja.js → ishikawaDiagram-YF4QCWOH-DJN1y2w8.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-Dlg4DsiO.js → journeyDiagram-JHISSGLW-BxLLMT70.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-BU0XZaSd.js → kanban-definition-UN3LZRKU-BdCPistJ.js} +1 -1
  39. package/dist/client/assets/{line-Bert22-_.js → line-4mIgXsZd.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-BvQSGDmL.js → mermaid-parser.core-Pu1QGWJ-.js} +1 -1
  41. package/dist/client/assets/{mermaid.core--qSAHJRj.js → mermaid.core-DnH9DrgZ.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgFIvWWD.js → mindmap-definition-RKZ34NQL-DqE4ba3C.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-DGcbXD0M.js → pieDiagram-4H26LBE5-Crjy9ejf.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-ClkBUeia.js → quadrantDiagram-W4KKPZXB-BUSZ2VB7.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-B8CwGToU.js → requirementDiagram-4Y6WPE33-CBhFKEoV.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DTk3jL6B.js → sankeyDiagram-5OEKKPKP-CgdbY41v.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQVrcnKf.js → sequenceDiagram-3UESZ5HK-hkp_H7AE.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-DD4yMZPB.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-BnCBVqcd.js → timeline-definition-PNZ67QCA-CjnZWttQ.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-BDmLMNXS.js → vennDiagram-CIIHVFJN-DU5ZyYju.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DSLuQiEv.js → wardleyDiagram-YWT4CUSO-CaCEnCB0.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DAq_LA4r.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 +1256 -214
  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/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 +44 -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/ticketRunOperations.js +51 -6
  126. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  127. package/dist/server/lib/ticketUndo.js +77 -168
  128. package/dist/server/lib/ticketUndo.js.map +1 -1
  129. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  130. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  131. package/dist/server/lib/tickets.js +41 -27
  132. package/dist/server/lib/tickets.js.map +1 -1
  133. package/dist/server/lib/transcriptRuns.js +3 -3
  134. package/dist/server/lib/transcriptRuns.js.map +1 -1
  135. package/dist/server/lib/turnExecutionErrors.js +12 -0
  136. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  137. package/dist/server/lib/types.js +57 -1
  138. package/dist/server/lib/types.js.map +1 -1
  139. package/dist/server/workers/build.js +596 -184
  140. package/dist/server/workers/build.js.map +1 -1
  141. package/dist/server/workers/chat.js +261 -130
  142. package/dist/server/workers/chat.js.map +1 -1
  143. package/dist/server/workers/followUp.js +716 -203
  144. package/dist/server/workers/followUp.js.map +1 -1
  145. package/dist/server/workers/plan.js +527 -188
  146. package/dist/server/workers/plan.js.map +1 -1
  147. package/dist/server/workers/planFollowUp.js +231 -69
  148. package/dist/server/workers/planFollowUp.js.map +1 -1
  149. package/dist/server/workers/review.js +790 -297
  150. package/dist/server/workers/review.js.map +1 -1
  151. package/dist/server/workers/specReview.js +211 -45
  152. package/dist/server/workers/specReview.js.map +1 -1
  153. package/dist/server/workers/specSession.js +425 -252
  154. package/dist/server/workers/specSession.js.map +1 -1
  155. package/package.json +1 -1
  156. package/dist/client/assets/GitHubActionsDialog-DAD5REmY.js +0 -1
  157. package/dist/client/assets/TestsDialog-B5NVrroq.js +0 -4
  158. package/dist/client/assets/TicketChangesModal-By-_gTGd.js +0 -1
  159. package/dist/client/assets/channel-Dd5Len0X.js +0 -1
  160. package/dist/client/assets/classDiagram-4FO5ZUOK-B7KUg6r8.js +0 -1
  161. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CQm-z5c4.js +0 -1
  162. package/dist/client/assets/index-BHq6-Eir.js +0 -270
  163. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  164. 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,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);
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
+ 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
+ };
190
321
  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;
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');
202
360
  }
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
- }
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');
211
406
  }
212
- updateStatus(ticket.id, 'error', 'Worktree not ready yet', io);
213
407
  return;
214
408
  }
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,
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',
226
461
  });
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({
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({
265
468
  ticketId: ticket.id,
266
- role: 'assistant',
267
- kind: 'final_response',
268
- content: assistantMessage,
269
- responseTiming,
270
- 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,
271
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;
272
567
  if (assistantMessageId) {
273
568
  bindTicketThinkingMessage(ticket.id, assistantMessageId);
274
569
  }
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');
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
+ }
284
585
  }
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({
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({
290
599
  projectId: ticket.project_id,
291
600
  bundleId: ticket.worktree_bundle_id,
292
- ticketId: ticket.id,
293
- questions: openQuestions,
294
- 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),
295
607
  });
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
608
  }
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
- }
609
+ if (isBuildTicket && !isPlanMode && completedTicket.agent_status === 'done' && parsedResponse.hasOpenQuestionsMetadata) {
328
610
  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');
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
+ });
333
628
  }
334
629
  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
630
  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
631
  }
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
632
  }
365
633
  emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
366
634
  }
367
635
  }
368
- else if (!isBuildTicket && updated?.agent_status === 'done') {
369
- captureStageSuccess(ticket.id, 'review');
370
- const handled = runReviewFollowUpCompletionAutomation(updated, project, io);
371
- if (!handled) {
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');
372
648
  const completed = autoParkTicketIfStale(ticket.id);
373
649
  if (completed) {
374
650
  io.emit('ticket:updated', completed);
375
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
+ });
666
+ }
667
+ }
668
+ }
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);
773
+ }
774
+ }
376
775
  }
377
776
  }
378
777
  }
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)}`);
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)}`);
808
+ }
809
+ emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
385
810
  }
386
- else {
811
+ return;
812
+ }
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
+ }
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) {
387
847
  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
848
  }
390
- emitBundleTickets(ticket.project_id, ticket.worktree_bundle_id, io);
391
849
  }
392
- else if (!buildOpenQuestionBatchCompleted) {
393
- failBuildOpenQuestionBatch(options.buildOpenQuestionBatchId, getErrorMessage(error));
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
+ });
394
902
  }
903
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
904
+ updateStatus(ticket.id, 'error', message, io);
395
905
  }
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);
906
+ finally {
907
+ unregisterBundleActivity(ticket, activityKind);
409
908
  }
410
- updateStatus(ticket.id, 'error', getErrorMessage(error), io);
411
- }
412
- finally {
413
- unregisterBundleActivity(ticket, activityKind);
414
- }
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.
415
928
  });
416
- return true;
417
- };
929
+ return structuredResult ? { accepted: true, executionContext } : true;
930
+ }
418
931
  //# sourceMappingURL=followUp.js.map