ai-sdlc 0.2.0-alpha.9 → 0.2.0-beta.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.
- package/README.md +53 -1058
- package/dist/agents/implementation.d.ts +6 -0
- package/dist/agents/implementation.d.ts.map +1 -1
- package/dist/agents/implementation.js +87 -13
- package/dist/agents/implementation.js.map +1 -1
- package/dist/agents/planning.d.ts.map +1 -1
- package/dist/agents/planning.js +22 -3
- package/dist/agents/planning.js.map +1 -1
- package/dist/agents/refinement.d.ts.map +1 -1
- package/dist/agents/refinement.js +22 -3
- package/dist/agents/refinement.js.map +1 -1
- package/dist/agents/research.d.ts +85 -1
- package/dist/agents/research.d.ts.map +1 -1
- package/dist/agents/research.js +506 -16
- package/dist/agents/research.js.map +1 -1
- package/dist/agents/review.d.ts +67 -2
- package/dist/agents/review.d.ts.map +1 -1
- package/dist/agents/review.js +477 -68
- package/dist/agents/review.js.map +1 -1
- package/dist/agents/rework.d.ts.map +1 -1
- package/dist/agents/rework.js +22 -3
- package/dist/agents/rework.js.map +1 -1
- package/dist/agents/state-assessor.d.ts +3 -3
- package/dist/agents/state-assessor.d.ts.map +1 -1
- package/dist/agents/state-assessor.js +6 -6
- package/dist/agents/state-assessor.js.map +1 -1
- package/dist/agents/test-pattern-detector.d.ts +49 -0
- package/dist/agents/test-pattern-detector.d.ts.map +1 -0
- package/dist/agents/test-pattern-detector.js +273 -0
- package/dist/agents/test-pattern-detector.js.map +1 -0
- package/dist/agents/verification.d.ts +11 -0
- package/dist/agents/verification.d.ts.map +1 -1
- package/dist/agents/verification.js +74 -1
- package/dist/agents/verification.js.map +1 -1
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands.d.ts +43 -3
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +588 -150
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +20 -3
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/runner.d.ts.map +1 -1
- package/dist/cli/runner.js +18 -6
- package/dist/cli/runner.js.map +1 -1
- package/dist/core/auth.d.ts +43 -0
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +105 -1
- package/dist/core/auth.js.map +1 -1
- package/dist/core/client.d.ts +6 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +57 -3
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.d.ts +5 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +27 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/conflict-detector.d.ts +108 -0
- package/dist/core/conflict-detector.d.ts.map +1 -0
- package/dist/core/conflict-detector.js +413 -0
- package/dist/core/conflict-detector.js.map +1 -0
- package/dist/core/git-utils.d.ts +10 -1
- package/dist/core/git-utils.d.ts.map +1 -1
- package/dist/core/git-utils.js +55 -4
- package/dist/core/git-utils.js.map +1 -1
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +17 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/kanban.d.ts +1 -1
- package/dist/core/kanban.d.ts.map +1 -1
- package/dist/core/kanban.js +3 -3
- package/dist/core/kanban.js.map +1 -1
- package/dist/core/logger.d.ts +92 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +221 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/story-logger.d.ts +102 -0
- package/dist/core/story-logger.d.ts.map +1 -0
- package/dist/core/story-logger.js +265 -0
- package/dist/core/story-logger.js.map +1 -0
- package/dist/core/story.d.ts +79 -20
- package/dist/core/story.d.ts.map +1 -1
- package/dist/core/story.js +221 -39
- package/dist/core/story.js.map +1 -1
- package/dist/core/workflow-state.d.ts +45 -6
- package/dist/core/workflow-state.d.ts.map +1 -1
- package/dist/core/workflow-state.js +201 -12
- package/dist/core/workflow-state.js.map +1 -1
- package/dist/core/worktree.d.ts +9 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +52 -1
- package/dist/core/worktree.js.map +1 -1
- package/dist/index.js +112 -6
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +123 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -1
package/dist/agents/review.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { execSync, spawn } from 'child_process';
|
|
1
|
+
import { execSync, spawn, spawnSync } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { parseStory, updateStoryStatus, appendToSection, updateStoryField, isAtMaxRetries, appendReviewHistory, snapshotMaxRetries, getEffectiveMaxRetries } from '../core/story.js';
|
|
5
|
+
import { parseStory, updateStoryStatus, appendToSection, updateStoryField, isAtMaxRetries, appendReviewHistory, snapshotMaxRetries, getEffectiveMaxRetries, getEffectiveMaxImplementationRetries } from '../core/story.js';
|
|
6
6
|
import { runAgentQuery } from '../core/client.js';
|
|
7
|
+
import { getLogger } from '../core/logger.js';
|
|
7
8
|
import { loadConfig, DEFAULT_TIMEOUTS } from '../core/config.js';
|
|
8
9
|
import { ReviewDecision, ReviewSeverity } from '../types/index.js';
|
|
9
10
|
import { sanitizeInput, truncateText } from '../cli/formatting.js';
|
|
11
|
+
import { detectTestDuplicationPatterns } from './test-pattern-detector.js';
|
|
10
12
|
/**
|
|
11
13
|
* Security: Validate Git branch name to prevent command injection
|
|
12
14
|
* Only allows alphanumeric characters, hyphens, underscores, and forward slashes
|
|
@@ -93,7 +95,9 @@ const ReviewIssueSchema = z.object({
|
|
|
93
95
|
// This handles LLM responses that return {"line": null} instead of omitting the field
|
|
94
96
|
file: z.string().nullish().transform(v => v ?? undefined),
|
|
95
97
|
line: z.number().int().positive().nullish().transform(v => v ?? undefined),
|
|
96
|
-
suggestedFix: z.string().max(
|
|
98
|
+
suggestedFix: z.string().max(5000).nullish().transform(v => v ?? undefined),
|
|
99
|
+
// Perspectives field for unified review (optional for backward compatibility)
|
|
100
|
+
perspectives: z.array(z.enum(['code', 'security', 'po'])).optional(),
|
|
97
101
|
});
|
|
98
102
|
const ReviewResponseSchema = z.object({
|
|
99
103
|
passed: z.boolean(),
|
|
@@ -252,7 +256,8 @@ Output your review as a JSON object with this structure:
|
|
|
252
256
|
"description": "Detailed description of the issue",
|
|
253
257
|
"file": "path/to/file.ts" (if applicable),
|
|
254
258
|
"line": 42 (if applicable),
|
|
255
|
-
"suggestedFix": "How to fix this issue"
|
|
259
|
+
"suggestedFix": "How to fix this issue",
|
|
260
|
+
"perspectives": ["code", "security", "po"] (which perspectives this issue relates to)
|
|
256
261
|
}
|
|
257
262
|
]
|
|
258
263
|
}
|
|
@@ -265,6 +270,70 @@ Severity guidelines:
|
|
|
265
270
|
|
|
266
271
|
If no issues found, return: {"passed": true, "issues": []}
|
|
267
272
|
`;
|
|
273
|
+
/**
|
|
274
|
+
* Unified Review Prompt - combines code, security, and product owner perspectives
|
|
275
|
+
* into a single collaborative review to eliminate duplicate issues.
|
|
276
|
+
*/
|
|
277
|
+
const UNIFIED_REVIEW_PROMPT = `You are a senior engineering team conducting a comprehensive collaborative review.
|
|
278
|
+
|
|
279
|
+
You must evaluate the implementation from THREE perspectives simultaneously, but produce ONE unified set of issues:
|
|
280
|
+
|
|
281
|
+
## Perspective 1: Code Quality (Senior Developer)
|
|
282
|
+
Evaluate:
|
|
283
|
+
- Code quality and maintainability
|
|
284
|
+
- Following best practices and design patterns
|
|
285
|
+
- Potential bugs or logic errors
|
|
286
|
+
- Test coverage adequacy and test quality
|
|
287
|
+
- Error handling completeness
|
|
288
|
+
- Performance considerations
|
|
289
|
+
|
|
290
|
+
## Perspective 2: Security (Security Engineer)
|
|
291
|
+
Evaluate:
|
|
292
|
+
- OWASP Top 10 vulnerabilities
|
|
293
|
+
- Input validation and sanitization
|
|
294
|
+
- Authentication and authorization issues
|
|
295
|
+
- Data exposure risks
|
|
296
|
+
- Command injection vulnerabilities
|
|
297
|
+
- Secure coding practices
|
|
298
|
+
|
|
299
|
+
## Perspective 3: Requirements (Product Owner)
|
|
300
|
+
Evaluate:
|
|
301
|
+
- Does it meet the acceptance criteria stated in the story?
|
|
302
|
+
- Is the user experience appropriate and intuitive?
|
|
303
|
+
- Are edge cases and error scenarios handled?
|
|
304
|
+
- Is documentation adequate for users and maintainers?
|
|
305
|
+
- Does the implementation align with the story goals?
|
|
306
|
+
|
|
307
|
+
## CRITICAL DEDUPLICATION INSTRUCTIONS:
|
|
308
|
+
|
|
309
|
+
1. **DO NOT repeat the same underlying issue from different perspectives**
|
|
310
|
+
- If multiple perspectives notice the same problem, list it ONCE
|
|
311
|
+
- Use the \`perspectives\` array to indicate which perspectives it affects
|
|
312
|
+
|
|
313
|
+
2. **Prioritize by actual impact, not by how many perspectives notice it**
|
|
314
|
+
- A issue seen by all 3 perspectives is still just ONE issue
|
|
315
|
+
- Focus on the distinct, actionable problems that need fixing
|
|
316
|
+
|
|
317
|
+
3. **If the fundamental problem is "no implementation exists" or "functionality completely missing":**
|
|
318
|
+
- Report this as ONE blocker issue, not three separate issues
|
|
319
|
+
- Use perspectives: ["code", "security", "po"] to show all perspectives agree
|
|
320
|
+
|
|
321
|
+
4. **Combine related issues into single, comprehensive descriptions:**
|
|
322
|
+
- Instead of: "No tests" (code) + "Untested security" (security) + "No validation tests" (po)
|
|
323
|
+
- Write: "No tests exist for the implementation" with perspectives: ["code", "security", "po"]
|
|
324
|
+
|
|
325
|
+
5. **Each issue should have a clear, single suggested fix**
|
|
326
|
+
- Avoid vague suggestions like "improve everything"
|
|
327
|
+
- Be specific and actionable
|
|
328
|
+
|
|
329
|
+
${REVIEW_OUTPUT_FORMAT}
|
|
330
|
+
|
|
331
|
+
Remember: Your goal is to produce a clean, deduplicated list of actual distinct problems, not to maximize issue count.`;
|
|
332
|
+
/**
|
|
333
|
+
* Legacy prompts - kept for reference only
|
|
334
|
+
* @deprecated These are replaced by UNIFIED_REVIEW_PROMPT which combines all three perspectives.
|
|
335
|
+
* The unified prompt reduces LLM calls from 3 to 1 and eliminates duplicate issues.
|
|
336
|
+
*/
|
|
268
337
|
const CODE_REVIEW_PROMPT = `You are a senior code reviewer. Review the implementation for:
|
|
269
338
|
1. Code quality and maintainability
|
|
270
339
|
2. Following best practices
|
|
@@ -272,6 +341,9 @@ const CODE_REVIEW_PROMPT = `You are a senior code reviewer. Review the implement
|
|
|
272
341
|
4. Test coverage adequacy
|
|
273
342
|
|
|
274
343
|
${REVIEW_OUTPUT_FORMAT}`;
|
|
344
|
+
/**
|
|
345
|
+
* @deprecated Use UNIFIED_REVIEW_PROMPT instead
|
|
346
|
+
*/
|
|
275
347
|
const SECURITY_REVIEW_PROMPT = `You are a security specialist. Review the implementation for:
|
|
276
348
|
1. OWASP Top 10 vulnerabilities
|
|
277
349
|
2. Input validation issues
|
|
@@ -279,6 +351,9 @@ const SECURITY_REVIEW_PROMPT = `You are a security specialist. Review the implem
|
|
|
279
351
|
4. Data exposure risks
|
|
280
352
|
|
|
281
353
|
${REVIEW_OUTPUT_FORMAT}`;
|
|
354
|
+
/**
|
|
355
|
+
* @deprecated Use UNIFIED_REVIEW_PROMPT instead
|
|
356
|
+
*/
|
|
282
357
|
const PO_REVIEW_PROMPT = `You are a product owner validating the implementation. Check:
|
|
283
358
|
1. Does it meet the acceptance criteria?
|
|
284
359
|
2. Is the user experience appropriate?
|
|
@@ -316,6 +391,7 @@ function parseReviewResponse(response, reviewType) {
|
|
|
316
391
|
file: issue.file,
|
|
317
392
|
line: issue.line,
|
|
318
393
|
suggestedFix: issue.suggestedFix,
|
|
394
|
+
perspectives: issue.perspectives,
|
|
319
395
|
}));
|
|
320
396
|
return {
|
|
321
397
|
passed: validated.passed !== false && issues.filter(i => i.severity === 'blocker' || i.severity === 'critical').length === 0,
|
|
@@ -383,8 +459,35 @@ function determineReviewSeverity(issues) {
|
|
|
383
459
|
return ReviewSeverity.LOW;
|
|
384
460
|
}
|
|
385
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Derive individual perspective pass/fail status from issues
|
|
464
|
+
*
|
|
465
|
+
* For backward compatibility with ReviewAttempt structure, determines whether
|
|
466
|
+
* each perspective (code, security, po) would pass based on issues flagged
|
|
467
|
+
* for that perspective.
|
|
468
|
+
*
|
|
469
|
+
* A perspective fails if it has any blocker or critical issues.
|
|
470
|
+
*
|
|
471
|
+
* @param issues - Array of review issues with perspectives field
|
|
472
|
+
* @returns Object with pass/fail status for each perspective
|
|
473
|
+
*/
|
|
474
|
+
export function deriveIndividualPassFailFromPerspectives(issues) {
|
|
475
|
+
// Check if any blocker/critical issues exist for each perspective
|
|
476
|
+
const codeIssues = issues.filter(i => i.perspectives?.includes('code') &&
|
|
477
|
+
(i.severity === 'blocker' || i.severity === 'critical'));
|
|
478
|
+
const securityIssues = issues.filter(i => i.perspectives?.includes('security') &&
|
|
479
|
+
(i.severity === 'blocker' || i.severity === 'critical'));
|
|
480
|
+
const poIssues = issues.filter(i => i.perspectives?.includes('po') &&
|
|
481
|
+
(i.severity === 'blocker' || i.severity === 'critical'));
|
|
482
|
+
return {
|
|
483
|
+
codeReviewPassed: codeIssues.length === 0,
|
|
484
|
+
securityReviewPassed: securityIssues.length === 0,
|
|
485
|
+
poReviewPassed: poIssues.length === 0,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
386
488
|
/**
|
|
387
489
|
* Aggregate issues from multiple reviews and determine overall pass/fail
|
|
490
|
+
* @deprecated No longer used with unified review. Kept for reference only.
|
|
388
491
|
*/
|
|
389
492
|
function aggregateReviews(codeResult, securityResult, poResult) {
|
|
390
493
|
const allIssues = [...codeResult.issues, ...securityResult.issues, ...poResult.issues];
|
|
@@ -399,6 +502,7 @@ function aggregateReviews(codeResult, securityResult, poResult) {
|
|
|
399
502
|
}
|
|
400
503
|
/**
|
|
401
504
|
* Format issues for display in review notes
|
|
505
|
+
* Shows perspectives (code, security, po) when available
|
|
402
506
|
*/
|
|
403
507
|
function formatIssuesForDisplay(issues) {
|
|
404
508
|
if (issues.length === 0) {
|
|
@@ -417,7 +521,11 @@ function formatIssuesForDisplay(issues) {
|
|
|
417
521
|
const icon = severity === 'blocker' ? '🛑' : severity === 'critical' ? '⚠️' : severity === 'major' ? '📋' : 'ℹ️';
|
|
418
522
|
output += `\n#### ${icon} ${severity.toUpperCase()} (${issueList.length})\n\n`;
|
|
419
523
|
for (const issue of issueList) {
|
|
420
|
-
|
|
524
|
+
// Format perspectives indicator if present
|
|
525
|
+
const perspectivesTag = issue.perspectives && issue.perspectives.length > 0
|
|
526
|
+
? ` [${issue.perspectives.join(', ')}]`
|
|
527
|
+
: '';
|
|
528
|
+
output += `**${issue.category}**${perspectivesTag}: ${issue.description}\n`;
|
|
421
529
|
if (issue.file) {
|
|
422
530
|
output += ` - File: \`${issue.file}\`${issue.line ? `:${issue.line}` : ''}\n`;
|
|
423
531
|
}
|
|
@@ -429,6 +537,41 @@ function formatIssuesForDisplay(issues) {
|
|
|
429
537
|
}
|
|
430
538
|
return output;
|
|
431
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* Get source code changes from git diff
|
|
542
|
+
*
|
|
543
|
+
* Returns list of source files that have been modified (excludes tests and story files).
|
|
544
|
+
* Uses spawnSync for security (prevents command injection).
|
|
545
|
+
*
|
|
546
|
+
* @param workingDir - Working directory to run git diff in
|
|
547
|
+
* @returns Array of source file paths that have changed, or ['unknown'] if git fails
|
|
548
|
+
*/
|
|
549
|
+
export function getSourceCodeChanges(workingDir) {
|
|
550
|
+
try {
|
|
551
|
+
// Security: Use spawnSync with explicit args (not shell) to prevent injection
|
|
552
|
+
const result = spawnSync('git', ['diff', '--name-only', 'HEAD~1'], {
|
|
553
|
+
cwd: workingDir,
|
|
554
|
+
encoding: 'utf-8',
|
|
555
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
556
|
+
});
|
|
557
|
+
if (result.status !== 0) {
|
|
558
|
+
// Git command failed - fail open (assume changes exist)
|
|
559
|
+
return ['unknown'];
|
|
560
|
+
}
|
|
561
|
+
const output = result.stdout.toString();
|
|
562
|
+
return output
|
|
563
|
+
.split('\n')
|
|
564
|
+
.filter(f => f.trim())
|
|
565
|
+
.filter(f => /\.(ts|tsx|js|jsx)$/.test(f)) // Source files only
|
|
566
|
+
.filter(f => !f.includes('.test.')) // Exclude test files
|
|
567
|
+
.filter(f => !f.includes('.spec.')) // Exclude spec files
|
|
568
|
+
.filter(f => !f.startsWith('.ai-sdlc/')); // Exclude story files
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
// If git diff fails, assume there are changes (fail open, not closed)
|
|
572
|
+
return ['unknown'];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
432
575
|
/**
|
|
433
576
|
* Generate executive summary from review issues (1-3 sentences)
|
|
434
577
|
*
|
|
@@ -526,9 +669,15 @@ export function generateReviewSummary(issues, terminalWidth) {
|
|
|
526
669
|
* Now returns structured ReviewResult with pass/fail and issues.
|
|
527
670
|
*/
|
|
528
671
|
export async function runReviewAgent(storyPath, sdlcRoot, options) {
|
|
672
|
+
const logger = getLogger();
|
|
673
|
+
const startTime = Date.now();
|
|
529
674
|
const story = parseStory(storyPath);
|
|
530
675
|
const changesMade = [];
|
|
531
676
|
const workingDir = path.dirname(sdlcRoot);
|
|
677
|
+
logger.info('review', 'Starting review phase', {
|
|
678
|
+
storyId: story.frontmatter.id,
|
|
679
|
+
retryCount: story.frontmatter.retry_count || 0,
|
|
680
|
+
});
|
|
532
681
|
// Security: Validate working directory before any operations
|
|
533
682
|
try {
|
|
534
683
|
validateWorkingDirectory(workingDir);
|
|
@@ -554,14 +703,14 @@ export async function runReviewAgent(storyPath, sdlcRoot, options) {
|
|
|
554
703
|
const config = loadConfig(workingDir);
|
|
555
704
|
try {
|
|
556
705
|
// Snapshot max_retries from config (protects against mid-cycle config changes)
|
|
557
|
-
snapshotMaxRetries(story, config);
|
|
706
|
+
await snapshotMaxRetries(story, config);
|
|
558
707
|
// Check if story has reached max retries
|
|
559
708
|
if (isAtMaxRetries(story, config)) {
|
|
560
709
|
const retryCount = story.frontmatter.retry_count || 0;
|
|
561
710
|
const maxRetries = getEffectiveMaxRetries(story, config);
|
|
562
711
|
const maxRetriesDisplay = Number.isFinite(maxRetries) ? maxRetries : '∞';
|
|
563
712
|
const errorMsg = `Story has reached maximum retry limit (${retryCount}/${maxRetriesDisplay}). Manual intervention required.`;
|
|
564
|
-
updateStoryField(story, 'last_error', errorMsg);
|
|
713
|
+
await updateStoryField(story, 'last_error', errorMsg);
|
|
565
714
|
changesMade.push(errorMsg);
|
|
566
715
|
return {
|
|
567
716
|
success: false,
|
|
@@ -579,6 +728,67 @@ export async function runReviewAgent(storyPath, sdlcRoot, options) {
|
|
|
579
728
|
feedback: errorMsg,
|
|
580
729
|
};
|
|
581
730
|
}
|
|
731
|
+
// PRE-CHECK GATE: Detect documentation-only implementations before running expensive LLM reviews
|
|
732
|
+
const sourceChanges = getSourceCodeChanges(workingDir);
|
|
733
|
+
if (sourceChanges.length === 0) {
|
|
734
|
+
// No source code changes detected - check if we can recover
|
|
735
|
+
const retryCount = story.frontmatter.implementation_retry_count || 0;
|
|
736
|
+
const maxRetries = getEffectiveMaxImplementationRetries(story, config);
|
|
737
|
+
if (retryCount < maxRetries) {
|
|
738
|
+
// RECOVERABLE: Trigger implementation recovery
|
|
739
|
+
logger.warn('review', 'No source code changes detected - triggering implementation recovery', {
|
|
740
|
+
storyId: story.frontmatter.id,
|
|
741
|
+
retryCount,
|
|
742
|
+
maxRetries,
|
|
743
|
+
});
|
|
744
|
+
await updateStoryField(story, 'implementation_complete', false);
|
|
745
|
+
await updateStoryField(story, 'last_restart_reason', 'No source code changes detected. Implementation wrote documentation only.');
|
|
746
|
+
return {
|
|
747
|
+
success: true,
|
|
748
|
+
story: parseStory(storyPath),
|
|
749
|
+
changesMade: ['Detected documentation-only implementation', 'Triggered implementation recovery'],
|
|
750
|
+
passed: false,
|
|
751
|
+
decision: ReviewDecision.RECOVERY,
|
|
752
|
+
reviewType: 'pre-check',
|
|
753
|
+
issues: [{
|
|
754
|
+
severity: 'critical',
|
|
755
|
+
category: 'implementation',
|
|
756
|
+
description: 'No source code modifications detected. Re-running implementation phase.',
|
|
757
|
+
}],
|
|
758
|
+
feedback: 'Implementation recovery triggered - no source changes found.',
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
// NON-RECOVERABLE: Max retries reached
|
|
763
|
+
const maxRetriesDisplay = Number.isFinite(maxRetries) ? maxRetries : '∞';
|
|
764
|
+
logger.error('review', 'No source code changes detected and max implementation retries reached', {
|
|
765
|
+
storyId: story.frontmatter.id,
|
|
766
|
+
retryCount,
|
|
767
|
+
maxRetries,
|
|
768
|
+
});
|
|
769
|
+
return {
|
|
770
|
+
success: true,
|
|
771
|
+
story: parseStory(storyPath),
|
|
772
|
+
changesMade: ['Detected documentation-only implementation', 'Max retries reached'],
|
|
773
|
+
passed: false,
|
|
774
|
+
decision: ReviewDecision.FAILED,
|
|
775
|
+
severity: ReviewSeverity.CRITICAL,
|
|
776
|
+
reviewType: 'pre-check',
|
|
777
|
+
issues: [{
|
|
778
|
+
severity: 'blocker',
|
|
779
|
+
category: 'implementation',
|
|
780
|
+
description: `Implementation phase wrote documentation/planning only - no source code was modified. This has occurred ${retryCount} time(s) (max: ${maxRetriesDisplay}). Manual intervention required.`,
|
|
781
|
+
suggestedFix: 'Review the story requirements and implementation plan. The agent may be confused about what needs to be built. Consider simplifying the story or providing more explicit guidance.',
|
|
782
|
+
}],
|
|
783
|
+
feedback: 'Implementation failed to produce code changes after multiple attempts.',
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// Source changes exist - proceed with normal review flow
|
|
788
|
+
logger.info('review', 'Source code changes detected - proceeding with verification', {
|
|
789
|
+
storyId: story.frontmatter.id,
|
|
790
|
+
fileCount: sourceChanges.length,
|
|
791
|
+
});
|
|
582
792
|
// Run build and tests BEFORE reviews (async with progress)
|
|
583
793
|
changesMade.push('Running build and test verification...');
|
|
584
794
|
const verification = await runVerificationAsync(workingDir, config, options?.onVerificationProgress);
|
|
@@ -646,60 +856,82 @@ export async function runReviewAgent(storyPath, sdlcRoot, options) {
|
|
|
646
856
|
feedback: formatIssuesForDisplay(verificationIssues),
|
|
647
857
|
};
|
|
648
858
|
}
|
|
649
|
-
// Verification passed - proceed with
|
|
650
|
-
changesMade.push('Verification passed - proceeding with
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
859
|
+
// Verification passed - proceed with unified collaborative review
|
|
860
|
+
changesMade.push('Verification passed - proceeding with unified collaborative review');
|
|
861
|
+
// Run test pattern detection if enabled
|
|
862
|
+
let testPatternIssues = [];
|
|
863
|
+
if (config.reviewConfig.detectTestAntipatterns !== false) {
|
|
864
|
+
try {
|
|
865
|
+
changesMade.push('Running test anti-pattern detection...');
|
|
866
|
+
testPatternIssues = await detectTestDuplicationPatterns(workingDir);
|
|
867
|
+
if (testPatternIssues.length > 0) {
|
|
868
|
+
changesMade.push(`Detected ${testPatternIssues.length} test anti-pattern(s)`);
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
changesMade.push('No test anti-patterns detected');
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
// Don't fail review if detection errors - just log and continue
|
|
876
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
877
|
+
changesMade.push(`Test pattern detection error: ${errorMsg}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const unifiedReviewResponse = await runSubReview(story, UNIFIED_REVIEW_PROMPT, 'Unified Collaborative Review', workingDir, verificationContext);
|
|
881
|
+
// Parse unified review response into structured issues
|
|
882
|
+
const unifiedResult = parseReviewResponse(unifiedReviewResponse, 'Unified Review');
|
|
660
883
|
// TDD Validation: Check TDD cycle completeness if TDD was enabled for this story
|
|
661
884
|
const tddEnabled = story.frontmatter.tdd_enabled ?? config.tdd?.enabled ?? false;
|
|
662
885
|
if (tddEnabled && story.frontmatter.tdd_test_history?.length) {
|
|
663
886
|
const tddViolations = validateTDDCycles(story.frontmatter.tdd_test_history);
|
|
664
887
|
if (tddViolations.length > 0) {
|
|
665
888
|
const tddIssues = generateTDDIssues(tddViolations);
|
|
666
|
-
|
|
667
|
-
|
|
889
|
+
unifiedResult.issues.push(...tddIssues);
|
|
890
|
+
unifiedResult.passed = false;
|
|
668
891
|
changesMade.push(`TDD validation: ${tddViolations.length} violation(s) detected`);
|
|
669
892
|
}
|
|
670
893
|
else {
|
|
671
894
|
changesMade.push('TDD validation: All cycles completed correctly');
|
|
672
895
|
}
|
|
673
896
|
}
|
|
674
|
-
// Add
|
|
675
|
-
|
|
897
|
+
// Add test pattern issues to unified result (they're code-quality related)
|
|
898
|
+
if (testPatternIssues.length > 0) {
|
|
899
|
+
unifiedResult.issues.push(...testPatternIssues);
|
|
900
|
+
unifiedResult.passed = false;
|
|
901
|
+
}
|
|
902
|
+
// Add verification issues to unified result (they're code-quality related)
|
|
903
|
+
unifiedResult.issues.unshift(...verificationIssues);
|
|
676
904
|
if (verificationIssues.length > 0) {
|
|
677
|
-
|
|
905
|
+
unifiedResult.passed = false;
|
|
678
906
|
}
|
|
679
|
-
//
|
|
680
|
-
const
|
|
681
|
-
|
|
907
|
+
// Determine overall pass/fail from unified review
|
|
908
|
+
const allIssues = unifiedResult.issues;
|
|
909
|
+
const blockerCount = allIssues.filter(i => i.severity === 'blocker').length;
|
|
910
|
+
const criticalCount = allIssues.filter(i => i.severity === 'critical').length;
|
|
911
|
+
const passed = blockerCount === 0 && criticalCount < 2;
|
|
912
|
+
const severity = determineReviewSeverity(allIssues);
|
|
913
|
+
// Derive individual perspective pass/fail for backward compatibility
|
|
914
|
+
const { codeReviewPassed, securityReviewPassed, poReviewPassed } = deriveIndividualPassFailFromPerspectives(allIssues);
|
|
915
|
+
// Compile review notes with structured format for unified review
|
|
682
916
|
const reviewNotes = `
|
|
683
|
-
###
|
|
684
|
-
${formatIssuesForDisplay(codeResult.issues)}
|
|
917
|
+
### Unified Collaborative Review
|
|
685
918
|
|
|
686
|
-
|
|
687
|
-
${formatIssuesForDisplay(securityResult.issues)}
|
|
919
|
+
${formatIssuesForDisplay(allIssues)}
|
|
688
920
|
|
|
689
|
-
###
|
|
690
|
-
${
|
|
921
|
+
### Perspective Summary
|
|
922
|
+
- Code Quality: ${codeReviewPassed ? '✅ Passed' : '❌ Failed'}
|
|
923
|
+
- Security: ${securityReviewPassed ? '✅ Passed' : '❌ Failed'}
|
|
924
|
+
- Requirements (PO): ${poReviewPassed ? '✅ Passed' : '❌ Failed'}
|
|
691
925
|
|
|
692
926
|
### Overall Result
|
|
693
927
|
${passed ? '✅ **PASSED** - All reviews approved' : '❌ **FAILED** - Issues must be addressed'}
|
|
694
928
|
|
|
695
929
|
---
|
|
696
|
-
*
|
|
930
|
+
*Review completed: ${new Date().toISOString().split('T')[0]}*
|
|
697
931
|
`;
|
|
698
932
|
// Append reviews to story
|
|
699
|
-
appendToSection(story, 'Review Notes', reviewNotes);
|
|
700
|
-
changesMade.push('Added
|
|
701
|
-
changesMade.push('Added security review notes');
|
|
702
|
-
changesMade.push('Added product owner review notes');
|
|
933
|
+
await appendToSection(story, 'Review Notes', reviewNotes);
|
|
934
|
+
changesMade.push('Added unified collaborative review notes');
|
|
703
935
|
// Determine decision
|
|
704
936
|
const decision = passed ? ReviewDecision.APPROVED : ReviewDecision.REJECTED;
|
|
705
937
|
// Create review attempt record (omit undefined fields to avoid YAML serialization errors)
|
|
@@ -709,21 +941,28 @@ ${passed ? '✅ **PASSED** - All reviews approved' : '❌ **FAILED** - Issues mu
|
|
|
709
941
|
...(passed ? {} : { severity }),
|
|
710
942
|
feedback: passed ? 'All reviews passed' : formatIssuesForDisplay(allIssues),
|
|
711
943
|
blockers: allIssues.filter(i => i.severity === 'blocker').map(i => i.description),
|
|
712
|
-
codeReviewPassed
|
|
713
|
-
securityReviewPassed
|
|
714
|
-
poReviewPassed
|
|
944
|
+
codeReviewPassed,
|
|
945
|
+
securityReviewPassed,
|
|
946
|
+
poReviewPassed,
|
|
715
947
|
};
|
|
716
948
|
// Append to review history
|
|
717
|
-
appendReviewHistory(story, reviewAttempt);
|
|
949
|
+
await appendReviewHistory(story, reviewAttempt);
|
|
718
950
|
changesMade.push('Recorded review attempt in history');
|
|
719
951
|
if (passed) {
|
|
720
|
-
updateStoryField(story, 'reviews_complete', true);
|
|
952
|
+
await updateStoryField(story, 'reviews_complete', true);
|
|
721
953
|
changesMade.push('Marked reviews_complete: true');
|
|
722
954
|
}
|
|
723
955
|
else {
|
|
724
956
|
changesMade.push(`Reviews failed with ${allIssues.length} issue(s) - rework required`);
|
|
725
957
|
// Don't mark reviews_complete, this will trigger rework
|
|
726
958
|
}
|
|
959
|
+
logger.info('review', 'Review phase complete', {
|
|
960
|
+
storyId: story.frontmatter.id,
|
|
961
|
+
durationMs: Date.now() - startTime,
|
|
962
|
+
passed,
|
|
963
|
+
decision,
|
|
964
|
+
issueCount: allIssues.length,
|
|
965
|
+
});
|
|
727
966
|
return {
|
|
728
967
|
success: true,
|
|
729
968
|
story: parseStory(storyPath),
|
|
@@ -739,6 +978,11 @@ ${passed ? '✅ **PASSED** - All reviews approved' : '❌ **FAILED** - Issues mu
|
|
|
739
978
|
catch (error) {
|
|
740
979
|
// Review agent failure - return FAILED decision (doesn't count as retry)
|
|
741
980
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
981
|
+
logger.error('review', 'Review phase failed', {
|
|
982
|
+
storyId: story.frontmatter.id,
|
|
983
|
+
durationMs: Date.now() - startTime,
|
|
984
|
+
error: errorMsg,
|
|
985
|
+
});
|
|
742
986
|
return {
|
|
743
987
|
success: false,
|
|
744
988
|
story,
|
|
@@ -756,6 +1000,139 @@ ${passed ? '✅ **PASSED** - All reviews approved' : '❌ **FAILED** - Issues mu
|
|
|
756
1000
|
};
|
|
757
1001
|
}
|
|
758
1002
|
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Parse story content into sections by level-2 headers (##)
|
|
1005
|
+
* Returns array of {title, content} objects
|
|
1006
|
+
*/
|
|
1007
|
+
export function parseContentSections(content) {
|
|
1008
|
+
const sections = [];
|
|
1009
|
+
const lines = content.split('\n');
|
|
1010
|
+
let currentSection = null;
|
|
1011
|
+
for (const line of lines) {
|
|
1012
|
+
const headerMatch = line.match(/^##\s+(.+)$/);
|
|
1013
|
+
if (headerMatch) {
|
|
1014
|
+
if (currentSection)
|
|
1015
|
+
sections.push(currentSection);
|
|
1016
|
+
currentSection = { title: headerMatch[1], content: '' };
|
|
1017
|
+
}
|
|
1018
|
+
else if (currentSection) {
|
|
1019
|
+
currentSection.content += line + '\n';
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (currentSection)
|
|
1023
|
+
sections.push(currentSection);
|
|
1024
|
+
return sections;
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Remove unfinished checkboxes from content (per CLAUDE.md requirement)
|
|
1028
|
+
* Removes lines with `- [ ]` or `* [ ]` patterns
|
|
1029
|
+
* Preserves completed checkboxes `- [x]` and `- [X]`
|
|
1030
|
+
*/
|
|
1031
|
+
export function removeUnfinishedCheckboxes(content) {
|
|
1032
|
+
const lines = content.split('\n');
|
|
1033
|
+
const filteredLines = [];
|
|
1034
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1035
|
+
const line = lines[i];
|
|
1036
|
+
// Match unchecked boxes: - [ ] or * [ ] with optional leading whitespace
|
|
1037
|
+
const isUnchecked = /^\s*[-*] \[ \]/.test(line);
|
|
1038
|
+
if (!isUnchecked) {
|
|
1039
|
+
filteredLines.push(line);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return filteredLines.join('\n');
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Generate GitHub blob URL for story file
|
|
1046
|
+
* Parses remote URL and constructs link to story in repository
|
|
1047
|
+
*/
|
|
1048
|
+
export function getStoryFileURL(storyPath, branch, workingDir) {
|
|
1049
|
+
try {
|
|
1050
|
+
const remoteUrl = execSync('git remote get-url origin', { cwd: workingDir, encoding: 'utf-8' }).trim();
|
|
1051
|
+
// Parse owner/repo from URL
|
|
1052
|
+
// HTTPS: https://github.com/owner/repo.git
|
|
1053
|
+
// SSH: git@github.com:owner/repo.git
|
|
1054
|
+
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/(.+?)(\.git)?$/);
|
|
1055
|
+
if (!match)
|
|
1056
|
+
return '';
|
|
1057
|
+
const [, owner, repo] = match;
|
|
1058
|
+
const relativePath = path.relative(workingDir, storyPath);
|
|
1059
|
+
return `https://github.com/${owner}/${repo}/blob/${branch}/${relativePath}`;
|
|
1060
|
+
}
|
|
1061
|
+
catch {
|
|
1062
|
+
return '';
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Format PR description from story sections
|
|
1067
|
+
* Includes: Story ID, User Story, Summary, Acceptance Criteria, Implementation Summary
|
|
1068
|
+
* Removes unfinished checkboxes from all sections
|
|
1069
|
+
*/
|
|
1070
|
+
export function formatPRDescription(story, storyFileUrl) {
|
|
1071
|
+
const sections = parseContentSections(story.content);
|
|
1072
|
+
// Extract key sections
|
|
1073
|
+
const userStory = sections.find(s => s.title === 'User Story')?.content || '';
|
|
1074
|
+
const summary = sections.find(s => s.title === 'Summary')?.content || '';
|
|
1075
|
+
const acceptanceCriteria = sections.find(s => s.title === 'Acceptance Criteria')?.content || '';
|
|
1076
|
+
const implementationSummary = sections.find(s => s.title === 'Implementation Summary')?.content || '';
|
|
1077
|
+
// Remove unfinished checkboxes from all sections
|
|
1078
|
+
const cleanAcceptanceCriteria = removeUnfinishedCheckboxes(acceptanceCriteria);
|
|
1079
|
+
const cleanImplementationSummary = removeUnfinishedCheckboxes(implementationSummary);
|
|
1080
|
+
// Build PR body
|
|
1081
|
+
let prBody = `## Story ID\n\n${story.frontmatter.id}\n\n`;
|
|
1082
|
+
if (userStory.trim()) {
|
|
1083
|
+
prBody += `## User Story\n\n${userStory.trim()}\n\n`;
|
|
1084
|
+
}
|
|
1085
|
+
if (summary.trim()) {
|
|
1086
|
+
prBody += `## Summary\n\n${summary.trim()}\n\n`;
|
|
1087
|
+
}
|
|
1088
|
+
if (cleanAcceptanceCriteria.trim()) {
|
|
1089
|
+
prBody += `## Acceptance Criteria\n\n${cleanAcceptanceCriteria.trim()}\n\n`;
|
|
1090
|
+
}
|
|
1091
|
+
if (cleanImplementationSummary.trim()) {
|
|
1092
|
+
prBody += `## Implementation Summary\n\n${cleanImplementationSummary.trim()}\n\n`;
|
|
1093
|
+
}
|
|
1094
|
+
// Add story file link
|
|
1095
|
+
if (storyFileUrl) {
|
|
1096
|
+
prBody += `---\n\n📋 [View Full Story](${storyFileUrl})\n`;
|
|
1097
|
+
}
|
|
1098
|
+
return prBody;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Truncate PR body to respect GitHub's 65K character limit
|
|
1102
|
+
* Truncates Implementation Summary first (most verbose section)
|
|
1103
|
+
* Adds clear truncation indicator with story link
|
|
1104
|
+
*/
|
|
1105
|
+
export function truncatePRBody(body, maxLength = 64000) {
|
|
1106
|
+
// Check if truncation needed
|
|
1107
|
+
if (body.length <= maxLength) {
|
|
1108
|
+
return body;
|
|
1109
|
+
}
|
|
1110
|
+
// Find Implementation Summary section
|
|
1111
|
+
const implSummaryMatch = body.match(/(## Implementation Summary\n\n)([\s\S]*?)(\n\n##|\n\n---|\n\n📋|$)/);
|
|
1112
|
+
if (implSummaryMatch) {
|
|
1113
|
+
const [fullMatch, header, content, trailer] = implSummaryMatch;
|
|
1114
|
+
const beforeImpl = body.substring(0, body.indexOf(fullMatch));
|
|
1115
|
+
const afterImpl = body.substring(body.indexOf(fullMatch) + fullMatch.length);
|
|
1116
|
+
// Calculate how much we need to remove
|
|
1117
|
+
const overhead = beforeImpl.length + header.length + trailer.length + afterImpl.length;
|
|
1118
|
+
const truncationIndicator = '\n\n⚠️ Implementation Summary truncated due to length. See full story for complete details.\n';
|
|
1119
|
+
const availableForContent = maxLength - overhead - truncationIndicator.length;
|
|
1120
|
+
if (availableForContent > 100) {
|
|
1121
|
+
// Truncate Implementation Summary at paragraph boundary
|
|
1122
|
+
let truncatedContent = content.substring(0, availableForContent);
|
|
1123
|
+
const lastParagraph = truncatedContent.lastIndexOf('\n\n');
|
|
1124
|
+
if (lastParagraph > 0) {
|
|
1125
|
+
truncatedContent = truncatedContent.substring(0, lastParagraph);
|
|
1126
|
+
}
|
|
1127
|
+
return beforeImpl + header + truncatedContent + truncationIndicator + trailer + afterImpl;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// Fallback: simple truncation if no Implementation Summary found
|
|
1131
|
+
const truncatedBody = body.substring(0, maxLength - 200);
|
|
1132
|
+
const lastParagraph = truncatedBody.lastIndexOf('\n\n');
|
|
1133
|
+
const finalBody = lastParagraph > 0 ? truncatedBody.substring(0, lastParagraph) : truncatedBody;
|
|
1134
|
+
return finalBody + '\n\n⚠️ Description truncated due to length. See full story for complete details.\n';
|
|
1135
|
+
}
|
|
759
1136
|
/**
|
|
760
1137
|
* Run a sub-review with a specific prompt
|
|
761
1138
|
*/
|
|
@@ -782,7 +1159,7 @@ Provide your ${reviewType} feedback. Be specific and actionable.`;
|
|
|
782
1159
|
/**
|
|
783
1160
|
* Create a pull request for the completed story
|
|
784
1161
|
*/
|
|
785
|
-
export async function createPullRequest(storyPath, sdlcRoot) {
|
|
1162
|
+
export async function createPullRequest(storyPath, sdlcRoot, options) {
|
|
786
1163
|
let story = parseStory(storyPath);
|
|
787
1164
|
const changesMade = [];
|
|
788
1165
|
const workingDir = path.dirname(sdlcRoot);
|
|
@@ -819,7 +1196,7 @@ export async function createPullRequest(storyPath, sdlcRoot) {
|
|
|
819
1196
|
catch {
|
|
820
1197
|
changesMade.push('GitHub CLI not available - PR creation skipped');
|
|
821
1198
|
// Still update to done for MVP
|
|
822
|
-
story = updateStoryStatus(story, 'done');
|
|
1199
|
+
story = await updateStoryStatus(story, 'done');
|
|
823
1200
|
changesMade.push('Updated status to done');
|
|
824
1201
|
return {
|
|
825
1202
|
success: true,
|
|
@@ -844,37 +1221,69 @@ export async function createPullRequest(storyPath, sdlcRoot) {
|
|
|
844
1221
|
// Push branch (already validated)
|
|
845
1222
|
execSync(`git push -u origin ${branchName}`, { cwd: workingDir, stdio: 'pipe' });
|
|
846
1223
|
changesMade.push(`Pushed branch: ${branchName}`);
|
|
847
|
-
//
|
|
848
|
-
|
|
1224
|
+
// Check if PR already exists for this branch
|
|
1225
|
+
try {
|
|
1226
|
+
const existingPROutput = execSync('gh pr view --json url', { cwd: workingDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
1227
|
+
const prData = JSON.parse(existingPROutput);
|
|
1228
|
+
if (prData.url) {
|
|
1229
|
+
changesMade.push(`PR already exists: ${prData.url}`);
|
|
1230
|
+
// Update story with PR URL if missing
|
|
1231
|
+
if (!story.frontmatter.pr_url) {
|
|
1232
|
+
await updateStoryField(story, 'pr_url', prData.url);
|
|
1233
|
+
changesMade.push('Updated story with existing PR URL');
|
|
1234
|
+
}
|
|
1235
|
+
// Don't create duplicate - skip to status update
|
|
1236
|
+
story = await updateStoryStatus(story, 'done');
|
|
1237
|
+
changesMade.push('Updated status to done');
|
|
1238
|
+
return {
|
|
1239
|
+
success: true,
|
|
1240
|
+
story,
|
|
1241
|
+
changesMade,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
// No existing PR - proceed with creation
|
|
1247
|
+
}
|
|
1248
|
+
// Create PR using gh CLI with rich formatted body
|
|
1249
|
+
// Security: Use escaped arguments and heredoc to prevent shell injection
|
|
849
1250
|
const prTitle = story.frontmatter.title;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const prOutput = execSync(`gh pr create --title ${escapeShellArg(prTitle)} --body ${escapeShellArg(prBody)}`, { cwd: workingDir, encoding: 'utf-8' });
|
|
1251
|
+
// Generate story file URL
|
|
1252
|
+
const storyFileUrl = getStoryFileURL(storyPath, branchName, workingDir);
|
|
1253
|
+
// Format rich PR description
|
|
1254
|
+
let prBody = formatPRDescription(story, storyFileUrl);
|
|
1255
|
+
// Truncate if needed to respect GitHub's 65K limit
|
|
1256
|
+
prBody = truncatePRBody(prBody);
|
|
1257
|
+
// Determine if draft PR should be created
|
|
1258
|
+
// Options parameter takes precedence, then config, default is false
|
|
1259
|
+
const config = loadConfig(workingDir);
|
|
1260
|
+
const createAsDraft = options?.draft ?? config.github?.createDraftPRs ?? false;
|
|
1261
|
+
const draftFlag = createAsDraft ? ' --draft' : '';
|
|
1262
|
+
// Use heredoc pattern for multi-line body to preserve formatting
|
|
1263
|
+
const ghCommand = `gh pr create --title ${escapeShellArg(prTitle)}${draftFlag} --body "$(cat <<'EOF'
|
|
1264
|
+
${prBody}
|
|
1265
|
+
EOF
|
|
1266
|
+
)"`;
|
|
1267
|
+
const prOutput = execSync(ghCommand, { cwd: workingDir, encoding: 'utf-8' });
|
|
868
1268
|
const prUrl = prOutput.trim();
|
|
869
|
-
updateStoryField(story, 'pr_url', prUrl);
|
|
870
|
-
|
|
1269
|
+
await updateStoryField(story, 'pr_url', prUrl);
|
|
1270
|
+
const prTypeLabel = createAsDraft ? 'draft PR' : 'PR';
|
|
1271
|
+
changesMade.push(`Created ${prTypeLabel}: ${prUrl}`);
|
|
871
1272
|
}
|
|
872
1273
|
catch (error) {
|
|
873
1274
|
const sanitizedError = sanitizeErrorMessage(error instanceof Error ? error.message : String(error), workingDir);
|
|
874
|
-
|
|
1275
|
+
// Provide actionable error messages for common issues
|
|
1276
|
+
let errorMessage = `PR creation failed: ${sanitizedError}`;
|
|
1277
|
+
if (sanitizedError.includes('authentication') || sanitizedError.includes('auth') || sanitizedError.includes('credentials')) {
|
|
1278
|
+
errorMessage = `GitHub authentication failed. Please authenticate using one of:
|
|
1279
|
+
1. Set GITHUB_TOKEN env var: export GITHUB_TOKEN=ghp_xxx
|
|
1280
|
+
2. Run: gh auth login
|
|
1281
|
+
3. Check: gh auth status`;
|
|
1282
|
+
}
|
|
1283
|
+
changesMade.push(errorMessage);
|
|
875
1284
|
}
|
|
876
1285
|
// Update status to done
|
|
877
|
-
story = updateStoryStatus(story, 'done');
|
|
1286
|
+
story = await updateStoryStatus(story, 'done');
|
|
878
1287
|
changesMade.push('Updated status to done');
|
|
879
1288
|
return {
|
|
880
1289
|
success: true,
|