@yemi33/minions 0.1.1599 → 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,18 +1,22 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1599 (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
13
14
  - redraw Copilot icon to match actual mascot — vertical eye pills, not grill bars
14
15
  - invert Copilot icon colors for better dark-theme visibility
15
16
 
17
+ ### Other
18
+ - Stream CC chunks incrementally
19
+
16
20
  ## 0.1.1592 (2026-04-28)
17
21
 
18
22
  ### Features
@@ -54,6 +54,22 @@ function _ccActiveTab() {
54
54
  return _ccTabs.find(function(t) { return t.id === _ccActiveTabId; }) || null;
55
55
  }
56
56
 
57
+ function _ccMergeStreamText(prev, incoming) {
58
+ var current = prev || '';
59
+ var next = incoming || '';
60
+ if (!current) return next;
61
+ if (!next) return current;
62
+ if (next === current) return current;
63
+ if (next.startsWith(current)) return next;
64
+ if (current.startsWith(next)) return current;
65
+ for (var overlap = Math.min(current.length, next.length); overlap > 0; overlap--) {
66
+ if (current.slice(-overlap) === next.slice(0, overlap)) {
67
+ return current + next.slice(overlap);
68
+ }
69
+ }
70
+ return current + '\n\n' + next;
71
+ }
72
+
57
73
  async function _ccDashboardHealth() {
58
74
  var controller = new AbortController();
59
75
  var timer = setTimeout(function() { controller.abort(); }, 3000);
@@ -580,7 +596,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
580
596
 
581
597
  async function _handleEvent(evt) {
582
598
  if (evt.type === 'chunk') {
583
- streamedText = evt.text;
599
+ streamedText = _ccMergeStreamText(streamedText, evt.text || '');
584
600
  if (activeTab) activeTab._streamedText = streamedText;
585
601
  updateStreamDiv();
586
602
  } else if (evt.type === 'heartbeat') {
@@ -35,6 +35,20 @@ function _changed(key, value) {
35
35
  return true;
36
36
  }
37
37
 
38
+ function _formatCcPowerLabel(autoMode) {
39
+ var runtime = autoMode && autoMode.ccCli ? String(autoMode.ccCli) : 'claude';
40
+ var runtimeLabel = runtime.charAt(0).toUpperCase() + runtime.slice(1);
41
+ var model = autoMode && autoMode.ccModel ? String(autoMode.ccModel) : '';
42
+ return 'Ask anything, dispatch work, manage plans — powered by ' + runtimeLabel + (model ? ' (' + model + ')' : '');
43
+ }
44
+
45
+ function _formatCcDrawerLabel(autoMode) {
46
+ var runtime = autoMode && autoMode.ccCli ? String(autoMode.ccCli) : 'claude';
47
+ var runtimeLabel = runtime.charAt(0).toUpperCase() + runtime.slice(1);
48
+ var model = autoMode && autoMode.ccModel ? String(autoMode.ccModel) : '';
49
+ return runtimeLabel + (model ? ' (' + model + ')' : '') + '-powered. Full minions context. Enter to send, Shift+Enter for newline.';
50
+ }
51
+
38
52
  function _processStatusUpdate(data) {
39
53
  // Detect fresh install — clear stale browser state if install ID changed
40
54
  if (data.installId) {
@@ -53,6 +67,10 @@ function _processStatusUpdate(data) {
53
67
  if (autoEl) autoEl.innerHTML = data.autoMode?.approvePlans
54
68
  ? '<span style="font-size:9px;font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(63,185,80,0.15);color:var(--green);border:1px solid rgba(63,185,80,0.3)">AUTO-APPROVE</span>'
55
69
  : '';
70
+ const ccLabelEl = document.getElementById('cmd-powered-by');
71
+ if (ccLabelEl) ccLabelEl.textContent = _formatCcPowerLabel(data.autoMode);
72
+ const ccDrawerLabelEl = document.getElementById('cc-powered-by');
73
+ if (ccDrawerLabelEl) ccDrawerLabelEl.textContent = _formatCcDrawerLabel(data.autoMode);
56
74
  const threshEl = document.getElementById('inbox-threshold');
57
75
  if (threshEl && data.autoMode?.inboxThreshold) threshEl.textContent = data.autoMode.inboxThreshold;
58
76
 
@@ -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">' +
@@ -43,7 +43,7 @@
43
43
  <textarea id="cc-input" rows="2" placeholder="Ask anything or give a command..." style="flex:1;padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:12px;resize:none;font-family:inherit" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();ccSend()}"></textarea>
44
44
  <button onclick="ccSend()" style="background:var(--blue);color:#fff;border:none;border-radius:6px;padding:8px 14px;font-size:12px;font-weight:600;cursor:pointer;align-self:flex-end">Send</button>
45
45
  </div>
46
- <div style="font-size:9px;color:var(--muted);margin-top:4px">Sonnet-powered. Full minions context. Enter to send, Shift+Enter for newline.</div>
46
+ <div id="cc-powered-by" style="font-size:9px;color:var(--muted);margin-top:4px">Full minions context. Enter to send, Shift+Enter for newline.</div>
47
47
  </div>
48
48
  </div>
49
49
 
@@ -10,7 +10,7 @@
10
10
  <div class="cmd-meta" id="cmd-meta" style="display:none"></div>
11
11
  <div class="cmd-hints">
12
12
  <span style="color:var(--blue);font-weight:600">Command Center</span>
13
- <span>Ask anything, dispatch work, manage plans — powered by Sonnet</span>
13
+ <span id="cmd-powered-by">Ask anything, dispatch work, manage plans</span>
14
14
  <button class="cmd-history-btn" onclick="cmdShowHistory()">Past Commands</button>
15
15
  <button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);border-color:var(--green)" onclick="openQuickNoteModal()">+ Note</button>
16
16
  </div>
package/dashboard.js CHANGED
@@ -474,7 +474,8 @@ function getStatus() {
474
474
  decompose: CONFIG.engine?.autoDecompose !== false,
475
475
  tempAgents: !!CONFIG.engine?.allowTempAgents,
476
476
  inboxThreshold: CONFIG.engine?.inboxConsolidateThreshold || shared.ENGINE_DEFAULTS.inboxConsolidateThreshold,
477
- ccModel: CONFIG.engine?.ccModel || shared.ENGINE_DEFAULTS.ccModel,
477
+ ccCli: shared.resolveCcCli(CONFIG.engine),
478
+ ccModel: shared.resolveCcModel(CONFIG.engine),
478
479
  ccEffort: CONFIG.engine?.ccEffort || shared.ENGINE_DEFAULTS.ccEffort,
479
480
  },
480
481
  initialized: !!(CONFIG.agents && Object.keys(CONFIG.agents).length > 0),
@@ -4917,13 +4918,58 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4917
4918
  else if (_validCli(e.ccCli)) config.engine.ccCli = String(e.ccCli);
4918
4919
  else _clamped.push(`ccCli: "${e.ccCli}" not registered (kept previous value)`);
4919
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
+ }
4920
4954
  if (e.defaultModel !== undefined) {
4921
4955
  if (_isClear(e.defaultModel)) delete config.engine.defaultModel;
4922
- 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
+ }
4923
4963
  }
4924
4964
  if (e.ccModel !== undefined) {
4925
4965
  if (_isClear(e.ccModel)) delete config.engine.ccModel;
4926
- 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
+ }
4927
4973
  }
4928
4974
  if (e.claudeFallbackModel !== undefined) {
4929
4975
  if (_isClear(e.claudeFallbackModel)) delete config.engine.claudeFallbackModel;
@@ -4981,6 +5027,34 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4981
5027
  }
4982
5028
 
4983
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
+ }
4984
5058
  for (const [id, updates] of Object.entries(body.agents)) {
4985
5059
  if (!config.agents[id]) continue;
4986
5060
  if (updates.role !== undefined) config.agents[id].role = String(updates.role);
@@ -5000,7 +5074,30 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5000
5074
  }
5001
5075
  if (updates.model !== undefined) {
5002
5076
  if (updates.model === '' || updates.model === null) delete config.agents[id].model;
5003
- 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
+ }
5004
5101
  }
5005
5102
  if (updates.maxBudgetUsd !== undefined) {
5006
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.1599",
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"