@rigour-labs/mcp 2.17.2 → 2.18.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 CHANGED
@@ -299,6 +299,163 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
299
299
  },
300
300
  required: ["cwd", "command"],
301
301
  },
302
+ },
303
+ {
304
+ name: "rigour_run_supervised",
305
+ 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.",
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ cwd: {
310
+ type: "string",
311
+ description: "Absolute path to the project root.",
312
+ },
313
+ command: {
314
+ type: "string",
315
+ description: "The agent command to run (e.g., 'claude \"fix the bug\"', 'aider --message \"refactor auth\"').",
316
+ },
317
+ maxRetries: {
318
+ type: "number",
319
+ description: "Maximum retry iterations (default: 3).",
320
+ },
321
+ dryRun: {
322
+ type: "boolean",
323
+ description: "If true, simulates the loop without executing the command. Useful for testing gate checks.",
324
+ },
325
+ },
326
+ required: ["cwd", "command"],
327
+ },
328
+ },
329
+ // === FRONTIER MODEL TOOLS (v2.14+) ===
330
+ // For Opus 4.6, GPT-5.3-Codex multi-agent and long-running sessions
331
+ {
332
+ name: "rigour_agent_register",
333
+ 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.",
334
+ inputSchema: {
335
+ type: "object",
336
+ properties: {
337
+ cwd: {
338
+ type: "string",
339
+ description: "Absolute path to the project root.",
340
+ },
341
+ agentId: {
342
+ type: "string",
343
+ description: "Unique identifier for this agent (e.g., 'agent-a', 'opus-frontend').",
344
+ },
345
+ taskScope: {
346
+ type: "array",
347
+ items: { type: "string" },
348
+ description: "Glob patterns defining the files/directories this agent will work on (e.g., ['src/api/**', 'tests/api/**']).",
349
+ },
350
+ },
351
+ required: ["cwd", "agentId", "taskScope"],
352
+ },
353
+ },
354
+ {
355
+ name: "rigour_checkpoint",
356
+ 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.",
357
+ inputSchema: {
358
+ type: "object",
359
+ properties: {
360
+ cwd: {
361
+ type: "string",
362
+ description: "Absolute path to the project root.",
363
+ },
364
+ progressPct: {
365
+ type: "number",
366
+ description: "Estimated progress percentage (0-100).",
367
+ },
368
+ filesChanged: {
369
+ type: "array",
370
+ items: { type: "string" },
371
+ description: "List of files modified since last checkpoint.",
372
+ },
373
+ summary: {
374
+ type: "string",
375
+ description: "Brief description of work done since last checkpoint.",
376
+ },
377
+ qualityScore: {
378
+ type: "number",
379
+ description: "Self-assessed quality score (0-100). Be honest - artificially high scores trigger drift detection.",
380
+ },
381
+ },
382
+ required: ["cwd", "progressPct", "summary", "qualityScore"],
383
+ },
384
+ },
385
+ {
386
+ name: "rigour_handoff",
387
+ description: "Handoff task to another agent in a multi-agent workflow. Use when delegating a subtask or completing your scope. Enables verified handoff governance.",
388
+ inputSchema: {
389
+ type: "object",
390
+ properties: {
391
+ cwd: {
392
+ type: "string",
393
+ description: "Absolute path to the project root.",
394
+ },
395
+ fromAgentId: {
396
+ type: "string",
397
+ description: "ID of the agent initiating the handoff.",
398
+ },
399
+ toAgentId: {
400
+ type: "string",
401
+ description: "ID of the agent receiving the handoff.",
402
+ },
403
+ taskDescription: {
404
+ type: "string",
405
+ description: "Description of the task being handed off.",
406
+ },
407
+ filesInScope: {
408
+ type: "array",
409
+ items: { type: "string" },
410
+ description: "Files relevant to the handoff.",
411
+ },
412
+ context: {
413
+ type: "string",
414
+ description: "Additional context for the receiving agent.",
415
+ },
416
+ },
417
+ required: ["cwd", "fromAgentId", "toAgentId", "taskDescription"],
418
+ },
419
+ },
420
+ {
421
+ name: "rigour_agent_deregister",
422
+ 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.",
423
+ inputSchema: {
424
+ type: "object",
425
+ properties: {
426
+ cwd: {
427
+ type: "string",
428
+ description: "Absolute path to the project root.",
429
+ },
430
+ agentId: {
431
+ type: "string",
432
+ description: "ID of the agent to deregister.",
433
+ },
434
+ },
435
+ required: ["cwd", "agentId"],
436
+ },
437
+ },
438
+ {
439
+ name: "rigour_handoff_accept",
440
+ description: "Accept a pending handoff from another agent. Use to formally acknowledge receipt of a task and verify you are the intended recipient.",
441
+ inputSchema: {
442
+ type: "object",
443
+ properties: {
444
+ cwd: {
445
+ type: "string",
446
+ description: "Absolute path to the project root.",
447
+ },
448
+ handoffId: {
449
+ type: "string",
450
+ description: "ID of the handoff to accept.",
451
+ },
452
+ agentId: {
453
+ type: "string",
454
+ description: "ID of the accepting agent (must match toAgentId in the handoff).",
455
+ },
456
+ },
457
+ required: ["cwd", "handoffId", "agentId"],
458
+ },
302
459
  }
303
460
  ],
304
461
  };
@@ -716,6 +873,403 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
716
873
  break;
717
874
  }
718
875
 
876
+ case "rigour_run_supervised": {
877
+ const { command, maxRetries = 3, dryRun = false } = args as any;
878
+ const { execa } = await import("execa");
879
+
880
+ let iteration = 0;
881
+ let lastReport: Report | null = null;
882
+ const iterations: { iteration: number; status: string; failures: number }[] = [];
883
+
884
+ await logStudioEvent(cwd, {
885
+ type: "supervisor_started",
886
+ requestId,
887
+ command,
888
+ maxRetries,
889
+ dryRun
890
+ });
891
+
892
+ while (iteration < maxRetries) {
893
+ iteration++;
894
+
895
+ // 1. Execute the agent command (skip in dryRun mode)
896
+ if (!dryRun) {
897
+ try {
898
+ await execa(command, { shell: true, cwd });
899
+ } catch (e: any) {
900
+ // Command failure is OK - agent might have partial progress
901
+ console.error(`[RIGOUR] Iteration ${iteration} command error: ${e.message}`);
902
+ }
903
+ } else {
904
+ console.error(`[RIGOUR] Iteration ${iteration} (DRY RUN - skipping command execution)`);
905
+ }
906
+
907
+
908
+ // 2. Check quality gates
909
+ lastReport = await runner.run(cwd);
910
+ iterations.push({
911
+ iteration,
912
+ status: lastReport.status,
913
+ failures: lastReport.failures.length
914
+ });
915
+
916
+ await logStudioEvent(cwd, {
917
+ type: "supervisor_iteration",
918
+ requestId,
919
+ iteration,
920
+ status: lastReport.status,
921
+ failures: lastReport.failures.length
922
+ });
923
+
924
+ // 3. If PASS, we're done
925
+ if (lastReport.status === "PASS") {
926
+ result = {
927
+ content: [
928
+ {
929
+ type: "text",
930
+ 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.`,
931
+ },
932
+ ],
933
+ };
934
+ break;
935
+ }
936
+
937
+ // 4. If not at max retries, continue the loop (agent will use fix packet next iteration)
938
+ if (iteration >= maxRetries) {
939
+ // Final failure - return fix packet
940
+ const fixPacket = lastReport.failures.map((f, i) => {
941
+ let text = `FIX TASK ${i + 1}: [${f.id.toUpperCase()}] ${f.title}\n`;
942
+ text += ` - CONTEXT: ${f.details}\n`;
943
+ if (f.files && f.files.length > 0) {
944
+ text += ` - TARGET FILES: ${f.files.join(", ")}\n`;
945
+ }
946
+ if (f.hint) {
947
+ text += ` - REFACTORING GUIDANCE: ${f.hint}\n`;
948
+ }
949
+ return text;
950
+ }).join("\n---\n");
951
+
952
+ result = {
953
+ content: [
954
+ {
955
+ type: "text",
956
+ 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}`,
957
+ },
958
+ ],
959
+ isError: true
960
+ };
961
+ }
962
+ }
963
+
964
+ await logStudioEvent(cwd, {
965
+ type: "supervisor_completed",
966
+ requestId,
967
+ finalStatus: lastReport?.status || "UNKNOWN",
968
+ totalIterations: iteration
969
+ });
970
+
971
+ break;
972
+ }
973
+
974
+ // === FRONTIER MODEL TOOL HANDLERS (v2.14+) ===
975
+
976
+ case "rigour_agent_register": {
977
+ const { agentId, taskScope } = args as any;
978
+
979
+ // Load or create agent session
980
+ const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
981
+ let session = { agents: [] as any[], startedAt: new Date().toISOString() };
982
+
983
+ if (await fs.pathExists(sessionPath)) {
984
+ session = JSON.parse(await fs.readFile(sessionPath, 'utf-8'));
985
+ }
986
+
987
+ // Check for existing agent
988
+ const existingIdx = session.agents.findIndex((a: any) => a.agentId === agentId);
989
+ if (existingIdx >= 0) {
990
+ session.agents[existingIdx] = {
991
+ agentId,
992
+ taskScope,
993
+ registeredAt: session.agents[existingIdx].registeredAt,
994
+ lastCheckpoint: new Date().toISOString(),
995
+ };
996
+ } else {
997
+ session.agents.push({
998
+ agentId,
999
+ taskScope,
1000
+ registeredAt: new Date().toISOString(),
1001
+ lastCheckpoint: new Date().toISOString(),
1002
+ });
1003
+ }
1004
+
1005
+ // Check for scope conflicts
1006
+ const conflicts: string[] = [];
1007
+ for (const agent of session.agents) {
1008
+ if (agent.agentId !== agentId) {
1009
+ for (const scope of taskScope) {
1010
+ if (agent.taskScope.includes(scope)) {
1011
+ conflicts.push(`${agent.agentId} also claims "${scope}"`);
1012
+ }
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ await fs.ensureDir(path.join(cwd, '.rigour'));
1018
+ await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
1019
+
1020
+ await logStudioEvent(cwd, {
1021
+ type: "agent_registered",
1022
+ requestId,
1023
+ agentId,
1024
+ taskScope,
1025
+ conflicts,
1026
+ });
1027
+
1028
+ let responseText = `✅ AGENT REGISTERED: "${agentId}" claimed scope: ${taskScope.join(', ')}\n\n`;
1029
+ responseText += `Active agents in session: ${session.agents.length}\n`;
1030
+
1031
+ if (conflicts.length > 0) {
1032
+ responseText += `\n⚠️ SCOPE CONFLICTS DETECTED:\n${conflicts.map(c => ` - ${c}`).join('\n')}\n`;
1033
+ responseText += `\nConsider coordinating with other agents or narrowing your scope.`;
1034
+ }
1035
+
1036
+ result = {
1037
+ content: [{ type: "text", text: responseText }],
1038
+ };
1039
+ break;
1040
+ }
1041
+
1042
+ case "rigour_checkpoint": {
1043
+ const { progressPct, filesChanged = [], summary, qualityScore } = args as any;
1044
+
1045
+ // Load checkpoint session
1046
+ const checkpointPath = path.join(cwd, '.rigour', 'checkpoint-session.json');
1047
+ let session = {
1048
+ sessionId: `chk-session-${Date.now()}`,
1049
+ startedAt: new Date().toISOString(),
1050
+ checkpoints: [] as any[],
1051
+ status: 'active'
1052
+ };
1053
+
1054
+ if (await fs.pathExists(checkpointPath)) {
1055
+ session = JSON.parse(await fs.readFile(checkpointPath, 'utf-8'));
1056
+ }
1057
+
1058
+ const checkpointId = `cp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1059
+ const warnings: string[] = [];
1060
+
1061
+ // Quality threshold check
1062
+ if (qualityScore < 80) {
1063
+ warnings.push(`Quality score ${qualityScore}% is below threshold 80%`);
1064
+ }
1065
+
1066
+ // Drift detection (quality degrading over time)
1067
+ if (session.checkpoints.length >= 2) {
1068
+ const recentScores = session.checkpoints.slice(-3).map((cp: any) => cp.qualityScore);
1069
+ const avgRecent = recentScores.reduce((a: number, b: number) => a + b, 0) / recentScores.length;
1070
+ if (qualityScore < avgRecent - 10) {
1071
+ warnings.push(`Drift detected: quality dropped from avg ${avgRecent.toFixed(0)}% to ${qualityScore}%`);
1072
+ }
1073
+ }
1074
+
1075
+ const checkpoint = {
1076
+ checkpointId,
1077
+ timestamp: new Date().toISOString(),
1078
+ progressPct,
1079
+ filesChanged,
1080
+ summary,
1081
+ qualityScore,
1082
+ warnings,
1083
+ };
1084
+
1085
+ session.checkpoints.push(checkpoint);
1086
+ await fs.ensureDir(path.join(cwd, '.rigour'));
1087
+ await fs.writeFile(checkpointPath, JSON.stringify(session, null, 2));
1088
+
1089
+ await logStudioEvent(cwd, {
1090
+ type: "checkpoint_recorded",
1091
+ requestId,
1092
+ checkpointId,
1093
+ progressPct,
1094
+ qualityScore,
1095
+ warnings,
1096
+ });
1097
+
1098
+ let responseText = `📍 CHECKPOINT RECORDED: ${checkpointId}\n\n`;
1099
+ responseText += `Progress: ${progressPct}% | Quality: ${qualityScore}%\n`;
1100
+ responseText += `Summary: ${summary}\n`;
1101
+ responseText += `Total checkpoints: ${session.checkpoints.length}\n`;
1102
+
1103
+ if (warnings.length > 0) {
1104
+ responseText += `\n⚠️ WARNINGS:\n${warnings.map(w => ` - ${w}`).join('\n')}\n`;
1105
+ if (qualityScore < 80) {
1106
+ responseText += `\n⛔ QUALITY BELOW THRESHOLD: Consider pausing and reviewing recent work.`;
1107
+ }
1108
+ }
1109
+
1110
+ const shouldContinue = qualityScore >= 80;
1111
+ (result as any)._shouldContinue = shouldContinue;
1112
+
1113
+ result = {
1114
+ content: [{ type: "text", text: responseText }],
1115
+ };
1116
+ break;
1117
+ }
1118
+
1119
+ case "rigour_handoff": {
1120
+ const { fromAgentId, toAgentId, taskDescription, filesInScope = [], context = '' } = args as any;
1121
+
1122
+ const handoffId = `handoff-${Date.now()}`;
1123
+ const handoffPath = path.join(cwd, '.rigour', 'handoffs.jsonl');
1124
+
1125
+ const handoff = {
1126
+ handoffId,
1127
+ timestamp: new Date().toISOString(),
1128
+ fromAgentId,
1129
+ toAgentId,
1130
+ taskDescription,
1131
+ filesInScope,
1132
+ context,
1133
+ status: 'pending',
1134
+ };
1135
+
1136
+ await fs.ensureDir(path.join(cwd, '.rigour'));
1137
+ await fs.appendFile(handoffPath, JSON.stringify(handoff) + '\n');
1138
+
1139
+ await logStudioEvent(cwd, {
1140
+ type: "handoff_initiated",
1141
+ requestId,
1142
+ handoffId,
1143
+ fromAgentId,
1144
+ toAgentId,
1145
+ taskDescription,
1146
+ });
1147
+
1148
+ let responseText = `🤝 HANDOFF INITIATED: ${handoffId}\n\n`;
1149
+ responseText += `From: ${fromAgentId} → To: ${toAgentId}\n`;
1150
+ responseText += `Task: ${taskDescription}\n`;
1151
+ if (filesInScope.length > 0) {
1152
+ responseText += `Files in scope: ${filesInScope.join(', ')}\n`;
1153
+ }
1154
+ if (context) {
1155
+ responseText += `Context: ${context}\n`;
1156
+ }
1157
+ responseText += `\nThe receiving agent should call rigour_agent_register to claim this scope.`;
1158
+
1159
+ result = {
1160
+ content: [{ type: "text", text: responseText }],
1161
+ };
1162
+ break;
1163
+ }
1164
+
1165
+ case "rigour_agent_deregister": {
1166
+ const { agentId } = args as any;
1167
+
1168
+ const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
1169
+
1170
+ if (!await fs.pathExists(sessionPath)) {
1171
+ result = {
1172
+ content: [{ type: "text", text: `❌ No active agent session found.` }],
1173
+ };
1174
+ break;
1175
+ }
1176
+
1177
+ const session = JSON.parse(await fs.readFile(sessionPath, 'utf-8'));
1178
+ const initialCount = session.agents.length;
1179
+ session.agents = session.agents.filter((a: any) => a.agentId !== agentId);
1180
+
1181
+ if (session.agents.length === initialCount) {
1182
+ result = {
1183
+ content: [{ type: "text", text: `❌ Agent "${agentId}" not found in session.` }],
1184
+ };
1185
+ break;
1186
+ }
1187
+
1188
+ await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
1189
+
1190
+ await logStudioEvent(cwd, {
1191
+ type: "agent_deregistered",
1192
+ requestId,
1193
+ agentId,
1194
+ remainingAgents: session.agents.length,
1195
+ });
1196
+
1197
+ let responseText = `✅ AGENT DEREGISTERED: "${agentId}" has been removed from the session.\n\n`;
1198
+ responseText += `Remaining agents: ${session.agents.length}\n`;
1199
+ if (session.agents.length > 0) {
1200
+ responseText += `Active: ${session.agents.map((a: any) => a.agentId).join(', ')}`;
1201
+ }
1202
+
1203
+ result = {
1204
+ content: [{ type: "text", text: responseText }],
1205
+ };
1206
+ break;
1207
+ }
1208
+
1209
+ case "rigour_handoff_accept": {
1210
+ const { handoffId, agentId } = args as any;
1211
+
1212
+ const handoffPath = path.join(cwd, '.rigour', 'handoffs.jsonl');
1213
+
1214
+ if (!await fs.pathExists(handoffPath)) {
1215
+ result = {
1216
+ content: [{ type: "text", text: `❌ No handoffs found.` }],
1217
+ };
1218
+ break;
1219
+ }
1220
+
1221
+ const content = await fs.readFile(handoffPath, 'utf-8');
1222
+ const handoffs = content.trim().split('\n').filter(l => l).map(line => JSON.parse(line));
1223
+
1224
+ const handoff = handoffs.find((h: any) => h.handoffId === handoffId);
1225
+ if (!handoff) {
1226
+ result = {
1227
+ content: [{ type: "text", text: `❌ Handoff "${handoffId}" not found.` }],
1228
+ };
1229
+ break;
1230
+ }
1231
+
1232
+ if (handoff.toAgentId !== agentId) {
1233
+ result = {
1234
+ content: [{
1235
+ type: "text",
1236
+ text: `❌ Agent "${agentId}" is not the intended recipient.\nHandoff is for: ${handoff.toAgentId}`
1237
+ }],
1238
+ isError: true
1239
+ };
1240
+ break;
1241
+ }
1242
+
1243
+ handoff.status = 'accepted';
1244
+ handoff.acceptedAt = new Date().toISOString();
1245
+ handoff.acceptedBy = agentId;
1246
+
1247
+ // Rewrite the file with updated handoff
1248
+ const updatedContent = handoffs.map((h: any) => JSON.stringify(h)).join('\n') + '\n';
1249
+ await fs.writeFile(handoffPath, updatedContent);
1250
+
1251
+ await logStudioEvent(cwd, {
1252
+ type: "handoff_accepted",
1253
+ requestId,
1254
+ handoffId,
1255
+ acceptedBy: agentId,
1256
+ fromAgentId: handoff.fromAgentId,
1257
+ });
1258
+
1259
+ let responseText = `✅ HANDOFF ACCEPTED: ${handoffId}\n\n`;
1260
+ responseText += `From: ${handoff.fromAgentId}\n`;
1261
+ responseText += `Task: ${handoff.taskDescription}\n`;
1262
+ if (handoff.filesInScope?.length > 0) {
1263
+ responseText += `Files in scope: ${handoff.filesInScope.join(', ')}\n`;
1264
+ }
1265
+ responseText += `\nYou should now call rigour_agent_register to formally claim the scope.`;
1266
+
1267
+ result = {
1268
+ content: [{ type: "text", text: responseText }],
1269
+ };
1270
+ break;
1271
+ }
1272
+
719
1273
  default:
720
1274
  throw new Error(`Unknown tool: ${name}`);
721
1275
  }