regent-code 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +38 -0
- package/.opencode/INSTALL.md +82 -0
- package/.opencode/agents/regent-explore.md +10 -0
- package/.opencode/agents/regent-general.md +8 -0
- package/.opencode/commands/accept.md +15 -0
- package/.opencode/commands/delegate.md +16 -0
- package/.opencode/commands/diagnose.md +19 -0
- package/.opencode/commands/orchestrate.md +22 -0
- package/.opencode/commands/plan.md +20 -0
- package/.opencode/commands/research.md +12 -0
- package/.opencode/commands/review.md +23 -0
- package/.opencode/commands/ship.md +18 -0
- package/.opencode/commands/spec.md +18 -0
- package/.opencode/commands/status.md +13 -0
- package/.opencode/commands/tdd.md +18 -0
- package/.opencode/commands/verify.md +18 -0
- package/.opencode/package.json +6 -0
- package/.opencode/plugins/regent.js +1623 -0
- package/.opencode/skills/code-review/SKILL.md +89 -0
- package/.opencode/skills/diagnose/SKILL.md +118 -0
- package/.opencode/skills/grilling/SKILL.md +59 -0
- package/.opencode/skills/handoff/SKILL.md +61 -0
- package/.opencode/skills/merge-conflicts/SKILL.md +39 -0
- package/.opencode/skills/orchestrator/SKILL.md +206 -0
- package/.opencode/skills/prototype/SKILL.md +40 -0
- package/.opencode/skills/ship/SKILL.md +42 -0
- package/.opencode/skills/spec/SKILL.md +61 -0
- package/.opencode/skills/tdd/SKILL.md +102 -0
- package/.opencode/skills/tickets/SKILL.md +71 -0
- package/.opencode/skills/using-regent/SKILL.md +71 -0
- package/.opencode/skills/verification-before-completion/SKILL.md +82 -0
- package/.opencode/skills/wizard/SKILL.md +45 -0
- package/.opencode/skills/worktrees/SKILL.md +39 -0
- package/.opencode/skills/zoom-out/SKILL.md +38 -0
- package/.prettierignore +2 -0
- package/.prettierrc +7 -0
- package/AGENTS.md +38 -0
- package/CONSTITUTION.md +101 -0
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/docs/contributing.md +86 -0
- package/docs/superpowers/plans/windows-guardrail/plan.md +49 -0
- package/docs/superpowers/plans/windows-guardrail/tasks.md +58 -0
- package/docs/superpowers/specs/2026-06-12-regent-health-audit-design.md +49 -0
- package/docs/superpowers/specs/2026-08-26-windows-guardrail.md +66 -0
- package/eslint.config.js +23 -0
- package/handoff.md +100 -0
- package/mcp/cli.js +9 -0
- package/mcp/index.js +805 -0
- package/mcp/install.js +204 -0
- package/mcp/prompts.js +99 -0
- package/mcp/shared.js +428 -0
- package/package.json +52 -0
- package/tsconfig.json +17 -0
package/mcp/index.js
ADDED
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ErrorCode,
|
|
9
|
+
GetPromptRequestSchema,
|
|
10
|
+
ListPromptsRequestSchema,
|
|
11
|
+
ListToolsRequestSchema,
|
|
12
|
+
McpError,
|
|
13
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
+
import { OpenCode } from '@opencode-ai/client';
|
|
15
|
+
import { Service } from '@opencode-ai/client/service';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
MAX_DISPATCH_ITEMS,
|
|
19
|
+
MAX_STRING_LENGTH,
|
|
20
|
+
MAX_ID_LENGTH,
|
|
21
|
+
MAX_DISPATCHES_PER_WINDOW,
|
|
22
|
+
DISPATCH_WINDOW_MS,
|
|
23
|
+
withRetry,
|
|
24
|
+
evidenceFreshness,
|
|
25
|
+
recordEvidence,
|
|
26
|
+
markEvidenceVerified,
|
|
27
|
+
evidenceForScope,
|
|
28
|
+
redactSecrets,
|
|
29
|
+
isSensitiveFocusPath,
|
|
30
|
+
parseSubagentTextResponse,
|
|
31
|
+
unwrapData,
|
|
32
|
+
sessionFileChanges,
|
|
33
|
+
workerSessionIds,
|
|
34
|
+
dispatchRateLimit,
|
|
35
|
+
circuitIsOpen,
|
|
36
|
+
recordCircuitResult,
|
|
37
|
+
toContent,
|
|
38
|
+
structuredBlockedResult,
|
|
39
|
+
TOOL_INPUTS,
|
|
40
|
+
} from './shared.js';
|
|
41
|
+
|
|
42
|
+
import { readPackagePrompts, renderPrompt } from './prompts.js';
|
|
43
|
+
|
|
44
|
+
const version = '3.0.0';
|
|
45
|
+
|
|
46
|
+
// ── OpenCode client (lazy singleton) ─────────────────────────
|
|
47
|
+
let clientPromise = null;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a connection to the local OpenCode background service and wrap it as
|
|
51
|
+
* a client. Kept as a singleton for the lifetime of the MCP process.
|
|
52
|
+
* @returns {Promise<ReturnType<typeof OpenCode.make>>}
|
|
53
|
+
*/
|
|
54
|
+
async function getClient() {
|
|
55
|
+
if (clientPromise) return clientPromise;
|
|
56
|
+
clientPromise = (async () => {
|
|
57
|
+
try {
|
|
58
|
+
const endpoint = await Service.ensure();
|
|
59
|
+
return OpenCode.make({
|
|
60
|
+
baseUrl: endpoint.url,
|
|
61
|
+
headers: Service.headers(endpoint),
|
|
62
|
+
});
|
|
63
|
+
} catch (err) {
|
|
64
|
+
clientPromise = null;
|
|
65
|
+
const cause = /** @type {any} */ (err);
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Cannot connect to local OpenCode service: ${cause?.message || String(cause)}`,
|
|
68
|
+
{ cause: err },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
})();
|
|
72
|
+
return clientPromise;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Worker agent resolution (ported from the plugin) ─────────
|
|
76
|
+
function normalizeAgents(result) {
|
|
77
|
+
const data = unwrapData(result);
|
|
78
|
+
if (Array.isArray(data)) return data;
|
|
79
|
+
if (Array.isArray(data?.agents)) return data.agents;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function agentMode(agent) {
|
|
84
|
+
return agent?.mode;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isVisibleAgent(agent) {
|
|
88
|
+
return Boolean(agent?.id) && agent.hidden !== true && agent.disabled !== true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isChildCapableAgent(agent) {
|
|
92
|
+
return isVisibleAgent(agent) && ['subagent', 'all'].includes(agentMode(agent));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isUnavailableAgentError(err) {
|
|
96
|
+
const message = (err?.message || String(err)).toLowerCase();
|
|
97
|
+
return (
|
|
98
|
+
message.includes('not found') ||
|
|
99
|
+
message.includes('unavailable') ||
|
|
100
|
+
message.includes('invalid agent') ||
|
|
101
|
+
message.includes('agent not')
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {ReturnType<typeof OpenCode.make>} client
|
|
107
|
+
* @param {{ workerAgent?: string }} [options]
|
|
108
|
+
* @returns {Promise<(requestedAgent?: string) => Promise<{ ok: true, agents: string[], automatic: boolean } | { ok: false, error: string }>>}
|
|
109
|
+
*/
|
|
110
|
+
async function createWorkerResolver(client, options = {}) {
|
|
111
|
+
let catalog = null;
|
|
112
|
+
try {
|
|
113
|
+
catalog = normalizeAgents(await client.agent.list());
|
|
114
|
+
} catch {
|
|
115
|
+
catalog = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const findAgent = async (id) => {
|
|
119
|
+
const fromCatalog = catalog?.find((agent) => agent.id === id);
|
|
120
|
+
if (fromCatalog) return fromCatalog;
|
|
121
|
+
if (catalog !== null) return undefined;
|
|
122
|
+
try {
|
|
123
|
+
return unwrapData(await client.agent.get({ agentID: id }));
|
|
124
|
+
} catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} id
|
|
131
|
+
* @param {string} [label]
|
|
132
|
+
* @returns {Promise<{ ok: true, agents: string[], automatic: boolean } | { ok: false, error: string }>}
|
|
133
|
+
*/
|
|
134
|
+
const validate = async (id, label = 'agent') => {
|
|
135
|
+
const agent = await findAgent(id);
|
|
136
|
+
if (!agent || !isVisibleAgent(agent)) {
|
|
137
|
+
return { ok: false, error: `${label} "${id}" is unavailable` };
|
|
138
|
+
}
|
|
139
|
+
if (!isChildCapableAgent(agent)) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: `${label} "${id}" is primary-only and cannot be used for subagent dispatch`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return { ok: true, agents: [id], automatic: false };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** @type {(requestedAgent?: string) => Promise<{ ok: true, agents: string[], automatic: boolean } | { ok: false, error: string }>} */
|
|
149
|
+
const resolveWorker = async (requestedAgent = '') => {
|
|
150
|
+
const requested = typeof requestedAgent === 'string' ? requestedAgent.trim() : requestedAgent;
|
|
151
|
+
if (requested) return validate(requested);
|
|
152
|
+
|
|
153
|
+
const configuredWorker =
|
|
154
|
+
typeof options.workerAgent === 'string' ? options.workerAgent.trim() : '';
|
|
155
|
+
if (configuredWorker) return validate(configuredWorker, 'workerAgent');
|
|
156
|
+
|
|
157
|
+
if (catalog === null) {
|
|
158
|
+
// `general` is a documented built-in. It is the only safe fallback when
|
|
159
|
+
// the server cannot inspect project agent definitions.
|
|
160
|
+
return { ok: true, agents: ['general'], automatic: false };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const candidates = [];
|
|
164
|
+
if (isChildCapableAgent(catalog.find((agent) => agent.id === 'regent-general'))) {
|
|
165
|
+
candidates.push('regent-general');
|
|
166
|
+
}
|
|
167
|
+
const configuredGeneral = catalog.find((agent) => agent.id === 'general');
|
|
168
|
+
if (!configuredGeneral || isChildCapableAgent(configuredGeneral)) {
|
|
169
|
+
candidates.push('general');
|
|
170
|
+
}
|
|
171
|
+
for (const agent of catalog) {
|
|
172
|
+
if (isChildCapableAgent(agent) && !candidates.includes(agent.id)) {
|
|
173
|
+
candidates.push(agent.id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (candidates.length === 0) {
|
|
178
|
+
return { ok: false, error: 'No visible child-capable worker agent is available' };
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, agents: candidates, automatic: true };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return resolveWorker;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Shared subagent dispatch (ported from the plugin) ─────────
|
|
187
|
+
/**
|
|
188
|
+
* @param {{ task: string, context: string, expected_output: string, id?: string, agent?: string }} args
|
|
189
|
+
* @param {ReturnType<typeof OpenCode.make>} client
|
|
190
|
+
* @param {(requestedAgent?: string) => Promise<{ ok: true, agents: string[], automatic: boolean } | { ok: false, error: string }>} resolveWorker
|
|
191
|
+
* @returns {Promise<{ status: string, output: string, concerns: string[], files_changed: string[], session_id?: string }>}
|
|
192
|
+
*/
|
|
193
|
+
async function dispatchSubagent(client, resolveWorker, args, taskId = '') {
|
|
194
|
+
/** @type {Array<[string, string, number]>} */
|
|
195
|
+
const inputChecks = [
|
|
196
|
+
[args.task, 'task', MAX_STRING_LENGTH],
|
|
197
|
+
[args.context, 'context', MAX_STRING_LENGTH],
|
|
198
|
+
[args.expected_output, 'expected_output', MAX_STRING_LENGTH],
|
|
199
|
+
[taskId, 'id', MAX_ID_LENGTH],
|
|
200
|
+
];
|
|
201
|
+
for (const [value, label, limit] of inputChecks) {
|
|
202
|
+
if (typeof value !== 'string' || value.length > limit) {
|
|
203
|
+
return structuredBlockedResult(`${label} is too long (limit ${limit} characters)`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (args.agent !== undefined && typeof args.agent !== 'string') {
|
|
208
|
+
return structuredBlockedResult('agent must be a string');
|
|
209
|
+
}
|
|
210
|
+
if (typeof args.agent === 'string' && args.agent.length > MAX_ID_LENGTH) {
|
|
211
|
+
return structuredBlockedResult(`agent is too long (limit ${MAX_ID_LENGTH} characters)`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const key = 'mcp';
|
|
215
|
+
const directory = process.cwd();
|
|
216
|
+
let session;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const selection = await resolveWorker(args.agent);
|
|
220
|
+
if (!selection.ok) return structuredBlockedResult(selection.error);
|
|
221
|
+
if (!dispatchRateLimit(key)) {
|
|
222
|
+
return structuredBlockedResult(
|
|
223
|
+
`dispatch rate limit exceeded (${MAX_DISPATCHES_PER_WINDOW} dispatches per ${DISPATCH_WINDOW_MS / 1000}s)`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const title = args.task;
|
|
228
|
+
for (let index = 0; index < selection.agents.length; index++) {
|
|
229
|
+
const agent = selection.agents[index];
|
|
230
|
+
try {
|
|
231
|
+
const createInput = { title, agent, location: { directory } };
|
|
232
|
+
const sessionResult = await withRetry(() => client.session.create(createInput));
|
|
233
|
+
session = unwrapData(sessionResult);
|
|
234
|
+
if (session?.id) break;
|
|
235
|
+
} catch (err) {
|
|
236
|
+
if (
|
|
237
|
+
!selection.automatic ||
|
|
238
|
+
index === selection.agents.length - 1 ||
|
|
239
|
+
!isUnavailableAgentError(err)
|
|
240
|
+
) {
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!session?.id) throw new Error('session create returned no session');
|
|
247
|
+
workerSessionIds.add(session.id);
|
|
248
|
+
|
|
249
|
+
const prompt = [
|
|
250
|
+
'## Task',
|
|
251
|
+
args.task,
|
|
252
|
+
'',
|
|
253
|
+
'## Context',
|
|
254
|
+
args.context,
|
|
255
|
+
'',
|
|
256
|
+
'## Expected Output',
|
|
257
|
+
args.expected_output,
|
|
258
|
+
'',
|
|
259
|
+
'Complete the task. When you finish, provide:',
|
|
260
|
+
'- summary: What you did',
|
|
261
|
+
'- status: one of: done, blocked, needs_context, done_with_concerns',
|
|
262
|
+
'- concerns: Any issues encountered (if status is done_with_concerns)',
|
|
263
|
+
'- files_changed: List of files created or modified',
|
|
264
|
+
'',
|
|
265
|
+
'If you need more context, say NEEDS_CONTEXT and explain what you need.',
|
|
266
|
+
'If you cannot complete the task, say BLOCKED and explain why.',
|
|
267
|
+
].join('\n');
|
|
268
|
+
|
|
269
|
+
const result = await withRetry(() =>
|
|
270
|
+
client.session.generate({ sessionID: session.id, prompt }),
|
|
271
|
+
);
|
|
272
|
+
const message = unwrapData(result);
|
|
273
|
+
const output = typeof message?.text === 'string' ? message.text : '';
|
|
274
|
+
const parsed = parseSubagentTextResponse(output);
|
|
275
|
+
const { status, concerns, filesChanged } = parsed;
|
|
276
|
+
|
|
277
|
+
if (filesChanged.length > 0) {
|
|
278
|
+
sessionFileChanges.set(session.id, {
|
|
279
|
+
taskId,
|
|
280
|
+
files: filesChanged,
|
|
281
|
+
timestamp: Date.now(),
|
|
282
|
+
verified: false,
|
|
283
|
+
});
|
|
284
|
+
recordEvidence(session.id, filesChanged, directory);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { status, output, concerns, files_changed: filesChanged, session_id: session.id };
|
|
288
|
+
} catch (err) {
|
|
289
|
+
const message = redactSecrets(err instanceof Error ? err.message : String(err)).slice(0, 500);
|
|
290
|
+
return {
|
|
291
|
+
status: 'blocked',
|
|
292
|
+
output: `Subagent error: ${message}`,
|
|
293
|
+
concerns: [],
|
|
294
|
+
files_changed: [],
|
|
295
|
+
session_id: session?.id,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Explore (ported from the plugin; directory from cwd) ─────
|
|
301
|
+
function runExplore(query, focus) {
|
|
302
|
+
const worktree = process.cwd();
|
|
303
|
+
let result = `Codebase exploration for: ${query}\n\n`;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const items = fs.readdirSync(worktree, { withFileTypes: true });
|
|
307
|
+
result += '## Top-level contents\n';
|
|
308
|
+
for (const item of items) {
|
|
309
|
+
if (item.name.startsWith('.') && item.name !== '.gitignore') continue;
|
|
310
|
+
result += `${item.isDirectory() ? '/' : ''} ${item.name}\n`;
|
|
311
|
+
}
|
|
312
|
+
result += '\n';
|
|
313
|
+
} catch {
|
|
314
|
+
/* ignore read errors */
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const srcDir = path.join(worktree, 'src');
|
|
319
|
+
if (fs.existsSync(srcDir)) {
|
|
320
|
+
result += '## src/ directory\n';
|
|
321
|
+
/** @param {string} dir @param {number} depth */
|
|
322
|
+
const walk = (dir, depth) => {
|
|
323
|
+
if (depth > 3) return;
|
|
324
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
325
|
+
for (const entry of entries) {
|
|
326
|
+
if (entry.name.startsWith('.')) continue;
|
|
327
|
+
const full = path.join(dir, entry.name);
|
|
328
|
+
const indent = ' '.repeat(depth);
|
|
329
|
+
if (entry.isDirectory()) {
|
|
330
|
+
result += `${indent}${entry.name}/\n`;
|
|
331
|
+
walk(full, depth + 1);
|
|
332
|
+
} else {
|
|
333
|
+
result += `${indent}${entry.name}\n`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
walk(srcDir, 0);
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
/* ignore read errors */
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (focus) {
|
|
344
|
+
const worktreeRoot = path.resolve(worktree);
|
|
345
|
+
const focusPath = path.resolve(worktree, focus);
|
|
346
|
+
const lexicalInside =
|
|
347
|
+
focusPath.startsWith(worktreeRoot + path.sep) || focusPath === worktreeRoot;
|
|
348
|
+
if (!lexicalInside) {
|
|
349
|
+
result += `\n## Focus: ${focus}\n(path outside project directory)\n`;
|
|
350
|
+
} else if (isSensitiveFocusPath(focusPath, worktreeRoot)) {
|
|
351
|
+
result += `\n## Focus: ${focus}\n(access denied: sensitive path)\n`;
|
|
352
|
+
} else {
|
|
353
|
+
let realRoot = worktreeRoot;
|
|
354
|
+
let realFocus = focusPath;
|
|
355
|
+
try {
|
|
356
|
+
realRoot = fs.realpathSync(worktreeRoot);
|
|
357
|
+
realFocus = fs.realpathSync(focusPath);
|
|
358
|
+
} catch {
|
|
359
|
+
/* fall back to lexical paths */
|
|
360
|
+
}
|
|
361
|
+
const inside =
|
|
362
|
+
(realFocus.startsWith(realRoot + path.sep) || realFocus === realRoot) &&
|
|
363
|
+
(focusPath.startsWith(worktreeRoot + path.sep) || focusPath === worktreeRoot);
|
|
364
|
+
if (!inside) {
|
|
365
|
+
result += `\n## Focus: ${focus}\n(path outside project directory)\n`;
|
|
366
|
+
} else if (isSensitiveFocusPath(realFocus, realRoot)) {
|
|
367
|
+
result += `\n## Focus: ${focus}\n(access denied: sensitive path)\n`;
|
|
368
|
+
} else if (fs.existsSync(focusPath)) {
|
|
369
|
+
const stat = fs.statSync(focusPath);
|
|
370
|
+
if (stat.isDirectory()) {
|
|
371
|
+
const items = fs.readdirSync(focusPath);
|
|
372
|
+
result += items.join('\n') + '\n';
|
|
373
|
+
} else {
|
|
374
|
+
try {
|
|
375
|
+
const content = redactSecrets(fs.readFileSync(focusPath, 'utf8').slice(0, 3000));
|
|
376
|
+
result += '```\n' + content + '\n```\n';
|
|
377
|
+
} catch {
|
|
378
|
+
result += '(path not found)\n';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
result += '(path not found)\n';
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { structure: result, summary: `Explored ${query}` };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── Verify (ported from the plugin) ──────────────────────────
|
|
391
|
+
function runVerify(requirements, implementationContext, sessionId) {
|
|
392
|
+
if (
|
|
393
|
+
requirements == null ||
|
|
394
|
+
implementationContext == null ||
|
|
395
|
+
typeof requirements !== 'string' ||
|
|
396
|
+
typeof implementationContext !== 'string'
|
|
397
|
+
) {
|
|
398
|
+
const scopeEvidence = evidenceForScope();
|
|
399
|
+
return {
|
|
400
|
+
compliant: false,
|
|
401
|
+
requirements_met: [],
|
|
402
|
+
requirements_unmet: [],
|
|
403
|
+
extras_built: [],
|
|
404
|
+
evidence_gate: {
|
|
405
|
+
unverified_changes: scopeEvidence.filter((entry) => !entry.verified).length,
|
|
406
|
+
freshness: evidenceFreshness(scopeEvidence),
|
|
407
|
+
},
|
|
408
|
+
summary: 'Missing required arguments: provide "requirements" and "implementation_context"',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** @param {string} s */
|
|
413
|
+
const getKeyBigrams = (s) => {
|
|
414
|
+
const words = s
|
|
415
|
+
.toLowerCase()
|
|
416
|
+
.split(/\s+/)
|
|
417
|
+
.filter((w) => w.length > 3);
|
|
418
|
+
const bigrams = /** @type {string[]} */ ([]);
|
|
419
|
+
for (let i = 0; i < words.length - 1; i++) {
|
|
420
|
+
bigrams.push(words[i] + ' ' + words[i + 1]);
|
|
421
|
+
}
|
|
422
|
+
return bigrams;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/** @param {string} s */
|
|
426
|
+
const getKeyUnigrams = (s) =>
|
|
427
|
+
s
|
|
428
|
+
.toLowerCase()
|
|
429
|
+
.split(/\s+/)
|
|
430
|
+
.filter((w) => w.length > 3);
|
|
431
|
+
|
|
432
|
+
/** @param {string} s */
|
|
433
|
+
const stripCheckbox = (s) =>
|
|
434
|
+
s
|
|
435
|
+
.replace(/^[-*]\s*\[\s*[x ]?\s*\]\s*/i, '')
|
|
436
|
+
.replace(/^[-*\d+.]\s+/, '')
|
|
437
|
+
.trim();
|
|
438
|
+
|
|
439
|
+
const reqs = requirements
|
|
440
|
+
.split('\n')
|
|
441
|
+
.map((r) => stripCheckbox(r))
|
|
442
|
+
.filter((r) => r.length > 2 && !r.startsWith('#') && !r.startsWith('```') && !r.endsWith(':'));
|
|
443
|
+
|
|
444
|
+
const impl = implementationContext.toLowerCase();
|
|
445
|
+
|
|
446
|
+
const met = [];
|
|
447
|
+
const unmet = [];
|
|
448
|
+
|
|
449
|
+
for (const req of reqs) {
|
|
450
|
+
const reqBigrams = getKeyBigrams(req);
|
|
451
|
+
const reqUnigrams = getKeyUnigrams(req);
|
|
452
|
+
let found;
|
|
453
|
+
if (reqBigrams.length > 0) {
|
|
454
|
+
found = reqBigrams.some((b) => impl.includes(b));
|
|
455
|
+
} else {
|
|
456
|
+
found = reqUnigrams.some((w) => impl.includes(w));
|
|
457
|
+
}
|
|
458
|
+
if (found) {
|
|
459
|
+
met.push(req);
|
|
460
|
+
} else {
|
|
461
|
+
unmet.push(req);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const implLines = implementationContext
|
|
466
|
+
.split('\n')
|
|
467
|
+
.map((l) => stripCheckbox(l))
|
|
468
|
+
.filter((l) => l.length > 3 && !l.startsWith('#') && !l.startsWith('```'));
|
|
469
|
+
|
|
470
|
+
const extras = implLines.filter((line) => {
|
|
471
|
+
const lineLower = line.toLowerCase();
|
|
472
|
+
return !reqs.some((req) => {
|
|
473
|
+
const reqBigrams = getKeyBigrams(req);
|
|
474
|
+
const reqUnigrams = getKeyUnigrams(req);
|
|
475
|
+
if (reqBigrams.length > 0) {
|
|
476
|
+
return reqBigrams.some((b) => lineLower.includes(b));
|
|
477
|
+
}
|
|
478
|
+
return reqUnigrams.some((w) => lineLower.includes(w));
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// No session lineage in MCP: verify marks evidence verified if a session_id
|
|
483
|
+
// matches a worker session this process created and dispatched.
|
|
484
|
+
let verificationNote = null;
|
|
485
|
+
if (sessionId) {
|
|
486
|
+
if (sessionFileChanges.has(sessionId)) {
|
|
487
|
+
markEvidenceVerified(sessionId);
|
|
488
|
+
} else {
|
|
489
|
+
verificationNote =
|
|
490
|
+
'session_id does not match any worker session; evidence was not marked verified';
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const scopeEvidence = evidenceForScope();
|
|
495
|
+
const unverifiedCount = scopeEvidence.filter((entry) => !entry.verified).length;
|
|
496
|
+
const freshness = evidenceFreshness(scopeEvidence);
|
|
497
|
+
|
|
498
|
+
const implWords = implementationContext.split(/\s+/).length;
|
|
499
|
+
const lowConfidence = unmet.length > 0 && implWords < 20;
|
|
500
|
+
|
|
501
|
+
const warningParts = [];
|
|
502
|
+
if (unverifiedCount > 0) {
|
|
503
|
+
warningParts.push(`${unverifiedCount} file change(s) not followed by verification command`);
|
|
504
|
+
}
|
|
505
|
+
if (freshness.stale > 0) {
|
|
506
|
+
warningParts.push(
|
|
507
|
+
`${freshness.stale} verified change(s) went stale (evidence no longer matches files on disk)`,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
compliant: unmet.length === 0 && unverifiedCount === 0 && freshness.stale === 0,
|
|
513
|
+
requirements_met: met,
|
|
514
|
+
requirements_unmet: unmet,
|
|
515
|
+
extras_built: extras,
|
|
516
|
+
verification_note: verificationNote,
|
|
517
|
+
evidence_gate: {
|
|
518
|
+
unverified_changes: unverifiedCount,
|
|
519
|
+
total_changes: scopeEvidence.length,
|
|
520
|
+
freshness,
|
|
521
|
+
warning: warningParts.length > 0 ? warningParts.join('; ') : null,
|
|
522
|
+
},
|
|
523
|
+
confidence_assessment: lowConfidence
|
|
524
|
+
? 'low — sparse implementation context may hide unverified requirements'
|
|
525
|
+
: 'adequate',
|
|
526
|
+
recommend_delegation:
|
|
527
|
+
unmet.length > 0
|
|
528
|
+
? 'Consider delegating each unmet requirement to a subagent for detailed verification'
|
|
529
|
+
: null,
|
|
530
|
+
summary: `${met.length}/${reqs.length} requirements met, ${extras.length} extras flagged, ${unverifiedCount} unverified changes`,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Tool definitions ─────────────────────────────────────────
|
|
535
|
+
const TOOL_DEFINITIONS = [
|
|
536
|
+
{
|
|
537
|
+
name: 'delegate',
|
|
538
|
+
description:
|
|
539
|
+
'Dispatch a single focused task to a subagent. Returns structured result with status (done, blocked, needs_context). Use when a task is well-defined and self-contained. Requires a local OpenCode service to be running.',
|
|
540
|
+
inputSchema: TOOL_INPUTS.delegate,
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: 'delegate_many',
|
|
544
|
+
description:
|
|
545
|
+
'Dispatch multiple independent tasks to subagents in PARALLEL. All tasks run simultaneously via Promise.all with work-stealing. Use for tasks that have no dependencies on each other. Requires a local OpenCode service.',
|
|
546
|
+
inputSchema: TOOL_INPUTS.delegate_many,
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: 'research',
|
|
550
|
+
description:
|
|
551
|
+
'Research multiple questions in parallel by dispatching independent research subagents. Each question gets a focused agent. Returns combined findings with synthesis. Requires a local OpenCode service.',
|
|
552
|
+
inputSchema: TOOL_INPUTS.research,
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: 'explore',
|
|
556
|
+
description:
|
|
557
|
+
'Analyze the project codebase to answer structural questions. Uses file operations to understand directory layout, key files, and patterns. Call this before planning to understand what exists. Operates relative to the MCP server working directory.',
|
|
558
|
+
inputSchema: TOOL_INPUTS.explore,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: 'changed-files',
|
|
562
|
+
description:
|
|
563
|
+
'View files changed by subagent dispatches made through this MCP server. Returns a list of what each dispatched worker session touched.',
|
|
564
|
+
inputSchema: TOOL_INPUTS['changed-files'],
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: 'verify',
|
|
568
|
+
description:
|
|
569
|
+
'Compare implementation against requirements. Returns structured pass/fail per requirement, flags extras (YAGNI). Includes an evidence gate that reports unverified or stale file changes. Use after execution to check if work meets the plan.',
|
|
570
|
+
inputSchema: TOOL_INPUTS.verify,
|
|
571
|
+
},
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
// ── Server construction ──────────────────────────────────────
|
|
575
|
+
let promptsCache = null;
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Build the Regent MCP server with tools and prompts registered.
|
|
579
|
+
* Exported for tests to drive over an in-memory transport.
|
|
580
|
+
* @returns {Server}
|
|
581
|
+
*/
|
|
582
|
+
export function createRegentServer() {
|
|
583
|
+
const server = new Server(
|
|
584
|
+
{ name: 'regent-code', version },
|
|
585
|
+
{ capabilities: { tools: {}, prompts: {} } },
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_DEFINITIONS }));
|
|
589
|
+
|
|
590
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
591
|
+
const { name, arguments: args = {} } = request.params;
|
|
592
|
+
try {
|
|
593
|
+
switch (name) {
|
|
594
|
+
case 'delegate': {
|
|
595
|
+
const client = await getClient();
|
|
596
|
+
const resolveWorker = await createWorkerResolver(client, {
|
|
597
|
+
workerAgent: process.env.REGENT_WORKER_AGENT,
|
|
598
|
+
});
|
|
599
|
+
const result = await dispatchSubagent(client, resolveWorker, /** @type {any} */ (args));
|
|
600
|
+
return toContent(result);
|
|
601
|
+
}
|
|
602
|
+
case 'delegate_many': {
|
|
603
|
+
if (!Array.isArray(args.tasks) || args.tasks.length > MAX_DISPATCH_ITEMS) {
|
|
604
|
+
return toContent({
|
|
605
|
+
results: [],
|
|
606
|
+
summary: { total: 0, completed: 0, failed: 1, needs_context: 0 },
|
|
607
|
+
_error: `delegate_many supports at most ${MAX_DISPATCH_ITEMS} tasks`,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (circuitIsOpen('mcp')) {
|
|
611
|
+
return toContent({
|
|
612
|
+
results: [],
|
|
613
|
+
summary: { total: 0, completed: 0, failed: 1, needs_context: 0 },
|
|
614
|
+
_circuit_open: true,
|
|
615
|
+
_warning:
|
|
616
|
+
'Circuit breaker open after repeated delegate_many failures. One half-open recovery attempt is allowed; escalate to Inspector if it fails.',
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
const client = await getClient();
|
|
620
|
+
const resolveWorker = await createWorkerResolver(client, {
|
|
621
|
+
workerAgent: process.env.REGENT_WORKER_AGENT,
|
|
622
|
+
});
|
|
623
|
+
const queue = [...args.tasks];
|
|
624
|
+
const results = [];
|
|
625
|
+
/** @returns {Promise<void>} */
|
|
626
|
+
async function worker() {
|
|
627
|
+
while (queue.length > 0) {
|
|
628
|
+
const t = queue.shift();
|
|
629
|
+
if (!t) break;
|
|
630
|
+
const result = await dispatchSubagent(
|
|
631
|
+
client,
|
|
632
|
+
resolveWorker,
|
|
633
|
+
/** @type {any} */ (t),
|
|
634
|
+
t.id,
|
|
635
|
+
);
|
|
636
|
+
results.push({ id: t.id, ...result });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const workerCount = Math.min(queue.length, 10);
|
|
640
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
641
|
+
const failed = results.filter((r) => r.status === 'blocked').length;
|
|
642
|
+
recordCircuitResult('mcp', results.length > 0 && failed === 0);
|
|
643
|
+
return toContent({
|
|
644
|
+
results,
|
|
645
|
+
summary: {
|
|
646
|
+
total: results.length,
|
|
647
|
+
completed: results.filter(
|
|
648
|
+
(r) => r.status === 'done' || r.status === 'done_with_concerns',
|
|
649
|
+
).length,
|
|
650
|
+
failed,
|
|
651
|
+
needs_context: results.filter((r) => r.status === 'needs_context').length,
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
case 'research': {
|
|
656
|
+
if (!Array.isArray(args.questions) || args.questions.length > MAX_DISPATCH_ITEMS) {
|
|
657
|
+
return toContent({
|
|
658
|
+
findings: [],
|
|
659
|
+
synthesis: `research supports at most ${MAX_DISPATCH_ITEMS} questions per call`,
|
|
660
|
+
_error: `research supports at most ${MAX_DISPATCH_ITEMS} questions per call`,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
const client = await getClient();
|
|
664
|
+
const resolveWorker = await createWorkerResolver(client, {
|
|
665
|
+
workerAgent: process.env.REGENT_WORKER_AGENT,
|
|
666
|
+
});
|
|
667
|
+
const results = await Promise.all(
|
|
668
|
+
args.questions.map(async (q) => {
|
|
669
|
+
const task = `Research this question thoroughly:\n${q.question}`;
|
|
670
|
+
const taskContext = q.scope
|
|
671
|
+
? `Scope: ${q.scope}`
|
|
672
|
+
: 'Be thorough and concise. Return key findings, data points, and sources.';
|
|
673
|
+
const result = await dispatchSubagent(
|
|
674
|
+
client,
|
|
675
|
+
resolveWorker,
|
|
676
|
+
{
|
|
677
|
+
task,
|
|
678
|
+
context: taskContext,
|
|
679
|
+
expected_output: 'Key findings, data points, sources, and recommendations',
|
|
680
|
+
},
|
|
681
|
+
q.id,
|
|
682
|
+
);
|
|
683
|
+
return { id: q.id, question: q.question, ...result };
|
|
684
|
+
}),
|
|
685
|
+
);
|
|
686
|
+
const completed = results.filter(
|
|
687
|
+
(r) => r.status === 'done' || r.status === 'done_with_concerns',
|
|
688
|
+
);
|
|
689
|
+
const blocked = results.filter((r) => r.status === 'blocked');
|
|
690
|
+
const needsContext = results.filter((r) => r.status === 'needs_context');
|
|
691
|
+
const allOutputs = completed.map((r) => r.output || '');
|
|
692
|
+
const commonThemes = [];
|
|
693
|
+
if (allOutputs.length >= 2) {
|
|
694
|
+
const words = {};
|
|
695
|
+
for (const output of allOutputs) {
|
|
696
|
+
const seen = new Set();
|
|
697
|
+
for (const w of output
|
|
698
|
+
.toLowerCase()
|
|
699
|
+
.split(/\s+/)
|
|
700
|
+
.filter((w) => w.length > 5)) {
|
|
701
|
+
if (!seen.has(w)) {
|
|
702
|
+
seen.add(w);
|
|
703
|
+
words[w] = (words[w] || 0) + 1;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
for (const [word, count] of Object.entries(words)) {
|
|
708
|
+
if (count >= Math.ceil(allOutputs.length / 2)) {
|
|
709
|
+
commonThemes.push(word);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const summaryParts = [];
|
|
714
|
+
if (completed.length > 0) {
|
|
715
|
+
summaryParts.push(`Addressed: ${completed.map((r) => r.question).join(', ')}`);
|
|
716
|
+
}
|
|
717
|
+
if (blocked.length > 0) {
|
|
718
|
+
summaryParts.push(`Blocked: ${blocked.map((r) => r.question).join(', ')}`);
|
|
719
|
+
}
|
|
720
|
+
if (needsContext.length > 0) {
|
|
721
|
+
summaryParts.push(`Needs context: ${needsContext.map((r) => r.question).join(', ')}`);
|
|
722
|
+
}
|
|
723
|
+
if (commonThemes.length > 0) {
|
|
724
|
+
summaryParts.push(`Common themes: ${commonThemes.slice(0, 10).join(', ')}`);
|
|
725
|
+
}
|
|
726
|
+
return toContent({
|
|
727
|
+
findings: results,
|
|
728
|
+
synthesis:
|
|
729
|
+
summaryParts.length > 0
|
|
730
|
+
? summaryParts.join('. ') + '.'
|
|
731
|
+
: 'No research results returned.',
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
case 'explore':
|
|
735
|
+
return toContent(runExplore(args.query, args.focus));
|
|
736
|
+
case 'changed-files': {
|
|
737
|
+
const filters = args || {};
|
|
738
|
+
const entries = [];
|
|
739
|
+
for (const [sid, data] of sessionFileChanges) {
|
|
740
|
+
if (filters.session_id && sid !== filters.session_id) continue;
|
|
741
|
+
if (filters.task_id && data.taskId !== filters.task_id) continue;
|
|
742
|
+
entries.push({ session_id: sid, ...data });
|
|
743
|
+
}
|
|
744
|
+
return toContent({
|
|
745
|
+
entries,
|
|
746
|
+
total: entries.length,
|
|
747
|
+
unverified: entries.filter((e) => !e.verified).length,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
case 'verify':
|
|
751
|
+
return toContent(
|
|
752
|
+
runVerify(args.requirements, args.implementation_context, args.session_id),
|
|
753
|
+
);
|
|
754
|
+
default:
|
|
755
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
756
|
+
}
|
|
757
|
+
} catch (err) {
|
|
758
|
+
if (err instanceof McpError) throw err;
|
|
759
|
+
return toContent(
|
|
760
|
+
structuredBlockedResult(redactSecrets(err instanceof Error ? err.message : String(err))),
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
766
|
+
if (!promptsCache) promptsCache = readPackagePrompts();
|
|
767
|
+
return { prompts: promptsCache };
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
771
|
+
if (!promptsCache) promptsCache = readPackagePrompts();
|
|
772
|
+
const prompt = promptsCache.find((p) => p.name === request.params.name);
|
|
773
|
+
if (!prompt) {
|
|
774
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${request.params.name}`);
|
|
775
|
+
}
|
|
776
|
+
const text = renderPrompt(prompt, request.params.arguments);
|
|
777
|
+
return {
|
|
778
|
+
description: prompt.description,
|
|
779
|
+
messages: [{ role: 'user', content: { type: 'text', text } }],
|
|
780
|
+
};
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
return server;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ── CLI entry ────────────────────────────────────────────────
|
|
787
|
+
/** Start the server over the stdio transport. */
|
|
788
|
+
export async function main() {
|
|
789
|
+
const server = createRegentServer();
|
|
790
|
+
const transport = new StdioServerTransport();
|
|
791
|
+
await server.connect(transport);
|
|
792
|
+
process.stdin.on('end', async () => {
|
|
793
|
+
await server.close();
|
|
794
|
+
process.exit(0);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Run when invoked directly (`node mcp/index.js`).
|
|
799
|
+
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
800
|
+
if (isDirectRun) {
|
|
801
|
+
void main().catch((err) => {
|
|
802
|
+
console.error(err);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
});
|
|
805
|
+
}
|