mstro-app 0.4.20 → 0.4.22

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 (177) hide show
  1. package/README.md +66 -0
  2. package/dist/server/cli/headless/claude-invoker-process.js +1 -1
  3. package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
  4. package/dist/server/cli/headless/headless-logger.js +1 -1
  5. package/dist/server/cli/headless/headless-logger.js.map +1 -1
  6. package/dist/server/cli/headless/mcp-config.d.ts +1 -1
  7. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
  8. package/dist/server/cli/headless/mcp-config.js +4 -1
  9. package/dist/server/cli/headless/mcp-config.js.map +1 -1
  10. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  11. package/dist/server/cli/headless/runner.js +1 -0
  12. package/dist/server/cli/headless/runner.js.map +1 -1
  13. package/dist/server/cli/headless/types.d.ts +4 -1
  14. package/dist/server/cli/headless/types.d.ts.map +1 -1
  15. package/dist/server/index.js +9 -1
  16. package/dist/server/index.js.map +1 -1
  17. package/dist/server/mcp/bouncer-integration.d.ts +2 -2
  18. package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
  19. package/dist/server/mcp/bouncer-integration.js +20 -20
  20. package/dist/server/mcp/bouncer-integration.js.map +1 -1
  21. package/dist/server/mcp/security-analysis.d.ts +6 -0
  22. package/dist/server/mcp/security-analysis.d.ts.map +1 -1
  23. package/dist/server/mcp/security-analysis.js +16 -1
  24. package/dist/server/mcp/security-analysis.js.map +1 -1
  25. package/dist/server/mcp/security-patterns.d.ts +8 -0
  26. package/dist/server/mcp/security-patterns.d.ts.map +1 -1
  27. package/dist/server/mcp/security-patterns.js +47 -2
  28. package/dist/server/mcp/security-patterns.js.map +1 -1
  29. package/dist/server/services/deploy/ai-broker.d.ts +63 -0
  30. package/dist/server/services/deploy/ai-broker.d.ts.map +1 -0
  31. package/dist/server/services/deploy/ai-broker.js +360 -0
  32. package/dist/server/services/deploy/ai-broker.js.map +1 -0
  33. package/dist/server/services/deploy/board-execution-handler.d.ts +114 -0
  34. package/dist/server/services/deploy/board-execution-handler.d.ts.map +1 -0
  35. package/dist/server/services/deploy/board-execution-handler.js +621 -0
  36. package/dist/server/services/deploy/board-execution-handler.js.map +1 -0
  37. package/dist/server/services/deploy/credentials.d.ts +35 -0
  38. package/dist/server/services/deploy/credentials.d.ts.map +1 -0
  39. package/dist/server/services/deploy/credentials.js +177 -0
  40. package/dist/server/services/deploy/credentials.js.map +1 -0
  41. package/dist/server/services/deploy/deploy-ai-service.d.ts +107 -0
  42. package/dist/server/services/deploy/deploy-ai-service.d.ts.map +1 -0
  43. package/dist/server/services/deploy/deploy-ai-service.js +294 -0
  44. package/dist/server/services/deploy/deploy-ai-service.js.map +1 -0
  45. package/dist/server/services/deploy/headless-session-handler.d.ts +94 -0
  46. package/dist/server/services/deploy/headless-session-handler.d.ts.map +1 -0
  47. package/dist/server/services/deploy/headless-session-handler.js +274 -0
  48. package/dist/server/services/deploy/headless-session-handler.js.map +1 -0
  49. package/dist/server/services/pathUtils.d.ts.map +1 -1
  50. package/dist/server/services/pathUtils.js +33 -1
  51. package/dist/server/services/pathUtils.js.map +1 -1
  52. package/dist/server/services/plan/agent-loader.d.ts +10 -0
  53. package/dist/server/services/plan/agent-loader.d.ts.map +1 -0
  54. package/dist/server/services/plan/agent-loader.js +65 -0
  55. package/dist/server/services/plan/agent-loader.js.map +1 -0
  56. package/dist/server/services/plan/composer.d.ts.map +1 -1
  57. package/dist/server/services/plan/composer.js +5 -1
  58. package/dist/server/services/plan/composer.js.map +1 -1
  59. package/dist/server/services/plan/dependency-resolver.d.ts +1 -1
  60. package/dist/server/services/plan/dependency-resolver.js +2 -2
  61. package/dist/server/services/plan/dependency-resolver.js.map +1 -1
  62. package/dist/server/services/plan/executor.d.ts +7 -3
  63. package/dist/server/services/plan/executor.d.ts.map +1 -1
  64. package/dist/server/services/plan/executor.js +27 -14
  65. package/dist/server/services/plan/executor.js.map +1 -1
  66. package/dist/server/services/plan/front-matter.d.ts +5 -0
  67. package/dist/server/services/plan/front-matter.d.ts.map +1 -1
  68. package/dist/server/services/plan/front-matter.js +19 -0
  69. package/dist/server/services/plan/front-matter.js.map +1 -1
  70. package/dist/server/services/plan/issue-prompt-builder.d.ts +1 -1
  71. package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
  72. package/dist/server/services/plan/issue-prompt-builder.js +1 -1
  73. package/dist/server/services/plan/issue-retry.d.ts +25 -0
  74. package/dist/server/services/plan/issue-retry.d.ts.map +1 -0
  75. package/dist/server/services/plan/issue-retry.js +216 -0
  76. package/dist/server/services/plan/issue-retry.js.map +1 -0
  77. package/dist/server/services/plan/output-manager.d.ts +2 -2
  78. package/dist/server/services/plan/output-manager.js +2 -2
  79. package/dist/server/services/plan/parser-core.d.ts +1 -1
  80. package/dist/server/services/plan/parser-core.js +1 -1
  81. package/dist/server/services/plan/parser-core.js.map +1 -1
  82. package/dist/server/services/plan/parser-migration.d.ts +2 -2
  83. package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
  84. package/dist/server/services/plan/parser-migration.js +5 -5
  85. package/dist/server/services/plan/parser-migration.js.map +1 -1
  86. package/dist/server/services/plan/parser.d.ts.map +1 -1
  87. package/dist/server/services/plan/parser.js +4 -7
  88. package/dist/server/services/plan/parser.js.map +1 -1
  89. package/dist/server/services/plan/prompt-builder.d.ts +1 -1
  90. package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
  91. package/dist/server/services/plan/review-gate.d.ts +4 -0
  92. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  93. package/dist/server/services/plan/review-gate.js +90 -35
  94. package/dist/server/services/plan/review-gate.js.map +1 -1
  95. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
  96. package/dist/server/services/plan/state-reconciler.js +21 -11
  97. package/dist/server/services/plan/state-reconciler.js.map +1 -1
  98. package/dist/server/services/plan/types.d.ts +2 -2
  99. package/dist/server/services/plan/types.d.ts.map +1 -1
  100. package/dist/server/services/plan/watcher.js +1 -1
  101. package/dist/server/services/sentry.d.ts.map +1 -1
  102. package/dist/server/services/sentry.js +8 -4
  103. package/dist/server/services/sentry.js.map +1 -1
  104. package/dist/server/services/websocket/deploy-handlers.d.ts +14 -0
  105. package/dist/server/services/websocket/deploy-handlers.d.ts.map +1 -0
  106. package/dist/server/services/websocket/deploy-handlers.js +409 -0
  107. package/dist/server/services/websocket/deploy-handlers.js.map +1 -0
  108. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  109. package/dist/server/services/websocket/handler.js +12 -0
  110. package/dist/server/services/websocket/handler.js.map +1 -1
  111. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +11 -0
  112. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +1 -0
  113. package/dist/server/services/websocket/handlers/deploy-handlers.js +180 -0
  114. package/dist/server/services/websocket/handlers/deploy-handlers.js.map +1 -0
  115. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
  116. package/dist/server/services/websocket/plan-board-handlers.js +54 -1
  117. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
  118. package/dist/server/services/websocket/plan-helpers.d.ts +1 -1
  119. package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
  120. package/dist/server/services/websocket/plan-helpers.js +3 -4
  121. package/dist/server/services/websocket/plan-helpers.js.map +1 -1
  122. package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
  123. package/dist/server/services/websocket/plan-issue-handlers.js +5 -1
  124. package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
  125. package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
  126. package/dist/server/services/websocket/plan-sprint-handlers.js +3 -11
  127. package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
  128. package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
  129. package/dist/server/services/websocket/settings-handlers.js +17 -21
  130. package/dist/server/services/websocket/settings-handlers.js.map +1 -1
  131. package/dist/server/services/websocket/types.d.ts +264 -2
  132. package/dist/server/services/websocket/types.d.ts.map +1 -1
  133. package/package.json +1 -1
  134. package/server/cli/headless/claude-invoker-process.ts +1 -1
  135. package/server/cli/headless/headless-logger.ts +1 -1
  136. package/server/cli/headless/mcp-config.ts +4 -1
  137. package/server/cli/headless/runner.ts +1 -0
  138. package/server/cli/headless/types.ts +4 -1
  139. package/server/index.ts +9 -1
  140. package/server/mcp/bouncer-integration.ts +19 -17
  141. package/server/mcp/security-analysis.ts +19 -0
  142. package/server/mcp/security-patterns.ts +53 -2
  143. package/server/services/deploy/ai-broker.ts +512 -0
  144. package/server/services/deploy/board-execution-handler.ts +847 -0
  145. package/server/services/deploy/credentials.ts +200 -0
  146. package/server/services/deploy/deploy-ai-service.ts +401 -0
  147. package/server/services/deploy/headless-session-handler.ts +415 -0
  148. package/server/services/pathUtils.ts +35 -1
  149. package/server/services/plan/agent-loader.ts +73 -0
  150. package/server/services/plan/agents/review-code.md +28 -0
  151. package/server/services/plan/agents/review-custom.md +27 -0
  152. package/server/services/plan/agents/review-quality.md +42 -0
  153. package/server/services/plan/composer.ts +5 -1
  154. package/server/services/plan/dependency-resolver.ts +2 -2
  155. package/server/services/plan/executor.ts +27 -15
  156. package/server/services/plan/front-matter.ts +23 -0
  157. package/server/services/plan/issue-prompt-builder.ts +2 -2
  158. package/server/services/plan/issue-retry.ts +297 -0
  159. package/server/services/plan/output-manager.ts +2 -2
  160. package/server/services/plan/parser-core.ts +2 -2
  161. package/server/services/plan/parser-migration.ts +5 -5
  162. package/server/services/plan/parser.ts +4 -5
  163. package/server/services/plan/prompt-builder.ts +1 -1
  164. package/server/services/plan/review-gate.ts +105 -34
  165. package/server/services/plan/state-reconciler.ts +21 -11
  166. package/server/services/plan/types.ts +3 -3
  167. package/server/services/plan/watcher.ts +1 -1
  168. package/server/services/sentry.ts +8 -4
  169. package/server/services/websocket/deploy-handlers.ts +544 -0
  170. package/server/services/websocket/handler.ts +11 -1
  171. package/server/services/websocket/handlers/deploy-handlers.ts +230 -0
  172. package/server/services/websocket/plan-board-handlers.ts +53 -1
  173. package/server/services/websocket/plan-helpers.ts +3 -4
  174. package/server/services/websocket/plan-issue-handlers.ts +6 -1
  175. package/server/services/websocket/plan-sprint-handlers.ts +3 -9
  176. package/server/services/websocket/settings-handlers.ts +18 -22
  177. package/server/services/websocket/types.ts +333 -2
@@ -0,0 +1,230 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Deploy HTTP Handler
6
+ *
7
+ * Handles deployHttpRequest messages from the platform server relay.
8
+ * Proxies HTTP requests to the developer's local server and returns
9
+ * the response back through the WebSocket relay.
10
+ */
11
+
12
+ import type { HandlerContext } from '../handler-context.js';
13
+ import type { DeployHttpRequestData, DeployHttpResponseChunkData, DeployHttpResponseData, WebSocketMessage, WSContext } from '../types.js';
14
+
15
+ /** Hop-by-hop headers that must not be forwarded through a proxy (RFC 2616 §13.5.1) */
16
+ const HOP_BY_HOP_HEADERS = new Set([
17
+ 'connection',
18
+ 'keep-alive',
19
+ 'transfer-encoding',
20
+ 'te',
21
+ 'trailers',
22
+ 'upgrade',
23
+ ]);
24
+
25
+ /** Request timeout in milliseconds (30 seconds) */
26
+ const REQUEST_TIMEOUT_MS = 30_000;
27
+
28
+ /** Maximum total header size in bytes (16 KB) */
29
+ const MAX_HEADER_SIZE_BYTES = 16_384;
30
+
31
+ /** Chunking threshold: responses larger than 1 MB are streamed in chunks */
32
+ const CHUNK_THRESHOLD_BYTES = 1_048_576;
33
+
34
+ /** Size of each chunk (~256 KB of raw data → ~341 KB base64) */
35
+ const CHUNK_SIZE_BYTES = 262_144;
36
+
37
+ function isHopByHopHeader(name: string): boolean {
38
+ const lower = name.toLowerCase();
39
+ return HOP_BY_HOP_HEADERS.has(lower) || lower.startsWith('proxy-');
40
+ }
41
+
42
+ function stripHopByHopHeaders(headers: Record<string, string>): Record<string, string> {
43
+ const result: Record<string, string> = {};
44
+ for (const [key, value] of Object.entries(headers)) {
45
+ if (!isHopByHopHeader(key)) {
46
+ result[key] = value;
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+
52
+ /** Check headers for null bytes or CRLF injection attempts */
53
+ function containsHeaderInjection(headers: Record<string, string>): boolean {
54
+ for (const [key, value] of Object.entries(headers)) {
55
+ if (key.includes('\0') || value.includes('\0')) return true;
56
+ if (/\r|\n/.test(key) || /\r|\n/.test(value)) return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ /** Calculate total size of request headers in bytes */
62
+ function calculateHeaderSize(headers: Record<string, string>): number {
63
+ let size = 0;
64
+ for (const [key, value] of Object.entries(headers)) {
65
+ // key: value\r\n
66
+ size += key.length + 2 + value.length + 2;
67
+ }
68
+ return size;
69
+ }
70
+
71
+ function sendDeployHttpResponse(
72
+ ctx: HandlerContext,
73
+ ws: WSContext,
74
+ data: DeployHttpResponseData,
75
+ ): void {
76
+ ctx.send(ws, { type: 'deployHttpResponse', data });
77
+ }
78
+
79
+ /** Send a large response body in chunks via deployHttpResponseChunk messages */
80
+ function sendChunkedResponse(
81
+ ctx: HandlerContext,
82
+ ws: WSContext,
83
+ requestId: string,
84
+ status: number,
85
+ headers: Record<string, string>,
86
+ bodyBuffer: Buffer,
87
+ ): void {
88
+ const totalChunks = Math.ceil(bodyBuffer.length / CHUNK_SIZE_BYTES);
89
+
90
+ for (let i = 0; i < totalChunks; i++) {
91
+ const start = i * CHUNK_SIZE_BYTES;
92
+ const end = Math.min(start + CHUNK_SIZE_BYTES, bodyBuffer.length);
93
+ const chunk = bodyBuffer.subarray(start, end);
94
+ const isLast = i === totalChunks - 1;
95
+
96
+ const chunkData: DeployHttpResponseChunkData = {
97
+ requestId,
98
+ chunkIndex: i,
99
+ totalChunks,
100
+ data: chunk.toString('base64'),
101
+ isLast,
102
+ };
103
+
104
+ // Include status and headers only in the first chunk
105
+ if (i === 0) {
106
+ chunkData.status = status;
107
+ chunkData.headers = headers;
108
+ }
109
+
110
+ ctx.send(ws, { type: 'deployHttpResponseChunk', data: chunkData });
111
+ }
112
+ }
113
+
114
+ export async function handleDeployHttpRequest(
115
+ ctx: HandlerContext,
116
+ ws: WSContext,
117
+ msg: WebSocketMessage,
118
+ ): Promise<void> {
119
+ const data = msg.data as DeployHttpRequestData;
120
+
121
+ if (!data?.requestId || !data?.method || !data?.url || !data?.port) {
122
+ sendDeployHttpResponse(ctx, ws, {
123
+ requestId: data?.requestId || 'unknown',
124
+ status: 400,
125
+ headers: {},
126
+ body: 'Bad Request: missing required fields (requestId, method, url, port)',
127
+ });
128
+ return;
129
+ }
130
+
131
+ // Reject headers with null bytes or CRLF injection
132
+ if (data.headers && containsHeaderInjection(data.headers)) {
133
+ sendDeployHttpResponse(ctx, ws, {
134
+ requestId: data.requestId,
135
+ status: 400,
136
+ headers: {},
137
+ body: 'Bad Request: headers contain null bytes or CRLF injection',
138
+ });
139
+ return;
140
+ }
141
+
142
+ // Enforce header size limit
143
+ if (data.headers && calculateHeaderSize(data.headers) > MAX_HEADER_SIZE_BYTES) {
144
+ sendDeployHttpResponse(ctx, ws, {
145
+ requestId: data.requestId,
146
+ status: 431,
147
+ headers: {},
148
+ body: 'Request Header Fields Too Large: total headers exceed 16KB',
149
+ });
150
+ return;
151
+ }
152
+
153
+ // Build local URL: localhost:{port}{path with query string}
154
+ const localUrl = `http://localhost:${data.port}${data.url}`;
155
+
156
+ try {
157
+ const requestHeaders = stripHopByHopHeaders(data.headers);
158
+
159
+ // Only include body for methods that support it
160
+ const hasBody = data.body !== undefined && data.method !== 'GET' && data.method !== 'HEAD';
161
+
162
+ const controller = new AbortController();
163
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
164
+
165
+ let response: Response;
166
+ try {
167
+ response = await fetch(localUrl, {
168
+ method: data.method,
169
+ headers: requestHeaders,
170
+ body: hasBody ? data.body : undefined,
171
+ signal: controller.signal,
172
+ redirect: 'manual',
173
+ });
174
+ } finally {
175
+ clearTimeout(timeout);
176
+ }
177
+
178
+ // Serialize response headers, stripping hop-by-hop
179
+ const responseHeaders: Record<string, string> = {};
180
+ response.headers.forEach((value, key) => {
181
+ if (!isHopByHopHeader(key)) {
182
+ responseHeaders[key] = value;
183
+ }
184
+ });
185
+
186
+ // Read response as binary to handle both text and binary payloads
187
+ const bodyBuffer = Buffer.from(await response.arrayBuffer());
188
+
189
+ // Stream large responses in chunks
190
+ if (bodyBuffer.length > CHUNK_THRESHOLD_BYTES) {
191
+ sendChunkedResponse(ctx, ws, data.requestId, response.status, responseHeaders, bodyBuffer);
192
+ return;
193
+ }
194
+
195
+ // Small response — send as a single message
196
+ sendDeployHttpResponse(ctx, ws, {
197
+ requestId: data.requestId,
198
+ status: response.status,
199
+ headers: responseHeaders,
200
+ body: bodyBuffer.toString('utf-8'),
201
+ });
202
+ } catch (error: unknown) {
203
+ let status = 502;
204
+ let body = 'Bad Gateway';
205
+
206
+ if (error instanceof Error) {
207
+ if (error.name === 'AbortError') {
208
+ status = 504;
209
+ body = 'Gateway Timeout';
210
+ } else if (isConnectionRefused(error)) {
211
+ status = 502;
212
+ body = 'Bad Gateway: target server is not running';
213
+ }
214
+ }
215
+
216
+ sendDeployHttpResponse(ctx, ws, {
217
+ requestId: data.requestId,
218
+ status,
219
+ headers: {},
220
+ body,
221
+ });
222
+ }
223
+ }
224
+
225
+ /** Detect ECONNREFUSED across Node.js error shapes */
226
+ function isConnectionRefused(error: Error): boolean {
227
+ if (error.message.includes('ECONNREFUSED')) return true;
228
+ const cause = (error as Error & { cause?: { code?: string } }).cause;
229
+ return cause?.code === 'ECONNREFUSED';
230
+ }
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
2
  // Licensed under the MIT License. See LICENSE file for details.
3
3
 
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { replaceFrontMatterField } from '../plan/front-matter.js';
7
7
  import { getNextBoardId, getNextBoardNumber, parseBoardArtifacts, parseBoardDirectory, parsePlanDirectory, resolvePmDir } from '../plan/parser.js';
@@ -125,6 +125,24 @@ export function handleUpdateBoard(
125
125
  }
126
126
  writeFileSync(boardMdPath, content, 'utf-8');
127
127
 
128
+ // When review criteria are set, also write a board-level review agent file
129
+ // so users can discover and edit the full prompt as markdown.
130
+ const typedFields = fields as Record<string, unknown>;
131
+ if ('reviewCriteria' in typedFields) {
132
+ const boardDir = join(pmDir, 'boards', boardId);
133
+ const agentsDir = join(boardDir, 'agents');
134
+ const agentPath = join(agentsDir, 'review-custom.md');
135
+ const criteriaValue = String(typedFields.reviewCriteria ?? '').trim();
136
+
137
+ if (criteriaValue) {
138
+ if (!existsSync(agentsDir)) mkdirSync(agentsDir, { recursive: true });
139
+ writeFileSync(agentPath, buildBoardReviewAgent(criteriaValue), 'utf-8');
140
+ } else if (existsSync(agentPath)) {
141
+ // Clear the agent file when criteria are removed
142
+ try { unlinkSync(agentPath); } catch { /* non-fatal */ }
143
+ }
144
+ }
145
+
128
146
  const boardState = parseBoardDirectory(pmDir, boardId);
129
147
  if (boardState) {
130
148
  ctx.broadcastToAll({ type: 'planBoardUpdated', data: boardState.board });
@@ -275,3 +293,37 @@ export function handleGetBoardArtifacts(
275
293
 
276
294
  ctx.send(ws, { type: 'planBoardArtifacts', data: artifacts });
277
295
  }
296
+
297
+ // ── Private helpers ──────────────────────────────────────────────────
298
+
299
+ /** Build a board-level review-custom agent file from user-provided criteria. */
300
+ function buildBoardReviewAgent(criteria: string): string {
301
+ return `---
302
+ name: review-custom
303
+ description: Board-specific review agent with custom criteria
304
+ type: review
305
+ variables: [issue_id, issue_title, context_section, acceptance_criteria, review_criteria, read_instruction]
306
+ checks: [criteria_met, review_criteria]
307
+ ---
308
+
309
+ You are a reviewer. Review the work done for issue {{issue_id}}: {{issue_title}}.
310
+ {{context_section}}
311
+
312
+ ## Acceptance Criteria
313
+ {{acceptance_criteria}}
314
+
315
+ ## Review Criteria
316
+ ${criteria}
317
+
318
+ ## Instructions
319
+ 1. {{read_instruction}}
320
+ 2. Check if all acceptance criteria are met — evaluate each criterion individually
321
+ 3. Evaluate thoroughly against the review criteria above
322
+ 4. Consider the overall quality of the work: does it fully address the issue's intent, is it well-structured, and is it ready to ship?
323
+
324
+ Output EXACTLY one JSON object on its own line (no markdown fencing):
325
+ {"passed": true, "checks": [{"name": "criteria_met", "passed": true, "details": "..."}]}
326
+
327
+ Include checks for: criteria_met, review_criteria.
328
+ `;
329
+ }
@@ -13,7 +13,7 @@ import type { WSContext } from './types.js';
13
13
  export const watcherCache = new Map<string, PlanWatcher>();
14
14
  export const executorCache = new Map<string, PlanExecutor>();
15
15
 
16
- /** Validate that a user-supplied path resolves within the .pm/ (or legacy .plan/) directory. */
16
+ /** Validate that a user-supplied path resolves within the .mstro/pm/ directory. */
17
17
  export function resolvePlanPath(workingDir: string, relativePath: string): string | null {
18
18
  const pmDir = resolvePmDir(workingDir);
19
19
  if (!pmDir) return null;
@@ -52,7 +52,7 @@ export function buildIssueMarkdown(
52
52
  id: ${id}
53
53
  title: "${title.replace(/"/g, '\\"')}"
54
54
  type: ${type}
55
- status: backlog
55
+ status: todo
56
56
  priority: ${priority}
57
57
  estimate: null
58
58
  labels: ${labelsYaml}
@@ -109,8 +109,7 @@ labels: []
109
109
  ## Workflows
110
110
  | Status | Category | Description |
111
111
  |---|---|---|
112
- | backlog | unstarted | Accepted, not yet scheduled |
113
- | todo | unstarted | Scheduled for current sprint |
112
+ | todo | ready | Refined and ready for agent execution |
114
113
  | in_progress | started | Actively being worked on |
115
114
  | in_review | started | PR open, awaiting review |
116
115
  | done | completed | Merged and verified |
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
5
5
  import { basename, join } from 'node:path';
6
- import { replaceFrontMatterField } from '../plan/front-matter.js';
6
+ import { checkAllAcceptanceCriteria, replaceFrontMatterField } from '../plan/front-matter.js';
7
7
  import { defaultPmDir, getNextId, parseBoardDirectory, parsePlanDirectory, parseSingleIssue, parseSingleMilestone, parseSingleSprint, planDirExists, resolvePmDir } from '../plan/parser.js';
8
8
  import { tryCompleteParentEpic } from '../plan/state-reconciler.js';
9
9
  import type { HandlerContext } from './handler-context.js';
@@ -163,6 +163,11 @@ export function handleUpdateIssue(
163
163
  content = replaceFrontMatterField(content, yamlKey, formatYamlValue(value));
164
164
  }
165
165
 
166
+ // Check off all acceptance criteria when status transitions to done
167
+ if ((fields as Record<string, unknown>).status === 'done') {
168
+ content = checkAllAcceptanceCriteria(content);
169
+ }
170
+
166
171
  writeFileSync(fullPath, content, 'utf-8');
167
172
 
168
173
  const issue = parseSingleIssue(workingDir, path);
@@ -97,15 +97,9 @@ export function handleCreateSprint(
97
97
  ctx.broadcastToAll({ type: 'planSprintCreated', data: sprint });
98
98
  }
99
99
 
100
- /** Promote sprint issues from 'backlog' to 'todo' status. */
101
- function promoteSprintIssues(pmDir: string, sprint: { issues: Array<{ id: string; path: string }> }, allIssues: Issue[]): void {
102
- for (const issueSummary of sprint.issues) {
103
- const issue = allIssues.find(i => i.id === issueSummary.id || i.path === issueSummary.path);
104
- if (!issue || issue.status !== 'backlog') continue;
105
- const issuePath = join(pmDir, issue.path);
106
- if (!existsSync(issuePath)) continue;
107
- writeFileSync(issuePath, replaceFrontMatterField(readFileSync(issuePath, 'utf-8'), 'status', 'todo'), 'utf-8');
108
- }
100
+ /** @deprecated Legacy sprint promotion backlog status removed in v2 board-centric model. */
101
+ function promoteSprintIssues(_pmDir: string, _sprint: { issues: Array<{ id: string; path: string }> }, _allIssues: Issue[]): void {
102
+ // No-op: all issues are created with status 'todo' in v2. Legacy sprint promotion is no longer needed.
109
103
  }
110
104
 
111
105
  /** Update a file's front matter field if the file exists. */
@@ -78,6 +78,17 @@ Respond with ONLY the summary text, nothing else.`;
78
78
 
79
79
  let stdout = '';
80
80
  let stderr = '';
81
+ let responseSent = false;
82
+
83
+ const sendSummaryOnce = (summary: string) => {
84
+ if (responseSent) return;
85
+ responseSent = true;
86
+ ctx.send(ws, {
87
+ type: 'notificationSummary',
88
+ tabId,
89
+ data: { summary }
90
+ });
91
+ };
81
92
 
82
93
  claude.stdout?.on('data', (data: Buffer) => {
83
94
  stdout += data.toString();
@@ -94,42 +105,27 @@ Respond with ONLY the summary text, nothing else.`;
94
105
  // Ignore cleanup errors
95
106
  }
96
107
 
97
- let summary: string;
98
108
  if (code === 0 && stdout.trim()) {
99
- summary = stdout.trim().slice(0, 150);
109
+ sendSummaryOnce(stdout.trim().slice(0, 150));
100
110
  } else {
101
111
  console.error('[WebSocketImproviseHandler] Claude error:', stderr || 'Unknown error');
102
- summary = createFallbackSummary(userPrompt);
112
+ sendSummaryOnce(createFallbackSummary(userPrompt));
103
113
  }
104
-
105
- ctx.send(ws, {
106
- type: 'notificationSummary',
107
- tabId,
108
- data: { summary }
109
- });
110
114
  });
111
115
 
112
116
  claude.on('error', (err: Error) => {
113
117
  console.error('[WebSocketImproviseHandler] Failed to spawn Claude:', err);
114
- const summary = createFallbackSummary(userPrompt);
115
- ctx.send(ws, {
116
- type: 'notificationSummary',
117
- tabId,
118
- data: { summary }
119
- });
118
+ sendSummaryOnce(createFallbackSummary(userPrompt));
120
119
  });
121
120
 
122
121
  // Timeout after 10 seconds
123
- setTimeout(() => {
122
+ const timeout = setTimeout(() => {
124
123
  claude.kill();
125
- const summary = createFallbackSummary(userPrompt);
126
- ctx.send(ws, {
127
- type: 'notificationSummary',
128
- tabId,
129
- data: { summary }
130
- });
124
+ sendSummaryOnce(createFallbackSummary(userPrompt));
131
125
  }, 10000);
132
126
 
127
+ claude.on('close', () => { clearTimeout(timeout); });
128
+
133
129
  } catch (error) {
134
130
  console.error('[WebSocketImproviseHandler] Error generating summary:', error);
135
131
  const summary = createFallbackSummary(userPrompt);