@hyperdrive.bot/bmad-workflow 1.0.17 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/config/show.js +8 -2
- package/dist/commands/decompose.js +26 -5
- package/dist/commands/epics/create.d.ts +1 -0
- package/dist/commands/mcp/add.d.ts +16 -0
- package/dist/commands/mcp/add.js +77 -0
- package/dist/commands/mcp/credential/get.d.ts +14 -0
- package/dist/commands/mcp/credential/get.js +35 -0
- package/dist/commands/mcp/credential/list.d.ts +17 -0
- package/dist/commands/mcp/credential/list.js +67 -0
- package/dist/commands/mcp/credential/remove.d.ts +18 -0
- package/dist/commands/mcp/credential/remove.js +84 -0
- package/dist/commands/mcp/credential/set.d.ts +16 -0
- package/dist/commands/mcp/credential/set.js +41 -0
- package/dist/commands/mcp/credential/validate.d.ts +12 -0
- package/dist/commands/mcp/credential/validate.js +150 -0
- package/dist/commands/mcp/list.d.ts +17 -0
- package/dist/commands/mcp/list.js +80 -0
- package/dist/commands/mcp/logs.d.ts +15 -0
- package/dist/commands/mcp/logs.js +64 -0
- package/dist/commands/mcp/preset.d.ts +15 -0
- package/dist/commands/mcp/preset.js +84 -0
- package/dist/commands/mcp/remove.d.ts +14 -0
- package/dist/commands/mcp/remove.js +36 -0
- package/dist/commands/mcp/start.d.ts +12 -0
- package/dist/commands/mcp/start.js +80 -0
- package/dist/commands/mcp/status.d.ts +30 -0
- package/dist/commands/mcp/status.js +180 -0
- package/dist/commands/mcp/stop.d.ts +12 -0
- package/dist/commands/mcp/stop.js +47 -0
- package/dist/commands/stories/create.d.ts +1 -0
- package/dist/commands/stories/develop.d.ts +1 -0
- package/dist/commands/stories/qa.js +34 -75
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +89 -0
- package/dist/commands/workflow.js +487 -14
- package/dist/mcp/types.d.ts +99 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/utils/docker-utils.d.ts +56 -0
- package/dist/mcp/utils/docker-utils.js +108 -0
- package/dist/mcp/utils/template-loader.d.ts +21 -0
- package/dist/mcp/utils/template-loader.js +60 -0
- package/dist/models/agent-options.d.ts +10 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/workflow-callbacks.d.ts +251 -0
- package/dist/models/workflow-callbacks.js +10 -0
- package/dist/models/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/WorkflowReporter.d.ts +165 -0
- package/dist/services/WorkflowReporter.js +691 -0
- package/dist/services/agents/claude-agent-runner.js +25 -4
- package/dist/services/file-system/path-resolver.d.ts +10 -0
- package/dist/services/file-system/path-resolver.js +12 -0
- package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
- package/dist/services/mcp/mcp-config-manager.js +146 -0
- package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
- package/dist/services/mcp/mcp-context-injector.js +168 -0
- package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
- package/dist/services/mcp/mcp-credential-manager.js +124 -0
- package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
- package/dist/services/mcp/mcp-health-checker.js +162 -0
- package/dist/services/mcp/types/health-types.d.ts +31 -0
- package/dist/services/mcp/types/health-types.js +7 -0
- package/dist/services/orchestration/dependency-graph-executor.js +1 -1
- package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
- package/dist/services/orchestration/task-decomposition-service.js +90 -36
- package/dist/services/orchestration/workflow-orchestrator.d.ts +87 -3
- package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
- package/dist/services/review/ai-review-scanner.d.ts +66 -0
- package/dist/services/review/ai-review-scanner.js +142 -0
- package/dist/services/review/coderabbit-scanner.d.ts +25 -0
- package/dist/services/review/coderabbit-scanner.js +31 -0
- package/dist/services/review/index.d.ts +20 -0
- package/dist/services/review/index.js +15 -0
- package/dist/services/review/lint-scanner.d.ts +46 -0
- package/dist/services/review/lint-scanner.js +172 -0
- package/dist/services/review/review-config.d.ts +62 -0
- package/dist/services/review/review-config.js +91 -0
- package/dist/services/review/review-phase-executor.d.ts +69 -0
- package/dist/services/review/review-phase-executor.js +152 -0
- package/dist/services/review/review-queue.d.ts +98 -0
- package/dist/services/review/review-queue.js +174 -0
- package/dist/services/review/review-reporter.d.ts +94 -0
- package/dist/services/review/review-reporter.js +386 -0
- package/dist/services/review/scanner-factory.d.ts +42 -0
- package/dist/services/review/scanner-factory.js +60 -0
- package/dist/services/review/self-heal-loop.d.ts +58 -0
- package/dist/services/review/self-heal-loop.js +132 -0
- package/dist/services/review/severity-classifier.d.ts +17 -0
- package/dist/services/review/severity-classifier.js +314 -0
- package/dist/services/review/tech-debt-tracker.d.ts +52 -0
- package/dist/services/review/tech-debt-tracker.js +245 -0
- package/dist/services/review/types.d.ts +93 -0
- package/dist/services/review/types.js +23 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
- package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
- package/dist/services/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- package/dist/utils/colors.d.ts +10 -10
- package/dist/utils/colors.js +15 -15
- package/dist/utils/credential-utils.d.ts +14 -0
- package/dist/utils/credential-utils.js +19 -0
- package/dist/utils/duration.d.ts +41 -0
- package/dist/utils/duration.js +89 -0
- package/dist/utils/listr2-helpers.d.ts +216 -0
- package/dist/utils/listr2-helpers.js +334 -0
- package/dist/utils/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- package/package.json +6 -3
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Reporter
|
|
3
|
+
*
|
|
4
|
+
* Generates review report artifacts: per-story markdown appended to story files,
|
|
5
|
+
* and session-level aggregate reports (summary + tech debt backlog + per-story
|
|
6
|
+
* review files) written to the workflow session directory.
|
|
7
|
+
*
|
|
8
|
+
* Consumed by ReviewPhaseExecutor (after each story) and the standalone
|
|
9
|
+
* `stories review` command.
|
|
10
|
+
*/
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { Severity } from './types.js';
|
|
13
|
+
/** Severity display order (most severe first) */
|
|
14
|
+
const SEVERITY_ORDER = [Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW];
|
|
15
|
+
/** Markdown heading that marks the insertion point for per-story reports */
|
|
16
|
+
const DEV_AGENT_RECORD_HEADING = '## Dev Agent Record';
|
|
17
|
+
/** Heading used for the appended review section */
|
|
18
|
+
const REVIEW_SECTION_HEADING = '## Code Review Results';
|
|
19
|
+
/**
|
|
20
|
+
* Generates review report markdown for stories and session directories.
|
|
21
|
+
*
|
|
22
|
+
* Follows DI pattern from WorkflowSessionScaffolder: FileManager + Logger.
|
|
23
|
+
*/
|
|
24
|
+
export class ReviewReporter {
|
|
25
|
+
fileManager;
|
|
26
|
+
logger;
|
|
27
|
+
constructor(fileManager, logger) {
|
|
28
|
+
this.fileManager = fileManager;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Append a `## Code Review Results` section to a story markdown file.
|
|
33
|
+
* Inserts before `## Dev Agent Record` if present, otherwise appends at end.
|
|
34
|
+
*
|
|
35
|
+
* Always works regardless of session mode (AC #8).
|
|
36
|
+
*
|
|
37
|
+
* @param storyFilePath - Absolute path to the story markdown file
|
|
38
|
+
* @param result - ReviewResult from the review phase
|
|
39
|
+
*/
|
|
40
|
+
async appendStoryReport(storyFilePath, result) {
|
|
41
|
+
this.logger.info({ storyFilePath, verdict: result.verdict }, 'Appending review report to story');
|
|
42
|
+
let content;
|
|
43
|
+
try {
|
|
44
|
+
content = await this.fileManager.readFile(storyFilePath);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
this.logger.warn({ storyFilePath }, 'Could not read story file for report append');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const report = this.formatStoryReport(result);
|
|
51
|
+
// Find insertion point: before ## Dev Agent Record, or end of file
|
|
52
|
+
const devAgentIndex = content.indexOf(DEV_AGENT_RECORD_HEADING);
|
|
53
|
+
if (devAgentIndex >= 0) {
|
|
54
|
+
const before = content.slice(0, devAgentIndex);
|
|
55
|
+
const after = content.slice(devAgentIndex);
|
|
56
|
+
content = before + report + '\n' + after;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const separator = content.endsWith('\n') ? '\n' : '\n\n';
|
|
60
|
+
content = content + separator + report;
|
|
61
|
+
}
|
|
62
|
+
await this.fileManager.writeFile(storyFilePath, content);
|
|
63
|
+
this.logger.info({ storyFilePath }, 'Review report appended to story');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Write all session-level review artifacts to the session directory.
|
|
67
|
+
* Creates `review/` subdirectory containing per-story files, summary, and tech debt backlog.
|
|
68
|
+
*
|
|
69
|
+
* When sessionDir is undefined/null, silently returns (AC #8 — standalone mode guard).
|
|
70
|
+
*
|
|
71
|
+
* @param sessionDir - Absolute path to the workflow session directory, or undefined
|
|
72
|
+
* @param results - Map of storyId → ReviewResult
|
|
73
|
+
*/
|
|
74
|
+
async writeSessionReports(sessionDir, results) {
|
|
75
|
+
if (!sessionDir) {
|
|
76
|
+
this.logger.debug('No session directory provided, skipping session reports');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.logger.info({ sessionDir, storyCount: results.size }, 'Writing session review reports');
|
|
80
|
+
const reviewDir = join(sessionDir, 'review');
|
|
81
|
+
await this.fileManager.createDirectory(reviewDir);
|
|
82
|
+
// Write per-story review files
|
|
83
|
+
for (const [storyId, result] of results) {
|
|
84
|
+
const storyNum = this.extractStoryNum(storyId);
|
|
85
|
+
const fileName = `story-${storyNum}-review.md`;
|
|
86
|
+
const filePath = join(reviewDir, fileName);
|
|
87
|
+
const content = this.formatStoryReviewFile(storyId, result);
|
|
88
|
+
await this.fileManager.writeFile(filePath, content);
|
|
89
|
+
}
|
|
90
|
+
// Write review-summary.md
|
|
91
|
+
const summaryPath = join(reviewDir, 'review-summary.md');
|
|
92
|
+
const summaryContent = this.formatSessionSummary(results);
|
|
93
|
+
await this.fileManager.writeFile(summaryPath, summaryContent);
|
|
94
|
+
// Write tech-debt-backlog.md
|
|
95
|
+
const backlogPath = join(reviewDir, 'tech-debt-backlog.md');
|
|
96
|
+
const backlogContent = this.formatTechDebtBacklog(results);
|
|
97
|
+
await this.fileManager.writeFile(backlogPath, backlogContent);
|
|
98
|
+
this.logger.info({ reviewDir }, 'Session review reports written');
|
|
99
|
+
}
|
|
100
|
+
// ─── Per-Story Report (appended to story markdown) ───────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* Format the per-story review report block to be appended to a story file.
|
|
103
|
+
*
|
|
104
|
+
* @param result - ReviewResult for the story
|
|
105
|
+
* @returns Markdown string for the review section
|
|
106
|
+
*/
|
|
107
|
+
formatStoryReport(result) {
|
|
108
|
+
const date = new Date().toISOString().split('T')[0];
|
|
109
|
+
const lines = [];
|
|
110
|
+
lines.push(REVIEW_SECTION_HEADING, '');
|
|
111
|
+
if (result.issues.length === 0) {
|
|
112
|
+
lines.push(`**Date**: ${date}`);
|
|
113
|
+
lines.push(`**Verdict**: PASS — No issues found`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
// Verdict line with iteration count
|
|
118
|
+
const iterationNote = result.iterations > 0 ? ` (after ${result.iterations} iteration${result.iterations > 1 ? 's' : ''})` : '';
|
|
119
|
+
lines.push(`**Date**: ${date}`);
|
|
120
|
+
lines.push(`**Verdict**: ${result.verdict}${iterationNote}`);
|
|
121
|
+
lines.push(`**Scanners**: ${this.extractScannerList(result)}`);
|
|
122
|
+
lines.push('');
|
|
123
|
+
// Findings summary table
|
|
124
|
+
lines.push('### Findings Summary');
|
|
125
|
+
lines.push('| Severity | Count | Action |');
|
|
126
|
+
lines.push('|----------|-------|--------|');
|
|
127
|
+
const grouped = this.groupBySeverity(result.issues);
|
|
128
|
+
const fixedIssues = result.issues.filter((i) => i.fix);
|
|
129
|
+
for (const severity of SEVERITY_ORDER) {
|
|
130
|
+
const issues = grouped.get(severity) ?? [];
|
|
131
|
+
const count = issues.length;
|
|
132
|
+
const action = this.determineSeverityAction(severity, issues, fixedIssues);
|
|
133
|
+
lines.push(`| ${severity} | ${count} | ${action} |`);
|
|
134
|
+
}
|
|
135
|
+
lines.push('');
|
|
136
|
+
// Fixed Issues subsection
|
|
137
|
+
const fixed = result.issues.filter((i) => i.fix);
|
|
138
|
+
if (fixed.length > 0) {
|
|
139
|
+
lines.push('### Fixed Issues');
|
|
140
|
+
for (const issue of fixed) {
|
|
141
|
+
lines.push(`- **[${issue.severity}] ${issue.file}:${issue.line}** — ${issue.issue}. Fixed: ${issue.fix}.`);
|
|
142
|
+
}
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
// Tech Debt (MEDIUM) subsection
|
|
146
|
+
const mediumIssues = grouped.get(Severity.MEDIUM) ?? [];
|
|
147
|
+
const unFixedMedium = mediumIssues.filter((i) => !i.fix);
|
|
148
|
+
if (unFixedMedium.length > 0) {
|
|
149
|
+
lines.push('### Tech Debt (MEDIUM)');
|
|
150
|
+
for (const issue of unFixedMedium) {
|
|
151
|
+
lines.push(`- **${issue.file}:${issue.line}** — ${issue.issue}`);
|
|
152
|
+
}
|
|
153
|
+
lines.push('');
|
|
154
|
+
}
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
// ─── Session Summary ─────────────────────────────────────────────────────
|
|
158
|
+
/**
|
|
159
|
+
* Format the session-level review summary markdown.
|
|
160
|
+
*
|
|
161
|
+
* @param results - Map of storyId → ReviewResult
|
|
162
|
+
* @returns Complete markdown content for review-summary.md
|
|
163
|
+
*/
|
|
164
|
+
formatSessionSummary(results) {
|
|
165
|
+
const timestamp = new Date().toISOString();
|
|
166
|
+
const lines = [];
|
|
167
|
+
lines.push('# Review Summary', '');
|
|
168
|
+
lines.push(`**Review Phase**: ${timestamp}`);
|
|
169
|
+
// Compute totals
|
|
170
|
+
let passCount = 0;
|
|
171
|
+
let failCount = 0;
|
|
172
|
+
let totalDuration = 0;
|
|
173
|
+
for (const result of results.values()) {
|
|
174
|
+
if (result.verdict === 'PASS')
|
|
175
|
+
passCount++;
|
|
176
|
+
else
|
|
177
|
+
failCount++;
|
|
178
|
+
}
|
|
179
|
+
const totalStories = results.size;
|
|
180
|
+
lines.push(`**Duration**: ${totalDuration > 0 ? `${(totalDuration / 1000).toFixed(1)}s` : 'N/A'}`);
|
|
181
|
+
lines.push('');
|
|
182
|
+
// Pass/fail summary
|
|
183
|
+
lines.push(`**${passCount}/${totalStories} stories passed review**`);
|
|
184
|
+
lines.push('');
|
|
185
|
+
// Per-story table
|
|
186
|
+
lines.push('## Stories');
|
|
187
|
+
lines.push('| Story ID | Verdict | Issues | Fix Iterations |');
|
|
188
|
+
lines.push('|----------|---------|--------|----------------|');
|
|
189
|
+
for (const [storyId, result] of results) {
|
|
190
|
+
lines.push(`| ${storyId} | ${result.verdict} | ${result.issues.length} | ${result.iterations} |`);
|
|
191
|
+
}
|
|
192
|
+
lines.push('');
|
|
193
|
+
// Aggregate severity breakdown
|
|
194
|
+
const allIssues = Array.from(results.values()).flatMap((r) => r.issues);
|
|
195
|
+
const grouped = this.groupBySeverity(allIssues);
|
|
196
|
+
const allFixed = allIssues.filter((i) => i.fix);
|
|
197
|
+
const fixedGrouped = this.groupBySeverity(allFixed);
|
|
198
|
+
lines.push('## Severity Breakdown');
|
|
199
|
+
lines.push('| Severity | Total Found | Fixed | Remaining |');
|
|
200
|
+
lines.push('|----------|-------------|-------|-----------|');
|
|
201
|
+
for (const severity of SEVERITY_ORDER) {
|
|
202
|
+
const total = (grouped.get(severity) ?? []).length;
|
|
203
|
+
const fixed = (fixedGrouped.get(severity) ?? []).length;
|
|
204
|
+
const remaining = total - fixed;
|
|
205
|
+
lines.push(`| ${severity} | ${total} | ${fixed} | ${remaining} |`);
|
|
206
|
+
}
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push('---');
|
|
209
|
+
lines.push('');
|
|
210
|
+
lines.push('<!-- Powered by BMAD™ Core -->');
|
|
211
|
+
lines.push('');
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
// ─── Tech Debt Backlog ───────────────────────────────────────────────────
|
|
215
|
+
/**
|
|
216
|
+
* Format the session-level tech debt backlog markdown.
|
|
217
|
+
* Aggregates all MEDIUM-severity issues across all stories, grouped by file path.
|
|
218
|
+
*
|
|
219
|
+
* @param results - Map of storyId → ReviewResult
|
|
220
|
+
* @returns Complete markdown content for tech-debt-backlog.md
|
|
221
|
+
*/
|
|
222
|
+
formatTechDebtBacklog(results) {
|
|
223
|
+
const lines = [];
|
|
224
|
+
// Collect all MEDIUM issues with story context
|
|
225
|
+
const debtEntries = [];
|
|
226
|
+
for (const [storyId, result] of results) {
|
|
227
|
+
const mediumIssues = result.issues.filter((i) => i.severity === Severity.MEDIUM);
|
|
228
|
+
for (const issue of mediumIssues) {
|
|
229
|
+
debtEntries.push({
|
|
230
|
+
file: issue.file,
|
|
231
|
+
issue: issue.issue,
|
|
232
|
+
line: issue.line,
|
|
233
|
+
storyId,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Count stories that contributed debt
|
|
238
|
+
const storyIds = new Set(debtEntries.map((e) => e.storyId));
|
|
239
|
+
lines.push('# Tech Debt Backlog', '');
|
|
240
|
+
lines.push(`**${debtEntries.length} tech debt items across ${storyIds.size} stories**`);
|
|
241
|
+
lines.push('');
|
|
242
|
+
if (debtEntries.length === 0) {
|
|
243
|
+
lines.push('No tech debt items found.');
|
|
244
|
+
lines.push('');
|
|
245
|
+
lines.push('---');
|
|
246
|
+
lines.push('');
|
|
247
|
+
lines.push('<!-- Powered by BMAD™ Core -->');
|
|
248
|
+
lines.push('');
|
|
249
|
+
return lines.join('\n');
|
|
250
|
+
}
|
|
251
|
+
// Group by file path
|
|
252
|
+
const byFile = new Map();
|
|
253
|
+
for (const entry of debtEntries) {
|
|
254
|
+
const existing = byFile.get(entry.file) ?? [];
|
|
255
|
+
existing.push(entry);
|
|
256
|
+
byFile.set(entry.file, existing);
|
|
257
|
+
}
|
|
258
|
+
// Sort file paths alphabetically
|
|
259
|
+
const sortedFiles = Array.from(byFile.keys()).sort();
|
|
260
|
+
for (const filePath of sortedFiles) {
|
|
261
|
+
const entries = byFile.get(filePath);
|
|
262
|
+
lines.push(`## \`${filePath}\``);
|
|
263
|
+
lines.push('');
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
lines.push(`- **Line ${entry.line}** — ${entry.issue} _(from ${entry.storyId})_`);
|
|
266
|
+
}
|
|
267
|
+
lines.push('');
|
|
268
|
+
}
|
|
269
|
+
lines.push('---');
|
|
270
|
+
lines.push('');
|
|
271
|
+
lines.push('<!-- Powered by BMAD™ Core -->');
|
|
272
|
+
lines.push('');
|
|
273
|
+
return lines.join('\n');
|
|
274
|
+
}
|
|
275
|
+
// ─── Per-Story Session Review File ────────────────────────────────────────
|
|
276
|
+
/**
|
|
277
|
+
* Format an expanded per-story review file for the session review/ directory.
|
|
278
|
+
* More detailed than the appended story report — includes full issue details.
|
|
279
|
+
*
|
|
280
|
+
* @param storyId - Story identifier
|
|
281
|
+
* @param result - ReviewResult for the story
|
|
282
|
+
* @returns Complete markdown content for the per-story review file
|
|
283
|
+
*/
|
|
284
|
+
formatStoryReviewFile(storyId, result) {
|
|
285
|
+
const timestamp = new Date().toISOString();
|
|
286
|
+
const lines = [];
|
|
287
|
+
lines.push(`# Review: ${storyId}`, '');
|
|
288
|
+
lines.push(`- **Date:** ${timestamp}`);
|
|
289
|
+
lines.push(`- **Verdict:** ${result.verdict}`);
|
|
290
|
+
lines.push(`- **Iterations:** ${result.iterations}`);
|
|
291
|
+
lines.push(`- **Total Issues:** ${result.issues.length}`);
|
|
292
|
+
lines.push('');
|
|
293
|
+
if (result.issues.length === 0) {
|
|
294
|
+
lines.push('No issues found. Clean pass.');
|
|
295
|
+
lines.push('');
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
// Group by severity for detailed listing
|
|
299
|
+
const grouped = this.groupBySeverity(result.issues);
|
|
300
|
+
for (const severity of SEVERITY_ORDER) {
|
|
301
|
+
const issues = grouped.get(severity) ?? [];
|
|
302
|
+
if (issues.length === 0)
|
|
303
|
+
continue;
|
|
304
|
+
lines.push(`## ${severity} Issues (${issues.length})`, '');
|
|
305
|
+
for (const issue of issues) {
|
|
306
|
+
lines.push(`### \`${issue.file}:${issue.line}\``);
|
|
307
|
+
lines.push('');
|
|
308
|
+
lines.push(`**Issue:** ${issue.issue}`);
|
|
309
|
+
if (issue.fix) {
|
|
310
|
+
lines.push(`**Fix:** ${issue.fix}`);
|
|
311
|
+
}
|
|
312
|
+
lines.push('');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (result.message) {
|
|
317
|
+
lines.push('## Notes', '');
|
|
318
|
+
lines.push(result.message);
|
|
319
|
+
lines.push('');
|
|
320
|
+
}
|
|
321
|
+
lines.push('---');
|
|
322
|
+
lines.push('');
|
|
323
|
+
lines.push('<!-- Powered by BMAD™ Core -->');
|
|
324
|
+
lines.push('');
|
|
325
|
+
return lines.join('\n');
|
|
326
|
+
}
|
|
327
|
+
// ─── Private helpers ──────────────────────────────────────────────────────
|
|
328
|
+
/**
|
|
329
|
+
* Group classified issues by severity.
|
|
330
|
+
*/
|
|
331
|
+
groupBySeverity(issues) {
|
|
332
|
+
const map = new Map();
|
|
333
|
+
for (const severity of SEVERITY_ORDER) {
|
|
334
|
+
map.set(severity, []);
|
|
335
|
+
}
|
|
336
|
+
for (const issue of issues) {
|
|
337
|
+
const list = map.get(issue.severity);
|
|
338
|
+
if (list) {
|
|
339
|
+
list.push(issue);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return map;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Determine the action column text for a severity row in the findings summary table.
|
|
346
|
+
*/
|
|
347
|
+
determineSeverityAction(severity, issues, allFixedIssues) {
|
|
348
|
+
if (issues.length === 0)
|
|
349
|
+
return '-';
|
|
350
|
+
const fixedForSeverity = allFixedIssues.filter((i) => i.severity === severity);
|
|
351
|
+
if (severity === Severity.MEDIUM) {
|
|
352
|
+
return 'Documented as tech debt';
|
|
353
|
+
}
|
|
354
|
+
if (severity === Severity.LOW) {
|
|
355
|
+
return 'Noted';
|
|
356
|
+
}
|
|
357
|
+
// CRITICAL or HIGH
|
|
358
|
+
if (fixedForSeverity.length === issues.length) {
|
|
359
|
+
return `Fixed (iteration ${fixedForSeverity.length > 0 ? 'applied' : '1'})`;
|
|
360
|
+
}
|
|
361
|
+
if (fixedForSeverity.length > 0) {
|
|
362
|
+
return `${fixedForSeverity.length}/${issues.length} fixed`;
|
|
363
|
+
}
|
|
364
|
+
return 'Blocking — not fixed';
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Extract scanner list from a ReviewResult.
|
|
368
|
+
* Since ReviewResult doesn't carry scanner names directly, derive from issue sources
|
|
369
|
+
* or return a default.
|
|
370
|
+
*/
|
|
371
|
+
extractScannerList(result) {
|
|
372
|
+
// ClassifiedIssue doesn't have a `scanner` field, so we return a placeholder.
|
|
373
|
+
// The architecture shows scanners in the report but the type doesn't carry this.
|
|
374
|
+
// Use 'review' as default; in the future, extend ReviewResult with scannersUsed.
|
|
375
|
+
return 'ai, lint';
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Extract the story number portion from a story ID.
|
|
379
|
+
* e.g., "PROJ-story-1.001" → "1.001"
|
|
380
|
+
* e.g., "BMAD-ENHANCED-AUTOMATED-CODE-REVIEW-story-3.006" → "3.006"
|
|
381
|
+
*/
|
|
382
|
+
extractStoryNum(storyId) {
|
|
383
|
+
const match = /story-(\d+\.\d+)/.exec(storyId);
|
|
384
|
+
return match ? match[1] : storyId;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner Factory
|
|
3
|
+
*
|
|
4
|
+
* Async factory function that creates and configures ReviewScanner instances
|
|
5
|
+
* based on project configuration. Always includes LintScanner; conditionally
|
|
6
|
+
* adds AIReviewScanner and CodeRabbitScanner based on config flags and
|
|
7
|
+
* tool availability.
|
|
8
|
+
*/
|
|
9
|
+
import type pino from 'pino';
|
|
10
|
+
import type { AIProviderRunner } from '../agents/agent-runner.js';
|
|
11
|
+
import type { ReviewConfig } from './ai-review-scanner.js';
|
|
12
|
+
import type { ReviewScanner } from './types.js';
|
|
13
|
+
/** Known scanner names that can be selected via --review-scanners */
|
|
14
|
+
export declare const REGISTERED_SCANNER_NAMES: readonly ["lint", "ai", "coderabbit"];
|
|
15
|
+
/**
|
|
16
|
+
* Get the list of registered scanner names
|
|
17
|
+
*
|
|
18
|
+
* @returns Array of valid scanner name strings
|
|
19
|
+
*/
|
|
20
|
+
export declare function getRegisteredScannerNames(): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Create all configured review scanners
|
|
23
|
+
*
|
|
24
|
+
* Always includes LintScanner. AIReviewScanner is included unless explicitly
|
|
25
|
+
* disabled via `config.aiReview === false`. CodeRabbitScanner is included only
|
|
26
|
+
* when `config.coderabbit === true` AND the CLI is installed.
|
|
27
|
+
*
|
|
28
|
+
* @param agentRunner - AI provider runner for spawning agents
|
|
29
|
+
* @param config - Review configuration from core-config.yaml
|
|
30
|
+
* @param logger - Pino logger instance
|
|
31
|
+
* @returns Array of configured ReviewScanner instances
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Timeout options for scanner creation
|
|
35
|
+
*/
|
|
36
|
+
export interface ScannerTimeouts {
|
|
37
|
+
/** Timeout for AI review scanner (ms). Falls back to 300_000 (5m). */
|
|
38
|
+
reviewTimeout?: number;
|
|
39
|
+
/** Timeout for lint scanner (ms). Falls back to 30_000 (30s). */
|
|
40
|
+
timeout?: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function createScanners(agentRunner: AIProviderRunner, config: ReviewConfig, logger: pino.Logger, timeouts?: ScannerTimeouts): Promise<ReviewScanner[]>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scanner Factory
|
|
3
|
+
*
|
|
4
|
+
* Async factory function that creates and configures ReviewScanner instances
|
|
5
|
+
* based on project configuration. Always includes LintScanner; conditionally
|
|
6
|
+
* adds AIReviewScanner and CodeRabbitScanner based on config flags and
|
|
7
|
+
* tool availability.
|
|
8
|
+
*/
|
|
9
|
+
import { exec } from 'node:child_process';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
11
|
+
import { AIReviewScanner } from './ai-review-scanner.js';
|
|
12
|
+
import { CodeRabbitScanner } from './coderabbit-scanner.js';
|
|
13
|
+
import { LintScanner } from './lint-scanner.js';
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
/** Timeout for CodeRabbit CLI availability check */
|
|
16
|
+
const CODERABBIT_CHECK_TIMEOUT = 5_000;
|
|
17
|
+
/**
|
|
18
|
+
* Check if the CodeRabbit CLI is installed and available
|
|
19
|
+
*
|
|
20
|
+
* @returns true if `coderabbit --version` succeeds within timeout
|
|
21
|
+
*/
|
|
22
|
+
async function isCodeRabbitAvailable() {
|
|
23
|
+
try {
|
|
24
|
+
await execAsync('coderabbit --version', { timeout: CODERABBIT_CHECK_TIMEOUT });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Known scanner names that can be selected via --review-scanners */
|
|
32
|
+
export const REGISTERED_SCANNER_NAMES = ['lint', 'ai', 'coderabbit'];
|
|
33
|
+
/**
|
|
34
|
+
* Get the list of registered scanner names
|
|
35
|
+
*
|
|
36
|
+
* @returns Array of valid scanner name strings
|
|
37
|
+
*/
|
|
38
|
+
export function getRegisteredScannerNames() {
|
|
39
|
+
return [...REGISTERED_SCANNER_NAMES];
|
|
40
|
+
}
|
|
41
|
+
export async function createScanners(agentRunner, config, logger, timeouts) {
|
|
42
|
+
const scanners = [];
|
|
43
|
+
// Lint scanner is always included
|
|
44
|
+
scanners.push(new LintScanner(logger, timeouts?.timeout));
|
|
45
|
+
// AI review scanner is default unless explicitly disabled
|
|
46
|
+
if (config.aiReview !== false) {
|
|
47
|
+
scanners.push(new AIReviewScanner(agentRunner, config, logger, timeouts?.reviewTimeout ?? timeouts?.timeout));
|
|
48
|
+
}
|
|
49
|
+
// CodeRabbit scanner — only when requested and CLI is available
|
|
50
|
+
if (config.coderabbit === true) {
|
|
51
|
+
const available = await isCodeRabbitAvailable();
|
|
52
|
+
if (available) {
|
|
53
|
+
scanners.push(new CodeRabbitScanner(logger));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
logger.warn('CodeRabbit CLI not found, skipping');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return scanners;
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Healing Loop
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates scanners, classifies issues, and spawns dev agents to fix
|
|
5
|
+
* CRITICAL/HIGH blocking issues in a retry loop. Exits with PASS when no
|
|
6
|
+
* blocking issues remain, or FAIL when maxIterations is exhausted.
|
|
7
|
+
*
|
|
8
|
+
* Composition pattern: composes scanners + classifier + agent runner.
|
|
9
|
+
*/
|
|
10
|
+
import type pino from 'pino';
|
|
11
|
+
import type { AIProviderRunner } from '../agents/agent-runner.js';
|
|
12
|
+
import type { ClassifiedIssue, RawReviewOutput, ReviewContext, ReviewResult } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Configuration for the self-heal loop
|
|
15
|
+
*/
|
|
16
|
+
export interface SelfHealConfig {
|
|
17
|
+
/** Timeout per fix attempt in ms (default: 300000 = 5 min) */
|
|
18
|
+
fixTimeout: number;
|
|
19
|
+
/** Maximum number of scan→fix iterations (default: 3) */
|
|
20
|
+
maxIterations: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Classifier function signature matching the `classify` export from severity-classifier.ts
|
|
24
|
+
*/
|
|
25
|
+
export type ClassifyFn = (outputs: RawReviewOutput[]) => ClassifiedIssue[];
|
|
26
|
+
/**
|
|
27
|
+
* Scanner interface — matches ReviewScanner from types.ts
|
|
28
|
+
*/
|
|
29
|
+
export interface ScannerLike {
|
|
30
|
+
scan(context: ReviewContext): Promise<RawReviewOutput>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Self-healing review loop that scans, classifies, and auto-fixes blocking issues.
|
|
34
|
+
*/
|
|
35
|
+
export declare class SelfHealLoop {
|
|
36
|
+
private readonly agentRunner;
|
|
37
|
+
private readonly classifier;
|
|
38
|
+
private readonly config;
|
|
39
|
+
private readonly logger;
|
|
40
|
+
private readonly scanners;
|
|
41
|
+
constructor(scanners: ScannerLike[], classifier: ClassifyFn, agentRunner: AIProviderRunner, config: Partial<SelfHealConfig>, logger: pino.Logger);
|
|
42
|
+
/**
|
|
43
|
+
* Execute the self-heal loop: scan → classify → fix → rescan until clean or exhausted.
|
|
44
|
+
*/
|
|
45
|
+
execute(context: ReviewContext): Promise<ReviewResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Build a scoped fix prompt containing only the listed blocking issues.
|
|
48
|
+
*/
|
|
49
|
+
private buildFixPrompt;
|
|
50
|
+
/**
|
|
51
|
+
* Run all registered scanners, handling individual failures gracefully.
|
|
52
|
+
*/
|
|
53
|
+
private runAllScanners;
|
|
54
|
+
/**
|
|
55
|
+
* Spawn a dev agent to apply fixes, handling timeout and errors gracefully.
|
|
56
|
+
*/
|
|
57
|
+
private spawnFixAgent;
|
|
58
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Healing Loop
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates scanners, classifies issues, and spawns dev agents to fix
|
|
5
|
+
* CRITICAL/HIGH blocking issues in a retry loop. Exits with PASS when no
|
|
6
|
+
* blocking issues remain, or FAIL when maxIterations is exhausted.
|
|
7
|
+
*
|
|
8
|
+
* Composition pattern: composes scanners + classifier + agent runner.
|
|
9
|
+
*/
|
|
10
|
+
import { Severity } from './types.js';
|
|
11
|
+
/** Default configuration values */
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
fixTimeout: 300_000,
|
|
14
|
+
maxIterations: 3,
|
|
15
|
+
};
|
|
16
|
+
/** Blocking severities that prevent pipeline from passing */
|
|
17
|
+
const BLOCKING_SEVERITIES = new Set([Severity.CRITICAL, Severity.HIGH]);
|
|
18
|
+
/**
|
|
19
|
+
* Self-healing review loop that scans, classifies, and auto-fixes blocking issues.
|
|
20
|
+
*/
|
|
21
|
+
export class SelfHealLoop {
|
|
22
|
+
agentRunner;
|
|
23
|
+
classifier;
|
|
24
|
+
config;
|
|
25
|
+
logger;
|
|
26
|
+
scanners;
|
|
27
|
+
constructor(scanners, classifier, agentRunner, config, logger) {
|
|
28
|
+
this.scanners = scanners;
|
|
29
|
+
this.classifier = classifier;
|
|
30
|
+
this.agentRunner = agentRunner;
|
|
31
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Execute the self-heal loop: scan → classify → fix → rescan until clean or exhausted.
|
|
36
|
+
*/
|
|
37
|
+
async execute(context) {
|
|
38
|
+
for (let iteration = 0; iteration < this.config.maxIterations; iteration++) {
|
|
39
|
+
// Step 1: Run all scanners
|
|
40
|
+
const rawOutputs = await this.runAllScanners(context);
|
|
41
|
+
// Step 2: Classify issues
|
|
42
|
+
const allIssues = this.classifier(rawOutputs);
|
|
43
|
+
// Step 3: Filter blocking issues
|
|
44
|
+
const blockingIssues = allIssues.filter((issue) => BLOCKING_SEVERITIES.has(issue.severity));
|
|
45
|
+
this.logger.info({ blockingCount: blockingIssues.length, iteration: iteration + 1, maxIterations: this.config.maxIterations, storyId: context.storyId }, 'Review iteration');
|
|
46
|
+
// Step 4: No blocking issues → PASS
|
|
47
|
+
if (blockingIssues.length === 0) {
|
|
48
|
+
return {
|
|
49
|
+
issues: allIssues,
|
|
50
|
+
iterations: iteration + 1,
|
|
51
|
+
verdict: 'PASS',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Step 5: Last iteration — no more fix attempts
|
|
55
|
+
if (iteration === this.config.maxIterations - 1) {
|
|
56
|
+
return {
|
|
57
|
+
issues: allIssues,
|
|
58
|
+
iterations: iteration + 1,
|
|
59
|
+
message: `${blockingIssues.length} blocking issue(s) remain after ${iteration + 1} iteration(s)`,
|
|
60
|
+
verdict: 'FAIL',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Step 6: Build fix prompt and spawn dev agent
|
|
64
|
+
const fixPrompt = this.buildFixPrompt(blockingIssues, context);
|
|
65
|
+
await this.spawnFixAgent(fixPrompt, context);
|
|
66
|
+
}
|
|
67
|
+
// Should not reach here, but satisfy TS return type
|
|
68
|
+
return {
|
|
69
|
+
issues: [],
|
|
70
|
+
iterations: this.config.maxIterations,
|
|
71
|
+
message: 'Loop exited unexpectedly',
|
|
72
|
+
verdict: 'FAIL',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build a scoped fix prompt containing only the listed blocking issues.
|
|
77
|
+
*/
|
|
78
|
+
buildFixPrompt(issues, context) {
|
|
79
|
+
const lines = [
|
|
80
|
+
'You are a senior developer fixing specific code review issues.',
|
|
81
|
+
'',
|
|
82
|
+
'Fix ONLY the listed issues — do not refactor unrelated code.',
|
|
83
|
+
'',
|
|
84
|
+
`Story: ${context.storyId} (${context.storyFile})`,
|
|
85
|
+
'',
|
|
86
|
+
'## Issues to Fix',
|
|
87
|
+
'',
|
|
88
|
+
];
|
|
89
|
+
for (const issue of issues) {
|
|
90
|
+
lines.push(`- **${issue.severity}** in \`${issue.file}\` line ${issue.line}: ${issue.issue}`);
|
|
91
|
+
if (issue.fix) {
|
|
92
|
+
lines.push(` Suggested fix: ${issue.fix}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
lines.push('');
|
|
96
|
+
lines.push('After fixing, run existing tests to verify nothing is broken.');
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Run all registered scanners, handling individual failures gracefully.
|
|
101
|
+
*/
|
|
102
|
+
async runAllScanners(context) {
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const scanner of this.scanners) {
|
|
105
|
+
try {
|
|
106
|
+
const output = await scanner.scan(context);
|
|
107
|
+
results.push(output);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
this.logger.warn({ error: error.message, storyId: context.storyId }, 'Scanner failed, continuing with remaining scanners');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Spawn a dev agent to apply fixes, handling timeout and errors gracefully.
|
|
117
|
+
*/
|
|
118
|
+
async spawnFixAgent(prompt, context) {
|
|
119
|
+
try {
|
|
120
|
+
const result = await this.agentRunner.runAgent(prompt, {
|
|
121
|
+
agentType: 'dev',
|
|
122
|
+
references: [context.storyFile, ...context.changedFiles],
|
|
123
|
+
timeout: this.config.fixTimeout,
|
|
124
|
+
});
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
this.logger.error({ error: error.message, storyId: context.storyId }, 'Fix agent failed');
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|