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.
- package/README.md +10 -5
- package/bin/mstro.js +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.js +7 -2
- package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +1 -1
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +63 -67
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +9 -4
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts +16 -0
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -0
- package/dist/server/cli/improvisation-history-store.js +52 -0
- package/dist/server/cli/improvisation-history-store.js.map +1 -0
- package/dist/server/cli/improvisation-movements.d.ts +31 -0
- package/dist/server/cli/improvisation-movements.d.ts.map +1 -0
- package/dist/server/cli/improvisation-movements.js +93 -0
- package/dist/server/cli/improvisation-movements.js.map +1 -0
- package/dist/server/cli/improvisation-output-queue.d.ts +13 -0
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -0
- package/dist/server/cli/improvisation-output-queue.js +40 -0
- package/dist/server/cli/improvisation-output-queue.js.map +1 -0
- package/dist/server/cli/improvisation-retry.d.ts +21 -51
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +18 -433
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +10 -8
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +53 -148
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/retry/retry-best-result.d.ts +4 -0
- package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-best-result.js +61 -0
- package/dist/server/cli/retry/retry-best-result.js.map +1 -0
- package/dist/server/cli/retry/retry-context-loss.d.ts +6 -0
- package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-context-loss.js +68 -0
- package/dist/server/cli/retry/retry-context-loss.js.map +1 -0
- package/dist/server/cli/retry/retry-premature-completion.d.ts +5 -0
- package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-premature-completion.js +81 -0
- package/dist/server/cli/retry/retry-premature-completion.js.map +1 -0
- package/dist/server/cli/retry/retry-recovery-strategies.d.ts +13 -0
- package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-recovery-strategies.js +166 -0
- package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -0
- package/dist/server/cli/retry/retry-resume-strategy.d.ts +12 -0
- package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-resume-strategy.js +22 -0
- package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -0
- package/dist/server/cli/retry/retry-runner-factory.d.ts +11 -0
- package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-runner-factory.js +60 -0
- package/dist/server/cli/retry/retry-runner-factory.js.map +1 -0
- package/dist/server/cli/retry/retry-tool-results.d.ts +9 -0
- package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-tool-results.js +24 -0
- package/dist/server/cli/retry/retry-tool-results.js.map +1 -0
- package/dist/server/cli/retry/retry-types.d.ts +30 -0
- package/dist/server/cli/retry/retry-types.d.ts.map +1 -0
- package/dist/server/cli/retry/retry-types.js +4 -0
- package/dist/server/cli/retry/retry-types.js.map +1 -0
- package/dist/server/index.js +21 -109
- package/dist/server/index.js.map +1 -1
- package/dist/server/server-setup.d.ts +16 -1
- package/dist/server/server-setup.d.ts.map +1 -1
- package/dist/server/server-setup.js +107 -0
- package/dist/server/server-setup.js.map +1 -1
- package/dist/server/services/plan/board-config.d.ts +21 -0
- package/dist/server/services/plan/board-config.d.ts.map +1 -0
- package/dist/server/services/plan/board-config.js +112 -0
- package/dist/server/services/plan/board-config.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +7 -5
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +48 -48
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +157 -455
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/issue-loader.d.ts +16 -0
- package/dist/server/services/plan/issue-loader.d.ts.map +1 -0
- package/dist/server/services/plan/issue-loader.js +46 -0
- package/dist/server/services/plan/issue-loader.js.map +1 -0
- package/dist/server/services/plan/issue-writer.d.ts +34 -0
- package/dist/server/services/plan/issue-writer.d.ts.map +1 -0
- package/dist/server/services/plan/issue-writer.js +110 -0
- package/dist/server/services/plan/issue-writer.js.map +1 -0
- package/dist/server/services/plan/output-manager.d.ts.map +1 -1
- package/dist/server/services/plan/output-manager.js +2 -1
- package/dist/server/services/plan/output-manager.js.map +1 -1
- package/dist/server/services/plan/progress-log.d.ts +11 -0
- package/dist/server/services/plan/progress-log.d.ts.map +1 -0
- package/dist/server/services/plan/progress-log.js +81 -0
- package/dist/server/services/plan/progress-log.js.map +1 -0
- package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/prompt-builder.js +48 -31
- package/dist/server/services/plan/prompt-builder.js.map +1 -1
- package/dist/server/services/plan/readiness-planner.d.ts +15 -0
- package/dist/server/services/plan/readiness-planner.d.ts.map +1 -0
- package/dist/server/services/plan/readiness-planner.js +41 -0
- package/dist/server/services/plan/readiness-planner.js.map +1 -0
- package/dist/server/services/plan/review-gate.d.ts +31 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +52 -2
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/platform.d.ts +56 -0
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +154 -52
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/websocket/file-download-handler.d.ts +17 -0
- package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -0
- package/dist/server/services/websocket/file-download-handler.js +165 -0
- package/dist/server/services/websocket/file-download-handler.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +28 -2
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +15 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +7 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +73 -11
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/msg-id-tracker.d.ts +21 -0
- package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -0
- package/dist/server/services/websocket/msg-id-tracker.js +77 -0
- package/dist/server/services/websocket/msg-id-tracker.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.js +15 -3
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +2 -2
- package/dist/server/services/websocket/session-handlers.d.ts +48 -2
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +204 -65
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts +2 -2
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +75 -17
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +29 -1
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +53 -4
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +24 -0
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-broadcast.js +13 -0
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -0
- package/dist/server/services/websocket/tab-event-buffer.d.ts +103 -0
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-event-buffer.js +107 -0
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -0
- package/dist/server/services/websocket/tab-event-replay.d.ts +20 -0
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -0
- package/dist/server/services/websocket/tab-event-replay.js +21 -0
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -0
- package/dist/server/services/websocket/tab-handlers.d.ts +0 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +2 -9
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +15 -6
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +6 -4
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/README.md +1 -1
- package/server/cli/headless/claude-invoker-stall.ts +7 -2
- package/server/cli/headless/claude-invoker.ts +1 -1
- package/server/cli/headless/runner.ts +67 -72
- package/server/cli/headless/stall-assessor.ts +9 -4
- package/server/cli/headless/types.ts +1 -1
- package/server/cli/improvisation-history-store.ts +62 -0
- package/server/cli/improvisation-movements.ts +120 -0
- package/server/cli/improvisation-output-queue.ts +42 -0
- package/server/cli/improvisation-retry.ts +25 -600
- package/server/cli/improvisation-session-manager.ts +74 -160
- package/server/cli/retry/retry-best-result.ts +70 -0
- package/server/cli/retry/retry-context-loss.ts +87 -0
- package/server/cli/retry/retry-premature-completion.ts +113 -0
- package/server/cli/retry/retry-recovery-strategies.ts +247 -0
- package/server/cli/retry/retry-resume-strategy.ts +33 -0
- package/server/cli/retry/retry-runner-factory.ts +70 -0
- package/server/cli/retry/retry-tool-results.ts +31 -0
- package/server/cli/retry/retry-types.ts +32 -0
- package/server/index.ts +37 -123
- package/server/server-setup.ts +126 -1
- package/server/services/plan/agents/assess-stall.md +11 -4
- package/server/services/plan/board-config.ts +122 -0
- package/server/services/plan/composer.ts +7 -5
- package/server/services/plan/executor.ts +214 -467
- package/server/services/plan/issue-loader.ts +64 -0
- package/server/services/plan/issue-writer.ts +137 -0
- package/server/services/plan/output-manager.ts +2 -1
- package/server/services/plan/progress-log.ts +92 -0
- package/server/services/plan/prompt-builder.ts +73 -35
- package/server/services/plan/readiness-planner.ts +50 -0
- package/server/services/plan/review-gate.ts +102 -2
- package/server/services/platform.ts +163 -58
- package/server/services/websocket/file-download-handler.ts +191 -0
- package/server/services/websocket/git-worktree-handlers.ts +29 -2
- package/server/services/websocket/handler-context.ts +15 -0
- package/server/services/websocket/handler.ts +76 -12
- package/server/services/websocket/msg-id-tracker.ts +84 -0
- package/server/services/websocket/quality-handlers.ts +16 -3
- package/server/services/websocket/quality-review-agent.ts +2 -2
- package/server/services/websocket/session-handlers.ts +213 -68
- package/server/services/websocket/session-initialization.ts +83 -19
- package/server/services/websocket/session-registry.ts +61 -4
- package/server/services/websocket/tab-broadcast.ts +38 -0
- package/server/services/websocket/tab-event-buffer.ts +159 -0
- package/server/services/websocket/tab-event-replay.ts +42 -0
- package/server/services/websocket/tab-handlers.ts +2 -9
- package/server/services/websocket/types.ts +17 -4
package/server/server-setup.ts
CHANGED
|
@@ -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
|
|
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
|
|
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: <
|
|
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
|
-
|
|
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:
|
|
304
|
+
stallHardCapMs: 7_200_000,
|
|
305
305
|
verbose: true,
|
|
306
306
|
imageAttachments,
|
|
307
307
|
outputCallback: (text: string) => {
|
|
308
|
-
ctx.send
|
|
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.
|
|
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.
|
|
359
|
+
ctx.broadcastToAll({
|
|
358
360
|
type: 'planError',
|
|
359
361
|
data: { error: error instanceof Error ? error.message : String(error), boardId: streamBoardId },
|
|
360
362
|
});
|