@codebakers/cli 3.3.16 → 3.3.18

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.
@@ -796,6 +796,49 @@ class CodeBakersServer {
796
796
  },
797
797
  },
798
798
  },
799
+ {
800
+ name: 'validate_complete',
801
+ description: 'MANDATORY: Call this BEFORE saying "done" or "complete" on any feature. Validates that tests exist, tests pass, and TypeScript compiles. Returns { valid: true } or { valid: false, missing: [...] }. You are NOT ALLOWED to complete a feature without calling this first.',
802
+ inputSchema: {
803
+ type: 'object',
804
+ properties: {
805
+ feature: {
806
+ type: 'string',
807
+ description: 'Name of the feature being completed (e.g., "login page", "payment form")',
808
+ },
809
+ files: {
810
+ type: 'array',
811
+ items: { type: 'string' },
812
+ description: 'Files that were created/modified for this feature',
813
+ },
814
+ },
815
+ required: ['feature'],
816
+ },
817
+ },
818
+ {
819
+ name: 'discover_patterns',
820
+ description: 'MANDATORY: Call this BEFORE writing or modifying ANY code. Searches codebase for similar implementations and identifies relevant patterns. Returns existing code patterns you MUST follow. You are NOT ALLOWED to write code without calling this first.',
821
+ inputSchema: {
822
+ type: 'object',
823
+ properties: {
824
+ task: {
825
+ type: 'string',
826
+ description: 'What you are about to do (e.g., "add signup form", "fix auth bug", "create payment endpoint")',
827
+ },
828
+ files: {
829
+ type: 'array',
830
+ items: { type: 'string' },
831
+ description: 'Files you plan to create or modify',
832
+ },
833
+ keywords: {
834
+ type: 'array',
835
+ items: { type: 'string' },
836
+ description: 'Keywords to search for in codebase (e.g., ["auth", "login", "user"])',
837
+ },
838
+ },
839
+ required: ['task'],
840
+ },
841
+ },
799
842
  {
800
843
  name: 'report_pattern_gap',
801
844
  description: 'Report when a user request cannot be fully handled by existing patterns. This helps improve CodeBakers by tracking what patterns are missing. The AI should automatically call this when it encounters something outside pattern coverage.',
@@ -1456,6 +1499,10 @@ class CodeBakersServer {
1456
1499
  return this.handleProjectStatus();
1457
1500
  case 'run_tests':
1458
1501
  return this.handleRunTests(args);
1502
+ case 'validate_complete':
1503
+ return this.handleValidateComplete(args);
1504
+ case 'discover_patterns':
1505
+ return this.handleDiscoverPatterns(args);
1459
1506
  case 'report_pattern_gap':
1460
1507
  return this.handleReportPatternGap(args);
1461
1508
  case 'track_analytics':
@@ -3336,6 +3383,385 @@ Just describe what you want to build! I'll automatically:
3336
3383
  }],
3337
3384
  };
3338
3385
  }
3386
+ /**
3387
+ * MANDATORY: Validate that a feature is complete before AI can say "done"
3388
+ * Checks: discover_patterns was called, tests exist, tests pass, TypeScript compiles
3389
+ */
3390
+ handleValidateComplete(args) {
3391
+ const { feature, files = [] } = args;
3392
+ const cwd = process.cwd();
3393
+ const issues = [];
3394
+ let patternsDiscovered = false;
3395
+ let testsExist = false;
3396
+ let testsPass = false;
3397
+ let typescriptPass = false;
3398
+ // Step 0: Check if discover_patterns was called (compliance tracking)
3399
+ try {
3400
+ const stateFile = path.join(cwd, '.codebakers.json');
3401
+ if (fs.existsSync(stateFile)) {
3402
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
3403
+ const compliance = state.compliance;
3404
+ if (compliance?.discoveries && Array.isArray(compliance.discoveries)) {
3405
+ // Check if there's a recent discovery (within last 30 minutes)
3406
+ const recentDiscovery = compliance.discoveries.find((d) => {
3407
+ const discovery = d;
3408
+ if (!discovery.timestamp)
3409
+ return false;
3410
+ const age = Date.now() - new Date(discovery.timestamp).getTime();
3411
+ return age < 30 * 60 * 1000; // 30 minutes
3412
+ });
3413
+ if (recentDiscovery) {
3414
+ patternsDiscovered = true;
3415
+ }
3416
+ else {
3417
+ issues.push('PATTERNS_NOT_CHECKED: You did not call `discover_patterns` before writing code. You MUST check existing patterns first.');
3418
+ }
3419
+ }
3420
+ else {
3421
+ issues.push('PATTERNS_NOT_CHECKED: You did not call `discover_patterns` before writing code. You MUST check existing patterns first.');
3422
+ }
3423
+ }
3424
+ else {
3425
+ // No state file - patterns weren't discovered
3426
+ issues.push('PATTERNS_NOT_CHECKED: You did not call `discover_patterns` before writing code. You MUST check existing patterns first.');
3427
+ }
3428
+ }
3429
+ catch {
3430
+ // If we can't read state, warn but don't fail
3431
+ issues.push('PATTERNS_UNKNOWN: Could not verify if `discover_patterns` was called. Please call it before continuing.');
3432
+ }
3433
+ // Step 1: Check if test files exist
3434
+ try {
3435
+ const testDirs = ['tests', 'test', '__tests__', 'src/__tests__', 'src/tests'];
3436
+ const testExtensions = ['.test.ts', '.test.tsx', '.spec.ts', '.spec.tsx'];
3437
+ for (const dir of testDirs) {
3438
+ const testDir = path.join(cwd, dir);
3439
+ if (fs.existsSync(testDir)) {
3440
+ const testFiles = fs.readdirSync(testDir, { recursive: true })
3441
+ .filter((f) => testExtensions.some(ext => String(f).endsWith(ext)));
3442
+ if (testFiles.length > 0) {
3443
+ testsExist = true;
3444
+ break;
3445
+ }
3446
+ }
3447
+ }
3448
+ // Also check for test files adjacent to source files
3449
+ if (!testsExist && files.length > 0) {
3450
+ for (const file of files) {
3451
+ const testFile = file.replace(/\.tsx?$/, '.test.ts');
3452
+ const specFile = file.replace(/\.tsx?$/, '.spec.ts');
3453
+ if (fs.existsSync(path.join(cwd, testFile)) || fs.existsSync(path.join(cwd, specFile))) {
3454
+ testsExist = true;
3455
+ break;
3456
+ }
3457
+ }
3458
+ }
3459
+ if (!testsExist) {
3460
+ issues.push('NO_TESTS: No test files found. You MUST write tests before completing this feature.');
3461
+ }
3462
+ }
3463
+ catch {
3464
+ issues.push('NO_TESTS: Could not verify test files exist.');
3465
+ }
3466
+ // Step 2: Run tests
3467
+ if (testsExist) {
3468
+ try {
3469
+ let testCommand = 'npm test';
3470
+ const pkgPath = path.join(cwd, 'package.json');
3471
+ if (fs.existsSync(pkgPath)) {
3472
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
3473
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3474
+ if (deps['@playwright/test'])
3475
+ testCommand = 'npx playwright test';
3476
+ else if (deps['vitest'])
3477
+ testCommand = 'npx vitest run';
3478
+ else if (deps['jest'])
3479
+ testCommand = 'npx jest';
3480
+ }
3481
+ (0, child_process_1.execSync)(testCommand, {
3482
+ cwd,
3483
+ encoding: 'utf-8',
3484
+ timeout: 120000,
3485
+ stdio: ['pipe', 'pipe', 'pipe'],
3486
+ });
3487
+ testsPass = true;
3488
+ }
3489
+ catch (error) {
3490
+ const execError = error;
3491
+ issues.push(`TESTS_FAIL: Tests are failing. Fix them before completing.\n${execError.stderr?.slice(0, 500) || ''}`);
3492
+ }
3493
+ }
3494
+ // Step 3: Run TypeScript check
3495
+ try {
3496
+ (0, child_process_1.execSync)('npx tsc --noEmit', {
3497
+ cwd,
3498
+ encoding: 'utf-8',
3499
+ timeout: 60000,
3500
+ stdio: ['pipe', 'pipe', 'pipe'],
3501
+ });
3502
+ typescriptPass = true;
3503
+ }
3504
+ catch (error) {
3505
+ const execError = error;
3506
+ const output = execError.stdout || execError.stderr || '';
3507
+ issues.push(`TYPESCRIPT_ERRORS: TypeScript compilation failed.\n${output.slice(0, 500)}`);
3508
+ }
3509
+ // Generate response
3510
+ const valid = patternsDiscovered && testsExist && testsPass && typescriptPass;
3511
+ let response = `# ✅ Feature Validation: ${feature}\n\n`;
3512
+ response += `| Check | Status |\n|-------|--------|\n`;
3513
+ response += `| Patterns discovered | ${patternsDiscovered ? '✅ PASS' : '❌ FAIL'} |\n`;
3514
+ response += `| Tests exist | ${testsExist ? '✅ PASS' : '❌ FAIL'} |\n`;
3515
+ response += `| Tests pass | ${testsPass ? '✅ PASS' : testsExist ? '❌ FAIL' : '⏭️ SKIP'} |\n`;
3516
+ response += `| TypeScript compiles | ${typescriptPass ? '✅ PASS' : '❌ FAIL'} |\n\n`;
3517
+ if (valid) {
3518
+ response += `## ✅ Feature is COMPLETE\n\n`;
3519
+ response += `All validation checks passed. You may now mark this feature as done.\n`;
3520
+ }
3521
+ else {
3522
+ response += `## ❌ Feature is NOT COMPLETE\n\n`;
3523
+ response += `**You are NOT ALLOWED to say "done" or "complete" until all checks pass.**\n\n`;
3524
+ response += `### Issues to fix:\n\n`;
3525
+ for (const issue of issues) {
3526
+ response += `- ${issue}\n\n`;
3527
+ }
3528
+ if (!patternsDiscovered) {
3529
+ response += `---\n\n**First, call \`discover_patterns\` to check existing code patterns. Then fix remaining issues and call \`validate_complete\` again.**`;
3530
+ }
3531
+ else {
3532
+ response += `---\n\n**Fix these issues and call \`validate_complete\` again.**`;
3533
+ }
3534
+ }
3535
+ return {
3536
+ content: [{
3537
+ type: 'text',
3538
+ text: response,
3539
+ }],
3540
+ // Return structured data for programmatic use
3541
+ isError: !valid,
3542
+ };
3543
+ }
3544
+ /**
3545
+ * discover_patterns - START gate for pattern compliance
3546
+ * MUST be called before writing any code
3547
+ */
3548
+ handleDiscoverPatterns(args) {
3549
+ const { task, files = [], keywords = [] } = args;
3550
+ const cwd = process.cwd();
3551
+ // Extract keywords from task if not provided
3552
+ const taskKeywords = this.extractKeywords(task);
3553
+ const allKeywords = [...new Set([...keywords, ...taskKeywords])];
3554
+ // Results to return
3555
+ const patterns = [];
3556
+ const existingCode = [];
3557
+ const mustFollow = [];
3558
+ // Step 1: Identify relevant .claude/ patterns based on keywords
3559
+ const patternMap = {
3560
+ 'auth': ['02-auth.md'],
3561
+ 'login': ['02-auth.md'],
3562
+ 'signup': ['02-auth.md'],
3563
+ 'password': ['02-auth.md'],
3564
+ 'session': ['02-auth.md'],
3565
+ 'oauth': ['02-auth.md'],
3566
+ 'payment': ['05-payments.md'],
3567
+ 'stripe': ['05-payments.md'],
3568
+ 'billing': ['05-payments.md'],
3569
+ 'subscription': ['05-payments.md'],
3570
+ 'checkout': ['05-payments.md'],
3571
+ 'database': ['01-database.md'],
3572
+ 'schema': ['01-database.md'],
3573
+ 'drizzle': ['01-database.md'],
3574
+ 'query': ['01-database.md'],
3575
+ 'api': ['03-api.md'],
3576
+ 'route': ['03-api.md'],
3577
+ 'endpoint': ['03-api.md'],
3578
+ 'form': ['04-frontend.md'],
3579
+ 'component': ['04-frontend.md'],
3580
+ 'react': ['04-frontend.md'],
3581
+ 'email': ['06b-email.md'],
3582
+ 'resend': ['06b-email.md'],
3583
+ 'voice': ['06a-voice.md'],
3584
+ 'vapi': ['06a-voice.md'],
3585
+ 'test': ['08-testing.md'],
3586
+ 'playwright': ['08-testing.md'],
3587
+ };
3588
+ for (const keyword of allKeywords) {
3589
+ const lowerKeyword = keyword.toLowerCase();
3590
+ for (const [key, patternFiles] of Object.entries(patternMap)) {
3591
+ if (lowerKeyword.includes(key) || key.includes(lowerKeyword)) {
3592
+ patterns.push(...patternFiles);
3593
+ }
3594
+ }
3595
+ }
3596
+ // Always include 00-core.md
3597
+ if (!patterns.includes('00-core.md')) {
3598
+ patterns.unshift('00-core.md');
3599
+ }
3600
+ // Deduplicate
3601
+ const uniquePatterns = [...new Set(patterns)];
3602
+ // Step 2: Search codebase for similar implementations
3603
+ const searchDirs = ['src/services', 'src/lib', 'src/app/api', 'src/components', 'lib', 'services'];
3604
+ const searchExtensions = ['.ts', '.tsx'];
3605
+ for (const keyword of allKeywords.slice(0, 5)) { // Limit to avoid too many searches
3606
+ for (const dir of searchDirs) {
3607
+ const searchDir = path.join(cwd, dir);
3608
+ if (!fs.existsSync(searchDir))
3609
+ continue;
3610
+ try {
3611
+ const files = this.findFilesRecursive(searchDir, searchExtensions);
3612
+ for (const file of files.slice(0, 20)) { // Limit files per dir
3613
+ try {
3614
+ const content = fs.readFileSync(file, 'utf-8');
3615
+ const lines = content.split('\n');
3616
+ for (let i = 0; i < lines.length; i++) {
3617
+ if (lines[i].toLowerCase().includes(keyword.toLowerCase())) {
3618
+ // Found a match - extract context
3619
+ const startLine = Math.max(0, i - 2);
3620
+ const endLine = Math.min(lines.length - 1, i + 5);
3621
+ const snippet = lines.slice(startLine, endLine + 1).join('\n');
3622
+ const relativePath = path.relative(cwd, file);
3623
+ // Avoid duplicates
3624
+ if (!existingCode.some(e => e.file === relativePath && Math.abs(parseInt(e.lines.split('-')[0]) - (startLine + 1)) < 5)) {
3625
+ existingCode.push({
3626
+ file: relativePath,
3627
+ lines: `${startLine + 1}-${endLine + 1}`,
3628
+ snippet: snippet.slice(0, 300),
3629
+ });
3630
+ }
3631
+ // Only get first match per file
3632
+ break;
3633
+ }
3634
+ }
3635
+ }
3636
+ catch {
3637
+ // Skip unreadable files
3638
+ }
3639
+ }
3640
+ }
3641
+ catch {
3642
+ // Skip inaccessible dirs
3643
+ }
3644
+ }
3645
+ }
3646
+ // Step 3: Extract patterns from existing code
3647
+ if (existingCode.length > 0) {
3648
+ // Look for common patterns in existing code
3649
+ for (const code of existingCode) {
3650
+ if (code.snippet.includes('.insert(')) {
3651
+ mustFollow.push(`Use .insert() for creating new records (found in ${code.file})`);
3652
+ }
3653
+ if (code.snippet.includes('.upsert(')) {
3654
+ mustFollow.push(`Use .upsert() for create-or-update operations (found in ${code.file})`);
3655
+ }
3656
+ if (code.snippet.includes('try {') && code.snippet.includes('catch')) {
3657
+ mustFollow.push(`Wrap database/API operations in try/catch (found in ${code.file})`);
3658
+ }
3659
+ if (code.snippet.includes('NextResponse.json')) {
3660
+ mustFollow.push(`Use NextResponse.json() for API responses (found in ${code.file})`);
3661
+ }
3662
+ if (code.snippet.includes('z.object')) {
3663
+ mustFollow.push(`Use Zod for input validation (found in ${code.file})`);
3664
+ }
3665
+ }
3666
+ }
3667
+ // Deduplicate mustFollow
3668
+ const uniqueMustFollow = [...new Set(mustFollow)];
3669
+ // Step 4: Log discovery to .codebakers.json for compliance tracking
3670
+ try {
3671
+ const stateFile = path.join(cwd, '.codebakers.json');
3672
+ let state = {};
3673
+ if (fs.existsSync(stateFile)) {
3674
+ state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
3675
+ }
3676
+ if (!state.compliance) {
3677
+ state.compliance = { discoveries: [], violations: [] };
3678
+ }
3679
+ const compliance = state.compliance;
3680
+ compliance.discoveries.push({
3681
+ task,
3682
+ patterns: uniquePatterns,
3683
+ existingCodeChecked: existingCode.map(e => e.file),
3684
+ timestamp: new Date().toISOString(),
3685
+ });
3686
+ // Keep only last 50 discoveries
3687
+ if (compliance.discoveries.length > 50) {
3688
+ compliance.discoveries = compliance.discoveries.slice(-50);
3689
+ }
3690
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
3691
+ }
3692
+ catch {
3693
+ // Ignore state file errors
3694
+ }
3695
+ // Generate response
3696
+ let response = `# 🔍 Pattern Discovery: ${task}\n\n`;
3697
+ response += `## ⛔ MANDATORY: You MUST follow these patterns before writing code\n\n`;
3698
+ response += `### 📦 Patterns to Load\n\n`;
3699
+ response += `Load these from \`.claude/\` folder:\n`;
3700
+ for (const pattern of uniquePatterns) {
3701
+ response += `- \`${pattern}\`\n`;
3702
+ }
3703
+ if (existingCode.length > 0) {
3704
+ response += `\n### 🔎 Existing Code to Follow\n\n`;
3705
+ response += `Found ${existingCode.length} relevant implementation(s):\n\n`;
3706
+ for (const code of existingCode.slice(0, 5)) { // Limit output
3707
+ response += `**${code.file}:${code.lines}**\n`;
3708
+ response += `\`\`\`typescript\n${code.snippet}\n\`\`\`\n\n`;
3709
+ }
3710
+ }
3711
+ if (uniqueMustFollow.length > 0) {
3712
+ response += `### ✅ Patterns You MUST Follow\n\n`;
3713
+ for (const rule of uniqueMustFollow) {
3714
+ response += `- ${rule}\n`;
3715
+ }
3716
+ }
3717
+ response += `\n---\n\n`;
3718
+ response += `## ⚠️ BEFORE WRITING CODE:\n\n`;
3719
+ response += `1. ✅ Read the patterns listed above\n`;
3720
+ response += `2. ✅ Check the existing code snippets\n`;
3721
+ response += `3. ✅ Follow the same patterns in your new code\n`;
3722
+ response += `4. ✅ When done, call \`validate_complete\` to verify\n\n`;
3723
+ response += `**You are NOT ALLOWED to skip these steps.**`;
3724
+ return {
3725
+ content: [{
3726
+ type: 'text',
3727
+ text: response,
3728
+ }],
3729
+ };
3730
+ }
3731
+ /**
3732
+ * Extract keywords from a task description
3733
+ */
3734
+ extractKeywords(task) {
3735
+ const words = task.toLowerCase()
3736
+ .replace(/[^a-z0-9\s]/g, ' ')
3737
+ .split(/\s+/)
3738
+ .filter(w => w.length > 2);
3739
+ // Filter out common words
3740
+ const stopWords = ['the', 'and', 'for', 'add', 'fix', 'create', 'make', 'build', 'implement', 'update', 'modify', 'change', 'new', 'with', 'from', 'this', 'that'];
3741
+ return words.filter(w => !stopWords.includes(w));
3742
+ }
3743
+ /**
3744
+ * Find files recursively with given extensions
3745
+ */
3746
+ findFilesRecursive(dir, extensions) {
3747
+ const results = [];
3748
+ try {
3749
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
3750
+ for (const entry of entries) {
3751
+ const fullPath = path.join(dir, entry.name);
3752
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
3753
+ results.push(...this.findFilesRecursive(fullPath, extensions));
3754
+ }
3755
+ else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
3756
+ results.push(fullPath);
3757
+ }
3758
+ }
3759
+ }
3760
+ catch {
3761
+ // Ignore errors
3762
+ }
3763
+ return results;
3764
+ }
3339
3765
  async handleReportPatternGap(args) {
3340
3766
  const { category, request, context, handledWith, wasSuccessful = true } = args;
3341
3767
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebakers/cli",
3
- "version": "3.3.16",
3
+ "version": "3.3.18",
4
4
  "description": "CodeBakers CLI - Production patterns for AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/mcp/server.ts CHANGED
@@ -876,6 +876,51 @@ class CodeBakersServer {
876
876
  },
877
877
  },
878
878
  },
879
+ {
880
+ name: 'validate_complete',
881
+ description:
882
+ 'MANDATORY: Call this BEFORE saying "done" or "complete" on any feature. Validates that tests exist, tests pass, and TypeScript compiles. Returns { valid: true } or { valid: false, missing: [...] }. You are NOT ALLOWED to complete a feature without calling this first.',
883
+ inputSchema: {
884
+ type: 'object' as const,
885
+ properties: {
886
+ feature: {
887
+ type: 'string',
888
+ description: 'Name of the feature being completed (e.g., "login page", "payment form")',
889
+ },
890
+ files: {
891
+ type: 'array',
892
+ items: { type: 'string' },
893
+ description: 'Files that were created/modified for this feature',
894
+ },
895
+ },
896
+ required: ['feature'],
897
+ },
898
+ },
899
+ {
900
+ name: 'discover_patterns',
901
+ description:
902
+ 'MANDATORY: Call this BEFORE writing or modifying ANY code. Searches codebase for similar implementations and identifies relevant patterns. Returns existing code patterns you MUST follow. You are NOT ALLOWED to write code without calling this first.',
903
+ inputSchema: {
904
+ type: 'object' as const,
905
+ properties: {
906
+ task: {
907
+ type: 'string',
908
+ description: 'What you are about to do (e.g., "add signup form", "fix auth bug", "create payment endpoint")',
909
+ },
910
+ files: {
911
+ type: 'array',
912
+ items: { type: 'string' },
913
+ description: 'Files you plan to create or modify',
914
+ },
915
+ keywords: {
916
+ type: 'array',
917
+ items: { type: 'string' },
918
+ description: 'Keywords to search for in codebase (e.g., ["auth", "login", "user"])',
919
+ },
920
+ },
921
+ required: ['task'],
922
+ },
923
+ },
879
924
  {
880
925
  name: 'report_pattern_gap',
881
926
  description:
@@ -1596,6 +1641,12 @@ class CodeBakersServer {
1596
1641
  case 'run_tests':
1597
1642
  return this.handleRunTests(args as { filter?: string; watch?: boolean });
1598
1643
 
1644
+ case 'validate_complete':
1645
+ return this.handleValidateComplete(args as { feature: string; files?: string[] });
1646
+
1647
+ case 'discover_patterns':
1648
+ return this.handleDiscoverPatterns(args as { task: string; files?: string[]; keywords?: string[] });
1649
+
1599
1650
  case 'report_pattern_gap':
1600
1651
  return this.handleReportPatternGap(args as { category: string; request: string; context?: string; handledWith?: string; wasSuccessful?: boolean });
1601
1652
 
@@ -3771,6 +3822,411 @@ Just describe what you want to build! I'll automatically:
3771
3822
  };
3772
3823
  }
3773
3824
 
3825
+ /**
3826
+ * MANDATORY: Validate that a feature is complete before AI can say "done"
3827
+ * Checks: discover_patterns was called, tests exist, tests pass, TypeScript compiles
3828
+ */
3829
+ private handleValidateComplete(args: { feature: string; files?: string[] }) {
3830
+ const { feature, files = [] } = args;
3831
+ const cwd = process.cwd();
3832
+ const issues: string[] = [];
3833
+ let patternsDiscovered = false;
3834
+ let testsExist = false;
3835
+ let testsPass = false;
3836
+ let typescriptPass = false;
3837
+
3838
+ // Step 0: Check if discover_patterns was called (compliance tracking)
3839
+ try {
3840
+ const stateFile = path.join(cwd, '.codebakers.json');
3841
+ if (fs.existsSync(stateFile)) {
3842
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
3843
+ const compliance = state.compliance as { discoveries?: unknown[] } | undefined;
3844
+
3845
+ if (compliance?.discoveries && Array.isArray(compliance.discoveries)) {
3846
+ // Check if there's a recent discovery (within last 30 minutes)
3847
+ const recentDiscovery = compliance.discoveries.find((d: unknown) => {
3848
+ const discovery = d as { timestamp?: string; task?: string };
3849
+ if (!discovery.timestamp) return false;
3850
+ const age = Date.now() - new Date(discovery.timestamp).getTime();
3851
+ return age < 30 * 60 * 1000; // 30 minutes
3852
+ });
3853
+
3854
+ if (recentDiscovery) {
3855
+ patternsDiscovered = true;
3856
+ } else {
3857
+ issues.push('PATTERNS_NOT_CHECKED: You did not call `discover_patterns` before writing code. You MUST check existing patterns first.');
3858
+ }
3859
+ } else {
3860
+ issues.push('PATTERNS_NOT_CHECKED: You did not call `discover_patterns` before writing code. You MUST check existing patterns first.');
3861
+ }
3862
+ } else {
3863
+ // No state file - patterns weren't discovered
3864
+ issues.push('PATTERNS_NOT_CHECKED: You did not call `discover_patterns` before writing code. You MUST check existing patterns first.');
3865
+ }
3866
+ } catch {
3867
+ // If we can't read state, warn but don't fail
3868
+ issues.push('PATTERNS_UNKNOWN: Could not verify if `discover_patterns` was called. Please call it before continuing.');
3869
+ }
3870
+
3871
+ // Step 1: Check if test files exist
3872
+ try {
3873
+ const testDirs = ['tests', 'test', '__tests__', 'src/__tests__', 'src/tests'];
3874
+ const testExtensions = ['.test.ts', '.test.tsx', '.spec.ts', '.spec.tsx'];
3875
+
3876
+ for (const dir of testDirs) {
3877
+ const testDir = path.join(cwd, dir);
3878
+ if (fs.existsSync(testDir)) {
3879
+ const testFiles = fs.readdirSync(testDir, { recursive: true })
3880
+ .filter((f: string | Buffer) => testExtensions.some(ext => String(f).endsWith(ext)));
3881
+ if (testFiles.length > 0) {
3882
+ testsExist = true;
3883
+ break;
3884
+ }
3885
+ }
3886
+ }
3887
+
3888
+ // Also check for test files adjacent to source files
3889
+ if (!testsExist && files.length > 0) {
3890
+ for (const file of files) {
3891
+ const testFile = file.replace(/\.tsx?$/, '.test.ts');
3892
+ const specFile = file.replace(/\.tsx?$/, '.spec.ts');
3893
+ if (fs.existsSync(path.join(cwd, testFile)) || fs.existsSync(path.join(cwd, specFile))) {
3894
+ testsExist = true;
3895
+ break;
3896
+ }
3897
+ }
3898
+ }
3899
+
3900
+ if (!testsExist) {
3901
+ issues.push('NO_TESTS: No test files found. You MUST write tests before completing this feature.');
3902
+ }
3903
+ } catch {
3904
+ issues.push('NO_TESTS: Could not verify test files exist.');
3905
+ }
3906
+
3907
+ // Step 2: Run tests
3908
+ if (testsExist) {
3909
+ try {
3910
+ let testCommand = 'npm test';
3911
+ const pkgPath = path.join(cwd, 'package.json');
3912
+ if (fs.existsSync(pkgPath)) {
3913
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
3914
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3915
+ if (deps['@playwright/test']) testCommand = 'npx playwright test';
3916
+ else if (deps['vitest']) testCommand = 'npx vitest run';
3917
+ else if (deps['jest']) testCommand = 'npx jest';
3918
+ }
3919
+
3920
+ execSync(testCommand, {
3921
+ cwd,
3922
+ encoding: 'utf-8',
3923
+ timeout: 120000,
3924
+ stdio: ['pipe', 'pipe', 'pipe'],
3925
+ });
3926
+ testsPass = true;
3927
+ } catch (error) {
3928
+ const execError = error as { stderr?: string };
3929
+ issues.push(`TESTS_FAIL: Tests are failing. Fix them before completing.\n${execError.stderr?.slice(0, 500) || ''}`);
3930
+ }
3931
+ }
3932
+
3933
+ // Step 3: Run TypeScript check
3934
+ try {
3935
+ execSync('npx tsc --noEmit', {
3936
+ cwd,
3937
+ encoding: 'utf-8',
3938
+ timeout: 60000,
3939
+ stdio: ['pipe', 'pipe', 'pipe'],
3940
+ });
3941
+ typescriptPass = true;
3942
+ } catch (error) {
3943
+ const execError = error as { stdout?: string; stderr?: string };
3944
+ const output = execError.stdout || execError.stderr || '';
3945
+ issues.push(`TYPESCRIPT_ERRORS: TypeScript compilation failed.\n${output.slice(0, 500)}`);
3946
+ }
3947
+
3948
+ // Generate response
3949
+ const valid = patternsDiscovered && testsExist && testsPass && typescriptPass;
3950
+
3951
+ let response = `# ✅ Feature Validation: ${feature}\n\n`;
3952
+ response += `| Check | Status |\n|-------|--------|\n`;
3953
+ response += `| Patterns discovered | ${patternsDiscovered ? '✅ PASS' : '❌ FAIL'} |\n`;
3954
+ response += `| Tests exist | ${testsExist ? '✅ PASS' : '❌ FAIL'} |\n`;
3955
+ response += `| Tests pass | ${testsPass ? '✅ PASS' : testsExist ? '❌ FAIL' : '⏭️ SKIP'} |\n`;
3956
+ response += `| TypeScript compiles | ${typescriptPass ? '✅ PASS' : '❌ FAIL'} |\n\n`;
3957
+
3958
+ if (valid) {
3959
+ response += `## ✅ Feature is COMPLETE\n\n`;
3960
+ response += `All validation checks passed. You may now mark this feature as done.\n`;
3961
+ } else {
3962
+ response += `## ❌ Feature is NOT COMPLETE\n\n`;
3963
+ response += `**You are NOT ALLOWED to say "done" or "complete" until all checks pass.**\n\n`;
3964
+ response += `### Issues to fix:\n\n`;
3965
+ for (const issue of issues) {
3966
+ response += `- ${issue}\n\n`;
3967
+ }
3968
+ if (!patternsDiscovered) {
3969
+ response += `---\n\n**First, call \`discover_patterns\` to check existing code patterns. Then fix remaining issues and call \`validate_complete\` again.**`;
3970
+ } else {
3971
+ response += `---\n\n**Fix these issues and call \`validate_complete\` again.**`;
3972
+ }
3973
+ }
3974
+
3975
+ return {
3976
+ content: [{
3977
+ type: 'text' as const,
3978
+ text: response,
3979
+ }],
3980
+ // Return structured data for programmatic use
3981
+ isError: !valid,
3982
+ };
3983
+ }
3984
+
3985
+ /**
3986
+ * discover_patterns - START gate for pattern compliance
3987
+ * MUST be called before writing any code
3988
+ */
3989
+ private handleDiscoverPatterns(args: { task: string; files?: string[]; keywords?: string[] }) {
3990
+ const { task, files = [], keywords = [] } = args;
3991
+ const cwd = process.cwd();
3992
+
3993
+ // Extract keywords from task if not provided
3994
+ const taskKeywords = this.extractKeywords(task);
3995
+ const allKeywords = [...new Set([...keywords, ...taskKeywords])];
3996
+
3997
+ // Results to return
3998
+ const patterns: string[] = [];
3999
+ const existingCode: { file: string; lines: string; snippet: string }[] = [];
4000
+ const mustFollow: string[] = [];
4001
+
4002
+ // Step 1: Identify relevant .claude/ patterns based on keywords
4003
+ const patternMap: Record<string, string[]> = {
4004
+ 'auth': ['02-auth.md'],
4005
+ 'login': ['02-auth.md'],
4006
+ 'signup': ['02-auth.md'],
4007
+ 'password': ['02-auth.md'],
4008
+ 'session': ['02-auth.md'],
4009
+ 'oauth': ['02-auth.md'],
4010
+ 'payment': ['05-payments.md'],
4011
+ 'stripe': ['05-payments.md'],
4012
+ 'billing': ['05-payments.md'],
4013
+ 'subscription': ['05-payments.md'],
4014
+ 'checkout': ['05-payments.md'],
4015
+ 'database': ['01-database.md'],
4016
+ 'schema': ['01-database.md'],
4017
+ 'drizzle': ['01-database.md'],
4018
+ 'query': ['01-database.md'],
4019
+ 'api': ['03-api.md'],
4020
+ 'route': ['03-api.md'],
4021
+ 'endpoint': ['03-api.md'],
4022
+ 'form': ['04-frontend.md'],
4023
+ 'component': ['04-frontend.md'],
4024
+ 'react': ['04-frontend.md'],
4025
+ 'email': ['06b-email.md'],
4026
+ 'resend': ['06b-email.md'],
4027
+ 'voice': ['06a-voice.md'],
4028
+ 'vapi': ['06a-voice.md'],
4029
+ 'test': ['08-testing.md'],
4030
+ 'playwright': ['08-testing.md'],
4031
+ };
4032
+
4033
+ for (const keyword of allKeywords) {
4034
+ const lowerKeyword = keyword.toLowerCase();
4035
+ for (const [key, patternFiles] of Object.entries(patternMap)) {
4036
+ if (lowerKeyword.includes(key) || key.includes(lowerKeyword)) {
4037
+ patterns.push(...patternFiles);
4038
+ }
4039
+ }
4040
+ }
4041
+
4042
+ // Always include 00-core.md
4043
+ if (!patterns.includes('00-core.md')) {
4044
+ patterns.unshift('00-core.md');
4045
+ }
4046
+
4047
+ // Deduplicate
4048
+ const uniquePatterns = [...new Set(patterns)];
4049
+
4050
+ // Step 2: Search codebase for similar implementations
4051
+ const searchDirs = ['src/services', 'src/lib', 'src/app/api', 'src/components', 'lib', 'services'];
4052
+ const searchExtensions = ['.ts', '.tsx'];
4053
+
4054
+ for (const keyword of allKeywords.slice(0, 5)) { // Limit to avoid too many searches
4055
+ for (const dir of searchDirs) {
4056
+ const searchDir = path.join(cwd, dir);
4057
+ if (!fs.existsSync(searchDir)) continue;
4058
+
4059
+ try {
4060
+ const files = this.findFilesRecursive(searchDir, searchExtensions);
4061
+ for (const file of files.slice(0, 20)) { // Limit files per dir
4062
+ try {
4063
+ const content = fs.readFileSync(file, 'utf-8');
4064
+ const lines = content.split('\n');
4065
+
4066
+ for (let i = 0; i < lines.length; i++) {
4067
+ if (lines[i].toLowerCase().includes(keyword.toLowerCase())) {
4068
+ // Found a match - extract context
4069
+ const startLine = Math.max(0, i - 2);
4070
+ const endLine = Math.min(lines.length - 1, i + 5);
4071
+ const snippet = lines.slice(startLine, endLine + 1).join('\n');
4072
+
4073
+ const relativePath = path.relative(cwd, file);
4074
+
4075
+ // Avoid duplicates
4076
+ if (!existingCode.some(e => e.file === relativePath && Math.abs(parseInt(e.lines.split('-')[0]) - (startLine + 1)) < 5)) {
4077
+ existingCode.push({
4078
+ file: relativePath,
4079
+ lines: `${startLine + 1}-${endLine + 1}`,
4080
+ snippet: snippet.slice(0, 300),
4081
+ });
4082
+ }
4083
+
4084
+ // Only get first match per file
4085
+ break;
4086
+ }
4087
+ }
4088
+ } catch {
4089
+ // Skip unreadable files
4090
+ }
4091
+ }
4092
+ } catch {
4093
+ // Skip inaccessible dirs
4094
+ }
4095
+ }
4096
+ }
4097
+
4098
+ // Step 3: Extract patterns from existing code
4099
+ if (existingCode.length > 0) {
4100
+ // Look for common patterns in existing code
4101
+ for (const code of existingCode) {
4102
+ if (code.snippet.includes('.insert(')) {
4103
+ mustFollow.push(`Use .insert() for creating new records (found in ${code.file})`);
4104
+ }
4105
+ if (code.snippet.includes('.upsert(')) {
4106
+ mustFollow.push(`Use .upsert() for create-or-update operations (found in ${code.file})`);
4107
+ }
4108
+ if (code.snippet.includes('try {') && code.snippet.includes('catch')) {
4109
+ mustFollow.push(`Wrap database/API operations in try/catch (found in ${code.file})`);
4110
+ }
4111
+ if (code.snippet.includes('NextResponse.json')) {
4112
+ mustFollow.push(`Use NextResponse.json() for API responses (found in ${code.file})`);
4113
+ }
4114
+ if (code.snippet.includes('z.object')) {
4115
+ mustFollow.push(`Use Zod for input validation (found in ${code.file})`);
4116
+ }
4117
+ }
4118
+ }
4119
+
4120
+ // Deduplicate mustFollow
4121
+ const uniqueMustFollow = [...new Set(mustFollow)];
4122
+
4123
+ // Step 4: Log discovery to .codebakers.json for compliance tracking
4124
+ try {
4125
+ const stateFile = path.join(cwd, '.codebakers.json');
4126
+ let state: Record<string, unknown> = {};
4127
+ if (fs.existsSync(stateFile)) {
4128
+ state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
4129
+ }
4130
+
4131
+ if (!state.compliance) {
4132
+ state.compliance = { discoveries: [], violations: [] };
4133
+ }
4134
+
4135
+ const compliance = state.compliance as { discoveries: unknown[]; violations: unknown[] };
4136
+ compliance.discoveries.push({
4137
+ task,
4138
+ patterns: uniquePatterns,
4139
+ existingCodeChecked: existingCode.map(e => e.file),
4140
+ timestamp: new Date().toISOString(),
4141
+ });
4142
+
4143
+ // Keep only last 50 discoveries
4144
+ if (compliance.discoveries.length > 50) {
4145
+ compliance.discoveries = compliance.discoveries.slice(-50);
4146
+ }
4147
+
4148
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
4149
+ } catch {
4150
+ // Ignore state file errors
4151
+ }
4152
+
4153
+ // Generate response
4154
+ let response = `# 🔍 Pattern Discovery: ${task}\n\n`;
4155
+ response += `## ⛔ MANDATORY: You MUST follow these patterns before writing code\n\n`;
4156
+
4157
+ response += `### 📦 Patterns to Load\n\n`;
4158
+ response += `Load these from \`.claude/\` folder:\n`;
4159
+ for (const pattern of uniquePatterns) {
4160
+ response += `- \`${pattern}\`\n`;
4161
+ }
4162
+
4163
+ if (existingCode.length > 0) {
4164
+ response += `\n### 🔎 Existing Code to Follow\n\n`;
4165
+ response += `Found ${existingCode.length} relevant implementation(s):\n\n`;
4166
+ for (const code of existingCode.slice(0, 5)) { // Limit output
4167
+ response += `**${code.file}:${code.lines}**\n`;
4168
+ response += `\`\`\`typescript\n${code.snippet}\n\`\`\`\n\n`;
4169
+ }
4170
+ }
4171
+
4172
+ if (uniqueMustFollow.length > 0) {
4173
+ response += `### ✅ Patterns You MUST Follow\n\n`;
4174
+ for (const rule of uniqueMustFollow) {
4175
+ response += `- ${rule}\n`;
4176
+ }
4177
+ }
4178
+
4179
+ response += `\n---\n\n`;
4180
+ response += `## ⚠️ BEFORE WRITING CODE:\n\n`;
4181
+ response += `1. ✅ Read the patterns listed above\n`;
4182
+ response += `2. ✅ Check the existing code snippets\n`;
4183
+ response += `3. ✅ Follow the same patterns in your new code\n`;
4184
+ response += `4. ✅ When done, call \`validate_complete\` to verify\n\n`;
4185
+ response += `**You are NOT ALLOWED to skip these steps.**`;
4186
+
4187
+ return {
4188
+ content: [{
4189
+ type: 'text' as const,
4190
+ text: response,
4191
+ }],
4192
+ };
4193
+ }
4194
+
4195
+ /**
4196
+ * Extract keywords from a task description
4197
+ */
4198
+ private extractKeywords(task: string): string[] {
4199
+ const words = task.toLowerCase()
4200
+ .replace(/[^a-z0-9\s]/g, ' ')
4201
+ .split(/\s+/)
4202
+ .filter(w => w.length > 2);
4203
+
4204
+ // Filter out common words
4205
+ const stopWords = ['the', 'and', 'for', 'add', 'fix', 'create', 'make', 'build', 'implement', 'update', 'modify', 'change', 'new', 'with', 'from', 'this', 'that'];
4206
+ return words.filter(w => !stopWords.includes(w));
4207
+ }
4208
+
4209
+ /**
4210
+ * Find files recursively with given extensions
4211
+ */
4212
+ private findFilesRecursive(dir: string, extensions: string[]): string[] {
4213
+ const results: string[] = [];
4214
+ try {
4215
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
4216
+ for (const entry of entries) {
4217
+ const fullPath = path.join(dir, entry.name);
4218
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
4219
+ results.push(...this.findFilesRecursive(fullPath, extensions));
4220
+ } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
4221
+ results.push(fullPath);
4222
+ }
4223
+ }
4224
+ } catch {
4225
+ // Ignore errors
4226
+ }
4227
+ return results;
4228
+ }
4229
+
3774
4230
  private async handleReportPatternGap(args: { category: string; request: string; context?: string; handledWith?: string; wasSuccessful?: boolean }) {
3775
4231
  const { category, request, context, handledWith, wasSuccessful = true } = args;
3776
4232