mstro-app 0.4.33 → 0.4.35
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/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.js +63 -0
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.js +10 -5
- package/dist/server/cli/headless/haiku-assessments.js.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +17 -2
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +1 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +13 -5
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +1 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +2 -2
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +3 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +8 -3
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +15 -0
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +5 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/websocket/git-head-watcher.d.ts +25 -0
- package/dist/server/services/websocket/git-head-watcher.d.ts.map +1 -0
- package/dist/server/services/websocket/git-head-watcher.js +136 -0
- package/dist/server/services/websocket/git-head-watcher.js.map +1 -0
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +84 -13
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts +2 -0
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +3 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +18 -6
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.d.ts +1 -0
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.js +94 -0
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +4 -2
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js +3 -1
- package/dist/server/services/websocket/plan-handlers.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 +9 -0
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-persistence.d.ts +7 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +15 -7
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +2 -13
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +12 -3
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +101 -81
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +6 -1
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +15 -2
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +2 -2
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +9 -2
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +2 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/server/cli/headless/claude-invoker-stream.ts +63 -0
- package/server/cli/headless/haiku-assessments.ts +10 -5
- package/server/cli/improvisation-retry.ts +18 -2
- package/server/cli/improvisation-session-manager.ts +13 -5
- package/server/cli/improvisation-types.ts +1 -0
- package/server/services/plan/agents/assess-stall.md +21 -0
- package/server/services/plan/agents/check-injection.md +36 -0
- package/server/services/plan/agents/classify-error.md +29 -0
- package/server/services/plan/agents/detect-context-loss.md +29 -0
- package/server/services/plan/agents/execute-issue.md +42 -0
- package/server/services/plan/agents/plan-coordinator.md +71 -0
- package/server/services/plan/agents/retry-task.md +26 -0
- package/server/services/plan/agents/review-code.md +4 -1
- package/server/services/plan/agents/review-criteria.md +53 -0
- package/server/services/plan/agents/review-custom.md +4 -1
- package/server/services/plan/agents/review-quality.md +4 -1
- package/server/services/plan/agents/verify-review.md +56 -0
- package/server/services/plan/composer.ts +2 -1
- package/server/services/plan/executor.ts +8 -3
- package/server/services/plan/parser-core.ts +14 -0
- package/server/services/plan/types.ts +6 -0
- package/server/services/websocket/git-head-watcher.ts +120 -0
- package/server/services/websocket/git-worktree-handlers.ts +85 -15
- package/server/services/websocket/handler-context.ts +2 -0
- package/server/services/websocket/handler.ts +19 -6
- package/server/services/websocket/plan-board-handlers.ts +116 -0
- package/server/services/websocket/plan-execution-handlers.ts +4 -2
- package/server/services/websocket/plan-handlers.ts +3 -1
- package/server/services/websocket/plan-issue-handlers.ts +10 -0
- package/server/services/websocket/quality-persistence.ts +23 -7
- package/server/services/websocket/quality-review-agent.ts +2 -12
- package/server/services/websocket/quality-service.ts +116 -99
- package/server/services/websocket/quality-tools.ts +6 -1
- package/server/services/websocket/quality-types.ts +17 -2
- package/server/services/websocket/session-handlers.ts +2 -2
- package/server/services/websocket/tab-handlers.ts +8 -2
- package/server/services/websocket/types.ts +7 -2
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { handlePlanPrompt } from '../plan/composer.js';
|
|
6
7
|
import { replaceFrontMatterField } from '../plan/front-matter.js';
|
|
7
8
|
import { getNextBoardId, getNextBoardNumber, parseBoardArtifacts, parseBoardDirectory, parsePlanDirectory, resolvePmDir } from '../plan/parser.js';
|
|
8
9
|
import type { Workspace } from '../plan/types.js';
|
|
@@ -309,6 +310,121 @@ export function handleGetBoardArtifacts(
|
|
|
309
310
|
ctx.send(ws, { type: 'planBoardArtifacts', data: artifacts });
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Chat-to-board: create board from conversation and run prompt
|
|
315
|
+
// ============================================================================
|
|
316
|
+
|
|
317
|
+
export function handleChatToBoard(
|
|
318
|
+
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
319
|
+
workingDir: string, permission?: 'view',
|
|
320
|
+
): void {
|
|
321
|
+
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
322
|
+
|
|
323
|
+
const { conversation, autoImplement, focusHint } = (msg.data || {}) as {
|
|
324
|
+
conversation?: string;
|
|
325
|
+
autoImplement?: boolean;
|
|
326
|
+
focusHint?: string;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
if (!conversation) {
|
|
330
|
+
ctx.send(ws, { type: 'planError', data: { error: 'Conversation text is required' } });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const pmDir = resolvePmDir(workingDir);
|
|
335
|
+
if (!pmDir) {
|
|
336
|
+
ctx.send(ws, { type: 'planError', data: { error: 'No PM directory found. Run planScaffold first.' } });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const fullState = parsePlanDirectory(workingDir);
|
|
341
|
+
if (!fullState) {
|
|
342
|
+
ctx.send(ws, { type: 'planError', data: { error: 'Failed to parse PM directory' } });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const boardId = getNextBoardId(fullState.boards);
|
|
347
|
+
const boardNum = getNextBoardNumber(fullState.boards);
|
|
348
|
+
const title = `Board ${boardNum}`;
|
|
349
|
+
const boardDir = join(pmDir, 'boards', boardId);
|
|
350
|
+
|
|
351
|
+
for (const dir of ['backlog', 'out', 'reviews', 'logs']) {
|
|
352
|
+
mkdirSync(join(boardDir, dir), { recursive: true });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const today = new Date().toISOString().split('T')[0];
|
|
356
|
+
const goalLine = focusHint || 'Generated from chat conversation';
|
|
357
|
+
|
|
358
|
+
writeFileSync(join(boardDir, 'board.md'), `---
|
|
359
|
+
id: ${boardId}
|
|
360
|
+
title: "${title}"
|
|
361
|
+
status: draft
|
|
362
|
+
created: "${today}"
|
|
363
|
+
completed_at: null
|
|
364
|
+
goal: "${goalLine.replace(/"/g, '\\"')}"
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
# ${title}
|
|
368
|
+
|
|
369
|
+
## Goal
|
|
370
|
+
${goalLine}
|
|
371
|
+
|
|
372
|
+
## Notes
|
|
373
|
+
`, 'utf-8');
|
|
374
|
+
|
|
375
|
+
writeFileSync(join(boardDir, 'STATE.md'), `---
|
|
376
|
+
project: ../../project.md
|
|
377
|
+
board: board.md
|
|
378
|
+
paused: false
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
# Board State
|
|
382
|
+
|
|
383
|
+
## Ready to Work
|
|
384
|
+
|
|
385
|
+
## In Progress
|
|
386
|
+
|
|
387
|
+
## Blocked
|
|
388
|
+
|
|
389
|
+
## Recently Completed
|
|
390
|
+
|
|
391
|
+
## Warnings
|
|
392
|
+
`, 'utf-8');
|
|
393
|
+
|
|
394
|
+
writeFileSync(join(boardDir, 'progress.md'), '# Board Progress\n', 'utf-8');
|
|
395
|
+
|
|
396
|
+
const wsPath = join(pmDir, 'workspace.json');
|
|
397
|
+
if (!existsSync(wsPath)) {
|
|
398
|
+
writeFileSync(wsPath, JSON.stringify({ activeBoardId: null, boardOrder: [] }, null, 2), 'utf-8');
|
|
399
|
+
}
|
|
400
|
+
const workspace: Workspace = JSON.parse(readFileSync(wsPath, 'utf-8'));
|
|
401
|
+
workspace.boardOrder.push(boardId);
|
|
402
|
+
workspace.activeBoardId = boardId;
|
|
403
|
+
writeFileSync(wsPath, JSON.stringify(workspace, null, 2), 'utf-8');
|
|
404
|
+
|
|
405
|
+
const boardState = parseBoardDirectory(pmDir, boardId);
|
|
406
|
+
if (boardState) {
|
|
407
|
+
ctx.broadcastToAll({ type: 'planBoardCreated', data: boardState.board });
|
|
408
|
+
ctx.broadcastToAll({ type: 'planWorkspaceUpdated', data: workspace });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
ctx.send(ws, {
|
|
412
|
+
type: 'chatToBoardCreated',
|
|
413
|
+
data: { boardId, autoImplement: !!autoImplement },
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
let prompt = conversation;
|
|
417
|
+
if (focusHint) {
|
|
418
|
+
prompt = `Focus on: ${focusHint}\n\n${conversation}`;
|
|
419
|
+
}
|
|
420
|
+
handlePlanPrompt(ctx, ws, prompt, workingDir, boardId).catch(error => {
|
|
421
|
+
ctx.send(ws, {
|
|
422
|
+
type: 'planError',
|
|
423
|
+
data: { error: error instanceof Error ? error.message : String(error) },
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
312
428
|
// ── Private helpers ──────────────────────────────────────────────────
|
|
313
429
|
|
|
314
430
|
/** Build a board-level review-custom agent file from user-provided criteria. */
|
|
@@ -24,7 +24,8 @@ export function handlePrompt(
|
|
|
24
24
|
ctx.send(ws, { type: 'planError', data: { error: 'Prompt required' } });
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
const executionDir = boardId ? ctx.gitDirectories.get(boardId) : undefined;
|
|
28
|
+
handlePlanPrompt(ctx, ws, prompt, workingDir, boardId, executionDir).catch(error => {
|
|
28
29
|
ctx.send(ws, {
|
|
29
30
|
type: 'planError',
|
|
30
31
|
data: { error: error instanceof Error ? error.message : String(error) },
|
|
@@ -109,8 +110,9 @@ export function handleExecute(
|
|
|
109
110
|
wireExecutorEvents(executor, ctx, workingDir);
|
|
110
111
|
|
|
111
112
|
const boardId = msg.data?.boardId as string | undefined;
|
|
113
|
+
const executionDir = boardId ? ctx.gitDirectories.get(boardId) : undefined;
|
|
112
114
|
ctx.send(ws, { type: 'planExecutionStarted', data: { status: 'executing', boardId } });
|
|
113
|
-
const startPromise = boardId ? executor.startBoard(boardId) : executor.start();
|
|
115
|
+
const startPromise = boardId ? executor.startBoard(boardId, executionDir) : executor.start();
|
|
114
116
|
startPromise.catch(error => {
|
|
115
117
|
ctx.send(ws, {
|
|
116
118
|
type: 'planExecutionError',
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { HandlerContext } from './handler-context.js';
|
|
12
|
-
import { handleArchiveBoard, handleCreateBoard, handleGetBoard, handleGetBoardArtifacts, handleGetBoardState, handleReorderBoards, handleSetActiveBoard, handleUpdateBoard } from './plan-board-handlers.js';
|
|
12
|
+
import { handleArchiveBoard, handleChatToBoard, handleCreateBoard, handleGetBoard, handleGetBoardArtifacts, handleGetBoardState, handleReorderBoards, handleSetActiveBoard, handleUpdateBoard } from './plan-board-handlers.js';
|
|
13
13
|
import { handleExecute, handleExecuteEpic, handlePause, handlePrompt, handleResume, handleStop } from './plan-execution-handlers.js';
|
|
14
14
|
import { handleCreateIssue, handleDeleteIssue, handleGetIssue, handleGetMilestone, handleGetSprint, handleListIssues, handlePlanInit, handleScaffold, handleUpdateIssue } from './plan-issue-handlers.js';
|
|
15
15
|
import { handleActivateSprint, handleCompleteSprint, handleCreateSprint, handleGetSprintArtifacts } from './plan-sprint-handlers.js';
|
|
@@ -52,6 +52,8 @@ export function handlePlanMessage(
|
|
|
52
52
|
planReorderBoards: () => handleReorderBoards(ctx, ws, msg, workingDir, permission),
|
|
53
53
|
planSetActiveBoard: () => handleSetActiveBoard(ctx, ws, msg, workingDir, permission),
|
|
54
54
|
planGetBoardArtifacts: () => handleGetBoardArtifacts(ctx, ws, msg, workingDir),
|
|
55
|
+
// Chat-to-board (from /board and /ship skills)
|
|
56
|
+
chatToBoard: () => handleChatToBoard(ctx, ws, msg, workingDir, permission),
|
|
55
57
|
// Sprint lifecycle (legacy)
|
|
56
58
|
planCreateSprint: () => handleCreateSprint(ctx, ws, msg, workingDir, permission),
|
|
57
59
|
planActivateSprint: () => handleActivateSprint(ctx, ws, msg, workingDir, permission),
|
|
@@ -26,6 +26,16 @@ export function handlePlanInit(ctx: HandlerContext, ws: WSContext, workingDir: s
|
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Restore board worktree assignments into runtime maps
|
|
30
|
+
if (fullState.workspace?.boardWorktrees) {
|
|
31
|
+
for (const [boardId, entry] of Object.entries(fullState.workspace.boardWorktrees)) {
|
|
32
|
+
if (!ctx.gitDirectories.has(boardId)) {
|
|
33
|
+
ctx.gitDirectories.set(boardId, entry.path);
|
|
34
|
+
ctx.gitBranches.set(boardId, entry.branch);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
ctx.send(ws, { type: 'planState', data: fullState });
|
|
30
40
|
|
|
31
41
|
const watcher = getWatcher(workingDir, ctx);
|
|
@@ -34,10 +34,18 @@ export interface HistoryDirectoryEntry {
|
|
|
34
34
|
grade: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export interface HistoryCategoryScore {
|
|
38
|
+
category: string;
|
|
39
|
+
score: number;
|
|
40
|
+
grade: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
37
43
|
export interface QualityHistoryEntry {
|
|
38
44
|
timestamp: string;
|
|
39
45
|
overall: number;
|
|
40
46
|
grade: string;
|
|
47
|
+
issueDensity?: number;
|
|
48
|
+
categoryScores?: HistoryCategoryScore[];
|
|
41
49
|
directories: HistoryDirectoryEntry[];
|
|
42
50
|
}
|
|
43
51
|
|
|
@@ -187,12 +195,10 @@ export class QualityPersistence {
|
|
|
187
195
|
appendHistory(results: QualityResults, dirPath: string): void {
|
|
188
196
|
const history = this.loadHistory();
|
|
189
197
|
|
|
190
|
-
// Find or create entry for this timestamp batch
|
|
191
|
-
// If the last entry was within 60 seconds, merge into it (for multi-dir scans)
|
|
192
198
|
const now = new Date();
|
|
193
199
|
const lastEntry = history[history.length - 1];
|
|
194
200
|
const lastTime = lastEntry ? new Date(lastEntry.timestamp).getTime() : 0;
|
|
195
|
-
const mergeWindow = 60_000;
|
|
201
|
+
const mergeWindow = 60_000;
|
|
196
202
|
|
|
197
203
|
const dirEntry: HistoryDirectoryEntry = {
|
|
198
204
|
path: dirPath,
|
|
@@ -200,30 +206,40 @@ export class QualityPersistence {
|
|
|
200
206
|
grade: results.grade,
|
|
201
207
|
};
|
|
202
208
|
|
|
209
|
+
const categoryScores: HistoryCategoryScore[] | undefined = results.scoreBreakdown
|
|
210
|
+
? results.scoreBreakdown.categoryPenalties.map((cp) => ({
|
|
211
|
+
category: cp.category,
|
|
212
|
+
score: cp.score,
|
|
213
|
+
grade: cp.grade,
|
|
214
|
+
}))
|
|
215
|
+
: undefined;
|
|
216
|
+
|
|
217
|
+
const issueDensity = results.scoreBreakdown?.issueDensity;
|
|
218
|
+
|
|
203
219
|
if (lastEntry && now.getTime() - lastTime < mergeWindow) {
|
|
204
|
-
// Merge: update or add this directory in the last entry
|
|
205
220
|
const existing = lastEntry.directories.findIndex((d) => d.path === dirPath);
|
|
206
221
|
if (existing >= 0) {
|
|
207
222
|
lastEntry.directories[existing] = dirEntry;
|
|
208
223
|
} else {
|
|
209
224
|
lastEntry.directories.push(dirEntry);
|
|
210
225
|
}
|
|
211
|
-
// Recompute overall as average of all directories in this entry
|
|
212
226
|
const totalScore = lastEntry.directories.reduce((sum, d) => sum + d.score, 0);
|
|
213
227
|
lastEntry.overall = Math.round(totalScore / lastEntry.directories.length);
|
|
214
228
|
lastEntry.grade = gradeFromScore(lastEntry.overall);
|
|
215
229
|
lastEntry.timestamp = now.toISOString();
|
|
230
|
+
if (categoryScores) lastEntry.categoryScores = categoryScores;
|
|
231
|
+
if (issueDensity !== undefined) lastEntry.issueDensity = issueDensity;
|
|
216
232
|
} else {
|
|
217
|
-
// New entry
|
|
218
233
|
history.push({
|
|
219
234
|
timestamp: now.toISOString(),
|
|
220
235
|
overall: results.overall,
|
|
221
236
|
grade: results.grade,
|
|
237
|
+
issueDensity,
|
|
238
|
+
categoryScores,
|
|
222
239
|
directories: [dirEntry],
|
|
223
240
|
});
|
|
224
241
|
}
|
|
225
242
|
|
|
226
|
-
// Trim to max entries
|
|
227
243
|
while (history.length > MAX_HISTORY_ENTRIES) {
|
|
228
244
|
history.shift();
|
|
229
245
|
}
|
|
@@ -443,18 +443,8 @@ function persistReviewResults(
|
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
let updatedResults: import('./quality-service.js').QualityResults;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
...existingReport,
|
|
449
|
-
overall: reviewResult.score,
|
|
450
|
-
grade: reviewResult.grade,
|
|
451
|
-
codeReview: findings,
|
|
452
|
-
scoreRationale: reviewResult.scoreRationale ?? undefined,
|
|
453
|
-
};
|
|
454
|
-
} else {
|
|
455
|
-
updatedResults = recomputeWithAiReview(existingReport, reviewResult.findings);
|
|
456
|
-
updatedResults = { ...updatedResults, codeReview: findings };
|
|
457
|
-
}
|
|
446
|
+
updatedResults = recomputeWithAiReview(existingReport, reviewResult.findings);
|
|
447
|
+
updatedResults = { ...updatedResults, codeReview: findings };
|
|
458
448
|
|
|
459
449
|
persistence.saveReport(reportPath, updatedResults);
|
|
460
450
|
persistence.appendHistory(updatedResults, reportPath);
|
|
@@ -5,11 +5,11 @@ import { extname } from 'node:path';
|
|
|
5
5
|
import { analyzeComplexity, analyzeFunctionLength } from './quality-complexity.js';
|
|
6
6
|
import { analyzeLinting } from './quality-linting.js';
|
|
7
7
|
import { collectSourceFiles, detectEcosystem, runCommand, type SourceFile } from './quality-tools.js';
|
|
8
|
-
import { type CategoryScore, type Ecosystem, FILE_LENGTH_THRESHOLD, hasInstalledToolInCategory, type QualityFinding, type QualityResults, type ScanProgress, TOTAL_STEPS } from './quality-types.js';
|
|
8
|
+
import { type CategoryPenalty, type CategoryScore, type Ecosystem, FILE_LENGTH_THRESHOLD, hasInstalledToolInCategory, type QualityFinding, type QualityResults, type ScanProgress, type ScoreBreakdown, TOTAL_STEPS } from './quality-types.js';
|
|
9
9
|
|
|
10
10
|
export { detectEcosystem, detectTools, installTools } from './quality-tools.js';
|
|
11
11
|
// Re-export public API for backward compatibility
|
|
12
|
-
export type { CategoryScore, QualityFinding, QualityResults, QualityTool, ScanProgress } from './quality-types.js';
|
|
12
|
+
export type { CategoryPenalty, CategoryScore, QualityFinding, QualityResults, QualityTool, ScanProgress, ScoreBreakdown } from './quality-types.js';
|
|
13
13
|
|
|
14
14
|
// ============================================================================
|
|
15
15
|
// Formatting Analysis
|
|
@@ -124,7 +124,7 @@ function analyzeFileLength(files: SourceFile[]): { score: number; findings: Qual
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// ============================================================================
|
|
127
|
-
// Scoring
|
|
127
|
+
// Deterministic Scoring — penalty-density exponential decay
|
|
128
128
|
// ============================================================================
|
|
129
129
|
|
|
130
130
|
function computeGrade(score: number): string {
|
|
@@ -135,66 +135,107 @@ function computeGrade(score: number): string {
|
|
|
135
135
|
return 'F';
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
aiReview: number;
|
|
145
|
-
}
|
|
138
|
+
const SEVERITY_WEIGHT: Record<string, number> = {
|
|
139
|
+
critical: 10,
|
|
140
|
+
high: 5,
|
|
141
|
+
medium: 2,
|
|
142
|
+
low: 0.5,
|
|
143
|
+
};
|
|
146
144
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
const CATEGORY_MULTIPLIER: Record<string, number> = {
|
|
146
|
+
security: 2.0,
|
|
147
|
+
bugs: 1.5,
|
|
148
|
+
architecture: 1.2,
|
|
149
|
+
logic: 1.2,
|
|
150
|
+
performance: 1.0,
|
|
151
|
+
oop: 0.8,
|
|
152
|
+
maintainability: 0.8,
|
|
153
|
+
complexity: 0.7,
|
|
154
|
+
lint: 0.5,
|
|
155
|
+
linting: 0.5,
|
|
156
|
+
format: 0.3,
|
|
157
|
+
'file-length': 0.3,
|
|
158
|
+
'function-length': 0.3,
|
|
154
159
|
};
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// ============================================================================
|
|
161
|
+
const OVERALL_DECAY = 0.09;
|
|
162
|
+
const CATEGORY_DECAY = 0.20;
|
|
159
163
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
164
|
+
function findingPenalty(f: { severity: string; category: string }): number {
|
|
165
|
+
return (SEVERITY_WEIGHT[f.severity] ?? 2) * (CATEGORY_MULTIPLIER[f.category] ?? 1.0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function computeFormulaScore(
|
|
169
|
+
allFindings: Array<{ severity: string; category: string }>,
|
|
170
|
+
totalLines: number,
|
|
171
|
+
): { score: number; breakdown: ScoreBreakdown } {
|
|
172
|
+
const kloc = Math.max(totalLines / 1000, 1.0);
|
|
173
|
+
|
|
174
|
+
if (allFindings.length === 0) {
|
|
175
|
+
return {
|
|
176
|
+
score: 100,
|
|
177
|
+
breakdown: { penaltyDensity: 0, totalPenalty: 0, issueDensity: 0, kloc, categoryPenalties: [] },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const byCategory = new Map<string, { penalty: number; count: number }>();
|
|
182
|
+
let totalPenalty = 0;
|
|
183
|
+
|
|
184
|
+
for (const f of allFindings) {
|
|
185
|
+
const p = findingPenalty(f);
|
|
186
|
+
totalPenalty += p;
|
|
187
|
+
const existing = byCategory.get(f.category);
|
|
188
|
+
if (existing) {
|
|
189
|
+
existing.penalty += p;
|
|
190
|
+
existing.count++;
|
|
191
|
+
} else {
|
|
192
|
+
byCategory.set(f.category, { penalty: p, count: 1 });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const penaltyDensity = totalPenalty / kloc;
|
|
197
|
+
const score = Math.round(100 * Math.exp(-OVERALL_DECAY * penaltyDensity));
|
|
198
|
+
|
|
199
|
+
const categoryPenalties: CategoryPenalty[] = [];
|
|
200
|
+
for (const [cat, data] of byCategory) {
|
|
201
|
+
const catDensity = data.penalty / kloc;
|
|
202
|
+
const catScore = Math.round(100 * Math.exp(-CATEGORY_DECAY * catDensity));
|
|
203
|
+
categoryPenalties.push({
|
|
204
|
+
category: cat,
|
|
205
|
+
score: catScore,
|
|
206
|
+
grade: computeGrade(catScore),
|
|
207
|
+
penalty: Math.round(data.penalty * 10) / 10,
|
|
208
|
+
findingCount: data.count,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
166
211
|
|
|
167
|
-
|
|
168
|
-
const AI_REVIEW_DECAY = 0.10;
|
|
212
|
+
categoryPenalties.sort((a, b) => a.score - b.score);
|
|
169
213
|
|
|
214
|
+
return {
|
|
215
|
+
score,
|
|
216
|
+
breakdown: {
|
|
217
|
+
penaltyDensity: Math.round(penaltyDensity * 100) / 100,
|
|
218
|
+
totalPenalty: Math.round(totalPenalty * 10) / 10,
|
|
219
|
+
issueDensity: Math.round((allFindings.length / kloc) * 100) / 100,
|
|
220
|
+
kloc: Math.round(kloc * 10) / 10,
|
|
221
|
+
categoryPenalties,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** @deprecated — use computeFormulaScore instead */
|
|
170
227
|
export function computeAiReviewScore(
|
|
171
228
|
findings: Array<{ severity: string }>,
|
|
172
229
|
totalLines: number,
|
|
173
230
|
): number {
|
|
174
231
|
if (findings.length === 0) return 100;
|
|
175
|
-
|
|
176
232
|
const effectiveKloc = Math.max(totalLines / 1000, 1.0);
|
|
177
233
|
const totalPenalty = findings.reduce(
|
|
178
|
-
(sum, f) => sum + (
|
|
234
|
+
(sum, f) => sum + (SEVERITY_WEIGHT[f.severity] ?? 2.0),
|
|
179
235
|
0,
|
|
180
236
|
);
|
|
181
237
|
const penaltyDensity = totalPenalty / effectiveKloc;
|
|
182
|
-
return Math.round(100 * Math.exp(-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function computeOverallScore(categories: CategoryScore[]): number {
|
|
186
|
-
const available = categories.filter((c) => c.available);
|
|
187
|
-
if (available.length === 0) return 0;
|
|
188
|
-
|
|
189
|
-
const totalWeight = available.reduce((sum, c) => sum + c.weight, 0);
|
|
190
|
-
let weighted = 0;
|
|
191
|
-
for (const cat of available) {
|
|
192
|
-
const effectiveWeight = cat.weight / totalWeight;
|
|
193
|
-
cat.effectiveWeight = effectiveWeight;
|
|
194
|
-
weighted += cat.score * effectiveWeight;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return Math.round(Math.max(0, Math.min(100, weighted)));
|
|
238
|
+
return Math.round(100 * Math.exp(-0.10 * penaltyDensity));
|
|
198
239
|
}
|
|
199
240
|
|
|
200
241
|
// ============================================================================
|
|
@@ -250,64 +291,58 @@ export async function runQualityScan(
|
|
|
250
291
|
// Step 7: Compute scores
|
|
251
292
|
progress('Computing scores', 7);
|
|
252
293
|
|
|
294
|
+
const allFindings = [
|
|
295
|
+
...lintResult.findings,
|
|
296
|
+
...fmtResult.findings,
|
|
297
|
+
...complexityResult.findings,
|
|
298
|
+
...fileLengthResult.findings,
|
|
299
|
+
...funcLengthResult.findings,
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const totalLines = files.reduce((sum, f) => sum + f.lines, 0);
|
|
303
|
+
const { score: overall, breakdown } = computeFormulaScore(allFindings, totalLines);
|
|
304
|
+
|
|
253
305
|
const categories: CategoryScore[] = [
|
|
254
306
|
{
|
|
255
307
|
name: 'Linting',
|
|
256
308
|
score: lintResult.score,
|
|
257
|
-
weight:
|
|
258
|
-
effectiveWeight:
|
|
309
|
+
weight: 0,
|
|
310
|
+
effectiveWeight: 0,
|
|
259
311
|
available: lintResult.available,
|
|
260
312
|
issueCount: lintResult.issueCount,
|
|
261
313
|
},
|
|
262
314
|
{
|
|
263
315
|
name: 'Formatting',
|
|
264
316
|
score: fmtResult.score,
|
|
265
|
-
weight:
|
|
266
|
-
effectiveWeight:
|
|
317
|
+
weight: 0,
|
|
318
|
+
effectiveWeight: 0,
|
|
267
319
|
available: fmtResult.available,
|
|
268
320
|
issueCount: fmtResult.issueCount,
|
|
269
321
|
},
|
|
270
322
|
{
|
|
271
323
|
name: 'Complexity',
|
|
272
324
|
score: complexityResult.score,
|
|
273
|
-
weight:
|
|
274
|
-
effectiveWeight:
|
|
325
|
+
weight: 0,
|
|
326
|
+
effectiveWeight: 0,
|
|
275
327
|
available: complexityResult.available,
|
|
276
328
|
issueCount: complexityResult.issueCount,
|
|
277
329
|
},
|
|
278
330
|
{
|
|
279
331
|
name: 'File Length',
|
|
280
332
|
score: fileLengthResult.score,
|
|
281
|
-
weight:
|
|
282
|
-
effectiveWeight:
|
|
333
|
+
weight: 0,
|
|
334
|
+
effectiveWeight: 0,
|
|
283
335
|
available: true,
|
|
284
336
|
issueCount: fileLengthResult.issueCount,
|
|
285
337
|
},
|
|
286
338
|
{
|
|
287
339
|
name: 'Function Length',
|
|
288
340
|
score: funcLengthResult.score,
|
|
289
|
-
weight:
|
|
290
|
-
effectiveWeight:
|
|
341
|
+
weight: 0,
|
|
342
|
+
effectiveWeight: 0,
|
|
291
343
|
available: true,
|
|
292
344
|
issueCount: funcLengthResult.issueCount,
|
|
293
345
|
},
|
|
294
|
-
{
|
|
295
|
-
name: 'AI Review',
|
|
296
|
-
score: 0,
|
|
297
|
-
weight: DEFAULT_WEIGHTS.aiReview,
|
|
298
|
-
effectiveWeight: DEFAULT_WEIGHTS.aiReview,
|
|
299
|
-
available: false,
|
|
300
|
-
issueCount: 0,
|
|
301
|
-
},
|
|
302
|
-
];
|
|
303
|
-
|
|
304
|
-
const overall = computeOverallScore(categories);
|
|
305
|
-
const allFindings = [
|
|
306
|
-
...lintResult.findings,
|
|
307
|
-
...fmtResult.findings,
|
|
308
|
-
...complexityResult.findings,
|
|
309
|
-
...fileLengthResult.findings,
|
|
310
|
-
...funcLengthResult.findings,
|
|
311
346
|
];
|
|
312
347
|
|
|
313
348
|
return {
|
|
@@ -317,9 +352,10 @@ export async function runQualityScan(
|
|
|
317
352
|
findings: allFindings.slice(0, 200),
|
|
318
353
|
codeReview: [],
|
|
319
354
|
analyzedFiles: files.length,
|
|
320
|
-
totalLines
|
|
355
|
+
totalLines,
|
|
321
356
|
timestamp: new Date().toISOString(),
|
|
322
357
|
ecosystem: ecosystems,
|
|
358
|
+
scoreBreakdown: breakdown,
|
|
323
359
|
};
|
|
324
360
|
}
|
|
325
361
|
|
|
@@ -329,39 +365,20 @@ export async function runQualityScan(
|
|
|
329
365
|
|
|
330
366
|
/**
|
|
331
367
|
* Recompute the overall score after AI code review findings become available.
|
|
332
|
-
*
|
|
368
|
+
* Merges CLI + AI findings and runs the deterministic penalty-density formula.
|
|
333
369
|
*/
|
|
334
370
|
export function recomputeWithAiReview(
|
|
335
371
|
results: QualityResults,
|
|
336
|
-
aiFindings: Array<{ severity: string }>,
|
|
372
|
+
aiFindings: Array<{ severity: string; category: string }>,
|
|
337
373
|
): QualityResults {
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
// Update or add the AI Review category
|
|
341
|
-
const categories = results.categories.map((cat) => ({ ...cat }));
|
|
342
|
-
const aiCatIndex = categories.findIndex((c) => c.name === 'AI Review');
|
|
343
|
-
const aiCategory: CategoryScore = {
|
|
344
|
-
name: 'AI Review',
|
|
345
|
-
score: aiScore,
|
|
346
|
-
weight: DEFAULT_WEIGHTS.aiReview,
|
|
347
|
-
effectiveWeight: DEFAULT_WEIGHTS.aiReview,
|
|
348
|
-
available: true,
|
|
349
|
-
issueCount: aiFindings.length,
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
if (aiCatIndex >= 0) {
|
|
353
|
-
categories[aiCatIndex] = aiCategory;
|
|
354
|
-
} else {
|
|
355
|
-
categories.push(aiCategory);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const overall = computeOverallScore(categories);
|
|
374
|
+
const allFindings = [...results.findings, ...aiFindings];
|
|
375
|
+
const { score: overall, breakdown } = computeFormulaScore(allFindings, results.totalLines);
|
|
359
376
|
|
|
360
377
|
return {
|
|
361
378
|
...results,
|
|
362
379
|
overall,
|
|
363
380
|
grade: computeGrade(overall),
|
|
364
|
-
categories,
|
|
365
381
|
codeReview: results.codeReview,
|
|
382
|
+
scoreBreakdown: breakdown,
|
|
366
383
|
};
|
|
367
384
|
}
|
|
@@ -65,6 +65,7 @@ async function checkToolInstalled(check: string[], cwd: string): Promise<boolean
|
|
|
65
65
|
return new Promise((resolve) => {
|
|
66
66
|
const proc = spawn(check[0], check.slice(1), {
|
|
67
67
|
cwd,
|
|
68
|
+
shell: true,
|
|
68
69
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
69
70
|
timeout: 10000,
|
|
70
71
|
});
|
|
@@ -109,13 +110,16 @@ export async function installTools(
|
|
|
109
110
|
if (tool.installCommand.startsWith('(')) continue;
|
|
110
111
|
const commands = tool.installCommand.split(' || ');
|
|
111
112
|
let installed = false;
|
|
113
|
+
let lastStderr = '';
|
|
112
114
|
for (const cmd of commands) {
|
|
113
115
|
const parts = cmd.trim().split(' ');
|
|
114
116
|
const result = await runCommand(parts[0], parts.slice(1), dirPath);
|
|
115
117
|
if (result.exitCode === 0) { installed = true; break; }
|
|
118
|
+
lastStderr = result.stderr;
|
|
116
119
|
}
|
|
117
120
|
if (!installed) {
|
|
118
|
-
|
|
121
|
+
const detail = lastStderr ? ` (${lastStderr.trim().split('\n').pop()})` : '';
|
|
122
|
+
failures.push(`${tool.name}: install failed${detail}`);
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -241,6 +245,7 @@ export function runCommand(cmd: string, args: string[], cwd: string): Promise<{
|
|
|
241
245
|
return new Promise((resolve) => {
|
|
242
246
|
const proc = spawn(cmd, args, {
|
|
243
247
|
cwd,
|
|
248
|
+
shell: true,
|
|
244
249
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
245
250
|
timeout: 120000,
|
|
246
251
|
});
|
|
@@ -35,6 +35,22 @@ export interface QualityFinding {
|
|
|
35
35
|
verificationNote?: string;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface CategoryPenalty {
|
|
39
|
+
category: string;
|
|
40
|
+
score: number;
|
|
41
|
+
grade: string;
|
|
42
|
+
penalty: number;
|
|
43
|
+
findingCount: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ScoreBreakdown {
|
|
47
|
+
penaltyDensity: number;
|
|
48
|
+
totalPenalty: number;
|
|
49
|
+
issueDensity: number;
|
|
50
|
+
kloc: number;
|
|
51
|
+
categoryPenalties: CategoryPenalty[];
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
export interface QualityResults {
|
|
39
55
|
overall: number;
|
|
40
56
|
grade: string;
|
|
@@ -45,8 +61,7 @@ export interface QualityResults {
|
|
|
45
61
|
totalLines: number;
|
|
46
62
|
timestamp: string;
|
|
47
63
|
ecosystem: string[];
|
|
48
|
-
|
|
49
|
-
scoreRationale?: string;
|
|
64
|
+
scoreBreakdown?: ScoreBreakdown;
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
export interface ScanProgress {
|
|
@@ -104,8 +104,8 @@ export function setupSessionListeners(ctx: HandlerContext, session: Improvisatio
|
|
|
104
104
|
ctx.send(ws, { type: 'thinking', tabId, data: { text } });
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
session.on('onMovementStart', (sequenceNumber: number, prompt: string) => {
|
|
108
|
-
ctx.send(ws, { type: 'movementStart', tabId, data: { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp } });
|
|
107
|
+
session.on('onMovementStart', (sequenceNumber: number, prompt: string, isAutoContinue?: boolean) => {
|
|
108
|
+
ctx.send(ws, { type: 'movementStart', tabId, data: { sequenceNumber, prompt, timestamp: Date.now(), executionStartTimestamp: session.executionStartTimestamp, isAutoContinue } });
|
|
109
109
|
ctx.broadcastToAll({ type: 'tabStateChanged', data: { tabId, isExecuting: true, executionStartTimestamp: session.executionStartTimestamp } });
|
|
110
110
|
});
|
|
111
111
|
|