@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.
- package/bin/minions.js +42 -2
- package/dashboard/js/refresh.js +8 -2
- package/dashboard/js/render-other.js +38 -0
- package/dashboard/js/render-work-items.js +61 -20
- package/dashboard/js/settings.js +7 -8
- package/dashboard/pages/engine.html +6 -0
- package/dashboard.js +110 -27
- package/docs/README.md +1 -0
- package/docs/claude-md-propagation.md +118 -0
- package/docs/completion-reports.md +20 -1
- package/docs/engine-restart.md +10 -0
- 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 +23 -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/claude-md-context.js +195 -0
- package/engine/cli.js +9 -1
- package/engine/comment-format.js +51 -14
- package/engine/consolidation.js +47 -3
- package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
- package/engine/dispatch.js +80 -23
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/lifecycle.js +195 -56
- package/engine/llm.js +59 -83
- package/engine/memory-store.js +3 -1
- package/engine/pipeline.js +147 -20
- package/engine/playbook.js +39 -6
- package/engine/pooled-agent-process.js +28 -26
- package/engine/prd-store.js +14 -6
- package/engine/quarantine-refs.js +33 -0
- package/engine/queries.js +8 -6
- package/engine/restart-health.js +69 -4
- package/engine/runtimes/claude.js +100 -25
- package/engine/runtimes/codex.js +19 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +85 -8
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +157 -64
- package/engine/spawn-agent.js +79 -113
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine/supervisor.js +72 -16
- package/engine/timeout.js +87 -16
- package/engine/untrusted-fence.js +2 -1
- package/engine.js +434 -125
- 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
package/engine/restart-health.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
@@ -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
|
-
|
|
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
|
};
|
package/engine/runtimes/codex.js
CHANGED
|
@@ -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
|
+
};
|