@aionlabsai/aion 0.2.1 → 0.2.2

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 (87) hide show
  1. package/README.md +59 -0
  2. package/dist/agents/base-agent.d.ts.map +1 -1
  3. package/dist/agents/base-agent.js +23 -12
  4. package/dist/agents/base-agent.js.map +1 -1
  5. package/dist/agents/synthesizer.d.ts +3 -0
  6. package/dist/agents/synthesizer.d.ts.map +1 -1
  7. package/dist/agents/synthesizer.js +93 -52
  8. package/dist/agents/synthesizer.js.map +1 -1
  9. package/dist/cli/commands/audit.d.ts.map +1 -1
  10. package/dist/cli/commands/audit.js +87 -534
  11. package/dist/cli/commands/audit.js.map +1 -1
  12. package/dist/cli/commands/chat.d.ts.map +1 -1
  13. package/dist/cli/commands/chat.js +2 -1
  14. package/dist/cli/commands/chat.js.map +1 -1
  15. package/dist/cli/commands/context.d.ts +3 -0
  16. package/dist/cli/commands/context.d.ts.map +1 -0
  17. package/dist/cli/commands/context.js +72 -0
  18. package/dist/cli/commands/context.js.map +1 -0
  19. package/dist/cli/commands/next.d.ts +3 -0
  20. package/dist/cli/commands/next.d.ts.map +1 -0
  21. package/dist/cli/commands/next.js +49 -0
  22. package/dist/cli/commands/next.js.map +1 -0
  23. package/dist/cli/commands/report.d.ts.map +1 -1
  24. package/dist/cli/commands/report.js +52 -400
  25. package/dist/cli/commands/report.js.map +1 -1
  26. package/dist/cli/commands/search.d.ts +3 -0
  27. package/dist/cli/commands/search.d.ts.map +1 -0
  28. package/dist/cli/commands/search.js +50 -0
  29. package/dist/cli/commands/search.js.map +1 -0
  30. package/dist/cli/commands/tree.d.ts +3 -0
  31. package/dist/cli/commands/tree.d.ts.map +1 -0
  32. package/dist/cli/commands/tree.js +85 -0
  33. package/dist/cli/commands/tree.js.map +1 -0
  34. package/dist/cli/menu.d.ts.map +1 -1
  35. package/dist/cli/menu.js +13 -0
  36. package/dist/cli/menu.js.map +1 -1
  37. package/dist/core/cost-tracker.d.ts +10 -0
  38. package/dist/core/cost-tracker.d.ts.map +1 -1
  39. package/dist/core/cost-tracker.js +53 -0
  40. package/dist/core/cost-tracker.js.map +1 -1
  41. package/dist/core/pipelines/audit-pipeline.d.ts +4 -1
  42. package/dist/core/pipelines/audit-pipeline.d.ts.map +1 -1
  43. package/dist/core/pipelines/audit-pipeline.js +60 -30
  44. package/dist/core/pipelines/audit-pipeline.js.map +1 -1
  45. package/dist/index.js +11 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/infra/audit-cache.d.ts +2 -1
  48. package/dist/infra/audit-cache.d.ts.map +1 -1
  49. package/dist/infra/audit-cache.js +18 -6
  50. package/dist/infra/audit-cache.js.map +1 -1
  51. package/dist/infra/audit-model.d.ts +72 -0
  52. package/dist/infra/audit-model.d.ts.map +1 -0
  53. package/dist/infra/audit-model.js +146 -0
  54. package/dist/infra/audit-model.js.map +1 -0
  55. package/dist/infra/audit-report-writer.d.ts +4 -0
  56. package/dist/infra/audit-report-writer.d.ts.map +1 -0
  57. package/dist/infra/audit-report-writer.js +290 -0
  58. package/dist/infra/audit-report-writer.js.map +1 -0
  59. package/dist/infra/code-metrics.d.ts +11 -0
  60. package/dist/infra/code-metrics.d.ts.map +1 -1
  61. package/dist/infra/code-metrics.js +62 -0
  62. package/dist/infra/code-metrics.js.map +1 -1
  63. package/dist/infra/embeddings.d.ts +2 -0
  64. package/dist/infra/embeddings.d.ts.map +1 -1
  65. package/dist/infra/embeddings.js +2 -2
  66. package/dist/infra/embeddings.js.map +1 -1
  67. package/dist/infra/project-report.d.ts +67 -0
  68. package/dist/infra/project-report.d.ts.map +1 -0
  69. package/dist/infra/project-report.js +159 -0
  70. package/dist/infra/project-report.js.map +1 -0
  71. package/dist/infra/repo-vectors.d.ts +31 -0
  72. package/dist/infra/repo-vectors.d.ts.map +1 -0
  73. package/dist/infra/repo-vectors.js +83 -0
  74. package/dist/infra/repo-vectors.js.map +1 -0
  75. package/dist/infra/update-check.d.ts +3 -0
  76. package/dist/infra/update-check.d.ts.map +1 -0
  77. package/dist/infra/update-check.js +84 -0
  78. package/dist/infra/update-check.js.map +1 -0
  79. package/dist/prompts/scanner.js +1 -1
  80. package/dist/prompts/synthesizer.d.ts.map +1 -1
  81. package/dist/prompts/synthesizer.js +7 -23
  82. package/dist/prompts/synthesizer.js.map +1 -1
  83. package/dist/providers/cli-provider.d.ts +1 -0
  84. package/dist/providers/cli-provider.d.ts.map +1 -1
  85. package/dist/providers/cli-provider.js +22 -2
  86. package/dist/providers/cli-provider.js.map +1 -1
  87. package/package.json +1 -1
@@ -1,463 +1,28 @@
1
1
  import chalk from 'chalk';
2
- import { mkdirSync, writeFileSync } from 'fs';
3
- import { join } from 'path';
4
2
  import { Orchestrator } from '../../core/orchestrator.js';
5
3
  import { AuditPipeline } from '../../core/pipelines/audit-pipeline.js';
6
4
  import { createRuntimePolicy } from '../../core/runtime-policy.js';
7
5
  import { CostTracker } from '../../core/cost-tracker.js';
8
6
  import { Renderer } from '../ui/renderer.js';
9
- const SEVERITY_COLOR = {
10
- critical: chalk.bgRed.white.bold,
11
- high: chalk.red.bold,
12
- medium: chalk.yellow,
13
- low: chalk.gray,
14
- info: chalk.dim,
15
- };
16
- const SEVERITY_ICON = {
17
- critical: '🔴',
18
- high: '🟠',
19
- medium: '🟡',
20
- low: '⚪',
21
- info: '🔵',
22
- };
23
- const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
24
- function esc(value) {
25
- return String(value ?? '')
26
- .replace(/&/g, '&')
27
- .replace(/</g, '&lt;')
28
- .replace(/>/g, '&gt;')
29
- .replace(/"/g, '&quot;');
30
- }
7
+ import { saveAuditReport } from '../../infra/audit-report-writer.js';
8
+ import { SEVERITY_RANK } from '../../infra/audit-model.js';
31
9
  function renderAuditReport(report, durationMs) {
32
- console.log('\n' + chalk.bold('═'.repeat(60)));
33
- console.log(chalk.bold.cyan(' AUDIT REPORT') + chalk.gray(` — ${report.totalFiles} files — ${(durationMs / 1000).toFixed(1)}s`));
34
- console.log(chalk.bold('═'.repeat(60)));
35
- // Summary
36
- console.log('\n' + chalk.bold('Summary:'));
37
- console.log(' ' + report.summary);
38
- // Counts
10
+ const bySeverity = report.findings.reduce((acc, f) => { acc[f.severity] = (acc[f.severity] ?? 0) + 1; return acc; }, {});
39
11
  const counts = [
40
12
  report.criticalCount > 0 ? chalk.bgRed.white.bold(` ${report.criticalCount} critical `) : null,
41
13
  report.highCount > 0 ? chalk.red.bold(`${report.highCount} high`) : null,
42
- chalk.gray(`${report.findings.filter(f => f.severity === 'medium').length} medium`),
43
- chalk.gray(`${report.findings.filter(f => f.severity === 'low').length} low`),
14
+ (bySeverity['medium'] ?? 0) > 0 ? chalk.yellow(`${bySeverity['medium']} medium`) : null,
15
+ (bySeverity['low'] ?? 0) > 0 ? chalk.gray(`${bySeverity['low']} low`) : null,
44
16
  ].filter(Boolean);
45
- console.log('\n' + chalk.bold('Severity:') + ' ' + counts.join(' '));
46
- // Top priorities
17
+ console.log('\n' + chalk.bold.cyan('Audit complete') + chalk.gray(` ${report.totalFiles} files, ${(durationMs / 1000).toFixed(1)}s`));
18
+ console.log(chalk.bold('Findings:') + ' ' + counts.join(' ') + chalk.dim(` (${report.findings.length} total)`));
19
+ console.log(' ' + chalk.dim(report.summary));
47
20
  if (report.topPriorities.length > 0) {
48
- console.log('\n' + chalk.bold('Top Priorities:'));
49
- report.topPriorities.forEach((p, i) => {
50
- console.log(chalk.cyan(` ${i + 1}.`) + ' ' + p);
51
- });
52
- }
53
- // Findings: if sections exist, render by domain; otherwise by severity
54
- const auditReport = report;
55
- if (auditReport.sections && auditReport.sections.length > 0) {
56
- for (const section of auditReport.sections) {
57
- if (section.findings.length === 0)
58
- continue;
59
- console.log('\n' + chalk.bold.blue(` ▸ ${section.domain.toUpperCase()} (${section.findings.length})`));
60
- for (const f of section.findings) {
61
- const loc = f.line ? `${f.file}:${f.line}` : f.file;
62
- const color = SEVERITY_COLOR[f.severity] ?? chalk.white;
63
- console.log(color(` ${loc}`) + chalk.gray(` [${f.severity}]`));
64
- console.log(` ${f.finding}`);
65
- console.log(chalk.dim(` → ${f.recommendation}`));
66
- console.log();
67
- }
68
- }
69
- }
70
- else {
71
- const order = ['critical', 'high', 'medium', 'low', 'info'];
72
- for (const sev of order) {
73
- const group = report.findings.filter(f => f.severity === sev);
74
- if (group.length === 0)
75
- continue;
76
- const color = SEVERITY_COLOR[sev] ?? chalk.white;
77
- const icon = SEVERITY_ICON[sev] ?? '';
78
- console.log('\n' + color(` ${icon} ${sev.toUpperCase()} (${group.length}) `));
79
- for (const f of group) {
80
- const loc = f.line ? `${f.file}:${f.line}` : f.file;
81
- console.log(chalk.bold(` ${loc}`) + chalk.gray(` [${f.category}]`));
82
- console.log(` ${f.finding}`);
83
- console.log(chalk.dim(` → ${f.recommendation}`));
84
- console.log();
85
- }
86
- }
87
- }
88
- console.log(chalk.bold('═'.repeat(60)));
89
- }
90
- function saveAuditReport(report, durationMs) {
91
- const dir = join(process.cwd(), '.ai-runtime', 'reports');
92
- mkdirSync(dir, { recursive: true });
93
- const stamp = new Date().toISOString().replace(/[:.]/g, '-');
94
- const createdAt = new Date().toISOString();
95
- const fullReport = { ...report, durationMs, createdAt };
96
- const file = join(dir, `audit-${stamp}.json`);
97
- writeFileSync(file, JSON.stringify(fullReport, null, 2), 'utf8');
98
- const runDir = join(dir, 'audits', stamp);
99
- mkdirSync(runDir, { recursive: true });
100
- const html = join(runDir, 'index.html');
101
- const summary = join(runDir, 'summary.md');
102
- const actionPlan = join(runDir, 'action-plan.md');
103
- writeFileSync(join(runDir, 'report.json'), JSON.stringify(fullReport, null, 2), 'utf8');
104
- writeFileSync(join(runDir, 'findings-by-persona.json'), JSON.stringify(groupFindings(report.findings, (f) => f.persona ?? 'local'), null, 2), 'utf8');
105
- writeFileSync(join(runDir, 'findings-by-severity.json'), JSON.stringify(groupFindings(report.findings, (f) => f.severity), null, 2), 'utf8');
106
- writeFileSync(join(runDir, 'findings-by-category.json'), JSON.stringify(groupFindings(report.findings, (f) => f.category), null, 2), 'utf8');
107
- writeFileSync(join(runDir, 'files-hotspots.json'), JSON.stringify(buildFileHotspots(report.findings), null, 2), 'utf8');
108
- writeFileSync(join(runDir, 'action-items.json'), JSON.stringify(buildActionItems(report.findings), null, 2), 'utf8');
109
- writeFileSync(join(runDir, 'README.md'), renderAuditIndex(fullReport), 'utf8');
110
- writeFileSync(summary, renderAuditSummary(fullReport), 'utf8');
111
- writeFileSync(actionPlan, renderActionPlan(fullReport), 'utf8');
112
- writeFileSync(html, renderAuditHtml(fullReport), 'utf8');
113
- writeFileSync(join(dir, 'latest-audit.json'), JSON.stringify({ report: file, runDir, html, summary, actionPlan, createdAt }, null, 2), 'utf8');
114
- return { runDir, html, summary, actionPlan, report: file };
115
- }
116
- function groupFindings(findings, keyFn) {
117
- const grouped = {};
118
- for (const finding of findings) {
119
- const key = keyFn(finding);
120
- grouped[key] ??= [];
121
- grouped[key].push(finding);
122
- }
123
- return grouped;
124
- }
125
- function compact(value, max = 120) {
126
- const normalized = value.replace(/\s+/g, ' ').trim();
127
- if (normalized.length <= max)
128
- return normalized;
129
- return `${normalized.slice(0, max - 3)}...`;
130
- }
131
- function uniqueSorted(values) {
132
- return [...new Set(values.filter(Boolean))].sort();
133
- }
134
- function maxSeverity(findings) {
135
- return findings
136
- .map((f) => f.severity)
137
- .sort((a, b) => (SEVERITY_RANK[b] ?? 0) - (SEVERITY_RANK[a] ?? 0))[0] ?? 'info';
138
- }
139
- function actionKey(finding) {
140
- const normalized = finding.finding
141
- .toLowerCase()
142
- .replace(/src\/[^\s:]+(:\d+)?/g, '')
143
- .replace(/\d+/g, '#')
144
- .replace(/[^a-z0-9]+/g, ' ')
145
- .trim()
146
- .slice(0, 90);
147
- return `${finding.category}:${normalized}`;
148
- }
149
- function buildActionItems(findings) {
150
- return Object.values(groupFindings(findings, actionKey))
151
- .map((items) => {
152
- const files = Object.entries(items.reduce((acc, finding) => {
153
- acc[finding.file] ??= [];
154
- if (typeof finding.line === 'number')
155
- acc[finding.file].push(finding.line);
156
- return acc;
157
- }, {})).map(([file, lines]) => ({ file, lines: [...new Set(lines)].sort((a, b) => a - b) }));
158
- const severity = maxSeverity(items);
159
- return {
160
- id: 0,
161
- title: compact(items[0]?.finding ?? 'Review finding', 90),
162
- severity,
163
- category: items[0]?.category ?? 'maintainability',
164
- personas: uniqueSorted(items.map((f) => f.persona ?? 'local')),
165
- files,
166
- findings: items.sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0)),
167
- recommendation: compact(items[0]?.recommendation ?? 'Review and remediate.', 220),
168
- };
169
- })
170
- .sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0) || b.findings.length - a.findings.length)
171
- .map((item, index) => ({ ...item, id: index + 1 }));
172
- }
173
- function buildFileHotspots(findings) {
174
- return Object.entries(groupFindings(findings, (f) => f.file))
175
- .map(([file, items]) => ({
176
- file,
177
- findings: items.length,
178
- maxSeverity: maxSeverity(items),
179
- categories: uniqueSorted(items.map((f) => f.category)),
180
- personas: uniqueSorted(items.map((f) => f.persona ?? 'local')),
181
- }))
182
- .sort((a, b) => (SEVERITY_RANK[b.maxSeverity] ?? 0) - (SEVERITY_RANK[a.maxSeverity] ?? 0) || b.findings - a.findings);
183
- }
184
- function markdownLocation(finding) {
185
- return finding.line ? `${finding.file}:${finding.line}` : finding.file;
186
- }
187
- function renderAuditSummary(report) {
188
- const actions = buildActionItems(report.findings).slice(0, 10);
189
- const hotspots = buildFileHotspots(report.findings).slice(0, 10);
190
- const lines = [
191
- '# Audit Summary',
192
- '',
193
- `Generated: ${report.createdAt}`,
194
- `Duration: ${(report.durationMs / 1000).toFixed(1)}s`,
195
- `Files covered by local scan: ${report.totalFiles}`,
196
- `Findings: ${report.findings.length}`,
197
- `Critical: ${report.criticalCount}`,
198
- `High: ${report.highCount}`,
199
- '',
200
- '## Summary',
201
- '',
202
- report.summary,
203
- '',
204
- '## Top Priorities',
205
- '',
206
- ...(report.topPriorities.length > 0 ? report.topPriorities.map((p, i) => `${i + 1}. ${p}`) : ['No top priorities.']),
207
- '',
208
- '## Consolidated Actions',
209
- '',
210
- ...(actions.length > 0 ? actions.map((item) => `${item.id}. [${item.severity}] ${item.title} (${item.files.length} file(s))`) : ['No action items.']),
211
- '',
212
- '## File Hotspots',
213
- '',
214
- ...(hotspots.length > 0 ? hotspots.map((h) => `- ${h.file}: ${h.findings} finding(s), max ${h.maxSeverity}`) : ['No file hotspots.']),
215
- '',
216
- '## Generated Files',
217
- '',
218
- '- `index.html`: navigable report',
219
- '- `action-plan.md`: consolidated remediation plan',
220
- '- `report.json`: raw machine-readable audit',
221
- '- `findings-by-persona.json`: findings grouped by persona',
222
- '- `findings-by-severity.json`: findings grouped by severity',
223
- '- `findings-by-category.json`: findings grouped by category',
224
- ];
225
- return lines.join('\n');
226
- }
227
- function renderActionPlan(report) {
228
- const actions = buildActionItems(report.findings);
229
- const lines = [
230
- '# Audit Action Plan',
231
- '',
232
- `Generated: ${report.createdAt}`,
233
- `Findings consolidated into ${actions.length} action item(s).`,
234
- '',
235
- ];
236
- if (actions.length === 0) {
237
- lines.push('No remediation actions were produced by this audit.');
238
- lines.push('');
239
- return lines.join('\n');
21
+ console.log(chalk.bold('\nTop Priorities:'));
22
+ report.topPriorities.slice(0, 3).forEach((p, i) => console.log(chalk.cyan(` ${i + 1}.`) + ' ' + p));
23
+ if (report.topPriorities.length > 3)
24
+ console.log(chalk.dim(` ... ${report.topPriorities.length - 3} more in HTML`));
240
25
  }
241
- for (const item of actions) {
242
- lines.push(`## ${item.id}. [${item.severity.toUpperCase()}] ${item.title}`);
243
- lines.push('');
244
- lines.push(`Category: ${item.category}`);
245
- lines.push(`Personas: ${item.personas.join(', ') || 'local'}`);
246
- lines.push('');
247
- lines.push('Files:');
248
- for (const file of item.files) {
249
- const suffix = file.lines.length > 0 ? `:${file.lines.slice(0, 8).join(',')}` : '';
250
- lines.push(`- ${file.file}${suffix}`);
251
- }
252
- lines.push('');
253
- lines.push('Recommendation:');
254
- lines.push(item.recommendation);
255
- lines.push('');
256
- lines.push('Evidence:');
257
- for (const finding of item.findings.slice(0, 5)) {
258
- lines.push(`- ${markdownLocation(finding)}: ${finding.finding}`);
259
- }
260
- if (item.findings.length > 5)
261
- lines.push(`- ... ${item.findings.length - 5} more related finding(s)`);
262
- lines.push('');
263
- }
264
- return lines.join('\n');
265
- }
266
- function renderAuditHtml(report) {
267
- const actions = buildActionItems(report.findings);
268
- const hotspots = buildFileHotspots(report.findings);
269
- const byPersona = Object.entries(groupFindings(report.findings, (f) => f.persona ?? 'local'))
270
- .sort((a, b) => b[1].length - a[1].length);
271
- const bySeverity = Object.entries(groupFindings(report.findings, (f) => f.severity))
272
- .sort((a, b) => (SEVERITY_RANK[b[0]] ?? 0) - (SEVERITY_RANK[a[0]] ?? 0));
273
- const byCategory = Object.entries(groupFindings(report.findings, (f) => f.category))
274
- .sort((a, b) => b[1].length - a[1].length);
275
- const actionCards = actions.map((item) => `
276
- <article class="card action" data-severity="${esc(item.severity)}" data-category="${esc(item.category)}">
277
- <div class="row">
278
- <span class="badge sev-${esc(item.severity)}">${esc(item.severity)}</span>
279
- <span class="muted">#${item.id} · ${item.findings.length} finding(s) · ${item.files.length} file(s)</span>
280
- </div>
281
- <h3>${esc(item.title)}</h3>
282
- <p>${esc(item.recommendation)}</p>
283
- <div class="meta">Category: ${esc(item.category)} · Personas: ${esc(item.personas.join(', ') || 'local')}</div>
284
- <details>
285
- <summary>Evidence and files</summary>
286
- <ul>
287
- ${item.files.map((file) => `<li><code>${esc(file.file)}${file.lines.length ? ':' + esc(file.lines.slice(0, 8).join(',')) : ''}</code></li>`).join('')}
288
- </ul>
289
- <ol>
290
- ${item.findings.slice(0, 6).map((finding) => `<li><code>${esc(markdownLocation(finding))}</code> ${esc(finding.finding)}</li>`).join('')}
291
- </ol>
292
- </details>
293
- </article>`).join('');
294
- const findingRows = report.findings.map((finding) => `
295
- <tr data-severity="${esc(finding.severity)}" data-category="${esc(finding.category)}" data-persona="${esc(finding.persona ?? 'local')}" data-file="${esc(finding.file)}">
296
- <td><span class="badge sev-${esc(finding.severity)}">${esc(finding.severity)}</span></td>
297
- <td>${esc(finding.category)}</td>
298
- <td>${esc(finding.persona ?? 'local')}</td>
299
- <td><code>${esc(markdownLocation(finding))}</code></td>
300
- <td>${esc(finding.finding)}<div class="recommendation">${esc(finding.recommendation)}</div></td>
301
- </tr>`).join('');
302
- const statList = (items, label) => items.map(([key, findings]) => {
303
- const severity = maxSeverity(findings);
304
- return `<li><span>${esc(key)}</span><strong class="sev-text-${esc(severity)}">${findings.length}</strong><small>${esc(label)} · max ${esc(severity)}</small></li>`;
305
- }).join('');
306
- return `<!doctype html>
307
- <html lang="en">
308
- <head>
309
- <meta charset="utf-8">
310
- <meta name="viewport" content="width=device-width, initial-scale=1">
311
- <title>Aion Audit Report</title>
312
- <style>
313
- :root{color-scheme:dark;--bg:#0d1117;--panel:#161b22;--line:#30363d;--text:#e6edf3;--muted:#8b949e;--blue:#79c0ff;--green:#3fb950;--yellow:#e3b341;--red:#f85149;--gray:#6e7681}
314
- *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
315
- a{color:var(--blue);text-decoration:none}code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#c9d1d9}
316
- .layout{display:grid;grid-template-columns:250px 1fr;min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;border-right:1px solid var(--line);background:#010409;padding:22px;overflow:auto}
317
- .sidebar h1{font-size:18px;margin:0 0 4px}.sidebar p{margin:0 0 18px;color:var(--muted);font-size:12px}.sidebar a{display:block;padding:7px 0;color:var(--muted)}.sidebar a:hover{color:var(--text)}
318
- main{padding:28px;max-width:1300px}.hero{display:flex;justify-content:space-between;gap:20px;align-items:flex-start;border-bottom:1px solid var(--line);padding-bottom:22px;margin-bottom:22px}
319
- h2{font-size:20px;margin:34px 0 14px;color:var(--blue)}h3{font-size:15px;margin:10px 0}.muted,.meta{color:var(--muted);font-size:12px}.cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
320
- .card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:14px}.num{font-size:32px;font-weight:700}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
321
- .badge{display:inline-flex;align-items:center;border-radius:4px;padding:2px 8px;font-size:11px;font-weight:700;text-transform:uppercase}.sev-critical{background:#f8514920;color:var(--red)}.sev-high{background:#e3b34120;color:var(--yellow)}.sev-medium{background:#79c0ff20;color:var(--blue)}.sev-low{background:#8b949e20;color:var(--muted)}.sev-info{background:#6e768120;color:var(--gray)}
322
- .sev-text-critical{color:var(--red)}.sev-text-high{color:var(--yellow)}.sev-text-medium{color:var(--blue)}.sev-text-low,.sev-text-info{color:var(--muted)}
323
- .action{margin-bottom:12px}.action p{margin:6px 0 8px}.action details{margin-top:10px}.action summary{cursor:pointer;color:var(--blue)}
324
- .stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.stats ul{list-style:none;margin:0;padding:0}.stats li{display:grid;grid-template-columns:1fr auto;gap:3px;border-bottom:1px solid #21262d;padding:8px 0}.stats small{grid-column:1/-1;color:var(--muted)}
325
- .filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}.filters input,.filters select{background:#010409;color:var(--text);border:1px solid var(--line);border-radius:6px;padding:8px}
326
- table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:8px;overflow:hidden}th,td{text-align:left;vertical-align:top;border-bottom:1px solid #21262d;padding:9px}th{color:var(--muted);font-size:12px}.recommendation{color:var(--muted);font-size:12px;margin-top:4px}
327
- .hotspots{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.hotspot{display:grid;grid-template-columns:1fr auto;gap:4px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px}
328
- @media (max-width:900px){.layout{grid-template-columns:1fr}.sidebar{position:relative;height:auto}.cards,.stats,.hotspots{grid-template-columns:1fr}.hero{display:block}}
329
- </style>
330
- </head>
331
- <body>
332
- <div class="layout">
333
- <nav class="sidebar">
334
- <h1>Aion Audit</h1>
335
- <p>${esc(report.createdAt)}</p>
336
- <a href="#overview">Overview</a>
337
- <a href="#actions">Action Plan</a>
338
- <a href="#findings">Findings</a>
339
- <a href="#personas">Personas</a>
340
- <a href="#files">Files</a>
341
- <a href="#raw">Raw Data</a>
342
- </nav>
343
- <main>
344
- <section class="hero" id="overview">
345
- <div>
346
- <h1>Audit Report</h1>
347
- <p class="muted">${esc(report.summary)}</p>
348
- </div>
349
- <div class="meta">Duration ${(report.durationMs / 1000).toFixed(1)}s · ${report.totalFiles} files covered</div>
350
- </section>
351
- <section class="cards">
352
- <div class="card"><div class="num">${report.findings.length}</div><div class="muted">Findings</div></div>
353
- <div class="card"><div class="num sev-text-critical">${report.criticalCount}</div><div class="muted">Critical</div></div>
354
- <div class="card"><div class="num sev-text-high">${report.highCount}</div><div class="muted">High</div></div>
355
- <div class="card"><div class="num">${actions.length}</div><div class="muted">Action items</div></div>
356
- </section>
357
- <section>
358
- <h2>Top Priorities</h2>
359
- <ol>${report.topPriorities.length ? report.topPriorities.map((p) => `<li>${esc(p)}</li>`).join('') : '<li>No top priorities.</li>'}</ol>
360
- </section>
361
- <section id="actions">
362
- <h2>Action Plan</h2>
363
- ${actionCards || '<p class="muted">No action items.</p>'}
364
- </section>
365
- <section id="findings">
366
- <h2>Findings</h2>
367
- <div class="filters">
368
- <input id="q" placeholder="Search findings, files, recommendations">
369
- <select id="severity"><option value="">All severities</option>${bySeverity.map(([s]) => `<option value="${esc(s)}">${esc(s)}</option>`).join('')}</select>
370
- <select id="persona"><option value="">All personas</option>${byPersona.map(([p]) => `<option value="${esc(p)}">${esc(p)}</option>`).join('')}</select>
371
- <select id="category"><option value="">All categories</option>${byCategory.map(([c]) => `<option value="${esc(c)}">${esc(c)}</option>`).join('')}</select>
372
- </div>
373
- <table id="findingsTable"><thead><tr><th>Severity</th><th>Category</th><th>Persona</th><th>Location</th><th>Finding</th></tr></thead><tbody>${findingRows}</tbody></table>
374
- </section>
375
- <section id="personas">
376
- <h2>Personas And Categories</h2>
377
- <div class="stats">
378
- <div class="card"><h3>By Persona</h3><ul>${statList(byPersona, 'persona')}</ul></div>
379
- <div class="card"><h3>By Severity</h3><ul>${statList(bySeverity, 'severity')}</ul></div>
380
- <div class="card"><h3>By Category</h3><ul>${statList(byCategory, 'category')}</ul></div>
381
- </div>
382
- </section>
383
- <section id="files">
384
- <h2>File Hotspots</h2>
385
- <div class="hotspots">${hotspots.slice(0, 30).map((h) => `<div class="hotspot"><code>${esc(h.file)}</code><strong class="sev-text-${esc(h.maxSeverity)}">${h.findings}</strong><small class="muted">${esc(h.categories.join(', '))}</small></div>`).join('') || '<p class="muted">No hotspots.</p>'}</div>
386
- </section>
387
- <section id="raw">
388
- <h2>Raw Data</h2>
389
- <ul>
390
- <li><a href="report.json">report.json</a></li>
391
- <li><a href="action-items.json">action-items.json</a></li>
392
- <li><a href="findings-by-persona.json">findings-by-persona.json</a></li>
393
- <li><a href="findings-by-severity.json">findings-by-severity.json</a></li>
394
- <li><a href="findings-by-category.json">findings-by-category.json</a></li>
395
- <li><a href="files-hotspots.json">files-hotspots.json</a></li>
396
- </ul>
397
- </section>
398
- </main>
399
- </div>
400
- <script>
401
- const q = document.getElementById('q');
402
- const severity = document.getElementById('severity');
403
- const persona = document.getElementById('persona');
404
- const category = document.getElementById('category');
405
- const rows = Array.from(document.querySelectorAll('#findingsTable tbody tr'));
406
- function applyFilters() {
407
- const text = q.value.toLowerCase();
408
- for (const row of rows) {
409
- const okText = !text || row.textContent.toLowerCase().includes(text);
410
- const okSeverity = !severity.value || row.dataset.severity === severity.value;
411
- const okPersona = !persona.value || row.dataset.persona === persona.value;
412
- const okCategory = !category.value || row.dataset.category === category.value;
413
- row.style.display = okText && okSeverity && okPersona && okCategory ? '' : 'none';
414
- }
415
- }
416
- [q, severity, persona, category].forEach((el) => el.addEventListener('input', applyFilters));
417
- </script>
418
- </body>
419
- </html>`;
420
- }
421
- function renderAuditIndex(report) {
422
- const byPersona = Object.entries(groupFindings(report.findings, (f) => f.persona ?? 'local'))
423
- .sort((a, b) => b[1].length - a[1].length);
424
- const bySeverity = Object.entries(groupFindings(report.findings, (f) => f.severity))
425
- .sort((a, b) => b[1].length - a[1].length);
426
- const lines = [
427
- '# Audit Report',
428
- '',
429
- `- Created: ${report.createdAt}`,
430
- `- Duration: ${(report.durationMs / 1000).toFixed(1)}s`,
431
- `- Files covered by local scan: ${report.totalFiles}`,
432
- `- Findings: ${report.findings.length}`,
433
- `- Critical: ${report.criticalCount}`,
434
- `- High: ${report.highCount}`,
435
- '',
436
- '## Summary',
437
- '',
438
- report.summary,
439
- '',
440
- '## Top Priorities',
441
- '',
442
- ...report.topPriorities.map((priority, index) => `${index + 1}. ${priority}`),
443
- '',
444
- '## Findings By Persona',
445
- '',
446
- ...byPersona.map(([persona, findings]) => `- ${persona}: ${findings.length}`),
447
- '',
448
- '## Findings By Severity',
449
- '',
450
- ...bySeverity.map(([severity, findings]) => `- ${severity}: ${findings.length}`),
451
- '',
452
- '## Files',
453
- '',
454
- '- `report.json`: full machine-readable audit report',
455
- '- `findings-by-persona.json`: findings grouped by scanner persona',
456
- '- `findings-by-severity.json`: findings grouped by severity',
457
- '- `findings-by-category.json`: findings grouped by category',
458
- '',
459
- ];
460
- return lines.join('\n');
461
26
  }
462
27
  function renderDryRun(pipeline, stats, maxFilesForAi) {
463
28
  console.log('\n' + chalk.bold.cyan('Audit dry run'));
@@ -474,78 +39,96 @@ function renderDryRun(pipeline, stats, maxFilesForAi) {
474
39
  exts.forEach(([ext, count]) => console.log(` ${ext}: ${count}`));
475
40
  }
476
41
  if (stats.auditFiles.length > 0) {
477
- const prioritized = pipeline.prioritizeFiles(stats.auditFiles, maxFilesForAi);
478
42
  console.log('\n' + chalk.bold('AI target sample:'));
479
- prioritized.slice(0, 15).forEach((file) => console.log(` ${file}`));
43
+ pipeline.prioritizeFiles(stats.auditFiles, maxFilesForAi).slice(0, 15).forEach((file) => console.log(` ${file}`));
44
+ }
45
+ }
46
+ function parseBudget(value) {
47
+ return (['low', 'normal', 'deep'].includes(value) ? value : 'low');
48
+ }
49
+ function parsePositiveInt(value, fallback, max) {
50
+ if (!value)
51
+ return fallback;
52
+ return Math.max(1, Math.min(max, parseInt(String(value), 10) || fallback));
53
+ }
54
+ async function printPersonas() {
55
+ const { listPresets } = await import('../../infra/persona-presets.js');
56
+ console.log(chalk.bold.cyan('\nPersonas (scanner domains):\n'));
57
+ [
58
+ 'security', 'bugs', 'redundancy', 'error-handling', 'architecture', 'testing',
59
+ 'performance', 'infrastructure', 'observability', 'resilience', 'data',
60
+ 'dependencies', 'compliance', 'multitenancy', 'prompt-audit',
61
+ ].forEach((d) => console.log(` ${chalk.cyan(d)}`));
62
+ listPresets();
63
+ }
64
+ async function maybeAutoFix(options, report, orch) {
65
+ if (!options.fix)
66
+ return;
67
+ const maxFixes = parsePositiveInt(options.fixMax, 5, 20);
68
+ const minSev = (['critical', 'high', 'medium'].includes(options.fixMinSeverity)
69
+ ? options.fixMinSeverity : 'high');
70
+ const eligible = report.findings
71
+ .filter((f) => f.file && f.line && ((SEVERITY_RANK[f.severity] ?? 0) >= (SEVERITY_RANK[minSev] ?? 0)))
72
+ .sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0))
73
+ .slice(0, maxFixes);
74
+ if (options.dryRun) {
75
+ console.log('\n' + chalk.bold.yellow(`Fix dry-run: ${eligible.length} finding(s) would be targeted.`));
76
+ eligible.forEach((f, i) => console.log(` ${i + 1}. ${f.severity} ${f.file}:${f.line} ${f.finding}`));
77
+ }
78
+ else if (report.criticalCount > 0 || report.highCount > 0 || minSev === 'medium') {
79
+ console.log(chalk.bold.yellow(`\nAuto-fixing up to ${maxFixes} ${minSev}+ findings...\n`));
80
+ const fixReport = await orch.runAuditFixPipeline(report, { maxFixes, minSeverity: minSev });
81
+ console.log(chalk.bold(`Fix summary: ${fixReport.succeeded} fixed, ${fixReport.failed} failed, ${fixReport.skipped} skipped`));
480
82
  }
481
83
  }
482
84
  export function registerAudit(program) {
483
85
  program
484
86
  .command('audit [target]')
485
- .description('Deep parallel audit: N scanners cover all files, synthesizer unifies findings')
87
+ .description('Deep audit with local scan, capped AI file scope, and compact reports')
486
88
  .option('-n, --scanners <n>', 'number of scanner agents (auto-selected if omitted)')
487
89
  .option('--budget <budget>', 'low | normal | deep (default: low)', 'low')
488
90
  .option('--provider <provider>', 'claude | openrouter (default: claude)')
489
- .option('--model <model>', 'model override for openrouter (e.g. moonshotai/kimi-k2)')
91
+ .option('--model <model>', 'model override for openrouter')
490
92
  .option('--preset <name>', 'persona preset: security, ai, backend, devops, quality, saas, fintech, full')
491
- .option('--domains <list>', 'comma-separated scanner domains, e.g. security,compliance,data')
93
+ .option('--domains <list>', 'comma-separated scanner domains')
492
94
  .option('--list-personas', 'show all available personas and presets then exit')
493
95
  .option('--local-only', 'run only deterministic local scans; no AI scanners or synthesizer')
494
96
  .option('--force-full', 'allow the full preset to start all requested AI scanner domains')
495
- .option('--max-files <n>', 'max prioritized source files sent to AI scanners (default: 30 low, 60 normal, 120 deep)')
496
- .option('--scanner-timeout <seconds>', 'timeout per AI scanner in seconds (default: 90 low, 150 normal, 240 deep)')
97
+ .option('--max-files <n>', 'max prioritized source files sent to AI scanners')
98
+ .option('--scanner-timeout <seconds>', 'timeout per AI scanner in seconds')
99
+ .option('--ai-context-budget <tokens>', 'max approximate tokens for ai-context.md', '8000')
497
100
  .option('--fix', 'auto-fix critical/high findings after audit')
498
101
  .option('--fix-max <n>', 'max findings to auto-fix (default: 5)', '5')
499
102
  .option('--fix-min-severity <s>', 'minimum severity to fix: critical|high|medium (default: high)', 'high')
500
103
  .option('--dry-run', 'collect audit file stats without starting agents')
501
- .option('--incremental', 'only scan files changed since last audit (reuse cache for unchanged)')
104
+ .option('--incremental', 'only scan files changed since last audit')
502
105
  .action(async (target = '.', options) => {
503
- if (options.listPersonas) {
504
- const { listPresets, BUILT_IN_PRESETS } = await import('../../infra/persona-presets.js');
505
- console.log(chalk.bold.cyan('\nPersonas (scanner domains):\n'));
506
- const allDomains = [
507
- 'security', 'bugs', 'redundancy', 'error-handling', 'architecture', 'testing', 'performance',
508
- 'infrastructure', 'observability', 'resilience', 'data', 'dependencies', 'compliance', 'multitenancy', 'prompt-audit',
509
- ];
510
- allDomains.forEach((d) => console.log(` ${chalk.cyan(d)}`));
511
- listPresets();
512
- return;
513
- }
514
- // Merge .aionrc.json config under CLI options
106
+ if (options.listPersonas)
107
+ return printPersonas();
515
108
  const { loadAionConfig, mergeConfig } = await import('../../infra/aion-config.js');
516
- const aionConfig = loadAionConfig(process.cwd());
517
- const mergedOptions = mergeConfig(options, aionConfig);
109
+ const mergedOptions = mergeConfig(options, loadAionConfig(process.cwd()));
518
110
  const explicitN = mergedOptions.scanners ? Math.max(1, Math.min(15, parseInt(String(mergedOptions.scanners), 10) || 5)) : undefined;
519
- const budget = (['low', 'normal', 'deep'].includes(mergedOptions.budget) ? mergedOptions.budget : 'low');
111
+ const budget = parseBudget(mergedOptions.budget);
520
112
  const scannerTimeoutSeconds = mergedOptions.scannerTimeout
521
113
  ? Math.max(10, Math.min(600, parseInt(String(mergedOptions.scannerTimeout), 10) || 90))
522
114
  : budget === 'deep' ? 240 : budget === 'normal' ? 150 : 90;
523
- const maxFilesForAi = mergedOptions.maxFiles
524
- ? Math.max(1, Math.min(500, parseInt(String(mergedOptions.maxFiles), 10) || 30))
525
- : budget === 'deep' ? 120 : budget === 'normal' ? 60 : 30;
526
- const providerName = mergedOptions.provider === 'openrouter' ? 'openrouter' : undefined;
115
+ const maxFilesForAi = parsePositiveInt(mergedOptions.maxFiles, budget === 'deep' ? 120 : budget === 'normal' ? 60 : 30, 500);
527
116
  const policyInput = {
528
117
  budget,
529
118
  ...(mergedOptions.model ? { openrouterModel: mergedOptions.model } : {}),
530
- ...(providerName ? {
531
- plannerProvider: providerName,
532
- investigatorProvider: providerName,
533
- developerProvider: providerName,
534
- reviewerProvider: providerName,
535
- } : {}),
119
+ ...(mergedOptions.provider === 'openrouter' ? { plannerProvider: 'openrouter', investigatorProvider: 'openrouter', developerProvider: 'openrouter', reviewerProvider: 'openrouter' } : {}),
536
120
  };
537
121
  const policy = createRuntimePolicy(policyInput);
538
- const renderer = new Renderer();
539
- const orch = new Orchestrator(process.cwd(), policyInput);
540
122
  if (options.dryRun) {
541
123
  const pipeline = new AuditPipeline(process.cwd(), policy, new CostTracker(), () => { }, () => { });
542
124
  renderDryRun(pipeline, pipeline.collectAuditStats(target), maxFilesForAi);
543
125
  return;
544
126
  }
127
+ const renderer = new Renderer();
128
+ const orch = new Orchestrator(process.cwd(), policyInput);
545
129
  orch.on('agent:start', ({ agentName }) => renderer.agentStart(agentName));
546
130
  orch.on('agent:output', ({ agentName, text }) => renderer.agentChunk(agentName, text));
547
131
  orch.on('agent:done', ({ agentName, durationMs }) => renderer.agentDone(agentName, durationMs));
548
- // Resolve persona domains
549
132
  const { resolveDomainsFromConfig } = await import('../../infra/persona-presets.js');
550
133
  const { domains: explicitDomains, source: domainSource } = resolveDomainsFromConfig(process.cwd(), mergedOptions.preset, mergedOptions.domains, explicitN);
551
134
  const maxAiScanners = explicitN ?? policy.maxAgents;
@@ -553,22 +136,17 @@ export function registerAudit(program) {
553
136
  if (requestsFullPreset && !mergedOptions.forceFull && !mergedOptions.localOnly) {
554
137
  console.error(chalk.red.bold('\nRefusing expensive full audit by default.\n'));
555
138
  console.error(chalk.gray(`Requested ${explicitDomains.length} domains, but ${budget} budget allows ${maxAiScanners} AI scanner(s).`));
556
- console.error(chalk.gray('Use one of:'));
557
- console.error(` ${chalk.cyan('aion audit . --local-only')} no AI tokens`);
558
- console.error(` ${chalk.cyan(`aion audit . --preset ${mergedOptions.preset ?? 'security'} --scanners ${maxAiScanners}`)} capped AI audit`);
559
- console.error(` ${chalk.cyan('aion audit . --preset full --force-full')} explicit full-cost run`);
139
+ console.error(` ${chalk.cyan('aion audit . --local-only')} no AI tokens`);
140
+ console.error(` ${chalk.cyan(`aion audit . --preset ${mergedOptions.preset ?? 'security'} --scanners ${maxAiScanners}`)} capped AI audit`);
141
+ console.error(` ${chalk.cyan('aion audit . --preset full --force-full')} explicit full-cost run`);
560
142
  process.exitCode = 1;
561
143
  return;
562
144
  }
563
- const start = Date.now();
564
- const nLabel = mergedOptions.localOnly
565
- ? 'local-only scan (no AI tokens)'
566
- : explicitDomains.length > 0
567
- ? mergedOptions.localOnly
568
- ? `local-only scan [${domainSource}]`
569
- : `personas: ${explicitDomains.slice(0, maxAiScanners).join(', ')} [${domainSource}]`
145
+ const label = mergedOptions.localOnly ? 'local-only scan (no AI tokens)'
146
+ : explicitDomains.length > 0 ? `personas: ${explicitDomains.slice(0, maxAiScanners).join(', ')} [${domainSource}]`
570
147
  : explicitN ? `${explicitN} scanners` : `auto scanners (${budget} budget)`;
571
- console.log(chalk.bold.cyan(`\nStarting audit with ${nLabel}...\n`));
148
+ console.log(chalk.bold.cyan(`\nStarting audit with ${label}...\n`));
149
+ const start = Date.now();
572
150
  try {
573
151
  const report = await orch.runAuditPipeline(target, explicitN, explicitDomains.length > 0 ? explicitDomains : undefined, {
574
152
  incremental: options.incremental,
@@ -579,46 +157,21 @@ export function registerAudit(program) {
579
157
  });
580
158
  const durationMs = Date.now() - start;
581
159
  renderAuditReport(report, durationMs);
582
- const saved = saveAuditReport(report, durationMs);
583
- console.log(chalk.gray(`report: ${saved.runDir}`));
584
- console.log(chalk.gray(`html: ${saved.html}`));
585
- console.log(chalk.gray(`summary: ${saved.summary}`));
586
- console.log(chalk.gray(`action plan: ${saved.actionPlan}`));
160
+ const costSummary = {
161
+ totalUsd: orch.costs.totalUsd(),
162
+ model: policy.claudeModel,
163
+ perAgent: orch.costs.byAgent().map((e) => ({
164
+ name: e.agentName,
165
+ costUsd: e.costUsd,
166
+ inputTokens: e.usage.inputTokens,
167
+ outputTokens: e.usage.outputTokens,
168
+ })),
169
+ };
170
+ const saved = saveAuditReport(process.cwd(), report, durationMs, parsePositiveInt(mergedOptions.aiContextBudget, 8000, 100000), costSummary);
171
+ console.log(chalk.gray(`\nhtml: ${saved.html}`));
172
+ console.log(chalk.gray(`dashboard: ${saved.dashboard}`));
587
173
  console.log(chalk.dim(orch.costs.summary()));
588
- if (options.fix) {
589
- const maxFixes = Math.max(1, Math.min(20, parseInt(options.fixMax, 10) || 5));
590
- const minSev = (['critical', 'high', 'medium'].includes(options.fixMinSeverity)
591
- ? options.fixMinSeverity : 'high');
592
- const eligible = report.findings
593
- .filter((f) => f.file && f.line && ((SEVERITY_RANK[f.severity] ?? 0) >= (SEVERITY_RANK[minSev] ?? 0)))
594
- .sort((a, b) => (SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0))
595
- .slice(0, maxFixes);
596
- if (options.dryRun) {
597
- // Dry-run: show what would be fixed without running
598
- console.log('\n' + chalk.bold.yellow(`Fix dry-run — ${eligible.length} finding(s) would be targeted (up to ${maxFixes}, severity ≥ ${minSev}):\n`));
599
- if (eligible.length === 0) {
600
- console.log(chalk.dim(' No eligible findings (need file+line and severity ≥ ' + minSev + ')'));
601
- }
602
- else {
603
- eligible.forEach((f, i) => {
604
- const loc = f.line ? `${f.file}:${f.line}` : f.file;
605
- const sev = SEVERITY_COLOR[f.severity]?.(f.severity) ?? chalk.white(f.severity);
606
- console.log(` ${i + 1}. ${sev} ${chalk.bold(loc)}`);
607
- console.log(` ${f.finding}`);
608
- console.log(chalk.dim(` → ${f.recommendation}`));
609
- console.log();
610
- });
611
- }
612
- const skipped = report.findings.length - eligible.length;
613
- if (skipped > 0)
614
- console.log(chalk.dim(` ${skipped} finding(s) skipped (below threshold or missing file+line)`));
615
- }
616
- else if (report.criticalCount > 0 || report.highCount > 0 || minSev === 'medium') {
617
- console.log(chalk.bold.yellow(`\nAuto-fixing up to ${maxFixes} ${minSev}+ findings...\n`));
618
- const fixReport = await orch.runAuditFixPipeline(report, { maxFixes, minSeverity: minSev });
619
- console.log(chalk.bold(`Fix summary: ${fixReport.succeeded} fixed, ${fixReport.failed} failed, ${fixReport.skipped} skipped`));
620
- }
621
- }
174
+ await maybeAutoFix(options, report, orch);
622
175
  process.exit(report.criticalCount > 0 ? 2 : report.highCount > 0 ? 1 : 0);
623
176
  }
624
177
  catch (err) {