ai-sdlc 0.2.0 → 0.3.0

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 (87) hide show
  1. package/dist/agents/implementation.d.ts +30 -1
  2. package/dist/agents/implementation.d.ts.map +1 -1
  3. package/dist/agents/implementation.js +172 -17
  4. package/dist/agents/implementation.js.map +1 -1
  5. package/dist/agents/index.d.ts +2 -0
  6. package/dist/agents/index.d.ts.map +1 -1
  7. package/dist/agents/index.js +2 -0
  8. package/dist/agents/index.js.map +1 -1
  9. package/dist/agents/orchestrator.d.ts +61 -0
  10. package/dist/agents/orchestrator.d.ts.map +1 -0
  11. package/dist/agents/orchestrator.js +443 -0
  12. package/dist/agents/orchestrator.js.map +1 -0
  13. package/dist/agents/planning.d.ts +1 -1
  14. package/dist/agents/planning.d.ts.map +1 -1
  15. package/dist/agents/planning.js +33 -1
  16. package/dist/agents/planning.js.map +1 -1
  17. package/dist/agents/review.d.ts +37 -1
  18. package/dist/agents/review.d.ts.map +1 -1
  19. package/dist/agents/review.js +319 -44
  20. package/dist/agents/review.js.map +1 -1
  21. package/dist/agents/rework.d.ts.map +1 -1
  22. package/dist/agents/rework.js +3 -1
  23. package/dist/agents/rework.js.map +1 -1
  24. package/dist/agents/single-task.d.ts +41 -0
  25. package/dist/agents/single-task.d.ts.map +1 -0
  26. package/dist/agents/single-task.js +357 -0
  27. package/dist/agents/single-task.js.map +1 -0
  28. package/dist/agents/verification.d.ts.map +1 -1
  29. package/dist/agents/verification.js +26 -12
  30. package/dist/agents/verification.js.map +1 -1
  31. package/dist/cli/commands.d.ts +7 -0
  32. package/dist/cli/commands.d.ts.map +1 -1
  33. package/dist/cli/commands.js +560 -48
  34. package/dist/cli/commands.js.map +1 -1
  35. package/dist/cli/daemon.d.ts.map +1 -1
  36. package/dist/cli/daemon.js +5 -0
  37. package/dist/cli/daemon.js.map +1 -1
  38. package/dist/cli/runner.d.ts.map +1 -1
  39. package/dist/cli/runner.js +20 -9
  40. package/dist/cli/runner.js.map +1 -1
  41. package/dist/core/client.d.ts +19 -1
  42. package/dist/core/client.d.ts.map +1 -1
  43. package/dist/core/client.js +191 -5
  44. package/dist/core/client.js.map +1 -1
  45. package/dist/core/config.d.ts +9 -1
  46. package/dist/core/config.d.ts.map +1 -1
  47. package/dist/core/config.js +51 -2
  48. package/dist/core/config.js.map +1 -1
  49. package/dist/core/index.d.ts +2 -0
  50. package/dist/core/index.d.ts.map +1 -1
  51. package/dist/core/index.js +2 -0
  52. package/dist/core/index.js.map +1 -1
  53. package/dist/core/llm-utils.d.ts +103 -0
  54. package/dist/core/llm-utils.d.ts.map +1 -0
  55. package/dist/core/llm-utils.js +368 -0
  56. package/dist/core/llm-utils.js.map +1 -0
  57. package/dist/core/process-manager.d.ts +15 -0
  58. package/dist/core/process-manager.d.ts.map +1 -0
  59. package/dist/core/process-manager.js +132 -0
  60. package/dist/core/process-manager.js.map +1 -0
  61. package/dist/core/story.d.ts +35 -1
  62. package/dist/core/story.d.ts.map +1 -1
  63. package/dist/core/story.js +107 -1
  64. package/dist/core/story.js.map +1 -1
  65. package/dist/core/task-parser.d.ts +59 -0
  66. package/dist/core/task-parser.d.ts.map +1 -0
  67. package/dist/core/task-parser.js +235 -0
  68. package/dist/core/task-parser.js.map +1 -0
  69. package/dist/core/task-progress.d.ts +92 -0
  70. package/dist/core/task-progress.d.ts.map +1 -0
  71. package/dist/core/task-progress.js +280 -0
  72. package/dist/core/task-progress.js.map +1 -0
  73. package/dist/core/worktree.d.ts +111 -2
  74. package/dist/core/worktree.d.ts.map +1 -1
  75. package/dist/core/worktree.js +310 -2
  76. package/dist/core/worktree.js.map +1 -1
  77. package/dist/index.js +10 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/services/error-classifier.d.ts +119 -0
  80. package/dist/services/error-classifier.d.ts.map +1 -0
  81. package/dist/services/error-classifier.js +182 -0
  82. package/dist/services/error-classifier.js.map +1 -0
  83. package/dist/types/index.d.ts +230 -0
  84. package/dist/types/index.d.ts.map +1 -1
  85. package/dist/types/index.js.map +1 -1
  86. package/package.json +3 -2
  87. package/templates/story.md +5 -0
@@ -2,10 +2,12 @@ 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 { ProcessManager } from '../core/process-manager.js';
5
6
  import { parseStory, updateStoryStatus, appendToSection, updateStoryField, isAtMaxRetries, appendReviewHistory, snapshotMaxRetries, getEffectiveMaxRetries, getEffectiveMaxImplementationRetries } from '../core/story.js';
6
7
  import { runAgentQuery } from '../core/client.js';
7
8
  import { getLogger } from '../core/logger.js';
8
9
  import { loadConfig, DEFAULT_TIMEOUTS } from '../core/config.js';
10
+ import { extractStructuredResponseSync } from '../core/llm-utils.js';
9
11
  import { ReviewDecision, ReviewSeverity } from '../types/index.js';
10
12
  import { sanitizeInput, truncateText } from '../cli/formatting.js';
11
13
  import { detectTestDuplicationPatterns } from './test-pattern-detector.js';
@@ -172,6 +174,7 @@ async function runCommandAsync(command, workingDir, timeout, onProgress) {
172
174
  cwd: workingDir,
173
175
  stdio: ['pipe', 'pipe', 'pipe'],
174
176
  });
177
+ ProcessManager.getInstance().registerChild(child);
175
178
  const timeoutId = setTimeout(() => {
176
179
  killed = true;
177
180
  child.kill('SIGTERM');
@@ -252,7 +255,7 @@ Output your review as a JSON object with this structure:
252
255
  "issues": [
253
256
  {
254
257
  "severity": "blocker" | "critical" | "major" | "minor",
255
- "category": "code_quality" | "security" | "requirements" | "testing" | etc,
258
+ "category": "code_quality" | "security" | "requirements" | "testing" | "test_alignment" | etc,
256
259
  "description": "Detailed description of the issue",
257
260
  "file": "path/to/file.ts" (if applicable),
258
261
  "line": 42 (if applicable),
@@ -263,7 +266,7 @@ Output your review as a JSON object with this structure:
263
266
  }
264
267
 
265
268
  Severity guidelines:
266
- - blocker: Must be fixed before merging (security holes, broken functionality)
269
+ - blocker: Must be fixed before merging (security holes, broken functionality, test misalignment)
267
270
  - critical: Should be fixed before merging (major bugs, poor practices)
268
271
  - major: Should be addressed soon (code quality, maintainability)
269
272
  - minor: Nice to have improvements (style, optimizations)
@@ -304,6 +307,56 @@ Evaluate:
304
307
  - Is documentation adequate for users and maintainers?
305
308
  - Does the implementation align with the story goals?
306
309
 
310
+ ## Test-Implementation Alignment (BLOCKER category)
311
+
312
+ **CRITICAL PRE-REVIEW REQUIREMENT**: Tests have already been executed and passed. However, passing tests don't guarantee correctness if they verify outdated behavior.
313
+
314
+ During code review, you MUST verify test alignment:
315
+
316
+ 1. **For each changed production file, identify its test file**
317
+ - Check if tests exist for modified functions/modules
318
+ - Read the test assertions carefully
319
+
320
+ 2. **Verify tests match NEW behavior, not OLD**
321
+ - Do test assertions expect the current implementation behavior?
322
+ - If production code changed from sync to async, do tests use await?
323
+ - If function signature changed, do tests call it correctly?
324
+ - If return values changed, do tests expect the new values?
325
+
326
+ 3. **Flag misalignment as BLOCKER**
327
+ - If tests reference changed code but still expect old behavior:
328
+ - This is a **BLOCKER** severity issue
329
+ - Category MUST be: \`"test_alignment"\`
330
+ - Specify which test files need updating and why
331
+ - Provide example of correct assertion for new behavior
332
+
333
+ **Example of misaligned test (BLOCKER):**
334
+ \`\`\`typescript
335
+ // Production code changed from sync to async
336
+ async function loadConfig(): Promise<Config> {
337
+ return await fetchConfig();
338
+ }
339
+
340
+ // Test still expects sync behavior - MISSING await (BLOCKER)
341
+ test('loads config', () => {
342
+ const config = loadConfig(); // ❌ Missing await! Returns Promise<Config>, not Config
343
+ expect(config.port).toBe(3000); // ❌ Checking Promise.port, not config.port
344
+ });
345
+
346
+ // Correct aligned test:
347
+ test('loads config', async () => {
348
+ const config = await loadConfig(); // ✅ Awaits async function
349
+ expect(config.port).toBe(3000); // ✅ Checks actual config
350
+ });
351
+ \`\`\`
352
+
353
+ **When to flag test_alignment issues:**
354
+ - Tests verify old function signatures that no longer exist
355
+ - Tests expect old return value formats that changed
356
+ - Tests miss new error conditions introduced
357
+ - Tests pass but don't exercise the new code paths
358
+ - Mock expectations don't match the new implementation calls
359
+
307
360
  ## CRITICAL DEDUPLICATION INSTRUCTIONS:
308
361
 
309
362
  1. **DO NOT repeat the same underlying issue from different perspectives**
@@ -363,26 +416,25 @@ const PO_REVIEW_PROMPT = `You are a product owner validating the implementation.
363
416
  ${REVIEW_OUTPUT_FORMAT}`;
364
417
  /**
365
418
  * Parse review response and extract structured issues
419
+ * Uses extractStructuredResponseSync for robust parsing with multiple strategies:
420
+ * 1. Direct JSON parse
421
+ * 2. JSON within markdown code blocks
422
+ * 3. JSON with leading/trailing text stripped
423
+ * 4. YAML format fallback
424
+ *
366
425
  * Security: Uses zod schema validation to prevent malicious JSON
367
426
  */
368
427
  function parseReviewResponse(response, reviewType) {
369
- try {
370
- // Try to extract JSON from the response
371
- const jsonMatch = response.match(/\{[\s\S]*\}/);
372
- if (!jsonMatch) {
373
- // Fallback: no JSON found, analyze text
374
- return parseTextReview(response, reviewType);
375
- }
376
- const parsed = JSON.parse(jsonMatch[0]);
377
- // Security: Validate against zod schema before using the data
378
- const validationResult = ReviewResponseSchema.safeParse(parsed);
379
- if (!validationResult.success) {
380
- // Log validation errors for debugging
381
- console.warn('Review response failed schema validation:', validationResult.error);
382
- // Fallback to text analysis
383
- return parseTextReview(response, reviewType);
384
- }
385
- const validated = validationResult.data;
428
+ const logger = getLogger();
429
+ // Use the robust extraction utility with all strategies
430
+ const extractionResult = extractStructuredResponseSync(response, ReviewResponseSchema, false);
431
+ if (extractionResult.success && extractionResult.data) {
432
+ const validated = extractionResult.data;
433
+ logger.debug('review', `Successfully parsed review response using strategy: ${extractionResult.strategy}`, {
434
+ reviewType,
435
+ strategy: extractionResult.strategy,
436
+ issueCount: validated.issues.length,
437
+ });
386
438
  // Map validated data to ReviewIssue format (additional sanitization)
387
439
  const issues = validated.issues.map((issue) => ({
388
440
  severity: issue.severity,
@@ -398,11 +450,13 @@ function parseReviewResponse(response, reviewType) {
398
450
  issues,
399
451
  };
400
452
  }
401
- catch (error) {
402
- // Fallback to text analysis if JSON parsing fails
403
- console.warn('Review response parsing error:', error);
404
- return parseTextReview(response, reviewType);
405
- }
453
+ // All extraction strategies failed - log raw response for debugging and use text fallback
454
+ logger.warn('review', 'All extraction strategies failed for review response', {
455
+ reviewType,
456
+ error: extractionResult.error,
457
+ responsePreview: response.substring(0, 200),
458
+ });
459
+ return parseTextReview(response, reviewType);
406
460
  }
407
461
  /**
408
462
  * Fallback: Parse text-based review response (for when LLM doesn't return JSON)
@@ -572,6 +626,127 @@ export function getSourceCodeChanges(workingDir) {
572
626
  return ['unknown'];
573
627
  }
574
628
  }
629
+ /**
630
+ * Get configuration file changes from git diff
631
+ *
632
+ * Detects changes to configuration files including:
633
+ * - .claude/ directory (Agent SDK skills, CLAUDE.md)
634
+ * - .github/ directory (workflows, actions, issue templates)
635
+ * - Root config files (tsconfig.json, package.json, .gitignore, vitest.config.ts, etc.)
636
+ *
637
+ * Uses spawnSync for security (prevents command injection).
638
+ *
639
+ * @param workingDir - Working directory to run git diff in
640
+ * @returns Array of configuration file paths that have changed, or ['unknown'] if git fails
641
+ */
642
+ export function getConfigurationChanges(workingDir) {
643
+ try {
644
+ // Security: Use spawnSync with explicit args (not shell) to prevent injection
645
+ const result = spawnSync('git', ['diff', '--name-only', 'HEAD~1'], {
646
+ cwd: workingDir,
647
+ encoding: 'utf-8',
648
+ stdio: ['ignore', 'pipe', 'pipe'],
649
+ });
650
+ if (result.status !== 0) {
651
+ // Git command failed - fail open (assume changes exist)
652
+ return ['unknown'];
653
+ }
654
+ const output = result.stdout.toString();
655
+ return output
656
+ .split('\n')
657
+ .filter(f => f.trim())
658
+ .filter(f => {
659
+ // Configuration directories
660
+ if (f.startsWith('.claude/'))
661
+ return true;
662
+ if (f.startsWith('.github/'))
663
+ return true;
664
+ // Root configuration files (common patterns)
665
+ const rootConfigs = [
666
+ 'tsconfig.json',
667
+ 'package.json',
668
+ 'package-lock.json',
669
+ '.gitignore',
670
+ '.gitattributes',
671
+ 'vitest.config.ts',
672
+ 'vitest.config.js',
673
+ 'jest.config.js',
674
+ 'jest.config.ts',
675
+ '.eslintrc',
676
+ '.eslintrc.js',
677
+ '.eslintrc.json',
678
+ '.prettierrc',
679
+ '.prettierrc.js',
680
+ '.prettierrc.json',
681
+ 'Makefile',
682
+ 'Dockerfile',
683
+ 'docker-compose.yml',
684
+ '.env.example',
685
+ ];
686
+ return rootConfigs.includes(f);
687
+ });
688
+ }
689
+ catch {
690
+ // If git diff fails, assume there are changes (fail open, not closed)
691
+ return ['unknown'];
692
+ }
693
+ }
694
+ /**
695
+ * Determine the effective content type for validation
696
+ *
697
+ * Resolves the final content type based on story frontmatter fields:
698
+ * 1. If requires_source_changes === false, treat as 'configuration'
699
+ * 2. If requires_source_changes === true, treat as 'code'
700
+ * 3. Otherwise, use content_type field (default: 'code' for backward compatibility)
701
+ *
702
+ * @param story - Story with frontmatter to analyze
703
+ * @returns The effective content type to use for validation
704
+ */
705
+ export function determineEffectiveContentType(story) {
706
+ const frontmatter = story.frontmatter;
707
+ // Manual override takes precedence
708
+ if (frontmatter.requires_source_changes === false) {
709
+ return 'configuration';
710
+ }
711
+ if (frontmatter.requires_source_changes === true) {
712
+ return 'code';
713
+ }
714
+ // Use explicit content_type or default to 'code'
715
+ return frontmatter.content_type || 'code';
716
+ }
717
+ /**
718
+ * Check if test files exist in git diff
719
+ *
720
+ * Returns true if any test files have been modified/added, false otherwise.
721
+ * Uses spawnSync for security (prevents command injection).
722
+ *
723
+ * @param workingDir - Working directory to run git diff in
724
+ * @returns True if test files exist in changes, false otherwise
725
+ */
726
+ export function hasTestFiles(workingDir) {
727
+ try {
728
+ // Security: Use spawnSync with explicit args (not shell) to prevent injection
729
+ const result = spawnSync('git', ['diff', '--name-only', 'HEAD~1'], {
730
+ cwd: workingDir,
731
+ encoding: 'utf-8',
732
+ stdio: ['ignore', 'pipe', 'pipe'],
733
+ });
734
+ if (result.status !== 0) {
735
+ // Git command failed - fail open (assume tests exist to avoid false blocks)
736
+ return true;
737
+ }
738
+ const output = result.stdout.toString();
739
+ const files = output.split('\n').filter(f => f.trim());
740
+ // Check if any files match test patterns
741
+ return files.some(f => f.includes('.test.') ||
742
+ f.includes('.spec.') ||
743
+ f.includes('__tests__/'));
744
+ }
745
+ catch {
746
+ // If git diff fails, assume tests exist (fail open, not closed)
747
+ return true;
748
+ }
749
+ }
575
750
  /**
576
751
  * Generate executive summary from review issues (1-3 sentences)
577
752
  *
@@ -728,67 +903,167 @@ export async function runReviewAgent(storyPath, sdlcRoot, options) {
728
903
  feedback: errorMsg,
729
904
  };
730
905
  }
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
906
+ // PRE-CHECK GATE: Content type-aware validation before running expensive LLM reviews
907
+ const contentType = determineEffectiveContentType(story);
908
+ logger.info('review', 'Running content-type-specific validation', {
909
+ storyId: story.frontmatter.id,
910
+ contentType,
911
+ explicitContentType: story.frontmatter.content_type,
912
+ requiresSourceChanges: story.frontmatter.requires_source_changes,
913
+ });
914
+ // Validation flags
915
+ let validationFailed = false;
916
+ let validationReason = '';
917
+ let validationCategory = 'implementation';
918
+ // Check source code changes for 'code' and 'mixed' types
919
+ if (contentType === 'code' || contentType === 'mixed') {
920
+ const sourceChanges = getSourceCodeChanges(workingDir);
921
+ if (sourceChanges.length === 0) {
922
+ validationFailed = true;
923
+ validationReason = contentType === 'mixed'
924
+ ? 'Mixed story requires both source AND configuration changes - no source code was modified.'
925
+ : 'Implementation wrote documentation/planning only - no source code was modified.';
926
+ logger.warn('review', 'Source code validation failed', {
927
+ storyId: story.frontmatter.id,
928
+ contentType,
929
+ sourceChangesFound: sourceChanges.length,
930
+ });
931
+ }
932
+ else {
933
+ logger.info('review', 'Source code changes detected', {
934
+ storyId: story.frontmatter.id,
935
+ fileCount: sourceChanges.length,
936
+ });
937
+ }
938
+ }
939
+ // Check configuration changes for 'configuration' and 'mixed' types
940
+ if (!validationFailed && (contentType === 'configuration' || contentType === 'mixed')) {
941
+ const configChanges = getConfigurationChanges(workingDir);
942
+ if (configChanges.length === 0) {
943
+ validationFailed = true;
944
+ validationReason = contentType === 'mixed'
945
+ ? 'Mixed story requires both source AND configuration changes. No configuration file changes detected.'
946
+ : 'Configuration story requires changes to config files (.claude/, .github/, or root config files). No configuration changes detected.';
947
+ logger.warn('review', 'Configuration validation failed', {
948
+ storyId: story.frontmatter.id,
949
+ contentType,
950
+ configChangesFound: configChanges.length,
951
+ });
952
+ }
953
+ else {
954
+ logger.info('review', 'Configuration changes detected', {
955
+ storyId: story.frontmatter.id,
956
+ fileCount: configChanges.length,
957
+ });
958
+ }
959
+ }
960
+ // For 'documentation' type, skip all file change validation
961
+ if (contentType === 'documentation') {
962
+ logger.info('review', 'Documentation story - skipping file change validation', {
963
+ storyId: story.frontmatter.id,
964
+ });
965
+ }
966
+ // Handle validation failure (if any)
967
+ if (validationFailed) {
735
968
  const retryCount = story.frontmatter.implementation_retry_count || 0;
736
969
  const maxRetries = getEffectiveMaxImplementationRetries(story, config);
737
970
  if (retryCount < maxRetries) {
738
971
  // RECOVERABLE: Trigger implementation recovery
739
- logger.warn('review', 'No source code changes detected - triggering implementation recovery', {
972
+ logger.warn('review', 'Validation failed - triggering implementation recovery', {
740
973
  storyId: story.frontmatter.id,
741
974
  retryCount,
742
975
  maxRetries,
976
+ contentType,
743
977
  });
744
978
  await updateStoryField(story, 'implementation_complete', false);
745
- await updateStoryField(story, 'last_restart_reason', 'No source code changes detected. Implementation wrote documentation only.');
979
+ // Set restart reason (backward compatible message for default code stories)
980
+ const restartReason = contentType === 'configuration'
981
+ ? 'Configuration story requires changes to config files (.claude/, .github/, or root config files). No configuration changes detected.'
982
+ : contentType === 'mixed'
983
+ ? 'Mixed story requires both source AND configuration changes - no source code was modified.'
984
+ : 'No source code changes detected. Implementation wrote documentation only.';
985
+ await updateStoryField(story, 'last_restart_reason', restartReason);
986
+ // Create user-friendly recovery description
987
+ const recoveryDescription = contentType === 'configuration'
988
+ ? 'No configuration file modifications detected. Re-running implementation phase.'
989
+ : contentType === 'mixed'
990
+ ? 'No source code modifications detected. Re-running implementation phase.'
991
+ : 'No source code modifications detected. Re-running implementation phase.';
746
992
  return {
747
993
  success: true,
748
994
  story: parseStory(storyPath),
749
- changesMade: ['Detected documentation-only implementation', 'Triggered implementation recovery'],
995
+ changesMade: ['Detected incomplete implementation', 'Triggered implementation recovery'],
750
996
  passed: false,
751
997
  decision: ReviewDecision.RECOVERY,
752
998
  reviewType: 'pre-check',
753
999
  issues: [{
754
1000
  severity: 'critical',
755
- category: 'implementation',
756
- description: 'No source code modifications detected. Re-running implementation phase.',
1001
+ category: validationCategory,
1002
+ description: recoveryDescription,
757
1003
  }],
758
- feedback: 'Implementation recovery triggered - no source changes found.',
1004
+ feedback: `Implementation recovery triggered - ${validationReason}`,
759
1005
  };
760
1006
  }
761
1007
  else {
762
1008
  // NON-RECOVERABLE: Max retries reached
763
1009
  const maxRetriesDisplay = Number.isFinite(maxRetries) ? maxRetries : '∞';
764
- logger.error('review', 'No source code changes detected and max implementation retries reached', {
1010
+ logger.error('review', 'Validation failed and max implementation retries reached', {
765
1011
  storyId: story.frontmatter.id,
766
1012
  retryCount,
767
1013
  maxRetries,
1014
+ contentType,
768
1015
  });
769
1016
  return {
770
1017
  success: true,
771
1018
  story: parseStory(storyPath),
772
- changesMade: ['Detected documentation-only implementation', 'Max retries reached'],
1019
+ changesMade: ['Detected incomplete implementation', 'Max retries reached'],
773
1020
  passed: false,
774
1021
  decision: ReviewDecision.FAILED,
775
1022
  severity: ReviewSeverity.CRITICAL,
776
1023
  reviewType: 'pre-check',
777
1024
  issues: [{
778
1025
  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.',
1026
+ category: validationCategory,
1027
+ description: `${validationReason} This has occurred ${retryCount} time(s) (max: ${maxRetriesDisplay}). Manual intervention required.`,
1028
+ suggestedFix: 'Review the story requirements and implementation plan. Verify the content_type field matches the expected implementation. Consider simplifying the story or providing more explicit guidance.',
782
1029
  }],
783
- feedback: 'Implementation failed to produce code changes after multiple attempts.',
1030
+ feedback: 'Implementation failed validation after multiple attempts.',
784
1031
  };
785
1032
  }
786
1033
  }
787
- // Source changes exist - proceed with normal review flow
788
- logger.info('review', 'Source code changes detected - proceeding with verification', {
1034
+ // Validation passed - proceed with normal review flow
1035
+ logger.info('review', 'Content validation passed - proceeding with verification', {
789
1036
  storyId: story.frontmatter.id,
790
- fileCount: sourceChanges.length,
1037
+ contentType,
791
1038
  });
1039
+ // PRE-CHECK GATE: Check if test files exist
1040
+ const testsExist = hasTestFiles(workingDir);
1041
+ if (!testsExist) {
1042
+ logger.warn('review', 'No test files detected in implementation changes', {
1043
+ storyId: story.frontmatter.id,
1044
+ });
1045
+ return {
1046
+ success: true,
1047
+ story: parseStory(storyPath),
1048
+ changesMade: ['No test files found for implementation'],
1049
+ passed: false,
1050
+ decision: ReviewDecision.REJECTED,
1051
+ severity: ReviewSeverity.CRITICAL,
1052
+ reviewType: 'pre-check',
1053
+ issues: [{
1054
+ severity: 'blocker',
1055
+ category: 'testing',
1056
+ description: 'No tests found for this implementation. All implementations must include tests.',
1057
+ suggestedFix: 'Add test files (*.test.ts, *.spec.ts, or files in __tests__/ directory) that verify the implementation.',
1058
+ }],
1059
+ feedback: formatIssuesForDisplay([{
1060
+ severity: 'blocker',
1061
+ category: 'testing',
1062
+ description: 'No tests found for this implementation. All implementations must include tests.',
1063
+ suggestedFix: 'Add test files (*.test.ts, *.spec.ts, or files in __tests__/ directory) that verify the implementation.',
1064
+ }]),
1065
+ };
1066
+ }
792
1067
  // Run build and tests BEFORE reviews (async with progress)
793
1068
  changesMade.push('Running build and test verification...');
794
1069
  const verification = await runVerificationAsync(workingDir, config, options?.onVerificationProgress);
@@ -835,7 +1110,7 @@ export async function runReviewAgent(storyPath, sdlcRoot, options) {
835
1110
  severity: 'blocker',
836
1111
  category: 'testing',
837
1112
  description: `Tests must pass before code review can proceed.\n\nCommand: ${config.testCommand}\n\nTest output:\n\`\`\`\n${testOutput}${truncationNote}\n\`\`\``,
838
- suggestedFix: 'Fix failing tests before review can proceed.',
1113
+ suggestedFix: 'Fix failing tests before review can proceed. If tests are failing after implementation changes, verify that tests were updated to match the new behavior (not just the old behavior).',
839
1114
  });
840
1115
  verificationContext += `\n## Test Results ❌\nTest command \`${config.testCommand}\` FAILED:\n\`\`\`\n${testOutput}${truncationNote}\n\`\`\`\n`;
841
1116
  }