ai-sdlc 0.2.0-alpha.4 → 0.2.0-alpha.41

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 (118) hide show
  1. package/README.md +53 -1058
  2. package/dist/agents/implementation.d.ts +62 -0
  3. package/dist/agents/implementation.d.ts.map +1 -1
  4. package/dist/agents/implementation.js +494 -90
  5. package/dist/agents/implementation.js.map +1 -1
  6. package/dist/agents/index.d.ts +1 -0
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/index.js +1 -0
  9. package/dist/agents/index.js.map +1 -1
  10. package/dist/agents/planning.d.ts +1 -1
  11. package/dist/agents/planning.d.ts.map +1 -1
  12. package/dist/agents/planning.js +55 -4
  13. package/dist/agents/planning.js.map +1 -1
  14. package/dist/agents/refinement.d.ts.map +1 -1
  15. package/dist/agents/refinement.js +22 -3
  16. package/dist/agents/refinement.js.map +1 -1
  17. package/dist/agents/research.d.ts +85 -1
  18. package/dist/agents/research.d.ts.map +1 -1
  19. package/dist/agents/research.js +506 -16
  20. package/dist/agents/research.js.map +1 -1
  21. package/dist/agents/review.d.ts +79 -2
  22. package/dist/agents/review.d.ts.map +1 -1
  23. package/dist/agents/review.js +568 -68
  24. package/dist/agents/review.js.map +1 -1
  25. package/dist/agents/rework.d.ts.map +1 -1
  26. package/dist/agents/rework.js +22 -3
  27. package/dist/agents/rework.js.map +1 -1
  28. package/dist/agents/single-task.d.ts +41 -0
  29. package/dist/agents/single-task.d.ts.map +1 -0
  30. package/dist/agents/single-task.js +357 -0
  31. package/dist/agents/single-task.js.map +1 -0
  32. package/dist/agents/state-assessor.d.ts +3 -3
  33. package/dist/agents/state-assessor.d.ts.map +1 -1
  34. package/dist/agents/state-assessor.js +6 -6
  35. package/dist/agents/state-assessor.js.map +1 -1
  36. package/dist/agents/test-pattern-detector.d.ts +49 -0
  37. package/dist/agents/test-pattern-detector.d.ts.map +1 -0
  38. package/dist/agents/test-pattern-detector.js +273 -0
  39. package/dist/agents/test-pattern-detector.js.map +1 -0
  40. package/dist/agents/verification.d.ts +11 -0
  41. package/dist/agents/verification.d.ts.map +1 -1
  42. package/dist/agents/verification.js +74 -1
  43. package/dist/agents/verification.js.map +1 -1
  44. package/dist/cli/commands/migrate.js +1 -1
  45. package/dist/cli/commands/migrate.js.map +1 -1
  46. package/dist/cli/commands.d.ts +59 -3
  47. package/dist/cli/commands.d.ts.map +1 -1
  48. package/dist/cli/commands.js +1057 -217
  49. package/dist/cli/commands.js.map +1 -1
  50. package/dist/cli/daemon.d.ts.map +1 -1
  51. package/dist/cli/daemon.js +40 -10
  52. package/dist/cli/daemon.js.map +1 -1
  53. package/dist/cli/runner.d.ts.map +1 -1
  54. package/dist/cli/runner.js +51 -20
  55. package/dist/cli/runner.js.map +1 -1
  56. package/dist/core/auth.d.ts +43 -0
  57. package/dist/core/auth.d.ts.map +1 -1
  58. package/dist/core/auth.js +105 -1
  59. package/dist/core/auth.js.map +1 -1
  60. package/dist/core/client.d.ts +6 -0
  61. package/dist/core/client.d.ts.map +1 -1
  62. package/dist/core/client.js +57 -3
  63. package/dist/core/client.js.map +1 -1
  64. package/dist/core/config.d.ts +36 -1
  65. package/dist/core/config.d.ts.map +1 -1
  66. package/dist/core/config.js +162 -1
  67. package/dist/core/config.js.map +1 -1
  68. package/dist/core/conflict-detector.d.ts +108 -0
  69. package/dist/core/conflict-detector.d.ts.map +1 -0
  70. package/dist/core/conflict-detector.js +413 -0
  71. package/dist/core/conflict-detector.js.map +1 -0
  72. package/dist/core/git-utils.d.ts +28 -0
  73. package/dist/core/git-utils.d.ts.map +1 -0
  74. package/dist/core/git-utils.js +146 -0
  75. package/dist/core/git-utils.js.map +1 -0
  76. package/dist/core/index.d.ts +18 -0
  77. package/dist/core/index.d.ts.map +1 -0
  78. package/dist/core/index.js +18 -0
  79. package/dist/core/index.js.map +1 -0
  80. package/dist/core/kanban.d.ts +1 -6
  81. package/dist/core/kanban.d.ts.map +1 -1
  82. package/dist/core/kanban.js +10 -49
  83. package/dist/core/kanban.js.map +1 -1
  84. package/dist/core/logger.d.ts +92 -0
  85. package/dist/core/logger.d.ts.map +1 -0
  86. package/dist/core/logger.js +221 -0
  87. package/dist/core/logger.js.map +1 -0
  88. package/dist/core/story-logger.d.ts +102 -0
  89. package/dist/core/story-logger.d.ts.map +1 -0
  90. package/dist/core/story-logger.js +265 -0
  91. package/dist/core/story-logger.js.map +1 -0
  92. package/dist/core/story.d.ts +122 -18
  93. package/dist/core/story.d.ts.map +1 -1
  94. package/dist/core/story.js +390 -58
  95. package/dist/core/story.js.map +1 -1
  96. package/dist/core/task-parser.d.ts +59 -0
  97. package/dist/core/task-parser.d.ts.map +1 -0
  98. package/dist/core/task-parser.js +235 -0
  99. package/dist/core/task-parser.js.map +1 -0
  100. package/dist/core/task-progress.d.ts +92 -0
  101. package/dist/core/task-progress.d.ts.map +1 -0
  102. package/dist/core/task-progress.js +280 -0
  103. package/dist/core/task-progress.js.map +1 -0
  104. package/dist/core/workflow-state.d.ts +45 -6
  105. package/dist/core/workflow-state.d.ts.map +1 -1
  106. package/dist/core/workflow-state.js +201 -12
  107. package/dist/core/workflow-state.js.map +1 -1
  108. package/dist/core/worktree.d.ts +77 -0
  109. package/dist/core/worktree.d.ts.map +1 -0
  110. package/dist/core/worktree.js +246 -0
  111. package/dist/core/worktree.js.map +1 -0
  112. package/dist/index.js +135 -5
  113. package/dist/index.js.map +1 -1
  114. package/dist/types/index.d.ts +288 -1
  115. package/dist/types/index.d.ts.map +1 -1
  116. package/dist/types/index.js +1 -0
  117. package/dist/types/index.js.map +1 -1
  118. package/package.json +3 -1
@@ -1,11 +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';
10
+ import { sanitizeInput, truncateText } from '../cli/formatting.js';
11
+ import { detectTestDuplicationPatterns } from './test-pattern-detector.js';
9
12
  /**
10
13
  * Security: Validate Git branch name to prevent command injection
11
14
  * Only allows alphanumeric characters, hyphens, underscores, and forward slashes
@@ -92,7 +95,9 @@ const ReviewIssueSchema = z.object({
92
95
  // This handles LLM responses that return {"line": null} instead of omitting the field
93
96
  file: z.string().nullish().transform(v => v ?? undefined),
94
97
  line: z.number().int().positive().nullish().transform(v => v ?? undefined),
95
- suggestedFix: z.string().max(2000).nullish().transform(v => v ?? undefined),
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(),
96
101
  });
97
102
  const ReviewResponseSchema = z.object({
98
103
  passed: z.boolean(),
@@ -251,7 +256,8 @@ Output your review as a JSON object with this structure:
251
256
  "description": "Detailed description of the issue",
252
257
  "file": "path/to/file.ts" (if applicable),
253
258
  "line": 42 (if applicable),
254
- "suggestedFix": "How to fix this issue"
259
+ "suggestedFix": "How to fix this issue",
260
+ "perspectives": ["code", "security", "po"] (which perspectives this issue relates to)
255
261
  }
256
262
  ]
257
263
  }
@@ -264,6 +270,70 @@ Severity guidelines:
264
270
 
265
271
  If no issues found, return: {"passed": true, "issues": []}
266
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
+ */
267
337
  const CODE_REVIEW_PROMPT = `You are a senior code reviewer. Review the implementation for:
268
338
  1. Code quality and maintainability
269
339
  2. Following best practices
@@ -271,6 +341,9 @@ const CODE_REVIEW_PROMPT = `You are a senior code reviewer. Review the implement
271
341
  4. Test coverage adequacy
272
342
 
273
343
  ${REVIEW_OUTPUT_FORMAT}`;
344
+ /**
345
+ * @deprecated Use UNIFIED_REVIEW_PROMPT instead
346
+ */
274
347
  const SECURITY_REVIEW_PROMPT = `You are a security specialist. Review the implementation for:
275
348
  1. OWASP Top 10 vulnerabilities
276
349
  2. Input validation issues
@@ -278,6 +351,9 @@ const SECURITY_REVIEW_PROMPT = `You are a security specialist. Review the implem
278
351
  4. Data exposure risks
279
352
 
280
353
  ${REVIEW_OUTPUT_FORMAT}`;
354
+ /**
355
+ * @deprecated Use UNIFIED_REVIEW_PROMPT instead
356
+ */
281
357
  const PO_REVIEW_PROMPT = `You are a product owner validating the implementation. Check:
282
358
  1. Does it meet the acceptance criteria?
283
359
  2. Is the user experience appropriate?
@@ -315,6 +391,7 @@ function parseReviewResponse(response, reviewType) {
315
391
  file: issue.file,
316
392
  line: issue.line,
317
393
  suggestedFix: issue.suggestedFix,
394
+ perspectives: issue.perspectives,
318
395
  }));
319
396
  return {
320
397
  passed: validated.passed !== false && issues.filter(i => i.severity === 'blocker' || i.severity === 'critical').length === 0,
@@ -382,8 +459,35 @@ function determineReviewSeverity(issues) {
382
459
  return ReviewSeverity.LOW;
383
460
  }
384
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
+ }
385
488
  /**
386
489
  * Aggregate issues from multiple reviews and determine overall pass/fail
490
+ * @deprecated No longer used with unified review. Kept for reference only.
387
491
  */
388
492
  function aggregateReviews(codeResult, securityResult, poResult) {
389
493
  const allIssues = [...codeResult.issues, ...securityResult.issues, ...poResult.issues];
@@ -398,6 +502,7 @@ function aggregateReviews(codeResult, securityResult, poResult) {
398
502
  }
399
503
  /**
400
504
  * Format issues for display in review notes
505
+ * Shows perspectives (code, security, po) when available
401
506
  */
402
507
  function formatIssuesForDisplay(issues) {
403
508
  if (issues.length === 0) {
@@ -416,7 +521,11 @@ function formatIssuesForDisplay(issues) {
416
521
  const icon = severity === 'blocker' ? '🛑' : severity === 'critical' ? '⚠️' : severity === 'major' ? '📋' : 'ℹ️';
417
522
  output += `\n#### ${icon} ${severity.toUpperCase()} (${issueList.length})\n\n`;
418
523
  for (const issue of issueList) {
419
- output += `**${issue.category}**: ${issue.description}\n`;
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`;
420
529
  if (issue.file) {
421
530
  output += ` - File: \`${issue.file}\`${issue.line ? `:${issue.line}` : ''}\n`;
422
531
  }
@@ -428,6 +537,131 @@ function formatIssuesForDisplay(issues) {
428
537
  }
429
538
  return output;
430
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
+ }
575
+ /**
576
+ * Generate executive summary from review issues (1-3 sentences)
577
+ *
578
+ * Prioritizes by severity: blocker > critical > major > minor
579
+ * Shows top 2-3 issues with file names when available
580
+ * Truncates gracefully if many issues exist
581
+ *
582
+ * @param issues - Array of review issues to summarize
583
+ * @param terminalWidth - Terminal width for text wrapping
584
+ * @returns Executive summary string (1-3 sentences)
585
+ */
586
+ export function generateReviewSummary(issues, terminalWidth) {
587
+ // Validate terminal width (ensure minimum viable width)
588
+ if (terminalWidth <= 0 || !Number.isFinite(terminalWidth)) {
589
+ terminalWidth = 80;
590
+ }
591
+ // Edge case: no issues but still rejected (system error)
592
+ if (issues.length === 0) {
593
+ return 'Review rejected due to system error or policy violation.';
594
+ }
595
+ // Sort issues by severity priority
596
+ const severityOrder = {
597
+ blocker: 0,
598
+ critical: 1,
599
+ major: 2,
600
+ minor: 3,
601
+ };
602
+ const sortedIssues = [...issues].sort((a, b) => {
603
+ const severityA = severityOrder[a.severity] ?? 3;
604
+ const severityB = severityOrder[b.severity] ?? 3;
605
+ return severityA - severityB;
606
+ });
607
+ // Select top 2-3 issues to include in summary
608
+ const maxIssuesToShow = 3;
609
+ const topIssues = sortedIssues.slice(0, maxIssuesToShow);
610
+ const remainingCount = sortedIssues.length - topIssues.length;
611
+ // Calculate available width per issue (leave room for "...and X more issues")
612
+ // Terminal width - indent (2 spaces) - "Summary: " (9 chars) = available
613
+ const summaryPrefix = 'Summary: ';
614
+ const indent = 2;
615
+ const availableWidth = Math.max(40, terminalWidth - indent - summaryPrefix.length);
616
+ // Build summary sentences
617
+ const sentences = [];
618
+ // Calculate maxCharsPerIssue based on availableWidth to ensure 3 issues fit
619
+ const moreIndicatorLength = remainingCount > 0 ? 30 : 0; // Approximate length of "...and X more issues."
620
+ const maxCharsPerIssue = Math.max(40, Math.floor((availableWidth - moreIndicatorLength) / 3));
621
+ for (const issue of topIssues) {
622
+ // Sanitize description for security
623
+ let description = sanitizeInput(issue.description || '');
624
+ // Remove code blocks and excessive whitespace for conciseness
625
+ description = description.replace(/```[\s\S]*?```/g, '');
626
+ description = description.replace(/\n+/g, ' ');
627
+ description = description.replace(/\s+/g, ' '); // Collapse multiple spaces
628
+ description = description.trim();
629
+ // Skip empty descriptions
630
+ if (!description) {
631
+ continue;
632
+ }
633
+ // Add file reference if available
634
+ let sentence = description;
635
+ if (issue.file) {
636
+ const fileName = issue.file.split('/').pop() || issue.file;
637
+ sentence = `${description} (${fileName}${issue.line ? `:${issue.line}` : ''})`;
638
+ }
639
+ // Truncate individual issue to max chars
640
+ if (sentence.length > maxCharsPerIssue) {
641
+ sentence = truncateText(sentence, maxCharsPerIssue);
642
+ }
643
+ sentences.push(sentence);
644
+ }
645
+ // Edge case: all issues had empty descriptions after sanitization
646
+ if (sentences.length === 0) {
647
+ return 'Review rejected (no actionable issue details available).';
648
+ }
649
+ // Combine sentences
650
+ let summary = sentences.join('. ');
651
+ if (!summary.endsWith('.')) {
652
+ summary += '.';
653
+ }
654
+ // Add "more issues" indicator if needed
655
+ if (remainingCount > 0) {
656
+ summary += ` ...and ${remainingCount} more issue${remainingCount > 1 ? 's' : ''}.`;
657
+ }
658
+ // Final truncation to respect terminal width
659
+ const maxSummaryLength = availableWidth - 10; // Leave some margin
660
+ if (summary.length > maxSummaryLength) {
661
+ summary = truncateText(summary, maxSummaryLength);
662
+ }
663
+ return summary;
664
+ }
431
665
  /**
432
666
  * Review Agent
433
667
  *
@@ -435,9 +669,15 @@ function formatIssuesForDisplay(issues) {
435
669
  * Now returns structured ReviewResult with pass/fail and issues.
436
670
  */
437
671
  export async function runReviewAgent(storyPath, sdlcRoot, options) {
672
+ const logger = getLogger();
673
+ const startTime = Date.now();
438
674
  const story = parseStory(storyPath);
439
675
  const changesMade = [];
440
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
+ });
441
681
  // Security: Validate working directory before any operations
442
682
  try {
443
683
  validateWorkingDirectory(workingDir);
@@ -463,14 +703,14 @@ export async function runReviewAgent(storyPath, sdlcRoot, options) {
463
703
  const config = loadConfig(workingDir);
464
704
  try {
465
705
  // Snapshot max_retries from config (protects against mid-cycle config changes)
466
- snapshotMaxRetries(story, config);
706
+ await snapshotMaxRetries(story, config);
467
707
  // Check if story has reached max retries
468
708
  if (isAtMaxRetries(story, config)) {
469
709
  const retryCount = story.frontmatter.retry_count || 0;
470
710
  const maxRetries = getEffectiveMaxRetries(story, config);
471
711
  const maxRetriesDisplay = Number.isFinite(maxRetries) ? maxRetries : '∞';
472
712
  const errorMsg = `Story has reached maximum retry limit (${retryCount}/${maxRetriesDisplay}). Manual intervention required.`;
473
- updateStoryField(story, 'last_error', errorMsg);
713
+ await updateStoryField(story, 'last_error', errorMsg);
474
714
  changesMade.push(errorMsg);
475
715
  return {
476
716
  success: false,
@@ -488,6 +728,67 @@ export async function runReviewAgent(storyPath, sdlcRoot, options) {
488
728
  feedback: errorMsg,
489
729
  };
490
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
+ });
491
792
  // Run build and tests BEFORE reviews (async with progress)
492
793
  changesMade.push('Running build and test verification...');
493
794
  const verification = await runVerificationAsync(workingDir, config, options?.onVerificationProgress);
@@ -555,60 +856,82 @@ export async function runReviewAgent(storyPath, sdlcRoot, options) {
555
856
  feedback: formatIssuesForDisplay(verificationIssues),
556
857
  };
557
858
  }
558
- // Verification passed - proceed with all reviews in parallel, passing verification context
559
- changesMade.push('Verification passed - proceeding with code/security/PO reviews');
560
- const [codeReview, securityReview, poReview] = await Promise.all([
561
- runSubReview(story, CODE_REVIEW_PROMPT, 'Code Review', workingDir, verificationContext),
562
- runSubReview(story, SECURITY_REVIEW_PROMPT, 'Security Review', workingDir, verificationContext),
563
- runSubReview(story, PO_REVIEW_PROMPT, 'Product Owner Review', workingDir, verificationContext),
564
- ]);
565
- // Parse each review response into structured issues
566
- const codeResult = parseReviewResponse(codeReview, 'Code Review');
567
- const securityResult = parseReviewResponse(securityReview, 'Security Review');
568
- const poResult = parseReviewResponse(poReview, 'Product Owner Review');
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');
569
883
  // TDD Validation: Check TDD cycle completeness if TDD was enabled for this story
570
884
  const tddEnabled = story.frontmatter.tdd_enabled ?? config.tdd?.enabled ?? false;
571
885
  if (tddEnabled && story.frontmatter.tdd_test_history?.length) {
572
886
  const tddViolations = validateTDDCycles(story.frontmatter.tdd_test_history);
573
887
  if (tddViolations.length > 0) {
574
888
  const tddIssues = generateTDDIssues(tddViolations);
575
- codeResult.issues.push(...tddIssues);
576
- codeResult.passed = false;
889
+ unifiedResult.issues.push(...tddIssues);
890
+ unifiedResult.passed = false;
577
891
  changesMade.push(`TDD validation: ${tddViolations.length} violation(s) detected`);
578
892
  }
579
893
  else {
580
894
  changesMade.push('TDD validation: All cycles completed correctly');
581
895
  }
582
896
  }
583
- // Add verification issues to code result (they're code-quality related)
584
- codeResult.issues.unshift(...verificationIssues);
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);
585
904
  if (verificationIssues.length > 0) {
586
- codeResult.passed = false;
905
+ unifiedResult.passed = false;
587
906
  }
588
- // Aggregate all issues and determine overall pass/fail
589
- const { passed, allIssues, severity } = aggregateReviews(codeResult, securityResult, poResult);
590
- // Compile review notes with structured format
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
591
916
  const reviewNotes = `
592
- ### Code Review
593
- ${formatIssuesForDisplay(codeResult.issues)}
917
+ ### Unified Collaborative Review
594
918
 
595
- ### Security Review
596
- ${formatIssuesForDisplay(securityResult.issues)}
919
+ ${formatIssuesForDisplay(allIssues)}
597
920
 
598
- ### Product Owner Review
599
- ${formatIssuesForDisplay(poResult.issues)}
921
+ ### Perspective Summary
922
+ - Code Quality: ${codeReviewPassed ? '✅ Passed' : '❌ Failed'}
923
+ - Security: ${securityReviewPassed ? '✅ Passed' : '❌ Failed'}
924
+ - Requirements (PO): ${poReviewPassed ? '✅ Passed' : '❌ Failed'}
600
925
 
601
926
  ### Overall Result
602
927
  ${passed ? '✅ **PASSED** - All reviews approved' : '❌ **FAILED** - Issues must be addressed'}
603
928
 
604
929
  ---
605
- *Reviews completed: ${new Date().toISOString().split('T')[0]}*
930
+ *Review completed: ${new Date().toISOString().split('T')[0]}*
606
931
  `;
607
932
  // Append reviews to story
608
- appendToSection(story, 'Review Notes', reviewNotes);
609
- changesMade.push('Added code review notes');
610
- changesMade.push('Added security review notes');
611
- changesMade.push('Added product owner review notes');
933
+ await appendToSection(story, 'Review Notes', reviewNotes);
934
+ changesMade.push('Added unified collaborative review notes');
612
935
  // Determine decision
613
936
  const decision = passed ? ReviewDecision.APPROVED : ReviewDecision.REJECTED;
614
937
  // Create review attempt record (omit undefined fields to avoid YAML serialization errors)
@@ -618,21 +941,28 @@ ${passed ? '✅ **PASSED** - All reviews approved' : '❌ **FAILED** - Issues mu
618
941
  ...(passed ? {} : { severity }),
619
942
  feedback: passed ? 'All reviews passed' : formatIssuesForDisplay(allIssues),
620
943
  blockers: allIssues.filter(i => i.severity === 'blocker').map(i => i.description),
621
- codeReviewPassed: codeResult.passed,
622
- securityReviewPassed: securityResult.passed,
623
- poReviewPassed: poResult.passed,
944
+ codeReviewPassed,
945
+ securityReviewPassed,
946
+ poReviewPassed,
624
947
  };
625
948
  // Append to review history
626
- appendReviewHistory(story, reviewAttempt);
949
+ await appendReviewHistory(story, reviewAttempt);
627
950
  changesMade.push('Recorded review attempt in history');
628
951
  if (passed) {
629
- updateStoryField(story, 'reviews_complete', true);
952
+ await updateStoryField(story, 'reviews_complete', true);
630
953
  changesMade.push('Marked reviews_complete: true');
631
954
  }
632
955
  else {
633
956
  changesMade.push(`Reviews failed with ${allIssues.length} issue(s) - rework required`);
634
957
  // Don't mark reviews_complete, this will trigger rework
635
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
+ });
636
966
  return {
637
967
  success: true,
638
968
  story: parseStory(storyPath),
@@ -648,6 +978,11 @@ ${passed ? '✅ **PASSED** - All reviews approved' : '❌ **FAILED** - Issues mu
648
978
  catch (error) {
649
979
  // Review agent failure - return FAILED decision (doesn't count as retry)
650
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
+ });
651
986
  return {
652
987
  success: false,
653
988
  story,
@@ -665,6 +1000,139 @@ ${passed ? '✅ **PASSED** - All reviews approved' : '❌ **FAILED** - Issues mu
665
1000
  };
666
1001
  }
667
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
+ }
668
1136
  /**
669
1137
  * Run a sub-review with a specific prompt
670
1138
  */
@@ -691,7 +1159,7 @@ Provide your ${reviewType} feedback. Be specific and actionable.`;
691
1159
  /**
692
1160
  * Create a pull request for the completed story
693
1161
  */
694
- export async function createPullRequest(storyPath, sdlcRoot) {
1162
+ export async function createPullRequest(storyPath, sdlcRoot, options) {
695
1163
  let story = parseStory(storyPath);
696
1164
  const changesMade = [];
697
1165
  const workingDir = path.dirname(sdlcRoot);
@@ -728,7 +1196,7 @@ export async function createPullRequest(storyPath, sdlcRoot) {
728
1196
  catch {
729
1197
  changesMade.push('GitHub CLI not available - PR creation skipped');
730
1198
  // Still update to done for MVP
731
- story = updateStoryStatus(story, 'done');
1199
+ story = await updateStoryStatus(story, 'done');
732
1200
  changesMade.push('Updated status to done');
733
1201
  return {
734
1202
  success: true,
@@ -753,37 +1221,69 @@ export async function createPullRequest(storyPath, sdlcRoot) {
753
1221
  // Push branch (already validated)
754
1222
  execSync(`git push -u origin ${branchName}`, { cwd: workingDir, stdio: 'pipe' });
755
1223
  changesMade.push(`Pushed branch: ${branchName}`);
756
- // Create PR using gh CLI with safe arguments
757
- // Security: Use escaped arguments to prevent shell injection
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
758
1250
  const prTitle = story.frontmatter.title;
759
- const prBody = `## Summary
760
-
761
- ${story.frontmatter.title}
762
-
763
- ## Story
764
-
765
- ${story.content.substring(0, 1000)}...
766
-
767
- ## Checklist
768
-
769
- - [x] Implementation complete
770
- - [x] Code review passed
771
- - [x] Security review passed
772
- - [x] Product owner approved
773
-
774
- ---
775
- *Created by ai-sdlc*`;
776
- 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' });
777
1268
  const prUrl = prOutput.trim();
778
- updateStoryField(story, 'pr_url', prUrl);
779
- changesMade.push(`Created PR: ${prUrl}`);
1269
+ await updateStoryField(story, 'pr_url', prUrl);
1270
+ const prTypeLabel = createAsDraft ? 'draft PR' : 'PR';
1271
+ changesMade.push(`Created ${prTypeLabel}: ${prUrl}`);
780
1272
  }
781
1273
  catch (error) {
782
1274
  const sanitizedError = sanitizeErrorMessage(error instanceof Error ? error.message : String(error), workingDir);
783
- changesMade.push(`PR creation failed: ${sanitizedError}`);
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);
784
1284
  }
785
1285
  // Update status to done
786
- story = updateStoryStatus(story, 'done');
1286
+ story = await updateStoryStatus(story, 'done');
787
1287
  changesMade.push('Updated status to done');
788
1288
  return {
789
1289
  success: true,