mstro-app 0.5.1 → 0.5.6

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 (283) hide show
  1. package/PRIVACY.md +9 -9
  2. package/README.md +71 -28
  3. package/bin/commands/config.js +1 -1
  4. package/bin/mstro.js +55 -4
  5. package/dist/server/cli/eta-estimator.d.ts +55 -0
  6. package/dist/server/cli/eta-estimator.d.ts.map +1 -0
  7. package/dist/server/cli/eta-estimator.js +222 -0
  8. package/dist/server/cli/eta-estimator.js.map +1 -0
  9. package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
  10. package/dist/server/cli/headless/claude-invoker-process.js +9 -1
  11. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
  12. package/dist/server/cli/headless/mcp-config.d.ts +22 -5
  13. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
  14. package/dist/server/cli/headless/mcp-config.js +7 -5
  15. package/dist/server/cli/headless/mcp-config.js.map +1 -1
  16. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  17. package/dist/server/cli/headless/runner.js +19 -0
  18. package/dist/server/cli/headless/runner.js.map +1 -1
  19. package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
  20. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  21. package/dist/server/cli/headless/stall-assessor.js +64 -9
  22. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  23. package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
  24. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  25. package/dist/server/cli/headless/tool-watchdog.js +19 -12
  26. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  27. package/dist/server/cli/headless/types.d.ts +16 -1
  28. package/dist/server/cli/headless/types.d.ts.map +1 -1
  29. package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
  30. package/dist/server/cli/improvisation-history-store.js +5 -1
  31. package/dist/server/cli/improvisation-history-store.js.map +1 -1
  32. package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
  33. package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
  34. package/dist/server/cli/improvisation-output-queue.js +30 -7
  35. package/dist/server/cli/improvisation-output-queue.js.map +1 -1
  36. package/dist/server/cli/improvisation-session-manager.d.ts +35 -0
  37. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  38. package/dist/server/cli/improvisation-session-manager.js +58 -1
  39. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  40. package/dist/server/cli/improvisation-types.d.ts +9 -0
  41. package/dist/server/cli/improvisation-types.d.ts.map +1 -1
  42. package/dist/server/cli/improvisation-types.js.map +1 -1
  43. package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -1
  44. package/dist/server/cli/retry/retry-runner-factory.js +1 -0
  45. package/dist/server/cli/retry/retry-runner-factory.js.map +1 -1
  46. package/dist/server/engines/EngineEvent.d.ts +126 -0
  47. package/dist/server/engines/EngineEvent.d.ts.map +1 -0
  48. package/dist/server/engines/EngineEvent.js +11 -0
  49. package/dist/server/engines/EngineEvent.js.map +1 -0
  50. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
  51. package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
  52. package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
  53. package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
  54. package/dist/server/engines/factory.d.ts +21 -0
  55. package/dist/server/engines/factory.d.ts.map +1 -0
  56. package/dist/server/engines/factory.js +152 -0
  57. package/dist/server/engines/factory.js.map +1 -0
  58. package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
  59. package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
  60. package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
  61. package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
  62. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
  63. package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
  64. package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
  65. package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
  66. package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
  67. package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
  68. package/dist/server/engines/opencode/model-catalog.js +141 -0
  69. package/dist/server/engines/opencode/model-catalog.js.map +1 -0
  70. package/dist/server/engines/types.d.ts +146 -0
  71. package/dist/server/engines/types.d.ts.map +1 -0
  72. package/dist/server/engines/types.js +4 -0
  73. package/dist/server/engines/types.js.map +1 -0
  74. package/dist/server/index.js +9 -2
  75. package/dist/server/index.js.map +1 -1
  76. package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
  77. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
  78. package/dist/server/mcp/bouncer-haiku.js +8 -124
  79. package/dist/server/mcp/bouncer-haiku.js.map +1 -1
  80. package/dist/server/mcp/bouncer-integration.d.ts +45 -0
  81. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  82. package/dist/server/mcp/bouncer-integration.js +69 -5
  83. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  84. package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
  85. package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
  86. package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
  87. package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
  88. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
  89. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
  90. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
  91. package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
  92. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
  93. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
  94. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
  95. package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
  96. package/dist/server/mcp/classifier/factory.d.ts +70 -0
  97. package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
  98. package/dist/server/mcp/classifier/factory.js +155 -0
  99. package/dist/server/mcp/classifier/factory.js.map +1 -0
  100. package/dist/server/mcp/server.js +52 -0
  101. package/dist/server/mcp/server.js.map +1 -1
  102. package/dist/server/routes/index.d.ts +1 -0
  103. package/dist/server/routes/index.d.ts.map +1 -1
  104. package/dist/server/routes/index.js +1 -0
  105. package/dist/server/routes/index.js.map +1 -1
  106. package/dist/server/routes/internal.d.ts +16 -0
  107. package/dist/server/routes/internal.d.ts.map +1 -0
  108. package/dist/server/routes/internal.js +94 -0
  109. package/dist/server/routes/internal.js.map +1 -0
  110. package/dist/server/services/plan/agent-resolver.d.ts +26 -0
  111. package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
  112. package/dist/server/services/plan/agent-resolver.js +102 -0
  113. package/dist/server/services/plan/agent-resolver.js.map +1 -0
  114. package/dist/server/services/plan/composer.d.ts.map +1 -1
  115. package/dist/server/services/plan/composer.js +59 -11
  116. package/dist/server/services/plan/composer.js.map +1 -1
  117. package/dist/server/services/plan/executor.d.ts.map +1 -1
  118. package/dist/server/services/plan/executor.js +3 -1
  119. package/dist/server/services/plan/executor.js.map +1 -1
  120. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  121. package/dist/server/services/plan/issue-prompt-builder.js +33 -1
  122. package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
  123. package/dist/server/services/plan/parser-core.d.ts.map +1 -1
  124. package/dist/server/services/plan/parser-core.js +1 -0
  125. package/dist/server/services/plan/parser-core.js.map +1 -1
  126. package/dist/server/services/plan/types.d.ts +1 -0
  127. package/dist/server/services/plan/types.d.ts.map +1 -1
  128. package/dist/server/services/runtime-info.d.ts +3 -0
  129. package/dist/server/services/runtime-info.d.ts.map +1 -0
  130. package/dist/server/services/runtime-info.js +21 -0
  131. package/dist/server/services/runtime-info.js.map +1 -0
  132. package/dist/server/services/settings.d.ts +76 -2
  133. package/dist/server/services/settings.d.ts.map +1 -1
  134. package/dist/server/services/settings.js +127 -4
  135. package/dist/server/services/settings.js.map +1 -1
  136. package/dist/server/services/websocket/ask-user-question-bridge.d.ts +32 -0
  137. package/dist/server/services/websocket/ask-user-question-bridge.d.ts.map +1 -0
  138. package/dist/server/services/websocket/ask-user-question-bridge.js +115 -0
  139. package/dist/server/services/websocket/ask-user-question-bridge.js.map +1 -0
  140. package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
  141. package/dist/server/services/websocket/git-branch-handlers.js +19 -6
  142. package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
  143. package/dist/server/services/websocket/handler.d.ts +25 -1
  144. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  145. package/dist/server/services/websocket/handler.js +84 -2
  146. package/dist/server/services/websocket/handler.js.map +1 -1
  147. package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
  148. package/dist/server/services/websocket/quality-complexity.js +78 -26
  149. package/dist/server/services/websocket/quality-complexity.js.map +1 -1
  150. package/dist/server/services/websocket/quality-eta.d.ts +47 -0
  151. package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
  152. package/dist/server/services/websocket/quality-eta.js +110 -0
  153. package/dist/server/services/websocket/quality-eta.js.map +1 -0
  154. package/dist/server/services/websocket/quality-grading.d.ts +27 -4
  155. package/dist/server/services/websocket/quality-grading.d.ts.map +1 -1
  156. package/dist/server/services/websocket/quality-grading.js +369 -201
  157. package/dist/server/services/websocket/quality-grading.js.map +1 -1
  158. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  159. package/dist/server/services/websocket/quality-handlers.js +145 -7
  160. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  161. package/dist/server/services/websocket/quality-operations.d.ts +34 -0
  162. package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
  163. package/dist/server/services/websocket/quality-operations.js +47 -0
  164. package/dist/server/services/websocket/quality-operations.js.map +1 -0
  165. package/dist/server/services/websocket/quality-persistence.d.ts +9 -0
  166. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
  167. package/dist/server/services/websocket/quality-persistence.js +10 -0
  168. package/dist/server/services/websocket/quality-persistence.js.map +1 -1
  169. package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
  170. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  171. package/dist/server/services/websocket/quality-review-agent.js +105 -56
  172. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  173. package/dist/server/services/websocket/quality-service.d.ts +9 -1
  174. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  175. package/dist/server/services/websocket/quality-service.js +334 -14
  176. package/dist/server/services/websocket/quality-service.js.map +1 -1
  177. package/dist/server/services/websocket/quality-tools.d.ts +21 -0
  178. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
  179. package/dist/server/services/websocket/quality-tools.js +49 -0
  180. package/dist/server/services/websocket/quality-tools.js.map +1 -1
  181. package/dist/server/services/websocket/quality-types.d.ts +35 -2
  182. package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
  183. package/dist/server/services/websocket/quality-types.js +1 -1
  184. package/dist/server/services/websocket/quality-types.js.map +1 -1
  185. package/dist/server/services/websocket/session-handlers.d.ts +3 -1
  186. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  187. package/dist/server/services/websocket/session-handlers.js +60 -9
  188. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  189. package/dist/server/services/websocket/session-history.js +3 -0
  190. package/dist/server/services/websocket/session-history.js.map +1 -1
  191. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  192. package/dist/server/services/websocket/session-initialization.js +158 -42
  193. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  194. package/dist/server/services/websocket/session-registry.d.ts +25 -0
  195. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  196. package/dist/server/services/websocket/session-registry.js +19 -0
  197. package/dist/server/services/websocket/session-registry.js.map +1 -1
  198. package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
  199. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
  200. package/dist/server/services/websocket/settings-handlers.js +35 -4
  201. package/dist/server/services/websocket/settings-handlers.js.map +1 -1
  202. package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
  203. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
  204. package/dist/server/services/websocket/tab-broadcast.js +10 -2
  205. package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
  206. package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
  207. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
  208. package/dist/server/services/websocket/tab-event-buffer.js +138 -12
  209. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
  210. package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
  211. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
  212. package/dist/server/services/websocket/tab-event-replay.js +55 -2
  213. package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
  214. package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
  215. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  216. package/dist/server/services/websocket/tab-handlers.js +47 -2
  217. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  218. package/dist/server/services/websocket/types.d.ts +67 -7
  219. package/dist/server/services/websocket/types.d.ts.map +1 -1
  220. package/dist/server/services/websocket/types.js +12 -6
  221. package/dist/server/services/websocket/types.js.map +1 -1
  222. package/package.json +5 -3
  223. package/server/cli/eta-estimator.ts +249 -0
  224. package/server/cli/headless/claude-invoker-process.ts +9 -1
  225. package/server/cli/headless/mcp-config.ts +30 -5
  226. package/server/cli/headless/runner.ts +21 -0
  227. package/server/cli/headless/stall-assessor.ts +93 -0
  228. package/server/cli/headless/tool-watchdog.ts +21 -0
  229. package/server/cli/headless/types.ts +16 -1
  230. package/server/cli/improvisation-history-store.ts +4 -1
  231. package/server/cli/improvisation-output-queue.ts +29 -7
  232. package/server/cli/improvisation-session-manager.ts +63 -1
  233. package/server/cli/improvisation-types.ts +9 -0
  234. package/server/cli/retry/retry-runner-factory.ts +1 -0
  235. package/server/engines/EngineEvent.ts +156 -0
  236. package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
  237. package/server/engines/factory.ts +176 -0
  238. package/server/engines/opencode/OpenCodeEngine.ts +786 -0
  239. package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
  240. package/server/engines/opencode/model-catalog.ts +217 -0
  241. package/server/engines/types.ts +173 -0
  242. package/server/index.ts +9 -1
  243. package/server/mcp/bouncer-haiku.ts +21 -145
  244. package/server/mcp/bouncer-integration.ts +107 -5
  245. package/server/mcp/classifier/BouncerClassifier.ts +40 -0
  246. package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
  247. package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
  248. package/server/mcp/classifier/factory.ts +195 -0
  249. package/server/mcp/server.ts +57 -0
  250. package/server/routes/index.ts +1 -0
  251. package/server/routes/internal.ts +112 -0
  252. package/server/services/plan/agent-resolver.ts +115 -0
  253. package/server/services/plan/agents/code-review.md +38 -8
  254. package/server/services/plan/composer.ts +63 -11
  255. package/server/services/plan/executor.ts +3 -1
  256. package/server/services/plan/issue-prompt-builder.ts +39 -1
  257. package/server/services/plan/parser-core.ts +1 -0
  258. package/server/services/plan/types.ts +4 -0
  259. package/server/services/runtime-info.ts +24 -0
  260. package/server/services/settings.ts +161 -4
  261. package/server/services/websocket/ask-user-question-bridge.ts +148 -0
  262. package/server/services/websocket/git-branch-handlers.ts +20 -6
  263. package/server/services/websocket/handler.ts +89 -2
  264. package/server/services/websocket/quality-complexity.ts +80 -26
  265. package/server/services/websocket/quality-eta.ts +155 -0
  266. package/server/services/websocket/quality-grading.ts +445 -222
  267. package/server/services/websocket/quality-handlers.ts +153 -7
  268. package/server/services/websocket/quality-operations.ts +72 -0
  269. package/server/services/websocket/quality-persistence.ts +17 -0
  270. package/server/services/websocket/quality-review-agent.ts +154 -64
  271. package/server/services/websocket/quality-service.ts +361 -13
  272. package/server/services/websocket/quality-tools.ts +51 -0
  273. package/server/services/websocket/quality-types.ts +41 -2
  274. package/server/services/websocket/session-handlers.ts +67 -10
  275. package/server/services/websocket/session-history.ts +3 -0
  276. package/server/services/websocket/session-initialization.ts +189 -46
  277. package/server/services/websocket/session-registry.ts +37 -0
  278. package/server/services/websocket/settings-handlers.ts +41 -4
  279. package/server/services/websocket/tab-broadcast.ts +10 -2
  280. package/server/services/websocket/tab-event-buffer.ts +143 -11
  281. package/server/services/websocket/tab-event-replay.ts +70 -3
  282. package/server/services/websocket/tab-handlers.ts +53 -5
  283. package/server/services/websocket/types.ts +85 -7
@@ -16,7 +16,10 @@
16
16
  import { EventEmitter } from 'node:events';
17
17
  import { existsSync, readFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
+ import { createEngine } from '../engines/factory.js';
20
+ import type { EngineId } from '../engines/types.js';
19
21
  import { AnalyticsEvents, trackEvent } from '../services/analytics.js';
22
+ import { type EtaProfile, getEtaProfileCached } from './eta-estimator.js';
20
23
  import { herror } from './headless/headless-logger.js';
21
24
  import { cleanupAttachments, preparePromptAndAttachments } from './improvisation-attachments.js';
22
25
  import {
@@ -55,6 +58,12 @@ export class ImprovisationSessionManager extends EventEmitter {
55
58
  private history: SessionHistory;
56
59
  private currentRunner: import('./headless/index.js').HeadlessRunner | null = null;
57
60
  private options: ImprovisationOptions;
61
+ /**
62
+ * Coding-agent backend identifier. Routed through `createEngine` from
63
+ * `engines/factory.ts` so Epic 3 can swap in OpenCodeEngine by changing
64
+ * this value — the retry loop and headless runner remain unchanged.
65
+ */
66
+ private engineId: EngineId = 'claude-code';
58
67
  private pendingApproval?: {
59
68
  plan: unknown;
60
69
  resolve: (approved: boolean) => void;
@@ -73,6 +82,13 @@ export class ImprovisationSessionManager extends EventEmitter {
73
82
  private _currentUserPrompt: string = '';
74
83
  private _currentSequenceNumber: number = 0;
75
84
  private _hasPersistedToDisk: boolean = false;
85
+ /**
86
+ * Cached duration-quantile profile used by the web "Composing" indicator
87
+ * to render an ETA. Built lazily on first executePrompt and refreshed by
88
+ * the eta-estimator's TTL cache. Null means "not enough history yet" — the
89
+ * web falls back to elapsed-only display.
90
+ */
91
+ private _etaProfile: EtaProfile | null = null;
76
92
 
77
93
  static resumeFromHistory(workingDir: string, historicalSessionId: string, overrides?: Partial<ImprovisationOptions>): ImprovisationSessionManager {
78
94
  const historyDir = join(workingDir, '.mstro', 'history');
@@ -84,6 +100,7 @@ export class ImprovisationSessionManager extends EventEmitter {
84
100
  }
85
101
 
86
102
  const historyData = JSON.parse(readFileSync(historyPath, 'utf-8')) as SessionHistory;
103
+ if (!historyData.engine) historyData.engine = 'claude-code';
87
104
  const manager = new ImprovisationSessionManager({
88
105
  workingDir,
89
106
  sessionId: historyData.sessionId,
@@ -126,6 +143,11 @@ export class ImprovisationSessionManager extends EventEmitter {
126
143
  this.historyPath = paths.historyPath;
127
144
  ensureHistoryDir(this.improviseDir);
128
145
 
146
+ // Validate the engine is available via the factory. Epic 3 will extend
147
+ // `createEngine` to also return OpenCodeEngine; today this asserts that
148
+ // a known engine id was requested and surfaces config errors early.
149
+ createEngine(this.engineId);
150
+
129
151
  this.history = loadHistory(this.historyPath, this.sessionId);
130
152
  // History is persisted lazily on the first `persistHistory` call (see
131
153
  // `executePrompt`). Deferring the initial write keeps the Chat History
@@ -165,7 +187,8 @@ export class ImprovisationSessionManager extends EventEmitter {
165
187
  const sequenceNumber = this.history.movements.length + 1;
166
188
  this._currentUserPrompt = displayPrompt;
167
189
  this._currentSequenceNumber = sequenceNumber;
168
- this.emit('onMovementStart', sequenceNumber, displayPrompt, isAutoContinue);
190
+ await this.refreshEtaProfile();
191
+ this.emit('onMovementStart', sequenceNumber, displayPrompt, isAutoContinue, this._etaProfile);
169
192
  trackEvent(AnalyticsEvents.IMPROVISE_PROMPT_RECEIVED, {
170
193
  prompt_length: userPrompt.length,
171
194
  has_attachments: !!(attachments && attachments.length > 0),
@@ -499,6 +522,22 @@ export class ImprovisationSessionManager extends EventEmitter {
499
522
  });
500
523
  }
501
524
 
525
+ // ========== ETA profile ==========
526
+
527
+ /**
528
+ * Resolve the duration-quantile profile before announcing the movement
529
+ * so the web indicator can render an ETA from t=0. The cache amortizes
530
+ * I/O across prompts; only the very first prompt of a project pays the
531
+ * ~50–100ms scan cost. Failures degrade silently to "no ETA".
532
+ */
533
+ private async refreshEtaProfile(): Promise<void> {
534
+ try {
535
+ this._etaProfile = await getEtaProfileCached(this.improviseDir);
536
+ } catch {
537
+ this._etaProfile = null;
538
+ }
539
+ }
540
+
502
541
  // ========== History I/O ==========
503
542
 
504
543
  private persistHistory(): void {
@@ -588,10 +627,33 @@ export class ImprovisationSessionManager extends EventEmitter {
588
627
  return this._isExecuting;
589
628
  }
590
629
 
630
+ /**
631
+ * Bind this session to a web tab. Used so the headless runner can route
632
+ * AskUserQuestion calls back to that tab's web clients via the bridge.
633
+ * Idempotent and safe to call repeatedly across reconnects.
634
+ */
635
+ setTabId(tabId: string): void {
636
+ this.options.tabId = tabId;
637
+ }
638
+
639
+ /**
640
+ * AI engine that produced this session. Read from SessionHistory (populated
641
+ * on load/creation). Defaults to 'claude-code' if the history record is
642
+ * missing the field (older sessions).
643
+ */
644
+ get engine(): string {
645
+ return this.history.engine || 'claude-code';
646
+ }
647
+
591
648
  get executionStartTimestamp(): number | undefined {
592
649
  return this._executionStartTimestamp;
593
650
  }
594
651
 
652
+ /** Most recently resolved ETA quantile profile, or null if none yet built. */
653
+ get etaProfile(): EtaProfile | null {
654
+ return this._etaProfile;
655
+ }
656
+
595
657
  getExecutionEventLog(): Array<{ type: string; data: unknown; timestamp: number }> {
596
658
  return this.executionEventLog;
597
659
  }
@@ -22,6 +22,13 @@ export interface ImprovisationOptions {
22
22
  * Unsupported levels gracefully fall back to the highest supported level per model.
23
23
  */
24
24
  effortLevel?: string;
25
+ /**
26
+ * Web tab id this session is bound to. Used for AskUserQuestion routing —
27
+ * the MCP bouncer pauses Claude on AskUserQuestion and the bridge fans the
28
+ * questions out to this tab's web clients. Optional: when unset the
29
+ * bouncer falls back to legacy behavior (allow with no answers).
30
+ */
31
+ tabId?: string;
25
32
  }
26
33
 
27
34
  // File attachment for multimodal prompts (images)
@@ -66,6 +73,8 @@ export interface SessionHistory {
66
73
  totalTokens: number;
67
74
  movements: MovementRecord[];
68
75
  claudeSessionId?: string;
76
+ /** AI engine that produced this session (e.g. 'claude-code', 'opencode'). Older histories default to 'claude-code' on read. */
77
+ engine: string;
69
78
  }
70
79
 
71
80
  /** Entry in the retry log for debugging recovery paths */
@@ -31,6 +31,7 @@ export function createExecutionRunner(
31
31
  noColor: session.options.noColor,
32
32
  model: session.options.model,
33
33
  effortLevel: session.options.effortLevel,
34
+ tabId: session.options.tabId,
34
35
  improvisationMode: true,
35
36
  movementNumber: sequenceNumber,
36
37
  continueSession: useResume,
@@ -0,0 +1,156 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * EngineEvent — the engine-agnostic event stream produced by every
6
+ * CodingAgentEngine (see ./types.ts).
7
+ *
8
+ * Contract:
9
+ * - Every event has a `kind` discriminator, a `sessionId` (the engine's own
10
+ * session identifier — Claude Code session id, OpenCode session id, etc.),
11
+ * and a `timestamp` in Unix ms.
12
+ * - Payloads must carry enough information to populate OutputLine in the web
13
+ * client without the consumer knowing which engine produced them. Do not
14
+ * leak engine-specific fields (e.g. Claude tool_use_ids, OpenCode part ids)
15
+ * through typed event fields — keep them in `raw` when needed for debugging.
16
+ * - Ordering is guaranteed within a single session: for a given tool call,
17
+ * `tool.start` precedes `tool.end`; `usage.update` values are monotonic.
18
+ * - `session.idle` marks the end of a turn (assistant finished responding),
19
+ * not the end of the session. Multiple idle events per session are normal.
20
+ * - `engine.error` with `fatal: true` is terminal — the async iterator must
21
+ * complete after emitting it.
22
+ */
23
+
24
+ /** Identifier for which concrete engine produced an event. */
25
+ export type EngineId = 'claude-code' | 'opencode';
26
+
27
+ /** Fields shared by every engine event. */
28
+ interface EngineEventBase {
29
+ /** Engine-reported session id (e.g. Claude Code session id, OpenCode session id). */
30
+ sessionId: string;
31
+ /** Unix epoch milliseconds when the engine observed this event. */
32
+ timestamp: number;
33
+ /** Optional raw payload from the engine for debugging/audit. Must not be used for business logic. */
34
+ raw?: unknown;
35
+ }
36
+
37
+ /** Streaming assistant text (user-visible output). */
38
+ export interface MessageDeltaEvent extends EngineEventBase {
39
+ kind: 'message.delta';
40
+ /** Incremental chunk of assistant-visible text. Consumers concatenate. */
41
+ text: string;
42
+ }
43
+
44
+ /** Streaming assistant thinking/reasoning text (collapsed by default in UI). */
45
+ export interface MessageThinkingEvent extends EngineEventBase {
46
+ kind: 'message.thinking';
47
+ /** Incremental chunk of thinking text. */
48
+ text: string;
49
+ }
50
+
51
+ /** A tool invocation has started. */
52
+ export interface ToolStartEvent extends EngineEventBase {
53
+ kind: 'tool.start';
54
+ /** Engine-agnostic tool call id (unique within the session). */
55
+ toolCallId: string;
56
+ /** Name of the tool (e.g. "Read", "Bash"). */
57
+ toolName: string;
58
+ /**
59
+ * Arguments passed to the tool. May be partial at start — some engines
60
+ * stream arguments. Consumers should treat this as best-effort until
61
+ * `tool.end` arrives with the authoritative input.
62
+ */
63
+ input: Record<string, unknown>;
64
+ }
65
+
66
+ /** A tool invocation has completed (successfully or with an error). */
67
+ export interface ToolEndEvent extends EngineEventBase {
68
+ kind: 'tool.end';
69
+ toolCallId: string;
70
+ toolName: string;
71
+ /** Authoritative tool input as executed. */
72
+ input: Record<string, unknown>;
73
+ /** Serialized tool result (stdout, file contents, JSON, etc.). */
74
+ result: string;
75
+ /** True if the tool returned an error. */
76
+ isError: boolean;
77
+ /** Wall-clock duration in ms between tool.start and tool.end. */
78
+ durationMs: number;
79
+ }
80
+
81
+ /**
82
+ * The engine is asking whether a tool call should proceed. Consumed by
83
+ * the Bouncer in Epic 4 which must resolve the request via the engine's
84
+ * matching permission-response channel.
85
+ */
86
+ export interface PermissionRequestEvent extends EngineEventBase {
87
+ kind: 'permission.request';
88
+ /** Opaque id the engine will expect echoed back in a decision. */
89
+ requestId: string;
90
+ toolName: string;
91
+ /** Tool arguments to be classified. */
92
+ input: Record<string, unknown>;
93
+ /** Engine-provided reason string, if any. */
94
+ reason?: string;
95
+ }
96
+
97
+ /**
98
+ * Session returned to idle — the assistant finished its current turn.
99
+ * Not the end of the session; a new prompt may still be sent.
100
+ */
101
+ export interface SessionIdleEvent extends EngineEventBase {
102
+ kind: 'session.idle';
103
+ /** Engine's stop reason if known (e.g. 'end_turn', 'max_tokens'). */
104
+ stopReason?: string;
105
+ }
106
+
107
+ /**
108
+ * Running token counts. Values are cumulative across the session
109
+ * (not per-turn) and must be monotonically non-decreasing.
110
+ */
111
+ export interface UsageUpdateEvent extends EngineEventBase {
112
+ kind: 'usage.update';
113
+ inputTokens: number;
114
+ outputTokens: number;
115
+ cacheCreationTokens?: number;
116
+ cacheReadTokens?: number;
117
+ }
118
+
119
+ /**
120
+ * An engine-level error occurred. With `fatal: true`, the session is
121
+ * unrecoverable and the async iterator completes after this event.
122
+ */
123
+ export interface EngineErrorEvent extends EngineEventBase {
124
+ kind: 'engine.error';
125
+ /** Short error code for UI mapping (see ClaudeErrorCode in web/src/types/output.ts). */
126
+ code: string;
127
+ /** Human-readable message. */
128
+ message: string;
129
+ /** True if the session is unrecoverable and should be torn down. */
130
+ fatal: boolean;
131
+ }
132
+
133
+ /** Discriminated union of every event a CodingAgentEngine may emit. */
134
+ export type EngineEvent =
135
+ | MessageDeltaEvent
136
+ | MessageThinkingEvent
137
+ | ToolStartEvent
138
+ | ToolEndEvent
139
+ | PermissionRequestEvent
140
+ | SessionIdleEvent
141
+ | UsageUpdateEvent
142
+ | EngineErrorEvent;
143
+
144
+ /** Narrow helper — returns true for events that carry user-visible text. */
145
+ export function isMessageEvent(
146
+ event: EngineEvent,
147
+ ): event is MessageDeltaEvent | MessageThinkingEvent {
148
+ return event.kind === 'message.delta' || event.kind === 'message.thinking';
149
+ }
150
+
151
+ /** Narrow helper — returns true for the tool lifecycle events. */
152
+ export function isToolEvent(
153
+ event: EngineEvent,
154
+ ): event is ToolStartEvent | ToolEndEvent {
155
+ return event.kind === 'tool.start' || event.kind === 'tool.end';
156
+ }