@straiffi/archon 1.8.22 → 1.10.0

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 (219) hide show
  1. package/dist/client/assets/{ActivityView-CdEPjmqD.js → ActivityView-Y5IaJiim.js} +1 -1
  2. package/dist/client/assets/GitHubActionsDialog-GWVxWGPK.js +1 -0
  3. package/dist/client/assets/VerificationDialog-C4RMuI4y.js +4 -0
  4. package/dist/client/assets/{architectureDiagram-3BPJPVTR-BrZ08mqq.js → architectureDiagram-3BPJPVTR-CeM6gbLN.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-GPEHLZMM-b93XwJ6Z.js → blockDiagram-GPEHLZMM-BP4hta96.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AAUBKEIU-BKOdbZ_v.js → c4Diagram-AAUBKEIU-5aJScvhn.js} +1 -1
  7. package/dist/client/assets/channel-B3DjZXbm.js +1 -0
  8. package/dist/client/assets/{chunk-2J33WTMH-D7Vxn_1m.js → chunk-2J33WTMH-BKc2IelV.js} +1 -1
  9. package/dist/client/assets/{chunk-3OPIFGDE-B1zo2UZq.js → chunk-3OPIFGDE-DCc15la5.js} +1 -1
  10. package/dist/client/assets/{chunk-5ZQYHXKU-BajvkDdQ.js → chunk-5ZQYHXKU-DSD9HbTp.js} +1 -1
  11. package/dist/client/assets/{chunk-727SXJPM-CSH7cVpV.js → chunk-727SXJPM-DrAjnKiA.js} +1 -1
  12. package/dist/client/assets/{chunk-AQP2D5EJ-DE-wgUNP.js → chunk-AQP2D5EJ-DxKnpHfQ.js} +1 -1
  13. package/dist/client/assets/{chunk-CSCIHK7Q-Ba7JgCEf.js → chunk-CSCIHK7Q-DLejrAB_.js} +1 -1
  14. package/dist/client/assets/{chunk-KSCS5N6A-0o3YWkHM.js → chunk-KSCS5N6A-BWRNmF8S.js} +1 -1
  15. package/dist/client/assets/{chunk-L5ZTLDWV-DihWczP8.js → chunk-L5ZTLDWV-D48i966f.js} +1 -1
  16. package/dist/client/assets/{chunk-LZXEDZCA-Bxnu-hme.js → chunk-LZXEDZCA-DLhg6Bok.js} +2 -2
  17. package/dist/client/assets/{chunk-ND2GUHAM-CoaR5kmP.js → chunk-ND2GUHAM-B4-7cOEq.js} +1 -1
  18. package/dist/client/assets/{chunk-NZK2D7GU-By92Rd3d.js → chunk-NZK2D7GU-D2ttXA_d.js} +1 -1
  19. package/dist/client/assets/{chunk-O5CBEL6O-D83Pkgfu.js → chunk-O5CBEL6O-BrWrOVCq.js} +1 -1
  20. package/dist/client/assets/{chunk-WU5MYG2G-v8OVf5Lz.js → chunk-WU5MYG2G-CLcPwxPe.js} +1 -1
  21. package/dist/client/assets/classDiagram-4FO5ZUOK-DproSkn2.js +1 -0
  22. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-JVMMzJxJ.js +1 -0
  23. package/dist/client/assets/{dagre-BM42HDAG-Dq1ALF7Z.js → dagre-BM42HDAG-DIXN8_VX.js} +1 -1
  24. package/dist/client/assets/{diagram-2AECGRRQ-1t1_TKtu.js → diagram-2AECGRRQ-D2wjdCij.js} +1 -1
  25. package/dist/client/assets/{diagram-5GNKFQAL-DtvYRL6f.js → diagram-5GNKFQAL-E1Og_PHE.js} +1 -1
  26. package/dist/client/assets/{diagram-KO2AKTUF-PRNrSX5F.js → diagram-KO2AKTUF-B6P1Ud8a.js} +1 -1
  27. package/dist/client/assets/{diagram-LMA3HP47-DaTX0DhT.js → diagram-LMA3HP47-C3Jxq56v.js} +1 -1
  28. package/dist/client/assets/{diagram-OG6HWLK6-C2p9p-92.js → diagram-OG6HWLK6-Do9LVhdL.js} +1 -1
  29. package/dist/client/assets/{erDiagram-TEJ5UH35-DKzLtnp-.js → erDiagram-TEJ5UH35-DLb5E4-f.js} +1 -1
  30. package/dist/client/assets/{flowDiagram-I6XJVG4X-GdL8lm6e.js → flowDiagram-I6XJVG4X-CcTs_wU9.js} +1 -1
  31. package/dist/client/assets/{ganttDiagram-6RSMTGT7-Ok-4XYwT.js → ganttDiagram-6RSMTGT7-q9ez5fri.js} +1 -1
  32. package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DSMrOCK7.js → gitGraphDiagram-PVQCEYII-BcaKiz4c.js} +1 -1
  33. package/dist/client/assets/index-1CokwSFm.css +2 -0
  34. package/dist/client/assets/index-CSOKbu3w.js +276 -0
  35. package/dist/client/assets/{infoDiagram-5YYISTIA-Dl4hRXis.js → infoDiagram-5YYISTIA-Cu0w4frE.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-C1Dc1fTT.js → ishikawaDiagram-YF4QCWOH-CJ47Z-ak.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-JHISSGLW-CUscmJze.js → journeyDiagram-JHISSGLW-fAL_Zcvs.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-UN3LZRKU-C0nwYjQV.js → kanban-definition-UN3LZRKU-BTNIybMC.js} +1 -1
  39. package/dist/client/assets/{line-ByiuEIg_.js → line-BvsadeZR.js} +1 -1
  40. package/dist/client/assets/{mermaid-parser.core-B-XdAyDR.js → mermaid-parser.core-C0EkH2y_.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-D_bIfuZD.js → mermaid.core-BU5QIIbY.js} +3 -3
  42. package/dist/client/assets/{mindmap-definition-RKZ34NQL-BGNiPJDK.js → mindmap-definition-RKZ34NQL-CWbSYK8f.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-4H26LBE5-CP6FpAoO.js → pieDiagram-4H26LBE5-t4w2cB-y.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-W4KKPZXB-dsAWBAQn.js → quadrantDiagram-W4KKPZXB-Cqasvrpa.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-4Y6WPE33-DJH-zHqF.js → requirementDiagram-4Y6WPE33-_l-mY-tb.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-5OEKKPKP-BJMnZrqH.js → sankeyDiagram-5OEKKPKP-Cr9YrZ-Y.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-3UESZ5HK-CzNjUSlM.js → sequenceDiagram-3UESZ5HK-DvzZhQpb.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-AJRCARHV-jppMhOwh.js → stateDiagram-AJRCARHV-cFjPpAO3.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-btHMH5Cg.js +1 -0
  50. package/dist/client/assets/{timeline-definition-PNZ67QCA-BX2CI5H_.js → timeline-definition-PNZ67QCA-BjRNrEJY.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-CIIHVFJN-Bd1KZaXh.js → vennDiagram-CIIHVFJN-xs9afjrw.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-YWT4CUSO-D-0BNz49.js → wardleyDiagram-YWT4CUSO-DH68ZYti.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-2RQKCTM6-DbYa0ZWK.js → xychartDiagram-2RQKCTM6-DLW5ktTU.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/server/db.js +231 -14
  56. package/dist/server/db.js.map +1 -1
  57. package/dist/server/index.js +93 -9
  58. package/dist/server/index.js.map +1 -1
  59. package/dist/server/lib/agent.js +368 -143
  60. package/dist/server/lib/agent.js.map +1 -1
  61. package/dist/server/lib/agentProvider.js.map +1 -1
  62. package/dist/server/lib/approvalReconciliation.js +14 -2
  63. package/dist/server/lib/approvalReconciliation.js.map +1 -1
  64. package/dist/server/lib/buildExecutionEnvironment.js +251 -57
  65. package/dist/server/lib/buildExecutionEnvironment.js.map +1 -1
  66. package/dist/server/lib/chatOperations.js +125 -3
  67. package/dist/server/lib/chatOperations.js.map +1 -1
  68. package/dist/server/lib/chatPostBuildRuns.js +12 -6
  69. package/dist/server/lib/chatPostBuildRuns.js.map +1 -1
  70. package/dist/server/lib/chatTargets.js +2 -1
  71. package/dist/server/lib/chatTargets.js.map +1 -1
  72. package/dist/server/lib/checkDefinitionCwd.js +34 -0
  73. package/dist/server/lib/checkDefinitionCwd.js.map +1 -0
  74. package/dist/server/lib/checkExecutionEnvironment.js +95 -0
  75. package/dist/server/lib/checkExecutionEnvironment.js.map +1 -0
  76. package/dist/server/lib/checkPolicyDefinitionIds.js +41 -0
  77. package/dist/server/lib/checkPolicyDefinitionIds.js.map +1 -0
  78. package/dist/server/lib/checkRepairDispatch.js +2 -0
  79. package/dist/server/lib/checkRepairDispatch.js.map +1 -1
  80. package/dist/server/lib/checkRunner.js +371 -175
  81. package/dist/server/lib/checkRunner.js.map +1 -1
  82. package/dist/server/lib/checkRuns.js +99 -8
  83. package/dist/server/lib/checkRuns.js.map +1 -1
  84. package/dist/server/lib/claudeAgentProvider.js +3 -6
  85. package/dist/server/lib/claudeAgentProvider.js.map +1 -1
  86. package/dist/server/lib/claudeStdin.js +27 -0
  87. package/dist/server/lib/claudeStdin.js.map +1 -0
  88. package/dist/server/lib/completedAssistantOutput.js +38 -0
  89. package/dist/server/lib/completedAssistantOutput.js.map +1 -0
  90. package/dist/server/lib/consequentialActionAuthority.js +99 -8
  91. package/dist/server/lib/consequentialActionAuthority.js.map +1 -1
  92. package/dist/server/lib/consequentialActions.js +42 -2
  93. package/dist/server/lib/consequentialActions.js.map +1 -1
  94. package/dist/server/lib/dockerSandboxCredentials.js +10 -3
  95. package/dist/server/lib/dockerSandboxCredentials.js.map +1 -1
  96. package/dist/server/lib/dockerSandboxEnvironmentOperations.js +9 -8
  97. package/dist/server/lib/dockerSandboxEnvironmentOperations.js.map +1 -1
  98. package/dist/server/lib/dockerSandboxExecutionEnvironment.js +167 -75
  99. package/dist/server/lib/dockerSandboxExecutionEnvironment.js.map +1 -1
  100. package/dist/server/lib/dockerSandboxExecutionProvider.js +22 -14
  101. package/dist/server/lib/dockerSandboxExecutionProvider.js.map +1 -1
  102. package/dist/server/lib/dockerSandboxNetworkPolicy.js +23 -14
  103. package/dist/server/lib/dockerSandboxNetworkPolicy.js.map +1 -1
  104. package/dist/server/lib/dockerSandboxReleaseProfiles.js +91 -0
  105. package/dist/server/lib/dockerSandboxReleaseProfiles.js.map +1 -0
  106. package/dist/server/lib/dockerSandboxRuntimeController.js +16 -1
  107. package/dist/server/lib/dockerSandboxRuntimeController.js.map +1 -1
  108. package/dist/server/lib/executionCredentialCapabilities.js +9 -6
  109. package/dist/server/lib/executionCredentialCapabilities.js.map +1 -1
  110. package/dist/server/lib/executionEnvironment.js.map +1 -1
  111. package/dist/server/lib/executionEnvironmentLifecycle.js +7 -0
  112. package/dist/server/lib/executionEnvironmentLifecycle.js.map +1 -1
  113. package/dist/server/lib/executionEnvironmentOperations.js +241 -0
  114. package/dist/server/lib/executionEnvironmentOperations.js.map +1 -0
  115. package/dist/server/lib/executionEnvironmentReconciliation.js +98 -3
  116. package/dist/server/lib/executionEnvironmentReconciliation.js.map +1 -1
  117. package/dist/server/lib/executionEnvironmentRoutes.js +83 -29
  118. package/dist/server/lib/executionEnvironmentRoutes.js.map +1 -1
  119. package/dist/server/lib/executionEnvironmentValidation.js +19 -5
  120. package/dist/server/lib/executionEnvironmentValidation.js.map +1 -1
  121. package/dist/server/lib/executionEnvironmentValidationOperations.js +58 -31
  122. package/dist/server/lib/executionEnvironmentValidationOperations.js.map +1 -1
  123. package/dist/server/lib/executionEnvironments.js +187 -9
  124. package/dist/server/lib/executionEnvironments.js.map +1 -1
  125. package/dist/server/lib/executionProfileCheckPolicy.js +14 -0
  126. package/dist/server/lib/executionProfileCheckPolicy.js.map +1 -0
  127. package/dist/server/lib/executionProfileRoutes.js +9 -1
  128. package/dist/server/lib/executionProfileRoutes.js.map +1 -1
  129. package/dist/server/lib/executionProfileStore.js +5 -3
  130. package/dist/server/lib/executionProfileStore.js.map +1 -1
  131. package/dist/server/lib/executionProfiles.js +97 -15
  132. package/dist/server/lib/executionProfiles.js.map +1 -1
  133. package/dist/server/lib/executionReconciliation.js +43 -6
  134. package/dist/server/lib/executionReconciliation.js.map +1 -1
  135. package/dist/server/lib/executionRunAuthority.js +115 -24
  136. package/dist/server/lib/executionRunAuthority.js.map +1 -1
  137. package/dist/server/lib/executionTargets.js +30 -4
  138. package/dist/server/lib/executionTargets.js.map +1 -1
  139. package/dist/server/lib/executionWorkloads.js +75 -0
  140. package/dist/server/lib/executionWorkloads.js.map +1 -0
  141. package/dist/server/lib/git.js +10 -1
  142. package/dist/server/lib/git.js.map +1 -1
  143. package/dist/server/lib/opencodeAgentProvider.js +3 -1
  144. package/dist/server/lib/opencodeAgentProvider.js.map +1 -1
  145. package/dist/server/lib/opencodeSessionOperations.js +5 -3
  146. package/dist/server/lib/opencodeSessionOperations.js.map +1 -1
  147. package/dist/server/lib/planQuestions.js +2 -1
  148. package/dist/server/lib/planQuestions.js.map +1 -1
  149. package/dist/server/lib/postBuildCheckRuns.js +9 -5
  150. package/dist/server/lib/postBuildCheckRuns.js.map +1 -1
  151. package/dist/server/lib/preview.js +47 -6
  152. package/dist/server/lib/preview.js.map +1 -1
  153. package/dist/server/lib/previewProxy.js +11 -3
  154. package/dist/server/lib/previewProxy.js.map +1 -1
  155. package/dist/server/lib/productionCheckApprovals.js +97 -35
  156. package/dist/server/lib/productionCheckApprovals.js.map +1 -1
  157. package/dist/server/lib/projectMemorySuggestions.js +29 -6
  158. package/dist/server/lib/projectMemorySuggestions.js.map +1 -1
  159. package/dist/server/lib/requiredCheckPolicy.js +3 -40
  160. package/dist/server/lib/requiredCheckPolicy.js.map +1 -1
  161. package/dist/server/lib/reviewAgentWatchdog.js +75 -0
  162. package/dist/server/lib/reviewAgentWatchdog.js.map +1 -0
  163. package/dist/server/lib/reviewFindingReconciliation.js +176 -0
  164. package/dist/server/lib/reviewFindingReconciliation.js.map +1 -0
  165. package/dist/server/lib/reviewFindings.js +45 -17
  166. package/dist/server/lib/reviewFindings.js.map +1 -1
  167. package/dist/server/lib/run.js +370 -80
  168. package/dist/server/lib/run.js.map +1 -1
  169. package/dist/server/lib/serviceRuns.js +196 -0
  170. package/dist/server/lib/serviceRuns.js.map +1 -0
  171. package/dist/server/lib/sessionTargets.js +34 -0
  172. package/dist/server/lib/sessionTargets.js.map +1 -1
  173. package/dist/server/lib/shell.js +20 -0
  174. package/dist/server/lib/shell.js.map +1 -1
  175. package/dist/server/lib/specArtifact.js +36 -15
  176. package/dist/server/lib/specArtifact.js.map +1 -1
  177. package/dist/server/lib/specBundleGroups.js +4 -4
  178. package/dist/server/lib/specBundleGroups.js.map +1 -1
  179. package/dist/server/lib/specOperations.js +2 -0
  180. package/dist/server/lib/specOperations.js.map +1 -1
  181. package/dist/server/lib/specReviewOperations.js +2 -0
  182. package/dist/server/lib/specReviewOperations.js.map +1 -1
  183. package/dist/server/lib/specTicketMaterialization.js +37 -24
  184. package/dist/server/lib/specTicketMaterialization.js.map +1 -1
  185. package/dist/server/lib/ticketFollowUpOperations.js +9 -4
  186. package/dist/server/lib/ticketFollowUpOperations.js.map +1 -1
  187. package/dist/server/lib/ticketRunOperations.js +1 -0
  188. package/dist/server/lib/ticketRunOperations.js.map +1 -1
  189. package/dist/server/lib/types.js +1 -0
  190. package/dist/server/lib/types.js.map +1 -1
  191. package/dist/server/lib/workloadExecutionEnvironment.js +2 -0
  192. package/dist/server/lib/workloadExecutionEnvironment.js.map +1 -0
  193. package/dist/server/lib/workspaceIdentity.js +30 -6
  194. package/dist/server/lib/workspaceIdentity.js.map +1 -1
  195. package/dist/server/workers/build.js +39 -8
  196. package/dist/server/workers/build.js.map +1 -1
  197. package/dist/server/workers/chat.js +142 -15
  198. package/dist/server/workers/chat.js.map +1 -1
  199. package/dist/server/workers/followUp.js +77 -20
  200. package/dist/server/workers/followUp.js.map +1 -1
  201. package/dist/server/workers/plan.js +295 -48
  202. package/dist/server/workers/plan.js.map +1 -1
  203. package/dist/server/workers/planFollowUp.js +77 -7
  204. package/dist/server/workers/planFollowUp.js.map +1 -1
  205. package/dist/server/workers/review.js +153 -40
  206. package/dist/server/workers/review.js.map +1 -1
  207. package/dist/server/workers/specReview.js +43 -5
  208. package/dist/server/workers/specReview.js.map +1 -1
  209. package/dist/server/workers/specSession.js +156 -31
  210. package/dist/server/workers/specSession.js.map +1 -1
  211. package/package.json +1 -1
  212. package/dist/client/assets/GitHubActionsDialog-CA189pr2.js +0 -1
  213. package/dist/client/assets/VerificationDialog-BPVkhKRj.js +0 -4
  214. package/dist/client/assets/channel-D0zqzBT_.js +0 -1
  215. package/dist/client/assets/classDiagram-4FO5ZUOK-ChhyvHH2.js +0 -1
  216. package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Cl02kurK.js +0 -1
  217. package/dist/client/assets/index-Bs8KsdV9.js +0 -276
  218. package/dist/client/assets/index-DdwM8Wgt.css +0 -2
  219. package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CLR1WgdV.js +0 -1
@@ -1,7 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { randomUUID } from 'crypto';
3
- import { readFileSync } from 'fs';
4
- import { extname, isAbsolute, join, relative, resolve, sep } from 'path';
3
+ import { isAbsolute, relative, resolve, sep } from 'path';
5
4
  import db from '../db.js';
6
5
  import { getChatSession, updateChatSessionUsageSnapshot } from './chats.js';
7
6
  import { normalizeClaudeModelId, normalizeModelId } from './models.js';
@@ -23,6 +22,8 @@ import { createHostLauncherEnvironment, createLegacyHostRuntimeEnvironment, crea
23
22
  import { OpencodeModelResolutionError, parseOpencodeStderrModelResolutionError, } from './opencodeErrors.js';
24
23
  import { extractLeadingOpencodeJsonObjects } from './opencodePromptOutput.js';
25
24
  import { assertAgentSpawnPermit, AgentSpawnPermitRevokedError } from './agentSpawnPermits.js';
25
+ import { buildClaudeStdinMessage } from './claudeStdin.js';
26
+ import { createReviewAgentWatchdog, resolveWithin, REVIEW_AGENT_CLOSE_ENRICHMENT_TIMEOUT_MS, REVIEW_AGENT_INACTIVITY_TIMEOUT_MS, REVIEW_AGENT_KILL_GRACE_MS, } from './reviewAgentWatchdog.js';
26
27
  const withTranscriptRuns = (value, transcriptRuns) => {
27
28
  return transcriptRuns ? { ...value, transcriptRuns } : value;
28
29
  };
@@ -57,33 +58,6 @@ const resolveAgentModel = (tool, model) => {
57
58
  }
58
59
  return normalizeModelId(model) || null;
59
60
  };
60
- const CLAUDE_IMAGE_EXTENSIONS = {
61
- '.png': 'image/png',
62
- '.jpg': 'image/jpeg',
63
- '.jpeg': 'image/jpeg',
64
- '.webp': 'image/webp',
65
- '.gif': 'image/gif',
66
- };
67
- const buildClaudeStdinMessage = (prompt, files, cwd) => {
68
- if (files.length === 0) {
69
- return JSON.stringify({ type: 'user', message: { role: 'user', content: prompt } });
70
- }
71
- const content = [{ type: 'text', text: prompt }];
72
- for (const filePath of files) {
73
- const absolutePath = isAbsolute(filePath) ? filePath : join(cwd, filePath);
74
- const ext = extname(absolutePath).toLowerCase();
75
- const mediaType = CLAUDE_IMAGE_EXTENSIONS[ext];
76
- if (!mediaType) {
77
- continue;
78
- }
79
- const data = readFileSync(absolutePath).toString('base64');
80
- content.push({
81
- type: 'image',
82
- source: { type: 'base64', media_type: mediaType, data },
83
- });
84
- }
85
- return JSON.stringify({ type: 'user', message: { role: 'user', content } });
86
- };
87
61
  const writeClaudeStdin = (child, message) => {
88
62
  const stdin = child.stdin;
89
63
  if (!stdin) {
@@ -454,9 +428,9 @@ export const readOpencodeUsageSnapshot = async ({ sessionId, spawnTimestamp = nu
454
428
  }
455
429
  return snapshot;
456
430
  };
457
- const resolveOpencodeUsageSnapshot = async ({ current, sessionId, spawnTimestamp = null, }) => {
431
+ const resolveOpencodeUsageSnapshot = async ({ current, sessionId, spawnTimestamp = null, sessionOperations = hostOpencodeSessionOperations, }) => {
458
432
  try {
459
- return mergeChatUsageSnapshot(current, await readOpencodeUsageSnapshot({ sessionId, spawnTimestamp }));
433
+ return mergeChatUsageSnapshot(current, await readOpencodeUsageSnapshot({ sessionId, spawnTimestamp, sessionOperations }));
460
434
  }
461
435
  catch {
462
436
  return current;
@@ -532,7 +506,7 @@ const updateChatSessionOpencodeMetadata = (chatSessionId, sessionId, title, io)
532
506
  io.emit('chat-session:updated', chatSession);
533
507
  }
534
508
  };
535
- const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, initialSessionId = null, }) => {
509
+ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, initialSessionId = null, sessionOperations = hostOpencodeSessionOperations, }) => {
536
510
  let lookupTimer = null;
537
511
  let stopped = false;
538
512
  let knownSessionId = initialSessionId;
@@ -547,12 +521,12 @@ const createOpencodeMetadataRefresher = ({ chatSessionId, io, spawnTimestamp, in
547
521
  };
548
522
  const readCurrentSession = async () => {
549
523
  if (knownSessionId) {
550
- return hostOpencodeSessionOperations.findSessionById(knownSessionId);
524
+ return sessionOperations.findSessionById(knownSessionId);
551
525
  }
552
526
  if (spawnTimestamp === null) {
553
527
  return null;
554
528
  }
555
- return hostOpencodeSessionOperations.findLatestSessionAfter(spawnTimestamp);
529
+ return sessionOperations.findLatestSessionAfter(spawnTimestamp);
556
530
  };
557
531
  const refresh = async () => {
558
532
  if (stopped) {
@@ -1316,12 +1290,14 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, ex
1316
1290
  };
1317
1291
  const applySnapshot = ({ callId, label, status, log, response, sessionId, }) => {
1318
1292
  let run = findRun(callId);
1293
+ let created = false;
1319
1294
  if (!run) {
1320
1295
  handle([{ kind: 'start', callId, label, sessionId }]);
1321
1296
  run = findRun(callId);
1297
+ created = Boolean(run);
1322
1298
  }
1323
1299
  if (!run) {
1324
- return;
1300
+ return false;
1325
1301
  }
1326
1302
  bindSessionId(run, sessionId);
1327
1303
  updateLabel(run, label);
@@ -1330,7 +1306,7 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, ex
1330
1306
  const nextStatus = run.status !== 'running' && status === 'running' ? run.status : status;
1331
1307
  const hasChanged = run.log !== nextLog || run.response !== nextResponse || run.status !== nextStatus;
1332
1308
  if (!hasChanged) {
1333
- return;
1309
+ return created;
1334
1310
  }
1335
1311
  run.log = nextLog;
1336
1312
  run.response = nextResponse;
@@ -1343,7 +1319,9 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, ex
1343
1319
  finalizeAgentRun(run.id, nextStatus, run.response || null);
1344
1320
  }
1345
1321
  emitUpdate();
1322
+ return true;
1346
1323
  };
1324
+ const hasRunningSubagents = () => trackedRuns.some(run => run.status === 'running');
1347
1325
  const finalize = (status, parentLog, parentResponse) => {
1348
1326
  if (trackedRuns.length === 0 && !mainRun) {
1349
1327
  return null;
@@ -1454,11 +1432,13 @@ export const createSubagentRunTracker = ({ parentType, parentId, io, mainRun, ex
1454
1432
  updateAgentRunSessionId(mainRun.id, normalized);
1455
1433
  }
1456
1434
  };
1457
- return { applySnapshot, handle, finalize, setMainSessionId, updateMain };
1435
+ return { applySnapshot, handle, finalize, hasRunningSubagents, setMainSessionId, updateMain };
1458
1436
  };
1459
1437
  export const reconstructOpencodeSubagentTranscript = (parts, cwd) => {
1460
1438
  let log = '';
1461
1439
  let response = '';
1440
+ let finalResponse = '';
1441
+ let finalResponseMessageId = null;
1462
1442
  for (const persistedPart of parts) {
1463
1443
  if (persistedPart.role && persistedPart.role !== 'assistant') {
1464
1444
  continue;
@@ -1479,11 +1459,22 @@ export const reconstructOpencodeSubagentTranscript = (parts, cwd) => {
1479
1459
  if (parsed.responseFragment) {
1480
1460
  response = appendTranscriptText(response, parsed.responseFragment);
1481
1461
  }
1462
+ const finalAnswerFragments = (parsed.recoveryEvents ?? []).flatMap(event => (event.type === 'final_answer' ? [event.text] : []));
1463
+ if (finalAnswerFragments.length > 0) {
1464
+ const messageId = persistedPart.messageId ?? persistedPart.id;
1465
+ if (finalResponseMessageId !== messageId) {
1466
+ finalResponse = '';
1467
+ finalResponseMessageId = messageId;
1468
+ }
1469
+ for (const fragment of finalAnswerFragments) {
1470
+ finalResponse = appendTranscriptText(finalResponse, fragment);
1471
+ }
1472
+ }
1482
1473
  }
1483
- return { log: log || null, response: response || null };
1474
+ return { finalResponse: finalResponse || null, log: log || null, response: response || null };
1484
1475
  };
1485
- export const readOpencodeSessionTranscript = async ({ sessionId, since, cwd, }) => {
1486
- const reader = createOpencodeTaskSessionReader();
1476
+ export const readOpencodeSessionTranscript = async ({ sessionId, since, cwd, sessionOperations = hostOpencodeSessionOperations, }) => {
1477
+ const reader = createOpencodeTaskSessionReader(sessionOperations);
1487
1478
  if (!reader) {
1488
1479
  return null;
1489
1480
  }
@@ -1497,7 +1488,7 @@ export const readOpencodeSessionTranscript = async ({ sessionId, since, cwd, })
1497
1488
  reader.close();
1498
1489
  }
1499
1490
  };
1500
- export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTimestamp, cwd, tracker, onParentSessionId, taskSessionReader, discoverParentSession = true, parentDiscoveryDelayMs = 0, }) => {
1491
+ export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTimestamp, cwd, tracker, onParentSessionId, onActivity, taskSessionReader, discoverParentSession = true, parentDiscoveryDelayMs = 0, }) => {
1501
1492
  const reader = taskSessionReader === undefined ? createOpencodeTaskSessionReader() : taskSessionReader;
1502
1493
  let parentSessionId = initialParentSessionId;
1503
1494
  let timer = null;
@@ -1557,7 +1548,7 @@ export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTim
1557
1548
  const transcript = childSessionId
1558
1549
  ? reconstructOpencodeSubagentTranscript(await reader.listSessionParts(childSessionId), cwd)
1559
1550
  : { log: null, response: null };
1560
- tracker.applySnapshot({
1551
+ const changed = tracker.applySnapshot({
1561
1552
  callId: invocation.callId,
1562
1553
  label: invocation.label,
1563
1554
  status: invocation.status,
@@ -1565,6 +1556,9 @@ export const createOpencodeSubagentMonitor = ({ initialParentSessionId, spawnTim
1565
1556
  response: transcript.response ?? invocation.output,
1566
1557
  sessionId: childSessionId ?? null,
1567
1558
  });
1559
+ if (changed) {
1560
+ onActivity?.();
1561
+ }
1568
1562
  }
1569
1563
  }
1570
1564
  catch {
@@ -1648,6 +1642,19 @@ const interruptProcess = (state, signal) => {
1648
1642
  }
1649
1643
  killProcess(state.child, signal);
1650
1644
  };
1645
+ const interruptReviewProcess = async (state, child, signal) => {
1646
+ if (!isProcessAlive(child)) {
1647
+ return;
1648
+ }
1649
+ const interrupt = state.interrupts.get(child);
1650
+ if (interrupt) {
1651
+ await Promise.resolve(interrupt(signal)).catch(() => {
1652
+ // The execution provider owns interruption failure reporting.
1653
+ });
1654
+ return;
1655
+ }
1656
+ killProcess(child, signal);
1657
+ };
1651
1658
  const waitForProcessExit = async (proc, timeoutMs) => {
1652
1659
  const deadline = Date.now() + timeoutMs;
1653
1660
  await new Promise(resolve => {
@@ -1682,6 +1689,7 @@ export const registerChatProcessPlaceholder = (chatSessionId) => {
1682
1689
  const state = {
1683
1690
  child: null,
1684
1691
  interrupt: null,
1692
+ abortController: new AbortController(),
1685
1693
  researchChildren: new Set(),
1686
1694
  stopRequested: false,
1687
1695
  metadataRefresher: null,
@@ -1722,12 +1730,13 @@ const untrackSpecProcess = (specSessionId, state) => {
1722
1730
  // Registers a cancellation token for a spec session before its child process
1723
1731
  // exists. This closes the race window between the worker flipping the DB
1724
1732
  // status to 'running' and runSpecAgent() registering the abortable child: an
1725
- // interrupt during that window sets stopRequested on this placeholder, which
1726
- // the worker honors once its pre-step (prepareLinkedPlanningResearch) resolves.
1733
+ // interrupt during that window aborts preparation and sets stopRequested on
1734
+ // this placeholder, which the worker honors before starting the agent.
1727
1735
  export const registerSpecProcessPlaceholder = (specSessionId) => {
1728
1736
  const state = {
1729
1737
  child: null,
1730
1738
  interrupt: null,
1739
+ abortController: new AbortController(),
1731
1740
  researchChildren: new Set(),
1732
1741
  stopRequested: false,
1733
1742
  metadataRefresher: null,
@@ -1752,6 +1761,7 @@ export const registerSpecReviewProcessPlaceholder = (specReviewRunId) => {
1752
1761
  const state = {
1753
1762
  child: null,
1754
1763
  interrupt: null,
1764
+ abortController: new AbortController(),
1755
1765
  researchChildren: new Set(),
1756
1766
  stopRequested: false,
1757
1767
  metadataRefresher: null,
@@ -1767,7 +1777,7 @@ export const untrackSpecReviewProcessById = (specReviewRunId) => {
1767
1777
  };
1768
1778
  export const isBuildAgentRunning = (ticketId) => activeBuildProcesses.has(ticketId);
1769
1779
  export const registerReviewProcess = (ticketId) => {
1770
- const state = { children: new Set(), stopRequested: false };
1780
+ const state = { children: new Set(), interrupts: new Map(), stopRequested: false };
1771
1781
  activeReviewProcesses.set(ticketId, state);
1772
1782
  return state;
1773
1783
  };
@@ -1823,14 +1833,10 @@ export const stopReviewAgent = async (ticketId) => {
1823
1833
  }
1824
1834
  state.stopRequested = true;
1825
1835
  const children = [...state.children];
1826
- for (const child of children) {
1827
- killProcess(child, 'SIGTERM');
1828
- }
1836
+ await Promise.all(children.map(child => interruptReviewProcess(state, child, 'SIGTERM')));
1829
1837
  await Promise.all(children.map(child => waitForProcessExit(child, 5000)));
1830
1838
  const aliveChildren = children.filter(isProcessAlive);
1831
- for (const child of aliveChildren) {
1832
- killProcess(child, 'SIGKILL');
1833
- }
1839
+ await Promise.all(aliveChildren.map(child => interruptReviewProcess(state, child, 'SIGKILL')));
1834
1840
  await Promise.all(aliveChildren.map(child => waitForProcessExit(child, 1000)));
1835
1841
  return true;
1836
1842
  };
@@ -1840,14 +1846,25 @@ export const stopChatAgent = async (chatSessionId) => {
1840
1846
  return false;
1841
1847
  }
1842
1848
  state.stopRequested = true;
1849
+ state.abortController.abort(new Error('Chat stop requested'));
1843
1850
  state.metadataRefresher?.stop();
1844
1851
  if (!state.child) {
1845
1852
  return true;
1846
1853
  }
1847
- killProcess(state.child, 'SIGTERM');
1854
+ if (state.interrupt) {
1855
+ await state.interrupt('SIGTERM');
1856
+ }
1857
+ else {
1858
+ killProcess(state.child, 'SIGTERM');
1859
+ }
1848
1860
  await waitForProcessExit(state.child, 5000);
1849
1861
  if (isProcessAlive(state.child)) {
1850
- killProcess(state.child, 'SIGKILL');
1862
+ if (state.interrupt) {
1863
+ await state.interrupt('SIGKILL');
1864
+ }
1865
+ else {
1866
+ killProcess(state.child, 'SIGKILL');
1867
+ }
1851
1868
  await waitForProcessExit(state.child, 1000);
1852
1869
  }
1853
1870
  return true;
@@ -1858,23 +1875,43 @@ export const stopSpecAgent = async (specSessionId) => {
1858
1875
  return false;
1859
1876
  }
1860
1877
  state.stopRequested = true;
1878
+ state.abortController.abort(new Error('Spec stop requested'));
1861
1879
  state.metadataRefresher?.stop();
1862
- const children = [...state.researchChildren];
1863
- if (state.child) {
1864
- children.push(state.child);
1865
- }
1866
- if (children.length === 0) {
1880
+ const researchChildren = [...state.researchChildren];
1881
+ if (researchChildren.length === 0 && !state.child) {
1867
1882
  return true;
1868
1883
  }
1869
- for (const child of children) {
1884
+ for (const child of researchChildren) {
1870
1885
  killProcess(child, 'SIGTERM');
1871
1886
  }
1872
- await Promise.all(children.map(child => waitForProcessExit(child, 5000)));
1873
- const aliveChildren = children.filter(isProcessAlive);
1874
- for (const child of aliveChildren) {
1887
+ if (state.child) {
1888
+ if (state.interrupt) {
1889
+ await state.interrupt('SIGTERM');
1890
+ }
1891
+ else {
1892
+ killProcess(state.child, 'SIGTERM');
1893
+ }
1894
+ }
1895
+ await Promise.all([
1896
+ ...researchChildren.map(child => waitForProcessExit(child, 5000)),
1897
+ ...(state.child ? [waitForProcessExit(state.child, 5000)] : []),
1898
+ ]);
1899
+ const aliveResearchChildren = researchChildren.filter(isProcessAlive);
1900
+ for (const child of aliveResearchChildren) {
1875
1901
  killProcess(child, 'SIGKILL');
1876
1902
  }
1877
- await Promise.all(aliveChildren.map(child => waitForProcessExit(child, 1000)));
1903
+ if (isProcessAlive(state.child)) {
1904
+ if (state.interrupt) {
1905
+ await state.interrupt('SIGKILL');
1906
+ }
1907
+ else {
1908
+ killProcess(state.child, 'SIGKILL');
1909
+ }
1910
+ }
1911
+ await Promise.all([
1912
+ ...aliveResearchChildren.map(child => waitForProcessExit(child, 1000)),
1913
+ ...(isProcessAlive(state.child) ? [waitForProcessExit(state.child, 1000)] : []),
1914
+ ]);
1878
1915
  return true;
1879
1916
  };
1880
1917
  export const stopSpecReviewAgent = async (specReviewRunId) => {
@@ -1883,6 +1920,7 @@ export const stopSpecReviewAgent = async (specReviewRunId) => {
1883
1920
  return false;
1884
1921
  }
1885
1922
  state.stopRequested = true;
1923
+ state.abortController.abort(new Error('Spec review stop requested'));
1886
1924
  state.metadataRefresher?.stop();
1887
1925
  if (!state.child) {
1888
1926
  return true;
@@ -1942,24 +1980,32 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
1942
1980
  updateSessionId(ticketId, sessionId, sessionField, io);
1943
1981
  };
1944
1982
  const finalizeRun = (status, log, responseText, options = {}) => {
1983
+ const { transcriptResponseText = responseText, ...statusOptions } = options;
1945
1984
  if (runStream) {
1946
- finalizeAgentRun(runStream.runId, status, responseText);
1985
+ finalizeAgentRun(runStream.runId, status, transcriptResponseText);
1947
1986
  emitRunUpdate();
1948
- return subagentTracker.finalize(status, log, responseText);
1987
+ return subagentTracker.finalize(status, log, transcriptResponseText);
1949
1988
  }
1950
- updateStatus(ticketId, status, log, io, responseText, options);
1951
- return subagentTracker.finalize(status, log, responseText);
1989
+ updateStatus(ticketId, status, log, io, responseText, statusOptions);
1990
+ return subagentTracker.finalize(status, log, transcriptResponseText);
1952
1991
  };
1953
1992
  return new Promise((resolve, reject) => {
1954
1993
  let sessionId = null;
1955
1994
  let spawnTimestamp = null;
1956
1995
  let sessionLookupTimer = null;
1957
1996
  let hasResolvedOpencodeSessionId = Boolean(resumeSessionId);
1997
+ let hasTrustedOpencodeSessionId = Boolean(resumeSessionId);
1958
1998
  let opencodeSubagentMonitor = null;
1959
1999
  let settled = false;
1960
2000
  let startTime = null;
1961
2001
  let firstFinalAnswerTime = null;
2002
+ let reviewFinalAnswerObserved = false;
1962
2003
  let reviewChild = null;
2004
+ let reviewWatchdog = null;
2005
+ let reviewKillTimer = null;
2006
+ let reviewCompletionForced = false;
2007
+ let reviewInactivityTimeoutForced = false;
2008
+ let lastOutputAt = Date.now();
1963
2009
  let executionCapableEventObserved = false;
1964
2010
  const buildState = trackBuildProcess
1965
2011
  ? activeBuildProcesses.get(ticketId) ?? registerBuildProcessPlaceholder(ticketId)
@@ -1970,6 +2016,11 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
1970
2016
  }
1971
2017
  settled = true;
1972
2018
  opencodeSubagentMonitor?.stop();
2019
+ reviewWatchdog?.stop();
2020
+ if (reviewKillTimer) {
2021
+ clearTimeout(reviewKillTimer);
2022
+ reviewKillTimer = null;
2023
+ }
1973
2024
  if (sessionLookupTimer) {
1974
2025
  clearTimeout(sessionLookupTimer);
1975
2026
  sessionLookupTimer = null;
@@ -1979,12 +2030,13 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
1979
2030
  }
1980
2031
  if (reviewChild) {
1981
2032
  reviewProcessState?.children.delete(reviewChild);
2033
+ reviewProcessState?.interrupts.delete(reviewChild);
1982
2034
  }
1983
2035
  handler(value);
1984
2036
  };
1985
2037
  const resolveOnce = settle(resolve);
1986
2038
  const rejectOnce = settle(reject);
1987
- const bindResolvedOpencodeSession = (resolvedId) => {
2039
+ const bindResolvedOpencodeSession = (resolvedId, trusted = false) => {
1988
2040
  const normalized = resolvedId?.trim();
1989
2041
  if (!normalized) {
1990
2042
  return;
@@ -1992,6 +2044,7 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
1992
2044
  const shouldPersist = sessionId !== normalized;
1993
2045
  sessionId = normalized;
1994
2046
  hasResolvedOpencodeSessionId = true;
2047
+ hasTrustedOpencodeSessionId = hasTrustedOpencodeSessionId || trusted;
1995
2048
  opencodeSubagentMonitor?.setParentSessionId(normalized);
1996
2049
  if (shouldPersist) {
1997
2050
  writeRunSessionId(normalized);
@@ -2078,9 +2131,7 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2078
2131
  }
2079
2132
  if (reviewProcessState) {
2080
2133
  reviewProcessState.children.add(child);
2081
- if (reviewProcessState.stopRequested) {
2082
- killProcess(child, 'SIGTERM');
2083
- }
2134
+ reviewProcessState.interrupts.set(child, interrupt);
2084
2135
  }
2085
2136
  void placementFailure.then(failure => {
2086
2137
  if (!failure || settled) {
@@ -2105,9 +2156,12 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2105
2156
  onParentSessionId: discoveredSessionId => {
2106
2157
  bindResolvedOpencodeSession(discoveredSessionId);
2107
2158
  },
2159
+ onActivity: () => {
2160
+ lastOutputAt = Date.now();
2161
+ },
2108
2162
  });
2109
2163
  opencodeSubagentMonitor.start();
2110
- bindResolvedOpencodeSession(resumeSessionId);
2164
+ bindResolvedOpencodeSession(resumeSessionId, true);
2111
2165
  }
2112
2166
  if (tool === 'claude') {
2113
2167
  const logLines = [];
@@ -2196,6 +2250,9 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2196
2250
  const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2197
2251
  rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2198
2252
  });
2253
+ if (reviewProcessState?.stopRequested) {
2254
+ void interruptReviewProcess(reviewProcessState, child, 'SIGTERM');
2255
+ }
2199
2256
  return;
2200
2257
  }
2201
2258
  const logLines = [];
@@ -2207,7 +2264,27 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2207
2264
  const progressUpdater = createThrottledProgressUpdater(() => {
2208
2265
  writeRunStatus('running', logLines.join('\n\n'), responseLines.join('\n\n'));
2209
2266
  });
2267
+ if (reviewProcessState && startTime !== null) {
2268
+ reviewWatchdog = createReviewAgentWatchdog({
2269
+ getLastOutputAt: () => lastOutputAt,
2270
+ checkForCompletion: async () => reviewFinalAnswerObserved && !subagentTracker.hasRunningSubagents(),
2271
+ onTrigger: async (trigger) => {
2272
+ if (trigger === 'completed') {
2273
+ reviewCompletionForced = true;
2274
+ }
2275
+ else {
2276
+ reviewInactivityTimeoutForced = true;
2277
+ }
2278
+ await interruptReviewProcess(reviewProcessState, child, 'SIGTERM');
2279
+ reviewKillTimer = setTimeout(() => {
2280
+ void interruptReviewProcess(reviewProcessState, child, 'SIGKILL');
2281
+ }, REVIEW_AGENT_KILL_GRACE_MS);
2282
+ reviewKillTimer.unref?.();
2283
+ },
2284
+ });
2285
+ }
2210
2286
  child.stdout?.on('data', (chunk) => {
2287
+ lastOutputAt = Date.now();
2211
2288
  const text = remainder + chunk.toString();
2212
2289
  const lines = text.split('\n');
2213
2290
  remainder = lines.pop() ?? '';
@@ -2230,13 +2307,19 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2230
2307
  }
2231
2308
  const parsed = parseOpencodeJsonLine(line);
2232
2309
  const { logFragment, responseFragment, subagentEvents, providerSessionId } = parsed;
2310
+ const hasExplicitFinalAnswer = parsed.recoveryEvents?.some(event => event.type === 'final_answer') === true;
2233
2311
  if (isExecutionCapableParsedStreamLine(parsed)) {
2234
2312
  executionCapableEventObserved = true;
2313
+ reviewFinalAnswerObserved = hasExplicitFinalAnswer;
2314
+ }
2315
+ else if (hasExplicitFinalAnswer) {
2316
+ reviewFinalAnswerObserved = true;
2235
2317
  }
2236
- bindResolvedOpencodeSession(providerSessionId);
2318
+ bindResolvedOpencodeSession(providerSessionId, true);
2237
2319
  subagentTracker.handle(subagentEvents);
2238
2320
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2239
2321
  firstFinalAnswerTime = null;
2322
+ reviewFinalAnswerObserved = false;
2240
2323
  }
2241
2324
  if (logFragment !== null || responseFragment !== null) {
2242
2325
  progressUpdater.schedule();
@@ -2256,6 +2339,11 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2256
2339
  }
2257
2340
  child.on('close', async (code) => {
2258
2341
  progressUpdater.cancel();
2342
+ reviewWatchdog?.stop();
2343
+ if (reviewKillTimer) {
2344
+ clearTimeout(reviewKillTimer);
2345
+ reviewKillTimer = null;
2346
+ }
2259
2347
  const finalPlacementFailure = await placementCompletion;
2260
2348
  if (finalPlacementFailure || settled) {
2261
2349
  return;
@@ -2266,34 +2354,82 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2266
2354
  const { subagentEvents, providerSessionId } = parsed;
2267
2355
  if (isExecutionCapableParsedStreamLine(parsed)) {
2268
2356
  executionCapableEventObserved = true;
2357
+ reviewFinalAnswerObserved = false;
2269
2358
  }
2270
- bindResolvedOpencodeSession(providerSessionId);
2359
+ bindResolvedOpencodeSession(providerSessionId, true);
2271
2360
  subagentTracker.handle(subagentEvents);
2272
2361
  if (appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker })) {
2273
2362
  firstFinalAnswerTime = null;
2274
2363
  }
2275
2364
  }
2276
- const resolvedSessionOnClose = await attemptOpencodeSessionLookup();
2365
+ const collectCloseEnrichment = async () => {
2366
+ const resolvedSessionOnClose = await attemptOpencodeSessionLookup();
2367
+ await opencodeSubagentMonitor?.refresh();
2368
+ const hasPersistedCompletion = Boolean(opencodeSessionOperations
2369
+ && hasTrustedOpencodeSessionId
2370
+ && sessionId
2371
+ && spawnTimestamp !== null
2372
+ && await opencodeSessionOperations.hasCompletedAssistantSince(sessionId, spawnTimestamp));
2373
+ const persistedTranscript = hasPersistedCompletion && opencodeSessionOperations && sessionId
2374
+ ? await readOpencodeSessionTranscript({
2375
+ sessionId,
2376
+ since: spawnTimestamp ?? undefined,
2377
+ cwd,
2378
+ sessionOperations: opencodeSessionOperations,
2379
+ })
2380
+ : null;
2381
+ return { resolvedSessionOnClose, hasPersistedCompletion, persistedTranscript };
2382
+ };
2383
+ const closeEnrichmentFallback = {
2384
+ resolvedSessionOnClose: hasResolvedOpencodeSessionId,
2385
+ hasPersistedCompletion: false,
2386
+ persistedTranscript: null,
2387
+ };
2388
+ const closeEnrichment = reviewProcessState
2389
+ ? await resolveWithin(collectCloseEnrichment(), REVIEW_AGENT_CLOSE_ENRICHMENT_TIMEOUT_MS, closeEnrichmentFallback)
2390
+ : await collectCloseEnrichment();
2391
+ const { resolvedSessionOnClose, hasPersistedCompletion, persistedTranscript } = closeEnrichment;
2277
2392
  if (sessionLookupTimer && (resolvedSessionOnClose || hasResolvedOpencodeSessionId)) {
2278
2393
  clearTimeout(sessionLookupTimer);
2279
2394
  sessionLookupTimer = null;
2280
2395
  }
2281
2396
  const stderrOutput = Buffer.concat(stderrChunks).toString();
2282
- await opencodeSubagentMonitor?.refresh();
2283
- const log = logLines.join('\n\n') || stderrOutput;
2397
+ const log = logLines.join('\n\n') || persistedTranscript?.log || stderrOutput;
2284
2398
  const recoveredResponse = recoveryTracker.getRecoveredResponse();
2285
- const finalResponse = recoveryTracker.hasRecoveredFinalAnswer()
2399
+ const streamedResponse = recoveryTracker.hasRecoveredFinalAnswer()
2286
2400
  ? recoveredResponse
2287
- : responseLines.join('\n\n') || stderrOutput;
2401
+ : responseLines.join('\n\n');
2402
+ const persistedResponse = persistedTranscript?.response?.trim() ?? '';
2403
+ const persistedFinalResponse = persistedTranscript?.finalResponse?.trim() ?? '';
2404
+ const selectedPersistedResponse = reviewProcessState
2405
+ ? persistedFinalResponse || (!streamedResponse ? persistedResponse : '')
2406
+ : persistedResponse;
2407
+ const finalResponse = selectedPersistedResponse && (reviewCompletionForced || hasPersistedCompletion || !streamedResponse)
2408
+ ? selectedPersistedResponse
2409
+ : streamedResponse || stderrOutput;
2410
+ const transcriptResponseText = reviewProcessState && persistedResponse
2411
+ ? persistedResponse
2412
+ : finalResponse;
2288
2413
  const stderrModelError = parseOpencodeStderrModelResolutionError(stderrOutput);
2289
2414
  const timing = buildResponseTiming({ startTime, firstFinalAnswerTime, isStopped: Boolean(buildState?.stopRequested || reviewProcessState?.stopRequested) });
2290
2415
  if (buildState?.stopRequested || reviewProcessState?.stopRequested) {
2291
- const transcriptRuns = finalizeRun('stopped', log, finalResponse);
2416
+ const transcriptRuns = finalizeRun('stopped', log, finalResponse, { transcriptResponseText });
2292
2417
  resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing, usageSnapshot }, transcriptRuns));
2293
2418
  return;
2294
2419
  }
2295
- if (isSuccessfulOpencodeExit(code, stderrModelError, recoveryTracker)) {
2296
- const transcriptRuns = finalizeRun('done', log, finalResponse, { emit: emitSuccessUpdate });
2420
+ const hasUsableReviewResponse = Boolean((persistedResponse || streamedResponse).trim());
2421
+ if (reviewInactivityTimeoutForced) {
2422
+ const message = `Review agent produced no output for ${REVIEW_AGENT_INACTIVITY_TIMEOUT_MS} ms.`;
2423
+ const transcriptRuns = finalizeRun('error', log || message, finalResponse || null, { transcriptResponseText });
2424
+ rejectOnce(attachTranscriptRunsToError(new Error(message), transcriptRuns));
2425
+ return;
2426
+ }
2427
+ const forcedReviewCompletion = Boolean(reviewProcessState
2428
+ && hasUsableReviewResponse
2429
+ && !recoveryTracker.hasUnrecoveredError()
2430
+ && reviewCompletionForced);
2431
+ if (isSuccessfulOpencodeExit(code, stderrModelError, recoveryTracker) || forcedReviewCompletion) {
2432
+ const transcriptRuns = finalizeRun('done', log, finalResponse, { emit: emitSuccessUpdate, transcriptResponseText });
2297
2433
  resolveOnce(withTranscriptRuns({ response: finalResponse, responseTiming: timing, usageSnapshot }, transcriptRuns));
2298
2434
  return;
2299
2435
  }
@@ -2313,27 +2449,51 @@ const runAgentOnce = ({ ticketId, tool, prompt, cwd, io, model, variant = null,
2313
2449
  });
2314
2450
  child.on('error', err => {
2315
2451
  progressUpdater.cancel();
2452
+ reviewWatchdog?.stop();
2453
+ if (reviewKillTimer) {
2454
+ clearTimeout(reviewKillTimer);
2455
+ reviewKillTimer = null;
2456
+ }
2457
+ if (remainder.trim()) {
2458
+ usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2459
+ const parsed = parseOpencodeJsonLine(remainder);
2460
+ bindResolvedOpencodeSession(parsed.providerSessionId, true);
2461
+ subagentTracker.handle(parsed.subagentEvents);
2462
+ appendOpencodeTranscriptLine({ parsed, logLines, responseLines, recoveryTracker });
2463
+ }
2464
+ const stderrOutput = Buffer.concat(stderrChunks).toString();
2465
+ const log = logLines.join('\n\n') || stderrOutput;
2466
+ const response = recoveryTracker.hasRecoveredFinalAnswer()
2467
+ ? recoveryTracker.getRecoveredResponse()
2468
+ : responseLines.join('\n\n') || stderrOutput;
2316
2469
  if (sessionLookupTimer && hasResolvedOpencodeSessionId) {
2317
2470
  clearTimeout(sessionLookupTimer);
2318
2471
  sessionLookupTimer = null;
2319
2472
  }
2320
2473
  if (buildState?.stopRequested || reviewProcessState?.stopRequested) {
2321
- const transcriptRuns = finalizeRun('stopped', null, null);
2322
- resolveOnce(withTranscriptRuns({ response: null, responseTiming: null }, transcriptRuns));
2474
+ const transcriptRuns = finalizeRun('stopped', log, response);
2475
+ resolveOnce(withTranscriptRuns({ response, responseTiming: null, usageSnapshot }, transcriptRuns));
2323
2476
  return;
2324
2477
  }
2325
2478
  const resolvedError = agentProvider.classifyExecutionFailure(err);
2326
- const transcriptRuns = finalizeRun('error', getErrorMessage(resolvedError), null);
2479
+ const transcriptRuns = finalizeRun('error', log || getErrorMessage(resolvedError), response);
2327
2480
  rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2328
2481
  });
2482
+ if (reviewProcessState?.stopRequested) {
2483
+ void interruptReviewProcess(reviewProcessState, child, 'SIGTERM');
2484
+ }
2485
+ else {
2486
+ reviewWatchdog?.start();
2487
+ }
2329
2488
  });
2330
2489
  };
2331
2490
  export const runAgent = (options) => {
2332
2491
  const run = () => runAgentOnce(options);
2333
2492
  return options.tool === 'opencode' ? retryOpenCodeDatabaseLocks(run) : run();
2334
2493
  };
2335
- const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, opencodeAgentMode = 'build', executionContext, mainRun, spawnPermitId, }) => {
2494
+ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, opencodeAgentMode = 'build', executionContext, mainRun, spawnPermitId, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), }) => {
2336
2495
  const resolvedModel = resolveAgentModel(tool, model);
2496
+ const opencodeSessionOperations = executionProvider.sessionOperations?.opencode ?? null;
2337
2497
  const subagentTracker = createSubagentRunTracker({
2338
2498
  parentType: 'chat',
2339
2499
  parentId: chatSessionId,
@@ -2375,15 +2535,20 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2375
2535
  }, transcriptRuns));
2376
2536
  return;
2377
2537
  }
2378
- const [cmd, args] = tool === 'claude'
2379
- ? (() => {
2380
- sessionId = resumeSessionId || randomUUID();
2381
- return [
2382
- 'claude',
2383
- buildClaudeArgs(opencodeAgentMode, resolvedModel, resumeSessionId, sessionId),
2384
- ];
2385
- })()
2386
- : ['opencode', ['run', ...(opencodeAgentMode === 'plan' ? ['--agent', 'plan'] : []), '--format', 'json', '--thinking', ...(resumeSessionId ? ['--session', resumeSessionId] : []), ...(resolvedModel ? ['--model', resolvedModel] : []), ...(variant ? ['--variant', variant] : []), ...files.flatMap(filePath => ['--file', filePath]), ...(files.length > 0 ? ['--'] : []), prompt]];
2538
+ const agentProvider = tool === 'claude'
2539
+ ? claudeAgentProvider
2540
+ : opencodeAgentProvider;
2541
+ const invocation = agentProvider.buildStreamingInvocation({
2542
+ cwd,
2543
+ files,
2544
+ prompt,
2545
+ model: resolvedModel,
2546
+ variant,
2547
+ mode: opencodeAgentMode,
2548
+ resumeSessionId,
2549
+ sessionId: randomUUID(),
2550
+ });
2551
+ sessionId = invocation.sessionId;
2387
2552
  updateChatSessionStatus(chatSessionId, 'running', null, io);
2388
2553
  subagentTracker.updateMain(null, null);
2389
2554
  if (tool === 'claude' && sessionId) {
@@ -2397,6 +2562,7 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2397
2562
  io,
2398
2563
  spawnTimestamp,
2399
2564
  initialSessionId: resumeSessionId,
2565
+ ...(opencodeSessionOperations ? { sessionOperations: opencodeSessionOperations } : {}),
2400
2566
  });
2401
2567
  chatState.metadataRefresher = opencodeMetadataRefresher;
2402
2568
  if (resumeSessionId) {
@@ -2409,6 +2575,7 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2409
2575
  }
2410
2576
  }
2411
2577
  let child;
2578
+ let placementCompletion;
2412
2579
  try {
2413
2580
  if (chatState.stopRequested) {
2414
2581
  throw new AgentSpawnPermitRevokedError();
@@ -2416,27 +2583,40 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2416
2583
  if (spawnPermitId) {
2417
2584
  assertAgentSpawnPermit(spawnPermitId, executionContext.runId);
2418
2585
  }
2419
- child = spawn(cmd, args, {
2586
+ const handle = executionProvider.execute({
2587
+ phase: 'agent',
2588
+ target: executionTarget ?? null,
2589
+ command: invocation.command,
2590
+ args: invocation.args,
2420
2591
  cwd,
2421
- stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2422
- env: createWorkspaceProcessEnvironment(cwd, { PWD: cwd }),
2592
+ stdio: invocation.stdio,
2593
+ launcherEnvironment: executionLauncherEnvironment,
2594
+ runtimeEnvironment: executionRuntimeEnvironment,
2423
2595
  detached: process.platform !== 'win32',
2424
2596
  });
2597
+ child = handle.child;
2598
+ chatState.interrupt = handle.interrupt;
2599
+ placementCompletion = handle.placementFailure;
2425
2600
  }
2426
2601
  catch (error) {
2427
- const resolvedError = error instanceof Error ? error : new Error(String(error));
2602
+ const resolvedError = agentProvider.classifyExecutionFailure(error);
2428
2603
  const transcriptRuns = subagentTracker.finalize('error', resolvedError.message, null);
2429
2604
  rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2430
2605
  return;
2431
2606
  }
2432
2607
  chatState.child = child;
2433
2608
  activeChatProcesses.set(chatSessionId, chatState);
2434
- if (tool === 'claude') {
2435
- const claudePrompt = opencodeAgentMode === 'plan'
2436
- ? appendClaudeHeadlessPlanPrompt(prompt)
2437
- : prompt;
2438
- writeClaudeStdin(child, buildClaudeStdinMessage(claudePrompt, files, cwd));
2609
+ if (invocation.stdin !== null) {
2610
+ writeClaudeStdin(child, invocation.stdin);
2439
2611
  }
2612
+ void placementCompletion.then(failure => {
2613
+ if (!failure || settled) {
2614
+ return;
2615
+ }
2616
+ const resolvedError = agentProvider.classifyExecutionFailure(failure);
2617
+ const transcriptRuns = subagentTracker.finalize('error', resolvedError.message, null);
2618
+ rejectOnce(attachTranscriptRunsToError(resolvedError, transcriptRuns));
2619
+ });
2440
2620
  startTime = Date.now();
2441
2621
  if (tool === 'opencode' && spawnTimestamp !== null) {
2442
2622
  opencodeSubagentMonitor = createOpencodeSubagentMonitor({
@@ -2444,6 +2624,9 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2444
2624
  spawnTimestamp,
2445
2625
  cwd,
2446
2626
  tracker: subagentTracker,
2627
+ taskSessionReader: opencodeSessionOperations
2628
+ ? createOpencodeTaskSessionReader(opencodeSessionOperations)
2629
+ : null,
2447
2630
  discoverParentSession: false,
2448
2631
  onParentSessionId: discoveredSessionId => {
2449
2632
  updateChatSessionId(chatSessionId, 'opencode', discoveredSessionId, io);
@@ -2487,8 +2670,12 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2487
2670
  }
2488
2671
  });
2489
2672
  child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
2490
- child.on('close', code => {
2673
+ child.on('close', async (code) => {
2491
2674
  progressUpdater.cancel();
2675
+ const placementFailure = await placementCompletion;
2676
+ if (placementFailure || settled) {
2677
+ return;
2678
+ }
2492
2679
  if (remainder.trim()) {
2493
2680
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2494
2681
  const parsed = parseClaudeStreamJsonLine(remainder);
@@ -2627,6 +2814,10 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2627
2814
  }
2628
2815
  child.on('close', async (code) => {
2629
2816
  progressUpdater.cancel();
2817
+ const placementFailure = await placementCompletion;
2818
+ if (placementFailure || settled) {
2819
+ return;
2820
+ }
2630
2821
  if (remainder.trim()) {
2631
2822
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2632
2823
  const parsed = parseOpencodeJsonLine(remainder, cwd);
@@ -2665,6 +2856,7 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2665
2856
  current: usageSnapshot,
2666
2857
  sessionId: resolvedOpencodeSessionId,
2667
2858
  spawnTimestamp,
2859
+ ...(opencodeSessionOperations ? { sessionOperations: opencodeSessionOperations } : {}),
2668
2860
  });
2669
2861
  }
2670
2862
  updateChatSessionStatus(chatSessionId, 'running', preservedOutput?.log ?? null, io, preservedOutput?.response ?? null);
@@ -2689,6 +2881,7 @@ const runChatAgentOnce = ({ chatSessionId, tool, prompt, files = [], cwd, io, mo
2689
2881
  current: usageSnapshot,
2690
2882
  sessionId: resolvedOpencodeSessionId,
2691
2883
  spawnTimestamp,
2884
+ ...(opencodeSessionOperations ? { sessionOperations: opencodeSessionOperations } : {}),
2692
2885
  });
2693
2886
  }
2694
2887
  updateChatSessionStatus(chatSessionId, 'running', finalOutput.log, io, finalOutput.response);
@@ -2746,7 +2939,7 @@ export const runChatAgent = (options) => {
2746
2939
  const run = () => runChatAgentOnce(options);
2747
2940
  return options.tool === 'opencode' ? retryOpenCodeDatabaseLocks(run) : run();
2748
2941
  };
2749
- const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, executionContext, mainRun, }) => {
2942
+ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, resumeSessionId = null, executionContext, mainRun, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), }) => {
2750
2943
  const resolvedModel = resolveAgentModel(tool, model);
2751
2944
  const subagentTracker = createSubagentRunTracker({
2752
2945
  parentType: 'spec',
@@ -2822,13 +3015,27 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
2822
3015
  }
2823
3016
  }
2824
3017
  let child;
3018
+ let placementFailure = Promise.resolve(null);
2825
3019
  try {
2826
- child = spawn(cmd, args, {
3020
+ const handle = executionProvider.execute({
3021
+ phase: 'agent',
3022
+ target: executionTarget ?? null,
3023
+ command: cmd,
3024
+ args,
2827
3025
  cwd,
2828
3026
  stdio: tool === 'claude' ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
2829
- env: createWorkspaceProcessEnvironment(cwd, { PWD: cwd }),
3027
+ launcherEnvironment: executionLauncherEnvironment,
3028
+ runtimeEnvironment: executionRuntimeEnvironment,
2830
3029
  detached: process.platform !== 'win32',
2831
3030
  });
3031
+ child = handle.child;
3032
+ specState.interrupt = handle.interrupt;
3033
+ placementFailure = handle.placementFailure;
3034
+ void placementFailure.then(failure => {
3035
+ if (failure) {
3036
+ rejectOnce(failure);
3037
+ }
3038
+ });
2832
3039
  }
2833
3040
  catch (error) {
2834
3041
  const resolvedError = error instanceof Error ? error : new Error(String(error));
@@ -2887,8 +3094,13 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
2887
3094
  }
2888
3095
  });
2889
3096
  child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
2890
- child.on('close', code => {
3097
+ child.on('close', async (code) => {
2891
3098
  progressUpdater.cancel();
3099
+ const placementError = await placementFailure;
3100
+ if (placementError) {
3101
+ rejectOnce(placementError);
3102
+ return;
3103
+ }
2892
3104
  if (remainder.trim()) {
2893
3105
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
2894
3106
  const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(remainder);
@@ -3016,6 +3228,11 @@ const runSpecAgentOnce = ({ specSessionId, tool, prompt, files = [], cwd, io, mo
3016
3228
  }
3017
3229
  child.on('close', async (code) => {
3018
3230
  progressUpdater.cancel();
3231
+ const placementError = await placementFailure;
3232
+ if (placementError) {
3233
+ rejectOnce(placementError);
3234
+ return;
3235
+ }
3019
3236
  await opencodeSubagentMonitor?.refresh();
3020
3237
  if (remainder.trim()) {
3021
3238
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
@@ -3141,7 +3358,7 @@ export const runSpecAgent = (options) => {
3141
3358
  * updateSpecReviewRunStatus / updateSpecReviewRunStreaming / updateSpecReviewRunUsage —
3142
3359
  * the spec_sessions row is never touched by this path.
3143
3360
  */
3144
- const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, executionContext, }) => {
3361
+ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt, files = [], cwd, io, model, variant = null, executionContext, executionProvider = hostExecutionEnvironment, executionTarget, executionLauncherEnvironment = createHostLauncherEnvironment(), executionRuntimeEnvironment = createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }), }) => {
3145
3362
  const registeredState = activeSpecReviewProcesses.get(specReviewRunId);
3146
3363
  if (registeredState?.stopRequested) {
3147
3364
  return Promise.resolve({
@@ -3169,6 +3386,7 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3169
3386
  const reviewState = registeredState ?? {
3170
3387
  child: null,
3171
3388
  interrupt: null,
3389
+ abortController: new AbortController(),
3172
3390
  researchChildren: new Set(),
3173
3391
  stopRequested: false,
3174
3392
  metadataRefresher: null,
@@ -3189,42 +3407,39 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3189
3407
  buildClaudeArgs('plan', resolvedModel, null, randomUUID()),
3190
3408
  ];
3191
3409
  updateSpecReviewRunStreaming(specReviewRunId, null);
3192
- const env = createWorkspaceProcessEnvironment(cwd, { PWD: cwd });
3193
3410
  let child;
3194
- if (tool === 'opencode') {
3195
- const invocation = buildOpencodeSpecReviewInvocation({ prompt, files, model: resolvedModel, variant });
3196
- try {
3197
- const handle = hostExecutionEnvironment.execute({
3198
- phase: 'agent',
3199
- target: null,
3200
- command: invocation.command,
3201
- args: invocation.args,
3202
- cwd,
3203
- stdio: invocation.stdio,
3204
- launcherEnvironment: createHostLauncherEnvironment(),
3205
- runtimeEnvironment: createLegacyHostRuntimeEnvironment(cwd, { PWD: cwd }),
3206
- detached: process.platform !== 'win32',
3207
- });
3208
- child = handle.child;
3209
- reviewState.interrupt = handle.interrupt;
3210
- void handle.placementFailure.then(failure => {
3211
- if (failure) {
3212
- rejectOnce(opencodeAgentProvider.classifyExecutionFailure(failure));
3213
- }
3214
- });
3215
- }
3216
- catch (error) {
3217
- rejectOnce(opencodeAgentProvider.classifyExecutionFailure(error));
3218
- return;
3219
- }
3220
- }
3221
- else {
3222
- child = spawn(cmd, args, {
3223
- cwd,
3411
+ let placementFailure = Promise.resolve(null);
3412
+ const invocation = tool === 'opencode'
3413
+ ? buildOpencodeSpecReviewInvocation({ prompt, files, model: resolvedModel, variant })
3414
+ : {
3415
+ command: cmd,
3416
+ args,
3224
3417
  stdio: ['pipe', 'pipe', 'pipe'],
3225
- env,
3418
+ };
3419
+ try {
3420
+ const handle = executionProvider.execute({
3421
+ phase: 'agent',
3422
+ target: executionTarget ?? null,
3423
+ command: invocation.command,
3424
+ args: invocation.args,
3425
+ cwd,
3426
+ stdio: invocation.stdio,
3427
+ launcherEnvironment: executionLauncherEnvironment,
3428
+ runtimeEnvironment: executionRuntimeEnvironment,
3226
3429
  detached: process.platform !== 'win32',
3227
3430
  });
3431
+ child = handle.child;
3432
+ reviewState.interrupt = handle.interrupt;
3433
+ placementFailure = handle.placementFailure;
3434
+ void placementFailure.then(failure => {
3435
+ if (failure) {
3436
+ rejectOnce(tool === 'opencode' ? opencodeAgentProvider.classifyExecutionFailure(failure) : failure);
3437
+ }
3438
+ });
3439
+ }
3440
+ catch (error) {
3441
+ rejectOnce(tool === 'opencode' ? opencodeAgentProvider.classifyExecutionFailure(error) : error);
3442
+ return;
3228
3443
  }
3229
3444
  reviewState.child = child;
3230
3445
  activeSpecReviewProcesses.set(specReviewRunId, reviewState);
@@ -3274,8 +3489,13 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3274
3489
  }
3275
3490
  });
3276
3491
  child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
3277
- child.on('close', code => {
3492
+ child.on('close', async (code) => {
3278
3493
  progressUpdater.cancel();
3494
+ const placementError = await placementFailure;
3495
+ if (placementError) {
3496
+ rejectOnce(placementError);
3497
+ return;
3498
+ }
3279
3499
  if (remainder.trim()) {
3280
3500
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);
3281
3501
  const { logFragment, responseFragment, subagentEvents } = parseClaudeStreamJsonLine(remainder);
@@ -3368,6 +3588,11 @@ const runSpecReviewAgentOnce = ({ specReviewRunId, specSessionId, tool, prompt,
3368
3588
  child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
3369
3589
  child.on('close', async (code) => {
3370
3590
  progressUpdater.cancel();
3591
+ const placementError = await placementFailure;
3592
+ if (placementError) {
3593
+ rejectOnce(tool === 'opencode' ? opencodeAgentProvider.classifyExecutionFailure(placementError) : placementError);
3594
+ return;
3595
+ }
3371
3596
  await opencodeSubagentMonitor?.refresh();
3372
3597
  if (remainder.trim()) {
3373
3598
  usageSnapshot = mergeUsageFromJsonLine(usageSnapshot, remainder);