@yemi33/minions 0.1.1587 → 0.1.1589
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/CHANGELOG.md +10 -0
- package/bin/minions.js +5 -3
- package/dashboard/js/settings.js +216 -22
- package/dashboard.js +136 -8
- package/docs/copilot-cli-schema.md +637 -0
- package/docs/copilot-output-sample-claude.jsonl +72 -0
- package/docs/copilot-output-sample-default.jsonl +26 -0
- package/docs/copilot-output-sample-gpt4o.jsonl +23 -0
- package/engine/cli.js +250 -18
- package/engine/lifecycle.js +14 -9
- package/engine/llm.js +346 -94
- package/engine/model-discovery.js +167 -0
- package/engine/preflight.js +247 -19
- package/engine/runtimes/claude.js +413 -0
- package/engine/runtimes/copilot.js +566 -0
- package/engine/runtimes/index.js +61 -0
- package/engine/shared.js +299 -63
- package/engine/spawn-agent.js +265 -181
- package/engine.js +118 -31
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/model-discovery.js — Per-runtime model catalog cache + REST helpers.
|
|
3
|
+
*
|
|
4
|
+
* Backs the dashboard endpoints:
|
|
5
|
+
* GET /api/runtimes → listAllRuntimes()
|
|
6
|
+
* GET /api/runtimes/:name/models → getRuntimeModels(name)
|
|
7
|
+
* POST /api/runtimes/:name/models/refresh → invalidateRuntimeModelsCache(name)
|
|
8
|
+
* → getRuntimeModels(name, { force: true })
|
|
9
|
+
*
|
|
10
|
+
* Cache shape (per-runtime file at `adapter.modelsCache`):
|
|
11
|
+
* { runtime: 'copilot', models: [{ id, name, provider }, ...] | null, cachedAt: ISO }
|
|
12
|
+
*
|
|
13
|
+
* Returning `{ models: null }` is the universal "free-text fallback" signal —
|
|
14
|
+
* the settings UI renders a free-text input instead of a dropdown. It happens in
|
|
15
|
+
* four cases:
|
|
16
|
+
* 1. `config.engine.disableModelDiscovery === true` (fleet-wide opt-out)
|
|
17
|
+
* 2. `adapter.capabilities.modelDiscovery !== true` (e.g. Claude — no API)
|
|
18
|
+
* 3. `adapter.listModels()` resolves to null/empty (no token, network error)
|
|
19
|
+
* 4. `adapter.listModels()` throws (transient API error — caller treats as null)
|
|
20
|
+
*
|
|
21
|
+
* Cache is persisted only when discovery actually ran (cases 3+4 still write a
|
|
22
|
+
* `{ models: null }` cache so we don't hammer the API on every refresh — TTL
|
|
23
|
+
* eventually re-tries). Cases 1+2 skip the cache write entirely.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const { resolveRuntime, listRuntimes } = require('./runtimes');
|
|
28
|
+
|
|
29
|
+
// 1 hour — matches the spec ("< 1hr TTL"). Keep separate from ENGINE_DEFAULTS
|
|
30
|
+
// so the test suite can override per-call without polluting the global config.
|
|
31
|
+
const MODEL_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
function _readCacheFile(p) {
|
|
34
|
+
try {
|
|
35
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
36
|
+
const obj = JSON.parse(text);
|
|
37
|
+
if (obj && typeof obj === 'object') return obj;
|
|
38
|
+
} catch { /* missing / malformed → cache miss */ }
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _writeCacheFile(p, obj) {
|
|
43
|
+
try { fs.writeFileSync(p, JSON.stringify(obj, null, 2)); } catch { /* best effort */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Return the registered runtime catalog for `GET /api/runtimes`.
|
|
48
|
+
*
|
|
49
|
+
* Shape: `[{ name, capabilities }, ...]`. Capabilities are spread into a fresh
|
|
50
|
+
* object so callers can't mutate the adapter's authoritative capability block.
|
|
51
|
+
*/
|
|
52
|
+
function listAllRuntimes() {
|
|
53
|
+
const names = listRuntimes();
|
|
54
|
+
const runtimes = [];
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
let adapter;
|
|
57
|
+
try { adapter = resolveRuntime(name); } catch { continue; }
|
|
58
|
+
runtimes.push({
|
|
59
|
+
name,
|
|
60
|
+
capabilities: { ...(adapter && adapter.capabilities ? adapter.capabilities : {}) },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return runtimes;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read or refresh the cached model list for a runtime.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} runtimeName Must be a registered runtime — caller maps the
|
|
70
|
+
* `Unknown runtime` throw onto an HTTP 404.
|
|
71
|
+
* @param {object} opts
|
|
72
|
+
* @param {boolean} opts.force Skip the cache and call `listModels()` directly.
|
|
73
|
+
* @param {number} opts.ttlMs Cache freshness window (default 1h). Tests use a
|
|
74
|
+
* tiny window to exercise the miss path.
|
|
75
|
+
* @param {object} opts.config Pass `getConfig()` result so the helper can
|
|
76
|
+
* honour `config.engine.disableModelDiscovery`
|
|
77
|
+
* without re-reading the file on every request.
|
|
78
|
+
* @returns {Promise<{runtime: string, models: object[]|null, cachedAt: string|null}>}
|
|
79
|
+
*/
|
|
80
|
+
async function getRuntimeModels(runtimeName, { force = false, ttlMs = MODEL_CACHE_TTL_MS, config = null } = {}) {
|
|
81
|
+
// Throws "Unknown runtime ..." for unregistered names. Caller turns this into
|
|
82
|
+
// a 404; bubbling the throw is intentional so misconfigurations surface loudly
|
|
83
|
+
// (mirrors the registry contract documented in engine/runtimes/index.js).
|
|
84
|
+
const adapter = resolveRuntime(runtimeName);
|
|
85
|
+
|
|
86
|
+
// Case 1: fleet-wide opt-out. Skip the adapter entirely — never call its
|
|
87
|
+
// listModels() (which can hit the network) when the user explicitly disabled
|
|
88
|
+
// discovery. No cache write either; flipping the flag back on must produce a
|
|
89
|
+
// fresh fetch, not a stale `null` cache hit.
|
|
90
|
+
if (config && config.engine && config.engine.disableModelDiscovery === true) {
|
|
91
|
+
return { runtime: runtimeName, models: null, cachedAt: null };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Case 2: adapter doesn't support discovery (Claude). Same short-circuit —
|
|
95
|
+
// don't write a cache file (`engine/claude-models.json` stays absent / empty
|
|
96
|
+
// per the spec: "engine/claude-models.json (always null)").
|
|
97
|
+
if (!adapter.capabilities || adapter.capabilities.modelDiscovery !== true) {
|
|
98
|
+
return { runtime: runtimeName, models: null, cachedAt: null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cachePath = adapter.modelsCache || null;
|
|
102
|
+
|
|
103
|
+
// Cache hit path — only on non-forced reads. We accept any cached payload
|
|
104
|
+
// whose `cachedAt` parses to a valid timestamp within the TTL window;
|
|
105
|
+
// `models: null` cached entries also count as fresh so we don't re-hit the
|
|
106
|
+
// API every page load when the token is missing.
|
|
107
|
+
if (!force && cachePath) {
|
|
108
|
+
const cached = _readCacheFile(cachePath);
|
|
109
|
+
if (cached && typeof cached.cachedAt === 'string') {
|
|
110
|
+
const ts = Date.parse(cached.cachedAt);
|
|
111
|
+
if (Number.isFinite(ts)) {
|
|
112
|
+
const age = Date.now() - ts;
|
|
113
|
+
if (age >= 0 && age < ttlMs) {
|
|
114
|
+
const models = Array.isArray(cached.models) ? cached.models : null;
|
|
115
|
+
return { runtime: runtimeName, models, cachedAt: cached.cachedAt };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Cache miss / forced refresh — call the adapter. Any failure (null return,
|
|
122
|
+
// empty array, throw) collapses to `models: null` so the dashboard falls back
|
|
123
|
+
// to free-text input. We do NOT distinguish "API unreachable" from "API
|
|
124
|
+
// returned an empty list" — both are unactionable from a UI standpoint.
|
|
125
|
+
let models = null;
|
|
126
|
+
try {
|
|
127
|
+
const result = await adapter.listModels();
|
|
128
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
129
|
+
// Defensive copy — adapter caches its own result internally and we don't
|
|
130
|
+
// want that to be mutable through the response we return.
|
|
131
|
+
models = result.map(m => ({ ...m }));
|
|
132
|
+
}
|
|
133
|
+
} catch { /* swallow — network/transient errors map to null */ }
|
|
134
|
+
|
|
135
|
+
const cachedAt = new Date().toISOString();
|
|
136
|
+
if (cachePath) {
|
|
137
|
+
_writeCacheFile(cachePath, { runtime: runtimeName, models, cachedAt });
|
|
138
|
+
}
|
|
139
|
+
return { runtime: runtimeName, models, cachedAt };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Delete the cached models file for a runtime. Returns `true` if a file was
|
|
144
|
+
* removed, `false` if there was nothing to remove. Throws on unknown runtime
|
|
145
|
+
* (caller maps to 404). Filesystem errors other than ENOENT propagate so the
|
|
146
|
+
* route handler can surface a real I/O failure to the operator instead of
|
|
147
|
+
* silently no-oping.
|
|
148
|
+
*/
|
|
149
|
+
function invalidateRuntimeModelsCache(runtimeName) {
|
|
150
|
+
const adapter = resolveRuntime(runtimeName);
|
|
151
|
+
const cachePath = adapter && adapter.modelsCache;
|
|
152
|
+
if (!cachePath) return false;
|
|
153
|
+
try {
|
|
154
|
+
fs.unlinkSync(cachePath);
|
|
155
|
+
return true;
|
|
156
|
+
} catch (e) {
|
|
157
|
+
if (e && e.code === 'ENOENT') return false;
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
listAllRuntimes,
|
|
164
|
+
getRuntimeModels,
|
|
165
|
+
invalidateRuntimeModelsCache,
|
|
166
|
+
MODEL_CACHE_TTL_MS,
|
|
167
|
+
};
|
package/engine/preflight.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* engine/preflight.js — Prerequisite and health checks for Minions.
|
|
3
3
|
* Used by `minions init`, `minions start`, and `minions doctor`.
|
|
4
|
+
*
|
|
5
|
+
* Per-runtime binary + model-discovery checks (P-9e8a3f1d) — for every distinct
|
|
6
|
+
* CLI in use across `engine.defaultCli`, `engine.ccCli`, and `agents.<id>.cli`,
|
|
7
|
+
* we resolve the adapter from the registry and run its `resolveBinary()`. The
|
|
8
|
+
* cache is warmed via `listModels()` when the runtime supports discovery.
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
const fs = require('fs');
|
|
@@ -8,9 +13,12 @@ const path = require('path');
|
|
|
8
13
|
const { execSync } = require('child_process');
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
|
-
* Resolve the Claude Code CLI binary path.
|
|
16
|
+
* Resolve the Claude Code CLI binary path. Legacy helper preserved for back-
|
|
17
|
+
* compat — the runtime registry's `resolveRuntime('claude').resolveBinary()`
|
|
18
|
+
* is now the canonical resolver. This wrapper only exists so external tooling
|
|
19
|
+
* that still calls `findClaudeBinary()` keeps working until cleanup ships.
|
|
20
|
+
*
|
|
12
21
|
* Returns the path if found, null otherwise.
|
|
13
|
-
* Reuses the same search logic as spawn-agent.js.
|
|
14
22
|
*/
|
|
15
23
|
function findClaudeBinary() {
|
|
16
24
|
const searchPaths = [
|
|
@@ -71,6 +79,91 @@ function findClaudeBinary() {
|
|
|
71
79
|
return null;
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
// ─── Runtime fleet enumeration (P-9e8a3f1d) ─────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Collect the unique set of CLI runtimes that any part of the fleet would
|
|
86
|
+
* spawn given a config. Mirrors the union scanned by
|
|
87
|
+
* `shared.runtimeConfigWarnings` so unknown-CLI warnings and binary checks
|
|
88
|
+
* always cover the same surface.
|
|
89
|
+
*
|
|
90
|
+
* Without a config (legacy callers), returns just `['claude']` — the
|
|
91
|
+
* historical default.
|
|
92
|
+
*/
|
|
93
|
+
function _distinctRuntimes(config) {
|
|
94
|
+
const set = new Set();
|
|
95
|
+
if (!config || typeof config !== 'object') {
|
|
96
|
+
set.add('claude');
|
|
97
|
+
return Array.from(set);
|
|
98
|
+
}
|
|
99
|
+
const engine = config.engine || {};
|
|
100
|
+
set.add(engine.defaultCli ? String(engine.defaultCli) : 'claude');
|
|
101
|
+
if (engine.ccCli) set.add(String(engine.ccCli));
|
|
102
|
+
for (const agent of Object.values(config.agents || {})) {
|
|
103
|
+
if (agent && agent.cli) set.add(String(agent.cli));
|
|
104
|
+
}
|
|
105
|
+
return Array.from(set).sort();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Try to resolve a runtime's binary via the registry. Returns a preflight
|
|
110
|
+
* result entry — never throws. Unknown-runtime errors collapse to a single
|
|
111
|
+
* warn entry so the rest of the loop keeps running.
|
|
112
|
+
*/
|
|
113
|
+
function _checkRuntimeBinary(runtimeName) {
|
|
114
|
+
let adapter;
|
|
115
|
+
try {
|
|
116
|
+
adapter = require('./runtimes').resolveRuntime(runtimeName);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return {
|
|
119
|
+
name: `Runtime: ${runtimeName}`,
|
|
120
|
+
ok: false,
|
|
121
|
+
message: `unknown runtime — ${e.message}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
let resolved = null;
|
|
125
|
+
try { resolved = adapter.resolveBinary({ env: process.env }); }
|
|
126
|
+
catch { /* defensive — treat any throw as "not found" so the loop keeps running */ }
|
|
127
|
+
if (resolved && resolved.bin) {
|
|
128
|
+
const shim = resolved.native === false ? ' (node shim)' : '';
|
|
129
|
+
const lead = Array.isArray(resolved.leadingArgs) && resolved.leadingArgs.length
|
|
130
|
+
? ` (leadingArgs: ${resolved.leadingArgs.join(' ')})` : '';
|
|
131
|
+
return {
|
|
132
|
+
name: `Runtime: ${runtimeName}`,
|
|
133
|
+
ok: true,
|
|
134
|
+
message: `${resolved.bin}${shim}${lead}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const hint = (typeof adapter.installHint === 'string' && adapter.installHint)
|
|
138
|
+
? adapter.installHint
|
|
139
|
+
: `${runtimeName} CLI binary not found on PATH`;
|
|
140
|
+
return {
|
|
141
|
+
name: `Runtime: ${runtimeName}`,
|
|
142
|
+
ok: false,
|
|
143
|
+
message: `not found — ${hint}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fire-and-forget cache warm. We never await: cache warming is a side effect
|
|
149
|
+
* for the next dashboard / doctor read, not something runPreflight should
|
|
150
|
+
* block on. Errors are swallowed — discovery is best-effort.
|
|
151
|
+
*
|
|
152
|
+
* Silent no-op when the runtime can't enumerate models (Claude has no public
|
|
153
|
+
* mechanism, hence `capabilities.modelDiscovery: false`) or when the user
|
|
154
|
+
* explicitly disabled discovery via `engine.disableModelDiscovery`. Those
|
|
155
|
+
* branches live inside `model-discovery.getRuntimeModels`, so we just delegate.
|
|
156
|
+
*/
|
|
157
|
+
function _warmModelCache(runtimeName, config) {
|
|
158
|
+
if (!runtimeName) return;
|
|
159
|
+
let md;
|
|
160
|
+
try { md = require('./model-discovery'); }
|
|
161
|
+
catch { return; /* legacy installs may not have model-discovery yet */ }
|
|
162
|
+
Promise.resolve()
|
|
163
|
+
.then(() => md.getRuntimeModels(runtimeName, { config }))
|
|
164
|
+
.catch(() => { /* swallow — best effort */ });
|
|
165
|
+
}
|
|
166
|
+
|
|
74
167
|
/**
|
|
75
168
|
* Run prerequisite checks. Returns { passed, results } where results is an
|
|
76
169
|
* array of { name, ok, message } objects.
|
|
@@ -78,6 +171,7 @@ function findClaudeBinary() {
|
|
|
78
171
|
* Options:
|
|
79
172
|
* - warnOnly: if true, missing items don't cause passed=false (for init)
|
|
80
173
|
* - verbose: include extra detail in messages
|
|
174
|
+
* - config: fleet config for runtime checks + runtime-config warnings
|
|
81
175
|
*/
|
|
82
176
|
function runPreflight(opts = {}) {
|
|
83
177
|
const results = [];
|
|
@@ -102,18 +196,37 @@ function runPreflight(opts = {}) {
|
|
|
102
196
|
allOk = false;
|
|
103
197
|
}
|
|
104
198
|
|
|
105
|
-
// 3.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
results.push(
|
|
113
|
-
|
|
199
|
+
// 3. Per-runtime binary check (P-9e8a3f1d). Legacy single-runtime callers
|
|
200
|
+
// (no config passed) still get exactly one entry — for `claude` — so the
|
|
201
|
+
// historical "3 results" shape is preserved.
|
|
202
|
+
const runtimes = _distinctRuntimes(opts.config);
|
|
203
|
+
for (const runtimeName of runtimes) {
|
|
204
|
+
const r = _checkRuntimeBinary(runtimeName);
|
|
205
|
+
if (r.ok === false) allOk = false;
|
|
206
|
+
results.push(r);
|
|
207
|
+
// Warm the model cache in the background — silent no-op when the
|
|
208
|
+
// runtime can't enumerate or the user disabled discovery.
|
|
209
|
+
if (opts.config) _warmModelCache(runtimeName, opts.config);
|
|
114
210
|
}
|
|
115
211
|
|
|
116
|
-
// Auth is handled by
|
|
212
|
+
// Auth is handled by each runtime CLI itself (Claude API key, GH_TOKEN for
|
|
213
|
+
// Copilot, etc.) — preflight doesn't probe credentials.
|
|
214
|
+
|
|
215
|
+
// 4. Runtime fleet config warnings (P-3b8e5f1d) — only when the caller hands
|
|
216
|
+
// us the config. checkOrExit() / cli start() / doctor() pass it; legacy
|
|
217
|
+
// callers don't, in which case we skip silently.
|
|
218
|
+
if (opts && opts.config && typeof opts.config === 'object') {
|
|
219
|
+
try {
|
|
220
|
+
const shared = require('./shared');
|
|
221
|
+
let runtimeNames = [];
|
|
222
|
+
try { runtimeNames = require('./runtimes').listRuntimes(); }
|
|
223
|
+
catch { /* registry may be missing during partial installs */ }
|
|
224
|
+
const warns = shared.runtimeConfigWarnings(opts.config, runtimeNames);
|
|
225
|
+
for (const w of warns) {
|
|
226
|
+
results.push({ name: `Runtime config (${w.id})`, ok: 'warn', message: w.message });
|
|
227
|
+
}
|
|
228
|
+
} catch { /* defensive — preflight must never throw */ }
|
|
229
|
+
}
|
|
117
230
|
|
|
118
231
|
return { passed: allOk, results };
|
|
119
232
|
}
|
|
@@ -125,7 +238,7 @@ function printPreflight(results, { label = 'Preflight checks' } = {}) {
|
|
|
125
238
|
console.log(`\n ${label}:\n`);
|
|
126
239
|
let allOk = true;
|
|
127
240
|
for (const r of results) {
|
|
128
|
-
const icon = r.ok === true ? '
|
|
241
|
+
const icon = r.ok === true ? '✓' : r.ok === 'warn' ? '!' : '✗';
|
|
129
242
|
const prefix = r.ok === true ? ' ' : r.ok === 'warn' ? ' ' : ' ';
|
|
130
243
|
console.log(`${prefix} ${icon} ${r.name}: ${r.message}`);
|
|
131
244
|
if (r.ok === false) allOk = false;
|
|
@@ -148,12 +261,108 @@ function checkOrExit({ exitOnFail = false, label = 'Preflight checks' } = {}) {
|
|
|
148
261
|
return ok;
|
|
149
262
|
}
|
|
150
263
|
|
|
264
|
+
// ─── Doctor extras (P-9e8a3f1d) ─────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
const _FEATURE_FLAG_DEFAULTS = {
|
|
267
|
+
claudeBareMode: false,
|
|
268
|
+
claudeFallbackModel: undefined,
|
|
269
|
+
copilotDisableBuiltinMcps: true,
|
|
270
|
+
copilotSuppressAgentsMd: true,
|
|
271
|
+
copilotStreamMode: 'on',
|
|
272
|
+
copilotReasoningSummaries: false,
|
|
273
|
+
maxBudgetUsd: undefined,
|
|
274
|
+
disableModelDiscovery: false,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Build the fleet-defaults summary entries surfaced by `minions doctor`.
|
|
279
|
+
* Three classes of output:
|
|
280
|
+
* 1. `Fleet` — `defaultCli` + `defaultModel`
|
|
281
|
+
* 2. `CC overrides` — only when `ccCli` or `ccModel` is set
|
|
282
|
+
* 3. `Active fleet flags` — only when any feature flag deviates from default
|
|
283
|
+
*/
|
|
284
|
+
function _fleetSummaryResults(config) {
|
|
285
|
+
const results = [];
|
|
286
|
+
if (!config || typeof config !== 'object') return results;
|
|
287
|
+
const engine = config.engine || {};
|
|
288
|
+
const defaultCli = engine.defaultCli ? String(engine.defaultCli) : 'claude';
|
|
289
|
+
const defaultModel = engine.defaultModel ? String(engine.defaultModel) : '(runtime default)';
|
|
290
|
+
results.push({ name: 'Fleet', ok: true, message: `defaultCli=${defaultCli} defaultModel=${defaultModel}` });
|
|
291
|
+
|
|
292
|
+
const ccBits = [];
|
|
293
|
+
if (engine.ccCli) ccBits.push(`ccCli=${engine.ccCli}`);
|
|
294
|
+
if (engine.ccModel) ccBits.push(`ccModel=${engine.ccModel}`);
|
|
295
|
+
if (ccBits.length > 0) {
|
|
296
|
+
results.push({ name: 'CC overrides', ok: true, message: ccBits.join(' ') });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const nonDefault = [];
|
|
300
|
+
for (const [k, def] of Object.entries(_FEATURE_FLAG_DEFAULTS)) {
|
|
301
|
+
if (engine[k] === undefined) continue;
|
|
302
|
+
if (engine[k] !== def) nonDefault.push(`${k}=${JSON.stringify(engine[k])}`);
|
|
303
|
+
}
|
|
304
|
+
if (nonDefault.length > 0) {
|
|
305
|
+
results.push({ name: 'Active fleet flags', ok: true, message: nonDefault.join(' ') });
|
|
306
|
+
}
|
|
307
|
+
return results;
|
|
308
|
+
}
|
|
309
|
+
|
|
151
310
|
/**
|
|
152
|
-
*
|
|
311
|
+
* Build the per-runtime model-discovery entries surfaced by `minions doctor`.
|
|
312
|
+
* Emits one entry per distinct runtime in the fleet:
|
|
313
|
+
* - "discovery disabled (engine.disableModelDiscovery)" — fleet-wide opt-out
|
|
314
|
+
* - "discovery unavailable (no enumeration mechanism)" — adapter doesn't support it
|
|
315
|
+
* - "<N> models cached" — listModels returned a non-empty array
|
|
316
|
+
* - "discovery unavailable (...)" — listModels returned null/threw (no token, transient API error)
|
|
317
|
+
*/
|
|
318
|
+
async function _modelDiscoveryResults(config) {
|
|
319
|
+
const results = [];
|
|
320
|
+
if (!config || typeof config !== 'object') return results;
|
|
321
|
+
let md;
|
|
322
|
+
try { md = require('./model-discovery'); } catch { return results; }
|
|
323
|
+
let registry;
|
|
324
|
+
try { registry = require('./runtimes'); } catch { return results; }
|
|
325
|
+
const fleetDisabled = config.engine && config.engine.disableModelDiscovery === true;
|
|
326
|
+
const runtimes = _distinctRuntimes(config);
|
|
327
|
+
for (const runtimeName of runtimes) {
|
|
328
|
+
let adapter;
|
|
329
|
+
try { adapter = registry.resolveRuntime(runtimeName); }
|
|
330
|
+
catch { continue; /* unknown-cli warning was already emitted by runtimeConfigWarnings */ }
|
|
331
|
+
|
|
332
|
+
if (fleetDisabled) {
|
|
333
|
+
results.push({ name: `Models: ${runtimeName}`, ok: 'warn', message: 'discovery disabled (engine.disableModelDiscovery)' });
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (!adapter.capabilities || adapter.capabilities.modelDiscovery !== true) {
|
|
337
|
+
results.push({ name: `Models: ${runtimeName}`, ok: 'warn', message: 'discovery unavailable (no enumeration mechanism)' });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const out = await md.getRuntimeModels(runtimeName, { config });
|
|
342
|
+
if (Array.isArray(out.models) && out.models.length > 0) {
|
|
343
|
+
results.push({ name: `Models: ${runtimeName}`, ok: true, message: `${out.models.length} models cached` });
|
|
344
|
+
} else {
|
|
345
|
+
results.push({ name: `Models: ${runtimeName}`, ok: 'warn', message: 'discovery unavailable (API returned no models — check token)' });
|
|
346
|
+
}
|
|
347
|
+
} catch (e) {
|
|
348
|
+
results.push({ name: `Models: ${runtimeName}`, ok: 'warn', message: `discovery error — ${e && e.message ? e.message : 'unknown'}` });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return results;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Run extended doctor checks (preflight + runtime health + fleet summary +
|
|
356
|
+
* per-runtime model discovery).
|
|
153
357
|
* Requires minionsHome path for runtime checks.
|
|
154
358
|
*/
|
|
155
359
|
function doctor(minionsHome) {
|
|
156
|
-
|
|
360
|
+
// Read config first so preflight can include runtime-fleet warnings.
|
|
361
|
+
const configPathForPreflight = path.join(minionsHome, 'config.json');
|
|
362
|
+
let preflightConfig = null;
|
|
363
|
+
try { preflightConfig = JSON.parse(fs.readFileSync(configPathForPreflight, 'utf8')); }
|
|
364
|
+
catch { /* missing/invalid config is its own check below */ }
|
|
365
|
+
const { passed, results } = runPreflight({ config: preflightConfig });
|
|
157
366
|
|
|
158
367
|
// Runtime checks
|
|
159
368
|
const runtimeResults = [];
|
|
@@ -248,12 +457,11 @@ function doctor(minionsHome) {
|
|
|
248
457
|
req.on('timeout', () => { req.destroy(); resolve({ name: 'Dashboard', ok: 'warn', message: 'not reachable on :7331 — run: minions dash' }); });
|
|
249
458
|
});
|
|
250
459
|
|
|
251
|
-
return dashCheck.then(dashResult => {
|
|
460
|
+
return dashCheck.then(async dashResult => {
|
|
252
461
|
runtimeResults.push(dashResult);
|
|
253
462
|
|
|
254
463
|
// Check playbooks
|
|
255
464
|
const playbooksDir = path.join(minionsHome, 'playbooks');
|
|
256
|
-
// Discover all .md playbooks in the directory — don't hardcode
|
|
257
465
|
let playbooks = [];
|
|
258
466
|
try { playbooks = fs.readdirSync(playbooksDir).filter(f => f.endsWith('.md')); } catch { /* dir may not exist */ }
|
|
259
467
|
if (playbooks.length > 0) {
|
|
@@ -264,10 +472,17 @@ function doctor(minionsHome) {
|
|
|
264
472
|
|
|
265
473
|
// Check port 7331 availability (only if dashboard isn't running)
|
|
266
474
|
if (dashResult.ok !== true) {
|
|
267
|
-
// Dashboard isn't running, port should be free
|
|
268
475
|
runtimeResults.push({ name: 'Port 7331', ok: 'warn', message: 'dashboard not running — port status unknown' });
|
|
269
476
|
}
|
|
270
477
|
|
|
478
|
+
// Fleet defaults + per-runtime model discovery (P-9e8a3f1d). Both depend
|
|
479
|
+
// on the config that we already loaded above; re-using `preflightConfig`
|
|
480
|
+
// avoids a second JSON.parse round-trip.
|
|
481
|
+
const fleetSummary = _fleetSummaryResults(preflightConfig);
|
|
482
|
+
runtimeResults.push(...fleetSummary);
|
|
483
|
+
const modelResults = await _modelDiscoveryResults(preflightConfig);
|
|
484
|
+
runtimeResults.push(...modelResults);
|
|
485
|
+
|
|
271
486
|
// Print all
|
|
272
487
|
const allResults = [...results, ...runtimeResults];
|
|
273
488
|
const ok = printPreflight(allResults, { label: 'Minions Doctor' });
|
|
@@ -286,4 +501,17 @@ function doctor(minionsHome) {
|
|
|
286
501
|
});
|
|
287
502
|
}
|
|
288
503
|
|
|
289
|
-
module.exports = {
|
|
504
|
+
module.exports = {
|
|
505
|
+
findClaudeBinary,
|
|
506
|
+
runPreflight,
|
|
507
|
+
printPreflight,
|
|
508
|
+
checkOrExit,
|
|
509
|
+
doctor,
|
|
510
|
+
// Exposed for unit tests (P-9e8a3f1d) — engine code MUST go through
|
|
511
|
+
// runPreflight/doctor, never these helpers directly.
|
|
512
|
+
_distinctRuntimes,
|
|
513
|
+
_checkRuntimeBinary,
|
|
514
|
+
_warmModelCache,
|
|
515
|
+
_fleetSummaryResults,
|
|
516
|
+
_modelDiscoveryResults,
|
|
517
|
+
};
|