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