@straiffi/archon 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/client/assets/ActivityView-DTEenQas.js +1 -0
  2. package/dist/client/assets/GitHubActionsDialog-BKWHonUS.js +1 -0
  3. package/dist/client/assets/TestsDialog-cBaoffCs.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-CJgwCv5O.js → architectureDiagram-3BPJPVTR-BzzGotac.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM--xjes8FA.js → blockDiagram-GPEHLZMM-DIcqLHKg.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-DHnp82NG.js → c4Diagram-AAUBKEIU-Hc1Sqmhs.js} +1 -1
  7. package/dist/client/assets/channel-Cym17waa.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-DAOyJCKy.js → chunk-2J33WTMH-CWSwjsgk.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-C9YI_tk4.js → chunk-3OPIFGDE-BnX3DljN.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-BW9khkin.js → chunk-5ZQYHXKU-EyyJlI-d.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-DL4t5Hpi.js → chunk-727SXJPM-Ci6CfxWj.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-C4VMKa_O.js → chunk-AQP2D5EJ-RKZNsJTm.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-DHP7zrZK.js → chunk-CSCIHK7Q-DBf0Euoj.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-CqZWek3t.js → chunk-KSCS5N6A-BmLQI_2S.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-D3q55pCI.js → chunk-L5ZTLDWV-DKwVAb8G.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-UDY_Lo6v.js → chunk-LZXEDZCA-axotKkbz.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-ZCiOxkYV.js → chunk-ND2GUHAM-BBlJ4nXC.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-CuryoBtd.js → chunk-NZK2D7GU-BKJGoZZt.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-PnPsz2TB.js → chunk-O5CBEL6O-C-Ghigo1.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-BzOopX-U.js → chunk-WU5MYG2G-CCzVWYcd.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-BcigvvIC.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-3AdSuE0N.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-5LBwQ83I.js → dagre-BM42HDAG-rwIdPcjI.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-_4pckiGR.js → diagram-2AECGRRQ-DJWxOQsg.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-CqElv5rJ.js → diagram-5GNKFQAL-C-mQeFtd.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-DKv4zS4F.js → diagram-KO2AKTUF-BAu6u4sR.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-iLmTH4UP.js → diagram-LMA3HP47-U6CbvBn0.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-DpFYIa0o.js → diagram-OG6HWLK6-DNTwFcs1.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DnfbvbKs.js → erDiagram-TEJ5UH35-DaHX6Xw5.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-C2MN70Qv.js → flowDiagram-I6XJVG4X-B9hKqL2l.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-yonpliOJ.js → ganttDiagram-6RSMTGT7-BQ1HPHVO.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CaKDWx7y.js → gitGraphDiagram-PVQCEYII-BBRcsmMK.js} +1 -1
  33. package/dist/client/assets/index-CEMaJCPS.js +273 -0
  34. package/dist/client/assets/index-lWftq65U.css +2 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-82-8njnx.js → infoDiagram-5YYISTIA-DUoNiSGC.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-BMiyLWwK.js → ishikawaDiagram-YF4QCWOH-DJN1y2w8.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-C1qBnR37.js → journeyDiagram-JHISSGLW-BxLLMT70.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-zByEuCzn.js → kanban-definition-UN3LZRKU-BdCPistJ.js} +1 -1
  39. package/dist/client/assets/{line-EpLqyzw7.js → line-4mIgXsZd.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-o124REPN.js → mermaid-parser.core-Pu1QGWJ-.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-DgiAn2-k.js → mermaid.core-DnH9DrgZ.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-UK6V0Bxq.js → mindmap-definition-RKZ34NQL-DqE4ba3C.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-B2s9oGFl.js → pieDiagram-4H26LBE5-Crjy9ejf.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-d5H7-zcc.js → quadrantDiagram-W4KKPZXB-BUSZ2VB7.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DlwN9Vcv.js → requirementDiagram-4Y6WPE33-CBhFKEoV.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-9dGyAxu5.js → sankeyDiagram-5OEKKPKP-CgdbY41v.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-DKwRjEym.js → sequenceDiagram-3UESZ5HK-hkp_H7AE.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-D_6cuqsX.js → stateDiagram-AJRCARHV-C6x45RWp.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DVzPjRPN.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-ChqD4j8I.js → timeline-definition-PNZ67QCA-CjnZWttQ.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-CSAZk0IW.js → vennDiagram-CIIHVFJN-DU5ZyYju.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-BUE6IF4q.js → wardleyDiagram-YWT4CUSO-CaCEnCB0.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-MOkC53m_.js → xychartDiagram-2RQKCTM6-Ce3xa37U.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +471 -39
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +1267 -213
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +281 -76
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentRuns.js +205 -38
  62. package/dist/server/lib/agentRuns.js.map +1 -1
  63. package/dist/server/lib/automationProgression.js +464 -0
  64. package/dist/server/lib/automationProgression.js.map +1 -0
  65. package/dist/server/lib/automationReconciliation.js +498 -0
  66. package/dist/server/lib/automationReconciliation.js.map +1 -0
  67. package/dist/server/lib/automationStageDetails.js +232 -0
  68. package/dist/server/lib/automationStageDetails.js.map +1 -0
  69. package/dist/server/lib/buildOpenQuestionOperations.js +223 -0
  70. package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
  71. package/dist/server/lib/buildOpenQuestions.js +52 -0
  72. package/dist/server/lib/buildOpenQuestions.js.map +1 -1
  73. package/dist/server/lib/buildSequences.js +130 -46
  74. package/dist/server/lib/buildSequences.js.map +1 -1
  75. package/dist/server/lib/bundleReviewOperations.js +82 -9
  76. package/dist/server/lib/bundleReviewOperations.js.map +1 -1
  77. package/dist/server/lib/chatOperations.js +31 -0
  78. package/dist/server/lib/chatOperations.js.map +1 -1
  79. package/dist/server/lib/executionReconciliation.js +518 -0
  80. package/dist/server/lib/executionReconciliation.js.map +1 -0
  81. package/dist/server/lib/executionRunRetention.js +141 -0
  82. package/dist/server/lib/executionRunRetention.js.map +1 -0
  83. package/dist/server/lib/executionRuns.js +1264 -0
  84. package/dist/server/lib/executionRuns.js.map +1 -0
  85. package/dist/server/lib/git.js +385 -6
  86. package/dist/server/lib/git.js.map +1 -1
  87. package/dist/server/lib/integrations/github.js +43 -0
  88. package/dist/server/lib/integrations/github.js.map +1 -1
  89. package/dist/server/lib/integrations/githubActions.js +1 -0
  90. package/dist/server/lib/integrations/githubActions.js.map +1 -1
  91. package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
  92. package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
  93. package/dist/server/lib/projectMemorySelections.js +7 -3
  94. package/dist/server/lib/projectMemorySelections.js.map +1 -1
  95. package/dist/server/lib/shutdown.js +2 -2
  96. package/dist/server/lib/shutdown.js.map +1 -1
  97. package/dist/server/lib/specArtifact.js +3 -0
  98. package/dist/server/lib/specArtifact.js.map +1 -1
  99. package/dist/server/lib/specFormatting.js +1 -1
  100. package/dist/server/lib/specFormatting.js.map +1 -1
  101. package/dist/server/lib/specOperations.js +31 -0
  102. package/dist/server/lib/specOperations.js.map +1 -1
  103. package/dist/server/lib/specReviewContext.js +44 -14
  104. package/dist/server/lib/specReviewContext.js.map +1 -1
  105. package/dist/server/lib/specReviewDraft.js +3 -2
  106. package/dist/server/lib/specReviewDraft.js.map +1 -1
  107. package/dist/server/lib/specReviewOperations.js +44 -7
  108. package/dist/server/lib/specReviewOperations.js.map +1 -1
  109. package/dist/server/lib/specReviewValidation.js +217 -0
  110. package/dist/server/lib/specReviewValidation.js.map +1 -0
  111. package/dist/server/lib/specReviews.js +30 -2
  112. package/dist/server/lib/specReviews.js.map +1 -1
  113. package/dist/server/lib/specs.js +18 -1
  114. package/dist/server/lib/specs.js.map +1 -1
  115. package/dist/server/lib/stageSnapshots.js +197 -0
  116. package/dist/server/lib/stageSnapshots.js.map +1 -0
  117. package/dist/server/lib/testSessions.js +164 -19
  118. package/dist/server/lib/testSessions.js.map +1 -1
  119. package/dist/server/lib/ticketAutomation.js +80 -4
  120. package/dist/server/lib/ticketAutomation.js.map +1 -1
  121. package/dist/server/lib/ticketAutomationCommit.js +466 -91
  122. package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
  123. package/dist/server/lib/ticketFollowUpOperations.js +214 -20
  124. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  125. package/dist/server/lib/ticketOpenQuestions.js +30 -14
  126. package/dist/server/lib/ticketOpenQuestions.js.map +1 -1
  127. package/dist/server/lib/ticketRunOperations.js +75 -7
  128. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  129. package/dist/server/lib/ticketUndo.js +77 -168
  130. package/dist/server/lib/ticketUndo.js.map +1 -1
  131. package/dist/server/lib/ticketWorkflowOperations.js +132 -28
  132. package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
  133. package/dist/server/lib/tickets.js +46 -29
  134. package/dist/server/lib/tickets.js.map +1 -1
  135. package/dist/server/lib/transcriptRuns.js +3 -3
  136. package/dist/server/lib/transcriptRuns.js.map +1 -1
  137. package/dist/server/lib/turnExecutionErrors.js +12 -0
  138. package/dist/server/lib/turnExecutionErrors.js.map +1 -0
  139. package/dist/server/lib/types.js +57 -1
  140. package/dist/server/lib/types.js.map +1 -1
  141. package/dist/server/workers/build.js +596 -184
  142. package/dist/server/workers/build.js.map +1 -1
  143. package/dist/server/workers/chat.js +261 -130
  144. package/dist/server/workers/chat.js.map +1 -1
  145. package/dist/server/workers/followUp.js +720 -195
  146. package/dist/server/workers/followUp.js.map +1 -1
  147. package/dist/server/workers/plan.js +527 -188
  148. package/dist/server/workers/plan.js.map +1 -1
  149. package/dist/server/workers/planFollowUp.js +231 -69
  150. package/dist/server/workers/planFollowUp.js.map +1 -1
  151. package/dist/server/workers/review.js +796 -239
  152. package/dist/server/workers/review.js.map +1 -1
  153. package/dist/server/workers/specReview.js +211 -45
  154. package/dist/server/workers/specReview.js.map +1 -1
  155. package/dist/server/workers/specSession.js +425 -252
  156. package/dist/server/workers/specSession.js.map +1 -1
  157. package/package.json +1 -1
  158. package/dist/client/assets/GitHubActionsDialog-BrUq-zO1.js +0 -1
  159. package/dist/client/assets/TestsDialog-D9N4GD77.js +0 -4
  160. package/dist/client/assets/TicketChangesModal-bYHJSO6d.js +0 -1
  161. package/dist/client/assets/channel-B4kCvLCt.js +0 -1
  162. package/dist/client/assets/classDiagram-4FO5ZUOK-C-eRRNNj.js +0 -1
  163. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-B3vxoc73.js +0 -1
  164. package/dist/client/assets/index-9q1GXkUw.js +0 -270
  165. package/dist/client/assets/index-BxTc5qQT.css +0 -2
  166. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BkMKz90T.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import db from '../db.js';
2
- import { formatAgentErrorForTranscript, getAgentErrorTranscriptRuns, runAgent, storeTicketUsageSnapshot, storeTicketUsageSnapshotDirect } from '../lib/agent.js';
2
+ import { formatAgentErrorForTranscript, getAgentErrorTranscriptRuns, registerReviewProcess, runAgent, storeTicketUsageSnapshot, storeTicketUsageSnapshotDirect, untrackReviewProcess, } from '../lib/agent.js';
3
3
  import config from '../lib/config.js';
4
4
  import { persistProjectMemorySuggestions } from '../lib/projectMemory.js';
5
5
  import { getReviewDiffDataAsync, resolveWorktreePath } from '../lib/git.js';
@@ -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,288 +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
- beginTicketThinkingRun(ticket.id);
288
- updateStatus(ticket.id, 'running', 'Preparing review baseline...', io);
289
- try {
290
- await captureStageBaseline(ticket.id, 'review');
291
- updateStatus(ticket.id, 'running', 'Computing review diff...', io);
292
- const diffText = (await getReviewDiffDataAsync({ cwd, branch, project })).diff;
293
- const reviewContext = buildReviewContext({ cwd, branch, project });
294
- const diffSignature = computeDiffSignature(diffText);
295
- const supplementalRequirements = ticket.project_id && ticket.worktree_bundle_id
296
- ? formatBuildOpenQuestionAnswersPrompt(listAnsweredBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id))
297
- : '';
298
- const runSpecCoverage = Boolean(project.spec_aware_code_review_enabled)
299
- && Boolean(ticket.origin_spec_snapshot_id)
300
- && ticket.project_id != null
301
- && ticket.worktree_bundle_id != null
302
- && !hasPriorReviewRunForBundle(ticket.project_id, ticket.worktree_bundle_id);
303
- let specContext = null;
304
- if (runSpecCoverage && ticket.project_id && ticket.worktree_bundle_id) {
305
- const bundleTickets = listTickets(ticket.project_id)
306
- .filter(bundleTicket => bundleTicket.worktree_bundle_id === ticket.worktree_bundle_id);
307
- specContext = buildSpecCoverageContext({ ticket, bundleTickets });
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;
308
469
  }
309
- const blindRunStreamTarget = (() => {
310
- try {
311
- const runId = createAgentRun({
312
- parentType: 'ticket',
313
- parentId: ticket.id,
314
- runIndex: 0,
315
- label: 'Code Review',
316
- role: 'code-review',
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
+ },
317
495
  });
318
- return { runId, label: 'Code Review', role: 'code-review', runIndex: 0 };
319
496
  }
320
- catch (err) {
321
- console.warn('[review] createAgentRun failed for blind run; falling back to legacy streaming', getErrorMessage(err));
322
- return null;
497
+ }
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)) {
533
+ return;
534
+ }
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');
323
544
  }
324
- })();
325
- let specCoverageRunStreamTarget = null;
326
- if (specContext) {
327
- try {
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',
656
+ });
657
+ }, asGenericEmitter(io));
658
+ updateStatus(ticket.id, 'running', 'Preparing review baseline...', io, 'review');
659
+ await captureStageBaseline(ticket.id, 'review');
660
+ if (stopReviewBeforeAgentIfRequested()) {
661
+ return;
662
+ }
663
+ updateStatus(ticket.id, 'running', 'Computing review diff...', io);
664
+ const diffText = (await getReviewDiffDataAsync({ cwd, branch, project })).diff;
665
+ if (stopReviewBeforeAgentIfRequested()) {
666
+ return;
667
+ }
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) {
328
696
  const runId = createAgentRun({
697
+ executionRunId: executionContext.runId,
329
698
  parentType: 'ticket',
330
699
  parentId: ticket.id,
331
700
  runIndex: 1,
332
701
  label: 'Spec-Coverage Review',
333
702
  role: 'spec-coverage',
334
703
  });
704
+ createdAgentRunIds.push(runId);
335
705
  specCoverageRunStreamTarget = { runId, label: 'Spec-Coverage Review', role: 'spec-coverage', runIndex: 1 };
336
706
  }
337
- catch (err) {
338
- console.warn('[review] createAgentRun failed for spec-coverage run; falling back to legacy streaming', getErrorMessage(err));
339
- }
340
- }
341
- const createdAgentRuns = Boolean(blindRunStreamTarget) || Boolean(specCoverageRunStreamTarget);
342
- if (createdAgentRuns) {
343
707
  emitAgentRunsUpdated(io, 'ticket', ticket.id);
344
- }
345
- const blindPrompt = reviewPrompt({
346
- ticket,
347
- diff: diffText,
348
- memoryEnabled: Boolean(project.memory_enabled),
349
- reviewContext,
350
- agentRunId: blindRunStreamTarget?.runId ?? null,
351
- supplementalRequirements,
352
- });
353
- const blindRun = async () => {
354
- return runAgent({
355
- ticketId: ticket.id,
356
- tool,
357
- prompt: blindPrompt,
358
- cwd,
359
- model: ticket.model,
360
- variant: ticket.variant,
361
- emitSuccessUpdate: false,
362
- io,
363
- runStream: blindRunStreamTarget ?? undefined,
708
+ const blindPrompt = reviewPrompt({
709
+ ticket,
710
+ diff: diffText,
711
+ memoryEnabled: Boolean(project.memory_enabled),
712
+ reviewContext,
713
+ agentRunId: blindRunStreamTarget.runId,
714
+ supplementalRequirements,
364
715
  });
365
- };
366
- const specCoverageRun = async () => {
367
- if (!specContext) {
368
- return null;
369
- }
370
- try {
371
- return await runAgent({
716
+ const blindRun = async () => {
717
+ return runAgent({
372
718
  ticketId: ticket.id,
373
719
  tool,
374
- prompt: specCoverageReviewPrompt({
375
- ticket,
376
- diff: diffText,
377
- reviewContext,
378
- specContext,
379
- agentRunId: specCoverageRunStreamTarget?.runId ?? null,
380
- supplementalRequirements,
381
- }),
720
+ prompt: blindPrompt,
382
721
  cwd,
383
722
  model: ticket.model,
384
723
  variant: ticket.variant,
385
724
  emitSuccessUpdate: false,
725
+ executionContext,
386
726
  io,
387
- runStream: specCoverageRunStreamTarget ?? undefined,
727
+ runStream: blindRunStreamTarget,
728
+ reviewProcessState,
388
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;
389
774
  }
390
- catch (err) {
391
- console.warn('[review] spec-coverage pass failed; falling back to blind-only', getErrorMessage(err));
392
- const transcriptRuns = getAgentErrorTranscriptRuns(err);
393
- return transcriptRuns
394
- ? { response: null, responseTiming: null, transcriptRuns }
395
- : null;
775
+ else {
776
+ blindResult = await blindRun();
396
777
  }
397
- };
398
- let blindResult;
399
- let specCoverageResult = null;
400
- if (specContext) {
401
- const [blindSettled, specCoverageSettled] = await Promise.allSettled([blindRun(), specCoverageRun()]);
402
- if (blindSettled.status === 'rejected') {
403
- 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;
404
792
  }
405
- blindResult = blindSettled.value;
406
- specCoverageResult = specCoverageSettled.status === 'fulfilled' ? specCoverageSettled.value : null;
407
- }
408
- else {
409
- blindResult = await blindRun();
410
- }
411
- const { response: finalResponse, responseTiming, usageSnapshot } = blindResult;
412
- const transcriptRuns = [
413
- ...(blindResult.transcriptRuns ?? []),
414
- ...(specCoverageResult?.transcriptRuns ?? []),
415
- ];
416
- const normalizedResponse = typeof finalResponse === 'string'
417
- ? normalizeReviewOutput(finalResponse)
418
- : { cleanedOutput: '', suggestions: [] };
419
- let reviewRunId = null;
420
- let parsedFindings = [];
421
- let parsedStatus = null;
422
- let persistedMessageId = null;
423
- if (normalizedResponse.cleanedOutput) {
424
- const blindParsed = parseStructuredReviewOutput(normalizedResponse.cleanedOutput);
425
- const specCoverageSpecOutput = typeof specCoverageResult?.response === 'string'
426
- ? 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
+ || ''
427
827
  : '';
428
- const specParsed = specCoverageSpecOutput
429
- ? parseStructuredReviewOutput(specCoverageSpecOutput)
430
- : null;
431
- const mergedFindings = specParsed && specParsed.status === 'completed'
432
- ? [...blindParsed.findings, ...specParsed.findings.map(finding => ({
433
- ...finding,
434
- ordinal: finding.ordinal + blindParsed.findings.length,
435
- }))]
436
- : blindParsed.findings;
437
- const combinedMarkdown = specParsed && specParsed.cleanedMarkdown
438
- ? `${blindParsed.cleanedMarkdown}\n\n## Spec-Coverage Review\n${specParsed.cleanedMarkdown}`
439
- : blindParsed.cleanedMarkdown;
440
- parsedFindings = mergedFindings;
441
- parsedStatus = blindParsed.status;
442
- persistedMessageId = createTicketMessage({
443
- ticketId: ticket.id,
444
- role: 'assistant',
445
- kind: 'final_response',
446
- content: combinedMarkdown,
447
- responseTiming,
448
- transcriptRuns: transcriptRuns.length > 0 ? transcriptRuns : null,
449
- });
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;
450
896
  if (persistedMessageId) {
451
897
  bindTicketThinkingMessage(ticket.id, persistedMessageId);
452
898
  }
453
- reviewRunId = createReviewRun({
454
- ticketId: ticket.id,
455
- projectId: ticket.project_id,
456
- worktreeBundleId: ticket.worktree_bundle_id,
457
- diffSignature,
458
- workspaceSignature: reviewContext.workspaceSignature,
459
- messageId: persistedMessageId,
460
- status: parsedStatus,
461
- });
462
- if (mergedFindings.length > 0) {
463
- insertReviewFindings(reviewRunId, mergedFindings);
464
- }
465
- }
466
- if (!persistedMessageId && transcriptRuns.length > 0) {
467
- const fallbackContent = finalResponse?.trim()
468
- || specCoverageResult?.response?.trim()
469
- || transcriptRuns.find(run => run.response_text?.trim())?.response_text?.trim()
470
- || transcriptRuns.find(run => run.thinking_log?.trim())?.thinking_log?.trim()
471
- || '';
472
- if (fallbackContent) {
473
- persistedMessageId = createTicketMessage({
474
- ticketId: ticket.id,
475
- role: 'assistant',
476
- kind: 'final_response',
477
- content: fallbackContent,
478
- responseTiming,
479
- 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,
480
904
  });
481
- if (persistedMessageId) {
482
- 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
+ }
483
957
  }
484
958
  }
485
959
  }
486
- const agentRuns = listAgentRunsForParent('ticket', ticket.id);
487
- for (const agentRun of agentRuns) {
488
- if (agentRun.session_id && tool === 'opencode') {
489
- storeTicketUsageSnapshot(ticket.id, agentRun.session_id);
960
+ catch (err) {
961
+ if (registeredReviewProcess?.stopRequested && finalizeStoppedReviewAfterFailure) {
962
+ finalizeStoppedReviewAfterFailure({ transcriptRuns: getAgentErrorTranscriptRuns(err) ?? [] });
963
+ return;
490
964
  }
491
- }
492
- if (tool === 'claude' && usageSnapshot) {
493
- storeTicketUsageSnapshotDirect(ticket.id, usageSnapshot);
494
- }
495
- clearTicketStreamingResponse(ticket.id);
496
- clearAgentRunsForParent('ticket', ticket.id);
497
- emitAgentRunsUpdated(io, 'ticket', ticket.id);
498
- updateStatus(ticket.id, 'done', null, io);
499
- const completedWithMessage = getTicket(ticket.id);
500
- if (completedWithMessage) {
501
- io.emit('ticket:updated', completedWithMessage);
502
- }
503
- const updated = getTicket(ticket.id);
504
- if (updated?.agent_status === 'done') {
505
- if (project.memory_enabled && normalizedResponse.suggestions.length > 0) {
506
- 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;
971
+ }
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;
983
+ }
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));
507
1030
  }
508
- captureStageSuccess(ticket.id, 'review');
509
- const handled = await runReviewCompletionAutomation(updated, project, reviewRunId, parsedStatus, parsedFindings, io);
510
- if (!handled) {
511
- const completed = autoParkTicketIfStale(ticket.id);
512
- if (completed) {
513
- io.emit('ticket:updated', completed);
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
+ }
514
1060
  }
515
1061
  }
1062
+ if (getTicket(acceptedTicket.id)) {
1063
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', acceptedTicket.id);
1064
+ updateStatus(acceptedTicket.id, 'error', message, io);
1065
+ }
516
1066
  }
517
- }
518
- catch (err) {
519
- const transcriptRuns = getAgentErrorTranscriptRuns(err);
520
- const errorContent = formatAgentErrorForTranscript(err);
521
- if (errorContent) {
522
- createTicketMessage({
523
- ticketId: ticket.id,
524
- role: 'assistant',
525
- kind: 'error',
526
- content: errorContent,
527
- transcriptRuns,
528
- });
1067
+ finally {
1068
+ if (registeredReviewProcess) {
1069
+ untrackReviewProcess(acceptedTicket.id, registeredReviewProcess);
1070
+ }
1071
+ if (activityRegistered) {
1072
+ unregisterBundleActivity(acceptedTicket, 'review');
1073
+ }
529
1074
  }
530
- clearAgentRunsForParent('ticket', ticket.id);
531
- emitAgentRunsUpdated(io, 'ticket', ticket.id);
532
- updateStatus(ticket.id, 'error', getErrorMessage(err), io);
533
- }
534
- finally {
535
- unregisterBundleActivity(ticket, 'review');
536
- }
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.
537
1094
  });
538
- return true;
539
- };
1095
+ return legacyResult ? true : { accepted: true, executionContext };
1096
+ }
540
1097
  //# sourceMappingURL=review.js.map