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.
Files changed (54) hide show
  1. package/.github/workflows/ci.yml +38 -0
  2. package/.opencode/INSTALL.md +82 -0
  3. package/.opencode/agents/regent-explore.md +10 -0
  4. package/.opencode/agents/regent-general.md +8 -0
  5. package/.opencode/commands/accept.md +15 -0
  6. package/.opencode/commands/delegate.md +16 -0
  7. package/.opencode/commands/diagnose.md +19 -0
  8. package/.opencode/commands/orchestrate.md +22 -0
  9. package/.opencode/commands/plan.md +20 -0
  10. package/.opencode/commands/research.md +12 -0
  11. package/.opencode/commands/review.md +23 -0
  12. package/.opencode/commands/ship.md +18 -0
  13. package/.opencode/commands/spec.md +18 -0
  14. package/.opencode/commands/status.md +13 -0
  15. package/.opencode/commands/tdd.md +18 -0
  16. package/.opencode/commands/verify.md +18 -0
  17. package/.opencode/package.json +6 -0
  18. package/.opencode/plugins/regent.js +1623 -0
  19. package/.opencode/skills/code-review/SKILL.md +89 -0
  20. package/.opencode/skills/diagnose/SKILL.md +118 -0
  21. package/.opencode/skills/grilling/SKILL.md +59 -0
  22. package/.opencode/skills/handoff/SKILL.md +61 -0
  23. package/.opencode/skills/merge-conflicts/SKILL.md +39 -0
  24. package/.opencode/skills/orchestrator/SKILL.md +206 -0
  25. package/.opencode/skills/prototype/SKILL.md +40 -0
  26. package/.opencode/skills/ship/SKILL.md +42 -0
  27. package/.opencode/skills/spec/SKILL.md +61 -0
  28. package/.opencode/skills/tdd/SKILL.md +102 -0
  29. package/.opencode/skills/tickets/SKILL.md +71 -0
  30. package/.opencode/skills/using-regent/SKILL.md +71 -0
  31. package/.opencode/skills/verification-before-completion/SKILL.md +82 -0
  32. package/.opencode/skills/wizard/SKILL.md +45 -0
  33. package/.opencode/skills/worktrees/SKILL.md +39 -0
  34. package/.opencode/skills/zoom-out/SKILL.md +38 -0
  35. package/.prettierignore +2 -0
  36. package/.prettierrc +7 -0
  37. package/AGENTS.md +38 -0
  38. package/CONSTITUTION.md +101 -0
  39. package/LICENSE +21 -0
  40. package/README.md +264 -0
  41. package/docs/contributing.md +86 -0
  42. package/docs/superpowers/plans/windows-guardrail/plan.md +49 -0
  43. package/docs/superpowers/plans/windows-guardrail/tasks.md +58 -0
  44. package/docs/superpowers/specs/2026-06-12-regent-health-audit-design.md +49 -0
  45. package/docs/superpowers/specs/2026-08-26-windows-guardrail.md +66 -0
  46. package/eslint.config.js +23 -0
  47. package/handoff.md +100 -0
  48. package/mcp/cli.js +9 -0
  49. package/mcp/index.js +805 -0
  50. package/mcp/install.js +204 -0
  51. package/mcp/prompts.js +99 -0
  52. package/mcp/shared.js +428 -0
  53. package/package.json +52 -0
  54. package/tsconfig.json +17 -0
@@ -0,0 +1,1623 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { Plugin } from '@opencode-ai/plugin';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const rootDir = path.resolve(__dirname, '../..');
8
+ const skillsDir = path.resolve(rootDir, '.opencode', 'skills');
9
+
10
+ let regentVersion = 'unknown';
11
+ try {
12
+ const pkgPath = path.join(rootDir, 'package.json');
13
+ if (fs.existsSync(pkgPath)) {
14
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
15
+ regentVersion = pkg.version || 'unknown';
16
+ }
17
+ } catch {
18
+ /* version non-critical */
19
+ }
20
+
21
+ // ── State ──
22
+ const sessionFileChanges = new Map();
23
+ let evidenceLog = [];
24
+ const dispatchTimesByRoot = new Map();
25
+ const circuitStateByRoot = new Map();
26
+ const pluginWorkerSessionIds = new Set();
27
+ const sessionRoots = new Map();
28
+
29
+ // ── Dispatch safety limits ──
30
+ const MAX_DISPATCH_ITEMS = 10;
31
+ const MAX_STRING_LENGTH = 8000;
32
+ const MAX_ID_LENGTH = 200;
33
+ const MAX_DISPATCHES_PER_WINDOW = 20;
34
+ const DISPATCH_WINDOW_MS = 60000;
35
+
36
+ // ── Typed recovery actions ──
37
+ const RecoveryAction = { RETRY: 'retry', ABORT: 'abort', SKIP: 'skip', ESCALATE: 'escalate' };
38
+
39
+ function classifyError(err) {
40
+ const msg = (err?.message || String(err)).toLowerCase();
41
+ if (msg.includes('timeout') || msg.includes('rate limit') || msg.includes('too many'))
42
+ return RecoveryAction.RETRY;
43
+ if (
44
+ msg.includes('not found') ||
45
+ msg.includes('missing') ||
46
+ msg.includes('invalid') ||
47
+ msg.includes('enoent')
48
+ )
49
+ return RecoveryAction.ABORT;
50
+ if (msg.includes('permission') || msg.includes('denied') || msg.includes('unauthorized'))
51
+ return RecoveryAction.ESCALATE;
52
+ return RecoveryAction.RETRY;
53
+ }
54
+
55
+ // ── Shell guardrail ──────────────────────────────────────────
56
+ // Deterministic, code-enforced safety net for destructive commands. The model
57
+ // cannot reason its way past these; the hook rejects before any shell exists.
58
+ // Patterns are deliberately precise: a hard block on root/home/wiping targets
59
+ // and force-writes, while routine scoped commands pass untouched.
60
+
61
+ const SHELL_ROOT_TARGET = /^(?:\/|~|\$HOME|\/[A-Za-z]|[a-zA-Z]:[\\/]|\.\.?(?:[\\/].*)?$)/;
62
+ const SHELL_FORCE_PUSH = /\bgit\s+push\b[^|;&]*?(?:--force(?!-|\w)|\s-f(?![-\w]))/;
63
+ const SHELL_WIN_ROOT_TARGET =
64
+ /^(?:\/|~(?=[\\/]|$)|\$HOME(?=[\\/]|$)|\$env:USERPROFILE(?=[\\/]|$)|\.\.?(?=[\\/]|$)|[a-zA-Z]:[\\/](?:\*|$))/i;
65
+ const SHELL_CMD_RMDIR = /\b(?:rmdir|rd)\s+\/s\s*\/q\s+((?:'[^']*')|(?:"[^"]*")|\S+)/gi;
66
+ const SHELL_WIN_DELETE_CMD = /^(?:remove-item|ri|rm|del|erase|rd|rmdir)$/i;
67
+ const SHELL_WIN_RECURSE = /^-(?:recurse|r)$/i;
68
+ const SHELL_WIN_FORCE = /^-(?:force|f)$/i;
69
+ const SHELL_WIN_PATH = /^-(?:literalpath|path)(?::(.+))?$/i;
70
+
71
+ function stripQuotes(value) {
72
+ return value.replace(/^['"]|['"]$/g, '');
73
+ }
74
+
75
+ function collectDeleteTargets(value) {
76
+ return String(value)
77
+ .split(',')
78
+ .map((part) => stripQuotes(part.trim()));
79
+ }
80
+
81
+ function guardWindowsDeleteReason(command) {
82
+ const cmd = String(command || '').trim();
83
+ for (const rmdirMatch of cmd.matchAll(SHELL_CMD_RMDIR)) {
84
+ const target = stripQuotes(rmdirMatch[1].trim());
85
+ if (SHELL_WIN_ROOT_TARGET.test(target)) return `cmd rmdir /s /q targets ${target}`;
86
+ }
87
+ if (/^\s*cmd(?:\.exe)?\s+\/c\b/i.test(cmd)) return null;
88
+
89
+ const tokens = cmd.split(/\s+/);
90
+ const start = tokens.findIndex((token) => SHELL_WIN_DELETE_CMD.test(token));
91
+ if (start < 0) return null;
92
+ let recurse = false;
93
+ let force = false;
94
+ const targets = [];
95
+ for (let i = start + 1; i < tokens.length; i++) {
96
+ const token = tokens[i];
97
+ if (SHELL_WIN_RECURSE.test(token)) {
98
+ recurse = true;
99
+ continue;
100
+ }
101
+ if (SHELL_WIN_FORCE.test(token)) {
102
+ force = true;
103
+ continue;
104
+ }
105
+ const pathSwitch = token.match(SHELL_WIN_PATH);
106
+ if (pathSwitch) {
107
+ if (pathSwitch[1] !== undefined) targets.push(...collectDeleteTargets(pathSwitch[1]));
108
+ else if (i + 1 < tokens.length) targets.push(...collectDeleteTargets(tokens[++i]));
109
+ continue;
110
+ }
111
+ if (token.startsWith('-')) continue;
112
+ targets.push(...collectDeleteTargets(token));
113
+ }
114
+ if (!recurse || !force) return null;
115
+ for (const target of targets) {
116
+ if (SHELL_WIN_ROOT_TARGET.test(target)) return `Remove-Item -Recurse -Force targets ${target}`;
117
+ }
118
+ return null;
119
+ }
120
+
121
+ /** @param {string} command @returns {string | null} */
122
+ function guardShellReason(command) {
123
+ const cmd = String(command || '').trim();
124
+ if (!cmd) return null;
125
+
126
+ const rmMatch = cmd.match(
127
+ /\brm\s+(?:-[a-z]*r[a-z]*f[a-z]*|-f[a-z]*r[a-z]*|-r[a-z]*\s+-?f[a-z]*|-f[a-z]*\s+-?r[a-z]*)\s+(.+)$/,
128
+ );
129
+ if (rmMatch && SHELL_ROOT_TARGET.test(rmMatch[1].trim())) {
130
+ return `rm -rf targets ${rmMatch[1].trim()}`;
131
+ }
132
+ const windowsReason = guardWindowsDeleteReason(cmd);
133
+ if (windowsReason) return windowsReason;
134
+ if (SHELL_FORCE_PUSH.test(cmd)) {
135
+ return 'git push --force overwrites remote history';
136
+ }
137
+ if (/\bgit\s+clean\b[^|;&]*?\s-f/.test(cmd)) {
138
+ return 'git clean -f deletes untracked files';
139
+ }
140
+ if (/\bgit\s+reset\b[^|;&]*?--hard/.test(cmd)) {
141
+ return 'git reset --hard discards working tree changes';
142
+ }
143
+ if (/\bgit\s+(?:checkout|restore)\b[^|;&]*?(?:--\s*\.|\.\s*$|-\.$)/.test(cmd)) {
144
+ return 'git checkout/restore of the whole tree discards uncommitted work';
145
+ }
146
+ if (
147
+ /\b(?:drop\s+(?:table|database|schema|index|view|extension|function)|dropdb|truncate\s+table)\b/i.test(
148
+ cmd,
149
+ )
150
+ ) {
151
+ return 'database drop/truncate is destructive';
152
+ }
153
+ return null;
154
+ }
155
+
156
+ /**
157
+ * @param {{ command?: string }} event
158
+ */
159
+ function guardShellCreate(event) {
160
+ const reason = guardShellReason(event?.command || '');
161
+ if (reason) {
162
+ throw new Error(
163
+ `Regent guardrail: blocked shell command — ${reason}. Use a precise, reviewed alternative.`,
164
+ );
165
+ }
166
+ }
167
+
168
+ // ── Session lineage and dispatch limits ──
169
+ function resolveRootSessionId(sessionId) {
170
+ if (!sessionId) return '';
171
+
172
+ let current = sessionId;
173
+ const visited = new Set();
174
+ while (sessionRoots.has(current) && !visited.has(current)) {
175
+ visited.add(current);
176
+ const parent = sessionRoots.get(current);
177
+ if (parent === '' || parent === current) return parent;
178
+ current = parent;
179
+ }
180
+ return current;
181
+ }
182
+
183
+ function trackSessionLineage(sessionId, rootSessionId) {
184
+ if (sessionId) sessionRoots.set(sessionId, rootSessionId ?? sessionId);
185
+ }
186
+
187
+ function dispatchRateLimit(rootSessionId) {
188
+ const key = rootSessionId || '';
189
+ const now = Date.now();
190
+ const times = dispatchTimesByRoot.get(key) || [];
191
+ while (times.length > 0 && times[0] < now - DISPATCH_WINDOW_MS) times.shift();
192
+ if (times.length >= MAX_DISPATCHES_PER_WINDOW) return false;
193
+ times.push(now);
194
+ dispatchTimesByRoot.set(key, times);
195
+ return true;
196
+ }
197
+
198
+ function circuitIsOpen(rootSessionId) {
199
+ const key = rootSessionId || '';
200
+ const state = circuitStateByRoot.get(key);
201
+ if ((state?.failures || 0) < 2) return false;
202
+
203
+ // Block once when open, then permit one half-open recovery attempt.
204
+ if (state.recoveryReady) {
205
+ circuitStateByRoot.set(key, { ...state, recoveryReady: false, halfOpen: true });
206
+ return false;
207
+ }
208
+
209
+ circuitStateByRoot.set(key, { ...state, recoveryReady: true });
210
+ return true;
211
+ }
212
+
213
+ function recordCircuitResult(rootSessionId, success) {
214
+ const key = rootSessionId || '';
215
+ if (success) {
216
+ circuitStateByRoot.delete(key);
217
+ return;
218
+ }
219
+
220
+ const current = circuitStateByRoot.get(key) || { failures: 0 };
221
+ circuitStateByRoot.set(key, { failures: current.failures + 1 });
222
+ }
223
+
224
+ // ── Evidence tracking ──
225
+ /** @param {string} absolutePath @returns {{ exists: boolean, size?: number, mtimeMs?: number }} */
226
+ function fileFingerprint(absolutePath) {
227
+ try {
228
+ const stat = fs.statSync(absolutePath);
229
+ return { exists: true, size: stat.size, mtimeMs: stat.mtimeMs };
230
+ } catch {
231
+ return { exists: false };
232
+ }
233
+ }
234
+
235
+ /**
236
+ * @param {string[]} files
237
+ * @param {string} directory
238
+ * @returns {Array<{ file: string, absolute: string, stat: { exists: boolean, size?: number, mtimeMs?: number } }>}
239
+ */
240
+ function buildFingerprints(files, directory = '') {
241
+ return files.map((file) => {
242
+ const absolute = path.isAbsolute(file) ? file : path.resolve(directory || process.cwd(), file);
243
+ return { file, absolute, stat: fileFingerprint(absolute) };
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Grade verification evidence by the current state of the files it points at.
249
+ * An entry is FRESH while every file it claimed still matches the fingerprint
250
+ * recorded when the evidence was captured; STALE when any of them changed.
251
+ *
252
+ * @param {any[]} entries
253
+ * @returns {{ total: number, fresh: number, stale: number, missing: number, stale_files: string[] }}
254
+ */
255
+ function evidenceFreshness(entries) {
256
+ const grades = [];
257
+ const staleFiles = [];
258
+ for (const entry of entries) {
259
+ if (entry.verified !== true) continue;
260
+ if (!Array.isArray(entry.fingerprints) || entry.fingerprints.length === 0) {
261
+ grades.push('missing');
262
+ continue;
263
+ }
264
+ let checkable = 0;
265
+ let stale = false;
266
+ for (const fp of entry.fingerprints) {
267
+ if (fp?.stat?.exists !== true) continue;
268
+ checkable++;
269
+ const current = fileFingerprint(fp.absolute);
270
+ if (!current.exists || current.size !== fp.stat.size || current.mtimeMs !== fp.stat.mtimeMs) {
271
+ stale = true;
272
+ staleFiles.push(fp.file);
273
+ }
274
+ }
275
+ if (checkable === 0) {
276
+ grades.push('missing');
277
+ continue;
278
+ }
279
+ grades.push(stale ? 'stale' : 'fresh');
280
+ }
281
+ return {
282
+ total: grades.length,
283
+ fresh: grades.filter((grade) => grade === 'fresh').length,
284
+ stale: grades.filter((grade) => grade === 'stale').length,
285
+ missing: grades.filter((grade) => grade === 'missing').length,
286
+ stale_files: [...new Set(staleFiles)],
287
+ };
288
+ }
289
+
290
+ function recordEvidence(sessionId, files, rootSessionId = '', directory = '') {
291
+ if (files.length > 0) {
292
+ evidenceLog.push({
293
+ sessionId,
294
+ files,
295
+ rootSessionId,
296
+ timestamp: Date.now(),
297
+ verified: false,
298
+ fingerprints: buildFingerprints(files, directory),
299
+ });
300
+ }
301
+ }
302
+ function markEvidenceVerified(sessionId, rootSessionId = '') {
303
+ const root = resolveRootSessionId(rootSessionId);
304
+ for (const entry of evidenceLog) {
305
+ if (
306
+ entry.rootSessionId === root &&
307
+ (entry.sessionId === sessionId || entry.rootSessionId === sessionId)
308
+ ) {
309
+ entry.verified = true;
310
+ }
311
+ }
312
+ for (const [sid, data] of sessionFileChanges) {
313
+ if (data.root === root && (sid === sessionId || data.root === sessionId)) {
314
+ data.verified = true;
315
+ }
316
+ }
317
+ }
318
+ function evidenceForRoot(rootSessionId) {
319
+ const root = resolveRootSessionId(rootSessionId);
320
+ return evidenceLog.filter((entry) => entry.rootSessionId === root);
321
+ }
322
+
323
+ /** @param {string} text @returns {string} */
324
+ function redactSecrets(text) {
325
+ return String(text)
326
+ .replace(/\bauthorization\s*:\s*(?:bearer\s+)?[^\s,;}\]]+/gi, 'authorization: <REDACTED>')
327
+ .replace(
328
+ /((?:api[_-]?key|api key|secret|token|passwd|password|authorization|bearer)(?:\s+provided)?(?:\s*[:=]\s*|\s+(?:is|was|are|were)\s+))["']?[^"'\s,;}\]]+/gi,
329
+ '$1<REDACTED>',
330
+ )
331
+ .replace(/\beyJ[A-Za-z0-9_-]{10,}(?:\.[A-Za-z0-9_-]{10,}){2}\b/g, '<REDACTED>')
332
+ .replace(
333
+ /\b(?:sk-(?:ant|proj|live)-[A-Za-z0-9]{8,}|sk-[A-Za-z0-9]{16,}|sk_live_[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{6,}|AKIA[0-9A-Z]{16})\b/g,
334
+ '<REDACTED>',
335
+ );
336
+ }
337
+
338
+ const SENSITIVE_FOCUS_NAMES = new Set([
339
+ '.aws',
340
+ '.azure',
341
+ '.docker',
342
+ '.htpasswd',
343
+ '.kube',
344
+ '.netrc',
345
+ '.npmrc',
346
+ '.pypirc',
347
+ '.ssh',
348
+ '.yarnrc',
349
+ '.yarnrc.yml',
350
+ '.git-credentials',
351
+ 'authorized_keys',
352
+ 'cookies',
353
+ 'cookies.json',
354
+ 'known_hosts',
355
+ 'login.json',
356
+ 'auth.json',
357
+ 'oauth.json',
358
+ 'passwd',
359
+ 'password',
360
+ 'passwords',
361
+ 'secrets.json',
362
+ 'secret.json',
363
+ 'session.json',
364
+ 'shadow',
365
+ 'token.json',
366
+ 'cert',
367
+ 'certs',
368
+ 'certificates',
369
+ 'key',
370
+ 'keys',
371
+ 'private',
372
+ 'private-keys',
373
+ ]);
374
+
375
+ const SENSITIVE_FOCUS_MATERIAL =
376
+ /(?:^|[._-])(?:certificate|cert|certificates|certs|key|keys|private(?:[._-]?key)?|service[-_]?account|firebase[-_]?adminsdk)(?:$|[._-])/i;
377
+ const SENSITIVE_FOCUS_CREDENTIAL =
378
+ /(?:^|[._-])(?:auth|authorization|cookie|cookies|credential|credentials|oauth|pass(?:word|wd)s?|secret|secrets|session|sessions|token|tokens)(?:$|[._-])/i;
379
+ const SENSITIVE_FOCUS_KEY = /^id_(?:rsa|dsa|ecdsa|ed25519)(?:$|[._-])/i;
380
+ const SENSITIVE_FOCUS_EXTENSION =
381
+ /\.(?:asc|cer|crt|csr|der|gpg|jks|keystore|key|p12|p8|pem|pgp|pfx|ppk)$/i;
382
+
383
+ function isSensitiveFocusPath(focusPath, worktreeRoot) {
384
+ const relative = path.relative(worktreeRoot, focusPath);
385
+ const segments = relative
386
+ .split(path.sep)
387
+ .filter(Boolean)
388
+ .map((segment) => segment.toLowerCase());
389
+ const name = segments.at(-1) || '';
390
+ const parentSegments = segments.slice(0, -1);
391
+
392
+ if (
393
+ parentSegments.some(
394
+ (segment) =>
395
+ segment === '.git' ||
396
+ SENSITIVE_FOCUS_NAMES.has(segment) ||
397
+ SENSITIVE_FOCUS_CREDENTIAL.test(segment) ||
398
+ SENSITIVE_FOCUS_MATERIAL.test(segment),
399
+ )
400
+ ) {
401
+ return true;
402
+ }
403
+ if (name === '.env' || name.startsWith('.env.')) return true;
404
+ if (name === '.git' || SENSITIVE_FOCUS_NAMES.has(name)) return true;
405
+ if (SENSITIVE_FOCUS_CREDENTIAL.test(name) && !/\.(?:log|md|txt)$/i.test(name)) return true;
406
+ return (
407
+ SENSITIVE_FOCUS_MATERIAL.test(name) ||
408
+ SENSITIVE_FOCUS_KEY.test(name) ||
409
+ SENSITIVE_FOCUS_EXTENSION.test(name)
410
+ );
411
+ }
412
+
413
+ /** @param {any} toolContext @returns {string} */
414
+ function resolveDirectory(toolContext) {
415
+ return toolContext?.directory || toolContext?.worktree || process.cwd();
416
+ }
417
+
418
+ async function resolveDirectoryFromSession(sessionApi, toolContext) {
419
+ const directory = resolveDirectory(toolContext);
420
+ if (
421
+ toolContext?.directory ||
422
+ toolContext?.worktree ||
423
+ !toolContext?.sessionID ||
424
+ typeof sessionApi?.get !== 'function'
425
+ ) {
426
+ return directory;
427
+ }
428
+
429
+ try {
430
+ const session = unwrapData(await sessionApi.get({ sessionID: toolContext.sessionID }));
431
+ return session?.location?.directory || directory;
432
+ } catch {
433
+ return directory;
434
+ }
435
+ }
436
+
437
+ /** @param {string} content @returns {string} */
438
+ const extractContent = (content) => {
439
+ const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
440
+ return match ? match[1] : content;
441
+ };
442
+
443
+ // ── Bootstrap cache ──────────────────────────────────────────
444
+ /** @type {string | null | undefined} */
445
+ let bootstrapCache;
446
+
447
+ const getBootstrap = () => {
448
+ if (bootstrapCache !== undefined) return bootstrapCache;
449
+
450
+ const skillPath = path.join(skillsDir, 'using-regent', 'SKILL.md');
451
+ if (!fs.existsSync(skillPath)) {
452
+ bootstrapCache = null;
453
+ return null;
454
+ }
455
+
456
+ const content = extractContent(fs.readFileSync(skillPath, 'utf8'));
457
+
458
+ let constitutionText = '';
459
+ const constitutionPath = path.join(rootDir, 'CONSTITUTION.md');
460
+ if (fs.existsSync(constitutionPath)) {
461
+ constitutionText = fs.readFileSync(constitutionPath, 'utf8');
462
+ }
463
+
464
+ bootstrapCache = `<EXTREMELY_IMPORTANT>
465
+ ${content}
466
+
467
+ ${constitutionText}
468
+
469
+ ## Regent Version
470
+ Regent v${regentVersion}
471
+ </EXTREMELY_IMPORTANT>`;
472
+
473
+ return bootstrapCache;
474
+ };
475
+
476
+ // ── Retry with exponential backoff ──
477
+ async function withRetry(fn, maxRetries = 2) {
478
+ let lastError;
479
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
480
+ try {
481
+ return await fn();
482
+ } catch (err) {
483
+ lastError = err;
484
+ const action = classifyError(err);
485
+ if (action === RecoveryAction.ABORT || action === RecoveryAction.SKIP) throw err;
486
+ if (attempt < maxRetries) {
487
+ const delay = Math.min(1000 * Math.pow(2, attempt), 4000);
488
+ await new Promise((r) => setTimeout(r, delay));
489
+ }
490
+ }
491
+ }
492
+ throw lastError;
493
+ }
494
+
495
+ /**
496
+ * @param {string} text
497
+ * @returns {{ status: string, concerns: string[], filesChanged: string[] }}
498
+ */
499
+ function parseSubagentTextResponse(text) {
500
+ let status = 'done';
501
+ let concerns = [];
502
+
503
+ if (text.includes('BLOCKED')) {
504
+ status = 'blocked';
505
+ } else if (text.includes('NEEDS_CONTEXT')) {
506
+ status = 'needs_context';
507
+ } else if (text.includes('CONCERN:')) {
508
+ status = 'done_with_concerns';
509
+ concerns = text.match(/CONCERN:.*$/gm)?.map((c) => c.replace('CONCERN:', '').trim()) || [];
510
+ }
511
+
512
+ const pathPattern =
513
+ /(?:^|\n)(?:[\w./\\-]+\.[a-zA-Z0-9]+|[\w.-]+(?:[\\/][\w.-]+)+(?:\.[a-zA-Z0-9]+)?|^[A-Za-z][\w-]+\.[\w-]+|^[A-Za-z][\w-]+(?:\.[\w-]+)*$(?!\.))/gm;
514
+ const matches = text.match(pathPattern);
515
+ const filesChanged = (matches || [])
516
+ .map((f) => f.trim())
517
+ .filter(
518
+ (f) =>
519
+ !f.startsWith('CONCERN:') &&
520
+ !f.startsWith('NEEDS_CONTEXT') &&
521
+ !f.startsWith('BLOCKED') &&
522
+ !/^\d+\.\s/.test(f),
523
+ )
524
+ .slice(0, 20);
525
+
526
+ return { status, concerns, filesChanged };
527
+ }
528
+
529
+ function unwrapData(result) {
530
+ return result?.data ?? result;
531
+ }
532
+
533
+ function normalizeAgents(result) {
534
+ const data = unwrapData(result);
535
+ if (Array.isArray(data)) return data;
536
+ if (Array.isArray(data?.agents)) return data.agents;
537
+ return null;
538
+ }
539
+
540
+ function agentMode(agent) {
541
+ return agent?.mode;
542
+ }
543
+
544
+ function isVisibleAgent(agent) {
545
+ return Boolean(agent?.id) && agent.hidden !== true && agent.disabled !== true;
546
+ }
547
+
548
+ function isChildCapableAgent(agent) {
549
+ return isVisibleAgent(agent) && ['subagent', 'all'].includes(agentMode(agent));
550
+ }
551
+
552
+ function isPrimaryCapableAgent(agent) {
553
+ return isVisibleAgent(agent) && ['primary', 'all'].includes(agentMode(agent));
554
+ }
555
+
556
+ function isUnavailableAgentError(err) {
557
+ const message = (err?.message || String(err)).toLowerCase();
558
+ return (
559
+ message.includes('not found') ||
560
+ message.includes('unavailable') ||
561
+ message.includes('invalid agent') ||
562
+ message.includes('agent not')
563
+ );
564
+ }
565
+
566
+ async function createWorkerResolver(agentApi, options = {}) {
567
+ let catalog = null;
568
+ if (typeof agentApi?.list === 'function') {
569
+ try {
570
+ catalog = normalizeAgents(await agentApi.list());
571
+ } catch {
572
+ catalog = null;
573
+ }
574
+ }
575
+
576
+ const findAgent = async (id) => {
577
+ const fromCatalog = catalog?.find((agent) => agent.id === id);
578
+ if (fromCatalog) return fromCatalog;
579
+ if (catalog !== null || typeof agentApi?.get !== 'function') return undefined;
580
+ try {
581
+ return unwrapData(await agentApi.get({ agentID: id }));
582
+ } catch {
583
+ return undefined;
584
+ }
585
+ };
586
+
587
+ const validate = async (id, label = 'agent') => {
588
+ const agent = await findAgent(id);
589
+ if (!agent || !isVisibleAgent(agent)) {
590
+ return { error: `${label} "${id}" is unavailable` };
591
+ }
592
+ if (!isChildCapableAgent(agent)) {
593
+ return { error: `${label} "${id}" is primary-only and cannot be used for subagent dispatch` };
594
+ }
595
+ return { agents: [id], automatic: false };
596
+ };
597
+
598
+ const resolveWorker = async (requestedAgent = '') => {
599
+ const requested = typeof requestedAgent === 'string' ? requestedAgent.trim() : requestedAgent;
600
+ if (requested) return validate(requested);
601
+
602
+ const configuredWorker =
603
+ typeof options.workerAgent === 'string' ? options.workerAgent.trim() : '';
604
+ if (configuredWorker) return validate(configuredWorker, 'workerAgent');
605
+
606
+ if (catalog === null) {
607
+ // `general` is a documented built-in. It is the only safe fallback when
608
+ // a globally installed package cannot inspect project agent definitions.
609
+ return { agents: ['general'], automatic: false };
610
+ }
611
+
612
+ const candidates = [];
613
+ if (isChildCapableAgent(catalog.find((agent) => agent.id === 'regent-general'))) {
614
+ candidates.push('regent-general');
615
+ }
616
+ const configuredGeneral = catalog.find((agent) => agent.id === 'general');
617
+ if (!configuredGeneral || isChildCapableAgent(configuredGeneral)) {
618
+ candidates.push('general');
619
+ }
620
+ for (const agent of catalog) {
621
+ if (isChildCapableAgent(agent) && !candidates.includes(agent.id)) {
622
+ candidates.push(agent.id);
623
+ }
624
+ }
625
+
626
+ if (candidates.length === 0) {
627
+ return { error: 'No visible child-capable worker agent is available' };
628
+ }
629
+ return { agents: candidates, automatic: true };
630
+ };
631
+
632
+ resolveWorker.authorizeCaller = async (toolContext) => {
633
+ const hasAgent =
634
+ toolContext !== null && typeof toolContext === 'object' && 'agent' in toolContext;
635
+ if (!hasAgent) return null;
636
+
637
+ const caller = toolContext.agent;
638
+ const callerId =
639
+ typeof caller === 'string'
640
+ ? caller.trim()
641
+ : caller && typeof caller === 'object'
642
+ ? caller.id
643
+ : '';
644
+ const callerAgent = callerId ? await findAgent(callerId) : undefined;
645
+ if (!isPrimaryCapableAgent(callerAgent)) {
646
+ return 'caller is not a visible primary-capable agent; subagent dispatch is blocked';
647
+ }
648
+ return null;
649
+ };
650
+
651
+ return resolveWorker;
652
+ }
653
+
654
+ async function authorizeDispatchCaller(resolveWorker, toolContext) {
655
+ if (typeof resolveWorker?.authorizeCaller !== 'function') return null;
656
+ return resolveWorker.authorizeCaller(toolContext);
657
+ }
658
+
659
+ function structuredBlockedResult(message) {
660
+ return {
661
+ status: 'blocked',
662
+ output: `Subagent error: ${message}`,
663
+ concerns: [],
664
+ files_changed: [],
665
+ };
666
+ }
667
+
668
+ // ── Shared subagent dispatch ─────────────────────────────────
669
+ /**
670
+ * @param {{ create: Function, generate: Function }} sessionApi
671
+ * @param {string} task
672
+ * @param {string} context
673
+ * @param {string} expectedOutput
674
+ * @param {string} [taskId]
675
+ * @param {any} [toolContext]
676
+ * @param {Function} [resolveWorker]
677
+ * @param {string} [agentOverride]
678
+ * @returns {Promise<{ status: string, output: string, concerns: string[], files_changed: string[], session_id?: string }>}
679
+ */
680
+ async function dispatchSubagent(
681
+ sessionApi,
682
+ task,
683
+ context,
684
+ expectedOutput,
685
+ taskId = '',
686
+ toolContext = undefined,
687
+ resolveWorker = async () => ({ agents: ['general'], automatic: false }),
688
+ agentOverride = '',
689
+ ) {
690
+ const blockedResult = structuredBlockedResult;
691
+
692
+ /** @type {Array<[string, string, number]>} */
693
+ const inputChecks = [
694
+ [task, 'task', MAX_STRING_LENGTH],
695
+ [context, 'context', MAX_STRING_LENGTH],
696
+ [expectedOutput, 'expected_output', MAX_STRING_LENGTH],
697
+ [taskId, 'id', MAX_ID_LENGTH],
698
+ ];
699
+ for (const [value, label, limit] of inputChecks) {
700
+ if (typeof value !== 'string' || value.length > limit) {
701
+ return blockedResult(`${label} is too long (limit ${limit} characters)`);
702
+ }
703
+ }
704
+
705
+ if (agentOverride !== '' && typeof agentOverride !== 'string') {
706
+ return blockedResult('agent must be a string');
707
+ }
708
+ if (typeof agentOverride === 'string' && agentOverride.length > MAX_ID_LENGTH) {
709
+ return blockedResult(`agent is too long (limit ${MAX_ID_LENGTH} characters)`);
710
+ }
711
+
712
+ const callerError = await authorizeDispatchCaller(resolveWorker, toolContext);
713
+ if (callerError) return blockedResult(callerError);
714
+
715
+ const callerSessionId = typeof toolContext?.sessionID === 'string' ? toolContext.sessionID : '';
716
+ if (callerSessionId && !sessionRoots.has(callerSessionId)) {
717
+ trackSessionLineage(callerSessionId, callerSessionId);
718
+ }
719
+ const rootSessionId = resolveRootSessionId(callerSessionId);
720
+ let session;
721
+ try {
722
+ const title = task;
723
+ const directory = await resolveDirectoryFromSession(sessionApi, toolContext);
724
+ const workerSelection = await resolveWorker(agentOverride);
725
+ if (workerSelection.error) return blockedResult(workerSelection.error);
726
+ if (!dispatchRateLimit(rootSessionId)) {
727
+ return blockedResult(
728
+ `dispatch rate limit exceeded (${MAX_DISPATCHES_PER_WINDOW} dispatches per ${DISPATCH_WINDOW_MS / 1000}s)`,
729
+ );
730
+ }
731
+
732
+ for (let index = 0; index < workerSelection.agents.length; index++) {
733
+ const agent = workerSelection.agents[index];
734
+ try {
735
+ const createInput = { title, agent, location: { directory } };
736
+ const sessionResult = await withRetry(() => sessionApi.create(createInput));
737
+ session = unwrapData(sessionResult);
738
+ if (session?.id) break;
739
+ } catch (err) {
740
+ if (
741
+ !workerSelection.automatic ||
742
+ index === workerSelection.agents.length - 1 ||
743
+ !isUnavailableAgentError(err)
744
+ ) {
745
+ throw err;
746
+ }
747
+ }
748
+ }
749
+
750
+ if (!session?.id) throw new Error('session create returned no session');
751
+ pluginWorkerSessionIds.add(session.id);
752
+ trackSessionLineage(session.id, rootSessionId);
753
+
754
+ const prompt = [
755
+ `## Task`,
756
+ task,
757
+ ``,
758
+ `## Context`,
759
+ context,
760
+ ``,
761
+ `## Expected Output`,
762
+ expectedOutput,
763
+ ``,
764
+ `Complete the task. When you finish, provide:`,
765
+ `- summary: What you did`,
766
+ `- status: one of: done, blocked, needs_context, done_with_concerns`,
767
+ `- concerns: Any issues encountered (if status is done_with_concerns)`,
768
+ `- files_changed: List of files created or modified`,
769
+ ``,
770
+ `If you need more context, say NEEDS_CONTEXT and explain what you need.`,
771
+ `If you cannot complete the task, say BLOCKED and explain why.`,
772
+ ].join('\n');
773
+
774
+ const result = await withRetry(() => sessionApi.generate({ sessionID: session.id, prompt }));
775
+ const message = unwrapData(result);
776
+ const output = typeof message?.text === 'string' ? message.text : '';
777
+ const parsed = parseSubagentTextResponse(output);
778
+ const { status, concerns, filesChanged } = parsed;
779
+
780
+ // Track file changes + evidence
781
+ if (filesChanged.length > 0) {
782
+ sessionFileChanges.set(session.id, {
783
+ taskId,
784
+ files: filesChanged,
785
+ timestamp: Date.now(),
786
+ verified: false,
787
+ root: rootSessionId,
788
+ });
789
+ recordEvidence(session.id, filesChanged, rootSessionId, directory);
790
+ }
791
+
792
+ // Session is NOT deleted — remains as visible child session in TUI tree
793
+ return { status, output, concerns, files_changed: filesChanged, session_id: session.id };
794
+ } catch (err) {
795
+ const message = redactSecrets(err instanceof Error ? err.message : String(err)).slice(0, 500);
796
+ return {
797
+ status: 'blocked',
798
+ output: `Subagent error: ${message}`,
799
+ concerns: [],
800
+ files_changed: [],
801
+ session_id: session?.id,
802
+ };
803
+ }
804
+ // No finally block — sessions persist as visible TUI children
805
+ }
806
+
807
+ const TOOL_INPUTS = {
808
+ delegate: {
809
+ type: 'object',
810
+ properties: {
811
+ task: { type: 'string', description: 'The specific task for the subagent to complete' },
812
+ context: { type: 'string', description: 'Background context for the task' },
813
+ expected_output: { type: 'string', description: 'What done looks like for the task' },
814
+ agent: { type: 'string', description: 'Optional child-capable agent ID override' },
815
+ },
816
+ required: ['task', 'context', 'expected_output'],
817
+ additionalProperties: false,
818
+ },
819
+ delegate_many: {
820
+ type: 'object',
821
+ properties: {
822
+ tasks: {
823
+ type: 'array',
824
+ items: {
825
+ type: 'object',
826
+ properties: {
827
+ id: { type: 'string', description: 'Unique identifier for this task' },
828
+ task: { type: 'string', description: 'What this subagent should do' },
829
+ context: { type: 'string', description: 'Background context for this task' },
830
+ expected_output: { type: 'string', description: 'What done looks like for this task' },
831
+ agent: { type: 'string', description: 'Optional child-capable agent ID override' },
832
+ },
833
+ required: ['id', 'task', 'context', 'expected_output'],
834
+ additionalProperties: false,
835
+ },
836
+ },
837
+ },
838
+ required: ['tasks'],
839
+ additionalProperties: false,
840
+ },
841
+ research: {
842
+ type: 'object',
843
+ properties: {
844
+ questions: {
845
+ type: 'array',
846
+ items: {
847
+ type: 'object',
848
+ properties: {
849
+ id: { type: 'string', description: 'Unique identifier' },
850
+ question: { type: 'string', description: 'The question to research' },
851
+ scope: { type: 'string', description: 'Optional narrowing scope' },
852
+ agent: { type: 'string', description: 'Optional child-capable agent ID override' },
853
+ },
854
+ required: ['id', 'question'],
855
+ additionalProperties: false,
856
+ },
857
+ },
858
+ },
859
+ required: ['questions'],
860
+ additionalProperties: false,
861
+ },
862
+ explore: {
863
+ type: 'object',
864
+ properties: {
865
+ query: { type: 'string', description: 'What to understand in the codebase' },
866
+ focus: { type: 'string', description: 'Optional directory path, file pattern, or topic' },
867
+ },
868
+ required: ['query'],
869
+ additionalProperties: false,
870
+ },
871
+ 'changed-files': {
872
+ type: 'object',
873
+ properties: {
874
+ session_id: { type: 'string', description: 'Optional session ID filter' },
875
+ task_id: { type: 'string', description: 'Optional task ID filter' },
876
+ },
877
+ additionalProperties: false,
878
+ },
879
+ verify: {
880
+ type: 'object',
881
+ properties: {
882
+ requirements: { type: 'string', description: 'The requirements text' },
883
+ implementation_context: { type: 'string', description: 'What was built' },
884
+ session_id: { type: 'string', description: 'Optional session ID to verify' },
885
+ },
886
+ required: ['requirements', 'implementation_context'],
887
+ additionalProperties: false,
888
+ },
889
+ };
890
+
891
+ const toContent = (payload) => ({ content: JSON.stringify(payload) });
892
+
893
+ const RUNTIME_COMMAND_NAMES = [
894
+ 'orchestrate',
895
+ 'delegate',
896
+ 'research',
897
+ 'tdd',
898
+ 'diagnose',
899
+ 'verify',
900
+ 'review',
901
+ 'spec',
902
+ 'plan',
903
+ 'ship',
904
+ 'status',
905
+ 'accept',
906
+ ];
907
+
908
+ function frontmatterValue(content, key) {
909
+ return content.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1]?.trim() || '';
910
+ }
911
+
912
+ function readPackageSkills() {
913
+ if (!fs.existsSync(skillsDir)) return [];
914
+
915
+ try {
916
+ return fs
917
+ .readdirSync(skillsDir, { withFileTypes: true })
918
+ .filter((entry) => entry.isDirectory())
919
+ .map((entry) => {
920
+ const location = path.join(skillsDir, entry.name, 'SKILL.md');
921
+ if (!fs.existsSync(location)) return null;
922
+ const content = fs.readFileSync(location, 'utf8');
923
+ return {
924
+ id: entry.name,
925
+ name: frontmatterValue(content, 'name') || entry.name,
926
+ description: frontmatterValue(content, 'description'),
927
+ location,
928
+ content: extractContent(content),
929
+ };
930
+ })
931
+ .filter((skill) => skill !== null);
932
+ } catch {
933
+ return [];
934
+ }
935
+ }
936
+
937
+ function readPackageCommands() {
938
+ const commandsDir = path.join(rootDir, '.opencode', 'commands');
939
+ return RUNTIME_COMMAND_NAMES.map((name) => {
940
+ const location = path.join(commandsDir, `${name}.md`);
941
+ if (!fs.existsSync(location)) {
942
+ return { name, description: `Regent ${name}`, template: '' };
943
+ }
944
+
945
+ try {
946
+ const content = fs.readFileSync(location, 'utf8');
947
+ return {
948
+ name,
949
+ description: frontmatterValue(content, 'description') || `Regent ${name}`,
950
+ template: extractContent(content).trim(),
951
+ };
952
+ } catch {
953
+ return { name, description: `Regent ${name}`, template: '' };
954
+ }
955
+ });
956
+ }
957
+
958
+ const resetState = () => {
959
+ evidenceLog = [];
960
+ sessionFileChanges.clear();
961
+ dispatchTimesByRoot.clear();
962
+ circuitStateByRoot.clear();
963
+ pluginWorkerSessionIds.clear();
964
+ sessionRoots.clear();
965
+ bootstrapCache = undefined;
966
+ };
967
+
968
+ // ── Plugin export ────────────────────────────────────────────
969
+ export default Plugin.define({
970
+ id: 'regent',
971
+ async setup(ctx) {
972
+ const registrations = [];
973
+ const options = ctx.options && typeof ctx.options === 'object' ? ctx.options : {};
974
+ const resolveWorker = await createWorkerResolver(ctx.agent, options);
975
+
976
+ if (typeof options.primaryAgent === 'string' && options.primaryAgent.trim()) {
977
+ if (typeof ctx.agent?.transform === 'function') {
978
+ try {
979
+ const primaryRegistration = await ctx.agent.transform((draft) => {
980
+ const target =
981
+ typeof draft.get === 'function'
982
+ ? draft.get(options.primaryAgent.trim())
983
+ : draft.list?.().find((agent) => agent.id === options.primaryAgent.trim());
984
+ if (isPrimaryCapableAgent(target)) draft.default(options.primaryAgent.trim());
985
+ });
986
+ if (primaryRegistration) registrations.push(primaryRegistration);
987
+ } catch {
988
+ /* invalid optional primaryAgent must not prevent plugin loading */
989
+ }
990
+ }
991
+ }
992
+
993
+ const toolRegistration = await ctx.tool.transform((draft) => {
994
+ draft.add({
995
+ name: 'delegate',
996
+ description:
997
+ '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.',
998
+ input: TOOL_INPUTS.delegate,
999
+ async execute(/** @type {any} */ args, toolContext) {
1000
+ const callerError = await authorizeDispatchCaller(resolveWorker, toolContext);
1001
+ if (callerError) return toContent(structuredBlockedResult(callerError));
1002
+
1003
+ const result = await dispatchSubagent(
1004
+ ctx.session,
1005
+ args.task,
1006
+ args.context,
1007
+ args.expected_output,
1008
+ '',
1009
+ toolContext,
1010
+ resolveWorker,
1011
+ args.agent,
1012
+ );
1013
+ return toContent(result);
1014
+ },
1015
+ });
1016
+
1017
+ draft.add({
1018
+ name: 'delegate_many',
1019
+ description:
1020
+ '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.',
1021
+ input: TOOL_INPUTS.delegate_many,
1022
+ async execute(/** @type {any} */ args, toolContext) {
1023
+ const callerError = await authorizeDispatchCaller(resolveWorker, toolContext);
1024
+ if (callerError) {
1025
+ const blocked = structuredBlockedResult(callerError);
1026
+ const blockedResults = (Array.isArray(args?.tasks) ? args.tasks : [])
1027
+ .slice(0, MAX_DISPATCH_ITEMS)
1028
+ .map((task) => ({ id: task?.id, ...blocked }));
1029
+ return toContent({
1030
+ ...blocked,
1031
+ results: blockedResults,
1032
+ summary: {
1033
+ total: blockedResults.length,
1034
+ completed: 0,
1035
+ failed: blockedResults.length || 1,
1036
+ needs_context: 0,
1037
+ },
1038
+ _blocked: true,
1039
+ });
1040
+ }
1041
+
1042
+ const rootSessionId = resolveRootSessionId(toolContext?.sessionID || '');
1043
+
1044
+ if (!Array.isArray(args.tasks) || args.tasks.length > MAX_DISPATCH_ITEMS) {
1045
+ return toContent({
1046
+ results: [],
1047
+ summary: { total: 0, completed: 0, failed: 1, needs_context: 0 },
1048
+ _error: `delegate_many supports at most ${MAX_DISPATCH_ITEMS} tasks`,
1049
+ });
1050
+ }
1051
+
1052
+ if (circuitIsOpen(rootSessionId)) {
1053
+ return toContent({
1054
+ results: [],
1055
+ summary: { total: 0, completed: 0, failed: 1, needs_context: 0 },
1056
+ _circuit_open: true,
1057
+ _warning:
1058
+ 'Circuit breaker open after repeated delegate_many failures. One half-open recovery attempt is allowed; escalate to Inspector if it fails.',
1059
+ });
1060
+ }
1061
+
1062
+ // Work-stealing: convert to shared queue
1063
+ const queue = [...args.tasks];
1064
+ const results = [];
1065
+
1066
+ async function worker() {
1067
+ while (queue.length > 0) {
1068
+ const t = queue.shift();
1069
+ if (!t) break;
1070
+ const result = await dispatchSubagent(
1071
+ ctx.session,
1072
+ t.task,
1073
+ t.context,
1074
+ t.expected_output,
1075
+ t.id,
1076
+ toolContext,
1077
+ resolveWorker,
1078
+ t.agent,
1079
+ );
1080
+ results.push({ id: t.id, ...result });
1081
+ }
1082
+ }
1083
+
1084
+ const workerCount = Math.min(queue.length, 10);
1085
+ const workers_arr = Array.from({ length: workerCount }, () => worker());
1086
+ await Promise.all(workers_arr);
1087
+
1088
+ const failed = results.filter((r) => r.status === 'blocked').length;
1089
+ recordCircuitResult(rootSessionId, results.length > 0 && failed === 0);
1090
+
1091
+ return toContent({
1092
+ results,
1093
+ summary: {
1094
+ total: results.length,
1095
+ completed: results.filter(
1096
+ (r) => r.status === 'done' || r.status === 'done_with_concerns',
1097
+ ).length,
1098
+ failed,
1099
+ needs_context: results.filter((r) => r.status === 'needs_context').length,
1100
+ },
1101
+ });
1102
+ },
1103
+ });
1104
+
1105
+ draft.add({
1106
+ name: 'research',
1107
+ description:
1108
+ 'Research multiple questions in parallel by dispatching independent research subagents. Each question gets a focused agent. Returns combined findings with synthesis.',
1109
+ input: TOOL_INPUTS.research,
1110
+ async execute(/** @type {any} */ args, toolContext) {
1111
+ const callerError = await authorizeDispatchCaller(resolveWorker, toolContext);
1112
+ if (callerError) {
1113
+ const blocked = structuredBlockedResult(callerError);
1114
+ const blockedFindings = (Array.isArray(args?.questions) ? args.questions : [])
1115
+ .slice(0, MAX_DISPATCH_ITEMS)
1116
+ .map((question) => ({
1117
+ id: question?.id,
1118
+ question: question?.question,
1119
+ ...blocked,
1120
+ }));
1121
+ return toContent({
1122
+ ...blocked,
1123
+ findings: blockedFindings,
1124
+ synthesis: `Blocked: ${callerError}`,
1125
+ _blocked: true,
1126
+ });
1127
+ }
1128
+
1129
+ if (!Array.isArray(args.questions) || args.questions.length > MAX_DISPATCH_ITEMS) {
1130
+ return toContent({
1131
+ findings: [],
1132
+ synthesis: `research supports at most ${MAX_DISPATCH_ITEMS} questions per call`,
1133
+ _error: `research supports at most ${MAX_DISPATCH_ITEMS} questions per call`,
1134
+ });
1135
+ }
1136
+
1137
+ const results = await Promise.all(
1138
+ args.questions.map(async (q) => {
1139
+ const task = `Research this question thoroughly:\n${q.question}`;
1140
+ const taskContext = q.scope
1141
+ ? `Scope: ${q.scope}`
1142
+ : 'Be thorough and concise. Return key findings, data points, and sources.';
1143
+ const result = await dispatchSubagent(
1144
+ ctx.session,
1145
+ task,
1146
+ taskContext,
1147
+ 'Key findings, data points, sources, and recommendations',
1148
+ q.id,
1149
+ toolContext,
1150
+ resolveWorker,
1151
+ q.agent,
1152
+ );
1153
+ return { id: q.id, question: q.question, ...result };
1154
+ }),
1155
+ );
1156
+
1157
+ const completed = results.filter(
1158
+ (r) => r.status === 'done' || r.status === 'done_with_concerns',
1159
+ );
1160
+ const blocked = results.filter((r) => r.status === 'blocked');
1161
+ const needsContext = results.filter((r) => r.status === 'needs_context');
1162
+
1163
+ // Cross-question synthesis: find common themes and contradictions
1164
+ const allOutputs = completed.map((r) => r.output || '');
1165
+ const commonThemes = [];
1166
+ if (allOutputs.length >= 2) {
1167
+ const words = {};
1168
+ for (const output of allOutputs) {
1169
+ const seen = new Set();
1170
+ for (const w of output
1171
+ .toLowerCase()
1172
+ .split(/\s+/)
1173
+ .filter((w) => w.length > 5)) {
1174
+ if (!seen.has(w)) {
1175
+ seen.add(w);
1176
+ words[w] = (words[w] || 0) + 1;
1177
+ }
1178
+ }
1179
+ }
1180
+ for (const [word, count] of Object.entries(words)) {
1181
+ if (count >= Math.ceil(allOutputs.length / 2)) {
1182
+ commonThemes.push(word);
1183
+ }
1184
+ }
1185
+ }
1186
+
1187
+ const summaryParts = [];
1188
+ if (completed.length > 0) {
1189
+ summaryParts.push(`Addressed: ${completed.map((r) => r.question).join(', ')}`);
1190
+ }
1191
+ if (blocked.length > 0) {
1192
+ summaryParts.push(`Blocked: ${blocked.map((r) => r.question).join(', ')}`);
1193
+ }
1194
+ if (needsContext.length > 0) {
1195
+ summaryParts.push(`Needs context: ${needsContext.map((r) => r.question).join(', ')}`);
1196
+ }
1197
+ if (commonThemes.length > 0) {
1198
+ summaryParts.push(`Common themes: ${commonThemes.slice(0, 10).join(', ')}`);
1199
+ }
1200
+
1201
+ return toContent({
1202
+ findings: results,
1203
+ synthesis:
1204
+ summaryParts.length > 0
1205
+ ? summaryParts.join('. ') + '.'
1206
+ : 'No research results returned.',
1207
+ });
1208
+ },
1209
+ });
1210
+
1211
+ draft.add({
1212
+ name: 'explore',
1213
+ description:
1214
+ 'Analyze the project codebase to answer structural questions. Uses SDK file operations to understand directory layout, key files, and patterns. Call this before planning to understand what exists.',
1215
+ input: TOOL_INPUTS.explore,
1216
+ async execute(/** @type {any} */ args, context) {
1217
+ const toolContext = /** @type {any} */ (context);
1218
+ const worktree = await resolveDirectoryFromSession(ctx.session, toolContext);
1219
+ if (!worktree) {
1220
+ return toContent({
1221
+ structure:
1222
+ 'Error: Cannot determine project directory. OpenCode runtime did not provide context.directory or context.worktree.',
1223
+ summary: 'Exploration requires context.directory or context.worktree',
1224
+ });
1225
+ }
1226
+ let result = `Codebase exploration for: ${args.query}\n\n`;
1227
+
1228
+ // Top-level listing
1229
+ try {
1230
+ const items = fs.readdirSync(worktree, { withFileTypes: true });
1231
+ result += '## Top-level contents\n';
1232
+ for (const item of items) {
1233
+ if (item.name.startsWith('.') && item.name !== '.gitignore') continue;
1234
+ result += `${item.isDirectory() ? '/' : ''} ${item.name}\n`;
1235
+ }
1236
+ result += '\n';
1237
+ } catch {
1238
+ /* ignore read errors */
1239
+ }
1240
+
1241
+ // Walk src/ if it exists (up to 3 levels)
1242
+ try {
1243
+ const srcDir = path.join(worktree, 'src');
1244
+ if (fs.existsSync(srcDir)) {
1245
+ result += '## src/ directory\n';
1246
+ /** @param {string} dir @param {number} depth */
1247
+ const walk = (dir, depth) => {
1248
+ if (depth > 3) return;
1249
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1250
+ for (const entry of entries) {
1251
+ if (entry.name.startsWith('.')) continue;
1252
+ const full = path.join(dir, entry.name);
1253
+ const indent = ' '.repeat(depth);
1254
+ if (entry.isDirectory()) {
1255
+ result += `${indent}${entry.name}/\n`;
1256
+ walk(full, depth + 1);
1257
+ } else {
1258
+ result += `${indent}${entry.name}\n`;
1259
+ }
1260
+ }
1261
+ };
1262
+ walk(srcDir, 0);
1263
+ }
1264
+ } catch {
1265
+ /* ignore read errors */
1266
+ }
1267
+
1268
+ // Focus path
1269
+ if (args.focus) {
1270
+ const worktreeRoot = path.resolve(worktree);
1271
+ const focusPath = path.resolve(worktree, args.focus);
1272
+ const lexicalInside =
1273
+ focusPath.startsWith(worktreeRoot + path.sep) || focusPath === worktreeRoot;
1274
+ if (!lexicalInside) {
1275
+ result += `\n## Focus: ${args.focus}\n(path outside project directory)\n`;
1276
+ } else if (isSensitiveFocusPath(focusPath, worktreeRoot)) {
1277
+ result += `\n## Focus: ${args.focus}\n(access denied: sensitive path)\n`;
1278
+ } else {
1279
+ let realRoot = worktreeRoot;
1280
+ let realFocus = focusPath;
1281
+ try {
1282
+ realRoot = fs.realpathSync(worktreeRoot);
1283
+ realFocus = fs.realpathSync(focusPath);
1284
+ } catch {
1285
+ /* fall back to lexical paths */
1286
+ }
1287
+ const inside =
1288
+ (realFocus.startsWith(realRoot + path.sep) || realFocus === realRoot) &&
1289
+ (focusPath.startsWith(worktreeRoot + path.sep) || focusPath === worktreeRoot);
1290
+ if (!inside) {
1291
+ result += `\n## Focus: ${args.focus}\n(path outside project directory)\n`;
1292
+ } else if (isSensitiveFocusPath(realFocus, realRoot)) {
1293
+ result += `\n## Focus: ${args.focus}\n(access denied: sensitive path)\n`;
1294
+ } else if (fs.existsSync(focusPath)) {
1295
+ const stat = fs.statSync(focusPath);
1296
+ if (stat.isDirectory()) {
1297
+ const items = fs.readdirSync(focusPath);
1298
+ result += items.join('\n') + '\n';
1299
+ } else {
1300
+ try {
1301
+ const content = redactSecrets(
1302
+ fs.readFileSync(focusPath, 'utf8').slice(0, 3000),
1303
+ );
1304
+ result += '```\n' + content + '\n```\n';
1305
+ } catch {
1306
+ result += '(path not found)\n';
1307
+ }
1308
+ }
1309
+ } else {
1310
+ result += '(path not found)\n';
1311
+ }
1312
+ }
1313
+ }
1314
+
1315
+ return toContent({ structure: result, summary: `Explored ${args.query}` });
1316
+ },
1317
+ });
1318
+
1319
+ draft.add({
1320
+ name: 'changed-files',
1321
+ description:
1322
+ 'View files changed by subagent dispatches in this session. Returns navigable tree of what each subagent touched.',
1323
+ input: TOOL_INPUTS['changed-files'],
1324
+ async execute(/** @type {any} */ args, toolContext) {
1325
+ const filters = args || {};
1326
+ const rootSessionId = resolveRootSessionId(toolContext?.sessionID || '');
1327
+ const entries = [];
1328
+ for (const [sid, data] of sessionFileChanges) {
1329
+ if (data.root !== rootSessionId) continue;
1330
+ if (filters.session_id && sid !== filters.session_id) continue;
1331
+ if (filters.task_id && data.taskId !== filters.task_id) continue;
1332
+ entries.push({ session_id: sid, ...data });
1333
+ }
1334
+ return toContent({
1335
+ entries,
1336
+ total: entries.length,
1337
+ unverified: entries.filter((e) => !e.verified).length,
1338
+ });
1339
+ },
1340
+ });
1341
+
1342
+ draft.add({
1343
+ name: 'verify',
1344
+ description:
1345
+ 'Compare implementation against requirements. Returns structured pass/fail per requirement, flags extras (YAGNI). Includes evidence gate: reports unverified file changes. Use after execution to check if work meets the plan.',
1346
+ input: TOOL_INPUTS.verify,
1347
+ async execute(/** @type {any} */ args, toolContext) {
1348
+ const callerSession =
1349
+ typeof toolContext?.sessionID === 'string' ? toolContext.sessionID : '';
1350
+ const callerRoot = resolveRootSessionId(callerSession);
1351
+ if (!args || args.requirements == null || args.implementation_context == null) {
1352
+ const callerEvidence = evidenceForRoot(callerRoot);
1353
+ return toContent({
1354
+ compliant: false,
1355
+ requirements_met: [],
1356
+ requirements_unmet: [],
1357
+ extras_built: [],
1358
+ evidence_gate: {
1359
+ unverified_changes: callerEvidence.filter((entry) => !entry.verified).length,
1360
+ freshness: evidenceFreshness(callerEvidence),
1361
+ },
1362
+ summary:
1363
+ 'Missing required arguments: provide "requirements" and "implementation_context"',
1364
+ });
1365
+ }
1366
+
1367
+ /** @param {string} s */
1368
+ const getKeyBigrams = (s) => {
1369
+ const words = s
1370
+ .toLowerCase()
1371
+ .split(/\s+/)
1372
+ .filter((w) => w.length > 3);
1373
+ const bigrams = /** @type {string[]} */ ([]);
1374
+ for (let i = 0; i < words.length - 1; i++) {
1375
+ bigrams.push(words[i] + ' ' + words[i + 1]);
1376
+ }
1377
+ return bigrams;
1378
+ };
1379
+
1380
+ /** @param {string} s */
1381
+ const getKeyUnigrams = (s) =>
1382
+ s
1383
+ .toLowerCase()
1384
+ .split(/\s+/)
1385
+ .filter((w) => w.length > 3);
1386
+
1387
+ /** @param {string} s */
1388
+ const stripCheckbox = (s) =>
1389
+ s
1390
+ .replace(/^[-*]\s*\[\s*[x ]?\s*\]\s*/i, '')
1391
+ .replace(/^[-*\d+.]\s+/, '')
1392
+ .trim();
1393
+
1394
+ // Support checklist format: category headers with - [x] items
1395
+ const reqs = args.requirements
1396
+ .split('\n')
1397
+ .map((r) => stripCheckbox(r))
1398
+ .filter(
1399
+ (r) => r.length > 2 && !r.startsWith('#') && !r.startsWith('```') && !r.endsWith(':'),
1400
+ );
1401
+
1402
+ const impl = args.implementation_context.toLowerCase();
1403
+
1404
+ const met = [];
1405
+ const unmet = [];
1406
+
1407
+ for (const req of reqs) {
1408
+ const reqBigrams = getKeyBigrams(req);
1409
+ const reqUnigrams = getKeyUnigrams(req);
1410
+ let found;
1411
+ if (reqBigrams.length > 0) {
1412
+ found = reqBigrams.some((b) => impl.includes(b));
1413
+ } else {
1414
+ found = reqUnigrams.some((w) => impl.includes(w));
1415
+ }
1416
+ if (found) {
1417
+ met.push(req);
1418
+ } else {
1419
+ unmet.push(req);
1420
+ }
1421
+ }
1422
+
1423
+ const implLines = args.implementation_context
1424
+ .split('\n')
1425
+ .map((l) => stripCheckbox(l))
1426
+ .filter((l) => l.length > 3 && !l.startsWith('#') && !l.startsWith('```'));
1427
+
1428
+ const extras = implLines.filter((line) => {
1429
+ const lineLower = line.toLowerCase();
1430
+ return !reqs.some((req) => {
1431
+ const reqBigrams = getKeyBigrams(req);
1432
+ const reqUnigrams = getKeyUnigrams(req);
1433
+ if (reqBigrams.length > 0) {
1434
+ return reqBigrams.some((b) => lineLower.includes(b));
1435
+ }
1436
+ return reqUnigrams.some((w) => lineLower.includes(w));
1437
+ });
1438
+ });
1439
+
1440
+ // Mark evidence as verified only for the caller's own lineage
1441
+ let verificationNote = null;
1442
+ if (args.session_id) {
1443
+ if (callerSession && resolveRootSessionId(args.session_id) === callerRoot) {
1444
+ markEvidenceVerified(args.session_id, callerRoot);
1445
+ } else {
1446
+ verificationNote =
1447
+ 'session_id does not match the calling session; evidence was not marked verified';
1448
+ }
1449
+ }
1450
+
1451
+ const callerEvidence = evidenceForRoot(callerRoot);
1452
+ const unverifiedCount = callerEvidence.filter((entry) => !entry.verified).length;
1453
+ const freshness = evidenceFreshness(callerEvidence);
1454
+
1455
+ // Assess confidence: low when many unmet with sparse context
1456
+ const implWords = args.implementation_context.split(/\s+/).length;
1457
+ const lowConfidence = unmet.length > 0 && implWords < 20;
1458
+
1459
+ const warningParts = [];
1460
+ if (unverifiedCount > 0) {
1461
+ warningParts.push(
1462
+ `${unverifiedCount} file change(s) not followed by verification command`,
1463
+ );
1464
+ }
1465
+ if (freshness.stale > 0) {
1466
+ warningParts.push(
1467
+ `${freshness.stale} verified change(s) went stale (evidence no longer matches files on disk)`,
1468
+ );
1469
+ }
1470
+
1471
+ return toContent({
1472
+ compliant: unmet.length === 0 && unverifiedCount === 0 && freshness.stale === 0,
1473
+ requirements_met: met,
1474
+ requirements_unmet: unmet,
1475
+ extras_built: extras,
1476
+ verification_note: verificationNote,
1477
+ evidence_gate: {
1478
+ unverified_changes: unverifiedCount,
1479
+ total_changes: callerEvidence.length,
1480
+ freshness,
1481
+ warning: warningParts.length > 0 ? warningParts.join('; ') : null,
1482
+ },
1483
+ confidence_assessment: lowConfidence
1484
+ ? 'low — sparse implementation context may hide unverified requirements'
1485
+ : 'adequate',
1486
+ recommend_delegation:
1487
+ unmet.length > 0
1488
+ ? 'Consider delegating each unmet requirement to a subagent for detailed verification'
1489
+ : null,
1490
+ summary: `${met.length}/${reqs.length} requirements met, ${extras.length} extras flagged, ${unverifiedCount} unverified changes`,
1491
+ });
1492
+ },
1493
+ });
1494
+ });
1495
+
1496
+ if (toolRegistration) registrations.push(toolRegistration);
1497
+
1498
+ if (typeof ctx.shell?.hook === 'function') {
1499
+ try {
1500
+ const shellRegistration = await ctx.shell.hook('create.before', guardShellCreate);
1501
+ if (shellRegistration) registrations.push(shellRegistration);
1502
+ } catch {
1503
+ /* guardrail must never prevent the plugin from loading */
1504
+ }
1505
+ }
1506
+
1507
+ if (typeof ctx.skill?.transform === 'function') {
1508
+ const skillRegistration = await ctx.skill.transform((draft) => {
1509
+ const existingIds = new Set(
1510
+ typeof draft.list === 'function'
1511
+ ? draft.list().map((skill) => String(skill.id || skill.name))
1512
+ : [],
1513
+ );
1514
+ for (const skill of readPackageSkills()) {
1515
+ if (!existingIds.has(skill.id)) draft.add(/** @type {any} */ (skill));
1516
+ }
1517
+ });
1518
+ if (skillRegistration) registrations.push(skillRegistration);
1519
+ }
1520
+
1521
+ if (typeof ctx.command?.transform === 'function') {
1522
+ const commandRegistration = await ctx.command.transform((draft) => {
1523
+ for (const command of readPackageCommands()) {
1524
+ draft.add({
1525
+ name: `regent/${command.name}`,
1526
+ description: command.description,
1527
+ execute: async ({ sessionID, prompt, delivery }) => {
1528
+ const invocationPrompt = prompt && typeof prompt === 'object' ? prompt : { text: '' };
1529
+ const argumentsText =
1530
+ typeof invocationPrompt.text === 'string' ? invocationPrompt.text : '';
1531
+ const text = command.template.includes('$ARGUMENTS')
1532
+ ? command.template.replaceAll('$ARGUMENTS', () => argumentsText)
1533
+ : argumentsText.trim()
1534
+ ? `${command.template}\n\n${argumentsText}`
1535
+ : command.template;
1536
+ const sessionApi = /** @type {any} */ (ctx.session);
1537
+ await sessionApi.prompt({
1538
+ ...invocationPrompt,
1539
+ sessionID,
1540
+ text,
1541
+ delivery,
1542
+ });
1543
+ },
1544
+ });
1545
+ }
1546
+ });
1547
+ if (commandRegistration) registrations.push(commandRegistration);
1548
+ }
1549
+
1550
+ const contextRegistration = await ctx.session.hook('context', async (event) => {
1551
+ const sessionId = typeof event?.sessionID === 'string' ? event.sessionID : '';
1552
+ let isWorkerSession = pluginWorkerSessionIds.has(sessionId);
1553
+ if (!isWorkerSession && sessionId && typeof ctx.session?.get === 'function') {
1554
+ try {
1555
+ const sessionInfo = unwrapData(await ctx.session.get({ sessionID: sessionId }));
1556
+ const pluginManagedSession =
1557
+ pluginWorkerSessionIds.has(sessionInfo?.id) ||
1558
+ pluginWorkerSessionIds.has(sessionInfo?.parentID);
1559
+ isWorkerSession = Boolean(sessionInfo?.parentID) || pluginManagedSession;
1560
+ if (pluginManagedSession && sessionInfo?.id) {
1561
+ pluginWorkerSessionIds.add(sessionInfo.id);
1562
+ if (sessionInfo.parentID) {
1563
+ trackSessionLineage(sessionInfo.id, resolveRootSessionId(sessionInfo.parentID));
1564
+ }
1565
+ }
1566
+ } catch {
1567
+ /* test doubles and unavailable session metadata are non-fatal */
1568
+ }
1569
+ }
1570
+
1571
+ if (isWorkerSession) {
1572
+ if (event?.tools && typeof event.tools === 'object') {
1573
+ for (const name of ['delegate', 'delegate_many', 'research']) {
1574
+ delete event.tools[name];
1575
+ }
1576
+ }
1577
+ return;
1578
+ }
1579
+
1580
+ if (!Array.isArray(event.system)) event.system = [];
1581
+ const bootstrap = getBootstrap();
1582
+ const hasBootstrap = event.system.some(
1583
+ (part) => part.type === 'text' && part.text.includes('EXTREMELY_IMPORTANT'),
1584
+ );
1585
+ const additions = [];
1586
+ const rootSessionId = resolveRootSessionId(sessionId);
1587
+
1588
+ if (bootstrap && !hasBootstrap) additions.push(bootstrap);
1589
+
1590
+ const unverified = evidenceLog.filter(
1591
+ (entry) => !entry.verified && entry.rootSessionId === rootSessionId,
1592
+ );
1593
+ if (unverified.length > 0) {
1594
+ additions.push(`## Regent Evidence Log (unverified)\n${JSON.stringify(unverified)}\n`);
1595
+ }
1596
+
1597
+ const activeChanges = [...sessionFileChanges.entries()]
1598
+ .filter(([_, data]) => !data.verified && data.root === rootSessionId)
1599
+ .map(([sessionId, data]) => `${sessionId}: ${data.files.join(', ')}`);
1600
+ if (activeChanges.length > 0) {
1601
+ additions.push(`## Regent Active File Changes\n${activeChanges.join('\n')}\n`);
1602
+ }
1603
+
1604
+ if (additions.length > 0) {
1605
+ event.system.push({ type: 'text', text: additions.join('\n') });
1606
+ }
1607
+ });
1608
+
1609
+ if (contextRegistration) registrations.push(contextRegistration);
1610
+
1611
+ return async () => {
1612
+ try {
1613
+ await Promise.all(
1614
+ registrations
1615
+ .filter((registration) => typeof registration?.dispose === 'function')
1616
+ .map((registration) => registration.dispose()),
1617
+ );
1618
+ } finally {
1619
+ resetState();
1620
+ }
1621
+ };
1622
+ },
1623
+ });