@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.
- package/dist/mcp/server.js +426 -0
- package/package.json +1 -1
- package/src/mcp/server.ts +456 -0
package/dist/mcp/server.js
CHANGED
|
@@ -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
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
|
|