@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.
Files changed (110) hide show
  1. package/dist/commands/config/show.js +8 -2
  2. package/dist/commands/decompose.js +26 -5
  3. package/dist/commands/epics/create.d.ts +1 -0
  4. package/dist/commands/mcp/add.d.ts +16 -0
  5. package/dist/commands/mcp/add.js +77 -0
  6. package/dist/commands/mcp/credential/get.d.ts +14 -0
  7. package/dist/commands/mcp/credential/get.js +35 -0
  8. package/dist/commands/mcp/credential/list.d.ts +17 -0
  9. package/dist/commands/mcp/credential/list.js +67 -0
  10. package/dist/commands/mcp/credential/remove.d.ts +18 -0
  11. package/dist/commands/mcp/credential/remove.js +84 -0
  12. package/dist/commands/mcp/credential/set.d.ts +16 -0
  13. package/dist/commands/mcp/credential/set.js +41 -0
  14. package/dist/commands/mcp/credential/validate.d.ts +12 -0
  15. package/dist/commands/mcp/credential/validate.js +150 -0
  16. package/dist/commands/mcp/list.d.ts +17 -0
  17. package/dist/commands/mcp/list.js +80 -0
  18. package/dist/commands/mcp/logs.d.ts +15 -0
  19. package/dist/commands/mcp/logs.js +64 -0
  20. package/dist/commands/mcp/preset.d.ts +15 -0
  21. package/dist/commands/mcp/preset.js +84 -0
  22. package/dist/commands/mcp/remove.d.ts +14 -0
  23. package/dist/commands/mcp/remove.js +36 -0
  24. package/dist/commands/mcp/start.d.ts +12 -0
  25. package/dist/commands/mcp/start.js +80 -0
  26. package/dist/commands/mcp/status.d.ts +30 -0
  27. package/dist/commands/mcp/status.js +180 -0
  28. package/dist/commands/mcp/stop.d.ts +12 -0
  29. package/dist/commands/mcp/stop.js +47 -0
  30. package/dist/commands/stories/create.d.ts +1 -0
  31. package/dist/commands/stories/develop.d.ts +1 -0
  32. package/dist/commands/stories/qa.js +34 -75
  33. package/dist/commands/stories/review.d.ts +124 -0
  34. package/dist/commands/stories/review.js +516 -0
  35. package/dist/commands/workflow.d.ts +89 -0
  36. package/dist/commands/workflow.js +487 -14
  37. package/dist/mcp/types.d.ts +99 -0
  38. package/dist/mcp/types.js +7 -0
  39. package/dist/mcp/utils/docker-utils.d.ts +56 -0
  40. package/dist/mcp/utils/docker-utils.js +108 -0
  41. package/dist/mcp/utils/template-loader.d.ts +21 -0
  42. package/dist/mcp/utils/template-loader.js +60 -0
  43. package/dist/models/agent-options.d.ts +10 -1
  44. package/dist/models/index.d.ts +1 -0
  45. package/dist/models/index.js +1 -0
  46. package/dist/models/workflow-callbacks.d.ts +251 -0
  47. package/dist/models/workflow-callbacks.js +10 -0
  48. package/dist/models/workflow-config.d.ts +77 -0
  49. package/dist/models/workflow-result.d.ts +7 -0
  50. package/dist/services/WorkflowReporter.d.ts +165 -0
  51. package/dist/services/WorkflowReporter.js +691 -0
  52. package/dist/services/agents/claude-agent-runner.js +25 -4
  53. package/dist/services/file-system/path-resolver.d.ts +10 -0
  54. package/dist/services/file-system/path-resolver.js +12 -0
  55. package/dist/services/mcp/mcp-config-manager.d.ts +54 -0
  56. package/dist/services/mcp/mcp-config-manager.js +146 -0
  57. package/dist/services/mcp/mcp-context-injector.d.ts +92 -0
  58. package/dist/services/mcp/mcp-context-injector.js +168 -0
  59. package/dist/services/mcp/mcp-credential-manager.d.ts +48 -0
  60. package/dist/services/mcp/mcp-credential-manager.js +124 -0
  61. package/dist/services/mcp/mcp-health-checker.d.ts +56 -0
  62. package/dist/services/mcp/mcp-health-checker.js +162 -0
  63. package/dist/services/mcp/types/health-types.d.ts +31 -0
  64. package/dist/services/mcp/types/health-types.js +7 -0
  65. package/dist/services/orchestration/dependency-graph-executor.js +1 -1
  66. package/dist/services/orchestration/task-decomposition-service.d.ts +2 -1
  67. package/dist/services/orchestration/task-decomposition-service.js +90 -36
  68. package/dist/services/orchestration/workflow-orchestrator.d.ts +87 -3
  69. package/dist/services/orchestration/workflow-orchestrator.js +1169 -289
  70. package/dist/services/review/ai-review-scanner.d.ts +66 -0
  71. package/dist/services/review/ai-review-scanner.js +142 -0
  72. package/dist/services/review/coderabbit-scanner.d.ts +25 -0
  73. package/dist/services/review/coderabbit-scanner.js +31 -0
  74. package/dist/services/review/index.d.ts +20 -0
  75. package/dist/services/review/index.js +15 -0
  76. package/dist/services/review/lint-scanner.d.ts +46 -0
  77. package/dist/services/review/lint-scanner.js +172 -0
  78. package/dist/services/review/review-config.d.ts +62 -0
  79. package/dist/services/review/review-config.js +91 -0
  80. package/dist/services/review/review-phase-executor.d.ts +69 -0
  81. package/dist/services/review/review-phase-executor.js +152 -0
  82. package/dist/services/review/review-queue.d.ts +98 -0
  83. package/dist/services/review/review-queue.js +174 -0
  84. package/dist/services/review/review-reporter.d.ts +94 -0
  85. package/dist/services/review/review-reporter.js +386 -0
  86. package/dist/services/review/scanner-factory.d.ts +42 -0
  87. package/dist/services/review/scanner-factory.js +60 -0
  88. package/dist/services/review/self-heal-loop.d.ts +58 -0
  89. package/dist/services/review/self-heal-loop.js +132 -0
  90. package/dist/services/review/severity-classifier.d.ts +17 -0
  91. package/dist/services/review/severity-classifier.js +314 -0
  92. package/dist/services/review/tech-debt-tracker.d.ts +52 -0
  93. package/dist/services/review/tech-debt-tracker.js +245 -0
  94. package/dist/services/review/types.d.ts +93 -0
  95. package/dist/services/review/types.js +23 -0
  96. package/dist/services/scaffolding/workflow-session-scaffolder.d.ts +182 -0
  97. package/dist/services/scaffolding/workflow-session-scaffolder.js +236 -0
  98. package/dist/services/validation/config-validator.d.ts +84 -0
  99. package/dist/services/validation/config-validator.js +78 -0
  100. package/dist/utils/colors.d.ts +10 -10
  101. package/dist/utils/colors.js +15 -15
  102. package/dist/utils/credential-utils.d.ts +14 -0
  103. package/dist/utils/credential-utils.js +19 -0
  104. package/dist/utils/duration.d.ts +41 -0
  105. package/dist/utils/duration.js +89 -0
  106. package/dist/utils/listr2-helpers.d.ts +216 -0
  107. package/dist/utils/listr2-helpers.js +334 -0
  108. package/dist/utils/shared-flags.d.ts +1 -0
  109. package/dist/utils/shared-flags.js +11 -2
  110. 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
+ }