@codebakers/cli 3.3.17 → 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.
- package/dist/mcp/server.js +292 -3
- package/package.json +1 -1
- package/src/mcp/server.ts +315 -3
package/dist/mcp/server.js
CHANGED
|
@@ -815,6 +815,30 @@ class CodeBakersServer {
|
|
|
815
815
|
required: ['feature'],
|
|
816
816
|
},
|
|
817
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
|
+
},
|
|
818
842
|
{
|
|
819
843
|
name: 'report_pattern_gap',
|
|
820
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.',
|
|
@@ -1477,6 +1501,8 @@ class CodeBakersServer {
|
|
|
1477
1501
|
return this.handleRunTests(args);
|
|
1478
1502
|
case 'validate_complete':
|
|
1479
1503
|
return this.handleValidateComplete(args);
|
|
1504
|
+
case 'discover_patterns':
|
|
1505
|
+
return this.handleDiscoverPatterns(args);
|
|
1480
1506
|
case 'report_pattern_gap':
|
|
1481
1507
|
return this.handleReportPatternGap(args);
|
|
1482
1508
|
case 'track_analytics':
|
|
@@ -3359,15 +3385,51 @@ Just describe what you want to build! I'll automatically:
|
|
|
3359
3385
|
}
|
|
3360
3386
|
/**
|
|
3361
3387
|
* MANDATORY: Validate that a feature is complete before AI can say "done"
|
|
3362
|
-
* Checks: tests exist, tests pass, TypeScript compiles
|
|
3388
|
+
* Checks: discover_patterns was called, tests exist, tests pass, TypeScript compiles
|
|
3363
3389
|
*/
|
|
3364
3390
|
handleValidateComplete(args) {
|
|
3365
3391
|
const { feature, files = [] } = args;
|
|
3366
3392
|
const cwd = process.cwd();
|
|
3367
3393
|
const issues = [];
|
|
3394
|
+
let patternsDiscovered = false;
|
|
3368
3395
|
let testsExist = false;
|
|
3369
3396
|
let testsPass = false;
|
|
3370
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
|
+
}
|
|
3371
3433
|
// Step 1: Check if test files exist
|
|
3372
3434
|
try {
|
|
3373
3435
|
const testDirs = ['tests', 'test', '__tests__', 'src/__tests__', 'src/tests'];
|
|
@@ -3445,9 +3507,10 @@ Just describe what you want to build! I'll automatically:
|
|
|
3445
3507
|
issues.push(`TYPESCRIPT_ERRORS: TypeScript compilation failed.\n${output.slice(0, 500)}`);
|
|
3446
3508
|
}
|
|
3447
3509
|
// Generate response
|
|
3448
|
-
const valid = testsExist && testsPass && typescriptPass;
|
|
3510
|
+
const valid = patternsDiscovered && testsExist && testsPass && typescriptPass;
|
|
3449
3511
|
let response = `# ✅ Feature Validation: ${feature}\n\n`;
|
|
3450
3512
|
response += `| Check | Status |\n|-------|--------|\n`;
|
|
3513
|
+
response += `| Patterns discovered | ${patternsDiscovered ? '✅ PASS' : '❌ FAIL'} |\n`;
|
|
3451
3514
|
response += `| Tests exist | ${testsExist ? '✅ PASS' : '❌ FAIL'} |\n`;
|
|
3452
3515
|
response += `| Tests pass | ${testsPass ? '✅ PASS' : testsExist ? '❌ FAIL' : '⏭️ SKIP'} |\n`;
|
|
3453
3516
|
response += `| TypeScript compiles | ${typescriptPass ? '✅ PASS' : '❌ FAIL'} |\n\n`;
|
|
@@ -3462,7 +3525,12 @@ Just describe what you want to build! I'll automatically:
|
|
|
3462
3525
|
for (const issue of issues) {
|
|
3463
3526
|
response += `- ${issue}\n\n`;
|
|
3464
3527
|
}
|
|
3465
|
-
|
|
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
|
+
}
|
|
3466
3534
|
}
|
|
3467
3535
|
return {
|
|
3468
3536
|
content: [{
|
|
@@ -3473,6 +3541,227 @@ Just describe what you want to build! I'll automatically:
|
|
|
3473
3541
|
isError: !valid,
|
|
3474
3542
|
};
|
|
3475
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
|
+
}
|
|
3476
3765
|
async handleReportPatternGap(args) {
|
|
3477
3766
|
const { category, request, context, handledWith, wasSuccessful = true } = args;
|
|
3478
3767
|
try {
|
package/package.json
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -896,6 +896,31 @@ class CodeBakersServer {
|
|
|
896
896
|
required: ['feature'],
|
|
897
897
|
},
|
|
898
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
|
+
},
|
|
899
924
|
{
|
|
900
925
|
name: 'report_pattern_gap',
|
|
901
926
|
description:
|
|
@@ -1619,6 +1644,9 @@ class CodeBakersServer {
|
|
|
1619
1644
|
case 'validate_complete':
|
|
1620
1645
|
return this.handleValidateComplete(args as { feature: string; files?: string[] });
|
|
1621
1646
|
|
|
1647
|
+
case 'discover_patterns':
|
|
1648
|
+
return this.handleDiscoverPatterns(args as { task: string; files?: string[]; keywords?: string[] });
|
|
1649
|
+
|
|
1622
1650
|
case 'report_pattern_gap':
|
|
1623
1651
|
return this.handleReportPatternGap(args as { category: string; request: string; context?: string; handledWith?: string; wasSuccessful?: boolean });
|
|
1624
1652
|
|
|
@@ -3796,16 +3824,50 @@ Just describe what you want to build! I'll automatically:
|
|
|
3796
3824
|
|
|
3797
3825
|
/**
|
|
3798
3826
|
* MANDATORY: Validate that a feature is complete before AI can say "done"
|
|
3799
|
-
* Checks: tests exist, tests pass, TypeScript compiles
|
|
3827
|
+
* Checks: discover_patterns was called, tests exist, tests pass, TypeScript compiles
|
|
3800
3828
|
*/
|
|
3801
3829
|
private handleValidateComplete(args: { feature: string; files?: string[] }) {
|
|
3802
3830
|
const { feature, files = [] } = args;
|
|
3803
3831
|
const cwd = process.cwd();
|
|
3804
3832
|
const issues: string[] = [];
|
|
3833
|
+
let patternsDiscovered = false;
|
|
3805
3834
|
let testsExist = false;
|
|
3806
3835
|
let testsPass = false;
|
|
3807
3836
|
let typescriptPass = false;
|
|
3808
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
|
+
|
|
3809
3871
|
// Step 1: Check if test files exist
|
|
3810
3872
|
try {
|
|
3811
3873
|
const testDirs = ['tests', 'test', '__tests__', 'src/__tests__', 'src/tests'];
|
|
@@ -3884,10 +3946,11 @@ Just describe what you want to build! I'll automatically:
|
|
|
3884
3946
|
}
|
|
3885
3947
|
|
|
3886
3948
|
// Generate response
|
|
3887
|
-
const valid = testsExist && testsPass && typescriptPass;
|
|
3949
|
+
const valid = patternsDiscovered && testsExist && testsPass && typescriptPass;
|
|
3888
3950
|
|
|
3889
3951
|
let response = `# ✅ Feature Validation: ${feature}\n\n`;
|
|
3890
3952
|
response += `| Check | Status |\n|-------|--------|\n`;
|
|
3953
|
+
response += `| Patterns discovered | ${patternsDiscovered ? '✅ PASS' : '❌ FAIL'} |\n`;
|
|
3891
3954
|
response += `| Tests exist | ${testsExist ? '✅ PASS' : '❌ FAIL'} |\n`;
|
|
3892
3955
|
response += `| Tests pass | ${testsPass ? '✅ PASS' : testsExist ? '❌ FAIL' : '⏭️ SKIP'} |\n`;
|
|
3893
3956
|
response += `| TypeScript compiles | ${typescriptPass ? '✅ PASS' : '❌ FAIL'} |\n\n`;
|
|
@@ -3902,7 +3965,11 @@ Just describe what you want to build! I'll automatically:
|
|
|
3902
3965
|
for (const issue of issues) {
|
|
3903
3966
|
response += `- ${issue}\n\n`;
|
|
3904
3967
|
}
|
|
3905
|
-
|
|
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
|
+
}
|
|
3906
3973
|
}
|
|
3907
3974
|
|
|
3908
3975
|
return {
|
|
@@ -3915,6 +3982,251 @@ Just describe what you want to build! I'll automatically:
|
|
|
3915
3982
|
};
|
|
3916
3983
|
}
|
|
3917
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
|
+
|
|
3918
4230
|
private async handleReportPatternGap(args: { category: string; request: string; context?: string; handledWith?: string; wasSuccessful?: boolean }) {
|
|
3919
4231
|
const { category, request, context, handledWith, wasSuccessful = true } = args;
|
|
3920
4232
|
|