ruvnet-kb-first 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +41 -23
  2. package/SKILL.md +153 -75
  3. package/package.json +1 -1
  4. package/src/mcp-server.js +592 -258
package/src/mcp-server.js CHANGED
@@ -1,364 +1,712 @@
1
1
  /**
2
- * RuvNet KB-First MCP Server
2
+ * RuvNet KB-First MCP Server - Score-Driven Architecture
3
+ * Version 6.0.0
3
4
  *
4
- * Model Context Protocol server for Claude Code integration.
5
- * Provides KB-First tools directly within Claude Code.
5
+ * PHILOSOPHY: Scoring IS the enforcement mechanism.
6
+ * - Every operation requires baseline score first
7
+ * - Every operation shows delta (before/after)
8
+ * - Hard gates BLOCK on negative deltas
9
+ * - No shortcuts - rigorous measurement drives quality
10
+ *
11
+ * 4 Tools (not 7):
12
+ * 1. kb_first_assess - Calculate baseline scores (KB + App + Process)
13
+ * 2. kb_first_phase - Execute phase work with delta tracking
14
+ * 3. kb_first_delta - Explicit before/after comparison
15
+ * 4. kb_first_gate - Hard gate that blocks on negative delta
6
16
  *
7
17
  * Usage:
8
18
  * npx ruvnet-kb-first mcp
9
19
  * node src/mcp-server.js
10
20
  */
11
21
 
12
- import { createServer } from 'http';
13
- import { existsSync, readFileSync, writeFileSync } from 'fs';
22
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
14
23
  import { join } from 'path';
24
+ import { globSync } from 'glob';
15
25
 
16
26
  // MCP Protocol Constants
17
27
  const MCP_VERSION = '0.1.0';
18
28
  const SERVER_NAME = 'ruvnet-kb-first';
19
- const SERVER_VERSION = '5.0.0';
29
+ const SERVER_VERSION = '6.0.0';
20
30
 
21
31
  /**
22
- * Available MCP Tools
32
+ * Score Categories (total 100 points)
33
+ * These are the ONLY metrics that matter
23
34
  */
24
- const TOOLS = [
25
- {
26
- name: 'kb_first_init',
27
- description: 'Initialize KB-First structure in the current project',
28
- inputSchema: {
29
- type: 'object',
30
- properties: {
31
- force: {
32
- type: 'boolean',
33
- description: 'Overwrite existing configuration'
34
- },
35
- template: {
36
- type: 'string',
37
- enum: ['basic', 'api', 'fullstack'],
38
- description: 'Project template type'
39
- }
40
- }
35
+ const SCORE_WEIGHTS = {
36
+ kb: {
37
+ weight: 40,
38
+ components: {
39
+ entries: 10, // KB has content
40
+ coverage: 10, // Domain coverage completeness
41
+ embeddings: 10, // Vectors generated
42
+ freshness: 10 // Recent updates
41
43
  }
42
44
  },
43
- {
44
- name: 'kb_first_score',
45
- description: 'Calculate KB-First compliance score for the project',
46
- inputSchema: {
47
- type: 'object',
48
- properties: {
49
- detailed: {
50
- type: 'boolean',
51
- description: 'Show detailed breakdown'
52
- }
53
- }
45
+ app: {
46
+ weight: 40,
47
+ components: {
48
+ kbCitations: 15, // Code files cite KB sources
49
+ gapResolution: 10, // Gaps identified and resolved
50
+ testCoverage: 10, // Tests exist and pass
51
+ security: 5 // Security basics in place
54
52
  }
55
53
  },
54
+ process: {
55
+ weight: 20,
56
+ components: {
57
+ phaseCompletion: 10, // Phases properly completed
58
+ gatesPassed: 5, // Hard gates verified
59
+ documentation: 5 // Docs exist
60
+ }
61
+ }
62
+ };
63
+
64
+ /**
65
+ * Phase definitions
66
+ */
67
+ const PHASES = {
68
+ 0: { name: 'Assessment', gate: 'assessment_documented' },
69
+ 1: { name: 'KB Design', gate: 'schema_designed' },
70
+ 1.5: { name: 'Hooks Setup', gate: 'hooks_verified' },
71
+ 2: { name: 'Schema Definition', gate: 'schema_created' },
72
+ 3: { name: 'KB Population', gate: 'kb_score_50' },
73
+ 4: { name: 'Scoring & Gaps', gate: 'kb_score_80' },
74
+ 5: { name: 'Integration', gate: 'integration_tested' },
75
+ 6: { name: 'Testing', gate: 'tests_passing' },
76
+ 7: { name: 'Optimization', gate: 'performance_met' },
77
+ 7.5: { name: 'Testing Gate', gate: 'coverage_80' },
78
+ 8: { name: 'Verification', gate: 'all_checks_pass' },
79
+ 9: { name: 'Security', gate: 'security_audit_passed' },
80
+ 10: { name: 'Documentation', gate: 'docs_complete' },
81
+ 11: { name: 'Deployment', gate: 'deployed' },
82
+ 11.5: { name: 'Observability', gate: 'monitoring_active' },
83
+ 12: { name: 'KB Operations', gate: 'operations_ready' }
84
+ };
85
+
86
+ /**
87
+ * MCP Tools - Score-Driven Architecture
88
+ */
89
+ const TOOLS = [
56
90
  {
57
- name: 'kb_first_verify',
58
- description: 'Run KB-First verification checks',
91
+ name: 'kb_first_assess',
92
+ description: `Calculate comprehensive baseline scores for KB, App, and Process.
93
+ ALWAYS RUN THIS FIRST before any work. Returns:
94
+ - KB Score (40 points): entries, coverage, embeddings, freshness
95
+ - App Score (40 points): citations, gap resolution, tests, security
96
+ - Process Score (20 points): phases, gates, documentation
97
+ - Total (100 points)
98
+
99
+ This becomes your BASELINE for delta comparison.`,
59
100
  inputSchema: {
60
101
  type: 'object',
61
102
  properties: {
62
- phase: {
63
- type: 'number',
64
- description: 'Verify specific phase (0-11)'
65
- },
66
- all: {
67
- type: 'boolean',
68
- description: 'Run all verification scripts'
69
- }
103
+ detailed: { type: 'boolean', description: 'Show component breakdown', default: true },
104
+ saveBaseline: { type: 'boolean', description: 'Save as baseline for delta comparison', default: true }
70
105
  }
71
106
  }
72
107
  },
73
108
  {
74
- name: 'kb_first_status',
75
- description: 'Show KB-First project status',
109
+ name: 'kb_first_phase',
110
+ description: `Execute a phase with automatic delta tracking.
111
+ REQUIRES: kb_first_assess must be run first to establish baseline.
112
+
113
+ Workflow:
114
+ 1. Loads baseline score from last kb_first_assess
115
+ 2. Shows phase requirements and sub-phases
116
+ 3. Returns guidance for completing the phase
117
+ 4. REMINDS you to run kb_first_delta when done
118
+
119
+ Will WARN if baseline is stale (>1 hour old).`,
76
120
  inputSchema: {
77
121
  type: 'object',
78
122
  properties: {
79
- detailed: {
80
- type: 'boolean',
81
- description: 'Show detailed status'
82
- }
83
- }
123
+ phase: { type: 'number', description: 'Phase number (0-12, including 1.5, 7.5, 11.5)' }
124
+ },
125
+ required: ['phase']
84
126
  }
85
127
  },
86
128
  {
87
- name: 'kb_first_phase',
88
- description: 'Get information about a specific build phase',
129
+ name: 'kb_first_delta',
130
+ description: `Compare current scores against baseline. THE ENFORCEMENT MECHANISM.
131
+ Shows:
132
+ - Baseline score (from kb_first_assess)
133
+ - Current score (calculated now)
134
+ - Delta (+ improvement or - regression)
135
+ - VERDICT: PASS (positive delta) or FAIL (negative delta)
136
+
137
+ If delta is negative, you CANNOT proceed to next phase.
138
+ This prevents shortcuts and enforces rigor.`,
89
139
  inputSchema: {
90
140
  type: 'object',
91
141
  properties: {
92
- phase: {
93
- type: 'number',
94
- description: 'Phase number (0, 1, 1.5, 2-11)'
95
- }
96
- },
97
- required: ['phase']
142
+ showBreakdown: { type: 'boolean', description: 'Show which components changed', default: true }
143
+ }
98
144
  }
99
145
  },
100
146
  {
101
- name: 'kb_first_hooks',
102
- description: 'Manage KB-First hooks',
147
+ name: 'kb_first_gate',
148
+ description: `Hard gate check for phase transition.
149
+ BLOCKS progress if:
150
+ - Delta is negative (score dropped)
151
+ - Required gate condition not met
152
+ - Baseline not established
153
+
154
+ Returns:
155
+ - canProceed: boolean
156
+ - blockReason: string (if blocked)
157
+ - nextPhase: number (if can proceed)
158
+
159
+ THIS IS THE HARD GATE. No bypassing.`,
103
160
  inputSchema: {
104
161
  type: 'object',
105
162
  properties: {
106
- action: {
107
- type: 'string',
108
- enum: ['install', 'verify', 'train', 'status'],
109
- description: 'Hook action to perform'
110
- }
163
+ phase: { type: 'number', description: 'Phase to verify gate for' }
111
164
  },
112
- required: ['action']
165
+ required: ['phase']
113
166
  }
114
167
  }
115
168
  ];
116
169
 
117
170
  /**
118
- * Handle MCP tool calls
171
+ * Calculate all scores
119
172
  */
120
- async function handleToolCall(toolName, args) {
121
- const cwd = process.cwd();
122
-
123
- switch (toolName) {
124
- case 'kb_first_init':
125
- return await handleInit(cwd, args);
126
-
127
- case 'kb_first_score':
128
- return await handleScore(cwd, args);
129
-
130
- case 'kb_first_verify':
131
- return await handleVerify(cwd, args);
173
+ function calculateScores(cwd) {
174
+ const scores = {
175
+ kb: { total: 0, max: 40, components: {} },
176
+ app: { total: 0, max: 40, components: {} },
177
+ process: { total: 0, max: 20, components: {} },
178
+ total: 0,
179
+ max: 100,
180
+ grade: 'F',
181
+ timestamp: new Date().toISOString()
182
+ };
132
183
 
133
- case 'kb_first_status':
134
- return await handleStatus(cwd, args);
184
+ // ===== KB SCORE (40 points) =====
185
+ const ruvectorDir = join(cwd, '.ruvector');
186
+ const kbDir = join(cwd, 'src', 'kb');
187
+
188
+ // KB Entries (10 points)
189
+ let kbEntries = 0;
190
+ if (existsSync(kbDir)) {
191
+ try {
192
+ const files = readdirSync(kbDir);
193
+ kbEntries = files.length;
194
+ } catch {}
195
+ }
196
+ scores.kb.components.entries = Math.min(10, Math.floor(kbEntries / 5) * 2);
197
+
198
+ // KB Coverage (10 points) - based on documented domains
199
+ const docsDir = join(cwd, 'docs');
200
+ let domainDocs = 0;
201
+ if (existsSync(docsDir)) {
202
+ try {
203
+ const files = readdirSync(docsDir);
204
+ domainDocs = files.filter(f => f.endsWith('.md')).length;
205
+ } catch {}
206
+ }
207
+ scores.kb.components.coverage = Math.min(10, domainDocs * 2);
208
+
209
+ // KB Embeddings (10 points) - check for vector files or config
210
+ const configPath = join(ruvectorDir, 'config.json');
211
+ let hasEmbeddings = false;
212
+ if (existsSync(configPath)) {
213
+ try {
214
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
215
+ hasEmbeddings = config.kbFirst?.embeddings === true || kbEntries > 0;
216
+ } catch {}
217
+ }
218
+ scores.kb.components.embeddings = hasEmbeddings ? 10 : 0;
219
+
220
+ // KB Freshness (10 points) - recent updates
221
+ let freshness = 0;
222
+ if (existsSync(ruvectorDir)) {
223
+ try {
224
+ const stat = statSync(ruvectorDir);
225
+ const daysSinceUpdate = (Date.now() - stat.mtime.getTime()) / (1000 * 60 * 60 * 24);
226
+ if (daysSinceUpdate < 1) freshness = 10;
227
+ else if (daysSinceUpdate < 7) freshness = 7;
228
+ else if (daysSinceUpdate < 30) freshness = 4;
229
+ else freshness = 0;
230
+ } catch {}
231
+ }
232
+ scores.kb.components.freshness = freshness;
233
+
234
+ scores.kb.total = Object.values(scores.kb.components).reduce((a, b) => a + b, 0);
235
+
236
+ // ===== APP SCORE (40 points) =====
237
+ const srcDir = join(cwd, 'src');
238
+
239
+ // KB Citations (15 points)
240
+ let codeFiles = [];
241
+ let filesWithCitation = 0;
242
+ if (existsSync(srcDir)) {
243
+ try {
244
+ codeFiles = globSync('**/*.{ts,tsx,js,jsx,py,go,rs}', { cwd: srcDir });
245
+ for (const file of codeFiles) {
246
+ try {
247
+ const content = readFileSync(join(srcDir, file), 'utf-8');
248
+ if (content.includes('KB-Generated:') || content.includes('Sources:') || content.includes('@kb-source')) {
249
+ filesWithCitation++;
250
+ }
251
+ } catch {}
252
+ }
253
+ } catch {}
254
+ }
255
+ const citationPercent = codeFiles.length > 0 ? filesWithCitation / codeFiles.length : 1;
256
+ scores.app.components.kbCitations = Math.round(citationPercent * 15);
257
+
258
+ // Gap Resolution (10 points)
259
+ const gapsPath = join(ruvectorDir, 'gaps.jsonl');
260
+ let gapCount = 0;
261
+ if (existsSync(gapsPath)) {
262
+ try {
263
+ const content = readFileSync(gapsPath, 'utf-8').trim();
264
+ gapCount = content ? content.split('\n').length : 0;
265
+ } catch {}
266
+ }
267
+ scores.app.components.gapResolution = Math.max(0, 10 - gapCount);
268
+
269
+ // Test Coverage (10 points)
270
+ let hasTests = false;
271
+ const testDirs = ['tests', 'test', '__tests__', 'src/__tests__'];
272
+ for (const td of testDirs) {
273
+ if (existsSync(join(cwd, td))) {
274
+ hasTests = true;
275
+ break;
276
+ }
277
+ }
278
+ const testFiles = existsSync(srcDir) ? globSync('**/*.{test,spec}.{ts,tsx,js,jsx}', { cwd: srcDir }) : [];
279
+ scores.app.components.testCoverage = hasTests ? 5 : 0;
280
+ scores.app.components.testCoverage += Math.min(5, testFiles.length);
281
+
282
+ // Security (5 points)
283
+ let secScore = 5;
284
+ const gitignorePath = join(cwd, '.gitignore');
285
+ if (existsSync(gitignorePath)) {
286
+ try {
287
+ const content = readFileSync(gitignorePath, 'utf-8');
288
+ if (!content.includes('.env')) secScore -= 2;
289
+ if (!content.includes('node_modules')) secScore -= 1;
290
+ } catch {}
291
+ } else {
292
+ secScore -= 3;
293
+ }
294
+ scores.app.components.security = Math.max(0, secScore);
135
295
 
136
- case 'kb_first_phase':
137
- return await handlePhase(cwd, args);
296
+ scores.app.total = Object.values(scores.app.components).reduce((a, b) => a + b, 0);
138
297
 
139
- case 'kb_first_hooks':
140
- return await handleHooks(cwd, args);
298
+ // ===== PROCESS SCORE (20 points) =====
141
299
 
142
- default:
143
- return {
144
- error: `Unknown tool: ${toolName}`
145
- };
300
+ // Phase Completion (10 points)
301
+ let completedPhases = [];
302
+ if (existsSync(configPath)) {
303
+ try {
304
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
305
+ completedPhases = config.phases?.completed || [];
306
+ } catch {}
146
307
  }
308
+ const totalPhases = Object.keys(PHASES).length;
309
+ scores.process.components.phaseCompletion = Math.round((completedPhases.length / totalPhases) * 10);
310
+
311
+ // Gates Passed (5 points)
312
+ let gatesPassed = 0;
313
+ if (existsSync(configPath)) {
314
+ try {
315
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
316
+ gatesPassed = Object.values(config.phases?.gates || {}).filter(v => v === true).length;
317
+ } catch {}
318
+ }
319
+ scores.process.components.gatesPassed = Math.min(5, Math.round((gatesPassed / totalPhases) * 5));
320
+
321
+ // Documentation (5 points)
322
+ let docScore = 0;
323
+ if (existsSync(join(cwd, 'README.md'))) docScore += 2;
324
+ if (existsSync(join(cwd, 'docs', 'api.md')) || existsSync(join(cwd, 'docs', 'API.md'))) docScore += 1;
325
+ if (existsSync(join(cwd, 'docs', 'architecture.md'))) docScore += 1;
326
+ if (existsSync(join(cwd, 'CHANGELOG.md'))) docScore += 1;
327
+ scores.process.components.documentation = Math.min(5, docScore);
328
+
329
+ scores.process.total = Object.values(scores.process.components).reduce((a, b) => a + b, 0);
330
+
331
+ // ===== TOTAL =====
332
+ scores.total = scores.kb.total + scores.app.total + scores.process.total;
333
+
334
+ // Grade
335
+ if (scores.total >= 98) scores.grade = 'A+';
336
+ else if (scores.total >= 93) scores.grade = 'A';
337
+ else if (scores.total >= 90) scores.grade = 'A-';
338
+ else if (scores.total >= 87) scores.grade = 'B+';
339
+ else if (scores.total >= 83) scores.grade = 'B';
340
+ else if (scores.total >= 80) scores.grade = 'B-';
341
+ else if (scores.total >= 70) scores.grade = 'C';
342
+ else if (scores.total >= 60) scores.grade = 'D';
343
+ else scores.grade = 'F';
344
+
345
+ return scores;
147
346
  }
148
347
 
149
- async function handleInit(cwd, args) {
150
- const { initCommand } = await import('./commands/init.js');
151
-
152
- // Run init (capture output)
153
- const configPath = join(cwd, '.ruvector', 'config.json');
154
- const wasInitialized = existsSync(configPath);
348
+ /**
349
+ * Tool Handlers
350
+ */
351
+ async function handleKbFirstAssess(cwd, args) {
352
+ const scores = calculateScores(cwd);
353
+
354
+ // Save baseline
355
+ if (args.saveBaseline !== false) {
356
+ const ruvectorDir = join(cwd, '.ruvector');
357
+ if (!existsSync(ruvectorDir)) {
358
+ mkdirSync(ruvectorDir, { recursive: true });
359
+ }
360
+ writeFileSync(join(ruvectorDir, 'baseline.json'), JSON.stringify(scores, null, 2));
361
+ }
155
362
 
156
- try {
157
- await initCommand({
158
- force: args.force || false,
159
- template: args.template || 'basic',
160
- hooks: true
161
- });
363
+ const result = {
364
+ action: 'BASELINE_ESTABLISHED',
365
+ timestamp: scores.timestamp,
366
+ total: scores.total,
367
+ max: scores.max,
368
+ grade: scores.grade,
369
+ summary: {
370
+ kb: `${scores.kb.total}/${scores.kb.max}`,
371
+ app: `${scores.app.total}/${scores.app.max}`,
372
+ process: `${scores.process.total}/${scores.process.max}`
373
+ },
374
+ nextStep: 'Run kb_first_phase to begin work, then kb_first_delta to measure improvement'
375
+ };
162
376
 
163
- return {
164
- success: true,
165
- message: wasInitialized && !args.force
166
- ? 'Project already initialized'
167
- : 'KB-First project initialized successfully',
168
- configPath
169
- };
170
- } catch (error) {
171
- return {
172
- success: false,
173
- error: error.message
377
+ if (args.detailed !== false) {
378
+ result.breakdown = {
379
+ kb: scores.kb.components,
380
+ app: scores.app.components,
381
+ process: scores.process.components
174
382
  };
175
383
  }
384
+
385
+ return result;
176
386
  }
177
387
 
178
- async function handleScore(cwd, args) {
179
- const configPath = join(cwd, '.ruvector', 'config.json');
388
+ async function handleKbFirstPhase(cwd, args) {
389
+ const phase = args.phase;
390
+ const phaseInfo = PHASES[phase];
180
391
 
181
- if (!existsSync(configPath)) {
392
+ if (!phaseInfo) {
182
393
  return {
183
- success: false,
184
- error: 'Not a KB-First project. Run kb_first_init first.'
394
+ error: `Unknown phase: ${phase}`,
395
+ validPhases: Object.entries(PHASES).map(([k, v]) => ({ phase: parseFloat(k), name: v.name }))
185
396
  };
186
397
  }
187
398
 
188
- // Import and run score calculation
189
- const { scoreCommand } = await import('./commands/score.js');
399
+ // Check for baseline
400
+ const baselinePath = join(cwd, '.ruvector', 'baseline.json');
401
+ let baseline = null;
402
+ let baselineWarning = null;
403
+
404
+ if (existsSync(baselinePath)) {
405
+ try {
406
+ baseline = JSON.parse(readFileSync(baselinePath, 'utf-8'));
407
+ const baselineAge = (Date.now() - new Date(baseline.timestamp).getTime()) / (1000 * 60);
408
+ if (baselineAge > 60) {
409
+ baselineWarning = `Baseline is ${Math.round(baselineAge)} minutes old. Consider running kb_first_assess for fresh baseline.`;
410
+ }
411
+ } catch {}
412
+ }
413
+
414
+ // Phase-specific guidance
415
+ const phaseGuidance = {
416
+ 0: ['Document project scope', 'Identify domain complexity', 'Assess KB-First suitability', 'Estimate resources', 'Make go/no-go decision'],
417
+ 1: ['Map domain concepts', 'Design taxonomy', 'Define relationships', 'Plan query patterns', 'Review with stakeholders'],
418
+ 1.5: ['Install enforcement hooks', 'Configure hook behavior', 'Train on project patterns', 'Verify hooks work'],
419
+ 2: ['Create database tables', 'Add vector columns', 'Design indexes', 'Write migration scripts'],
420
+ 3: ['Collect domain content', 'Process and clean data', 'Generate embeddings', 'Import to KB', 'Validate entries'],
421
+ 4: ['Analyze KB coverage', 'Calculate quality score', 'Identify gaps', 'Prioritize fixes', 'Create remediation plan'],
422
+ 5: ['Build search API', 'Implement code generation', 'Add citation system', 'Enable gap logging'],
423
+ 6: ['Write unit tests', 'Create integration tests', 'Test KB accuracy', 'Performance tests', 'Edge case testing'],
424
+ 7: ['Optimize queries', 'Tune indexes', 'Add caching', 'Run benchmarks'],
425
+ 7.5: ['Verify test coverage ≥80%', 'Run E2E suite', 'Load testing', 'Build regression suite'],
426
+ 8: ['Run code scan', 'Check imports', 'Verify source returns', 'Test startup', 'Check fallbacks', 'Validate attribution', 'Test confidence', 'Review gap logs'],
427
+ 9: ['Audit dependencies', 'Check OWASP Top 10', 'Test SQL injection', 'Review auth', 'Audit secrets', 'Secure APIs'],
428
+ 10: ['Write README', 'Document API', 'Schema documentation', 'Architecture docs', 'Operator guide'],
429
+ 11: ['Setup infrastructure', 'Configure environments', 'Build CI/CD', 'Run migrations', 'Setup monitoring', 'Go live'],
430
+ 11.5: ['Setup OpenTelemetry', 'Build KB dashboard', 'Configure alerts', 'Write runbooks'],
431
+ 12: ['Define gap triage', 'Setup expert review', 'Document KB updates', 'Version control', 'A/B testing']
432
+ };
190
433
 
191
- // Return score data
192
434
  return {
193
- success: true,
194
- message: 'Score calculated. Run `kb-first score --detailed` for full breakdown.',
195
- hint: 'Use CLI for detailed output'
435
+ phase,
436
+ name: phaseInfo.name,
437
+ gate: phaseInfo.gate,
438
+ baseline: baseline ? {
439
+ score: baseline.total,
440
+ grade: baseline.grade,
441
+ timestamp: baseline.timestamp
442
+ } : null,
443
+ baselineWarning,
444
+ tasks: phaseGuidance[phase] || [],
445
+ reminder: '⚠️ IMPORTANT: Run kb_first_delta when phase work is complete to measure improvement',
446
+ gateRequirement: `Gate "${phaseInfo.gate}" must be satisfied to proceed`
196
447
  };
197
448
  }
198
449
 
199
- async function handleVerify(cwd, args) {
200
- const configPath = join(cwd, '.ruvector', 'config.json');
450
+ async function handleKbFirstDelta(cwd, args) {
451
+ const baselinePath = join(cwd, '.ruvector', 'baseline.json');
201
452
 
202
- if (!existsSync(configPath)) {
453
+ if (!existsSync(baselinePath)) {
203
454
  return {
204
- success: false,
205
- error: 'Not a KB-First project. Run kb_first_init first.'
455
+ error: 'NO_BASELINE',
456
+ message: 'No baseline found. Run kb_first_assess first to establish baseline.',
457
+ action: 'Run kb_first_assess with saveBaseline=true'
206
458
  };
207
459
  }
208
460
 
209
- const phase = args.phase;
210
- const runAll = args.all;
461
+ let baseline;
462
+ try {
463
+ baseline = JSON.parse(readFileSync(baselinePath, 'utf-8'));
464
+ } catch {
465
+ return { error: 'CORRUPT_BASELINE', message: 'Baseline file is corrupt. Run kb_first_assess again.' };
466
+ }
211
467
 
212
- return {
213
- success: true,
214
- message: phase
215
- ? `Run: kb-first verify --phase=${phase}`
216
- : runAll
217
- ? 'Run: kb-first verify --all'
218
- : 'Run: kb-first verify',
219
- hint: 'Verification scripts require terminal execution'
468
+ const current = calculateScores(cwd);
469
+
470
+ const delta = {
471
+ total: current.total - baseline.total,
472
+ kb: current.kb.total - baseline.kb.total,
473
+ app: current.app.total - baseline.app.total,
474
+ process: current.process.total - baseline.process.total
220
475
  };
221
- }
222
476
 
223
- async function handleStatus(cwd, args) {
224
- const configPath = join(cwd, '.ruvector', 'config.json');
477
+ const verdict = delta.total >= 0 ? 'PASS' : 'FAIL';
478
+ const canProceed = delta.total >= 0;
479
+
480
+ const result = {
481
+ verdict,
482
+ canProceed,
483
+ baseline: {
484
+ score: baseline.total,
485
+ grade: baseline.grade,
486
+ timestamp: baseline.timestamp
487
+ },
488
+ current: {
489
+ score: current.total,
490
+ grade: current.grade,
491
+ timestamp: current.timestamp
492
+ },
493
+ delta: {
494
+ total: delta.total > 0 ? `+${delta.total}` : `${delta.total}`,
495
+ kb: delta.kb > 0 ? `+${delta.kb}` : `${delta.kb}`,
496
+ app: delta.app > 0 ? `+${delta.app}` : `${delta.app}`,
497
+ process: delta.process > 0 ? `+${delta.process}` : `${delta.process}`
498
+ }
499
+ };
225
500
 
226
- if (!existsSync(configPath)) {
227
- return {
228
- initialized: false,
229
- error: 'Not a KB-First project'
230
- };
501
+ if (!canProceed) {
502
+ result.blockReason = `Score dropped by ${Math.abs(delta.total)} points. You CANNOT proceed until score improves.`;
503
+ result.action = 'Fix issues causing score regression, then run kb_first_delta again.';
504
+ } else {
505
+ result.action = 'Run kb_first_gate to verify phase completion and proceed.';
231
506
  }
232
507
 
233
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
508
+ if (args.showBreakdown !== false) {
509
+ result.componentChanges = {
510
+ kb: {
511
+ before: baseline.kb.components,
512
+ after: current.kb.components
513
+ },
514
+ app: {
515
+ before: baseline.app.components,
516
+ after: current.app.components
517
+ },
518
+ process: {
519
+ before: baseline.process.components,
520
+ after: current.process.components
521
+ }
522
+ };
523
+ }
234
524
 
235
- return {
236
- initialized: true,
237
- namespace: config.kbFirst?.namespace,
238
- version: config.kbFirst?.version,
239
- currentPhase: config.phases?.current || 0,
240
- completedPhases: config.phases?.completed || [],
241
- hooksEnabled: config.hooks?.enabled || false
242
- };
525
+ return result;
243
526
  }
244
527
 
245
- async function handlePhase(cwd, args) {
246
- const PHASES = {
247
- 0: { name: 'Assessment', subphases: 5 },
248
- 1: { name: 'KB Design', subphases: 5 },
249
- 1.5: { name: 'Hooks Setup', subphases: 4 },
250
- 2: { name: 'Schema Definition', subphases: 4 },
251
- 3: { name: 'KB Population', subphases: 5 },
252
- 4: { name: 'Scoring & Gaps', subphases: 5 },
253
- 5: { name: 'Integration', subphases: 4 },
254
- 6: { name: 'Testing', subphases: 5 },
255
- 7: { name: 'Optimization', subphases: 4 },
256
- 8: { name: 'Verification', subphases: 8 },
257
- 9: { name: 'Security', subphases: 6 },
258
- 10: { name: 'Documentation', subphases: 6 },
259
- 11: { name: 'Deployment', subphases: 6 }
260
- };
261
-
528
+ async function handleKbFirstGate(cwd, args) {
262
529
  const phase = args.phase;
263
530
  const phaseInfo = PHASES[phase];
264
531
 
265
532
  if (!phaseInfo) {
533
+ return { error: `Unknown phase: ${phase}` };
534
+ }
535
+
536
+ // Check baseline exists
537
+ const baselinePath = join(cwd, '.ruvector', 'baseline.json');
538
+ if (!existsSync(baselinePath)) {
266
539
  return {
267
- error: `Unknown phase: ${phase}`,
268
- validPhases: Object.keys(PHASES)
540
+ canProceed: false,
541
+ blockReason: 'GATE_BLOCKED: No baseline established. Run kb_first_assess first.',
542
+ phase,
543
+ phaseName: phaseInfo.name
544
+ };
545
+ }
546
+
547
+ // Check delta
548
+ let baseline;
549
+ try {
550
+ baseline = JSON.parse(readFileSync(baselinePath, 'utf-8'));
551
+ } catch {
552
+ return {
553
+ canProceed: false,
554
+ blockReason: 'GATE_BLOCKED: Corrupt baseline. Run kb_first_assess again.',
555
+ phase,
556
+ phaseName: phaseInfo.name
557
+ };
558
+ }
559
+
560
+ const current = calculateScores(cwd);
561
+ const delta = current.total - baseline.total;
562
+
563
+ if (delta < 0) {
564
+ return {
565
+ canProceed: false,
566
+ blockReason: `GATE_BLOCKED: Score regression detected (${delta} points). Fix issues before proceeding.`,
567
+ phase,
568
+ phaseName: phaseInfo.name,
569
+ baseline: baseline.total,
570
+ current: current.total,
571
+ delta
572
+ };
573
+ }
574
+
575
+ // Check gate-specific conditions
576
+ const configPath = join(cwd, '.ruvector', 'config.json');
577
+ let config = { phases: { current: 0, completed: [], gates: {} } };
578
+ if (existsSync(configPath)) {
579
+ try {
580
+ config = JSON.parse(readFileSync(configPath, 'utf-8'));
581
+ } catch {}
582
+ }
583
+
584
+ // Phase-specific gate checks
585
+ let gateConditionMet = false;
586
+ let gateMessage = '';
587
+
588
+ switch (phaseInfo.gate) {
589
+ case 'kb_score_50':
590
+ gateConditionMet = current.kb.total >= 20; // 50% of 40
591
+ gateMessage = gateConditionMet ? 'KB score ≥50%' : `KB score ${current.kb.total}/40 < 50%`;
592
+ break;
593
+ case 'kb_score_80':
594
+ gateConditionMet = current.kb.total >= 32; // 80% of 40
595
+ gateMessage = gateConditionMet ? 'KB score ≥80%' : `KB score ${current.kb.total}/40 < 80%`;
596
+ break;
597
+ case 'coverage_80':
598
+ gateConditionMet = current.app.components.testCoverage >= 8; // 80% of 10
599
+ gateMessage = gateConditionMet ? 'Test coverage ≥80%' : `Test coverage ${current.app.components.testCoverage}/10 < 80%`;
600
+ break;
601
+ default:
602
+ // Default: pass if delta is non-negative
603
+ gateConditionMet = true;
604
+ gateMessage = 'Gate condition satisfied (positive delta)';
605
+ }
606
+
607
+ if (!gateConditionMet) {
608
+ return {
609
+ canProceed: false,
610
+ blockReason: `GATE_BLOCKED: ${gateMessage}`,
611
+ phase,
612
+ phaseName: phaseInfo.name,
613
+ gate: phaseInfo.gate
269
614
  };
270
615
  }
271
616
 
617
+ // Mark phase as completed
618
+ if (!config.phases.completed.includes(parseFloat(phase))) {
619
+ config.phases.completed.push(parseFloat(phase));
620
+ }
621
+ config.phases.gates[phaseInfo.gate] = true;
622
+
623
+ // Determine next phase
624
+ const phaseOrder = [0, 1, 1.5, 2, 3, 4, 5, 6, 7, 7.5, 8, 9, 10, 11, 11.5, 12];
625
+ const currentIdx = phaseOrder.indexOf(parseFloat(phase));
626
+ const nextPhase = currentIdx < phaseOrder.length - 1 ? phaseOrder[currentIdx + 1] : null;
627
+
628
+ if (nextPhase !== null) {
629
+ config.phases.current = nextPhase;
630
+ }
631
+
632
+ // Save config
633
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
634
+
635
+ // Save current scores as new baseline for next phase
636
+ writeFileSync(baselinePath, JSON.stringify(current, null, 2));
637
+
272
638
  return {
639
+ canProceed: true,
273
640
  phase,
274
- name: phaseInfo.name,
275
- subphases: phaseInfo.subphases,
276
- command: `kb-first phase ${phase}`
641
+ phaseName: phaseInfo.name,
642
+ gateStatus: 'PASSED',
643
+ gateMessage,
644
+ scoreImprovement: delta > 0 ? `+${delta}` : '0',
645
+ nextPhase,
646
+ nextPhaseName: nextPhase !== null ? PHASES[nextPhase]?.name : 'PROJECT COMPLETE',
647
+ action: nextPhase !== null
648
+ ? `Run kb_first_assess to establish baseline for Phase ${nextPhase}: ${PHASES[nextPhase]?.name}`
649
+ : 'All phases complete! Project ready for production.'
277
650
  };
278
651
  }
279
652
 
280
- async function handleHooks(cwd, args) {
281
- const action = args.action;
653
+ /**
654
+ * Handle MCP tool calls
655
+ */
656
+ async function handleToolCall(toolName, args) {
657
+ const cwd = process.cwd();
282
658
 
283
- return {
284
- success: true,
285
- action,
286
- command: `kb-first hooks --${action}`,
287
- hint: 'Hook management requires terminal execution'
288
- };
659
+ switch (toolName) {
660
+ case 'kb_first_assess':
661
+ return await handleKbFirstAssess(cwd, args);
662
+ case 'kb_first_phase':
663
+ return await handleKbFirstPhase(cwd, args);
664
+ case 'kb_first_delta':
665
+ return await handleKbFirstDelta(cwd, args);
666
+ case 'kb_first_gate':
667
+ return await handleKbFirstGate(cwd, args);
668
+ default:
669
+ return { error: `Unknown tool: ${toolName}` };
670
+ }
289
671
  }
290
672
 
291
673
  /**
292
674
  * MCP Protocol Handler
293
675
  */
294
- function handleMCPMessage(message) {
676
+ async function handleMCPMessage(message) {
295
677
  const { jsonrpc, id, method, params } = message;
296
678
 
297
679
  if (jsonrpc !== '2.0') {
298
- return {
299
- jsonrpc: '2.0',
300
- id,
301
- error: { code: -32600, message: 'Invalid JSON-RPC version' }
302
- };
680
+ return { jsonrpc: '2.0', id, error: { code: -32600, message: 'Invalid JSON-RPC version' } };
303
681
  }
304
682
 
305
683
  switch (method) {
306
684
  case 'initialize':
307
685
  return {
308
- jsonrpc: '2.0',
309
- id,
686
+ jsonrpc: '2.0', id,
310
687
  result: {
311
688
  protocolVersion: MCP_VERSION,
312
- serverInfo: {
313
- name: SERVER_NAME,
314
- version: SERVER_VERSION
315
- },
316
- capabilities: {
317
- tools: {}
318
- }
689
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
690
+ capabilities: { tools: {} }
319
691
  }
320
692
  };
321
693
 
322
694
  case 'tools/list':
323
- return {
324
- jsonrpc: '2.0',
325
- id,
326
- result: { tools: TOOLS }
327
- };
695
+ return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
328
696
 
329
697
  case 'tools/call':
330
- return handleToolCallAsync(id, params.name, params.arguments);
698
+ try {
699
+ const result = await handleToolCall(params.name, params.arguments || {});
700
+ return {
701
+ jsonrpc: '2.0', id,
702
+ result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
703
+ };
704
+ } catch (error) {
705
+ return { jsonrpc: '2.0', id, error: { code: -32000, message: error.message } };
706
+ }
331
707
 
332
708
  default:
333
- return {
334
- jsonrpc: '2.0',
335
- id,
336
- error: { code: -32601, message: `Unknown method: ${method}` }
337
- };
338
- }
339
- }
340
-
341
- async function handleToolCallAsync(id, toolName, args) {
342
- try {
343
- const result = await handleToolCall(toolName, args || {});
344
- return {
345
- jsonrpc: '2.0',
346
- id,
347
- result: {
348
- content: [
349
- {
350
- type: 'text',
351
- text: JSON.stringify(result, null, 2)
352
- }
353
- ]
354
- }
355
- };
356
- } catch (error) {
357
- return {
358
- jsonrpc: '2.0',
359
- id,
360
- error: { code: -32000, message: error.message }
361
- };
709
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown method: ${method}` } };
362
710
  }
363
711
  }
364
712
 
@@ -366,47 +714,33 @@ async function handleToolCallAsync(id, toolName, args) {
366
714
  * Start MCP Server (stdio mode)
367
715
  */
368
716
  export async function startMCPServer(options = {}) {
369
- console.error('RuvNet KB-First MCP Server starting...');
370
- console.error(`Version: ${SERVER_VERSION}`);
371
- console.error('Mode: stdio');
717
+ console.error(`RuvNet KB-First MCP Server v${SERVER_VERSION}`);
718
+ console.error('Architecture: Score-Driven | Tools: 4 | Phases: 15');
719
+ console.error('Philosophy: Scoring IS enforcement. No shortcuts.');
372
720
 
373
- // Read from stdin
374
721
  let buffer = '';
375
-
376
722
  process.stdin.setEncoding('utf-8');
723
+
377
724
  process.stdin.on('data', async (chunk) => {
378
725
  buffer += chunk;
379
-
380
- // Try to parse complete JSON-RPC messages
381
726
  const lines = buffer.split('\n');
382
727
  buffer = lines.pop() || '';
383
728
 
384
729
  for (const line of lines) {
385
730
  if (!line.trim()) continue;
386
-
387
731
  try {
388
- const message = JSON.parse(line);
389
- const response = await handleMCPMessage(message);
390
-
391
- if (response) {
392
- process.stdout.write(JSON.stringify(response) + '\n');
393
- }
732
+ const response = await handleMCPMessage(JSON.parse(line));
733
+ if (response) process.stdout.write(JSON.stringify(response) + '\n');
394
734
  } catch (error) {
395
735
  console.error('Parse error:', error.message);
396
736
  }
397
737
  }
398
738
  });
399
739
 
400
- process.stdin.on('end', () => {
401
- console.error('MCP Server shutting down');
402
- process.exit(0);
403
- });
404
-
405
- // Keep process alive
740
+ process.stdin.on('end', () => process.exit(0));
406
741
  process.stdin.resume();
407
742
  }
408
743
 
409
- // Run if called directly
410
744
  if (import.meta.url === `file://${process.argv[1]}`) {
411
745
  startMCPServer();
412
746
  }