@hyperdrive.bot/bmad-workflow 1.0.18 → 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 +5 -2
- package/dist/commands/stories/review.d.ts +124 -0
- package/dist/commands/stories/review.js +516 -0
- package/dist/commands/workflow.d.ts +8 -0
- package/dist/commands/workflow.js +110 -2
- 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/workflow-config.d.ts +77 -0
- package/dist/models/workflow-result.d.ts +7 -0
- package/dist/services/agents/claude-agent-runner.js +19 -3
- 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 +54 -2
- package/dist/services/orchestration/workflow-orchestrator.js +303 -17
- 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/validation/config-validator.d.ts +84 -0
- package/dist/services/validation/config-validator.js +78 -0
- 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/shared-flags.d.ts +1 -0
- package/dist/utils/shared-flags.js +11 -2
- package/package.json +4 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Phase Executor
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates automated code review on completed stories. Runs scanners
|
|
5
|
+
* via SelfHealLoop, classifies issues, tracks tech debt, and produces
|
|
6
|
+
* PASS/FAIL verdicts per story.
|
|
7
|
+
*
|
|
8
|
+
* The interface is consumed by WorkflowOrchestrator for both sequential
|
|
9
|
+
* and pipelined review execution paths. The concrete implementation
|
|
10
|
+
* (DefaultReviewPhaseExecutor) composes SelfHealLoop and TechDebtTracker.
|
|
11
|
+
*/
|
|
12
|
+
import { Severity } from './types.js';
|
|
13
|
+
/** Default severity threshold that blocks the pipeline */
|
|
14
|
+
const DEFAULT_BLOCK_ON = Severity.HIGH;
|
|
15
|
+
/** Severity rank for comparison (higher = more severe) */
|
|
16
|
+
const SEVERITY_RANK = {
|
|
17
|
+
[Severity.CRITICAL]: 4,
|
|
18
|
+
[Severity.HIGH]: 3,
|
|
19
|
+
[Severity.MEDIUM]: 2,
|
|
20
|
+
[Severity.LOW]: 1,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Concrete implementation of ReviewPhaseExecutor.
|
|
24
|
+
*
|
|
25
|
+
* Composes SelfHealLoop (scan + classify + fix) and TechDebtTracker
|
|
26
|
+
* (MEDIUM issues → story file / session backlog). Accepts dependencies
|
|
27
|
+
* via constructor injection for testability.
|
|
28
|
+
*/
|
|
29
|
+
export class DefaultReviewPhaseExecutor {
|
|
30
|
+
logger;
|
|
31
|
+
reporter;
|
|
32
|
+
selfHealLoop;
|
|
33
|
+
techDebtTracker;
|
|
34
|
+
constructor(selfHealLoop, techDebtTracker, logger, reporter) {
|
|
35
|
+
this.selfHealLoop = selfHealLoop;
|
|
36
|
+
this.techDebtTracker = techDebtTracker;
|
|
37
|
+
this.logger = logger;
|
|
38
|
+
this.reporter = reporter;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Review a single story using the self-heal loop.
|
|
42
|
+
*
|
|
43
|
+
* @param story - Story to review
|
|
44
|
+
* @param config - Workflow configuration
|
|
45
|
+
* @returns ReviewResult with verdict and issues
|
|
46
|
+
*/
|
|
47
|
+
async reviewStory(story, config) {
|
|
48
|
+
const storyId = story.fullNumber;
|
|
49
|
+
const storyFile = story.filePath ?? '';
|
|
50
|
+
const blockOn = config.reviewBlockOn ?? DEFAULT_BLOCK_ON;
|
|
51
|
+
this.logger.info({ storyId, blockOn }, 'Reviewing story');
|
|
52
|
+
const context = {
|
|
53
|
+
baseBranch: 'main',
|
|
54
|
+
changedFiles: [],
|
|
55
|
+
projectRoot: config.cwd ?? process.cwd(),
|
|
56
|
+
referenceFiles: config.references ?? [],
|
|
57
|
+
storyFile,
|
|
58
|
+
storyId,
|
|
59
|
+
};
|
|
60
|
+
// Run self-heal loop (scan → classify → fix → rescan)
|
|
61
|
+
const result = await this.selfHealLoop.execute(context);
|
|
62
|
+
// Override verdict based on config.reviewBlockOn threshold
|
|
63
|
+
const blockingIssues = result.issues.filter((issue) => SEVERITY_RANK[issue.severity] >= SEVERITY_RANK[blockOn]);
|
|
64
|
+
const verdict = blockingIssues.length > 0 ? 'FAIL' : 'PASS';
|
|
65
|
+
// Track MEDIUM issues as tech debt
|
|
66
|
+
if (storyFile) {
|
|
67
|
+
await this.techDebtTracker.appendToStory(result.issues, storyFile);
|
|
68
|
+
}
|
|
69
|
+
// Build final result early so reporter gets the enriched result
|
|
70
|
+
const finalResult = {
|
|
71
|
+
issues: result.issues,
|
|
72
|
+
iterations: result.iterations,
|
|
73
|
+
message: verdict === 'FAIL'
|
|
74
|
+
? `${blockingIssues.length} blocking issue(s) (>= ${blockOn}) remain after ${result.iterations} iteration(s)`
|
|
75
|
+
: result.message,
|
|
76
|
+
verdict,
|
|
77
|
+
};
|
|
78
|
+
// Append review report to story file
|
|
79
|
+
if (this.reporter && storyFile) {
|
|
80
|
+
await this.reporter.appendStoryReport(storyFile, finalResult);
|
|
81
|
+
}
|
|
82
|
+
if (verdict === 'FAIL') {
|
|
83
|
+
this.logger.warn({
|
|
84
|
+
blockingCount: blockingIssues.length,
|
|
85
|
+
iterations: result.iterations,
|
|
86
|
+
storyId,
|
|
87
|
+
verdict,
|
|
88
|
+
}, `Story ${storyId} review FAILED — ${blockingIssues.length} blocking issue(s) remain`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
this.logger.info({ iterations: result.iterations, storyId, verdict }, `Story ${storyId} review PASSED`);
|
|
92
|
+
}
|
|
93
|
+
return finalResult;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Review all stories and return a map of storyId → ReviewResult.
|
|
97
|
+
*
|
|
98
|
+
* Only stories with verdict PASS are considered passing. FAIL stories
|
|
99
|
+
* are logged with a warning including failure reasons.
|
|
100
|
+
*
|
|
101
|
+
* @param stories - Story file paths to review
|
|
102
|
+
* @param config - Workflow configuration
|
|
103
|
+
* @returns Map of story identifier to ReviewResult
|
|
104
|
+
*/
|
|
105
|
+
async reviewAll(stories, config) {
|
|
106
|
+
const results = new Map();
|
|
107
|
+
if (stories.length === 0) {
|
|
108
|
+
this.logger.info('No stories to review');
|
|
109
|
+
return results;
|
|
110
|
+
}
|
|
111
|
+
this.logger.info({ storyCount: stories.length }, 'Starting review for all stories');
|
|
112
|
+
for (const storyFile of stories) {
|
|
113
|
+
const storyId = this.extractStoryId(storyFile);
|
|
114
|
+
const story = {
|
|
115
|
+
epicNumber: 0,
|
|
116
|
+
filePath: storyFile,
|
|
117
|
+
fullNumber: storyId,
|
|
118
|
+
number: 0,
|
|
119
|
+
title: storyId,
|
|
120
|
+
};
|
|
121
|
+
const result = await this.reviewStory(story, config);
|
|
122
|
+
results.set(storyId, result);
|
|
123
|
+
}
|
|
124
|
+
// Log summary
|
|
125
|
+
let passCount = 0;
|
|
126
|
+
let failCount = 0;
|
|
127
|
+
for (const [storyId, result] of results) {
|
|
128
|
+
if (result.verdict === 'PASS') {
|
|
129
|
+
passCount++;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
failCount++;
|
|
133
|
+
this.logger.warn({ reason: result.message, storyId }, 'Story excluded from QA — review FAILED');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
this.logger.info({ failCount, passCount, totalStories: stories.length }, 'Review phase complete');
|
|
137
|
+
// Write session-level reports (reporter handles undefined sessionDir gracefully)
|
|
138
|
+
if (this.reporter) {
|
|
139
|
+
const sessionDir = config.sessionDir;
|
|
140
|
+
await this.reporter.writeSessionReports(sessionDir, results);
|
|
141
|
+
}
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Extract a story identifier from a file path.
|
|
146
|
+
* e.g. "docs/stories/PROJ-story-1.001.md" → "PROJ-story-1.001"
|
|
147
|
+
*/
|
|
148
|
+
extractStoryId(filePath) {
|
|
149
|
+
const filename = filePath.split('/').pop() ?? filePath;
|
|
150
|
+
return filename.replace(/\.md$/, '');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Queue
|
|
3
|
+
*
|
|
4
|
+
* FIFO queue for coordinating pipelined review execution. Dev workers enqueue
|
|
5
|
+
* completed stories; review workers dequeue and process them. Supports concurrent
|
|
6
|
+
* consumers with async waiting when queue is empty.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle:
|
|
9
|
+
* Producer: dev workers → enqueue(story) on successful dev completion
|
|
10
|
+
* Consumer: review workers → dequeue() to process stories for review
|
|
11
|
+
* Close: when all dev workers finish → close() → consumers drain → terminate
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import pino from 'pino'
|
|
16
|
+
* import { ReviewQueue } from './review-queue.js'
|
|
17
|
+
*
|
|
18
|
+
* const logger = pino()
|
|
19
|
+
* const queue = new ReviewQueue(logger)
|
|
20
|
+
*
|
|
21
|
+
* // Dev worker: enqueue completed story
|
|
22
|
+
* queue.enqueue(story)
|
|
23
|
+
*
|
|
24
|
+
* // Review worker: dequeue story for review
|
|
25
|
+
* const story = await queue.dequeue()
|
|
26
|
+
* if (story === null) {
|
|
27
|
+
* // Queue is closed and empty
|
|
28
|
+
* return
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* // When all dev workers finish, close the queue
|
|
32
|
+
* queue.close()
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
import type pino from 'pino';
|
|
36
|
+
import type { Story } from '../../models/story.js';
|
|
37
|
+
/**
|
|
38
|
+
* FIFO queue for pipelined review coordination.
|
|
39
|
+
*
|
|
40
|
+
* Provides async waiting for consumers when queue is empty. Safe for concurrent
|
|
41
|
+
* access in Node.js event loop (no mutex needed due to single-threaded execution).
|
|
42
|
+
*/
|
|
43
|
+
export declare class ReviewQueue {
|
|
44
|
+
/**
|
|
45
|
+
* Flag indicating no more stories will be added
|
|
46
|
+
*/
|
|
47
|
+
private closed;
|
|
48
|
+
/**
|
|
49
|
+
* Structured logger instance
|
|
50
|
+
*/
|
|
51
|
+
private readonly logger;
|
|
52
|
+
/**
|
|
53
|
+
* Internal FIFO queue of stories
|
|
54
|
+
*/
|
|
55
|
+
private queue;
|
|
56
|
+
/**
|
|
57
|
+
* Array of waiting dequeue promises
|
|
58
|
+
*/
|
|
59
|
+
private waiters;
|
|
60
|
+
/**
|
|
61
|
+
* Creates a new ReviewQueue instance
|
|
62
|
+
*
|
|
63
|
+
* @param logger - Pino logger for structured logging
|
|
64
|
+
*/
|
|
65
|
+
constructor(logger: pino.Logger);
|
|
66
|
+
/**
|
|
67
|
+
* Signals that no more stories will be added to the queue.
|
|
68
|
+
* All waiting dequeue calls will be resolved with null.
|
|
69
|
+
*/
|
|
70
|
+
close(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Removes and returns the next story from the queue.
|
|
73
|
+
* If queue is empty and not closed, waits asynchronously for a story to be enqueued.
|
|
74
|
+
* If queue is closed and empty, returns null.
|
|
75
|
+
*
|
|
76
|
+
* @returns Promise that resolves to the next story, or null if queue is closed and empty
|
|
77
|
+
*/
|
|
78
|
+
dequeue(): Promise<null | Story>;
|
|
79
|
+
/**
|
|
80
|
+
* Adds a story to the queue
|
|
81
|
+
*
|
|
82
|
+
* @param story - The story to enqueue
|
|
83
|
+
* @throws Error if queue is already closed
|
|
84
|
+
*/
|
|
85
|
+
enqueue(story: Story): void;
|
|
86
|
+
/**
|
|
87
|
+
* Gets the number of pending stories in the queue
|
|
88
|
+
*
|
|
89
|
+
* @returns Number of stories waiting to be dequeued
|
|
90
|
+
*/
|
|
91
|
+
getPendingCount(): number;
|
|
92
|
+
/**
|
|
93
|
+
* Checks if the queue is empty
|
|
94
|
+
*
|
|
95
|
+
* @returns true if queue has no items, false otherwise
|
|
96
|
+
*/
|
|
97
|
+
isEmpty(): boolean;
|
|
98
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Queue
|
|
3
|
+
*
|
|
4
|
+
* FIFO queue for coordinating pipelined review execution. Dev workers enqueue
|
|
5
|
+
* completed stories; review workers dequeue and process them. Supports concurrent
|
|
6
|
+
* consumers with async waiting when queue is empty.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle:
|
|
9
|
+
* Producer: dev workers → enqueue(story) on successful dev completion
|
|
10
|
+
* Consumer: review workers → dequeue() to process stories for review
|
|
11
|
+
* Close: when all dev workers finish → close() → consumers drain → terminate
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import pino from 'pino'
|
|
16
|
+
* import { ReviewQueue } from './review-queue.js'
|
|
17
|
+
*
|
|
18
|
+
* const logger = pino()
|
|
19
|
+
* const queue = new ReviewQueue(logger)
|
|
20
|
+
*
|
|
21
|
+
* // Dev worker: enqueue completed story
|
|
22
|
+
* queue.enqueue(story)
|
|
23
|
+
*
|
|
24
|
+
* // Review worker: dequeue story for review
|
|
25
|
+
* const story = await queue.dequeue()
|
|
26
|
+
* if (story === null) {
|
|
27
|
+
* // Queue is closed and empty
|
|
28
|
+
* return
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* // When all dev workers finish, close the queue
|
|
32
|
+
* queue.close()
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* FIFO queue for pipelined review coordination.
|
|
37
|
+
*
|
|
38
|
+
* Provides async waiting for consumers when queue is empty. Safe for concurrent
|
|
39
|
+
* access in Node.js event loop (no mutex needed due to single-threaded execution).
|
|
40
|
+
*/
|
|
41
|
+
export class ReviewQueue {
|
|
42
|
+
/**
|
|
43
|
+
* Flag indicating no more stories will be added
|
|
44
|
+
*/
|
|
45
|
+
closed = false;
|
|
46
|
+
/**
|
|
47
|
+
* Structured logger instance
|
|
48
|
+
*/
|
|
49
|
+
logger;
|
|
50
|
+
/**
|
|
51
|
+
* Internal FIFO queue of stories
|
|
52
|
+
*/
|
|
53
|
+
queue = [];
|
|
54
|
+
/**
|
|
55
|
+
* Array of waiting dequeue promises
|
|
56
|
+
*/
|
|
57
|
+
waiters = [];
|
|
58
|
+
/**
|
|
59
|
+
* Creates a new ReviewQueue instance
|
|
60
|
+
*
|
|
61
|
+
* @param logger - Pino logger for structured logging
|
|
62
|
+
*/
|
|
63
|
+
constructor(logger) {
|
|
64
|
+
this.logger = logger;
|
|
65
|
+
this.logger.debug('ReviewQueue initialized');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Signals that no more stories will be added to the queue.
|
|
69
|
+
* All waiting dequeue calls will be resolved with null.
|
|
70
|
+
*/
|
|
71
|
+
close() {
|
|
72
|
+
if (this.closed) {
|
|
73
|
+
this.logger.warn('Close called on already closed ReviewQueue');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.closed = true;
|
|
77
|
+
this.logger.info({
|
|
78
|
+
remainingQueueSize: this.queue.length,
|
|
79
|
+
waitersToNotify: this.waiters.length,
|
|
80
|
+
}, 'ReviewQueue closed');
|
|
81
|
+
// Wake up all waiting dequeue calls with null
|
|
82
|
+
while (this.waiters.length > 0) {
|
|
83
|
+
const waiter = this.waiters.shift();
|
|
84
|
+
if (waiter) {
|
|
85
|
+
waiter(null);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Removes and returns the next story from the queue.
|
|
91
|
+
* If queue is empty and not closed, waits asynchronously for a story to be enqueued.
|
|
92
|
+
* If queue is closed and empty, returns null.
|
|
93
|
+
*
|
|
94
|
+
* @returns Promise that resolves to the next story, or null if queue is closed and empty
|
|
95
|
+
*/
|
|
96
|
+
async dequeue() {
|
|
97
|
+
// If queue has items, return immediately
|
|
98
|
+
if (this.queue.length > 0) {
|
|
99
|
+
const story = this.queue.shift() ?? null;
|
|
100
|
+
this.logger.info({
|
|
101
|
+
remainingQueueSize: this.queue.length,
|
|
102
|
+
storyNumber: story?.fullNumber,
|
|
103
|
+
storyTitle: story?.title,
|
|
104
|
+
}, 'Story dequeued from ReviewQueue');
|
|
105
|
+
return story;
|
|
106
|
+
}
|
|
107
|
+
// If closed and empty, return null
|
|
108
|
+
if (this.closed) {
|
|
109
|
+
this.logger.debug({
|
|
110
|
+
closed: this.closed,
|
|
111
|
+
isEmpty: true,
|
|
112
|
+
}, 'ReviewQueue dequeue returning null: queue closed and empty');
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// Wait for item or close
|
|
116
|
+
this.logger.debug({
|
|
117
|
+
closed: this.closed,
|
|
118
|
+
isEmpty: this.isEmpty(),
|
|
119
|
+
waiters: this.waiters.length,
|
|
120
|
+
}, 'ReviewQueue dequeue waiting for story');
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
this.waiters.push(resolve);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Adds a story to the queue
|
|
127
|
+
*
|
|
128
|
+
* @param story - The story to enqueue
|
|
129
|
+
* @throws Error if queue is already closed
|
|
130
|
+
*/
|
|
131
|
+
enqueue(story) {
|
|
132
|
+
if (this.closed) {
|
|
133
|
+
const error = new Error('Cannot enqueue to closed ReviewQueue');
|
|
134
|
+
this.logger.error({
|
|
135
|
+
closed: this.closed,
|
|
136
|
+
storyNumber: story.fullNumber,
|
|
137
|
+
}, 'ReviewQueue enqueue failed: queue closed');
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
this.queue.push(story);
|
|
141
|
+
this.logger.info({
|
|
142
|
+
queueSize: this.queue.length,
|
|
143
|
+
storyNumber: story.fullNumber,
|
|
144
|
+
storyTitle: story.title,
|
|
145
|
+
waiters: this.waiters.length,
|
|
146
|
+
}, 'Story enqueued to ReviewQueue');
|
|
147
|
+
// Wake up a waiting dequeue call if any
|
|
148
|
+
const waiter = this.waiters.shift();
|
|
149
|
+
if (waiter) {
|
|
150
|
+
const dequeuedStory = this.queue.shift() ?? null;
|
|
151
|
+
this.logger.debug({
|
|
152
|
+
remainingQueueSize: this.queue.length,
|
|
153
|
+
storyNumber: dequeuedStory?.fullNumber,
|
|
154
|
+
}, 'Woke up waiting ReviewQueue dequeue consumer');
|
|
155
|
+
waiter(dequeuedStory);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Gets the number of pending stories in the queue
|
|
160
|
+
*
|
|
161
|
+
* @returns Number of stories waiting to be dequeued
|
|
162
|
+
*/
|
|
163
|
+
getPendingCount() {
|
|
164
|
+
return this.queue.length;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Checks if the queue is empty
|
|
168
|
+
*
|
|
169
|
+
* @returns true if queue has no items, false otherwise
|
|
170
|
+
*/
|
|
171
|
+
isEmpty() {
|
|
172
|
+
return this.queue.length === 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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 type pino from 'pino';
|
|
12
|
+
import type { FileManager } from '../file-system/file-manager.js';
|
|
13
|
+
import type { ReviewResult } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Generates review report markdown for stories and session directories.
|
|
16
|
+
*
|
|
17
|
+
* Follows DI pattern from WorkflowSessionScaffolder: FileManager + Logger.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ReviewReporter {
|
|
20
|
+
private readonly fileManager;
|
|
21
|
+
private readonly logger;
|
|
22
|
+
constructor(fileManager: FileManager, logger: pino.Logger);
|
|
23
|
+
/**
|
|
24
|
+
* Append a `## Code Review Results` section to a story markdown file.
|
|
25
|
+
* Inserts before `## Dev Agent Record` if present, otherwise appends at end.
|
|
26
|
+
*
|
|
27
|
+
* Always works regardless of session mode (AC #8).
|
|
28
|
+
*
|
|
29
|
+
* @param storyFilePath - Absolute path to the story markdown file
|
|
30
|
+
* @param result - ReviewResult from the review phase
|
|
31
|
+
*/
|
|
32
|
+
appendStoryReport(storyFilePath: string, result: ReviewResult): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Write all session-level review artifacts to the session directory.
|
|
35
|
+
* Creates `review/` subdirectory containing per-story files, summary, and tech debt backlog.
|
|
36
|
+
*
|
|
37
|
+
* When sessionDir is undefined/null, silently returns (AC #8 — standalone mode guard).
|
|
38
|
+
*
|
|
39
|
+
* @param sessionDir - Absolute path to the workflow session directory, or undefined
|
|
40
|
+
* @param results - Map of storyId → ReviewResult
|
|
41
|
+
*/
|
|
42
|
+
writeSessionReports(sessionDir: string | undefined, results: Map<string, ReviewResult>): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Format the per-story review report block to be appended to a story file.
|
|
45
|
+
*
|
|
46
|
+
* @param result - ReviewResult for the story
|
|
47
|
+
* @returns Markdown string for the review section
|
|
48
|
+
*/
|
|
49
|
+
formatStoryReport(result: ReviewResult): string;
|
|
50
|
+
/**
|
|
51
|
+
* Format the session-level review summary markdown.
|
|
52
|
+
*
|
|
53
|
+
* @param results - Map of storyId → ReviewResult
|
|
54
|
+
* @returns Complete markdown content for review-summary.md
|
|
55
|
+
*/
|
|
56
|
+
formatSessionSummary(results: Map<string, ReviewResult>): string;
|
|
57
|
+
/**
|
|
58
|
+
* Format the session-level tech debt backlog markdown.
|
|
59
|
+
* Aggregates all MEDIUM-severity issues across all stories, grouped by file path.
|
|
60
|
+
*
|
|
61
|
+
* @param results - Map of storyId → ReviewResult
|
|
62
|
+
* @returns Complete markdown content for tech-debt-backlog.md
|
|
63
|
+
*/
|
|
64
|
+
formatTechDebtBacklog(results: Map<string, ReviewResult>): string;
|
|
65
|
+
/**
|
|
66
|
+
* Format an expanded per-story review file for the session review/ directory.
|
|
67
|
+
* More detailed than the appended story report — includes full issue details.
|
|
68
|
+
*
|
|
69
|
+
* @param storyId - Story identifier
|
|
70
|
+
* @param result - ReviewResult for the story
|
|
71
|
+
* @returns Complete markdown content for the per-story review file
|
|
72
|
+
*/
|
|
73
|
+
formatStoryReviewFile(storyId: string, result: ReviewResult): string;
|
|
74
|
+
/**
|
|
75
|
+
* Group classified issues by severity.
|
|
76
|
+
*/
|
|
77
|
+
private groupBySeverity;
|
|
78
|
+
/**
|
|
79
|
+
* Determine the action column text for a severity row in the findings summary table.
|
|
80
|
+
*/
|
|
81
|
+
private determineSeverityAction;
|
|
82
|
+
/**
|
|
83
|
+
* Extract scanner list from a ReviewResult.
|
|
84
|
+
* Since ReviewResult doesn't carry scanner names directly, derive from issue sources
|
|
85
|
+
* or return a default.
|
|
86
|
+
*/
|
|
87
|
+
private extractScannerList;
|
|
88
|
+
/**
|
|
89
|
+
* Extract the story number portion from a story ID.
|
|
90
|
+
* e.g., "PROJ-story-1.001" → "1.001"
|
|
91
|
+
* e.g., "BMAD-ENHANCED-AUTOMATED-CODE-REVIEW-story-3.006" → "3.006"
|
|
92
|
+
*/
|
|
93
|
+
private extractStoryNum;
|
|
94
|
+
}
|