@yemi33/minions 0.1.2382 → 0.1.2384

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 (51) hide show
  1. package/bin/minions.js +1 -0
  2. package/dashboard/js/refresh.js +2 -1
  3. package/dashboard/js/settings.js +1 -1
  4. package/dashboard.js +37 -19
  5. package/docs/completion-reports.md +20 -1
  6. package/docs/keep-processes.md +7 -2
  7. package/docs/live-checkout-mode.md +7 -7
  8. package/docs/managed-spawn.md +14 -1
  9. package/docs/runtime-adapters.md +61 -26
  10. package/docs/skills.md +2 -0
  11. package/docs/workspace-manifests.md +1 -1
  12. package/docs/worktree-lifecycle.md +36 -0
  13. package/engine/acp-transport.js +273 -58
  14. package/engine/ado-comment.js +3 -1
  15. package/engine/agent-worker-pool.js +278 -54
  16. package/engine/cc-worker-pool.js +12 -3
  17. package/engine/cleanup.js +15 -11
  18. package/engine/cli.js +56 -28
  19. package/engine/comment-format.js +51 -14
  20. package/engine/create-pr-worktree.js +57 -79
  21. package/engine/dispatch.js +7 -2
  22. package/engine/execution-model.js +68 -0
  23. package/engine/gh-comment.js +6 -3
  24. package/engine/github.js +6 -7
  25. package/engine/keep-process-sweep.js +131 -9
  26. package/engine/lifecycle.js +126 -45
  27. package/engine/live-checkout.js +4 -2
  28. package/engine/llm.js +59 -83
  29. package/engine/managed-spawn.js +112 -5
  30. package/engine/memory-store.js +3 -1
  31. package/engine/playbook.js +4 -6
  32. package/engine/pooled-agent-process.js +512 -45
  33. package/engine/pr-action.js +6 -8
  34. package/engine/process-utils.js +154 -18
  35. package/engine/runtimes/claude.js +94 -25
  36. package/engine/runtimes/codex.js +1 -0
  37. package/engine/runtimes/contract.js +239 -0
  38. package/engine/runtimes/copilot.js +97 -9
  39. package/engine/runtimes/index.js +37 -9
  40. package/engine/shared.js +349 -82
  41. package/engine/spawn-agent.js +85 -116
  42. package/engine/spawn-phase-watchdog.js +8 -37
  43. package/engine/timeout.js +14 -10
  44. package/engine/worktree-gc.js +55 -46
  45. package/engine.js +418 -241
  46. package/package.json +1 -1
  47. package/playbooks/fix.md +20 -0
  48. package/playbooks/review.md +2 -0
  49. package/playbooks/shared-rules.md +2 -2
  50. package/prompts/cc-system.md +11 -11
  51. package/skills/check-self-authored-review-comment/SKILL.md +34 -0
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ const ADAPTER_API_VERSION = 1;
4
+ const RUNTIME_OPTIONS_FLAG = '--runtime-options';
5
+ const RUNTIME_IMAGES_FILE_OPTION = '_minionsImagesFile';
6
+ const legacyAdapters = new WeakSet();
7
+
8
+ const CORE_METHODS = Object.freeze([
9
+ 'resolveBinary',
10
+ 'listModels',
11
+ 'buildArgs',
12
+ 'buildPrompt',
13
+ 'resolveModel',
14
+ 'parseOutput',
15
+ 'parseStreamChunk',
16
+ 'parseError',
17
+ ]);
18
+
19
+ function normalizeRuntimeAdapter(name, adapter, { strict = false } = {}) {
20
+ if (!name || typeof name !== 'string') {
21
+ throw new Error('registerRuntime: name must be a non-empty string');
22
+ }
23
+ if (!adapter || typeof adapter !== 'object' || Array.isArray(adapter)) {
24
+ throw new Error('registerRuntime: adapter must be an object');
25
+ }
26
+ if (adapter.name != null && adapter.name !== name) {
27
+ throw new Error(`registerRuntime: adapter name "${adapter.name}" does not match registry key "${name}"`);
28
+ }
29
+ const hasExplicitApiVersion = adapter.apiVersion != null;
30
+ if (strict && !hasExplicitApiVersion) {
31
+ throw new Error(`Runtime "${name}" adapter must declare apiVersion ${ADAPTER_API_VERSION}`);
32
+ }
33
+ if (hasExplicitApiVersion && adapter.apiVersion !== ADAPTER_API_VERSION) {
34
+ throw new Error(
35
+ `Runtime "${name}" adapter API version ${adapter.apiVersion} is unsupported; expected ${ADAPTER_API_VERSION}`
36
+ );
37
+ }
38
+
39
+ adapter.name = name;
40
+ adapter.apiVersion = ADAPTER_API_VERSION;
41
+ if (!hasExplicitApiVersion) legacyAdapters.add(adapter);
42
+ if (adapter.capabilities == null) {
43
+ adapter.capabilities = {};
44
+ } else if (typeof adapter.capabilities !== 'object' || Array.isArray(adapter.capabilities)) {
45
+ throw new Error(`Runtime "${name}" adapter member capabilities must be an object`);
46
+ }
47
+
48
+ for (const method of CORE_METHODS) {
49
+ if (adapter[method] != null && typeof adapter[method] !== 'function') {
50
+ throw new Error(`Runtime "${name}" adapter member ${method} must be a function`);
51
+ }
52
+ if (strict && typeof adapter[method] !== 'function') {
53
+ throw new Error(`Runtime "${name}" adapter is missing required method ${method}()`);
54
+ }
55
+ }
56
+
57
+ if (adapter.capabilities.streamConsumer === true && typeof adapter.createStreamConsumer !== 'function') {
58
+ throw new Error(`Runtime "${name}" declares streamConsumer but does not implement createStreamConsumer()`);
59
+ }
60
+ if (adapter.capabilities.acpWorkerPool === true) {
61
+ for (const method of ['buildWorkerArgs', 'encodePooledOutput']) {
62
+ if (typeof adapter[method] !== 'function') {
63
+ throw new Error(`Runtime "${name}" declares acpWorkerPool but does not implement ${method}()`);
64
+ }
65
+ }
66
+ }
67
+ return adapter;
68
+ }
69
+
70
+ function sanitizeInvocationOptions(runtime, options = {}) {
71
+ const caps = runtime?.capabilities || {};
72
+ const sanitized = { ...(options || {}) };
73
+ if (!caps.effortLevels) delete sanitized.effort;
74
+ if (!caps.sessionResume
75
+ || typeof sanitized.sessionId !== 'string'
76
+ || !sanitized.sessionId.trim()) {
77
+ delete sanitized.sessionId;
78
+ } else {
79
+ sanitized.sessionId = sanitized.sessionId.trim();
80
+ }
81
+ if (!caps.budgetCap) delete sanitized.maxBudget;
82
+ if (!caps.bareMode) delete sanitized.bare;
83
+ if (!caps.fallbackModel) delete sanitized.fallbackModel;
84
+ if (!caps.imageInput) {
85
+ delete sanitized.images;
86
+ delete sanitized[RUNTIME_IMAGES_FILE_OPTION];
87
+ }
88
+ return sanitized;
89
+ }
90
+
91
+ function resolveInvocationOptions(runtime, options = {}, context = {}) {
92
+ if (!runtime || typeof runtime !== 'object') throw new Error('resolveInvocationOptions: runtime adapter is required');
93
+ let resolved = { ...(options || {}) };
94
+ if (typeof runtime.resolveInvocationOptions === 'function') {
95
+ resolved = runtime.resolveInvocationOptions({
96
+ ...context,
97
+ options: resolved,
98
+ });
99
+ }
100
+ if (!resolved || typeof resolved !== 'object' || Array.isArray(resolved)) {
101
+ throw new Error(`Runtime "${runtime.name}" resolveInvocationOptions() must return an object`);
102
+ }
103
+ return sanitizeInvocationOptions(runtime, resolved);
104
+ }
105
+
106
+ function encodeRuntimeOptions(options = {}) {
107
+ return Buffer.from(JSON.stringify(options || {}), 'utf8').toString('base64url');
108
+ }
109
+
110
+ function decodeRuntimeOptions(encoded) {
111
+ if (typeof encoded !== 'string' || !encoded) {
112
+ throw new Error(`${RUNTIME_OPTIONS_FLAG} requires a non-empty base64url value`);
113
+ }
114
+ let decoded;
115
+ try {
116
+ decoded = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8'));
117
+ } catch (err) {
118
+ throw new Error(`Invalid ${RUNTIME_OPTIONS_FLAG} payload: ${err.message}`);
119
+ }
120
+ if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded)) {
121
+ throw new Error(`Invalid ${RUNTIME_OPTIONS_FLAG} payload: expected a JSON object`);
122
+ }
123
+ for (const key of ['__proto__', 'prototype', 'constructor']) {
124
+ if (Object.prototype.hasOwnProperty.call(decoded, key)) {
125
+ throw new Error(`Invalid ${RUNTIME_OPTIONS_FLAG} payload: forbidden key "${key}"`);
126
+ }
127
+ }
128
+ return decoded;
129
+ }
130
+
131
+ function buildSpawnFlags(runtime, options = {}) {
132
+ if (!runtime || typeof runtime !== 'object') throw new Error('buildSpawnFlags: runtime adapter is required');
133
+ const isLegacy = runtime.apiVersion !== ADAPTER_API_VERSION || legacyAdapters.has(runtime);
134
+ if (isLegacy && typeof runtime.buildSpawnFlags === 'function') {
135
+ const legacyFlags = runtime.buildSpawnFlags(options || {});
136
+ if (!Array.isArray(legacyFlags) || legacyFlags.some(flag => typeof flag !== 'string')) {
137
+ throw new Error(`Runtime "${runtime.name}" buildSpawnFlags() must return string[]`);
138
+ }
139
+ return legacyFlags;
140
+ }
141
+
142
+ const sanitized = sanitizeInvocationOptions(runtime, options);
143
+ let flags;
144
+ if (typeof runtime.buildSpawnFlags === 'function') {
145
+ flags = runtime.buildSpawnFlags(sanitized);
146
+ } else {
147
+ const caps = runtime.capabilities || {};
148
+ flags = ['--runtime', String(runtime.name)];
149
+ if (sanitized.maxTurns != null) flags.push('--max-turns', String(sanitized.maxTurns));
150
+ if (sanitized.model) flags.push('--model', String(sanitized.model));
151
+ if (sanitized.allowedTools) flags.push('--allowedTools', String(sanitized.allowedTools));
152
+ if (caps.effortLevels && sanitized.effort) flags.push('--effort', String(sanitized.effort));
153
+ if (caps.sessionResume && sanitized.sessionId) flags.push('--resume', String(sanitized.sessionId));
154
+ if (caps.budgetCap && sanitized.maxBudget != null) flags.push('--max-budget-usd', String(sanitized.maxBudget));
155
+ if (caps.bareMode && sanitized.bare === true) flags.push('--bare');
156
+ if (caps.fallbackModel && sanitized.fallbackModel) {
157
+ flags.push('--fallback-model', String(sanitized.fallbackModel));
158
+ }
159
+ if (sanitized.stream === 'on' || sanitized.stream === 'off') {
160
+ flags.push('--stream', sanitized.stream);
161
+ }
162
+ if (sanitized.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
163
+ if (sanitized.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
164
+ }
165
+ if (!Array.isArray(flags) || flags.some(flag => typeof flag !== 'string')) {
166
+ throw new Error(`Runtime "${runtime.name}" buildSpawnFlags() must return string[]`);
167
+ }
168
+ if (flags[0] !== '--runtime') flags = ['--runtime', String(runtime.name), ...flags];
169
+ if (flags[1] !== runtime.name) {
170
+ throw new Error(`Runtime "${runtime.name}" buildSpawnFlags() emitted mismatched runtime "${flags[1]}"`);
171
+ }
172
+ if (isLegacy) return flags;
173
+ return [...flags, RUNTIME_OPTIONS_FLAG, encodeRuntimeOptions(sanitized)];
174
+ }
175
+
176
+ function prepareWorkspace(runtime, context = {}) {
177
+ if (!runtime || typeof runtime.prepareWorkspace !== 'function') {
178
+ return { wrote: false, reason: 'unsupported-runtime', servers: [] };
179
+ }
180
+ return runtime.prepareWorkspace(context);
181
+ }
182
+
183
+ function isSpawnStartupEvent(runtime, event) {
184
+ if (!runtime || typeof runtime.isSpawnStartupEvent !== 'function') return false;
185
+ try {
186
+ return runtime.isSpawnStartupEvent(event) === true;
187
+ } catch {
188
+ return false;
189
+ }
190
+ }
191
+
192
+ function shouldSuppressPostMutationError(runtime, context = {}) {
193
+ if (!runtime || typeof runtime.shouldSuppressPostMutationError !== 'function') return false;
194
+ try {
195
+ return runtime.shouldSuppressPostMutationError(context) === true;
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+
201
+ function buildWorkerArgs(runtime, options = {}) {
202
+ if (runtime?.capabilities?.acpWorkerPool !== true) {
203
+ throw new Error(`Runtime "${runtime?.name || '<unknown>'}" does not support persistent ACP workers`);
204
+ }
205
+ const args = runtime.buildWorkerArgs(options);
206
+ if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) {
207
+ throw new Error(`Runtime "${runtime.name}" buildWorkerArgs() must return string[]`);
208
+ }
209
+ return args;
210
+ }
211
+
212
+ function encodePooledOutput(runtime, event) {
213
+ if (runtime?.capabilities?.acpWorkerPool !== true) {
214
+ throw new Error(`Runtime "${runtime?.name || '<unknown>'}" does not support pooled output`);
215
+ }
216
+ const encoded = runtime.encodePooledOutput(event);
217
+ if (typeof encoded !== 'string') {
218
+ throw new Error(`Runtime "${runtime.name}" encodePooledOutput() must return a string`);
219
+ }
220
+ return encoded;
221
+ }
222
+
223
+ module.exports = {
224
+ ADAPTER_API_VERSION,
225
+ RUNTIME_OPTIONS_FLAG,
226
+ RUNTIME_IMAGES_FILE_OPTION,
227
+ CORE_METHODS,
228
+ normalizeRuntimeAdapter,
229
+ sanitizeInvocationOptions,
230
+ resolveInvocationOptions,
231
+ encodeRuntimeOptions,
232
+ decodeRuntimeOptions,
233
+ buildSpawnFlags,
234
+ prepareWorkspace,
235
+ isSpawnStartupEvent,
236
+ shouldSuppressPostMutationError,
237
+ buildWorkerArgs,
238
+ encodePooledOutput,
239
+ };
@@ -32,11 +32,19 @@ const https = require('https');
32
32
  const os = require('os');
33
33
  const path = require('path');
34
34
  const { execSync } = require('child_process');
35
- const { FAILURE_CLASS, safeWrite, ts, resolveEngineCacheDir } = require('../shared');
35
+ const { ENGINE_DEFAULTS, FAILURE_CLASS, safeWrite, ts, resolveEngineCacheDir } = require('../shared');
36
36
 
37
37
  const ENGINE_DIR = __dirname.replace(/[\\/]runtimes$/, '');
38
38
  const _CACHE_DIR = resolveEngineCacheDir(ENGINE_DIR);
39
39
  const isWin = process.platform === 'win32';
40
+ const RUNTIME_NAME = 'copilot';
41
+ const SPAWN_STARTUP_TYPES = new Set([
42
+ 'session.mcp_server_status_changed',
43
+ 'session.mcp_servers_loaded',
44
+ 'session.skills_loaded',
45
+ 'session.tools_updated',
46
+ 'session.info',
47
+ ]);
40
48
 
41
49
  // ── Binary Resolution ───────────────────────────────────────────────────────
42
50
  //
@@ -529,7 +537,7 @@ function buildArgs(opts = {}) {
529
537
  }
530
538
 
531
539
  function buildSpawnFlags(opts = {}) {
532
- const flags = ['--runtime', 'copilot'];
540
+ const flags = ['--runtime', RUNTIME_NAME];
533
541
  if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
534
542
  if (opts.model) flags.push('--model', String(opts.model));
535
543
  if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
@@ -552,12 +560,84 @@ function buildSpawnFlags(opts = {}) {
552
560
  return flags;
553
561
  }
554
562
 
555
- // Stamped into every session.json this adapter writes so the pre-spawn resume
556
- // path can detect "session was produced by a different runtime" — Copilot
557
- // rejects Claude session IDs (and vice versa) with "No conversation found",
558
- // which would otherwise burn a retry slot before the post-failure cleanup at
559
- // engine.js:1195 fires. See W-mot9fwya000d09cb.
560
- const RUNTIME_NAME = 'copilot';
563
+ function resolveInvocationOptions({
564
+ options = {},
565
+ engineConfig = {},
566
+ failureClass = null,
567
+ } = {}) {
568
+ const resolved = { ...options };
569
+ if (resolved.stream == null) resolved.stream = engineConfig.copilotStreamMode;
570
+ if (resolved.disableBuiltinMcps == null) {
571
+ resolved.disableBuiltinMcps = engineConfig.copilotDisableBuiltinMcps
572
+ ?? ENGINE_DEFAULTS.copilotDisableBuiltinMcps;
573
+ }
574
+ if (resolved.reasoningSummaries == null) {
575
+ resolved.reasoningSummaries = engineConfig.copilotReasoningSummaries;
576
+ }
577
+ if (failureClass === FAILURE_CLASS.MODEL_UNAVAILABLE && engineConfig.copilotFallbackModel) {
578
+ resolved.model = engineConfig.copilotFallbackModel;
579
+ }
580
+ if (Array.isArray(resolved.disabledMcpServers)) {
581
+ resolved.disabledMcpServers = [...new Set(
582
+ resolved.disabledMcpServers.filter(Boolean).map(String)
583
+ )].sort();
584
+ }
585
+ return resolved;
586
+ }
587
+
588
+ function isSpawnStartupEvent(event) {
589
+ return !!event && SPAWN_STARTUP_TYPES.has(event.type);
590
+ }
591
+
592
+ function shouldSuppressPostMutationError({ mutationApplied, error } = {}) {
593
+ if (mutationApplied !== true || !error) return false;
594
+ return error.errorClass === 'unknown-model' || error.errorClass === 'model-unavailable';
595
+ }
596
+
597
+ function buildWorkerArgs(options = {}) {
598
+ const maxTurns = Number(options.maxTurns);
599
+ const args = [
600
+ '--acp',
601
+ '--allow-all',
602
+ '--max-autopilot-continues',
603
+ String(Number.isFinite(maxTurns) && maxTurns > 0 ? Math.floor(maxTurns) : 1),
604
+ ];
605
+ if (options.disableBuiltinMcps === true) args.push('--disable-builtin-mcps');
606
+ if (options.reasoningSummaries === true) args.push('--enable-reasoning-summaries');
607
+ for (const name of options.disabledMcpServers || []) {
608
+ if (name) args.push('--disable-mcp-server', String(name));
609
+ }
610
+ return args;
611
+ }
612
+
613
+ function encodePooledOutput(event = {}) {
614
+ if (event.kind === 'model') {
615
+ return JSON.stringify({
616
+ type: 'session.tools_updated',
617
+ data: { model: event.model || null },
618
+ }) + '\n';
619
+ }
620
+ if (event.kind === 'chunk') {
621
+ return JSON.stringify({
622
+ type: 'assistant.message_delta',
623
+ data: { deltaContent: event.text || '' },
624
+ }) + '\n';
625
+ }
626
+ if (event.kind === 'result') {
627
+ return JSON.stringify({
628
+ type: 'result',
629
+ sessionId: event.sessionId || null,
630
+ usage: {
631
+ totalApiDurationMs: event.durationMs,
632
+ stopReason: event.stopReason || null,
633
+ },
634
+ }) + '\n';
635
+ }
636
+ throw new Error(`Unsupported pooled output event kind: ${event.kind || '<missing>'}`);
637
+ }
638
+
639
+ // RUNTIME_NAME is stamped into every session.json so the pre-spawn resume path
640
+ // can reject session IDs produced by another adapter before invoking the CLI.
561
641
 
562
642
  function getResumeSessionId({ agentId, branchName, agentsDir, maxAgeMs = 2 * 60 * 60 * 1000, logger = console } = {}) {
563
643
  if (!agentId || agentId.startsWith('temp-') || !agentsDir) return null;
@@ -1370,7 +1450,8 @@ function getMcpConfigPaths({ homeDir = os.homedir(), project = null } = {}) {
1370
1450
  }
1371
1451
 
1372
1452
  module.exports = {
1373
- name: 'copilot',
1453
+ apiVersion: 1,
1454
+ name: RUNTIME_NAME,
1374
1455
  capabilities,
1375
1456
  resolveBinary,
1376
1457
  capsFile: CAPS_FILE,
@@ -1379,7 +1460,11 @@ module.exports = {
1379
1460
  // Use the same wrapper as Claude — spawn-agent.js is runtime-agnostic per P-9c4f2d6a
1380
1461
  spawnScript: path.join(ENGINE_DIR, 'spawn-agent.js'),
1381
1462
  installHint: INSTALL_HINT,
1463
+ workerErrorHint: 'copilot --acp failed -- ensure copilot CLI >=1.0.46 and copilot login is complete',
1464
+ resolveInvocationOptions,
1382
1465
  buildSpawnFlags,
1466
+ buildWorkerArgs,
1467
+ encodePooledOutput,
1383
1468
  buildArgs,
1384
1469
  buildPrompt,
1385
1470
  getUserAssetDirs,
@@ -1399,6 +1484,8 @@ module.exports = {
1399
1484
  parseStreamChunk,
1400
1485
  parseError,
1401
1486
  createStreamConsumer,
1487
+ isSpawnStartupEvent,
1488
+ shouldSuppressPostMutationError,
1402
1489
  permissionBypassFlags: PERMISSION_BYPASS_FLAGS,
1403
1490
  // Exposed for unit tests — engine code MUST go through resolveRuntime + the
1404
1491
  // adapter contract; never reach into these helpers directly.
@@ -1419,4 +1506,5 @@ module.exports = {
1419
1506
  _extractInvalidModelName,
1420
1507
  CAPS_SCHEMA_VERSION,
1421
1508
  KNOWN_EVENT_TYPES,
1509
+ SPAWN_STARTUP_TYPES,
1422
1510
  };
@@ -7,18 +7,16 @@
7
7
  * runtime name fails loudly with a clear list of registered options.
8
8
  *
9
9
  * Adding a new runtime:
10
- * 1. Implement the full adapter contract documented in claude.js.
11
- * 2. `registry.set('<name>', require('./<name>'));` below.
10
+ * 1. Implement API version 1 from contract.js.
11
+ * 2. `registerRuntime('<name>', require('./<name>'), { strict: true });`.
12
12
  * 3. Expose its capabilities via the `/api/runtimes` endpoint (free).
13
13
  *
14
14
  * Engine code MUST gate behavior on `runtime.capabilities.*` flags, not on
15
15
  * `runtime.name === 'claude'` comparisons. The whole point of this layer.
16
16
  */
17
17
 
18
+ const contract = require('./contract');
18
19
  const registry = new Map();
19
- registry.set('claude', require('./claude'));
20
- registry.set('codex', require('./codex'));
21
- registry.set('copilot', require('./copilot'));
22
20
 
23
21
  /**
24
22
  * Look up a runtime adapter by name. Throws when the name is unknown so
@@ -47,16 +45,46 @@ function listRuntimes() {
47
45
  * Register a runtime adapter. Exposed for tests and for downstream tooling
48
46
  * that wants to register a custom runtime without editing this file.
49
47
  */
50
- function registerRuntime(name, adapter) {
51
- if (!name || typeof name !== 'string') throw new Error('registerRuntime: name must be a non-empty string');
52
- if (!adapter || typeof adapter !== 'object') throw new Error('registerRuntime: adapter must be an object');
53
- registry.set(name, adapter);
48
+ function registerRuntime(name, adapter, options) {
49
+ const normalized = contract.normalizeRuntimeAdapter(name, adapter, options);
50
+ registry.set(name, normalized);
51
+ return normalized;
54
52
  }
55
53
 
54
+ function resolveRuntimeByCapability(capability) {
55
+ const matches = listRuntimes()
56
+ .map(name => registry.get(name))
57
+ .filter(adapter => adapter?.capabilities?.[capability] === true);
58
+ if (matches.length === 0) {
59
+ throw new Error(
60
+ `No registered runtime declares capability "${capability}"`
61
+ );
62
+ }
63
+ return matches[0];
64
+ }
65
+
66
+ registerRuntime('claude', require('./claude'), { strict: true });
67
+ registerRuntime('codex', require('./codex'), { strict: true });
68
+ registerRuntime('copilot', require('./copilot'), { strict: true });
69
+
56
70
  module.exports = {
71
+ ADAPTER_API_VERSION: contract.ADAPTER_API_VERSION,
72
+ RUNTIME_OPTIONS_FLAG: contract.RUNTIME_OPTIONS_FLAG,
73
+ RUNTIME_IMAGES_FILE_OPTION: contract.RUNTIME_IMAGES_FILE_OPTION,
57
74
  resolveRuntime,
75
+ resolveRuntimeByCapability,
58
76
  listRuntimes,
59
77
  registerRuntime,
78
+ sanitizeInvocationOptions: contract.sanitizeInvocationOptions,
79
+ resolveInvocationOptions: contract.resolveInvocationOptions,
80
+ decodeRuntimeOptions: contract.decodeRuntimeOptions,
81
+ buildSpawnFlags: contract.buildSpawnFlags,
82
+ prepareWorkspace: contract.prepareWorkspace,
83
+ isSpawnStartupEvent: contract.isSpawnStartupEvent,
84
+ shouldSuppressPostMutationError: contract.shouldSuppressPostMutationError,
85
+ buildWorkerArgs: contract.buildWorkerArgs,
86
+ encodePooledOutput: contract.encodePooledOutput,
87
+ validateRuntimeAdapter: contract.normalizeRuntimeAdapter,
60
88
  // Exposed for tests — engine code MUST go through resolveRuntime/listRuntimes
61
89
  _registry: registry,
62
90
  };