@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,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Review Scanner
|
|
3
|
+
*
|
|
4
|
+
* Spawns a QA agent to perform structured code review on story implementations.
|
|
5
|
+
* Produces RawReviewOutput with source 'ai' for downstream SeverityClassifier parsing.
|
|
6
|
+
*
|
|
7
|
+
* Prompt enforces strict output format: SEVERITY/FILE/LINE/ISSUE/FIX blocks
|
|
8
|
+
* separated by `---`, or `REVIEW_PASS: No issues found` sentinel for clean reviews.
|
|
9
|
+
*/
|
|
10
|
+
import type pino from 'pino';
|
|
11
|
+
import type { AIProviderRunner } from '../agents/agent-runner.js';
|
|
12
|
+
import type { RawReviewOutput, ReviewContext, ReviewScanner } from './types.js';
|
|
13
|
+
/** Path-specific review rule loaded from core-config.yaml */
|
|
14
|
+
export interface PathRule {
|
|
15
|
+
/** Review focus areas for files matching this pattern */
|
|
16
|
+
focus: string[];
|
|
17
|
+
/** Glob pattern to match against changed files */
|
|
18
|
+
pattern: string;
|
|
19
|
+
}
|
|
20
|
+
/** Review configuration — subset of core-config relevant to AI review */
|
|
21
|
+
export interface ReviewConfig {
|
|
22
|
+
/** Set to false to disable AI review scanner */
|
|
23
|
+
aiReview?: boolean;
|
|
24
|
+
/** Set to true to enable CodeRabbit scanner (requires CLI installed) */
|
|
25
|
+
coderabbit?: boolean;
|
|
26
|
+
/** Path-specific review rules */
|
|
27
|
+
pathRules?: PathRule[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* AIReviewScanner — spawns a QA agent and parses structured review output.
|
|
31
|
+
*
|
|
32
|
+
* Implements ReviewScanner interface. Uses composition over inheritance —
|
|
33
|
+
* composes AIProviderRunner, doesn't extend it.
|
|
34
|
+
*/
|
|
35
|
+
export declare class AIReviewScanner implements ReviewScanner {
|
|
36
|
+
private readonly agentRunner;
|
|
37
|
+
private readonly config;
|
|
38
|
+
private readonly logger;
|
|
39
|
+
private readonly timeout;
|
|
40
|
+
constructor(agentRunner: AIProviderRunner, config: ReviewConfig, logger: pino.Logger, timeout?: number);
|
|
41
|
+
/**
|
|
42
|
+
* Scan the given context by spawning a QA agent for AI-powered code review
|
|
43
|
+
*
|
|
44
|
+
* @param context - Review context describing what to scan
|
|
45
|
+
* @returns Raw output from the AI agent with source 'ai'
|
|
46
|
+
*/
|
|
47
|
+
scan(context: ReviewContext): Promise<RawReviewOutput>;
|
|
48
|
+
/**
|
|
49
|
+
* Build the review prompt with severity definitions, output format spec,
|
|
50
|
+
* changed files, and path-specific rules.
|
|
51
|
+
*/
|
|
52
|
+
private buildReviewPrompt;
|
|
53
|
+
/**
|
|
54
|
+
* Get path rules that match any of the changed files
|
|
55
|
+
*/
|
|
56
|
+
private getMatchingPathRules;
|
|
57
|
+
/**
|
|
58
|
+
* Parse agent output into RawReviewOutput
|
|
59
|
+
*
|
|
60
|
+
* Handles three cases:
|
|
61
|
+
* 1. REVIEW_PASS sentinel → clean result
|
|
62
|
+
* 2. Structured output → pass raw text for SeverityClassifier
|
|
63
|
+
* 3. Malformed/empty output → graceful fallback
|
|
64
|
+
*/
|
|
65
|
+
private parseAgentOutput;
|
|
66
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Review Scanner
|
|
3
|
+
*
|
|
4
|
+
* Spawns a QA agent to perform structured code review on story implementations.
|
|
5
|
+
* Produces RawReviewOutput with source 'ai' for downstream SeverityClassifier parsing.
|
|
6
|
+
*
|
|
7
|
+
* Prompt enforces strict output format: SEVERITY/FILE/LINE/ISSUE/FIX blocks
|
|
8
|
+
* separated by `---`, or `REVIEW_PASS: No issues found` sentinel for clean reviews.
|
|
9
|
+
*/
|
|
10
|
+
/** Default AI review timeout: 5 minutes (NFR1) */
|
|
11
|
+
const DEFAULT_AI_REVIEW_TIMEOUT = 300_000;
|
|
12
|
+
/**
|
|
13
|
+
* Simple glob matcher for path rules.
|
|
14
|
+
* Supports `**` (any path segments) and `*` (single segment wildcard).
|
|
15
|
+
*/
|
|
16
|
+
function matchGlob(file, pattern) {
|
|
17
|
+
// Escape regex special chars, then convert glob wildcards
|
|
18
|
+
const regexStr = pattern
|
|
19
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex chars (not * and ?)
|
|
20
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}') // placeholder for **
|
|
21
|
+
.replace(/\*/g, '[^/]*') // * matches within a segment
|
|
22
|
+
.replace(/\?/g, '[^/]') // ? matches single char
|
|
23
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*'); // ** matches across segments
|
|
24
|
+
return new RegExp(`^${regexStr}$`).test(file);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* AIReviewScanner — spawns a QA agent and parses structured review output.
|
|
28
|
+
*
|
|
29
|
+
* Implements ReviewScanner interface. Uses composition over inheritance —
|
|
30
|
+
* composes AIProviderRunner, doesn't extend it.
|
|
31
|
+
*/
|
|
32
|
+
export class AIReviewScanner {
|
|
33
|
+
agentRunner;
|
|
34
|
+
config;
|
|
35
|
+
logger;
|
|
36
|
+
timeout;
|
|
37
|
+
constructor(agentRunner, config, logger, timeout) {
|
|
38
|
+
this.agentRunner = agentRunner;
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.logger = logger;
|
|
41
|
+
this.timeout = timeout ?? DEFAULT_AI_REVIEW_TIMEOUT;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Scan the given context by spawning a QA agent for AI-powered code review
|
|
45
|
+
*
|
|
46
|
+
* @param context - Review context describing what to scan
|
|
47
|
+
* @returns Raw output from the AI agent with source 'ai'
|
|
48
|
+
*/
|
|
49
|
+
async scan(context) {
|
|
50
|
+
this.logger.info({ changedFiles: context.changedFiles, storyId: context.storyId }, 'Starting AI review scan');
|
|
51
|
+
const prompt = this.buildReviewPrompt(context);
|
|
52
|
+
const references = [context.storyFile, ...context.changedFiles, ...context.referenceFiles];
|
|
53
|
+
try {
|
|
54
|
+
const result = await this.agentRunner.runAgent(prompt, {
|
|
55
|
+
agentType: 'qa',
|
|
56
|
+
references,
|
|
57
|
+
timeout: this.timeout,
|
|
58
|
+
});
|
|
59
|
+
this.logger.info({ duration: result.duration, exitCode: result.exitCode, storyId: context.storyId, success: result.success }, 'AI review scan completed');
|
|
60
|
+
return this.parseAgentOutput(result);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const err = error;
|
|
64
|
+
this.logger.error({ error: err.message, storyId: context.storyId }, 'AI review scan failed');
|
|
65
|
+
return {
|
|
66
|
+
raw: `AI_REVIEW_ERROR: ${err.message}`,
|
|
67
|
+
source: 'ai',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build the review prompt with severity definitions, output format spec,
|
|
73
|
+
* changed files, and path-specific rules.
|
|
74
|
+
*/
|
|
75
|
+
buildReviewPrompt(context) {
|
|
76
|
+
const sections = [];
|
|
77
|
+
// Preamble
|
|
78
|
+
sections.push('You are a senior code reviewer performing a structured quality review.', 'Review the following changed files for the given story and report any issues found.', '');
|
|
79
|
+
// Severity definitions (Architecture Section 5.1)
|
|
80
|
+
sections.push('## Severity Definitions', '', 'CRITICAL — Security vulnerabilities, data loss risks, authentication/authorization bypasses, injection flaws. Maps to NON-NEGOTIABLE governance tier.', 'HIGH — Logic errors, missing error handling, broken functionality, race conditions, missing input validation. Maps to MUST governance tier.', 'MEDIUM — Code style violations, missing tests, poor naming, missing documentation, unnecessary complexity. Maps to SHOULD governance tier (tech debt).', 'LOW — Minor suggestions, optional improvements, cosmetic issues, nitpicks. Maps to MAY governance tier (noted only).', '');
|
|
81
|
+
// Output format spec
|
|
82
|
+
sections.push('## Output Format', '', 'For each issue found, output a block with these fields:', '', 'SEVERITY: <CRITICAL|HIGH|MEDIUM|LOW>', 'FILE: <relative file path>', 'LINE: <line number>', 'ISSUE: <description of the issue>', 'FIX: <suggested fix>', '', 'Separate multiple issue blocks with a line containing only `---`.', '', 'If no issues are found, output exactly:', 'REVIEW_PASS: No issues found', '', 'Do NOT include any other text, explanations, or markdown formatting outside of these blocks.', '');
|
|
83
|
+
// Changed files
|
|
84
|
+
sections.push('## Changed Files', '');
|
|
85
|
+
for (const file of context.changedFiles) {
|
|
86
|
+
sections.push(`- ${file}`);
|
|
87
|
+
}
|
|
88
|
+
sections.push('');
|
|
89
|
+
// Story context
|
|
90
|
+
sections.push(`## Story`, '', `Story file: ${context.storyFile}`, `Story ID: ${context.storyId}`, '');
|
|
91
|
+
// Path-specific rules
|
|
92
|
+
const matchingRules = this.getMatchingPathRules(context.changedFiles);
|
|
93
|
+
if (matchingRules.length > 0) {
|
|
94
|
+
sections.push('## Path-Specific Review Focus', '');
|
|
95
|
+
for (const rule of matchingRules) {
|
|
96
|
+
sections.push(`Files matching \`${rule.pattern}\`:`);
|
|
97
|
+
for (const focus of rule.focus) {
|
|
98
|
+
sections.push(` - ${focus}`);
|
|
99
|
+
}
|
|
100
|
+
sections.push('');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return sections.join('\n');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get path rules that match any of the changed files
|
|
107
|
+
*/
|
|
108
|
+
getMatchingPathRules(changedFiles) {
|
|
109
|
+
const pathRules = this.config.pathRules ?? [];
|
|
110
|
+
if (pathRules.length === 0 || changedFiles.length === 0) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
return pathRules.filter((rule) => changedFiles.some((file) => matchGlob(file, rule.pattern)));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse agent output into RawReviewOutput
|
|
117
|
+
*
|
|
118
|
+
* Handles three cases:
|
|
119
|
+
* 1. REVIEW_PASS sentinel → clean result
|
|
120
|
+
* 2. Structured output → pass raw text for SeverityClassifier
|
|
121
|
+
* 3. Malformed/empty output → graceful fallback
|
|
122
|
+
*/
|
|
123
|
+
parseAgentOutput(result) {
|
|
124
|
+
const output = result.output.trim();
|
|
125
|
+
// Empty or failed output
|
|
126
|
+
if (!output || !result.success) {
|
|
127
|
+
const raw = output || result.errors || 'AI_REVIEW_ERROR: No output from agent';
|
|
128
|
+
this.logger.warn({ exitCode: result.exitCode, hasOutput: !!output }, 'AI review produced no usable output');
|
|
129
|
+
return { raw, source: 'ai' };
|
|
130
|
+
}
|
|
131
|
+
// REVIEW_PASS sentinel
|
|
132
|
+
if (output.includes('REVIEW_PASS:')) {
|
|
133
|
+
this.logger.info('AI review passed with no issues found');
|
|
134
|
+
return {
|
|
135
|
+
raw: 'REVIEW_PASS: No issues found',
|
|
136
|
+
source: 'ai',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Structured output — pass through for SeverityClassifier
|
|
140
|
+
return { raw: output, source: 'ai' };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeRabbit Scanner (Stub)
|
|
3
|
+
*
|
|
4
|
+
* Placeholder for future CodeRabbit integration (Epic 2/3).
|
|
5
|
+
* Implements ReviewScanner interface to allow scanner-factory to
|
|
6
|
+
* compile and dynamically import this module.
|
|
7
|
+
*/
|
|
8
|
+
import type pino from 'pino';
|
|
9
|
+
import type { RawReviewOutput, ReviewContext, ReviewScanner } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* CodeRabbitScanner — runs CodeRabbit CLI for AI-powered code review.
|
|
12
|
+
*
|
|
13
|
+
* Stub implementation — will be fully implemented in a future story.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CodeRabbitScanner implements ReviewScanner {
|
|
16
|
+
private readonly logger;
|
|
17
|
+
constructor(logger: pino.Logger);
|
|
18
|
+
/**
|
|
19
|
+
* Scan the given context using CodeRabbit CLI
|
|
20
|
+
*
|
|
21
|
+
* @param _context - Review context describing what to scan
|
|
22
|
+
* @returns Raw output from CodeRabbit
|
|
23
|
+
*/
|
|
24
|
+
scan(_context: ReviewContext): Promise<RawReviewOutput>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeRabbit Scanner (Stub)
|
|
3
|
+
*
|
|
4
|
+
* Placeholder for future CodeRabbit integration (Epic 2/3).
|
|
5
|
+
* Implements ReviewScanner interface to allow scanner-factory to
|
|
6
|
+
* compile and dynamically import this module.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* CodeRabbitScanner — runs CodeRabbit CLI for AI-powered code review.
|
|
10
|
+
*
|
|
11
|
+
* Stub implementation — will be fully implemented in a future story.
|
|
12
|
+
*/
|
|
13
|
+
export class CodeRabbitScanner {
|
|
14
|
+
logger;
|
|
15
|
+
constructor(logger) {
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Scan the given context using CodeRabbit CLI
|
|
20
|
+
*
|
|
21
|
+
* @param _context - Review context describing what to scan
|
|
22
|
+
* @returns Raw output from CodeRabbit
|
|
23
|
+
*/
|
|
24
|
+
async scan(_context) {
|
|
25
|
+
this.logger.warn('CodeRabbitScanner is a stub — not yet implemented');
|
|
26
|
+
return {
|
|
27
|
+
raw: 'CODERABBIT_NOT_IMPLEMENTED: This scanner is a placeholder for future implementation',
|
|
28
|
+
source: 'coderabbit',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Services
|
|
3
|
+
*
|
|
4
|
+
* Type definitions, interfaces, and classification for the automated code review system.
|
|
5
|
+
*/
|
|
6
|
+
export { AIReviewScanner } from './ai-review-scanner.js';
|
|
7
|
+
export type { PathRule, ReviewConfig } from './ai-review-scanner.js';
|
|
8
|
+
export { CodeRabbitScanner } from './coderabbit-scanner.js';
|
|
9
|
+
export { LintScanner } from './lint-scanner.js';
|
|
10
|
+
export { DefaultReviewPhaseExecutor } from './review-phase-executor.js';
|
|
11
|
+
export type { ReviewPhaseExecutor } from './review-phase-executor.js';
|
|
12
|
+
export { ReviewQueue } from './review-queue.js';
|
|
13
|
+
export { createScanners } from './scanner-factory.js';
|
|
14
|
+
export type { ScannerTimeouts } from './scanner-factory.js';
|
|
15
|
+
export { SelfHealLoop } from './self-heal-loop.js';
|
|
16
|
+
export type { ClassifyFn, SelfHealConfig } from './self-heal-loop.js';
|
|
17
|
+
export { classify } from './severity-classifier.js';
|
|
18
|
+
export { TechDebtTracker } from './tech-debt-tracker.js';
|
|
19
|
+
export { Severity } from './types.js';
|
|
20
|
+
export type { ClassifiedIssue, RawReviewOutput, ReviewContext, ReviewResult, ReviewScanner, ReviewVerdict, } from './types.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Services
|
|
3
|
+
*
|
|
4
|
+
* Type definitions, interfaces, and classification for the automated code review system.
|
|
5
|
+
*/
|
|
6
|
+
export { AIReviewScanner } from './ai-review-scanner.js';
|
|
7
|
+
export { CodeRabbitScanner } from './coderabbit-scanner.js';
|
|
8
|
+
export { LintScanner } from './lint-scanner.js';
|
|
9
|
+
export { DefaultReviewPhaseExecutor } from './review-phase-executor.js';
|
|
10
|
+
export { ReviewQueue } from './review-queue.js';
|
|
11
|
+
export { createScanners } from './scanner-factory.js';
|
|
12
|
+
export { SelfHealLoop } from './self-heal-loop.js';
|
|
13
|
+
export { classify } from './severity-classifier.js';
|
|
14
|
+
export { TechDebtTracker } from './tech-debt-tracker.js';
|
|
15
|
+
export { Severity } from './types.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LintScanner Service
|
|
3
|
+
*
|
|
4
|
+
* Runs ESLint and tsc on changed files and returns structured raw output.
|
|
5
|
+
* Always available in every review pass — provides deterministic, tool-based
|
|
6
|
+
* code quality findings regardless of AI scanner availability.
|
|
7
|
+
*
|
|
8
|
+
* Returns RawReviewOutput; classification into ClassifiedIssue[] happens
|
|
9
|
+
* separately in SeverityClassifier (Story 1.2).
|
|
10
|
+
*/
|
|
11
|
+
import type pino from 'pino';
|
|
12
|
+
import type { RawReviewOutput, ReviewContext, ReviewScanner } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* LintScanner — runs ESLint and tsc and returns raw output.
|
|
15
|
+
*
|
|
16
|
+
* Implements ReviewScanner interface. Both tools run in parallel via
|
|
17
|
+
* Promise.all so wall-clock time is max(eslint, tsc), not the sum.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LintScanner implements ReviewScanner {
|
|
20
|
+
private readonly execOptions;
|
|
21
|
+
private readonly logger;
|
|
22
|
+
constructor(logger: pino.Logger, timeout?: number);
|
|
23
|
+
/**
|
|
24
|
+
* Scan the given context with ESLint and tsc
|
|
25
|
+
*
|
|
26
|
+
* @param context - Review context describing what to scan
|
|
27
|
+
* @returns Raw output from lint tools
|
|
28
|
+
*/
|
|
29
|
+
scan(context: ReviewContext): Promise<RawReviewOutput>;
|
|
30
|
+
/**
|
|
31
|
+
* Run ESLint with --format json on the changed files
|
|
32
|
+
*
|
|
33
|
+
* @returns ESLint JSON output string, or null if skipped/timed out
|
|
34
|
+
*/
|
|
35
|
+
private runEslint;
|
|
36
|
+
/**
|
|
37
|
+
* Run tsc with --noEmit --pretty false on the project
|
|
38
|
+
*
|
|
39
|
+
* @returns tsc error output string, or null if skipped/timed out
|
|
40
|
+
*/
|
|
41
|
+
private runTsc;
|
|
42
|
+
/**
|
|
43
|
+
* Check if any of the candidate files exist in the given directory
|
|
44
|
+
*/
|
|
45
|
+
private hasAnyFile;
|
|
46
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LintScanner Service
|
|
3
|
+
*
|
|
4
|
+
* Runs ESLint and tsc on changed files and returns structured raw output.
|
|
5
|
+
* Always available in every review pass — provides deterministic, tool-based
|
|
6
|
+
* code quality findings regardless of AI scanner availability.
|
|
7
|
+
*
|
|
8
|
+
* Returns RawReviewOutput; classification into ClassifiedIssue[] happens
|
|
9
|
+
* separately in SeverityClassifier (Story 1.2).
|
|
10
|
+
*/
|
|
11
|
+
import { exec } from 'node:child_process';
|
|
12
|
+
import { access } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { promisify } from 'node:util';
|
|
15
|
+
const execAsync = promisify(exec);
|
|
16
|
+
/** ESLint config file candidates (v8 legacy + v9 flat config) */
|
|
17
|
+
const ESLINT_CONFIG_FILES = [
|
|
18
|
+
'.eslintrc',
|
|
19
|
+
'.eslintrc.js',
|
|
20
|
+
'.eslintrc.cjs',
|
|
21
|
+
'.eslintrc.json',
|
|
22
|
+
'.eslintrc.yml',
|
|
23
|
+
'.eslintrc.yaml',
|
|
24
|
+
'eslint.config.js',
|
|
25
|
+
'eslint.config.mjs',
|
|
26
|
+
'eslint.config.cjs',
|
|
27
|
+
];
|
|
28
|
+
/** Default lint timeout: 30 seconds (NFR2) */
|
|
29
|
+
const DEFAULT_LINT_TIMEOUT = 30_000;
|
|
30
|
+
/**
|
|
31
|
+
* LintScanner — runs ESLint and tsc and returns raw output.
|
|
32
|
+
*
|
|
33
|
+
* Implements ReviewScanner interface. Both tools run in parallel via
|
|
34
|
+
* Promise.all so wall-clock time is max(eslint, tsc), not the sum.
|
|
35
|
+
*/
|
|
36
|
+
export class LintScanner {
|
|
37
|
+
execOptions;
|
|
38
|
+
logger;
|
|
39
|
+
constructor(logger, timeout) {
|
|
40
|
+
this.logger = logger;
|
|
41
|
+
this.execOptions = {
|
|
42
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
43
|
+
timeout: timeout ?? DEFAULT_LINT_TIMEOUT,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Scan the given context with ESLint and tsc
|
|
48
|
+
*
|
|
49
|
+
* @param context - Review context describing what to scan
|
|
50
|
+
* @returns Raw output from lint tools
|
|
51
|
+
*/
|
|
52
|
+
async scan(context) {
|
|
53
|
+
const { changedFiles, projectRoot } = context;
|
|
54
|
+
const [eslintOutput, tscOutput] = await Promise.all([
|
|
55
|
+
this.runEslint(changedFiles, projectRoot),
|
|
56
|
+
this.runTsc(projectRoot),
|
|
57
|
+
]);
|
|
58
|
+
// Both tools unavailable
|
|
59
|
+
if (eslintOutput === null && tscOutput === null) {
|
|
60
|
+
return {
|
|
61
|
+
raw: 'NO_LINT_TOOLS_AVAILABLE: Neither ESLint nor tsc are configured in this project',
|
|
62
|
+
source: 'lint',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// Only ESLint
|
|
66
|
+
if (eslintOutput !== null && tscOutput === null) {
|
|
67
|
+
return { raw: eslintOutput, source: 'lint-eslint' };
|
|
68
|
+
}
|
|
69
|
+
// Only tsc
|
|
70
|
+
if (eslintOutput === null && tscOutput !== null) {
|
|
71
|
+
return { raw: tscOutput, source: 'lint-tsc' };
|
|
72
|
+
}
|
|
73
|
+
// Both
|
|
74
|
+
return { raw: eslintOutput + '\n' + tscOutput, source: 'lint' };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Run ESLint with --format json on the changed files
|
|
78
|
+
*
|
|
79
|
+
* @returns ESLint JSON output string, or null if skipped/timed out
|
|
80
|
+
*/
|
|
81
|
+
async runEslint(changedFiles, projectRoot) {
|
|
82
|
+
if (changedFiles.length === 0) {
|
|
83
|
+
this.logger.info('ESLint: no changed files to lint, skipping');
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
// Check for any ESLint config file
|
|
87
|
+
const hasConfig = await this.hasAnyFile(projectRoot, ESLINT_CONFIG_FILES);
|
|
88
|
+
if (!hasConfig) {
|
|
89
|
+
this.logger.info('ESLint: no configuration file found in project root, skipping');
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const fileArgs = changedFiles.join(' ');
|
|
93
|
+
const command = `npx eslint --format json ${fileArgs}`;
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await execAsync(command, {
|
|
96
|
+
...this.execOptions,
|
|
97
|
+
cwd: projectRoot,
|
|
98
|
+
shell: process.env.SHELL || '/bin/bash',
|
|
99
|
+
});
|
|
100
|
+
return stdout;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const execError = error;
|
|
104
|
+
// Timeout
|
|
105
|
+
if (execError.killed === true && execError.signal === 'SIGTERM') {
|
|
106
|
+
this.logger.warn({ timeout: this.execOptions.timeout }, 'ESLint: timed out, skipping');
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// ESLint exits 1 when lint errors are found — stdout still has valid JSON
|
|
110
|
+
if (execError.stdout) {
|
|
111
|
+
return execError.stdout;
|
|
112
|
+
}
|
|
113
|
+
this.logger.warn({ error: error.message }, 'ESLint: execution failed');
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run tsc with --noEmit --pretty false on the project
|
|
119
|
+
*
|
|
120
|
+
* @returns tsc error output string, or null if skipped/timed out
|
|
121
|
+
*/
|
|
122
|
+
async runTsc(projectRoot) {
|
|
123
|
+
const tsconfigPath = join(projectRoot, 'tsconfig.json');
|
|
124
|
+
try {
|
|
125
|
+
await access(tsconfigPath);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
this.logger.info('tsc: no tsconfig.json found in project root, skipping');
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const command = 'npx tsc --noEmit --pretty false';
|
|
132
|
+
try {
|
|
133
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
134
|
+
...this.execOptions,
|
|
135
|
+
cwd: projectRoot,
|
|
136
|
+
shell: process.env.SHELL || '/bin/bash',
|
|
137
|
+
});
|
|
138
|
+
const output = (stdout + '\n' + stderr).trim();
|
|
139
|
+
return output || null;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const execError = error;
|
|
143
|
+
// Timeout
|
|
144
|
+
if (execError.killed === true && execError.signal === 'SIGTERM') {
|
|
145
|
+
this.logger.warn({ timeout: this.execOptions.timeout }, 'tsc: timed out, skipping');
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
// tsc exits 2 when type errors are found — stdout/stderr still has valid output
|
|
149
|
+
const output = ((execError.stdout || '') + '\n' + (execError.stderr || '')).trim();
|
|
150
|
+
if (output) {
|
|
151
|
+
return output;
|
|
152
|
+
}
|
|
153
|
+
this.logger.warn({ error: error.message }, 'tsc: execution failed');
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if any of the candidate files exist in the given directory
|
|
159
|
+
*/
|
|
160
|
+
async hasAnyFile(directory, candidates) {
|
|
161
|
+
for (const file of candidates) {
|
|
162
|
+
try {
|
|
163
|
+
await access(join(directory, file));
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// File doesn't exist, try next
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Configuration Builder
|
|
3
|
+
*
|
|
4
|
+
* Merges review configuration from core-config.yaml with CLI flag overrides.
|
|
5
|
+
* CLI flags always take precedence over config file values. When neither is
|
|
6
|
+
* provided, hardcoded defaults are used.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReviewConfig } from '../validation/config-validator.js';
|
|
9
|
+
import { Severity } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* CLI flag values that can override config file settings.
|
|
12
|
+
* All fields are optional — only provided flags override config.
|
|
13
|
+
*/
|
|
14
|
+
export interface ReviewCliFlags {
|
|
15
|
+
/** Comma-separated severity levels that block the pipeline */
|
|
16
|
+
blockOn?: string;
|
|
17
|
+
/** Maximum self-heal iterations */
|
|
18
|
+
maxFix?: number;
|
|
19
|
+
/** Comma-separated scanner IDs */
|
|
20
|
+
scanners?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Merged review configuration used at runtime
|
|
24
|
+
*/
|
|
25
|
+
export interface MergedReviewConfig {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
pathRules: Array<{
|
|
28
|
+
focus: string;
|
|
29
|
+
pattern: string;
|
|
30
|
+
}>;
|
|
31
|
+
scanners: string[];
|
|
32
|
+
selfHeal: {
|
|
33
|
+
fixAgent: string;
|
|
34
|
+
fixTimeout: number;
|
|
35
|
+
maxIterations: number;
|
|
36
|
+
};
|
|
37
|
+
severity: {
|
|
38
|
+
blockOn: string[];
|
|
39
|
+
documentOn: string[];
|
|
40
|
+
ignoreOn: string[];
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build merged review configuration from config file defaults and CLI flag overrides.
|
|
45
|
+
*
|
|
46
|
+
* Merge precedence: CLI flags > config file > hardcoded defaults.
|
|
47
|
+
*
|
|
48
|
+
* @param coreConfig - Review section from core-config.yaml (may be undefined)
|
|
49
|
+
* @param cliFlags - CLI flag values (may be undefined or partially provided)
|
|
50
|
+
* @returns Fully resolved review configuration
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildReviewConfig(coreConfig?: ReviewConfig, cliFlags?: ReviewCliFlags): MergedReviewConfig;
|
|
53
|
+
/**
|
|
54
|
+
* Convert merged severity.blockOn array to a single Severity threshold.
|
|
55
|
+
*
|
|
56
|
+
* Returns the lowest (most permissive) severity from the blockOn list,
|
|
57
|
+
* matching the pattern used by the review phase executor.
|
|
58
|
+
*
|
|
59
|
+
* @param blockOn - Array of severity level strings
|
|
60
|
+
* @returns Single Severity enum value (lowest rank in the list)
|
|
61
|
+
*/
|
|
62
|
+
export declare function blockOnToSeverity(blockOn: string[]): Severity;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Configuration Builder
|
|
3
|
+
*
|
|
4
|
+
* Merges review configuration from core-config.yaml with CLI flag overrides.
|
|
5
|
+
* CLI flags always take precedence over config file values. When neither is
|
|
6
|
+
* provided, hardcoded defaults are used.
|
|
7
|
+
*/
|
|
8
|
+
import { Severity } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Hardcoded defaults used when neither config file nor CLI flags provide a value
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULTS = {
|
|
13
|
+
enabled: false,
|
|
14
|
+
pathRules: [],
|
|
15
|
+
scanners: ['ai', 'lint'],
|
|
16
|
+
selfHeal: {
|
|
17
|
+
fixAgent: 'dev',
|
|
18
|
+
fixTimeout: 300_000,
|
|
19
|
+
maxIterations: 3,
|
|
20
|
+
},
|
|
21
|
+
severity: {
|
|
22
|
+
blockOn: ['CRITICAL', 'HIGH'],
|
|
23
|
+
documentOn: ['MEDIUM'],
|
|
24
|
+
ignoreOn: ['LOW'],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Build merged review configuration from config file defaults and CLI flag overrides.
|
|
29
|
+
*
|
|
30
|
+
* Merge precedence: CLI flags > config file > hardcoded defaults.
|
|
31
|
+
*
|
|
32
|
+
* @param coreConfig - Review section from core-config.yaml (may be undefined)
|
|
33
|
+
* @param cliFlags - CLI flag values (may be undefined or partially provided)
|
|
34
|
+
* @returns Fully resolved review configuration
|
|
35
|
+
*/
|
|
36
|
+
export function buildReviewConfig(coreConfig, cliFlags) {
|
|
37
|
+
// Start with defaults
|
|
38
|
+
const base = {
|
|
39
|
+
enabled: coreConfig?.enabled ?? DEFAULTS.enabled,
|
|
40
|
+
pathRules: coreConfig?.pathRules ?? DEFAULTS.pathRules,
|
|
41
|
+
scanners: coreConfig?.scanners ?? DEFAULTS.scanners,
|
|
42
|
+
selfHeal: {
|
|
43
|
+
fixAgent: coreConfig?.selfHeal?.fixAgent ?? DEFAULTS.selfHeal.fixAgent,
|
|
44
|
+
fixTimeout: coreConfig?.selfHeal?.fixTimeout ?? DEFAULTS.selfHeal.fixTimeout,
|
|
45
|
+
maxIterations: coreConfig?.selfHeal?.maxIterations ?? DEFAULTS.selfHeal.maxIterations,
|
|
46
|
+
},
|
|
47
|
+
severity: {
|
|
48
|
+
blockOn: coreConfig?.severity?.blockOn ?? DEFAULTS.severity.blockOn,
|
|
49
|
+
documentOn: coreConfig?.severity?.documentOn ?? DEFAULTS.severity.documentOn,
|
|
50
|
+
ignoreOn: coreConfig?.severity?.ignoreOn ?? DEFAULTS.severity.ignoreOn,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
// Apply CLI flag overrides
|
|
54
|
+
if (cliFlags?.scanners) {
|
|
55
|
+
base.scanners = cliFlags.scanners.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
if (cliFlags?.blockOn) {
|
|
58
|
+
base.severity.blockOn = cliFlags.blockOn.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
if (cliFlags?.maxFix !== undefined) {
|
|
61
|
+
base.selfHeal.maxIterations = cliFlags.maxFix;
|
|
62
|
+
}
|
|
63
|
+
return base;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Convert merged severity.blockOn array to a single Severity threshold.
|
|
67
|
+
*
|
|
68
|
+
* Returns the lowest (most permissive) severity from the blockOn list,
|
|
69
|
+
* matching the pattern used by the review phase executor.
|
|
70
|
+
*
|
|
71
|
+
* @param blockOn - Array of severity level strings
|
|
72
|
+
* @returns Single Severity enum value (lowest rank in the list)
|
|
73
|
+
*/
|
|
74
|
+
export function blockOnToSeverity(blockOn) {
|
|
75
|
+
const SEVERITY_RANK = {
|
|
76
|
+
CRITICAL: 4,
|
|
77
|
+
HIGH: 3,
|
|
78
|
+
LOW: 1,
|
|
79
|
+
MEDIUM: 2,
|
|
80
|
+
};
|
|
81
|
+
let lowestRank = Infinity;
|
|
82
|
+
let lowestSeverity = Severity.HIGH;
|
|
83
|
+
for (const sev of blockOn) {
|
|
84
|
+
const rank = SEVERITY_RANK[sev] ?? 0;
|
|
85
|
+
if (rank < lowestRank) {
|
|
86
|
+
lowestRank = rank;
|
|
87
|
+
lowestSeverity = sev;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return lowestSeverity;
|
|
91
|
+
}
|