@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 CHANGED
@@ -1,12 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1600 (2026-04-28)
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
@@ -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><input data-agent="' + escHtml(id) + '" data-field="model" value="' + escHtml(a.model || '') + '" placeholder="' + escHtml(fleetModelLabel) + ' (fleet)" style="width:120px;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px"></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 config.engine.defaultModel = String(e.defaultModel);
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 config.engine.ccModel = String(e.ccModel);
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 config.agents[id].model = String(updates.model);
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.1600",
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"