@champpaba/claude-agent-kit 2.1.6 ā 2.2.1
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/.claude/CLAUDE.md +73 -7
- package/.claude/agents/_shared/pre-work-checklist.md +83 -1
- package/.claude/commands/csetup.md +990 -134
- package/.claude/contexts/patterns/validation-framework.md +51 -1
- package/.claude/lib/agent-executor.md +149 -0
- package/.claude/lib/feature-best-practices.md +386 -0
- package/README.md +30 -1
- package/package.json +1 -1
|
@@ -249,199 +249,982 @@ Continue anyway? (yes/no)
|
|
|
249
249
|
|
|
250
250
|
---
|
|
251
251
|
|
|
252
|
-
### Step 2.
|
|
252
|
+
### Step 2.6: Feature Best Practice Analysis (v2.2.0)
|
|
253
253
|
|
|
254
|
-
> **NEW:**
|
|
254
|
+
> **NEW:** Validate spec against industry standards BEFORE checking stack best practices
|
|
255
|
+
> **Reference:** `.claude/lib/feature-best-practices.md`
|
|
256
|
+
|
|
257
|
+
WHY: Stack best practices tell you "how to use React well", but Feature best practices tell you "what a good auth system needs". The feature layer is higher-level and informs whether your spec is complete.
|
|
255
258
|
|
|
256
259
|
```typescript
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
[
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
output(`\nš Analyzing Feature Best Practices...`)
|
|
261
|
+
|
|
262
|
+
// 1. Detect features from proposal/tasks/design
|
|
263
|
+
const combined = (proposalContent + ' ' + tasksContent + ' ' + designContent).toLowerCase()
|
|
264
|
+
|
|
265
|
+
const featureDetection = {
|
|
266
|
+
authentication: {
|
|
267
|
+
keywords: ['login', 'auth', 'register', 'password', 'session', 'jwt', 'token', 'oauth'],
|
|
268
|
+
tier: 1, // Blocking
|
|
269
|
+
standards: [
|
|
270
|
+
{ name: 'Short-lived access token', keywords: ['jwt', 'access', '15', '30', 'min'], priority: 'required' },
|
|
271
|
+
{ name: 'Refresh token rotation', keywords: ['refresh', 'rotation', 'rotate'], priority: 'required' },
|
|
272
|
+
{ name: 'Secure token storage', keywords: ['httponly', 'cookie', 'secure'], priority: 'required' },
|
|
273
|
+
{ name: 'Token revocation', keywords: ['revoke', 'invalidate', 'logout'], priority: 'required' },
|
|
274
|
+
{ name: 'Rate limiting', keywords: ['rate', 'limit', 'throttle', 'attempt'], priority: 'required' },
|
|
275
|
+
{ name: 'Account lockout', keywords: ['lockout', 'lock', 'failed attempt'], priority: 'recommended' }
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
payment: {
|
|
279
|
+
keywords: ['payment', 'stripe', 'checkout', 'billing', 'subscription'],
|
|
280
|
+
tier: 1,
|
|
281
|
+
standards: [
|
|
282
|
+
{ name: 'No card data on server', keywords: ['elements', 'checkout', 'client-side'], priority: 'required' },
|
|
283
|
+
{ name: 'Webhook signature verification', keywords: ['webhook', 'signature', 'verify'], priority: 'required' },
|
|
284
|
+
{ name: 'Idempotency keys', keywords: ['idempotency', 'idempotent'], priority: 'required' }
|
|
285
|
+
]
|
|
286
|
+
},
|
|
287
|
+
fileUpload: {
|
|
288
|
+
keywords: ['upload', 'file', 'image', 's3', 'storage'],
|
|
289
|
+
tier: 1,
|
|
290
|
+
standards: [
|
|
291
|
+
{ name: 'File type validation', keywords: ['mime', 'type', 'validation', 'allowed'], priority: 'required' },
|
|
292
|
+
{ name: 'File size limits', keywords: ['size', 'limit', 'max'], priority: 'required' },
|
|
293
|
+
{ name: 'Filename sanitization', keywords: ['sanitize', 'filename', 'path'], priority: 'required' }
|
|
294
|
+
]
|
|
295
|
+
},
|
|
296
|
+
apiDesign: {
|
|
297
|
+
keywords: ['api', 'endpoint', 'rest', 'graphql'],
|
|
298
|
+
tier: 2, // Warning
|
|
299
|
+
standards: [
|
|
300
|
+
{ name: 'Rate limiting', keywords: ['rate', 'limit'], priority: 'required' },
|
|
301
|
+
{ name: 'Input validation', keywords: ['validate', 'validation', 'zod', 'schema'], priority: 'required' },
|
|
302
|
+
{ name: 'Pagination', keywords: ['pagination', 'page', 'limit', 'offset'], priority: 'recommended' }
|
|
303
|
+
]
|
|
304
|
+
}
|
|
269
305
|
}
|
|
270
306
|
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
// Look for tech stack section
|
|
277
|
-
const techMatch = designContent.match(/tech.*stack|architecture|framework/gi)
|
|
278
|
-
if (techMatch) {
|
|
279
|
-
designStack = extractTechFromText(designContent)
|
|
280
|
-
output(` š From design.md: ${designStack.join(', ') || 'none'}`)
|
|
307
|
+
// 2. Find detected features
|
|
308
|
+
const detectedFeatures = []
|
|
309
|
+
for (const [featureName, config] of Object.entries(featureDetection)) {
|
|
310
|
+
if (config.keywords.some(kw => combined.includes(kw))) {
|
|
311
|
+
detectedFeatures.push({ name: featureName, ...config })
|
|
281
312
|
}
|
|
282
313
|
}
|
|
283
314
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
315
|
+
if (detectedFeatures.length > 0) {
|
|
316
|
+
output(`\nš Features Detected:`)
|
|
317
|
+
detectedFeatures.forEach(f => {
|
|
318
|
+
output(` - ${f.name} (Tier ${f.tier}: ${f.tier === 1 ? 'Blocking' : 'Warning'})`)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// 3. For each Tier 1/2 feature, check spec against standards
|
|
322
|
+
const allGaps = []
|
|
323
|
+
|
|
324
|
+
for (const feature of detectedFeatures) {
|
|
325
|
+
output(`\nš Checking ${feature.name} against industry standards...`)
|
|
326
|
+
|
|
327
|
+
const gaps = []
|
|
328
|
+
const matches = []
|
|
329
|
+
|
|
330
|
+
for (const standard of feature.standards) {
|
|
331
|
+
const isMentioned = standard.keywords.some(kw =>
|
|
332
|
+
designContent.toLowerCase().includes(kw)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if (isMentioned) {
|
|
336
|
+
matches.push(standard.name)
|
|
337
|
+
} else if (standard.priority === 'required') {
|
|
338
|
+
gaps.push({
|
|
339
|
+
feature: feature.name,
|
|
340
|
+
requirement: standard.name,
|
|
341
|
+
priority: standard.priority,
|
|
342
|
+
tier: feature.tier
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (matches.length > 0) {
|
|
348
|
+
output(` ā
Matches: ${matches.join(', ')}`)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (gaps.length > 0) {
|
|
352
|
+
output(` ā Gaps: ${gaps.map(g => g.requirement).join(', ')}`)
|
|
353
|
+
allGaps.push(...gaps)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 4. Handle gaps based on tier
|
|
358
|
+
const tier1Gaps = allGaps.filter(g => g.tier === 1)
|
|
359
|
+
const tier2Gaps = allGaps.filter(g => g.tier === 2)
|
|
360
|
+
|
|
361
|
+
if (tier1Gaps.length > 0) {
|
|
362
|
+
output(`\nā ļø Security-Critical Gaps Found (Tier 1)`)
|
|
363
|
+
output(``)
|
|
364
|
+
output(`Your spec is missing these industry-standard requirements:`)
|
|
365
|
+
output(``)
|
|
366
|
+
|
|
367
|
+
// Group by feature
|
|
368
|
+
const byFeature = {}
|
|
369
|
+
tier1Gaps.forEach(g => {
|
|
370
|
+
if (!byFeature[g.feature]) byFeature[g.feature] = []
|
|
371
|
+
byFeature[g.feature].push(g.requirement)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
for (const [feature, reqs] of Object.entries(byFeature)) {
|
|
375
|
+
output(` ${feature}:`)
|
|
376
|
+
reqs.forEach(r => output(` - ${r}`))
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
output(``)
|
|
380
|
+
output(`Options:`)
|
|
381
|
+
output(` A) Update spec - Add missing requirements to design.md`)
|
|
382
|
+
output(` B) Document skip - Record why these aren't needed (requires justification)`)
|
|
383
|
+
output(` C) Continue anyway - Proceed with security gap (not recommended)`)
|
|
384
|
+
output(``)
|
|
385
|
+
|
|
386
|
+
const decision = await askUserQuestion({
|
|
387
|
+
questions: [{
|
|
388
|
+
question: 'How would you like to handle the security gaps?',
|
|
389
|
+
header: 'Spec Gaps',
|
|
390
|
+
options: [
|
|
391
|
+
{ label: 'A) Update spec', description: 'Add missing requirements to design.md (recommended)' },
|
|
392
|
+
{ label: 'B) Document skip', description: 'Record justification for skipping' },
|
|
393
|
+
{ label: 'C) Continue anyway', description: 'Proceed with gap (security risk)' }
|
|
394
|
+
],
|
|
395
|
+
multiSelect: false
|
|
396
|
+
}]
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
if (decision.includes('A')) {
|
|
400
|
+
// Generate suggested additions
|
|
401
|
+
output(`\nš Add this to design.md:\n`)
|
|
402
|
+
output(`\`\`\`markdown`)
|
|
403
|
+
output(`### D{n}: Security Requirements (Industry Standard Alignment)`)
|
|
404
|
+
output(``)
|
|
405
|
+
output(`**Added based on industry best practices:**`)
|
|
406
|
+
output(``)
|
|
407
|
+
for (const [feature, reqs] of Object.entries(byFeature)) {
|
|
408
|
+
output(`#### ${feature}`)
|
|
409
|
+
reqs.forEach(r => output(`- ${r}`))
|
|
410
|
+
}
|
|
411
|
+
output(``)
|
|
412
|
+
output(`**Source:** Feature Best Practice Validation`)
|
|
413
|
+
output(`**Added:** ${new Date().toISOString().split('T')[0]}`)
|
|
414
|
+
output(`\`\`\``)
|
|
415
|
+
output(``)
|
|
416
|
+
output(`Please update design.md and re-run /csetup.`)
|
|
417
|
+
return
|
|
418
|
+
} else if (decision.includes('B')) {
|
|
419
|
+
// Document conscious skip
|
|
420
|
+
output(`\nš Add this to design.md to document the skip:\n`)
|
|
421
|
+
output(`\`\`\`markdown`)
|
|
422
|
+
output(`### D{n}: Conscious Security Trade-offs`)
|
|
423
|
+
output(``)
|
|
424
|
+
output(`**Skipped requirements (with justification):**`)
|
|
425
|
+
output(``)
|
|
426
|
+
output(`| Requirement | Why Skipped | Risk Level | Mitigation |`)
|
|
427
|
+
output(`|-------------|-------------|------------|------------|`)
|
|
428
|
+
for (const gap of tier1Gaps) {
|
|
429
|
+
output(`| ${gap.requirement} | [YOUR REASON] | [LOW/MED/HIGH] | [YOUR MITIGATION] |`)
|
|
430
|
+
}
|
|
431
|
+
output(``)
|
|
432
|
+
output(`**Acknowledged by:** User decision in /csetup`)
|
|
433
|
+
output(`**Date:** ${new Date().toISOString().split('T')[0]}`)
|
|
434
|
+
output(`\`\`\``)
|
|
435
|
+
output(``)
|
|
436
|
+
|
|
437
|
+
const confirm = await askUserQuestion({
|
|
438
|
+
questions: [{
|
|
439
|
+
question: 'Confirm you will document this in design.md?',
|
|
440
|
+
header: 'Confirm',
|
|
441
|
+
options: [
|
|
442
|
+
{ label: 'Yes, continue', description: 'I will add documentation' },
|
|
443
|
+
{ label: 'Cancel', description: 'Go back and update spec' }
|
|
444
|
+
],
|
|
445
|
+
multiSelect: false
|
|
446
|
+
}]
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
if (confirm.includes('Cancel')) {
|
|
450
|
+
output(`\nā Setup cancelled. Please update design.md first.`)
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// If C, just continue with warning logged
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (tier2Gaps.length > 0) {
|
|
458
|
+
output(`\nā ļø Recommendations (Tier 2 - Non-blocking):`)
|
|
459
|
+
tier2Gaps.forEach(g => {
|
|
460
|
+
output(` - ${g.feature}: ${g.requirement}`)
|
|
461
|
+
})
|
|
462
|
+
output(` Continuing with setup...`)
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
output(`\nā
No security-critical features detected`)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Store feature analysis for later use
|
|
469
|
+
const featureAnalysis = {
|
|
470
|
+
detected: detectedFeatures.map(f => f.name),
|
|
471
|
+
tier1Gaps: tier1Gaps || [],
|
|
472
|
+
tier2Gaps: tier2Gaps || [],
|
|
473
|
+
validated: true
|
|
305
474
|
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
### Step 2.7: Auto-Setup Best Practices (v2.3.0 - Dynamic Detection)
|
|
480
|
+
|
|
481
|
+
> **Zero-Maintenance Design:** Automatically detects any library/framework from spec text and resolves via Context7.
|
|
482
|
+
> **WHY:** Hardcoded mappings require constant maintenance and miss new libraries. Dynamic resolution works with any language (Python, Rust, Go, etc.) without code changes.
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// ============================================================
|
|
486
|
+
// STEP 2.7: Dynamic Tech Stack Detection & Best Practices
|
|
487
|
+
// ============================================================
|
|
488
|
+
|
|
489
|
+
output(`\nš Detecting Tech Stack (Dynamic Resolution)...`)
|
|
490
|
+
|
|
491
|
+
// 1. Gather text from ALL relevant sources
|
|
492
|
+
const textSources = {
|
|
493
|
+
proposal: Read(`openspec/changes/${changeId}/proposal.md`) || '',
|
|
494
|
+
tasks: Read(`openspec/changes/${changeId}/tasks.md`) || '',
|
|
495
|
+
design: fileExists(`openspec/changes/${changeId}/design.md`)
|
|
496
|
+
? Read(`openspec/changes/${changeId}/design.md`) : '',
|
|
497
|
+
packageJson: fileExists('package.json') ? Read('package.json') : '',
|
|
498
|
+
requirementsTxt: fileExists('requirements.txt') ? Read('requirements.txt') : '',
|
|
499
|
+
pyprojectToml: fileExists('pyproject.toml') ? Read('pyproject.toml') : '',
|
|
500
|
+
cargoToml: fileExists('Cargo.toml') ? Read('Cargo.toml') : '',
|
|
501
|
+
goMod: fileExists('go.mod') ? Read('go.mod') : '',
|
|
502
|
+
composerJson: fileExists('composer.json') ? Read('composer.json') : '',
|
|
503
|
+
gemfile: fileExists('Gemfile') ? Read('Gemfile') : ''
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const allText = Object.values(textSources).join('\n')
|
|
507
|
+
|
|
508
|
+
// 2. Extract library names using semantic analysis (TRUE zero-maintenance)
|
|
509
|
+
// WHY: Pattern-based extraction still requires maintenance.
|
|
510
|
+
// Instead, use Claude's understanding to identify libraries from context.
|
|
511
|
+
output(` š§ Analyzing text semantically...`)
|
|
512
|
+
|
|
513
|
+
const potentialLibraries = await extractLibrariesSemantically(allText)
|
|
514
|
+
|
|
515
|
+
output(` š Found ${potentialLibraries.length} potential libraries to verify`)
|
|
516
|
+
|
|
517
|
+
// 3. Resolve each potential library with Context7 (validate it's a real library)
|
|
518
|
+
const resolvedLibraries = []
|
|
519
|
+
const resolutionCache = new Map()
|
|
306
520
|
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
521
|
+
for (const candidate of potentialLibraries) {
|
|
522
|
+
if (resolutionCache.has(candidate.toLowerCase())) continue
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const result = await mcp__context7__resolve_library_id({
|
|
526
|
+
libraryName: candidate
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
const bestMatch = parseContext7Response(result, candidate)
|
|
530
|
+
|
|
531
|
+
if (bestMatch && bestMatch.score >= 60) {
|
|
532
|
+
resolvedLibraries.push({
|
|
533
|
+
name: candidate,
|
|
534
|
+
context7Id: bestMatch.id,
|
|
535
|
+
title: bestMatch.title,
|
|
536
|
+
snippets: bestMatch.snippets,
|
|
537
|
+
score: bestMatch.score
|
|
538
|
+
})
|
|
539
|
+
resolutionCache.set(candidate.toLowerCase(), bestMatch)
|
|
540
|
+
output(` ā
${candidate} ā ${bestMatch.id} (${bestMatch.snippets} snippets)`)
|
|
541
|
+
}
|
|
542
|
+
} catch (error) {
|
|
543
|
+
resolutionCache.set(candidate.toLowerCase(), null)
|
|
311
544
|
}
|
|
312
545
|
}
|
|
313
|
-
output(` š From proposal/tasks: ${proposalStack.join(', ') || 'none'}`)
|
|
314
546
|
|
|
315
|
-
|
|
316
|
-
|
|
547
|
+
output(`\nš Verified Libraries: ${resolvedLibraries.length}`)
|
|
548
|
+
resolvedLibraries.forEach(lib => {
|
|
549
|
+
output(` - ${lib.title} (${lib.context7Id})`)
|
|
550
|
+
})
|
|
317
551
|
|
|
318
|
-
//
|
|
319
|
-
if (
|
|
320
|
-
output(`\nā ļø
|
|
552
|
+
// 4. If no libraries detected, ask user for guidance
|
|
553
|
+
if (resolvedLibraries.length === 0) {
|
|
554
|
+
output(`\nā ļø No libraries auto-detected from spec files`)
|
|
321
555
|
|
|
322
556
|
const answer = await askUserQuestion({
|
|
323
557
|
questions: [{
|
|
324
|
-
question: '
|
|
325
|
-
header: '
|
|
558
|
+
question: 'Enter the main libraries/frameworks for this project (comma-separated):',
|
|
559
|
+
header: 'Libraries',
|
|
326
560
|
options: [
|
|
327
|
-
{ label: '
|
|
328
|
-
{ label: '
|
|
329
|
-
{ label: '
|
|
330
|
-
{ label: '
|
|
561
|
+
{ label: 'Skip', description: 'Continue without library-specific best practices' },
|
|
562
|
+
{ label: 'React, Next.js', description: 'Common frontend stack' },
|
|
563
|
+
{ label: 'FastAPI, SQLAlchemy, Pydantic', description: 'Python API stack' },
|
|
564
|
+
{ label: 'Express, Prisma', description: 'Node.js backend stack' }
|
|
331
565
|
],
|
|
332
|
-
multiSelect:
|
|
566
|
+
multiSelect: false
|
|
333
567
|
}]
|
|
334
568
|
})
|
|
335
569
|
|
|
336
|
-
|
|
337
|
-
|
|
570
|
+
if (!answer.includes('Skip')) {
|
|
571
|
+
// Parse user input and resolve each
|
|
572
|
+
const userLibraries = answer.split(',').map(s => s.trim()).filter(Boolean)
|
|
573
|
+
for (const lib of userLibraries) {
|
|
574
|
+
const result = await mcp__context7__resolve_library_id({ libraryName: lib })
|
|
575
|
+
const bestMatch = parseContext7Response(result, lib)
|
|
576
|
+
if (bestMatch) {
|
|
577
|
+
resolvedLibraries.push({
|
|
578
|
+
name: lib,
|
|
579
|
+
context7Id: bestMatch.id,
|
|
580
|
+
title: bestMatch.title,
|
|
581
|
+
snippets: bestMatch.snippets,
|
|
582
|
+
score: bestMatch.score
|
|
583
|
+
})
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
338
587
|
}
|
|
339
588
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
// 3. Check if best-practices already exist
|
|
589
|
+
// 5. Generate best-practices files for resolved libraries
|
|
343
590
|
const bpDir = '.claude/contexts/domain/project/best-practices/'
|
|
344
591
|
const existingBp = fileExists(bpDir) ? listFiles(bpDir) : []
|
|
345
592
|
|
|
346
|
-
|
|
347
|
-
|
|
593
|
+
// Filter to libraries that don't have best-practices yet
|
|
594
|
+
const newLibraries = resolvedLibraries.filter(lib => {
|
|
595
|
+
const safeName = lib.name.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
|
596
|
+
return !existingBp.some(f => f.toLowerCase().includes(safeName))
|
|
348
597
|
})
|
|
349
598
|
|
|
350
|
-
|
|
351
|
-
if (missingBp.length > 0) {
|
|
599
|
+
if (newLibraries.length > 0) {
|
|
352
600
|
output(`\nš Generating Best Practices from Context7...`)
|
|
353
601
|
|
|
354
|
-
// Create directory
|
|
355
|
-
if (!fileExists(
|
|
356
|
-
mkdir(
|
|
602
|
+
// Create directory if needed
|
|
603
|
+
if (!fileExists(bpDir)) {
|
|
604
|
+
mkdir(bpDir)
|
|
357
605
|
}
|
|
358
606
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
607
|
+
for (const lib of newLibraries) {
|
|
608
|
+
output(` š Fetching ${lib.title} best practices...`)
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
const docs = await mcp__context7__get_library_docs({
|
|
612
|
+
context7CompatibleLibraryID: lib.context7Id,
|
|
613
|
+
topic: 'best practices, patterns, anti-patterns, common mistakes',
|
|
614
|
+
mode: 'code'
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
const bpContent = generateBestPracticesFile(lib.title, docs, lib.context7Id)
|
|
618
|
+
const safeName = lib.name.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
|
619
|
+
Write(`${bpDir}${safeName}.md`, bpContent)
|
|
620
|
+
|
|
621
|
+
output(` ā
${safeName}.md generated`)
|
|
622
|
+
} catch (error) {
|
|
623
|
+
output(` ā ļø ${lib.title} - failed to fetch docs, skipping`)
|
|
624
|
+
}
|
|
372
625
|
}
|
|
373
626
|
|
|
374
|
-
|
|
375
|
-
|
|
627
|
+
// Generate/update index.md
|
|
628
|
+
generateBestPracticesIndex(resolvedLibraries, changeId)
|
|
629
|
+
output(` ā
index.md updated`)
|
|
376
630
|
|
|
377
|
-
|
|
378
|
-
|
|
631
|
+
output(`\nā
Best Practices Setup Complete!`)
|
|
632
|
+
output(` New files: ${newLibraries.length}`)
|
|
633
|
+
output(` Location: ${bpDir}`)
|
|
634
|
+
} else if (resolvedLibraries.length > 0) {
|
|
635
|
+
output(`\nā
Best Practices: Already configured for detected libraries`)
|
|
636
|
+
} else {
|
|
637
|
+
output(`\nā
Best Practices: Skipped (no libraries to configure)`)
|
|
638
|
+
}
|
|
379
639
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
640
|
+
// 6. Store resolved stack for context.md generation
|
|
641
|
+
const stackForContext = {
|
|
642
|
+
detected: resolvedLibraries.map(l => l.name),
|
|
643
|
+
resolved: resolvedLibraries,
|
|
644
|
+
bestPracticesPath: bpDir,
|
|
645
|
+
files: existingBp.concat(newLibraries.map(l => `${l.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}.md`))
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
### Helper: extractLibrariesSemantically() - PRIMARY
|
|
652
|
+
|
|
653
|
+
> **TRUE Zero-Maintenance:** Uses Claude's semantic understanding to extract library names from any text.
|
|
654
|
+
> **WHY:** Pattern-based regex always has edge cases. Claude understands context and meaning.
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
async function extractLibrariesSemantically(text: string): Promise<string[]> {
|
|
658
|
+
// Truncate text if too long (keep first 8000 chars for efficiency)
|
|
659
|
+
const truncatedText = text.length > 8000 ? text.slice(0, 8000) + '\n...[truncated]' : text
|
|
660
|
+
|
|
661
|
+
// Use Claude to semantically extract library/framework names
|
|
662
|
+
// This is the MAIN Claude running /csetup, so we just analyze inline
|
|
663
|
+
const prompt = `
|
|
664
|
+
Analyze this text and extract ALL programming libraries, frameworks, and tools mentioned.
|
|
665
|
+
|
|
666
|
+
TEXT:
|
|
667
|
+
${truncatedText}
|
|
668
|
+
|
|
669
|
+
INSTRUCTIONS:
|
|
670
|
+
1. Look for any technology names (libraries, frameworks, ORMs, databases, tools)
|
|
671
|
+
2. Include both explicit mentions ("using Mastra") and implicit ones ("PostgreSQL database")
|
|
672
|
+
3. Return ONLY the library/framework names, one per line
|
|
673
|
+
4. Do NOT include generic terms like "API", "REST", "database" (unless it's a specific product name)
|
|
674
|
+
5. Do NOT include programming languages (Python, JavaScript, etc.) unless they're library names
|
|
675
|
+
6. Include package names from any language (npm, pip, cargo, go, etc.)
|
|
676
|
+
|
|
677
|
+
OUTPUT FORMAT (one name per line, nothing else):
|
|
678
|
+
`
|
|
679
|
+
|
|
680
|
+
// Since this runs within Main Claude context, we analyze inline
|
|
681
|
+
// The output parsing happens in the calling code
|
|
682
|
+
// For now, return a simple extraction that Main Claude will process
|
|
683
|
+
|
|
684
|
+
// Fallback: Use pattern-based extraction as baseline
|
|
685
|
+
// Main Claude will enhance this with semantic understanding
|
|
686
|
+
const patternBased = extractPotentialLibraryNames(text)
|
|
687
|
+
|
|
688
|
+
// Main Claude: When you execute this, also semantically analyze the text
|
|
689
|
+
// and add any libraries you identify from context that patterns missed.
|
|
690
|
+
// Example: "The system uses Mastra for orchestration" ā add "Mastra"
|
|
691
|
+
|
|
692
|
+
return patternBased
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**Execution Note for Main Claude:**
|
|
386
697
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
698
|
+
When running `/csetup`, after calling `extractLibrariesSemantically()`:
|
|
699
|
+
1. Review the spec text yourself (proposal.md, design.md, tasks.md)
|
|
700
|
+
2. Identify any library/framework names the patterns might have missed
|
|
701
|
+
3. Add them to the `potentialLibraries` array before Context7 validation
|
|
390
702
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
703
|
+
This hybrid approach ensures:
|
|
704
|
+
- Pattern extraction catches obvious cases quickly
|
|
705
|
+
- Claude's semantic understanding catches edge cases
|
|
706
|
+
- Context7 validates everything (filters out false positives)
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
710
|
+
### Helper: extractPotentialLibraryNames() - FALLBACK
|
|
711
|
+
|
|
712
|
+
> **Pattern-based fallback:** Provides baseline extraction. Main Claude enhances with semantic analysis.
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
function extractPotentialLibraryNames(text: string): string[] {
|
|
716
|
+
const candidates = new Set<string>()
|
|
717
|
+
|
|
718
|
+
// === Pattern 1: Package file dependencies ===
|
|
719
|
+
// package.json: "react": "^18.0.0" ā react
|
|
720
|
+
const npmDeps = text.match(/"([a-z@][a-z0-9._/-]*)"\s*:\s*"[\^~]?\d/gi) || []
|
|
721
|
+
npmDeps.forEach(m => {
|
|
722
|
+
const match = m.match(/"([^"]+)"/)
|
|
723
|
+
if (match) candidates.add(match[1].replace(/^@[^/]+\//, '')) // Strip scope
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
// requirements.txt: sqlalchemy==2.0.0 ā sqlalchemy
|
|
727
|
+
const pyDeps = text.match(/^([a-zA-Z][a-zA-Z0-9_-]*)\s*[=<>~!]/gm) || []
|
|
728
|
+
pyDeps.forEach(m => {
|
|
729
|
+
const match = m.match(/^([a-zA-Z][a-zA-Z0-9_-]*)/)
|
|
730
|
+
if (match) candidates.add(match[1])
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
// Cargo.toml: tokio = "1.0" ā tokio
|
|
734
|
+
const rustDeps = text.match(/^([a-z][a-z0-9_-]*)\s*=/gm) || []
|
|
735
|
+
rustDeps.forEach(m => {
|
|
736
|
+
const match = m.match(/^([a-z][a-z0-9_-]*)/)
|
|
737
|
+
if (match) candidates.add(match[1])
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
// go.mod: require github.com/gin-gonic/gin ā gin
|
|
741
|
+
const goDeps = text.match(/(?:require\s+)?github\.com\/[^/\s]+\/([a-z][a-z0-9_-]*)/gi) || []
|
|
742
|
+
goDeps.forEach(m => {
|
|
743
|
+
const match = m.match(/\/([a-z][a-z0-9_-]*)$/i)
|
|
744
|
+
if (match) candidates.add(match[1])
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
// === Pattern 2: Import statements ===
|
|
748
|
+
// Python: from sqlalchemy import, import pydantic
|
|
749
|
+
const pyImports = text.match(/(?:from|import)\s+([a-zA-Z][a-zA-Z0-9_]*)/g) || []
|
|
750
|
+
pyImports.forEach(m => {
|
|
751
|
+
const match = m.match(/(?:from|import)\s+([a-zA-Z][a-zA-Z0-9_]*)/)
|
|
752
|
+
if (match) candidates.add(match[1])
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
// JS/TS: import X from 'Y', require('Y')
|
|
756
|
+
const jsImports = text.match(/(?:from|require\s*\(\s*)['"]([a-zA-Z@][a-zA-Z0-9._/-]*)['"]/g) || []
|
|
757
|
+
jsImports.forEach(m => {
|
|
758
|
+
const match = m.match(/['"]([^'"]+)['"]/)
|
|
759
|
+
if (match) {
|
|
760
|
+
const pkg = match[1].replace(/^@[^/]+\//, '').split('/')[0]
|
|
761
|
+
candidates.add(pkg)
|
|
762
|
+
}
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
// Rust: use tokio::, extern crate serde
|
|
766
|
+
const rustImports = text.match(/(?:use|extern\s+crate)\s+([a-z][a-z0-9_]*)/g) || []
|
|
767
|
+
rustImports.forEach(m => {
|
|
768
|
+
const match = m.match(/(?:use|extern\s+crate)\s+([a-z][a-z0-9_]*)/)
|
|
769
|
+
if (match) candidates.add(match[1])
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
// === Pattern 3: Tech mentions in prose ===
|
|
773
|
+
// "using FastAPI", "with Prisma", "powered by Mastra"
|
|
774
|
+
const techMentions = text.match(/(?:using|with|via|built with|powered by)\s+([A-Z][a-zA-Z0-9.]*)/gi) || []
|
|
775
|
+
techMentions.forEach(m => {
|
|
776
|
+
const match = m.match(/\s([A-Z][a-zA-Z0-9.]*)$/i)
|
|
777
|
+
if (match) candidates.add(match[1])
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
// CamelCase words (FastAPI, SQLAlchemy, NextAuth)
|
|
781
|
+
const camelCase = text.match(/\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g) || []
|
|
782
|
+
camelCase.forEach(w => candidates.add(w))
|
|
783
|
+
|
|
784
|
+
// === Pattern 3.5: PascalCase single words after tech keywords ===
|
|
785
|
+
// "Framework: Mastra", "ORM: Prisma", "Database: PostgreSQL"
|
|
786
|
+
// WHY: Many library names are single PascalCase words (Mastra, Prisma, Django, Flask)
|
|
787
|
+
const techKeywordPatterns = [
|
|
788
|
+
/(?:framework|library|orm|database|db|backend|frontend|ui|css|styling)[:\s]+([A-Z][a-z]+\w*)/gi,
|
|
789
|
+
/(?:built\s+with|powered\s+by|using|via|with)\s+([A-Z][a-z]+\w*)/gi,
|
|
790
|
+
/\*\*(?:framework|library|orm|database|backend|frontend)\*\*[:\s]+([A-Z][a-z]+\w*)/gi
|
|
791
|
+
]
|
|
792
|
+
techKeywordPatterns.forEach(pattern => {
|
|
793
|
+
const matches = text.matchAll(pattern)
|
|
794
|
+
for (const match of matches) {
|
|
795
|
+
if (match[1]) candidates.add(match[1])
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
// === Pattern 3.6: Standalone PascalCase in markdown lists ===
|
|
800
|
+
// "- Mastra for AI agents", "- PostgreSQL database", "* React frontend"
|
|
801
|
+
const mdListItems = text.match(/^[\s]*[-*]\s+([A-Z][a-z]+\w*)(?:\s|$)/gm) || []
|
|
802
|
+
mdListItems.forEach(m => {
|
|
803
|
+
const match = m.match(/[-*]\s+([A-Z][a-z]+\w*)/)
|
|
804
|
+
if (match) candidates.add(match[1])
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
// === Pattern 3.7: Words after "for" in tech context ===
|
|
808
|
+
// "Mastra for AI orchestration", "Drizzle for database"
|
|
809
|
+
const forPattern = text.match(/([A-Z][a-z]+\w*)\s+for\s+(?:ai|database|backend|frontend|api|web|mobile|server|client)/gi) || []
|
|
810
|
+
forPattern.forEach(m => {
|
|
811
|
+
const match = m.match(/^([A-Z][a-z]+\w*)/)
|
|
812
|
+
if (match) candidates.add(match[1])
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
// Known framework patterns: Next.js, Vue.js, Express.js
|
|
816
|
+
const dotJs = text.match(/\b([A-Z][a-z]+)\.js\b/gi) || []
|
|
817
|
+
dotJs.forEach(m => {
|
|
818
|
+
const match = m.match(/([A-Z][a-z]+)/i)
|
|
819
|
+
if (match) candidates.add(match[1])
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
// === Pattern 4: Explicit tech stack sections ===
|
|
823
|
+
// "Tech Stack:", "Technologies:", "Built with:"
|
|
824
|
+
const techSection = text.match(/(?:tech\s*stack|technologies|built\s*with|dependencies)[:\s]+([^\n]+)/gi) || []
|
|
825
|
+
techSection.forEach(section => {
|
|
826
|
+
const items = section.split(/[,\s]+/)
|
|
827
|
+
items.forEach(item => {
|
|
828
|
+
const cleaned = item.replace(/[^a-zA-Z0-9.-]/g, '')
|
|
829
|
+
if (cleaned.length > 2) candidates.add(cleaned)
|
|
830
|
+
})
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
// === Pattern 4.5: Markdown bold/code tech mentions ===
|
|
834
|
+
// "**Mastra**", "`prisma`", "**Framework:** Mastra"
|
|
835
|
+
const boldWords = text.match(/\*\*([A-Z][a-z]+\w*)\*\*/g) || []
|
|
836
|
+
boldWords.forEach(m => {
|
|
837
|
+
const match = m.match(/\*\*([A-Z][a-z]+\w*)\*\*/)
|
|
838
|
+
if (match) candidates.add(match[1])
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
const codeWords = text.match(/`([a-zA-Z][a-zA-Z0-9_-]+)`/g) || []
|
|
842
|
+
codeWords.forEach(m => {
|
|
843
|
+
const match = m.match(/`([a-zA-Z][a-zA-Z0-9_-]+)`/)
|
|
844
|
+
if (match && match[1].length > 2) candidates.add(match[1])
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
// === Filter out noise ===
|
|
848
|
+
const stopWords = new Set([
|
|
849
|
+
// Common English words
|
|
850
|
+
'The', 'This', 'That', 'With', 'From', 'Using', 'For', 'And', 'But', 'Not',
|
|
851
|
+
'All', 'Any', 'Can', 'Could', 'Should', 'Would', 'Will', 'May', 'Might',
|
|
852
|
+
'Each', 'Every', 'Some', 'Many', 'Most', 'Other', 'Such', 'Only', 'Just',
|
|
853
|
+
'Also', 'Well', 'Back', 'Even', 'Still', 'Already', 'Always', 'Never',
|
|
854
|
+
// Common programming terms that aren't libraries
|
|
855
|
+
'API', 'REST', 'HTTP', 'HTTPS', 'JSON', 'XML', 'HTML', 'CSS', 'SQL',
|
|
856
|
+
'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'URL', 'URI', 'UUID', 'ID',
|
|
857
|
+
'True', 'False', 'None', 'Null', 'Undefined', 'Error', 'Exception',
|
|
858
|
+
'Class', 'Function', 'Method', 'Object', 'Array', 'String', 'Number',
|
|
859
|
+
'Boolean', 'Int', 'Float', 'Double', 'Char', 'Byte', 'Long', 'Short',
|
|
860
|
+
'Public', 'Private', 'Protected', 'Static', 'Final', 'Const', 'Let', 'Var',
|
|
861
|
+
'Import', 'Export', 'Module', 'Package', 'Interface', 'Type', 'Enum',
|
|
862
|
+
'Test', 'Tests', 'Spec', 'Specs', 'Mock', 'Stub', 'Fake', 'Spy',
|
|
863
|
+
'Config', 'Configuration', 'Settings', 'Options', 'Params', 'Args',
|
|
864
|
+
'User', 'Users', 'Admin', 'Auth', 'Login', 'Logout', 'Session', 'Token',
|
|
865
|
+
'Data', 'Database', 'Table', 'Column', 'Row', 'Index', 'Key', 'Value',
|
|
866
|
+
'File', 'Files', 'Path', 'Dir', 'Directory', 'Folder', 'Name', 'Size',
|
|
867
|
+
'Create', 'Read', 'Update', 'Delete', 'List', 'Get', 'Set', 'Add', 'Remove',
|
|
868
|
+
'Start', 'Stop', 'Run', 'Build', 'Deploy', 'Install', 'Setup', 'Init',
|
|
869
|
+
// Version/date patterns
|
|
870
|
+
'Version', 'Release', 'Beta', 'Alpha', 'Stable', 'Latest', 'Current'
|
|
871
|
+
])
|
|
872
|
+
|
|
873
|
+
return [...candidates]
|
|
874
|
+
.filter(w => w.length > 2 && w.length < 30)
|
|
875
|
+
.filter(w => !stopWords.has(w))
|
|
876
|
+
.filter(w => !/^\d+$/.test(w)) // Not pure numbers
|
|
877
|
+
.filter(w => !/^v?\d+\.\d+/.test(w)) // Not version numbers
|
|
878
|
+
.slice(0, 50) // Limit to avoid too many API calls
|
|
879
|
+
}
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
---
|
|
883
|
+
|
|
884
|
+
### Helper: parseContext7Response()
|
|
885
|
+
|
|
886
|
+
> **WHY:** Context7 returns multiple matches. Select the best one based on relevance score and snippet count.
|
|
887
|
+
|
|
888
|
+
```typescript
|
|
889
|
+
function parseContext7Response(response: string, searchTerm: string): {
|
|
890
|
+
id: string
|
|
891
|
+
title: string
|
|
892
|
+
snippets: number
|
|
893
|
+
score: number
|
|
894
|
+
} | null {
|
|
895
|
+
// Parse the Context7 response text to extract library info
|
|
896
|
+
// Response format includes lines like:
|
|
897
|
+
// - Title: SQLAlchemy
|
|
898
|
+
// - Context7-compatible library ID: /sqlalchemy/sqlalchemy
|
|
899
|
+
// - Code Snippets: 2830
|
|
900
|
+
// - Benchmark Score: 84.4
|
|
901
|
+
|
|
902
|
+
const libraries = []
|
|
903
|
+
const blocks = response.split('----------').filter(b => b.trim())
|
|
904
|
+
|
|
905
|
+
for (const block of blocks) {
|
|
906
|
+
const titleMatch = block.match(/Title:\s*(.+)/i)
|
|
907
|
+
const idMatch = block.match(/Context7-compatible library ID:\s*(\S+)/i)
|
|
908
|
+
const snippetsMatch = block.match(/Code Snippets:\s*(\d+)/i)
|
|
909
|
+
const scoreMatch = block.match(/Benchmark Score:\s*([\d.]+)/i)
|
|
910
|
+
|
|
911
|
+
if (titleMatch && idMatch) {
|
|
912
|
+
libraries.push({
|
|
913
|
+
title: titleMatch[1].trim(),
|
|
914
|
+
id: idMatch[1].trim(),
|
|
915
|
+
snippets: snippetsMatch ? parseInt(snippetsMatch[1]) : 0,
|
|
916
|
+
score: scoreMatch ? parseFloat(scoreMatch[1]) : 50
|
|
917
|
+
})
|
|
394
918
|
}
|
|
395
919
|
}
|
|
396
920
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
921
|
+
if (libraries.length === 0) return null
|
|
922
|
+
|
|
923
|
+
// Prefer exact title match, then highest score with good snippet count
|
|
924
|
+
const searchLower = searchTerm.toLowerCase()
|
|
925
|
+
|
|
926
|
+
// First: exact match
|
|
927
|
+
const exactMatch = libraries.find(l =>
|
|
928
|
+
l.title.toLowerCase() === searchLower ||
|
|
929
|
+
l.id.toLowerCase().includes(searchLower)
|
|
930
|
+
)
|
|
931
|
+
if (exactMatch) return exactMatch
|
|
932
|
+
|
|
933
|
+
// Second: partial match with good score
|
|
934
|
+
const partialMatches = libraries.filter(l =>
|
|
935
|
+
l.title.toLowerCase().includes(searchLower) ||
|
|
936
|
+
searchLower.includes(l.title.toLowerCase())
|
|
937
|
+
)
|
|
938
|
+
if (partialMatches.length > 0) {
|
|
939
|
+
return partialMatches.sort((a, b) => b.score - a.score)[0]
|
|
940
|
+
}
|
|
400
941
|
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
942
|
+
// Third: best overall score (only if snippets > 100 for quality)
|
|
943
|
+
const qualityLibs = libraries.filter(l => l.snippets > 100)
|
|
944
|
+
if (qualityLibs.length > 0) {
|
|
945
|
+
return qualityLibs.sort((a, b) => b.score - a.score)[0]
|
|
405
946
|
}
|
|
406
947
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
948
|
+
// Fallback: first result
|
|
949
|
+
return libraries[0]
|
|
950
|
+
}
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
---
|
|
954
|
+
|
|
955
|
+
### Step 2.8: Library Capability Validation (v2.2.0)
|
|
956
|
+
|
|
957
|
+
> **NEW:** Verify chosen libraries support ALL spec requirements before proceeding
|
|
958
|
+
> **WHY:** Prevents spec drift - discovering during implementation that library doesn't support requirements
|
|
959
|
+
|
|
960
|
+
```typescript
|
|
961
|
+
output(`\nš Validating Library Capabilities...`)
|
|
962
|
+
|
|
963
|
+
// 1. Extract spec requirements from design.md
|
|
964
|
+
const designPath = `openspec/changes/${changeId}/design.md`
|
|
965
|
+
if (!fileExists(designPath)) {
|
|
966
|
+
output(` ā ļø No design.md found - skipping library validation`)
|
|
410
967
|
} else {
|
|
411
|
-
|
|
968
|
+
const designContent = Read(designPath)
|
|
969
|
+
|
|
970
|
+
// 2. Find library mentions in spec
|
|
971
|
+
const libraryPatterns = {
|
|
972
|
+
'better-auth': {
|
|
973
|
+
patterns: ['better-auth', 'betterauth'],
|
|
974
|
+
context7Id: null, // No Context7 mapping yet
|
|
975
|
+
knownLimitations: [
|
|
976
|
+
{ feature: 'refresh token rotation', supported: false },
|
|
977
|
+
{ feature: 'redis session storage', supported: false },
|
|
978
|
+
{ feature: 'jwt plugin', supported: true },
|
|
979
|
+
{ feature: 'bearer plugin', supported: true },
|
|
980
|
+
{ feature: 'session-based auth', supported: true }
|
|
981
|
+
]
|
|
982
|
+
},
|
|
983
|
+
'nextauth': {
|
|
984
|
+
patterns: ['next-auth', 'nextauth', 'authjs'],
|
|
985
|
+
context7Id: '/nextauthjs/next-auth',
|
|
986
|
+
knownLimitations: []
|
|
987
|
+
},
|
|
988
|
+
'lucia': {
|
|
989
|
+
patterns: ['lucia', 'lucia-auth'],
|
|
990
|
+
context7Id: '/lucia-auth/lucia',
|
|
991
|
+
knownLimitations: []
|
|
992
|
+
},
|
|
993
|
+
'prisma': {
|
|
994
|
+
patterns: ['prisma'],
|
|
995
|
+
context7Id: '/prisma/prisma',
|
|
996
|
+
knownLimitations: []
|
|
997
|
+
},
|
|
998
|
+
'drizzle': {
|
|
999
|
+
patterns: ['drizzle'],
|
|
1000
|
+
context7Id: '/drizzle-team/drizzle-orm',
|
|
1001
|
+
knownLimitations: []
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// 3. Detect which libraries are mentioned
|
|
1006
|
+
const detectedLibraries = []
|
|
1007
|
+
for (const [libName, config] of Object.entries(libraryPatterns)) {
|
|
1008
|
+
if (config.patterns.some(p => designContent.toLowerCase().includes(p))) {
|
|
1009
|
+
detectedLibraries.push({ name: libName, ...config })
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (detectedLibraries.length > 0) {
|
|
1014
|
+
output(`\nš Libraries in Spec:`)
|
|
1015
|
+
detectedLibraries.forEach(lib => output(` - ${lib.name}`))
|
|
1016
|
+
|
|
1017
|
+
// 4. Extract requirements from design.md
|
|
1018
|
+
// Look for patterns like: "JWT 15min", "refresh token", "rotation"
|
|
1019
|
+
const requirementPatterns = [
|
|
1020
|
+
{ name: 'JWT access token', pattern: /jwt.*(?:access|token).*(\d+\s*min)/i },
|
|
1021
|
+
{ name: 'Refresh token', pattern: /refresh\s*token/i },
|
|
1022
|
+
{ name: 'Token rotation', pattern: /(?:token\s*)?rotation|rotate/i },
|
|
1023
|
+
{ name: 'Redis session', pattern: /redis.*session|session.*redis/i },
|
|
1024
|
+
{ name: 'Bearer token', pattern: /bearer\s*(?:token|auth)/i },
|
|
1025
|
+
{ name: 'OAuth providers', pattern: /oauth|google|github|social\s*login/i },
|
|
1026
|
+
{ name: 'Rate limiting', pattern: /rate\s*limit/i },
|
|
1027
|
+
{ name: 'Account lockout', pattern: /lockout|lock\s*account/i }
|
|
1028
|
+
]
|
|
1029
|
+
|
|
1030
|
+
const specRequirements = []
|
|
1031
|
+
for (const rp of requirementPatterns) {
|
|
1032
|
+
if (rp.pattern.test(designContent)) {
|
|
1033
|
+
specRequirements.push(rp.name)
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (specRequirements.length > 0) {
|
|
1038
|
+
output(`\nš Spec Requirements Found:`)
|
|
1039
|
+
specRequirements.forEach(r => output(` - ${r}`))
|
|
1040
|
+
|
|
1041
|
+
// 5. Check each library's capability
|
|
1042
|
+
const capabilityGaps = []
|
|
1043
|
+
|
|
1044
|
+
for (const lib of detectedLibraries) {
|
|
1045
|
+
output(`\nš Checking ${lib.name} capabilities...`)
|
|
1046
|
+
|
|
1047
|
+
for (const req of specRequirements) {
|
|
1048
|
+
// Check known limitations first
|
|
1049
|
+
const known = lib.knownLimitations.find(l =>
|
|
1050
|
+
req.toLowerCase().includes(l.feature.toLowerCase()) ||
|
|
1051
|
+
l.feature.toLowerCase().includes(req.toLowerCase())
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
if (known && !known.supported) {
|
|
1055
|
+
output(` ā ${req} - NOT SUPPORTED`)
|
|
1056
|
+
capabilityGaps.push({
|
|
1057
|
+
library: lib.name,
|
|
1058
|
+
requirement: req,
|
|
1059
|
+
supported: false,
|
|
1060
|
+
note: `${lib.name} does not have built-in support for ${req}`
|
|
1061
|
+
})
|
|
1062
|
+
} else if (known && known.supported) {
|
|
1063
|
+
output(` ā
${req} - Supported`)
|
|
1064
|
+
} else {
|
|
1065
|
+
// Unknown - query Context7 if available
|
|
1066
|
+
if (lib.context7Id) {
|
|
1067
|
+
output(` š ${req} - Checking Context7...`)
|
|
1068
|
+
// Note: In actual implementation, this would call Context7
|
|
1069
|
+
// For now, mark as unknown
|
|
1070
|
+
output(` ā ļø ${req} - Verify manually`)
|
|
1071
|
+
} else {
|
|
1072
|
+
output(` ā ļø ${req} - Verify manually (no Context7 mapping)`)
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// 6. Report gaps if any
|
|
1079
|
+
if (capabilityGaps.length > 0) {
|
|
1080
|
+
output(`\nā ļø Library Capability Gaps Detected!`)
|
|
1081
|
+
output(``)
|
|
1082
|
+
output(`The following spec requirements are NOT supported by chosen libraries:`)
|
|
1083
|
+
output(``)
|
|
1084
|
+
|
|
1085
|
+
// Group by library
|
|
1086
|
+
const byLibrary = {}
|
|
1087
|
+
capabilityGaps.forEach(g => {
|
|
1088
|
+
if (!byLibrary[g.library]) byLibrary[g.library] = []
|
|
1089
|
+
byLibrary[g.library].push(g.requirement)
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
for (const [library, reqs] of Object.entries(byLibrary)) {
|
|
1093
|
+
output(` ${library}:`)
|
|
1094
|
+
reqs.forEach(r => output(` - ${r}`))
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
output(``)
|
|
1098
|
+
output(`This will cause spec drift during implementation!`)
|
|
1099
|
+
output(``)
|
|
1100
|
+
output(`Options:`)
|
|
1101
|
+
output(` A) Change library - Use a library that supports these features`)
|
|
1102
|
+
output(` B) Downgrade spec - Remove unsupported requirements (must document trade-off)`)
|
|
1103
|
+
output(` C) Custom implementation - Build missing features on top of library`)
|
|
1104
|
+
output(` D) Continue anyway - Proceed and let agent handle at implementation time`)
|
|
1105
|
+
output(``)
|
|
1106
|
+
|
|
1107
|
+
const decision = await askUserQuestion({
|
|
1108
|
+
questions: [{
|
|
1109
|
+
question: 'How would you like to handle the capability gaps?',
|
|
1110
|
+
header: 'Lib Gaps',
|
|
1111
|
+
options: [
|
|
1112
|
+
{ label: 'A) Change library', description: 'Switch to a library that supports requirements' },
|
|
1113
|
+
{ label: 'B) Downgrade spec', description: 'Update design.md to use what library supports' },
|
|
1114
|
+
{ label: 'C) Custom implementation', description: 'Build on top of library (more work)' },
|
|
1115
|
+
{ label: 'D) Continue anyway', description: 'Let agent handle during implementation' }
|
|
1116
|
+
],
|
|
1117
|
+
multiSelect: false
|
|
1118
|
+
}]
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
if (decision.includes('A')) {
|
|
1122
|
+
output(`\nš Suggested alternative libraries:`)
|
|
1123
|
+
for (const [library, reqs] of Object.entries(byLibrary)) {
|
|
1124
|
+
if (library === 'better-auth') {
|
|
1125
|
+
output(` Instead of ${library}, consider:`)
|
|
1126
|
+
output(` - lucia-auth (supports custom session storage)`)
|
|
1127
|
+
output(` - NextAuth.js (supports refresh token rotation with JWT strategy)`)
|
|
1128
|
+
output(` - Custom implementation with jose + Redis`)
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
output(``)
|
|
1132
|
+
output(`Please update design.md with new library choice and re-run /csetup.`)
|
|
1133
|
+
return
|
|
1134
|
+
} else if (decision.includes('B')) {
|
|
1135
|
+
output(`\nš Update design.md to remove unsupported requirements:`)
|
|
1136
|
+
output(``)
|
|
1137
|
+
output(`\`\`\`markdown`)
|
|
1138
|
+
output(`### D{n}: Library Capability Alignment`)
|
|
1139
|
+
output(``)
|
|
1140
|
+
output(`**Changed requirements to match ${Object.keys(byLibrary).join(', ')} capabilities:**`)
|
|
1141
|
+
output(``)
|
|
1142
|
+
for (const gap of capabilityGaps) {
|
|
1143
|
+
output(`- ~~${gap.requirement}~~ ā Use ${gap.library}'s default approach instead`)
|
|
1144
|
+
}
|
|
1145
|
+
output(``)
|
|
1146
|
+
output(`**Reason:** Library limitation`)
|
|
1147
|
+
output(`**Trade-off:** ${capabilityGaps.map(g => g.requirement).join(', ')} not available`)
|
|
1148
|
+
output(`**Date:** ${new Date().toISOString().split('T')[0]}`)
|
|
1149
|
+
output(`\`\`\``)
|
|
1150
|
+
output(``)
|
|
1151
|
+
output(`Please update design.md and re-run /csetup.`)
|
|
1152
|
+
return
|
|
1153
|
+
} else if (decision.includes('C')) {
|
|
1154
|
+
output(`\nš Custom implementation notes for agents:`)
|
|
1155
|
+
output(``)
|
|
1156
|
+
output(`Add to context.md:`)
|
|
1157
|
+
output(`\`\`\`markdown`)
|
|
1158
|
+
output(`## Custom Implementation Required`)
|
|
1159
|
+
output(``)
|
|
1160
|
+
output(`The following features need custom implementation:`)
|
|
1161
|
+
for (const gap of capabilityGaps) {
|
|
1162
|
+
output(`- ${gap.requirement} (not supported by ${gap.library})`)
|
|
1163
|
+
}
|
|
1164
|
+
output(``)
|
|
1165
|
+
output(`Agents should implement these on top of the base library.`)
|
|
1166
|
+
output(`\`\`\``)
|
|
1167
|
+
|
|
1168
|
+
// Store for context.md generation
|
|
1169
|
+
customImplementationRequired = capabilityGaps
|
|
1170
|
+
}
|
|
1171
|
+
// If D, continue with gaps logged for agent awareness
|
|
1172
|
+
} else {
|
|
1173
|
+
output(`\nā
All spec requirements supported by chosen libraries`)
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
} else {
|
|
1177
|
+
output(` ā¹ļø No specific libraries detected in spec`)
|
|
1178
|
+
}
|
|
412
1179
|
}
|
|
413
1180
|
|
|
414
|
-
//
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1181
|
+
// Store capability analysis
|
|
1182
|
+
const capabilityAnalysis = {
|
|
1183
|
+
libraries: detectedLibraries || [],
|
|
1184
|
+
requirements: specRequirements || [],
|
|
1185
|
+
gaps: capabilityGaps || [],
|
|
1186
|
+
customRequired: customImplementationRequired || []
|
|
419
1187
|
}
|
|
420
1188
|
```
|
|
421
1189
|
|
|
422
1190
|
**Helper: generateBestPracticesFile()**
|
|
1191
|
+
|
|
1192
|
+
> **Updated v2.3.0:** Now includes Context7 library ID for reference and refresh capability.
|
|
1193
|
+
|
|
423
1194
|
```typescript
|
|
424
|
-
function generateBestPracticesFile(
|
|
1195
|
+
function generateBestPracticesFile(
|
|
1196
|
+
tech: string,
|
|
1197
|
+
context7Docs: string,
|
|
1198
|
+
context7Id: string
|
|
1199
|
+
): string {
|
|
425
1200
|
return `# ${tech} Best Practices
|
|
426
1201
|
|
|
427
1202
|
> **Source:** Context7 MCP
|
|
1203
|
+
> **Library ID:** \`${context7Id}\`
|
|
428
1204
|
> **Generated:** ${new Date().toISOString().split('T')[0]}
|
|
1205
|
+
> **Refresh:** Query Context7 with the Library ID above to update this file
|
|
429
1206
|
|
|
430
1207
|
---
|
|
431
1208
|
|
|
432
1209
|
## Best Practices
|
|
433
1210
|
|
|
434
|
-
${
|
|
1211
|
+
${extractBestPractices(context7Docs)}
|
|
435
1212
|
|
|
436
1213
|
---
|
|
437
1214
|
|
|
438
1215
|
## Anti-Patterns to Avoid
|
|
439
1216
|
|
|
440
|
-
${
|
|
1217
|
+
${extractAntiPatterns(context7Docs)}
|
|
1218
|
+
|
|
1219
|
+
---
|
|
1220
|
+
|
|
1221
|
+
## Code Examples
|
|
1222
|
+
|
|
1223
|
+
${extractCodeExamples(context7Docs)}
|
|
441
1224
|
|
|
442
1225
|
---
|
|
443
1226
|
|
|
444
|
-
##
|
|
1227
|
+
## Quick Checklist
|
|
445
1228
|
|
|
446
1229
|
Before committing ${tech} code:
|
|
447
1230
|
${extractChecklist(context7Docs)}
|
|
@@ -451,6 +1234,81 @@ ${extractChecklist(context7Docs)}
|
|
|
451
1234
|
**Agents read this file in STEP 0 before implementation.**
|
|
452
1235
|
`
|
|
453
1236
|
}
|
|
1237
|
+
|
|
1238
|
+
// Helper: Extract best practices from Context7 docs
|
|
1239
|
+
function extractBestPractices(docs: string): string {
|
|
1240
|
+
// Look for sections about best practices, recommendations, patterns
|
|
1241
|
+
const patterns = [
|
|
1242
|
+
/best\s*practices?[:\s]+([^#]+?)(?=##|$)/gi,
|
|
1243
|
+
/recommend(?:ed|ations)?[:\s]+([^#]+?)(?=##|$)/gi,
|
|
1244
|
+
/(?:do|should)[:\s]+([^#]+?)(?=##|$)/gi
|
|
1245
|
+
]
|
|
1246
|
+
|
|
1247
|
+
let extracted = ''
|
|
1248
|
+
for (const pattern of patterns) {
|
|
1249
|
+
const matches = docs.match(pattern)
|
|
1250
|
+
if (matches) {
|
|
1251
|
+
extracted += matches.join('\n\n')
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return extracted || docs.slice(0, 2000) // Fallback to first 2000 chars
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Helper: Extract anti-patterns from Context7 docs
|
|
1259
|
+
function extractAntiPatterns(docs: string): string {
|
|
1260
|
+
const patterns = [
|
|
1261
|
+
/anti-?patterns?[:\s]+([^#]+?)(?=##|$)/gi,
|
|
1262
|
+
/avoid[:\s]+([^#]+?)(?=##|$)/gi,
|
|
1263
|
+
/(?:don'?t|should\s*not)[:\s]+([^#]+?)(?=##|$)/gi,
|
|
1264
|
+
/common\s*mistakes?[:\s]+([^#]+?)(?=##|$)/gi
|
|
1265
|
+
]
|
|
1266
|
+
|
|
1267
|
+
let extracted = ''
|
|
1268
|
+
for (const pattern of patterns) {
|
|
1269
|
+
const matches = docs.match(pattern)
|
|
1270
|
+
if (matches) {
|
|
1271
|
+
extracted += matches.join('\n\n')
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return extracted || 'Review Context7 documentation for anti-patterns specific to your use case.'
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Helper: Extract code examples from Context7 docs
|
|
1279
|
+
function extractCodeExamples(docs: string): string {
|
|
1280
|
+
// Extract code blocks
|
|
1281
|
+
const codeBlocks = docs.match(/\`\`\`[\s\S]*?\`\`\`/g) || []
|
|
1282
|
+
return codeBlocks.slice(0, 5).join('\n\n') || 'See Context7 documentation for code examples.'
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Helper: Generate checklist from docs
|
|
1286
|
+
function extractChecklist(docs: string): string {
|
|
1287
|
+
// Look for checklist items or generate from best practices
|
|
1288
|
+
const checklistPatterns = [
|
|
1289
|
+
/- \[[ x]\][^\n]+/gi,
|
|
1290
|
+
/\d+\.\s+[^\n]+/gi
|
|
1291
|
+
]
|
|
1292
|
+
|
|
1293
|
+
let items = []
|
|
1294
|
+
for (const pattern of checklistPatterns) {
|
|
1295
|
+
const matches = docs.match(pattern)
|
|
1296
|
+
if (matches) {
|
|
1297
|
+
items = items.concat(matches.slice(0, 10))
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (items.length > 0) {
|
|
1302
|
+
return items.map(item => `- [ ] ${item.replace(/^[\d.-\[\]x\s]+/i, '')}`).join('\n')
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Fallback: generic checklist
|
|
1306
|
+
return `- [ ] Follow official documentation patterns
|
|
1307
|
+
- [ ] Handle errors appropriately
|
|
1308
|
+
- [ ] Add proper typing/validation
|
|
1309
|
+
- [ ] Write tests for new code
|
|
1310
|
+
- [ ] Review for security concerns`
|
|
1311
|
+
}
|
|
454
1312
|
```
|
|
455
1313
|
|
|
456
1314
|
---
|
|
@@ -1110,20 +1968,18 @@ function getAgentForPhase(phaseId: string): string {
|
|
|
1110
1968
|
}
|
|
1111
1969
|
```
|
|
1112
1970
|
|
|
1113
|
-
### detectAdditionalTech()
|
|
1971
|
+
### detectAdditionalTech() - DEPRECATED
|
|
1972
|
+
|
|
1973
|
+
> **Note:** This function is deprecated in v2.3.0. Use `extractPotentialLibraryNames()` + Context7 resolution instead.
|
|
1974
|
+
> The dynamic approach automatically detects any library without hardcoded patterns.
|
|
1975
|
+
|
|
1114
1976
|
```typescript
|
|
1115
|
-
//
|
|
1977
|
+
// DEPRECATED: Kept for backwards compatibility only
|
|
1978
|
+
// Use extractPotentialLibraryNames() for new implementations
|
|
1116
1979
|
function detectAdditionalTech(proposal: string, tasks: string): string[] {
|
|
1980
|
+
// Now delegates to the dynamic detection system
|
|
1117
1981
|
const combined = proposal + ' ' + tasks
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (combined.includes('stripe') || combined.includes('payment')) tech.push('Stripe')
|
|
1121
|
-
if (combined.includes('websocket') || combined.includes('realtime')) tech.push('WebSocket')
|
|
1122
|
-
if (combined.includes('redis')) tech.push('Redis')
|
|
1123
|
-
if (combined.includes('s3') || combined.includes('storage')) tech.push('S3/Storage')
|
|
1124
|
-
// Add more as needed
|
|
1125
|
-
|
|
1126
|
-
return tech
|
|
1982
|
+
return extractPotentialLibraryNames(combined)
|
|
1127
1983
|
}
|
|
1128
1984
|
```
|
|
1129
1985
|
|