ai-sdlc 0.1.0-alpha.1

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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +847 -0
  3. package/dist/agents/implementation.d.ts +11 -0
  4. package/dist/agents/implementation.d.ts.map +1 -0
  5. package/dist/agents/implementation.js +123 -0
  6. package/dist/agents/implementation.js.map +1 -0
  7. package/dist/agents/index.d.ts +7 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/index.js +8 -0
  10. package/dist/agents/index.js.map +1 -0
  11. package/dist/agents/planning.d.ts +9 -0
  12. package/dist/agents/planning.d.ts.map +1 -0
  13. package/dist/agents/planning.js +84 -0
  14. package/dist/agents/planning.js.map +1 -0
  15. package/dist/agents/refinement.d.ts +10 -0
  16. package/dist/agents/refinement.d.ts.map +1 -0
  17. package/dist/agents/refinement.js +98 -0
  18. package/dist/agents/refinement.js.map +1 -0
  19. package/dist/agents/research.d.ts +16 -0
  20. package/dist/agents/research.d.ts.map +1 -0
  21. package/dist/agents/research.js +141 -0
  22. package/dist/agents/research.js.map +1 -0
  23. package/dist/agents/review.d.ts +24 -0
  24. package/dist/agents/review.d.ts.map +1 -0
  25. package/dist/agents/review.js +740 -0
  26. package/dist/agents/review.js.map +1 -0
  27. package/dist/agents/rework.d.ts +17 -0
  28. package/dist/agents/rework.d.ts.map +1 -0
  29. package/dist/agents/rework.js +139 -0
  30. package/dist/agents/rework.js.map +1 -0
  31. package/dist/agents/state-assessor.d.ts +21 -0
  32. package/dist/agents/state-assessor.d.ts.map +1 -0
  33. package/dist/agents/state-assessor.js +29 -0
  34. package/dist/agents/state-assessor.js.map +1 -0
  35. package/dist/cli/commands.d.ts +87 -0
  36. package/dist/cli/commands.d.ts.map +1 -0
  37. package/dist/cli/commands.js +1183 -0
  38. package/dist/cli/commands.js.map +1 -0
  39. package/dist/cli/formatting.d.ts +68 -0
  40. package/dist/cli/formatting.d.ts.map +1 -0
  41. package/dist/cli/formatting.js +194 -0
  42. package/dist/cli/formatting.js.map +1 -0
  43. package/dist/cli/runner.d.ts +57 -0
  44. package/dist/cli/runner.d.ts.map +1 -0
  45. package/dist/cli/runner.js +272 -0
  46. package/dist/cli/runner.js.map +1 -0
  47. package/dist/cli/story-utils.d.ts +19 -0
  48. package/dist/cli/story-utils.d.ts.map +1 -0
  49. package/dist/cli/story-utils.js +44 -0
  50. package/dist/cli/story-utils.js.map +1 -0
  51. package/dist/cli/table-renderer.d.ts +22 -0
  52. package/dist/cli/table-renderer.d.ts.map +1 -0
  53. package/dist/cli/table-renderer.js +159 -0
  54. package/dist/cli/table-renderer.js.map +1 -0
  55. package/dist/core/auth.d.ts +39 -0
  56. package/dist/core/auth.d.ts.map +1 -0
  57. package/dist/core/auth.js +128 -0
  58. package/dist/core/auth.js.map +1 -0
  59. package/dist/core/client.d.ts +73 -0
  60. package/dist/core/client.d.ts.map +1 -0
  61. package/dist/core/client.js +140 -0
  62. package/dist/core/client.js.map +1 -0
  63. package/dist/core/config.d.ts +48 -0
  64. package/dist/core/config.d.ts.map +1 -0
  65. package/dist/core/config.js +330 -0
  66. package/dist/core/config.js.map +1 -0
  67. package/dist/core/kanban.d.ts +34 -0
  68. package/dist/core/kanban.d.ts.map +1 -0
  69. package/dist/core/kanban.js +253 -0
  70. package/dist/core/kanban.js.map +1 -0
  71. package/dist/core/story.d.ts +91 -0
  72. package/dist/core/story.d.ts.map +1 -0
  73. package/dist/core/story.js +349 -0
  74. package/dist/core/story.js.map +1 -0
  75. package/dist/core/theme.d.ts +17 -0
  76. package/dist/core/theme.d.ts.map +1 -0
  77. package/dist/core/theme.js +136 -0
  78. package/dist/core/theme.js.map +1 -0
  79. package/dist/core/workflow-state.d.ts +56 -0
  80. package/dist/core/workflow-state.d.ts.map +1 -0
  81. package/dist/core/workflow-state.js +162 -0
  82. package/dist/core/workflow-state.js.map +1 -0
  83. package/dist/index.d.ts +3 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +103 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/types/index.d.ts +228 -0
  88. package/dist/types/index.d.ts.map +1 -0
  89. package/dist/types/index.js +38 -0
  90. package/dist/types/index.js.map +1 -0
  91. package/dist/types/workflow-state.d.ts +54 -0
  92. package/dist/types/workflow-state.d.ts.map +1 -0
  93. package/dist/types/workflow-state.js +5 -0
  94. package/dist/types/workflow-state.js.map +1 -0
  95. package/package.json +71 -0
  96. package/templates/story.md +35 -0
@@ -0,0 +1,740 @@
1
+ import { execSync, spawn } from 'child_process';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { z } from 'zod';
5
+ import { parseStory, moveStory, appendToSection, updateStoryField, isAtMaxRetries, appendReviewHistory, snapshotMaxRetries, getEffectiveMaxRetries } from '../core/story.js';
6
+ import { runAgentQuery } from '../core/client.js';
7
+ import { loadConfig, DEFAULT_TIMEOUTS } from '../core/config.js';
8
+ import { ReviewDecision, ReviewSeverity } from '../types/index.js';
9
+ /**
10
+ * Security: Validate Git branch name to prevent command injection
11
+ * Only allows alphanumeric characters, hyphens, underscores, and forward slashes
12
+ */
13
+ function validateGitBranchName(branchName) {
14
+ return /^[a-zA-Z0-9/_-]+$/.test(branchName);
15
+ }
16
+ /**
17
+ * Security: Escape shell arguments for safe use in commands
18
+ * For use with execSync when shell execution is required
19
+ */
20
+ function escapeShellArg(arg) {
21
+ // Replace single quotes with '\'' and wrap in single quotes
22
+ return `'${arg.replace(/'/g, "'\\''")}'`;
23
+ }
24
+ /**
25
+ * Security: Validate and normalize working directory path
26
+ * Prevents path traversal attacks
27
+ */
28
+ function validateWorkingDirectory(workingDir) {
29
+ // Normalize the path
30
+ const normalized = path.resolve(workingDir);
31
+ // Check if it's an absolute path
32
+ if (!path.isAbsolute(normalized)) {
33
+ throw new Error(`Invalid working directory: must be absolute path (got: ${workingDir})`);
34
+ }
35
+ // Check for path traversal patterns
36
+ if (workingDir.includes('../') || workingDir.includes('..\\')) {
37
+ throw new Error(`Invalid working directory: path traversal detected (${workingDir})`);
38
+ }
39
+ // Verify directory exists
40
+ if (!fs.existsSync(normalized)) {
41
+ throw new Error(`Invalid working directory: does not exist (${normalized})`);
42
+ }
43
+ // Verify it's actually a directory
44
+ if (!fs.statSync(normalized).isDirectory()) {
45
+ throw new Error(`Invalid working directory: not a directory (${normalized})`);
46
+ }
47
+ }
48
+ /**
49
+ * Security: Sanitize error messages to prevent information leakage
50
+ * Removes absolute paths, environment details, and stack traces
51
+ */
52
+ function sanitizeErrorMessage(message, workingDir) {
53
+ let sanitized = message;
54
+ // Replace absolute paths with [PROJECT_ROOT]
55
+ const normalizedWorkingDir = path.resolve(workingDir);
56
+ sanitized = sanitized.replace(new RegExp(normalizedWorkingDir, 'g'), '[PROJECT_ROOT]');
57
+ // Remove home directory paths
58
+ if (process.env.HOME) {
59
+ sanitized = sanitized.replace(new RegExp(process.env.HOME, 'g'), '~');
60
+ }
61
+ // Strip stack traces (keep only first line of error)
62
+ const lines = sanitized.split('\n');
63
+ if (lines.length > 3) {
64
+ sanitized = lines.slice(0, 3).join('\n') + '\n... (stack trace removed)';
65
+ }
66
+ return sanitized;
67
+ }
68
+ /**
69
+ * Security: Sanitize command output before display
70
+ * Strips ANSI codes, control characters, and potential secrets
71
+ */
72
+ function sanitizeCommandOutput(output) {
73
+ let sanitized = output;
74
+ // Strip ANSI escape codes
75
+ sanitized = sanitized.replace(/\x1b\[[0-9;]*m/g, '');
76
+ // Strip other control characters except newlines and tabs
77
+ sanitized = sanitized.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g, '');
78
+ // Redact potential secrets (basic patterns)
79
+ // API keys: long alphanumeric strings after key= or token=
80
+ sanitized = sanitized.replace(/(api[_-]?key|token|password|secret)[\s=:]+[a-zA-Z0-9_-]{20,}/gi, '$1=[REDACTED]');
81
+ return sanitized;
82
+ }
83
+ /**
84
+ * Security: Zod schema for validating LLM review responses
85
+ * Prevents malicious or malformed JSON from causing issues
86
+ */
87
+ const ReviewIssueSchema = z.object({
88
+ severity: z.enum(['blocker', 'critical', 'major', 'minor']),
89
+ category: z.string().max(100),
90
+ description: z.string().max(5000),
91
+ file: z.string().optional(),
92
+ line: z.number().int().positive().optional(),
93
+ suggestedFix: z.string().max(2000).optional(),
94
+ });
95
+ const ReviewResponseSchema = z.object({
96
+ passed: z.boolean(),
97
+ issues: z.array(ReviewIssueSchema),
98
+ });
99
+ /**
100
+ * Maximum size for test output before truncation (10KB)
101
+ */
102
+ const MAX_TEST_OUTPUT_SIZE = 10000;
103
+ /**
104
+ * Run a command asynchronously with timeout and progress updates
105
+ */
106
+ async function runCommandAsync(command, workingDir, timeout, onProgress) {
107
+ return new Promise((resolve) => {
108
+ const outputChunks = [];
109
+ let killed = false;
110
+ // Parse command into executable and args (simple split, handles most cases)
111
+ const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g) || [command];
112
+ const executable = parts[0];
113
+ const args = parts.slice(1).map(arg => arg.replace(/^"|"$/g, ''));
114
+ // Security: Use spawn without shell to prevent command injection
115
+ // Commands must be parseable as: executable + space-separated args
116
+ const child = spawn(executable, args, {
117
+ cwd: workingDir,
118
+ stdio: ['pipe', 'pipe', 'pipe'],
119
+ });
120
+ const timeoutId = setTimeout(() => {
121
+ killed = true;
122
+ child.kill('SIGTERM');
123
+ // Force kill after 5 seconds if SIGTERM didn't work
124
+ setTimeout(() => child.kill('SIGKILL'), 5000);
125
+ }, timeout);
126
+ child.stdout?.on('data', (data) => {
127
+ const text = data.toString();
128
+ outputChunks.push(text);
129
+ onProgress?.(text);
130
+ });
131
+ child.stderr?.on('data', (data) => {
132
+ const text = data.toString();
133
+ outputChunks.push(text);
134
+ onProgress?.(text);
135
+ });
136
+ child.on('close', (code) => {
137
+ clearTimeout(timeoutId);
138
+ const output = outputChunks.join('');
139
+ if (killed) {
140
+ resolve({
141
+ success: false,
142
+ output: output + `\n[Command timed out after ${Math.round(timeout / 1000)} seconds]`,
143
+ });
144
+ }
145
+ else {
146
+ resolve({
147
+ success: code === 0,
148
+ output,
149
+ });
150
+ }
151
+ });
152
+ child.on('error', (error) => {
153
+ clearTimeout(timeoutId);
154
+ const sanitizedError = sanitizeErrorMessage(error.message, workingDir);
155
+ resolve({
156
+ success: false,
157
+ output: outputChunks.join('') + `\n[Command error: ${sanitizedError}]`,
158
+ });
159
+ });
160
+ });
161
+ }
162
+ /**
163
+ * Run build and test commands before review (async version with progress)
164
+ * Returns structured results that can be included in review context
165
+ */
166
+ async function runVerificationAsync(workingDir, config, onProgress) {
167
+ const result = {
168
+ buildPassed: true,
169
+ buildOutput: '',
170
+ testsPassed: true,
171
+ testsOutput: '',
172
+ };
173
+ const buildTimeout = config.timeouts?.buildTimeout ?? DEFAULT_TIMEOUTS.buildTimeout;
174
+ const testTimeout = config.timeouts?.testTimeout ?? DEFAULT_TIMEOUTS.testTimeout;
175
+ // Run build command if configured
176
+ if (config.buildCommand) {
177
+ onProgress?.('build', 'starting', config.buildCommand);
178
+ const buildResult = await runCommandAsync(config.buildCommand, workingDir, buildTimeout, (output) => onProgress?.('build', 'running', output));
179
+ result.buildPassed = buildResult.success;
180
+ result.buildOutput = buildResult.output;
181
+ onProgress?.('build', buildResult.success ? 'passed' : 'failed');
182
+ }
183
+ // Run test command if configured
184
+ if (config.testCommand) {
185
+ onProgress?.('test', 'starting', config.testCommand);
186
+ const testResult = await runCommandAsync(config.testCommand, workingDir, testTimeout, (output) => onProgress?.('test', 'running', output));
187
+ result.testsPassed = testResult.success;
188
+ result.testsOutput = testResult.output;
189
+ onProgress?.('test', testResult.success ? 'passed' : 'failed');
190
+ }
191
+ return result;
192
+ }
193
+ const REVIEW_OUTPUT_FORMAT = `
194
+ Output your review as a JSON object with this structure:
195
+ {
196
+ "passed": true/false,
197
+ "issues": [
198
+ {
199
+ "severity": "blocker" | "critical" | "major" | "minor",
200
+ "category": "code_quality" | "security" | "requirements" | "testing" | etc,
201
+ "description": "Detailed description of the issue",
202
+ "file": "path/to/file.ts" (if applicable),
203
+ "line": 42 (if applicable),
204
+ "suggestedFix": "How to fix this issue"
205
+ }
206
+ ]
207
+ }
208
+
209
+ Severity guidelines:
210
+ - blocker: Must be fixed before merging (security holes, broken functionality)
211
+ - critical: Should be fixed before merging (major bugs, poor practices)
212
+ - major: Should be addressed soon (code quality, maintainability)
213
+ - minor: Nice to have improvements (style, optimizations)
214
+
215
+ If no issues found, return: {"passed": true, "issues": []}
216
+ `;
217
+ const CODE_REVIEW_PROMPT = `You are a senior code reviewer. Review the implementation for:
218
+ 1. Code quality and maintainability
219
+ 2. Following best practices
220
+ 3. Potential bugs or issues
221
+ 4. Test coverage adequacy
222
+
223
+ ${REVIEW_OUTPUT_FORMAT}`;
224
+ const SECURITY_REVIEW_PROMPT = `You are a security specialist. Review the implementation for:
225
+ 1. OWASP Top 10 vulnerabilities
226
+ 2. Input validation issues
227
+ 3. Authentication/authorization problems
228
+ 4. Data exposure risks
229
+
230
+ ${REVIEW_OUTPUT_FORMAT}`;
231
+ const PO_REVIEW_PROMPT = `You are a product owner validating the implementation. Check:
232
+ 1. Does it meet the acceptance criteria?
233
+ 2. Is the user experience appropriate?
234
+ 3. Are edge cases handled?
235
+ 4. Is documentation adequate?
236
+
237
+ ${REVIEW_OUTPUT_FORMAT}`;
238
+ /**
239
+ * Parse review response and extract structured issues
240
+ * Security: Uses zod schema validation to prevent malicious JSON
241
+ */
242
+ function parseReviewResponse(response, reviewType) {
243
+ try {
244
+ // Try to extract JSON from the response
245
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
246
+ if (!jsonMatch) {
247
+ // Fallback: no JSON found, analyze text
248
+ return parseTextReview(response, reviewType);
249
+ }
250
+ const parsed = JSON.parse(jsonMatch[0]);
251
+ // Security: Validate against zod schema before using the data
252
+ const validationResult = ReviewResponseSchema.safeParse(parsed);
253
+ if (!validationResult.success) {
254
+ // Log validation errors for debugging
255
+ console.warn('Review response failed schema validation:', validationResult.error);
256
+ // Fallback to text analysis
257
+ return parseTextReview(response, reviewType);
258
+ }
259
+ const validated = validationResult.data;
260
+ // Map validated data to ReviewIssue format (additional sanitization)
261
+ const issues = validated.issues.map((issue) => ({
262
+ severity: issue.severity,
263
+ category: issue.category,
264
+ description: issue.description,
265
+ file: issue.file,
266
+ line: issue.line,
267
+ suggestedFix: issue.suggestedFix,
268
+ }));
269
+ return {
270
+ passed: validated.passed !== false && issues.filter(i => i.severity === 'blocker' || i.severity === 'critical').length === 0,
271
+ issues,
272
+ };
273
+ }
274
+ catch (error) {
275
+ // Fallback to text analysis if JSON parsing fails
276
+ console.warn('Review response parsing error:', error);
277
+ return parseTextReview(response, reviewType);
278
+ }
279
+ }
280
+ /**
281
+ * Fallback: Parse text-based review response (for when LLM doesn't return JSON)
282
+ */
283
+ function parseTextReview(response, reviewType) {
284
+ const lowerResponse = response.toLowerCase();
285
+ const issues = [];
286
+ // Check for blocking keywords
287
+ if (lowerResponse.includes('block') || lowerResponse.includes('must fix') || lowerResponse.includes('critical security')) {
288
+ issues.push({
289
+ severity: 'blocker',
290
+ category: reviewType.toLowerCase().replace(' ', '_'),
291
+ description: response.substring(0, 500), // First 500 chars as description
292
+ });
293
+ }
294
+ else if (lowerResponse.includes('critical') || lowerResponse.includes('major issue') || lowerResponse.includes('reject')) {
295
+ issues.push({
296
+ severity: 'critical',
297
+ category: reviewType.toLowerCase().replace(' ', '_'),
298
+ description: response.substring(0, 500),
299
+ });
300
+ }
301
+ else if (lowerResponse.includes('should fix') || lowerResponse.includes('improvement needed')) {
302
+ issues.push({
303
+ severity: 'major',
304
+ category: reviewType.toLowerCase().replace(' ', '_'),
305
+ description: response.substring(0, 500),
306
+ });
307
+ }
308
+ // Determine pass/fail
309
+ const passed = lowerResponse.includes('approve') ||
310
+ lowerResponse.includes('looks good') ||
311
+ lowerResponse.includes('pass') ||
312
+ issues.length === 0;
313
+ return { passed: passed && issues.length === 0, issues };
314
+ }
315
+ /**
316
+ * Determine the overall severity of review issues
317
+ */
318
+ function determineReviewSeverity(issues) {
319
+ const blockerCount = issues.filter(i => i.severity === 'blocker').length;
320
+ const criticalCount = issues.filter(i => i.severity === 'critical').length;
321
+ const majorCount = issues.filter(i => i.severity === 'major').length;
322
+ if (blockerCount > 0) {
323
+ return ReviewSeverity.CRITICAL;
324
+ }
325
+ else if (criticalCount >= 2) {
326
+ return ReviewSeverity.HIGH;
327
+ }
328
+ else if (criticalCount === 1 || majorCount > 0) {
329
+ return ReviewSeverity.MEDIUM;
330
+ }
331
+ else {
332
+ return ReviewSeverity.LOW;
333
+ }
334
+ }
335
+ /**
336
+ * Aggregate issues from multiple reviews and determine overall pass/fail
337
+ */
338
+ function aggregateReviews(codeResult, securityResult, poResult) {
339
+ const allIssues = [...codeResult.issues, ...securityResult.issues, ...poResult.issues];
340
+ // Count blocking issues
341
+ const blockerCount = allIssues.filter(i => i.severity === 'blocker').length;
342
+ const criticalCount = allIssues.filter(i => i.severity === 'critical').length;
343
+ // Fail if any blockers or 2+ critical issues
344
+ const passed = blockerCount === 0 && criticalCount < 2;
345
+ // Determine severity
346
+ const severity = determineReviewSeverity(allIssues);
347
+ return { passed, allIssues, severity };
348
+ }
349
+ /**
350
+ * Format issues for display in review notes
351
+ */
352
+ function formatIssuesForDisplay(issues) {
353
+ if (issues.length === 0) {
354
+ return '✅ No issues found';
355
+ }
356
+ const grouped = {
357
+ blocker: issues.filter(i => i.severity === 'blocker'),
358
+ critical: issues.filter(i => i.severity === 'critical'),
359
+ major: issues.filter(i => i.severity === 'major'),
360
+ minor: issues.filter(i => i.severity === 'minor'),
361
+ };
362
+ let output = '';
363
+ for (const [severity, issueList] of Object.entries(grouped)) {
364
+ if (issueList.length === 0)
365
+ continue;
366
+ const icon = severity === 'blocker' ? '🛑' : severity === 'critical' ? '⚠️' : severity === 'major' ? '📋' : 'ℹ️';
367
+ output += `\n#### ${icon} ${severity.toUpperCase()} (${issueList.length})\n\n`;
368
+ for (const issue of issueList) {
369
+ output += `**${issue.category}**: ${issue.description}\n`;
370
+ if (issue.file) {
371
+ output += ` - File: \`${issue.file}\`${issue.line ? `:${issue.line}` : ''}\n`;
372
+ }
373
+ if (issue.suggestedFix) {
374
+ output += ` - Suggested fix: ${issue.suggestedFix}\n`;
375
+ }
376
+ output += '\n';
377
+ }
378
+ }
379
+ return output;
380
+ }
381
+ /**
382
+ * Review Agent
383
+ *
384
+ * Orchestrates code review, security review, and PO acceptance.
385
+ * Now returns structured ReviewResult with pass/fail and issues.
386
+ */
387
+ export async function runReviewAgent(storyPath, sdlcRoot, options) {
388
+ const story = parseStory(storyPath);
389
+ const changesMade = [];
390
+ const workingDir = path.dirname(sdlcRoot);
391
+ // Security: Validate working directory before any operations
392
+ try {
393
+ validateWorkingDirectory(workingDir);
394
+ }
395
+ catch (error) {
396
+ const errorMsg = error instanceof Error ? error.message : String(error);
397
+ return {
398
+ success: false,
399
+ story,
400
+ changesMade,
401
+ error: errorMsg,
402
+ passed: false,
403
+ decision: ReviewDecision.FAILED,
404
+ reviewType: 'combined',
405
+ issues: [{
406
+ severity: 'blocker',
407
+ category: 'security',
408
+ description: `Working directory validation failed: ${errorMsg}`,
409
+ }],
410
+ feedback: errorMsg,
411
+ };
412
+ }
413
+ const config = loadConfig(workingDir);
414
+ try {
415
+ // Snapshot max_retries from config (protects against mid-cycle config changes)
416
+ snapshotMaxRetries(story, config);
417
+ // Check if story has reached max retries
418
+ if (isAtMaxRetries(story, config)) {
419
+ const retryCount = story.frontmatter.retry_count || 0;
420
+ const maxRetries = getEffectiveMaxRetries(story, config);
421
+ const maxRetriesDisplay = Number.isFinite(maxRetries) ? maxRetries : '∞';
422
+ const errorMsg = `Story has reached maximum retry limit (${retryCount}/${maxRetriesDisplay}). Manual intervention required.`;
423
+ updateStoryField(story, 'last_error', errorMsg);
424
+ changesMade.push(errorMsg);
425
+ return {
426
+ success: false,
427
+ story: parseStory(storyPath),
428
+ changesMade,
429
+ error: errorMsg,
430
+ passed: false,
431
+ decision: ReviewDecision.FAILED,
432
+ reviewType: 'combined',
433
+ issues: [{
434
+ severity: 'blocker',
435
+ category: 'max_retries_reached',
436
+ description: errorMsg,
437
+ }],
438
+ feedback: errorMsg,
439
+ };
440
+ }
441
+ // Run build and tests BEFORE reviews (async with progress)
442
+ changesMade.push('Running build and test verification...');
443
+ const verification = await runVerificationAsync(workingDir, config, options?.onVerificationProgress);
444
+ // Create verification issues if build/tests failed
445
+ const verificationIssues = [];
446
+ let verificationContext = '';
447
+ if (config.buildCommand) {
448
+ if (verification.buildPassed) {
449
+ changesMade.push(`Build passed: ${config.buildCommand}`);
450
+ verificationContext += `\n## Build Results ✅\nBuild command \`${config.buildCommand}\` passed successfully.\n`;
451
+ }
452
+ else {
453
+ changesMade.push(`Build FAILED: ${config.buildCommand}`);
454
+ const sanitizedBuildOutput = sanitizeCommandOutput(verification.buildOutput);
455
+ verificationIssues.push({
456
+ severity: 'blocker',
457
+ category: 'build',
458
+ description: `Build failed. Command: ${config.buildCommand}`,
459
+ suggestedFix: 'Fix build errors before review can proceed.',
460
+ });
461
+ verificationContext += `\n## Build Results ❌\nBuild command \`${config.buildCommand}\` FAILED:\n\`\`\`\n${sanitizedBuildOutput.substring(0, 2000)}\n\`\`\`\n`;
462
+ }
463
+ }
464
+ if (config.testCommand) {
465
+ if (verification.testsPassed) {
466
+ changesMade.push(`Tests passed: ${config.testCommand}`);
467
+ verificationContext += `\n## Test Results ✅\nTest command \`${config.testCommand}\` passed successfully.\n`;
468
+ // Include summary of test output (last 500 chars typically has summary)
469
+ const testSummary = verification.testsOutput.slice(-500);
470
+ if (testSummary) {
471
+ verificationContext += `\`\`\`\n${testSummary}\n\`\`\`\n`;
472
+ }
473
+ }
474
+ else {
475
+ changesMade.push(`Tests FAILED: ${config.testCommand}`);
476
+ // Sanitize and truncate test output if too large, preserving readability
477
+ let testOutput = sanitizeCommandOutput(verification.testsOutput);
478
+ let truncationNote = '';
479
+ if (testOutput.length > MAX_TEST_OUTPUT_SIZE) {
480
+ testOutput = testOutput.substring(0, MAX_TEST_OUTPUT_SIZE);
481
+ truncationNote = '\n\n... (output truncated - showing first 10KB)';
482
+ }
483
+ verificationIssues.push({
484
+ severity: 'blocker',
485
+ category: 'testing',
486
+ description: `Tests must pass before code review can proceed.\n\nCommand: ${config.testCommand}\n\nTest output:\n\`\`\`\n${testOutput}${truncationNote}\n\`\`\``,
487
+ suggestedFix: 'Fix failing tests before review can proceed.',
488
+ });
489
+ verificationContext += `\n## Test Results ❌\nTest command \`${config.testCommand}\` FAILED:\n\`\`\`\n${testOutput}${truncationNote}\n\`\`\`\n`;
490
+ }
491
+ }
492
+ // OPTIMIZATION: If verification failed (build or tests), skip LLM-based reviews to save tokens and time.
493
+ // Return immediately with BLOCKER issues - developers should fix verification issues before review feedback is useful.
494
+ if (verificationIssues.length > 0) {
495
+ changesMade.push('Skipping code/security/PO reviews - verification must pass first');
496
+ return {
497
+ success: true, // Agent executed successfully
498
+ story: parseStory(storyPath),
499
+ changesMade,
500
+ passed: false, // Review did not pass
501
+ decision: ReviewDecision.REJECTED,
502
+ severity: ReviewSeverity.CRITICAL,
503
+ reviewType: 'combined',
504
+ issues: verificationIssues,
505
+ feedback: formatIssuesForDisplay(verificationIssues),
506
+ };
507
+ }
508
+ // Verification passed - proceed with all reviews in parallel, passing verification context
509
+ changesMade.push('Verification passed - proceeding with code/security/PO reviews');
510
+ const [codeReview, securityReview, poReview] = await Promise.all([
511
+ runSubReview(story, CODE_REVIEW_PROMPT, 'Code Review', workingDir, verificationContext),
512
+ runSubReview(story, SECURITY_REVIEW_PROMPT, 'Security Review', workingDir, verificationContext),
513
+ runSubReview(story, PO_REVIEW_PROMPT, 'Product Owner Review', workingDir, verificationContext),
514
+ ]);
515
+ // Parse each review response into structured issues
516
+ const codeResult = parseReviewResponse(codeReview, 'Code Review');
517
+ const securityResult = parseReviewResponse(securityReview, 'Security Review');
518
+ const poResult = parseReviewResponse(poReview, 'Product Owner Review');
519
+ // Add verification issues to code result (they're code-quality related)
520
+ codeResult.issues.unshift(...verificationIssues);
521
+ if (verificationIssues.length > 0) {
522
+ codeResult.passed = false;
523
+ }
524
+ // Aggregate all issues and determine overall pass/fail
525
+ const { passed, allIssues, severity } = aggregateReviews(codeResult, securityResult, poResult);
526
+ // Compile review notes with structured format
527
+ const reviewNotes = `
528
+ ### Code Review
529
+ ${formatIssuesForDisplay(codeResult.issues)}
530
+
531
+ ### Security Review
532
+ ${formatIssuesForDisplay(securityResult.issues)}
533
+
534
+ ### Product Owner Review
535
+ ${formatIssuesForDisplay(poResult.issues)}
536
+
537
+ ### Overall Result
538
+ ${passed ? '✅ **PASSED** - All reviews approved' : '❌ **FAILED** - Issues must be addressed'}
539
+
540
+ ---
541
+ *Reviews completed: ${new Date().toISOString().split('T')[0]}*
542
+ `;
543
+ // Append reviews to story
544
+ appendToSection(story, 'Review Notes', reviewNotes);
545
+ changesMade.push('Added code review notes');
546
+ changesMade.push('Added security review notes');
547
+ changesMade.push('Added product owner review notes');
548
+ // Determine decision
549
+ const decision = passed ? ReviewDecision.APPROVED : ReviewDecision.REJECTED;
550
+ // Create review attempt record (omit undefined fields to avoid YAML serialization errors)
551
+ const reviewAttempt = {
552
+ timestamp: new Date().toISOString(),
553
+ decision,
554
+ ...(passed ? {} : { severity }),
555
+ feedback: passed ? 'All reviews passed' : formatIssuesForDisplay(allIssues),
556
+ blockers: allIssues.filter(i => i.severity === 'blocker').map(i => i.description),
557
+ codeReviewPassed: codeResult.passed,
558
+ securityReviewPassed: securityResult.passed,
559
+ poReviewPassed: poResult.passed,
560
+ };
561
+ // Append to review history
562
+ appendReviewHistory(story, reviewAttempt);
563
+ changesMade.push('Recorded review attempt in history');
564
+ if (passed) {
565
+ updateStoryField(story, 'reviews_complete', true);
566
+ changesMade.push('Marked reviews_complete: true');
567
+ }
568
+ else {
569
+ changesMade.push(`Reviews failed with ${allIssues.length} issue(s) - rework required`);
570
+ // Don't mark reviews_complete, this will trigger rework
571
+ }
572
+ return {
573
+ success: true,
574
+ story: parseStory(storyPath),
575
+ changesMade,
576
+ passed,
577
+ decision,
578
+ ...(passed ? {} : { severity }),
579
+ reviewType: 'combined',
580
+ issues: allIssues,
581
+ feedback: passed ? 'All reviews passed' : formatIssuesForDisplay(allIssues),
582
+ };
583
+ }
584
+ catch (error) {
585
+ // Review agent failure - return FAILED decision (doesn't count as retry)
586
+ const errorMsg = error instanceof Error ? error.message : String(error);
587
+ return {
588
+ success: false,
589
+ story,
590
+ changesMade,
591
+ error: errorMsg,
592
+ passed: false,
593
+ decision: ReviewDecision.FAILED,
594
+ reviewType: 'combined',
595
+ issues: [{
596
+ severity: 'blocker',
597
+ category: 'review_error',
598
+ description: `Review process failed: ${errorMsg}`,
599
+ }],
600
+ feedback: `Review process failed: ${errorMsg}`,
601
+ };
602
+ }
603
+ }
604
+ /**
605
+ * Run a sub-review with a specific prompt
606
+ */
607
+ async function runSubReview(story, systemPrompt, reviewType, workingDir, verificationContext = '') {
608
+ try {
609
+ const prompt = `Review this story implementation:
610
+
611
+ Title: ${story.frontmatter.title}
612
+ ${verificationContext ? `\n---\n# Build & Test Verification Results\n${verificationContext}\n---\n` : ''}
613
+ Full story content:
614
+ ${story.content}
615
+
616
+ Provide your ${reviewType} feedback. Be specific and actionable.`;
617
+ return await runAgentQuery({
618
+ prompt,
619
+ systemPrompt,
620
+ workingDirectory: workingDir,
621
+ });
622
+ }
623
+ catch (error) {
624
+ return `${reviewType} failed: ${error instanceof Error ? error.message : String(error)}`;
625
+ }
626
+ }
627
+ /**
628
+ * Create a pull request for the completed story
629
+ */
630
+ export async function createPullRequest(storyPath, sdlcRoot) {
631
+ let story = parseStory(storyPath);
632
+ const changesMade = [];
633
+ const workingDir = path.dirname(sdlcRoot);
634
+ // Security: Validate working directory
635
+ try {
636
+ validateWorkingDirectory(workingDir);
637
+ }
638
+ catch (error) {
639
+ const errorMsg = error instanceof Error ? error.message : String(error);
640
+ return {
641
+ success: false,
642
+ story,
643
+ changesMade,
644
+ error: errorMsg,
645
+ };
646
+ }
647
+ try {
648
+ const branchName = story.frontmatter.branch || `agentic-sdlc/${story.slug}`;
649
+ // Security: Validate branch name to prevent command injection
650
+ if (!validateGitBranchName(branchName)) {
651
+ const errorMsg = `Invalid branch name: ${branchName} (only alphanumeric, hyphens, underscores, and slashes allowed)`;
652
+ changesMade.push(errorMsg);
653
+ return {
654
+ success: false,
655
+ story,
656
+ changesMade,
657
+ error: errorMsg,
658
+ };
659
+ }
660
+ // Check if gh CLI is available
661
+ try {
662
+ execSync('gh --version', { stdio: 'pipe' });
663
+ }
664
+ catch {
665
+ changesMade.push('GitHub CLI not available - PR creation skipped');
666
+ // Still move to done for MVP
667
+ story = moveStory(story, 'done', sdlcRoot);
668
+ changesMade.push('Moved story to done/');
669
+ return {
670
+ success: true,
671
+ story,
672
+ changesMade,
673
+ };
674
+ }
675
+ // Create PR using gh CLI
676
+ try {
677
+ // First, ensure we're on the right branch and have changes committed
678
+ // Security: Branch name is already validated above
679
+ execSync(`git checkout ${branchName}`, { cwd: workingDir, stdio: 'pipe' });
680
+ // Check for uncommitted changes and commit them
681
+ const status = execSync('git status --porcelain', { cwd: workingDir, encoding: 'utf-8' });
682
+ if (status.trim()) {
683
+ execSync('git add -A', { cwd: workingDir, stdio: 'pipe' });
684
+ // Security: Escape shell arguments for commit message
685
+ const commitMsg = `feat: ${story.frontmatter.title}`;
686
+ execSync(`git commit -m ${escapeShellArg(commitMsg)}`, { cwd: workingDir, stdio: 'pipe' });
687
+ changesMade.push('Committed changes');
688
+ }
689
+ // Push branch (already validated)
690
+ execSync(`git push -u origin ${branchName}`, { cwd: workingDir, stdio: 'pipe' });
691
+ changesMade.push(`Pushed branch: ${branchName}`);
692
+ // Create PR using gh CLI with safe arguments
693
+ // Security: Use escaped arguments to prevent shell injection
694
+ const prTitle = story.frontmatter.title;
695
+ const prBody = `## Summary
696
+
697
+ ${story.frontmatter.title}
698
+
699
+ ## Story
700
+
701
+ ${story.content.substring(0, 1000)}...
702
+
703
+ ## Checklist
704
+
705
+ - [x] Implementation complete
706
+ - [x] Code review passed
707
+ - [x] Security review passed
708
+ - [x] Product owner approved
709
+
710
+ ---
711
+ *Created by agentic-sdlc*`;
712
+ const prOutput = execSync(`gh pr create --title ${escapeShellArg(prTitle)} --body ${escapeShellArg(prBody)}`, { cwd: workingDir, encoding: 'utf-8' });
713
+ const prUrl = prOutput.trim();
714
+ updateStoryField(story, 'pr_url', prUrl);
715
+ changesMade.push(`Created PR: ${prUrl}`);
716
+ }
717
+ catch (error) {
718
+ const sanitizedError = sanitizeErrorMessage(error instanceof Error ? error.message : String(error), workingDir);
719
+ changesMade.push(`PR creation failed: ${sanitizedError}`);
720
+ }
721
+ // Move story to done
722
+ story = moveStory(story, 'done', sdlcRoot);
723
+ changesMade.push('Moved story to done/');
724
+ return {
725
+ success: true,
726
+ story,
727
+ changesMade,
728
+ };
729
+ }
730
+ catch (error) {
731
+ const sanitizedError = sanitizeErrorMessage(error instanceof Error ? error.message : String(error), workingDir);
732
+ return {
733
+ success: false,
734
+ story,
735
+ changesMade,
736
+ error: sanitizedError,
737
+ };
738
+ }
739
+ }
740
+ //# sourceMappingURL=review.js.map