mstro-app 0.4.29 → 0.4.33

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 (105) hide show
  1. package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -1
  2. package/dist/server/cli/headless/haiku-assessments.js +20 -28
  3. package/dist/server/cli/headless/haiku-assessments.js.map +1 -1
  4. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  5. package/dist/server/cli/headless/stall-assessor.js +17 -3
  6. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  7. package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
  8. package/dist/server/cli/improvisation-retry.js +18 -1
  9. package/dist/server/cli/improvisation-retry.js.map +1 -1
  10. package/dist/server/cli/improvisation-session-manager.d.ts +5 -0
  11. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  12. package/dist/server/cli/improvisation-session-manager.js +41 -1
  13. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  14. package/dist/server/cli/prompt-builders.d.ts.map +1 -1
  15. package/dist/server/cli/prompt-builders.js +35 -19
  16. package/dist/server/cli/prompt-builders.js.map +1 -1
  17. package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
  18. package/dist/server/mcp/bouncer-haiku.js +5 -30
  19. package/dist/server/mcp/bouncer-haiku.js.map +1 -1
  20. package/dist/server/mcp/security-analysis.d.ts.map +1 -1
  21. package/dist/server/mcp/security-analysis.js +19 -11
  22. package/dist/server/mcp/security-analysis.js.map +1 -1
  23. package/dist/server/services/deploy/headless-session-handler.d.ts.map +1 -1
  24. package/dist/server/services/deploy/headless-session-handler.js +61 -69
  25. package/dist/server/services/deploy/headless-session-handler.js.map +1 -1
  26. package/dist/server/services/files.d.ts.map +1 -1
  27. package/dist/server/services/files.js +6 -2
  28. package/dist/server/services/files.js.map +1 -1
  29. package/dist/server/services/pathUtils.d.ts.map +1 -1
  30. package/dist/server/services/pathUtils.js +46 -38
  31. package/dist/server/services/pathUtils.js.map +1 -1
  32. package/dist/server/services/plan/agent-loader.d.ts +20 -4
  33. package/dist/server/services/plan/agent-loader.d.ts.map +1 -1
  34. package/dist/server/services/plan/agent-loader.js +69 -16
  35. package/dist/server/services/plan/agent-loader.js.map +1 -1
  36. package/dist/server/services/plan/issue-retry.d.ts +0 -8
  37. package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
  38. package/dist/server/services/plan/issue-retry.js +72 -63
  39. package/dist/server/services/plan/issue-retry.js.map +1 -1
  40. package/dist/server/services/plan/review-gate.js +16 -88
  41. package/dist/server/services/plan/review-gate.js.map +1 -1
  42. package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
  43. package/dist/server/services/websocket/file-explorer-handlers.js +23 -2
  44. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  45. package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
  46. package/dist/server/services/websocket/git-handlers.js +21 -19
  47. package/dist/server/services/websocket/git-handlers.js.map +1 -1
  48. package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
  49. package/dist/server/services/websocket/git-pr-handlers.js +5 -21
  50. package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
  51. package/dist/server/services/websocket/handler.d.ts +2 -0
  52. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  53. package/dist/server/services/websocket/handler.js +36 -18
  54. package/dist/server/services/websocket/handler.js.map +1 -1
  55. package/dist/server/services/websocket/handlers/deploy-handlers.d.ts.map +1 -1
  56. package/dist/server/services/websocket/handlers/deploy-handlers.js +28 -33
  57. package/dist/server/services/websocket/handlers/deploy-handlers.js.map +1 -1
  58. package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
  59. package/dist/server/services/websocket/plan-board-handlers.js +31 -25
  60. package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
  61. package/dist/server/services/websocket/quality-fix-agent.d.ts.map +1 -1
  62. package/dist/server/services/websocket/quality-fix-agent.js +11 -18
  63. package/dist/server/services/websocket/quality-fix-agent.js.map +1 -1
  64. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  65. package/dist/server/services/websocket/quality-review-agent.js +13 -150
  66. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  67. package/dist/server/services/websocket/session-history.d.ts.map +1 -1
  68. package/dist/server/services/websocket/session-history.js +10 -8
  69. package/dist/server/services/websocket/session-history.js.map +1 -1
  70. package/dist/server/services/websocket/skill-handlers.d.ts +4 -0
  71. package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -0
  72. package/dist/server/services/websocket/skill-handlers.js +93 -0
  73. package/dist/server/services/websocket/skill-handlers.js.map +1 -0
  74. package/dist/server/services/websocket/types.d.ts +8 -2
  75. package/dist/server/services/websocket/types.d.ts.map +1 -1
  76. package/dist/server/utils/paths.d.ts +4 -0
  77. package/dist/server/utils/paths.d.ts.map +1 -1
  78. package/dist/server/utils/paths.js +18 -1
  79. package/dist/server/utils/paths.js.map +1 -1
  80. package/package.json +1 -1
  81. package/server/cli/headless/haiku-assessments.ts +21 -28
  82. package/server/cli/headless/stall-assessor.ts +17 -3
  83. package/server/cli/improvisation-retry.ts +19 -1
  84. package/server/cli/improvisation-session-manager.ts +44 -1
  85. package/server/cli/prompt-builders.ts +34 -23
  86. package/server/mcp/bouncer-haiku.ts +5 -30
  87. package/server/mcp/security-analysis.ts +19 -12
  88. package/server/services/deploy/headless-session-handler.ts +75 -76
  89. package/server/services/files.ts +7 -2
  90. package/server/services/pathUtils.ts +55 -42
  91. package/server/services/plan/agent-loader.ts +73 -15
  92. package/server/services/plan/issue-retry.ts +93 -68
  93. package/server/services/plan/review-gate.ts +13 -89
  94. package/server/services/websocket/file-explorer-handlers.ts +23 -2
  95. package/server/services/websocket/git-handlers.ts +23 -18
  96. package/server/services/websocket/git-pr-handlers.ts +5 -20
  97. package/server/services/websocket/handler.ts +35 -16
  98. package/server/services/websocket/handlers/deploy-handlers.ts +34 -37
  99. package/server/services/websocket/plan-board-handlers.ts +36 -21
  100. package/server/services/websocket/quality-fix-agent.ts +10 -17
  101. package/server/services/websocket/quality-review-agent.ts +12 -149
  102. package/server/services/websocket/session-history.ts +10 -8
  103. package/server/services/websocket/skill-handlers.ts +90 -0
  104. package/server/services/websocket/types.ts +13 -2
  105. package/server/utils/paths.ts +17 -1
@@ -1,6 +1,8 @@
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 { resolve } from 'node:path';
5
+ import { loadSkillPrompt } from '../plan/agent-loader.js';
4
6
  import { handleGitCheckout, handleGitCreateBranch, handleGitDeleteBranch, handleGitListBranches } from './git-branch-handlers.js';
5
7
  import { handleGitCommitDiff, handleGitDiff, handleGitShowCommit } from './git-diff-handlers.js';
6
8
  import { handleGitDiscoverRepos, handleGitLog, handleGitSetDirectory } from './git-log-handlers.js';
@@ -119,6 +121,16 @@ async function handleGitStage(ctx: HandlerContext, ws: WSContext, msg: WebSocket
119
121
  return;
120
122
  }
121
123
 
124
+ if (!stageAll && paths) {
125
+ const resolvedRoot = resolve(workingDir);
126
+ for (const p of paths) {
127
+ if (!resolve(workingDir, p).startsWith(resolvedRoot)) {
128
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: `Path traversal not allowed: ${p}` } });
129
+ return;
130
+ }
131
+ }
132
+ }
133
+
122
134
  try {
123
135
  const args = stageAll ? ['add', '-A'] : ['add', '--', ...paths!];
124
136
  const result = await executeGitCommand(args, workingDir);
@@ -153,12 +165,18 @@ async function handleGitUnstage(ctx: HandlerContext, ws: WSContext, msg: WebSock
153
165
  }
154
166
  }
155
167
 
168
+ const MAX_COMMIT_MESSAGE_LENGTH = 10_000;
169
+
156
170
  async function handleGitCommit(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string): Promise<void> {
157
171
  const message = msg.data?.message as string | undefined;
158
172
  if (!message) {
159
173
  ctx.send(ws, { type: 'gitError', tabId, data: { error: 'Commit message is required' } });
160
174
  return;
161
175
  }
176
+ if (message.length > MAX_COMMIT_MESSAGE_LENGTH) {
177
+ ctx.send(ws, { type: 'gitError', tabId, data: { error: `Commit message too long (${message.length} chars, max ${MAX_COMMIT_MESSAGE_LENGTH})` } });
178
+ return;
179
+ }
162
180
 
163
181
  try {
164
182
  const result = await executeGitCommand(['commit', '-m', message], workingDir);
@@ -195,25 +213,12 @@ async function handleGitCommitWithAI(ctx: HandlerContext, ws: WSContext, msg: We
195
213
  const diffResult = await executeGitCommand(['diff', '--cached'], workingDir);
196
214
  const logResult = await executeGitCommand(['log', '--oneline', '-5'], workingDir);
197
215
 
198
- const prompt = `You are generating a git commit message for the following staged changes.
199
-
200
- RECENT COMMIT MESSAGES (for style reference):
201
- ${logResult.stdout.trim() || 'No recent commits'}
202
-
203
- STAGED FILES:
204
- ${staged.map(f => `${f.status} ${f.path}`).join('\n')}
205
-
206
- DIFF OF STAGED CHANGES:
207
- ${truncateDiff(diffResult.stdout)}
208
-
209
- Generate a commit message following these rules:
210
- 1. First line: imperative mood, max 72 characters (e.g., "Add user authentication", "Fix memory leak in parser")
211
- 2. If the changes are complex, add a blank line then bullet points explaining the key changes
212
- 3. Focus on the "why" not just the "what"
213
- 4. Match the style of recent commits if possible
214
- 5. No emojis unless the repo already uses them
216
+ const recentCommits = logResult.stdout.trim() || 'No recent commits';
217
+ const stagedFiles = staged.map(f => `${f.status} ${f.path}`).join('\n');
218
+ const diff = truncateDiff(diffResult.stdout);
215
219
 
216
- Respond with ONLY the commit message, nothing else.`;
220
+ const prompt = loadSkillPrompt('commit-message', { recentCommits, stagedFiles, diff }, workingDir)
221
+ ?? `You are generating a git commit message for the following staged changes.\n\nRECENT COMMIT MESSAGES (for style reference):\n${recentCommits}\n\nSTAGED FILES:\n${stagedFiles}\n\nDIFF OF STAGED CHANGES:\n${diff}\n\nGenerate a commit message: imperative mood, max 72 characters, focus on "why". Respond with ONLY the commit message.`;
217
222
 
218
223
  const result = await spawnHaikuWithPrompt(
219
224
  prompt,
@@ -1,6 +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 { loadSkillPrompt } from '../plan/agent-loader.js';
4
5
  import { getPrBaseBranch, setPrBaseBranch } from '../settings.js';
5
6
  import { detectGitProvider, executeGitCommand, spawnCheck, spawnHaikuWithPrompt, spawnWithOutput, stripCoauthorLines, truncateDiff } from './git-handlers.js';
6
7
  import type { HandlerContext } from './handler-context.js';
@@ -272,27 +273,11 @@ async function handleGitGeneratePRDescription(ctx: HandlerContext, ws: WSContext
272
273
  const diffResult = await executeGitCommand(['diff', `${compareRef}...HEAD`], workingDir);
273
274
  const statResult = await executeGitCommand(['diff', `${compareRef}...HEAD`, '--stat'], workingDir);
274
275
 
275
- const prompt = `You are generating a pull request title and description for the following changes.
276
+ const filesChanged = statResult.exitCode === 0 ? statResult.stdout.trim() : '';
277
+ const diff = truncateDiff(diffResult.exitCode === 0 ? diffResult.stdout : '');
276
278
 
277
- COMMITS (${baseBranch}..HEAD):
278
- ${commits}
279
-
280
- FILES CHANGED:
281
- ${statResult.exitCode === 0 ? statResult.stdout.trim() : ''}
282
-
283
- DIFF:
284
- ${truncateDiff(diffResult.exitCode === 0 ? diffResult.stdout : '')}
285
-
286
- Generate a pull request title and description following these rules:
287
- 1. TITLE: First line must be the PR title — imperative mood, under 70 characters
288
- 2. Leave a blank line after the title
289
- 3. BODY: Write a concise description in markdown with:
290
- - A "## Summary" section with 1-3 bullet points explaining what changed and why
291
- - Optionally a "## Details" section if the changes are complex
292
- 4. Focus on the "why" not just the "what"
293
- 5. No emojis
294
-
295
- Respond with ONLY the title and description, nothing else.`;
279
+ const prompt = loadSkillPrompt('pr-description', { baseBranch, commits, filesChanged, diff }, workingDir)
280
+ ?? `You are generating a pull request title and description.\n\nCOMMITS (${baseBranch}..HEAD):\n${commits}\n\nFILES CHANGED:\n${filesChanged}\n\nDIFF:\n${diff}\n\nGenerate PR title (imperative, <70 chars) then body with ## Summary (1-3 bullets). No emojis. Respond with ONLY the title and description.`;
296
281
 
297
282
  const result = await spawnHaikuWithPrompt(
298
283
  prompt,
@@ -25,6 +25,7 @@ import { handleQualityMessage } from './quality-handlers.js';
25
25
  import { handleHistoryMessage, handleSessionMessage, initializeTab, resumeHistoricalSession } from './session-handlers.js';
26
26
  import { SessionRegistry } from './session-registry.js';
27
27
  import { generateNotificationSummary, handleGetSettings, handleUpdateSettings } from './settings-handlers.js';
28
+ import { handleListSkills } from './skill-handlers.js';
28
29
  import { handleCreateTab, handleGetActiveTabs, handleMarkTabViewed, handleRemoveTab, handleReorderTabs, handleSyncPromptText, handleSyncTabMeta } from './tab-handlers.js';
29
30
  import { cleanupTerminalSubscribers, handleTerminalMessage } from './terminal-handlers.js';
30
31
  import type { FrecencyData, WebSocketMessage, WebSocketResponse, WSContext } from './types.js';
@@ -53,6 +54,9 @@ export class WebSocketImproviseHandler implements HandlerContext {
53
54
  }
54
55
 
55
56
  getRegistry(workingDir: string): SessionRegistry {
57
+ if (!this.sessionRegistry && workingDir) {
58
+ this.sessionRegistry = new SessionRegistry(workingDir);
59
+ }
56
60
  if (!this.sessionRegistry) {
57
61
  this.sessionRegistry = new SessionRegistry(workingDir);
58
62
  }
@@ -87,9 +91,16 @@ export class WebSocketImproviseHandler implements HandlerContext {
87
91
  }
88
92
  }
89
93
 
94
+ private frecencySaveTimer: ReturnType<typeof setTimeout> | null = null;
95
+
90
96
  recordFileSelection(filePath: string): void {
91
97
  this.autocompleteService.recordFileSelection(filePath);
92
- this.saveFrecencyData();
98
+ if (!this.frecencySaveTimer) {
99
+ this.frecencySaveTimer = setTimeout(() => {
100
+ this.frecencySaveTimer = null;
101
+ this.saveFrecencyData();
102
+ }, 2000);
103
+ }
93
104
  }
94
105
 
95
106
  handleConnection(ws: WSContext, _workingDir: string): void {
@@ -175,6 +186,8 @@ export class WebSocketImproviseHandler implements HandlerContext {
175
186
  return handleGetSettings(this, ws);
176
187
  case 'updateSettings':
177
188
  return handleUpdateSettings(this, ws, msg);
189
+ case 'listSkills':
190
+ return handleListSkills(this, ws, workingDir);
178
191
  }
179
192
 
180
193
  // Dispatch table lookup for domain handlers
@@ -222,28 +235,14 @@ export class WebSocketImproviseHandler implements HandlerContext {
222
235
  }
223
236
 
224
237
  handleClose(ws: WSContext): void {
225
- // Destroy sessions owned by this connection to free interval timers
226
238
  const tabMap = this.connections.get(ws);
227
239
  if (tabMap) {
228
- const sessionIds = new Set(tabMap.values());
229
- for (const sessionId of sessionIds) {
230
- const session = this.sessions.get(sessionId);
231
- if (session) {
232
- session.destroy();
233
- this.sessions.delete(sessionId);
234
- }
235
- }
240
+ this.cleanupConnectionResources(tabMap);
236
241
  }
237
242
  this.connections.delete(ws);
238
243
  this.allConnections.delete(ws);
239
244
  cleanupTerminalSubscribers(this, ws);
240
245
 
241
- // Kill any active search processes to prevent resource leaks
242
- for (const [key, process] of this.activeSearches) {
243
- try { process.kill(); } catch { /* ignore */ }
244
- this.activeSearches.delete(key);
245
- }
246
-
247
246
  // Clean up file upload handler when no connections remain
248
247
  if (this.allConnections.size === 0 && this.fileUploadHandler) {
249
248
  this.fileUploadHandler.destroy();
@@ -251,6 +250,26 @@ export class WebSocketImproviseHandler implements HandlerContext {
251
250
  }
252
251
  }
253
252
 
253
+ private cleanupConnectionResources(tabMap: Map<string, string>): void {
254
+ // Destroy sessions owned by this connection
255
+ const sessionIds = new Set(tabMap.values());
256
+ for (const sessionId of sessionIds) {
257
+ const session = this.sessions.get(sessionId);
258
+ if (session) {
259
+ session.destroy();
260
+ this.sessions.delete(sessionId);
261
+ }
262
+ }
263
+ // Kill search processes owned by this connection's tabs
264
+ for (const tabId of tabMap.keys()) {
265
+ const searchProcess = this.activeSearches.get(tabId);
266
+ if (searchProcess) {
267
+ try { searchProcess.kill(); } catch { /* ignore */ }
268
+ this.activeSearches.delete(tabId);
269
+ }
270
+ }
271
+ }
272
+
254
273
  send(ws: WSContext, response: WebSocketResponse): void {
255
274
  try {
256
275
  ws.send(JSON.stringify(response));
@@ -111,6 +111,35 @@ function sendChunkedResponse(
111
111
  }
112
112
  }
113
113
 
114
+ /** Validate the incoming deploy HTTP request data. Returns an error response body string or null if valid. */
115
+ function validateDeployRequest(
116
+ data: DeployHttpRequestData,
117
+ ): { status: number; body: string } | null {
118
+ if (!data?.requestId || !data?.method || !data?.url || !data?.port) {
119
+ return { status: 400, body: 'Bad Request: missing required fields (requestId, method, url, port)' };
120
+ }
121
+ if (data.headers && containsHeaderInjection(data.headers)) {
122
+ return { status: 400, body: 'Bad Request: headers contain null bytes or CRLF injection' };
123
+ }
124
+ if (data.headers && calculateHeaderSize(data.headers) > MAX_HEADER_SIZE_BYTES) {
125
+ return { status: 431, body: 'Request Header Fields Too Large: total headers exceed 16KB' };
126
+ }
127
+ return null;
128
+ }
129
+
130
+ /** Classify a fetch error into an HTTP status code and message. */
131
+ function classifyFetchError(error: unknown): { status: number; body: string } {
132
+ if (error instanceof Error) {
133
+ if (error.name === 'AbortError') {
134
+ return { status: 504, body: 'Gateway Timeout' };
135
+ }
136
+ if (isConnectionRefused(error)) {
137
+ return { status: 502, body: 'Bad Gateway: target server is not running' };
138
+ }
139
+ }
140
+ return { status: 502, body: 'Bad Gateway' };
141
+ }
142
+
114
143
  export async function handleDeployHttpRequest(
115
144
  ctx: HandlerContext,
116
145
  ws: WSContext,
@@ -118,34 +147,13 @@ export async function handleDeployHttpRequest(
118
147
  ): Promise<void> {
119
148
  const data = msg.data as DeployHttpRequestData;
120
149
 
121
- if (!data?.requestId || !data?.method || !data?.url || !data?.port) {
150
+ const validationError = validateDeployRequest(data);
151
+ if (validationError) {
122
152
  sendDeployHttpResponse(ctx, ws, {
123
153
  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,
154
+ status: validationError.status,
147
155
  headers: {},
148
- body: 'Request Header Fields Too Large: total headers exceed 16KB',
156
+ body: validationError.body,
149
157
  });
150
158
  return;
151
159
  }
@@ -201,18 +209,7 @@ export async function handleDeployHttpRequest(
201
209
  body: bodyBuffer.toString('utf-8'),
202
210
  });
203
211
  } catch (error: unknown) {
204
- let status = 502;
205
- let body = 'Bad Gateway';
206
-
207
- if (error instanceof Error) {
208
- if (error.name === 'AbortError') {
209
- status = 504;
210
- body = 'Gateway Timeout';
211
- } else if (isConnectionRefused(error)) {
212
- status = 502;
213
- body = 'Bad Gateway: target server is not running';
214
- }
215
- }
212
+ const { status, body } = classifyFetchError(error);
216
213
 
217
214
  sendDeployHttpResponse(ctx, ws, {
218
215
  requestId: data.requestId,
@@ -97,6 +97,40 @@ paused: false
97
97
  }
98
98
  }
99
99
 
100
+ /** Update front-matter fields in the board.md file. */
101
+ function applyBoardFieldUpdates(
102
+ boardMdPath: string,
103
+ fields: Record<string, unknown>,
104
+ ): void {
105
+ let content = readFileSync(boardMdPath, 'utf-8');
106
+ for (const [key, value] of Object.entries(fields)) {
107
+ const yamlKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
108
+ content = replaceFrontMatterField(content, yamlKey, formatYamlValue(value));
109
+ }
110
+ writeFileSync(boardMdPath, content, 'utf-8');
111
+ }
112
+
113
+ /** Sync the review-custom agent file when review criteria change. */
114
+ function syncReviewCriteriaAgent(
115
+ fields: Record<string, unknown>,
116
+ pmDir: string,
117
+ boardId: string,
118
+ ): void {
119
+ if (!('reviewCriteria' in fields)) return;
120
+
121
+ const boardDir = join(pmDir, 'boards', boardId);
122
+ const agentsDir = join(boardDir, 'agents');
123
+ const agentPath = join(agentsDir, 'review-custom.md');
124
+ const criteriaValue = String(fields.reviewCriteria ?? '').trim();
125
+
126
+ if (criteriaValue) {
127
+ if (!existsSync(agentsDir)) mkdirSync(agentsDir, { recursive: true });
128
+ writeFileSync(agentPath, buildBoardReviewAgent(criteriaValue), 'utf-8');
129
+ } else if (existsSync(agentPath)) {
130
+ try { unlinkSync(agentPath); } catch { /* non-fatal */ }
131
+ }
132
+ }
133
+
100
134
  export function handleUpdateBoard(
101
135
  ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
102
136
  workingDir: string, permission?: 'view',
@@ -118,30 +152,11 @@ export function handleUpdateBoard(
118
152
  return;
119
153
  }
120
154
 
121
- let content = readFileSync(boardMdPath, 'utf-8');
122
- for (const [key, value] of Object.entries(fields as Record<string, unknown>)) {
123
- const yamlKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
124
- content = replaceFrontMatterField(content, yamlKey, formatYamlValue(value));
125
- }
126
- writeFileSync(boardMdPath, content, 'utf-8');
155
+ applyBoardFieldUpdates(boardMdPath, fields as Record<string, unknown>);
127
156
 
128
157
  // When review criteria are set, also write a board-level review agent file
129
158
  // 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
- }
159
+ syncReviewCriteriaAgent(fields as Record<string, unknown>, pmDir, boardId);
145
160
 
146
161
  const boardState = parseBoardDirectory(pmDir, boardId);
147
162
  if (boardState) {
@@ -10,6 +10,7 @@
10
10
  import { runWithFileLogger } from '../../cli/headless/headless-logger.js';
11
11
  import { HeadlessRunner } from '../../cli/headless/index.js';
12
12
  import type { ToolUseEvent } from '../../cli/headless/types.js';
13
+ import { loadSkillPrompt } from '../plan/agent-loader.js';
13
14
  import type { HandlerContext } from './handler-context.js';
14
15
  import type { QualityPersistence } from './quality-persistence.js';
15
16
  import { detectTools, runQualityScan } from './quality-service.js';
@@ -58,7 +59,7 @@ export function createToolProgressCallback(ctx: HandlerContext, ws: WSContext, r
58
59
 
59
60
  // ── Prompt ────────────────────────────────────────────────────
60
61
 
61
- function buildFixPrompt(findings: FindingForFix[], section?: string): string {
62
+ function buildFixPrompt(findings: FindingForFix[], section?: string, workingDir?: string): string {
62
63
  const filtered = section ? findings.filter((f) => f.category === section) : findings;
63
64
  const sorted = filtered.sort((a, b) => {
64
65
  const order: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
@@ -73,22 +74,14 @@ function buildFixPrompt(findings: FindingForFix[], section?: string): string {
73
74
  return parts.join('\n');
74
75
  }).join('\n\n');
75
76
 
76
- return `You are a code quality fix agent. Fix the following quality issues in the codebase.
77
+ const fromSkill = loadSkillPrompt('fix-quality', {
78
+ issueList,
79
+ issueCount: String(sorted.length),
80
+ showCount: String(Math.min(30, sorted.length)),
81
+ }, workingDir);
82
+ if (fromSkill) return fromSkill;
77
83
 
78
- ## Issues to Fix (${sorted.length} total, showing top ${Math.min(30, sorted.length)})
79
-
80
- ${issueList}
81
-
82
- ## Rules
83
-
84
- - Fix each issue by editing the relevant file at the specified location.
85
- - For complexity issues: refactor into smaller functions. For long files: split or extract modules. For long functions: break into smaller functions.
86
- - For security issues: apply the suggested fix or use secure coding best practices.
87
- - For bugs: fix the root cause, not just the symptom.
88
- - For linting/formatting: apply the standard for the project.
89
- - Do NOT introduce new issues. Make minimal, focused changes.
90
- - After fixing, verify the changes compile/pass linting if tools are available.
91
- - Work through the issues systematically from most to least severe.`;
84
+ return `You are a code quality fix agent. Fix the following quality issues in the codebase.\n\n## Issues to Fix (${sorted.length} total, showing top ${Math.min(30, sorted.length)})\n\n${issueList}\n\nFix each issue by editing the relevant file. Work from most to least severe. Do NOT introduce new issues.`;
92
85
  }
93
86
 
94
87
  // ── Handler ───────────────────────────────────────────────────
@@ -128,7 +121,7 @@ export async function handleFixIssues(
128
121
  data: { path: reportPath, message: 'Starting Claude Code to fix issues...' },
129
122
  });
130
123
 
131
- const prompt = buildFixPrompt(findings, section);
124
+ const prompt = buildFixPrompt(findings, section, workingDir);
132
125
 
133
126
  const runner = new HeadlessRunner({
134
127
  workingDir: dirPath,
@@ -12,6 +12,7 @@ import { isAbsolute, join } from 'node:path';
12
12
  import { runWithFileLogger } from '../../cli/headless/headless-logger.js';
13
13
  import { HeadlessRunner } from '../../cli/headless/index.js';
14
14
  import type { ToolUseEvent } from '../../cli/headless/types.js';
15
+ import { loadSkillPrompt } from '../plan/agent-loader.js';
15
16
  import type { HandlerContext } from './handler-context.js';
16
17
  import type { QualityPersistence } from './quality-persistence.js';
17
18
  import { recomputeWithAiReview } from './quality-service.js';
@@ -39,106 +40,11 @@ export function buildCodeReviewPrompt(dirPath: string, cliFindings?: Array<{ sev
39
40
  ? `\n## CLI Tool Findings (already detected)\n\nThe following issues were found by automated CLI tools (linters, formatters, complexity analyzers). Review these for context — they are already included in the final report. Focus your analysis on DEEPER issues these tools cannot detect.\n\n${cliFindings.slice(0, 50).map((f, i) => `${i + 1}. [${f.severity.toUpperCase()}] ${f.category} — ${f.file}${f.line ? `:${f.line}` : ''} — ${f.title}: ${f.description}`).join('\n')}\n${cliFindings.length > 50 ? `\n...and ${cliFindings.length - 50} more issues from CLI tools.\n` : ''}`
40
41
  : '';
41
42
 
42
- return `You are a senior staff engineer performing a rigorous, honest code review. Your job is to surface the most impactful quality bottlenecks — the issues a principal engineer would flag in a code review. Be critical and objective. Do NOT inflate scores.
43
-
44
- IMPORTANT: Your current working directory is "${dirPath}". Only review files within this directory.
45
- ${cliFindingsSection}
46
- ## Review Process
47
-
48
- 1. **Discover**: Use Glob to find source files (e.g. "**/*.{ts,tsx,js,py,rs,go,java,rb,php}"). Understand the project structure.
49
- 2. **Read**: Read the most important files — entry points, core modules, handlers, services. Prioritize files with recent git changes (\`git diff --name-only HEAD~5\` via Bash if available).
50
- 3. **Analyze**: Look for real, actionable issues across ALL of these categories:
51
-
52
- ### Architecture
53
- - What is the current architecture (monolith, microservices, layered, etc.)?
54
- - Are there architectural violations? (e.g., presentation layer directly accessing data layer, circular dependencies between modules)
55
- - Is there proper separation of concerns?
56
- - Are there god objects or god modules that do too much?
57
-
58
- ### SOLID / OOP Principles
59
- - **SRP**: Classes/modules with multiple unrelated responsibilities
60
- - **OCP**: Code that requires modification instead of extension for new features
61
- - **LSP**: Subtypes that don't properly substitute for their base types
62
- - **ISP**: Interfaces/contracts that force implementations to depend on methods they don't use
63
- - **DIP**: High-level modules directly depending on low-level modules instead of abstractions
64
-
65
- ### Security
66
- - Injection vulnerabilities (SQL, XSS, command), hardcoded secrets/credentials, auth bypasses, insecure crypto, path traversal, SSRF, unsafe deserialization
67
-
68
- ### Bugs & Logic
69
- - Null/undefined errors, race conditions, logic errors, unhandled edge cases, off-by-one errors, resource leaks, incorrect error handling, incorrect algorithms
70
-
71
- ### Performance
72
- - N+1 queries, unnecessary re-renders, missing memoization, blocking I/O in hot paths, unbounded data structures, missing pagination
73
-
74
- ## CRITICAL — Structured Evidence Requirement
75
-
76
- For EACH finding, you MUST provide structured evidence that grounds the finding in actual code. This is required to prevent false positives.
77
-
78
- For each finding, use this reasoning process:
79
-
80
- 1. **PREMISE**: State the observable fact from the code. Quote the exact code you see.
81
- 2. **CONTEXT**: What is the surrounding code doing? Are there guards, fixes, or patterns elsewhere that might handle this?
82
- 3. **COUNTER-CHECK**: Actively look for evidence that CONTRADICTS your finding. Check for:
83
- - Guards or validation earlier in the call chain
84
- - Error handling wrapping the code
85
- - Configuration that changes behavior (e.g., NODE_ENV checks)
86
- - Comments explaining intentional design choices
87
- 4. **CONCLUSION**: Only report the finding if you could not find contradicting evidence.
88
-
89
- ### Common False Positive Patterns to AVOID
90
-
91
- - Claiming a function uses API X when it actually uses API Y (e.g., claiming Math.random() when code uses crypto.randomInt()) — ALWAYS quote the actual function call
92
- - Claiming a header/value is leaked when code already deletes/filters it — READ the full function
93
- - Claiming there's no guard when a condition check exists nearby — READ surrounding lines
94
- - Claiming N fields/methods when the actual count differs — COUNT explicitly
95
- - Claiming a resource leaks when cleanup exists in a different handler — SEARCH for the cleanup code
96
-
97
- ## Rules
98
-
99
- - Only report findings you are >90% confident about after completing the counter-check step.
100
- - Focus on architecture, SOLID violations, bugs, and security over style nits.
101
- - Each finding MUST reference a specific file and line number. Do not report vague or file-level issues.
102
- - Each finding MUST include an "evidence" field with the exact code snippet (1-5 lines) proving the issue exists.
103
- - Limit to the 25 most important findings, ranked by severity.
104
- - Do NOT modify any files. This is a read-only review.
105
- - Be HONEST about the overall quality. A codebase with serious issues should score low.
106
-
107
- ## Scoring Guidelines
108
-
109
- After your analysis, provide an honest overall quality score (0-100) and letter grade:
110
- - **A (90-100)**: Excellent — clean architecture, minimal issues, well-tested, follows best practices
111
- - **B (80-89)**: Good — solid code with minor issues, mostly well-structured
112
- - **C (70-79)**: Adequate — functional but has notable quality issues that should be addressed
113
- - **D (60-69)**: Below average — significant issues in architecture, testing, or code quality
114
- - **F (0-59)**: Poor — serious problems: security vulnerabilities, broken architecture, major bugs, or unmaintainable code
115
-
116
- Consider ALL findings (both CLI tool findings and your own) when determining the score. The score should reflect the overall state of the codebase honestly. A project with 50+ linting errors, formatting issues, complex functions, AND architectural problems should NOT score above 70.
117
-
118
- ## Output
119
-
120
- After your analysis, output EXACTLY one JSON code block with your findings. No other text after the JSON block.
121
-
122
- \`\`\`json
123
- {
124
- "score": 72,
125
- "grade": "C",
126
- "scoreRationale": "Brief explanation of why this score was given, referencing key issues",
127
- "findings": [
128
- {
129
- "severity": "critical|high|medium|low",
130
- "category": "architecture|oop|security|bugs|performance|logic",
131
- "file": "relative/path/to/file.ts",
132
- "line": 42,
133
- "title": "Short title describing the issue",
134
- "description": "What the problem is and why it matters.",
135
- "suggestion": "How to fix it.",
136
- "evidence": "const token = Math.random().toString(36) // exact code from file proving the issue"
137
- }
138
- ],
139
- "summary": "Brief 1-2 sentence summary of overall code quality."
140
- }
141
- \`\`\``;
43
+ const fromSkill = loadSkillPrompt('code-review', { dirPath, cliFindingsSection }, dirPath);
44
+ if (fromSkill) return fromSkill;
45
+
46
+ // Inline fallback when Skill file is not available (e.g., standalone CLI install)
47
+ return `You are a senior staff engineer performing a rigorous code review.\n\nIMPORTANT: Your current working directory is "${dirPath}". Only review files within this directory.\n${cliFindingsSection}\nDiscover source files with Glob, read important files, analyze for architecture, SOLID, security, bugs, and performance issues. Each finding needs file, line, evidence. Output one JSON code block with score, grade, findings array, and summary.`;
142
48
  }
143
49
 
144
50
  // ── Response parsing ──────────────────────────────────────────
@@ -343,7 +249,7 @@ export function buildVerificationPrompt(
343
249
  dirPath: string,
344
250
  findings: CodeReviewFinding[],
345
251
  ): string {
346
- const findingsJson = findings.map((f, i) => ({
252
+ const findingsJson = JSON.stringify(findings.map((f, i) => ({
347
253
  id: i + 1,
348
254
  severity: f.severity,
349
255
  category: f.category,
@@ -352,56 +258,13 @@ export function buildVerificationPrompt(
352
258
  title: f.title,
353
259
  description: f.description,
354
260
  evidence: f.evidence || '(none provided)',
355
- }));
356
-
357
- return `You are an independent code review VERIFIER. A separate reviewer produced the findings below. Your job is to VERIFY each finding against the actual code. You are a skeptic — do NOT trust the original reviewer's claims.
358
-
359
- IMPORTANT: Your current working directory is "${dirPath}". Only read files within this directory.
360
-
361
- ## Findings to Verify
362
-
363
- ${JSON.stringify(findingsJson, null, 2)}
364
-
365
- ## Verification Process
261
+ })), null, 2);
366
262
 
367
- For EACH finding:
263
+ const fromSkill = loadSkillPrompt('verify-review', { dirPath, findingsJson }, dirPath);
264
+ if (fromSkill) return fromSkill;
368
265
 
369
- 1. **Read the cited file and line** using the Read tool. Read at least 20 lines around the cited line for context.
370
- 2. **Check the specific claim** in the description. Does the code actually do what the finding claims?
371
- 3. **Search for counter-evidence**:
372
- - If the finding claims something is missing (no validation, no cleanup, no guard): search for it with Grep
373
- - If the finding claims an API is used: verify the actual API call at that line
374
- - If the finding claims a value is leaked/exposed: check if it's filtered/deleted elsewhere in the same function
375
- 4. **Verdict**: Mark as "confirmed" or "rejected" with a brief explanation
376
-
377
- ## Rules
378
-
379
- - You MUST actually Read each cited file. Do not rely on memory or assumptions.
380
- - Use Grep to search for patterns the finding claims exist (or don't exist).
381
- - A finding is "rejected" if:
382
- - The code does NOT match what the description claims
383
- - There IS a guard/fix that the finding claims is missing
384
- - The line number doesn't contain the relevant code
385
- - The finding is about a different version of the code than what exists now
386
- - A finding is "confirmed" if you can independently verify the issue exists in the current code.
387
- - Be thorough but efficient — focus verification effort on high/critical severity findings.
388
-
389
- ## Output
390
-
391
- Output EXACTLY one JSON code block. No other text after the JSON block.
392
-
393
- \`\`\`json
394
- {
395
- "verifications": [
396
- {
397
- "id": 1,
398
- "verdict": "confirmed|rejected",
399
- "confidence": 0.95,
400
- "note": "Brief explanation of what you found when checking the code"
401
- }
402
- ]
403
- }
404
- \`\`\``;
266
+ // Inline fallback
267
+ return `You are an independent code review VERIFIER. Verify each finding below against actual code in "${dirPath}".\n\n## Findings to Verify\n\n${findingsJson}\n\nFor each finding: Read the cited file, check the claim, search for counter-evidence. Output one JSON code block with verifications array containing id, verdict (confirmed|rejected), confidence, and note.`;
405
268
  }
406
269
 
407
270
  interface VerificationVerdict {