@xenosystem/agent-interface-host 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (482) hide show
  1. package/LICENSE +67 -0
  2. package/dist/acp/AcpAgentRegistryService.d.ts +77 -0
  3. package/dist/acp/AcpAgentRegistryService.d.ts.map +1 -0
  4. package/dist/acp/AcpAgentRegistryService.js +754 -0
  5. package/dist/acp/AcpAgentRegistryService.js.map +1 -0
  6. package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts +56 -0
  7. package/dist/acp/AcpAgentTurnExecutionAdapter.d.ts.map +1 -0
  8. package/dist/acp/AcpAgentTurnExecutionAdapter.js +323 -0
  9. package/dist/acp/AcpAgentTurnExecutionAdapter.js.map +1 -0
  10. package/dist/acp/AcpApprovalPortAdapter.d.ts +41 -0
  11. package/dist/acp/AcpApprovalPortAdapter.d.ts.map +1 -0
  12. package/dist/acp/AcpApprovalPortAdapter.js +82 -0
  13. package/dist/acp/AcpApprovalPortAdapter.js.map +1 -0
  14. package/dist/acp/AcpCoordinationBridge.d.ts +13 -0
  15. package/dist/acp/AcpCoordinationBridge.d.ts.map +1 -0
  16. package/dist/acp/AcpCoordinationBridge.js +13 -0
  17. package/dist/acp/AcpCoordinationBridge.js.map +1 -0
  18. package/dist/acp/AcpDiagnosticsService.d.ts +61 -0
  19. package/dist/acp/AcpDiagnosticsService.d.ts.map +1 -0
  20. package/dist/acp/AcpDiagnosticsService.js +203 -0
  21. package/dist/acp/AcpDiagnosticsService.js.map +1 -0
  22. package/dist/acp/AcpExternalCertificationHarness.d.ts +47 -0
  23. package/dist/acp/AcpExternalCertificationHarness.d.ts.map +1 -0
  24. package/dist/acp/AcpExternalCertificationHarness.js +176 -0
  25. package/dist/acp/AcpExternalCertificationHarness.js.map +1 -0
  26. package/dist/acp/AcpHostBridge.d.ts +81 -0
  27. package/dist/acp/AcpHostBridge.d.ts.map +1 -0
  28. package/dist/acp/AcpHostBridge.js +101 -0
  29. package/dist/acp/AcpHostBridge.js.map +1 -0
  30. package/dist/acp/AcpInteractivePermissionBridge.d.ts +77 -0
  31. package/dist/acp/AcpInteractivePermissionBridge.d.ts.map +1 -0
  32. package/dist/acp/AcpInteractivePermissionBridge.js +127 -0
  33. package/dist/acp/AcpInteractivePermissionBridge.js.map +1 -0
  34. package/dist/acp/AcpPermissionBridge.d.ts +19 -0
  35. package/dist/acp/AcpPermissionBridge.d.ts.map +1 -0
  36. package/dist/acp/AcpPermissionBridge.js +110 -0
  37. package/dist/acp/AcpPermissionBridge.js.map +1 -0
  38. package/dist/acp/AcpPermissionPolicy.d.ts +111 -0
  39. package/dist/acp/AcpPermissionPolicy.d.ts.map +1 -0
  40. package/dist/acp/AcpPermissionPolicy.js +167 -0
  41. package/dist/acp/AcpPermissionPolicy.js.map +1 -0
  42. package/dist/acp/AcpProcessSupervisor.d.ts +134 -0
  43. package/dist/acp/AcpProcessSupervisor.d.ts.map +1 -0
  44. package/dist/acp/AcpProcessSupervisor.js +674 -0
  45. package/dist/acp/AcpProcessSupervisor.js.map +1 -0
  46. package/dist/acp/AcpProviderApprovalService.d.ts +27 -0
  47. package/dist/acp/AcpProviderApprovalService.d.ts.map +1 -0
  48. package/dist/acp/AcpProviderApprovalService.js +128 -0
  49. package/dist/acp/AcpProviderApprovalService.js.map +1 -0
  50. package/dist/acp/AcpProviderCatalogService.d.ts +69 -0
  51. package/dist/acp/AcpProviderCatalogService.d.ts.map +1 -0
  52. package/dist/acp/AcpProviderCatalogService.js +707 -0
  53. package/dist/acp/AcpProviderCatalogService.js.map +1 -0
  54. package/dist/acp/AcpProviderModelService.d.ts +6 -0
  55. package/dist/acp/AcpProviderModelService.d.ts.map +1 -0
  56. package/dist/acp/AcpProviderModelService.js +274 -0
  57. package/dist/acp/AcpProviderModelService.js.map +1 -0
  58. package/dist/acp/AcpProviderVersionReadiness.d.ts +6 -0
  59. package/dist/acp/AcpProviderVersionReadiness.d.ts.map +1 -0
  60. package/dist/acp/AcpProviderVersionReadiness.js +100 -0
  61. package/dist/acp/AcpProviderVersionReadiness.js.map +1 -0
  62. package/dist/acp/AcpRuntimeEventMapper.d.ts +16 -0
  63. package/dist/acp/AcpRuntimeEventMapper.d.ts.map +1 -0
  64. package/dist/acp/AcpRuntimeEventMapper.js +326 -0
  65. package/dist/acp/AcpRuntimeEventMapper.js.map +1 -0
  66. package/dist/acp/AcpSessionService.d.ts +65 -0
  67. package/dist/acp/AcpSessionService.d.ts.map +1 -0
  68. package/dist/acp/AcpSessionService.js +931 -0
  69. package/dist/acp/AcpSessionService.js.map +1 -0
  70. package/dist/acp/DurableAcpPermissionPolicyStore.d.ts +25 -0
  71. package/dist/acp/DurableAcpPermissionPolicyStore.d.ts.map +1 -0
  72. package/dist/acp/DurableAcpPermissionPolicyStore.js +100 -0
  73. package/dist/acp/DurableAcpPermissionPolicyStore.js.map +1 -0
  74. package/dist/acp/acpCommandSafety.d.ts +63 -0
  75. package/dist/acp/acpCommandSafety.d.ts.map +1 -0
  76. package/dist/acp/acpCommandSafety.js +90 -0
  77. package/dist/acp/acpCommandSafety.js.map +1 -0
  78. package/dist/acp/acpErrors.d.ts +47 -0
  79. package/dist/acp/acpErrors.d.ts.map +1 -0
  80. package/dist/acp/acpErrors.js +93 -0
  81. package/dist/acp/acpErrors.js.map +1 -0
  82. package/dist/acp/acpRedaction.d.ts +9 -0
  83. package/dist/acp/acpRedaction.d.ts.map +1 -0
  84. package/dist/acp/acpRedaction.js +69 -0
  85. package/dist/acp/acpRedaction.js.map +1 -0
  86. package/dist/acp/acpSessionResume.d.ts +8 -0
  87. package/dist/acp/acpSessionResume.d.ts.map +1 -0
  88. package/dist/acp/acpSessionResume.js +31 -0
  89. package/dist/acp/acpSessionResume.js.map +1 -0
  90. package/dist/acp/acpTypes.d.ts +467 -0
  91. package/dist/acp/acpTypes.d.ts.map +1 -0
  92. package/dist/acp/acpTypes.js +2 -0
  93. package/dist/acp/acpTypes.js.map +1 -0
  94. package/dist/agent/chooseDirectoryBridge.d.ts +29 -0
  95. package/dist/agent/chooseDirectoryBridge.d.ts.map +1 -0
  96. package/dist/agent/chooseDirectoryBridge.js +116 -0
  97. package/dist/agent/chooseDirectoryBridge.js.map +1 -0
  98. package/dist/agent/permissionProfiles.d.ts +29 -0
  99. package/dist/agent/permissionProfiles.d.ts.map +1 -0
  100. package/dist/agent/permissionProfiles.js +107 -0
  101. package/dist/agent/permissionProfiles.js.map +1 -0
  102. package/dist/agent/runtimeAuthorityPrompt.d.ts +13 -0
  103. package/dist/agent/runtimeAuthorityPrompt.d.ts.map +1 -0
  104. package/dist/agent/runtimeAuthorityPrompt.js +45 -0
  105. package/dist/agent/runtimeAuthorityPrompt.js.map +1 -0
  106. package/dist/agent/runtimeModeOutputGuard.d.ts +6 -0
  107. package/dist/agent/runtimeModeOutputGuard.d.ts.map +1 -0
  108. package/dist/agent/runtimeModeOutputGuard.js +40 -0
  109. package/dist/agent/runtimeModeOutputGuard.js.map +1 -0
  110. package/dist/agent/workspacePlacementBridge.d.ts +29 -0
  111. package/dist/agent/workspacePlacementBridge.d.ts.map +1 -0
  112. package/dist/agent/workspacePlacementBridge.js +143 -0
  113. package/dist/agent/workspacePlacementBridge.js.map +1 -0
  114. package/dist/agentSearch/AgentSearchService.d.ts +81 -0
  115. package/dist/agentSearch/AgentSearchService.d.ts.map +1 -0
  116. package/dist/agentSearch/AgentSearchService.js +1038 -0
  117. package/dist/agentSearch/AgentSearchService.js.map +1 -0
  118. package/dist/agentSearch/JsonInvertedIndexAdapter.d.ts +44 -0
  119. package/dist/agentSearch/JsonInvertedIndexAdapter.d.ts.map +1 -0
  120. package/dist/agentSearch/JsonInvertedIndexAdapter.js +307 -0
  121. package/dist/agentSearch/JsonInvertedIndexAdapter.js.map +1 -0
  122. package/dist/agentSearch/SearchIndexAdapter.d.ts +33 -0
  123. package/dist/agentSearch/SearchIndexAdapter.d.ts.map +1 -0
  124. package/dist/agentSearch/SearchIndexAdapter.js +2 -0
  125. package/dist/agentSearch/SearchIndexAdapter.js.map +1 -0
  126. package/dist/agentSearch/SearchSourceWatcher.d.ts +29 -0
  127. package/dist/agentSearch/SearchSourceWatcher.d.ts.map +1 -0
  128. package/dist/agentSearch/SearchSourceWatcher.js +150 -0
  129. package/dist/agentSearch/SearchSourceWatcher.js.map +1 -0
  130. package/dist/agentSearch/SqliteFtsIndexAdapter.d.ts +58 -0
  131. package/dist/agentSearch/SqliteFtsIndexAdapter.d.ts.map +1 -0
  132. package/dist/agentSearch/SqliteFtsIndexAdapter.js +420 -0
  133. package/dist/agentSearch/SqliteFtsIndexAdapter.js.map +1 -0
  134. package/dist/agentSearch/conversationIndexer.d.ts +69 -0
  135. package/dist/agentSearch/conversationIndexer.d.ts.map +1 -0
  136. package/dist/agentSearch/conversationIndexer.js +313 -0
  137. package/dist/agentSearch/conversationIndexer.js.map +1 -0
  138. package/dist/agentSearch/searchTypes.d.ts +264 -0
  139. package/dist/agentSearch/searchTypes.d.ts.map +1 -0
  140. package/dist/agentSearch/searchTypes.js +2 -0
  141. package/dist/agentSearch/searchTypes.js.map +1 -0
  142. package/dist/agentSearch/searchUtils.d.ts +12 -0
  143. package/dist/agentSearch/searchUtils.d.ts.map +1 -0
  144. package/dist/agentSearch/searchUtils.js +125 -0
  145. package/dist/agentSearch/searchUtils.js.map +1 -0
  146. package/dist/agentSearch/workspaceMetadataIndexer.d.ts +3 -0
  147. package/dist/agentSearch/workspaceMetadataIndexer.d.ts.map +1 -0
  148. package/dist/agentSearch/workspaceMetadataIndexer.js +125 -0
  149. package/dist/agentSearch/workspaceMetadataIndexer.js.map +1 -0
  150. package/dist/cli/CliControlPlaneService.d.ts +269 -0
  151. package/dist/cli/CliControlPlaneService.d.ts.map +1 -0
  152. package/dist/cli/CliControlPlaneService.js +1874 -0
  153. package/dist/cli/CliControlPlaneService.js.map +1 -0
  154. package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.d.ts +91 -0
  155. package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.d.ts.map +1 -0
  156. package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.js +379 -0
  157. package/dist/cloud/XenoCloudAgentTurnExecutionAdapter.js.map +1 -0
  158. package/dist/codebase/CodebaseIntelligenceService.d.ts +68 -0
  159. package/dist/codebase/CodebaseIntelligenceService.d.ts.map +1 -0
  160. package/dist/codebase/CodebaseIntelligenceService.js +1717 -0
  161. package/dist/codebase/CodebaseIntelligenceService.js.map +1 -0
  162. package/dist/codebase/LanguageServerProcessManager.d.ts +85 -0
  163. package/dist/codebase/LanguageServerProcessManager.d.ts.map +1 -0
  164. package/dist/codebase/LanguageServerProcessManager.js +607 -0
  165. package/dist/codebase/LanguageServerProcessManager.js.map +1 -0
  166. package/dist/codebase/codeTypes.d.ts +485 -0
  167. package/dist/codebase/codeTypes.d.ts.map +1 -0
  168. package/dist/codebase/codeTypes.js +2 -0
  169. package/dist/codebase/codeTypes.js.map +1 -0
  170. package/dist/codebase/codeUtils.d.ts +24 -0
  171. package/dist/codebase/codeUtils.d.ts.map +1 -0
  172. package/dist/codebase/codeUtils.js +177 -0
  173. package/dist/codebase/codeUtils.js.map +1 -0
  174. package/dist/codebase/contextPacks.d.ts +18 -0
  175. package/dist/codebase/contextPacks.d.ts.map +1 -0
  176. package/dist/codebase/contextPacks.js +180 -0
  177. package/dist/codebase/contextPacks.js.map +1 -0
  178. package/dist/codebase/dependencyGraph.d.ts +3 -0
  179. package/dist/codebase/dependencyGraph.d.ts.map +1 -0
  180. package/dist/codebase/dependencyGraph.js +209 -0
  181. package/dist/codebase/dependencyGraph.js.map +1 -0
  182. package/dist/codebase/fileIndexer.d.ts +27 -0
  183. package/dist/codebase/fileIndexer.d.ts.map +1 -0
  184. package/dist/codebase/fileIndexer.js +167 -0
  185. package/dist/codebase/fileIndexer.js.map +1 -0
  186. package/dist/codebase/symbolIndexer.d.ts +3 -0
  187. package/dist/codebase/symbolIndexer.d.ts.map +1 -0
  188. package/dist/codebase/symbolIndexer.js +184 -0
  189. package/dist/codebase/symbolIndexer.js.map +1 -0
  190. package/dist/codebase/whatChanged.d.ts +4 -0
  191. package/dist/codebase/whatChanged.d.ts.map +1 -0
  192. package/dist/codebase/whatChanged.js +138 -0
  193. package/dist/codebase/whatChanged.js.map +1 -0
  194. package/dist/coordination/CoordinationService.d.ts +166 -0
  195. package/dist/coordination/CoordinationService.d.ts.map +1 -0
  196. package/dist/coordination/CoordinationService.js +247 -0
  197. package/dist/coordination/CoordinationService.js.map +1 -0
  198. package/dist/coordination/claims.d.ts +122 -0
  199. package/dist/coordination/claims.d.ts.map +1 -0
  200. package/dist/coordination/claims.js +196 -0
  201. package/dist/coordination/claims.js.map +1 -0
  202. package/dist/coordination/coordinationTools.d.ts +40 -0
  203. package/dist/coordination/coordinationTools.d.ts.map +1 -0
  204. package/dist/coordination/coordinationTools.js +92 -0
  205. package/dist/coordination/coordinationTools.js.map +1 -0
  206. package/dist/coordination/laneBridge.d.ts +84 -0
  207. package/dist/coordination/laneBridge.d.ts.map +1 -0
  208. package/dist/coordination/laneBridge.js +86 -0
  209. package/dist/coordination/laneBridge.js.map +1 -0
  210. package/dist/coordination/messages.d.ts +225 -0
  211. package/dist/coordination/messages.d.ts.map +1 -0
  212. package/dist/coordination/messages.js +335 -0
  213. package/dist/coordination/messages.js.map +1 -0
  214. package/dist/coordination/permissionWaitRegistry.d.ts +32 -0
  215. package/dist/coordination/permissionWaitRegistry.d.ts.map +1 -0
  216. package/dist/coordination/permissionWaitRegistry.js +59 -0
  217. package/dist/coordination/permissionWaitRegistry.js.map +1 -0
  218. package/dist/coordination/query.d.ts +187 -0
  219. package/dist/coordination/query.d.ts.map +1 -0
  220. package/dist/coordination/query.js +138 -0
  221. package/dist/coordination/query.js.map +1 -0
  222. package/dist/coordination/renderInbox.d.ts +15 -0
  223. package/dist/coordination/renderInbox.d.ts.map +1 -0
  224. package/dist/coordination/renderInbox.js +19 -0
  225. package/dist/coordination/renderInbox.js.map +1 -0
  226. package/dist/coordination/runtimeEvents.d.ts +53 -0
  227. package/dist/coordination/runtimeEvents.d.ts.map +1 -0
  228. package/dist/coordination/runtimeEvents.js +112 -0
  229. package/dist/coordination/runtimeEvents.js.map +1 -0
  230. package/dist/detachedHostService.d.ts +77 -0
  231. package/dist/detachedHostService.d.ts.map +1 -0
  232. package/dist/detachedHostService.js +66 -0
  233. package/dist/detachedHostService.js.map +1 -0
  234. package/dist/detachedHostSupervisor.d.ts +102 -0
  235. package/dist/detachedHostSupervisor.d.ts.map +1 -0
  236. package/dist/detachedHostSupervisor.js +168 -0
  237. package/dist/detachedHostSupervisor.js.map +1 -0
  238. package/dist/durableRuntimeEventStore.d.ts +49 -0
  239. package/dist/durableRuntimeEventStore.d.ts.map +1 -0
  240. package/dist/durableRuntimeEventStore.js +364 -0
  241. package/dist/durableRuntimeEventStore.js.map +1 -0
  242. package/dist/engineering/EngineeringWorkbenchService.d.ts +142 -0
  243. package/dist/engineering/EngineeringWorkbenchService.d.ts.map +1 -0
  244. package/dist/engineering/EngineeringWorkbenchService.js +2301 -0
  245. package/dist/engineering/EngineeringWorkbenchService.js.map +1 -0
  246. package/dist/engineering/LandingService.d.ts +112 -0
  247. package/dist/engineering/LandingService.d.ts.map +1 -0
  248. package/dist/engineering/LandingService.js +239 -0
  249. package/dist/engineering/LandingService.js.map +1 -0
  250. package/dist/engineering/engineeringTypes.d.ts +2 -0
  251. package/dist/engineering/engineeringTypes.d.ts.map +1 -0
  252. package/dist/engineering/engineeringTypes.js +2 -0
  253. package/dist/engineering/engineeringTypes.js.map +1 -0
  254. package/dist/engineering/patchReview.d.ts +11 -0
  255. package/dist/engineering/patchReview.d.ts.map +1 -0
  256. package/dist/engineering/patchReview.js +174 -0
  257. package/dist/engineering/patchReview.js.map +1 -0
  258. package/dist/index.d.ts +834 -0
  259. package/dist/index.d.ts.map +1 -0
  260. package/dist/index.js +3242 -0
  261. package/dist/index.js.map +1 -0
  262. package/dist/intake/IntakeService.d.ts +84 -0
  263. package/dist/intake/IntakeService.d.ts.map +1 -0
  264. package/dist/intake/IntakeService.js +117 -0
  265. package/dist/intake/IntakeService.js.map +1 -0
  266. package/dist/intake/RepositoryTodoIntakeSource.d.ts +72 -0
  267. package/dist/intake/RepositoryTodoIntakeSource.d.ts.map +1 -0
  268. package/dist/intake/RepositoryTodoIntakeSource.js +168 -0
  269. package/dist/intake/RepositoryTodoIntakeSource.js.map +1 -0
  270. package/dist/intake/WorkspaceTodoIntakeSource.d.ts +53 -0
  271. package/dist/intake/WorkspaceTodoIntakeSource.d.ts.map +1 -0
  272. package/dist/intake/WorkspaceTodoIntakeSource.js +81 -0
  273. package/dist/intake/WorkspaceTodoIntakeSource.js.map +1 -0
  274. package/dist/isolation/CommandExecution.d.ts +132 -0
  275. package/dist/isolation/CommandExecution.d.ts.map +1 -0
  276. package/dist/isolation/CommandExecution.js +48 -0
  277. package/dist/isolation/CommandExecution.js.map +1 -0
  278. package/dist/isolation/LocalCommandExecutor.d.ts +27 -0
  279. package/dist/isolation/LocalCommandExecutor.d.ts.map +1 -0
  280. package/dist/isolation/LocalCommandExecutor.js +97 -0
  281. package/dist/isolation/LocalCommandExecutor.js.map +1 -0
  282. package/dist/isolation/RunExecutionEnvironment.d.ts +135 -0
  283. package/dist/isolation/RunExecutionEnvironment.d.ts.map +1 -0
  284. package/dist/isolation/RunExecutionEnvironment.js +206 -0
  285. package/dist/isolation/RunExecutionEnvironment.js.map +1 -0
  286. package/dist/isolation/RunIsolationService.d.ts +90 -0
  287. package/dist/isolation/RunIsolationService.d.ts.map +1 -0
  288. package/dist/isolation/RunIsolationService.js +86 -0
  289. package/dist/isolation/RunIsolationService.js.map +1 -0
  290. package/dist/isolation/SandboxCommandExecutor.d.ts +93 -0
  291. package/dist/isolation/SandboxCommandExecutor.d.ts.map +1 -0
  292. package/dist/isolation/SandboxCommandExecutor.js +189 -0
  293. package/dist/isolation/SandboxCommandExecutor.js.map +1 -0
  294. package/dist/knowledge/WorkspaceKnowledgeService.d.ts +71 -0
  295. package/dist/knowledge/WorkspaceKnowledgeService.d.ts.map +1 -0
  296. package/dist/knowledge/WorkspaceKnowledgeService.js +103 -0
  297. package/dist/knowledge/WorkspaceKnowledgeService.js.map +1 -0
  298. package/dist/localHostAuthority.d.ts +24 -0
  299. package/dist/localHostAuthority.d.ts.map +1 -0
  300. package/dist/localHostAuthority.js +87 -0
  301. package/dist/localHostAuthority.js.map +1 -0
  302. package/dist/localHostLease.d.ts +51 -0
  303. package/dist/localHostLease.d.ts.map +1 -0
  304. package/dist/localHostLease.js +167 -0
  305. package/dist/localHostLease.js.map +1 -0
  306. package/dist/localHostPaths.d.ts +30 -0
  307. package/dist/localHostPaths.d.ts.map +1 -0
  308. package/dist/localHostPaths.js +60 -0
  309. package/dist/localHostPaths.js.map +1 -0
  310. package/dist/localRpcTransport.d.ts +21 -0
  311. package/dist/localRpcTransport.d.ts.map +1 -0
  312. package/dist/localRpcTransport.js +423 -0
  313. package/dist/localRpcTransport.js.map +1 -0
  314. package/dist/node.d.ts +104 -0
  315. package/dist/node.d.ts.map +1 -0
  316. package/dist/node.js +103 -0
  317. package/dist/node.js.map +1 -0
  318. package/dist/observation/DirectoryBaselineScanner.d.ts +44 -0
  319. package/dist/observation/DirectoryBaselineScanner.d.ts.map +1 -0
  320. package/dist/observation/DirectoryBaselineScanner.js +112 -0
  321. package/dist/observation/DirectoryBaselineScanner.js.map +1 -0
  322. package/dist/observation/MountContentReader.d.ts +63 -0
  323. package/dist/observation/MountContentReader.d.ts.map +1 -0
  324. package/dist/observation/MountContentReader.js +158 -0
  325. package/dist/observation/MountContentReader.js.map +1 -0
  326. package/dist/observation/RunObservationService.d.ts +229 -0
  327. package/dist/observation/RunObservationService.d.ts.map +1 -0
  328. package/dist/observation/RunObservationService.js +248 -0
  329. package/dist/observation/RunObservationService.js.map +1 -0
  330. package/dist/observation/XenoUseObservationSource.d.ts +42 -0
  331. package/dist/observation/XenoUseObservationSource.d.ts.map +1 -0
  332. package/dist/observation/XenoUseObservationSource.js +104 -0
  333. package/dist/observation/XenoUseObservationSource.js.map +1 -0
  334. package/dist/observation/XenoUseRunObserver.d.ts +140 -0
  335. package/dist/observation/XenoUseRunObserver.d.ts.map +1 -0
  336. package/dist/observation/XenoUseRunObserver.js +256 -0
  337. package/dist/observation/XenoUseRunObserver.js.map +1 -0
  338. package/dist/pluginMcp/PluginMcpManagerService.d.ts +141 -0
  339. package/dist/pluginMcp/PluginMcpManagerService.d.ts.map +1 -0
  340. package/dist/pluginMcp/PluginMcpManagerService.js +1821 -0
  341. package/dist/pluginMcp/PluginMcpManagerService.js.map +1 -0
  342. package/dist/pluginMcp/pluginMcpTypes.d.ts +349 -0
  343. package/dist/pluginMcp/pluginMcpTypes.d.ts.map +1 -0
  344. package/dist/pluginMcp/pluginMcpTypes.js +2 -0
  345. package/dist/pluginMcp/pluginMcpTypes.js.map +1 -0
  346. package/dist/policy/WorkspacePolicyService.d.ts +83 -0
  347. package/dist/policy/WorkspacePolicyService.d.ts.map +1 -0
  348. package/dist/policy/WorkspacePolicyService.js +809 -0
  349. package/dist/policy/WorkspacePolicyService.js.map +1 -0
  350. package/dist/policy/policyGuard.d.ts +27 -0
  351. package/dist/policy/policyGuard.d.ts.map +1 -0
  352. package/dist/policy/policyGuard.js +143 -0
  353. package/dist/policy/policyGuard.js.map +1 -0
  354. package/dist/policy/policyTypes.d.ts +190 -0
  355. package/dist/policy/policyTypes.d.ts.map +1 -0
  356. package/dist/policy/policyTypes.js +2 -0
  357. package/dist/policy/policyTypes.js.map +1 -0
  358. package/dist/runtime/RuntimeEventStore.d.ts +42 -0
  359. package/dist/runtime/RuntimeEventStore.d.ts.map +1 -0
  360. package/dist/runtime/RuntimeEventStore.js +412 -0
  361. package/dist/runtime/RuntimeEventStore.js.map +1 -0
  362. package/dist/runtime/RuntimeEventStoreRepositoryAdapter.d.ts +21 -0
  363. package/dist/runtime/RuntimeEventStoreRepositoryAdapter.d.ts.map +1 -0
  364. package/dist/runtime/RuntimeEventStoreRepositoryAdapter.js +41 -0
  365. package/dist/runtime/RuntimeEventStoreRepositoryAdapter.js.map +1 -0
  366. package/dist/runtime/RuntimeTimelineProjection.d.ts +5 -0
  367. package/dist/runtime/RuntimeTimelineProjection.d.ts.map +1 -0
  368. package/dist/runtime/RuntimeTimelineProjection.js +705 -0
  369. package/dist/runtime/RuntimeTimelineProjection.js.map +1 -0
  370. package/dist/runtime/runtimeEventTypes.d.ts +73 -0
  371. package/dist/runtime/runtimeEventTypes.d.ts.map +1 -0
  372. package/dist/runtime/runtimeEventTypes.js +2 -0
  373. package/dist/runtime/runtimeEventTypes.js.map +1 -0
  374. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts +117 -0
  375. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.d.ts.map +1 -0
  376. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js +214 -0
  377. package/dist/sdk/SdkNativeAgentTurnExecutionAdapter.js.map +1 -0
  378. package/dist/session/AgentSessionService.d.ts +285 -0
  379. package/dist/session/AgentSessionService.d.ts.map +1 -0
  380. package/dist/session/AgentSessionService.js +444 -0
  381. package/dist/session/AgentSessionService.js.map +1 -0
  382. package/dist/session/bufferedSessionPty.d.ts +27 -0
  383. package/dist/session/bufferedSessionPty.d.ts.map +1 -0
  384. package/dist/session/bufferedSessionPty.js +49 -0
  385. package/dist/session/bufferedSessionPty.js.map +1 -0
  386. package/dist/session/renderSupport.d.ts +53 -0
  387. package/dist/session/renderSupport.d.ts.map +1 -0
  388. package/dist/session/renderSupport.js +41 -0
  389. package/dist/session/renderSupport.js.map +1 -0
  390. package/dist/session/runtimeEventChannel.d.ts +56 -0
  391. package/dist/session/runtimeEventChannel.d.ts.map +1 -0
  392. package/dist/session/runtimeEventChannel.js +53 -0
  393. package/dist/session/runtimeEventChannel.js.map +1 -0
  394. package/dist/state/JsonAgentStateRepository.d.ts +29 -0
  395. package/dist/state/JsonAgentStateRepository.d.ts.map +1 -0
  396. package/dist/state/JsonAgentStateRepository.js +165 -0
  397. package/dist/state/JsonAgentStateRepository.js.map +1 -0
  398. package/dist/state/SqliteAgentStateRepository.d.ts +58 -0
  399. package/dist/state/SqliteAgentStateRepository.d.ts.map +1 -0
  400. package/dist/state/SqliteAgentStateRepository.js +777 -0
  401. package/dist/state/SqliteAgentStateRepository.js.map +1 -0
  402. package/dist/state/createAgentStateRepository.d.ts +4 -0
  403. package/dist/state/createAgentStateRepository.d.ts.map +1 -0
  404. package/dist/state/createAgentStateRepository.js +12 -0
  405. package/dist/state/createAgentStateRepository.js.map +1 -0
  406. package/dist/state/stateSanitizers.d.ts +7 -0
  407. package/dist/state/stateSanitizers.d.ts.map +1 -0
  408. package/dist/state/stateSanitizers.js +87 -0
  409. package/dist/state/stateSanitizers.js.map +1 -0
  410. package/dist/subagents/AcpSubagentExecutionAdapter.d.ts +12 -0
  411. package/dist/subagents/AcpSubagentExecutionAdapter.d.ts.map +1 -0
  412. package/dist/subagents/AcpSubagentExecutionAdapter.js +119 -0
  413. package/dist/subagents/AcpSubagentExecutionAdapter.js.map +1 -0
  414. package/dist/subagents/CliSubagentExecutionAdapter.d.ts +13 -0
  415. package/dist/subagents/CliSubagentExecutionAdapter.d.ts.map +1 -0
  416. package/dist/subagents/CliSubagentExecutionAdapter.js +217 -0
  417. package/dist/subagents/CliSubagentExecutionAdapter.js.map +1 -0
  418. package/dist/subagents/HostTurnSubagentExecutionAdapter.d.ts +78 -0
  419. package/dist/subagents/HostTurnSubagentExecutionAdapter.d.ts.map +1 -0
  420. package/dist/subagents/HostTurnSubagentExecutionAdapter.js +160 -0
  421. package/dist/subagents/HostTurnSubagentExecutionAdapter.js.map +1 -0
  422. package/dist/subagents/SubagentRunService.d.ts +103 -0
  423. package/dist/subagents/SubagentRunService.d.ts.map +1 -0
  424. package/dist/subagents/SubagentRunService.js +1183 -0
  425. package/dist/subagents/SubagentRunService.js.map +1 -0
  426. package/dist/subagents/SubagentScheduler.d.ts +167 -0
  427. package/dist/subagents/SubagentScheduler.d.ts.map +1 -0
  428. package/dist/subagents/SubagentScheduler.js +530 -0
  429. package/dist/subagents/SubagentScheduler.js.map +1 -0
  430. package/dist/subagents/TeamAgentRouter.d.ts +104 -0
  431. package/dist/subagents/TeamAgentRouter.d.ts.map +1 -0
  432. package/dist/subagents/TeamAgentRouter.js +435 -0
  433. package/dist/subagents/TeamAgentRouter.js.map +1 -0
  434. package/dist/subagents/capabilityMatcher.d.ts +3 -0
  435. package/dist/subagents/capabilityMatcher.d.ts.map +1 -0
  436. package/dist/subagents/capabilityMatcher.js +127 -0
  437. package/dist/subagents/capabilityMatcher.js.map +1 -0
  438. package/dist/subagents/subagentRunPort.d.ts +43 -0
  439. package/dist/subagents/subagentRunPort.d.ts.map +1 -0
  440. package/dist/subagents/subagentRunPort.js +24 -0
  441. package/dist/subagents/subagentRunPort.js.map +1 -0
  442. package/dist/subagents/subagentTypes.d.ts +330 -0
  443. package/dist/subagents/subagentTypes.d.ts.map +1 -0
  444. package/dist/subagents/subagentTypes.js +2 -0
  445. package/dist/subagents/subagentTypes.js.map +1 -0
  446. package/dist/subagents/subagentWorkspaceAuthority.d.ts +29 -0
  447. package/dist/subagents/subagentWorkspaceAuthority.d.ts.map +1 -0
  448. package/dist/subagents/subagentWorkspaceAuthority.js +173 -0
  449. package/dist/subagents/subagentWorkspaceAuthority.js.map +1 -0
  450. package/dist/support/SupportDiagnosticsService.d.ts +87 -0
  451. package/dist/support/SupportDiagnosticsService.d.ts.map +1 -0
  452. package/dist/support/SupportDiagnosticsService.js +1199 -0
  453. package/dist/support/SupportDiagnosticsService.js.map +1 -0
  454. package/dist/turns/DurableInterruptedTurnStore.d.ts +14 -0
  455. package/dist/turns/DurableInterruptedTurnStore.d.ts.map +1 -0
  456. package/dist/turns/DurableInterruptedTurnStore.js +63 -0
  457. package/dist/turns/DurableInterruptedTurnStore.js.map +1 -0
  458. package/dist/turns/interruptedTurns.d.ts +50 -0
  459. package/dist/turns/interruptedTurns.d.ts.map +1 -0
  460. package/dist/turns/interruptedTurns.js +54 -0
  461. package/dist/turns/interruptedTurns.js.map +1 -0
  462. package/dist/turns/turnAdmission.d.ts +113 -0
  463. package/dist/turns/turnAdmission.d.ts.map +1 -0
  464. package/dist/turns/turnAdmission.js +136 -0
  465. package/dist/turns/turnAdmission.js.map +1 -0
  466. package/dist/workspace/WorkspaceHostService.d.ts +126 -0
  467. package/dist/workspace/WorkspaceHostService.d.ts.map +1 -0
  468. package/dist/workspace/WorkspaceHostService.js +841 -0
  469. package/dist/workspace/WorkspaceHostService.js.map +1 -0
  470. package/dist/workspace/WorkspaceShellService.d.ts +71 -0
  471. package/dist/workspace/WorkspaceShellService.d.ts.map +1 -0
  472. package/dist/workspace/WorkspaceShellService.js +273 -0
  473. package/dist/workspace/WorkspaceShellService.js.map +1 -0
  474. package/dist/workspace/fileAssociations.d.ts +12 -0
  475. package/dist/workspace/fileAssociations.d.ts.map +1 -0
  476. package/dist/workspace/fileAssociations.js +202 -0
  477. package/dist/workspace/fileAssociations.js.map +1 -0
  478. package/dist/workspace/scrollbackBuffer.d.ts +48 -0
  479. package/dist/workspace/scrollbackBuffer.d.ts.map +1 -0
  480. package/dist/workspace/scrollbackBuffer.js +94 -0
  481. package/dist/workspace/scrollbackBuffer.js.map +1 -0
  482. package/package.json +42 -0
@@ -0,0 +1,2301 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { spawn } from 'child_process';
3
+ import { commandSucceeded } from '../isolation/CommandExecution.js';
4
+ import { LocalCommandExecutor } from '../isolation/LocalCommandExecutor.js';
5
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from 'fs';
6
+ import { dirname, isAbsolute, join, relative, resolve } from 'path';
7
+ import { isPathInside, hashText, resolveRootDirectory, safeReadJson, safeWriteJson, workspaceIndexDirectory, } from '../codebase/codeUtils.js';
8
+ import { getRuntimeEventStore } from '../runtime/RuntimeEventStore.js';
9
+ import { buildPatchReviewHunks, composePatchSnapshot, effectivePatchDecisions, } from './patchReview.js';
10
+ const MAX_GIT_OUTPUT = 1_500_000;
11
+ const DEFAULT_RUN_OUTPUT = 2_000_000;
12
+ const MAX_STORED_RUNS = 80;
13
+ const MAX_STORED_BACKGROUND_RUNS = 80;
14
+ const MAX_STORED_PATCH_REVIEWS = 120;
15
+ const MAX_CHECKPOINT_COMPARE_PATCHES = 40;
16
+ const MAX_TRANSACTIONAL_SNAPSHOT_BYTES = 1_000_000;
17
+ const MAX_TRANSACTIONAL_SNAPSHOT_CHARS = 250_000;
18
+ export class EngineeringWorkbenchService {
19
+ activeBackgroundRuns = new Map();
20
+ runtimeEventStore;
21
+ /**
22
+ * The landing queue, when the composition mounted one.
23
+ *
24
+ * Optional in the same way the host's observation service is: landing consults
25
+ * observed diffs and verification results, and a host with neither has nothing
26
+ * to gate with. Answering `unavailable` is honest; answering "landed" after
27
+ * consulting nothing is the merge-debt failure with extra steps.
28
+ */
29
+ landingService;
30
+ constructor(options = {}) {
31
+ this.runtimeEventStore = options.runtimeEventStore || getRuntimeEventStore();
32
+ this.landingService = options.landingService;
33
+ }
34
+ /** Mounted after construction when the composition builds the queue later. */
35
+ configureLandingService(landingService) {
36
+ this.landingService = landingService;
37
+ }
38
+ configureRuntimeEventStore(runtimeEventStore) {
39
+ this.runtimeEventStore = runtimeEventStore;
40
+ }
41
+ close() {
42
+ for (const active of this.activeBackgroundRuns.values()) {
43
+ active.stopRequested = true;
44
+ killProcessTree(active.child);
45
+ }
46
+ }
47
+ async request(method, input) {
48
+ if (method === 'engineering.git.status')
49
+ return await this.gitStatus(input);
50
+ if (method === 'engineering.git.diff')
51
+ return await this.gitDiff(input);
52
+ if (method === 'engineering.git.stage')
53
+ return await this.gitStage(input);
54
+ if (method === 'engineering.git.unstage')
55
+ return await this.gitUnstage(input);
56
+ if (method === 'engineering.git.commit')
57
+ return await this.gitCommit(input);
58
+ if (method === 'engineering.git.branch')
59
+ return await this.gitBranch(input);
60
+ if (method === 'engineering.git.push')
61
+ return await this.gitPush(input);
62
+ if (method === 'engineering.git.pr.create')
63
+ return await this.gitCreatePullRequest(input);
64
+ if (method === 'engineering.landing.enqueue')
65
+ return this.landing('enqueue', input);
66
+ if (method === 'engineering.landing.remove')
67
+ return this.landing('remove', input);
68
+ if (method === 'engineering.landing.status')
69
+ return this.landing('status', input);
70
+ if (method === 'engineering.landing.land')
71
+ return await this.landing('land', input);
72
+ if (method === 'engineering.landing.verify')
73
+ return await this.landing('verify', input);
74
+ if (method === 'engineering.checks.discover')
75
+ return this.discoverChecks(input);
76
+ if (method === 'engineering.checks.run')
77
+ return await this.runCheck(input);
78
+ if (method === 'engineering.background.start')
79
+ return this.startBackgroundCheck(input);
80
+ if (method === 'engineering.background.list')
81
+ return this.listBackgroundRuns(input);
82
+ if (method === 'engineering.background.output')
83
+ return this.readBackgroundRunOutput(input);
84
+ if (method === 'engineering.background.stop')
85
+ return this.stopBackgroundRun(input);
86
+ if (method === 'engineering.runs.list')
87
+ return this.listRuns(input);
88
+ if (method === 'engineering.diagnostics')
89
+ return await this.diagnostics(input);
90
+ if (method === 'engineering.patches.list')
91
+ return this.listPatchReviews(input);
92
+ if (method === 'engineering.patches.enqueue')
93
+ return this.enqueuePatchReview(input);
94
+ if (method === 'engineering.patches.update')
95
+ return this.updatePatchReview(input);
96
+ if (method === 'engineering.patches.review')
97
+ return this.reviewPatchSelection(input);
98
+ if (method === 'engineering.checkpoints.list')
99
+ return this.listCheckpoints(input);
100
+ if (method === 'engineering.checkpoints.compare')
101
+ return this.compareCheckpoints(input);
102
+ if (method === 'engineering.checkpoints.restore')
103
+ return this.restoreCheckpoint(input);
104
+ if (method === 'engineering.sessions.list')
105
+ return this.listSessions(input);
106
+ if (method === 'engineering.runtime.timeline')
107
+ return this.runtimeTimeline(input);
108
+ throw new Error(`Unsupported engineering workbench method: ${String(method)}`);
109
+ }
110
+ async gitStatus(request) {
111
+ try {
112
+ const rootDirectory = this.resolveRoot(request);
113
+ if (!await this.isGitRepo(rootDirectory)) {
114
+ return { success: true, isGitRepo: false, rootDirectory, clean: true, files: [] };
115
+ }
116
+ const [statusResult, numstatResult] = await Promise.all([
117
+ runProcess('git', ['status', '--porcelain=v1', '-b'], rootDirectory, 8000, MAX_GIT_OUTPUT),
118
+ runProcess('git', ['diff', '--numstat'], rootDirectory, 8000, MAX_GIT_OUTPUT),
119
+ ]);
120
+ if (!statusResult.success && statusResult.exitCode !== 0) {
121
+ return {
122
+ success: false,
123
+ isGitRepo: true,
124
+ rootDirectory,
125
+ clean: true,
126
+ files: [],
127
+ error: statusResult.error || statusResult.stderr || 'Failed to read git status.',
128
+ };
129
+ }
130
+ const branch = parseBranch(statusResult.stdout);
131
+ const statsByPath = parseNumstat(numstatResult.stdout);
132
+ const files = parseGitStatusFiles(rootDirectory, statusResult.stdout);
133
+ for (const file of files) {
134
+ const stats = statsByPath.get(file.relativePath);
135
+ if (stats) {
136
+ file.additions = stats.additions;
137
+ file.deletions = stats.deletions;
138
+ }
139
+ }
140
+ return {
141
+ success: true,
142
+ isGitRepo: true,
143
+ rootDirectory,
144
+ branch: branch.branch,
145
+ upstream: branch.upstream,
146
+ ahead: branch.ahead,
147
+ behind: branch.behind,
148
+ clean: files.length === 0,
149
+ files,
150
+ raw: statusResult.stdout,
151
+ };
152
+ }
153
+ catch (err) {
154
+ return {
155
+ success: false,
156
+ isGitRepo: false,
157
+ rootDirectory: request.rootDirectory || '',
158
+ clean: true,
159
+ files: [],
160
+ error: err instanceof Error ? err.message : String(err),
161
+ };
162
+ }
163
+ }
164
+ async gitDiff(request) {
165
+ try {
166
+ const rootDirectory = this.resolveRoot(request);
167
+ if (!await this.isGitRepo(rootDirectory)) {
168
+ return { success: true, isGitRepo: false, rootDirectory, diff: '', truncated: false };
169
+ }
170
+ const safePaths = this.validateRelativePaths(rootDirectory, request.paths || []);
171
+ const args = ['diff', ...(request.staged ? ['--staged'] : []), '--', ...safePaths];
172
+ const result = await runProcess('git', args, rootDirectory, 12000, request.maxBytes || MAX_GIT_OUTPUT);
173
+ return {
174
+ success: result.success || result.exitCode === 1 || result.exitCode === 0,
175
+ isGitRepo: true,
176
+ rootDirectory,
177
+ diff: result.stdout,
178
+ truncated: result.truncated,
179
+ error: result.success || result.exitCode === 1 || result.exitCode === 0 ? undefined : result.error || result.stderr,
180
+ };
181
+ }
182
+ catch (err) {
183
+ return {
184
+ success: false,
185
+ isGitRepo: false,
186
+ rootDirectory: request.rootDirectory || '',
187
+ diff: '',
188
+ truncated: false,
189
+ error: err instanceof Error ? err.message : String(err),
190
+ };
191
+ }
192
+ }
193
+ async gitStage(request) {
194
+ return this.runGitMutation(request, ['add', '--', ...this.validateRelativePaths(this.resolveRoot(request), request.paths)]);
195
+ }
196
+ async gitUnstage(request) {
197
+ return this.runGitMutation(request, ['restore', '--staged', '--', ...this.validateRelativePaths(this.resolveRoot(request), request.paths)]);
198
+ }
199
+ /**
200
+ * Create or switch to a branch.
201
+ *
202
+ * Local and reversible, so unlike push and PR creation it needs no
203
+ * confirmation. The NAME is validated rather than trusted: it reaches a
204
+ * command line, and git itself accepts things a caller rarely means โ€”
205
+ * a leading dash reads as a flag.
206
+ */
207
+ async gitBranch(request) {
208
+ const name = String(request.name || '').trim();
209
+ const invalid = validateRefName(name);
210
+ if (invalid)
211
+ return this.gitFailure(request, invalid);
212
+ // `git switch`, never `git checkout`. `checkout` is overloaded: it takes
213
+ // branches AND pathspecs, so `checkout -- main` restores a FILE called main
214
+ // rather than switching branch โ€” which is what the first version of this
215
+ // did, and the test caught. `switch` only ever means a branch, so the
216
+ // ambiguity is not something to be careful about; it is gone.
217
+ if (request.create === false) {
218
+ return this.runGitMutation(request, ['switch', name]);
219
+ }
220
+ const startPoint = String(request.startPoint || '').trim();
221
+ if (startPoint) {
222
+ const invalidStart = validateRefName(startPoint);
223
+ if (invalidStart)
224
+ return this.gitFailure(request, invalidStart);
225
+ return this.runGitMutation(request, ['switch', '-c', name, startPoint]);
226
+ }
227
+ return this.runGitMutation(request, ['switch', '-c', name]);
228
+ }
229
+ /**
230
+ * Push a branch to a remote.
231
+ *
232
+ * ๐Ÿ”ด The first verb here whose effect leaves the machine, and the first that
233
+ * refuses without `confirmed`. The host cannot otherwise distinguish an agent
234
+ * deciding to publish from a human asking it to, and those are different acts
235
+ * with different consequences โ€” a push is visible to other people and, unlike
236
+ * every other verb on this surface, cannot be undone locally.
237
+ *
238
+ * There is no `--force`, by construction rather than by policy: it destroys
239
+ * history others may already hold, every XENO repo rule forbids it, and a
240
+ * command that cannot express it cannot be talked into it.
241
+ */
242
+ async gitPush(request) {
243
+ const base = await this.gitStatus(request);
244
+ if (request.confirmed !== true) {
245
+ return { ...base, success: false, pushed: false, error: 'Pushing publishes work to a remote and was not confirmed. Send confirmed: true from a human action.' };
246
+ }
247
+ if (!base.isGitRepo)
248
+ return { ...base, pushed: false };
249
+ const remote = String(request.remote || 'origin').trim();
250
+ const invalidRemote = validateRemoteName(remote);
251
+ if (invalidRemote)
252
+ return { ...base, success: false, pushed: false, error: invalidRemote };
253
+ const branch = String(request.branch || base.branch || '').trim();
254
+ const invalidBranch = validateRefName(branch);
255
+ if (invalidBranch)
256
+ return { ...base, success: false, pushed: false, error: invalidBranch };
257
+ const args = ['push'];
258
+ if (request.setUpstream)
259
+ args.push('--set-upstream');
260
+ args.push(remote, branch);
261
+ const result = await this.runGitMutation(request, args);
262
+ return {
263
+ ...result,
264
+ pushed: result.success,
265
+ remote,
266
+ pushedBranch: branch,
267
+ };
268
+ }
269
+ /**
270
+ * Open a pull request, through the `gh` CLI.
271
+ *
272
+ * `gh` rather than a GitHub API client on purpose: it already holds the
273
+ * operator's credential, so this adds no token storage, no scope decisions and
274
+ * no second place a credential can leak. The cost is a dependency on `gh`
275
+ * being installed, which is reported plainly when it is not โ€” a missing tool
276
+ * is a legible failure, while a stored token is a permanent liability.
277
+ *
278
+ * Outward-facing, so confirmed like a push. `draft` defaults to TRUE: opening
279
+ * a review request notifies people, and an action taken on someone's behalf
280
+ * should default to the quieter form.
281
+ */
282
+ async gitCreatePullRequest(request) {
283
+ if (request.confirmed !== true) {
284
+ return { success: false, created: false, error: 'Opening a pull request notifies reviewers and was not confirmed. Send confirmed: true from a human action.' };
285
+ }
286
+ const title = String(request.title || '').trim();
287
+ if (title.length < 3)
288
+ return { success: false, created: false, error: 'A pull request title of at least 3 characters is required.' };
289
+ try {
290
+ const rootDirectory = this.resolveRoot(request);
291
+ if (!await this.isGitRepo(rootDirectory)) {
292
+ return { success: false, created: false, error: 'Workspace is not a git repository.' };
293
+ }
294
+ const args = ['pr', 'create', '--title', title, '--body', String(request.body || '')];
295
+ // Draft unless explicitly told otherwise โ€” see the note above.
296
+ if (request.draft !== false)
297
+ args.push('--draft');
298
+ for (const [flag, value] of [['--base', request.base], ['--head', request.head]]) {
299
+ const ref = String(value || '').trim();
300
+ if (!ref)
301
+ continue;
302
+ const invalid = validateRefName(ref);
303
+ if (invalid)
304
+ return { success: false, created: false, error: invalid };
305
+ args.push(flag, ref);
306
+ }
307
+ const result = await runProcess('gh', args, rootDirectory, 60000, MAX_GIT_OUTPUT);
308
+ if (!result.success) {
309
+ return {
310
+ success: false,
311
+ created: false,
312
+ error: result.error || result.stderr || 'Failed to create the pull request. Is the GitHub CLI installed and authenticated?',
313
+ };
314
+ }
315
+ const url = /https:\/\/\S+/.exec(result.stdout)?.[0];
316
+ const number = url ? Number(/\/(\d+)(?:\s|$)/.exec(url)?.[1]) : Number.NaN;
317
+ return {
318
+ success: true,
319
+ created: true,
320
+ ...(url ? { url } : {}),
321
+ ...(Number.isFinite(number) ? { number } : {}),
322
+ };
323
+ }
324
+ catch (err) {
325
+ return { success: false, created: false, error: err instanceof Error ? err.message : String(err) };
326
+ }
327
+ }
328
+ /**
329
+ * Routes a landing call, or says plainly that landing is not mounted.
330
+ *
331
+ * ๐Ÿ”ด An unmounted queue reports `success: false` with a reason, never an empty
332
+ * queue. An empty queue reads as "nothing is waiting", which is a fact about
333
+ * the world; "no queue exists here" is a fact about this installation. A
334
+ * caller that cannot tell them apart will wait for a landing that can never
335
+ * be attempted.
336
+ */
337
+ landing(operation, input) {
338
+ const service = this.landingService;
339
+ if (!service) {
340
+ const unavailable = {
341
+ success: false,
342
+ entries: [],
343
+ fingerprint: 'empty',
344
+ error: 'Landing is not available: this host has no landing queue mounted.',
345
+ };
346
+ if (operation === 'land') {
347
+ return Promise.resolve({ ...unavailable, landed: false, verdict: unavailable.error });
348
+ }
349
+ if (operation === 'verify') {
350
+ return Promise.resolve({ ...unavailable, verified: false, checks: [], problem: unavailable.error });
351
+ }
352
+ return unavailable;
353
+ }
354
+ if (operation === 'enqueue')
355
+ return service.enqueue(input);
356
+ if (operation === 'remove')
357
+ return service.remove(input);
358
+ if (operation === 'status')
359
+ return service.status(input);
360
+ if (operation === 'verify')
361
+ return service.verify(input);
362
+ return service.land(input);
363
+ }
364
+ /**
365
+ * The merge behind a landing โ€” deliberately NOT a host method.
366
+ *
367
+ * There is no `engineering.git.merge` in the contract: a free-standing merge
368
+ * verb would sit beside the gate rather than behind it, and a gate that can be
369
+ * walked around is documentation. This is reachable only from
370
+ * `engineering.landing.land`, which runs it after the queue admitted the run.
371
+ *
372
+ * `--no-ff` on purpose. A landing is a decision, and a decision should appear
373
+ * in the history as one revertable commit rather than dissolving into trunk.
374
+ */
375
+ async gitMergeForLanding(request) {
376
+ const branch = String(request.branch || '').trim();
377
+ const invalid = validateRefName(branch);
378
+ if (invalid)
379
+ return { merged: false, error: invalid };
380
+ const result = await this.runGitMutation(request, ['merge', '--no-ff', '--no-edit', branch]);
381
+ return result.success ? { merged: true } : { merged: false, ...(result.error ? { error: result.error } : {}) };
382
+ }
383
+ /** A failed git response that still reports where it was looking. */
384
+ gitFailure(request, error) {
385
+ return { success: false, isGitRepo: false, rootDirectory: request.rootDirectory || '', clean: true, files: [], error };
386
+ }
387
+ async gitCommit(request) {
388
+ const message = String(request.message || '').trim();
389
+ if (message.length < 3) {
390
+ return {
391
+ success: false,
392
+ isGitRepo: false,
393
+ rootDirectory: request.rootDirectory || '',
394
+ clean: true,
395
+ files: [],
396
+ error: 'Commit message must be at least 3 characters.',
397
+ };
398
+ }
399
+ return this.runGitMutation(request, ['commit', '-m', message]);
400
+ }
401
+ discoverChecks(request) {
402
+ try {
403
+ const rootDirectory = this.resolveRoot(request);
404
+ const packagePath = join(rootDirectory, 'package.json');
405
+ if (!existsSync(packagePath)) {
406
+ return { success: true, rootDirectory, checks: [] };
407
+ }
408
+ const parsed = JSON.parse(readFileSync(packagePath, 'utf-8'));
409
+ const scripts = parsed.scripts && typeof parsed.scripts === 'object' ? parsed.scripts : {};
410
+ const checks = [];
411
+ for (const [scriptName, rawCommand] of Object.entries(scripts)) {
412
+ if (typeof rawCommand !== 'string')
413
+ continue;
414
+ if (!/^[a-zA-Z0-9:_-]+$/.test(scriptName))
415
+ continue;
416
+ const kind = classifyScript(scriptName, rawCommand);
417
+ if (!kind)
418
+ continue;
419
+ const invocation = npmScriptInvocation(scriptName);
420
+ checks.push({
421
+ id: `script:${scriptName}`,
422
+ kind,
423
+ name: scriptName,
424
+ command: invocation.command,
425
+ args: invocation.args,
426
+ packageScript: scriptName,
427
+ });
428
+ }
429
+ checks.sort((a, b) => checkSortScore(a) - checkSortScore(b) || a.name.localeCompare(b.name));
430
+ return { success: true, rootDirectory, checks, packageManager: 'npm' };
431
+ }
432
+ catch (err) {
433
+ return {
434
+ success: false,
435
+ rootDirectory: request.rootDirectory || '',
436
+ checks: [],
437
+ error: err instanceof Error ? err.message : String(err),
438
+ };
439
+ }
440
+ }
441
+ async runCheck(request) {
442
+ try {
443
+ const rootDirectory = this.resolveRoot(request);
444
+ const check = this.discoverChecks(request).checks.find((item) => item.id === request.checkId);
445
+ if (!check)
446
+ return { success: false, error: 'Check was not discovered for this workspace.' };
447
+ const startedAt = Date.now();
448
+ const outputLimit = Math.max(20_000, Math.min(request.maxOutputBytes || DEFAULT_RUN_OUTPUT, 5_000_000));
449
+ const result = await runProcess(check.command, check.args, rootDirectory, Math.max(5_000, Math.min(request.timeoutMs || 10 * 60_000, 30 * 60_000)), outputLimit);
450
+ const endedAt = Date.now();
451
+ const run = {
452
+ id: `eng-run-${startedAt}-${Math.random().toString(36).slice(2, 8)}`,
453
+ rootDirectory,
454
+ check,
455
+ startedAt,
456
+ endedAt,
457
+ durationMs: endedAt - startedAt,
458
+ success: result.success,
459
+ exitCode: result.exitCode,
460
+ stdout: result.stdout,
461
+ stderr: result.stderr,
462
+ truncated: result.truncated,
463
+ error: result.error || (result.timedOut ? 'Check timed out.' : undefined),
464
+ };
465
+ this.appendRun(rootDirectory, run);
466
+ return { success: true, run };
467
+ }
468
+ catch (err) {
469
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
470
+ }
471
+ }
472
+ listRuns(request) {
473
+ try {
474
+ const rootDirectory = this.resolveRoot(request);
475
+ return { success: true, rootDirectory, runs: this.readRunStore(rootDirectory).runs };
476
+ }
477
+ catch (err) {
478
+ return {
479
+ success: false,
480
+ rootDirectory: request.rootDirectory || '',
481
+ runs: [],
482
+ error: err instanceof Error ? err.message : String(err),
483
+ };
484
+ }
485
+ }
486
+ startBackgroundCheck(request) {
487
+ try {
488
+ const rootDirectory = this.resolveRoot(request);
489
+ const check = this.discoverChecks(request).checks.find((item) => item.id === request.checkId);
490
+ if (!check)
491
+ return { success: false, rootDirectory, error: 'Check was not discovered for this workspace.' };
492
+ const startedAt = Date.now();
493
+ const outputLimit = Math.max(20_000, Math.min(request.maxOutputBytes || DEFAULT_RUN_OUTPUT, 5_000_000));
494
+ const run = {
495
+ id: `eng-bg-${startedAt}-${Math.random().toString(36).slice(2, 8)}`,
496
+ rootDirectory,
497
+ check,
498
+ status: 'running',
499
+ startedAt,
500
+ updatedAt: startedAt,
501
+ stdout: '',
502
+ stderr: '',
503
+ truncated: false,
504
+ };
505
+ const child = spawn(check.command, check.args, {
506
+ cwd: rootDirectory,
507
+ windowsHide: true,
508
+ shell: false,
509
+ env: process.env,
510
+ });
511
+ const active = {
512
+ run,
513
+ child,
514
+ outputLimit,
515
+ stopRequested: false,
516
+ finalized: false,
517
+ };
518
+ this.activeBackgroundRuns.set(run.id, active);
519
+ this.writeBackgroundRun(rootDirectory, run);
520
+ const append = (target, chunk) => {
521
+ const text = chunk.toString();
522
+ if (target === 'stdout')
523
+ active.run.stdout += text;
524
+ else
525
+ active.run.stderr += text;
526
+ if (active.run.stdout.length + active.run.stderr.length > active.outputLimit) {
527
+ active.run.truncated = true;
528
+ const keep = Math.max(4_000, Math.floor(active.outputLimit / 2));
529
+ active.run.stdout = active.run.stdout.slice(-keep);
530
+ active.run.stderr = active.run.stderr.slice(-keep);
531
+ }
532
+ active.run.updatedAt = Date.now();
533
+ this.writeBackgroundRun(rootDirectory, active.run);
534
+ };
535
+ const finalize = (status, exitCode, error) => {
536
+ if (active.finalized)
537
+ return;
538
+ active.finalized = true;
539
+ const endedAt = Date.now();
540
+ active.run.status = status;
541
+ active.run.exitCode = exitCode;
542
+ active.run.error = error || active.run.error;
543
+ active.run.endedAt = endedAt;
544
+ active.run.updatedAt = endedAt;
545
+ active.run.durationMs = endedAt - active.run.startedAt;
546
+ this.activeBackgroundRuns.delete(active.run.id);
547
+ this.writeBackgroundRun(rootDirectory, active.run);
548
+ this.appendRun(rootDirectory, {
549
+ id: active.run.id,
550
+ rootDirectory,
551
+ check: active.run.check,
552
+ startedAt: active.run.startedAt,
553
+ endedAt,
554
+ durationMs: active.run.durationMs,
555
+ success: status === 'completed',
556
+ exitCode,
557
+ stdout: active.run.stdout,
558
+ stderr: active.run.stderr,
559
+ truncated: active.run.truncated,
560
+ error: active.run.error,
561
+ });
562
+ };
563
+ child.stdout.on('data', (chunk) => append('stdout', chunk));
564
+ child.stderr.on('data', (chunk) => append('stderr', chunk));
565
+ child.on('error', (err) => finalize('failed', null, err.message));
566
+ child.on('close', (code) => {
567
+ if (active.stopRequested)
568
+ finalize('stopped', code, 'Background check stopped by user.');
569
+ else
570
+ finalize(code === 0 ? 'completed' : 'failed', code);
571
+ });
572
+ return { success: true, rootDirectory, run };
573
+ }
574
+ catch (err) {
575
+ return { success: false, rootDirectory: request.rootDirectory || '', error: err instanceof Error ? err.message : String(err) };
576
+ }
577
+ }
578
+ listBackgroundRuns(request) {
579
+ try {
580
+ const rootDirectory = this.resolveRoot(request);
581
+ return { success: true, rootDirectory, runs: this.readBackgroundRunStore(rootDirectory).runs };
582
+ }
583
+ catch (err) {
584
+ return {
585
+ success: false,
586
+ rootDirectory: request.rootDirectory || '',
587
+ runs: [],
588
+ error: err instanceof Error ? err.message : String(err),
589
+ };
590
+ }
591
+ }
592
+ readBackgroundRunOutput(request) {
593
+ try {
594
+ const rootDirectory = this.resolveRoot(request);
595
+ const runId = String(request.runId || '').trim();
596
+ if (!runId)
597
+ return { success: false, rootDirectory, error: 'runId is required.' };
598
+ const run = this.activeBackgroundRuns.get(runId)?.run
599
+ || this.readBackgroundRunStore(rootDirectory).runs.find((entry) => entry.id === runId);
600
+ if (!run)
601
+ return { success: false, rootDirectory, error: 'Background run was not found.' };
602
+ return { success: true, rootDirectory, run };
603
+ }
604
+ catch (err) {
605
+ return { success: false, rootDirectory: request.rootDirectory || '', error: err instanceof Error ? err.message : String(err) };
606
+ }
607
+ }
608
+ stopBackgroundRun(request) {
609
+ try {
610
+ const rootDirectory = this.resolveRoot(request);
611
+ const runId = String(request.runId || '').trim();
612
+ if (!runId)
613
+ return { success: false, rootDirectory, error: 'runId is required.' };
614
+ const active = this.activeBackgroundRuns.get(runId);
615
+ if (active) {
616
+ active.stopRequested = true;
617
+ active.run.status = 'stopped';
618
+ active.run.updatedAt = Date.now();
619
+ active.run.error = 'Background check stopped by user.';
620
+ this.writeBackgroundRun(rootDirectory, active.run);
621
+ killProcessTree(active.child);
622
+ return { success: true, rootDirectory, run: active.run };
623
+ }
624
+ const store = this.readBackgroundRunStore(rootDirectory);
625
+ const run = store.runs.find((entry) => entry.id === runId);
626
+ if (!run)
627
+ return { success: false, rootDirectory, error: 'Background run was not found.' };
628
+ if (run.status === 'running') {
629
+ const now = Date.now();
630
+ run.status = 'stopped';
631
+ run.updatedAt = now;
632
+ run.endedAt = now;
633
+ run.durationMs = now - run.startedAt;
634
+ run.error = 'Background process is no longer attached to this Hub session.';
635
+ this.writeBackgroundRun(rootDirectory, run);
636
+ }
637
+ return { success: true, rootDirectory, run };
638
+ }
639
+ catch (err) {
640
+ return { success: false, rootDirectory: request.rootDirectory || '', error: err instanceof Error ? err.message : String(err) };
641
+ }
642
+ }
643
+ async diagnostics(request) {
644
+ try {
645
+ const rootDirectory = this.resolveRoot(request);
646
+ const [git, discovery] = await Promise.all([
647
+ this.gitStatus(request),
648
+ Promise.resolve(this.discoverChecks(request)),
649
+ ]);
650
+ const recentRuns = this.readRunStore(rootDirectory).runs.slice(0, 12);
651
+ const backgroundRuns = this.readBackgroundRunStore(rootDirectory).runs.slice(0, 12);
652
+ const failedRuns = recentRuns.filter((run) => !run.success).length;
653
+ const activeBackgroundRuns = backgroundRuns.filter((run) => run.status === 'running').length;
654
+ const dirtyFiles = git.files?.length || 0;
655
+ const issues = collectDiagnosticIssues(rootDirectory, recentRuns, backgroundRuns);
656
+ const summaryParts = [
657
+ git.isGitRepo ? `${dirtyFiles} changed git file${dirtyFiles === 1 ? '' : 's'}` : 'not a git repository',
658
+ `${discovery.checks.length} runnable check${discovery.checks.length === 1 ? '' : 's'}`,
659
+ recentRuns.length ? `${failedRuns} failing recent run${failedRuns === 1 ? '' : 's'}` : 'no recent runs',
660
+ backgroundRuns.length ? `${activeBackgroundRuns} active background check${activeBackgroundRuns === 1 ? '' : 's'}` : 'no background checks',
661
+ issues.length ? `${issues.length} parsed diagnostic${issues.length === 1 ? '' : 's'}` : 'no parsed diagnostics',
662
+ ];
663
+ return {
664
+ success: true,
665
+ rootDirectory,
666
+ summary: summaryParts.join(' / '),
667
+ git,
668
+ checks: discovery.checks,
669
+ recentRuns,
670
+ backgroundRuns,
671
+ issues,
672
+ };
673
+ }
674
+ catch (err) {
675
+ return {
676
+ success: false,
677
+ rootDirectory: request.rootDirectory || '',
678
+ summary: '',
679
+ checks: [],
680
+ recentRuns: [],
681
+ issues: [],
682
+ error: err instanceof Error ? err.message : String(err),
683
+ };
684
+ }
685
+ }
686
+ listPatchReviews(request) {
687
+ try {
688
+ const rootDirectory = this.resolveRoot(request);
689
+ return { success: true, rootDirectory, items: this.readPatchReviewStore(rootDirectory).items };
690
+ }
691
+ catch (err) {
692
+ return {
693
+ success: false,
694
+ rootDirectory: request.rootDirectory || '',
695
+ items: [],
696
+ error: err instanceof Error ? err.message : String(err),
697
+ };
698
+ }
699
+ }
700
+ enqueuePatchReview(request) {
701
+ try {
702
+ const rootDirectory = this.resolveRoot(request);
703
+ const diff = String(request.diff || '').trim();
704
+ if (!diff)
705
+ return { success: false, rootDirectory, error: 'Patch review diff is required.' };
706
+ const now = Date.now();
707
+ const store = this.readPatchReviewStore(rootDirectory);
708
+ const id = String(request.id || `patch-review-${now}-${Math.random().toString(36).slice(2, 8)}`);
709
+ const existing = store.items.find((item) => item.id === id);
710
+ const rawFiles = request.files || request.filesChanged || existing?.filesChanged || existing?.files || [];
711
+ const reviewFiles = rawFiles
712
+ .map((file) => String(file || '').trim())
713
+ .filter(Boolean)
714
+ .map((file) => isAbsolute(file) && isPathInside(file, rootDirectory)
715
+ ? relative(rootDirectory, file).replace(/\\/g, '/')
716
+ : file.replace(/\\/g, '/'));
717
+ const item = {
718
+ id,
719
+ rootDirectory,
720
+ title: String(request.title || existing?.title || 'Patch review').trim() || 'Patch review',
721
+ description: request.description || existing?.description,
722
+ diff,
723
+ files: this.validateRelativePaths(rootDirectory, reviewFiles),
724
+ filesChanged: this.validateRelativePaths(rootDirectory, reviewFiles),
725
+ summary: String(request.summary || request.title || existing?.summary || existing?.title || 'Patch review').trim() || 'Patch review',
726
+ timestamp: Number.isFinite(request.timestamp) ? Number(request.timestamp) : (existing?.timestamp || now),
727
+ conversationId: normalizeOptionalId(request.conversationId || existing?.conversationId),
728
+ requestId: normalizeOptionalId(request.requestId || existing?.requestId),
729
+ checkpointId: normalizeOptionalId(request.checkpointId || existing?.checkpointId),
730
+ testsRun: normalizeStringList(request.testsRun || existing?.testsRun || []),
731
+ finalSummary: request.finalSummary || existing?.finalSummary,
732
+ fileSnapshots: normalizePatchFileSnapshots(rootDirectory, request.fileSnapshots || existing?.fileSnapshots || []),
733
+ hunks: [],
734
+ decisions: normalizePatchReviewDecisions(existing?.decisions || []),
735
+ status: existing?.status || 'open',
736
+ notes: existing?.notes,
737
+ source: request.source || existing?.source || 'manual',
738
+ createdAt: existing?.createdAt || now,
739
+ updatedAt: now,
740
+ };
741
+ item.hunks = buildPatchReviewHunks(item.fileSnapshots || []);
742
+ store.items = [item, ...store.items.filter((entry) => entry.id !== item.id)].slice(0, MAX_STORED_PATCH_REVIEWS);
743
+ this.writePatchReviewStore(rootDirectory, store);
744
+ return { success: true, rootDirectory, item, items: store.items };
745
+ }
746
+ catch (err) {
747
+ return {
748
+ success: false,
749
+ rootDirectory: request.rootDirectory || '',
750
+ error: err instanceof Error ? err.message : String(err),
751
+ };
752
+ }
753
+ }
754
+ updatePatchReview(request) {
755
+ try {
756
+ const rootDirectory = this.resolveRoot(request);
757
+ const id = String(request.id || '').trim();
758
+ if (!id)
759
+ return { success: false, rootDirectory, error: 'Patch review id is required.' };
760
+ const store = this.readPatchReviewStore(rootDirectory);
761
+ const item = store.items.find((entry) => entry.id === id);
762
+ if (!item)
763
+ return { success: false, rootDirectory, error: 'Patch review was not found.' };
764
+ item.status = normalizePatchReviewStatus(request.status, item.status);
765
+ item.notes = request.notes ?? item.notes;
766
+ item.updatedAt = Date.now();
767
+ this.writePatchReviewStore(rootDirectory, store);
768
+ return { success: true, rootDirectory, item, items: store.items };
769
+ }
770
+ catch (err) {
771
+ return {
772
+ success: false,
773
+ rootDirectory: request.rootDirectory || '',
774
+ error: err instanceof Error ? err.message : String(err),
775
+ };
776
+ }
777
+ }
778
+ reviewPatchSelection(request) {
779
+ const empty = { selectedHunkIds: [], wouldChangeFiles: [] };
780
+ try {
781
+ const rootDirectory = this.resolveRoot(request);
782
+ const id = normalizeRequiredId(request.id, 'id');
783
+ const relativePath = this.validateRelativePaths(rootDirectory, [String(request.relativePath || '').replace(/\\/g, '/')])[0];
784
+ if (!relativePath)
785
+ return { success: false, rootDirectory, ...empty, error: 'A safe relative patch path is required.' };
786
+ if (request.action !== 'accept' && request.action !== 'reject' && request.action !== 'comment') {
787
+ return { success: false, rootDirectory, ...empty, error: 'Patch decision action must be accept, reject, or comment.' };
788
+ }
789
+ const store = this.readPatchReviewStore(rootDirectory);
790
+ const item = store.items.find((entry) => entry.id === id);
791
+ if (!item)
792
+ return { success: false, rootDirectory, ...empty, error: 'Patch review was not found.' };
793
+ item.fileSnapshots = normalizePatchFileSnapshots(rootDirectory, item.fileSnapshots || []);
794
+ item.hunks = buildPatchReviewHunks(item.fileSnapshots);
795
+ const decisions = normalizePatchReviewDecisions(item.decisions || []);
796
+ item.decisions = decisions;
797
+ const pathKey = reviewPathKey(relativePath);
798
+ const snapshot = item.fileSnapshots.find((entry) => reviewPathKey(entry.relativePath) === pathKey);
799
+ const pathHunks = item.hunks.filter((entry) => reviewPathKey(entry.relativePath) === pathKey);
800
+ const requestedHunk = request.hunkId
801
+ ? pathHunks.find((entry) => entry.id === request.hunkId)
802
+ : undefined;
803
+ if (request.action === 'comment') {
804
+ const comment = normalizeReviewComment(request.comment);
805
+ if (!comment)
806
+ return { success: false, rootDirectory, ...empty, error: 'A non-empty review comment is required.' };
807
+ if (request.hunkId && !requestedHunk) {
808
+ return { success: false, rootDirectory, ...empty, error: 'The selected patch hunk was not found.' };
809
+ }
810
+ const now = Date.now();
811
+ decisions.push({
812
+ id: `patch-decision-${now}-${randomUUID()}`,
813
+ relativePath,
814
+ ...(requestedHunk ? { hunkId: requestedHunk.id } : {}),
815
+ action: 'comment',
816
+ comment,
817
+ state: 'applied',
818
+ createdAt: now,
819
+ updatedAt: now,
820
+ });
821
+ item.decisions = decisions.slice(-1_000);
822
+ item.updatedAt = now;
823
+ this.writePatchReviewStore(rootDirectory, store);
824
+ return {
825
+ success: true,
826
+ rootDirectory,
827
+ item,
828
+ selectedHunkIds: requestedHunk ? [requestedHunk.id] : [],
829
+ wouldChangeFiles: [],
830
+ applied: true,
831
+ };
832
+ }
833
+ const conflicts = [];
834
+ if (!snapshot) {
835
+ conflicts.push({ relativePath, reason: 'selection_not_found', detail: 'The patch has no immutable snapshot for this file.' });
836
+ }
837
+ if (request.hunkId && !requestedHunk) {
838
+ conflicts.push({ relativePath, reason: 'selection_not_found', detail: 'The selected patch hunk was not found.' });
839
+ }
840
+ const effective = effectivePatchDecisions(decisions);
841
+ const targetHunks = request.hunkId
842
+ ? (requestedHunk ? [requestedHunk] : [])
843
+ : pathHunks.filter((hunk) => !effective.has(hunk.id));
844
+ for (const hunk of targetHunks) {
845
+ if (effective.has(hunk.id)) {
846
+ conflicts.push({ relativePath, reason: 'already_decided', detail: `Patch hunk ${hunk.id} already has a final decision.` });
847
+ }
848
+ }
849
+ if (targetHunks.length === 0 && conflicts.length === 0) {
850
+ conflicts.push({ relativePath, reason: 'already_decided', detail: 'Every hunk in this file already has a final decision.' });
851
+ }
852
+ let expectedBefore;
853
+ let resulting;
854
+ let inspected;
855
+ if (snapshot && conflicts.length === 0) {
856
+ if (snapshot.originalExists === undefined || snapshot.modifiedExists === undefined) {
857
+ conflicts.push({
858
+ relativePath,
859
+ reason: 'legacy_snapshot_ambiguous',
860
+ detail: 'This patch predates explicit file-existence metadata and cannot be selectively applied.',
861
+ });
862
+ }
863
+ else {
864
+ try {
865
+ expectedBefore = composePatchSnapshot(snapshot, pathHunks, decisions);
866
+ const prospectiveDecisions = [
867
+ ...decisions,
868
+ ...targetHunks.map((hunk, index) => ({
869
+ id: `preview-${index}`,
870
+ relativePath,
871
+ hunkId: hunk.id,
872
+ action: request.action,
873
+ state: 'applied',
874
+ createdAt: Date.now() + index,
875
+ updatedAt: Date.now() + index,
876
+ })),
877
+ ];
878
+ resulting = composePatchSnapshot(snapshot, pathHunks, prospectiveDecisions);
879
+ inspected = inspectRestoreTarget(rootDirectory, relativePath);
880
+ if (inspected.exists !== expectedBefore.exists
881
+ || (inspected.exists && inspected.sha256 !== expectedBefore.sha256)) {
882
+ conflicts.push({
883
+ relativePath,
884
+ reason: 'current_state_changed',
885
+ expectedExists: expectedBefore.exists,
886
+ actualExists: inspected.exists,
887
+ ...(expectedBefore.exists ? { expectedSha256: expectedBefore.sha256 } : {}),
888
+ ...(inspected.sha256 ? { actualSha256: inspected.sha256 } : {}),
889
+ detail: 'The workspace file no longer matches the exact reviewed patch state.',
890
+ });
891
+ }
892
+ }
893
+ catch (error) {
894
+ const message = error instanceof Error ? error.message : String(error);
895
+ conflicts.push({
896
+ relativePath,
897
+ reason: /regular text file|size limit|snapshot/i.test(message) ? 'unsupported_file_type' : 'unsafe_path',
898
+ detail: message,
899
+ });
900
+ }
901
+ }
902
+ }
903
+ const selectedHunkIds = targetHunks.map((hunk) => hunk.id);
904
+ const changesFile = request.action === 'reject'
905
+ && !!expectedBefore
906
+ && !!resulting
907
+ && (expectedBefore.exists !== resulting.exists || expectedBefore.sha256 !== resulting.sha256);
908
+ const wouldChangeFiles = changesFile ? [relativePath] : [];
909
+ const previewToken = hashText(JSON.stringify({
910
+ id,
911
+ rootDirectory: resolve(rootDirectory),
912
+ relativePath,
913
+ action: request.action,
914
+ selectedHunkIds,
915
+ expectedBefore,
916
+ resulting,
917
+ actual: inspected ? { exists: inspected.exists, sha256: inspected.sha256 || null } : null,
918
+ conflicts,
919
+ }));
920
+ if (!request.confirmed) {
921
+ return {
922
+ success: true,
923
+ rootDirectory,
924
+ item,
925
+ selectedHunkIds,
926
+ wouldChangeFiles,
927
+ previewToken,
928
+ conflicts,
929
+ applied: false,
930
+ };
931
+ }
932
+ if (!request.previewToken || request.previewToken !== previewToken) {
933
+ return {
934
+ success: false,
935
+ rootDirectory,
936
+ item,
937
+ selectedHunkIds,
938
+ wouldChangeFiles,
939
+ previewToken,
940
+ conflicts,
941
+ applied: false,
942
+ error: 'Patch review preview is missing or stale. Preview the selection again before confirming.',
943
+ };
944
+ }
945
+ if (conflicts.length > 0 || !snapshot || !expectedBefore || !resulting || !inspected) {
946
+ return {
947
+ success: false,
948
+ rootDirectory,
949
+ item,
950
+ selectedHunkIds,
951
+ wouldChangeFiles,
952
+ previewToken,
953
+ conflicts,
954
+ applied: false,
955
+ error: 'Patch selection conflicts with the immutable snapshot or current workspace state.',
956
+ };
957
+ }
958
+ const now = Date.now();
959
+ const pending = targetHunks.map((hunk, index) => ({
960
+ id: `patch-decision-${now}-${index}-${randomUUID()}`,
961
+ relativePath,
962
+ hunkId: hunk.id,
963
+ action: request.action,
964
+ ...(normalizeReviewComment(request.comment) ? { comment: normalizeReviewComment(request.comment) } : {}),
965
+ state: changesFile ? 'pending' : 'applied',
966
+ expectedBeforeExists: expectedBefore.exists,
967
+ expectedBeforeSha256: expectedBefore.sha256,
968
+ resultingExists: resulting.exists,
969
+ resultingSha256: resulting.sha256,
970
+ createdAt: now + index,
971
+ updatedAt: now + index,
972
+ }));
973
+ decisions.push(...pending);
974
+ item.decisions = decisions.slice(-1_000);
975
+ item.updatedAt = now;
976
+ if (changesFile)
977
+ this.writePatchReviewStore(rootDirectory, store);
978
+ let transactionId;
979
+ if (changesFile) {
980
+ try {
981
+ const transaction = applyCheckpointRestoreTransaction(rootDirectory, `patch-review-${id}`, [{
982
+ relativePath,
983
+ targetPath: inspected.targetPath,
984
+ originalContent: resulting.content,
985
+ originalExists: resulting.exists,
986
+ originalSha256: resulting.sha256,
987
+ expectedCurrentExists: expectedBefore.exists,
988
+ expectedCurrentSha256: expectedBefore.sha256,
989
+ actualExists: inspected.exists,
990
+ actualSha256: inspected.sha256,
991
+ }]);
992
+ transactionId = transaction.transactionId;
993
+ for (const decision of pending) {
994
+ decision.state = 'applied';
995
+ decision.transactionId = transactionId;
996
+ decision.updatedAt = Date.now();
997
+ }
998
+ }
999
+ catch (error) {
1000
+ const message = error instanceof Error ? error.message : String(error);
1001
+ for (const decision of pending) {
1002
+ decision.state = 'failed';
1003
+ decision.error = message;
1004
+ decision.updatedAt = Date.now();
1005
+ }
1006
+ this.writePatchReviewStore(rootDirectory, store);
1007
+ return {
1008
+ success: false,
1009
+ rootDirectory,
1010
+ item,
1011
+ selectedHunkIds,
1012
+ wouldChangeFiles,
1013
+ previewToken,
1014
+ conflicts,
1015
+ applied: false,
1016
+ error: message,
1017
+ };
1018
+ }
1019
+ }
1020
+ item.status = derivePatchReviewStatus(item);
1021
+ item.updatedAt = Date.now();
1022
+ this.writePatchReviewStore(rootDirectory, store);
1023
+ return {
1024
+ success: true,
1025
+ rootDirectory,
1026
+ item,
1027
+ selectedHunkIds,
1028
+ wouldChangeFiles,
1029
+ previewToken,
1030
+ conflicts: [],
1031
+ applied: true,
1032
+ ...(transactionId ? { transactionId } : {}),
1033
+ };
1034
+ }
1035
+ catch (err) {
1036
+ return {
1037
+ success: false,
1038
+ rootDirectory: request.rootDirectory || '',
1039
+ ...empty,
1040
+ error: err instanceof Error ? err.message : String(err),
1041
+ };
1042
+ }
1043
+ }
1044
+ listCheckpoints(request) {
1045
+ try {
1046
+ const rootDirectory = this.resolveRoot(request);
1047
+ return { success: true, rootDirectory, checkpoints: this.buildCheckpointSummaries(rootDirectory) };
1048
+ }
1049
+ catch (err) {
1050
+ return {
1051
+ success: false,
1052
+ rootDirectory: request.rootDirectory || '',
1053
+ checkpoints: [],
1054
+ error: err instanceof Error ? err.message : String(err),
1055
+ };
1056
+ }
1057
+ }
1058
+ compareCheckpoints(request) {
1059
+ try {
1060
+ const rootDirectory = this.resolveRoot(request);
1061
+ const fromId = normalizeRequiredId(request.fromCheckpointId, 'fromCheckpointId');
1062
+ const toId = normalizeRequiredId(request.toCheckpointId, 'toCheckpointId');
1063
+ const checkpoints = this.buildCheckpointSummaries(rootDirectory);
1064
+ const from = checkpoints.find((checkpoint) => checkpoint.checkpointId === fromId);
1065
+ const to = checkpoints.find((checkpoint) => checkpoint.checkpointId === toId);
1066
+ if (!from || !to) {
1067
+ return {
1068
+ success: false,
1069
+ rootDirectory,
1070
+ filesChanged: [],
1071
+ diff: '',
1072
+ patches: [],
1073
+ error: 'Both checkpoints must exist in patch history before they can be compared.',
1074
+ };
1075
+ }
1076
+ const patches = selectPatchesBetweenCheckpoints(this.readPatchReviewStore(rootDirectory).items, fromId, toId);
1077
+ const filesChanged = uniqueStrings(patches.flatMap((patch) => patch.filesChanged.length ? patch.filesChanged : patch.files));
1078
+ return {
1079
+ success: true,
1080
+ rootDirectory,
1081
+ from,
1082
+ to,
1083
+ filesChanged,
1084
+ diff: joinPatchDiffs(patches),
1085
+ patches,
1086
+ };
1087
+ }
1088
+ catch (err) {
1089
+ return {
1090
+ success: false,
1091
+ rootDirectory: request.rootDirectory || '',
1092
+ filesChanged: [],
1093
+ diff: '',
1094
+ patches: [],
1095
+ error: err instanceof Error ? err.message : String(err),
1096
+ };
1097
+ }
1098
+ }
1099
+ restoreCheckpoint(request) {
1100
+ try {
1101
+ const rootDirectory = this.resolveRoot(request);
1102
+ recoverInterruptedRestoreTransactions(rootDirectory);
1103
+ const checkpointId = normalizeRequiredId(request.checkpointId, 'checkpointId');
1104
+ const checkpoint = this.buildCheckpointSummaries(rootDirectory).find((entry) => entry.checkpointId === checkpointId);
1105
+ if (!checkpoint) {
1106
+ return {
1107
+ success: false,
1108
+ rootDirectory,
1109
+ previewDiff: '',
1110
+ wouldRestoreFiles: [],
1111
+ error: 'Checkpoint was not found in engineering patch history.',
1112
+ };
1113
+ }
1114
+ const patches = this.readPatchReviewStore(rootDirectory).items.filter((patch) => patch.checkpointId === checkpointId);
1115
+ const previewDiff = joinPatchDiffs(patches);
1116
+ const plan = buildCheckpointRestorePlan(rootDirectory, checkpointId, patches);
1117
+ const wouldRestoreFiles = plan.entries.map((entry) => entry.relativePath);
1118
+ if (!request.confirmed) {
1119
+ return {
1120
+ success: true,
1121
+ rootDirectory,
1122
+ checkpoint,
1123
+ previewDiff,
1124
+ wouldRestoreFiles,
1125
+ previewToken: plan.previewToken,
1126
+ conflicts: plan.conflicts,
1127
+ restored: false,
1128
+ restoredFiles: [],
1129
+ };
1130
+ }
1131
+ if (!request.previewToken || request.previewToken !== plan.previewToken) {
1132
+ return {
1133
+ success: false,
1134
+ rootDirectory,
1135
+ checkpoint,
1136
+ previewDiff,
1137
+ wouldRestoreFiles,
1138
+ previewToken: plan.previewToken,
1139
+ conflicts: plan.conflicts,
1140
+ restored: false,
1141
+ restoredFiles: [],
1142
+ error: 'Checkpoint restore preview is missing or stale. Preview the checkpoint again before confirming.',
1143
+ };
1144
+ }
1145
+ const nonOverridableConflicts = plan.conflicts.filter((conflict) => (conflict.reason === 'unsafe_path'
1146
+ || conflict.reason === 'unsupported_file_type'
1147
+ || conflict.reason === 'legacy_snapshot_ambiguous'
1148
+ || conflict.reason === 'missing_snapshot_data'
1149
+ || conflict.reason === 'snapshot_chain_broken'));
1150
+ if (nonOverridableConflicts.length > 0
1151
+ || (plan.conflicts.length > 0 && request.conflictStrategy !== 'overwrite')) {
1152
+ return {
1153
+ success: false,
1154
+ rootDirectory,
1155
+ checkpoint,
1156
+ previewDiff,
1157
+ wouldRestoreFiles,
1158
+ previewToken: plan.previewToken,
1159
+ conflicts: plan.conflicts,
1160
+ restored: false,
1161
+ restoredFiles: [],
1162
+ error: nonOverridableConflicts.length > 0
1163
+ ? 'Checkpoint contains unsafe, unsupported, or ambiguous legacy snapshots and cannot be restored automatically.'
1164
+ : 'Workspace files changed after this checkpoint. Choose an explicit overwrite strategy or keep the current files.',
1165
+ };
1166
+ }
1167
+ const transaction = applyCheckpointRestoreTransaction(rootDirectory, checkpointId, plan.entries);
1168
+ const conversationId = normalizeOptionalId(request.conversationId || checkpoint.conversationId);
1169
+ const requestId = normalizeOptionalId(request.requestId || checkpoint.requestId) || `checkpoint-restore-${Date.now()}`;
1170
+ const runtimeEvent = {
1171
+ id: `hub-checkpoint-restore-${checkpointId}-${Date.now()}`,
1172
+ type: 'hub.checkpoint.restore',
1173
+ checkpointId,
1174
+ rootDirectory,
1175
+ filesChanged: transaction.restoredFiles,
1176
+ transactionId: transaction.transactionId,
1177
+ restoredHashes: transaction.restoredHashes,
1178
+ summary: `Restored ${transaction.restoredFiles.length} file${transaction.restoredFiles.length === 1 ? '' : 's'} from ${checkpointId}`,
1179
+ };
1180
+ if (conversationId) {
1181
+ this.runtimeEventStore.append({
1182
+ conversationId,
1183
+ requestId,
1184
+ event: runtimeEvent,
1185
+ source: 'engineering-workbench',
1186
+ });
1187
+ }
1188
+ return {
1189
+ success: true,
1190
+ rootDirectory,
1191
+ checkpoint,
1192
+ previewDiff,
1193
+ wouldRestoreFiles,
1194
+ previewToken: plan.previewToken,
1195
+ conflicts: plan.conflicts,
1196
+ restored: true,
1197
+ restoredFiles: transaction.restoredFiles,
1198
+ transactionId: transaction.transactionId,
1199
+ runtimeEvent,
1200
+ };
1201
+ }
1202
+ catch (err) {
1203
+ return {
1204
+ success: false,
1205
+ rootDirectory: request.rootDirectory || '',
1206
+ previewDiff: '',
1207
+ wouldRestoreFiles: [],
1208
+ error: err instanceof Error ? err.message : String(err),
1209
+ };
1210
+ }
1211
+ }
1212
+ listSessions(request) {
1213
+ try {
1214
+ const rootDirectory = this.resolveRoot(request);
1215
+ return { success: true, rootDirectory, sessions: this.buildEngineeringSessions(rootDirectory) };
1216
+ }
1217
+ catch (err) {
1218
+ return {
1219
+ success: false,
1220
+ rootDirectory: request.rootDirectory || '',
1221
+ sessions: [],
1222
+ error: err instanceof Error ? err.message : String(err),
1223
+ };
1224
+ }
1225
+ }
1226
+ runtimeTimeline(request) {
1227
+ try {
1228
+ const rootDirectory = this.resolveRoot(request);
1229
+ const conversationId = normalizeRequiredId(request.conversationId, 'conversationId');
1230
+ const projection = this.runtimeEventStore.project(conversationId);
1231
+ if (!projection.success) {
1232
+ return { success: false, rootDirectory, conversationId, error: projection.error || 'Failed to project runtime timeline.' };
1233
+ }
1234
+ return { success: true, rootDirectory, conversationId, timeline: projection.projection?.timeline };
1235
+ }
1236
+ catch (err) {
1237
+ return {
1238
+ success: false,
1239
+ rootDirectory: request.rootDirectory || '',
1240
+ error: err instanceof Error ? err.message : String(err),
1241
+ };
1242
+ }
1243
+ }
1244
+ resolveRoot(request) {
1245
+ return resolveRootDirectory(request.rootDirectory, request.allowedDirectories);
1246
+ }
1247
+ buildCheckpointSummaries(rootDirectory) {
1248
+ const byCheckpoint = new Map();
1249
+ for (const patch of this.readPatchReviewStore(rootDirectory).items) {
1250
+ if (!patch.checkpointId)
1251
+ continue;
1252
+ const list = byCheckpoint.get(patch.checkpointId) || [];
1253
+ list.push(patch);
1254
+ byCheckpoint.set(patch.checkpointId, list);
1255
+ }
1256
+ return [...byCheckpoint.entries()]
1257
+ .map(([checkpointId, patches]) => {
1258
+ const ordered = patches.slice().sort((a, b) => a.timestamp - b.timestamp || a.id.localeCompare(b.id));
1259
+ const first = ordered[0];
1260
+ const last = ordered[ordered.length - 1];
1261
+ return {
1262
+ id: `checkpoint-summary-${checkpointId}`,
1263
+ checkpointId,
1264
+ rootDirectory,
1265
+ createdAt: first?.timestamp || first?.createdAt || Date.now(),
1266
+ conversationId: first?.conversationId,
1267
+ requestId: first?.requestId,
1268
+ summary: uniqueStrings(ordered.map((patch) => patch.summary || patch.title)).join(' / ') || `Checkpoint ${checkpointId}`,
1269
+ filesChanged: uniqueStrings(ordered.flatMap((patch) => patch.filesChanged.length ? patch.filesChanged : patch.files)),
1270
+ patchIds: ordered.map((patch) => patch.id),
1271
+ patchCount: ordered.length,
1272
+ ...(last?.requestId ? { requestId: last.requestId } : {}),
1273
+ };
1274
+ })
1275
+ .sort((a, b) => b.createdAt - a.createdAt || b.checkpointId.localeCompare(a.checkpointId));
1276
+ }
1277
+ buildEngineeringSessions(rootDirectory) {
1278
+ const patches = this.readPatchReviewStore(rootDirectory).items
1279
+ .filter((patch) => patch.conversationId)
1280
+ .slice()
1281
+ .sort((a, b) => a.timestamp - b.timestamp || a.id.localeCompare(b.id));
1282
+ const runs = this.readRunStore(rootDirectory).runs;
1283
+ const checkpoints = this.buildCheckpointSummaries(rootDirectory);
1284
+ const byConversation = new Map();
1285
+ for (const patch of patches) {
1286
+ const key = patch.conversationId || 'manual';
1287
+ const list = byConversation.get(key) || [];
1288
+ list.push(patch);
1289
+ byConversation.set(key, list);
1290
+ }
1291
+ return [...byConversation.entries()].map(([conversationId, sessionPatches]) => {
1292
+ const startedAt = Math.min(...sessionPatches.map((patch) => patch.timestamp || patch.createdAt));
1293
+ const endedAt = Math.max(...sessionPatches.map((patch) => patch.updatedAt || patch.timestamp || patch.createdAt));
1294
+ const sessionCheckpoints = checkpoints.filter((checkpoint) => checkpoint.conversationId === conversationId);
1295
+ const testsRun = runs.filter((run) => run.startedAt >= startedAt && run.startedAt <= Math.max(endedAt, startedAt));
1296
+ const changedFiles = uniqueStrings(sessionPatches.flatMap((patch) => patch.filesChanged.length ? patch.filesChanged : patch.files));
1297
+ const passCount = testsRun.filter((run) => run.success).length;
1298
+ const failCount = testsRun.length - passCount;
1299
+ return {
1300
+ id: `engineering-session-${conversationId}`,
1301
+ rootDirectory,
1302
+ conversationId,
1303
+ startedAt,
1304
+ endedAt,
1305
+ patches: sessionPatches.slice().reverse(),
1306
+ checkpoints: sessionCheckpoints,
1307
+ testsRun,
1308
+ finalSummary: `${sessionPatches.length} patch${sessionPatches.length === 1 ? '' : 'es'} / ${changedFiles.length} file${changedFiles.length === 1 ? '' : 's'} changed / ${testsRun.length} test run${testsRun.length === 1 ? '' : 's'} (${passCount} pass, ${failCount} fail)`,
1309
+ };
1310
+ }).sort((a, b) => b.startedAt - a.startedAt);
1311
+ }
1312
+ async isGitRepo(rootDirectory) {
1313
+ const result = await runProcess('git', ['rev-parse', '--is-inside-work-tree'], rootDirectory, 5000, 8000);
1314
+ return result.success && result.stdout.trim() === 'true';
1315
+ }
1316
+ validateRelativePaths(rootDirectory, paths) {
1317
+ const safe = paths
1318
+ .map((item) => String(item || '').trim())
1319
+ .filter(Boolean)
1320
+ .map((item) => item.replace(/\\/g, '/'))
1321
+ .filter((item) => !item.startsWith('/') && !/^[A-Za-z]:/.test(item))
1322
+ .filter((item) => {
1323
+ const full = join(rootDirectory, item);
1324
+ return isPathInside(full, rootDirectory);
1325
+ });
1326
+ if (paths.length > 0 && safe.length === 0)
1327
+ throw new Error('No safe relative file paths were provided.');
1328
+ return safe;
1329
+ }
1330
+ async runGitMutation(request, args) {
1331
+ try {
1332
+ const rootDirectory = this.resolveRoot(request);
1333
+ if (!await this.isGitRepo(rootDirectory)) {
1334
+ return { success: false, isGitRepo: false, rootDirectory, clean: true, files: [], error: 'Workspace is not a git repository.' };
1335
+ }
1336
+ const result = await runProcess('git', args, rootDirectory, 120000, MAX_GIT_OUTPUT);
1337
+ if (!result.success) {
1338
+ return {
1339
+ success: false,
1340
+ isGitRepo: true,
1341
+ rootDirectory,
1342
+ clean: true,
1343
+ files: [],
1344
+ error: result.error || result.stderr || 'Git operation failed.',
1345
+ };
1346
+ }
1347
+ return this.gitStatus(request);
1348
+ }
1349
+ catch (err) {
1350
+ return {
1351
+ success: false,
1352
+ isGitRepo: false,
1353
+ rootDirectory: request.rootDirectory || '',
1354
+ clean: true,
1355
+ files: [],
1356
+ error: err instanceof Error ? err.message : String(err),
1357
+ };
1358
+ }
1359
+ }
1360
+ appendRun(rootDirectory, run) {
1361
+ const store = this.readRunStore(rootDirectory);
1362
+ store.runs = [run, ...store.runs.filter((entry) => entry.id !== run.id)].slice(0, MAX_STORED_RUNS);
1363
+ safeWriteJson(this.runStorePath(rootDirectory), store);
1364
+ }
1365
+ readRunStore(rootDirectory) {
1366
+ return safeReadJson(this.runStorePath(rootDirectory), { version: 1, runs: [] });
1367
+ }
1368
+ runStorePath(rootDirectory) {
1369
+ return join(workspaceIndexDirectory(rootDirectory), 'engineering-runs.json');
1370
+ }
1371
+ readBackgroundRunStore(rootDirectory) {
1372
+ const store = safeReadJson(this.backgroundRunStorePath(rootDirectory), { version: 1, runs: [] });
1373
+ let changed = false;
1374
+ const runs = store.runs
1375
+ .map((run) => {
1376
+ const active = this.activeBackgroundRuns.get(run.id);
1377
+ if (active)
1378
+ return active.run;
1379
+ if (run.status !== 'running')
1380
+ return run;
1381
+ changed = true;
1382
+ const now = Date.now();
1383
+ return {
1384
+ ...run,
1385
+ status: 'stopped',
1386
+ updatedAt: run.updatedAt || now,
1387
+ endedAt: run.endedAt || now,
1388
+ durationMs: run.durationMs || Math.max(0, now - run.startedAt),
1389
+ error: run.error || 'Background process is no longer attached to this Hub session.',
1390
+ };
1391
+ })
1392
+ .slice(0, MAX_STORED_BACKGROUND_RUNS);
1393
+ const normalized = { version: 1, runs };
1394
+ if (changed || runs.length !== store.runs.length)
1395
+ safeWriteJson(this.backgroundRunStorePath(rootDirectory), normalized);
1396
+ return normalized;
1397
+ }
1398
+ writeBackgroundRun(rootDirectory, run) {
1399
+ const store = safeReadJson(this.backgroundRunStorePath(rootDirectory), { version: 1, runs: [] });
1400
+ const runs = [run, ...store.runs.filter((entry) => entry.id !== run.id)].slice(0, MAX_STORED_BACKGROUND_RUNS);
1401
+ safeWriteJson(this.backgroundRunStorePath(rootDirectory), { version: 1, runs });
1402
+ }
1403
+ backgroundRunStorePath(rootDirectory) {
1404
+ return join(workspaceIndexDirectory(rootDirectory), 'engineering-background-runs.json');
1405
+ }
1406
+ readPatchReviewStore(rootDirectory) {
1407
+ recoverInterruptedRestoreTransactions(rootDirectory);
1408
+ const store = safeReadJson(this.patchReviewStorePath(rootDirectory), { version: 1, items: [] });
1409
+ let changed = false;
1410
+ store.items = Array.isArray(store.items) ? store.items.slice(0, MAX_STORED_PATCH_REVIEWS) : [];
1411
+ for (const item of store.items) {
1412
+ const snapshots = normalizePatchFileSnapshots(rootDirectory, item.fileSnapshots || []);
1413
+ const hunks = buildPatchReviewHunks(snapshots);
1414
+ const decisions = normalizePatchReviewDecisions(item.decisions || []);
1415
+ item.fileSnapshots = snapshots;
1416
+ item.hunks = hunks;
1417
+ item.decisions = decisions;
1418
+ for (const decision of decisions) {
1419
+ if (decision.state !== 'pending')
1420
+ continue;
1421
+ try {
1422
+ const inspected = inspectRestoreTarget(rootDirectory, decision.relativePath);
1423
+ if (matchesReviewDecisionState(inspected, decision.resultingExists, decision.resultingSha256)) {
1424
+ decision.state = 'applied';
1425
+ decision.error = 'Recovered a committed selective-review filesystem transaction.';
1426
+ }
1427
+ else if (matchesReviewDecisionState(inspected, decision.expectedBeforeExists, decision.expectedBeforeSha256)) {
1428
+ decision.state = 'failed';
1429
+ decision.error = 'Recovered a selective-review decision before its filesystem transaction committed.';
1430
+ }
1431
+ else {
1432
+ decision.state = 'conflict';
1433
+ decision.error = 'Selective-review recovery found a file state matching neither the pre-decision nor resulting snapshot.';
1434
+ }
1435
+ }
1436
+ catch (error) {
1437
+ decision.state = 'conflict';
1438
+ decision.error = error instanceof Error ? error.message : String(error);
1439
+ }
1440
+ decision.updatedAt = Date.now();
1441
+ changed = true;
1442
+ }
1443
+ const status = derivePatchReviewStatus(item);
1444
+ if (status !== item.status) {
1445
+ item.status = status;
1446
+ changed = true;
1447
+ }
1448
+ }
1449
+ if (changed)
1450
+ this.writePatchReviewStore(rootDirectory, store);
1451
+ return store;
1452
+ }
1453
+ writePatchReviewStore(rootDirectory, store) {
1454
+ safeWriteJson(this.patchReviewStorePath(rootDirectory), {
1455
+ version: 1,
1456
+ items: store.items.slice(0, MAX_STORED_PATCH_REVIEWS),
1457
+ });
1458
+ }
1459
+ patchReviewStorePath(rootDirectory) {
1460
+ return join(workspaceIndexDirectory(rootDirectory), 'patch-review-queue.json');
1461
+ }
1462
+ }
1463
+ function buildCheckpointRestorePlan(rootDirectory, checkpointId, patches) {
1464
+ const entriesByPath = new Map();
1465
+ const conflicts = [];
1466
+ const orderedPatches = patches.slice().sort((left, right) => (left.timestamp - right.timestamp || left.id.localeCompare(right.id)));
1467
+ for (const patch of orderedPatches) {
1468
+ for (const snapshot of patch.fileSnapshots || []) {
1469
+ const relativePath = snapshot.relativePath.replace(/\\/g, '/');
1470
+ const key = process.platform === 'win32' ? relativePath.toLowerCase() : relativePath;
1471
+ const prior = entriesByPath.get(key);
1472
+ const originalSha256 = snapshot.originalSha256 || hashText(snapshot.originalContent);
1473
+ const modifiedSha256 = snapshot.modifiedSha256 || hashText(snapshot.modifiedContent);
1474
+ if (!prior) {
1475
+ entriesByPath.set(key, {
1476
+ relativePath,
1477
+ targetPath: resolve(rootDirectory, relativePath),
1478
+ originalContent: snapshot.originalContent,
1479
+ originalExists: snapshot.originalExists,
1480
+ originalSha256,
1481
+ expectedCurrentExists: snapshot.modifiedExists,
1482
+ expectedCurrentSha256: modifiedSha256,
1483
+ actualExists: false,
1484
+ });
1485
+ }
1486
+ else {
1487
+ if (prior.expectedCurrentExists !== undefined
1488
+ && snapshot.originalExists !== undefined
1489
+ && (prior.expectedCurrentExists !== snapshot.originalExists
1490
+ || (snapshot.originalExists && prior.expectedCurrentSha256 !== originalSha256))) {
1491
+ conflicts.push({
1492
+ relativePath,
1493
+ reason: 'snapshot_chain_broken',
1494
+ expectedExists: prior.expectedCurrentExists,
1495
+ actualExists: snapshot.originalExists,
1496
+ ...(prior.expectedCurrentExists ? { expectedSha256: prior.expectedCurrentSha256 } : {}),
1497
+ ...(snapshot.originalExists ? { actualSha256: originalSha256 } : {}),
1498
+ detail: 'Consecutive snapshots for this checkpoint do not form one continuous file history.',
1499
+ });
1500
+ }
1501
+ prior.expectedCurrentExists = snapshot.modifiedExists;
1502
+ prior.expectedCurrentSha256 = modifiedSha256;
1503
+ }
1504
+ }
1505
+ }
1506
+ const entries = [...entriesByPath.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
1507
+ if (entries.length === 0) {
1508
+ conflicts.push({
1509
+ relativePath: '(checkpoint)',
1510
+ reason: 'missing_snapshot_data',
1511
+ detail: 'This checkpoint has no restorable file snapshots.',
1512
+ });
1513
+ }
1514
+ for (const entry of entries) {
1515
+ if (entry.originalExists === undefined || entry.expectedCurrentExists === undefined) {
1516
+ conflicts.push({
1517
+ relativePath: entry.relativePath,
1518
+ reason: 'legacy_snapshot_ambiguous',
1519
+ detail: 'This snapshot predates explicit file-existence metadata, so automatic deletion or recreation would be unsafe.',
1520
+ });
1521
+ continue;
1522
+ }
1523
+ try {
1524
+ const inspected = inspectRestoreTarget(rootDirectory, entry.relativePath);
1525
+ entry.targetPath = inspected.targetPath;
1526
+ entry.actualExists = inspected.exists;
1527
+ entry.actualSha256 = inspected.sha256;
1528
+ if (entry.actualExists !== entry.expectedCurrentExists
1529
+ || (entry.actualExists && entry.actualSha256 !== entry.expectedCurrentSha256)) {
1530
+ conflicts.push({
1531
+ relativePath: entry.relativePath,
1532
+ reason: 'current_state_changed',
1533
+ expectedExists: entry.expectedCurrentExists,
1534
+ actualExists: entry.actualExists,
1535
+ ...(entry.expectedCurrentExists ? { expectedSha256: entry.expectedCurrentSha256 } : {}),
1536
+ ...(entry.actualSha256 ? { actualSha256: entry.actualSha256 } : {}),
1537
+ detail: 'The current file no longer matches the exact post-edit snapshot.',
1538
+ });
1539
+ }
1540
+ }
1541
+ catch (error) {
1542
+ const message = error instanceof Error ? error.message : String(error);
1543
+ conflicts.push({
1544
+ relativePath: entry.relativePath,
1545
+ reason: /regular text file|size limit/i.test(message) ? 'unsupported_file_type' : 'unsafe_path',
1546
+ detail: message,
1547
+ });
1548
+ }
1549
+ }
1550
+ const previewToken = hashText(JSON.stringify({
1551
+ checkpointId,
1552
+ rootDirectory: resolve(rootDirectory),
1553
+ files: entries.map((entry) => ({
1554
+ relativePath: entry.relativePath,
1555
+ originalExists: entry.originalExists,
1556
+ originalSha256: entry.originalSha256,
1557
+ expectedCurrentExists: entry.expectedCurrentExists,
1558
+ expectedCurrentSha256: entry.expectedCurrentSha256,
1559
+ actualExists: entry.actualExists,
1560
+ actualSha256: entry.actualSha256 || null,
1561
+ })),
1562
+ conflicts,
1563
+ }));
1564
+ return { entries, conflicts, previewToken };
1565
+ }
1566
+ function inspectRestoreTarget(rootDirectory, relativePath) {
1567
+ const targetPath = assertSafeRestorePath(rootDirectory, relativePath);
1568
+ if (!existsSync(targetPath))
1569
+ return { targetPath, exists: false };
1570
+ const stats = lstatSync(targetPath);
1571
+ if (stats.isSymbolicLink() || !stats.isFile()) {
1572
+ throw new Error('Checkpoint targets must be regular text files, never symlinks or directories.');
1573
+ }
1574
+ if (stats.size > MAX_TRANSACTIONAL_SNAPSHOT_BYTES) {
1575
+ throw new Error(`Current file exceeds the ${MAX_TRANSACTIONAL_SNAPSHOT_BYTES}-byte transactional snapshot size limit.`);
1576
+ }
1577
+ const content = readFileSync(targetPath, 'utf8');
1578
+ if (content.length > MAX_TRANSACTIONAL_SNAPSHOT_CHARS) {
1579
+ throw new Error(`Current file exceeds the ${MAX_TRANSACTIONAL_SNAPSHOT_CHARS}-character transactional snapshot size limit.`);
1580
+ }
1581
+ return { targetPath, exists: true, sha256: hashText(content) };
1582
+ }
1583
+ function assertSafeRestorePath(rootDirectory, relativePath) {
1584
+ const normalized = relativePath.replace(/\\/g, '/');
1585
+ const parts = normalized.split('/').filter(Boolean);
1586
+ if (!normalized || isAbsolute(normalized) || parts.some((part) => part === '.' || part === '..')) {
1587
+ throw new Error('Checkpoint contains an unsafe relative path.');
1588
+ }
1589
+ const targetPath = resolve(rootDirectory, normalized);
1590
+ if (!isPathInside(targetPath, rootDirectory))
1591
+ throw new Error('Checkpoint target escapes the workspace root.');
1592
+ const canonicalRoot = realpathSync(rootDirectory);
1593
+ let cursor = resolve(rootDirectory);
1594
+ for (const part of parts.slice(0, -1)) {
1595
+ cursor = join(cursor, part);
1596
+ if (!existsSync(cursor))
1597
+ continue;
1598
+ const stats = lstatSync(cursor);
1599
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
1600
+ throw new Error('Checkpoint target crosses a symlink or non-directory path component.');
1601
+ }
1602
+ if (!isPathInside(realpathSync(cursor), canonicalRoot)) {
1603
+ throw new Error('Checkpoint target resolves outside the canonical workspace root.');
1604
+ }
1605
+ }
1606
+ return targetPath;
1607
+ }
1608
+ function applyCheckpointRestoreTransaction(rootDirectory, checkpointId, entries) {
1609
+ const transactionId = `checkpoint-restore-${Date.now()}-${randomUUID()}`;
1610
+ const transactionDirectory = checkpointTransactionDirectory(rootDirectory);
1611
+ mkdirSync(transactionDirectory, { recursive: true });
1612
+ const manifestPath = join(transactionDirectory, `${transactionId}.json`);
1613
+ const transactionFiles = [];
1614
+ for (const entry of entries) {
1615
+ if (entry.originalExists === undefined)
1616
+ throw new Error(`Checkpoint snapshot is ambiguous: ${entry.relativePath}`);
1617
+ const targetPath = assertSafeRestorePath(rootDirectory, entry.relativePath);
1618
+ const inspected = inspectRestoreTarget(rootDirectory, entry.relativePath);
1619
+ if (inspected.exists !== entry.actualExists
1620
+ || (inspected.exists && inspected.sha256 !== entry.actualSha256)) {
1621
+ throw new Error(`Checkpoint restore preview became stale for ${entry.relativePath}.`);
1622
+ }
1623
+ const parentDirectory = dirname(targetPath);
1624
+ mkdirSync(parentDirectory, { recursive: true });
1625
+ const suffix = `.xeno-${transactionId}`;
1626
+ const stagePath = entry.originalExists ? `${targetPath}${suffix}.stage` : null;
1627
+ const backupPath = `${targetPath}${suffix}.backup`;
1628
+ if (stagePath)
1629
+ writeFileSync(stagePath, entry.originalContent, { encoding: 'utf8', flag: 'wx' });
1630
+ transactionFiles.push({
1631
+ relativePath: entry.relativePath,
1632
+ targetPath,
1633
+ stagePath,
1634
+ backupPath,
1635
+ hadCurrent: inspected.exists,
1636
+ expectedCurrentExists: inspected.exists,
1637
+ expectedCurrentSha256: inspected.sha256,
1638
+ restoreExists: entry.originalExists,
1639
+ restoreSha256: entry.originalSha256,
1640
+ });
1641
+ }
1642
+ const manifest = {
1643
+ version: 1,
1644
+ transactionId,
1645
+ checkpointId,
1646
+ rootDirectory,
1647
+ phase: 'prepared',
1648
+ createdAt: Date.now(),
1649
+ updatedAt: Date.now(),
1650
+ files: transactionFiles,
1651
+ };
1652
+ safeWriteJson(manifestPath, manifest);
1653
+ try {
1654
+ manifest.phase = 'applying';
1655
+ manifest.updatedAt = Date.now();
1656
+ safeWriteJson(manifestPath, manifest);
1657
+ for (const file of transactionFiles) {
1658
+ validateRestoreTransactionFile(rootDirectory, transactionId, file);
1659
+ const inspected = inspectRestoreTarget(rootDirectory, file.relativePath);
1660
+ if (inspected.exists !== file.expectedCurrentExists
1661
+ || (inspected.exists && inspected.sha256 !== file.expectedCurrentSha256)) {
1662
+ throw new Error(`Checkpoint restore target changed during the transaction: ${file.relativePath}`);
1663
+ }
1664
+ if (existsSync(file.backupPath))
1665
+ throw new Error(`Checkpoint backup path already exists: ${file.relativePath}`);
1666
+ if (existsSync(file.targetPath))
1667
+ renameSync(file.targetPath, file.backupPath);
1668
+ if (file.restoreExists && file.stagePath)
1669
+ renameSync(file.stagePath, file.targetPath);
1670
+ }
1671
+ const restoredHashes = {};
1672
+ for (const file of transactionFiles) {
1673
+ if (!file.restoreExists) {
1674
+ if (existsSync(file.targetPath))
1675
+ throw new Error(`Checkpoint verification failed: ${file.relativePath} still exists.`);
1676
+ restoredHashes[file.relativePath] = null;
1677
+ continue;
1678
+ }
1679
+ if (!existsSync(file.targetPath))
1680
+ throw new Error(`Checkpoint verification failed: ${file.relativePath} is missing.`);
1681
+ const restoredHash = hashText(readFileSync(file.targetPath, 'utf8'));
1682
+ if (restoredHash !== file.restoreSha256) {
1683
+ throw new Error(`Checkpoint verification failed for ${file.relativePath}.`);
1684
+ }
1685
+ restoredHashes[file.relativePath] = restoredHash;
1686
+ }
1687
+ manifest.phase = 'committed';
1688
+ manifest.updatedAt = Date.now();
1689
+ safeWriteJson(manifestPath, manifest);
1690
+ cleanupTransactionArtifacts(transactionFiles);
1691
+ return {
1692
+ transactionId,
1693
+ restoredFiles: transactionFiles.map((file) => file.relativePath),
1694
+ restoredHashes,
1695
+ };
1696
+ }
1697
+ catch (error) {
1698
+ const rolledBack = rollbackTransactionFiles(rootDirectory, transactionId, transactionFiles);
1699
+ manifest.phase = rolledBack ? 'rolled_back' : 'rollback_failed';
1700
+ manifest.updatedAt = Date.now();
1701
+ manifest.error = `${error instanceof Error ? error.message : String(error)}${rolledBack ? '' : ' Automatic rollback was incomplete; recovery artifacts were retained.'}`;
1702
+ safeWriteJson(manifestPath, manifest);
1703
+ throw error;
1704
+ }
1705
+ }
1706
+ function recoverInterruptedRestoreTransactions(rootDirectory) {
1707
+ const directory = checkpointTransactionDirectory(rootDirectory);
1708
+ if (!existsSync(directory))
1709
+ return;
1710
+ for (const name of readdirSync(directory)) {
1711
+ if (!name.endsWith('.json'))
1712
+ continue;
1713
+ const manifestPath = join(directory, name);
1714
+ const manifest = safeReadJson(manifestPath, null);
1715
+ if (!manifest || manifest.version !== 1 || !sameResolvedPath(manifest.rootDirectory, rootDirectory))
1716
+ continue;
1717
+ if (name !== `${manifest.transactionId}.json` || !/^checkpoint-restore-\d+-[0-9a-f-]{36}$/i.test(manifest.transactionId))
1718
+ continue;
1719
+ try {
1720
+ validateRestoreTransactionFiles(rootDirectory, manifest.transactionId, manifest.files);
1721
+ if (manifest.phase === 'committed') {
1722
+ cleanupTransactionArtifacts(manifest.files);
1723
+ continue;
1724
+ }
1725
+ if (manifest.phase !== 'prepared' && manifest.phase !== 'applying' && manifest.phase !== 'rollback_failed')
1726
+ continue;
1727
+ const recovered = manifest.phase === 'prepared'
1728
+ ? (cleanupTransactionArtifacts(manifest.files), true)
1729
+ : rollbackTransactionFiles(rootDirectory, manifest.transactionId, manifest.files);
1730
+ manifest.phase = recovered ? 'recovered_rollback' : 'rollback_failed';
1731
+ manifest.updatedAt = Date.now();
1732
+ manifest.error = recovered
1733
+ ? 'Recovered and rolled back an interrupted checkpoint restore transaction.'
1734
+ : 'Checkpoint restore recovery was incomplete; retained backup artifacts require another recovery pass.';
1735
+ safeWriteJson(manifestPath, manifest);
1736
+ }
1737
+ catch (error) {
1738
+ manifest.phase = 'rollback_failed';
1739
+ manifest.updatedAt = Date.now();
1740
+ manifest.error = `Checkpoint restore recovery rejected unsafe transaction metadata: ${error instanceof Error ? error.message : String(error)}`;
1741
+ safeWriteJson(manifestPath, manifest);
1742
+ }
1743
+ }
1744
+ }
1745
+ function rollbackTransactionFiles(rootDirectory, transactionId, files) {
1746
+ validateRestoreTransactionFiles(rootDirectory, transactionId, files);
1747
+ let complete = true;
1748
+ for (const file of files.slice().reverse()) {
1749
+ try {
1750
+ if (existsSync(file.backupPath)) {
1751
+ if (existsSync(file.targetPath))
1752
+ unlinkSync(file.targetPath);
1753
+ renameSync(file.backupPath, file.targetPath);
1754
+ }
1755
+ else if (!file.hadCurrent && existsSync(file.targetPath)) {
1756
+ unlinkSync(file.targetPath);
1757
+ }
1758
+ }
1759
+ catch {
1760
+ complete = false;
1761
+ // Keep the transaction manifest and remaining artifacts for the next recovery pass.
1762
+ }
1763
+ }
1764
+ cleanupTransactionStageArtifacts(files);
1765
+ return complete && files.every((file) => !existsSync(file.backupPath));
1766
+ }
1767
+ function validateRestoreTransactionFiles(rootDirectory, transactionId, files) {
1768
+ for (const file of files)
1769
+ validateRestoreTransactionFile(rootDirectory, transactionId, file);
1770
+ }
1771
+ function validateRestoreTransactionFile(rootDirectory, transactionId, file) {
1772
+ const targetPath = assertSafeRestorePath(rootDirectory, file.relativePath);
1773
+ const suffix = `.xeno-${transactionId}`;
1774
+ const expectedStagePath = file.restoreExists ? `${targetPath}${suffix}.stage` : null;
1775
+ const expectedBackupPath = `${targetPath}${suffix}.backup`;
1776
+ if (!sameResolvedPath(file.targetPath, targetPath)
1777
+ || (file.stagePath === null ? expectedStagePath !== null : expectedStagePath === null || !sameResolvedPath(file.stagePath, expectedStagePath))
1778
+ || !sameResolvedPath(file.backupPath, expectedBackupPath)) {
1779
+ throw new Error(`Transaction artifact paths do not match the workspace target for ${file.relativePath}.`);
1780
+ }
1781
+ }
1782
+ function sameResolvedPath(left, right) {
1783
+ const normalizedLeft = resolve(left);
1784
+ const normalizedRight = resolve(right);
1785
+ return process.platform === 'win32'
1786
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
1787
+ : normalizedLeft === normalizedRight;
1788
+ }
1789
+ function cleanupTransactionStageArtifacts(files) {
1790
+ for (const file of files) {
1791
+ if (!file.stagePath || !existsSync(file.stagePath))
1792
+ continue;
1793
+ try {
1794
+ unlinkSync(file.stagePath);
1795
+ }
1796
+ catch { /* retained for a later recovery/repair pass */ }
1797
+ }
1798
+ }
1799
+ function cleanupTransactionArtifacts(files) {
1800
+ for (const file of files) {
1801
+ for (const artifact of [file.stagePath, file.backupPath]) {
1802
+ if (!artifact || !existsSync(artifact))
1803
+ continue;
1804
+ try {
1805
+ unlinkSync(artifact);
1806
+ }
1807
+ catch { /* retained for a later recovery/repair pass */ }
1808
+ }
1809
+ }
1810
+ }
1811
+ function checkpointTransactionDirectory(rootDirectory) {
1812
+ return join(workspaceIndexDirectory(rootDirectory), 'snapshots', 'transactions');
1813
+ }
1814
+ function normalizeRequiredId(value, label) {
1815
+ const normalized = normalizeOptionalId(value);
1816
+ if (!normalized)
1817
+ throw new Error(`${label} is required.`);
1818
+ return normalized;
1819
+ }
1820
+ function normalizeOptionalId(value) {
1821
+ return typeof value === 'string' && value.trim() ? value.trim().slice(0, 240) : undefined;
1822
+ }
1823
+ function normalizeStringList(value) {
1824
+ if (!Array.isArray(value))
1825
+ return [];
1826
+ return uniqueStrings(value.map((entry) => typeof entry === 'string' ? entry.trim() : '').filter(Boolean));
1827
+ }
1828
+ function normalizePatchFileSnapshots(rootDirectory, value) {
1829
+ if (!Array.isArray(value))
1830
+ return [];
1831
+ const snapshots = [];
1832
+ for (const entry of value) {
1833
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
1834
+ continue;
1835
+ const record = entry;
1836
+ const rawPath = typeof record.path === 'string' ? record.path.trim() : '';
1837
+ const rawRelativePath = typeof record.relativePath === 'string' ? record.relativePath.trim() : '';
1838
+ const relativePath = (rawRelativePath || (isAbsolute(rawPath) && isPathInside(rawPath, rootDirectory)
1839
+ ? relative(rootDirectory, rawPath)
1840
+ : rawPath)).replace(/\\/g, '/');
1841
+ if (!relativePath || relativePath.startsWith('/') || /^[A-Za-z]:/.test(relativePath))
1842
+ continue;
1843
+ const absolutePath = join(rootDirectory, relativePath);
1844
+ if (!isPathInside(absolutePath, rootDirectory))
1845
+ continue;
1846
+ const originalContent = typeof record.originalContent === 'string' ? record.originalContent : '';
1847
+ const modifiedContent = typeof record.modifiedContent === 'string' ? record.modifiedContent : '';
1848
+ const originalSha256 = hashText(originalContent);
1849
+ const modifiedSha256 = hashText(modifiedContent);
1850
+ persistSnapshotBlob(rootDirectory, originalSha256, originalContent);
1851
+ persistSnapshotBlob(rootDirectory, modifiedSha256, modifiedContent);
1852
+ snapshots.push({
1853
+ path: absolutePath,
1854
+ relativePath,
1855
+ originalContent,
1856
+ modifiedContent,
1857
+ ...(typeof record.originalExists === 'boolean' ? { originalExists: record.originalExists } : {}),
1858
+ ...(typeof record.modifiedExists === 'boolean' ? { modifiedExists: record.modifiedExists } : {}),
1859
+ originalSha256,
1860
+ modifiedSha256,
1861
+ originalBlobId: `sha256:${originalSha256}`,
1862
+ modifiedBlobId: `sha256:${modifiedSha256}`,
1863
+ language: typeof record.language === 'string' ? record.language : undefined,
1864
+ addedLines: typeof record.addedLines === 'number' && Number.isFinite(record.addedLines) ? record.addedLines : undefined,
1865
+ removedLines: typeof record.removedLines === 'number' && Number.isFinite(record.removedLines) ? record.removedLines : undefined,
1866
+ });
1867
+ }
1868
+ return snapshots.slice(0, 80);
1869
+ }
1870
+ function persistSnapshotBlob(rootDirectory, sha256, content) {
1871
+ const directory = join(workspaceIndexDirectory(rootDirectory), 'snapshots', 'blobs');
1872
+ const blobPath = join(directory, sha256);
1873
+ if (existsSync(blobPath))
1874
+ return;
1875
+ mkdirSync(directory, { recursive: true });
1876
+ const temporaryPath = `${blobPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
1877
+ try {
1878
+ writeFileSync(temporaryPath, content, 'utf8');
1879
+ if (!existsSync(blobPath))
1880
+ renameSync(temporaryPath, blobPath);
1881
+ }
1882
+ finally {
1883
+ if (existsSync(temporaryPath)) {
1884
+ try {
1885
+ unlinkSync(temporaryPath);
1886
+ }
1887
+ catch { /* best-effort cleanup */ }
1888
+ }
1889
+ }
1890
+ }
1891
+ function uniqueStrings(values) {
1892
+ const seen = new Set();
1893
+ const out = [];
1894
+ for (const value of values) {
1895
+ const normalized = String(value || '').trim().replace(/\\/g, '/');
1896
+ const key = normalized.toLowerCase();
1897
+ if (!normalized || seen.has(key))
1898
+ continue;
1899
+ seen.add(key);
1900
+ out.push(normalized);
1901
+ }
1902
+ return out;
1903
+ }
1904
+ function joinPatchDiffs(patches) {
1905
+ return patches
1906
+ .slice(0, MAX_CHECKPOINT_COMPARE_PATCHES)
1907
+ .map((patch) => [
1908
+ `# ${patch.id} / ${patch.summary || patch.title}`,
1909
+ patch.diff.trim(),
1910
+ ].filter(Boolean).join('\n'))
1911
+ .filter(Boolean)
1912
+ .join('\n\n');
1913
+ }
1914
+ function selectPatchesBetweenCheckpoints(items, fromCheckpointId, toCheckpointId) {
1915
+ const ordered = items
1916
+ .filter((patch) => patch.checkpointId)
1917
+ .slice()
1918
+ .sort((a, b) => a.timestamp - b.timestamp || a.id.localeCompare(b.id));
1919
+ const fromIndex = ordered.findIndex((patch) => patch.checkpointId === fromCheckpointId);
1920
+ const toIndex = ordered.findIndex((patch) => patch.checkpointId === toCheckpointId);
1921
+ if (fromIndex === -1 || toIndex === -1)
1922
+ return [];
1923
+ const start = Math.min(fromIndex, toIndex);
1924
+ const end = Math.max(fromIndex, toIndex);
1925
+ return ordered.slice(start, end + 1);
1926
+ }
1927
+ function normalizePatchReviewStatus(value, fallback) {
1928
+ return value === 'open'
1929
+ || value === 'approved'
1930
+ || value === 'changes-requested'
1931
+ || value === 'applied'
1932
+ || value === 'dismissed'
1933
+ ? value
1934
+ : fallback;
1935
+ }
1936
+ function normalizePatchReviewDecisions(value) {
1937
+ if (!Array.isArray(value))
1938
+ return [];
1939
+ const decisions = [];
1940
+ for (const entry of value.slice(-1_000)) {
1941
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
1942
+ continue;
1943
+ const record = entry;
1944
+ const id = normalizeOptionalId(record.id);
1945
+ const relativePath = typeof record.relativePath === 'string'
1946
+ ? record.relativePath.trim().replace(/\\/g, '/').slice(0, 2_000)
1947
+ : '';
1948
+ const action = record.action === 'accept' || record.action === 'reject' || record.action === 'comment'
1949
+ ? record.action
1950
+ : undefined;
1951
+ const state = record.state === 'pending' || record.state === 'applied' || record.state === 'failed' || record.state === 'conflict'
1952
+ ? record.state
1953
+ : undefined;
1954
+ if (!id || !relativePath || !action || !state)
1955
+ continue;
1956
+ const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt) ? record.createdAt : Date.now();
1957
+ const updatedAt = typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt) ? record.updatedAt : createdAt;
1958
+ decisions.push({
1959
+ id,
1960
+ relativePath,
1961
+ ...(normalizeOptionalId(record.hunkId) ? { hunkId: normalizeOptionalId(record.hunkId) } : {}),
1962
+ action,
1963
+ ...(normalizeReviewComment(record.comment) ? { comment: normalizeReviewComment(record.comment) } : {}),
1964
+ state,
1965
+ ...(typeof record.expectedBeforeExists === 'boolean' ? { expectedBeforeExists: record.expectedBeforeExists } : {}),
1966
+ ...(normalizeSha256(record.expectedBeforeSha256) ? { expectedBeforeSha256: normalizeSha256(record.expectedBeforeSha256) } : {}),
1967
+ ...(typeof record.resultingExists === 'boolean' ? { resultingExists: record.resultingExists } : {}),
1968
+ ...(normalizeSha256(record.resultingSha256) ? { resultingSha256: normalizeSha256(record.resultingSha256) } : {}),
1969
+ ...(normalizeOptionalId(record.transactionId) ? { transactionId: normalizeOptionalId(record.transactionId) } : {}),
1970
+ ...(typeof record.error === 'string' && record.error.trim() ? { error: record.error.trim().slice(0, 4_000) } : {}),
1971
+ createdAt,
1972
+ updatedAt,
1973
+ });
1974
+ }
1975
+ return decisions;
1976
+ }
1977
+ function normalizeReviewComment(value) {
1978
+ return typeof value === 'string' && value.trim() ? value.trim().slice(0, 20_000) : undefined;
1979
+ }
1980
+ function normalizeSha256(value) {
1981
+ return typeof value === 'string' && /^[0-9a-f]{64}$/i.test(value) ? value.toLowerCase() : undefined;
1982
+ }
1983
+ function reviewPathKey(value) {
1984
+ const normalized = value.replace(/\\/g, '/');
1985
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
1986
+ }
1987
+ function derivePatchReviewStatus(item) {
1988
+ const hunks = item.hunks || [];
1989
+ if (hunks.length === 0)
1990
+ return item.status;
1991
+ const effective = effectivePatchDecisions(item.decisions || []);
1992
+ if (effective.size >= hunks.length) {
1993
+ return [...effective.values()].every((action) => action === 'accept') ? 'approved' : 'applied';
1994
+ }
1995
+ return [...effective.values()].some((action) => action === 'reject') ? 'changes-requested' : 'open';
1996
+ }
1997
+ function matchesReviewDecisionState(inspected, expectedExists, expectedSha256) {
1998
+ if (expectedExists === undefined || inspected.exists !== expectedExists)
1999
+ return false;
2000
+ if (!expectedExists)
2001
+ return true;
2002
+ return !!expectedSha256 && inspected.sha256 === expectedSha256;
2003
+ }
2004
+ function npmScriptInvocation(scriptName) {
2005
+ if (process.platform === 'win32') {
2006
+ return { command: 'cmd.exe', args: ['/d', '/s', '/c', 'npm', 'run', scriptName] };
2007
+ }
2008
+ return { command: 'npm', args: ['run', scriptName] };
2009
+ }
2010
+ function classifyScript(name, command) {
2011
+ const haystack = `${name} ${command}`.toLowerCase();
2012
+ if (haystack.includes('typecheck') || haystack.includes('tsc '))
2013
+ return 'typecheck';
2014
+ if (haystack.includes('test') || haystack.includes('vitest') || haystack.includes('jest') || haystack.includes('playwright'))
2015
+ return 'test';
2016
+ if (haystack.includes('lint') || haystack.includes('eslint'))
2017
+ return 'lint';
2018
+ if (haystack.includes('build') || haystack.includes('vite build') || haystack.includes('electron-vite build'))
2019
+ return 'build';
2020
+ return null;
2021
+ }
2022
+ function checkSortScore(check) {
2023
+ if (check.kind === 'typecheck')
2024
+ return 1;
2025
+ if (check.kind === 'test')
2026
+ return 2;
2027
+ if (check.kind === 'lint')
2028
+ return 3;
2029
+ if (check.kind === 'build')
2030
+ return 4;
2031
+ return 5;
2032
+ }
2033
+ function collectDiagnosticIssues(rootDirectory, recentRuns, backgroundRuns) {
2034
+ const issues = [];
2035
+ const seen = new Set();
2036
+ for (const run of recentRuns) {
2037
+ const output = [run.error, run.stderr, run.stdout].filter(Boolean).join('\n');
2038
+ for (const issue of parseDiagnosticOutput(rootDirectory, output, run.check.name, run.id)) {
2039
+ const key = diagnosticIssueKey(issue);
2040
+ if (seen.has(key))
2041
+ continue;
2042
+ seen.add(key);
2043
+ issues.push(issue);
2044
+ if (issues.length >= 100)
2045
+ return issues;
2046
+ }
2047
+ }
2048
+ for (const run of backgroundRuns) {
2049
+ const output = [run.error, run.stderr, run.stdout].filter(Boolean).join('\n');
2050
+ for (const issue of parseDiagnosticOutput(rootDirectory, output, run.check.name, run.id)) {
2051
+ const key = diagnosticIssueKey(issue);
2052
+ if (seen.has(key))
2053
+ continue;
2054
+ seen.add(key);
2055
+ issues.push(issue);
2056
+ if (issues.length >= 100)
2057
+ return issues;
2058
+ }
2059
+ }
2060
+ return issues;
2061
+ }
2062
+ function parseDiagnosticOutput(rootDirectory, output, source, runId) {
2063
+ const issues = [];
2064
+ const lines = output.split(/\r?\n/);
2065
+ for (const rawLine of lines) {
2066
+ const line = rawLine.trim();
2067
+ if (!line)
2068
+ continue;
2069
+ const parsed = parseDiagnosticLine(rootDirectory, line, source, runId);
2070
+ if (parsed)
2071
+ issues.push(parsed);
2072
+ if (issues.length >= 80)
2073
+ break;
2074
+ }
2075
+ return issues;
2076
+ }
2077
+ function parseDiagnosticLine(rootDirectory, line, source, runId) {
2078
+ const filePattern = String.raw `(?<file>(?:[A-Za-z]:)?[^:\r\n()]+?\.(?:ts|tsx|js|jsx|mjs|cjs|css|scss|sass|less|json|md|mdx|py|rs|go|c|cc|cpp|h|hpp|cs|java|kt|html|htm))`;
2079
+ const tsMatch = line.match(new RegExp(`^${filePattern}\\((?<line>\\d+),(?<column>\\d+)\\):\\s*(?<severity>error|warning|info)?\\s*(?<code>[A-Za-z]+\\d+)?\\s*:?\\s*(?<message>.+)$`, 'i'));
2080
+ const colonMatch = line.match(new RegExp(`^${filePattern}:(?<line>\\d+):(?<column>\\d+)\\s*(?:[-:]\\s*)?(?<severity>error|warning|warn|info)?\\s*(?<code>[A-Za-z]+\\d+)?\\s*:?\\s*(?<message>.+)$`, 'i'));
2081
+ const failMatch = line.match(new RegExp(`^(?:FAIL|ERROR|Error:)\\s+${filePattern}(?:\\s|$)(?<message>.*)$`, 'i'));
2082
+ const groups = tsMatch?.groups || colonMatch?.groups || failMatch?.groups;
2083
+ if (!groups?.file)
2084
+ return null;
2085
+ const pathInfo = resolveDiagnosticPath(rootDirectory, groups.file);
2086
+ const severityText = (groups.severity || '').toLowerCase();
2087
+ const severity = severityText === 'warning' || severityText === 'warn'
2088
+ ? 'warning'
2089
+ : severityText === 'info'
2090
+ ? 'info'
2091
+ : 'error';
2092
+ const lineNumber = Number(groups.line);
2093
+ const columnNumber = Number(groups.column);
2094
+ const message = String(groups.message || line).trim();
2095
+ return {
2096
+ id: `diag-${runId}-${hashDiagnosticLine(line)}`,
2097
+ rootDirectory,
2098
+ ...pathInfo,
2099
+ line: Number.isFinite(lineNumber) && lineNumber > 0 ? lineNumber : undefined,
2100
+ column: Number.isFinite(columnNumber) && columnNumber > 0 ? columnNumber : undefined,
2101
+ severity,
2102
+ source,
2103
+ code: groups.code,
2104
+ message,
2105
+ runId,
2106
+ };
2107
+ }
2108
+ function resolveDiagnosticPath(rootDirectory, rawPath) {
2109
+ const cleanPath = rawPath.trim().replace(/^"|"$/g, '').replace(/\\/g, '/');
2110
+ const absolutePath = isAbsolute(cleanPath) ? cleanPath : join(rootDirectory, cleanPath);
2111
+ if (isPathInside(absolutePath, rootDirectory)) {
2112
+ return {
2113
+ filePath: absolutePath,
2114
+ relativePath: relative(rootDirectory, absolutePath).replace(/\\/g, '/'),
2115
+ };
2116
+ }
2117
+ return { relativePath: cleanPath };
2118
+ }
2119
+ function diagnosticIssueKey(issue) {
2120
+ return [
2121
+ issue.relativePath || issue.filePath || '',
2122
+ issue.line || 0,
2123
+ issue.column || 0,
2124
+ issue.code || '',
2125
+ issue.message,
2126
+ ].join(':');
2127
+ }
2128
+ function hashDiagnosticLine(line) {
2129
+ let hash = 0;
2130
+ for (let i = 0; i < line.length; i += 1) {
2131
+ hash = ((hash << 5) - hash + line.charCodeAt(i)) | 0;
2132
+ }
2133
+ return Math.abs(hash).toString(36);
2134
+ }
2135
+ function parseBranch(status) {
2136
+ const line = status.split(/\r?\n/).find((entry) => entry.startsWith('## '));
2137
+ if (!line)
2138
+ return {};
2139
+ const content = line.slice(3).trim();
2140
+ const match = content.match(/^([^.\s]+)(?:\.\.\.([^\s\[]+))?(?:\s+\[(.+)\])?/);
2141
+ if (!match)
2142
+ return { branch: content };
2143
+ const flags = match[3] || '';
2144
+ const aheadMatch = flags.match(/ahead\s+(\d+)/);
2145
+ const behindMatch = flags.match(/behind\s+(\d+)/);
2146
+ return {
2147
+ branch: match[1],
2148
+ upstream: match[2],
2149
+ ahead: aheadMatch ? Number(aheadMatch[1]) : 0,
2150
+ behind: behindMatch ? Number(behindMatch[1]) : 0,
2151
+ };
2152
+ }
2153
+ function parseGitStatusFiles(rootDirectory, status) {
2154
+ const lines = status.split(/\r?\n/).filter((line) => line && !line.startsWith('## '));
2155
+ return lines.map((line) => {
2156
+ const indexStatus = line[0] || ' ';
2157
+ const workTreeStatus = line[1] || ' ';
2158
+ const statusCode = `${indexStatus}${workTreeStatus}`.trim() || 'M';
2159
+ let body = line.slice(3).trim();
2160
+ let renamedFrom;
2161
+ if (body.includes(' -> ')) {
2162
+ const [from, to] = body.split(' -> ');
2163
+ if (from !== undefined && to !== undefined) {
2164
+ renamedFrom = cleanGitPath(from);
2165
+ body = to;
2166
+ }
2167
+ }
2168
+ const relativePath = cleanGitPath(body);
2169
+ return {
2170
+ path: join(rootDirectory, relativePath),
2171
+ relativePath,
2172
+ status: statusCode,
2173
+ indexStatus: indexStatus.trim() || ' ',
2174
+ workTreeStatus: workTreeStatus.trim() || ' ',
2175
+ renamedFrom,
2176
+ };
2177
+ });
2178
+ }
2179
+ function cleanGitPath(value) {
2180
+ return value.trim().replace(/^"|"$/g, '').replace(/\\/g, '/');
2181
+ }
2182
+ function parseNumstat(numstat) {
2183
+ const map = new Map();
2184
+ for (const line of numstat.split(/\r?\n/)) {
2185
+ const parts = line.split(/\t/);
2186
+ if (parts.length < 3)
2187
+ continue;
2188
+ const additions = Number(parts[0]);
2189
+ const deletions = Number(parts[1]);
2190
+ const relativePath = cleanGitPath(parts.slice(2).join('\t'));
2191
+ if (!relativePath)
2192
+ continue;
2193
+ map.set(relativePath, {
2194
+ additions: Number.isFinite(additions) ? additions : 0,
2195
+ deletions: Number.isFinite(deletions) ? deletions : 0,
2196
+ });
2197
+ }
2198
+ return map;
2199
+ }
2200
+ /**
2201
+ * The first adopter of `CommandExecutionPort` โ€” ADE ยง5.3.
2202
+ *
2203
+ * ๐Ÿ”ด A thin adapter now, deliberately. The spawning moved to
2204
+ * `LocalCommandExecutor` unchanged; what remains here is the mapping from the
2205
+ * port's result to `ProcessResult`, which differs by exactly one field:
2206
+ * `success`. Seven call sites read `result.success`, and rewriting them was not
2207
+ * the point of this change โ€” proving the port's shape against real code was.
2208
+ *
2209
+ * `success` now comes from `commandSucceeded` rather than being recomputed
2210
+ * here. The old expression was `!timedOut && code === 0`, which is equivalent
2211
+ * for every case this site can produce, and the shared one additionally refuses
2212
+ * success when `error` is set โ€” the case the spawn-failure branch used to
2213
+ * handle by writing `success: false` by hand. One spelling, and the site that
2214
+ * had it right stops being the one that has its own copy.
2215
+ *
2216
+ * This is why engineering went first: its `ProcessResult` WAS the model for
2217
+ * `CommandExecutionResult`, so nothing had to bend to fit. An adoption that has
2218
+ * to contort the caller is evidence the port is wrong.
2219
+ */
2220
+ const commandExecutor = new LocalCommandExecutor();
2221
+ function runProcess(command, args, cwd, timeoutMs, outputLimit) {
2222
+ return commandExecutor
2223
+ .execute({
2224
+ // The port takes argv as a LIST, and this site already had it split โ€”
2225
+ // which is the shape the sandbox implementation needs and the reason the
2226
+ // rule cost nothing here.
2227
+ argv: [command, ...args],
2228
+ cwd,
2229
+ timeoutMs,
2230
+ maxOutputBytes: outputLimit,
2231
+ })
2232
+ .then((result) => ({
2233
+ stdout: result.stdout,
2234
+ stderr: result.stderr,
2235
+ exitCode: result.exitCode,
2236
+ success: commandSucceeded(result),
2237
+ timedOut: result.timedOut,
2238
+ truncated: result.truncated,
2239
+ ...(result.error === undefined ? {} : { error: result.error }),
2240
+ }));
2241
+ }
2242
+ function killProcessTree(child) {
2243
+ if (process.platform === 'win32' && child.pid) {
2244
+ const killer = spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], {
2245
+ windowsHide: true,
2246
+ shell: false,
2247
+ stdio: 'ignore',
2248
+ });
2249
+ killer.on('error', () => {
2250
+ try {
2251
+ child.kill();
2252
+ }
2253
+ catch { /* process already exited */ }
2254
+ });
2255
+ return;
2256
+ }
2257
+ try {
2258
+ child.kill();
2259
+ }
2260
+ catch { /* process already exited */ }
2261
+ }
2262
+ let singleton = null;
2263
+ export function getEngineeringWorkbenchService() {
2264
+ if (!singleton)
2265
+ singleton = new EngineeringWorkbenchService();
2266
+ return singleton;
2267
+ }
2268
+ /**
2269
+ * Refuses a ref name git would accept but a caller rarely means.
2270
+ *
2271
+ * These names reach a command line. `--upload-pack=...` is a git OPTION, not a
2272
+ * branch, and a name beginning with a dash is read as one wherever the argument
2273
+ * is not guarded by `--`. Rejecting the shape is simpler and stronger than
2274
+ * remembering to place `--` correctly at every call site, and the failure is
2275
+ * legible rather than mysterious.
2276
+ */
2277
+ function validateRefName(name) {
2278
+ if (!name)
2279
+ return 'A branch name is required.';
2280
+ if (name.length > 255)
2281
+ return 'Branch names are limited to 255 characters.';
2282
+ if (name.startsWith('-'))
2283
+ return 'A branch name may not begin with a dash; git would read it as an option.';
2284
+ // git check-ref-format's own rules, the ones that matter here.
2285
+ if (/[\s~^:?*\[\\]/.test(name))
2286
+ return 'Branch names may not contain whitespace or any of ~ ^ : ? * [ \\';
2287
+ if (name.includes('..') || name.endsWith('.lock') || name.endsWith('/') || name.startsWith('/')) {
2288
+ return 'That is not a valid git ref name.';
2289
+ }
2290
+ return undefined;
2291
+ }
2292
+ function validateRemoteName(name) {
2293
+ if (!name)
2294
+ return 'A remote name is required.';
2295
+ if (name.startsWith('-'))
2296
+ return 'A remote name may not begin with a dash; git would read it as an option.';
2297
+ if (!/^[A-Za-z0-9._\/-]+$/.test(name))
2298
+ return 'That is not a valid git remote name.';
2299
+ return undefined;
2300
+ }
2301
+ //# sourceMappingURL=EngineeringWorkbenchService.js.map