@rigour-labs/mcp 2.22.0 → 3.0.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/src/index.ts DELETED
@@ -1,1432 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import {
5
- CallToolRequestSchema,
6
- ListToolsRequestSchema,
7
- } from "@modelcontextprotocol/sdk/types.js";
8
- import fs from "fs-extra";
9
- import path from "path";
10
- import yaml from "yaml";
11
- import { randomUUID } from "crypto";
12
- import {
13
- GateRunner,
14
- ConfigSchema,
15
- Report,
16
- } from "@rigour-labs/core";
17
- import {
18
- PatternMatcher,
19
- loadPatternIndex,
20
- getDefaultIndexPath,
21
- StalenessDetector,
22
- SecurityDetector
23
- } from "@rigour-labs/core/pattern-index";
24
-
25
- const server = new Server(
26
- {
27
- name: "rigour-mcp",
28
- version: "1.0.0",
29
- },
30
- {
31
- capabilities: {
32
- tools: {},
33
- },
34
- }
35
- );
36
-
37
- async function loadConfig(cwd: string) {
38
- const configPath = path.join(cwd, "rigour.yml");
39
- if (!(await fs.pathExists(configPath))) {
40
- // Auto-initialize Rigour if config doesn't exist
41
- console.error(`[RIGOUR] rigour.yml not found in ${cwd}, auto-initializing...`);
42
- const { execa } = await import("execa");
43
- try {
44
- await execa("npx", ["rigour", "init"], { cwd, shell: true });
45
- console.error(`[RIGOUR] Auto-initialization complete.`);
46
- } catch (initError: any) {
47
- throw new Error(`Rigour auto-initialization failed: ${initError.message}. Please run 'npx rigour init' manually.`);
48
- }
49
- }
50
- const configContent = await fs.readFile(configPath, "utf-8");
51
- return ConfigSchema.parse(yaml.parse(configContent));
52
- }
53
-
54
- // Memory persistence for context retention
55
- interface MemoryStore {
56
- memories: Record<string, { value: string; timestamp: string }>;
57
- }
58
-
59
- async function getMemoryPath(cwd: string): Promise<string> {
60
- const rigourDir = path.join(cwd, ".rigour");
61
- await fs.ensureDir(rigourDir);
62
- return path.join(rigourDir, "memory.json");
63
- }
64
-
65
- async function loadMemory(cwd: string): Promise<MemoryStore> {
66
- const memPath = await getMemoryPath(cwd);
67
- if (await fs.pathExists(memPath)) {
68
- const content = await fs.readFile(memPath, "utf-8");
69
- return JSON.parse(content);
70
- }
71
- return { memories: {} };
72
- }
73
-
74
- async function saveMemory(cwd: string, store: MemoryStore): Promise<void> {
75
- const memPath = await getMemoryPath(cwd);
76
- await fs.writeFile(memPath, JSON.stringify(store, null, 2));
77
- }
78
-
79
- // Helper to log events for Rigour Studio
80
- async function logStudioEvent(cwd: string, event: any) {
81
- try {
82
- const rigourDir = path.join(cwd, ".rigour");
83
- await fs.ensureDir(rigourDir);
84
- const eventsPath = path.join(rigourDir, "events.jsonl");
85
- const logEntry = JSON.stringify({
86
- id: randomUUID(),
87
- timestamp: new Date().toISOString(),
88
- ...event
89
- }) + "\n";
90
- await fs.appendFile(eventsPath, logEntry);
91
- } catch {
92
- // Silent fail - Studio logging is non-blocking and zero-telemetry
93
- }
94
- }
95
-
96
- // Helper to parse diff and get modified lines per file
97
- function parseDiff(diff: string): Record<string, Set<number>> {
98
- const lines = diff.split('\n');
99
- const mapping: Record<string, Set<number>> = {};
100
- let currentFile = "";
101
- let currentLine = 0;
102
-
103
- for (const line of lines) {
104
- if (line.startsWith('+++ b/')) {
105
- currentFile = line.slice(6);
106
- mapping[currentFile] = new Set();
107
- } else if (line.startsWith('@@')) {
108
- const match = line.match(/\+(\d+)/);
109
- if (match) {
110
- currentLine = parseInt(match[1], 10);
111
- }
112
- } else if (line.startsWith('+') && !line.startsWith('+++')) {
113
- if (currentFile) {
114
- mapping[currentFile].add(currentLine);
115
- }
116
- currentLine++;
117
- } else if (!line.startsWith('-')) {
118
- currentLine++;
119
- }
120
- }
121
- return mapping;
122
- }
123
-
124
- server.setRequestHandler(ListToolsRequestSchema, async () => {
125
- return {
126
- tools: [
127
- {
128
- name: "rigour_check",
129
- description: "Run quality gate checks on the project. Matches the CLI 'check' command.",
130
- inputSchema: {
131
- type: "object",
132
- properties: {
133
- cwd: {
134
- type: "string",
135
- description: "Absolute path to the project root.",
136
- },
137
- },
138
- required: ["cwd"],
139
- },
140
- },
141
- {
142
- name: "rigour_explain",
143
- description: "Explain the last quality gate failures with actionable bullets. Matches the CLI 'explain' command.",
144
- inputSchema: {
145
- type: "object",
146
- properties: {
147
- cwd: {
148
- type: "string",
149
- description: "Absolute path to the project root.",
150
- },
151
- },
152
- required: ["cwd"],
153
- },
154
- },
155
- {
156
- name: "rigour_status",
157
- description: "Quick PASS/FAIL check with JSON-friendly output for polling current project state.",
158
- inputSchema: {
159
- type: "object",
160
- properties: {
161
- cwd: {
162
- type: "string",
163
- description: "Absolute path to the project root.",
164
- },
165
- },
166
- required: ["cwd"],
167
- },
168
- },
169
- {
170
- name: "rigour_get_fix_packet",
171
- description: "Retrieves a prioritized 'Fix Packet' (v2 schema) containing detailed machine-readable diagnostic data.",
172
- inputSchema: {
173
- type: "object",
174
- properties: {
175
- cwd: {
176
- type: "string",
177
- description: "Absolute path to the project root.",
178
- },
179
- },
180
- required: ["cwd"],
181
- },
182
- },
183
- {
184
- name: "rigour_list_gates",
185
- description: "Lists all configured quality gates and their thresholds for the current project.",
186
- inputSchema: {
187
- type: "object",
188
- properties: {
189
- cwd: {
190
- type: "string",
191
- description: "Absolute path to the project root.",
192
- },
193
- },
194
- required: ["cwd"],
195
- },
196
- },
197
- {
198
- name: "rigour_get_config",
199
- description: "Returns the current Rigour configuration (rigour.yml) for agent reasoning.",
200
- inputSchema: {
201
- type: "object",
202
- properties: {
203
- cwd: {
204
- type: "string",
205
- description: "Absolute path to the project root.",
206
- },
207
- },
208
- required: ["cwd"],
209
- },
210
- },
211
- {
212
- name: "rigour_remember",
213
- description: "Store a persistent instruction or context that the AI should remember across sessions. Use this to persist user preferences, project conventions, or critical instructions.",
214
- inputSchema: {
215
- type: "object",
216
- properties: {
217
- cwd: {
218
- type: "string",
219
- description: "Absolute path to the project root.",
220
- },
221
- key: {
222
- type: "string",
223
- description: "A unique key for this memory (e.g., 'user_preferences', 'coding_style').",
224
- },
225
- value: {
226
- type: "string",
227
- description: "The instruction or context to remember.",
228
- },
229
- },
230
- required: ["cwd", "key", "value"],
231
- },
232
- },
233
- {
234
- name: "rigour_recall",
235
- description: "Retrieve stored instructions or context. Call this at the start of each session to restore memory. Returns all stored memories if no key specified.",
236
- inputSchema: {
237
- type: "object",
238
- properties: {
239
- cwd: {
240
- type: "string",
241
- description: "Absolute path to the project root.",
242
- },
243
- key: {
244
- type: "string",
245
- description: "Optional. Key of specific memory to retrieve.",
246
- },
247
- },
248
- required: ["cwd"],
249
- },
250
- },
251
- {
252
- name: "rigour_forget",
253
- description: "Remove a stored memory by key.",
254
- inputSchema: {
255
- type: "object",
256
- properties: {
257
- cwd: {
258
- type: "string",
259
- description: "Absolute path to the project root.",
260
- },
261
- key: {
262
- type: "string",
263
- description: "Key of the memory to remove.",
264
- },
265
- },
266
- required: ["cwd", "key"],
267
- },
268
- },
269
- {
270
- name: "rigour_check_pattern",
271
- description: "Checks if a proposed code pattern (function, component, etc.) already exists, is stale, or has security vulnerabilities (CVEs). CALL THIS BEFORE CREATING NEW CODE.",
272
- inputSchema: {
273
- type: "object",
274
- properties: {
275
- cwd: {
276
- type: "string",
277
- description: "Absolute path to the project root.",
278
- },
279
- name: {
280
- type: "string",
281
- description: "The name of the function, class, or component you want to create.",
282
- },
283
- type: {
284
- type: "string",
285
- description: "The type of pattern (e.g., 'function', 'component', 'hook', 'type').",
286
- },
287
- intent: {
288
- type: "string",
289
- description: "What the code is for (e.g., 'format dates', 'user authentication').",
290
- },
291
- },
292
- required: ["cwd", "name"],
293
- },
294
- },
295
- {
296
- name: "rigour_security_audit",
297
- description: "Runs a live security audit (CVE check) on the project dependencies.",
298
- inputSchema: {
299
- type: "object",
300
- properties: {
301
- cwd: {
302
- type: "string",
303
- description: "Absolute path to the project root.",
304
- },
305
- },
306
- required: ["cwd"],
307
- },
308
- },
309
- {
310
- name: "rigour_run",
311
- description: "Execute a command under Rigour supervision. This tool can be INTERCEPTED and ARBITRATED by the Governance Studio.",
312
- inputSchema: {
313
- type: "object",
314
- properties: {
315
- cwd: {
316
- type: "string",
317
- description: "Absolute path to the project root.",
318
- },
319
- command: {
320
- type: "string",
321
- description: "The command to run (e.g., 'npm test', 'pytest').",
322
- },
323
- silent: {
324
- type: "boolean",
325
- description: "If true, hides the command output from the agent.",
326
- }
327
- },
328
- required: ["cwd", "command"],
329
- },
330
- },
331
- {
332
- name: "rigour_run_supervised",
333
- description: "Run a command under FULL Supervisor Mode. Iteratively executes the command, checks quality gates, and returns fix packets until PASS or max retries reached. Use this for self-healing agent loops.",
334
- inputSchema: {
335
- type: "object",
336
- properties: {
337
- cwd: {
338
- type: "string",
339
- description: "Absolute path to the project root.",
340
- },
341
- command: {
342
- type: "string",
343
- description: "The agent command to run (e.g., 'claude \"fix the bug\"', 'aider --message \"refactor auth\"').",
344
- },
345
- maxRetries: {
346
- type: "number",
347
- description: "Maximum retry iterations (default: 3).",
348
- },
349
- dryRun: {
350
- type: "boolean",
351
- description: "If true, simulates the loop without executing the command. Useful for testing gate checks.",
352
- },
353
- },
354
- required: ["cwd", "command"],
355
- },
356
- },
357
- // === FRONTIER MODEL TOOLS (v2.14+) ===
358
- // For Opus 4.6, GPT-5.3-Codex multi-agent and long-running sessions
359
- {
360
- name: "rigour_agent_register",
361
- description: "Register an agent in a multi-agent session. Use this at the START of agent execution to claim task scope and enable cross-agent conflict detection. Required for Agent Team Governance.",
362
- inputSchema: {
363
- type: "object",
364
- properties: {
365
- cwd: {
366
- type: "string",
367
- description: "Absolute path to the project root.",
368
- },
369
- agentId: {
370
- type: "string",
371
- description: "Unique identifier for this agent (e.g., 'agent-a', 'opus-frontend').",
372
- },
373
- taskScope: {
374
- type: "array",
375
- items: { type: "string" },
376
- description: "Glob patterns defining the files/directories this agent will work on (e.g., ['src/api/**', 'tests/api/**']).",
377
- },
378
- },
379
- required: ["cwd", "agentId", "taskScope"],
380
- },
381
- },
382
- {
383
- name: "rigour_checkpoint",
384
- description: "Record a quality checkpoint during long-running agent execution. Use periodically (every 15-30 min) to enable drift detection and quality monitoring. Essential for GPT-5.3 coworking mode.",
385
- inputSchema: {
386
- type: "object",
387
- properties: {
388
- cwd: {
389
- type: "string",
390
- description: "Absolute path to the project root.",
391
- },
392
- progressPct: {
393
- type: "number",
394
- description: "Estimated progress percentage (0-100).",
395
- },
396
- filesChanged: {
397
- type: "array",
398
- items: { type: "string" },
399
- description: "List of files modified since last checkpoint.",
400
- },
401
- summary: {
402
- type: "string",
403
- description: "Brief description of work done since last checkpoint.",
404
- },
405
- qualityScore: {
406
- type: "number",
407
- description: "Self-assessed quality score (0-100). Be honest - artificially high scores trigger drift detection.",
408
- },
409
- },
410
- required: ["cwd", "progressPct", "summary", "qualityScore"],
411
- },
412
- },
413
- {
414
- name: "rigour_handoff",
415
- description: "Handoff task to another agent in a multi-agent workflow. Use when delegating a subtask or completing your scope. Enables verified handoff governance.",
416
- inputSchema: {
417
- type: "object",
418
- properties: {
419
- cwd: {
420
- type: "string",
421
- description: "Absolute path to the project root.",
422
- },
423
- fromAgentId: {
424
- type: "string",
425
- description: "ID of the agent initiating the handoff.",
426
- },
427
- toAgentId: {
428
- type: "string",
429
- description: "ID of the agent receiving the handoff.",
430
- },
431
- taskDescription: {
432
- type: "string",
433
- description: "Description of the task being handed off.",
434
- },
435
- filesInScope: {
436
- type: "array",
437
- items: { type: "string" },
438
- description: "Files relevant to the handoff.",
439
- },
440
- context: {
441
- type: "string",
442
- description: "Additional context for the receiving agent.",
443
- },
444
- },
445
- required: ["cwd", "fromAgentId", "toAgentId", "taskDescription"],
446
- },
447
- },
448
- {
449
- name: "rigour_agent_deregister",
450
- description: "Deregister an agent from the multi-agent session. Use when an agent completes its work or needs to release its scope for another agent.",
451
- inputSchema: {
452
- type: "object",
453
- properties: {
454
- cwd: {
455
- type: "string",
456
- description: "Absolute path to the project root.",
457
- },
458
- agentId: {
459
- type: "string",
460
- description: "ID of the agent to deregister.",
461
- },
462
- },
463
- required: ["cwd", "agentId"],
464
- },
465
- },
466
- {
467
- name: "rigour_handoff_accept",
468
- description: "Accept a pending handoff from another agent. Use to formally acknowledge receipt of a task and verify you are the intended recipient.",
469
- inputSchema: {
470
- type: "object",
471
- properties: {
472
- cwd: {
473
- type: "string",
474
- description: "Absolute path to the project root.",
475
- },
476
- handoffId: {
477
- type: "string",
478
- description: "ID of the handoff to accept.",
479
- },
480
- agentId: {
481
- type: "string",
482
- description: "ID of the accepting agent (must match toAgentId in the handoff).",
483
- },
484
- },
485
- required: ["cwd", "handoffId", "agentId"],
486
- },
487
- },
488
- {
489
- name: "rigour_review",
490
- description: "Perform a high-fidelity code review on a pull request diff. Analyzes changed files using all active quality gates.",
491
- inputSchema: {
492
- type: "object",
493
- properties: {
494
- cwd: {
495
- type: "string",
496
- description: "Absolute path to the project root.",
497
- },
498
- repository: {
499
- type: "string",
500
- description: "Full repository name (e.g., 'owner/repo').",
501
- },
502
- branch: {
503
- type: "string",
504
- description: "The branch containing the changes.",
505
- },
506
- diff: {
507
- type: "string",
508
- description: "The git diff content to analyze.",
509
- },
510
- files: {
511
- type: "array",
512
- items: { type: "string" },
513
- description: "List of filenames that were changed.",
514
- },
515
- },
516
- required: ["cwd", "diff"],
517
- },
518
- }
519
- ],
520
- };
521
- });
522
-
523
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
524
- const { name, arguments: args } = request.params;
525
- const cwd = (args as any)?.cwd || process.cwd();
526
- const requestId = randomUUID();
527
-
528
- try {
529
- await logStudioEvent(cwd, {
530
- type: "tool_call",
531
- requestId,
532
- tool: name,
533
- arguments: args
534
- });
535
-
536
- const config = await loadConfig(cwd);
537
- const runner = new GateRunner(config);
538
-
539
- let result: any;
540
-
541
- switch (name) {
542
- case "rigour_check": {
543
- const report = await runner.run(cwd);
544
- result = {
545
- content: [
546
- {
547
- type: "text",
548
- text: `RIGOUR AUDIT RESULT: ${report.status}\n\nSummary:\n${Object.entries(report.summary).map(([k, v]) => `- ${k}: ${v}`).join("\n")}`,
549
- },
550
- ],
551
- };
552
-
553
- // Add the report to the tool_response log for high-fidelity Studio visualization
554
- (result as any)._rigour_report = report;
555
- break;
556
- }
557
-
558
- case "rigour_explain": {
559
- const report = await runner.run(cwd);
560
- if (report.status === "PASS") {
561
- result = {
562
- content: [
563
- {
564
- type: "text",
565
- text: "ALL QUALITY GATES PASSED. No failures to explain.",
566
- },
567
- ],
568
- };
569
- } else {
570
- const bullets = report.failures.map((f, i) => {
571
- return `${i + 1}. [${f.id.toUpperCase()}] ${f.title}: ${f.details}${f.hint ? ` (Hint: ${f.hint})` : ''}`;
572
- }).join("\n");
573
-
574
- result = {
575
- content: [
576
- {
577
- type: "text",
578
- text: `RIGOUR EXPLAIN:\n\n${bullets}`,
579
- },
580
- ],
581
- };
582
- }
583
- break;
584
- }
585
-
586
- case "rigour_status": {
587
- const report = await runner.run(cwd);
588
- result = {
589
- content: [
590
- {
591
- type: "text",
592
- text: JSON.stringify({
593
- status: report.status,
594
- summary: report.summary,
595
- failureCount: report.failures.length,
596
- durationMs: report.stats.duration_ms
597
- }, null, 2),
598
- },
599
- ],
600
- };
601
- break;
602
- }
603
-
604
- case "rigour_get_fix_packet": {
605
- const report = await runner.run(cwd);
606
- if (report.status === "PASS") {
607
- result = {
608
- content: [
609
- {
610
- type: "text",
611
- text: "ALL QUALITY GATES PASSED. The current state meets the required engineering standards.",
612
- },
613
- ],
614
- };
615
- } else {
616
- const packet = report.failures.map((f, i) => {
617
- let text = `FIX TASK ${i + 1}: [${f.id.toUpperCase()}] ${f.title}\n`;
618
- text += ` - CONTEXT: ${f.details}\n`;
619
- if (f.files && f.files.length > 0) {
620
- text += ` - TARGET FILES: ${f.files.join(", ")}\n`;
621
- }
622
- if (f.hint) {
623
- text += ` - REFACTORING GUIDANCE: ${f.hint}\n`;
624
- }
625
- return text;
626
- }).join("\n---\n");
627
-
628
- result = {
629
- content: [
630
- {
631
- type: "text",
632
- text: `ENGINEERING REFINEMENT REQUIRED:\n\nThe project state violated ${report.failures.length} quality gates. You MUST address these failures before declaring the task complete:\n\n${packet}`,
633
- },
634
- ],
635
- };
636
- }
637
- break;
638
- }
639
-
640
- case "rigour_list_gates":
641
- result = {
642
- content: [
643
- {
644
- type: "text",
645
- text: `ACTIVE QUALITY GATES:\n\n${Object.entries(config.gates).map(([k, v]) => {
646
- if (typeof v === 'object' && v !== null) {
647
- return `- ${k}: ${JSON.stringify(v)}`;
648
- }
649
- return `- ${k}: ${v}`;
650
- }).join("\n")}`,
651
- },
652
- ],
653
- };
654
- break;
655
-
656
- case "rigour_get_config":
657
- result = {
658
- content: [
659
- {
660
- type: "text",
661
- text: JSON.stringify(config, null, 2),
662
- },
663
- ],
664
- };
665
- break;
666
-
667
- case "rigour_remember": {
668
- const { key, value } = args as any;
669
- const store = await loadMemory(cwd);
670
- store.memories[key] = {
671
- value,
672
- timestamp: new Date().toISOString(),
673
- };
674
- await saveMemory(cwd, store);
675
- result = {
676
- content: [
677
- {
678
- type: "text",
679
- text: `MEMORY STORED: "${key}" has been saved. This instruction will persist across sessions.\n\nStored value: ${value}`,
680
- },
681
- ],
682
- };
683
- break;
684
- }
685
-
686
- case "rigour_recall": {
687
- const { key } = args as any;
688
- const store = await loadMemory(cwd);
689
-
690
- if (key) {
691
- const memory = store.memories[key];
692
- if (!memory) {
693
- result = {
694
- content: [
695
- {
696
- type: "text",
697
- text: `NO MEMORY FOUND for key "${key}". Use rigour_remember to store instructions.`,
698
- },
699
- ],
700
- };
701
- } else {
702
- result = {
703
- content: [
704
- {
705
- type: "text",
706
- text: `RECALLED MEMORY [${key}]:\n${memory.value}\n\n(Stored: ${memory.timestamp})`,
707
- },
708
- ],
709
- };
710
- }
711
- } else {
712
- const keys = Object.keys(store.memories);
713
- if (keys.length === 0) {
714
- result = {
715
- content: [
716
- {
717
- type: "text",
718
- text: "NO MEMORIES STORED. Use rigour_remember to persist important instructions.",
719
- },
720
- ],
721
- };
722
- } else {
723
- const allMemories = keys.map(k => {
724
- const mem = store.memories[k];
725
- return `## ${k}\n${mem.value}\n(Stored: ${mem.timestamp})`;
726
- }).join("\n\n---\n\n");
727
-
728
- result = {
729
- content: [
730
- {
731
- type: "text",
732
- text: `RECALLED ALL MEMORIES (${keys.length} items):\n\n${allMemories}\n\n---\nIMPORTANT: Follow these stored instructions throughout this session.`,
733
- },
734
- ],
735
- };
736
- }
737
- }
738
- break;
739
- }
740
-
741
- case "rigour_forget": {
742
- const { key } = args as any;
743
- const store = await loadMemory(cwd);
744
-
745
- if (!store.memories[key]) {
746
- result = {
747
- content: [
748
- {
749
- type: "text",
750
- text: `NO MEMORY FOUND for key "${key}". Nothing to forget.`,
751
- },
752
- ],
753
- };
754
- } else {
755
- delete store.memories[key];
756
- await saveMemory(cwd, store);
757
-
758
- result = {
759
- content: [
760
- {
761
- type: "text",
762
- text: `MEMORY DELETED: "${key}" has been removed.`,
763
- },
764
- ],
765
- };
766
- }
767
- break;
768
- }
769
-
770
- case "rigour_check_pattern": {
771
- const { name: patternName, type, intent } = args as any;
772
- const indexPath = getDefaultIndexPath(cwd);
773
- const index = await loadPatternIndex(indexPath);
774
-
775
- let resultText = "";
776
-
777
- // 1. Check for Reinvention
778
- if (index) {
779
- const matcher = new PatternMatcher(index);
780
- const matchResult = await matcher.match({ name: patternName, type, intent });
781
-
782
- if (matchResult.status === "FOUND_SIMILAR") {
783
- resultText += `🚨 PATTERN REINVENTION DETECTED\n`;
784
- resultText += `Similar pattern already exists: "${matchResult.matches[0].pattern.name}" in ${matchResult.matches[0].pattern.file}\n`;
785
- resultText += `SUGGESTION: ${matchResult.suggestion}\n\n`;
786
- }
787
- } else {
788
- resultText += `⚠️ Pattern index not found. Run 'rigour index' to enable reinvention detection.\n\n`;
789
- }
790
-
791
- // 2. Check for Staleness/Best Practices
792
- const detector = new StalenessDetector(cwd);
793
- const staleness = await detector.checkStaleness(`${type || 'function'} ${patternName} {}`);
794
-
795
- if (staleness.status !== "FRESH") {
796
- resultText += `⚠️ STALENESS/ANTI-PATTERN WARNING\n`;
797
- for (const issue of staleness.issues) {
798
- resultText += `- ${issue.reason}\n REPLACEMENT: ${issue.replacement}\n`;
799
- }
800
- resultText += `\n`;
801
- }
802
-
803
- // 3. Check Security for this library (if it's an import)
804
- if (intent && intent.includes('import')) {
805
- const security = new SecurityDetector(cwd);
806
- const audit = await security.runAudit();
807
- const relatedVulns = audit.vulnerabilities.filter(v =>
808
- patternName.toLowerCase().includes(v.packageName.toLowerCase()) ||
809
- intent.toLowerCase().includes(v.packageName.toLowerCase())
810
- );
811
-
812
- if (relatedVulns.length > 0) {
813
- resultText += `🛡️ SECURITY/CVE WARNING\n`;
814
- for (const v of relatedVulns) {
815
- resultText += `- [${v.severity.toUpperCase()}] ${v.packageName}: ${v.title} (${v.url})\n`;
816
- }
817
- resultText += `\n`;
818
- }
819
- }
820
-
821
- if (!resultText) {
822
- resultText = `✅ Pattern "${patternName}" is fresh, secure, and unique to the codebase.\n\nRECOMMENDED ACTION: Proceed with implementation.`;
823
- } else {
824
- let recommendation = "Proceed with caution, addressing the warnings above.";
825
- if (resultText.includes("🚨 PATTERN REINVENTION")) {
826
- recommendation = "STOP and REUSE the existing pattern mentioned above. Do not create a duplicate.";
827
- } else if (resultText.includes("🛡️ SECURITY/CVE WARNING")) {
828
- recommendation = "STOP and update your dependencies or find an alternative library. Do not proceed with vulnerable code.";
829
- } else if (resultText.includes("⚠️ STALENESS")) {
830
- recommendation = "Follow the replacement suggestion to ensure best practices.";
831
- }
832
-
833
- resultText += `\nRECOMMENDED ACTION: ${recommendation}`;
834
- }
835
-
836
- result = {
837
- content: [
838
- {
839
- type: "text",
840
- text: resultText,
841
- },
842
- ],
843
- };
844
- break;
845
- }
846
-
847
- case "rigour_security_audit": {
848
- const security = new SecurityDetector(cwd);
849
- const summary = await security.getSecuritySummary();
850
- result = {
851
- content: [
852
- {
853
- type: "text",
854
- text: summary,
855
- },
856
- ],
857
- };
858
- break;
859
- }
860
-
861
- case "rigour_run": {
862
- const { command } = args as any;
863
-
864
- // 1. Log Interceptable Event
865
- await logStudioEvent(cwd, {
866
- type: "interception_requested",
867
- requestId: requestId,
868
- tool: "rigour_run",
869
- command
870
- });
871
-
872
- // 2. Poll for Human Arbitration (Max 60s wait for this demo/test)
873
- // In production, this would be a blocking call wait
874
- console.error(`[RIGOUR] Waiting for human arbitration for command: ${command}`);
875
-
876
- const pollArbitration = async (rid: string, timeout: number): Promise<string | null> => {
877
- const start = Date.now();
878
- const eventsPath = path.join(cwd, '.rigour/events.jsonl');
879
- while (Date.now() - start < timeout) {
880
- if (await fs.pathExists(eventsPath)) {
881
- const content = await fs.readFile(eventsPath, 'utf-8');
882
- const lines = content.split('\n').filter(l => l.trim());
883
- for (const line of lines.reverse()) {
884
- const event = JSON.parse(line);
885
- if (event.tool === 'human_arbitration' && event.requestId === rid) {
886
- return event.decision;
887
- }
888
- }
889
- }
890
- await new Promise(r => setTimeout(r, 1000));
891
- }
892
- return "approve"; // Default to auto-approve if no human response (for non-blocking feel)
893
- };
894
-
895
- const decision = await pollArbitration(requestId, 60000);
896
-
897
- if (decision === 'reject') {
898
- result = {
899
- content: [
900
- {
901
- type: "text",
902
- text: `❌ COMMAND REJECTED BY GOVERNOR: The execution of "${command}" was blocked by a human operator in the Governance Studio.`,
903
- },
904
- ],
905
- isError: true
906
- };
907
- } else {
908
- // Execute
909
- const { execa } = await import("execa");
910
- try {
911
- const { stdout, stderr } = await execa(command, { shell: true, cwd });
912
- result = {
913
- content: [
914
- {
915
- type: "text",
916
- text: `✅ COMMAND EXECUTED (Approved by Governor):\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`,
917
- },
918
- ],
919
- };
920
- } catch (e: any) {
921
- result = {
922
- content: [
923
- {
924
- type: "text",
925
- text: `❌ COMMAND FAILED:\n\n${e.message}`,
926
- },
927
- ],
928
- isError: true
929
- };
930
- }
931
- }
932
- break;
933
- }
934
-
935
- case "rigour_run_supervised": {
936
- const { command, maxRetries = 3, dryRun = false } = args as any;
937
- const { execa } = await import("execa");
938
-
939
- let iteration = 0;
940
- let lastReport: Report | null = null;
941
- const iterations: { iteration: number; status: string; failures: number }[] = [];
942
-
943
- await logStudioEvent(cwd, {
944
- type: "supervisor_started",
945
- requestId,
946
- command,
947
- maxRetries,
948
- dryRun
949
- });
950
-
951
- while (iteration < maxRetries) {
952
- iteration++;
953
-
954
- // 1. Execute the agent command (skip in dryRun mode)
955
- if (!dryRun) {
956
- try {
957
- await execa(command, { shell: true, cwd });
958
- } catch (e: any) {
959
- // Command failure is OK - agent might have partial progress
960
- console.error(`[RIGOUR] Iteration ${iteration} command error: ${e.message}`);
961
- }
962
- } else {
963
- console.error(`[RIGOUR] Iteration ${iteration} (DRY RUN - skipping command execution)`);
964
- }
965
-
966
-
967
- // 2. Check quality gates
968
- lastReport = await runner.run(cwd);
969
- iterations.push({
970
- iteration,
971
- status: lastReport.status,
972
- failures: lastReport.failures.length
973
- });
974
-
975
- await logStudioEvent(cwd, {
976
- type: "supervisor_iteration",
977
- requestId,
978
- iteration,
979
- status: lastReport.status,
980
- failures: lastReport.failures.length
981
- });
982
-
983
- // 3. If PASS, we're done
984
- if (lastReport.status === "PASS") {
985
- result = {
986
- content: [
987
- {
988
- type: "text",
989
- text: `✅ SUPERVISOR MODE: PASSED on iteration ${iteration}/${maxRetries}\n\nIterations:\n${iterations.map(i => ` ${i.iteration}. ${i.status} (${i.failures} failures)`).join("\n")}\n\nAll quality gates have been satisfied.`,
990
- },
991
- ],
992
- };
993
- break;
994
- }
995
-
996
- // 4. If not at max retries, continue the loop (agent will use fix packet next iteration)
997
- if (iteration >= maxRetries) {
998
- // Final failure - return fix packet
999
- const fixPacket = lastReport.failures.map((f, i) => {
1000
- let text = `FIX TASK ${i + 1}: [${f.id.toUpperCase()}] ${f.title}\n`;
1001
- text += ` - CONTEXT: ${f.details}\n`;
1002
- if (f.files && f.files.length > 0) {
1003
- text += ` - TARGET FILES: ${f.files.join(", ")}\n`;
1004
- }
1005
- if (f.hint) {
1006
- text += ` - REFACTORING GUIDANCE: ${f.hint}\n`;
1007
- }
1008
- return text;
1009
- }).join("\n---\n");
1010
-
1011
- result = {
1012
- content: [
1013
- {
1014
- type: "text",
1015
- text: `❌ SUPERVISOR MODE: FAILED after ${iteration} iterations\n\nIterations:\n${iterations.map(i => ` ${i.iteration}. ${i.status} (${i.failures} failures)`).join("\n")}\n\nFINAL FIX PACKET:\n${fixPacket}`,
1016
- },
1017
- ],
1018
- isError: true
1019
- };
1020
- }
1021
- }
1022
-
1023
- await logStudioEvent(cwd, {
1024
- type: "supervisor_completed",
1025
- requestId,
1026
- finalStatus: lastReport?.status || "UNKNOWN",
1027
- totalIterations: iteration
1028
- });
1029
-
1030
- break;
1031
- }
1032
-
1033
- // === FRONTIER MODEL TOOL HANDLERS (v2.14+) ===
1034
-
1035
- case "rigour_agent_register": {
1036
- const { agentId, taskScope } = args as any;
1037
-
1038
- // Load or create agent session
1039
- const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
1040
- let session = { agents: [] as any[], startedAt: new Date().toISOString() };
1041
-
1042
- if (await fs.pathExists(sessionPath)) {
1043
- session = JSON.parse(await fs.readFile(sessionPath, 'utf-8'));
1044
- }
1045
-
1046
- // Check for existing agent
1047
- const existingIdx = session.agents.findIndex((a: any) => a.agentId === agentId);
1048
- if (existingIdx >= 0) {
1049
- session.agents[existingIdx] = {
1050
- agentId,
1051
- taskScope,
1052
- registeredAt: session.agents[existingIdx].registeredAt,
1053
- lastCheckpoint: new Date().toISOString(),
1054
- };
1055
- } else {
1056
- session.agents.push({
1057
- agentId,
1058
- taskScope,
1059
- registeredAt: new Date().toISOString(),
1060
- lastCheckpoint: new Date().toISOString(),
1061
- });
1062
- }
1063
-
1064
- // Check for scope conflicts
1065
- const conflicts: string[] = [];
1066
- for (const agent of session.agents) {
1067
- if (agent.agentId !== agentId) {
1068
- for (const scope of taskScope) {
1069
- if (agent.taskScope.includes(scope)) {
1070
- conflicts.push(`${agent.agentId} also claims "${scope}"`);
1071
- }
1072
- }
1073
- }
1074
- }
1075
-
1076
- await fs.ensureDir(path.join(cwd, '.rigour'));
1077
- await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
1078
-
1079
- await logStudioEvent(cwd, {
1080
- type: "agent_registered",
1081
- requestId,
1082
- agentId,
1083
- taskScope,
1084
- conflicts,
1085
- });
1086
-
1087
- let responseText = `✅ AGENT REGISTERED: "${agentId}" claimed scope: ${taskScope.join(', ')}\n\n`;
1088
- responseText += `Active agents in session: ${session.agents.length}\n`;
1089
-
1090
- if (conflicts.length > 0) {
1091
- responseText += `\n⚠️ SCOPE CONFLICTS DETECTED:\n${conflicts.map(c => ` - ${c}`).join('\n')}\n`;
1092
- responseText += `\nConsider coordinating with other agents or narrowing your scope.`;
1093
- }
1094
-
1095
- result = {
1096
- content: [{ type: "text", text: responseText }],
1097
- };
1098
- break;
1099
- }
1100
-
1101
- case "rigour_checkpoint": {
1102
- const { progressPct, filesChanged = [], summary, qualityScore } = args as any;
1103
-
1104
- // Load checkpoint session
1105
- const checkpointPath = path.join(cwd, '.rigour', 'checkpoint-session.json');
1106
- let session = {
1107
- sessionId: `chk-session-${Date.now()}`,
1108
- startedAt: new Date().toISOString(),
1109
- checkpoints: [] as any[],
1110
- status: 'active'
1111
- };
1112
-
1113
- if (await fs.pathExists(checkpointPath)) {
1114
- session = JSON.parse(await fs.readFile(checkpointPath, 'utf-8'));
1115
- }
1116
-
1117
- const checkpointId = `cp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1118
- const warnings: string[] = [];
1119
-
1120
- // Quality threshold check
1121
- if (qualityScore < 80) {
1122
- warnings.push(`Quality score ${qualityScore}% is below threshold 80%`);
1123
- }
1124
-
1125
- // Drift detection (quality degrading over time)
1126
- if (session.checkpoints.length >= 2) {
1127
- const recentScores = session.checkpoints.slice(-3).map((cp: any) => cp.qualityScore);
1128
- const avgRecent = recentScores.reduce((a: number, b: number) => a + b, 0) / recentScores.length;
1129
- if (qualityScore < avgRecent - 10) {
1130
- warnings.push(`Drift detected: quality dropped from avg ${avgRecent.toFixed(0)}% to ${qualityScore}%`);
1131
- }
1132
- }
1133
-
1134
- const checkpoint = {
1135
- checkpointId,
1136
- timestamp: new Date().toISOString(),
1137
- progressPct,
1138
- filesChanged,
1139
- summary,
1140
- qualityScore,
1141
- warnings,
1142
- };
1143
-
1144
- session.checkpoints.push(checkpoint);
1145
- await fs.ensureDir(path.join(cwd, '.rigour'));
1146
- await fs.writeFile(checkpointPath, JSON.stringify(session, null, 2));
1147
-
1148
- await logStudioEvent(cwd, {
1149
- type: "checkpoint_recorded",
1150
- requestId,
1151
- checkpointId,
1152
- progressPct,
1153
- qualityScore,
1154
- warnings,
1155
- });
1156
-
1157
- let responseText = `📍 CHECKPOINT RECORDED: ${checkpointId}\n\n`;
1158
- responseText += `Progress: ${progressPct}% | Quality: ${qualityScore}%\n`;
1159
- responseText += `Summary: ${summary}\n`;
1160
- responseText += `Total checkpoints: ${session.checkpoints.length}\n`;
1161
-
1162
- if (warnings.length > 0) {
1163
- responseText += `\n⚠️ WARNINGS:\n${warnings.map(w => ` - ${w}`).join('\n')}\n`;
1164
- if (qualityScore < 80) {
1165
- responseText += `\n⛔ QUALITY BELOW THRESHOLD: Consider pausing and reviewing recent work.`;
1166
- }
1167
- }
1168
-
1169
- const shouldContinue = qualityScore >= 80;
1170
- (result as any)._shouldContinue = shouldContinue;
1171
-
1172
- result = {
1173
- content: [{ type: "text", text: responseText }],
1174
- };
1175
- break;
1176
- }
1177
-
1178
- case "rigour_handoff": {
1179
- const { fromAgentId, toAgentId, taskDescription, filesInScope = [], context = '' } = args as any;
1180
-
1181
- const handoffId = `handoff-${Date.now()}`;
1182
- const handoffPath = path.join(cwd, '.rigour', 'handoffs.jsonl');
1183
-
1184
- const handoff = {
1185
- handoffId,
1186
- timestamp: new Date().toISOString(),
1187
- fromAgentId,
1188
- toAgentId,
1189
- taskDescription,
1190
- filesInScope,
1191
- context,
1192
- status: 'pending',
1193
- };
1194
-
1195
- await fs.ensureDir(path.join(cwd, '.rigour'));
1196
- await fs.appendFile(handoffPath, JSON.stringify(handoff) + '\n');
1197
-
1198
- await logStudioEvent(cwd, {
1199
- type: "handoff_initiated",
1200
- requestId,
1201
- handoffId,
1202
- fromAgentId,
1203
- toAgentId,
1204
- taskDescription,
1205
- });
1206
-
1207
- let responseText = `🤝 HANDOFF INITIATED: ${handoffId}\n\n`;
1208
- responseText += `From: ${fromAgentId} → To: ${toAgentId}\n`;
1209
- responseText += `Task: ${taskDescription}\n`;
1210
- if (filesInScope.length > 0) {
1211
- responseText += `Files in scope: ${filesInScope.join(', ')}\n`;
1212
- }
1213
- if (context) {
1214
- responseText += `Context: ${context}\n`;
1215
- }
1216
- responseText += `\nThe receiving agent should call rigour_agent_register to claim this scope.`;
1217
-
1218
- result = {
1219
- content: [{ type: "text", text: responseText }],
1220
- };
1221
- break;
1222
- }
1223
-
1224
- case "rigour_agent_deregister": {
1225
- const { agentId } = args as any;
1226
-
1227
- const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
1228
-
1229
- if (!await fs.pathExists(sessionPath)) {
1230
- result = {
1231
- content: [{ type: "text", text: `❌ No active agent session found.` }],
1232
- };
1233
- break;
1234
- }
1235
-
1236
- const session = JSON.parse(await fs.readFile(sessionPath, 'utf-8'));
1237
- const initialCount = session.agents.length;
1238
- session.agents = session.agents.filter((a: any) => a.agentId !== agentId);
1239
-
1240
- if (session.agents.length === initialCount) {
1241
- result = {
1242
- content: [{ type: "text", text: `❌ Agent "${agentId}" not found in session.` }],
1243
- };
1244
- break;
1245
- }
1246
-
1247
- await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
1248
-
1249
- await logStudioEvent(cwd, {
1250
- type: "agent_deregistered",
1251
- requestId,
1252
- agentId,
1253
- remainingAgents: session.agents.length,
1254
- });
1255
-
1256
- let responseText = `✅ AGENT DEREGISTERED: "${agentId}" has been removed from the session.\n\n`;
1257
- responseText += `Remaining agents: ${session.agents.length}\n`;
1258
- if (session.agents.length > 0) {
1259
- responseText += `Active: ${session.agents.map((a: any) => a.agentId).join(', ')}`;
1260
- }
1261
-
1262
- result = {
1263
- content: [{ type: "text", text: responseText }],
1264
- };
1265
- break;
1266
- }
1267
-
1268
- case "rigour_handoff_accept": {
1269
- const { handoffId, agentId } = args as any;
1270
-
1271
- const handoffPath = path.join(cwd, '.rigour', 'handoffs.jsonl');
1272
-
1273
- if (!await fs.pathExists(handoffPath)) {
1274
- result = {
1275
- content: [{ type: "text", text: `❌ No handoffs found.` }],
1276
- };
1277
- break;
1278
- }
1279
-
1280
- const content = await fs.readFile(handoffPath, 'utf-8');
1281
- const handoffs = content.trim().split('\n').filter(l => l).map(line => JSON.parse(line));
1282
-
1283
- const handoff = handoffs.find((h: any) => h.handoffId === handoffId);
1284
- if (!handoff) {
1285
- result = {
1286
- content: [{ type: "text", text: `❌ Handoff "${handoffId}" not found.` }],
1287
- };
1288
- break;
1289
- }
1290
-
1291
- if (handoff.toAgentId !== agentId) {
1292
- result = {
1293
- content: [{
1294
- type: "text",
1295
- text: `❌ Agent "${agentId}" is not the intended recipient.\nHandoff is for: ${handoff.toAgentId}`
1296
- }],
1297
- isError: true
1298
- };
1299
- break;
1300
- }
1301
-
1302
- handoff.status = 'accepted';
1303
- handoff.acceptedAt = new Date().toISOString();
1304
- handoff.acceptedBy = agentId;
1305
-
1306
- // Rewrite the file with updated handoff
1307
- const updatedContent = handoffs.map((h: any) => JSON.stringify(h)).join('\n') + '\n';
1308
- await fs.writeFile(handoffPath, updatedContent);
1309
-
1310
- await logStudioEvent(cwd, {
1311
- type: "handoff_accepted",
1312
- requestId,
1313
- handoffId,
1314
- acceptedBy: agentId,
1315
- fromAgentId: handoff.fromAgentId,
1316
- });
1317
-
1318
- let responseText = `✅ HANDOFF ACCEPTED: ${handoffId}\n\n`;
1319
- responseText += `From: ${handoff.fromAgentId}\n`;
1320
- responseText += `Task: ${handoff.taskDescription}\n`;
1321
- if (handoff.filesInScope?.length > 0) {
1322
- responseText += `Files in scope: ${handoff.filesInScope.join(', ')}\n`;
1323
- }
1324
- responseText += `\nYou should now call rigour_agent_register to formally claim the scope.`;
1325
-
1326
- result = {
1327
- content: [{ type: "text", text: responseText }],
1328
- };
1329
- break;
1330
- }
1331
-
1332
- case "rigour_review": {
1333
- const { diff, files: changedFiles } = args as any;
1334
-
1335
- // 1. Map diff to line numbers for filtering
1336
- const diffMapping = parseDiff(diff);
1337
- const targetFiles = changedFiles || Object.keys(diffMapping);
1338
-
1339
- // 2. Run high-fidelity analysis on changed files
1340
- const report = await runner.run(cwd, targetFiles);
1341
-
1342
- // 3. Filter failures to only those on changed lines (or global gate failures)
1343
- const filteredFailures = report.failures.filter(failure => {
1344
- // Global failures (no file associated) are always reported
1345
- if (!failure.files || failure.files.length === 0) return true;
1346
-
1347
- return failure.files.some(file => {
1348
- const fileModifiedLines = diffMapping[file];
1349
- // If we can't find the file in the diff, assume it's a side-effect or skip
1350
- if (!fileModifiedLines) return false;
1351
-
1352
- // If failure has line info, check if it's in modifiedLines
1353
- if (failure.line !== undefined) {
1354
- return fileModifiedLines.has(failure.line);
1355
- }
1356
-
1357
- // Fallback: if file is changed and no specific line is given, report it
1358
- return true;
1359
- });
1360
- });
1361
-
1362
- result = {
1363
- content: [
1364
- {
1365
- type: "text",
1366
- text: JSON.stringify({
1367
- status: filteredFailures.length > 0 ? "FAIL" : "PASS",
1368
- failures: filteredFailures.map(f => ({
1369
- id: f.id,
1370
- gate: f.title,
1371
- severity: "FAIL", // Rigour failures are FAIL by default
1372
- message: f.details,
1373
- file: f.files?.[0] || "",
1374
- line: f.line || 1,
1375
- suggestion: f.hint
1376
- }))
1377
- }),
1378
- },
1379
- ],
1380
- };
1381
- break;
1382
- }
1383
-
1384
- default:
1385
- throw new Error(`Unknown tool: ${name}`);
1386
- }
1387
-
1388
- await logStudioEvent(cwd, {
1389
- type: "tool_response",
1390
- requestId,
1391
- tool: name,
1392
- status: "success",
1393
- content: result.content,
1394
- _rigour_report: (result as any)._rigour_report
1395
- });
1396
-
1397
- return result;
1398
-
1399
- } catch (error: any) {
1400
- const errorResponse = {
1401
- content: [
1402
- {
1403
- type: "text",
1404
- text: `RIGOUR ERROR: ${error.message}`,
1405
- },
1406
- ],
1407
- isError: true,
1408
- };
1409
-
1410
- await logStudioEvent(cwd, {
1411
- type: "tool_response",
1412
- requestId,
1413
- tool: name,
1414
- status: "error",
1415
- error: error.message,
1416
- content: errorResponse.content
1417
- });
1418
-
1419
- return errorResponse;
1420
- }
1421
- });
1422
-
1423
- async function main() {
1424
- const transport = new StdioServerTransport();
1425
- await server.connect(transport);
1426
- console.error("Rigour MCP server v1.0.0 running on stdio");
1427
- }
1428
-
1429
- main().catch((error) => {
1430
- console.error("Fatal error in Rigour MCP server:", error);
1431
- process.exit(1);
1432
- });