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.
- package/README.md +66 -0
- package/dist/server/cli/headless/claude-invoker-process.js +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/headless-logger.js +1 -1
- package/dist/server/cli/headless/headless-logger.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +4 -1
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +1 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +4 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/index.js +9 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +2 -2
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +20 -20
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/security-analysis.d.ts +6 -0
- package/dist/server/mcp/security-analysis.d.ts.map +1 -1
- package/dist/server/mcp/security-analysis.js +16 -1
- package/dist/server/mcp/security-analysis.js.map +1 -1
- package/dist/server/mcp/security-patterns.d.ts +8 -0
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +47 -2
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/services/deploy/ai-broker.d.ts +63 -0
- package/dist/server/services/deploy/ai-broker.d.ts.map +1 -0
- package/dist/server/services/deploy/ai-broker.js +360 -0
- package/dist/server/services/deploy/ai-broker.js.map +1 -0
- package/dist/server/services/deploy/board-execution-handler.d.ts +114 -0
- package/dist/server/services/deploy/board-execution-handler.d.ts.map +1 -0
- package/dist/server/services/deploy/board-execution-handler.js +621 -0
- package/dist/server/services/deploy/board-execution-handler.js.map +1 -0
- package/dist/server/services/deploy/credentials.d.ts +35 -0
- package/dist/server/services/deploy/credentials.d.ts.map +1 -0
- package/dist/server/services/deploy/credentials.js +177 -0
- package/dist/server/services/deploy/credentials.js.map +1 -0
- package/dist/server/services/deploy/deploy-ai-service.d.ts +107 -0
- package/dist/server/services/deploy/deploy-ai-service.d.ts.map +1 -0
- package/dist/server/services/deploy/deploy-ai-service.js +294 -0
- package/dist/server/services/deploy/deploy-ai-service.js.map +1 -0
- package/dist/server/services/deploy/headless-session-handler.d.ts +94 -0
- package/dist/server/services/deploy/headless-session-handler.d.ts.map +1 -0
- package/dist/server/services/deploy/headless-session-handler.js +274 -0
- package/dist/server/services/deploy/headless-session-handler.js.map +1 -0
- package/dist/server/services/pathUtils.d.ts.map +1 -1
- package/dist/server/services/pathUtils.js +33 -1
- package/dist/server/services/pathUtils.js.map +1 -1
- package/dist/server/services/plan/agent-loader.d.ts +10 -0
- package/dist/server/services/plan/agent-loader.d.ts.map +1 -0
- package/dist/server/services/plan/agent-loader.js +65 -0
- package/dist/server/services/plan/agent-loader.js.map +1 -0
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +5 -1
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/dependency-resolver.d.ts +1 -1
- package/dist/server/services/plan/dependency-resolver.js +2 -2
- package/dist/server/services/plan/dependency-resolver.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +7 -3
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +27 -14
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts +5 -0
- package/dist/server/services/plan/front-matter.d.ts.map +1 -1
- package/dist/server/services/plan/front-matter.js +19 -0
- package/dist/server/services/plan/front-matter.js.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +1 -1
- package/dist/server/services/plan/issue-retry.d.ts +25 -0
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -0
- package/dist/server/services/plan/issue-retry.js +216 -0
- package/dist/server/services/plan/issue-retry.js.map +1 -0
- package/dist/server/services/plan/output-manager.d.ts +2 -2
- package/dist/server/services/plan/output-manager.js +2 -2
- package/dist/server/services/plan/parser-core.d.ts +1 -1
- package/dist/server/services/plan/parser-core.js +1 -1
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/parser-migration.d.ts +2 -2
- package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
- package/dist/server/services/plan/parser-migration.js +5 -5
- package/dist/server/services/plan/parser-migration.js.map +1 -1
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +4 -7
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/prompt-builder.d.ts +1 -1
- package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts +4 -0
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +90 -35
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
- package/dist/server/services/plan/state-reconciler.js +21 -11
- package/dist/server/services/plan/state-reconciler.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +2 -2
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/plan/watcher.js +1 -1
- package/dist/server/services/sentry.d.ts.map +1 -1
- package/dist/server/services/sentry.js +8 -4
- package/dist/server/services/sentry.js.map +1 -1
- package/dist/server/services/websocket/deploy-handlers.d.ts +14 -0
- package/dist/server/services/websocket/deploy-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/deploy-handlers.js +409 -0
- package/dist/server/services/websocket/deploy-handlers.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +12 -0
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts +11 -0
- package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +1 -0
- package/dist/server/services/websocket/handlers/deploy-handlers.js +180 -0
- package/dist/server/services/websocket/handlers/deploy-handlers.js.map +1 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.js +54 -1
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-helpers.js +3 -4
- package/dist/server/services/websocket/plan-helpers.js.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.js +5 -1
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -11
- package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +17 -21
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +264 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-process.ts +1 -1
- package/server/cli/headless/headless-logger.ts +1 -1
- package/server/cli/headless/mcp-config.ts +4 -1
- package/server/cli/headless/runner.ts +1 -0
- package/server/cli/headless/types.ts +4 -1
- package/server/index.ts +9 -1
- package/server/mcp/bouncer-integration.ts +19 -17
- package/server/mcp/security-analysis.ts +19 -0
- package/server/mcp/security-patterns.ts +53 -2
- package/server/services/deploy/ai-broker.ts +512 -0
- package/server/services/deploy/board-execution-handler.ts +847 -0
- package/server/services/deploy/credentials.ts +200 -0
- package/server/services/deploy/deploy-ai-service.ts +401 -0
- package/server/services/deploy/headless-session-handler.ts +415 -0
- package/server/services/pathUtils.ts +35 -1
- package/server/services/plan/agent-loader.ts +73 -0
- package/server/services/plan/agents/review-code.md +28 -0
- package/server/services/plan/agents/review-custom.md +27 -0
- package/server/services/plan/agents/review-quality.md +42 -0
- package/server/services/plan/composer.ts +5 -1
- package/server/services/plan/dependency-resolver.ts +2 -2
- package/server/services/plan/executor.ts +27 -15
- package/server/services/plan/front-matter.ts +23 -0
- package/server/services/plan/issue-prompt-builder.ts +2 -2
- package/server/services/plan/issue-retry.ts +297 -0
- package/server/services/plan/output-manager.ts +2 -2
- package/server/services/plan/parser-core.ts +2 -2
- package/server/services/plan/parser-migration.ts +5 -5
- package/server/services/plan/parser.ts +4 -5
- package/server/services/plan/prompt-builder.ts +1 -1
- package/server/services/plan/review-gate.ts +105 -34
- package/server/services/plan/state-reconciler.ts +21 -11
- package/server/services/plan/types.ts +3 -3
- package/server/services/plan/watcher.ts +1 -1
- package/server/services/sentry.ts +8 -4
- package/server/services/websocket/deploy-handlers.ts +544 -0
- package/server/services/websocket/handler.ts +11 -1
- package/server/services/websocket/handlers/deploy-handlers.ts +230 -0
- package/server/services/websocket/plan-board-handlers.ts +53 -1
- package/server/services/websocket/plan-helpers.ts +3 -4
- package/server/services/websocket/plan-issue-handlers.ts +6 -1
- package/server/services/websocket/plan-sprint-handlers.ts +3 -9
- package/server/services/websocket/settings-handlers.ts +18 -22
- 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/
|
|
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:
|
|
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
|
-
|
|
|
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
|
-
/**
|
|
101
|
-
function promoteSprintIssues(
|
|
102
|
-
|
|
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
|
-
|
|
109
|
+
sendSummaryOnce(stdout.trim().slice(0, 150));
|
|
100
110
|
} else {
|
|
101
111
|
console.error('[WebSocketImproviseHandler] Claude error:', stderr || 'Unknown error');
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|