mstro-app 0.3.9 → 0.4.1
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/LICENSE +191 -21
- package/PRIVACY.md +303 -62
- package/README.md +81 -58
- package/bin/commands/status.js +1 -1
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +4 -3
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +30 -5
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +2 -2
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
- package/dist/server/services/plan/dependency-resolver.js +2 -0
- package/dist/server/services/plan/dependency-resolver.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +27 -8
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +176 -80
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +39 -9
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
- package/dist/server/services/plan/state-reconciler.js +41 -1
- package/dist/server/services/plan/state-reconciler.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +14 -6
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +10 -0
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +105 -11
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/package.json +4 -3
- package/server/cli/headless/claude-invoker.ts +4 -3
- package/server/cli/headless/stall-assessor.ts +34 -5
- package/server/cli/improvisation-session-manager.ts +2 -2
- package/server/services/plan/dependency-resolver.ts +3 -0
- package/server/services/plan/executor.ts +176 -80
- package/server/services/plan/parser.ts +41 -9
- package/server/services/plan/state-reconciler.ts +44 -2
- package/server/services/plan/types.ts +2 -0
- package/server/services/websocket/quality-handlers.ts +15 -7
- package/server/services/websocket/quality-service.ts +123 -11
|
@@ -7,7 +7,7 @@ import { HeadlessRunner } from '../../cli/headless/index.js';
|
|
|
7
7
|
import type { ToolUseEvent } from '../../cli/headless/types.js';
|
|
8
8
|
import type { HandlerContext } from './handler-context.js';
|
|
9
9
|
import { QualityPersistence } from './quality-persistence.js';
|
|
10
|
-
import { detectTools, installTools, runQualityScan } from './quality-service.js';
|
|
10
|
+
import { detectTools, installTools, recomputeWithAiReview, runQualityScan } from './quality-service.js';
|
|
11
11
|
import type { WebSocketMessage, WSContext } from './types.js';
|
|
12
12
|
|
|
13
13
|
const TOOL_MESSAGES: Record<string, string> = {
|
|
@@ -412,18 +412,26 @@ async function handleCodeReview(
|
|
|
412
412
|
const responseText = result.assistantResponse || '';
|
|
413
413
|
const { findings, summary } = parseCodeReviewResponse(responseText);
|
|
414
414
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
data: { path: reportPath, findings, summary },
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
// Persist code review results
|
|
415
|
+
// Recompute overall score with AI review findings included
|
|
416
|
+
let updatedResults: import('./quality-service.js').QualityResults | null = null;
|
|
421
417
|
try {
|
|
422
418
|
const persistence = getPersistence(workingDir);
|
|
419
|
+
const existingReport = persistence.loadReport(reportPath);
|
|
420
|
+
if (existingReport) {
|
|
421
|
+
updatedResults = recomputeWithAiReview(existingReport, findings);
|
|
422
|
+
updatedResults = { ...updatedResults, codeReview: findings as unknown as typeof updatedResults.codeReview };
|
|
423
|
+
persistence.saveReport(reportPath, updatedResults);
|
|
424
|
+
persistence.appendHistory(updatedResults, reportPath);
|
|
425
|
+
}
|
|
423
426
|
persistence.saveCodeReview(reportPath, findings as unknown as Record<string, unknown>[], summary);
|
|
424
427
|
} catch {
|
|
425
428
|
// Persistence failure should not break the review flow
|
|
426
429
|
}
|
|
430
|
+
|
|
431
|
+
ctx.send(ws, {
|
|
432
|
+
type: 'qualityCodeReview',
|
|
433
|
+
data: { path: reportPath, findings, summary, results: updatedResults },
|
|
434
|
+
});
|
|
427
435
|
} catch (error) {
|
|
428
436
|
ctx.send(ws, {
|
|
429
437
|
type: 'qualityError',
|
|
@@ -156,6 +156,29 @@ export function detectEcosystem(dirPath: string): Ecosystem[] {
|
|
|
156
156
|
return ecosystems;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/** Detect the Node.js package manager from lockfiles */
|
|
160
|
+
function detectNodePackageManager(dirPath: string): 'npm' | 'yarn' | 'pnpm' | 'bun' {
|
|
161
|
+
try {
|
|
162
|
+
const files = readdirSync(dirPath);
|
|
163
|
+
if (files.includes('bun.lockb') || files.includes('bun.lock')) return 'bun';
|
|
164
|
+
if (files.includes('pnpm-lock.yaml')) return 'pnpm';
|
|
165
|
+
if (files.includes('yarn.lock')) return 'yarn';
|
|
166
|
+
} catch {
|
|
167
|
+
// Directory not readable
|
|
168
|
+
}
|
|
169
|
+
return 'npm';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Build the install command for a Node.js dev dependency */
|
|
173
|
+
function nodeInstallCmd(pm: 'npm' | 'yarn' | 'pnpm' | 'bun', pkg: string): string {
|
|
174
|
+
switch (pm) {
|
|
175
|
+
case 'yarn': return `yarn add -D ${pkg}`;
|
|
176
|
+
case 'pnpm': return `pnpm add -D ${pkg}`;
|
|
177
|
+
case 'bun': return `bun add -d ${pkg}`;
|
|
178
|
+
default: return `npm install -D ${pkg}`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
159
182
|
// ============================================================================
|
|
160
183
|
// Tool Detection
|
|
161
184
|
// ============================================================================
|
|
@@ -175,15 +198,20 @@ async function checkToolInstalled(check: string[], cwd: string): Promise<boolean
|
|
|
175
198
|
export async function detectTools(dirPath: string): Promise<{ tools: QualityTool[]; ecosystem: string[] }> {
|
|
176
199
|
const ecosystems = detectEcosystem(dirPath);
|
|
177
200
|
const tools: QualityTool[] = [];
|
|
201
|
+
const nodePm = ecosystems.includes('node') ? detectNodePackageManager(dirPath) : 'npm';
|
|
178
202
|
|
|
179
203
|
for (const eco of ecosystems) {
|
|
180
204
|
const specs = ECOSYSTEM_TOOLS[eco] || [];
|
|
181
205
|
for (const spec of specs) {
|
|
182
206
|
const installed = await checkToolInstalled(spec.check, dirPath);
|
|
207
|
+
// For node tools, resolve install command using the project's package manager
|
|
208
|
+
const installCommand = eco === 'node'
|
|
209
|
+
? nodeInstallCmd(nodePm, spec.installCmd.replace(/^npm install -D /, ''))
|
|
210
|
+
: spec.installCmd;
|
|
183
211
|
tools.push({
|
|
184
212
|
name: spec.name,
|
|
185
213
|
installed,
|
|
186
|
-
installCommand
|
|
214
|
+
installCommand,
|
|
187
215
|
category: spec.category,
|
|
188
216
|
});
|
|
189
217
|
}
|
|
@@ -222,11 +250,13 @@ export async function installTools(
|
|
|
222
250
|
// Re-detect after install
|
|
223
251
|
const detected = await detectTools(dirPath);
|
|
224
252
|
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
253
|
+
// Check if any requested tools are still missing after install
|
|
254
|
+
const requestedNames = new Set(toolNames ?? toInstall.map((t) => t.name));
|
|
255
|
+
const stillMissing = detected.tools.filter((t) => !t.installed && requestedNames.has(t.name)).map((t) => t.name);
|
|
256
|
+
|
|
257
|
+
if (stillMissing.length > 0) {
|
|
258
|
+
const detail = failures.length > 0 ? ` ${failures.join('; ')}` : '';
|
|
259
|
+
throw new Error(`Failed to install: ${stillMissing.join(', ')}.${detail}`);
|
|
230
260
|
}
|
|
231
261
|
|
|
232
262
|
return detected;
|
|
@@ -832,16 +862,47 @@ interface CategoryWeights {
|
|
|
832
862
|
complexity: number;
|
|
833
863
|
fileLength: number;
|
|
834
864
|
functionLength: number;
|
|
865
|
+
aiReview: number;
|
|
835
866
|
}
|
|
836
867
|
|
|
837
868
|
const DEFAULT_WEIGHTS: CategoryWeights = {
|
|
838
|
-
linting: 0.
|
|
839
|
-
formatting: 0.
|
|
840
|
-
complexity: 0.
|
|
841
|
-
fileLength: 0.
|
|
842
|
-
functionLength: 0.
|
|
869
|
+
linting: 0.25,
|
|
870
|
+
formatting: 0.10,
|
|
871
|
+
complexity: 0.20,
|
|
872
|
+
fileLength: 0.12,
|
|
873
|
+
functionLength: 0.13,
|
|
874
|
+
aiReview: 0.20,
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// ============================================================================
|
|
878
|
+
// AI Code Review Score
|
|
879
|
+
// ============================================================================
|
|
880
|
+
|
|
881
|
+
const SEVERITY_PENALTY: Record<string, number> = {
|
|
882
|
+
critical: 10.0,
|
|
883
|
+
high: 5.0,
|
|
884
|
+
medium: 2.0,
|
|
885
|
+
low: 0.5,
|
|
843
886
|
};
|
|
844
887
|
|
|
888
|
+
/** Exponential decay constant — higher = harsher scoring */
|
|
889
|
+
const AI_REVIEW_DECAY = 0.10;
|
|
890
|
+
|
|
891
|
+
export function computeAiReviewScore(
|
|
892
|
+
findings: Array<{ severity: string }>,
|
|
893
|
+
totalLines: number,
|
|
894
|
+
): number {
|
|
895
|
+
if (findings.length === 0) return 100;
|
|
896
|
+
|
|
897
|
+
const effectiveKloc = Math.max(totalLines / 1000, 1.0);
|
|
898
|
+
const totalPenalty = findings.reduce(
|
|
899
|
+
(sum, f) => sum + (SEVERITY_PENALTY[f.severity] ?? 2.0),
|
|
900
|
+
0,
|
|
901
|
+
);
|
|
902
|
+
const penaltyDensity = totalPenalty / effectiveKloc;
|
|
903
|
+
return Math.round(100 * Math.exp(-AI_REVIEW_DECAY * penaltyDensity));
|
|
904
|
+
}
|
|
905
|
+
|
|
845
906
|
function computeOverallScore(categories: CategoryScore[]): number {
|
|
846
907
|
const available = categories.filter((c) => c.available);
|
|
847
908
|
if (available.length === 0) return 0;
|
|
@@ -951,6 +1012,14 @@ export async function runQualityScan(
|
|
|
951
1012
|
available: true,
|
|
952
1013
|
issueCount: funcLengthResult.issueCount,
|
|
953
1014
|
},
|
|
1015
|
+
{
|
|
1016
|
+
name: 'AI Review',
|
|
1017
|
+
score: 0,
|
|
1018
|
+
weight: DEFAULT_WEIGHTS.aiReview,
|
|
1019
|
+
effectiveWeight: DEFAULT_WEIGHTS.aiReview,
|
|
1020
|
+
available: false,
|
|
1021
|
+
issueCount: 0,
|
|
1022
|
+
},
|
|
954
1023
|
];
|
|
955
1024
|
|
|
956
1025
|
const overall = computeOverallScore(categories);
|
|
@@ -973,3 +1042,46 @@ export async function runQualityScan(
|
|
|
973
1042
|
ecosystem: ecosystems,
|
|
974
1043
|
};
|
|
975
1044
|
}
|
|
1045
|
+
|
|
1046
|
+
// ============================================================================
|
|
1047
|
+
// Recompute with AI Review
|
|
1048
|
+
// ============================================================================
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Recompute the overall score after AI code review findings become available.
|
|
1052
|
+
* Returns a new QualityResults with the AI Review category enabled and score updated.
|
|
1053
|
+
*/
|
|
1054
|
+
export function recomputeWithAiReview(
|
|
1055
|
+
results: QualityResults,
|
|
1056
|
+
aiFindings: Array<{ severity: string }>,
|
|
1057
|
+
): QualityResults {
|
|
1058
|
+
const aiScore = computeAiReviewScore(aiFindings, results.totalLines);
|
|
1059
|
+
|
|
1060
|
+
// Update or add the AI Review category
|
|
1061
|
+
const categories = results.categories.map((cat) => ({ ...cat }));
|
|
1062
|
+
const aiCatIndex = categories.findIndex((c) => c.name === 'AI Review');
|
|
1063
|
+
const aiCategory: CategoryScore = {
|
|
1064
|
+
name: 'AI Review',
|
|
1065
|
+
score: aiScore,
|
|
1066
|
+
weight: DEFAULT_WEIGHTS.aiReview,
|
|
1067
|
+
effectiveWeight: DEFAULT_WEIGHTS.aiReview,
|
|
1068
|
+
available: true,
|
|
1069
|
+
issueCount: aiFindings.length,
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
if (aiCatIndex >= 0) {
|
|
1073
|
+
categories[aiCatIndex] = aiCategory;
|
|
1074
|
+
} else {
|
|
1075
|
+
categories.push(aiCategory);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const overall = computeOverallScore(categories);
|
|
1079
|
+
|
|
1080
|
+
return {
|
|
1081
|
+
...results,
|
|
1082
|
+
overall,
|
|
1083
|
+
grade: computeGrade(overall),
|
|
1084
|
+
categories,
|
|
1085
|
+
codeReview: results.codeReview,
|
|
1086
|
+
};
|
|
1087
|
+
}
|