@yemi33/minions 0.1.1600 → 0.1.1601
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 +2 -1
- package/dashboard/js/settings.js +66 -1
- package/dashboard.js +99 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.1601 (2026-04-28)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
6
|
- match runtime tags to actual logos (pixel-crab Claude, mascot Copilot)
|
|
7
7
|
- replace runtime text tag with inline SVG logos
|
|
8
8
|
|
|
9
9
|
### Fixes
|
|
10
|
+
- runtime-aware model picker + cross-runtime validation
|
|
10
11
|
- keep cc stream final text complete
|
|
11
12
|
- switch Copilot icon to outline style for cleaner inline read
|
|
12
13
|
- un-invert Copilot — purple silhouette + white cutouts
|
package/dashboard/js/settings.js
CHANGED
|
@@ -35,7 +35,11 @@ async function openSettings() {
|
|
|
35
35
|
// <select> populated from /api/runtimes once the registry resolves.
|
|
36
36
|
'<input value="' + escHtml(a.cli || '') + '" placeholder="' + escHtml(fleetCliLabel) + ' (fleet)" disabled style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:11px">' +
|
|
37
37
|
'</td>' +
|
|
38
|
-
'<td
|
|
38
|
+
'<td data-runtime-model="' + escHtml(id) + '" style="min-width:140px">' +
|
|
39
|
+
// Loading placeholder — initRuntimeFleetUI() replaces this with a
|
|
40
|
+
// <select> populated from /api/runtimes/<resolved-cli>/models.
|
|
41
|
+
'<input value="' + escHtml(a.model || '') + '" placeholder="' + escHtml(fleetModelLabel) + ' (fleet)" disabled style="width:120px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:11px">' +
|
|
42
|
+
'</td>' +
|
|
39
43
|
'<td><input data-agent="' + escHtml(id) + '" data-field="monthlyBudgetUsd" value="' + escHtml(a.monthlyBudgetUsd != null ? String(a.monthlyBudgetUsd) : '') + '" placeholder="unlimited" style="width:70px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px;text-align:right"></td>' +
|
|
40
44
|
'</tr>';
|
|
41
45
|
}).join('');
|
|
@@ -347,6 +351,26 @@ async function initRuntimeFleetUI(engineCfg, agentsCfg) {
|
|
|
347
351
|
).join('') +
|
|
348
352
|
'</select>';
|
|
349
353
|
}
|
|
354
|
+
// Hydrate per-agent model dropdowns. The model list is keyed off the
|
|
355
|
+
// agent's RESOLVED runtime: per-agent override → fleet default. Without
|
|
356
|
+
// this the input was free-text and a user could (and did) save an agent
|
|
357
|
+
// with cli=claude + model=<some gpt> — invalid combination that crashed
|
|
358
|
+
// dispatch. Refreshing on CLI change clears stale model values.
|
|
359
|
+
const fleetDefaultCli = engineCfg.defaultCli || 'claude';
|
|
360
|
+
for (const cell of cliCells) {
|
|
361
|
+
const agentId = cell.getAttribute('data-runtime-cli');
|
|
362
|
+
const agent = (agentsCfg || {})[agentId] || {};
|
|
363
|
+
const resolvedCli = agent.cli || fleetDefaultCli;
|
|
364
|
+
loadModelsForAgent(agentId, resolvedCli, agent.model || '');
|
|
365
|
+
// CLI dropdown change → refresh that agent's model dropdown to match.
|
|
366
|
+
const sel = cell.querySelector('select[data-field="cli"]');
|
|
367
|
+
if (sel) {
|
|
368
|
+
sel.addEventListener('change', () => {
|
|
369
|
+
const newCli = sel.value || fleetDefaultCli;
|
|
370
|
+
loadModelsForAgent(agentId, newCli, ''); // clear value: previous model may not exist for the new runtime
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
350
374
|
|
|
351
375
|
// Models load for the resolved default + CC CLIs. ccCli falls back to
|
|
352
376
|
// defaultCli when unset — same rule as resolveCcCli().
|
|
@@ -409,6 +433,47 @@ async function loadModelsForRuntime(runtimeName, inputId, currentValue) {
|
|
|
409
433
|
wrap.innerHTML = '<select id="' + inputId + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' + opts + '</select>';
|
|
410
434
|
}
|
|
411
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Per-agent model hydrator. Replaces the placeholder input in the cell
|
|
438
|
+
* `[data-runtime-model="<agentId>"]` with a <select> of valid models for the
|
|
439
|
+
* given runtime. Output element keeps `data-agent` + `data-field="model"` so
|
|
440
|
+
* the existing save flow picks it up unchanged. Free-text input fallback
|
|
441
|
+
* when the runtime returns no model list (Claude / discovery disabled).
|
|
442
|
+
*/
|
|
443
|
+
async function loadModelsForAgent(agentId, runtimeName, currentValue) {
|
|
444
|
+
const cell = document.querySelector('[data-runtime-model="' + agentId + '"]');
|
|
445
|
+
if (!cell) return;
|
|
446
|
+
const baseAttrs = 'data-agent="' + escHtml(agentId) + '" data-field="model"';
|
|
447
|
+
const baseStyle = 'width:120px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px';
|
|
448
|
+
if (!runtimeName) {
|
|
449
|
+
cell.innerHTML = '<input ' + baseAttrs + ' value="' + escHtml(currentValue || '') + '" placeholder="(no runtime)" disabled style="' + baseStyle + ';color:var(--muted)">';
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
let payload = { models: null };
|
|
453
|
+
try {
|
|
454
|
+
const res = await fetch('/api/runtimes/' + encodeURIComponent(runtimeName) + '/models');
|
|
455
|
+
if (res.ok) payload = await res.json();
|
|
456
|
+
} catch { /* fall through to free-text */ }
|
|
457
|
+
|
|
458
|
+
const models = Array.isArray(payload.models) ? payload.models : null;
|
|
459
|
+
if (!models || models.length === 0) {
|
|
460
|
+
cell.innerHTML = '<input ' + baseAttrs + ' value="' + escHtml(currentValue || '') + '" placeholder="' + escHtml(runtimeName) + ' default" style="' + baseStyle + '">';
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
let opts = '<option value=""' + (!currentValue ? ' selected' : '') + '>(fleet/default)</option>';
|
|
464
|
+
for (const m of models) {
|
|
465
|
+
const id = m.id || m.name || '';
|
|
466
|
+
if (!id) continue;
|
|
467
|
+
const label = m.name && m.name !== id ? (id + ' — ' + m.name) : id;
|
|
468
|
+
opts += '<option value="' + escHtml(id) + '"' + (id === currentValue ? ' selected' : '') + '>' + escHtml(label) + '</option>';
|
|
469
|
+
}
|
|
470
|
+
// Preserve unknown saved values so a user-set custom ID survives the next save.
|
|
471
|
+
if (currentValue && !models.some(m => (m.id || m.name) === currentValue)) {
|
|
472
|
+
opts += '<option value="' + escHtml(currentValue) + '" selected>' + escHtml(currentValue) + ' (custom — invalid for ' + escHtml(runtimeName) + '?)</option>';
|
|
473
|
+
}
|
|
474
|
+
cell.innerHTML = '<select ' + baseAttrs + ' style="' + baseStyle + '">' + opts + '</select>';
|
|
475
|
+
}
|
|
476
|
+
|
|
412
477
|
function settingsToggle(label, id, checked, hint) {
|
|
413
478
|
return '<div style="display:flex;align-items:center;gap:8px;padding:4px 0">' +
|
|
414
479
|
'<input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px;cursor:pointer">' +
|
package/dashboard.js
CHANGED
|
@@ -4918,13 +4918,58 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4918
4918
|
else if (_validCli(e.ccCli)) config.engine.ccCli = String(e.ccCli);
|
|
4919
4919
|
else _clamped.push(`ccCli: "${e.ccCli}" not registered (kept previous value)`);
|
|
4920
4920
|
}
|
|
4921
|
+
// Validate fleet-level model assignments against the resolved runtime.
|
|
4922
|
+
// This is where the bug bit: defaultCli=copilot + defaultModel=gpt-5.5
|
|
4923
|
+
// (where gpt-5.5 doesn't actually exist) cascaded into every agent
|
|
4924
|
+
// that didn't pin its own model. Reject when the model is known to
|
|
4925
|
+
// belong to a different runtime than the one it'll spawn against.
|
|
4926
|
+
const _engineModelDiscovery = require('./engine/model-discovery');
|
|
4927
|
+
const _engineRuntimes = require('./engine/runtimes');
|
|
4928
|
+
async function _validateFleetModel(modelStr, resolvedRuntime) {
|
|
4929
|
+
if (!modelStr) return null;
|
|
4930
|
+
let knownForResolved = null;
|
|
4931
|
+
try {
|
|
4932
|
+
const list = await _engineModelDiscovery.getRuntimeModels(resolvedRuntime, { config });
|
|
4933
|
+
if (Array.isArray(list?.models) && list.models.length > 0) {
|
|
4934
|
+
knownForResolved = new Set(list.models.map(m => m.id || m.name).filter(Boolean));
|
|
4935
|
+
}
|
|
4936
|
+
} catch { /* unknown runtime */ }
|
|
4937
|
+
if (knownForResolved && !knownForResolved.has(modelStr)) {
|
|
4938
|
+
return `not a valid model for runtime "${resolvedRuntime}" (known: ${[...knownForResolved].slice(0, 4).join(', ')}${knownForResolved.size > 4 ? '…' : ''})`;
|
|
4939
|
+
}
|
|
4940
|
+
if (!knownForResolved) {
|
|
4941
|
+
// Free-text runtime (Claude). Reject only if model belongs to a different runtime's published list.
|
|
4942
|
+
for (const rt of _engineRuntimes.listRuntimes()) {
|
|
4943
|
+
if (rt === resolvedRuntime) continue;
|
|
4944
|
+
try {
|
|
4945
|
+
const otherList = await _engineModelDiscovery.getRuntimeModels(rt, { config });
|
|
4946
|
+
if (Array.isArray(otherList?.models) && otherList.models.some(m => (m.id || m.name) === modelStr)) {
|
|
4947
|
+
return `belongs to runtime "${rt}" but resolved runtime is "${resolvedRuntime}" — incompatible combination`;
|
|
4948
|
+
}
|
|
4949
|
+
} catch { /* skip */ }
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
return null;
|
|
4953
|
+
}
|
|
4921
4954
|
if (e.defaultModel !== undefined) {
|
|
4922
4955
|
if (_isClear(e.defaultModel)) delete config.engine.defaultModel;
|
|
4923
|
-
else
|
|
4956
|
+
else {
|
|
4957
|
+
const candidate = String(e.defaultModel);
|
|
4958
|
+
const resolvedCli = config.engine.defaultCli || 'claude';
|
|
4959
|
+
const rejection = await _validateFleetModel(candidate, resolvedCli);
|
|
4960
|
+
if (rejection) _clamped.push(`engine.defaultModel: "${candidate}" ${rejection} — kept previous value`);
|
|
4961
|
+
else config.engine.defaultModel = candidate;
|
|
4962
|
+
}
|
|
4924
4963
|
}
|
|
4925
4964
|
if (e.ccModel !== undefined) {
|
|
4926
4965
|
if (_isClear(e.ccModel)) delete config.engine.ccModel;
|
|
4927
|
-
else
|
|
4966
|
+
else {
|
|
4967
|
+
const candidate = String(e.ccModel);
|
|
4968
|
+
const resolvedCli = config.engine.ccCli || config.engine.defaultCli || 'claude';
|
|
4969
|
+
const rejection = await _validateFleetModel(candidate, resolvedCli);
|
|
4970
|
+
if (rejection) _clamped.push(`engine.ccModel: "${candidate}" ${rejection} — kept previous value`);
|
|
4971
|
+
else config.engine.ccModel = candidate;
|
|
4972
|
+
}
|
|
4928
4973
|
}
|
|
4929
4974
|
if (e.claudeFallbackModel !== undefined) {
|
|
4930
4975
|
if (_isClear(e.claudeFallbackModel)) delete config.engine.claudeFallbackModel;
|
|
@@ -4982,6 +5027,34 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4982
5027
|
}
|
|
4983
5028
|
|
|
4984
5029
|
if (body.agents) {
|
|
5030
|
+
// Cache cross-runtime model lists once per request so we can reject
|
|
5031
|
+
// claude+gpt-* / copilot+claude-* combinations before they crash a
|
|
5032
|
+
// dispatch (see #model-validation: a stray engine.defaultModel='gpt-5.5'
|
|
5033
|
+
// pinned every Claude agent into a 404 spawn loop).
|
|
5034
|
+
const _modelDiscovery = require('./engine/model-discovery');
|
|
5035
|
+
const _runtimeModelsCache = new Map(); // runtimeName → Set<modelId> (or null when unknown / Claude)
|
|
5036
|
+
async function _modelsFor(runtimeName) {
|
|
5037
|
+
if (_runtimeModelsCache.has(runtimeName)) return _runtimeModelsCache.get(runtimeName);
|
|
5038
|
+
let set = null;
|
|
5039
|
+
try {
|
|
5040
|
+
const list = await _modelDiscovery.getRuntimeModels(runtimeName, { config });
|
|
5041
|
+
if (Array.isArray(list?.models) && list.models.length > 0) {
|
|
5042
|
+
set = new Set(list.models.map(m => m.id || m.name).filter(Boolean));
|
|
5043
|
+
}
|
|
5044
|
+
} catch { /* unknown runtime → free-text */ }
|
|
5045
|
+
_runtimeModelsCache.set(runtimeName, set);
|
|
5046
|
+
return set;
|
|
5047
|
+
}
|
|
5048
|
+
// Returns the runtime that "owns" this model, or null if no other
|
|
5049
|
+
// runtime claims it. Catches "claude + gpt-5.5" by spotting that
|
|
5050
|
+
// gpt-5.5 belongs to copilot's list.
|
|
5051
|
+
async function _ownerOfModel(modelId) {
|
|
5052
|
+
for (const rt of require('./engine/runtimes').listRuntimes()) {
|
|
5053
|
+
const set = await _modelsFor(rt);
|
|
5054
|
+
if (set && set.has(modelId)) return rt;
|
|
5055
|
+
}
|
|
5056
|
+
return null;
|
|
5057
|
+
}
|
|
4985
5058
|
for (const [id, updates] of Object.entries(body.agents)) {
|
|
4986
5059
|
if (!config.agents[id]) continue;
|
|
4987
5060
|
if (updates.role !== undefined) config.agents[id].role = String(updates.role);
|
|
@@ -5001,7 +5074,30 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5001
5074
|
}
|
|
5002
5075
|
if (updates.model !== undefined) {
|
|
5003
5076
|
if (updates.model === '' || updates.model === null) delete config.agents[id].model;
|
|
5004
|
-
else
|
|
5077
|
+
else {
|
|
5078
|
+
const candidate = String(updates.model);
|
|
5079
|
+
const resolvedCli = config.agents[id].cli || config.engine.defaultCli || 'claude';
|
|
5080
|
+
const knownModels = await _modelsFor(resolvedCli);
|
|
5081
|
+
// Two validation paths:
|
|
5082
|
+
// 1. If the runtime publishes a model list, enforce membership.
|
|
5083
|
+
// 2. If the runtime doesn't (Claude), still reject when the
|
|
5084
|
+
// model belongs to a DIFFERENT runtime's list — that's how
|
|
5085
|
+
// we catch claude+gpt-5.5 (gpt-5.5 is in Copilot's list).
|
|
5086
|
+
let rejection = null;
|
|
5087
|
+
if (knownModels && !knownModels.has(candidate)) {
|
|
5088
|
+
rejection = `not a valid model for runtime "${resolvedCli}" (known: ${[...knownModels].slice(0, 4).join(', ')}${knownModels.size > 4 ? '…' : ''})`;
|
|
5089
|
+
} else if (!knownModels) {
|
|
5090
|
+
const owner = await _ownerOfModel(candidate);
|
|
5091
|
+
if (owner && owner !== resolvedCli) {
|
|
5092
|
+
rejection = `belongs to runtime "${owner}" but agent uses "${resolvedCli}" — incompatible combination`;
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
if (rejection) {
|
|
5096
|
+
_clamped.push(`agents.${id}.model: "${candidate}" ${rejection} — kept previous value`);
|
|
5097
|
+
} else {
|
|
5098
|
+
config.agents[id].model = candidate;
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5005
5101
|
}
|
|
5006
5102
|
if (updates.maxBudgetUsd !== undefined) {
|
|
5007
5103
|
if (updates.maxBudgetUsd === '' || updates.maxBudgetUsd === null) delete config.agents[id].maxBudgetUsd;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1601",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|