@straiffi/archon 1.8.12 → 1.8.14

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 (139) hide show
  1. package/dist/client/assets/{ActivityView-CEETnHri.js → ActivityView-DYDYyqIN.js} +1 -1
  2. package/dist/client/assets/{GitHubActionsDialog-CjBW65OR.js → GitHubActionsDialog-jhi4GOyV.js} +1 -1
  3. package/dist/client/assets/{RepositoryGraphDialog-F3C0n89W.js → RepositoryGraphDialog-BZV0vHu6.js} +1 -1
  4. package/dist/client/assets/{VerificationDialog-DD1CLydA.js → VerificationDialog-Sc6e8AKH.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-3BPJPVTR-COl_GdZ6.js → architectureDiagram-3BPJPVTR-DFbki8mi.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-GPEHLZMM-qojIlH-q.js → blockDiagram-GPEHLZMM-BZFehG8P.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-AAUBKEIU-DFtG4fMg.js → c4Diagram-AAUBKEIU-FVDXRu1t.js} +1 -1
  8. package/dist/client/assets/channel-D1kOpezG.js +1 -0
  9. package/dist/client/assets/{chunk-2J33WTMH-TJkOJXbn.js → chunk-2J33WTMH-4tuVZaT0.js} +1 -1
  10. package/dist/client/assets/{chunk-3OPIFGDE-CELQ603z.js → chunk-3OPIFGDE-DSn14S35.js} +1 -1
  11. package/dist/client/assets/{chunk-5ZQYHXKU-BgPEJ16b.js → chunk-5ZQYHXKU-Cwx1KNjg.js} +1 -1
  12. package/dist/client/assets/{chunk-727SXJPM-Biph-aPf.js → chunk-727SXJPM-Odza4hes.js} +1 -1
  13. package/dist/client/assets/{chunk-AQP2D5EJ-CPVToZH6.js → chunk-AQP2D5EJ-BmVoQT7M.js} +1 -1
  14. package/dist/client/assets/{chunk-CSCIHK7Q-BHO9oB4A.js → chunk-CSCIHK7Q-Bg7Ktx0B.js} +1 -1
  15. package/dist/client/assets/{chunk-KSCS5N6A-B8vatrko.js → chunk-KSCS5N6A-2tnhar2x.js} +1 -1
  16. package/dist/client/assets/{chunk-L5ZTLDWV-DvzBHR4-.js → chunk-L5ZTLDWV-dAyg8qMJ.js} +1 -1
  17. package/dist/client/assets/{chunk-LZXEDZCA-Bm8cj-JK.js → chunk-LZXEDZCA-0gElxrlR.js} +2 -2
  18. package/dist/client/assets/{chunk-ND2GUHAM-BPQZvPl9.js → chunk-ND2GUHAM-Bu-8Lz91.js} +1 -1
  19. package/dist/client/assets/{chunk-NZK2D7GU-DEQVA6Ya.js → chunk-NZK2D7GU-CInZFdzd.js} +1 -1
  20. package/dist/client/assets/{chunk-O5CBEL6O-hPGqWilt.js → chunk-O5CBEL6O-rAzpvdNR.js} +1 -1
  21. package/dist/client/assets/{chunk-WU5MYG2G-D0VvFXkQ.js → chunk-WU5MYG2G-DOOw2vq7.js} +1 -1
  22. package/dist/client/assets/classDiagram-4FO5ZUOK-C253nwny.js +1 -0
  23. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-DFgDA23g.js +1 -0
  24. package/dist/client/assets/{dagre-BM42HDAG-rmJlMuw_.js → dagre-BM42HDAG-Dgua1iuh.js} +1 -1
  25. package/dist/client/assets/{diagram-2AECGRRQ-BqejYr09.js → diagram-2AECGRRQ-odacwlwu.js} +1 -1
  26. package/dist/client/assets/{diagram-5GNKFQAL-DGSf1BNT.js → diagram-5GNKFQAL-BiKjFmJ1.js} +1 -1
  27. package/dist/client/assets/{diagram-KO2AKTUF-e_TRdqvx.js → diagram-KO2AKTUF-CmYcvFri.js} +1 -1
  28. package/dist/client/assets/{diagram-LMA3HP47-CTSnwgUq.js → diagram-LMA3HP47-ncx0RBuX.js} +1 -1
  29. package/dist/client/assets/{diagram-OG6HWLK6-mUn9Y0aC.js → diagram-OG6HWLK6-DbchKIzw.js} +1 -1
  30. package/dist/client/assets/{erDiagram-TEJ5UH35-TmdWUwfA.js → erDiagram-TEJ5UH35-CMnpvRyQ.js} +1 -1
  31. package/dist/client/assets/{flowDiagram-I6XJVG4X-CoE0KTIp.js → flowDiagram-I6XJVG4X-Bi02_PK5.js} +1 -1
  32. package/dist/client/assets/{ganttDiagram-6RSMTGT7-FkX1d1D7.js → ganttDiagram-6RSMTGT7-BiS992US.js} +1 -1
  33. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-CXze3DV2.js → gitGraphDiagram-PVQCEYII-Cc1hrTsT.js} +1 -1
  34. package/dist/client/assets/index-B1gdJKwK.js +275 -0
  35. package/dist/client/assets/index-CCQwWUKF.css +2 -0
  36. package/dist/client/assets/{infoDiagram-5YYISTIA-CO0CivOy.js → infoDiagram-5YYISTIA-BKVfqJCe.js} +1 -1
  37. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-pEp8gScG.js → ishikawaDiagram-YF4QCWOH-B-IbFOty.js} +1 -1
  38. package/dist/client/assets/{journeyDiagram-JHISSGLW-BAnBAX-u.js → journeyDiagram-JHISSGLW-DAZBgAQW.js} +1 -1
  39. package/dist/client/assets/{kanban-definition-UN3LZRKU-Cqe-m5fR.js → kanban-definition-UN3LZRKU-Bkr_fXev.js} +1 -1
  40. package/dist/client/assets/{line-CKXoUXdX.js → line-CROuuXDO.js} +1 -1
  41. package/dist/client/assets/{mermaid-parser.core-7XZHbu9R.js → mermaid-parser.core-DrBw_Rsc.js} +1 -1
  42. package/dist/client/assets/{mermaid.core-CsF9gbdO.js → mermaid.core-pPd6OYYj.js} +3 -3
  43. package/dist/client/assets/{mindmap-definition-RKZ34NQL-88fK1Uq_.js → mindmap-definition-RKZ34NQL-BXSd3TZU.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-4H26LBE5-DHFlSAwL.js → pieDiagram-4H26LBE5-DoT7PIZC.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-D5WyQ5EU.js → quadrantDiagram-W4KKPZXB-ycZkLowm.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DGkMfp_N.js → requirementDiagram-4Y6WPE33-BPkO1yLu.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-loJAHDiT.js → sankeyDiagram-5OEKKPKP-D7AvhhCw.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-xgcwtjyW.js → sequenceDiagram-3UESZ5HK-C982swnH.js} +1 -1
  49. package/dist/client/assets/{stateDiagram-AJRCARHV-BB95DmQX.js → stateDiagram-AJRCARHV-xvJ3u8lc.js} +1 -1
  50. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-BKQG8NCn.js +1 -0
  51. package/dist/client/assets/{timeline-definition-PNZ67QCA-OCHTEVfL.js → timeline-definition-PNZ67QCA-D89qjzb5.js} +1 -1
  52. package/dist/client/assets/{vennDiagram-CIIHVFJN-Bo_7YNy2.js → vennDiagram-CIIHVFJN-CoKeC__u.js} +1 -1
  53. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DquywRz0.js → wardleyDiagram-YWT4CUSO-DKhjzvSE.js} +1 -1
  54. package/dist/client/assets/{xychartDiagram-2RQKCTM6-BLNrbCwd.js → xychartDiagram-2RQKCTM6-DFn9ass1.js} +1 -1
  55. package/dist/client/index.html +2 -2
  56. package/dist/server/db.js +77 -2
  57. package/dist/server/db.js.map +1 -1
  58. package/dist/server/index.js +90 -17
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/lib/agent.js +205 -242
  61. package/dist/server/lib/agent.js.map +1 -1
  62. package/dist/server/lib/agentProvider.js +64 -0
  63. package/dist/server/lib/agentProvider.js.map +1 -0
  64. package/dist/server/lib/agentRuns.js +1 -0
  65. package/dist/server/lib/agentRuns.js.map +1 -1
  66. package/dist/server/lib/automationProgression.js +6 -2
  67. package/dist/server/lib/automationProgression.js.map +1 -1
  68. package/dist/server/lib/automationReconciliation.js +20 -0
  69. package/dist/server/lib/automationReconciliation.js.map +1 -1
  70. package/dist/server/lib/automationResumeDecision.js +6 -5
  71. package/dist/server/lib/automationResumeDecision.js.map +1 -1
  72. package/dist/server/lib/buildCompletionContinuation.js +1 -1
  73. package/dist/server/lib/buildCompletionContinuation.js.map +1 -1
  74. package/dist/server/lib/claudeAgentProvider.js +48 -0
  75. package/dist/server/lib/claudeAgentProvider.js.map +1 -0
  76. package/dist/server/lib/executionEnvironment.js +20 -0
  77. package/dist/server/lib/executionEnvironment.js.map +1 -0
  78. package/dist/server/lib/executionReconciliation.js +43 -4
  79. package/dist/server/lib/executionReconciliation.js.map +1 -1
  80. package/dist/server/lib/executionRetry.js +16 -9
  81. package/dist/server/lib/executionRetry.js.map +1 -1
  82. package/dist/server/lib/executionRetryPredicates.js +12 -3
  83. package/dist/server/lib/executionRetryPredicates.js.map +1 -1
  84. package/dist/server/lib/git.js +4 -4
  85. package/dist/server/lib/git.js.map +1 -1
  86. package/dist/server/lib/hostExecutionEnvironment.js +77 -0
  87. package/dist/server/lib/hostExecutionEnvironment.js.map +1 -0
  88. package/dist/server/lib/lightweightPrompt.js +17 -54
  89. package/dist/server/lib/lightweightPrompt.js.map +1 -1
  90. package/dist/server/lib/opencodeAgentProvider.js +139 -0
  91. package/dist/server/lib/opencodeAgentProvider.js.map +1 -0
  92. package/dist/server/lib/opencodeErrors.js +35 -0
  93. package/dist/server/lib/opencodeErrors.js.map +1 -0
  94. package/dist/server/lib/opencodePromptOutput.js +112 -0
  95. package/dist/server/lib/opencodePromptOutput.js.map +1 -0
  96. package/dist/server/lib/opencodeSessionOperations.js +348 -0
  97. package/dist/server/lib/opencodeSessionOperations.js.map +1 -0
  98. package/dist/server/lib/opencodeTaskSessions.js +13 -225
  99. package/dist/server/lib/opencodeTaskSessions.js.map +1 -1
  100. package/dist/server/lib/postBuildCheckRuns.js +16 -12
  101. package/dist/server/lib/postBuildCheckRuns.js.map +1 -1
  102. package/dist/server/lib/postBuildChecks.js +45 -43
  103. package/dist/server/lib/postBuildChecks.js.map +1 -1
  104. package/dist/server/lib/previewProxy.js +36 -4
  105. package/dist/server/lib/previewProxy.js.map +1 -1
  106. package/dist/server/lib/projectAutoConfig.js +27 -97
  107. package/dist/server/lib/projectAutoConfig.js.map +1 -1
  108. package/dist/server/lib/projects.js +34 -6
  109. package/dist/server/lib/projects.js.map +1 -1
  110. package/dist/server/lib/reviewContext.js +10 -5
  111. package/dist/server/lib/reviewContext.js.map +1 -1
  112. package/dist/server/lib/reviewFindings.js +201 -27
  113. package/dist/server/lib/reviewFindings.js.map +1 -1
  114. package/dist/server/lib/reviewerConfig.js +23 -0
  115. package/dist/server/lib/reviewerConfig.js.map +1 -0
  116. package/dist/server/lib/specReviewOperations.js +64 -9
  117. package/dist/server/lib/specReviewOperations.js.map +1 -1
  118. package/dist/server/lib/ticketFollowUpOperations.js +28 -0
  119. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  120. package/dist/server/lib/tickets.js +15 -0
  121. package/dist/server/lib/tickets.js.map +1 -1
  122. package/dist/server/lib/types.js.map +1 -1
  123. package/dist/server/workers/build.js +8 -1
  124. package/dist/server/workers/build.js.map +1 -1
  125. package/dist/server/workers/followUp.js +15 -6
  126. package/dist/server/workers/followUp.js.map +1 -1
  127. package/dist/server/workers/plan.js +5 -21
  128. package/dist/server/workers/plan.js.map +1 -1
  129. package/dist/server/workers/review.js +176 -70
  130. package/dist/server/workers/review.js.map +1 -1
  131. package/dist/server/workers/specReview.js +26 -11
  132. package/dist/server/workers/specReview.js.map +1 -1
  133. package/package.json +1 -1
  134. package/dist/client/assets/channel-DJLjk5--.js +0 -1
  135. package/dist/client/assets/classDiagram-4FO5ZUOK-aHXCyXE_.js +0 -1
  136. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CRPsGis_.js +0 -1
  137. package/dist/client/assets/index-B9S66NLn.css +0 -2
  138. package/dist/client/assets/index-CHsj9z-M.js +0 -275
  139. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-DKhxPOLT.js +0 -1
@@ -2,15 +2,17 @@ import db from '../db.js';
2
2
  import { assertNoActiveAutomationChild } from '../lib/activeAutomationChild.js';
3
3
  import { formatAgentErrorForTranscript, getAgentErrorTranscriptRuns, registerReviewProcess, runAgent, storeTicketUsageSnapshot, storeTicketUsageSnapshotDirect, untrackReviewProcess, } from '../lib/agent.js';
4
4
  import config from '../lib/config.js';
5
+ import { getBundlePullRequestRecord } from '../lib/bundlePullRequests.js';
5
6
  import { persistProjectMemorySuggestions } from '../lib/projectMemory.js';
6
7
  import { getReviewDiffDataAsync, resolveWorktreePath } from '../lib/git.js';
7
8
  import { getProjectMemoryPromptBlock } from '../lib/projectMemoryPrompt.js';
8
9
  import { extractProjectMemorySuggestionsFromOutput, getProjectMemorySuggestionPromptRules } from '../lib/projectMemorySuggestions.js';
9
10
  import { getEffectiveProject } from '../lib/projects.js';
11
+ import { resolveReviewerConfig } from '../lib/reviewerConfig.js';
10
12
  import { enqueue, isRunning } from '../lib/queue.js';
11
13
  import { getExecutionRun, transitionExecutionRun, withExecutionRunTransaction } from '../lib/executionRuns.js';
12
14
  import { buildReviewContext } from '../lib/reviewContext.js';
13
- import { computeDiffSignature, createReviewRun, hasPriorReviewRunForBundle, insertReviewFindings, parseStructuredReviewOutput, } from '../lib/reviewFindings.js';
15
+ import { computeDiffSignature, constrainReviewOutputToDiff, createReviewRun, finalizeReviewRun, hasPriorReviewRunForBundle, insertReviewFindings, parseStructuredReviewOutput, updateReviewRunPreparation, } from '../lib/reviewFindings.js';
14
16
  import { buildSpecCoverageContext } from '../lib/specCoverageContext.js';
15
17
  import { listTickets } from '../lib/tickets.js';
16
18
  import { registerBundleActivity, unregisterBundleActivity } from '../lib/bundleActivity.js';
@@ -26,9 +28,17 @@ import { runAutomationCommitChain } from '../lib/ticketAutomationCommit.js';
26
28
  import { submitTicketFollowUp } from '../lib/ticketFollowUpOperations.js';
27
29
  import { completeAutomationProgression, createAutomationReviewFix, ensureAutomationProgression, finishAutomationReviewFix, pauseAutomationProgression, resolveProgressionForChild, waitAutomationProgression, } from '../lib/automationProgression.js';
28
30
  import { formatBuildOpenQuestionAnswersPrompt, listAnsweredBuildOpenQuestionsForBundle } from '../lib/buildOpenQuestions.js';
31
+ import { validateWorkflowContext } from '../lib/types.js';
32
+ import { isOpencodeModelResolutionError } from '../lib/opencodeErrors.js';
29
33
  const getErrorMessage = (error) => {
30
34
  return error instanceof Error ? error.message : String(error);
31
35
  };
36
+ const getReviewerExecutionError = (error, model, variant) => {
37
+ const reviewer = variant ? `${model} (${variant})` : model;
38
+ const wrapped = new Error(`Configured reviewer ${reviewer} could not run: ${getErrorMessage(error)}`, { cause: error });
39
+ wrapped.transcriptRuns = getAgentErrorTranscriptRuns(error);
40
+ return wrapped;
41
+ };
32
42
  export const resolveReviewContinuation = (executionRunId) => {
33
43
  const runs = listAgentRunsForExecutionRun(executionRunId);
34
44
  return {
@@ -73,7 +83,7 @@ const stripSystemReminderBlocks = (output) => {
73
83
  const normalizeReviewOutput = (output) => {
74
84
  return extractProjectMemorySuggestionsFromOutput(stripSystemReminderBlocks(output));
75
85
  };
76
- const resolveStructuredReviewOutput = (parentResponse, transcriptRuns) => {
86
+ const resolveStructuredReviewOutput = (parentResponse, transcriptRuns, mainRole = 'code-review') => {
77
87
  const normalizedParent = typeof parentResponse === 'string'
78
88
  ? normalizeReviewOutput(parentResponse)
79
89
  : { cleanedOutput: '', suggestions: [] };
@@ -84,7 +94,7 @@ const resolveStructuredReviewOutput = (parentResponse, transcriptRuns) => {
84
94
  return { normalizedParent, parsed: parsedParent };
85
95
  }
86
96
  const parsedMainTranscript = transcriptRuns
87
- .filter(run => run.role === 'code-review' && run.agent_status === 'done' && run.response_text?.trim())
97
+ .filter(run => run.role === mainRole && run.agent_status === 'done' && run.response_text?.trim())
88
98
  .flatMap(run => {
89
99
  const normalized = normalizeReviewOutput(run.response_text ?? '');
90
100
  if (!normalized.cleanedOutput) {
@@ -126,6 +136,15 @@ const resolveStructuredReviewOutput = (parentResponse, transcriptRuns) => {
126
136
  },
127
137
  };
128
138
  };
139
+ const formatParsedReviewMarkdown = (parsed) => {
140
+ if (parsed.cleanedMarkdown) {
141
+ return parsed.cleanedMarkdown;
142
+ }
143
+ if (parsed.findings.length === 0) {
144
+ return '## Findings\nNo genuine issues found.';
145
+ }
146
+ return `## Findings\n${parsed.findings.map((finding, index) => (`${index + 1}. ${finding.title}\n - Severity: ${finding.severity}\n - What: ${finding.what_text}${finding.why_text ? `\n - Why: ${finding.why_text}` : ''}${finding.fix_text ? `\n - Fix: ${finding.fix_text}` : ''}`)).join('\n\n')}`;
147
+ };
129
148
  const MAX_INLINE_DIFF_LINES = 400;
130
149
  const MAX_INLINE_DIFF_CHARS = 20_000;
131
150
  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.';
@@ -205,7 +224,9 @@ const reviewPrompt = ({ ticket, diff, memoryEnabled, reviewContext, agentRunId,
205
224
  const continuationBlock = buildReviewContinuationBlock(continuation, 'blind');
206
225
  return prependSkillPromptBlock(`Review the following code changes.
207
226
 
208
- This is a code review task, not an implementation task. Focus on genuine defects: bugs, regressions, broken assumptions, missing or incorrect tests, type or runtime issues, and risky behavior changes. Infer the intended behavior from the patch itself instead of relying on any external ticket context. Review only the current project worktree changes and treat any linked projects or other repositories as read-only context.
227
+ This is a code review task, not an implementation task. Focus on genuine defects: bugs, regressions, broken assumptions, missing or incorrect tests, type or runtime issues, and risky behavior changes. Infer the intended behavior from the patch itself instead of relying on any external ticket context. The supplied diff is the authoritative review scope. Treat linked projects and other repositories as read-only context.
228
+
229
+ You may inspect unchanged files, callers, helpers, tests, and project memory only to understand the behavior and impact of changed code. Unchanged code is supporting evidence, not an independent review target. Report a finding only when it is introduced, exposed, or materially worsened by this diff; it can be anchored to a changed path and changed line responsible for the issue; and fixing it requires changing this patch rather than unrelated pre-existing code. Do not report pre-existing defects, general cleanup, architectural improvements, or issues from another branch or ticket. If unchanged code demonstrates a regression, cite it as evidence but anchor the finding to the changed line that caused the regression. When causality is uncertain, omit the finding.
209
230
 
210
231
  Before writing findings, perform this investigation workflow:
211
232
  1. ${diffPromptBlock.investigationInstruction}
@@ -213,6 +234,7 @@ Before writing findings, perform this investigation workflow:
213
234
  3. Inspect directly related code paths such as nearby helpers, callers, consumers, and tests that could be affected.
214
235
  4. Check whether the change is consistent with the project's accepted decisions and conventions.
215
236
  5. Only then report genuine issues that are supported by the code you inspected.
237
+ 6. Before returning the review, discard every candidate finding that cannot be traced directly to a changed line in the supplied diff.
216
238
 
217
239
  ${SUBAGENT_REVIEW_GUIDANCE}
218
240
 
@@ -250,7 +272,7 @@ After the markdown findings, append a machine-readable block using these exact m
250
272
  {"findings":[{"ordinal":1,"title":"...","severity":"high","file_path":"path/to/file.ts","old_line_number":12,"new_line_number":15,"line_content":"...","hunk_header":"@@ -12,3 +15,4 @@","what_text":"...","why_text":"...","fix_text":"..."}]}
251
273
  </archon-review-findings>
252
274
 
253
- Use an empty findings array when there are no genuine issues. The JSON must be valid and must not be wrapped in code fences. Escape all JSON string contents correctly. If a snippet would be awkward to escape, set \`line_content\` to null. For pure additions, set only \`new_line_number\` and use \`old_line_number\`: null. For pure deletions, set only \`old_line_number\` and use \`new_line_number\`: null.${getProjectMemorySuggestionPromptRules(memoryEnabled, 'review') ? `
275
+ Every finding must set \`file_path\` to a path in \`<review_scope>\` and set at least one line number identifying a changed diff line responsible for the issue. Do not use an unchanged context line merely because it is near the defect. Use an empty findings array when there are no in-scope genuine issues. The JSON must be valid and must not be wrapped in code fences. Escape all JSON string contents correctly. If a snippet would be awkward to escape, set \`line_content\` to null. For pure additions, set only \`new_line_number\` and use \`old_line_number\`: null. For pure deletions, set only \`old_line_number\` and use \`new_line_number\`: null.${getProjectMemorySuggestionPromptRules(memoryEnabled, 'review') ? `
254
276
 
255
277
  ${getProjectMemorySuggestionPromptRules(memoryEnabled, 'review')}` : ''}
256
278
 
@@ -268,13 +290,16 @@ const specCoverageReviewPrompt = ({ ticket, diff, reviewContext, specContext, ag
268
290
  });
269
291
  const diffPromptBlock = buildDiffPromptBlock(diff);
270
292
  const continuationBlock = buildReviewContinuationBlock(continuation, 'spec-coverage');
271
- return prependSkillPromptBlock(`Verify the implementation against this specification. Focus on missing acceptance criteria, unhandled edge cases, and scope drift — not generic code-quality defects already covered by the blind review.
293
+ return prependSkillPromptBlock(`Verify the implementation against this specification. Focus on missing acceptance criteria, unhandled edge cases, and scope drift — not generic code-quality defects already covered by the blind review. The supplied diff is the authoritative review scope.
294
+
295
+ You may inspect unchanged code to understand the implementation, but it is supporting evidence rather than an independent review target. Report only conformance gaps introduced by, exposed by, or expected to be implemented in this diff, and anchor every finding to the changed path and changed line responsible. Do not report unrelated missing functionality, pre-existing defects, or cleanup opportunities. When causality or patch responsibility is uncertain, omit the finding.
272
296
 
273
297
  Before writing findings, perform this investigation workflow:
274
298
  1. ${diffPromptBlock.investigationInstruction}
275
299
  2. Read the full contents of every changed file that matters to the spec, not just the diff hunk.
276
300
  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?
277
301
  4. Only then report genuine conformance gaps that are supported by the code you inspected.
302
+ 5. Before returning the review, discard every candidate finding that cannot be traced directly to a changed line in the supplied diff.
278
303
 
279
304
  ${SUBAGENT_REVIEW_GUIDANCE}
280
305
 
@@ -314,7 +339,7 @@ After the markdown findings, append a machine-readable block using these exact m
314
339
  {"findings":[{"ordinal":1,"title":"...","severity":"high","file_path":"path/to/file.ts","old_line_number":12,"new_line_number":15,"line_content":"...","hunk_header":"@@ -12,3 +15,4 @@","what_text":"...","why_text":"...","fix_text":"..."}]}
315
340
  </archon-review-findings>
316
341
 
317
- Use an empty findings array when there are no genuine issues. The JSON must be valid and must not be wrapped in code fences. Escape all JSON string contents correctly. If a snippet would be awkward to escape, set \`line_content\` to null. For pure additions, set only \`new_line_number\` and use \`old_line_number\`: null. For pure deletions, set only \`old_line_number\` and use \`new_line_number\`: null.
342
+ Every finding must set \`file_path\` to a path in \`<review_scope>\` and set at least one line number identifying a changed diff line responsible for the conformance gap. Do not use an unchanged context line merely because it is related. Use an empty findings array when there are no in-scope genuine issues. The JSON must be valid and must not be wrapped in code fences. Escape all JSON string contents correctly. If a snippet would be awkward to escape, set \`line_content\` to null. For pure additions, set only \`new_line_number\` and use \`old_line_number\`: null. For pure deletions, set only \`old_line_number\` and use \`new_line_number\`: null.
318
343
 
319
344
  Do not include planning text, implementation steps, or commentary about changing modes.`, ticket.skills);
320
345
  };
@@ -378,6 +403,9 @@ const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parse
378
403
  return true;
379
404
  }
380
405
  case 'fix_iteration': {
406
+ if (!reviewRunId) {
407
+ throw new Error('Review fix cannot start without persisted review findings');
408
+ }
381
409
  const previousIteration = bundleState?.review_iteration_count ?? 0;
382
410
  const previousFixLoopActive = bundleState?.fix_loop_active ?? 0;
383
411
  upsertBundleAutomationState(projectId, bundleId, {
@@ -385,6 +413,11 @@ const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parse
385
413
  fix_loop_active: 1,
386
414
  });
387
415
  const instruction = buildAutomationFixInstruction(config.reviewSeverity);
416
+ const reviewFixWorkflowContext = validateWorkflowContext({
417
+ ...(initiation.workflowContext ?? {}),
418
+ review_finding_run_id: reviewRunId,
419
+ review_severity: config.reviewSeverity,
420
+ });
388
421
  let reviewFix = null;
389
422
  try {
390
423
  reviewFix = progression
@@ -394,8 +427,10 @@ const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parse
394
427
  ticketId: ticket.id,
395
428
  ticketTitle: ticket.title,
396
429
  progressionRunId: progression.id,
397
- workflowContext: initiation.workflowContext,
430
+ workflowContext: reviewFixWorkflowContext,
398
431
  triggeringReviewRunId: executionContext.runId,
432
+ reviewFindingRunId: reviewRunId,
433
+ reviewSeverity: config.reviewSeverity,
399
434
  broadcaster: asGenericEmitter(io),
400
435
  })
401
436
  : null;
@@ -416,7 +451,7 @@ const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parse
416
451
  initiation: {
417
452
  initiator: 'automation',
418
453
  causalParentRunId: reviewFix?.runId ?? executionContext.runId,
419
- workflowContext: initiation.workflowContext,
454
+ workflowContext: reviewFixWorkflowContext,
420
455
  },
421
456
  });
422
457
  if (!started.ok) {
@@ -613,6 +648,7 @@ export function startReview(ticket, io, options) {
613
648
  }
614
649
  let activityRegistered = false;
615
650
  let registeredReviewProcess = null;
651
+ let activeReviewRunId = null;
616
652
  let finalizeStoppedReviewAfterFailure = null;
617
653
  try {
618
654
  registerBundleActivity(acceptedTicket, 'review');
@@ -626,7 +662,17 @@ export function startReview(ticket, io, options) {
626
662
  throw new Error('Ticket project no longer exists');
627
663
  }
628
664
  const cwd = currentTicket.bundle?.kind === 'project_root' ? project.repo_path : resolveWorktreePath(branch, project);
629
- const tool = resolveTicketTool(ticket, config.tool ?? 'opencode');
665
+ const tool = resolveTicketTool(currentTicket, config.tool ?? 'opencode');
666
+ const reviewerConfig = tool === 'opencode'
667
+ ? resolveReviewerConfig(currentTicket.bundle, project, {
668
+ model: currentTicket.model,
669
+ variant: currentTicket.variant,
670
+ })
671
+ : {
672
+ model: currentTicket.model,
673
+ variant: currentTicket.model ? currentTicket.variant : null,
674
+ source: 'fallback',
675
+ };
630
676
  const reviewProcessState = registerReviewProcess(acceptedTicket.id);
631
677
  registeredReviewProcess = reviewProcessState;
632
678
  const createdAgentRunIds = [];
@@ -679,6 +725,14 @@ export function startReview(ticket, io, options) {
679
725
  if (messageId) {
680
726
  bindTicketThinkingMessage(ticket.id, messageId);
681
727
  }
728
+ if (activeReviewRunId) {
729
+ finalizeReviewRun({
730
+ reviewRunId: activeReviewRunId,
731
+ status: 'stopped',
732
+ messageId,
733
+ failureMessage: 'Review was stopped by the user',
734
+ });
735
+ }
682
736
  db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
683
737
  .run('stopped', ticket.id, executionContext.projectId);
684
738
  terminalizeAgentRunsForExecutionRun(executionContext.runId, 'stopped', null);
@@ -723,7 +777,6 @@ export function startReview(ticket, io, options) {
723
777
  finalizeStoppedReview();
724
778
  return true;
725
779
  };
726
- beginTicketThinkingRun(ticket.id);
727
780
  withExecutionRunTransaction(transaction => {
728
781
  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);
729
782
  transaction.transitionExecutionRun({
@@ -733,19 +786,59 @@ export function startReview(ticket, io, options) {
733
786
  domainStatus: 'running',
734
787
  eventType: 'review_started',
735
788
  });
789
+ activeReviewRunId = createReviewRun({
790
+ ticketId: currentTicket.id,
791
+ projectId: currentTicket.project_id,
792
+ worktreeBundleId: currentTicket.worktree_bundle_id,
793
+ diffSignature: null,
794
+ workspaceSignature: null,
795
+ messageId: null,
796
+ model: reviewerConfig.model,
797
+ variant: reviewerConfig.variant,
798
+ status: 'running',
799
+ });
800
+ transaction.addExecutionRunLink({
801
+ projectId: executionContext.projectId,
802
+ runId: executionContext.runId,
803
+ link: {
804
+ entityType: 'review_run',
805
+ entityId: activeReviewRunId,
806
+ relationship: 'result',
807
+ expectedParent: { entityType: 'ticket', entityId: currentTicket.id },
808
+ },
809
+ });
736
810
  }, asGenericEmitter(io));
811
+ beginTicketThinkingRun(ticket.id);
737
812
  updateStatus(ticket.id, 'running', 'Preparing review baseline...', io, 'review');
738
813
  await captureStageBaseline(ticket.id, 'review');
739
814
  if (stopReviewBeforeAgentIfRequested()) {
740
815
  return;
741
816
  }
742
817
  updateStatus(ticket.id, 'running', 'Computing review diff...', io);
743
- const diffText = (await getReviewDiffDataAsync({ cwd, branch, project })).diff;
818
+ const trackedPullRequest = ticket.project_id && ticket.worktree_bundle_id
819
+ ? getBundlePullRequestRecord(ticket.worktree_bundle_id, ticket.project_id)
820
+ : null;
821
+ const diffData = await getReviewDiffDataAsync({
822
+ cwd,
823
+ branch,
824
+ project,
825
+ pullRequestBaseBranch: trackedPullRequest?.baseBranch ?? null,
826
+ });
827
+ const diffText = diffData.diff;
744
828
  if (stopReviewBeforeAgentIfRequested()) {
745
829
  return;
746
830
  }
747
- const reviewContext = buildReviewContext({ cwd, branch, project });
831
+ const reviewContext = buildReviewContext({
832
+ cwd,
833
+ branch,
834
+ project,
835
+ diffData,
836
+ baseBranch: trackedPullRequest?.baseBranch ?? null,
837
+ });
748
838
  const diffSignature = computeDiffSignature(diffText);
839
+ if (!activeReviewRunId || !updateReviewRunPreparation(activeReviewRunId, diffSignature, reviewContext.workspaceSignature)) {
840
+ throw new Error('The running review record could not be updated after diff preparation');
841
+ }
749
842
  const supplementalRequirements = ticket.project_id && ticket.worktree_bundle_id
750
843
  ? formatBuildOpenQuestionAnswersPrompt(listAnsweredBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id))
751
844
  : '';
@@ -794,20 +887,28 @@ export function startReview(ticket, io, options) {
794
887
  continuation: options?.continuation,
795
888
  });
796
889
  const blindRun = async () => {
797
- return runAgent({
798
- ticketId: ticket.id,
799
- tool,
800
- prompt: blindPrompt,
801
- cwd,
802
- model: ticket.model,
803
- variant: ticket.variant,
804
- resumeSessionId: options?.continuation?.blindSessionId ?? null,
805
- emitSuccessUpdate: false,
806
- executionContext,
807
- io,
808
- runStream: blindRunStreamTarget,
809
- reviewProcessState,
810
- });
890
+ try {
891
+ return await runAgent({
892
+ ticketId: ticket.id,
893
+ tool,
894
+ prompt: blindPrompt,
895
+ cwd,
896
+ model: reviewerConfig.model,
897
+ variant: reviewerConfig.variant,
898
+ resumeSessionId: options?.continuation?.blindSessionId ?? null,
899
+ emitSuccessUpdate: false,
900
+ executionContext,
901
+ io,
902
+ runStream: blindRunStreamTarget,
903
+ reviewProcessState,
904
+ });
905
+ }
906
+ catch (error) {
907
+ if (tool === 'opencode' && reviewerConfig.model && isOpencodeModelResolutionError(error)) {
908
+ throw getReviewerExecutionError(error, reviewerConfig.model, reviewerConfig.variant);
909
+ }
910
+ throw error;
911
+ }
811
912
  };
812
913
  const specCoverageRun = async () => {
813
914
  if (!specContext) {
@@ -827,8 +928,8 @@ export function startReview(ticket, io, options) {
827
928
  continuation: options?.continuation,
828
929
  }),
829
930
  cwd,
830
- model: ticket.model,
831
- variant: ticket.variant,
931
+ model: reviewerConfig.model,
932
+ variant: reviewerConfig.variant,
832
933
  resumeSessionId: options?.continuation?.specCoverageSessionId ?? null,
833
934
  emitSuccessUpdate: false,
834
935
  executionContext,
@@ -852,8 +953,11 @@ export function startReview(ticket, io, options) {
852
953
  if (blindSettled.status === 'rejected') {
853
954
  throw blindSettled.reason;
854
955
  }
956
+ if (specCoverageSettled.status === 'rejected') {
957
+ throw specCoverageSettled.reason;
958
+ }
855
959
  blindResult = blindSettled.value;
856
- specCoverageResult = specCoverageSettled.status === 'fulfilled' ? specCoverageSettled.value : null;
960
+ specCoverageResult = specCoverageSettled.value;
857
961
  }
858
962
  else {
859
963
  blindResult = await blindRun();
@@ -874,30 +978,32 @@ export function startReview(ticket, io, options) {
874
978
  return;
875
979
  }
876
980
  const { normalizedParent: normalizedResponse, parsed: blindParsed, } = resolveStructuredReviewOutput(finalResponse, blindResult.transcriptRuns ?? []);
877
- let reviewRunId = null;
981
+ const constrainedBlindParsed = blindParsed
982
+ ? constrainReviewOutputToDiff(blindParsed, diffText)
983
+ : null;
984
+ let reviewRunId = activeReviewRunId;
878
985
  let parsedFindings = [];
879
986
  let parsedStatus = null;
880
987
  let persistedMessageId = null;
881
988
  let reviewContent = null;
882
989
  let findingsToPersist = [];
883
- if (blindParsed?.cleanedMarkdown) {
884
- const specCoverageSpecOutput = typeof specCoverageResult?.response === 'string'
885
- ? stripSystemReminderBlocks(specCoverageResult.response)
886
- : '';
887
- const specParsed = specCoverageSpecOutput
888
- ? parseStructuredReviewOutput(specCoverageSpecOutput)
990
+ if (constrainedBlindParsed?.status === 'completed') {
991
+ const specOutput = resolveStructuredReviewOutput(specCoverageResult?.response ?? null, specCoverageResult?.transcriptRuns ?? [], 'spec-coverage').parsed;
992
+ const specParsed = specOutput
993
+ ? constrainReviewOutputToDiff(specOutput, diffText)
889
994
  : null;
890
995
  const mergedFindings = specParsed && specParsed.status === 'completed'
891
- ? [...blindParsed.findings, ...specParsed.findings.map(finding => ({
996
+ ? [...constrainedBlindParsed.findings, ...specParsed.findings.map(finding => ({
892
997
  ...finding,
893
- ordinal: finding.ordinal + blindParsed.findings.length,
998
+ ordinal: finding.ordinal + constrainedBlindParsed.findings.length,
894
999
  }))]
895
- : blindParsed.findings;
896
- const combinedMarkdown = specParsed && specParsed.cleanedMarkdown
897
- ? `${blindParsed.cleanedMarkdown}\n\n## Spec-Coverage Review\n${specParsed.cleanedMarkdown}`
898
- : blindParsed.cleanedMarkdown;
1000
+ : constrainedBlindParsed.findings;
1001
+ const blindMarkdown = formatParsedReviewMarkdown(constrainedBlindParsed);
1002
+ const combinedMarkdown = specParsed?.status === 'completed'
1003
+ ? `${blindMarkdown}\n\n## Spec-Coverage Review\n${formatParsedReviewMarkdown(specParsed)}`
1004
+ : blindMarkdown;
899
1005
  parsedFindings = mergedFindings;
900
- parsedStatus = blindParsed.status;
1006
+ parsedStatus = constrainedBlindParsed.status;
901
1007
  reviewContent = combinedMarkdown;
902
1008
  findingsToPersist = mergedFindings;
903
1009
  }
@@ -922,19 +1028,15 @@ export function startReview(ticket, io, options) {
922
1028
  transcriptRuns: transcriptRuns.length > 0 ? transcriptRuns : null,
923
1029
  })
924
1030
  : null;
925
- let createdReviewRunId = null;
926
- if (messageId && reviewContent && parsedStatus) {
927
- createdReviewRunId = createReviewRun({
928
- ticketId: ticket.id,
929
- projectId: ticket.project_id,
930
- worktreeBundleId: ticket.worktree_bundle_id,
931
- diffSignature,
932
- workspaceSignature: reviewContext.workspaceSignature,
1031
+ if (activeReviewRunId) {
1032
+ finalizeReviewRun({
1033
+ reviewRunId: activeReviewRunId,
1034
+ status: parsedStatus ?? 'parse_failed',
933
1035
  messageId,
934
- status: parsedStatus,
1036
+ failureMessage: parseFailed ? 'Review output could not be parsed' : null,
935
1037
  });
936
1038
  if (findingsToPersist.length > 0) {
937
- insertReviewFindings(createdReviewRunId, findingsToPersist);
1039
+ insertReviewFindings(activeReviewRunId, findingsToPersist);
938
1040
  }
939
1041
  }
940
1042
  if (messageId) {
@@ -949,18 +1051,6 @@ export function startReview(ticket, io, options) {
949
1051
  },
950
1052
  });
951
1053
  }
952
- if (createdReviewRunId) {
953
- transaction.addExecutionRunLink({
954
- projectId: executionContext.projectId,
955
- runId: executionContext.runId,
956
- link: {
957
- entityType: 'review_run',
958
- entityId: createdReviewRunId,
959
- relationship: 'result',
960
- expectedParent: { entityType: 'ticket', entityId: ticket.id },
961
- },
962
- });
963
- }
964
1054
  db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
965
1055
  .run('done', ticket.id, executionContext.projectId);
966
1056
  terminalizeAgentRunsForExecutionRun(executionContext.runId, parseFailed ? 'error' : 'done', parseFailed ? 'Review output could not be parsed' : null);
@@ -972,7 +1062,7 @@ export function startReview(ticket, io, options) {
972
1062
  ...(parseFailed ? { failure: { code: 'parse_failed', message: 'Review output could not be parsed' } } : {}),
973
1063
  eventType: parseFailed ? 'review_parse_failed' : 'review_completed',
974
1064
  });
975
- return { messageId, reviewRunId: createdReviewRunId };
1065
+ return { messageId, reviewRunId: activeReviewRunId };
976
1066
  }, asGenericEmitter(io));
977
1067
  persistedMessageId = persisted.messageId;
978
1068
  reviewRunId = persisted.reviewRunId;
@@ -990,8 +1080,8 @@ export function startReview(ticket, io, options) {
990
1080
  if (tool === 'claude' && usageSnapshot) {
991
1081
  storeTicketUsageSnapshotDirect(ticket.id, usageSnapshot);
992
1082
  }
993
- emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
994
1083
  updateStatus(ticket.id, 'done', null, io);
1084
+ emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
995
1085
  const completedWithMessage = getTicket(ticket.id);
996
1086
  if (completedWithMessage) {
997
1087
  io.emit('ticket:updated', completedWithMessage);
@@ -1075,8 +1165,9 @@ export function startReview(ticket, io, options) {
1075
1165
  }
1076
1166
  try {
1077
1167
  withExecutionRunTransaction(transaction => {
1168
+ let errorMessageId = null;
1078
1169
  if (errorContent) {
1079
- const messageId = createTicketMessage({
1170
+ errorMessageId = createTicketMessage({
1080
1171
  ticketId: acceptedTicket.id,
1081
1172
  role: 'assistant',
1082
1173
  kind: 'error',
@@ -1088,7 +1179,7 @@ export function startReview(ticket, io, options) {
1088
1179
  runId: executionContext.runId,
1089
1180
  link: {
1090
1181
  entityType: 'ticket_message',
1091
- entityId: messageId,
1182
+ entityId: errorMessageId,
1092
1183
  relationship: 'result',
1093
1184
  expectedParent: { entityType: 'ticket', entityId: acceptedTicket.id },
1094
1185
  },
@@ -1097,6 +1188,14 @@ export function startReview(ticket, io, options) {
1097
1188
  db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
1098
1189
  .run('error', message, acceptedTicket.id, executionContext.projectId);
1099
1190
  terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
1191
+ if (activeReviewRunId) {
1192
+ finalizeReviewRun({
1193
+ reviewRunId: activeReviewRunId,
1194
+ status: 'error',
1195
+ messageId: errorMessageId,
1196
+ failureMessage: message,
1197
+ });
1198
+ }
1100
1199
  transaction.transitionExecutionRun({
1101
1200
  projectId: executionContext.projectId,
1102
1201
  runId: executionContext.runId,
@@ -1123,6 +1222,13 @@ export function startReview(ticket, io, options) {
1123
1222
  const currentExecutionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
1124
1223
  if (currentExecutionRun && ['queued', 'running', 'waiting'].includes(currentExecutionRun.status)) {
1125
1224
  terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
1225
+ if (activeReviewRunId) {
1226
+ finalizeReviewRun({
1227
+ reviewRunId: activeReviewRunId,
1228
+ status: 'error',
1229
+ failureMessage: message,
1230
+ });
1231
+ }
1126
1232
  transitionExecutionRun({
1127
1233
  projectId: executionContext.projectId,
1128
1234
  runId: executionContext.runId,