@rigour-labs/mcp 5.2.9 → 5.3.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/dist/index.js +57 -6
- package/dist/tools/agent-handlers.d.ts +3 -11
- package/dist/tools/agent-handlers.js +171 -11
- package/dist/tools/context-handlers.d.ts +16 -0
- package/dist/tools/context-handlers.js +217 -0
- package/dist/tools/definitions.d.ts +120 -0
- package/dist/tools/definitions.js +124 -3
- package/dist/tools/index-handlers.d.ts +6 -0
- package/dist/tools/index-handlers.js +95 -0
- package/dist/tools/memory-handlers.d.ts +1 -7
- package/dist/tools/memory-handlers.js +64 -5
- package/dist/tools/pattern-handlers.d.ts +1 -7
- package/dist/tools/pattern-handlers.js +71 -7
- package/dist/utils/context-footer.d.ts +6 -0
- package/dist/utils/context-footer.js +19 -0
- package/dist/utils/context-telemetry.d.ts +27 -0
- package/dist/utils/context-telemetry.js +34 -0
- package/package.json +75 -75
- package/LICENSE +0 -21
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -254
- package/dist/smoke.test.d.ts +0 -1
- package/dist/smoke.test.js +0 -6
- package/dist/supervisor.test.d.ts +0 -1
- package/dist/supervisor.test.js +0 -128
- package/dist/tools/deep-handlers.test.d.ts +0 -1
- package/dist/tools/deep-handlers.test.js +0 -48
- package/dist/tools/mcp-settings-handler.test.d.ts +0 -1
- package/dist/tools/mcp-settings-handler.test.js +0 -31
- package/dist/tools/quality-handlers.test.d.ts +0 -1
- package/dist/tools/quality-handlers.test.js +0 -108
package/dist/index.js
CHANGED
|
@@ -29,6 +29,9 @@ import { handleReview } from './tools/review-handler.js';
|
|
|
29
29
|
import { handleHooksCheck, handleHooksInit } from './tools/hooks-handler.js';
|
|
30
30
|
import { handleCheckDeep, handleDeepStats } from './tools/deep-handlers.js';
|
|
31
31
|
import { handleMcpGetSettings, handleMcpSetSettings } from './tools/mcp-settings-handler.js';
|
|
32
|
+
import { handleContextStats, handleTaskCost, handleCacheStats, handleContextExplain, handleContextScope } from './tools/context-handlers.js';
|
|
33
|
+
import { handleIndex } from './tools/index-handlers.js';
|
|
34
|
+
import { recordContextEvent, estimateTokenCount } from '@rigour-labs/core';
|
|
32
35
|
// ─── Server Setup ─────────────────────────────────────────────────
|
|
33
36
|
const server = new Server({ name: "rigour-mcp", version: "3.0.1" }, { capabilities: { tools: {}, prompts: {}, logging: {}, resources: {} } });
|
|
34
37
|
// Bind server for logging notifications from handlers
|
|
@@ -38,15 +41,21 @@ bindServer(server);
|
|
|
38
41
|
// Research shows agents degrade at 30+ tools (wrong picks, hallucinated args).
|
|
39
42
|
// Power-user tools are still callable — they just aren't advertised in the tool list.
|
|
40
43
|
const ESSENTIAL_TOOLS = new Set([
|
|
41
|
-
'rigour_check', // Run quality gates (BEFORE declaring done)
|
|
42
|
-
'rigour_check_pattern', // Check if code exists (BEFORE creating new code)
|
|
43
44
|
'rigour_recall', // Load project memory (START of every task)
|
|
45
|
+
'rigour_index', // Build/refresh pattern index (if stale)
|
|
46
|
+
'rigour_context_scope', // Scoped file retrieval BEFORE reading files
|
|
47
|
+
'rigour_check_pattern', // Check if code exists (BEFORE creating new code)
|
|
48
|
+
'rigour_check', // Run quality gates (BEFORE declaring done)
|
|
44
49
|
'rigour_remember', // Store conventions/decisions
|
|
45
50
|
'rigour_explain', // Explain gate failures
|
|
46
51
|
'rigour_get_fix_packet', // Get structured fix instructions on FAIL
|
|
47
52
|
'rigour_review', // Review diffs
|
|
48
53
|
'rigour_security_audit', // CVE check
|
|
49
54
|
'rigour_forget', // Remove stored memory
|
|
55
|
+
'rigour_context_stats', // Context retrieval efficiency & avoided tokens
|
|
56
|
+
'rigour_task_cost', // Verified vs estimated avoided costs
|
|
57
|
+
'rigour_cache_stats', // 4-layer cache statistics
|
|
58
|
+
'rigour_context_explain', // Explain context inclusion/exclusion audit
|
|
50
59
|
]);
|
|
51
60
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
52
61
|
tools: TOOL_DEFINITIONS.filter(t => ESSENTIAL_TOOLS.has(t.name)),
|
|
@@ -154,13 +163,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
154
163
|
result = await handleAgentRegister(cwd, args.agentId, args.taskScope, requestId);
|
|
155
164
|
break;
|
|
156
165
|
case "rigour_checkpoint": {
|
|
157
|
-
const { progressPct, filesChanged = [], summary, qualityScore } = args;
|
|
158
|
-
result = await handleCheckpoint(cwd, progressPct, filesChanged, summary, qualityScore, requestId);
|
|
166
|
+
const { progressPct, filesChanged = [], summary, qualityScore, agentId, taskId } = args;
|
|
167
|
+
result = await handleCheckpoint(cwd, progressPct, filesChanged, summary, qualityScore, requestId, agentId, taskId);
|
|
159
168
|
break;
|
|
160
169
|
}
|
|
161
170
|
case "rigour_handoff": {
|
|
162
|
-
const { fromAgentId, toAgentId, taskDescription, filesInScope = [], context = '' } = args;
|
|
163
|
-
result = await handleHandoff(cwd, fromAgentId, toAgentId, taskDescription, filesInScope, context, requestId);
|
|
171
|
+
const { fromAgentId, toAgentId, taskDescription, filesInScope = [], context = '', taskId } = args;
|
|
172
|
+
result = await handleHandoff(cwd, fromAgentId, toAgentId, taskDescription, filesInScope, context, requestId, taskId);
|
|
164
173
|
break;
|
|
165
174
|
}
|
|
166
175
|
case "rigour_agent_deregister":
|
|
@@ -189,9 +198,51 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
189
198
|
case "rigour_review":
|
|
190
199
|
result = await handleReview(runner, cwd, args.diff, args.files);
|
|
191
200
|
break;
|
|
201
|
+
// Context Telemetry & Cost Tools
|
|
202
|
+
case "rigour_context_stats":
|
|
203
|
+
result = await handleContextStats(cwd, args.taskId);
|
|
204
|
+
break;
|
|
205
|
+
case "rigour_task_cost":
|
|
206
|
+
result = await handleTaskCost(cwd, args.taskId);
|
|
207
|
+
break;
|
|
208
|
+
case "rigour_cache_stats":
|
|
209
|
+
result = await handleCacheStats(cwd);
|
|
210
|
+
break;
|
|
211
|
+
case "rigour_context_explain":
|
|
212
|
+
result = await handleContextExplain(cwd, args.target, args.taskId);
|
|
213
|
+
break;
|
|
214
|
+
// Pattern index & scoped context
|
|
215
|
+
case "rigour_index": {
|
|
216
|
+
const { semantic, force, output } = args;
|
|
217
|
+
result = await handleIndex(cwd, { semantic, force, output });
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "rigour_context_scope": {
|
|
221
|
+
const { query, limit } = args;
|
|
222
|
+
result = await handleContextScope(cwd, query, limit);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
192
225
|
default:
|
|
193
226
|
throw new Error(`Unknown tool: ${name}`);
|
|
194
227
|
}
|
|
228
|
+
// ── Context Telemetry Recording (honest — from handler _telemetry metadata) ──
|
|
229
|
+
try {
|
|
230
|
+
const telemetry = result?._telemetry;
|
|
231
|
+
const resultText = Array.isArray(result?.content) ? result.content.map((c) => c.text || '').join('\n') : '';
|
|
232
|
+
const returnedTokens = telemetry?.returnedTokens ?? estimateTokenCount(resultText);
|
|
233
|
+
await recordContextEvent({
|
|
234
|
+
taskId: args?.taskId || args?.task_id,
|
|
235
|
+
agentId: args?.agentId || args?.agent_id,
|
|
236
|
+
toolName: name,
|
|
237
|
+
cacheStatus: telemetry?.cacheStatus ?? 'none',
|
|
238
|
+
candidateTokens: telemetry?.candidateTokens ?? returnedTokens,
|
|
239
|
+
returnedTokens,
|
|
240
|
+
deduplicatedTokens: telemetry?.deduplicatedTokens ?? 0,
|
|
241
|
+
}, cwd);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// non-blocking context telemetry logging
|
|
245
|
+
}
|
|
195
246
|
// ── Prepend image DLP warning if image content was detected ──
|
|
196
247
|
if (hasImageContent && Array.isArray(result.content)) {
|
|
197
248
|
result.content.unshift({
|
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
type ToolResult
|
|
2
|
-
content: {
|
|
3
|
-
type: string;
|
|
4
|
-
text: string;
|
|
5
|
-
}[];
|
|
6
|
-
isError?: boolean;
|
|
7
|
-
_shouldContinue?: boolean;
|
|
8
|
-
};
|
|
1
|
+
import { type ToolResult } from '../utils/context-telemetry.js';
|
|
9
2
|
export declare function handleAgentRegister(cwd: string, agentId: string, taskScope: string[], requestId: string): Promise<ToolResult>;
|
|
10
|
-
export declare function handleCheckpoint(cwd: string, progressPct: number, filesChanged: string[], summary: string, qualityScore: number, requestId: string): Promise<ToolResult>;
|
|
11
|
-
export declare function handleHandoff(cwd: string, fromAgentId: string, toAgentId: string, taskDescription: string, filesInScope: string[], context: string, requestId: string): Promise<ToolResult>;
|
|
3
|
+
export declare function handleCheckpoint(cwd: string, progressPct: number, filesChanged: string[], summary: string, qualityScore: number, requestId: string, agentId?: string, taskId?: string): Promise<ToolResult>;
|
|
4
|
+
export declare function handleHandoff(cwd: string, fromAgentId: string, toAgentId: string, taskDescription: string, filesInScope: string[], context: string, requestId: string, taskId?: string): Promise<ToolResult>;
|
|
12
5
|
export declare function handleAgentDeregister(cwd: string, agentId: string, requestId: string): Promise<ToolResult>;
|
|
13
6
|
export declare function handleHandoffAccept(cwd: string, handoffId: string, agentId: string, requestId: string): Promise<ToolResult>;
|
|
14
|
-
export {};
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
import fs from "fs-extra";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { logStudioEvent } from '../utils/config.js';
|
|
12
|
+
import { setTaskCheckpointCache, getTaskCheckpointCache, estimateTokenCount, } from '@rigour-labs/core';
|
|
13
|
+
import { PatternIndexer, savePatternIndex, loadPatternIndex, getDefaultIndexPath, } from '@rigour-labs/core/pattern-index';
|
|
14
|
+
import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
|
|
15
|
+
import { appendContextFooter } from '../utils/context-footer.js';
|
|
16
|
+
const HANDOFF_CONTEXT_TOKEN_LIMIT = 2000;
|
|
12
17
|
function parseJsonOrNull(raw) {
|
|
13
18
|
try {
|
|
14
19
|
return JSON.parse(raw);
|
|
@@ -17,6 +22,64 @@ function parseJsonOrNull(raw) {
|
|
|
17
22
|
return null;
|
|
18
23
|
}
|
|
19
24
|
}
|
|
25
|
+
function globMatchesFile(filePath, glob) {
|
|
26
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
27
|
+
const pattern = glob
|
|
28
|
+
.replace(/\\/g, '/')
|
|
29
|
+
.replace(/\./g, '\\.')
|
|
30
|
+
.replace(/\*\*/g, '§§')
|
|
31
|
+
.replace(/\*/g, '[^/]*')
|
|
32
|
+
.replace(/§§/g, '.*');
|
|
33
|
+
if (new RegExp(`^${pattern}$`).test(normalized))
|
|
34
|
+
return true;
|
|
35
|
+
const prefix = glob.replace('/**', '').replace('/*', '').replace(/\*/g, '');
|
|
36
|
+
return normalized.startsWith(prefix);
|
|
37
|
+
}
|
|
38
|
+
async function refreshIndexForFiles(cwd, filesChanged) {
|
|
39
|
+
if (filesChanged.length === 0)
|
|
40
|
+
return null;
|
|
41
|
+
const indexPath = getDefaultIndexPath(cwd);
|
|
42
|
+
const existingIndex = await loadPatternIndex(indexPath);
|
|
43
|
+
if (!existingIndex) {
|
|
44
|
+
return 'Index not found — run rigour_index to enable incremental updates.';
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const indexer = new PatternIndexer(cwd, { useEmbeddings: existingIndex.patterns.some(p => p.embedding?.length) });
|
|
48
|
+
const updated = await indexer.updateIndex(existingIndex);
|
|
49
|
+
await savePatternIndex(updated, indexPath);
|
|
50
|
+
return `Index refreshed for ${filesChanged.length} changed file(s) — ${updated.stats.totalPatterns} patterns total.`;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return `Index refresh failed: ${error.message}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function validateTaskScopeAgainstIndex(cwd, taskScope) {
|
|
57
|
+
const index = await loadPatternIndex(getDefaultIndexPath(cwd));
|
|
58
|
+
if (!index) {
|
|
59
|
+
return { matchedFiles: [], unmatchedGlobs: taskScope, suggestions: ['Run rigour_index first to bind scope to indexed files.'] };
|
|
60
|
+
}
|
|
61
|
+
const indexedFiles = index.files.map(f => f.path);
|
|
62
|
+
const matchedFiles = [];
|
|
63
|
+
const unmatchedGlobs = [];
|
|
64
|
+
for (const glob of taskScope) {
|
|
65
|
+
const matches = indexedFiles.filter(f => globMatchesFile(f, glob));
|
|
66
|
+
if (matches.length > 0) {
|
|
67
|
+
matchedFiles.push(...matches);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
unmatchedGlobs.push(glob);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const uniqueMatched = Array.from(new Set(matchedFiles));
|
|
74
|
+
const suggestions = [];
|
|
75
|
+
if (uniqueMatched.length > 0 && uniqueMatched.length <= 8) {
|
|
76
|
+
suggestions.push(`Minimal scope: ${uniqueMatched.slice(0, 8).join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
if (unmatchedGlobs.length > 0) {
|
|
79
|
+
suggestions.push(`Unmatched globs (not in index): ${unmatchedGlobs.join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
return { matchedFiles: uniqueMatched, unmatchedGlobs, suggestions };
|
|
82
|
+
}
|
|
20
83
|
// ─── Agent Register ───────────────────────────────────────────────
|
|
21
84
|
export async function handleAgentRegister(cwd, agentId, taskScope, requestId) {
|
|
22
85
|
const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
|
|
@@ -27,12 +90,17 @@ export async function handleAgentRegister(cwd, agentId, taskScope, requestId) {
|
|
|
27
90
|
session = parsed;
|
|
28
91
|
}
|
|
29
92
|
}
|
|
93
|
+
const indexBinding = await validateTaskScopeAgainstIndex(cwd, taskScope);
|
|
30
94
|
const existingIdx = session.agents.findIndex((a) => a.agentId === agentId);
|
|
31
95
|
if (existingIdx >= 0) {
|
|
32
96
|
session.agents[existingIdx] = {
|
|
33
97
|
agentId, taskScope,
|
|
34
98
|
registeredAt: session.agents[existingIdx].registeredAt,
|
|
35
99
|
lastCheckpoint: new Date().toISOString(),
|
|
100
|
+
indexBinding: {
|
|
101
|
+
matchedFiles: indexBinding.matchedFiles.length,
|
|
102
|
+
unmatchedGlobs: indexBinding.unmatchedGlobs,
|
|
103
|
+
},
|
|
36
104
|
};
|
|
37
105
|
}
|
|
38
106
|
else {
|
|
@@ -40,6 +108,10 @@ export async function handleAgentRegister(cwd, agentId, taskScope, requestId) {
|
|
|
40
108
|
agentId, taskScope,
|
|
41
109
|
registeredAt: new Date().toISOString(),
|
|
42
110
|
lastCheckpoint: new Date().toISOString(),
|
|
111
|
+
indexBinding: {
|
|
112
|
+
matchedFiles: indexBinding.matchedFiles.length,
|
|
113
|
+
unmatchedGlobs: indexBinding.unmatchedGlobs,
|
|
114
|
+
},
|
|
43
115
|
});
|
|
44
116
|
}
|
|
45
117
|
// Scope conflict detection
|
|
@@ -55,17 +127,34 @@ export async function handleAgentRegister(cwd, agentId, taskScope, requestId) {
|
|
|
55
127
|
}
|
|
56
128
|
await fs.ensureDir(path.join(cwd, '.rigour'));
|
|
57
129
|
await fs.writeFile(sessionPath, JSON.stringify(session, null, 2));
|
|
58
|
-
await logStudioEvent(cwd, { type: "agent_registered", requestId, agentId, taskScope, conflicts });
|
|
130
|
+
await logStudioEvent(cwd, { type: "agent_registered", requestId, agentId, taskScope, conflicts, indexBinding });
|
|
59
131
|
let text = `✅ AGENT REGISTERED: "${agentId}" claimed scope: ${taskScope.join(', ')}\n\n`;
|
|
60
132
|
text += `Active agents in session: ${session.agents.length}\n`;
|
|
133
|
+
if (indexBinding.matchedFiles.length > 0) {
|
|
134
|
+
text += `\n📎 Index binding: ${indexBinding.matchedFiles.length} indexed file(s) matched.\n`;
|
|
135
|
+
if (indexBinding.suggestions.length > 0) {
|
|
136
|
+
text += indexBinding.suggestions.map(s => ` - ${s}`).join('\n') + '\n';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (indexBinding.suggestions.length > 0) {
|
|
140
|
+
text += `\n⚠️ ${indexBinding.suggestions.join(' ')}\n`;
|
|
141
|
+
}
|
|
61
142
|
if (conflicts.length > 0) {
|
|
62
143
|
text += `\n⚠️ SCOPE CONFLICTS DETECTED:\n${conflicts.map(c => ` - ${c}`).join('\n')}\n`;
|
|
63
144
|
text += `\nConsider coordinating with other agents or narrowing your scope.`;
|
|
64
145
|
}
|
|
65
|
-
|
|
146
|
+
const telemetry = buildTelemetryMeta({
|
|
147
|
+
candidateText: taskScope.join(' '),
|
|
148
|
+
returnedText: text,
|
|
149
|
+
cacheStatus: 'none',
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: appendContextFooter(text, telemetry, 'rigour_recall then rigour_context_scope') }],
|
|
153
|
+
_telemetry: telemetry,
|
|
154
|
+
};
|
|
66
155
|
}
|
|
67
156
|
// ─── Checkpoint ───────────────────────────────────────────────────
|
|
68
|
-
export async function handleCheckpoint(cwd, progressPct, filesChanged, summary, qualityScore, requestId) {
|
|
157
|
+
export async function handleCheckpoint(cwd, progressPct, filesChanged, summary, qualityScore, requestId, agentId = 'default-agent', taskId = 'default-task') {
|
|
69
158
|
const checkpointPath = path.join(cwd, '.rigour', 'checkpoint-session.json');
|
|
70
159
|
let session = {
|
|
71
160
|
sessionId: `chk-session-${Date.now()}`,
|
|
@@ -92,6 +181,8 @@ export async function handleCheckpoint(cwd, progressPct, filesChanged, summary,
|
|
|
92
181
|
warnings.push(`Drift detected: quality dropped from avg ${avgRecent.toFixed(0)}% to ${qualityScore}%`);
|
|
93
182
|
}
|
|
94
183
|
}
|
|
184
|
+
const rawStateText = JSON.stringify(session);
|
|
185
|
+
const phase = `progress-${progressPct}`;
|
|
95
186
|
session.checkpoints.push({
|
|
96
187
|
checkpointId,
|
|
97
188
|
timestamp: new Date().toISOString(),
|
|
@@ -99,42 +190,111 @@ export async function handleCheckpoint(cwd, progressPct, filesChanged, summary,
|
|
|
99
190
|
});
|
|
100
191
|
await fs.ensureDir(path.join(cwd, '.rigour'));
|
|
101
192
|
await fs.writeFile(checkpointPath, JSON.stringify(session, null, 2));
|
|
102
|
-
await
|
|
193
|
+
await setTaskCheckpointCache({
|
|
194
|
+
taskId,
|
|
195
|
+
agentId,
|
|
196
|
+
phase,
|
|
197
|
+
component: filesChanged[0] ? path.dirname(filesChanged[0]) : 'workspace',
|
|
198
|
+
changedFiles: filesChanged,
|
|
199
|
+
decisions: [summary],
|
|
200
|
+
validation: ['rigour_check'],
|
|
201
|
+
remainingWork: [`Continue from ${progressPct}%`],
|
|
202
|
+
risks: warnings,
|
|
203
|
+
}, estimateTokenCount(rawStateText), cwd);
|
|
204
|
+
let indexNote = null;
|
|
205
|
+
if (filesChanged.length > 0) {
|
|
206
|
+
indexNote = await refreshIndexForFiles(cwd, filesChanged);
|
|
207
|
+
}
|
|
208
|
+
await logStudioEvent(cwd, { type: "checkpoint_recorded", requestId, checkpointId, progressPct, qualityScore, warnings, filesChanged });
|
|
103
209
|
let text = `📍 CHECKPOINT RECORDED: ${checkpointId}\n\n`;
|
|
104
210
|
text += `Progress: ${progressPct}% | Quality: ${qualityScore}%\n`;
|
|
105
211
|
text += `Summary: ${summary}\n`;
|
|
106
212
|
text += `Total checkpoints: ${session.checkpoints.length}\n`;
|
|
213
|
+
if (indexNote)
|
|
214
|
+
text += `\n${indexNote}\n`;
|
|
107
215
|
if (warnings.length > 0) {
|
|
108
216
|
text += `\n⚠️ WARNINGS:\n${warnings.map(w => ` - ${w}`).join('\n')}\n`;
|
|
109
217
|
if (qualityScore < 80)
|
|
110
218
|
text += `\n⛔ QUALITY BELOW THRESHOLD: Consider pausing and reviewing recent work.`;
|
|
111
219
|
}
|
|
112
|
-
const
|
|
220
|
+
const packet = await getTaskCheckpointCache(taskId, agentId, phase, cwd);
|
|
221
|
+
const returnedText = packet ? JSON.stringify(packet) : text;
|
|
222
|
+
const telemetry = buildTelemetryMeta({
|
|
223
|
+
candidateText: rawStateText,
|
|
224
|
+
returnedText,
|
|
225
|
+
cacheStatus: 'miss',
|
|
226
|
+
deduplicatedTokens: Math.max(0, estimateTokenCount(rawStateText) - estimateTokenCount(returnedText)),
|
|
227
|
+
});
|
|
228
|
+
const result = {
|
|
229
|
+
content: [{ type: "text", text: appendContextFooter(text, telemetry, 'rigour_handoff with compact context') }],
|
|
230
|
+
_telemetry: telemetry,
|
|
231
|
+
};
|
|
113
232
|
result._shouldContinue = qualityScore >= 80;
|
|
114
233
|
return result;
|
|
115
234
|
}
|
|
116
235
|
// ─── Handoff ──────────────────────────────────────────────────────
|
|
117
|
-
export async function handleHandoff(cwd, fromAgentId, toAgentId, taskDescription, filesInScope, context, requestId) {
|
|
236
|
+
export async function handleHandoff(cwd, fromAgentId, toAgentId, taskDescription, filesInScope, context, requestId, taskId = 'default-task') {
|
|
237
|
+
const contextTokens = estimateTokenCount(context);
|
|
238
|
+
if (contextTokens > HANDOFF_CONTEXT_TOKEN_LIMIT) {
|
|
239
|
+
const text = `🛑 HANDOFF REJECTED: context is ~${contextTokens} tokens (limit: ${HANDOFF_CONTEXT_TOKEN_LIMIT}).\n\n`
|
|
240
|
+
+ `Call rigour_checkpoint first to compress state into a checkpoint packet, then hand off with a brief summary only.\n`
|
|
241
|
+
+ `Subagents should receive checkpoint packets — not parent transcripts.`;
|
|
242
|
+
return {
|
|
243
|
+
content: [{ type: 'text', text }],
|
|
244
|
+
isError: true,
|
|
245
|
+
_telemetry: buildTelemetryMeta({
|
|
246
|
+
candidateText: context,
|
|
247
|
+
returnedText: text,
|
|
248
|
+
cacheStatus: 'none',
|
|
249
|
+
}),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
118
252
|
const handoffId = `handoff-${Date.now()}`;
|
|
119
253
|
const handoffPath = path.join(cwd, '.rigour', 'handoffs.jsonl');
|
|
254
|
+
const commitSha = await getWorkspaceCommitSha(cwd);
|
|
255
|
+
const checkpointPacket = await getTaskCheckpointCache(taskId, fromAgentId, `progress-100`, cwd)
|
|
256
|
+
?? await getTaskCheckpointCache(taskId, fromAgentId, `progress-75`, cwd);
|
|
257
|
+
const compactContext = checkpointPacket
|
|
258
|
+
? JSON.stringify({
|
|
259
|
+
decisions: checkpointPacket.decisions,
|
|
260
|
+
changedFiles: checkpointPacket.changedFiles,
|
|
261
|
+
remainingWork: checkpointPacket.remainingWork,
|
|
262
|
+
risks: checkpointPacket.risks,
|
|
263
|
+
brief: context.slice(0, 500),
|
|
264
|
+
})
|
|
265
|
+
: context;
|
|
120
266
|
const handoff = {
|
|
121
267
|
handoffId,
|
|
122
268
|
timestamp: new Date().toISOString(),
|
|
123
|
-
fromAgentId, toAgentId, taskDescription, filesInScope,
|
|
269
|
+
fromAgentId, toAgentId, taskDescription, filesInScope,
|
|
270
|
+
context: compactContext,
|
|
271
|
+
checkpointBound: !!checkpointPacket,
|
|
272
|
+
commitSha,
|
|
124
273
|
status: 'pending',
|
|
125
274
|
};
|
|
126
275
|
await fs.ensureDir(path.join(cwd, '.rigour'));
|
|
127
276
|
await fs.appendFile(handoffPath, JSON.stringify(handoff) + '\n');
|
|
128
|
-
await logStudioEvent(cwd, { type: "handoff_initiated", requestId, handoffId, fromAgentId, toAgentId, taskDescription });
|
|
277
|
+
await logStudioEvent(cwd, { type: "handoff_initiated", requestId, handoffId, fromAgentId, toAgentId, taskDescription, contextTokens });
|
|
129
278
|
let text = `🤝 HANDOFF INITIATED: ${handoffId}\n\n`;
|
|
130
279
|
text += `From: ${fromAgentId} → To: ${toAgentId}\n`;
|
|
131
280
|
text += `Task: ${taskDescription}\n`;
|
|
132
281
|
if (filesInScope.length > 0)
|
|
133
282
|
text += `Files in scope: ${filesInScope.join(', ')}\n`;
|
|
134
|
-
if (
|
|
135
|
-
text += `Context: ${
|
|
283
|
+
if (compactContext)
|
|
284
|
+
text += `Context: ${compactContext}\n`;
|
|
285
|
+
if (checkpointPacket)
|
|
286
|
+
text += `\n✅ Checkpoint packet attached (compressed handoff).\n`;
|
|
136
287
|
text += `\nThe receiving agent should call rigour_agent_register to claim this scope.`;
|
|
137
|
-
|
|
288
|
+
const telemetry = buildTelemetryMeta({
|
|
289
|
+
candidateText: context || taskDescription,
|
|
290
|
+
returnedText: compactContext || text,
|
|
291
|
+
cacheStatus: checkpointPacket ? 'partial-hit' : 'miss',
|
|
292
|
+
deduplicatedTokens: Math.max(0, contextTokens - estimateTokenCount(compactContext)),
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
content: [{ type: "text", text: appendContextFooter(text, telemetry, 'rigour_handoff_accept') }],
|
|
296
|
+
_telemetry: telemetry,
|
|
297
|
+
};
|
|
138
298
|
}
|
|
139
299
|
// ─── Agent Deregister ─────────────────────────────────────────────
|
|
140
300
|
export async function handleAgentDeregister(cwd, agentId, requestId) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handlers for Context Telemetry & Cost Efficiency MCP Tools
|
|
3
|
+
*
|
|
4
|
+
* Handlers for:
|
|
5
|
+
* - rigour_context_stats
|
|
6
|
+
* - rigour_task_cost
|
|
7
|
+
* - rigour_cache_stats
|
|
8
|
+
* - rigour_context_explain
|
|
9
|
+
* - rigour_context_scope
|
|
10
|
+
*/
|
|
11
|
+
import { type ToolResult } from '../utils/context-telemetry.js';
|
|
12
|
+
export declare function handleContextStats(cwd: string, taskId?: string): Promise<ToolResult>;
|
|
13
|
+
export declare function handleTaskCost(cwd: string, taskId?: string): Promise<ToolResult>;
|
|
14
|
+
export declare function handleCacheStats(cwd: string): Promise<ToolResult>;
|
|
15
|
+
export declare function handleContextExplain(cwd: string, target: string, taskId?: string): Promise<ToolResult>;
|
|
16
|
+
export declare function handleContextScope(cwd: string, query: string, limit?: number): Promise<ToolResult>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handlers for Context Telemetry & Cost Efficiency MCP Tools
|
|
3
|
+
*
|
|
4
|
+
* Handlers for:
|
|
5
|
+
* - rigour_context_stats
|
|
6
|
+
* - rigour_task_cost
|
|
7
|
+
* - rigour_cache_stats
|
|
8
|
+
* - rigour_context_explain
|
|
9
|
+
* - rigour_context_scope
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { getTaskContextStats, getTaskCostStats, getCacheStats, explainContext, getSemanticQueryCache, setSemanticQueryCache, estimateTokenCount, } from '@rigour-labs/core';
|
|
14
|
+
import { loadPatternIndex, getDefaultIndexPath, generateEmbedding, semanticSearch, } from '@rigour-labs/core/pattern-index';
|
|
15
|
+
import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
|
|
16
|
+
import { appendContextFooter } from '../utils/context-footer.js';
|
|
17
|
+
export async function handleContextStats(cwd, taskId) {
|
|
18
|
+
try {
|
|
19
|
+
const stats = await getTaskContextStats(taskId, cwd);
|
|
20
|
+
const jsonText = JSON.stringify(stats, null, 2);
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: "text", text: jsonText }],
|
|
23
|
+
_telemetry: buildTelemetryMeta({
|
|
24
|
+
candidateText: jsonText,
|
|
25
|
+
returnedText: jsonText,
|
|
26
|
+
cacheStatus: 'none',
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text", text: `RIGOUR ERROR: ${e.message}` }],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function handleTaskCost(cwd, taskId) {
|
|
38
|
+
try {
|
|
39
|
+
const costStats = await getTaskCostStats(taskId, cwd);
|
|
40
|
+
const jsonText = JSON.stringify(costStats, null, 2);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: jsonText }],
|
|
43
|
+
_telemetry: buildTelemetryMeta({
|
|
44
|
+
candidateText: jsonText,
|
|
45
|
+
returnedText: jsonText,
|
|
46
|
+
cacheStatus: 'none',
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: `RIGOUR ERROR: ${e.message}` }],
|
|
53
|
+
isError: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function handleCacheStats(cwd) {
|
|
58
|
+
try {
|
|
59
|
+
const stats = await getCacheStats(cwd);
|
|
60
|
+
const jsonText = JSON.stringify(stats, null, 2);
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: jsonText }],
|
|
63
|
+
_telemetry: buildTelemetryMeta({
|
|
64
|
+
candidateText: jsonText,
|
|
65
|
+
returnedText: jsonText,
|
|
66
|
+
cacheStatus: 'none',
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: `RIGOUR ERROR: ${e.message}` }],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function handleContextExplain(cwd, target, taskId) {
|
|
78
|
+
try {
|
|
79
|
+
const explanation = await explainContext(target, taskId, cwd);
|
|
80
|
+
const jsonText = JSON.stringify(explanation, null, 2);
|
|
81
|
+
return {
|
|
82
|
+
content: [{ type: "text", text: jsonText }],
|
|
83
|
+
_telemetry: buildTelemetryMeta({
|
|
84
|
+
candidateText: jsonText,
|
|
85
|
+
returnedText: jsonText,
|
|
86
|
+
cacheStatus: explanation.servedFromCache ? explanation.cacheStatus : 'miss',
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text", text: `RIGOUR ERROR: ${e.message}` }],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function textMatchPatterns(query, patterns, limit) {
|
|
98
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
99
|
+
const scored = patterns.map(p => {
|
|
100
|
+
const haystack = `${p.name} ${p.type} ${p.description} ${p.file} ${p.keywords?.join(' ') ?? ''}`.toLowerCase();
|
|
101
|
+
const score = terms.reduce((acc, term) => acc + (haystack.includes(term) ? 1 : 0), 0);
|
|
102
|
+
return { pattern: p, score };
|
|
103
|
+
});
|
|
104
|
+
return scored
|
|
105
|
+
.filter(s => s.score > 0)
|
|
106
|
+
.sort((a, b) => b.score - a.score)
|
|
107
|
+
.slice(0, limit)
|
|
108
|
+
.map(s => s.pattern);
|
|
109
|
+
}
|
|
110
|
+
export async function handleContextScope(cwd, query, limit = 10) {
|
|
111
|
+
const commitSha = await getWorkspaceCommitSha(cwd);
|
|
112
|
+
const indexPath = getDefaultIndexPath(cwd);
|
|
113
|
+
const fullIndexCandidate = `Full repository scan for: ${query}`;
|
|
114
|
+
const cached = await getSemanticQueryCache(query, commitSha, cwd);
|
|
115
|
+
if (cached) {
|
|
116
|
+
const cachedText = JSON.stringify(cached, null, 2);
|
|
117
|
+
const telemetry = buildTelemetryMeta({
|
|
118
|
+
candidateText: fullIndexCandidate,
|
|
119
|
+
returnedText: cachedText,
|
|
120
|
+
cacheStatus: 'semantic-hit',
|
|
121
|
+
});
|
|
122
|
+
const text = `CONTEXT SCOPE (cached)\n\n${cachedText}`;
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: 'text', text: appendContextFooter(text, telemetry, 'rigour_check_pattern before creating code') }],
|
|
125
|
+
_telemetry: telemetry,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const index = await loadPatternIndex(indexPath);
|
|
129
|
+
if (!index) {
|
|
130
|
+
const text = '⚠️ Pattern index not found. Call rigour_index first to enable scoped context retrieval.\n\n'
|
|
131
|
+
+ 'Without an index, agents tend to read entire directories — wasting tokens.';
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: 'text', text }],
|
|
134
|
+
_telemetry: buildTelemetryMeta({
|
|
135
|
+
candidateText: fullIndexCandidate,
|
|
136
|
+
returnedText: text,
|
|
137
|
+
cacheStatus: 'miss',
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
let matched = [];
|
|
142
|
+
const hasEmbeddings = index.patterns.some(p => p.embedding && p.embedding.length > 0);
|
|
143
|
+
if (hasEmbeddings) {
|
|
144
|
+
try {
|
|
145
|
+
const queryVector = await generateEmbedding(query);
|
|
146
|
+
const similarities = semanticSearch(queryVector, index.patterns);
|
|
147
|
+
matched = index.patterns
|
|
148
|
+
.map((p, i) => ({ pattern: p, similarity: similarities[i] ?? 0 }))
|
|
149
|
+
.filter(r => r.similarity > 0.3)
|
|
150
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
151
|
+
.slice(0, limit)
|
|
152
|
+
.map(r => r.pattern);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
matched = textMatchPatterns(query, index.patterns, limit);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
matched = textMatchPatterns(query, index.patterns, limit);
|
|
160
|
+
}
|
|
161
|
+
const editScope = Array.from(new Set(matched.map(p => p.file)));
|
|
162
|
+
const allFiles = Array.from(new Set(index.patterns.map(p => p.file)));
|
|
163
|
+
const skipScope = allFiles
|
|
164
|
+
.filter(f => !editScope.includes(f))
|
|
165
|
+
.slice(0, Math.max(0, 20 - editScope.length))
|
|
166
|
+
.map(f => ({ file: f, reason: 'Outside semantic match for query' }));
|
|
167
|
+
const signatures = matched.map(p => `- ${p.file}:${p.line} ${p.type} ${p.name} — ${p.signature || p.description || '(no signature)'}`);
|
|
168
|
+
let fullFileTokens = 0;
|
|
169
|
+
for (const file of editScope) {
|
|
170
|
+
try {
|
|
171
|
+
const content = await fs.readFile(path.join(cwd, file), 'utf-8');
|
|
172
|
+
fullFileTokens += estimateTokenCount(content);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
fullFileTokens += 500;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const scopePayload = {
|
|
179
|
+
query,
|
|
180
|
+
editScope,
|
|
181
|
+
skipScope,
|
|
182
|
+
signatures,
|
|
183
|
+
indexHealth: {
|
|
184
|
+
totalPatterns: index.stats.totalPatterns,
|
|
185
|
+
totalFiles: index.stats.totalFiles,
|
|
186
|
+
lastUpdated: index.lastUpdated,
|
|
187
|
+
semanticEnabled: hasEmbeddings,
|
|
188
|
+
},
|
|
189
|
+
estimatedTokensSaved: Math.max(0, fullFileTokens - estimateTokenCount(signatures.join('\n'))),
|
|
190
|
+
recommendation: editScope.length > 0
|
|
191
|
+
? `Read ONLY these ${editScope.length} file(s). Do NOT scan the whole repo.`
|
|
192
|
+
: 'No strong matches — refine query or run rigour_index --semantic',
|
|
193
|
+
};
|
|
194
|
+
await setSemanticQueryCache(query, commitSha, {
|
|
195
|
+
query,
|
|
196
|
+
resolvedOwner: editScope[0] ? path.dirname(editScope[0]) : 'unknown',
|
|
197
|
+
editScope,
|
|
198
|
+
validationScope: editScope.map(f => `review ${f}`),
|
|
199
|
+
evidence: signatures.slice(0, 5),
|
|
200
|
+
commitSha,
|
|
201
|
+
confidence: matched.length > 0 ? 0.85 : 0.3,
|
|
202
|
+
}, cwd);
|
|
203
|
+
const resultText = JSON.stringify(scopePayload, null, 2);
|
|
204
|
+
const telemetry = buildTelemetryMeta({
|
|
205
|
+
candidateText: fullIndexCandidate,
|
|
206
|
+
returnedText: resultText,
|
|
207
|
+
cacheStatus: 'miss',
|
|
208
|
+
deduplicatedTokens: scopePayload.estimatedTokensSaved,
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
content: [{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: appendContextFooter(`CONTEXT SCOPE\n\n${resultText}`, telemetry, 'rigour_check_pattern before creating code'),
|
|
214
|
+
}],
|
|
215
|
+
_telemetry: telemetry,
|
|
216
|
+
};
|
|
217
|
+
}
|