@straiffi/archon 1.7.2 → 1.7.4
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.
- package/dist/client/assets/ActivityView-BgMz3NNT.js +1 -0
- package/dist/client/assets/GitHubActionsDialog-CNqWDS3E.js +1 -0
- package/dist/client/assets/TestsDialog-Cll7xJe0.js +4 -0
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-Do9nbC8v.js → architectureDiagram-3BPJPVTR-BL5dYiul.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-DGGOLHF8.js → blockDiagram-GPEHLZMM-Bi5x2v80.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-Bz7tQybs.js → c4Diagram-AAUBKEIU-esEm54Ay.js} +1 -1
- package/dist/client/assets/channel-CqNHF3FD.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-CJO-GmWJ.js → chunk-2J33WTMH-DACfY8xM.js} +1 -1
- package/dist/client/assets/{chunk-3OPIFGDE-DPAVagpC.js → chunk-3OPIFGDE-DZNUiaUs.js} +1 -1
- package/dist/client/assets/{chunk-5ZQYHXKU-Cof75Mu7.js → chunk-5ZQYHXKU-ClDfkMAp.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-COuPqQ8_.js → chunk-727SXJPM-CCwA3GWT.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-CFj5KabW.js → chunk-AQP2D5EJ-Byl4DRZ9.js} +1 -1
- package/dist/client/assets/{chunk-CSCIHK7Q-C4M6w5cT.js → chunk-CSCIHK7Q-CXymZQ7A.js} +1 -1
- package/dist/client/assets/{chunk-KSCS5N6A-ByaiwVue.js → chunk-KSCS5N6A-VV3D8g6a.js} +1 -1
- package/dist/client/assets/{chunk-L5ZTLDWV-Cx5jeot9.js → chunk-L5ZTLDWV-BL9N5GRI.js} +1 -1
- package/dist/client/assets/{chunk-LZXEDZCA-DmGtztud.js → chunk-LZXEDZCA-DKNN-zno.js} +2 -2
- package/dist/client/assets/{chunk-ND2GUHAM-i3yuspqL.js → chunk-ND2GUHAM-CHCpQ1yr.js} +1 -1
- package/dist/client/assets/{chunk-NZK2D7GU-CQ4E_jP4.js → chunk-NZK2D7GU-B0Dm74mt.js} +1 -1
- package/dist/client/assets/{chunk-O5CBEL6O-D2g4VDCd.js → chunk-O5CBEL6O-DAa5z2xG.js} +1 -1
- package/dist/client/assets/{chunk-WU5MYG2G-exIu-zQ2.js → chunk-WU5MYG2G-DrVJWKVR.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-BjEgQwKg.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-gGomH-F_.js +1 -0
- package/dist/client/assets/{dagre-BM42HDAG-DvF75yLZ.js → dagre-BM42HDAG-B0fsl8mE.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-DFv0HxLL.js → diagram-2AECGRRQ-Cx_VEbSS.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-C_LRir0-.js → diagram-5GNKFQAL-Cv7VVn5r.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-DGZxRDRy.js → diagram-KO2AKTUF-C8Sl5czV.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-UXt0uti4.js → diagram-LMA3HP47-C3dZaLz2.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-CFbZS4n5.js → diagram-OG6HWLK6-BAU9DtTr.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-CfAwZDdf.js → erDiagram-TEJ5UH35-DpNc-eBD.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Cdgi5z3n.js → flowDiagram-I6XJVG4X-DGwTAT1S.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-DOrkFkhu.js → ganttDiagram-6RSMTGT7-MPZhEgzJ.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-cEox0peD.js → gitGraphDiagram-PVQCEYII-CY0tHL5N.js} +1 -1
- package/dist/client/assets/index-CJRgmWwY.js +273 -0
- package/dist/client/assets/index-mRkJWu70.css +2 -0
- package/dist/client/assets/{infoDiagram-5YYISTIA-gm51u2Nn.js → infoDiagram-5YYISTIA-BBZeKjf4.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CjnoOIja.js → ishikawaDiagram-YF4QCWOH-CM_iFrES.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-Dlg4DsiO.js → journeyDiagram-JHISSGLW-Do_lY94X.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BU0XZaSd.js → kanban-definition-UN3LZRKU-brxg3j37.js} +1 -1
- package/dist/client/assets/{line-Bert22-_.js → line-PHHbm9rj.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-BvQSGDmL.js → mermaid-parser.core-CLxAkW69.js} +1 -1
- package/dist/client/assets/{mermaid.core--qSAHJRj.js → mermaid.core-CebvSmx9.js} +3 -3
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-BgFIvWWD.js → mindmap-definition-RKZ34NQL-CX_-ZaJF.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DGcbXD0M.js → pieDiagram-4H26LBE5-BJuHvql_.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-ClkBUeia.js → quadrantDiagram-W4KKPZXB-DiJll7l9.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-B8CwGToU.js → requirementDiagram-4Y6WPE33-Bg-tXgcj.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DTk3jL6B.js → sankeyDiagram-5OEKKPKP-BoK2egfZ.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQVrcnKf.js → sequenceDiagram-3UESZ5HK-DcrDWNzv.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-DD4yMZPB.js → stateDiagram-AJRCARHV-B995qt1S.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Dn8P7Sa1.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-BnCBVqcd.js → timeline-definition-PNZ67QCA-DqSVAhqL.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-BDmLMNXS.js → vennDiagram-CIIHVFJN-D9wVraVY.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-DSLuQiEv.js → wardleyDiagram-YWT4CUSO-CLff9zVd.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-DAq_LA4r.js → xychartDiagram-2RQKCTM6-BuHUMMH2.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/db.js +472 -39
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.js +1311 -212
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/agent.js +228 -71
- package/dist/server/lib/agent.js.map +1 -1
- package/dist/server/lib/agentRuns.js +205 -38
- package/dist/server/lib/agentRuns.js.map +1 -1
- package/dist/server/lib/attachments.js +24 -0
- package/dist/server/lib/attachments.js.map +1 -1
- package/dist/server/lib/automationProgression.js +464 -0
- package/dist/server/lib/automationProgression.js.map +1 -0
- package/dist/server/lib/automationReconciliation.js +498 -0
- package/dist/server/lib/automationReconciliation.js.map +1 -0
- package/dist/server/lib/automationStageDetails.js +232 -0
- package/dist/server/lib/automationStageDetails.js.map +1 -0
- package/dist/server/lib/buildOpenQuestionOperations.js +238 -0
- package/dist/server/lib/buildOpenQuestionOperations.js.map +1 -0
- package/dist/server/lib/buildOpenQuestions.js +44 -0
- package/dist/server/lib/buildOpenQuestions.js.map +1 -1
- package/dist/server/lib/buildSequences.js +130 -46
- package/dist/server/lib/buildSequences.js.map +1 -1
- package/dist/server/lib/bundleReviewOperations.js +91 -9
- package/dist/server/lib/bundleReviewOperations.js.map +1 -1
- package/dist/server/lib/chatOperations.js +67 -12
- package/dist/server/lib/chatOperations.js.map +1 -1
- package/dist/server/lib/executionReconciliation.js +518 -0
- package/dist/server/lib/executionReconciliation.js.map +1 -0
- package/dist/server/lib/executionRetry.js +694 -0
- package/dist/server/lib/executionRetry.js.map +1 -0
- package/dist/server/lib/executionRetryContext.js +489 -0
- package/dist/server/lib/executionRetryContext.js.map +1 -0
- package/dist/server/lib/executionRetryPredicates.js +74 -0
- package/dist/server/lib/executionRetryPredicates.js.map +1 -0
- package/dist/server/lib/executionRunRetention.js +141 -0
- package/dist/server/lib/executionRunRetention.js.map +1 -0
- package/dist/server/lib/executionRuns.js +1367 -0
- package/dist/server/lib/executionRuns.js.map +1 -0
- package/dist/server/lib/executionTimeline.js +45 -0
- package/dist/server/lib/executionTimeline.js.map +1 -0
- package/dist/server/lib/git.js +385 -6
- package/dist/server/lib/git.js.map +1 -1
- package/dist/server/lib/integrations/github.js +43 -0
- package/dist/server/lib/integrations/github.js.map +1 -1
- package/dist/server/lib/integrations/githubActions.js +1 -0
- package/dist/server/lib/integrations/githubActions.js.map +1 -1
- package/dist/server/lib/integrations/githubActionsSessions.js +337 -114
- package/dist/server/lib/integrations/githubActionsSessions.js.map +1 -1
- package/dist/server/lib/projectMemorySelections.js +7 -3
- package/dist/server/lib/projectMemorySelections.js.map +1 -1
- package/dist/server/lib/shutdown.js +2 -2
- package/dist/server/lib/shutdown.js.map +1 -1
- package/dist/server/lib/specArtifact.js +3 -0
- package/dist/server/lib/specArtifact.js.map +1 -1
- package/dist/server/lib/specFormatting.js +1 -1
- package/dist/server/lib/specFormatting.js.map +1 -1
- package/dist/server/lib/specOperations.js +78 -12
- package/dist/server/lib/specOperations.js.map +1 -1
- package/dist/server/lib/specReviewContext.js +54 -18
- package/dist/server/lib/specReviewContext.js.map +1 -1
- package/dist/server/lib/specReviewDraft.js +3 -2
- package/dist/server/lib/specReviewDraft.js.map +1 -1
- package/dist/server/lib/specReviewOperations.js +74 -9
- package/dist/server/lib/specReviewOperations.js.map +1 -1
- package/dist/server/lib/specReviewValidation.js +217 -0
- package/dist/server/lib/specReviewValidation.js.map +1 -0
- package/dist/server/lib/specReviews.js +30 -2
- package/dist/server/lib/specReviews.js.map +1 -1
- package/dist/server/lib/specs.js +18 -1
- package/dist/server/lib/specs.js.map +1 -1
- package/dist/server/lib/stageSnapshots.js +197 -0
- package/dist/server/lib/stageSnapshots.js.map +1 -0
- package/dist/server/lib/testSessions.js +160 -18
- package/dist/server/lib/testSessions.js.map +1 -1
- package/dist/server/lib/ticketAutomation.js +80 -4
- package/dist/server/lib/ticketAutomation.js.map +1 -1
- package/dist/server/lib/ticketAutomationCommit.js +466 -91
- package/dist/server/lib/ticketAutomationCommit.js.map +1 -1
- package/dist/server/lib/ticketFollowUpOperations.js +270 -29
- package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
- package/dist/server/lib/ticketMessages.js +16 -0
- package/dist/server/lib/ticketMessages.js.map +1 -1
- package/dist/server/lib/ticketRunOperations.js +51 -6
- package/dist/server/lib/ticketRunOperations.js.map +1 -1
- package/dist/server/lib/ticketUndo.js +77 -168
- package/dist/server/lib/ticketUndo.js.map +1 -1
- package/dist/server/lib/ticketWorkflowOperations.js +132 -28
- package/dist/server/lib/ticketWorkflowOperations.js.map +1 -1
- package/dist/server/lib/tickets.js +41 -27
- package/dist/server/lib/tickets.js.map +1 -1
- package/dist/server/lib/transcriptRuns.js +3 -3
- package/dist/server/lib/transcriptRuns.js.map +1 -1
- package/dist/server/lib/turnExecutionErrors.js +12 -0
- package/dist/server/lib/turnExecutionErrors.js.map +1 -0
- package/dist/server/lib/types.js +58 -1
- package/dist/server/lib/types.js.map +1 -1
- package/dist/server/workers/build.js +596 -184
- package/dist/server/workers/build.js.map +1 -1
- package/dist/server/workers/chat.js +269 -132
- package/dist/server/workers/chat.js.map +1 -1
- package/dist/server/workers/followUp.js +731 -203
- package/dist/server/workers/followUp.js.map +1 -1
- package/dist/server/workers/plan.js +546 -188
- package/dist/server/workers/plan.js.map +1 -1
- package/dist/server/workers/planFollowUp.js +240 -75
- package/dist/server/workers/planFollowUp.js.map +1 -1
- package/dist/server/workers/review.js +790 -297
- package/dist/server/workers/review.js.map +1 -1
- package/dist/server/workers/specReview.js +212 -46
- package/dist/server/workers/specReview.js.map +1 -1
- package/dist/server/workers/specSession.js +434 -254
- package/dist/server/workers/specSession.js.map +1 -1
- package/package.json +1 -1
- package/dist/client/assets/GitHubActionsDialog-DAD5REmY.js +0 -1
- package/dist/client/assets/TestsDialog-B5NVrroq.js +0 -4
- package/dist/client/assets/TicketChangesModal-By-_gTGd.js +0 -1
- package/dist/client/assets/channel-Dd5Len0X.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-B7KUg6r8.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CQm-z5c4.js +0 -1
- package/dist/client/assets/index-BHq6-Eir.js +0 -270
- package/dist/client/assets/index-BxTc5qQT.css +0 -2
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-C8yO6POT.js +0 -1
|
@@ -7,6 +7,7 @@ import { getProjectMemoryPromptBlock } from '../lib/projectMemoryPrompt.js';
|
|
|
7
7
|
import { extractProjectMemorySuggestionsFromOutput, getProjectMemorySuggestionPromptRules } from '../lib/projectMemorySuggestions.js';
|
|
8
8
|
import { getEffectiveProject } from '../lib/projects.js';
|
|
9
9
|
import { enqueue, isRunning } from '../lib/queue.js';
|
|
10
|
+
import { getExecutionRun, transitionExecutionRun, withExecutionRunTransaction } from '../lib/executionRuns.js';
|
|
10
11
|
import { buildReviewContext } from '../lib/reviewContext.js';
|
|
11
12
|
import { computeDiffSignature, createReviewRun, hasPriorReviewRunForBundle, insertReviewFindings, parseStructuredReviewOutput, } from '../lib/reviewFindings.js';
|
|
12
13
|
import { buildSpecCoverageContext } from '../lib/specCoverageContext.js';
|
|
@@ -18,16 +19,29 @@ import { clearTicketStreamingResponse, createTicketMessage, markTicketDescriptio
|
|
|
18
19
|
import { captureStageBaseline, captureStageSuccess } from '../lib/ticketUndo.js';
|
|
19
20
|
import { beginTicketThinkingRun, bindTicketThinkingMessage } from '../lib/thinkingState.js';
|
|
20
21
|
import { autoParkTicketIfStale, getTicket, resolveTicketBranch, resolveTicketTool, resolveTicketWorkerKey } from '../lib/tickets.js';
|
|
21
|
-
import {
|
|
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 {
|
|
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
|
|
30
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
391
|
+
if (progression) {
|
|
392
|
+
waitAutomationProgression({
|
|
393
|
+
projectId,
|
|
394
|
+
bundleId,
|
|
395
|
+
progressionRunId: progression.id,
|
|
396
|
+
pauseCode: 'review',
|
|
397
|
+
pauseReason: decision.reason,
|
|
398
|
+
reasonCode: 'precondition_failed',
|
|
399
|
+
pausedStageRunId: executionContext.runId,
|
|
400
|
+
broadcaster: asGenericEmitter(io),
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
recordAutomationPause(projectId, bundleId, 'review', decision.reason);
|
|
405
|
+
}
|
|
247
406
|
const pausedTicket = getTicket(ticket.id);
|
|
248
407
|
if (pausedTicket) {
|
|
249
408
|
io.emit('ticket:updated', pausedTicket);
|
|
@@ -253,352 +412,686 @@ const runReviewCompletionAutomation = async (ticket, project, reviewRunId, parse
|
|
|
253
412
|
case 'clean': {
|
|
254
413
|
resetBundleAutomationLoop(projectId, bundleId);
|
|
255
414
|
if (depthAtLeast(config.depth, 'auto_commit')) {
|
|
256
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
453
|
+
return reject('project_unavailable', 'Worktree not ready yet');
|
|
276
454
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (agentRun.session_id && tool === 'opencode') {
|
|
292
|
-
storeTicketUsageSnapshot(ticket.id, agentRun.session_id);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
if (tool === 'claude' && usageSnapshot) {
|
|
296
|
-
storeTicketUsageSnapshotDirect(ticket.id, usageSnapshot);
|
|
455
|
+
const project = getEffectiveProject(ticket);
|
|
456
|
+
if (!project || !ticket.project_id || project.id !== ticket.project_id) {
|
|
457
|
+
return reject('project_unavailable', 'Ticket project is unavailable');
|
|
458
|
+
}
|
|
459
|
+
const initiation = { ...defaultInitiation, ...options?.initiation };
|
|
460
|
+
let acceptedTicket = ticket;
|
|
461
|
+
let compensateAcceptance;
|
|
462
|
+
let executionContext;
|
|
463
|
+
try {
|
|
464
|
+
executionContext = withExecutionRunTransaction(transaction => {
|
|
465
|
+
const acceptance = options?.accept?.();
|
|
466
|
+
if (acceptance) {
|
|
467
|
+
acceptedTicket = acceptance.ticket;
|
|
468
|
+
compensateAcceptance = acceptance.compensate;
|
|
297
469
|
}
|
|
298
|
-
if (
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
470
|
+
if (initiation.initiator === 'automation' && initiation.causalParentRunId) {
|
|
471
|
+
const activeChild = db.prepare(`
|
|
472
|
+
SELECT id FROM execution_runs
|
|
473
|
+
WHERE project_id = ? AND parent_run_id = ?
|
|
474
|
+
AND kind IN ('review', 'automation_review_fix', 'automation_commit', 'automation_push', 'automation_pull_request')
|
|
475
|
+
AND status IN ('queued', 'running', 'waiting')
|
|
476
|
+
LIMIT 1
|
|
477
|
+
`).get(project.id, initiation.causalParentRunId);
|
|
478
|
+
if (activeChild) {
|
|
479
|
+
throw new Error('Automation progression already has an active child');
|
|
480
|
+
}
|
|
481
|
+
const progressionTicketLink = db.prepare(`
|
|
482
|
+
SELECT 1 FROM execution_run_links
|
|
483
|
+
WHERE execution_run_id = ? AND project_id = ? AND entity_type = 'ticket' AND entity_id = ?
|
|
484
|
+
`).get(initiation.causalParentRunId, project.id, acceptedTicket.id);
|
|
485
|
+
if (!progressionTicketLink) {
|
|
486
|
+
transaction.addExecutionRunLink({
|
|
487
|
+
projectId: project.id,
|
|
488
|
+
runId: initiation.causalParentRunId,
|
|
489
|
+
link: {
|
|
490
|
+
entityType: 'ticket',
|
|
491
|
+
entityId: acceptedTicket.id,
|
|
492
|
+
relationship: 'context',
|
|
493
|
+
label: acceptedTicket.title,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
314
496
|
}
|
|
315
497
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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: initiation.retrySourceRunId ? 'retry_started' : 'review_created',
|
|
516
|
+
});
|
|
517
|
+
return {
|
|
518
|
+
runId: executionRun.id,
|
|
519
|
+
correlationId: executionRun.correlation_id,
|
|
520
|
+
projectId: executionRun.project_id,
|
|
521
|
+
kind: executionRun.kind,
|
|
522
|
+
};
|
|
523
|
+
}, asGenericEmitter(io));
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
return reject('invalid_context', getErrorMessage(error));
|
|
527
|
+
}
|
|
528
|
+
let queuedJob;
|
|
529
|
+
try {
|
|
530
|
+
queuedJob = enqueue(jobKey, async () => {
|
|
531
|
+
const scheduledRun = getExecutionRun(executionContext.projectId, executionContext.runId);
|
|
532
|
+
if (!scheduledRun || !['queued', 'running', 'waiting'].includes(scheduledRun.status)) {
|
|
338
533
|
return;
|
|
339
534
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
535
|
+
let activityRegistered = false;
|
|
536
|
+
let registeredReviewProcess = null;
|
|
537
|
+
let finalizeStoppedReviewAfterFailure = null;
|
|
538
|
+
try {
|
|
539
|
+
registerBundleActivity(acceptedTicket, 'review');
|
|
540
|
+
activityRegistered = true;
|
|
541
|
+
const currentTicket = getTicket(acceptedTicket.id);
|
|
542
|
+
if (!currentTicket || currentTicket.project_id !== executionContext.projectId) {
|
|
543
|
+
throw new Error('Ticket no longer exists in the execution project');
|
|
544
|
+
}
|
|
545
|
+
const project = getEffectiveProject(currentTicket);
|
|
546
|
+
if (!project) {
|
|
547
|
+
throw new Error('Ticket project no longer exists');
|
|
548
|
+
}
|
|
549
|
+
const cwd = currentTicket.bundle?.kind === 'project_root' ? project.repo_path : resolveWorktreePath(branch, project);
|
|
550
|
+
const tool = resolveTicketTool(ticket, config.tool ?? 'opencode');
|
|
551
|
+
const reviewProcessState = registerReviewProcess(acceptedTicket.id);
|
|
552
|
+
registeredReviewProcess = reviewProcessState;
|
|
553
|
+
const createdAgentRunIds = [];
|
|
554
|
+
const storeCreatedAgentRunUsage = () => {
|
|
555
|
+
if (tool !== 'opencode') {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
for (const runId of createdAgentRunIds) {
|
|
559
|
+
const agentRun = getAgentRun(runId);
|
|
560
|
+
if (agentRun?.session_id) {
|
|
561
|
+
storeTicketUsageSnapshot(ticket.id, agentRun.session_id);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
const finalizeStoppedReview = ({ finalResponse = null, responseTiming = null, specCoverageResponse = null, transcriptRuns = [], usageSnapshot, } = {}) => {
|
|
566
|
+
storeCreatedAgentRunUsage();
|
|
567
|
+
if (tool === 'claude' && usageSnapshot) {
|
|
568
|
+
storeTicketUsageSnapshotDirect(ticket.id, usageSnapshot);
|
|
569
|
+
}
|
|
570
|
+
let messageId = null;
|
|
571
|
+
const fallbackContent = transcriptRuns.length > 0
|
|
572
|
+
? finalResponse?.trim()
|
|
573
|
+
|| specCoverageResponse?.trim()
|
|
574
|
+
|| transcriptRuns.find(run => run.response_text?.trim())?.response_text?.trim()
|
|
575
|
+
|| transcriptRuns.find(run => run.thinking_log?.trim())?.thinking_log?.trim()
|
|
576
|
+
|| ''
|
|
577
|
+
: '';
|
|
578
|
+
clearTicketStreamingResponse(ticket.id);
|
|
579
|
+
withExecutionRunTransaction(transaction => {
|
|
580
|
+
if (fallbackContent) {
|
|
581
|
+
messageId = createTicketMessage({
|
|
582
|
+
ticketId: ticket.id,
|
|
583
|
+
role: 'assistant',
|
|
584
|
+
kind: 'final_response',
|
|
585
|
+
content: fallbackContent,
|
|
586
|
+
responseTiming,
|
|
587
|
+
transcriptRuns,
|
|
588
|
+
});
|
|
589
|
+
transaction.addExecutionRunLink({
|
|
590
|
+
projectId: executionContext.projectId,
|
|
591
|
+
runId: executionContext.runId,
|
|
592
|
+
link: {
|
|
593
|
+
entityType: 'ticket_message',
|
|
594
|
+
entityId: messageId,
|
|
595
|
+
relationship: 'result',
|
|
596
|
+
expectedParent: { entityType: 'ticket', entityId: ticket.id },
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (messageId) {
|
|
601
|
+
bindTicketThinkingMessage(ticket.id, messageId);
|
|
602
|
+
}
|
|
603
|
+
db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
|
|
604
|
+
.run('stopped', ticket.id, executionContext.projectId);
|
|
605
|
+
terminalizeAgentRunsForExecutionRun(executionContext.runId, 'stopped', null);
|
|
606
|
+
transaction.transitionExecutionRun({
|
|
607
|
+
projectId: executionContext.projectId,
|
|
608
|
+
runId: executionContext.runId,
|
|
609
|
+
status: 'cancelled',
|
|
610
|
+
domainStatus: 'stopped',
|
|
611
|
+
eventType: 'review_cancelled',
|
|
612
|
+
});
|
|
613
|
+
}, asGenericEmitter(io));
|
|
614
|
+
if (initiation.initiator === 'automation' && acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
|
|
615
|
+
try {
|
|
616
|
+
const progression = resolveProgressionForChild({
|
|
617
|
+
projectId: acceptedTicket.project_id,
|
|
618
|
+
bundleId: acceptedTicket.worktree_bundle_id,
|
|
619
|
+
childRunId: executionContext.runId,
|
|
620
|
+
});
|
|
621
|
+
waitAutomationProgression({
|
|
622
|
+
projectId: acceptedTicket.project_id,
|
|
623
|
+
bundleId: acceptedTicket.worktree_bundle_id,
|
|
624
|
+
progressionRunId: progression.id,
|
|
625
|
+
pauseCode: 'review',
|
|
626
|
+
pauseReason: 'Automated review was stopped',
|
|
627
|
+
reasonCode: 'stage_failed',
|
|
628
|
+
pausedStageRunId: executionContext.runId,
|
|
629
|
+
broadcaster: asGenericEmitter(io),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
// A concurrent progression stop remains authoritative.
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
updateStatus(ticket.id, 'stopped', null, io);
|
|
637
|
+
emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
|
|
638
|
+
};
|
|
639
|
+
finalizeStoppedReviewAfterFailure = finalizeStoppedReview;
|
|
640
|
+
const stopReviewBeforeAgentIfRequested = () => {
|
|
641
|
+
if (!reviewProcessState.stopRequested) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
finalizeStoppedReview();
|
|
645
|
+
return true;
|
|
646
|
+
};
|
|
647
|
+
beginTicketThinkingRun(ticket.id);
|
|
648
|
+
withExecutionRunTransaction(transaction => {
|
|
649
|
+
db.prepare('UPDATE tickets SET agent_status = ?, agent_operation = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?').run('running', 'review', 'Preparing review baseline...', ticket.id, executionContext.projectId);
|
|
650
|
+
transaction.transitionExecutionRun({
|
|
651
|
+
projectId: executionContext.projectId,
|
|
652
|
+
runId: executionContext.runId,
|
|
653
|
+
status: 'running',
|
|
654
|
+
domainStatus: 'running',
|
|
655
|
+
eventType: 'review_started',
|
|
364
656
|
});
|
|
365
|
-
|
|
657
|
+
}, asGenericEmitter(io));
|
|
658
|
+
updateStatus(ticket.id, 'running', 'Preparing review baseline...', io, 'review');
|
|
659
|
+
await captureStageBaseline(ticket.id, 'review');
|
|
660
|
+
if (stopReviewBeforeAgentIfRequested()) {
|
|
661
|
+
return;
|
|
366
662
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
663
|
+
updateStatus(ticket.id, 'running', 'Computing review diff...', io);
|
|
664
|
+
const diffText = (await getReviewDiffDataAsync({ cwd, branch, project })).diff;
|
|
665
|
+
if (stopReviewBeforeAgentIfRequested()) {
|
|
666
|
+
return;
|
|
370
667
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
668
|
+
const reviewContext = buildReviewContext({ cwd, branch, project });
|
|
669
|
+
const diffSignature = computeDiffSignature(diffText);
|
|
670
|
+
const supplementalRequirements = ticket.project_id && ticket.worktree_bundle_id
|
|
671
|
+
? formatBuildOpenQuestionAnswersPrompt(listAnsweredBuildOpenQuestionsForBundle(ticket.project_id, ticket.worktree_bundle_id))
|
|
672
|
+
: '';
|
|
673
|
+
const runSpecCoverage = Boolean(project.spec_aware_code_review_enabled)
|
|
674
|
+
&& Boolean(ticket.origin_spec_snapshot_id)
|
|
675
|
+
&& ticket.project_id != null
|
|
676
|
+
&& ticket.worktree_bundle_id != null
|
|
677
|
+
&& !hasPriorReviewRunForBundle(ticket.project_id, ticket.worktree_bundle_id);
|
|
678
|
+
let specContext = null;
|
|
679
|
+
if (runSpecCoverage && ticket.project_id && ticket.worktree_bundle_id) {
|
|
680
|
+
const bundleTickets = listTickets(ticket.project_id)
|
|
681
|
+
.filter(bundleTicket => bundleTicket.worktree_bundle_id === ticket.worktree_bundle_id);
|
|
682
|
+
specContext = buildSpecCoverageContext({ ticket, bundleTickets });
|
|
683
|
+
}
|
|
684
|
+
const blindRunId = createAgentRun({
|
|
685
|
+
executionRunId: executionContext.runId,
|
|
686
|
+
parentType: 'ticket',
|
|
687
|
+
parentId: ticket.id,
|
|
688
|
+
runIndex: 0,
|
|
689
|
+
label: 'Code Review',
|
|
690
|
+
role: 'code-review',
|
|
691
|
+
});
|
|
692
|
+
createdAgentRunIds.push(blindRunId);
|
|
693
|
+
const blindRunStreamTarget = { runId: blindRunId, label: 'Code Review', role: 'code-review', runIndex: 0 };
|
|
694
|
+
let specCoverageRunStreamTarget = null;
|
|
695
|
+
if (specContext) {
|
|
375
696
|
const runId = createAgentRun({
|
|
697
|
+
executionRunId: executionContext.runId,
|
|
376
698
|
parentType: 'ticket',
|
|
377
699
|
parentId: ticket.id,
|
|
378
700
|
runIndex: 1,
|
|
379
701
|
label: 'Spec-Coverage Review',
|
|
380
702
|
role: 'spec-coverage',
|
|
381
703
|
});
|
|
704
|
+
createdAgentRunIds.push(runId);
|
|
382
705
|
specCoverageRunStreamTarget = { runId, label: 'Spec-Coverage Review', role: 'spec-coverage', runIndex: 1 };
|
|
383
706
|
}
|
|
384
|
-
catch (err) {
|
|
385
|
-
console.warn('[review] createAgentRun failed for spec-coverage run; falling back to legacy streaming', getErrorMessage(err));
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const createdAgentRuns = Boolean(blindRunStreamTarget) || Boolean(specCoverageRunStreamTarget);
|
|
389
|
-
if (createdAgentRuns) {
|
|
390
707
|
emitAgentRunsUpdated(io, 'ticket', ticket.id);
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
supplementalRequirements,
|
|
399
|
-
});
|
|
400
|
-
const blindRun = async () => {
|
|
401
|
-
return runAgent({
|
|
402
|
-
ticketId: ticket.id,
|
|
403
|
-
tool,
|
|
404
|
-
prompt: blindPrompt,
|
|
405
|
-
cwd,
|
|
406
|
-
model: ticket.model,
|
|
407
|
-
variant: ticket.variant,
|
|
408
|
-
emitSuccessUpdate: false,
|
|
409
|
-
io,
|
|
410
|
-
runStream: blindRunStreamTarget ?? undefined,
|
|
411
|
-
reviewProcessState,
|
|
708
|
+
const blindPrompt = reviewPrompt({
|
|
709
|
+
ticket,
|
|
710
|
+
diff: diffText,
|
|
711
|
+
memoryEnabled: Boolean(project.memory_enabled),
|
|
712
|
+
reviewContext,
|
|
713
|
+
agentRunId: blindRunStreamTarget.runId,
|
|
714
|
+
supplementalRequirements,
|
|
412
715
|
});
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (!specContext) {
|
|
416
|
-
return null;
|
|
417
|
-
}
|
|
418
|
-
try {
|
|
419
|
-
return await runAgent({
|
|
716
|
+
const blindRun = async () => {
|
|
717
|
+
return runAgent({
|
|
420
718
|
ticketId: ticket.id,
|
|
421
719
|
tool,
|
|
422
|
-
prompt:
|
|
423
|
-
ticket,
|
|
424
|
-
diff: diffText,
|
|
425
|
-
reviewContext,
|
|
426
|
-
specContext,
|
|
427
|
-
agentRunId: specCoverageRunStreamTarget?.runId ?? null,
|
|
428
|
-
supplementalRequirements,
|
|
429
|
-
}),
|
|
720
|
+
prompt: blindPrompt,
|
|
430
721
|
cwd,
|
|
431
722
|
model: ticket.model,
|
|
432
723
|
variant: ticket.variant,
|
|
433
724
|
emitSuccessUpdate: false,
|
|
725
|
+
executionContext,
|
|
434
726
|
io,
|
|
435
|
-
runStream:
|
|
727
|
+
runStream: blindRunStreamTarget,
|
|
436
728
|
reviewProcessState,
|
|
437
729
|
});
|
|
730
|
+
};
|
|
731
|
+
const specCoverageRun = async () => {
|
|
732
|
+
if (!specContext) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
return await runAgent({
|
|
737
|
+
ticketId: ticket.id,
|
|
738
|
+
tool,
|
|
739
|
+
prompt: specCoverageReviewPrompt({
|
|
740
|
+
ticket,
|
|
741
|
+
diff: diffText,
|
|
742
|
+
reviewContext,
|
|
743
|
+
specContext,
|
|
744
|
+
agentRunId: specCoverageRunStreamTarget?.runId ?? null,
|
|
745
|
+
supplementalRequirements,
|
|
746
|
+
}),
|
|
747
|
+
cwd,
|
|
748
|
+
model: ticket.model,
|
|
749
|
+
variant: ticket.variant,
|
|
750
|
+
emitSuccessUpdate: false,
|
|
751
|
+
executionContext,
|
|
752
|
+
io,
|
|
753
|
+
runStream: specCoverageRunStreamTarget ?? undefined,
|
|
754
|
+
reviewProcessState,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
console.warn('[review] spec-coverage pass failed; falling back to blind-only', getErrorMessage(err));
|
|
759
|
+
const transcriptRuns = getAgentErrorTranscriptRuns(err);
|
|
760
|
+
return transcriptRuns
|
|
761
|
+
? { response: null, responseTiming: null, transcriptRuns }
|
|
762
|
+
: null;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
let blindResult;
|
|
766
|
+
let specCoverageResult = null;
|
|
767
|
+
if (specContext) {
|
|
768
|
+
const [blindSettled, specCoverageSettled] = await Promise.allSettled([blindRun(), specCoverageRun()]);
|
|
769
|
+
if (blindSettled.status === 'rejected') {
|
|
770
|
+
throw blindSettled.reason;
|
|
771
|
+
}
|
|
772
|
+
blindResult = blindSettled.value;
|
|
773
|
+
specCoverageResult = specCoverageSettled.status === 'fulfilled' ? specCoverageSettled.value : null;
|
|
438
774
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const transcriptRuns = getAgentErrorTranscriptRuns(err);
|
|
442
|
-
return transcriptRuns
|
|
443
|
-
? { response: null, responseTiming: null, transcriptRuns }
|
|
444
|
-
: null;
|
|
775
|
+
else {
|
|
776
|
+
blindResult = await blindRun();
|
|
445
777
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (
|
|
452
|
-
|
|
778
|
+
const { response: finalResponse, responseTiming, usageSnapshot } = blindResult;
|
|
779
|
+
const transcriptRuns = [
|
|
780
|
+
...(blindResult.transcriptRuns ?? []),
|
|
781
|
+
...(specCoverageResult?.transcriptRuns ?? []),
|
|
782
|
+
];
|
|
783
|
+
if (reviewProcessState.stopRequested) {
|
|
784
|
+
finalizeStoppedReview({
|
|
785
|
+
finalResponse,
|
|
786
|
+
responseTiming,
|
|
787
|
+
specCoverageResponse: specCoverageResult?.response ?? null,
|
|
788
|
+
transcriptRuns,
|
|
789
|
+
usageSnapshot,
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
453
792
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
793
|
+
const { normalizedParent: normalizedResponse, parsed: blindParsed, } = resolveStructuredReviewOutput(finalResponse, blindResult.transcriptRuns ?? []);
|
|
794
|
+
let reviewRunId = null;
|
|
795
|
+
let parsedFindings = [];
|
|
796
|
+
let parsedStatus = null;
|
|
797
|
+
let persistedMessageId = null;
|
|
798
|
+
let reviewContent = null;
|
|
799
|
+
let findingsToPersist = [];
|
|
800
|
+
if (blindParsed?.cleanedMarkdown) {
|
|
801
|
+
const specCoverageSpecOutput = typeof specCoverageResult?.response === 'string'
|
|
802
|
+
? stripSystemReminderBlocks(specCoverageResult.response)
|
|
803
|
+
: '';
|
|
804
|
+
const specParsed = specCoverageSpecOutput
|
|
805
|
+
? parseStructuredReviewOutput(specCoverageSpecOutput)
|
|
806
|
+
: null;
|
|
807
|
+
const mergedFindings = specParsed && specParsed.status === 'completed'
|
|
808
|
+
? [...blindParsed.findings, ...specParsed.findings.map(finding => ({
|
|
809
|
+
...finding,
|
|
810
|
+
ordinal: finding.ordinal + blindParsed.findings.length,
|
|
811
|
+
}))]
|
|
812
|
+
: blindParsed.findings;
|
|
813
|
+
const combinedMarkdown = specParsed && specParsed.cleanedMarkdown
|
|
814
|
+
? `${blindParsed.cleanedMarkdown}\n\n## Spec-Coverage Review\n${specParsed.cleanedMarkdown}`
|
|
815
|
+
: blindParsed.cleanedMarkdown;
|
|
816
|
+
parsedFindings = mergedFindings;
|
|
817
|
+
parsedStatus = blindParsed.status;
|
|
818
|
+
reviewContent = combinedMarkdown;
|
|
819
|
+
findingsToPersist = mergedFindings;
|
|
820
|
+
}
|
|
821
|
+
const fallbackContent = !reviewContent && transcriptRuns.length > 0
|
|
822
|
+
? finalResponse?.trim()
|
|
823
|
+
|| specCoverageResult?.response?.trim()
|
|
824
|
+
|| transcriptRuns.find(run => run.response_text?.trim())?.response_text?.trim()
|
|
825
|
+
|| transcriptRuns.find(run => run.thinking_log?.trim())?.thinking_log?.trim()
|
|
826
|
+
|| ''
|
|
486
827
|
: '';
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
828
|
+
const parseFailed = parsedStatus !== 'completed';
|
|
829
|
+
const persisted = withExecutionRunTransaction(transaction => {
|
|
830
|
+
clearTicketStreamingResponse(ticket.id);
|
|
831
|
+
const content = reviewContent ?? fallbackContent;
|
|
832
|
+
const messageId = content
|
|
833
|
+
? createTicketMessage({
|
|
834
|
+
ticketId: ticket.id,
|
|
835
|
+
role: 'assistant',
|
|
836
|
+
kind: 'final_response',
|
|
837
|
+
content,
|
|
838
|
+
responseTiming,
|
|
839
|
+
transcriptRuns: transcriptRuns.length > 0 ? transcriptRuns : null,
|
|
840
|
+
})
|
|
841
|
+
: null;
|
|
842
|
+
let createdReviewRunId = null;
|
|
843
|
+
if (messageId && reviewContent && parsedStatus) {
|
|
844
|
+
createdReviewRunId = createReviewRun({
|
|
845
|
+
ticketId: ticket.id,
|
|
846
|
+
projectId: ticket.project_id,
|
|
847
|
+
worktreeBundleId: ticket.worktree_bundle_id,
|
|
848
|
+
diffSignature,
|
|
849
|
+
workspaceSignature: reviewContext.workspaceSignature,
|
|
850
|
+
messageId,
|
|
851
|
+
status: parsedStatus,
|
|
852
|
+
});
|
|
853
|
+
if (findingsToPersist.length > 0) {
|
|
854
|
+
insertReviewFindings(createdReviewRunId, findingsToPersist);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (messageId) {
|
|
858
|
+
transaction.addExecutionRunLink({
|
|
859
|
+
projectId: executionContext.projectId,
|
|
860
|
+
runId: executionContext.runId,
|
|
861
|
+
link: {
|
|
862
|
+
entityType: 'ticket_message',
|
|
863
|
+
entityId: messageId,
|
|
864
|
+
relationship: 'result',
|
|
865
|
+
expectedParent: { entityType: 'ticket', entityId: ticket.id },
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
if (createdReviewRunId) {
|
|
870
|
+
transaction.addExecutionRunLink({
|
|
871
|
+
projectId: executionContext.projectId,
|
|
872
|
+
runId: executionContext.runId,
|
|
873
|
+
link: {
|
|
874
|
+
entityType: 'review_run',
|
|
875
|
+
entityId: createdReviewRunId,
|
|
876
|
+
relationship: 'result',
|
|
877
|
+
expectedParent: { entityType: 'ticket', entityId: ticket.id },
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
db.prepare('UPDATE tickets SET agent_status = ?, agent_log = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
|
|
882
|
+
.run('done', ticket.id, executionContext.projectId);
|
|
883
|
+
terminalizeAgentRunsForExecutionRun(executionContext.runId, parseFailed ? 'error' : 'done', parseFailed ? 'Review output could not be parsed' : null);
|
|
884
|
+
transaction.transitionExecutionRun({
|
|
885
|
+
projectId: executionContext.projectId,
|
|
886
|
+
runId: executionContext.runId,
|
|
887
|
+
status: parseFailed ? 'failed' : 'completed',
|
|
888
|
+
domainStatus: parsedStatus ?? 'parse_failed',
|
|
889
|
+
...(parseFailed ? { failure: { code: 'parse_failed', message: 'Review output could not be parsed' } } : {}),
|
|
890
|
+
eventType: parseFailed ? 'review_parse_failed' : 'review_completed',
|
|
891
|
+
});
|
|
892
|
+
return { messageId, reviewRunId: createdReviewRunId };
|
|
893
|
+
}, asGenericEmitter(io));
|
|
894
|
+
persistedMessageId = persisted.messageId;
|
|
895
|
+
reviewRunId = persisted.reviewRunId;
|
|
509
896
|
if (persistedMessageId) {
|
|
510
897
|
bindTicketThinkingMessage(ticket.id, persistedMessageId);
|
|
511
898
|
}
|
|
512
|
-
reviewRunId
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
workspaceSignature: reviewContext.workspaceSignature,
|
|
518
|
-
messageId: persistedMessageId,
|
|
519
|
-
status: parsedStatus,
|
|
520
|
-
});
|
|
521
|
-
if (mergedFindings.length > 0) {
|
|
522
|
-
insertReviewFindings(reviewRunId, mergedFindings);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (!persistedMessageId && transcriptRuns.length > 0) {
|
|
526
|
-
const fallbackContent = finalResponse?.trim()
|
|
527
|
-
|| specCoverageResult?.response?.trim()
|
|
528
|
-
|| transcriptRuns.find(run => run.response_text?.trim())?.response_text?.trim()
|
|
529
|
-
|| transcriptRuns.find(run => run.thinking_log?.trim())?.thinking_log?.trim()
|
|
530
|
-
|| '';
|
|
531
|
-
if (fallbackContent) {
|
|
532
|
-
persistedMessageId = createTicketMessage({
|
|
533
|
-
ticketId: ticket.id,
|
|
534
|
-
role: 'assistant',
|
|
535
|
-
kind: 'final_response',
|
|
536
|
-
content: fallbackContent,
|
|
537
|
-
responseTiming,
|
|
538
|
-
transcriptRuns,
|
|
899
|
+
if (reviewRunId && ticket.project_id && ticket.worktree_bundle_id) {
|
|
900
|
+
io.emit('review-findings:updated', {
|
|
901
|
+
project_id: ticket.project_id,
|
|
902
|
+
worktree_bundle_id: ticket.worktree_bundle_id,
|
|
903
|
+
review_run_id: reviewRunId,
|
|
539
904
|
});
|
|
540
|
-
|
|
541
|
-
|
|
905
|
+
}
|
|
906
|
+
storeCreatedAgentRunUsage();
|
|
907
|
+
if (tool === 'claude' && usageSnapshot) {
|
|
908
|
+
storeTicketUsageSnapshotDirect(ticket.id, usageSnapshot);
|
|
909
|
+
}
|
|
910
|
+
emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', ticket.id);
|
|
911
|
+
updateStatus(ticket.id, 'done', null, io);
|
|
912
|
+
const completedWithMessage = getTicket(ticket.id);
|
|
913
|
+
if (completedWithMessage) {
|
|
914
|
+
io.emit('ticket:updated', completedWithMessage);
|
|
915
|
+
}
|
|
916
|
+
const updated = getTicket(ticket.id);
|
|
917
|
+
if (updated?.agent_status === 'done') {
|
|
918
|
+
if (project.memory_enabled && normalizedResponse.suggestions.length > 0) {
|
|
919
|
+
persistProjectMemorySuggestions(project.id, ticket.id, 'review', normalizedResponse.suggestions);
|
|
920
|
+
}
|
|
921
|
+
captureStageSuccess(ticket.id, 'review');
|
|
922
|
+
let handled = false;
|
|
923
|
+
try {
|
|
924
|
+
handled = await runReviewCompletionAutomation(updated, project, reviewRunId, parsedStatus, parsedFindings, io, executionContext, initiation);
|
|
925
|
+
}
|
|
926
|
+
catch (automationError) {
|
|
927
|
+
if (ticket.project_id && ticket.worktree_bundle_id) {
|
|
928
|
+
const reason = getErrorMessage(automationError);
|
|
929
|
+
if (initiation.initiator === 'automation') {
|
|
930
|
+
const progression = resolveProgressionForChild({
|
|
931
|
+
projectId: ticket.project_id,
|
|
932
|
+
bundleId: ticket.worktree_bundle_id,
|
|
933
|
+
childRunId: executionContext.runId,
|
|
934
|
+
});
|
|
935
|
+
waitAutomationProgression({
|
|
936
|
+
projectId: ticket.project_id,
|
|
937
|
+
bundleId: ticket.worktree_bundle_id,
|
|
938
|
+
progressionRunId: progression.id,
|
|
939
|
+
pauseCode: 'review',
|
|
940
|
+
pauseReason: reason,
|
|
941
|
+
reasonCode: 'stage_failed',
|
|
942
|
+
pausedStageRunId: executionContext.runId,
|
|
943
|
+
broadcaster: asGenericEmitter(io),
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
recordAutomationPause(ticket.project_id, ticket.worktree_bundle_id, 'review', reason);
|
|
948
|
+
}
|
|
949
|
+
handled = true;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (!handled) {
|
|
953
|
+
const completed = autoParkTicketIfStale(ticket.id);
|
|
954
|
+
if (completed) {
|
|
955
|
+
io.emit('ticket:updated', completed);
|
|
956
|
+
}
|
|
542
957
|
}
|
|
543
958
|
}
|
|
544
959
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
960
|
+
catch (err) {
|
|
961
|
+
if (registeredReviewProcess?.stopRequested && finalizeStoppedReviewAfterFailure) {
|
|
962
|
+
finalizeStoppedReviewAfterFailure({ transcriptRuns: getAgentErrorTranscriptRuns(err) ?? [] });
|
|
963
|
+
return;
|
|
549
964
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
emitAgentRunsUpdated(io, 'ticket', ticket.id);
|
|
557
|
-
updateStatus(ticket.id, 'done', null, io);
|
|
558
|
-
const completedWithMessage = getTicket(ticket.id);
|
|
559
|
-
if (completedWithMessage) {
|
|
560
|
-
io.emit('ticket:updated', completedWithMessage);
|
|
561
|
-
}
|
|
562
|
-
const updated = getTicket(ticket.id);
|
|
563
|
-
if (updated?.agent_status === 'done') {
|
|
564
|
-
if (project.memory_enabled && normalizedResponse.suggestions.length > 0) {
|
|
565
|
-
persistProjectMemorySuggestions(project.id, ticket.id, 'review', normalizedResponse.suggestions);
|
|
965
|
+
const transcriptRuns = getAgentErrorTranscriptRuns(err);
|
|
966
|
+
const errorContent = formatAgentErrorForTranscript(err);
|
|
967
|
+
const message = getErrorMessage(err);
|
|
968
|
+
const executionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
|
|
969
|
+
if (!executionRun || !['queued', 'running', 'waiting'].includes(executionRun.status)) {
|
|
970
|
+
return;
|
|
566
971
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
972
|
+
let ownedProgressionId = null;
|
|
973
|
+
if (initiation.initiator === 'automation' && acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
|
|
974
|
+
try {
|
|
975
|
+
ownedProgressionId = resolveProgressionForChild({
|
|
976
|
+
projectId: acceptedTicket.project_id,
|
|
977
|
+
bundleId: acceptedTicket.worktree_bundle_id,
|
|
978
|
+
childRunId: executionContext.runId,
|
|
979
|
+
}).id;
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
ownedProgressionId = null;
|
|
573
983
|
}
|
|
574
984
|
}
|
|
985
|
+
try {
|
|
986
|
+
withExecutionRunTransaction(transaction => {
|
|
987
|
+
if (errorContent) {
|
|
988
|
+
const messageId = createTicketMessage({
|
|
989
|
+
ticketId: acceptedTicket.id,
|
|
990
|
+
role: 'assistant',
|
|
991
|
+
kind: 'error',
|
|
992
|
+
content: errorContent,
|
|
993
|
+
transcriptRuns,
|
|
994
|
+
});
|
|
995
|
+
transaction.addExecutionRunLink({
|
|
996
|
+
projectId: executionContext.projectId,
|
|
997
|
+
runId: executionContext.runId,
|
|
998
|
+
link: {
|
|
999
|
+
entityType: 'ticket_message',
|
|
1000
|
+
entityId: messageId,
|
|
1001
|
+
relationship: 'result',
|
|
1002
|
+
expectedParent: { entityType: 'ticket', entityId: acceptedTicket.id },
|
|
1003
|
+
},
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
db.prepare('UPDATE tickets SET agent_status = ?, agent_log = ?, streaming_response = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND project_id = ?')
|
|
1007
|
+
.run('error', message, acceptedTicket.id, executionContext.projectId);
|
|
1008
|
+
terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
|
|
1009
|
+
transaction.transitionExecutionRun({
|
|
1010
|
+
projectId: executionContext.projectId,
|
|
1011
|
+
runId: executionContext.runId,
|
|
1012
|
+
status: 'failed',
|
|
1013
|
+
domainStatus: 'error',
|
|
1014
|
+
failure: { code: 'review_failed', message },
|
|
1015
|
+
eventType: 'review_failed',
|
|
1016
|
+
});
|
|
1017
|
+
if (ownedProgressionId && acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
|
|
1018
|
+
waitAutomationProgression({
|
|
1019
|
+
projectId: acceptedTicket.project_id,
|
|
1020
|
+
bundleId: acceptedTicket.worktree_bundle_id,
|
|
1021
|
+
progressionRunId: ownedProgressionId,
|
|
1022
|
+
pauseCode: 'review',
|
|
1023
|
+
pauseReason: message,
|
|
1024
|
+
reasonCode: 'stage_failed',
|
|
1025
|
+
pausedStageRunId: executionContext.runId,
|
|
1026
|
+
broadcaster: asGenericEmitter(io),
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
}, asGenericEmitter(io));
|
|
1030
|
+
}
|
|
1031
|
+
catch {
|
|
1032
|
+
const currentExecutionRun = getExecutionRun(executionContext.projectId, executionContext.runId);
|
|
1033
|
+
if (currentExecutionRun && ['queued', 'running', 'waiting'].includes(currentExecutionRun.status)) {
|
|
1034
|
+
terminalizeAgentRunsForExecutionRun(executionContext.runId, 'error', message);
|
|
1035
|
+
transitionExecutionRun({
|
|
1036
|
+
projectId: executionContext.projectId,
|
|
1037
|
+
runId: executionContext.runId,
|
|
1038
|
+
status: 'failed',
|
|
1039
|
+
domainStatus: 'error',
|
|
1040
|
+
failure: { code: 'review_failed', message },
|
|
1041
|
+
eventType: 'review_failed',
|
|
1042
|
+
}, asGenericEmitter(io));
|
|
1043
|
+
}
|
|
1044
|
+
if (ownedProgressionId && acceptedTicket.project_id && acceptedTicket.worktree_bundle_id) {
|
|
1045
|
+
try {
|
|
1046
|
+
waitAutomationProgression({
|
|
1047
|
+
projectId: acceptedTicket.project_id,
|
|
1048
|
+
bundleId: acceptedTicket.worktree_bundle_id,
|
|
1049
|
+
progressionRunId: ownedProgressionId,
|
|
1050
|
+
pauseCode: 'review',
|
|
1051
|
+
pauseReason: message,
|
|
1052
|
+
reasonCode: 'stage_failed',
|
|
1053
|
+
pausedStageRunId: executionContext.runId,
|
|
1054
|
+
broadcaster: asGenericEmitter(io),
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
catch {
|
|
1058
|
+
// Preserve the terminal Review result when ownership was concurrently cancelled.
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (getTicket(acceptedTicket.id)) {
|
|
1063
|
+
emitAgentRunsUpdated(asGenericEmitter(io), 'ticket', acceptedTicket.id);
|
|
1064
|
+
updateStatus(acceptedTicket.id, 'error', message, io);
|
|
1065
|
+
}
|
|
575
1066
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const errorContent = formatAgentErrorForTranscript(err);
|
|
584
|
-
if (errorContent) {
|
|
585
|
-
createTicketMessage({
|
|
586
|
-
ticketId: ticket.id,
|
|
587
|
-
role: 'assistant',
|
|
588
|
-
kind: 'error',
|
|
589
|
-
content: errorContent,
|
|
590
|
-
transcriptRuns,
|
|
591
|
-
});
|
|
1067
|
+
finally {
|
|
1068
|
+
if (registeredReviewProcess) {
|
|
1069
|
+
untrackReviewProcess(acceptedTicket.id, registeredReviewProcess);
|
|
1070
|
+
}
|
|
1071
|
+
if (activityRegistered) {
|
|
1072
|
+
unregisterBundleActivity(acceptedTicket, 'review');
|
|
1073
|
+
}
|
|
592
1074
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
const message = getErrorMessage(error);
|
|
1079
|
+
withExecutionRunTransaction(transaction => {
|
|
1080
|
+
compensateAcceptance?.();
|
|
1081
|
+
transaction.transitionExecutionRun({
|
|
1082
|
+
projectId: executionContext.projectId,
|
|
1083
|
+
runId: executionContext.runId,
|
|
1084
|
+
status: 'failed',
|
|
1085
|
+
domainStatus: ticket.agent_status ?? 'idle',
|
|
1086
|
+
failure: { code: 'queue_failed', message },
|
|
1087
|
+
eventType: 'review_failed',
|
|
1088
|
+
});
|
|
1089
|
+
}, asGenericEmitter(io));
|
|
1090
|
+
return reject('queue_failed', message);
|
|
1091
|
+
}
|
|
1092
|
+
queuedJob.catch(() => {
|
|
1093
|
+
// The queued callback owns domain and execution failure finalization.
|
|
601
1094
|
});
|
|
602
|
-
return true;
|
|
603
|
-
}
|
|
1095
|
+
return legacyResult ? true : { accepted: true, executionContext };
|
|
1096
|
+
}
|
|
604
1097
|
//# sourceMappingURL=review.js.map
|