@yemi33/minions 0.1.1693 → 0.1.1695

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1695 (2026-05-04)
4
+
5
+ ### Features
6
+ - harden command center stream retry (#2016)
7
+
3
8
  ## 0.1.1693 (2026-05-04)
4
9
 
5
10
  ### Features
package/README.md CHANGED
@@ -406,7 +406,7 @@ No bash or shell involved — Node spawns Node directly. Dependency branches are
406
406
  - **Working directory** — project root (agent creates worktrees as needed)
407
407
  - **MCP servers** — inherited from `~/.claude.json` (no extra config needed)
408
408
  - **Full tool access** — all built-in tools plus all MCP tools
409
- - **Permission mode** — `bypassPermissions` (no interactive prompts)
409
+ - **Permission bypass** — runtime-owned; Claude uses `--dangerously-skip-permissions`, while Copilot uses `--autopilot --allow-all --no-ask-user` (no dashboard `permissionMode` control)
410
410
  - **Output format** — `stream-json` (real-time streaming for live dashboard + completion recovery)
411
411
 
412
412
  ### Post-Completion
@@ -8,6 +8,7 @@ var CC_TITLE_MAX_LENGTH = 40;
8
8
  var _ccTabs = []; // [{id, title, sessionId, messages: [{role, html}]}]
9
9
  var _ccActiveTabId = null;
10
10
  var _ccOpen = false;
11
+ var _ccRetrySeq = 0;
11
12
  // Per-tab sending state stored on tab objects: tab._sending, tab._queue, tab._abortController
12
13
  // Legacy globals for backward compat (badge, drawer close check)
13
14
  var _ccSending = false; // true if active tab is sending (UI indicator only)
@@ -139,6 +140,79 @@ async function _ccDashboardHealth() {
139
140
  }
140
141
  }
141
142
 
143
+ function _ccIsReconnectableStreamError(err) {
144
+ if (!err) return false;
145
+ var name = String(err.name || '').toLowerCase();
146
+ var message = String(err.message || err || '').toLowerCase();
147
+ if (name === 'aborterror') return true;
148
+ return message === 'failed to fetch'
149
+ || message === 'load failed'
150
+ || message.includes('networkerror')
151
+ || message.includes('network request failed')
152
+ || message.includes('the internet connection appears to be offline')
153
+ || message.includes('network connection was lost')
154
+ || message.includes('cancelled')
155
+ || message.includes('canceled')
156
+ || message.includes('aborted');
157
+ }
158
+
159
+ function _ccJsArg(value) {
160
+ return escHtml(JSON.stringify(value == null ? '' : String(value)));
161
+ }
162
+
163
+ function _ccStoreRetryRequest(tab, tabId, message) {
164
+ if (!tab) return { id: '', tabId: tabId || '', message: String(message || '') };
165
+ var id = 'cc-retry-' + Date.now().toString(36) + '-' + (++_ccRetrySeq);
166
+ var request = { id: id, tabId: tabId || tab.id, message: String(message || ''), createdAt: Date.now() };
167
+ if (!tab._retryRequests) tab._retryRequests = {};
168
+ tab._retryRequests[id] = request;
169
+ tab._lastRetryRequestId = id;
170
+ tab._retryRequest = request;
171
+ return request;
172
+ }
173
+
174
+ function _ccFindRetryRequest(tab, retryId) {
175
+ if (!tab) return null;
176
+ if (retryId && tab._retryRequests && tab._retryRequests[retryId]) return tab._retryRequests[retryId];
177
+ if (tab._retryRequest) return tab._retryRequest;
178
+ if (tab._lastRetryRequestId && tab._retryRequests) return tab._retryRequests[tab._lastRetryRequestId] || null;
179
+ return null;
180
+ }
181
+
182
+ function _ccForgetRetryRequest(tab, retryId) {
183
+ if (!tab || !retryId) return;
184
+ if (tab._retryRequests) delete tab._retryRequests[retryId];
185
+ if (tab._lastRetryRequestId === retryId) delete tab._lastRetryRequestId;
186
+ if (tab._retryRequest && tab._retryRequest.id === retryId) delete tab._retryRequest;
187
+ }
188
+
189
+ function _ccRemoveRetryMessage(tab, retryId) {
190
+ var removed = false;
191
+ if (tab && retryId) {
192
+ for (var i = tab.messages.length - 1; i >= 0; i--) {
193
+ if (tab.messages[i] && tab.messages[i]._retryId === retryId) {
194
+ tab.messages.splice(i, 1);
195
+ removed = true;
196
+ break;
197
+ }
198
+ }
199
+ }
200
+ if (retryId && tab && tab.id === _ccActiveTabId) {
201
+ var msgs = document.getElementById('cc-messages');
202
+ if (msgs) {
203
+ Array.prototype.slice.call(msgs.children).some(function(child) {
204
+ if (child.getAttribute && child.getAttribute('data-cc-retry-id') === retryId) {
205
+ child.remove();
206
+ removed = true;
207
+ return true;
208
+ }
209
+ return false;
210
+ });
211
+ }
212
+ }
213
+ return removed;
214
+ }
215
+
142
216
  function _ccFindPinTarget(query) {
143
217
  for (var i = 0; i < (inboxData || []).length; i++) {
144
218
  if (inboxData[i].name.toLowerCase().includes(query)) {
@@ -159,6 +233,7 @@ function _ccFindPinTarget(query) {
159
233
  function ccAbort() {
160
234
  var tab = _ccActiveTab();
161
235
  if (tab && tab._abortController) {
236
+ tab._userAborted = true;
162
237
  try {
163
238
  fetch('/api/command-center/abort', {
164
239
  method: 'POST',
@@ -290,6 +365,7 @@ function ccCloseTab(id) {
290
365
  body: JSON.stringify({ tabId: id })
291
366
  }).catch(function() {});
292
367
  } catch {}
368
+ closingTab._userAborted = true;
293
369
  if (closingTab._abortController) { closingTab._abortController.abort(); closingTab._abortController = null; }
294
370
  closingTab._sending = false;
295
371
  closingTab._queue = [];
@@ -406,7 +482,7 @@ function ccUpdateSessionIndicator() {
406
482
  }
407
483
  }
408
484
 
409
- function ccAddMessage(role, html, skipSave, targetTabId) {
485
+ function ccAddMessage(role, html, skipSave, targetTabId, meta) {
410
486
  var isUser = role === 'user';
411
487
  var isSystem = role === 'system';
412
488
  var isAction = role === 'action';
@@ -418,6 +494,7 @@ function ccAddMessage(role, html, skipSave, targetTabId) {
418
494
  var el = document.getElementById('cc-messages');
419
495
  var div = document.createElement('div');
420
496
  div.className = isAssistant ? 'cc-msg-assistant' : '';
497
+ if (meta && meta.retryId) div.setAttribute('data-cc-retry-id', meta.retryId);
421
498
  div.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;' +
422
499
  (isUser ? 'background:var(--blue);color:#fff;align-self:flex-end' : isSystem ? 'align-self:center;max-width:100%' : isAction ? 'align-self:flex-start;padding:2px 0' : 'background:var(--surface2);color:var(--text);align-self:flex-start;border:1px solid var(--border);position:relative');
423
500
  div.innerHTML = (isAssistant && !html.includes('color:var(--red)') && !html.includes('cc-queued-pill') ? llmCopyBtn() : '') + html;
@@ -428,7 +505,9 @@ function ccAddMessage(role, html, skipSave, targetTabId) {
428
505
  if (!skipSave) {
429
506
  var tab = targetTab;
430
507
  if (tab) {
431
- tab.messages.push({ role: role, html: html });
508
+ var msg = { role: role, html: html };
509
+ if (meta && meta.retryId) msg._retryId = meta.retryId;
510
+ tab.messages.push(msg);
432
511
  // Auto-title from first user message
433
512
  if (role === 'user' && tab.title === 'New chat') {
434
513
  var tmp = document.createElement('div');
@@ -519,13 +598,14 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
519
598
  activeTab._sending = true;
520
599
  activeTab._sendStartedAt = Date.now();
521
600
  activeTab._abortController = new AbortController();
601
+ activeTab._userAborted = false;
522
602
  _ccSending = true;
523
603
  ccRenderTabBar();
524
604
  var _wasAborted = false;
525
605
  try { localStorage.setItem('cc-sending', JSON.stringify({ sending: true, startedAt: Date.now() })); } catch {}
526
606
 
527
607
  // Scoped helper — always targets the originating tab, even if user switches tabs
528
- function addMsg(role, html, skipSave) { ccAddMessage(role, html, skipSave, activeTabId); }
608
+ function addMsg(role, html, skipSave, meta) { ccAddMessage(role, html, skipSave, activeTabId, meta); }
529
609
 
530
610
  if (!skipUserMsg) addMsg('user', escHtml(message));
531
611
 
@@ -615,9 +695,11 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
615
695
  var seconds = Math.round((Date.now() - ccStartTime) / 1000);
616
696
  return '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + label.replace('{seconds}', seconds) + '</div>';
617
697
  }
618
- function _ccRetryControls(extraHtml, showReload) {
698
+ function _ccRetryControls(retryRequest, extraHtml, showReload) {
699
+ var retryTabId = retryRequest && retryRequest.tabId ? retryRequest.tabId : activeTabId;
700
+ var retryId = retryRequest && retryRequest.id ? retryRequest.id : '';
619
701
  return (extraHtml || '') +
620
- '<button onclick="ccRetryLast()" style="margin-top:6px;padding:4px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--blue);cursor:pointer;font-size:11px">Retry</button>' +
702
+ '<button onclick="ccRetryLast(' + _ccJsArg(retryTabId) + ',' + _ccJsArg(retryId) + ')" style="margin-top:6px;padding:4px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--blue);cursor:pointer;font-size:11px">Retry</button>' +
621
703
  (showReload ? ' <button onclick="location.reload()" style="margin-top:6px;padding:4px 12px;background:var(--orange);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px">Reload Page</button>' : '') +
622
704
  ' <button onclick="ccNewTab()" style="margin-top:6px;padding:4px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);cursor:pointer;font-size:11px">New Session</button>';
623
705
  }
@@ -748,11 +830,12 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
748
830
  if (!consume.interrupted) break;
749
831
  if (!consume.reconnectable || reconnectAttempts >= 2) {
750
832
  _cleanupStreamDiv();
751
- var streamEndedHint = '<div style="font-size:10px;color:var(--muted);margin-top:4px">The response stream ended before completion. Retry to continue from the last user message.</div>';
833
+ var streamEndedHint = '<div style="font-size:10px;color:var(--muted);margin-top:4px">The response stream ended before completion. Retry to resend the interrupted message.</div>';
834
+ var streamEndedRetry = _ccStoreRetryRequest(activeTab, activeTabId, message);
752
835
  if (streamedText) {
753
- addMsg('assistant', renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') + _ccRetryControls(streamEndedHint, false));
836
+ addMsg('assistant', renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') + _ccRetryControls(streamEndedRetry, streamEndedHint, false), false, { retryId: streamEndedRetry.id });
754
837
  } else {
755
- addMsg('assistant', '<span style="color:var(--red)">The response stream ended before completion.</span>' + _ccRetryControls(streamEndedHint, false));
838
+ addMsg('assistant', '<span style="color:var(--red)">The response stream ended before completion.</span>' + _ccRetryControls(streamEndedRetry, streamEndedHint, false), false, { retryId: streamEndedRetry.id });
756
839
  }
757
840
  break;
758
841
  }
@@ -762,8 +845,9 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
762
845
  var reconnectHint = reconnectHealth.restarted
763
846
  ? '<div style="font-size:10px;color:var(--muted);margin-top:4px">Dashboard restarted while this response was streaming. Reload the page to reconnect to the new instance.</div>'
764
847
  : '<div style="font-size:10px;color:var(--muted);margin-top:4px">The request stream was interrupted, but the dashboard is still reachable. Retry or start a new session.</div>';
848
+ var reconnectRetry = _ccStoreRetryRequest(activeTab, activeTabId, message);
765
849
  addMsg('assistant', (streamedText ? renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') : '') +
766
- _ccRetryControls(reconnectHint, reconnectHealth.restarted));
850
+ _ccRetryControls(reconnectRetry, reconnectHint, reconnectHealth.restarted), false, { retryId: reconnectRetry.id });
767
851
  break;
768
852
  }
769
853
  reconnectAttempts++;
@@ -776,7 +860,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
776
860
  }
777
861
  } catch (e) {
778
862
  _cleanupStreamDiv();
779
- if (e.name === 'AbortError') {
863
+ if (activeTab && activeTab._userAborted) {
780
864
  _wasAborted = true;
781
865
  if (streamedText) {
782
866
  addMsg('assistant', renderMd(streamedText) + _ccElapsedFooter('Stopped after {seconds}s'));
@@ -784,7 +868,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
784
868
  addMsg('assistant', '<span style="color:var(--red);font-size:11px">Stopped</span>');
785
869
  }
786
870
  } else {
787
- var isNetworkError = e.message === 'Failed to fetch' || e.message.includes('NetworkError');
871
+ var isNetworkError = _ccIsReconnectableStreamError(e);
788
872
  var dashboardHealth = isNetworkError ? await _ccDashboardHealth() : { reachable: false, restarted: false };
789
873
  var connectionHint = '';
790
874
  if (isNetworkError) {
@@ -794,12 +878,13 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
794
878
  ? '<div style="font-size:10px;color:var(--muted);margin-top:4px">The request stream was interrupted, but the dashboard is still reachable. Retry or start a new session.</div>'
795
879
  : '<div style="font-size:10px;color:var(--muted);margin-top:4px">Dashboard connection lost. Reload the page to reconnect.</div>';
796
880
  }
881
+ var errorRetry = _ccStoreRetryRequest(activeTab, activeTabId, message);
797
882
  addMsg('assistant', (streamedText ? renderMd(streamedText) + _ccElapsedFooter('Stream interrupted after {seconds}s') : '') +
798
883
  '<span style="color:var(--red)">Error: ' + escHtml(e.message) + '</span>' +
799
- _ccRetryControls(connectionHint, isNetworkError && (!dashboardHealth.reachable || dashboardHealth.restarted)));
884
+ _ccRetryControls(errorRetry, connectionHint, isNetworkError && (!dashboardHealth.reachable || dashboardHealth.restarted)), false, { retryId: errorRetry.id });
800
885
  }
801
886
  } finally {
802
- if (activeTab) { activeTab._sending = false; activeTab._abortController = null; activeTab._429retries = 0; delete activeTab._streamedText; delete activeTab._toolsUsed; delete activeTab._sendStartedAt; }
887
+ if (activeTab) { activeTab._sending = false; activeTab._abortController = null; activeTab._429retries = 0; delete activeTab._streamedText; delete activeTab._toolsUsed; delete activeTab._sendStartedAt; delete activeTab._userAborted; }
803
888
  _ccSending = (_ccTabs.some(function(t) { return t._sending; }));
804
889
  // Mark tab unread if response completed on a background tab or while drawer is closed
805
890
  if (activeTab && !_wasAborted && (activeTab.id !== _ccActiveTabId || !_ccOpen)) activeTab._unread = true;
@@ -813,28 +898,36 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
813
898
  return _wasAborted;
814
899
  }
815
900
 
816
- function ccRetryLast() {
817
- // Find the last user message and resend it
818
- var tab = _ccActiveTab();
901
+ function ccRetryLast(tabId, retryId) {
902
+ var tab = tabId ? _ccTabs.find(function(t) { return t.id === tabId; }) : _ccActiveTab();
819
903
  if (!tab) return;
820
- var last = tab.messages.filter(function(m) { return m.role === 'user'; }).pop();
821
- if (!last) return;
822
- // Extract text from the HTML (strip tags)
823
- var tmp = document.createElement('div');
824
- tmp.innerHTML = last.html;
825
- var text = tmp.textContent || tmp.innerText || '';
904
+ var retryRequest = _ccFindRetryRequest(tab, retryId);
905
+ var text = retryRequest ? retryRequest.message : '';
906
+ if (!text) {
907
+ var last = tab.messages.filter(function(m) { return m.role === 'user'; }).pop();
908
+ if (!last) return;
909
+ // Backward-compatible fallback for retry buttons rendered before retry context existed.
910
+ var tmp = document.createElement('div');
911
+ tmp.innerHTML = last.html;
912
+ text = tmp.textContent || tmp.innerText || '';
913
+ }
826
914
  if (!text.trim()) return;
827
- // Remove the error message (last assistant message)
828
- var el = document.getElementById('cc-messages');
829
- if (el?.lastElementChild) el.lastElementChild.remove();
830
- tab.messages = tab.messages.slice(0, -1); // remove error from history
915
+ var removed = _ccRemoveRetryMessage(tab, retryId);
916
+ if (!removed && tab.id === _ccActiveTabId) {
917
+ // Legacy fallback for old controls without retry ids: remove the visible error card.
918
+ var el = document.getElementById('cc-messages');
919
+ if (el?.lastElementChild) el.lastElementChild.remove();
920
+ tab.messages = tab.messages.slice(0, -1);
921
+ }
922
+ _ccForgetRetryRequest(tab, retryId);
923
+ ccSaveState();
831
924
  // Resend, then drain queue
832
- _ccDoSend(text.trim()).then(async function() {
833
- var retryTab = _ccActiveTab();
925
+ _ccDoSend(text.trim(), false, tab.id).then(async function() {
926
+ var retryTab = _ccTabs.find(function(t) { return t.id === tab.id; });
834
927
  while (retryTab && retryTab._queue && retryTab._queue.length > 0) {
835
928
  var next = retryTab._queue.shift();
836
929
  _renderQueueIndicator();
837
- await _ccDoSend(next);
930
+ await _ccDoSend(next, false, retryTab.id);
838
931
  }
839
932
  });
840
933
  }
@@ -54,7 +54,6 @@ function renderAgents(agents) {
54
54
  })()}
55
55
  ${a._blockingToolCall ? `<div style="margin-top:4px;padding:4px 8px;background:rgba(130,160,210,0.13);border:1px solid rgba(130,160,210,0.3);border-radius:4px;font-size:10px;color:var(--muted)">&#x23F3; Blocking tool call (${escapeHtml(a._blockingToolCall.tool)}) &mdash; silent ${Math.round(a._blockingToolCall.silentMs/60000)}min, timeout in ${Math.round(a._blockingToolCall.remainingMs/60000)}min</div>` : ''}
56
56
  ${a._warning ? `<div style="margin-top:4px;padding:4px 8px;background:rgba(210,153,34,0.15);border:1px solid rgba(210,153,34,0.3);border-radius:4px;font-size:10px;color:var(--yellow)">&#x26A0; ${escapeHtml(a._warning)}</div>` : ''}
57
- ${a._permissionMode && a._permissionMode !== 'bypassPermissions' && !a._warning ? `<div style="margin-top:4px;font-size:9px;color:var(--muted)">Permission mode: ${escapeHtml(a._permissionMode)}</div>` : ''}
58
57
  ${a.resultSummary ? `<div class="agent-result" title="${escapeHtml(a.resultSummary)}">${renderMd(a.resultSummary.slice(0, 200))}${a.resultSummary.length > 200 ? '...' : ''}</div>` : ''}
59
58
  </div>
60
59
  `).join('');
@@ -258,20 +258,12 @@ async function openSettings() {
258
258
  '</div>' +
259
259
 
260
260
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Claude CLI</h3>' +
261
- '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
261
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">' +
262
262
  settingsField('Output Format', 'set-outputFormat', c.outputFormat || 'stream-json', '', '') +
263
- settingsField('Allowed Tools', 'set-allowedTools', c.allowedTools || '', '', 'Tools agents can use without permission prompts. Add MCP tools here if using non-bypass mode (e.g. mcp__azure-ado__*)') +
263
+ settingsField('Allowed Tools', 'set-allowedTools', c.allowedTools || '', '', 'Claude allow-list passed through for compatibility; runtime bypass flags are adapter-owned.') +
264
264
  '</div>' +
265
- '<div style="margin-bottom:16px">' +
266
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Permission Mode <span style="opacity:0.6">(how agents handle tool approvals)</span></label>' +
267
- '<select id="set-permissionMode" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
268
- '<option value="bypassPermissions"' + ((c.permissionMode || 'bypassPermissions') === 'bypassPermissions' ? ' selected' : '') + '>Bypass (recommended) — agents run autonomously without permission prompts</option>' +
269
- '<option value="auto"' + ((c.permissionMode) === 'auto' ? ' selected' : '') + '>Auto — auto-approve safe tools, prompt for risky ones (agents may hang on risky tools)</option>' +
270
- '<option value="default"' + ((c.permissionMode) === 'default' ? ' selected' : '') + '>Default — prompt for every tool (agents WILL hang — not recommended)</option>' +
271
- '</select>' +
272
- '<div id="set-permissionMode-warn" style="font-size:9px;margin-top:4px;padding:4px 8px;border-radius:4px;' + ((c.permissionMode && c.permissionMode !== 'bypassPermissions') ? 'display:block;background:rgba(248,81,73,0.1);color:var(--red)' : 'display:none') + '">' +
273
- '\u26A0 Tools listed in Allowed Tools above are auto-approved even in non-bypass modes. Agents will only hang if they try to use a tool NOT on that list (e.g. MCP tools). In bypass mode, all tools are approved automatically.' +
274
- '</div>' +
265
+ '<div style="font-size:10px;color:var(--muted);margin-bottom:16px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--surface-subtle, rgba(130,160,210,0.08))">' +
266
+ 'Permission bypass is runtime-owned: Claude agents use <code>--dangerously-skip-permissions</code>; Copilot agents use <code>--autopilot --allow-all --no-ask-user</code>. There is no dashboard permission-mode setting.' +
275
267
  '</div>' +
276
268
 
277
269
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Agents</h3>' +
@@ -314,20 +306,6 @@ async function openSettings() {
314
306
  document.getElementById('modal-body').style.whiteSpace = '';
315
307
  document.getElementById('modal').classList.add('open');
316
308
 
317
- // Wire permission mode warning toggle
318
- var pmSelect = document.getElementById('set-permissionMode');
319
- if (pmSelect) pmSelect.addEventListener('change', function() {
320
- var warn = document.getElementById('set-permissionMode-warn');
321
- if (!warn) return;
322
- if (this.value !== 'bypassPermissions') {
323
- warn.style.display = 'block';
324
- warn.style.background = 'rgba(248,81,73,0.1)';
325
- warn.style.color = 'var(--red)';
326
- } else {
327
- warn.style.display = 'none';
328
- }
329
- });
330
-
331
309
  // ── Runtime fleet wiring (P-7a5c1f8e) ──────────────────────────────────────
332
310
  // 1. Load registered runtimes into the defaultCli + ccCli dropdowns.
333
311
  // 2. Load models for the selected defaultCli into the defaultModel input.
@@ -595,7 +573,6 @@ async function saveSettings() {
595
573
  const claudePayload = {
596
574
  outputFormat: document.getElementById('set-outputFormat').value,
597
575
  allowedTools: document.getElementById('set-allowedTools').value,
598
- permissionMode: document.getElementById('set-permissionMode').value,
599
576
  };
600
577
 
601
578
  const teamsPayload = {
package/dashboard.js CHANGED
@@ -5405,6 +5405,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5405
5405
  } catch (e) { return jsonReply(res, e.statusCode || 500, { error: e.message }); }
5406
5406
  }
5407
5407
 
5408
+ function settingsClaudeConfig(config) {
5409
+ const claude = { ...shared.DEFAULT_CLAUDE, ...(config.claude || {}) };
5410
+ delete claude.permissionMode;
5411
+ return claude;
5412
+ }
5413
+
5408
5414
  async function handleSettingsRead(req, res) {
5409
5415
  try {
5410
5416
  const config = queries.getConfig();
@@ -5414,7 +5420,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5414
5420
  if (engine.prPollCommentsEvery === undefined && engine.adoPollCommentsEvery !== undefined) engine.prPollCommentsEvery = engine.adoPollCommentsEvery;
5415
5421
  return jsonReply(res, 200, {
5416
5422
  engine,
5417
- claude: { ...shared.DEFAULT_CLAUDE, ...(config.claude || {}) },
5423
+ claude: settingsClaudeConfig(config),
5418
5424
  agents: config.agents || {},
5419
5425
  teams: { ...shared.ENGINE_DEFAULTS.teams, ...(config.teams || {}) },
5420
5426
  projects: (config.projects || []).map(p => ({
@@ -5437,6 +5443,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5437
5443
  if (!config.engine) config.engine = {};
5438
5444
  if (!config.claude) config.claude = {};
5439
5445
  if (!config.agents) config.agents = {};
5446
+ delete config.claude.permissionMode;
5440
5447
 
5441
5448
  const _clamped = [];
5442
5449
  const _engineModelDiscovery = require('./engine/model-discovery');
@@ -5607,10 +5614,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5607
5614
  for (const key of ['allowedTools', 'outputFormat']) {
5608
5615
  if (body.claude[key] !== undefined) config.claude[key] = String(body.claude[key]);
5609
5616
  }
5610
- if (body.claude.permissionMode !== undefined) {
5611
- const valid = ['bypassPermissions', 'auto', 'default'];
5612
- config.claude.permissionMode = valid.includes(body.claude.permissionMode) ? body.claude.permissionMode : 'bypassPermissions';
5613
- }
5614
5617
  }
5615
5618
 
5616
5619
  if (body.agents) {
@@ -250,10 +250,12 @@ Combines:
250
250
  ```bash
251
251
  claude -p --system-prompt-file <sysprompt-file> \
252
252
  --output-format stream-json --max-turns 100 --verbose \
253
- --permission-mode bypassPermissions
253
+ --dangerously-skip-permissions
254
254
  # Prompt text is piped via stdin (not passed as an arg).
255
255
  # Agent dispatches route through engine/spawn-agent.js; CC / doc-chat use a direct
256
256
  # spawn path in engine/llm.js that bypasses spawn-agent.js entirely.
257
+ # Permission bypass is runtime-owned: Copilot uses
258
+ # --autopilot --allow-all --no-ask-user instead.
257
259
  ```
258
260
 
259
261
  - Process runs in the worktree directory (or rootDir for reviews)
@@ -63,9 +63,9 @@ proc.stdin.end();
63
63
 
64
64
  Fix: switched to `--output-format stream-json` — streams events as they happen.
65
65
 
66
- ### Permission Mode
66
+ ### Permission Bypass
67
67
 
68
- Agents would hang waiting for permission prompts (invisible in headless mode). Added `--permission-mode bypassPermissions`.
68
+ Agents would hang waiting for permission prompts (invisible in headless mode). Modern Minions keeps bypass behavior inside runtime adapters: Claude emits `--dangerously-skip-permissions`; Copilot emits `--autopilot --allow-all --no-ask-user`. The legacy `config.claude.permissionMode` key is ignored.
69
69
 
70
70
  ### CLAUDECODE Environment Variable
71
71
 
@@ -85,7 +85,7 @@ When the engine restarts, the in-memory `activeProcesses` Map is lost. Active di
85
85
 
86
86
  **Dispatch 1773292681199** — Dallas, central work item, auto-route:
87
87
 
88
- 1. Engine spawns `node spawn-agent.js prompt.md sysprompt.md --output-format stream-json --verbose --permission-mode bypassPermissions`
88
+ 1. Engine spawns `node spawn-agent.js prompt.md sysprompt.md --runtime claude --output-format stream-json --verbose`
89
89
  2. spawn-agent.js resolves `cli.js`, spawns `node cli.js -p --system-prompt <content> ...`
90
90
  3. Prompt piped via stdin — no shell interpretation
91
91
  4. MCP servers connect (azure-ado, azure-kusto, mobile, DevBox)
@@ -22,5 +22,13 @@
22
22
  "reason": "These cadence settings gate both ADO and GitHub PR polling, so the ADO-prefixed names are misleading.",
23
23
  "locations": ["engine.js read-side fallback from config.engine.adoPollStatusEvery/adoPollCommentsEvery", "dashboard.js handleSettingsRead/handleSettingsUpdate alias fallback for old keys"],
24
24
  "cleanup": "Remove the adoPoll* alias fallback reads after existing configs have been migrated to prPollStatusEvery/prPollCommentsEvery."
25
+ },
26
+ {
27
+ "id": "config-claude-permission-mode",
28
+ "summary": "config.claude.permissionMode ignored; runtime adapters own permission bypass flags",
29
+ "deprecated": "2026-05-04",
30
+ "reason": "Claude and Copilot require different non-interactive bypass flags, so a shared Claude config field was misleading and no longer controls spawns.",
31
+ "locations": ["dashboard.js settings update strips config.claude.permissionMode", "dashboard/js/settings.js no longer renders a Permission Mode selector"],
32
+ "cleanup": "After old configs have been rewritten through settings, remove the deprecated-field preflight warning entry for permissionMode."
25
33
  }
26
34
  ]
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-04T03:30:25.499Z"
4
+ "cachedAt": "2026-05-04T05:14:26.307Z"
5
5
  }
package/engine/queries.js CHANGED
@@ -428,25 +428,11 @@ function getAgentStatus(agentId) {
428
428
  if (active._blockingToolCall) {
429
429
  result._blockingToolCall = active._blockingToolCall;
430
430
  }
431
- // Detect permission-waiting and in-flight tools: read only head+tail of live-output.log (max 2KB total)
431
+ // Detect in-flight tools: read only head+tail of live-output.log (max 2KB total)
432
432
  try {
433
433
  const liveLogPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
434
434
  const { head, tail } = readHeadTail(liveLogPath, 1024);
435
435
  if (head) {
436
- // Check init message (in head) for permission mode
437
- const initMatch = head.match(/"permissionMode"\s*:\s*"([^"]+)"/);
438
- if (initMatch && initMatch[1] !== 'bypassPermissions') {
439
- result._permissionMode = initMatch[1];
440
- }
441
- // Check if agent has been silent for >60s (use tail for recent activity)
442
- const lastLine = tail.trimEnd().split('\n').pop();
443
- if (lastLine && lastLine.includes('"type":"assistant"') && lastLine.includes('"tool_use"')) {
444
- const liveStat = fs.statSync(liveLogPath);
445
- const silentMs = Date.now() - liveStat.mtimeMs;
446
- if (silentMs > 60000 && result._permissionMode) {
447
- result._warning = 'Possibly waiting for permission approval — agent is not in bypass mode';
448
- }
449
- }
450
436
  // Detect in-flight tool calls (task_started with no task_notification)
451
437
  const inFlight = detectInFlightTool(tail);
452
438
  if (inFlight && inFlight.description) {
@@ -570,7 +556,6 @@ function getAgents(config) {
570
556
  completed_at: s.completed_at || null,
571
557
  _blockingToolCall: s._blockingToolCall || null,
572
558
  _warning: s._warning || null,
573
- _permissionMode: s._permissionMode || null,
574
559
  chartered, inboxCount: inboxFiles.length + steeringInboxFiles.length
575
560
  };
576
561
  });
package/engine/shared.js CHANGED
@@ -851,7 +851,7 @@ const ENGINE_DEFAULTS = {
851
851
  // Backward-compat: keep `engine.claude.*` field family deprecation tracker. Listed here so preflight
852
852
  // knows which subkeys to flag as deprecated. Do not consume `claude.*` in new code — use the runtime
853
853
  // adapter system (engine/runtimes/) and the resolveAgent*/resolveCc* helpers instead.
854
- _deprecatedConfigClaudeFields: ['binary', 'outputFormat', 'allowedTools', 'maxTurns', 'effort', 'budgetCap'],
854
+ _deprecatedConfigClaudeFields: ['binary', 'outputFormat', 'allowedTools', 'permissionMode', 'maxTurns', 'effort', 'budgetCap'],
855
855
  // Teams integration — config.teams shape: { enabled, appId, appPassword, certPath, privateKeyPath, tenantId, notifyEvents, ccMirror, inboxPollInterval }
856
856
  // Auth modes: (1) appId + appPassword (client secret), or (2) appId + certPath + privateKeyPath + tenantId (certificate)
857
857
  teams: {
package/minions.js CHANGED
@@ -470,6 +470,7 @@ async function initMinions({ skipScan = false, scanRoot, scanDepth } = {}) {
470
470
  if (config.engine[k] === undefined) config.engine[k] = v;
471
471
  }
472
472
  if (!config.claude) config.claude = {};
473
+ delete config.claude.permissionMode;
473
474
  for (const [k, v] of Object.entries(DEFAULT_CLAUDE)) {
474
475
  if (config.claude[k] === undefined) config.claude[k] = v;
475
476
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1693",
3
+ "version": "0.1.1695",
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"