@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
@@ -7,6 +7,7 @@ import { getProjectMemoryPromptBlock } from '../lib/projectMemoryPrompt.js';
7
7
  import { extractProjectMemorySuggestionsFromOutput, getProjectMemorySuggestionPromptRules } from '../lib/projectMemorySuggestions.js';
8
8
  import { getEffectiveProject } from '../lib/projects.js';
9
9
  import { enqueue, isRunning } from '../lib/queue.js';
10
+ import { getExecutionRun, transitionExecutionRun, withExecutionRunTransaction } from '../lib/executionRuns.js';
10
11
  import { buildReviewContext } from '../lib/reviewContext.js';
11
12
  import { computeDiffSignature, createReviewRun, hasPriorReviewRunForBundle, insertReviewFindings, parseStructuredReviewOutput, } from '../lib/reviewFindings.js';
12
13
  import { buildSpecCoverageContext } from '../lib/specCoverageContext.js';
@@ -18,16 +19,29 @@ import { clearTicketStreamingResponse, createTicketMessage, markTicketDescriptio
18
19
  import { captureStageBaseline, captureStageSuccess } from '../lib/ticketUndo.js';
19
20
  import { beginTicketThinkingRun, bindTicketThinkingMessage } from '../lib/thinkingState.js';
20
21
  import { autoParkTicketIfStale, getTicket, resolveTicketBranch, resolveTicketTool, resolveTicketWorkerKey } from '../lib/tickets.js';
21
- import { clearAgentRunsForParent, createAgentRun, emitAgentRunsUpdated, listAgentRunsForParent, } from '../lib/agentRuns.js';
22
+ import { createAgentRun, emitAgentRunsUpdated, getAgentRun, terminalizeAgentRunsForExecutionRun, } from '../lib/agentRuns.js';
22
23
  import { buildAutomationFixInstruction, buildAutomationFixStatusMessage, decideReviewAutomation, depthAtLeast, getBundleAutomationState, recordAutomationPause, resetBundleAutomationLoop, resolveAutomationConfig, upsertBundleAutomationState, } from '../lib/ticketAutomation.js';
23
24
  import { runAutomationCommitChain } from '../lib/ticketAutomationCommit.js';
24
- import { startFollowUp } from './followUp.js';
25
+ import { submitTicketFollowUp } from '../lib/ticketFollowUpOperations.js';
26
+ import { completeAutomationProgression, createAutomationReviewFix, ensureAutomationProgression, finishAutomationReviewFix, resolveProgressionForChild, waitAutomationProgression, } from '../lib/automationProgression.js';
25
27
  import { formatBuildOpenQuestionAnswersPrompt, listAnsweredBuildOpenQuestionsForBundle } from '../lib/buildOpenQuestions.js';
26
28
  const getErrorMessage = (error) => {
27
29
  return error instanceof Error ? error.message : String(error);
28
30
  };
29
- const updateStatus = (ticketId, status, log, io) => {
30
- db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, log ?? null, ticketId);
31
+ const defaultInitiation = {
32
+ initiator: 'user',
33
+ causalParentRunId: null,
34
+ retrySourceRunId: null,
35
+ workflowContext: null,
36
+ };
37
+ const asGenericEmitter = (io) => io;
38
+ const updateStatus = (ticketId, status, log, io, operation) => {
39
+ if (operation) {
40
+ 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);
41
+ }
42
+ else {
43
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, log ?? null, ticketId);
44
+ }
31
45
  const updatedTicket = getTicket(ticketId);
32
46
  if (updatedTicket?.worktree_bundle_id && updatedTicket.state !== 'plan') {
33
47
  markTicketDescriptionVisibleInBundle(ticketId);
@@ -43,8 +57,48 @@ const stripSystemReminderBlocks = (output) => {
43
57
  const normalizeReviewOutput = (output) => {
44
58
  return extractProjectMemorySuggestionsFromOutput(stripSystemReminderBlocks(output));
45
59
  };
60
+ const resolveStructuredReviewOutput = (parentResponse, transcriptRuns) => {
61
+ const normalizedParent = typeof parentResponse === 'string'
62
+ ? normalizeReviewOutput(parentResponse)
63
+ : { cleanedOutput: '', suggestions: [] };
64
+ const parsedParent = normalizedParent.cleanedOutput
65
+ ? parseStructuredReviewOutput(normalizedParent.cleanedOutput)
66
+ : null;
67
+ if (parsedParent?.status === 'completed') {
68
+ return { normalizedParent, parsed: parsedParent };
69
+ }
70
+ const parsedSubagentOutputs = transcriptRuns.flatMap(run => {
71
+ if (run.role !== 'subagent' || run.agent_status !== 'done' || !run.response_text?.trim()) {
72
+ return [];
73
+ }
74
+ const normalized = normalizeReviewOutput(run.response_text);
75
+ if (!normalized.cleanedOutput) {
76
+ return [];
77
+ }
78
+ const parsed = parseStructuredReviewOutput(normalized.cleanedOutput);
79
+ return parsed.status === 'completed' ? [parsed] : [];
80
+ });
81
+ if (parsedSubagentOutputs.length === 0) {
82
+ return { normalizedParent, parsed: parsedParent };
83
+ }
84
+ const findings = parsedSubagentOutputs
85
+ .flatMap(output => output.findings)
86
+ .map((finding, index) => ({ ...finding, ordinal: index + 1 }));
87
+ return {
88
+ normalizedParent,
89
+ parsed: {
90
+ cleanedMarkdown: parsedSubagentOutputs
91
+ .map(output => output.cleanedMarkdown)
92
+ .filter(Boolean)
93
+ .join('\n\n'),
94
+ findings,
95
+ status: 'completed',
96
+ },
97
+ };
98
+ };
46
99
  const MAX_INLINE_DIFF_LINES = 400;
47
100
  const MAX_INLINE_DIFF_CHARS = 20_000;
101
+ const SUBAGENT_REVIEW_GUIDANCE = 'If you delegate any part of the review to subagents, you remain responsible for evaluating their results and consolidating every accepted finding into your final response using the exact format below.';
48
102
  const countLines = (value) => {
49
103
  if (!value) {
50
104
  return 0;
@@ -98,6 +152,8 @@ Before writing findings, perform this investigation workflow:
98
152
  4. Check whether the change is consistent with the project's accepted decisions and conventions.
99
153
  5. Only then report genuine issues that are supported by the code you inspected.
100
154
 
155
+ ${SUBAGENT_REVIEW_GUIDANCE}
156
+
101
157
  Do not report that you performed these steps. Do not include summaries, praise, or implementation advice beyond the requested findings format.
102
158
 
103
159
  ${projectMemory ? `${projectMemory}
@@ -157,6 +213,8 @@ Before writing findings, perform this investigation workflow:
157
213
  3. Compare the implementation against the spec_context block: are all acceptance criteria satisfied, are listed edge cases handled, and does the change stay within scope?
158
214
  4. Only then report genuine conformance gaps that are supported by the code you inspected.
159
215
 
216
+ ${SUBAGENT_REVIEW_GUIDANCE}
217
+
160
218
  Do not report that you performed these steps. Do not include summaries, praise, or implementation advice beyond the requested findings format.
161
219
 
162
220
  ${projectMemory ? `${projectMemory}
@@ -197,22 +255,51 @@ Use an empty findings array when there are no genuine issues. The JSON must be v
197
255
 
198
256
  Do not include planning text, implementation steps, or commentary about changing modes.`, ticket.skills);
199
257
  };
200
- const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parsedStatus, findings, io) => {
258
+ const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parsedStatus, findings, io, executionContext, initiation) => {
201
259
  if (!ticket.worktree_bundle_id || !ticket.project_id) {
202
260
  return false;
203
261
  }
204
262
  const projectId = ticket.project_id;
205
263
  const bundleId = ticket.worktree_bundle_id;
206
- const config = resolveAutomationConfig(ticket.bundle ?? null, project);
264
+ const liveTicket = getTicket(ticket.id) ?? ticket;
265
+ const liveProject = getEffectiveProject(liveTicket) ?? project;
266
+ const config = resolveAutomationConfig(liveTicket.bundle ?? null, liveProject);
207
267
  const bundleState = getBundleAutomationState(projectId, bundleId);
208
268
  const decision = decideReviewAutomation(config, bundleState, reviewRunId, parsedStatus, findings);
269
+ const progression = initiation.initiator === 'automation'
270
+ ? resolveProgressionForChild({ projectId, bundleId, childRunId: executionContext.runId })
271
+ : null;
209
272
  switch (decision.kind) {
210
273
  case 'manual':
274
+ if (progression) {
275
+ completeAutomationProgression({
276
+ projectId,
277
+ bundleId,
278
+ progressionRunId: progression.id,
279
+ domainStatus: 'manual_boundary_reached',
280
+ broadcaster: asGenericEmitter(io),
281
+ });
282
+ return true;
283
+ }
211
284
  return false;
212
285
  case 'paused':
213
286
  return true;
214
287
  case 'parse_failed': {
215
- recordAutomationPause(projectId, bundleId, 'review', 'review parse failed');
288
+ if (progression) {
289
+ waitAutomationProgression({
290
+ projectId,
291
+ bundleId,
292
+ progressionRunId: progression.id,
293
+ pauseCode: 'review',
294
+ pauseReason: 'review parse failed',
295
+ reasonCode: 'stage_failed',
296
+ pausedStageRunId: executionContext.runId,
297
+ broadcaster: asGenericEmitter(io),
298
+ });
299
+ }
300
+ else {
301
+ recordAutomationPause(projectId, bundleId, 'review', 'review parse failed');
302
+ }
216
303
  const pausedTicket = getTicket(ticket.id);
217
304
  if (pausedTicket) {
218
305
  io.emit('ticket:updated', pausedTicket);
@@ -220,14 +307,72 @@ const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parse
220
307
  return true;
221
308
  }
222
309
  case 'fix_iteration': {
310
+ const previousIteration = bundleState?.review_iteration_count ?? 0;
311
+ const previousFixLoopActive = bundleState?.fix_loop_active ?? 0;
223
312
  upsertBundleAutomationState(projectId, bundleId, {
224
313
  review_iteration_count: decision.iteration,
225
314
  fix_loop_active: 1,
226
315
  });
227
316
  const instruction = buildAutomationFixInstruction(config.reviewSeverity);
228
- const started = startFollowUp(ticket, io, instruction, 'build');
229
- if (!started) {
230
- recordAutomationPause(projectId, bundleId, 'review', 'fix follow-up could not start (ticket already running)');
317
+ let reviewFix = null;
318
+ try {
319
+ reviewFix = progression
320
+ ? createAutomationReviewFix({
321
+ projectId,
322
+ bundleId,
323
+ ticketId: ticket.id,
324
+ ticketTitle: ticket.title,
325
+ progressionRunId: progression.id,
326
+ workflowContext: initiation.workflowContext,
327
+ triggeringReviewRunId: executionContext.runId,
328
+ broadcaster: asGenericEmitter(io),
329
+ })
330
+ : null;
331
+ }
332
+ catch (error) {
333
+ upsertBundleAutomationState(projectId, bundleId, {
334
+ review_iteration_count: previousIteration,
335
+ fix_loop_active: previousFixLoopActive,
336
+ });
337
+ throw error;
338
+ }
339
+ const started = submitTicketFollowUp({
340
+ ticketId: ticket.id,
341
+ requestedProjectId: projectId,
342
+ payload: { instruction, mode: 'build' },
343
+ broadcaster: asGenericEmitter(io),
344
+ initiation: {
345
+ initiator: 'automation',
346
+ causalParentRunId: reviewFix?.runId ?? executionContext.runId,
347
+ workflowContext: initiation.workflowContext,
348
+ },
349
+ });
350
+ if (!started.ok) {
351
+ upsertBundleAutomationState(projectId, bundleId, {
352
+ review_iteration_count: previousIteration,
353
+ fix_loop_active: previousFixLoopActive,
354
+ });
355
+ if (reviewFix && progression) {
356
+ finishAutomationReviewFix({
357
+ projectId,
358
+ reviewFixRunId: reviewFix.runId,
359
+ failedMessage: started.error,
360
+ broadcaster: asGenericEmitter(io),
361
+ });
362
+ waitAutomationProgression({
363
+ projectId,
364
+ bundleId,
365
+ progressionRunId: progression.id,
366
+ pauseCode: 'review',
367
+ pauseReason: `fix follow-up could not start: ${started.error}`,
368
+ reasonCode: 'stage_failed',
369
+ pausedStageRunId: reviewFix.runId,
370
+ broadcaster: asGenericEmitter(io),
371
+ });
372
+ }
373
+ else {
374
+ recordAutomationPause(projectId, bundleId, 'review', `fix follow-up could not start: ${started.error}`);
375
+ }
231
376
  const pausedTicket = getTicket(ticket.id);
232
377
  if (pausedTicket) {
233
378
  io.emit('ticket:updated', pausedTicket);
@@ -243,7 +388,21 @@ const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parse
243
388
  return true;
244
389
  }
245
390
  case 'pause_blocking': {
246
- recordAutomationPause(projectId, bundleId, 'review', decision.reason);
391
+ if (progression) {
392
+ waitAutomationProgression({
393
+ projectId,
394
+ bundleId,
395
+ progressionRunId: progression.id,
396
+ pauseCode: 'review',
397
+ pauseReason: decision.reason,
398
+ reasonCode: 'precondition_failed',
399
+ pausedStageRunId: executionContext.runId,
400
+ broadcaster: asGenericEmitter(io),
401
+ });
402
+ }
403
+ else {
404
+ recordAutomationPause(projectId, bundleId, 'review', decision.reason);
405
+ }
247
406
  const pausedTicket = getTicket(ticket.id);
248
407
  if (pausedTicket) {
249
408
  io.emit('ticket:updated', pausedTicket);
@@ -253,352 +412,686 @@ const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parse
253
412
  case 'clean': {
254
413
  resetBundleAutomationLoop(projectId, bundleId);
255
414
  if (depthAtLeast(config.depth, 'auto_commit')) {
256
- await runAutomationCommitChain(ticket, project, ticket.bundle ?? null, config, io);
415
+ const commitProgressionRunId = progression?.id ?? ensureAutomationProgression({
416
+ projectId,
417
+ bundleId,
418
+ ticketId: ticket.id,
419
+ ticketTitle: ticket.title,
420
+ parentRunId: executionContext.runId,
421
+ workflowContext: initiation.workflowContext,
422
+ broadcaster: asGenericEmitter(io),
423
+ }).runId;
424
+ await runAutomationCommitChain(ticket, project, ticket.bundle ?? null, config, io, commitProgressionRunId, undefined, executionContext.runId);
257
425
  return true;
258
426
  }
427
+ if (progression) {
428
+ completeAutomationProgression({
429
+ projectId,
430
+ bundleId,
431
+ progressionRunId: progression.id,
432
+ broadcaster: asGenericEmitter(io),
433
+ });
434
+ }
259
435
  return false;
260
436
  }
261
437
  }
262
438
  };
263
- export const startReview = (ticket, io) => {
439
+ export function startReview(ticket, io, options) {
440
+ const legacyResult = options === undefined;
441
+ const reject = (reason, error) => (legacyResult ? false : { accepted: false, reason, error });
264
442
  if (ticket.project_id
265
443
  && ticket.worktree_bundle_id
266
444
  && hasActiveBuildOpenQuestionBatch(ticket.project_id, ticket.worktree_bundle_id)) {
267
- return false;
445
+ return reject('busy', 'Build open question resolution is still in progress');
268
446
  }
269
447
  const jobKey = resolveTicketWorkerKey(ticket, 'review');
270
448
  if (!jobKey || isRunning(jobKey)) {
271
- return false;
449
+ return reject('busy', 'Ticket review is already scheduled or running');
272
450
  }
273
451
  const branch = resolveTicketBranch(ticket);
274
452
  if (!branch) {
275
- return false;
453
+ return reject('project_unavailable', 'Worktree not ready yet');
276
454
  }
277
- enqueue(jobKey, async () => {
278
- registerBundleActivity(ticket, 'review');
279
- const project = getEffectiveProject(ticket);
280
- if (!project) {
281
- updateStatus(ticket.id, 'error', 'Ticket project no longer exists', io);
282
- unregisterBundleActivity(ticket, 'review');
283
- return;
284
- }
285
- const cwd = ticket.bundle?.kind === 'project_root' ? project.repo_path : resolveWorktreePath(branch, project);
286
- const tool = resolveTicketTool(ticket, config.tool ?? 'opencode');
287
- const reviewProcessState = registerReviewProcess(ticket.id);
288
- const finalizeStoppedReview = ({ finalResponse = null, responseTiming = null, specCoverageResponse = null, transcriptRuns = [], usageSnapshot, } = {}) => {
289
- const agentRuns = listAgentRunsForParent('ticket', ticket.id);
290
- for (const agentRun of agentRuns) {
291
- if (agentRun.session_id && tool === 'opencode') {
292
- storeTicketUsageSnapshot(ticket.id, agentRun.session_id);
293
- }
294
- }
295
- if (tool === 'claude' && usageSnapshot) {
296
- storeTicketUsageSnapshotDirect(ticket.id, usageSnapshot);
455
+ const project = getEffectiveProject(ticket);
456
+ if (!project || !ticket.project_id || project.id !== ticket.project_id) {
457
+ return reject('project_unavailable', 'Ticket project is unavailable');
458
+ }
459
+ const initiation = { ...defaultInitiation, ...options?.initiation };
460
+ let acceptedTicket = ticket;
461
+ let compensateAcceptance;
462
+ let executionContext;
463
+ try {
464
+ executionContext = withExecutionRunTransaction(transaction => {
465
+ const acceptance = options?.accept?.();
466
+ if (acceptance) {
467
+ acceptedTicket = acceptance.ticket;
468
+ compensateAcceptance = acceptance.compensate;
297
469
  }
298
- if (transcriptRuns.length > 0) {
299
- const fallbackContent = finalResponse?.trim()
300
- || specCoverageResponse?.trim()
301
- || transcriptRuns.find(run => run.response_text?.trim())?.response_text?.trim()
302
- || transcriptRuns.find(run => run.thinking_log?.trim())?.thinking_log?.trim()
303
- || '';
304
- const messageId = createTicketMessage({
305
- ticketId: ticket.id,
306
- role: 'assistant',
307
- kind: 'final_response',
308
- content: fallbackContent,
309
- responseTiming,
310
- transcriptRuns,
311
- });
312
- if (messageId) {
313
- bindTicketThinkingMessage(ticket.id, messageId);
470
+ if (initiation.initiator === 'automation' && initiation.causalParentRunId) {
471
+ const activeChild = db.prepare(`
472
+ SELECT id FROM execution_runs
473
+ WHERE project_id = ? AND parent_run_id = ?
474
+ AND kind IN ('review', 'automation_review_fix', 'automation_commit', 'automation_push', 'automation_pull_request')
475
+ AND status IN ('queued', 'running', 'waiting')
476
+ LIMIT 1
477
+ `).get(project.id, initiation.causalParentRunId);
478
+ if (activeChild) {
479
+ throw new Error('Automation progression already has an active child');
480
+ }
481
+ const progressionTicketLink = db.prepare(`
482
+ SELECT 1 FROM execution_run_links
483
+ WHERE execution_run_id = ? AND project_id = ? AND entity_type = 'ticket' AND entity_id = ?
484
+ `).get(initiation.causalParentRunId, project.id, acceptedTicket.id);
485
+ if (!progressionTicketLink) {
486
+ transaction.addExecutionRunLink({
487
+ projectId: project.id,
488
+ runId: initiation.causalParentRunId,
489
+ link: {
490
+ entityType: 'ticket',
491
+ entityId: acceptedTicket.id,
492
+ relationship: 'context',
493
+ label: acceptedTicket.title,
494
+ },
495
+ });
314
496
  }
315
497
  }
316
- clearTicketStreamingResponse(ticket.id);
317
- updateStatus(ticket.id, 'stopped', null, io);
318
- clearAgentRunsForParent('ticket', ticket.id);
319
- emitAgentRunsUpdated(io, 'ticket', ticket.id);
320
- };
321
- const stopReviewBeforeAgentIfRequested = () => {
322
- if (!reviewProcessState.stopRequested) {
323
- return false;
324
- }
325
- finalizeStoppedReview();
326
- return true;
327
- };
328
- beginTicketThinkingRun(ticket.id);
329
- updateStatus(ticket.id, 'running', 'Preparing review baseline...', io);
330
- try {
331
- await captureStageBaseline(ticket.id, 'review');
332
- if (stopReviewBeforeAgentIfRequested()) {
333
- return;
334
- }
335
- updateStatus(ticket.id, 'running', 'Computing review diff...', io);
336
- const diffText = (await getReviewDiffDataAsync({ cwd, branch, project })).diff;
337
- if (stopReviewBeforeAgentIfRequested()) {
498
+ const executionRun = transaction.createExecutionRun({
499
+ projectId: project.id,
500
+ kind: 'review',
501
+ domainStatus: 'queued',
502
+ sourceReference: { kind: 'ticket', id: acceptedTicket.id, label: acceptedTicket.title },
503
+ links: [
504
+ ...(acceptedTicket.worktree_bundle_id
505
+ ? [{ entityType: 'bundle', entityId: acceptedTicket.worktree_bundle_id, relationship: 'context' }]
506
+ : []),
507
+ ...(acceptedTicket.origin_spec_snapshot_id
508
+ ? [{ entityType: 'spec_snapshot', entityId: acceptedTicket.origin_spec_snapshot_id, relationship: 'context' }]
509
+ : []),
510
+ ],
511
+ initiatedBy: initiation.initiator,
512
+ parentRunId: initiation.causalParentRunId,
513
+ retryOfRunId: initiation.retrySourceRunId,
514
+ metadata: initiation.workflowContext,
515
+ eventType: 'review_created',
516
+ });
517
+ return {
518
+ runId: executionRun.id,
519
+ correlationId: executionRun.correlation_id,
520
+ projectId: executionRun.project_id,
521
+ kind: executionRun.kind,
522
+ };
523
+ }, asGenericEmitter(io));
524
+ }
525
+ catch (error) {
526
+ return reject('invalid_context', getErrorMessage(error));
527
+ }
528
+ let queuedJob;
529
+ try {
530
+ queuedJob = enqueue(jobKey, async () => {
531
+ const scheduledRun = getExecutionRun(executionContext.projectId, executionContext.runId);
532
+ if (!scheduledRun || !['queued', 'running', 'waiting'].includes(scheduledRun.status)) {
338
533
  return;
339
534
  }
340
- const reviewContext = buildReviewContext({ cwd, branch, project });
341
- const diffSignature = computeDiffSignature(diffText);
342
- const supplementalRequirements = ticket.project_id && ticket.worktree_bundle_id
343
- ? formatBuildOpenQuestionAnswersPrompt(listAnsweredBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id))
344
- : '';
345
- const runSpecCoverage = Boolean(project.spec_aware_code_review_enabled)
346
- && Boolean(ticket.origin_spec_snapshot_id)
347
- && ticket.project_id != null
348
- && ticket.worktree_bundle_id != null
349
- && !hasPriorReviewRunForBundle(ticket.project_id, ticket.worktree_bundle_id);
350
- let specContext = null;
351
- if (runSpecCoverage && ticket.project_id && ticket.worktree_bundle_id) {
352
- const bundleTickets = listTickets(ticket.project_id)
353
- .filter(bundleTicket => bundleTicket.worktree_bundle_id === ticket.worktree_bundle_id);
354
- specContext = buildSpecCoverageContext({ ticket, bundleTickets });
355
- }
356
- const blindRunStreamTarget = (() => {
357
- try {
358
- const runId = createAgentRun({
359
- parentType: 'ticket',
360
- parentId: ticket.id,
361
- runIndex: 0,
362
- label: 'Code Review',
363
- role: 'code-review',
535
+ let activityRegistered = false;
536
+ let registeredReviewProcess = null;
537
+ let finalizeStoppedReviewAfterFailure = null;
538
+ try {
539
+ registerBundleActivity(acceptedTicket, 'review');
540
+ activityRegistered = true;
541
+ const currentTicket = getTicket(acceptedTicket.id);
542
+ if (!currentTicket || currentTicket.project_id !== executionContext.projectId) {
543
+ throw new Error('Ticket no longer exists in the execution project');
544
+ }
545
+ const project = getEffectiveProject(currentTicket);
546
+ if (!project) {
547
+ throw new Error('Ticket project no longer exists');
548
+ }
549
+ const cwd = ticket.bundle?.kind === 'project_root' ? project.repo_path : resolveWorktreePath(branch, project);
550
+ const tool = resolveTicketTool(ticket, config.tool ?? 'opencode');
551
+ const reviewProcessState = registerReviewProcess(acceptedTicket.id);
552
+ registeredReviewProcess = reviewProcessState;
553
+ const createdAgentRunIds = [];
554
+ const storeCreatedAgentRunUsage = () => {
555
+ if (tool !== 'opencode') {
556
+ return;
557
+ }
558
+ for (const runId of createdAgentRunIds) {
559
+ const agentRun = getAgentRun(runId);
560
+ if (agentRun?.session_id) {
561
+ storeTicketUsageSnapshot(ticket.id, agentRun.session_id);
562
+ }
563
+ }
564
+ };
565
+ const finalizeStoppedReview = ({ finalResponse = null, responseTiming = null, specCoverageResponse = null, transcriptRuns = [], usageSnapshot, } = {}) => {
566
+ storeCreatedAgentRunUsage();
567
+ if (tool === 'claude' && usageSnapshot) {
568
+ storeTicketUsageSnapshotDirect(ticket.id, usageSnapshot);
569
+ }
570
+ let messageId = null;
571
+ const fallbackContent = transcriptRuns.length > 0
572
+ ? finalResponse?.trim()
573
+ || specCoverageResponse?.trim()
574
+ || transcriptRuns.find(run => run.response_text?.trim())?.response_text?.trim()
575
+ || transcriptRuns.find(run => run.thinking_log?.trim())?.thinking_log?.trim()
576
+ || ''
577
+ : '';
578
+ clearTicketStreamingResponse(ticket.id);
579
+ withExecutionRunTransaction(transaction => {
580
+ if (fallbackContent) {
581
+ messageId = createTicketMessage({
582
+ ticketId: ticket.id,
583
+ role: 'assistant',
584
+ kind: 'final_response',
585
+ content: fallbackContent,
586
+ responseTiming,
587
+ transcriptRuns,
588
+ });
589
+ transaction.addExecutionRunLink({
590
+ projectId: executionContext.projectId,
591
+ runId: executionContext.runId,
592
+ link: {
593
+ entityType: 'ticket_message',
594
+ entityId: messageId,
595
+ relationship: 'result',
596
+ expectedParent: { entityType: 'ticket', entityId: ticket.id },
597
+ },
598
+ });
599
+ }
600
+ if (messageId) {
601
+ bindTicketThinkingMessage(ticket.id, messageId);
602
+ }
603
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
604
+ .run('stopped', ticket.id, executionContext.projectId);
605
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'stopped', null);
606
+ transaction.transitionExecutionRun({
607
+ projectId: executionContext.projectId,
608
+ runId: executionContext.runId,
609
+ status: 'cancelled',
610
+ domainStatus: 'stopped',
611
+ eventType: 'review_cancelled',
612
+ });
613
+ }, asGenericEmitter(io));
614
+ if (initiation.initiator === 'automation' && acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
615
+ try {
616
+ const progression = resolveProgressionForChild({
617
+ projectId: acceptedTicket.project_id,
618
+ bundleId: acceptedTicket.worktree_bundle_id,
619
+ childRunId: executionContext.runId,
620
+ });
621
+ waitAutomationProgression({
622
+ projectId: acceptedTicket.project_id,
623
+ bundleId: acceptedTicket.worktree_bundle_id,
624
+ progressionRunId: progression.id,
625
+ pauseCode: 'review',
626
+ pauseReason: 'Automated review was stopped',
627
+ reasonCode: 'stage_failed',
628
+ pausedStageRunId: executionContext.runId,
629
+ broadcaster: asGenericEmitter(io),
630
+ });
631
+ }
632
+ catch {
633
+ // A concurrent progression stop remains authoritative.
634
+ }
635
+ }
636
+ updateStatus(ticket.id, 'stopped', null, io);
637
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
638
+ };
639
+ finalizeStoppedReviewAfterFailure = finalizeStoppedReview;
640
+ const stopReviewBeforeAgentIfRequested = () => {
641
+ if (!reviewProcessState.stopRequested) {
642
+ return false;
643
+ }
644
+ finalizeStoppedReview();
645
+ return true;
646
+ };
647
+ beginTicketThinkingRun(ticket.id);
648
+ withExecutionRunTransaction(transaction => {
649
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_operation = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?').run('running', 'review', 'Preparing review baseline...', ticket.id, executionContext.projectId);
650
+ transaction.transitionExecutionRun({
651
+ projectId: executionContext.projectId,
652
+ runId: executionContext.runId,
653
+ status: 'running',
654
+ domainStatus: 'running',
655
+ eventType: 'review_started',
364
656
  });
365
- return { runId, label: 'Code Review', role: 'code-review', runIndex: 0 };
657
+ }, asGenericEmitter(io));
658
+ updateStatus(ticket.id, 'running', 'Preparing review baseline...', io, 'review');
659
+ await captureStageBaseline(ticket.id, 'review');
660
+ if (stopReviewBeforeAgentIfRequested()) {
661
+ return;
366
662
  }
367
- catch (err) {
368
- console.warn('[review] createAgentRun failed for blind run; falling back to legacy streaming', getErrorMessage(err));
369
- return null;
663
+ updateStatus(ticket.id, 'running', 'Computing review diff...', io);
664
+ const diffText = (await getReviewDiffDataAsync({ cwd, branch, project })).diff;
665
+ if (stopReviewBeforeAgentIfRequested()) {
666
+ return;
370
667
  }
371
- })();
372
- let specCoverageRunStreamTarget = null;
373
- if (specContext) {
374
- try {
668
+ const reviewContext = buildReviewContext({ cwd, branch, project });
669
+ const diffSignature = computeDiffSignature(diffText);
670
+ const supplementalRequirements = ticket.project_id && ticket.worktree_bundle_id
671
+ ? formatBuildOpenQuestionAnswersPrompt(listAnsweredBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id))
672
+ : '';
673
+ const runSpecCoverage = Boolean(project.spec_aware_code_review_enabled)
674
+ && Boolean(ticket.origin_spec_snapshot_id)
675
+ && ticket.project_id != null
676
+ && ticket.worktree_bundle_id != null
677
+ && !hasPriorReviewRunForBundle(ticket.project_id, ticket.worktree_bundle_id);
678
+ let specContext = null;
679
+ if (runSpecCoverage && ticket.project_id && ticket.worktree_bundle_id) {
680
+ const bundleTickets = listTickets(ticket.project_id)
681
+ .filter(bundleTicket => bundleTicket.worktree_bundle_id === ticket.worktree_bundle_id);
682
+ specContext = buildSpecCoverageContext({ ticket, bundleTickets });
683
+ }
684
+ const blindRunId = createAgentRun({
685
+ executionRunId: executionContext.runId,
686
+ parentType: 'ticket',
687
+ parentId: ticket.id,
688
+ runIndex: 0,
689
+ label: 'Code Review',
690
+ role: 'code-review',
691
+ });
692
+ createdAgentRunIds.push(blindRunId);
693
+ const blindRunStreamTarget = { runId: blindRunId, label: 'Code Review', role: 'code-review', runIndex: 0 };
694
+ let specCoverageRunStreamTarget = null;
695
+ if (specContext) {
375
696
  const runId = createAgentRun({
697
+ executionRunId: executionContext.runId,
376
698
  parentType: 'ticket',
377
699
  parentId: ticket.id,
378
700
  runIndex: 1,
379
701
  label: 'Spec-Coverage Review',
380
702
  role: 'spec-coverage',
381
703
  });
704
+ createdAgentRunIds.push(runId);
382
705
  specCoverageRunStreamTarget = { runId, label: 'Spec-Coverage Review', role: 'spec-coverage', runIndex: 1 };
383
706
  }
384
- catch (err) {
385
- console.warn('[review] createAgentRun failed for spec-coverage run; falling back to legacy streaming', getErrorMessage(err));
386
- }
387
- }
388
- const createdAgentRuns = Boolean(blindRunStreamTarget) || Boolean(specCoverageRunStreamTarget);
389
- if (createdAgentRuns) {
390
707
  emitAgentRunsUpdated(io, 'ticket', ticket.id);
391
- }
392
- const blindPrompt = reviewPrompt({
393
- ticket,
394
- diff: diffText,
395
- memoryEnabled: Boolean(project.memory_enabled),
396
- reviewContext,
397
- agentRunId: blindRunStreamTarget?.runId ?? null,
398
- supplementalRequirements,
399
- });
400
- const blindRun = async () => {
401
- return runAgent({
402
- ticketId: ticket.id,
403
- tool,
404
- prompt: blindPrompt,
405
- cwd,
406
- model: ticket.model,
407
- variant: ticket.variant,
408
- emitSuccessUpdate: false,
409
- io,
410
- runStream: blindRunStreamTarget ?? undefined,
411
- reviewProcessState,
708
+ const blindPrompt = reviewPrompt({
709
+ ticket,
710
+ diff: diffText,
711
+ memoryEnabled: Boolean(project.memory_enabled),
712
+ reviewContext,
713
+ agentRunId: blindRunStreamTarget.runId,
714
+ supplementalRequirements,
412
715
  });
413
- };
414
- const specCoverageRun = async () => {
415
- if (!specContext) {
416
- return null;
417
- }
418
- try {
419
- return await runAgent({
716
+ const blindRun = async () => {
717
+ return runAgent({
420
718
  ticketId: ticket.id,
421
719
  tool,
422
- prompt: specCoverageReviewPrompt({
423
- ticket,
424
- diff: diffText,
425
- reviewContext,
426
- specContext,
427
- agentRunId: specCoverageRunStreamTarget?.runId ?? null,
428
- supplementalRequirements,
429
- }),
720
+ prompt: blindPrompt,
430
721
  cwd,
431
722
  model: ticket.model,
432
723
  variant: ticket.variant,
433
724
  emitSuccessUpdate: false,
725
+ executionContext,
434
726
  io,
435
- runStream: specCoverageRunStreamTarget ?? undefined,
727
+ runStream: blindRunStreamTarget,
436
728
  reviewProcessState,
437
729
  });
730
+ };
731
+ const specCoverageRun = async () => {
732
+ if (!specContext) {
733
+ return null;
734
+ }
735
+ try {
736
+ return await runAgent({
737
+ ticketId: ticket.id,
738
+ tool,
739
+ prompt: specCoverageReviewPrompt({
740
+ ticket,
741
+ diff: diffText,
742
+ reviewContext,
743
+ specContext,
744
+ agentRunId: specCoverageRunStreamTarget?.runId ?? null,
745
+ supplementalRequirements,
746
+ }),
747
+ cwd,
748
+ model: ticket.model,
749
+ variant: ticket.variant,
750
+ emitSuccessUpdate: false,
751
+ executionContext,
752
+ io,
753
+ runStream: specCoverageRunStreamTarget ?? undefined,
754
+ reviewProcessState,
755
+ });
756
+ }
757
+ catch (err) {
758
+ console.warn('[review] spec-coverage pass failed; falling back to blind-only', getErrorMessage(err));
759
+ const transcriptRuns = getAgentErrorTranscriptRuns(err);
760
+ return transcriptRuns
761
+ ? { response: null, responseTiming: null, transcriptRuns }
762
+ : null;
763
+ }
764
+ };
765
+ let blindResult;
766
+ let specCoverageResult = null;
767
+ if (specContext) {
768
+ const [blindSettled, specCoverageSettled] = await Promise.allSettled([blindRun(), specCoverageRun()]);
769
+ if (blindSettled.status === 'rejected') {
770
+ throw blindSettled.reason;
771
+ }
772
+ blindResult = blindSettled.value;
773
+ specCoverageResult = specCoverageSettled.status === 'fulfilled' ? specCoverageSettled.value : null;
438
774
  }
439
- catch (err) {
440
- console.warn('[review] spec-coverage pass failed; falling back to blind-only', getErrorMessage(err));
441
- const transcriptRuns = getAgentErrorTranscriptRuns(err);
442
- return transcriptRuns
443
- ? { response: null, responseTiming: null, transcriptRuns }
444
- : null;
775
+ else {
776
+ blindResult = await blindRun();
445
777
  }
446
- };
447
- let blindResult;
448
- let specCoverageResult = null;
449
- if (specContext) {
450
- const [blindSettled, specCoverageSettled] = await Promise.allSettled([blindRun(), specCoverageRun()]);
451
- if (blindSettled.status === 'rejected') {
452
- throw blindSettled.reason;
778
+ const { response: finalResponse, responseTiming, usageSnapshot } = blindResult;
779
+ const transcriptRuns = [
780
+ ...(blindResult.transcriptRuns ?? []),
781
+ ...(specCoverageResult?.transcriptRuns ?? []),
782
+ ];
783
+ if (reviewProcessState.stopRequested) {
784
+ finalizeStoppedReview({
785
+ finalResponse,
786
+ responseTiming,
787
+ specCoverageResponse: specCoverageResult?.response ?? null,
788
+ transcriptRuns,
789
+ usageSnapshot,
790
+ });
791
+ return;
453
792
  }
454
- blindResult = blindSettled.value;
455
- specCoverageResult = specCoverageSettled.status === 'fulfilled' ? specCoverageSettled.value : null;
456
- }
457
- else {
458
- blindResult = await blindRun();
459
- }
460
- const { response: finalResponse, responseTiming, usageSnapshot } = blindResult;
461
- const transcriptRuns = [
462
- ...(blindResult.transcriptRuns ?? []),
463
- ...(specCoverageResult?.transcriptRuns ?? []),
464
- ];
465
- if (reviewProcessState.stopRequested) {
466
- finalizeStoppedReview({
467
- finalResponse,
468
- responseTiming,
469
- specCoverageResponse: specCoverageResult?.response ?? null,
470
- transcriptRuns,
471
- usageSnapshot,
472
- });
473
- return;
474
- }
475
- const normalizedResponse = typeof finalResponse === 'string'
476
- ? normalizeReviewOutput(finalResponse)
477
- : { cleanedOutput: '', suggestions: [] };
478
- let reviewRunId = null;
479
- let parsedFindings = [];
480
- let parsedStatus = null;
481
- let persistedMessageId = null;
482
- if (normalizedResponse.cleanedOutput) {
483
- const blindParsed = parseStructuredReviewOutput(normalizedResponse.cleanedOutput);
484
- const specCoverageSpecOutput = typeof specCoverageResult?.response === 'string'
485
- ? stripSystemReminderBlocks(specCoverageResult.response)
793
+ const { normalizedParent: normalizedResponse, parsed: blindParsed, } = resolveStructuredReviewOutput(finalResponse, blindResult.transcriptRuns ?? []);
794
+ let reviewRunId = null;
795
+ let parsedFindings = [];
796
+ let parsedStatus = null;
797
+ let persistedMessageId = null;
798
+ let reviewContent = null;
799
+ let findingsToPersist = [];
800
+ if (blindParsed?.cleanedMarkdown) {
801
+ const specCoverageSpecOutput = typeof specCoverageResult?.response === 'string'
802
+ ? stripSystemReminderBlocks(specCoverageResult.response)
803
+ : '';
804
+ const specParsed = specCoverageSpecOutput
805
+ ? parseStructuredReviewOutput(specCoverageSpecOutput)
806
+ : null;
807
+ const mergedFindings = specParsed && specParsed.status === 'completed'
808
+ ? [...blindParsed.findings, ...specParsed.findings.map(finding => ({
809
+ ...finding,
810
+ ordinal: finding.ordinal + blindParsed.findings.length,
811
+ }))]
812
+ : blindParsed.findings;
813
+ const combinedMarkdown = specParsed && specParsed.cleanedMarkdown
814
+ ? `${blindParsed.cleanedMarkdown}\n\n## Spec-Coverage Review\n${specParsed.cleanedMarkdown}`
815
+ : blindParsed.cleanedMarkdown;
816
+ parsedFindings = mergedFindings;
817
+ parsedStatus = blindParsed.status;
818
+ reviewContent = combinedMarkdown;
819
+ findingsToPersist = mergedFindings;
820
+ }
821
+ const fallbackContent = !reviewContent && transcriptRuns.length > 0
822
+ ? finalResponse?.trim()
823
+ || specCoverageResult?.response?.trim()
824
+ || transcriptRuns.find(run => run.response_text?.trim())?.response_text?.trim()
825
+ || transcriptRuns.find(run => run.thinking_log?.trim())?.thinking_log?.trim()
826
+ || ''
486
827
  : '';
487
- const specParsed = specCoverageSpecOutput
488
- ? parseStructuredReviewOutput(specCoverageSpecOutput)
489
- : null;
490
- const mergedFindings = specParsed && specParsed.status === 'completed'
491
- ? [...blindParsed.findings, ...specParsed.findings.map(finding => ({
492
- ...finding,
493
- ordinal: finding.ordinal + blindParsed.findings.length,
494
- }))]
495
- : blindParsed.findings;
496
- const combinedMarkdown = specParsed && specParsed.cleanedMarkdown
497
- ? `${blindParsed.cleanedMarkdown}\n\n## Spec-Coverage Review\n${specParsed.cleanedMarkdown}`
498
- : blindParsed.cleanedMarkdown;
499
- parsedFindings = mergedFindings;
500
- parsedStatus = blindParsed.status;
501
- persistedMessageId = createTicketMessage({
502
- ticketId: ticket.id,
503
- role: 'assistant',
504
- kind: 'final_response',
505
- content: combinedMarkdown,
506
- responseTiming,
507
- transcriptRuns: transcriptRuns.length > 0 ? transcriptRuns : null,
508
- });
828
+ const parseFailed = parsedStatus !== 'completed';
829
+ const persisted = withExecutionRunTransaction(transaction => {
830
+ clearTicketStreamingResponse(ticket.id);
831
+ const content = reviewContent ?? fallbackContent;
832
+ const messageId = content
833
+ ? createTicketMessage({
834
+ ticketId: ticket.id,
835
+ role: 'assistant',
836
+ kind: 'final_response',
837
+ content,
838
+ responseTiming,
839
+ transcriptRuns: transcriptRuns.length > 0 ? transcriptRuns : null,
840
+ })
841
+ : null;
842
+ let createdReviewRunId = null;
843
+ if (messageId && reviewContent && parsedStatus) {
844
+ createdReviewRunId = createReviewRun({
845
+ ticketId: ticket.id,
846
+ projectId: ticket.project_id,
847
+ worktreeBundleId: ticket.worktree_bundle_id,
848
+ diffSignature,
849
+ workspaceSignature: reviewContext.workspaceSignature,
850
+ messageId,
851
+ status: parsedStatus,
852
+ });
853
+ if (findingsToPersist.length > 0) {
854
+ insertReviewFindings(createdReviewRunId, findingsToPersist);
855
+ }
856
+ }
857
+ if (messageId) {
858
+ transaction.addExecutionRunLink({
859
+ projectId: executionContext.projectId,
860
+ runId: executionContext.runId,
861
+ link: {
862
+ entityType: 'ticket_message',
863
+ entityId: messageId,
864
+ relationship: 'result',
865
+ expectedParent: { entityType: 'ticket', entityId: ticket.id },
866
+ },
867
+ });
868
+ }
869
+ if (createdReviewRunId) {
870
+ transaction.addExecutionRunLink({
871
+ projectId: executionContext.projectId,
872
+ runId: executionContext.runId,
873
+ link: {
874
+ entityType: 'review_run',
875
+ entityId: createdReviewRunId,
876
+ relationship: 'result',
877
+ expectedParent: { entityType: 'ticket', entityId: ticket.id },
878
+ },
879
+ });
880
+ }
881
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
882
+ .run('done', ticket.id, executionContext.projectId);
883
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, parseFailed ? 'error' : 'done', parseFailed ? 'Review output could not be parsed' : null);
884
+ transaction.transitionExecutionRun({
885
+ projectId: executionContext.projectId,
886
+ runId: executionContext.runId,
887
+ status: parseFailed ? 'failed' : 'completed',
888
+ domainStatus: parsedStatus ?? 'parse_failed',
889
+ ...(parseFailed ? { failure: { code: 'parse_failed', message: 'Review output could not be parsed' } } : {}),
890
+ eventType: parseFailed ? 'review_parse_failed' : 'review_completed',
891
+ });
892
+ return { messageId, reviewRunId: createdReviewRunId };
893
+ }, asGenericEmitter(io));
894
+ persistedMessageId = persisted.messageId;
895
+ reviewRunId = persisted.reviewRunId;
509
896
  if (persistedMessageId) {
510
897
  bindTicketThinkingMessage(ticket.id, persistedMessageId);
511
898
  }
512
- reviewRunId = createReviewRun({
513
- ticketId: ticket.id,
514
- projectId: ticket.project_id,
515
- worktreeBundleId: ticket.worktree_bundle_id,
516
- diffSignature,
517
- workspaceSignature: reviewContext.workspaceSignature,
518
- messageId: persistedMessageId,
519
- status: parsedStatus,
520
- });
521
- if (mergedFindings.length > 0) {
522
- insertReviewFindings(reviewRunId, mergedFindings);
523
- }
524
- }
525
- if (!persistedMessageId && transcriptRuns.length > 0) {
526
- const fallbackContent = finalResponse?.trim()
527
- || specCoverageResult?.response?.trim()
528
- || transcriptRuns.find(run => run.response_text?.trim())?.response_text?.trim()
529
- || transcriptRuns.find(run => run.thinking_log?.trim())?.thinking_log?.trim()
530
- || '';
531
- if (fallbackContent) {
532
- persistedMessageId = createTicketMessage({
533
- ticketId: ticket.id,
534
- role: 'assistant',
535
- kind: 'final_response',
536
- content: fallbackContent,
537
- responseTiming,
538
- transcriptRuns,
899
+ if (reviewRunId && ticket.project_id && ticket.worktree_bundle_id) {
900
+ io.emit('review-findings:updated', {
901
+ project_id: ticket.project_id,
902
+ worktree_bundle_id: ticket.worktree_bundle_id,
903
+ review_run_id: reviewRunId,
539
904
  });
540
- if (persistedMessageId) {
541
- bindTicketThinkingMessage(ticket.id, persistedMessageId);
905
+ }
906
+ storeCreatedAgentRunUsage();
907
+ if (tool === 'claude' && usageSnapshot) {
908
+ storeTicketUsageSnapshotDirect(ticket.id, usageSnapshot);
909
+ }
910
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
911
+ updateStatus(ticket.id, 'done', null, io);
912
+ const completedWithMessage = getTicket(ticket.id);
913
+ if (completedWithMessage) {
914
+ io.emit('ticket:updated', completedWithMessage);
915
+ }
916
+ const updated = getTicket(ticket.id);
917
+ if (updated?.agent_status === 'done') {
918
+ if (project.memory_enabled && normalizedResponse.suggestions.length > 0) {
919
+ persistProjectMemorySuggestions(project.id, ticket.id, 'review', normalizedResponse.suggestions);
920
+ }
921
+ captureStageSuccess(ticket.id, 'review');
922
+ let handled = false;
923
+ try {
924
+ handled = await runReviewCompletionAutomation(updated, project, reviewRunId, parsedStatus, parsedFindings, io, executionContext, initiation);
925
+ }
926
+ catch (automationError) {
927
+ if (ticket.project_id && ticket.worktree_bundle_id) {
928
+ const reason = getErrorMessage(automationError);
929
+ if (initiation.initiator === 'automation') {
930
+ const progression = resolveProgressionForChild({
931
+ projectId: ticket.project_id,
932
+ bundleId: ticket.worktree_bundle_id,
933
+ childRunId: executionContext.runId,
934
+ });
935
+ waitAutomationProgression({
936
+ projectId: ticket.project_id,
937
+ bundleId: ticket.worktree_bundle_id,
938
+ progressionRunId: progression.id,
939
+ pauseCode: 'review',
940
+ pauseReason: reason,
941
+ reasonCode: 'stage_failed',
942
+ pausedStageRunId: executionContext.runId,
943
+ broadcaster: asGenericEmitter(io),
944
+ });
945
+ }
946
+ else {
947
+ recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'review', reason);
948
+ }
949
+ handled = true;
950
+ }
951
+ }
952
+ if (!handled) {
953
+ const completed = autoParkTicketIfStale(ticket.id);
954
+ if (completed) {
955
+ io.emit('ticket:updated', completed);
956
+ }
542
957
  }
543
958
  }
544
959
  }
545
- const agentRuns = listAgentRunsForParent('ticket', ticket.id);
546
- for (const agentRun of agentRuns) {
547
- if (agentRun.session_id && tool === 'opencode') {
548
- storeTicketUsageSnapshot(ticket.id, agentRun.session_id);
960
+ catch (err) {
961
+ if (registeredReviewProcess?.stopRequested && finalizeStoppedReviewAfterFailure) {
962
+ finalizeStoppedReviewAfterFailure({ transcriptRuns: getAgentErrorTranscriptRuns(err) ?? [] });
963
+ return;
549
964
  }
550
- }
551
- if (tool === 'claude' && usageSnapshot) {
552
- storeTicketUsageSnapshotDirect(ticket.id, usageSnapshot);
553
- }
554
- clearTicketStreamingResponse(ticket.id);
555
- clearAgentRunsForParent('ticket', ticket.id);
556
- emitAgentRunsUpdated(io, 'ticket', ticket.id);
557
- updateStatus(ticket.id, 'done', null, io);
558
- const completedWithMessage = getTicket(ticket.id);
559
- if (completedWithMessage) {
560
- io.emit('ticket:updated', completedWithMessage);
561
- }
562
- const updated = getTicket(ticket.id);
563
- if (updated?.agent_status === 'done') {
564
- if (project.memory_enabled && normalizedResponse.suggestions.length > 0) {
565
- persistProjectMemorySuggestions(project.id, ticket.id, 'review', normalizedResponse.suggestions);
965
+ const transcriptRuns = getAgentErrorTranscriptRuns(err);
966
+ const errorContent = formatAgentErrorForTranscript(err);
967
+ const message = getErrorMessage(err);
968
+ const executionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
969
+ if (!executionRun || !['queued', 'running', 'waiting'].includes(executionRun.status)) {
970
+ return;
566
971
  }
567
- captureStageSuccess(ticket.id, 'review');
568
- const handled = await runReviewCompletionAutomation(updated, project, reviewRunId, parsedStatus, parsedFindings, io);
569
- if (!handled) {
570
- const completed = autoParkTicketIfStale(ticket.id);
571
- if (completed) {
572
- io.emit('ticket:updated', completed);
972
+ let ownedProgressionId = null;
973
+ if (initiation.initiator === 'automation' && acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
974
+ try {
975
+ ownedProgressionId = resolveProgressionForChild({
976
+ projectId: acceptedTicket.project_id,
977
+ bundleId: acceptedTicket.worktree_bundle_id,
978
+ childRunId: executionContext.runId,
979
+ }).id;
980
+ }
981
+ catch {
982
+ ownedProgressionId = null;
573
983
  }
574
984
  }
985
+ try {
986
+ withExecutionRunTransaction(transaction => {
987
+ if (errorContent) {
988
+ const messageId = createTicketMessage({
989
+ ticketId: acceptedTicket.id,
990
+ role: 'assistant',
991
+ kind: 'error',
992
+ content: errorContent,
993
+ transcriptRuns,
994
+ });
995
+ transaction.addExecutionRunLink({
996
+ projectId: executionContext.projectId,
997
+ runId: executionContext.runId,
998
+ link: {
999
+ entityType: 'ticket_message',
1000
+ entityId: messageId,
1001
+ relationship: 'result',
1002
+ expectedParent: { entityType: 'ticket', entityId: acceptedTicket.id },
1003
+ },
1004
+ });
1005
+ }
1006
+ db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
1007
+ .run('error', message, acceptedTicket.id, executionContext.projectId);
1008
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
1009
+ transaction.transitionExecutionRun({
1010
+ projectId: executionContext.projectId,
1011
+ runId: executionContext.runId,
1012
+ status: 'failed',
1013
+ domainStatus: 'error',
1014
+ failure: { code: 'review_failed', message },
1015
+ eventType: 'review_failed',
1016
+ });
1017
+ if (ownedProgressionId && acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
1018
+ waitAutomationProgression({
1019
+ projectId: acceptedTicket.project_id,
1020
+ bundleId: acceptedTicket.worktree_bundle_id,
1021
+ progressionRunId: ownedProgressionId,
1022
+ pauseCode: 'review',
1023
+ pauseReason: message,
1024
+ reasonCode: 'stage_failed',
1025
+ pausedStageRunId: executionContext.runId,
1026
+ broadcaster: asGenericEmitter(io),
1027
+ });
1028
+ }
1029
+ }, asGenericEmitter(io));
1030
+ }
1031
+ catch {
1032
+ const currentExecutionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
1033
+ if (currentExecutionRun && ['queued', 'running', 'waiting'].includes(currentExecutionRun.status)) {
1034
+ terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
1035
+ transitionExecutionRun({
1036
+ projectId: executionContext.projectId,
1037
+ runId: executionContext.runId,
1038
+ status: 'failed',
1039
+ domainStatus: 'error',
1040
+ failure: { code: 'review_failed', message },
1041
+ eventType: 'review_failed',
1042
+ }, asGenericEmitter(io));
1043
+ }
1044
+ if (ownedProgressionId && acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
1045
+ try {
1046
+ waitAutomationProgression({
1047
+ projectId: acceptedTicket.project_id,
1048
+ bundleId: acceptedTicket.worktree_bundle_id,
1049
+ progressionRunId: ownedProgressionId,
1050
+ pauseCode: 'review',
1051
+ pauseReason: message,
1052
+ reasonCode: 'stage_failed',
1053
+ pausedStageRunId: executionContext.runId,
1054
+ broadcaster: asGenericEmitter(io),
1055
+ });
1056
+ }
1057
+ catch {
1058
+ // Preserve the terminal Review result when ownership was concurrently cancelled.
1059
+ }
1060
+ }
1061
+ }
1062
+ if (getTicket(acceptedTicket.id)) {
1063
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', acceptedTicket.id);
1064
+ updateStatus(acceptedTicket.id, 'error', message, io);
1065
+ }
575
1066
  }
576
- }
577
- catch (err) {
578
- if (reviewProcessState.stopRequested) {
579
- finalizeStoppedReview({ transcriptRuns: getAgentErrorTranscriptRuns(err) ?? [] });
580
- return;
581
- }
582
- const transcriptRuns = getAgentErrorTranscriptRuns(err);
583
- const errorContent = formatAgentErrorForTranscript(err);
584
- if (errorContent) {
585
- createTicketMessage({
586
- ticketId: ticket.id,
587
- role: 'assistant',
588
- kind: 'error',
589
- content: errorContent,
590
- transcriptRuns,
591
- });
1067
+ finally {
1068
+ if (registeredReviewProcess) {
1069
+ untrackReviewProcess(acceptedTicket.id, registeredReviewProcess);
1070
+ }
1071
+ if (activityRegistered) {
1072
+ unregisterBundleActivity(acceptedTicket, 'review');
1073
+ }
592
1074
  }
593
- clearAgentRunsForParent('ticket', ticket.id);
594
- emitAgentRunsUpdated(io, 'ticket', ticket.id);
595
- updateStatus(ticket.id, 'error', getErrorMessage(err), io);
596
- }
597
- finally {
598
- untrackReviewProcess(ticket.id, reviewProcessState);
599
- unregisterBundleActivity(ticket, 'review');
600
- }
1075
+ });
1076
+ }
1077
+ catch (error) {
1078
+ const message = getErrorMessage(error);
1079
+ withExecutionRunTransaction(transaction => {
1080
+ compensateAcceptance?.();
1081
+ transaction.transitionExecutionRun({
1082
+ projectId: executionContext.projectId,
1083
+ runId: executionContext.runId,
1084
+ status: 'failed',
1085
+ domainStatus: ticket.agent_status ?? 'idle',
1086
+ failure: { code: 'queue_failed', message },
1087
+ eventType: 'review_failed',
1088
+ });
1089
+ }, asGenericEmitter(io));
1090
+ return reject('queue_failed', message);
1091
+ }
1092
+ queuedJob.catch(() => {
1093
+ // The queued callback owns domain and execution failure finalization.
601
1094
  });
602
- return true;
603
- };
1095
+ return legacyResult ? true : { accepted: true, executionContext };
1096
+ }
604
1097
  //# sourceMappingURL=review.js.map