mstro-app 0.3.8 → 0.3.9

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/claude-invoker.d.ts.map +1 -1
  2. package/dist/server/cli/headless/claude-invoker.js +18 -9
  3. package/dist/server/cli/headless/claude-invoker.js.map +1 -1
  4. package/dist/server/cli/headless/headless-logger.d.ts +10 -0
  5. package/dist/server/cli/headless/headless-logger.d.ts.map +1 -0
  6. package/dist/server/cli/headless/headless-logger.js +66 -0
  7. package/dist/server/cli/headless/headless-logger.js.map +1 -0
  8. package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
  9. package/dist/server/cli/headless/mcp-config.js +6 -5
  10. package/dist/server/cli/headless/mcp-config.js.map +1 -1
  11. package/dist/server/cli/headless/runner.d.ts.map +1 -1
  12. package/dist/server/cli/headless/runner.js +4 -0
  13. package/dist/server/cli/headless/runner.js.map +1 -1
  14. package/dist/server/cli/headless/stall-assessor.d.ts +21 -0
  15. package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
  16. package/dist/server/cli/headless/stall-assessor.js +70 -19
  17. package/dist/server/cli/headless/stall-assessor.js.map +1 -1
  18. package/dist/server/cli/headless/tool-watchdog.d.ts +0 -12
  19. package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
  20. package/dist/server/cli/headless/tool-watchdog.js +22 -9
  21. package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
  22. package/dist/server/cli/headless/types.d.ts +8 -1
  23. package/dist/server/cli/headless/types.d.ts.map +1 -1
  24. package/dist/server/cli/improvisation-session-manager.d.ts +16 -0
  25. package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
  26. package/dist/server/cli/improvisation-session-manager.js +94 -11
  27. package/dist/server/cli/improvisation-session-manager.js.map +1 -1
  28. package/dist/server/mcp/bouncer-cli.d.ts +3 -0
  29. package/dist/server/mcp/bouncer-cli.d.ts.map +1 -0
  30. package/dist/server/mcp/bouncer-cli.js +54 -0
  31. package/dist/server/mcp/bouncer-cli.js.map +1 -0
  32. package/dist/server/services/plan/composer.d.ts +4 -0
  33. package/dist/server/services/plan/composer.d.ts.map +1 -0
  34. package/dist/server/services/plan/composer.js +181 -0
  35. package/dist/server/services/plan/composer.js.map +1 -0
  36. package/dist/server/services/plan/dependency-resolver.d.ts +28 -0
  37. package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -0
  38. package/dist/server/services/plan/dependency-resolver.js +152 -0
  39. package/dist/server/services/plan/dependency-resolver.js.map +1 -0
  40. package/dist/server/services/plan/executor.d.ts +91 -0
  41. package/dist/server/services/plan/executor.d.ts.map +1 -0
  42. package/dist/server/services/plan/executor.js +545 -0
  43. package/dist/server/services/plan/executor.js.map +1 -0
  44. package/dist/server/services/plan/parser.d.ts +11 -0
  45. package/dist/server/services/plan/parser.d.ts.map +1 -0
  46. package/dist/server/services/plan/parser.js +415 -0
  47. package/dist/server/services/plan/parser.js.map +1 -0
  48. package/dist/server/services/plan/state-reconciler.d.ts +2 -0
  49. package/dist/server/services/plan/state-reconciler.d.ts.map +1 -0
  50. package/dist/server/services/plan/state-reconciler.js +105 -0
  51. package/dist/server/services/plan/state-reconciler.js.map +1 -0
  52. package/dist/server/services/plan/types.d.ts +120 -0
  53. package/dist/server/services/plan/types.d.ts.map +1 -0
  54. package/dist/server/services/plan/types.js +4 -0
  55. package/dist/server/services/plan/types.js.map +1 -0
  56. package/dist/server/services/plan/watcher.d.ts +14 -0
  57. package/dist/server/services/plan/watcher.d.ts.map +1 -0
  58. package/dist/server/services/plan/watcher.js +69 -0
  59. package/dist/server/services/plan/watcher.js.map +1 -0
  60. package/dist/server/services/websocket/file-explorer-handlers.js +20 -0
  61. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  62. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  63. package/dist/server/services/websocket/handler.js +21 -0
  64. package/dist/server/services/websocket/handler.js.map +1 -1
  65. package/dist/server/services/websocket/plan-handlers.d.ts +6 -0
  66. package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -0
  67. package/dist/server/services/websocket/plan-handlers.js +494 -0
  68. package/dist/server/services/websocket/plan-handlers.js.map +1 -0
  69. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  70. package/dist/server/services/websocket/quality-handlers.js +375 -11
  71. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  72. package/dist/server/services/websocket/quality-persistence.d.ts +45 -0
  73. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -0
  74. package/dist/server/services/websocket/quality-persistence.js +187 -0
  75. package/dist/server/services/websocket/quality-persistence.js.map +1 -0
  76. package/dist/server/services/websocket/quality-service.d.ts +2 -2
  77. package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
  78. package/dist/server/services/websocket/quality-service.js +62 -12
  79. package/dist/server/services/websocket/quality-service.js.map +1 -1
  80. package/dist/server/services/websocket/types.d.ts +2 -2
  81. package/dist/server/services/websocket/types.d.ts.map +1 -1
  82. package/package.json +2 -2
  83. package/server/cli/headless/claude-invoker.ts +21 -9
  84. package/server/cli/headless/headless-logger.ts +78 -0
  85. package/server/cli/headless/mcp-config.ts +6 -5
  86. package/server/cli/headless/runner.ts +4 -0
  87. package/server/cli/headless/stall-assessor.ts +97 -19
  88. package/server/cli/headless/tool-watchdog.ts +10 -9
  89. package/server/cli/headless/types.ts +10 -1
  90. package/server/cli/improvisation-session-manager.ts +118 -11
  91. package/server/mcp/bouncer-cli.ts +73 -0
  92. package/server/services/plan/composer.ts +199 -0
  93. package/server/services/plan/dependency-resolver.ts +179 -0
  94. package/server/services/plan/executor.ts +604 -0
  95. package/server/services/plan/parser.ts +459 -0
  96. package/server/services/plan/state-reconciler.ts +132 -0
  97. package/server/services/plan/types.ts +164 -0
  98. package/server/services/plan/watcher.ts +73 -0
  99. package/server/services/websocket/file-explorer-handlers.ts +20 -0
  100. package/server/services/websocket/handler.ts +21 -0
  101. package/server/services/websocket/plan-handlers.ts +592 -0
  102. package/server/services/websocket/quality-handlers.ts +441 -11
  103. package/server/services/websocket/quality-persistence.ts +250 -0
  104. package/server/services/websocket/quality-service.ts +65 -12
  105. package/server/services/websocket/types.ts +48 -2
@@ -0,0 +1,250 @@
1
+ // Copyright (c) 2025-present Mstro, Inc. All rights reserved.
2
+ // Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ /**
5
+ * Quality Persistence — Persists quality config, reports, and history
6
+ * to .mstro/quality/ in the working directory.
7
+ *
8
+ * Files:
9
+ * .mstro/quality/config.json — Directory list (paths + labels)
10
+ * .mstro/quality/reports/<slug>.json — Latest full report per directory
11
+ * .mstro/quality/history.json — Score history entries for trend tracking
12
+ */
13
+
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import type { QualityResults } from './quality-service.js';
17
+
18
+ // ============================================================================
19
+ // Types
20
+ // ============================================================================
21
+
22
+ export interface QualityDirectoryConfig {
23
+ path: string;
24
+ label: string;
25
+ }
26
+
27
+ interface QualityConfig {
28
+ directories: QualityDirectoryConfig[];
29
+ }
30
+
31
+ export interface HistoryDirectoryEntry {
32
+ path: string;
33
+ score: number;
34
+ grade: string;
35
+ }
36
+
37
+ export interface QualityHistoryEntry {
38
+ timestamp: string;
39
+ overall: number;
40
+ grade: string;
41
+ directories: HistoryDirectoryEntry[];
42
+ }
43
+
44
+ interface QualityHistory {
45
+ entries: QualityHistoryEntry[];
46
+ }
47
+
48
+ export interface QualityPersistedState {
49
+ directories: QualityDirectoryConfig[];
50
+ reports: Record<string, QualityResults>;
51
+ history: QualityHistoryEntry[];
52
+ }
53
+
54
+ // ============================================================================
55
+ // Helpers
56
+ // ============================================================================
57
+
58
+ const MAX_HISTORY_ENTRIES = 100;
59
+
60
+ function slugify(dirPath: string): string {
61
+ if (dirPath === '.' || dirPath === './') return '_root';
62
+ return dirPath.replace(/[/\\]/g, '_').replace(/^_+|_+$/g, '') || '_root';
63
+ }
64
+
65
+ function ensureDir(dir: string): void {
66
+ if (!existsSync(dir)) {
67
+ mkdirSync(dir, { recursive: true });
68
+ }
69
+ }
70
+
71
+ function readJson<T>(filePath: string, fallback: T): T {
72
+ try {
73
+ if (existsSync(filePath)) {
74
+ return JSON.parse(readFileSync(filePath, 'utf-8')) as T;
75
+ }
76
+ } catch {
77
+ // Corrupted or unreadable — return fallback
78
+ }
79
+ return fallback;
80
+ }
81
+
82
+ function writeJson(filePath: string, data: unknown): void {
83
+ try {
84
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
85
+ } catch (error) {
86
+ console.error('[QualityPersistence] Error writing:', filePath, error);
87
+ }
88
+ }
89
+
90
+ // ============================================================================
91
+ // Quality Persistence
92
+ // ============================================================================
93
+
94
+ export class QualityPersistence {
95
+ private qualityDir: string;
96
+ private reportsDir: string;
97
+ private configPath: string;
98
+ private historyPath: string;
99
+
100
+ constructor(workingDir: string) {
101
+ this.qualityDir = join(workingDir, '.mstro', 'quality');
102
+ this.reportsDir = join(this.qualityDir, 'reports');
103
+ this.configPath = join(this.qualityDir, 'config.json');
104
+ this.historyPath = join(this.qualityDir, 'history.json');
105
+ ensureDir(this.reportsDir);
106
+ }
107
+
108
+ // ---- Config (directory list) ----
109
+
110
+ loadConfig(): QualityDirectoryConfig[] {
111
+ const config = readJson<QualityConfig>(this.configPath, { directories: [] });
112
+ return config.directories;
113
+ }
114
+
115
+ saveConfig(directories: QualityDirectoryConfig[]): void {
116
+ writeJson(this.configPath, { directories });
117
+ }
118
+
119
+ addDirectory(path: string, label: string): void {
120
+ const dirs = this.loadConfig();
121
+ if (!dirs.some((d) => d.path === path)) {
122
+ dirs.push({ path, label });
123
+ this.saveConfig(dirs);
124
+ }
125
+ }
126
+
127
+ removeDirectory(path: string): void {
128
+ const dirs = this.loadConfig().filter((d) => d.path !== path);
129
+ this.saveConfig(dirs);
130
+ }
131
+
132
+ // ---- Reports (latest per directory) ----
133
+
134
+ loadReport(dirPath: string): QualityResults | null {
135
+ const slug = slugify(dirPath);
136
+ const reportPath = join(this.reportsDir, `${slug}.json`);
137
+ return readJson<QualityResults | null>(reportPath, null);
138
+ }
139
+
140
+ saveReport(dirPath: string, results: QualityResults): void {
141
+ const slug = slugify(dirPath);
142
+ const reportPath = join(this.reportsDir, `${slug}.json`);
143
+ writeJson(reportPath, results);
144
+ }
145
+
146
+ loadAllReports(directories: QualityDirectoryConfig[]): Record<string, QualityResults> {
147
+ const reports: Record<string, QualityResults> = {};
148
+ for (const dir of directories) {
149
+ const report = this.loadReport(dir.path);
150
+ if (report) {
151
+ reports[dir.path] = report;
152
+ }
153
+ }
154
+ return reports;
155
+ }
156
+
157
+ // ---- History (trend tracking) ----
158
+
159
+ loadHistory(): QualityHistoryEntry[] {
160
+ const history = readJson<QualityHistory>(this.historyPath, { entries: [] });
161
+ return history.entries;
162
+ }
163
+
164
+ appendHistory(results: QualityResults, dirPath: string): void {
165
+ const history = this.loadHistory();
166
+
167
+ // Find or create entry for this timestamp batch
168
+ // If the last entry was within 60 seconds, merge into it (for multi-dir scans)
169
+ const now = new Date();
170
+ const lastEntry = history[history.length - 1];
171
+ const lastTime = lastEntry ? new Date(lastEntry.timestamp).getTime() : 0;
172
+ const mergeWindow = 60_000; // 60 seconds
173
+
174
+ const dirEntry: HistoryDirectoryEntry = {
175
+ path: dirPath,
176
+ score: results.overall,
177
+ grade: results.grade,
178
+ };
179
+
180
+ if (lastEntry && now.getTime() - lastTime < mergeWindow) {
181
+ // Merge: update or add this directory in the last entry
182
+ const existing = lastEntry.directories.findIndex((d) => d.path === dirPath);
183
+ if (existing >= 0) {
184
+ lastEntry.directories[existing] = dirEntry;
185
+ } else {
186
+ lastEntry.directories.push(dirEntry);
187
+ }
188
+ // Recompute overall as average of all directories in this entry
189
+ const totalScore = lastEntry.directories.reduce((sum, d) => sum + d.score, 0);
190
+ lastEntry.overall = Math.round(totalScore / lastEntry.directories.length);
191
+ lastEntry.grade = gradeFromScore(lastEntry.overall);
192
+ lastEntry.timestamp = now.toISOString();
193
+ } else {
194
+ // New entry
195
+ history.push({
196
+ timestamp: now.toISOString(),
197
+ overall: results.overall,
198
+ grade: results.grade,
199
+ directories: [dirEntry],
200
+ });
201
+ }
202
+
203
+ // Trim to max entries
204
+ while (history.length > MAX_HISTORY_ENTRIES) {
205
+ history.shift();
206
+ }
207
+
208
+ writeJson(this.historyPath, { entries: history });
209
+ }
210
+
211
+ // ---- Code Review (persisted per directory) ----
212
+
213
+ loadCodeReview(dirPath: string): { findings: Record<string, unknown>[]; summary: string; timestamp: string } | null {
214
+ const slug = slugify(dirPath);
215
+ const reviewPath = join(this.reportsDir, `${slug}-review.json`);
216
+ return readJson<{ findings: Record<string, unknown>[]; summary: string; timestamp: string } | null>(reviewPath, null);
217
+ }
218
+
219
+ saveCodeReview(dirPath: string, findings: Record<string, unknown>[], summary: string): void {
220
+ const slug = slugify(dirPath);
221
+ const reviewPath = join(this.reportsDir, `${slug}-review.json`);
222
+ writeJson(reviewPath, { findings, summary, timestamp: new Date().toISOString() });
223
+ }
224
+
225
+ // ---- Full state load ----
226
+
227
+ loadState(): QualityPersistedState {
228
+ const directories = this.loadConfig();
229
+ const reports = this.loadAllReports(directories);
230
+ const history = this.loadHistory();
231
+
232
+ // Merge persisted code reviews into reports
233
+ for (const dir of directories) {
234
+ const review = this.loadCodeReview(dir.path);
235
+ if (review && reports[dir.path]) {
236
+ reports[dir.path] = { ...reports[dir.path], codeReview: review.findings as unknown as QualityResults['codeReview'] };
237
+ }
238
+ }
239
+
240
+ return { directories, reports, history };
241
+ }
242
+ }
243
+
244
+ function gradeFromScore(score: number): string {
245
+ if (score >= 90) return 'A';
246
+ if (score >= 80) return 'B';
247
+ if (score >= 70) return 'C';
248
+ if (score >= 60) return 'D';
249
+ return 'F';
250
+ }
@@ -54,7 +54,7 @@ export interface ScanProgress {
54
54
  total: number;
55
55
  }
56
56
 
57
- type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'unknown';
57
+ type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'swift' | 'kotlin' | 'unknown';
58
58
 
59
59
  interface ToolSpec {
60
60
  name: string;
@@ -75,9 +75,9 @@ const ECOSYSTEM_TOOLS: Record<Ecosystem, ToolSpec[]> = {
75
75
  { name: 'typescript', check: ['npx', 'tsc', '--version'], category: 'general', installCmd: 'npm install -D typescript' },
76
76
  ],
77
77
  python: [
78
- { name: 'ruff', check: ['ruff', '--version'], category: 'linter', installCmd: 'pip install ruff' },
79
- { name: 'black', check: ['black', '--version'], category: 'formatter', installCmd: 'pip install black' },
80
- { name: 'radon', check: ['radon', '--version'], category: 'complexity', installCmd: 'pip install radon' },
78
+ { name: 'ruff', check: ['ruff', '--version'], category: 'linter', installCmd: 'uv tool install ruff || pip install ruff' },
79
+ { name: 'black', check: ['black', '--version'], category: 'formatter', installCmd: 'uv tool install black || pip install black' },
80
+ { name: 'radon', check: ['radon', '--version'], category: 'complexity', installCmd: 'uv tool install radon || pip install radon' },
81
81
  ],
82
82
  rust: [
83
83
  { name: 'clippy', check: ['cargo', 'clippy', '--version'], category: 'linter', installCmd: 'rustup component add clippy' },
@@ -87,6 +87,14 @@ const ECOSYSTEM_TOOLS: Record<Ecosystem, ToolSpec[]> = {
87
87
  { name: 'golangci-lint', check: ['golangci-lint', '--version'], category: 'linter', installCmd: 'go install github.com/golangci-lint/golangci-lint/cmd/golangci-lint@latest' },
88
88
  { name: 'gofmt', check: ['gofmt', '-h'], category: 'formatter', installCmd: '(built-in with Go)' },
89
89
  ],
90
+ swift: [
91
+ { name: 'swiftlint', check: ['swiftlint', '--version'], category: 'linter', installCmd: 'brew install swiftlint' },
92
+ { name: 'swiftformat', check: ['swiftformat', '--version'], category: 'formatter', installCmd: 'brew install swiftformat' },
93
+ ],
94
+ kotlin: [
95
+ { name: 'ktlint', check: ['ktlint', '--version'], category: 'linter', installCmd: 'brew install ktlint' },
96
+ { name: 'ktfmt', check: ['ktfmt', '--version'], category: 'formatter', installCmd: 'brew install ktfmt' },
97
+ ],
90
98
  unknown: [],
91
99
  };
92
100
 
@@ -113,6 +121,20 @@ const FILE_LENGTH_THRESHOLD = 300;
113
121
  const FUNCTION_LENGTH_THRESHOLD = 50;
114
122
  const TOTAL_STEPS = 7;
115
123
 
124
+ function hasInstalledToolInCategory(
125
+ installedSet: Set<string>,
126
+ ecosystems: Ecosystem[],
127
+ category: QualityTool['category'],
128
+ ): boolean {
129
+ for (const eco of ecosystems) {
130
+ const specs = ECOSYSTEM_TOOLS[eco] || [];
131
+ for (const spec of specs) {
132
+ if (spec.category === category && installedSet.has(spec.name)) return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+
116
138
  // ============================================================================
117
139
  // Ecosystem Detection
118
140
  // ============================================================================
@@ -125,6 +147,8 @@ export function detectEcosystem(dirPath: string): Ecosystem[] {
125
147
  if (files.includes('pyproject.toml') || files.includes('setup.py') || files.includes('requirements.txt')) ecosystems.push('python');
126
148
  if (files.includes('Cargo.toml')) ecosystems.push('rust');
127
149
  if (files.includes('go.mod')) ecosystems.push('go');
150
+ if (files.includes('Package.swift') || files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) ecosystems.push('swift');
151
+ if (files.includes('build.gradle') || files.includes('build.gradle.kts')) ecosystems.push('kotlin');
128
152
  } catch {
129
153
  // Directory not readable
130
154
  }
@@ -179,14 +203,33 @@ export async function installTools(
179
203
  const { tools } = await detectTools(dirPath);
180
204
  const toInstall = tools.filter((t) => !t.installed && (!toolNames || toolNames.includes(t.name)));
181
205
 
206
+ const failures: string[] = [];
182
207
  for (const tool of toInstall) {
183
208
  if (tool.installCommand.startsWith('(')) continue; // built-in, skip
184
- const parts = tool.installCommand.split(' ');
185
- await runCommand(parts[0], parts.slice(1), dirPath);
209
+ // Support chained commands with || (try first, fallback to second)
210
+ const commands = tool.installCommand.split(' || ');
211
+ let installed = false;
212
+ for (const cmd of commands) {
213
+ const parts = cmd.trim().split(' ');
214
+ const result = await runCommand(parts[0], parts.slice(1), dirPath);
215
+ if (result.exitCode === 0) { installed = true; break; }
216
+ }
217
+ if (!installed) {
218
+ failures.push(`${tool.name}: all install methods failed`);
219
+ }
186
220
  }
187
221
 
188
222
  // Re-detect after install
189
- return detectTools(dirPath);
223
+ const detected = await detectTools(dirPath);
224
+
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
+ }
230
+ }
231
+
232
+ return detected;
190
233
  }
191
234
 
192
235
  // ============================================================================
@@ -371,7 +414,7 @@ async function lintNode(dirPath: string, acc: LintAccumulator): Promise<void> {
371
414
 
372
415
  async function lintPython(dirPath: string, acc: LintAccumulator): Promise<void> {
373
416
  const result = await runCommand('ruff', ['check', '--output-format=json', '.'], dirPath);
374
- if (result.exitCode > 1) return;
417
+ if (result.exitCode !== 0 && !result.stdout.trim().startsWith('[')) return;
375
418
 
376
419
  acc.ran = true;
377
420
  try {
@@ -823,9 +866,13 @@ export type ProgressCallback = (progress: ScanProgress) => void;
823
866
  export async function runQualityScan(
824
867
  dirPath: string,
825
868
  onProgress?: ProgressCallback,
869
+ installedToolNames?: string[],
826
870
  ): Promise<QualityResults> {
827
871
  const ecosystems = detectEcosystem(dirPath);
828
872
 
873
+ // Build set of installed tools for gating analyses
874
+ const installedSet = installedToolNames ? new Set(installedToolNames) : null;
875
+
829
876
  const progress = (step: string, current: number) => {
830
877
  onProgress?.({ step, current, total: TOTAL_STEPS });
831
878
  };
@@ -834,13 +881,19 @@ export async function runQualityScan(
834
881
  progress('Collecting source files', 1);
835
882
  const files = collectSourceFiles(dirPath, dirPath);
836
883
 
837
- // Step 2: Run linting
884
+ // Step 2: Run linting (only if a linter is installed)
838
885
  progress('Running linters', 2);
839
- const lintResult = await analyzeLinting(dirPath, ecosystems, files);
886
+ const hasLinter = !installedSet || hasInstalledToolInCategory(installedSet, ecosystems, 'linter');
887
+ const lintResult = hasLinter
888
+ ? await analyzeLinting(dirPath, ecosystems, files)
889
+ : { score: 0, findings: [], available: false, issueCount: 0 };
840
890
 
841
- // Step 3: Check formatting
891
+ // Step 3: Check formatting (only if a formatter is installed)
842
892
  progress('Checking formatting', 3);
843
- const fmtResult = await analyzeFormatting(dirPath, ecosystems, files);
893
+ const hasFormatter = !installedSet || hasInstalledToolInCategory(installedSet, ecosystems, 'formatter');
894
+ const fmtResult = hasFormatter
895
+ ? await analyzeFormatting(dirPath, ecosystems, files)
896
+ : { score: 0, available: false, issueCount: 0 };
844
897
 
845
898
  // Step 4: Analyze complexity
846
899
  progress('Analyzing complexity', 4);
@@ -112,11 +112,31 @@ export interface WebSocketMessage {
112
112
  | 'qualityScan'
113
113
  | 'qualityInstallTools'
114
114
  | 'qualityCodeReview'
115
+ | 'qualityFixIssues'
116
+ | 'qualityLoadState'
117
+ | 'qualitySaveDirectories'
115
118
  // File upload message types (chunked remote uploads)
116
119
  | 'fileUploadStart'
117
120
  | 'fileUploadChunk'
118
121
  | 'fileUploadComplete'
119
- | 'fileUploadCancel';
122
+ | 'fileUploadCancel'
123
+ // Plan message types
124
+ | 'planInit'
125
+ | 'planGetState'
126
+ | 'planListIssues'
127
+ | 'planGetIssue'
128
+ | 'planGetSprint'
129
+ | 'planGetMilestone'
130
+ | 'planCreateIssue'
131
+ | 'planUpdateIssue'
132
+ | 'planDeleteIssue'
133
+ | 'planScaffold'
134
+ | 'planPrompt'
135
+ | 'planExecute'
136
+ | 'planExecuteEpic'
137
+ | 'planPause'
138
+ | 'planStop'
139
+ | 'planResume';
120
140
  tabId?: string;
121
141
  terminalId?: string;
122
142
  // biome-ignore lint/suspicious/noExplicitAny: message envelope carries heterogeneous payloads
@@ -231,12 +251,38 @@ export interface WebSocketResponse {
231
251
  | 'qualityInstallProgress'
232
252
  | 'qualityInstallComplete'
233
253
  | 'qualityCodeReview'
254
+ | 'qualityCodeReviewProgress'
234
255
  | 'qualityPostSession'
256
+ | 'qualityFixProgress'
257
+ | 'qualityFixComplete'
235
258
  | 'qualityError'
259
+ | 'qualityStateLoaded'
236
260
  // File upload response types
237
261
  | 'fileUploadAck'
238
262
  | 'fileUploadReady'
239
- | 'fileUploadError';
263
+ | 'fileUploadError'
264
+ // Plan response types
265
+ | 'planState'
266
+ | 'planIssueList'
267
+ | 'planIssue'
268
+ | 'planSprint'
269
+ | 'planMilestone'
270
+ | 'planNotFound'
271
+ | 'planStateUpdated'
272
+ | 'planIssueUpdated'
273
+ | 'planIssueCreated'
274
+ | 'planIssueDeleted'
275
+ | 'planScaffolded'
276
+ | 'planPromptStreaming'
277
+ | 'planPromptProgress'
278
+ | 'planPromptResponse'
279
+ | 'planExecutionStarted'
280
+ | 'planExecutionProgress'
281
+ | 'planExecutionOutput'
282
+ | 'planExecutionMetrics'
283
+ | 'planExecutionComplete'
284
+ | 'planExecutionError'
285
+ | 'planError';
240
286
  tabId?: string;
241
287
  terminalId?: string;
242
288
  // biome-ignore lint/suspicious/noExplicitAny: message envelope carries heterogeneous payloads