@yemi33/minions 0.1.2381 → 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.
Files changed (54) hide show
  1. package/bin/minions.js +42 -2
  2. package/dashboard/js/refresh.js +8 -2
  3. package/dashboard/js/render-other.js +38 -0
  4. package/dashboard/js/render-work-items.js +61 -20
  5. package/dashboard/js/settings.js +7 -8
  6. package/dashboard/pages/engine.html +6 -0
  7. package/dashboard.js +110 -27
  8. package/docs/README.md +1 -0
  9. package/docs/claude-md-propagation.md +118 -0
  10. package/docs/completion-reports.md +20 -1
  11. package/docs/engine-restart.md +10 -0
  12. package/docs/runtime-adapters.md +54 -22
  13. package/docs/skills.md +2 -0
  14. package/docs/workspace-manifests.md +1 -1
  15. package/docs/worktree-lifecycle.md +23 -0
  16. package/engine/acp-transport.js +63 -35
  17. package/engine/ado-comment.js +3 -1
  18. package/engine/agent-worker-pool.js +11 -4
  19. package/engine/cc-worker-pool.js +4 -2
  20. package/engine/claude-md-context.js +195 -0
  21. package/engine/cli.js +9 -1
  22. package/engine/comment-format.js +51 -14
  23. package/engine/consolidation.js +47 -3
  24. package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
  25. package/engine/dispatch.js +80 -23
  26. package/engine/execution-model.js +68 -0
  27. package/engine/gh-comment.js +6 -3
  28. package/engine/github.js +6 -7
  29. package/engine/lifecycle.js +195 -56
  30. package/engine/llm.js +59 -83
  31. package/engine/memory-store.js +3 -1
  32. package/engine/pipeline.js +147 -20
  33. package/engine/playbook.js +39 -6
  34. package/engine/pooled-agent-process.js +28 -26
  35. package/engine/prd-store.js +14 -6
  36. package/engine/quarantine-refs.js +33 -0
  37. package/engine/queries.js +8 -6
  38. package/engine/restart-health.js +69 -4
  39. package/engine/runtimes/claude.js +100 -25
  40. package/engine/runtimes/codex.js +19 -0
  41. package/engine/runtimes/contract.js +239 -0
  42. package/engine/runtimes/copilot.js +85 -8
  43. package/engine/runtimes/index.js +37 -9
  44. package/engine/shared.js +157 -64
  45. package/engine/spawn-agent.js +79 -113
  46. package/engine/spawn-phase-watchdog.js +8 -37
  47. package/engine/supervisor.js +72 -16
  48. package/engine/timeout.js +87 -16
  49. package/engine/untrusted-fence.js +2 -1
  50. package/engine.js +434 -125
  51. package/package.json +1 -1
  52. package/playbooks/fix.md +20 -0
  53. package/playbooks/review.md +2 -0
  54. package/skills/check-self-authored-review-comment/SKILL.md +34 -0
@@ -32,6 +32,14 @@ function readEngineControl(minionsHome) {
32
32
  }
33
33
  }
34
34
 
35
+ function readSupervisorPid(minionsHome) {
36
+ try {
37
+ return normalizePid(fs.readFileSync(path.join(minionsHome, 'engine', 'supervisor.pid'), 'utf8').trim());
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
35
43
  function normalizePid(pid) {
36
44
  const n = Number(pid);
37
45
  return Number.isInteger(n) && n > 0 ? n : null;
@@ -122,7 +130,10 @@ async function checkRestartHealth(options = {}) {
122
130
  dashboardUrl,
123
131
  dashboardPid,
124
132
  dashboardPort = 7331,
133
+ expectedEnginePid,
134
+ supervisorPid,
125
135
  readControl = readEngineControl,
136
+ readSupervisorPid: readSupPid = readSupervisorPid,
126
137
  isProcessAlive: isAlive = isProcessAlive,
127
138
  isPortListening: portCheck = isPortListening,
128
139
  httpGetJson: getJson = httpGetJson,
@@ -130,8 +141,10 @@ async function checkRestartHealth(options = {}) {
130
141
 
131
142
  const control = readControl(minionsHome);
132
143
  const pid = normalizePid(control && control.pid);
144
+ const expectedEngine = normalizePid(expectedEnginePid);
133
145
  const engineAlive = pid ? isAlive(pid) : false;
134
- const engineOk = control && control.state === 'running' && engineAlive;
146
+ const engineOwned = expectedEngine == null || pid === expectedEngine;
147
+ const engineOk = control && control.state === 'running' && engineAlive && engineOwned;
135
148
 
136
149
  // Two strategies for the dashboard check:
137
150
  // 1) Process + port-listening (preferred from `minions restart` since
@@ -196,11 +209,54 @@ async function checkRestartHealth(options = {}) {
196
209
  dashboardSnapshot = { kind: 'http', url, ok: !!(dashboard && dashboard.ok), statusCode: dashboard && dashboard.statusCode, status: dashboardStatus };
197
210
  }
198
211
 
212
+ let supervisorOk = true;
213
+ let supervisorSnapshot = null;
214
+ if (supervisorPid != null) {
215
+ const expectedSupervisor = normalizePid(supervisorPid);
216
+ const recordedSupervisor = normalizePid(readSupPid(minionsHome));
217
+ const supervisorAlive = expectedSupervisor ? isAlive(expectedSupervisor) : false;
218
+ supervisorOk = !!(expectedSupervisor && supervisorAlive && recordedSupervisor === expectedSupervisor);
219
+ supervisorSnapshot = {
220
+ pid: expectedSupervisor,
221
+ alive: supervisorAlive,
222
+ recordedPid: recordedSupervisor,
223
+ owned: recordedSupervisor === expectedSupervisor,
224
+ };
225
+ }
226
+
227
+ let topologyOk = true;
228
+ let topologySnapshot = null;
229
+ if (engineOk && dashboardOk && supervisorOk && options.requireExactTopology) {
230
+ const listPids = options.listProcessPids;
231
+ const scripts = options.topologyScripts || {};
232
+ if (typeof listPids !== 'function') {
233
+ topologyOk = false;
234
+ topologySnapshot = { error: 'process enumerator unavailable' };
235
+ } else {
236
+ const expected = {
237
+ engine: expectedEngine,
238
+ dashboard: normalizePid(dashboardPid),
239
+ supervisor: normalizePid(supervisorPid),
240
+ };
241
+ const actual = {};
242
+ for (const kind of ['engine', 'dashboard', 'supervisor']) {
243
+ try {
244
+ actual[kind] = [...new Set((listPids(scripts[kind]) || []).map(normalizePid).filter(Boolean))];
245
+ } catch {
246
+ actual[kind] = [];
247
+ }
248
+ }
249
+ topologyOk = Object.keys(expected).every(kind =>
250
+ expected[kind] != null && actual[kind].length === 1 && actual[kind][0] === expected[kind]);
251
+ topologySnapshot = { expected, actual };
252
+ }
253
+ }
254
+
199
255
  const errors = [];
200
256
  if (!engineOk) {
201
257
  const state = control && control.state ? control.state : 'unknown';
202
258
  const pidLabel = pid || 'none';
203
- errors.push(`Engine is not healthy (state=${state}, pid=${pidLabel}, alive=${engineAlive ? 'yes' : 'no'})`);
259
+ errors.push(`Engine is not healthy (state=${state}, pid=${pidLabel}, alive=${engineAlive ? 'yes' : 'no'}, owned=${engineOwned ? 'yes' : 'no'})`);
204
260
  }
205
261
  if (!dashboardOk) {
206
262
  const where = dashboardKind === 'http'
@@ -208,11 +264,19 @@ async function checkRestartHealth(options = {}) {
208
264
  : `dashboard pid=${normalizePid(dashboardPid) || 'none'} port=${dashboardPort}`;
209
265
  errors.push(`Dashboard failed health check at ${where} (${dashboardDetail})`);
210
266
  }
267
+ if (!supervisorOk) {
268
+ errors.push(`Supervisor is not healthy (pid=${supervisorSnapshot.pid || 'none'}, alive=${supervisorSnapshot.alive ? 'yes' : 'no'}, recordedPid=${supervisorSnapshot.recordedPid || 'none'}, owned=${supervisorSnapshot.owned ? 'yes' : 'no'})`);
269
+ }
270
+ if (!topologyOk) {
271
+ errors.push(`Minions process topology is not exclusive (${JSON.stringify(topologySnapshot)})`);
272
+ }
211
273
 
212
274
  return {
213
- ok: engineOk && dashboardOk,
214
- engine: { state: control && control.state, pid, alive: engineAlive },
275
+ ok: engineOk && dashboardOk && supervisorOk && topologyOk,
276
+ engine: { state: control && control.state, pid, alive: engineAlive, owned: engineOwned },
215
277
  dashboard: dashboardSnapshot,
278
+ supervisor: supervisorSnapshot,
279
+ topology: topologySnapshot,
216
280
  errors,
217
281
  };
218
282
  }
@@ -310,6 +374,7 @@ module.exports = {
310
374
  isProcessAlive,
311
375
  isPortListening,
312
376
  readEngineControl,
377
+ readSupervisorPid,
313
378
  normalizePid,
314
379
  },
315
380
  };
@@ -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
@@ -838,6 +827,12 @@ const capabilities = {
838
827
  budgetCap: true,
839
828
  // Supports `--bare` (suppress CLAUDE.md auto-discovery)
840
829
  bareMode: true,
830
+ // The Claude Code CLI reads repo-authored CLAUDE.md files itself at startup
831
+ // (unless `--bare`). TRUE here tells the engine's CLAUDE.md-propagation layer
832
+ // (engine/claude-md-context.js, injected in engine/playbook.js) to SKIP this
833
+ // runtime — re-injecting CLAUDE.md content into the prompt would double it.
834
+ // Copilot/Codex set this false because they have no native CLAUDE.md auto-load.
835
+ claudeMdNativeDiscovery: true,
841
836
  // Supports `--fallback-model <id>`
842
837
  fallbackModel: true,
843
838
  // Engine controls session persistence (writes session.json on completion)
@@ -928,6 +923,81 @@ function getMcpConfigPaths({ homeDir = os.homedir(), project = null } = {}) {
928
923
  return paths;
929
924
  }
930
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
+
931
1001
  // Heuristic: does `model` look like a Claude model identifier? Powers the
932
1002
  // preflight "stale model after CLI switch" warning in cli.js. Returning false
933
1003
  // means "this looks wrong for Claude" — gpt-5.4 / o3-* / codex etc. Keep this
@@ -942,7 +1012,8 @@ function modelLooksFamiliar(model) {
942
1012
  }
943
1013
 
944
1014
  module.exports = {
945
- name: 'claude',
1015
+ apiVersion: 1,
1016
+ name: RUNTIME_NAME,
946
1017
  capabilities,
947
1018
  resolveBinary,
948
1019
  capsFile: CAPS_FILE,
@@ -950,6 +1021,7 @@ module.exports = {
950
1021
  modelsCache: MODELS_CACHE,
951
1022
  spawnScript: path.join(ENGINE_DIR, 'spawn-agent.js'),
952
1023
  installHint: INSTALL_HINT,
1024
+ resolveInvocationOptions,
953
1025
  buildSpawnFlags,
954
1026
  buildArgs,
955
1027
  buildPrompt,
@@ -958,6 +1030,8 @@ module.exports = {
958
1030
  getSkillWriteTargets,
959
1031
  getCommandRoots,
960
1032
  getMcpConfigPaths,
1033
+ prepareWorkspace,
1034
+ isSpawnStartupEvent,
961
1035
  getResumeSessionId,
962
1036
  saveSession,
963
1037
  detectPermissionGate,
@@ -975,4 +1049,5 @@ module.exports = {
975
1049
  _CLAUDE_SHORTHANDS,
976
1050
  _extractInvalidModelName,
977
1051
  THINKING_BLOCK_TYPES,
1052
+ SPAWN_STARTUP_SUBTYPES,
978
1053
  };
@@ -840,6 +840,20 @@ function getModelCacheKey({ env = process.env, timeoutMs = 10000 } = {}) {
840
840
  }
841
841
  }
842
842
 
843
+ function getModelCacheKey({ env = process.env, timeoutMs = 10000 } = {}) {
844
+ const resolved = resolveBinary({ env });
845
+ if (!resolved) return null;
846
+ try {
847
+ return _execFileCapture(resolved.bin, [...(resolved.leadingArgs || []), '--version'], {
848
+ env,
849
+ timeoutMs,
850
+ native: resolved.native,
851
+ }).trim() || null;
852
+ } catch {
853
+ return null;
854
+ }
855
+ }
856
+
843
857
  function createStreamConsumer(ctx) {
844
858
  let deltaText = '';
845
859
  let terminalText = '';
@@ -901,6 +915,10 @@ const capabilities = {
901
915
  budgetCap: false,
902
916
  bareMode: false,
903
917
  fallbackModel: false,
918
+ // Codex has no native CLAUDE.md auto-load — FALSE opts it into the engine's
919
+ // CLAUDE.md-propagation layer (engine/claude-md-context.js) so repo-authored
920
+ // CLAUDE.md guidance reaches the agent prompt.
921
+ claudeMdNativeDiscovery: false,
904
922
  sessionPersistenceControl: false,
905
923
  resumePromptCarryover: true,
906
924
  streamConsumer: true,
@@ -911,6 +929,7 @@ const INSTALL_HINT = 'install Codex CLI with `npm install -g @openai/codex` or `
911
929
  const PERMISSION_BYPASS_FLAGS = ['--sandbox'];
912
930
 
913
931
  module.exports = {
932
+ apiVersion: 1,
914
933
  name: RUNTIME_NAME,
915
934
  capabilities,
916
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
+ };