mstro-app 0.4.52 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +10 -5
  2. package/bin/mstro.js +1 -1
  3. package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
  4. package/dist/server/cli/headless/claude-invoker-stall.js +7 -2
  5. package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
  6. package/dist/server/cli/headless/claude-invoker.js +1 -1
  7. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  8. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  9. package/dist/server/cli/headless/runner.js +63 -67
  10. package/dist/server/cli/headless/runner.js.map +1 -1
  11. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  12. package/dist/server/cli/headless/stall-assessor.js +9 -4
  13. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  14. package/dist/server/cli/improvisation-history-store.d.ts +16 -0
  15. package/dist/server/cli/improvisation-history-store.d.ts.map +1 -0
  16. package/dist/server/cli/improvisation-history-store.js +52 -0
  17. package/dist/server/cli/improvisation-history-store.js.map +1 -0
  18. package/dist/server/cli/improvisation-movements.d.ts +31 -0
  19. package/dist/server/cli/improvisation-movements.d.ts.map +1 -0
  20. package/dist/server/cli/improvisation-movements.js +93 -0
  21. package/dist/server/cli/improvisation-movements.js.map +1 -0
  22. package/dist/server/cli/improvisation-output-queue.d.ts +13 -0
  23. package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -0
  24. package/dist/server/cli/improvisation-output-queue.js +40 -0
  25. package/dist/server/cli/improvisation-output-queue.js.map +1 -0
  26. package/dist/server/cli/improvisation-retry.d.ts +21 -51
  27. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  28. package/dist/server/cli/improvisation-retry.js +18 -433
  29. package/dist/server/cli/improvisation-retry.js.map +1 -1
  30. package/dist/server/cli/improvisation-session-manager.d.ts +10 -8
  31. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  32. package/dist/server/cli/improvisation-session-manager.js +53 -148
  33. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  34. package/dist/server/cli/retry/retry-best-result.d.ts +4 -0
  35. package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -0
  36. package/dist/server/cli/retry/retry-best-result.js +61 -0
  37. package/dist/server/cli/retry/retry-best-result.js.map +1 -0
  38. package/dist/server/cli/retry/retry-context-loss.d.ts +6 -0
  39. package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -0
  40. package/dist/server/cli/retry/retry-context-loss.js +68 -0
  41. package/dist/server/cli/retry/retry-context-loss.js.map +1 -0
  42. package/dist/server/cli/retry/retry-premature-completion.d.ts +5 -0
  43. package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -0
  44. package/dist/server/cli/retry/retry-premature-completion.js +81 -0
  45. package/dist/server/cli/retry/retry-premature-completion.js.map +1 -0
  46. package/dist/server/cli/retry/retry-recovery-strategies.d.ts +13 -0
  47. package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -0
  48. package/dist/server/cli/retry/retry-recovery-strategies.js +166 -0
  49. package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -0
  50. package/dist/server/cli/retry/retry-resume-strategy.d.ts +12 -0
  51. package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -0
  52. package/dist/server/cli/retry/retry-resume-strategy.js +22 -0
  53. package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -0
  54. package/dist/server/cli/retry/retry-runner-factory.d.ts +11 -0
  55. package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -0
  56. package/dist/server/cli/retry/retry-runner-factory.js +60 -0
  57. package/dist/server/cli/retry/retry-runner-factory.js.map +1 -0
  58. package/dist/server/cli/retry/retry-tool-results.d.ts +9 -0
  59. package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -0
  60. package/dist/server/cli/retry/retry-tool-results.js +24 -0
  61. package/dist/server/cli/retry/retry-tool-results.js.map +1 -0
  62. package/dist/server/cli/retry/retry-types.d.ts +30 -0
  63. package/dist/server/cli/retry/retry-types.d.ts.map +1 -0
  64. package/dist/server/cli/retry/retry-types.js +4 -0
  65. package/dist/server/cli/retry/retry-types.js.map +1 -0
  66. package/dist/server/index.js +21 -109
  67. package/dist/server/index.js.map +1 -1
  68. package/dist/server/server-setup.d.ts +16 -1
  69. package/dist/server/server-setup.d.ts.map +1 -1
  70. package/dist/server/server-setup.js +107 -0
  71. package/dist/server/server-setup.js.map +1 -1
  72. package/dist/server/services/plan/board-config.d.ts +21 -0
  73. package/dist/server/services/plan/board-config.d.ts.map +1 -0
  74. package/dist/server/services/plan/board-config.js +112 -0
  75. package/dist/server/services/plan/board-config.js.map +1 -0
  76. package/dist/server/services/plan/composer.d.ts +1 -1
  77. package/dist/server/services/plan/composer.d.ts.map +1 -1
  78. package/dist/server/services/plan/composer.js +7 -5
  79. package/dist/server/services/plan/composer.js.map +1 -1
  80. package/dist/server/services/plan/executor.d.ts +48 -48
  81. package/dist/server/services/plan/executor.d.ts.map +1 -1
  82. package/dist/server/services/plan/executor.js +157 -455
  83. package/dist/server/services/plan/executor.js.map +1 -1
  84. package/dist/server/services/plan/issue-loader.d.ts +16 -0
  85. package/dist/server/services/plan/issue-loader.d.ts.map +1 -0
  86. package/dist/server/services/plan/issue-loader.js +46 -0
  87. package/dist/server/services/plan/issue-loader.js.map +1 -0
  88. package/dist/server/services/plan/issue-writer.d.ts +34 -0
  89. package/dist/server/services/plan/issue-writer.d.ts.map +1 -0
  90. package/dist/server/services/plan/issue-writer.js +110 -0
  91. package/dist/server/services/plan/issue-writer.js.map +1 -0
  92. package/dist/server/services/plan/output-manager.d.ts.map +1 -1
  93. package/dist/server/services/plan/output-manager.js +2 -1
  94. package/dist/server/services/plan/output-manager.js.map +1 -1
  95. package/dist/server/services/plan/progress-log.d.ts +11 -0
  96. package/dist/server/services/plan/progress-log.d.ts.map +1 -0
  97. package/dist/server/services/plan/progress-log.js +81 -0
  98. package/dist/server/services/plan/progress-log.js.map +1 -0
  99. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
  100. package/dist/server/services/plan/prompt-builder.js +48 -31
  101. package/dist/server/services/plan/prompt-builder.js.map +1 -1
  102. package/dist/server/services/plan/readiness-planner.d.ts +15 -0
  103. package/dist/server/services/plan/readiness-planner.d.ts.map +1 -0
  104. package/dist/server/services/plan/readiness-planner.js +41 -0
  105. package/dist/server/services/plan/readiness-planner.js.map +1 -0
  106. package/dist/server/services/plan/review-gate.d.ts +31 -0
  107. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  108. package/dist/server/services/plan/review-gate.js +52 -2
  109. package/dist/server/services/plan/review-gate.js.map +1 -1
  110. package/dist/server/services/platform.d.ts +56 -0
  111. package/dist/server/services/platform.d.ts.map +1 -1
  112. package/dist/server/services/platform.js +154 -52
  113. package/dist/server/services/platform.js.map +1 -1
  114. package/dist/server/services/websocket/file-download-handler.d.ts +17 -0
  115. package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -0
  116. package/dist/server/services/websocket/file-download-handler.js +165 -0
  117. package/dist/server/services/websocket/file-download-handler.js.map +1 -0
  118. package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
  119. package/dist/server/services/websocket/git-worktree-handlers.js +28 -2
  120. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  121. package/dist/server/services/websocket/handler-context.d.ts +15 -0
  122. package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
  123. package/dist/server/services/websocket/handler.d.ts +7 -0
  124. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  125. package/dist/server/services/websocket/handler.js +73 -11
  126. package/dist/server/services/websocket/handler.js.map +1 -1
  127. package/dist/server/services/websocket/msg-id-tracker.d.ts +21 -0
  128. package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -0
  129. package/dist/server/services/websocket/msg-id-tracker.js +77 -0
  130. package/dist/server/services/websocket/msg-id-tracker.js.map +1 -0
  131. package/dist/server/services/websocket/quality-handlers.js +15 -3
  132. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  133. package/dist/server/services/websocket/quality-review-agent.js +2 -2
  134. package/dist/server/services/websocket/session-handlers.d.ts +48 -2
  135. package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
  136. package/dist/server/services/websocket/session-handlers.js +204 -65
  137. package/dist/server/services/websocket/session-handlers.js.map +1 -1
  138. package/dist/server/services/websocket/session-initialization.d.ts +2 -2
  139. package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
  140. package/dist/server/services/websocket/session-initialization.js +75 -17
  141. package/dist/server/services/websocket/session-initialization.js.map +1 -1
  142. package/dist/server/services/websocket/session-registry.d.ts +29 -1
  143. package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
  144. package/dist/server/services/websocket/session-registry.js +53 -4
  145. package/dist/server/services/websocket/session-registry.js.map +1 -1
  146. package/dist/server/services/websocket/tab-broadcast.d.ts +24 -0
  147. package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -0
  148. package/dist/server/services/websocket/tab-broadcast.js +13 -0
  149. package/dist/server/services/websocket/tab-broadcast.js.map +1 -0
  150. package/dist/server/services/websocket/tab-event-buffer.d.ts +103 -0
  151. package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -0
  152. package/dist/server/services/websocket/tab-event-buffer.js +107 -0
  153. package/dist/server/services/websocket/tab-event-buffer.js.map +1 -0
  154. package/dist/server/services/websocket/tab-event-replay.d.ts +20 -0
  155. package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -0
  156. package/dist/server/services/websocket/tab-event-replay.js +21 -0
  157. package/dist/server/services/websocket/tab-event-replay.js.map +1 -0
  158. package/dist/server/services/websocket/tab-handlers.d.ts +0 -1
  159. package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
  160. package/dist/server/services/websocket/tab-handlers.js +2 -9
  161. package/dist/server/services/websocket/tab-handlers.js.map +1 -1
  162. package/dist/server/services/websocket/types.d.ts +15 -6
  163. package/dist/server/services/websocket/types.d.ts.map +1 -1
  164. package/dist/server/services/websocket/types.js +6 -4
  165. package/dist/server/services/websocket/types.js.map +1 -1
  166. package/package.json +1 -1
  167. package/server/README.md +1 -1
  168. package/server/cli/headless/claude-invoker-stall.ts +7 -2
  169. package/server/cli/headless/claude-invoker.ts +1 -1
  170. package/server/cli/headless/runner.ts +67 -72
  171. package/server/cli/headless/stall-assessor.ts +9 -4
  172. package/server/cli/headless/types.ts +1 -1
  173. package/server/cli/improvisation-history-store.ts +62 -0
  174. package/server/cli/improvisation-movements.ts +120 -0
  175. package/server/cli/improvisation-output-queue.ts +42 -0
  176. package/server/cli/improvisation-retry.ts +25 -600
  177. package/server/cli/improvisation-session-manager.ts +74 -160
  178. package/server/cli/retry/retry-best-result.ts +70 -0
  179. package/server/cli/retry/retry-context-loss.ts +87 -0
  180. package/server/cli/retry/retry-premature-completion.ts +113 -0
  181. package/server/cli/retry/retry-recovery-strategies.ts +247 -0
  182. package/server/cli/retry/retry-resume-strategy.ts +33 -0
  183. package/server/cli/retry/retry-runner-factory.ts +70 -0
  184. package/server/cli/retry/retry-tool-results.ts +31 -0
  185. package/server/cli/retry/retry-types.ts +32 -0
  186. package/server/index.ts +37 -123
  187. package/server/server-setup.ts +126 -1
  188. package/server/services/plan/agents/assess-stall.md +11 -4
  189. package/server/services/plan/board-config.ts +122 -0
  190. package/server/services/plan/composer.ts +7 -5
  191. package/server/services/plan/executor.ts +214 -467
  192. package/server/services/plan/issue-loader.ts +64 -0
  193. package/server/services/plan/issue-writer.ts +137 -0
  194. package/server/services/plan/output-manager.ts +2 -1
  195. package/server/services/plan/progress-log.ts +92 -0
  196. package/server/services/plan/prompt-builder.ts +73 -35
  197. package/server/services/plan/readiness-planner.ts +50 -0
  198. package/server/services/plan/review-gate.ts +102 -2
  199. package/server/services/platform.ts +163 -58
  200. package/server/services/websocket/file-download-handler.ts +191 -0
  201. package/server/services/websocket/git-worktree-handlers.ts +29 -2
  202. package/server/services/websocket/handler-context.ts +15 -0
  203. package/server/services/websocket/handler.ts +76 -12
  204. package/server/services/websocket/msg-id-tracker.ts +84 -0
  205. package/server/services/websocket/quality-handlers.ts +16 -3
  206. package/server/services/websocket/quality-review-agent.ts +2 -2
  207. package/server/services/websocket/session-handlers.ts +213 -68
  208. package/server/services/websocket/session-initialization.ts +83 -19
  209. package/server/services/websocket/session-registry.ts +61 -4
  210. package/server/services/websocket/tab-broadcast.ts +38 -0
  211. package/server/services/websocket/tab-event-buffer.ts +159 -0
  212. package/server/services/websocket/tab-event-replay.ts +42 -0
  213. package/server/services/websocket/tab-handlers.ts +2 -9
  214. package/server/services/websocket/types.ts +17 -4
@@ -9,8 +9,13 @@
9
9
  */
10
10
 
11
11
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
12
+ import type { IncomingMessage } from 'node:http'
12
13
  import { basename, join } from 'node:path'
13
- import type { WebSocket as NodeWebSocket } from 'ws'
14
+ import type { WebSocket as NodeWebSocket, WebSocketServer } from 'ws'
15
+ import type { AuthService } from './services/auth.js'
16
+ import { PlatformConnection } from './services/platform.js'
17
+ import { captureException } from './services/sentry.js'
18
+ import type { WebSocketImproviseHandler } from './services/websocket/index.js'
14
19
  import type { WSContext } from './services/websocket/types.js'
15
20
 
16
21
  /**
@@ -112,3 +117,123 @@ export function createPlatformRelayContext(
112
117
  _isPlatformRelay: true
113
118
  } as WSContext
114
119
  }
120
+
121
+ /** Strip the privileged `_permission` field from inbound local messages. */
122
+ function sanitizeLocalMessage(raw: Buffer | string): string {
123
+ const message = typeof raw === 'string' ? raw : raw.toString('utf-8')
124
+ if (!message.includes('_permission')) return message
125
+ try {
126
+ const parsed = JSON.parse(message)
127
+ if ('_permission' in parsed) {
128
+ delete parsed._permission
129
+ return JSON.stringify(parsed)
130
+ }
131
+ } catch { /* not JSON — pass through */ }
132
+ return message
133
+ }
134
+
135
+ /** Attach the local WebSocket connection handler to the WebSocketServer. */
136
+ export function attachLocalWebSocketRouting(opts: {
137
+ wss: WebSocketServer
138
+ port: number
139
+ workingDir: string
140
+ authService: AuthService
141
+ wsHandler: WebSocketImproviseHandler
142
+ }): void {
143
+ const { wss, port, workingDir, authService, wsHandler } = opts
144
+
145
+ wss.on('connection', (ws: NodeWebSocket, req: IncomingMessage) => {
146
+ const url = new URL(req.url || '/', `http://localhost:${port}`)
147
+ if (url.pathname !== '/ws') {
148
+ ws.close(1008, 'Invalid WebSocket path')
149
+ return
150
+ }
151
+
152
+ const wsToken = url.searchParams.get('token')
153
+ if (!wsToken || !authService.validateLocalToken(wsToken)) {
154
+ ws.close(4001, 'Unauthorized')
155
+ return
156
+ }
157
+
158
+ const wrappedWs = wrapWebSocket(ws, workingDir)
159
+ wsHandler.handleConnection(wrappedWs, workingDir)
160
+
161
+ ws.on('message', (data: Buffer | string) => {
162
+ wsHandler.handleMessage(wrappedWs, sanitizeLocalMessage(data), workingDir)
163
+ })
164
+ ws.on('close', () => wsHandler.handleClose(wrappedWs))
165
+ ws.on('error', (error: Error) => {
166
+ console.error('[WebSocket] Error:', error)
167
+ captureException(error, { context: 'websocket.connection' })
168
+ })
169
+ })
170
+ }
171
+
172
+ /** Connect to the platform relay and wire up message bridging to the local wsHandler. */
173
+ export function createPlatformRelay(workingDir: string, wsHandler: WebSocketImproviseHandler): PlatformConnection {
174
+ let platformRelayContext: WSContext | null = null
175
+ let pendingRelayMessages: unknown[] = []
176
+
177
+ const platformConnection = new PlatformConnection(workingDir, {
178
+ onConnected: () => {
179
+ console.log(`Connected: https://mstro.app`)
180
+ wsHandler.setUsageReporter((report) => {
181
+ platformConnection.send({ type: 'reportUsage', data: report })
182
+ })
183
+ },
184
+ onDisconnected: () => {
185
+ if (platformRelayContext) {
186
+ wsHandler.handleClose(platformRelayContext)
187
+ platformRelayContext = null
188
+ }
189
+ pendingRelayMessages = []
190
+ },
191
+ onWebConnected: () => {
192
+ if (platformRelayContext) {
193
+ wsHandler.handleClose(platformRelayContext)
194
+ }
195
+ platformRelayContext = createPlatformRelayContext(
196
+ (message) => platformConnection.send(message),
197
+ workingDir
198
+ )
199
+ wsHandler.handleConnection(platformRelayContext, workingDir)
200
+ if (pendingRelayMessages.length > 0) {
201
+ for (const message of pendingRelayMessages) {
202
+ wsHandler.handleMessage(platformRelayContext, JSON.stringify(message), workingDir)
203
+ }
204
+ pendingRelayMessages = []
205
+ }
206
+ },
207
+ onWebDisconnected: () => {
208
+ if (platformRelayContext) {
209
+ wsHandler.handleClose(platformRelayContext)
210
+ platformRelayContext = null
211
+ }
212
+ pendingRelayMessages = []
213
+ },
214
+ onRelayedMessage: (message) => {
215
+ if (platformRelayContext) {
216
+ wsHandler.handleMessage(platformRelayContext, JSON.stringify(message), workingDir)
217
+ } else {
218
+ // Cap pending messages to prevent unbounded memory growth while disconnected
219
+ if (pendingRelayMessages.length < 100) {
220
+ pendingRelayMessages.push(message)
221
+ }
222
+ }
223
+ }
224
+ })
225
+ platformConnection.connect()
226
+ return platformConnection
227
+ }
228
+
229
+ /** Install process-level error handlers that capture to Sentry. */
230
+ export function registerProcessErrorHandlers(): void {
231
+ process.on('uncaughtException', (err) => {
232
+ console.error('[Server] Uncaught exception:', err)
233
+ captureException(err, { context: 'uncaughtException' })
234
+ })
235
+ process.on('unhandledRejection', (reason) => {
236
+ console.error('[Server] Unhandled rejection:', reason)
237
+ captureException(reason instanceof Error ? reason : new Error(String(reason)), { context: 'unhandledRejection' })
238
+ })
239
+ }
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: assess-stall
3
- description: "Process health monitor that determines if a Claude Code subprocess is working or stalled based on silence duration, tool activity, and task context. Internal Haiku assessment."
3
+ description: "Process health monitor that determines if a Claude Code subprocess is working or stalled based on silence duration, total elapsed runtime, tool activity, and task complexity. Internal Haiku assessment."
4
4
  user-invocable: false
5
5
  ---
6
6
 
7
- You are a process health monitor. A Claude Code subprocess has been silent (no stdout) and you must determine if it is working or stalled.
7
+ You are a process health monitor. A Claude Code subprocess has gone silent (no stdout) and you must determine if it is working or stalled.
8
8
 
9
9
  Silent for: {{silenceMin}} minutes
10
10
  Total runtime: {{totalMin}} minutes
@@ -15,7 +15,14 @@ Total tool calls this session: {{totalToolCalls}}
15
15
  {{tokenLine}}
16
16
  Task being executed: {{promptPreview}}
17
17
 
18
+ Weigh BOTH silence and total runtime against task complexity.
19
+
20
+ - Simple tasks (single Read/Write, one-liner edit, `ls` a directory, trivial greps) should finish in a minute or two. If total runtime has already far exceeded what the task should need, verdict STALLED even if silence is short — something has clearly gone wrong.
21
+ - Complex tasks (agent teams with subagents, multi-step migrations, large refactors, dependency installs, board implementations) can legitimately run for hours. Extend generously when pending tool activity, subagents, or the prompt itself justify a long run.
22
+ - Recent token activity (see token line above) = process is alive and streaming; strongly favor WORKING.
23
+ - Absence of any tool calls + long runtime + no token activity = strong STALLED signal.
24
+
18
25
  Respond in EXACTLY this format (3 lines, no extra text):
19
26
  VERDICT: WORKING or STALLED
20
- MINUTES: <number 5-30, only if WORKING, how many more minutes to allow>
21
- REASON: <brief one-line explanation>
27
+ MINUTES: <integer 5-180, only if WORKING, how many more minutes to allow>
28
+ REASON: <brief one-line explanation that references task complexity vs elapsed time>
@@ -0,0 +1,122 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Board configuration helpers: read board.md + workspace.json settings
6
+ * used by the plan executor (max parallel agents, review criteria,
7
+ * active board resolution, board activation/completion).
8
+ */
9
+
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { readFile, writeFile } from 'node:fs/promises';
12
+ import { join } from 'node:path';
13
+ import { replaceFrontMatterField } from './front-matter.js';
14
+
15
+ /** Emits a warning message — caller typically maps this to executor 'output' events. */
16
+ export type WarnFn = (message: string) => void;
17
+
18
+ export const DEFAULT_MAX_PARALLEL_AGENTS = 3;
19
+
20
+ /** Read the board's maxParallelAgents setting, falling back to default. */
21
+ export async function getBoardMaxParallelAgents(
22
+ pmDir: string | null,
23
+ boardId: string | null,
24
+ warn: WarnFn,
25
+ ): Promise<number> {
26
+ if (!pmDir || !boardId) return DEFAULT_MAX_PARALLEL_AGENTS;
27
+ const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
28
+ if (!existsSync(boardMdPath)) return DEFAULT_MAX_PARALLEL_AGENTS;
29
+ try {
30
+ const content = await readFile(boardMdPath, 'utf-8');
31
+ const match = content.match(/^max_parallel_agents:\s*(\d+)/m);
32
+ return match ? Math.max(1, Math.min(Number(match[1]), 10)) : DEFAULT_MAX_PARALLEL_AGENTS;
33
+ } catch (err) {
34
+ warn(`Warning: failed to read board max_parallel_agents: ${errMsg(err)}`);
35
+ return DEFAULT_MAX_PARALLEL_AGENTS;
36
+ }
37
+ }
38
+
39
+ /** Read the board's custom review criteria, if set. */
40
+ export async function getBoardReviewCriteria(
41
+ pmDir: string | null,
42
+ boardId: string | null,
43
+ warn: WarnFn,
44
+ ): Promise<string | undefined> {
45
+ if (!pmDir || !boardId) return undefined;
46
+ const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
47
+ if (!existsSync(boardMdPath)) return undefined;
48
+ try {
49
+ const content = await readFile(boardMdPath, 'utf-8');
50
+ const match = content.match(/^review_criteria:\s*"(.+)"/m);
51
+ if (!match) return undefined;
52
+ const raw = match[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
53
+ return raw || undefined;
54
+ } catch (err) {
55
+ warn(`Warning: failed to read board review criteria: ${errMsg(err)}`);
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ /** Read workspace.json to find the currently active board. */
61
+ export function resolveActiveBoardId(pmDir: string | null): string | null {
62
+ if (!pmDir) return null;
63
+ try {
64
+ const workspacePath = join(pmDir, 'workspace.json');
65
+ if (!existsSync(workspacePath)) return null;
66
+ const workspace = JSON.parse(readFileSync(workspacePath, 'utf-8'));
67
+ return workspace.activeBoardId ?? null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /** Resolve the active board's directory path. */
74
+ export function resolveBoardDir(pmDir: string | null, boardId: string | null): string | null {
75
+ if (!pmDir) return null;
76
+ const effective = boardId ?? resolveActiveBoardId(pmDir);
77
+ if (!effective) return null;
78
+ const boardDir = join(pmDir, 'boards', effective);
79
+ return existsSync(boardDir) ? boardDir : null;
80
+ }
81
+
82
+ /** Activate a draft board by updating its status in board.md. */
83
+ export async function activateBoard(pmDir: string, boardId: string, warn: WarnFn): Promise<void> {
84
+ const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
85
+ if (!existsSync(boardMdPath)) return;
86
+ try {
87
+ const content = await readFile(boardMdPath, 'utf-8');
88
+ await writeFile(boardMdPath, replaceFrontMatterField(content, 'status', 'active'), 'utf-8');
89
+ } catch (err) {
90
+ warn(`Warning: failed to activate board ${boardId}: ${errMsg(err)}`);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Mark a board as completed iff all its issues are `done` or `cancelled`.
96
+ * No-op when the board is not fully complete.
97
+ */
98
+ export async function tryCompleteBoardIfDone(
99
+ pmDir: string,
100
+ boardId: string,
101
+ issues: { status: string }[],
102
+ warn: WarnFn,
103
+ ): Promise<void> {
104
+ const allDone = issues.length > 0 && issues.every(i => i.status === 'done' || i.status === 'cancelled');
105
+ if (!allDone) return;
106
+
107
+ const boardMdPath = join(pmDir, 'boards', boardId, 'board.md');
108
+ if (!existsSync(boardMdPath)) return;
109
+
110
+ try {
111
+ let content = await readFile(boardMdPath, 'utf-8');
112
+ content = replaceFrontMatterField(content, 'status', 'completed');
113
+ content = replaceFrontMatterField(content, 'completed_at', `"${new Date().toISOString()}"`);
114
+ await writeFile(boardMdPath, content, 'utf-8');
115
+ } catch (err) {
116
+ warn(`Warning: failed to mark board ${boardId} as completed: ${errMsg(err)}`);
117
+ }
118
+ }
119
+
120
+ function errMsg(err: unknown): string {
121
+ return err instanceof Error ? err.message : String(err);
122
+ }
@@ -156,7 +156,7 @@ Backlog directory: ${pmDir}/boards/${effectiveBoardId}/backlog/\n`;
156
156
 
157
157
  export async function handlePlanPrompt(
158
158
  ctx: HandlerContext,
159
- ws: WSContext,
159
+ _ws: WSContext,
160
160
  userPrompt: string,
161
161
  workingDir: string,
162
162
  boardId?: string,
@@ -301,11 +301,13 @@ User request: ${userPrompt}`;
301
301
  policy: 'STANDARD',
302
302
  stallWarningMs: 300_000,
303
303
  stallKillMs: 900_000,
304
- stallHardCapMs: 1_800_000,
304
+ stallHardCapMs: 7_200_000,
305
305
  verbose: true,
306
306
  imageAttachments,
307
307
  outputCallback: (text: string) => {
308
- ctx.send(ws, {
308
+ // Broadcast (not ctx.send) so the stream survives a relay reconnect
309
+ // and reaches every paired web client; mirrors session-handlers.ts.
310
+ ctx.broadcastToAll({
309
311
  type: 'planPromptStreaming',
310
312
  data: { token: text, boardId: streamBoardId },
311
313
  });
@@ -338,7 +340,7 @@ User request: ${userPrompt}`;
338
340
  data: { message: 'Finalizing project plan...', boardId: streamBoardId },
339
341
  });
340
342
 
341
- ctx.send(ws, {
343
+ ctx.broadcastToAll({
342
344
  type: 'planPromptResponse',
343
345
  data: {
344
346
  response: result.completed ? 'Prompt executed successfully.' : (result.error || 'Unknown error'),
@@ -354,7 +356,7 @@ User request: ${userPrompt}`;
354
356
  ctx.broadcastToAll({ type: 'planStateUpdated', data: updatedState });
355
357
  }
356
358
  } catch (error) {
357
- ctx.send(ws, {
359
+ ctx.broadcastToAll({
358
360
  type: 'planError',
359
361
  data: { error: error instanceof Error ? error.message : String(error), boardId: streamBoardId },
360
362
  });