@yemi33/minions 0.1.1588 → 0.1.1590

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,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1590 (2026-04-28)
4
+
5
+ ### Features
6
+ - Pluggable CLI runtime adapters (Claude + GitHub Copilot) (#1804)
7
+
8
+ ### Fixes
9
+ - remove duplicate DOC_CHAT_TIMEOUT_MS declaration crashing startup
10
+
3
11
  ## 0.1.1588 (2026-04-28)
4
12
 
5
13
  ### Other
package/bin/minions.js CHANGED
@@ -480,7 +480,7 @@ const engineCmds = new Set([
480
480
  'start', 'stop', 'status', 'pause', 'resume',
481
481
  'queue', 'sources', 'discover', 'dispatch',
482
482
  'spawn', 'work', 'cleanup', 'mcp-sync', 'plan',
483
- 'kill', 'complete',
483
+ 'kill', 'complete', 'config',
484
484
  ]);
485
485
 
486
486
  if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
@@ -548,14 +548,16 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
548
548
  } else if (cmd === 'add' || cmd === 'remove' || cmd === 'list' || cmd === 'scan') {
549
549
  delegate('minions.js', [cmd, ...rest]);
550
550
  } else if (cmd === 'restart') {
551
- // Start both engine and dashboard — the go-to command after a reboot
551
+ // Start both engine and dashboard — the go-to command after a reboot.
552
+ // `--cli` / `--model` flags forward to `engine.js start` so the runtime
553
+ // fleet flips before the daemon spawns (P-6b3f9c2e AC: works on restart).
552
554
  ensureInstalled();
553
555
  // Stop engine if running (graceful attempt)
554
556
  try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME }); } catch {}
555
557
  // Kill all existing engine/dashboard processes — handles crashed engines and orphan dashboards
556
558
  killByPort(7331);
557
559
  killMinionsProcesses(['engine.js', 'dashboard.js']);
558
- const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start'], {
560
+ const engineProc = spawn(process.execPath, [path.join(MINIONS_HOME, 'engine.js'), 'start', ...rest], {
559
561
  cwd: MINIONS_HOME, stdio: 'ignore', detached: true, windowsHide: true
560
562
  });
561
563
  engineProc.unref();
@@ -20,11 +20,22 @@ async function openSettings() {
20
20
  const agents = data.agents || {};
21
21
  const t = data.teams || {};
22
22
 
23
+ // Per-agent override placeholders surface the inherited fleet defaults as
24
+ // muted text — operators see exactly what each agent will resolve to without
25
+ // chasing config files. Empty input clears the override → re-inherit fleet.
26
+ const fleetCliLabel = e.defaultCli || 'claude';
27
+ const fleetModelLabel = e.defaultModel ? String(e.defaultModel) : 'CLI default';
23
28
  const agentRows = Object.entries(agents).map(function([id, a]) {
24
29
  return '<tr>' +
25
30
  '<td style="font-weight:600">' + escHtml(a.emoji || '') + ' ' + escHtml(a.name || id) + '</td>' +
26
31
  '<td><input data-agent="' + escHtml(id) + '" data-field="role" value="' + escHtml(a.role || '') + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px"></td>' +
27
32
  '<td><input data-agent="' + escHtml(id) + '" data-field="skills" value="' + escHtml((a.skills || []).join(', ')) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px"></td>' +
33
+ '<td data-runtime-cli="' + escHtml(id) + '" style="min-width:110px">' +
34
+ // Initial loading placeholder — initRuntimeFleetUI() replaces this with a
35
+ // <select> populated from /api/runtimes once the registry resolves.
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
+ '</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>' +
28
39
  '<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>' +
29
40
  '</tr>';
30
41
  }).join('');
@@ -112,27 +123,84 @@ async function openSettings() {
112
123
  settingsField('Decompose', 'set-mt-decompose', (e.maxTurnsByType || {}).decompose || '', '', 'Default: 15') +
113
124
  '</div>' +
114
125
 
115
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Command Center / Doc Chat</h3>' +
116
- '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
117
- '<div>' +
118
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Model</label>' +
119
- '<select id="set-ccModel" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
120
- '<option value="sonnet"' + ((e.ccModel || 'sonnet') === 'sonnet' ? ' selected' : '') + '>Sonnet (default)</option>' +
121
- '<option value="haiku"' + (e.ccModel === 'haiku' ? ' selected' : '') + '>Haiku (faster, cheaper)</option>' +
122
- '<option value="opus"' + (e.ccModel === 'opus' ? ' selected' : '') + '>Opus (most capable)</option>' +
123
- '</select>' +
124
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">Model used for CC and doc-chat conversations</div>' +
125
- '</div>' +
126
- '<div>' +
127
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Effort Level</label>' +
128
- '<select id="set-ccEffort" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
129
- '<option value=""' + (!e.ccEffort ? ' selected' : '') + '>Default</option>' +
130
- '<option value="low"' + (e.ccEffort === 'low' ? ' selected' : '') + '>Low (quick responses)</option>' +
131
- '<option value="medium"' + (e.ccEffort === 'medium' ? ' selected' : '') + '>Medium</option>' +
132
- '<option value="high"' + (e.ccEffort === 'high' ? ' selected' : '') + '>High (thorough)</option>' +
133
- '</select>' +
134
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">Controls response depth and reasoning effort</div>' +
126
+ // ── Runtime (P-7a5c1f8e) unified fleet runtime + CC overrides + advanced ──
127
+ '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Runtime</h3>' +
128
+ '<div id="set-runtime-section" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:16px">' +
129
+ '<div style="font-size:10px;color:var(--muted);margin-bottom:8px">Single source of truth for which CLI runtime + model the fleet spawns. Per-agent overrides live in the Agents table below.</div>' +
130
+ '<div style="display:grid;grid-template-columns:1fr 2fr;gap:8px;margin-bottom:8px">' +
131
+ '<div>' +
132
+ '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Default CLI</label>' +
133
+ '<select id="set-defaultCli" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
134
+ '<option value="">Loading…</option>' +
135
+ '</select>' +
136
+ '<div style="font-size:9px;color:var(--muted);margin-top:1px">Fleet-wide runtime — registered adapters from <code>/api/runtimes</code></div>' +
137
+ '</div>' +
138
+ '<div>' +
139
+ '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Default Model</label>' +
140
+ '<div id="set-defaultModel-wrap"><input id="set-defaultModel" value="' + escHtml(e.defaultModel || '') + '" placeholder="Default (CLI chooses)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px"></div>' +
141
+ '<div style="font-size:9px;color:var(--muted);margin-top:1px">Empty = let the runtime pick its own default</div>' +
142
+ '</div>' +
135
143
  '</div>' +
144
+ // CC overrides — collapsed by default
145
+ '<details id="set-cc-overrides-details"' + ((e.ccCli || e.ccModel) ? ' open' : '') + ' style="margin-top:8px;border-top:1px solid var(--border);padding-top:8px">' +
146
+ '<summary style="cursor:pointer;font-size:11px;color:var(--text);user-select:none">Customize CC separately ' +
147
+ '<span style="font-size:9px;color:var(--muted)">(Command Center + doc-chat use the fleet defaults unless overridden)</span>' +
148
+ '</summary>' +
149
+ '<div style="display:grid;grid-template-columns:1fr 2fr 1fr;gap:8px;margin-top:8px">' +
150
+ '<div>' +
151
+ '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">CC CLI</label>' +
152
+ '<select id="set-ccCli" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
153
+ '<option value="">Loading…</option>' +
154
+ '</select>' +
155
+ '<div style="font-size:9px;color:var(--muted);margin-top:1px">Empty = inherit Default CLI</div>' +
156
+ '</div>' +
157
+ '<div>' +
158
+ '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">CC Model</label>' +
159
+ '<input id="set-ccModel" value="' + escHtml(e.ccModel || '') + '" placeholder="(inherits Default Model)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
160
+ '<div style="font-size:9px;color:var(--muted);margin-top:1px">Empty = inherit Default Model</div>' +
161
+ '</div>' +
162
+ '<div>' +
163
+ '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Effort</label>' +
164
+ '<select id="set-ccEffort" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
165
+ '<option value=""' + (!e.ccEffort ? ' selected' : '') + '>Default</option>' +
166
+ '<option value="low"' + (e.ccEffort === 'low' ? ' selected' : '') + '>Low</option>' +
167
+ '<option value="medium"' + (e.ccEffort === 'medium' ? ' selected' : '') + '>Medium</option>' +
168
+ '<option value="high"' + (e.ccEffort === 'high' ? ' selected' : '') + '>High</option>' +
169
+ '</select>' +
170
+ '<div style="font-size:9px;color:var(--muted);margin-top:1px">CC reasoning depth</div>' +
171
+ '</div>' +
172
+ '</div>' +
173
+ '</details>' +
174
+ // Advanced runtime settings — collapsed by default
175
+ '<details id="set-runtime-advanced-details" style="margin-top:8px;border-top:1px solid var(--border);padding-top:8px">' +
176
+ '<summary style="cursor:pointer;font-size:11px;color:var(--text);user-select:none">Advanced runtime settings ' +
177
+ '<span style="font-size:9px;color:var(--muted)">(per-runtime feature flags)</span>' +
178
+ '</summary>' +
179
+ '<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
180
+ settingsToggle('Claude bare mode', 'set-claudeBareMode', !!e.claudeBareMode, '--bare suppresses CLAUDE.md auto-discovery; pair with explicit ccSystemPrompt or context will be lost') +
181
+ '</div>' +
182
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px">' +
183
+ settingsField('Claude fallback model', 'set-claudeFallbackModel', e.claudeFallbackModel || '', '', 'Used by --fallback-model on rate-limit / overload (Claude only)') +
184
+ settingsField('Max budget (USD)', 'set-maxBudgetUsd', e.maxBudgetUsd != null ? String(e.maxBudgetUsd) : '', '', 'Fleet ceiling for --max-budget-usd. 0 is a valid cap (read-only / dry-run). Empty = no cap. Claude only.') +
185
+ '</div>' +
186
+ '<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
187
+ // Tooltip on copilotDisableBuiltinMcps MUST warn about the split-brain risk
188
+ settingsToggle('Copilot: disable built-in MCPs', 'set-copilotDisableBuiltinMcps', e.copilotDisableBuiltinMcps !== false,
189
+ '⚠ When OFF, Copilot agents can autonomously create PRs/labels/comments via the github-mcp-server, bypassing pull-requests.json tracking — Minions and Copilot end up with split views of the same PR. Keep ON unless you understand the risk.') +
190
+ settingsToggle('Copilot: suppress AGENTS.md', 'set-copilotSuppressAgentsMd', e.copilotSuppressAgentsMd !== false, '--no-custom-instructions: stops AGENTS.md auto-load from fighting Minions playbook prompts') +
191
+ settingsToggle('Copilot: reasoning summaries', 'set-copilotReasoningSummaries', !!e.copilotReasoningSummaries, '--enable-reasoning-summaries (Anthropic-family models only)') +
192
+ settingsToggle('Disable model discovery', 'set-disableModelDiscovery', !!e.disableModelDiscovery, 'Skip /api/runtimes/<name>/models REST calls fleet-wide. Settings UI falls back to free-text.') +
193
+ '</div>' +
194
+ '<div style="display:grid;grid-template-columns:1fr 3fr;gap:8px;margin-top:8px">' +
195
+ '<div>' +
196
+ '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Copilot stream</label>' +
197
+ '<select id="set-copilotStreamMode" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
198
+ '<option value="on"' + ((e.copilotStreamMode || 'on') === 'on' ? ' selected' : '') + '>on (incremental)</option>' +
199
+ '<option value="off"' + (e.copilotStreamMode === 'off' ? ' selected' : '') + '>off (batched)</option>' +
200
+ '</select>' +
201
+ '</div>' +
202
+ '</div>' +
203
+ '</details>' +
136
204
  '</div>' +
137
205
 
138
206
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Teams Integration</h3>' +
@@ -175,8 +243,9 @@ async function openSettings() {
175
243
  '</div>' +
176
244
 
177
245
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Agents</h3>' +
246
+ '<div style="font-size:10px;color:var(--muted);margin-bottom:6px">CLI / Model placeholders show the fleet default each agent will inherit. Pick a value to pin per-agent; clear to re-inherit.</div>' +
178
247
  '<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:11px">' +
179
- '<tr style="text-align:left;color:var(--muted)"><th style="padding:4px">Agent</th><th style="padding:4px">Role</th><th style="padding:4px">Skills</th><th style="padding:4px">Budget $/mo</th></tr>' +
248
+ '<tr style="text-align:left;color:var(--muted)"><th style="padding:4px">Agent</th><th style="padding:4px">Role</th><th style="padding:4px">Skills</th><th style="padding:4px">CLI</th><th style="padding:4px">Model</th><th style="padding:4px">Budget $/mo</th></tr>' +
180
249
  agentRows +
181
250
  '</table>' +
182
251
 
@@ -226,6 +295,118 @@ async function openSettings() {
226
295
  warn.style.display = 'none';
227
296
  }
228
297
  });
298
+
299
+ // ── Runtime fleet wiring (P-7a5c1f8e) ──────────────────────────────────────
300
+ // 1. Load registered runtimes into the defaultCli + ccCli dropdowns.
301
+ // 2. Load models for the selected defaultCli into the defaultModel input.
302
+ // 3. On defaultCli change → re-fetch models so the input never shows stale list.
303
+ // The same pattern wires ccCli → ccModel; ccCli inherits defaultCli when unset.
304
+ initRuntimeFleetUI(e, agents);
305
+ }
306
+
307
+ async function initRuntimeFleetUI(engineCfg, agentsCfg) {
308
+ const cliSelect = document.getElementById('set-defaultCli');
309
+ const ccCliSelect = document.getElementById('set-ccCli');
310
+ if (!cliSelect || !ccCliSelect) return;
311
+
312
+ // Fetch the registry; render an empty fallback on failure so the rest of the
313
+ // settings panel still works.
314
+ let runtimes = [];
315
+ try {
316
+ const r = await fetch('/api/runtimes');
317
+ const d = await r.json();
318
+ runtimes = Array.isArray(d.runtimes) ? d.runtimes : [];
319
+ } catch { /* ignore — we'll surface a free-text-only path below */ }
320
+
321
+ // Always include 'claude' as a fallback option even if /api/runtimes is empty;
322
+ // legacy installs without the registry endpoint should still see something pickable.
323
+ const names = runtimes.length ? runtimes.map(rt => rt.name) : ['claude'];
324
+ const currentDefault = engineCfg.defaultCli || 'claude';
325
+ const currentCc = engineCfg.ccCli || '';
326
+ cliSelect.innerHTML = names.map(n =>
327
+ '<option value="' + escHtml(n) + '"' + (n === currentDefault ? ' selected' : '') + '>' + escHtml(n) + '</option>'
328
+ ).join('');
329
+ ccCliSelect.innerHTML =
330
+ '<option value=""' + (!currentCc ? ' selected' : '') + '>Inherit Default CLI</option>' +
331
+ names.map(n =>
332
+ '<option value="' + escHtml(n) + '"' + (n === currentCc ? ' selected' : '') + '>' + escHtml(n) + '</option>'
333
+ ).join('');
334
+
335
+ // Hydrate per-agent CLI dropdowns now that we know the registered names. The
336
+ // Agents table renders cells with `data-runtime-cli="<id>"` as a hook.
337
+ const cliCells = document.querySelectorAll('[data-runtime-cli]');
338
+ for (const cell of cliCells) {
339
+ const agentId = cell.getAttribute('data-runtime-cli');
340
+ const agent = (agentsCfg || {})[agentId] || {};
341
+ const current = agent.cli || '';
342
+ cell.innerHTML =
343
+ '<select data-agent="' + escHtml(agentId) + '" data-field="cli" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:11px">' +
344
+ '<option value=""' + (!current ? ' selected' : '') + '>(fleet default)</option>' +
345
+ names.map(n =>
346
+ '<option value="' + escHtml(n) + '"' + (n === current ? ' selected' : '') + '>' + escHtml(n) + '</option>'
347
+ ).join('') +
348
+ '</select>';
349
+ }
350
+
351
+ // Models load for the resolved default + CC CLIs. ccCli falls back to
352
+ // defaultCli when unset — same rule as resolveCcCli().
353
+ loadModelsForRuntime(cliSelect.value, 'set-defaultModel', engineCfg.defaultModel || '');
354
+ loadModelsForRuntime(currentCc || cliSelect.value, 'set-ccModel', engineCfg.ccModel || '');
355
+
356
+ // CLI change → re-fetch models. NEVER carry the previous runtime's list over.
357
+ cliSelect.addEventListener('change', () => {
358
+ loadModelsForRuntime(cliSelect.value, 'set-defaultModel', '');
359
+ if (!ccCliSelect.value) {
360
+ // CC inherits defaultCli — its model list must follow.
361
+ loadModelsForRuntime(cliSelect.value, 'set-ccModel', '');
362
+ }
363
+ });
364
+ ccCliSelect.addEventListener('change', () => {
365
+ const target = ccCliSelect.value || cliSelect.value;
366
+ loadModelsForRuntime(target, 'set-ccModel', '');
367
+ });
368
+ }
369
+
370
+ /**
371
+ * Replace the input/select at `inputId` with a dropdown when the runtime
372
+ * exposes a model list, or a free-text input when `{ models: null }` (e.g.
373
+ * Claude or model-discovery disabled). The "Default (CLI chooses)" option is
374
+ * always present and submits empty string.
375
+ */
376
+ async function loadModelsForRuntime(runtimeName, inputId, currentValue) {
377
+ const wrap = document.getElementById(inputId)?.parentElement;
378
+ if (!wrap) return;
379
+ if (!runtimeName) {
380
+ wrap.innerHTML = '<input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="(no runtime selected)" disabled style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:12px">';
381
+ return;
382
+ }
383
+ let payload = { models: null };
384
+ try {
385
+ const res = await fetch('/api/runtimes/' + encodeURIComponent(runtimeName) + '/models');
386
+ if (res.ok) payload = await res.json();
387
+ } catch { /* fall through to free-text */ }
388
+
389
+ const models = Array.isArray(payload.models) ? payload.models : null;
390
+ if (!models || models.length === 0) {
391
+ // Free-text fallback — let the user type anything (custom Anthropic /
392
+ // OpenAI model IDs, future models, etc.).
393
+ wrap.innerHTML = '<input id="' + inputId + '" value="' + escHtml(currentValue || '') + '" placeholder="Default (CLI chooses)" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">';
394
+ return;
395
+ }
396
+ // Dropdown. The first option submits empty string → "Default (CLI chooses)".
397
+ let opts = '<option value=""' + (!currentValue ? ' selected' : '') + '>Default (CLI chooses)</option>';
398
+ for (const m of models) {
399
+ const id = m.id || m.name || '';
400
+ if (!id) continue;
401
+ const label = m.name && m.name !== id ? (id + ' — ' + m.name) : id;
402
+ opts += '<option value="' + escHtml(id) + '"' + (id === currentValue ? ' selected' : '') + '>' + escHtml(label) + '</option>';
403
+ }
404
+ // If the current value isn't in the model list (custom / older choice),
405
+ // surface it as a selectable option so the user doesn't lose it on next save.
406
+ if (currentValue && !models.some(m => (m.id || m.name) === currentValue)) {
407
+ opts += '<option value="' + escHtml(currentValue) + '" selected>' + escHtml(currentValue) + ' (custom)</option>';
408
+ }
409
+ 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>';
229
410
  }
230
411
 
231
412
  function settingsToggle(label, id, checked, hint) {
@@ -284,8 +465,21 @@ async function saveSettings() {
284
465
  agentBusyReassignMs: document.getElementById('set-agentBusyReassignMs').value,
285
466
  ignoredCommentAuthors: document.getElementById('set-ignoredCommentAuthors').value,
286
467
  versionCheckInterval: document.getElementById('set-versionCheckInterval').value,
287
- ccModel: document.getElementById('set-ccModel').value,
468
+ // Runtime fleet (P-7a5c1f8e). Empty strings are intentional — they signal
469
+ // "clear this override". The server deletes the key from config.engine.
470
+ defaultCli: (document.getElementById('set-defaultCli')?.value ?? '').trim(),
471
+ defaultModel: (document.getElementById('set-defaultModel')?.value ?? '').trim(),
472
+ ccCli: (document.getElementById('set-ccCli')?.value ?? '').trim(),
473
+ ccModel: (document.getElementById('set-ccModel')?.value ?? '').trim(),
288
474
  ccEffort: document.getElementById('set-ccEffort').value || null,
475
+ claudeBareMode: !!document.getElementById('set-claudeBareMode')?.checked,
476
+ claudeFallbackModel: (document.getElementById('set-claudeFallbackModel')?.value ?? '').trim(),
477
+ copilotDisableBuiltinMcps: !!document.getElementById('set-copilotDisableBuiltinMcps')?.checked,
478
+ copilotSuppressAgentsMd: !!document.getElementById('set-copilotSuppressAgentsMd')?.checked,
479
+ copilotStreamMode: document.getElementById('set-copilotStreamMode')?.value || 'on',
480
+ copilotReasoningSummaries: !!document.getElementById('set-copilotReasoningSummaries')?.checked,
481
+ maxBudgetUsd: (document.getElementById('set-maxBudgetUsd')?.value ?? '').trim(),
482
+ disableModelDiscovery: !!document.getElementById('set-disableModelDiscovery')?.checked,
289
483
  maxTurnsByType: (function() {
290
484
  var mbt = {};
291
485
  var types = ['explore', 'ask', 'review', 'implement', 'fix', 'test', 'verify', 'plan', 'decompose'];
package/dashboard.js CHANGED
@@ -548,9 +548,12 @@ const ccLiveStreams = new Map(); // tabId → buffered live stream state for rec
548
548
  const CC_INFLIGHT_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes — auto-release if request hangs
549
549
  const CC_LOCK_WAIT_MS = 200; // grace period for previous handler's finally to release lock
550
550
  const CC_STREAM_HEARTBEAT_MS = 15000; // keep streaming responses alive across proxies/restart races
551
- const DOC_CHAT_TIMEOUT_MS = 360000; // allow longer doc-chat turns before timing out server-side
552
551
  const CC_STREAM_REATTACH_GRACE_MS = 60000; // keep CC job alive briefly after disconnect so the UI can reattach
553
552
  const CC_STREAM_DONE_RETENTION_MS = 30000; // retain final payload briefly so reconnect can still receive it
553
+ // Doc-chat is interactive — long-doc edits with multi-step Read+Write tool use can run
554
+ // 4–5 min on `canEdit:true` paths. CC's default 2-min timeout was killing legitimate
555
+ // edits mid-stream. Pinned to 6 min as the bounded but generous ceiling.
556
+ const DOC_CHAT_TIMEOUT_MS = 360000;
554
557
  function _releaseCCTab(tabId) { ccInFlightTabs.delete(tabId); ccInFlightAborts.delete(tabId); }
555
558
  function _getCcLiveStream(tabId) {
556
559
  return ccLiveStreams.get(tabId) || null;
@@ -1230,6 +1233,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
1230
1233
  if (sessionId && maxTurns > 1) {
1231
1234
  const p1 = llm.callLLM(buildPrompt({ includePreamble: false }), '', {
1232
1235
  timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
1236
+ engineConfig: CONFIG.engine,
1233
1237
  });
1234
1238
  if (onAbortReady) onAbortReady(p1.abort);
1235
1239
  result = await p1;
@@ -1241,9 +1245,10 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
1241
1245
  }
1242
1246
 
1243
1247
  // No text — distinguish "session exists but call failed" (e.g. tool timeout)
1244
- // from "session is truly dead" (no sessionId returned, or stderr indicates invalid session).
1245
- const sessionStillValid = llm.isResumeSessionStillValid(result);
1246
- if (sessionStillValid) {
1248
+ // from "session is truly dead" (no sessionId in the parsed output).
1249
+ // Per P-5e1b7a3c: trust the runtime adapter's parseOutput — if it found a
1250
+ // sessionId the session is alive; if not, treat it as dead and retry fresh.
1251
+ if (result.sessionId !== null) {
1247
1252
  console.log(`[${label}] Resume call failed (code=${result.code}, empty=${!result.text}) but session is still valid — preserving session for retry`);
1248
1253
  updateSession(store, sessionKey, result.sessionId || sessionId, true);
1249
1254
  return result;
@@ -1265,6 +1270,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
1265
1270
  const freshPrompt = buildPrompt();
1266
1271
  const p2 = llm.callLLM(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
1267
1272
  timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
1273
+ engineConfig: CONFIG.engine,
1268
1274
  });
1269
1275
  if (onAbortReady) onAbortReady(p2.abort);
1270
1276
  result = await p2;
@@ -1281,6 +1287,7 @@ async function ccCall(message, { store = 'cc', sessionKey, extraContext, label =
1281
1287
  await new Promise(r => setTimeout(r, 2000));
1282
1288
  const p3 = llm.callLLM(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
1283
1289
  timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
1290
+ engineConfig: CONFIG.engine,
1284
1291
  });
1285
1292
  if (onAbortReady) onAbortReady(p3.abort);
1286
1293
  result = await p3;
@@ -1311,6 +1318,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
1311
1318
  if (sessionId && maxTurns > 1) {
1312
1319
  const p1 = llm.callLLMStreaming(buildPrompt({ includePreamble: false }), '', {
1313
1320
  timeout, label, model, maxTurns, allowedTools, sessionId, effort: ccEffort, direct: true,
1321
+ engineConfig: CONFIG.engine,
1314
1322
  onChunk,
1315
1323
  onToolUse,
1316
1324
  });
@@ -1323,8 +1331,10 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
1323
1331
  return result;
1324
1332
  }
1325
1333
 
1326
- const sessionStillValid = llm.isResumeSessionStillValid(result);
1327
- if (sessionStillValid) {
1334
+ // Per P-5e1b7a3c: parsedOutput.sessionId !== null means the runtime adapter
1335
+ // successfully captured a session — preserve it for retry. null means the
1336
+ // session is truly dead (or never started); rotate it.
1337
+ if (result.sessionId !== null) {
1328
1338
  console.log(`[${label}] Resume call failed (code=${result.code}, empty=${!result.text}) but session is still valid — preserving session for retry`);
1329
1339
  updateSession(store, sessionKey, result.sessionId || sessionId, true);
1330
1340
  return result;
@@ -1344,6 +1354,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
1344
1354
  const freshPrompt = buildPrompt();
1345
1355
  const p2 = llm.callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
1346
1356
  timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
1357
+ engineConfig: CONFIG.engine,
1347
1358
  onChunk,
1348
1359
  onToolUse,
1349
1360
  });
@@ -1361,6 +1372,7 @@ async function ccCallStreaming(message, { store = 'cc', sessionKey, extraContext
1361
1372
  await new Promise(r => setTimeout(r, 2000));
1362
1373
  const p3 = llm.callLLMStreaming(freshPrompt, CC_STATIC_SYSTEM_PROMPT, {
1363
1374
  timeout, label, model, maxTurns, allowedTools, effort: ccEffort, direct: true,
1375
+ engineConfig: CONFIG.engine,
1364
1376
  onChunk,
1365
1377
  onToolUse,
1366
1378
  });
@@ -1429,6 +1441,7 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
1429
1441
  timeout: DOC_CHAT_TIMEOUT_MS,
1430
1442
  allowedTools: canEdit ? 'Read,Write,Edit,Glob,Grep' : 'Read,Glob,Grep',
1431
1443
  maxTurns: canEdit ? 25 : 10,
1444
+ timeout: DOC_CHAT_TIMEOUT_MS,
1432
1445
  skipStatePreamble: true,
1433
1446
  ...(model ? { model } : {}),
1434
1447
  onAbortReady,
@@ -1478,6 +1491,7 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
1478
1491
  timeout: DOC_CHAT_TIMEOUT_MS,
1479
1492
  allowedTools: canEdit ? 'Read,Write,Edit,Glob,Grep' : 'Read,Glob,Grep',
1480
1493
  maxTurns: canEdit ? 25 : 10,
1494
+ timeout: DOC_CHAT_TIMEOUT_MS,
1481
1495
  skipStatePreamble: true,
1482
1496
  ...(model ? { model } : {}),
1483
1497
  onAbortReady,
@@ -4525,6 +4539,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4525
4539
  timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
4526
4540
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
4527
4541
  sessionId, effort: streamEffort, direct: true,
4542
+ engineConfig: CONFIG.engine,
4528
4543
  onChunk: (text) => {
4529
4544
  const actIdx = findCCActionsDelimiter(text);
4530
4545
  const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
@@ -4554,6 +4569,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4554
4569
  timeout: 900000, label: 'command-center', model: streamModel, maxTurns: ccMaxTurns,
4555
4570
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
4556
4571
  effort: streamEffort, direct: true,
4572
+ engineConfig: CONFIG.engine,
4557
4573
  onChunk: (text) => {
4558
4574
  const actIdx = findCCActionsDelimiter(text);
4559
4575
  const display = actIdx >= 0 ? text.slice(0, actIdx).trim() : text;
@@ -4876,10 +4892,57 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4876
4892
  delete config.engine.adoPollCommentsEvery;
4877
4893
  // String fields
4878
4894
  if (e.worktreeRoot !== undefined) config.engine.worktreeRoot = String(e.worktreeRoot || D.worktreeRoot);
4879
- // CC model/effort
4895
+
4896
+ // ── Runtime fleet (P-7a5c1f8e) ─────────────────────────────────────
4897
+ // Empty string clears the override — the dashboard's "Default (CLI
4898
+ // chooses)" option submits '' and we must persist that as "unset".
4899
+ // Validate `defaultCli` and `ccCli` against the runtime registry so a
4900
+ // typo in the dashboard can't pin the fleet to a non-existent runtime.
4901
+ const _isClear = (v) => v === '' || v === null;
4902
+ let _registeredCliNames = null;
4903
+ const _validCli = (name) => {
4904
+ if (_registeredCliNames == null) {
4905
+ try { _registeredCliNames = require('./engine/runtimes').listRuntimes(); }
4906
+ catch { _registeredCliNames = []; }
4907
+ }
4908
+ return _registeredCliNames.length === 0 || _registeredCliNames.includes(String(name));
4909
+ };
4910
+ if (e.defaultCli !== undefined) {
4911
+ if (_isClear(e.defaultCli)) delete config.engine.defaultCli;
4912
+ else if (_validCli(e.defaultCli)) config.engine.defaultCli = String(e.defaultCli);
4913
+ else _clamped.push(`defaultCli: "${e.defaultCli}" not registered (kept previous value)`);
4914
+ }
4915
+ if (e.ccCli !== undefined) {
4916
+ if (_isClear(e.ccCli)) delete config.engine.ccCli;
4917
+ else if (_validCli(e.ccCli)) config.engine.ccCli = String(e.ccCli);
4918
+ else _clamped.push(`ccCli: "${e.ccCli}" not registered (kept previous value)`);
4919
+ }
4920
+ if (e.defaultModel !== undefined) {
4921
+ if (_isClear(e.defaultModel)) delete config.engine.defaultModel;
4922
+ else config.engine.defaultModel = String(e.defaultModel);
4923
+ }
4880
4924
  if (e.ccModel !== undefined) {
4881
- const valid = ['sonnet', 'haiku', 'opus'];
4882
- config.engine.ccModel = valid.includes(e.ccModel) ? e.ccModel : D.ccModel;
4925
+ if (_isClear(e.ccModel)) delete config.engine.ccModel;
4926
+ else config.engine.ccModel = String(e.ccModel);
4927
+ }
4928
+ if (e.claudeFallbackModel !== undefined) {
4929
+ if (_isClear(e.claudeFallbackModel)) delete config.engine.claudeFallbackModel;
4930
+ else config.engine.claudeFallbackModel = String(e.claudeFallbackModel);
4931
+ }
4932
+ if (e.copilotStreamMode !== undefined) {
4933
+ const valid = ['on', 'off'];
4934
+ if (_isClear(e.copilotStreamMode)) delete config.engine.copilotStreamMode;
4935
+ else if (valid.includes(e.copilotStreamMode)) config.engine.copilotStreamMode = e.copilotStreamMode;
4936
+ else _clamped.push(`copilotStreamMode: "${e.copilotStreamMode}" not in [on, off] (kept previous value)`);
4937
+ }
4938
+ // maxBudgetUsd uses ?? semantics — 0 is a valid cap (read-only / dry-run agents).
4939
+ if (e.maxBudgetUsd !== undefined) {
4940
+ if (_isClear(e.maxBudgetUsd)) delete config.engine.maxBudgetUsd;
4941
+ else {
4942
+ const n = Number(e.maxBudgetUsd);
4943
+ if (Number.isFinite(n) && n >= 0) config.engine.maxBudgetUsd = n;
4944
+ else _clamped.push(`maxBudgetUsd: "${e.maxBudgetUsd}" must be ≥ 0 (kept previous value)`);
4945
+ }
4883
4946
  }
4884
4947
  if (e.ccEffort !== undefined) {
4885
4948
  const valid = [null, 'low', 'medium', 'high'];
@@ -4927,6 +4990,31 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4927
4990
  if (val === undefined || isNaN(val)) delete config.agents[id].monthlyBudgetUsd;
4928
4991
  else config.agents[id].monthlyBudgetUsd = Math.max(0, val);
4929
4992
  }
4993
+ // Per-agent runtime overrides (P-7a5c1f8e). Empty string clears
4994
+ // the override so the agent inherits the fleet default; validated
4995
+ // CLI values pin the agent to a specific runtime. `0` is a valid
4996
+ // maxBudgetUsd (read-only / dry-run agents).
4997
+ if (updates.cli !== undefined) {
4998
+ if (updates.cli === '' || updates.cli === null) delete config.agents[id].cli;
4999
+ else config.agents[id].cli = String(updates.cli);
5000
+ }
5001
+ if (updates.model !== undefined) {
5002
+ if (updates.model === '' || updates.model === null) delete config.agents[id].model;
5003
+ else config.agents[id].model = String(updates.model);
5004
+ }
5005
+ if (updates.maxBudgetUsd !== undefined) {
5006
+ if (updates.maxBudgetUsd === '' || updates.maxBudgetUsd === null) delete config.agents[id].maxBudgetUsd;
5007
+ else {
5008
+ const n = Number(updates.maxBudgetUsd);
5009
+ if (Number.isFinite(n) && n >= 0) config.agents[id].maxBudgetUsd = n;
5010
+ }
5011
+ }
5012
+ if (updates.bareMode !== undefined) {
5013
+ // Boolean override — explicit false should override engine.claudeBareMode=true,
5014
+ // so we accept all three states (true, false, "unset" via empty/null).
5015
+ if (updates.bareMode === '' || updates.bareMode === null) delete config.agents[id].bareMode;
5016
+ else config.agents[id].bareMode = !!updates.bareMode;
5017
+ }
4930
5018
  }
4931
5019
  }
4932
5020
 
@@ -5749,6 +5837,44 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5749
5837
  }},
5750
5838
  { method: 'POST', path: '/api/engine/restart', desc: 'Force-kill engine and restart immediately', handler: handleEngineRestart },
5751
5839
 
5840
+ // Runtimes (CLI fleet) — model discovery + capability surface
5841
+ { method: 'GET', path: '/api/runtimes', desc: 'List registered CLI runtimes and their capability flags', handler: (req, res) => {
5842
+ const md = require('./engine/model-discovery');
5843
+ return jsonReply(res, 200, { runtimes: md.listAllRuntimes() }, req);
5844
+ }},
5845
+ { method: 'POST', path: /^\/api\/runtimes\/([\w-]+)\/models\/refresh$/, desc: 'Invalidate the models cache for a runtime and re-fetch', handler: async (req, res, match) => {
5846
+ const md = require('./engine/model-discovery');
5847
+ const name = match[1];
5848
+ try {
5849
+ md.invalidateRuntimeModelsCache(name);
5850
+ } catch (e) {
5851
+ if (/Unknown runtime/.test(e.message || '')) return jsonReply(res, 404, { error: e.message }, req);
5852
+ return jsonReply(res, 500, { error: String(e.message || e) }, req);
5853
+ }
5854
+ let payload;
5855
+ try {
5856
+ reloadConfig();
5857
+ payload = await md.getRuntimeModels(name, { force: true, config: CONFIG });
5858
+ } catch (e) {
5859
+ if (/Unknown runtime/.test(e.message || '')) return jsonReply(res, 404, { error: e.message }, req);
5860
+ return jsonReply(res, 500, { error: String(e.message || e) }, req);
5861
+ }
5862
+ return jsonReply(res, 200, payload, req);
5863
+ }},
5864
+ { method: 'GET', path: /^\/api\/runtimes\/([\w-]+)\/models$/, desc: 'Get cached or fresh model list for a runtime', handler: async (req, res, match) => {
5865
+ const md = require('./engine/model-discovery');
5866
+ const name = match[1];
5867
+ let payload;
5868
+ try {
5869
+ reloadConfig();
5870
+ payload = await md.getRuntimeModels(name, { config: CONFIG });
5871
+ } catch (e) {
5872
+ if (/Unknown runtime/.test(e.message || '')) return jsonReply(res, 404, { error: e.message }, req);
5873
+ return jsonReply(res, 500, { error: String(e.message || e) }, req);
5874
+ }
5875
+ return jsonReply(res, 200, payload, req);
5876
+ }},
5877
+
5752
5878
  // Settings
5753
5879
  { method: 'GET', path: '/api/settings', desc: 'Return current engine + claude + routing config', handler: handleSettingsRead },
5754
5880
  { method: 'POST', path: '/api/settings', desc: 'Update engine + claude + agent + teams + projects config', params: 'engine?, claude?, agents?, teams?, projects?', handler: handleSettingsUpdate },