@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.
- package/bin/minions.js +1 -0
- package/dashboard.js +26 -12
- package/docs/completion-reports.md +20 -1
- package/docs/runtime-adapters.md +54 -22
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +14 -0
- package/engine/acp-transport.js +63 -35
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +11 -4
- package/engine/cc-worker-pool.js +4 -2
- package/engine/cli.js +9 -1
- package/engine/comment-format.js +51 -14
- package/engine/dispatch.js +7 -2
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/lifecycle.js +125 -44
- package/engine/llm.js +59 -83
- package/engine/memory-store.js +3 -1
- package/engine/playbook.js +4 -6
- package/engine/pooled-agent-process.js +28 -26
- package/engine/runtimes/claude.js +94 -25
- package/engine/runtimes/codex.js +1 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +78 -8
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +95 -21
- package/engine/spawn-agent.js +79 -113
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine.js +183 -109
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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',
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
};
|
package/engine/runtimes/codex.js
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const
|
|
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
|
-
|
|
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
|
};
|
package/engine/runtimes/index.js
CHANGED
|
@@ -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
|
|
11
|
-
* 2. `
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
};
|