@yemi33/minions 0.1.2382 → 0.1.2383

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.
@@ -6,23 +6,9 @@
6
6
  * everything Claude-CLI-specific: binary resolution, arg construction, prompt
7
7
  * preparation, output parsing, and error normalization.
8
8
  *
9
- * Adapter contract (all runtimes must implement):
10
- * - name: string
11
- * - capabilities: { ... } feature flags consumed by engine code
12
- * - resolveBinary() → { bin, native, leadingArgs }
13
- * - capsFile: absolute path of the binary-resolution cache for this runtime
14
- * - listModels() → Promise<{id,name,provider}[] | null>
15
- * - modelsCache: absolute path of the model-list cache for this runtime
16
- * - spawnScript: absolute path of the spawn wrapper (or null if direct-only)
17
- * - buildArgs(opts) → string[] — CLI args excluding the binary
18
- * - buildPrompt(promptText, sysPromptText) → string — final prompt delivered
19
- * - getUserAssetDirs(opts) → string[] — runtime-native global asset roots
20
- * - getSkillRoots(opts) → {scope,dir,projectName?}[] — skill discovery roots
21
- * - getSkillWriteTargets(opts) → {personal,project} — extraction targets
22
- * - resolveModel(input) → string|undefined — shorthand expansion / passthrough
23
- * - parseOutput(raw) → { text, usage, sessionId, model }
24
- * - parseStreamChunk(line) → parsed event object or null
25
- * - parseError(rawOutput) → { message, code, retriable }
9
+ * The versioned adapter contract and compatibility defaults live in
10
+ * engine/runtimes/contract.js. Keep harness-specific argv, prompt, parser,
11
+ * workspace, and persistent-worker behavior in this module.
26
12
  */
27
13
 
28
14
  const fs = require('fs');
@@ -36,6 +22,8 @@ const MINIONS_DIR = path.resolve(ENGINE_DIR, '..');
36
22
  const _CACHE_DIR = resolveEngineCacheDir(ENGINE_DIR);
37
23
 
38
24
  const isWin = process.platform === 'win32';
25
+ const RUNTIME_NAME = 'claude';
26
+ const SPAWN_STARTUP_SUBTYPES = new Set(['init', 'hook_started', 'hook_response']);
39
27
 
40
28
  // ── Binary Resolution ────────────────────────────────────────────────────────
41
29
  // Mirrors engine/spawn-agent.js:26-91. Cached at engine/claude-caps.json so the
@@ -253,7 +241,7 @@ function buildArgs(opts = {}) {
253
241
  }
254
242
 
255
243
  function buildSpawnFlags(opts = {}) {
256
- const flags = ['--runtime', 'claude'];
244
+ const flags = ['--runtime', RUNTIME_NAME];
257
245
  if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
258
246
  if (opts.model) flags.push('--model', String(opts.model));
259
247
  if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
@@ -268,12 +256,13 @@ function buildSpawnFlags(opts = {}) {
268
256
  return flags;
269
257
  }
270
258
 
271
- // Stamped into every session.json this adapter writes so the pre-spawn resume
272
- // path can detect "session was produced by a different runtime" — Claude
273
- // rejects Copilot session IDs (and vice versa) with "No conversation found",
274
- // which would otherwise burn a retry slot before the post-failure cleanup at
275
- // engine.js:1195 fires. See W-mot9fwya000d09cb.
276
- const RUNTIME_NAME = 'claude';
259
+ function resolveInvocationOptions({ options = {}, engineConfig = {} } = {}) {
260
+ const resolved = { ...options };
261
+ if (resolved.fallbackModel == null && engineConfig.claudeFallbackModel) {
262
+ resolved.fallbackModel = engineConfig.claudeFallbackModel;
263
+ }
264
+ return resolved;
265
+ }
277
266
 
278
267
  /**
279
268
  * Compute the directory Claude CLI uses to persist a project's conversation
@@ -934,6 +923,81 @@ function getMcpConfigPaths({ homeDir = os.homedir(), project = null } = {}) {
934
923
  return paths;
935
924
  }
936
925
 
926
+ function prepareWorkspace({
927
+ worktreePath,
928
+ homeDir,
929
+ engineConfig,
930
+ mutateJsonFileLocked,
931
+ existsSync = fs.existsSync,
932
+ readFileSync = fs.readFileSync,
933
+ } = {}) {
934
+ if (engineConfig && engineConfig.claudePreApproveWorkspaceMcps === false) {
935
+ return { wrote: false, reason: 'flag-off', servers: [] };
936
+ }
937
+ if (!worktreePath || typeof worktreePath !== 'string') {
938
+ return { wrote: false, reason: 'no-worktree', servers: [] };
939
+ }
940
+ const workspaceMcpPath = path.join(worktreePath, '.mcp.json');
941
+ if (!existsSync(workspaceMcpPath)) {
942
+ return { wrote: false, reason: 'no-workspace-mcp', servers: [] };
943
+ }
944
+ let workspaceMcp;
945
+ try {
946
+ workspaceMcp = JSON.parse(readFileSync(workspaceMcpPath, 'utf8'));
947
+ } catch {
948
+ return { wrote: false, reason: 'invalid-workspace-mcp', servers: [] };
949
+ }
950
+ const serversObj = workspaceMcp && typeof workspaceMcp === 'object' ? workspaceMcp.mcpServers : null;
951
+ const servers = serversObj && typeof serversObj === 'object' && !Array.isArray(serversObj)
952
+ ? Object.keys(serversObj).filter(name => typeof name === 'string' && name.length > 0)
953
+ : [];
954
+ if (servers.length === 0) {
955
+ return { wrote: false, reason: 'no-servers', servers: [] };
956
+ }
957
+ if (!homeDir || typeof mutateJsonFileLocked !== 'function') {
958
+ return { wrote: false, reason: 'no-home-or-mutator', servers };
959
+ }
960
+
961
+ const claudeJsonPath = path.join(homeDir, '.claude.json');
962
+ let didWrite = false;
963
+ mutateJsonFileLocked(claudeJsonPath, (data) => {
964
+ const root = data && typeof data === 'object' ? data : {};
965
+ if (!root.projects || typeof root.projects !== 'object') root.projects = {};
966
+ const projectState = root.projects[worktreePath] && typeof root.projects[worktreePath] === 'object'
967
+ ? root.projects[worktreePath]
968
+ : {};
969
+ const enabled = Array.isArray(projectState.enabledMcpjsonServers)
970
+ ? projectState.enabledMcpjsonServers.slice()
971
+ : [];
972
+ const disabled = Array.isArray(projectState.disabledMcpjsonServers)
973
+ ? projectState.disabledMcpjsonServers.slice()
974
+ : [];
975
+ const enabledSet = new Set(enabled.filter(name => typeof name === 'string'));
976
+ let mutated = false;
977
+ for (const name of servers) {
978
+ if (!enabledSet.has(name)) {
979
+ enabledSet.add(name);
980
+ mutated = true;
981
+ }
982
+ }
983
+ const nextDisabled = disabled.filter(name => typeof name === 'string' && !servers.includes(name));
984
+ if (nextDisabled.length !== disabled.length) mutated = true;
985
+ if (!mutated) return data;
986
+ projectState.enabledMcpjsonServers = [...enabledSet];
987
+ projectState.disabledMcpjsonServers = nextDisabled;
988
+ root.projects[worktreePath] = projectState;
989
+ didWrite = true;
990
+ return root;
991
+ }, { defaultValue: {}, skipWriteIfUnchanged: true });
992
+
993
+ return { wrote: didWrite, reason: 'ok', servers, target: claudeJsonPath };
994
+ }
995
+
996
+ function isSpawnStartupEvent(event) {
997
+ if (!event || event.type !== 'system') return false;
998
+ return SPAWN_STARTUP_SUBTYPES.has(typeof event.subtype === 'string' ? event.subtype : '');
999
+ }
1000
+
937
1001
  // Heuristic: does `model` look like a Claude model identifier? Powers the
938
1002
  // preflight "stale model after CLI switch" warning in cli.js. Returning false
939
1003
  // means "this looks wrong for Claude" — gpt-5.4 / o3-* / codex etc. Keep this
@@ -948,7 +1012,8 @@ function modelLooksFamiliar(model) {
948
1012
  }
949
1013
 
950
1014
  module.exports = {
951
- name: 'claude',
1015
+ apiVersion: 1,
1016
+ name: RUNTIME_NAME,
952
1017
  capabilities,
953
1018
  resolveBinary,
954
1019
  capsFile: CAPS_FILE,
@@ -956,6 +1021,7 @@ module.exports = {
956
1021
  modelsCache: MODELS_CACHE,
957
1022
  spawnScript: path.join(ENGINE_DIR, 'spawn-agent.js'),
958
1023
  installHint: INSTALL_HINT,
1024
+ resolveInvocationOptions,
959
1025
  buildSpawnFlags,
960
1026
  buildArgs,
961
1027
  buildPrompt,
@@ -964,6 +1030,8 @@ module.exports = {
964
1030
  getSkillWriteTargets,
965
1031
  getCommandRoots,
966
1032
  getMcpConfigPaths,
1033
+ prepareWorkspace,
1034
+ isSpawnStartupEvent,
967
1035
  getResumeSessionId,
968
1036
  saveSession,
969
1037
  detectPermissionGate,
@@ -981,4 +1049,5 @@ module.exports = {
981
1049
  _CLAUDE_SHORTHANDS,
982
1050
  _extractInvalidModelName,
983
1051
  THINKING_BLOCK_TYPES,
1052
+ SPAWN_STARTUP_SUBTYPES,
984
1053
  };
@@ -929,6 +929,7 @@ const INSTALL_HINT = 'install Codex CLI with `npm install -g @openai/codex` or `
929
929
  const PERMISSION_BYPASS_FLAGS = ['--sandbox'];
930
930
 
931
931
  module.exports = {
932
+ apiVersion: 1,
932
933
  name: RUNTIME_NAME,
933
934
  capabilities,
934
935
  resolveBinary,
@@ -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
+ };
@@ -37,6 +37,14 @@ const { FAILURE_CLASS, safeWrite, ts, resolveEngineCacheDir } = require('../shar
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,66 @@ 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
+ }
573
+ if (resolved.reasoningSummaries == null) {
574
+ resolved.reasoningSummaries = engineConfig.copilotReasoningSummaries;
575
+ }
576
+ if (failureClass === FAILURE_CLASS.MODEL_UNAVAILABLE && engineConfig.copilotFallbackModel) {
577
+ resolved.model = engineConfig.copilotFallbackModel;
578
+ }
579
+ return resolved;
580
+ }
581
+
582
+ function isSpawnStartupEvent(event) {
583
+ return !!event && SPAWN_STARTUP_TYPES.has(event.type);
584
+ }
585
+
586
+ function shouldSuppressPostMutationError({ mutationApplied, error } = {}) {
587
+ if (mutationApplied !== true || !error) return false;
588
+ return error.errorClass === 'unknown-model' || error.errorClass === 'model-unavailable';
589
+ }
590
+
591
+ function buildWorkerArgs() {
592
+ return ['--acp', '--allow-all', '--max-autopilot-continues', '1'];
593
+ }
594
+
595
+ function encodePooledOutput(event = {}) {
596
+ if (event.kind === 'model') {
597
+ return JSON.stringify({
598
+ type: 'session.tools_updated',
599
+ data: { model: event.model || null },
600
+ }) + '\n';
601
+ }
602
+ if (event.kind === 'chunk') {
603
+ return JSON.stringify({
604
+ type: 'assistant.message_delta',
605
+ data: { deltaContent: event.text || '' },
606
+ }) + '\n';
607
+ }
608
+ if (event.kind === 'result') {
609
+ return JSON.stringify({
610
+ type: 'result',
611
+ sessionId: event.sessionId || null,
612
+ usage: {
613
+ totalApiDurationMs: event.durationMs,
614
+ stopReason: event.stopReason || null,
615
+ },
616
+ }) + '\n';
617
+ }
618
+ throw new Error(`Unsupported pooled output event kind: ${event.kind || '<missing>'}`);
619
+ }
620
+
621
+ // RUNTIME_NAME is stamped into every session.json so the pre-spawn resume path
622
+ // can reject session IDs produced by another adapter before invoking the CLI.
561
623
 
562
624
  function getResumeSessionId({ agentId, branchName, agentsDir, maxAgeMs = 2 * 60 * 60 * 1000, logger = console } = {}) {
563
625
  if (!agentId || agentId.startsWith('temp-') || !agentsDir) return null;
@@ -1370,7 +1432,8 @@ function getMcpConfigPaths({ homeDir = os.homedir(), project = null } = {}) {
1370
1432
  }
1371
1433
 
1372
1434
  module.exports = {
1373
- name: 'copilot',
1435
+ apiVersion: 1,
1436
+ name: RUNTIME_NAME,
1374
1437
  capabilities,
1375
1438
  resolveBinary,
1376
1439
  capsFile: CAPS_FILE,
@@ -1379,7 +1442,11 @@ module.exports = {
1379
1442
  // Use the same wrapper as Claude — spawn-agent.js is runtime-agnostic per P-9c4f2d6a
1380
1443
  spawnScript: path.join(ENGINE_DIR, 'spawn-agent.js'),
1381
1444
  installHint: INSTALL_HINT,
1445
+ workerErrorHint: 'copilot --acp failed -- ensure copilot CLI >=1.0.46 and copilot login is complete',
1446
+ resolveInvocationOptions,
1382
1447
  buildSpawnFlags,
1448
+ buildWorkerArgs,
1449
+ encodePooledOutput,
1383
1450
  buildArgs,
1384
1451
  buildPrompt,
1385
1452
  getUserAssetDirs,
@@ -1399,6 +1466,8 @@ module.exports = {
1399
1466
  parseStreamChunk,
1400
1467
  parseError,
1401
1468
  createStreamConsumer,
1469
+ isSpawnStartupEvent,
1470
+ shouldSuppressPostMutationError,
1402
1471
  permissionBypassFlags: PERMISSION_BYPASS_FLAGS,
1403
1472
  // Exposed for unit tests — engine code MUST go through resolveRuntime + the
1404
1473
  // adapter contract; never reach into these helpers directly.
@@ -1419,4 +1488,5 @@ module.exports = {
1419
1488
  _extractInvalidModelName,
1420
1489
  CAPS_SCHEMA_VERSION,
1421
1490
  KNOWN_EVENT_TYPES,
1491
+ SPAWN_STARTUP_TYPES,
1422
1492
  };
@@ -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
  };