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.
Files changed (44) hide show
  1. package/LICENSE +191 -21
  2. package/PRIVACY.md +303 -62
  3. package/README.md +81 -58
  4. package/bin/commands/status.js +1 -1
  5. package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
  6. package/dist/server/cli/headless/claude-invoker.js +4 -3
  7. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  8. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  9. package/dist/server/cli/headless/stall-assessor.js +30 -5
  10. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  11. package/dist/server/cli/improvisation-session-manager.js +2 -2
  12. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  13. package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
  14. package/dist/server/services/plan/dependency-resolver.js +2 -0
  15. package/dist/server/services/plan/dependency-resolver.js.map +1 -1
  16. package/dist/server/services/plan/executor.d.ts +27 -8
  17. package/dist/server/services/plan/executor.d.ts.map +1 -1
  18. package/dist/server/services/plan/executor.js +176 -80
  19. package/dist/server/services/plan/executor.js.map +1 -1
  20. package/dist/server/services/plan/parser.d.ts.map +1 -1
  21. package/dist/server/services/plan/parser.js +39 -9
  22. package/dist/server/services/plan/parser.js.map +1 -1
  23. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
  24. package/dist/server/services/plan/state-reconciler.js +41 -1
  25. package/dist/server/services/plan/state-reconciler.js.map +1 -1
  26. package/dist/server/services/plan/types.d.ts +1 -0
  27. package/dist/server/services/plan/types.d.ts.map +1 -1
  28. package/dist/server/services/websocket/quality-handlers.js +14 -6
  29. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  30. package/dist/server/services/websocket/quality-service.d.ts +10 -0
  31. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  32. package/dist/server/services/websocket/quality-service.js +105 -11
  33. package/dist/server/services/websocket/quality-service.js.map +1 -1
  34. package/package.json +4 -3
  35. package/server/cli/headless/claude-invoker.ts +4 -3
  36. package/server/cli/headless/stall-assessor.ts +34 -5
  37. package/server/cli/improvisation-session-manager.ts +2 -2
  38. package/server/services/plan/dependency-resolver.ts +3 -0
  39. package/server/services/plan/executor.ts +176 -80
  40. package/server/services/plan/parser.ts +41 -9
  41. package/server/services/plan/state-reconciler.ts +44 -2
  42. package/server/services/plan/types.ts +2 -0
  43. package/server/services/websocket/quality-handlers.ts +15 -7
  44. 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
- ctx.send(ws, {
416
- type: 'qualityCodeReview',
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: spec.installCmd,
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 (failures.length > 0) {
226
- const stillMissing = detected.tools.filter((t) => !t.installed).map((t) => t.name);
227
- if (stillMissing.length > 0) {
228
- throw new Error(`Failed to install: ${stillMissing.join(', ')}. ${failures.join('; ')}`);
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.30,
839
- formatting: 0.15,
840
- complexity: 0.25,
841
- fileLength: 0.15,
842
- functionLength: 0.15,
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
+ }