mstro-app 0.5.0 → 0.5.5

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 (649) hide show
  1. package/LICENSE +129 -190
  2. package/PRIVACY.md +11 -11
  3. package/README.md +75 -28
  4. package/bin/commands/config.js +1 -2
  5. package/bin/mstro.js +55 -5
  6. package/bin/postinstall.js +0 -1
  7. package/dist/server/cli/eta-estimator.d.ts +55 -0
  8. package/dist/server/cli/eta-estimator.d.ts.map +1 -0
  9. package/dist/server/cli/eta-estimator.js +222 -0
  10. package/dist/server/cli/eta-estimator.js.map +1 -0
  11. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
  12. package/dist/server/cli/headless/claude-invoker-process.js +0 -1
  13. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
  14. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
  15. package/dist/server/cli/headless/claude-invoker-stall.js +0 -1
  16. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
  17. package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
  18. package/dist/server/cli/headless/claude-invoker-stream.js +0 -1
  19. package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
  20. package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -1
  21. package/dist/server/cli/headless/claude-invoker-tools.js +0 -1
  22. package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -1
  23. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  24. package/dist/server/cli/headless/claude-invoker.js +0 -1
  25. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  26. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -1
  27. package/dist/server/cli/headless/haiku-assessments.js +0 -1
  28. package/dist/server/cli/headless/haiku-assessments.js.map +1 -1
  29. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
  30. package/dist/server/cli/headless/headless-logger.js +0 -1
  31. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  32. package/dist/server/cli/headless/index.d.ts.map +1 -1
  33. package/dist/server/cli/headless/index.js +0 -1
  34. package/dist/server/cli/headless/index.js.map +1 -1
  35. package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -1
  36. package/dist/server/cli/headless/native-timeout-detector.js +0 -1
  37. package/dist/server/cli/headless/native-timeout-detector.js.map +1 -1
  38. package/dist/server/cli/headless/output-utils.d.ts.map +1 -1
  39. package/dist/server/cli/headless/output-utils.js +0 -1
  40. package/dist/server/cli/headless/output-utils.js.map +1 -1
  41. package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -1
  42. package/dist/server/cli/headless/prompt-utils.js +0 -1
  43. package/dist/server/cli/headless/prompt-utils.js.map +1 -1
  44. package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -1
  45. package/dist/server/cli/headless/resilient-runner.js +0 -1
  46. package/dist/server/cli/headless/resilient-runner.js.map +1 -1
  47. package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -1
  48. package/dist/server/cli/headless/retry-strategies.js +0 -1
  49. package/dist/server/cli/headless/retry-strategies.js.map +1 -1
  50. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  51. package/dist/server/cli/headless/runner.js +0 -1
  52. package/dist/server/cli/headless/runner.js.map +1 -1
  53. package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
  54. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  55. package/dist/server/cli/headless/stall-assessor.js +64 -10
  56. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  57. package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
  58. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  59. package/dist/server/cli/headless/tool-watchdog.js +19 -13
  60. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  61. package/dist/server/cli/headless/types.d.ts.map +1 -1
  62. package/dist/server/cli/headless/types.js +0 -1
  63. package/dist/server/cli/headless/types.js.map +1 -1
  64. package/dist/server/cli/improvisation-attachments.d.ts.map +1 -1
  65. package/dist/server/cli/improvisation-attachments.js +0 -1
  66. package/dist/server/cli/improvisation-attachments.js.map +1 -1
  67. package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
  68. package/dist/server/cli/improvisation-history-store.js +5 -2
  69. package/dist/server/cli/improvisation-history-store.js.map +1 -1
  70. package/dist/server/cli/improvisation-movements.d.ts.map +1 -1
  71. package/dist/server/cli/improvisation-movements.js +0 -1
  72. package/dist/server/cli/improvisation-movements.js.map +1 -1
  73. package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
  74. package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
  75. package/dist/server/cli/improvisation-output-queue.js +30 -8
  76. package/dist/server/cli/improvisation-output-queue.js.map +1 -1
  77. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  78. package/dist/server/cli/improvisation-retry.js +0 -1
  79. package/dist/server/cli/improvisation-retry.js.map +1 -1
  80. package/dist/server/cli/improvisation-session-manager.d.ts +29 -0
  81. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  82. package/dist/server/cli/improvisation-session-manager.js +50 -2
  83. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  84. package/dist/server/cli/improvisation-types.d.ts +2 -0
  85. package/dist/server/cli/improvisation-types.d.ts.map +1 -1
  86. package/dist/server/cli/improvisation-types.js +0 -1
  87. package/dist/server/cli/improvisation-types.js.map +1 -1
  88. package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -1
  89. package/dist/server/cli/retry/retry-best-result.js +0 -1
  90. package/dist/server/cli/retry/retry-best-result.js.map +1 -1
  91. package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -1
  92. package/dist/server/cli/retry/retry-context-loss.js +0 -1
  93. package/dist/server/cli/retry/retry-context-loss.js.map +1 -1
  94. package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -1
  95. package/dist/server/cli/retry/retry-premature-completion.js +1 -2
  96. package/dist/server/cli/retry/retry-premature-completion.js.map +1 -1
  97. package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -1
  98. package/dist/server/cli/retry/retry-recovery-strategies.js +0 -1
  99. package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -1
  100. package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -1
  101. package/dist/server/cli/retry/retry-resume-strategy.js +0 -1
  102. package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -1
  103. package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -1
  104. package/dist/server/cli/retry/retry-runner-factory.js +0 -1
  105. package/dist/server/cli/retry/retry-runner-factory.js.map +1 -1
  106. package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -1
  107. package/dist/server/cli/retry/retry-tool-results.js +0 -1
  108. package/dist/server/cli/retry/retry-tool-results.js.map +1 -1
  109. package/dist/server/cli/retry/retry-types.d.ts.map +1 -1
  110. package/dist/server/cli/retry/retry-types.js +0 -1
  111. package/dist/server/cli/retry/retry-types.js.map +1 -1
  112. package/dist/server/engines/EngineEvent.d.ts +126 -0
  113. package/dist/server/engines/EngineEvent.d.ts.map +1 -0
  114. package/dist/server/engines/EngineEvent.js +11 -0
  115. package/dist/server/engines/EngineEvent.js.map +1 -0
  116. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
  117. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
  118. package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
  119. package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
  120. package/dist/server/engines/factory.d.ts +21 -0
  121. package/dist/server/engines/factory.d.ts.map +1 -0
  122. package/dist/server/engines/factory.js +152 -0
  123. package/dist/server/engines/factory.js.map +1 -0
  124. package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
  125. package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
  126. package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
  127. package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
  128. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
  129. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
  130. package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
  131. package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
  132. package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
  133. package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
  134. package/dist/server/engines/opencode/model-catalog.js +141 -0
  135. package/dist/server/engines/opencode/model-catalog.js.map +1 -0
  136. package/dist/server/engines/types.d.ts +146 -0
  137. package/dist/server/engines/types.d.ts.map +1 -0
  138. package/dist/server/engines/types.js +4 -0
  139. package/dist/server/engines/types.js.map +1 -0
  140. package/dist/server/index.js +1 -2
  141. package/dist/server/index.js.map +1 -1
  142. package/dist/server/mcp/bouncer-cli.js +0 -1
  143. package/dist/server/mcp/bouncer-cli.js.map +1 -1
  144. package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
  145. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
  146. package/dist/server/mcp/bouncer-haiku.js +8 -125
  147. package/dist/server/mcp/bouncer-haiku.js.map +1 -1
  148. package/dist/server/mcp/bouncer-integration.d.ts +45 -0
  149. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  150. package/dist/server/mcp/bouncer-integration.js +69 -6
  151. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  152. package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
  153. package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
  154. package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
  155. package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
  156. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
  157. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
  158. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
  159. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
  160. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
  161. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
  162. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
  163. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
  164. package/dist/server/mcp/classifier/factory.d.ts +70 -0
  165. package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
  166. package/dist/server/mcp/classifier/factory.js +155 -0
  167. package/dist/server/mcp/classifier/factory.js.map +1 -0
  168. package/dist/server/mcp/security-analysis.d.ts.map +1 -1
  169. package/dist/server/mcp/security-analysis.js +0 -1
  170. package/dist/server/mcp/security-analysis.js.map +1 -1
  171. package/dist/server/mcp/security-audit.d.ts.map +1 -1
  172. package/dist/server/mcp/security-audit.js +0 -1
  173. package/dist/server/mcp/security-audit.js.map +1 -1
  174. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  175. package/dist/server/mcp/security-patterns.js +0 -1
  176. package/dist/server/mcp/security-patterns.js.map +1 -1
  177. package/dist/server/mcp/server.js +0 -1
  178. package/dist/server/mcp/server.js.map +1 -1
  179. package/dist/server/routes/files.d.ts.map +1 -1
  180. package/dist/server/routes/files.js +0 -1
  181. package/dist/server/routes/files.js.map +1 -1
  182. package/dist/server/routes/improvise.d.ts.map +1 -1
  183. package/dist/server/routes/improvise.js +0 -1
  184. package/dist/server/routes/improvise.js.map +1 -1
  185. package/dist/server/routes/index.d.ts.map +1 -1
  186. package/dist/server/routes/index.js +0 -1
  187. package/dist/server/routes/index.js.map +1 -1
  188. package/dist/server/routes/instances.d.ts.map +1 -1
  189. package/dist/server/routes/instances.js +0 -1
  190. package/dist/server/routes/instances.js.map +1 -1
  191. package/dist/server/routes/notifications.d.ts.map +1 -1
  192. package/dist/server/routes/notifications.js +0 -1
  193. package/dist/server/routes/notifications.js.map +1 -1
  194. package/dist/server/server-setup.d.ts.map +1 -1
  195. package/dist/server/server-setup.js +0 -1
  196. package/dist/server/server-setup.js.map +1 -1
  197. package/dist/server/services/analytics.d.ts.map +1 -1
  198. package/dist/server/services/analytics.js +0 -1
  199. package/dist/server/services/analytics.js.map +1 -1
  200. package/dist/server/services/auth.d.ts.map +1 -1
  201. package/dist/server/services/auth.js +0 -1
  202. package/dist/server/services/auth.js.map +1 -1
  203. package/dist/server/services/client-id.d.ts.map +1 -1
  204. package/dist/server/services/client-id.js +0 -1
  205. package/dist/server/services/client-id.js.map +1 -1
  206. package/dist/server/services/file-explorer-ops.d.ts.map +1 -1
  207. package/dist/server/services/file-explorer-ops.js +0 -1
  208. package/dist/server/services/file-explorer-ops.js.map +1 -1
  209. package/dist/server/services/files.d.ts.map +1 -1
  210. package/dist/server/services/files.js +0 -1
  211. package/dist/server/services/files.js.map +1 -1
  212. package/dist/server/services/instances.d.ts.map +1 -1
  213. package/dist/server/services/instances.js +0 -1
  214. package/dist/server/services/instances.js.map +1 -1
  215. package/dist/server/services/pathUtils.d.ts.map +1 -1
  216. package/dist/server/services/pathUtils.js +0 -1
  217. package/dist/server/services/pathUtils.js.map +1 -1
  218. package/dist/server/services/plan/agent-loader.d.ts.map +1 -1
  219. package/dist/server/services/plan/agent-loader.js +0 -1
  220. package/dist/server/services/plan/agent-loader.js.map +1 -1
  221. package/dist/server/services/plan/agent-resolver.d.ts +26 -0
  222. package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
  223. package/dist/server/services/plan/agent-resolver.js +102 -0
  224. package/dist/server/services/plan/agent-resolver.js.map +1 -0
  225. package/dist/server/services/plan/board-config.d.ts.map +1 -1
  226. package/dist/server/services/plan/board-config.js +0 -1
  227. package/dist/server/services/plan/board-config.js.map +1 -1
  228. package/dist/server/services/plan/composer.d.ts.map +1 -1
  229. package/dist/server/services/plan/composer.js +59 -12
  230. package/dist/server/services/plan/composer.js.map +1 -1
  231. package/dist/server/services/plan/config-installer.d.ts.map +1 -1
  232. package/dist/server/services/plan/config-installer.js +0 -1
  233. package/dist/server/services/plan/config-installer.js.map +1 -1
  234. package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
  235. package/dist/server/services/plan/dependency-resolver.js +0 -1
  236. package/dist/server/services/plan/dependency-resolver.js.map +1 -1
  237. package/dist/server/services/plan/executor.d.ts.map +1 -1
  238. package/dist/server/services/plan/executor.js +48 -4
  239. package/dist/server/services/plan/executor.js.map +1 -1
  240. package/dist/server/services/plan/front-matter.d.ts.map +1 -1
  241. package/dist/server/services/plan/front-matter.js +0 -1
  242. package/dist/server/services/plan/front-matter.js.map +1 -1
  243. package/dist/server/services/plan/issue-classification.d.ts.map +1 -1
  244. package/dist/server/services/plan/issue-classification.js +0 -1
  245. package/dist/server/services/plan/issue-classification.js.map +1 -1
  246. package/dist/server/services/plan/issue-loader.d.ts.map +1 -1
  247. package/dist/server/services/plan/issue-loader.js +0 -1
  248. package/dist/server/services/plan/issue-loader.js.map +1 -1
  249. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  250. package/dist/server/services/plan/issue-prompt-builder.js +33 -2
  251. package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
  252. package/dist/server/services/plan/issue-retry.d.ts +3 -1
  253. package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
  254. package/dist/server/services/plan/issue-retry.js +2 -1
  255. package/dist/server/services/plan/issue-retry.js.map +1 -1
  256. package/dist/server/services/plan/issue-writer.d.ts.map +1 -1
  257. package/dist/server/services/plan/issue-writer.js +0 -1
  258. package/dist/server/services/plan/issue-writer.js.map +1 -1
  259. package/dist/server/services/plan/output-manager.d.ts.map +1 -1
  260. package/dist/server/services/plan/output-manager.js +0 -1
  261. package/dist/server/services/plan/output-manager.js.map +1 -1
  262. package/dist/server/services/plan/parser-core.d.ts.map +1 -1
  263. package/dist/server/services/plan/parser-core.js +1 -1
  264. package/dist/server/services/plan/parser-core.js.map +1 -1
  265. package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
  266. package/dist/server/services/plan/parser-migration.js +0 -1
  267. package/dist/server/services/plan/parser-migration.js.map +1 -1
  268. package/dist/server/services/plan/parser.d.ts.map +1 -1
  269. package/dist/server/services/plan/parser.js +0 -1
  270. package/dist/server/services/plan/parser.js.map +1 -1
  271. package/dist/server/services/plan/progress-log.d.ts.map +1 -1
  272. package/dist/server/services/plan/progress-log.js +0 -1
  273. package/dist/server/services/plan/progress-log.js.map +1 -1
  274. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
  275. package/dist/server/services/plan/prompt-builder.js +0 -1
  276. package/dist/server/services/plan/prompt-builder.js.map +1 -1
  277. package/dist/server/services/plan/readiness-planner.d.ts.map +1 -1
  278. package/dist/server/services/plan/readiness-planner.js +0 -1
  279. package/dist/server/services/plan/readiness-planner.js.map +1 -1
  280. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  281. package/dist/server/services/plan/review-gate.js +0 -1
  282. package/dist/server/services/plan/review-gate.js.map +1 -1
  283. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
  284. package/dist/server/services/plan/state-reconciler.js +0 -1
  285. package/dist/server/services/plan/state-reconciler.js.map +1 -1
  286. package/dist/server/services/plan/types.d.ts +1 -0
  287. package/dist/server/services/plan/types.d.ts.map +1 -1
  288. package/dist/server/services/plan/types.js +0 -1
  289. package/dist/server/services/plan/types.js.map +1 -1
  290. package/dist/server/services/plan/watcher.d.ts.map +1 -1
  291. package/dist/server/services/plan/watcher.js +0 -1
  292. package/dist/server/services/plan/watcher.js.map +1 -1
  293. package/dist/server/services/platform-credentials.d.ts.map +1 -1
  294. package/dist/server/services/platform-credentials.js +0 -1
  295. package/dist/server/services/platform-credentials.js.map +1 -1
  296. package/dist/server/services/platform-token-lifecycle.d.ts +70 -0
  297. package/dist/server/services/platform-token-lifecycle.d.ts.map +1 -0
  298. package/dist/server/services/platform-token-lifecycle.js +156 -0
  299. package/dist/server/services/platform-token-lifecycle.js.map +1 -0
  300. package/dist/server/services/platform.d.ts +21 -56
  301. package/dist/server/services/platform.d.ts.map +1 -1
  302. package/dist/server/services/platform.js +98 -142
  303. package/dist/server/services/platform.js.map +1 -1
  304. package/dist/server/services/sentry.d.ts.map +1 -1
  305. package/dist/server/services/sentry.js +0 -1
  306. package/dist/server/services/sentry.js.map +1 -1
  307. package/dist/server/services/settings.d.ts +76 -2
  308. package/dist/server/services/settings.d.ts.map +1 -1
  309. package/dist/server/services/settings.js +127 -5
  310. package/dist/server/services/settings.js.map +1 -1
  311. package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
  312. package/dist/server/services/terminal/pty-manager.js +0 -1
  313. package/dist/server/services/terminal/pty-manager.js.map +1 -1
  314. package/dist/server/services/terminal/pty-utils.d.ts.map +1 -1
  315. package/dist/server/services/terminal/pty-utils.js +0 -1
  316. package/dist/server/services/terminal/pty-utils.js.map +1 -1
  317. package/dist/server/services/websocket/autocomplete.d.ts.map +1 -1
  318. package/dist/server/services/websocket/autocomplete.js +0 -1
  319. package/dist/server/services/websocket/autocomplete.js.map +1 -1
  320. package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -1
  321. package/dist/server/services/websocket/file-definition-handlers.js +0 -1
  322. package/dist/server/services/websocket/file-definition-handlers.js.map +1 -1
  323. package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -1
  324. package/dist/server/services/websocket/file-download-handler.js +0 -1
  325. package/dist/server/services/websocket/file-download-handler.js.map +1 -1
  326. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  327. package/dist/server/services/websocket/file-explorer-handlers.js +0 -1
  328. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  329. package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -1
  330. package/dist/server/services/websocket/file-search-handlers.js +0 -1
  331. package/dist/server/services/websocket/file-search-handlers.js.map +1 -1
  332. package/dist/server/services/websocket/file-upload-handler.d.ts +2 -3
  333. package/dist/server/services/websocket/file-upload-handler.d.ts.map +1 -1
  334. package/dist/server/services/websocket/file-upload-handler.js +4 -7
  335. package/dist/server/services/websocket/file-upload-handler.js.map +1 -1
  336. package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
  337. package/dist/server/services/websocket/file-utils.js +0 -1
  338. package/dist/server/services/websocket/file-utils.js.map +1 -1
  339. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
  340. package/dist/server/services/websocket/git-branch-handlers.js +19 -7
  341. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
  342. package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -1
  343. package/dist/server/services/websocket/git-diff-handlers.js +0 -1
  344. package/dist/server/services/websocket/git-diff-handlers.js.map +1 -1
  345. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  346. package/dist/server/services/websocket/git-handlers.js +58 -6
  347. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  348. package/dist/server/services/websocket/git-head-watcher.d.ts.map +1 -1
  349. package/dist/server/services/websocket/git-head-watcher.js +0 -1
  350. package/dist/server/services/websocket/git-head-watcher.js.map +1 -1
  351. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -1
  352. package/dist/server/services/websocket/git-log-handlers.js +0 -1
  353. package/dist/server/services/websocket/git-log-handlers.js.map +1 -1
  354. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  355. package/dist/server/services/websocket/git-pr-handlers.js +0 -1
  356. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  357. package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -1
  358. package/dist/server/services/websocket/git-tag-handlers.js +0 -1
  359. package/dist/server/services/websocket/git-tag-handlers.js.map +1 -1
  360. package/dist/server/services/websocket/git-utils.d.ts +18 -3
  361. package/dist/server/services/websocket/git-utils.d.ts.map +1 -1
  362. package/dist/server/services/websocket/git-utils.js +58 -8
  363. package/dist/server/services/websocket/git-utils.js.map +1 -1
  364. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
  365. package/dist/server/services/websocket/git-worktree-handlers.js +230 -14
  366. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  367. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  368. package/dist/server/services/websocket/handler-context.js +0 -1
  369. package/dist/server/services/websocket/handler-context.js.map +1 -1
  370. package/dist/server/services/websocket/handler.d.ts +17 -1
  371. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  372. package/dist/server/services/websocket/handler.js +57 -6
  373. package/dist/server/services/websocket/handler.js.map +1 -1
  374. package/dist/server/services/websocket/index.d.ts.map +1 -1
  375. package/dist/server/services/websocket/index.js +0 -1
  376. package/dist/server/services/websocket/index.js.map +1 -1
  377. package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -1
  378. package/dist/server/services/websocket/msg-id-tracker.js +0 -1
  379. package/dist/server/services/websocket/msg-id-tracker.js.map +1 -1
  380. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
  381. package/dist/server/services/websocket/plan-board-handlers.js +0 -1
  382. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
  383. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
  384. package/dist/server/services/websocket/plan-execution-handlers.js +6 -2
  385. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
  386. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
  387. package/dist/server/services/websocket/plan-handlers.js +0 -1
  388. package/dist/server/services/websocket/plan-handlers.js.map +1 -1
  389. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
  390. package/dist/server/services/websocket/plan-helpers.js +0 -1
  391. package/dist/server/services/websocket/plan-helpers.js.map +1 -1
  392. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
  393. package/dist/server/services/websocket/plan-issue-handlers.js +0 -1
  394. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
  395. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
  396. package/dist/server/services/websocket/plan-sprint-handlers.js +0 -1
  397. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
  398. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
  399. package/dist/server/services/websocket/quality-complexity.js +78 -27
  400. package/dist/server/services/websocket/quality-complexity.js.map +1 -1
  401. package/dist/server/services/websocket/quality-eta.d.ts +47 -0
  402. package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
  403. package/dist/server/services/websocket/quality-eta.js +110 -0
  404. package/dist/server/services/websocket/quality-eta.js.map +1 -0
  405. package/dist/server/services/websocket/quality-grading.d.ts +69 -0
  406. package/dist/server/services/websocket/quality-grading.d.ts.map +1 -0
  407. package/dist/server/services/websocket/quality-grading.js +650 -0
  408. package/dist/server/services/websocket/quality-grading.js.map +1 -0
  409. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  410. package/dist/server/services/websocket/quality-handlers.js +145 -8
  411. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  412. package/dist/server/services/websocket/quality-linting.d.ts.map +1 -1
  413. package/dist/server/services/websocket/quality-linting.js +0 -1
  414. package/dist/server/services/websocket/quality-linting.js.map +1 -1
  415. package/dist/server/services/websocket/quality-operations.d.ts +34 -0
  416. package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
  417. package/dist/server/services/websocket/quality-operations.js +47 -0
  418. package/dist/server/services/websocket/quality-operations.js.map +1 -0
  419. package/dist/server/services/websocket/quality-persistence.d.ts +23 -0
  420. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
  421. package/dist/server/services/websocket/quality-persistence.js +38 -12
  422. package/dist/server/services/websocket/quality-persistence.js.map +1 -1
  423. package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
  424. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  425. package/dist/server/services/websocket/quality-review-agent.js +105 -57
  426. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  427. package/dist/server/services/websocket/quality-service.d.ts +12 -2
  428. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  429. package/dist/server/services/websocket/quality-service.js +387 -72
  430. package/dist/server/services/websocket/quality-service.js.map +1 -1
  431. package/dist/server/services/websocket/quality-tools.d.ts +22 -1
  432. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
  433. package/dist/server/services/websocket/quality-tools.js +55 -3
  434. package/dist/server/services/websocket/quality-tools.js.map +1 -1
  435. package/dist/server/services/websocket/quality-types.d.ts +52 -3
  436. package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
  437. package/dist/server/services/websocket/quality-types.js +1 -2
  438. package/dist/server/services/websocket/quality-types.js.map +1 -1
  439. package/dist/server/services/websocket/session-handlers.d.ts +3 -1
  440. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  441. package/dist/server/services/websocket/session-handlers.js +57 -10
  442. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  443. package/dist/server/services/websocket/session-history.d.ts.map +1 -1
  444. package/dist/server/services/websocket/session-history.js +3 -1
  445. package/dist/server/services/websocket/session-history.js.map +1 -1
  446. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  447. package/dist/server/services/websocket/session-initialization.js +158 -43
  448. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  449. package/dist/server/services/websocket/session-registry.d.ts +25 -0
  450. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  451. package/dist/server/services/websocket/session-registry.js +19 -1
  452. package/dist/server/services/websocket/session-registry.js.map +1 -1
  453. package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
  454. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
  455. package/dist/server/services/websocket/settings-handlers.js +35 -5
  456. package/dist/server/services/websocket/settings-handlers.js.map +1 -1
  457. package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
  458. package/dist/server/services/websocket/skill-handlers.js +0 -1
  459. package/dist/server/services/websocket/skill-handlers.js.map +1 -1
  460. package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -1
  461. package/dist/server/services/websocket/skill-watcher.js +0 -1
  462. package/dist/server/services/websocket/skill-watcher.js.map +1 -1
  463. package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
  464. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
  465. package/dist/server/services/websocket/tab-broadcast.js +10 -3
  466. package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
  467. package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
  468. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
  469. package/dist/server/services/websocket/tab-event-buffer.js +138 -13
  470. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
  471. package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
  472. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
  473. package/dist/server/services/websocket/tab-event-replay.js +55 -3
  474. package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
  475. package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
  476. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  477. package/dist/server/services/websocket/tab-handlers.js +47 -3
  478. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  479. package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -1
  480. package/dist/server/services/websocket/terminal-handlers.js +39 -4
  481. package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
  482. package/dist/server/services/websocket/types.d.ts +30 -7
  483. package/dist/server/services/websocket/types.d.ts.map +1 -1
  484. package/dist/server/services/websocket/types.js +12 -7
  485. package/dist/server/services/websocket/types.js.map +1 -1
  486. package/dist/server/utils/agent-manager.d.ts.map +1 -1
  487. package/dist/server/utils/agent-manager.js +0 -1
  488. package/dist/server/utils/agent-manager.js.map +1 -1
  489. package/dist/server/utils/paths.d.ts.map +1 -1
  490. package/dist/server/utils/paths.js +0 -1
  491. package/dist/server/utils/paths.js.map +1 -1
  492. package/dist/server/utils/port-manager.d.ts.map +1 -1
  493. package/dist/server/utils/port-manager.js +0 -1
  494. package/dist/server/utils/port-manager.js.map +1 -1
  495. package/dist/server/utils/port.d.ts.map +1 -1
  496. package/dist/server/utils/port.js +0 -1
  497. package/dist/server/utils/port.js.map +1 -1
  498. package/package.json +6 -4
  499. package/server/cli/eta-estimator.ts +249 -0
  500. package/server/cli/headless/claude-invoker-process.ts +0 -1
  501. package/server/cli/headless/claude-invoker-stall.ts +0 -1
  502. package/server/cli/headless/claude-invoker-stream.ts +0 -1
  503. package/server/cli/headless/claude-invoker-tools.ts +0 -1
  504. package/server/cli/headless/claude-invoker.ts +0 -1
  505. package/server/cli/headless/haiku-assessments.ts +0 -1
  506. package/server/cli/headless/headless-logger.ts +0 -1
  507. package/server/cli/headless/index.ts +0 -1
  508. package/server/cli/headless/native-timeout-detector.ts +0 -1
  509. package/server/cli/headless/output-utils.ts +0 -1
  510. package/server/cli/headless/prompt-utils.ts +0 -1
  511. package/server/cli/headless/resilient-runner.ts +0 -1
  512. package/server/cli/headless/retry-strategies.ts +0 -1
  513. package/server/cli/headless/runner.ts +0 -1
  514. package/server/cli/headless/stall-assessor.ts +93 -1
  515. package/server/cli/headless/tool-watchdog.ts +21 -1
  516. package/server/cli/headless/types.ts +0 -1
  517. package/server/cli/improvisation-attachments.ts +0 -1
  518. package/server/cli/improvisation-history-store.ts +4 -2
  519. package/server/cli/improvisation-movements.ts +0 -1
  520. package/server/cli/improvisation-output-queue.ts +29 -8
  521. package/server/cli/improvisation-retry.ts +0 -1
  522. package/server/cli/improvisation-session-manager.ts +54 -2
  523. package/server/cli/improvisation-types.ts +2 -1
  524. package/server/cli/retry/retry-best-result.ts +0 -1
  525. package/server/cli/retry/retry-context-loss.ts +0 -1
  526. package/server/cli/retry/retry-premature-completion.ts +1 -2
  527. package/server/cli/retry/retry-recovery-strategies.ts +0 -1
  528. package/server/cli/retry/retry-resume-strategy.ts +0 -1
  529. package/server/cli/retry/retry-runner-factory.ts +0 -1
  530. package/server/cli/retry/retry-tool-results.ts +0 -1
  531. package/server/cli/retry/retry-types.ts +0 -1
  532. package/server/engines/EngineEvent.ts +156 -0
  533. package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
  534. package/server/engines/factory.ts +176 -0
  535. package/server/engines/opencode/OpenCodeEngine.ts +786 -0
  536. package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
  537. package/server/engines/opencode/model-catalog.ts +217 -0
  538. package/server/engines/types.ts +173 -0
  539. package/server/index.ts +1 -2
  540. package/server/mcp/bouncer-cli.ts +0 -1
  541. package/server/mcp/bouncer-haiku.ts +21 -146
  542. package/server/mcp/bouncer-integration.ts +107 -6
  543. package/server/mcp/classifier/BouncerClassifier.ts +40 -0
  544. package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
  545. package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
  546. package/server/mcp/classifier/factory.ts +195 -0
  547. package/server/mcp/security-analysis.ts +0 -1
  548. package/server/mcp/security-audit.ts +0 -1
  549. package/server/mcp/security-patterns.ts +0 -1
  550. package/server/mcp/server.ts +0 -1
  551. package/server/routes/files.ts +0 -1
  552. package/server/routes/improvise.ts +0 -1
  553. package/server/routes/index.ts +0 -1
  554. package/server/routes/instances.ts +0 -1
  555. package/server/routes/notifications.ts +0 -1
  556. package/server/server-setup.ts +0 -1
  557. package/server/services/analytics.ts +0 -1
  558. package/server/services/auth.ts +0 -1
  559. package/server/services/client-id.ts +0 -1
  560. package/server/services/file-explorer-ops.ts +0 -1
  561. package/server/services/files.ts +0 -1
  562. package/server/services/instances.ts +0 -1
  563. package/server/services/pathUtils.ts +0 -1
  564. package/server/services/plan/agent-loader.ts +0 -1
  565. package/server/services/plan/agent-resolver.ts +115 -0
  566. package/server/services/plan/agents/code-review.md +43 -11
  567. package/server/services/plan/board-config.ts +0 -1
  568. package/server/services/plan/composer.ts +63 -12
  569. package/server/services/plan/config-installer.ts +0 -1
  570. package/server/services/plan/dependency-resolver.ts +0 -1
  571. package/server/services/plan/executor.ts +48 -4
  572. package/server/services/plan/front-matter.ts +0 -1
  573. package/server/services/plan/issue-classification.ts +0 -1
  574. package/server/services/plan/issue-loader.ts +0 -1
  575. package/server/services/plan/issue-prompt-builder.ts +39 -2
  576. package/server/services/plan/issue-retry.ts +5 -2
  577. package/server/services/plan/issue-writer.ts +0 -1
  578. package/server/services/plan/output-manager.ts +0 -1
  579. package/server/services/plan/parser-core.ts +1 -1
  580. package/server/services/plan/parser-migration.ts +0 -1
  581. package/server/services/plan/parser.ts +0 -1
  582. package/server/services/plan/progress-log.ts +0 -1
  583. package/server/services/plan/prompt-builder.ts +0 -1
  584. package/server/services/plan/readiness-planner.ts +0 -1
  585. package/server/services/plan/review-gate.ts +0 -1
  586. package/server/services/plan/state-reconciler.ts +0 -1
  587. package/server/services/plan/types.ts +4 -1
  588. package/server/services/plan/watcher.ts +0 -1
  589. package/server/services/platform-credentials.ts +0 -1
  590. package/server/services/platform-token-lifecycle.ts +171 -0
  591. package/server/services/platform.ts +106 -148
  592. package/server/services/sentry.ts +0 -1
  593. package/server/services/settings.ts +161 -5
  594. package/server/services/terminal/pty-manager.ts +0 -1
  595. package/server/services/terminal/pty-utils.ts +0 -1
  596. package/server/services/websocket/autocomplete.ts +0 -1
  597. package/server/services/websocket/file-definition-handlers.ts +0 -1
  598. package/server/services/websocket/file-download-handler.ts +0 -1
  599. package/server/services/websocket/file-explorer-handlers.ts +0 -1
  600. package/server/services/websocket/file-search-handlers.ts +0 -1
  601. package/server/services/websocket/file-upload-handler.ts +6 -5
  602. package/server/services/websocket/file-utils.ts +0 -1
  603. package/server/services/websocket/git-branch-handlers.ts +20 -7
  604. package/server/services/websocket/git-diff-handlers.ts +0 -1
  605. package/server/services/websocket/git-handlers.ts +66 -10
  606. package/server/services/websocket/git-head-watcher.ts +0 -1
  607. package/server/services/websocket/git-log-handlers.ts +0 -1
  608. package/server/services/websocket/git-pr-handlers.ts +0 -1
  609. package/server/services/websocket/git-tag-handlers.ts +0 -1
  610. package/server/services/websocket/git-utils.ts +69 -9
  611. package/server/services/websocket/git-worktree-handlers.ts +260 -17
  612. package/server/services/websocket/handler-context.ts +0 -1
  613. package/server/services/websocket/handler.ts +62 -6
  614. package/server/services/websocket/index.ts +0 -1
  615. package/server/services/websocket/msg-id-tracker.ts +0 -1
  616. package/server/services/websocket/plan-board-handlers.ts +0 -1
  617. package/server/services/websocket/plan-execution-handlers.ts +6 -2
  618. package/server/services/websocket/plan-handlers.ts +0 -1
  619. package/server/services/websocket/plan-helpers.ts +0 -1
  620. package/server/services/websocket/plan-issue-handlers.ts +0 -1
  621. package/server/services/websocket/plan-sprint-handlers.ts +0 -1
  622. package/server/services/websocket/quality-complexity.ts +80 -27
  623. package/server/services/websocket/quality-eta.ts +155 -0
  624. package/server/services/websocket/quality-grading.ts +834 -0
  625. package/server/services/websocket/quality-handlers.ts +153 -8
  626. package/server/services/websocket/quality-linting.ts +0 -1
  627. package/server/services/websocket/quality-operations.ts +72 -0
  628. package/server/services/websocket/quality-persistence.ts +47 -8
  629. package/server/services/websocket/quality-review-agent.ts +154 -65
  630. package/server/services/websocket/quality-service.ts +415 -68
  631. package/server/services/websocket/quality-tools.ts +62 -3
  632. package/server/services/websocket/quality-types.ts +61 -4
  633. package/server/services/websocket/session-handlers.ts +64 -11
  634. package/server/services/websocket/session-history.ts +3 -1
  635. package/server/services/websocket/session-initialization.ts +189 -47
  636. package/server/services/websocket/session-registry.ts +37 -1
  637. package/server/services/websocket/settings-handlers.ts +41 -5
  638. package/server/services/websocket/skill-handlers.ts +0 -1
  639. package/server/services/websocket/skill-watcher.ts +0 -1
  640. package/server/services/websocket/tab-broadcast.ts +10 -3
  641. package/server/services/websocket/tab-event-buffer.ts +143 -12
  642. package/server/services/websocket/tab-event-replay.ts +70 -4
  643. package/server/services/websocket/tab-handlers.ts +53 -6
  644. package/server/services/websocket/terminal-handlers.ts +39 -3
  645. package/server/services/websocket/types.ts +39 -8
  646. package/server/utils/agent-manager.ts +0 -1
  647. package/server/utils/paths.ts +0 -1
  648. package/server/utils/port-manager.ts +0 -1
  649. package/server/utils/port.ts +0 -1
@@ -1,5 +1,4 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
2
 
4
3
  import { spawn } from 'node:child_process';
5
4
  import { readdirSync, readFileSync, statSync } from 'node:fs';
@@ -104,7 +103,10 @@ async function tryInstallCommands(tool: QualityTool, dirPath: string): Promise<s
104
103
  let lastStderr = '';
105
104
  for (const cmd of commands) {
106
105
  const parts = cmd.trim().split(' ');
107
- const result = await runCommand(parts[0], parts.slice(1), dirPath);
106
+ // Force NODE_ENV=development for install: npm/yarn/pnpm silently skip
107
+ // devDependencies under NODE_ENV=production, making `-D` installs no-op
108
+ // (exit 0, nothing installed) so the post-install detection re-fails.
109
+ const result = await runCommand(parts[0], parts.slice(1), dirPath, { NODE_ENV: 'development' });
108
110
  if (result.exitCode === 0) return null;
109
111
  lastStderr = result.stderr;
110
112
  }
@@ -258,6 +260,57 @@ export function filesByExt(files: SourceFile[], exts: string[]): string[] {
258
260
  return out;
259
261
  }
260
262
 
263
+ /** Folders that signal "this whole tree is test code" by convention. */
264
+ const TEST_FOLDER_SEGMENTS = ['__tests__', '__mocks__', 'tests', 'test', 'e2e', 'spec'];
265
+
266
+ /** Filename regexes that mark a single file as a test, regardless of folder. */
267
+ const TEST_FILE_PATTERNS: RegExp[] = [
268
+ /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/, // JS/TS .test/.spec
269
+ /_test\.(go|py)$/, // Go / Python *_test
270
+ /^test_.+\.py$/, // Python test_*.py
271
+ ];
272
+
273
+ function pathHasTestFolder(path: string): boolean {
274
+ for (const segment of TEST_FOLDER_SEGMENTS) {
275
+ if (path.includes(`/${segment}/`) || path.startsWith(`${segment}/`)) return true;
276
+ }
277
+ return false;
278
+ }
279
+
280
+ function fileNameLooksLikeTest(name: string): boolean {
281
+ for (const pattern of TEST_FILE_PATTERNS) {
282
+ if (pattern.test(name)) return true;
283
+ }
284
+ return false;
285
+ }
286
+
287
+ /**
288
+ * Identify a path as a test/spec file. Test files are exempt from
289
+ * structural-length checks (long-file, long-function) because:
290
+ *
291
+ * - A 600-line test file with 50 small `it()` blocks is easy to read,
292
+ * each block is independent, and "split it" yields zero maintenance
293
+ * benefit while harming discoverability.
294
+ * - A 200-line test function (long Arrange-Act-Assert with helpers
295
+ * inlined) is normal for feature coverage and not a complexity smell.
296
+ *
297
+ * Linters and security/bug findings still apply to test files — only the
298
+ * structural-length heuristics defer. Pattern-matches the conventions used
299
+ * by Code Climate's default-exclude set:
300
+ *
301
+ * - JS/TS: *.test.ts, *.test.tsx, *.spec.js, *.spec.jsx, *.test.mts, ...
302
+ * - Folder: __tests__/, /tests/, /test/, e2e/, spec/, __mocks__/
303
+ * - Python: test_*.py, *_test.py
304
+ * - Go: *_test.go
305
+ * - Rust: files inside `tests/` are integration tests by convention
306
+ */
307
+ export function isTestFile(relativePath: string): boolean {
308
+ const path = relativePath.replace(/\\/g, '/').toLowerCase();
309
+ if (pathHasTestFolder(path)) return true;
310
+ const name = path.split('/').pop() ?? path;
311
+ return fileNameLooksLikeTest(name);
312
+ }
313
+
261
314
  /**
262
315
  * Split a file list into chunks so a single command invocation doesn't
263
316
  * blow past ARG_MAX. macOS ARG_MAX is ~256KB; 400 paths at ~200 chars each
@@ -276,13 +329,19 @@ export function chunkFileList(paths: string[], size = 400): string[][] {
276
329
  // Command Runner
277
330
  // ============================================================================
278
331
 
279
- export function runCommand(cmd: string, args: string[], cwd: string): Promise<{ stdout: string; stderr: string; exitCode: number }> {
332
+ export function runCommand(
333
+ cmd: string,
334
+ args: string[],
335
+ cwd: string,
336
+ envOverrides?: Record<string, string>,
337
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
280
338
  return new Promise((resolve) => {
281
339
  const proc = spawn(cmd, args, {
282
340
  cwd,
283
341
  shell: true,
284
342
  stdio: ['ignore', 'pipe', 'pipe'],
285
343
  timeout: 120000,
344
+ env: envOverrides ? { ...process.env, ...envOverrides } : undefined,
286
345
  });
287
346
  let stdout = '';
288
347
  let stderr = '';
@@ -1,10 +1,47 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
2
 
4
3
  // ============================================================================
5
4
  // Types
6
5
  // ============================================================================
7
6
 
7
+ /**
8
+ * Letter grades produced by the multi-dimensional quality rating.
9
+ *
10
+ * Score → grade mapping (per product spec — note: no `D`; F covers 56-69, F-
11
+ * covers 0-55. `D` is retained ONLY for backward compatibility with reports
12
+ * persisted by older versions of this module — new code never emits it):
13
+ *
14
+ * A+ 97-100 A 93-96 A- 90-92
15
+ * B+ 87-89 B 83-86 B- 80-82
16
+ * C+ 77-79 C 73-76 C- 70-72
17
+ * F+ 65-69 F 56-64 F- 0-55
18
+ *
19
+ * `N/A` means the dimension had no tooling available to evaluate it.
20
+ */
21
+ export type Grade =
22
+ | 'A+' | 'A' | 'A-'
23
+ | 'B+' | 'B' | 'B-'
24
+ | 'C+' | 'C' | 'C-'
25
+ | 'D' // legacy — only appears on reports persisted before the +/- rollout
26
+ | 'F+' | 'F' | 'F-'
27
+ | 'N/A';
28
+ export type DimensionName = 'security' | 'reliability' | 'maintainability';
29
+
30
+ export interface DimensionScore {
31
+ name: DimensionName;
32
+ score: number;
33
+ grade: Grade;
34
+ rationale: string;
35
+ available: boolean;
36
+ findingCount: number;
37
+ worstSeverity: 'critical' | 'high' | 'medium' | 'low' | null;
38
+ }
39
+
40
+ export interface QualityGate {
41
+ passed: boolean;
42
+ failingConditions: string[];
43
+ }
44
+
8
45
  export interface QualityTool {
9
46
  name: string;
10
47
  installed: boolean;
@@ -15,8 +52,6 @@ export interface QualityTool {
15
52
  export interface CategoryScore {
16
53
  name: string;
17
54
  score: number;
18
- weight: number;
19
- effectiveWeight: number;
20
55
  available: boolean;
21
56
  issueCount?: number;
22
57
  details?: Record<string, unknown>;
@@ -62,12 +97,34 @@ export interface QualityResults {
62
97
  timestamp: string;
63
98
  ecosystem: string[];
64
99
  scoreBreakdown?: ScoreBreakdown;
100
+ dimensions?: DimensionScore[];
101
+ qualityGate?: QualityGate;
102
+ gradeRationale?: string;
103
+ /** Wall-clock duration of the CLI scan that produced this report. Used to estimate ETA on subsequent scans of the same directory. */
104
+ scanDurationMs?: number;
105
+ /** Wall-clock duration of the AI code-review pass, when one ran. */
106
+ reviewDurationMs?: number;
65
107
  }
66
108
 
67
109
  export interface ScanProgress {
68
110
  step: string;
69
111
  current: number;
70
112
  total: number;
113
+ /**
114
+ * Wall-clock estimate of the total scan duration, in milliseconds. Sent on
115
+ * the first progress event of a scan and again on subsequent events so
116
+ * reconnecting clients still get an ETA. The web subtracts elapsed time to
117
+ * render "≈ X remaining".
118
+ */
119
+ etaMs?: number;
120
+ /**
121
+ * Server-side timestamp (ms since epoch) of when the scan started — paired
122
+ * with `etaMs` so the web can compute elapsed time without trusting its
123
+ * own clock alignment.
124
+ */
125
+ startedAt?: number;
126
+ /** Optional sub-step detail used by long-running steps (e.g. "tsc --noEmit, 18s elapsed") to keep the UI from looking stuck. */
127
+ detail?: string;
71
128
  }
72
129
 
73
130
  export type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'swift' | 'kotlin' | 'unknown';
@@ -143,7 +200,7 @@ export const ADDITIONAL_EXCLUDES = new Set([
143
200
 
144
201
  export const FILE_LENGTH_THRESHOLD = 300;
145
202
  export const FUNCTION_LENGTH_THRESHOLD = 50;
146
- export const TOTAL_STEPS = 7;
203
+ export const TOTAL_STEPS = 8;
147
204
 
148
205
  export function hasInstalledToolInCategory(
149
206
  installedSet: Set<string>,
@@ -1,5 +1,4 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
2
 
4
3
  import { type FileAttachment, ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
5
4
  import { getEffortLevel, getModel } from '../settings.js';
@@ -8,7 +7,42 @@ import { runQualityScan } from './quality-service.js';
8
7
  import type { SessionRegistry } from './session-registry.js';
9
8
  import { resolveSkillPrompt } from './skill-handlers.js';
10
9
  import { broadcastTabEvent } from './tab-broadcast.js';
11
- import type { WebSocketMessage, WSContext } from './types.js';
10
+ import { type EngineId, normalizeEngineId, type WebSocketMessage, type WSContext } from './types.js';
11
+
12
+ /**
13
+ * Apply per-prompt engine/model/effortLevel overrides from the `execute`
14
+ * WebSocket payload onto the session. `session.options` is mutated in place
15
+ * so the next `executePrompt` call (which reads options.model / effortLevel
16
+ * inside the retry loop) picks up the new values without needing a new
17
+ * session. Pass-through for undefined fields — global defaults already live
18
+ * on the session from initialization.
19
+ */
20
+ function applyExecuteOverrides(
21
+ session: ImprovisationSessionManager,
22
+ data: { engine?: unknown; model?: unknown; effortLevel?: unknown },
23
+ ): void {
24
+ const options = (session as unknown as { options: { model?: string; effortLevel?: string } }).options;
25
+ if (typeof data.model === 'string' && data.model.length > 0) {
26
+ options.model = data.model;
27
+ }
28
+ if (typeof data.effortLevel === 'string' && data.effortLevel.length > 0) {
29
+ options.effortLevel = data.effortLevel;
30
+ }
31
+ if (data.engine === 'claude-code' || data.engine === 'opencode') {
32
+ // Record the engine on the session history so `resolveEngineForSession`
33
+ // returns the user-chosen value on subsequent movementStart /
34
+ // movementComplete broadcasts. The actual engine factory swap happens
35
+ // at session-start (later-epic work); per-prompt engine flips flow
36
+ // through here so model/effort stay in sync.
37
+ const history = (session as unknown as { history: { engine: string } }).history;
38
+ history.engine = data.engine;
39
+ }
40
+ }
41
+
42
+ /** Resolve the engine for a session, defaulting to 'claude-code' for pre-engine sessions. */
43
+ export function resolveEngineForSession(session: ImprovisationSessionManager | undefined): EngineId {
44
+ return normalizeEngineId(session?.engine);
45
+ }
12
46
 
13
47
  // Re-export from extracted modules for backward compatibility
14
48
  export { handleHistoryMessage } from './session-history.js';
@@ -214,6 +248,7 @@ export function buildOutputHistory(session: ImprovisationSessionManager): Array<
214
248
  */
215
249
  export function setupSessionListeners(ctx: HandlerContext, session: ImprovisationSessionManager, _ws: WSContext, tabId: string): void {
216
250
  session.removeAllListeners();
251
+ const engine = resolveEngineForSession(session);
217
252
 
218
253
  session.on('onHistoryPersisted', () => {
219
254
  const registry = ctx.getRegistry('');
@@ -228,19 +263,19 @@ export function setupSessionListeners(ctx: HandlerContext, session: Improvisatio
228
263
  broadcastTabEvent(ctx, tabId, 'thinking', { text });
229
264
  });
230
265
 
231
- session.on('onMovementStart', (sequenceNumber: number, prompt: string, isAutoContinue?: boolean) => {
232
- broadcastTabEvent(ctx, tabId, 'movementStart', { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp, isAutoContinue });
233
- ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: true, executionStartTimestamp: session.executionStartTimestamp } });
266
+ session.on('onMovementStart', (sequenceNumber: number, prompt: string, isAutoContinue?: boolean, etaProfile?: import('../../cli/eta-estimator.js').EtaProfile | null) => {
267
+ broadcastTabEvent(ctx, tabId, 'movementStart', { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp, isAutoContinue, etaProfile: etaProfile ?? undefined }, engine);
268
+ ctx.broadcastToAll({ type: 'tabStateChanged', engine, data: { tabId, isExecuting: true, executionStartTimestamp: session.executionStartTimestamp } });
234
269
  });
235
270
 
236
271
  session.on('onMovementComplete', (movement: Record<string, unknown>) => {
237
- broadcastTabEvent(ctx, tabId, 'movementComplete', movement);
272
+ broadcastTabEvent(ctx, tabId, 'movementComplete', movement, engine);
238
273
 
239
274
  const registry = ctx.getRegistry('');
240
275
  // Use a try/catch since getRegistry may not have been initialized with the right workingDir
241
276
  try { registry.markTabUnviewed(tabId); } catch { /* ignore */ }
242
277
 
243
- ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: false, hasUnviewedCompletion: true } });
278
+ ctx.broadcastToAll({ type: 'tabStateChanged', engine, data: { tabId, isExecuting: false, hasUnviewedCompletion: true } });
244
279
 
245
280
  if (ctx.usageReporter && movement.tokensUsed) {
246
281
  ctx.usageReporter({
@@ -258,12 +293,12 @@ export function setupSessionListeners(ctx: HandlerContext, session: Improvisatio
258
293
  });
259
294
 
260
295
  session.on('onMovementError', (error: Error) => {
261
- broadcastTabEvent(ctx, tabId, 'movementError', { message: error.message });
262
- ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: false } });
296
+ broadcastTabEvent(ctx, tabId, 'movementError', { message: error.message }, engine);
297
+ ctx.broadcastToAll({ type: 'tabStateChanged', engine, data: { tabId, isExecuting: false } });
263
298
  });
264
299
 
265
300
  session.on('onSessionUpdate', (history: Record<string, unknown>) => {
266
- broadcastTabEvent(ctx, tabId, 'sessionUpdate', history);
301
+ broadcastTabEvent(ctx, tabId, 'sessionUpdate', history, engine);
267
302
  });
268
303
 
269
304
  session.on('onPlanNeedsConfirmation', (plan: Record<string, unknown>) => {
@@ -337,6 +372,13 @@ function handleExecuteMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocket
337
372
  console.log(`[session] execute accepted msgId=${msgId} tabId=${tabId} sessionId=${sessionId}`);
338
373
  }
339
374
 
375
+ // Apply per-prompt engine/model/effort overrides from the payload. The
376
+ // web client populates these from the tab's effective EnginePicker state
377
+ // (override > global). Missing fields fall through to whatever the session
378
+ // already has — typically the machine-level defaults from `settings.json`
379
+ // applied at session init.
380
+ applyExecuteOverrides(session, msg.data);
381
+
340
382
  const worktreeDir = ctx.gitDirectories.get(tabId);
341
383
  const attachments = mergePreUploadedAttachments(ctx, tabId, msg.data.attachments);
342
384
 
@@ -347,6 +389,17 @@ function handleExecuteMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocket
347
389
  const effectiveDir = worktreeDir || session.getSessionInfo().workingDir;
348
390
  const resolved = resolveSkillPrompt(rawPrompt, effectiveDir);
349
391
 
392
+ // Authoritative prompt-input clear for all connected devices. The submitter
393
+ // already cleared locally; this guarantees other devices clear even if the
394
+ // submitter's debounced syncPromptText never fires (e.g. mobile tab
395
+ // suspended after Send). Clients suppress this via locallyEditingTabs if
396
+ // the user is actively typing a new prompt.
397
+ ctx.broadcastToAll({
398
+ type: 'promptTextSync',
399
+ tabId,
400
+ data: { tabId, text: '' },
401
+ });
402
+
350
403
  session.executePrompt(
351
404
  resolved ? resolved.prompt : rawPrompt,
352
405
  attachments,
@@ -373,7 +426,7 @@ function handleNewSessionMessage(ctx: HandlerContext, ws: WSContext, tabId: stri
373
426
  if (tabMap) tabMap.set(tabId, newSessionId);
374
427
  const registry = ctx.getRegistry('');
375
428
  try { registry.updateTabSession(tabId, newSessionId); } catch { /* ignore */ }
376
- ctx.send(ws, { type: 'newSession', tabId, data: newSession.getSessionInfo() });
429
+ ctx.send(ws, { type: 'newSession', tabId, engine: resolveEngineForSession(newSession), data: newSession.getSessionInfo() });
377
430
  }
378
431
 
379
432
  export function handleSessionMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, permission?: 'view'): void {
@@ -1,5 +1,4 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
2
 
4
3
  import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
5
4
  import { readFile } from 'node:fs/promises';
@@ -92,6 +91,7 @@ function getSessionById(workingDir: string, sessionId: string): Record<string, u
92
91
  startedAt: historyData.startedAt,
93
92
  lastActivityAt: historyData.lastActivityAt,
94
93
  totalTokens: historyData.totalTokens,
94
+ engine: historyData.engine || 'claude-code',
95
95
  movementCount: historyData.movements?.length || 0,
96
96
  title: firstPrompt.slice(0, 80) + (firstPrompt.length > 80 ? '...' : ''),
97
97
  movements: historyData.movements || [],
@@ -172,11 +172,13 @@ function buildSessionSummary(historyData: Record<string, unknown>): Record<strin
172
172
  const movementPreviews = (movements || []).slice(0, 3).map((m: Record<string, unknown>) => ({
173
173
  userPrompt: (typeof m.userPrompt === 'string' ? m.userPrompt : '').slice(0, 100) || ''
174
174
  }));
175
+ const engine = typeof historyData.engine === 'string' && historyData.engine ? historyData.engine : 'claude-code';
175
176
  return {
176
177
  sessionId: historyData.sessionId,
177
178
  startedAt: historyData.startedAt,
178
179
  lastActivityAt: historyData.lastActivityAt,
179
180
  totalTokens: historyData.totalTokens,
181
+ engine,
180
182
  movementCount: movements?.length || 0,
181
183
  title: firstPrompt.slice(0, 80) + (firstPrompt.length > 80 ? '...' : ''),
182
184
  movements: movementPreviews
@@ -1,12 +1,12 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
- // Licensed under the MIT License. See LICENSE file for details.
3
2
 
3
+ import type { EtaProfile } from '../../cli/eta-estimator.js';
4
4
  import { ImprovisationSessionManager } from '../../cli/improvisation-session-manager.js';
5
5
  import { getEffortLevel, getModel } from '../settings.js';
6
6
  import type { HandlerContext } from './handler-context.js';
7
- import { buildOutputHistory, setupSessionListeners } from './session-handlers.js';
8
- import type { SessionRegistry } from './session-registry.js';
9
- import { replayTabEventsSince } from './tab-event-replay.js';
7
+ import { buildOutputHistory, resolveEngineForSession, setupSessionListeners } from './session-handlers.js';
8
+ import type { SessionRegistry, TabEngineOverride } from './session-registry.js';
9
+ import { type ReplayResult, replayTabEventsSince } from './tab-event-replay.js';
10
10
  import type { WSContext } from './types.js';
11
11
 
12
12
  /**
@@ -23,6 +23,138 @@ function extractLastSeenSeq(data: unknown): number | undefined {
23
23
  return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined;
24
24
  }
25
25
 
26
+ /**
27
+ * When the session is mid-execution, expose the cached eta profile so the
28
+ * web's ComposingIndicator can render an ETA immediately on reconnect
29
+ * instead of waiting for the next movementStart (which won't fire until
30
+ * the user submits a fresh prompt).
31
+ */
32
+ function inflightEtaPayload(session: ImprovisationSessionManager): { etaProfile?: EtaProfile } {
33
+ if (session.isExecuting && session.etaProfile) return { etaProfile: session.etaProfile };
34
+ return {};
35
+ }
36
+
37
+ /**
38
+ * Build the full-snapshot data payload for a `tabInitialized` message.
39
+ *
40
+ * Used in three situations:
41
+ * 1. Cold init (no `lastSeenSeq`) — web has no prior state to merge with.
42
+ * 2. Cold reattach (existing session, no prior seq) — same shape.
43
+ * 3. Replay-gap recovery — `replayTabEventsSince` returned `hadGap`, so
44
+ * the web's incremental state is provably stale; we replace it.
45
+ *
46
+ * `replayGap` flags the recovery case so the web can branch: drop any
47
+ * already-rendered tab output and rebuild from `outputHistory` +
48
+ * `executionEvents` instead of merging on top of stale incremental state.
49
+ * Old web clients that don't know the flag still get the full snapshot and
50
+ * render correctly — `replayGap` is purely additive telemetry.
51
+ */
52
+ function buildFullSnapshotData(
53
+ session: ImprovisationSessionManager,
54
+ options: {
55
+ worktreePath?: string;
56
+ worktreeBranch?: string;
57
+ engineOverride?: TabEngineOverride;
58
+ replayGap?: boolean;
59
+ extra?: Record<string, unknown>;
60
+ } = {},
61
+ ): Record<string, unknown> {
62
+ const isExecuting = session.isExecuting;
63
+ return {
64
+ ...session.getSessionInfo(),
65
+ engine: resolveEngineForSession(session),
66
+ outputHistory: buildOutputHistory(session),
67
+ isExecuting,
68
+ ...(isExecuting ? { executionEvents: session.getExecutionEventLog() } : {}),
69
+ ...(isExecuting && session.executionStartTimestamp
70
+ ? { executionStartTimestamp: session.executionStartTimestamp }
71
+ : {}),
72
+ ...inflightEtaPayload(session),
73
+ ...(options.worktreePath
74
+ ? { worktreePath: options.worktreePath, worktreeBranch: options.worktreeBranch }
75
+ : {}),
76
+ ...(options.engineOverride ? { engineOverride: options.engineOverride } : {}),
77
+ ...(options.replayGap ? { replayGap: true } : {}),
78
+ ...(options.extra ?? {}),
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Snapshot vs incremental decision based on the replay outcome.
84
+ *
85
+ * - `incremental`: the web should keep its current state and append the
86
+ * replayed events that already arrived (handled by `replayTabEventsSince`
87
+ * itself when there's no gap). We send `tabInitialized` with
88
+ * `resumedFromSeq: true`.
89
+ * - `snapshot`: the web should discard tab output and rebuild from a full
90
+ * snapshot. Triggered either by `lastSeenSeq === undefined` (cold start)
91
+ * or by `result.hadGap` (replay would silently skip events).
92
+ */
93
+ function decideRecoveryMode(
94
+ result: ReplayResult,
95
+ lastSeenSeq: number | undefined,
96
+ ): 'incremental' | 'snapshot' {
97
+ if (lastSeenSeq === undefined) return 'snapshot';
98
+ if (result.hadGap) return 'snapshot';
99
+ return 'incremental';
100
+ }
101
+
102
+ /**
103
+ * Send `tabInitialized` for a resume path (`tryResumeFromDisk` /
104
+ * `resumeHistoricalSession`). Picks the snapshot or incremental envelope
105
+ * shape based on `mode` and threads any extra fields the caller needs to
106
+ * carry (e.g. `resumeFailed`, worktree state, engine override).
107
+ *
108
+ * Extracted to keep the resume call sites flat — without this helper, each
109
+ * caller pushes the function over the project's cognitive-complexity gate.
110
+ */
111
+ function sendResumedTabInitialized(
112
+ ctx: HandlerContext,
113
+ ws: WSContext,
114
+ tabId: string,
115
+ session: ImprovisationSessionManager,
116
+ mode: 'incremental' | 'snapshot',
117
+ replay: ReplayResult,
118
+ options: {
119
+ worktreePath?: string;
120
+ worktreeBranch?: string;
121
+ engineOverride?: TabEngineOverride;
122
+ extra?: Record<string, unknown>;
123
+ } = {},
124
+ ): void {
125
+ const engine = resolveEngineForSession(session);
126
+ if (mode === 'snapshot') {
127
+ ctx.send(ws, {
128
+ type: 'tabInitialized',
129
+ tabId,
130
+ engine,
131
+ data: buildFullSnapshotData(session, {
132
+ worktreePath: options.worktreePath,
133
+ worktreeBranch: options.worktreeBranch,
134
+ engineOverride: options.engineOverride,
135
+ replayGap: replay.hadGap,
136
+ extra: options.extra,
137
+ }),
138
+ });
139
+ return;
140
+ }
141
+ ctx.send(ws, {
142
+ type: 'tabInitialized',
143
+ tabId,
144
+ engine,
145
+ data: {
146
+ ...session.getSessionInfo(),
147
+ engine,
148
+ resumedFromSeq: true,
149
+ ...(options.worktreePath
150
+ ? { worktreePath: options.worktreePath, worktreeBranch: options.worktreeBranch }
151
+ : {}),
152
+ ...(options.engineOverride ? { engineOverride: options.engineOverride } : {}),
153
+ ...(options.extra ?? {}),
154
+ },
155
+ });
156
+ }
157
+
26
158
  function tryResumeFromDisk(
27
159
  ctx: HandlerContext,
28
160
  ws: WSContext,
@@ -55,16 +187,17 @@ function tryResumeFromDisk(
55
187
  // BEFORE tabInitialized so they arrive in the right order. Web-side
56
188
  // handlers append; `tabInitialized` does NOT reset when `resumedFromSeq`
57
189
  // is set, preserving the replayed additions.
58
- replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
59
-
60
- ctx.send(ws, {
61
- type: 'tabInitialized',
62
- tabId,
63
- data: {
64
- ...diskSession.getSessionInfo(),
65
- ...(lastSeenSeq === undefined ? { outputHistory: buildOutputHistory(diskSession) } : { resumedFromSeq: true }),
66
- ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
67
- }
190
+ //
191
+ // If `replayTabEventsSince` reports `hadGap`, no events were emitted and
192
+ // we fall back to a full-snapshot `tabInitialized` so the web replaces
193
+ // its (now-known-stale) incremental state instead of merging on top.
194
+ const replay = replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
195
+ const mode = decideRecoveryMode(replay, lastSeenSeq);
196
+
197
+ sendResumedTabInitialized(ctx, ws, tabId, diskSession, mode, replay, {
198
+ worktreePath,
199
+ worktreeBranch,
200
+ engineOverride: regTab?.engineOverride,
68
201
  });
69
202
  return true;
70
203
  } catch {
@@ -122,21 +255,32 @@ export async function initializeTab(ctx: HandlerContext, ws: WSContext, tabId: s
122
255
 
123
256
  registry.registerTab(tabId, sessionId, tabName || existingTab?.tabName);
124
257
  const registeredTab = registry.getTab(tabId);
125
- ctx.broadcastToAll({
258
+ const engine = resolveEngineForSession(session);
259
+ // Mirror terminal-handlers.ts: broadcastToOthers, not broadcastToAll. The
260
+ // requesting client already drove this initTab and will receive
261
+ // `tabInitialized` below — echoing `tabCreated` back risks racing the
262
+ // discovery handler during a flicker and producing a phantom tab.
263
+ ctx.broadcastToOthers(ws, {
126
264
  type: 'tabCreated',
127
- data: { tabId, tabName: registeredTab?.tabName || 'Chat', createdAt: registeredTab?.createdAt, order: registeredTab?.order, sessionInfo: session.getSessionInfo() }
265
+ engine,
266
+ data: { tabId, tabName: registeredTab?.tabName || 'Chat', createdAt: registeredTab?.createdAt, order: registeredTab?.order, engine, sessionInfo: session.getSessionInfo() }
128
267
  });
129
268
 
130
269
  // Fresh session (no disk/memory predecessor) has nothing to replay,
131
270
  // but we still pass lastSeenSeq through so the web flag is consistent.
132
- replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
271
+ // hadGap is impossible here (buffer is empty for a brand-new tab), but
272
+ // route through `decideRecoveryMode` for uniformity with the resume paths.
273
+ const replay = replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
274
+ const mode = decideRecoveryMode(replay, lastSeenSeq);
133
275
 
134
276
  ctx.send(ws, {
135
277
  type: 'tabInitialized',
136
278
  tabId,
279
+ engine,
137
280
  data: {
138
281
  ...session.getSessionInfo(),
139
- ...(lastSeenSeq !== undefined ? { resumedFromSeq: true } : {}),
282
+ ...(mode === 'incremental' ? { resumedFromSeq: true } : {}),
283
+ ...(replay.hadGap ? { replayGap: true } : {}),
140
284
  }
141
285
  });
142
286
  }
@@ -193,17 +337,14 @@ export async function resumeHistoricalSession(
193
337
 
194
338
  registry.registerTab(tabId, sessionId);
195
339
 
196
- replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
340
+ const replay = replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
341
+ const mode = decideRecoveryMode(replay, lastSeenSeq);
197
342
 
198
- ctx.send(ws, {
199
- type: 'tabInitialized',
200
- tabId,
201
- data: {
202
- ...session.getSessionInfo(),
203
- ...(lastSeenSeq === undefined ? { outputHistory: buildOutputHistory(session) } : { resumedFromSeq: true }),
343
+ sendResumedTabInitialized(ctx, ws, tabId, session, mode, replay, {
344
+ extra: {
204
345
  resumeFailed: isNewSession,
205
- originalSessionId: isNewSession ? historicalSessionId : undefined
206
- }
346
+ originalSessionId: isNewSession ? historicalSessionId : undefined,
347
+ },
207
348
  });
208
349
  }
209
350
 
@@ -232,11 +373,14 @@ function reattachSession(
232
373
  const worktreePath = ctx.gitDirectories.get(tabId);
233
374
  const worktreeBranch = ctx.gitBranches.get(tabId);
234
375
 
235
- // Fast path: the web already has local state (via Zustand), so just replay
236
- // anything newer than `lastSeenSeq` and tell the client to skip the
237
- // destructive reset in its tabInitialized handler.
238
- if (lastSeenSeq !== undefined) {
239
- replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
376
+ const inflightEta = inflightEtaPayload(session);
377
+ const replay = replayTabEventsSince(ctx, ws, tabId, lastSeenSeq);
378
+ const mode = decideRecoveryMode(replay, lastSeenSeq);
379
+
380
+ // Fast path: the web already has local state (via Zustand) AND the replay
381
+ // covered the gap cleanly — just replay the buffered events and tell the
382
+ // client to skip the destructive reset in its tabInitialized handler.
383
+ if (mode === 'incremental') {
240
384
  ctx.send(ws, {
241
385
  type: 'tabInitialized',
242
386
  tabId,
@@ -245,29 +389,27 @@ function reattachSession(
245
389
  resumedFromSeq: true,
246
390
  isExecuting: session.isExecuting,
247
391
  ...(session.isExecuting && session.executionStartTimestamp ? { executionStartTimestamp: session.executionStartTimestamp } : {}),
392
+ ...inflightEta,
248
393
  ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
249
394
  }
250
395
  });
251
396
  return;
252
397
  }
253
398
 
254
- // Cold-start reattach (no prior seq): send the full snapshot so the web
255
- // can rebuild from scratch.
256
- const outputHistory = buildOutputHistory(session);
257
- const executionEvents = session.isExecuting
258
- ? session.getExecutionEventLog()
259
- : undefined;
260
-
399
+ // Snapshot path: either cold-start reattach (no prior seq) or replay-gap
400
+ // recovery (`hadGap=true`, no events sent). Both want a full snapshot so
401
+ // the web rebuilds from `outputHistory` + `executionEvents`. The
402
+ // `replayGap` flag distinguishes the two for telemetry — the wire
403
+ // payload shape is otherwise identical.
261
404
  ctx.send(ws, {
262
405
  type: 'tabInitialized',
263
406
  tabId,
264
- data: {
265
- ...session.getSessionInfo(),
266
- outputHistory,
267
- isExecuting: session.isExecuting,
268
- executionEvents,
269
- ...(session.isExecuting && session.executionStartTimestamp ? { executionStartTimestamp: session.executionStartTimestamp } : {}),
270
- ...(worktreePath ? { worktreePath, worktreeBranch } : {}),
271
- }
407
+ engine: resolveEngineForSession(session),
408
+ data: buildFullSnapshotData(session, {
409
+ worktreePath,
410
+ worktreeBranch,
411
+ engineOverride: regTab?.engineOverride,
412
+ replayGap: replay.hadGap,
413
+ }),
272
414
  });
273
415
  }