@yemi33/minions 0.1.2049 → 0.1.2050

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.
@@ -7,6 +7,10 @@ function closeModal() {
7
7
  clearModalBackStack();
8
8
  // Hide Q&A section (only shown for document modals)
9
9
  document.getElementById('modal-qa').style.display = 'none';
10
+ // Remove settings-body marker so the next modal opens with the default
11
+ // monospace/pre-wrap modal-body styling rather than inheriting the Segoe-UI
12
+ // tabbed-settings overrides (W-mpmwxkcn000646cc).
13
+ document.getElementById('modal-body').classList.remove('settings-body');
10
14
  // Remove settings buttons if present
11
15
  const settingsBtn = document.getElementById('modal-settings-save');
12
16
  if (settingsBtn) settingsBtn.remove();
@@ -89,6 +89,11 @@ const RENDER_VERSIONS = {
89
89
  pipelines: 1,
90
90
  pinned: 1,
91
91
  kbPayload: 2,
92
+ // settings is a modal-driven section, not a tick-driven render — but the
93
+ // cache-bust entry exists so future tick-driven code (or feature-flag-aware
94
+ // renderers) can call `_changed('settings', …)` and bump on visual revamps.
95
+ // Bumped to 2 by W-mpmwxkcn000646cc (left-rail tabbed Settings layout).
96
+ settings: 2,
92
97
  };
93
98
  const _sectionCache = {};
94
99
  const _lastValueByKey = {};
@@ -25,6 +25,9 @@ async function openSettings() {
25
25
  document.getElementById('modal-title').textContent = 'Settings';
26
26
  document.getElementById('modal-body').innerHTML = '<p style="color:var(--muted)">Loading...</p>';
27
27
  document.getElementById('modal').classList.add('open');
28
+ // Settings needs more horizontal room for the left rail + content split.
29
+ const modalEl = document.querySelector('#modal .modal');
30
+ if (modalEl) modalEl.classList.add('modal-wide');
28
31
 
29
32
  _settingsData = null;
30
33
  let data;
@@ -70,141 +73,24 @@ async function openSettings() {
70
73
  '</tr>';
71
74
  }).join('');
72
75
 
73
- const html = '<div style="padding:8px 0;max-height:70vh;overflow-y:auto">' +
74
-
75
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Engine</h3>' +
76
- '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
77
- settingsField('Tick Interval', 'set-tickInterval', e.tickInterval || 60000, 'ms', 'How often the engine runs discovery + dispatch') +
78
- settingsField('Max Concurrent Agents', 'set-maxConcurrent', e.maxConcurrent || 5, '', 'Max agents working simultaneously') +
79
- settingsField('Consolidation Threshold', 'set-inboxConsolidateThreshold', e.inboxConsolidateThreshold || 5, 'notes', 'Inbox notes before auto-consolidation') +
80
- settingsField('Agent Timeout', 'set-agentTimeout', e.agentTimeout || 18000000, 'ms', 'Kill agent after this duration') +
81
- settingsField('Max Turns', 'set-maxTurns', e.maxTurns || 100, '', 'Claude CLI --max-turns per agent') +
82
- settingsField('Heartbeat Timeout', 'set-heartbeatTimeout', e.heartbeatTimeout || 300000, 'ms', 'No output = dead after this') +
83
- settingsField('Worktree Create Timeout', 'set-worktreeCreateTimeout', e.worktreeCreateTimeout || 300000, 'ms', 'Timeout for git worktree add (increase for large repos/Windows)') +
84
- settingsField('Worktree Create Retries', 'set-worktreeCreateRetries', e.worktreeCreateRetries || 1, '', 'Retry count for transient worktree add failures (0-3)') +
85
- settingsField('Worktree Root', 'set-worktreeRoot', e.worktreeRoot || '../worktrees', '', 'Relative or absolute path for git worktrees; on Windows prefer a short path like C:\\wt') +
86
- settingsField('Idle Alert', 'set-idleAlertMinutes', e.idleAlertMinutes || 15, 'min', 'Alert after agent idle this long') +
87
- settingsField('Shutdown Timeout', 'set-shutdownTimeout', e.shutdownTimeout || 300000, 'ms', 'Max wait for agents during graceful shutdown') +
88
- settingsField('Restart Grace Period', 'set-restartGracePeriod', e.restartGracePeriod || 1200000, 'ms', 'Grace period before orphan detection on restart') +
89
- settingsField('Meeting Round Timeout', 'set-meetingRoundTimeout', e.meetingRoundTimeout || 900000, 'ms', 'Auto-advance meeting round after this') +
90
- settingsField('Operator login (used in branch names)', 'set-operatorLogin', e.operatorLogin || '', '', 'Override the human operator login used in user/<loginname>/<wi-id>-<slug> branches. Empty = auto-resolve via gh / git email / OS username (currently resolves to: ' + (e._resolvedOperatorLogin || 'unknown') + ')') +
91
- settingsField('Status WorkItems Retention', 'set-statusWorkItemsRetentionDays', e.statusWorkItemsRetentionDays ?? 7, 'days', 'Trim done/failed/cancelled work items older than N days from the /api/status workItems slice (active items are always shipped). Cuts SPA payload from ~3MB to <500KB. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
92
- settingsField('Status Meetings Retention', 'set-statusMeetingsRetentionDays', e.statusMeetingsRetentionDays ?? 7, 'days', 'Trim completed/archived meetings older than N days from the /api/status meetings slice (active meetings are always shipped). Cuts SPA payload from ~4.3MB to <500KB. Detail modal still fetches full transcripts via /api/meetings/:id. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
93
- '</div>' +
94
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Automation</h3>' +
95
- '<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:16px">' +
96
- settingsToggle('Auto-approve Plans', 'set-autoApprovePlans', !!e.autoApprovePlans, 'PRDs are approved automatically without human review') +
97
- settingsToggle('Eval Loop', 'set-evalLoop', e.evalLoop !== false, 'Auto-review implementations and iterate fix cycles until pass') +
98
- settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks') +
99
- settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
100
- settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard)') +
101
- settingsToggle('Auto-consolidate Memory', 'set-autoConsolidateMemory', !!e.autoConsolidateMemory, 'Periodically spawn the KB sweep (dedup + compress + normalize knowledge/) from the engine tick on a 4h cadence. Inbox→notes consolidation already runs every tick (gated by the Consolidation Threshold above); this toggle controls only the KB sweep that was previously dashboard-button-only.') +
102
- settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in)') +
103
- settingsToggle('CC Worker Pool', 'set-ccUseWorkerPool', (e.ccUseWorkerPool === undefined ? ((e.ccCli || e.defaultCli) === 'copilot') : !!e.ccUseWorkerPool), 'Route Command Center / doc-chat through a persistent copilot --acp worker per tab instead of spawning a fresh CLI per turn. Copilot-only (Agent Client Protocol transport); Claude does not implement ACP, so this toggle has no effect when CC runtime is Claude. Default ON for copilot (cold-spawn ~20s on Windows); forced OFF for non-copilot CC runtimes regardless of this toggle.') +
104
- '</div>' +
105
-
106
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">PR Polling &amp; Dispatch Gates</h3>' +
107
- '<div style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:16px">' +
108
- '<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:10px">' +
109
- settingsToggle('ADO Polling', 'set-adoPollEnabled', e.adoPollEnabled !== false, 'Keeps ADO PR build results, votes, and comments fresh each tick; ADO PR dispatch gates are inert when this is off') +
110
- settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Keeps GitHub PR build results, votes, and comments fresh each tick; GitHub PR dispatch gates are inert when this is off') +
111
- '</div>' +
112
- '<div style="margin-top:10px;padding-top:10px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:4px">' +
113
- settingsToggle('Auto-apply review vote to PR', 'set-autoApplyReviewVote', !!e.autoApplyReviewVote, 'When ON, Minions review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote on ADO/GitHub. When OFF (default), verdicts are informational only and the human casts the final vote.') +
114
- settingsToggle('Auto-fix Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Shared dispatch gate: auto-fix agent when a PR build fails; also requires that PR provider polling is enabled') +
115
- settingsToggle('Auto-fix Conflicts', 'set-autoFixConflicts', e.autoFixConflicts !== false, 'Shared dispatch gate: auto-fix agent when a PR merge conflict is detected; also requires that PR provider polling is enabled') +
116
- settingsToggle('Auto-review PRs', 'set-autoReviewPrs', e.autoReviewPrs !== false, 'Shared dispatch gate: review agent for newly opened agent PRs; also requires that PR provider polling is enabled') +
117
- settingsToggle('Auto-re-review PRs', 'set-autoReReviewPrs', e.autoReReviewPrs !== false, 'Shared dispatch gate: review agent after a fix push is awaiting re-review; also requires that PR provider polling is enabled') +
118
- settingsToggle('Auto-fix Review Feedback', 'set-autoFixReviewFeedback', e.autoFixReviewFeedback !== false, 'Shared dispatch gate: fix agent for minions changes-requested verdicts; also requires that PR provider polling is enabled') +
119
- settingsToggle('Auto-fix Human Comments', 'set-autoFixHumanComments', e.autoFixHumanComments !== false, 'Shared dispatch gate: fix agent for actionable human PR comments; also requires that PR provider polling is enabled') +
120
- '</div>' +
121
- '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px">' +
122
- settingsField('PR Status Poll Frequency', 'set-prPollStatusEvery', e.prPollStatusEvery ?? 12, 'ticks', 'Poll PR build/review/merge status every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
123
- settingsField('PR Comments Poll Frequency', 'set-prPollCommentsEvery', e.prPollCommentsEvery ?? 12, 'ticks', 'Poll PR human comments every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
124
- '</div>' +
125
- '</div>' +
126
-
127
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Limits &amp; Thresholds</h3>' +
128
- '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
129
- settingsField('Eval Max Cost', 'set-evalMaxCost', e.evalMaxCost === null || e.evalMaxCost === undefined ? '' : e.evalMaxCost, '$', 'USD ceiling per work item across all eval iterations (blank = no limit)') +
130
- settingsField('Agent Busy Reassign', 'set-agentBusyReassignMs', e.agentBusyReassignMs || 600000, 'ms', 'Reassign work to another agent after it waits this long on a busy agent') +
131
- settingsField('Max Retries Per Agent', 'set-maxRetriesPerAgent', e.maxRetriesPerAgent ?? 2, '', 'After the same agent fails the same work item this many times, the next retry reassigns to a different eligible agent (consults routing.md + availability). Falls back to the same agent only when no alternate is available. Counted separately from total maxRetries (which still caps overall retries).') +
132
- settingsField('Version Check Interval', 'set-versionCheckInterval', e.versionCheckInterval || 3600000, 'ms', 'How often to check npm for updates (default: 1 hour)') +
133
- settingsField('Ignored Comment Authors', 'set-ignoredCommentAuthors', (e.ignoredCommentAuthors || []).join(', '), '', 'Comma-separated usernames — comments auto-closed, never trigger fixes') +
134
- '</div>' +
135
-
136
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Projects</h3>' +
137
- '<div style="display:flex;flex-direction:column;gap:12px;margin-bottom:16px">' +
138
- (data.projects || []).map(function(p) {
139
- // Cross-reference live /api/status to pull in the auto-detected
140
- // remoteDefaultBranch (read-only) — settings already has localPath +
141
- // mainBranch from /api/settings, but origin/HEAD is probe-derived and
142
- // lives in /api/status only. W-mpg3whgp000d09ec / #2732.
143
- var liveProj = null;
144
- try {
145
- if (window._lastStatus && Array.isArray(window._lastStatus.projects)) {
146
- liveProj = window._lastStatus.projects.find(function(x) { return x.name === p.name; }) || null;
147
- }
148
- } catch (e) { liveProj = null; }
149
- var remoteDefault = (liveProj && liveProj.remoteDefaultBranch) || '';
150
- var mismatch = !!(liveProj && liveProj.branchMismatch);
151
- var localBranch = (liveProj && liveProj.gitBranch) || '';
152
- var driftNote = mismatch
153
- ? '<div style="font-size:10px;color:var(--yellow);margin-top:4px">⚠ Configured main (<code>' + escHtml(p.mainBranch || '') + '</code>) differs from origin/HEAD (<code>' + escHtml(remoteDefault) + '</code>) — config is likely stale.</div>'
154
- : '';
155
- var pathRow = p.localPath
156
- ? '<div style="font-size:10px;color:var(--muted);margin-bottom:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">' + escHtml(p.localPath) + '</div>'
157
- : '';
158
- var branchGrid = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:6px">' +
159
- settingsField('Configured main branch', 'set-mainBranch-' + p.name, p.mainBranch || '', '', 'Used by branch-naming + dependency-merge to identify mainline. Empty = auto-detect from origin/HEAD.') +
160
- '<div>' +
161
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Remote default <span style="opacity:0.6">(read-only)</span></label>' +
162
- '<input id="set-remoteDefaultBranch-' + p.name + '" value="' + escHtml(remoteDefault || '(unset)') + '" readonly style="width:100%;padding:4px 6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">' +
163
- '<div style="font-size:9px;color:var(--muted);margin-top:1px">Parsed from <code>git symbolic-ref refs/remotes/origin/HEAD</code>' + (localBranch ? '. Local HEAD: <code>' + escHtml(localBranch) + '</code>' : '') + '.</div>' +
164
- '</div>' +
165
- '</div>';
166
- return '<div data-settings-project="' + escHtml(p.name) + '" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px">' +
167
- '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
168
- '<div style="font-size:12px;font-weight:600">' + escHtml(p.name) + '</div>' +
169
- '<button onclick="MinionsSettings.removeProject(\'' + escHtml(p.name) + '\')" style="font-size:9px;padding:2px 8px;background:transparent;color:var(--red);border:1px solid var(--red);border-radius:3px;cursor:pointer">Remove</button>' +
170
- '</div>' +
171
- pathRow +
172
- branchGrid +
173
- driftNote +
174
- '<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
175
- settingsToggle('Discover from PRs', 'set-ws-prs-' + p.name, p.workSources.pullRequests.enabled, 'Discovery gate: scan repo for open PRs and surface them as review tasks. Independent of ADO/GitHub polling — does not affect already-tracked PRs.') +
176
- settingsToggle('Discover from Work Items', 'set-ws-wi-' + p.name, p.workSources.workItems.enabled, 'Auto-discover work from ADO/GitHub work items') +
177
- '</div></div>';
178
- }).join('') +
179
- '</div>' +
180
-
181
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Max Turns by Task Type</h3>' +
182
- '<div style="font-size:10px;color:var(--muted);margin-bottom:6px">How many tool-use turns each task type gets before forced stop. Blank = built-in default.</div>' +
183
- '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:16px">' +
184
- settingsField('Explore', 'set-mt-explore', (e.maxTurnsByType || {}).explore || '', '', 'Default: 30') +
185
- settingsField('Ask', 'set-mt-ask', (e.maxTurnsByType || {}).ask || '', '', 'Default: 20') +
186
- settingsField('Review', 'set-mt-review', (e.maxTurnsByType || {}).review || '', '', 'Default: 30') +
187
- settingsField('Implement', 'set-mt-implement', (e.maxTurnsByType || {}).implement || '', '', 'Default: 75') +
188
- settingsField('Fix', 'set-mt-fix', (e.maxTurnsByType || {}).fix || '', '', 'Default: 75') +
189
- settingsField('Test', 'set-mt-test', (e.maxTurnsByType || {}).test || '', '', 'Default: 50') +
190
- settingsField('Verify', 'set-mt-verify', (e.maxTurnsByType || {}).verify || '', '', 'Default: 100') +
191
- settingsField('Plan', 'set-mt-plan', (e.maxTurnsByType || {}).plan || '', '', 'Default: 30') +
192
- settingsField('Decompose', 'set-mt-decompose', (e.maxTurnsByType || {}).decompose || '', '', 'Default: 15') +
193
- '</div>' +
194
-
195
- // ── Runtime (P-7a5c1f8e) — unified fleet runtime + CC overrides + advanced ──
196
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Runtime</h3>' +
76
+ // ── Section bodies every original control is grouped under one of these
77
+ // tabs. Top of the rail is Runtime + Auto-fix (Caleb's #1 feedback was that
78
+ // these were buried under generic Engine headers). The disclosure formerly
79
+ // wrapping the 8 advanced runtime toggles was split into Copilot Tuning and
80
+ // Claude Tuning tabs so each runtime's knobs live behind its own rail entry.
81
+ const paneRuntime =
82
+ '<h3>Runtime &amp; Models</h3>' +
83
+ '<div class="settings-pane-sub">Single source of truth for which CLI runtime + model the fleet spawns. Per-agent overrides live in the Agents table below.</div>' +
197
84
  '<div id="set-runtime-section" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:16px">' +
198
- '<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>' +
199
85
  '<div style="display:grid;grid-template-columns:1fr 2fr;gap:8px;margin-bottom:8px">' +
200
- '<div>' +
86
+ '<div data-search="default cli fleet runtime">' +
201
87
  '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Default CLI</label>' +
202
88
  '<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">' +
203
89
  '<option value="">Loading…</option>' +
204
90
  '</select>' +
205
91
  '<div style="font-size:9px;color:var(--muted);margin-top:1px">Fleet-wide runtime — registered adapters from <code>/api/runtimes</code></div>' +
206
92
  '</div>' +
207
- '<div>' +
93
+ '<div data-search="default model fleet">' +
208
94
  '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Default Model</label>' +
209
95
  '<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>' +
210
96
  '<div style="font-size:9px;color:var(--muted);margin-top:1px">Empty = let the runtime pick its own default</div>' +
@@ -216,14 +102,14 @@ async function openSettings() {
216
102
  '<span style="font-size:9px;color:var(--muted)">(Command Center + doc-chat use the fleet defaults unless overridden)</span>' +
217
103
  '</summary>' +
218
104
  '<div style="display:grid;grid-template-columns:1fr 2fr 1fr;gap:8px;margin-top:8px">' +
219
- '<div>' +
105
+ '<div data-search="cc cli command center runtime">' +
220
106
  '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">CC CLI</label>' +
221
107
  '<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">' +
222
108
  '<option value="">Loading…</option>' +
223
109
  '</select>' +
224
110
  '<div style="font-size:9px;color:var(--muted);margin-top:1px">Empty = inherit Default CLI</div>' +
225
111
  '</div>' +
226
- '<div>' +
112
+ '<div data-search="cc model command center">' +
227
113
  '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">CC Model</label>' +
228
114
  // Wrap the input in a dedicated container so loadModelsForRuntime
229
115
  // (which does `wrap.innerHTML = …` on the input's parent) only
@@ -233,7 +119,7 @@ async function openSettings() {
233
119
  '<div id="set-ccModel-wrap"><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"></div>' +
234
120
  '<div style="font-size:9px;color:var(--muted);margin-top:1px">Empty = inherit Default Model</div>' +
235
121
  '</div>' +
236
- '<div>' +
122
+ '<div data-search="cc effort reasoning">' +
237
123
  '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Effort</label>' +
238
124
  '<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">' +
239
125
  '<option value=""' + (!e.ccEffort ? ' selected' : '') + '>Default</option>' +
@@ -245,63 +131,193 @@ async function openSettings() {
245
131
  '</div>' +
246
132
  '</div>' +
247
133
  '</details>' +
248
- // Advanced runtime settings — collapsed by default
249
- '<details id="set-runtime-advanced-details" style="margin-top:8px;border-top:1px solid var(--border);padding-top:8px">' +
250
- '<summary style="cursor:pointer;font-size:11px;color:var(--text);user-select:none">Advanced runtime settings ' +
251
- '<span style="font-size:9px;color:var(--muted)">(per-runtime feature flags)</span>' +
252
- '</summary>' +
253
- '<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
254
- settingsToggle('Claude bare mode', 'set-claudeBareMode', !!e.claudeBareMode, '--bare suppresses CLAUDE.md auto-discovery; pair with explicit ccSystemPrompt or context will be lost') +
255
- '</div>' +
256
- '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:8px">' +
257
- settingsField('Claude fallback model', 'set-claudeFallbackModel', e.claudeFallbackModel || '', 'e.g. sonnet', 'Passed via Claude --fallback-model. The Claude CLI swaps to this only on rate-limit (429). Used together with engine.copilotFallbackModel for the engine-level MODEL_UNAVAILABLE (overloaded/503) retry — see CLAUDE.md.') +
258
- settingsField('Copilot fallback model', 'set-copilotFallbackModel', e.copilotFallbackModel || '', 'e.g. gpt-5.4', 'Copilot has no --fallback-model flag. On a MODEL_UNAVAILABLE (overloaded/503) retry, the engine OVERRIDES --model with this value (Copilot only).') +
259
- 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.') +
260
- '</div>' +
261
- '<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
262
- // Tooltip on copilotDisableBuiltinMcps MUST warn about the split-brain risk
263
- settingsToggle('Copilot: disable built-in MCPs', 'set-copilotDisableBuiltinMcps', e.copilotDisableBuiltinMcps !== false,
264
- '⚠ 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.') +
265
- settingsToggle('Copilot: suppress AGENTS.md', 'set-copilotSuppressAgentsMd', e.copilotSuppressAgentsMd !== false, '--no-custom-instructions: stops AGENTS.md auto-load from fighting Minions playbook prompts') +
266
- settingsToggle('Copilot: reasoning summaries', 'set-copilotReasoningSummaries', !!e.copilotReasoningSummaries, '--enable-reasoning-summaries (Anthropic-family models only)') +
267
- settingsToggle('Disable model discovery', 'set-disableModelDiscovery', !!e.disableModelDiscovery, 'Skip /api/runtimes/<name>/models REST calls fleet-wide. Settings UI falls back to free-text.') +
268
- '</div>' +
269
- '<div style="display:grid;grid-template-columns:1fr 3fr;gap:8px;margin-top:8px">' +
270
- '<div>' +
271
- '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Copilot stream</label>' +
272
- '<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">' +
273
- '<option value="on"' + ((e.copilotStreamMode || 'on') === 'on' ? ' selected' : '') + '>on (incremental)</option>' +
274
- '<option value="off"' + (e.copilotStreamMode === 'off' ? ' selected' : '') + '>off (batched)</option>' +
275
- '</select>' +
276
- '</div>' +
277
- '</div>' +
278
- '</details>' +
279
- '</div>' +
280
-
281
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Claude CLI</h3>' +
282
- '<div style="display:grid;grid-template-columns:1fr;gap:8px;margin-bottom:8px">' +
283
- settingsField('Allowed Tools', 'set-allowedTools', c.allowedTools || '', '', 'Claude allow-list passed through for compatibility; runtime bypass flags are adapter-owned.') +
284
- '</div>' +
285
- '<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))">' +
286
- '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.' +
287
134
  '</div>' +
288
-
289
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Agents</h3>' +
290
- '<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>' +
135
+ '<h4>Agents</h4>' +
136
+ '<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. Per-agent monthly budget overrides the fleet ceiling.</div>' +
291
137
  '<table style="width:100%;border-collapse:collapse;margin-bottom:16px;font-size:11px">' +
292
138
  '<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>' +
293
139
  agentRows +
294
- '</table>' +
140
+ '</table>';
141
+
142
+ const paneAutoFix =
143
+ '<h3>Auto-fix &amp; Review Loop</h3>' +
144
+ '<div class="settings-pane-sub">Dispatch gates that decide when an agent is auto-spawned in response to PR build failures, merge conflicts, review verdicts, and human comments. All require the matching provider polling toggle (Polling tab).</div>' +
145
+ '<div class="settings-stack" style="margin-bottom:16px">' +
146
+ settingsToggle('Auto-fix Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Shared dispatch gate: auto-fix agent when a PR build fails; also requires that PR provider polling is enabled') +
147
+ settingsToggle('Auto-fix Conflicts', 'set-autoFixConflicts', e.autoFixConflicts !== false, 'Shared dispatch gate: auto-fix agent when a PR merge conflict is detected; also requires that PR provider polling is enabled') +
148
+ settingsToggle('Auto-review PRs', 'set-autoReviewPrs', e.autoReviewPrs !== false, 'Shared dispatch gate: review agent for newly opened agent PRs; also requires that PR provider polling is enabled') +
149
+ settingsToggle('Auto-re-review PRs', 'set-autoReReviewPrs', e.autoReReviewPrs !== false, 'Shared dispatch gate: review agent after a fix push is awaiting re-review; also requires that PR provider polling is enabled') +
150
+ settingsToggle('Auto-fix Review Feedback', 'set-autoFixReviewFeedback', e.autoFixReviewFeedback !== false, 'Shared dispatch gate: fix agent for minions changes-requested verdicts; also requires that PR provider polling is enabled') +
151
+ settingsToggle('Auto-fix Human Comments', 'set-autoFixHumanComments', e.autoFixHumanComments !== false, 'Shared dispatch gate: fix agent for actionable human PR comments; also requires that PR provider polling is enabled') +
152
+ settingsToggle('Auto-apply review vote to PR', 'set-autoApplyReviewVote', !!e.autoApplyReviewVote, 'When ON, Minions review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote on ADO/GitHub. When OFF (default), verdicts are informational only and the human casts the final vote.') +
153
+ settingsToggle('Eval Loop', 'set-evalLoop', e.evalLoop !== false, 'Auto-review implementations and iterate fix cycles until pass') +
154
+ settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks') +
155
+ settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in)') +
156
+ settingsToggle('Auto-approve Plans', 'set-autoApprovePlans', !!e.autoApprovePlans, 'PRDs are approved automatically without human review') +
157
+ settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard)') +
158
+ settingsToggle('Auto-consolidate Memory', 'set-autoConsolidateMemory', !!e.autoConsolidateMemory, 'Periodically spawn the KB sweep (dedup + compress + normalize knowledge/) from the engine tick on a 4h cadence. Inbox→notes consolidation already runs every tick (gated by the Consolidation Threshold above); this toggle controls only the KB sweep that was previously dashboard-button-only.') +
159
+ '</div>' +
160
+ '<div class="settings-grid-2">' +
161
+ settingsField('Eval Max Cost', 'set-evalMaxCost', e.evalMaxCost === null || e.evalMaxCost === undefined ? '' : e.evalMaxCost, '$', 'USD ceiling per work item across all eval iterations (blank = no limit)') +
162
+ '</div>';
163
+
164
+ const projectsHtml = (data.projects || []).map(function(p) {
165
+ // Cross-reference live /api/status to pull in the auto-detected
166
+ // remoteDefaultBranch (read-only) — settings already has localPath +
167
+ // mainBranch from /api/settings, but origin/HEAD is probe-derived and
168
+ // lives in /api/status only. W-mpg3whgp000d09ec / #2732.
169
+ var liveProj = null;
170
+ try {
171
+ if (window._lastStatus && Array.isArray(window._lastStatus.projects)) {
172
+ liveProj = window._lastStatus.projects.find(function(x) { return x.name === p.name; }) || null;
173
+ }
174
+ } catch (e) { liveProj = null; }
175
+ var remoteDefault = (liveProj && liveProj.remoteDefaultBranch) || '';
176
+ var mismatch = !!(liveProj && liveProj.branchMismatch);
177
+ var localBranch = (liveProj && liveProj.gitBranch) || '';
178
+ var driftNote = mismatch
179
+ ? '<div style="font-size:10px;color:var(--yellow);margin-top:4px">⚠ Configured main (<code>' + escHtml(p.mainBranch || '') + '</code>) differs from origin/HEAD (<code>' + escHtml(remoteDefault) + '</code>) — config is likely stale.</div>'
180
+ : '';
181
+ var pathRow = p.localPath
182
+ ? '<div style="font-size:10px;color:var(--muted);margin-bottom:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">' + escHtml(p.localPath) + '</div>'
183
+ : '';
184
+ var branchGrid = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:6px">' +
185
+ settingsField('Configured main branch', 'set-mainBranch-' + p.name, p.mainBranch || '', '', 'Used by branch-naming + dependency-merge to identify mainline. Empty = auto-detect from origin/HEAD.') +
186
+ '<div>' +
187
+ '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Remote default <span style="opacity:0.6">(read-only)</span></label>' +
188
+ '<input id="set-remoteDefaultBranch-' + p.name + '" value="' + escHtml(remoteDefault || '(unset)') + '" readonly style="width:100%;padding:4px 6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--muted);font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">' +
189
+ '<div style="font-size:9px;color:var(--muted);margin-top:1px">Parsed from <code>git symbolic-ref refs/remotes/origin/HEAD</code>' + (localBranch ? '. Local HEAD: <code>' + escHtml(localBranch) + '</code>' : '') + '.</div>' +
190
+ '</div>' +
191
+ '</div>';
192
+ return '<div data-settings-project="' + escHtml(p.name) + '" data-search="project ' + escHtml(p.name.toLowerCase()) + '" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:12px">' +
193
+ '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
194
+ '<div style="font-size:12px;font-weight:600">' + escHtml(p.name) + '</div>' +
195
+ '<button onclick="MinionsSettings.removeProject(\'' + escHtml(p.name) + '\')" style="font-size:9px;padding:2px 8px;background:transparent;color:var(--red);border:1px solid var(--red);border-radius:3px;cursor:pointer">Remove</button>' +
196
+ '</div>' +
197
+ pathRow +
198
+ branchGrid +
199
+ driftNote +
200
+ '<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
201
+ settingsToggle('Discover from PRs', 'set-ws-prs-' + p.name, p.workSources.pullRequests.enabled, 'Discovery gate: scan repo for open PRs and surface them as review tasks. Independent of ADO/GitHub polling — does not affect already-tracked PRs.') +
202
+ settingsToggle('Discover from Work Items', 'set-ws-wi-' + p.name, p.workSources.workItems.enabled, 'Auto-discover work from ADO/GitHub work items') +
203
+ '</div></div>';
204
+ }).join('');
295
205
 
296
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Routing Table</h3>' +
297
- '<textarea id="set-routing" rows="12" style="width:100%;padding:8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:monospace;font-size:11px;resize:vertical">' + escHtml(data.routing || '') + '</textarea>' +
206
+ const paneProjects =
207
+ '<h3>Projects</h3>' +
208
+ '<div class="settings-pane-sub">Repositories linked to this Minions install. Each project controls its own discovery sources + mainline branch. Add new projects via the Projects bar at the top of the dashboard or <code>minions add &lt;path&gt;</code>.</div>' +
209
+ (projectsHtml || '<div class="settings-pane-empty">No projects linked yet.</div>');
210
+
211
+ const panePolling =
212
+ '<h3>Polling</h3>' +
213
+ '<div class="settings-pane-sub">Cadence for fetching PR build status, votes, and comments from the platforms. Disabling a provider here turns the matching Auto-fix gates into no-ops.</div>' +
214
+ '<div class="settings-stack" style="margin-bottom:12px">' +
215
+ settingsToggle('ADO Polling', 'set-adoPollEnabled', e.adoPollEnabled !== false, 'Keeps ADO PR build results, votes, and comments fresh each tick; ADO PR dispatch gates are inert when this is off') +
216
+ settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Keeps GitHub PR build results, votes, and comments fresh each tick; GitHub PR dispatch gates are inert when this is off') +
217
+ '</div>' +
218
+ '<div class="settings-grid-2">' +
219
+ settingsField('PR Status Poll Frequency', 'set-prPollStatusEvery', e.prPollStatusEvery ?? 12, 'ticks', 'Poll PR build/review/merge status every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
220
+ settingsField('PR Comments Poll Frequency', 'set-prPollCommentsEvery', e.prPollCommentsEvery ?? 12, 'ticks', 'Poll PR human comments every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
221
+ '</div>';
222
+
223
+ const paneConcurrency =
224
+ '<h3>Concurrency &amp; Timeouts</h3>' +
225
+ '<div class="settings-pane-sub">How many agents run at once and how long they get before being killed for silence or runaway duration.</div>' +
226
+ '<div class="settings-grid-2">' +
227
+ settingsField('Max Concurrent Agents', 'set-maxConcurrent', e.maxConcurrent || 5, '', 'Max agents working simultaneously') +
228
+ settingsField('Tick Interval', 'set-tickInterval', e.tickInterval || 60000, 'ms', 'How often the engine runs discovery + dispatch') +
229
+ settingsField('Agent Timeout', 'set-agentTimeout', e.agentTimeout || 18000000, 'ms', 'Kill agent after this duration') +
230
+ settingsField('Heartbeat Timeout', 'set-heartbeatTimeout', e.heartbeatTimeout || 300000, 'ms', 'No output = dead after this') +
231
+ settingsField('Idle Alert', 'set-idleAlertMinutes', e.idleAlertMinutes || 15, 'min', 'Alert after agent idle this long') +
232
+ settingsField('Agent Busy Reassign', 'set-agentBusyReassignMs', e.agentBusyReassignMs || 600000, 'ms', 'Reassign work to another agent after it waits this long on a busy agent') +
233
+ settingsField('Max Retries Per Agent', 'set-maxRetriesPerAgent', e.maxRetriesPerAgent ?? 2, '', 'After the same agent fails the same work item this many times, the next retry reassigns to a different eligible agent (consults routing.md + availability). Falls back to the same agent only when no alternate is available. Counted separately from total maxRetries (which still caps overall retries).') +
234
+ settingsField('Restart Grace Period', 'set-restartGracePeriod', e.restartGracePeriod || 1200000, 'ms', 'Grace period before orphan detection on restart') +
235
+ settingsField('Shutdown Timeout', 'set-shutdownTimeout', e.shutdownTimeout || 300000, 'ms', 'Max wait for agents during graceful shutdown') +
236
+ settingsField('Meeting Round Timeout', 'set-meetingRoundTimeout', e.meetingRoundTimeout || 900000, 'ms', 'Auto-advance meeting round after this') +
237
+ '</div>';
298
238
 
299
- // Toggles persist immediately via POST /api/features/toggle — no Save needed.
300
- '<details id="settings-features-details" style="margin-top:16px;border-top:1px solid var(--border);padding-top:12px">' +
301
- '<summary style="cursor:pointer;font-size:13px;color:var(--blue);user-select:none">Show experimental flags ' +
239
+ const paneWorktree =
240
+ '<h3>Worker Pool &amp; Worktrees</h3>' +
241
+ '<div class="settings-pane-sub">Persistent CC worker pool and git worktree provisioning. Tune these only if you see worktree-create timeouts or slow CC cold-spawns.</div>' +
242
+ '<div class="settings-stack" style="margin-bottom:12px">' +
243
+ settingsToggle('CC Worker Pool', 'set-ccUseWorkerPool', (e.ccUseWorkerPool === undefined ? ((e.ccCli || e.defaultCli) === 'copilot') : !!e.ccUseWorkerPool), 'Route Command Center / doc-chat through a persistent copilot --acp worker per tab instead of spawning a fresh CLI per turn. Copilot-only (Agent Client Protocol transport); Claude does not implement ACP, so this toggle has no effect when CC runtime is Claude. Default ON for copilot (cold-spawn ~20s on Windows); forced OFF for non-copilot CC runtimes regardless of this toggle.') +
244
+ '</div>' +
245
+ '<div class="settings-grid-2">' +
246
+ settingsField('Worktree Create Timeout', 'set-worktreeCreateTimeout', e.worktreeCreateTimeout || 300000, 'ms', 'Timeout for git worktree add (increase for large repos/Windows)') +
247
+ settingsField('Worktree Create Retries', 'set-worktreeCreateRetries', e.worktreeCreateRetries || 1, '', 'Retry count for transient worktree add failures (0-3)') +
248
+ settingsField('Worktree Root', 'set-worktreeRoot', e.worktreeRoot || '../worktrees', '', 'Relative or absolute path for git worktrees; on Windows prefer a short path like C:\\wt') +
249
+ '</div>';
250
+
251
+ const paneCopilot =
252
+ '<h3>Copilot Tuning</h3>' +
253
+ '<div class="settings-pane-sub">Knobs that only apply when the runtime is Copilot. Includes the split-brain warning for the built-in MCPs toggle.</div>' +
254
+ '<div class="settings-stack" style="margin-bottom:12px">' +
255
+ // Tooltip on copilotDisableBuiltinMcps MUST warn about the split-brain risk
256
+ settingsToggle('Copilot: disable built-in MCPs', 'set-copilotDisableBuiltinMcps', e.copilotDisableBuiltinMcps !== false,
257
+ '⚠ 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.') +
258
+ settingsToggle('Copilot: suppress AGENTS.md', 'set-copilotSuppressAgentsMd', e.copilotSuppressAgentsMd !== false, '--no-custom-instructions: stops AGENTS.md auto-load from fighting Minions playbook prompts') +
259
+ settingsToggle('Copilot: reasoning summaries', 'set-copilotReasoningSummaries', !!e.copilotReasoningSummaries, '--enable-reasoning-summaries (Anthropic-family models only)') +
260
+ '</div>' +
261
+ '<div class="settings-grid-2">' +
262
+ '<div data-search="copilot stream mode incremental batched">' +
263
+ '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Copilot stream</label>' +
264
+ '<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">' +
265
+ '<option value="on"' + ((e.copilotStreamMode || 'on') === 'on' ? ' selected' : '') + '>on (incremental)</option>' +
266
+ '<option value="off"' + (e.copilotStreamMode === 'off' ? ' selected' : '') + '>off (batched)</option>' +
267
+ '</select>' +
268
+ '</div>' +
269
+ settingsField('Copilot fallback model', 'set-copilotFallbackModel', e.copilotFallbackModel || '', 'e.g. gpt-5.4', 'Copilot has no --fallback-model flag. On a MODEL_UNAVAILABLE (overloaded/503) retry, the engine OVERRIDES --model with this value (Copilot only).') +
270
+ '</div>';
271
+
272
+ const paneClaude =
273
+ '<h3>Claude Tuning</h3>' +
274
+ '<div class="settings-pane-sub">Knobs that only apply when the runtime is Claude. Permission bypass is runtime-owned and not configurable here.</div>' +
275
+ '<div class="settings-stack" style="margin-bottom:12px">' +
276
+ settingsToggle('Claude bare mode', 'set-claudeBareMode', !!e.claudeBareMode, '--bare suppresses CLAUDE.md auto-discovery; pair with explicit ccSystemPrompt or context will be lost') +
277
+ '</div>' +
278
+ '<div class="settings-grid-2">' +
279
+ settingsField('Claude fallback model', 'set-claudeFallbackModel', e.claudeFallbackModel || '', 'e.g. sonnet', 'Passed via Claude --fallback-model. The Claude CLI swaps to this only on rate-limit (429). Used together with engine.copilotFallbackModel for the engine-level MODEL_UNAVAILABLE (overloaded/503) retry — see CLAUDE.md.') +
280
+ '</div>' +
281
+ '<div style="display:grid;grid-template-columns:1fr;gap:8px;margin-top:12px">' +
282
+ settingsField('Allowed Tools', 'set-allowedTools', c.allowedTools || '', '', 'Claude allow-list passed through for compatibility; runtime bypass flags are adapter-owned.') +
283
+ '</div>' +
284
+ '<div data-search="permission bypass dangerously-skip-permissions autopilot allow-all" style="font-size:10px;color:var(--muted);margin-top:12px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:var(--surface-subtle, rgba(130,160,210,0.08))">' +
285
+ '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.' +
286
+ '</div>';
287
+
288
+ const paneBudget =
289
+ '<h3>Budget</h3>' +
290
+ '<div class="settings-pane-sub">Fleet-wide spend ceiling. Per-agent monthly caps live in the Agents table on the Runtime &amp; Models tab.</div>' +
291
+ '<div class="settings-grid-2">' +
292
+ 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.') +
293
+ '</div>';
294
+
295
+ const paneMaxTurns =
296
+ '<h3>Max Turns by Task Type</h3>' +
297
+ '<div class="settings-pane-sub">How many tool-use turns each task type gets before forced stop. Blank = built-in default. The fleet-wide Max Turns acts as a fallback for any task type not listed.</div>' +
298
+ '<div class="settings-grid-2" style="margin-bottom:12px">' +
299
+ settingsField('Max Turns (fleet default)', 'set-maxTurns', e.maxTurns || 100, '', 'Claude CLI --max-turns per agent') +
300
+ '</div>' +
301
+ '<div class="settings-grid-3">' +
302
+ settingsField('Explore', 'set-mt-explore', (e.maxTurnsByType || {}).explore || '', '', 'Default: 30') +
303
+ settingsField('Ask', 'set-mt-ask', (e.maxTurnsByType || {}).ask || '', '', 'Default: 20') +
304
+ settingsField('Review', 'set-mt-review', (e.maxTurnsByType || {}).review || '', '', 'Default: 30') +
305
+ settingsField('Implement', 'set-mt-implement', (e.maxTurnsByType || {}).implement || '', '', 'Default: 75') +
306
+ settingsField('Fix', 'set-mt-fix', (e.maxTurnsByType || {}).fix || '', '', 'Default: 75') +
307
+ settingsField('Test', 'set-mt-test', (e.maxTurnsByType || {}).test || '', '', 'Default: 50') +
308
+ settingsField('Verify', 'set-mt-verify', (e.maxTurnsByType || {}).verify || '', '', 'Default: 100') +
309
+ settingsField('Plan', 'set-mt-plan', (e.maxTurnsByType || {}).plan || '', '', 'Default: 30') +
310
+ settingsField('Decompose', 'set-mt-decompose', (e.maxTurnsByType || {}).decompose || '', '', 'Default: 15') +
311
+ '</div>';
312
+
313
+ // Toggles persist immediately via POST /api/features/toggle — no Save needed.
314
+ const paneFeatures =
315
+ '<h3>Feature Flags</h3>' +
316
+ '<div class="settings-pane-sub">In-progress UX or behavior gates. Toggles persist immediately. Registry: <code>engine/features.js</code>. Env override: <code>MINIONS_FEATURE_&lt;NAME&gt;=1</code>.</div>' +
317
+ '<details id="settings-features-details" open style="border:1px solid var(--border);border-radius:6px;padding:12px">' +
318
+ '<summary style="cursor:pointer;font-size:12px;color:var(--text);user-select:none;margin-bottom:8px">Show experimental flags ' +
302
319
  '<span style="font-size:10px;color:var(--muted)">(' + featuresList.length + ' registered)</span>' +
303
320
  '</summary>' +
304
- '<div style="font-size:10px;color:var(--muted);margin:8px 0 10px">In-progress UX or behavior gates. Toggles persist immediately. Registry: <code>engine/features.js</code>. Env override: <code>MINIONS_FEATURE_&lt;NAME&gt;=1</code>.</div>' +
305
321
  (featuresList.length === 0
306
322
  ? '<div style="font-size:11px;color:var(--muted);padding:12px;border:1px dashed var(--border);border-radius:4px;text-align:center">No experimental features registered. Add entries to <code>engine/features.js</code> to gate new work.</div>'
307
323
  : '<div style="display:flex;flex-direction:column;gap:8px">' +
@@ -314,7 +330,8 @@ async function openSettings() {
314
330
  if (f.addedIn) meta.push('added in ' + escHtml(f.addedIn));
315
331
  if (f.expires) meta.push('expires ' + escHtml(f.expires));
316
332
  meta.push(f.default ? 'default: on' : 'default: off');
317
- return '<label data-feature-id="' + escHtml(f.id) + '" style="display:flex;align-items:flex-start;gap:8px;padding:8px;border:1px solid var(--border);border-radius:4px;cursor:pointer">' +
333
+ const searchText = (f.id + ' ' + (f.description || '')).toLowerCase().replace(/"/g, '');
334
+ return '<label data-feature-id="' + escHtml(f.id) + '" data-search="' + escHtml(searchText) + '" style="display:flex;align-items:flex-start;gap:8px;padding:8px;border:1px solid var(--border);border-radius:4px;cursor:pointer">' +
318
335
  '<input type="checkbox" data-feature-toggle="' + escHtml(f.id) + '"' + checked + ' style="margin-top:3px;cursor:pointer">' +
319
336
  '<div style="flex:1">' +
320
337
  '<div style="font-size:11px;font-weight:600;color:var(--text)">' + escHtml(f.id) + expiredBadge + '</div>' +
@@ -324,9 +341,85 @@ async function openSettings() {
324
341
  '</label>';
325
342
  }).join('') +
326
343
  '</div>') +
327
- '</details>' +
344
+ '</details>';
345
+
346
+ const paneAdvanced =
347
+ '<h3>Advanced</h3>' +
348
+ '<div class="settings-pane-sub">Routing table + low-level engine knobs that most operators never touch. Change with care.</div>' +
349
+ '<h4>Routing Table</h4>' +
350
+ '<div data-search="routing table playbook agent assignment" style="margin-bottom:16px">' +
351
+ '<textarea id="set-routing" rows="10" style="width:100%;padding:8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:monospace;font-size:11px;resize:vertical">' + escHtml(data.routing || '') + '</textarea>' +
352
+ '</div>' +
353
+ '<h4>Inbox &amp; Status Retention</h4>' +
354
+ '<div class="settings-grid-2">' +
355
+ settingsField('Consolidation Threshold', 'set-inboxConsolidateThreshold', e.inboxConsolidateThreshold || 5, 'notes', 'Inbox notes before auto-consolidation') +
356
+ settingsField('Status WorkItems Retention', 'set-statusWorkItemsRetentionDays', e.statusWorkItemsRetentionDays ?? 7, 'days', 'Trim done/failed/cancelled work items older than N days from the /api/status workItems slice (active items are always shipped). Cuts SPA payload from ~3MB to <500KB. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
357
+ settingsField('Status Meetings Retention', 'set-statusMeetingsRetentionDays', e.statusMeetingsRetentionDays ?? 7, 'days', 'Trim completed/archived meetings older than N days from the /api/status meetings slice (active meetings are always shipped). Cuts SPA payload from ~4.3MB to <500KB. Detail modal still fetches full transcripts via /api/meetings/:id. Set to 0 to disable trimming (full list shipped, restoring legacy behavior).') +
358
+ settingsField('Version Check Interval', 'set-versionCheckInterval', e.versionCheckInterval || 3600000, 'ms', 'How often to check npm for updates (default: 1 hour)') +
359
+ '</div>' +
360
+ '<h4>Operator &amp; Comments</h4>' +
361
+ '<div class="settings-grid-2">' +
362
+ settingsField('Operator login (used in branch names)', 'set-operatorLogin', e.operatorLogin || '', '', 'Override the human operator login used in user/<loginname>/<wi-id>-<slug> branches. Empty = auto-resolve via gh / git email / OS username (currently resolves to: ' + (e._resolvedOperatorLogin || 'unknown') + ')') +
363
+ settingsField('Ignored Comment Authors', 'set-ignoredCommentAuthors', (e.ignoredCommentAuthors || []).join(', '), '', 'Comma-separated usernames — comments auto-closed, never trigger fixes') +
364
+ '</div>' +
365
+ '<h4>Experimental Behavior</h4>' +
366
+ '<div class="settings-stack">' +
367
+ settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
368
+ settingsToggle('Disable model discovery', 'set-disableModelDiscovery', !!e.disableModelDiscovery, 'Skip /api/runtimes/<name>/models REST calls fleet-wide. Settings UI falls back to free-text.') +
369
+ '</div>';
370
+
371
+ // Section registry — order is intentional (Runtime + Auto-fix surface first
372
+ // per Caleb's feedback). Each entry maps a rail-button id → pane HTML.
373
+ const sections = [
374
+ { id: 'runtime', label: 'Runtime & Models', featured: true, html: paneRuntime },
375
+ { id: 'autofix', label: 'Auto-fix & Review Loop', featured: true, html: paneAutoFix },
376
+ { id: 'projects', label: 'Projects', html: paneProjects },
377
+ { id: 'polling', label: 'Polling', html: panePolling },
378
+ { id: 'concurrency', label: 'Concurrency & Timeouts', html: paneConcurrency },
379
+ { id: 'worktree', label: 'Worker Pool & Worktrees', html: paneWorktree },
380
+ { id: 'copilot', label: 'Copilot Tuning', html: paneCopilot },
381
+ { id: 'claude', label: 'Claude Tuning', html: paneClaude },
382
+ { id: 'budget', label: 'Budget', html: paneBudget },
383
+ { id: 'maxturns', label: 'Max Turns', html: paneMaxTurns },
384
+ { id: 'features', label: 'Feature Flags', html: paneFeatures },
385
+ { id: 'advanced', label: 'Advanced', html: paneAdvanced },
386
+ ];
387
+
388
+ // localStorage-persisted active tab. Falls back to the first section when no
389
+ // saved value or the saved value points at a section we no longer render.
390
+ const SAVED_TAB_KEY = 'minions.settings.activeTab';
391
+ let activeTab = sections[0].id;
392
+ try {
393
+ const saved = localStorage.getItem(SAVED_TAB_KEY);
394
+ if (saved && sections.some(s => s.id === saved)) activeTab = saved;
395
+ } catch { /* localStorage may be blocked in some embeds */ }
396
+
397
+ const railHtml = sections.map(s =>
398
+ '<button class="settings-rail-btn' + (s.featured ? ' featured' : '') + (s.id === activeTab ? ' active' : '') + '" data-settings-tab="' + s.id + '" type="button">' +
399
+ '<span class="rail-label">' + escHtml(s.label) + '</span>' +
400
+ '<span class="match-count" style="display:none">0</span>' +
401
+ '</button>'
402
+ ).join('');
328
403
 
329
- '</div>';
404
+ const panesHtml = sections.map(s =>
405
+ '<div class="settings-pane' + (s.id === activeTab ? ' active' : '') + '" data-settings-pane="' + s.id + '">' +
406
+ s.html +
407
+ '</div>'
408
+ ).join('');
409
+
410
+ const html =
411
+ '<div class="settings-layout">' +
412
+ '<aside class="settings-rail">' +
413
+ '<div class="settings-search-wrap">' +
414
+ '<input id="settings-search" class="settings-search" type="text" placeholder="Search settings…" autocomplete="off">' +
415
+ '</div>' +
416
+ '<nav class="settings-rail-nav" id="settings-rail-nav">' + railHtml + '</nav>' +
417
+ '</aside>' +
418
+ '<div class="settings-content" id="settings-content">' +
419
+ '<div id="settings-status" style="font-size:11px;min-height:16px;margin-bottom:8px"></div>' +
420
+ panesHtml +
421
+ '</div>' +
422
+ '</div>';
330
423
 
331
424
  document.getElementById('modal-title').textContent = 'Settings';
332
425
 
@@ -351,12 +444,34 @@ async function openSettings() {
351
444
  saveBtn.onclick = saveSettings;
352
445
  actions.insertBefore(saveBtn, actions.lastElementChild);
353
446
  }
447
+ const body = document.getElementById('modal-body');
448
+ body.classList.add('settings-body');
354
449
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: engine, claude, agents, projects, routing, features)
355
- document.getElementById('modal-body').innerHTML = '<div id="settings-status" style="font-size:11px;min-height:16px;margin-bottom:4px"></div>' + html;
356
- document.getElementById('modal-body').style.fontFamily = '';
357
- document.getElementById('modal-body').style.whiteSpace = '';
450
+ body.innerHTML = html;
451
+ body.style.fontFamily = '';
452
+ body.style.whiteSpace = '';
358
453
  document.getElementById('modal').classList.add('open');
359
454
 
455
+ // ── Wire up rail buttons (tab switching + localStorage persist) ─────────
456
+ body.querySelectorAll('[data-settings-tab]').forEach(function(btn) {
457
+ btn.addEventListener('click', function() {
458
+ const id = btn.getAttribute('data-settings-tab');
459
+ _selectSettingsTab(id);
460
+ try { localStorage.setItem(SAVED_TAB_KEY, id); } catch { /* ignore */ }
461
+ });
462
+ });
463
+
464
+ // ── Cross-tab search box ───────────────────────────────────────────────
465
+ // Filters every [data-search] row across every pane. Empty query restores
466
+ // the active-tab view; non-empty query shows ALL panes with matches and
467
+ // hides unmatched rows so operators can see results without tab-hopping.
468
+ const searchInput = document.getElementById('settings-search');
469
+ if (searchInput) {
470
+ searchInput.addEventListener('input', function() {
471
+ _applySettingsSearch(searchInput.value || '');
472
+ });
473
+ }
474
+
360
475
  // ── Runtime fleet wiring (P-7a5c1f8e) ──────────────────────────────────────
361
476
  // 1. Load registered runtimes into the defaultCli + ccCli dropdowns.
362
477
  // 2. Load models for the selected defaultCli into the defaultModel input.
@@ -573,7 +688,11 @@ async function loadModelsForAgent(agentId, runtimeName, currentValue) {
573
688
  }
574
689
 
575
690
  function settingsToggle(label, id, checked, hint) {
576
- return '<div style="display:flex;align-items:center;gap:8px;padding:4px 0">' +
691
+ // data-search powers the cross-tab search box — lowercase label + hint so
692
+ // the input filter can substring-match without dragging the visible text
693
+ // through .toLowerCase() on every keystroke.
694
+ const searchKey = (String(label || '') + ' ' + String(hint || '')).toLowerCase().replace(/"/g, '');
695
+ return '<div class="settings-row" data-search="' + escHtml(searchKey) + '" style="display:flex;align-items:center;gap:8px;padding:4px 0">' +
577
696
  '<input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') + ' style="accent-color:var(--blue);width:16px;height:16px;cursor:pointer">' +
578
697
  '<label for="' + id + '" style="font-size:12px;color:var(--text);cursor:pointer">' + escHtml(label) + '</label>' +
579
698
  (hint ? '<span style="font-size:9px;color:var(--muted)">' + escHtml(hint) + '</span>' : '') +
@@ -581,13 +700,83 @@ function settingsToggle(label, id, checked, hint) {
581
700
  }
582
701
 
583
702
  function settingsField(label, id, value, unit, hint) {
584
- return '<div>' +
703
+ const searchKey = (String(label || '') + ' ' + String(hint || '') + ' ' + String(unit || '')).toLowerCase().replace(/"/g, '');
704
+ return '<div class="settings-row" data-search="' + escHtml(searchKey) + '">' +
585
705
  '<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">' + escHtml(label) + (unit ? ' <span style="opacity:0.6">(' + escHtml(unit) + ')</span>' : '') + '</label>' +
586
706
  '<input id="' + id + '" value="' + escHtml(String(value)) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
587
707
  (hint ? '<div style="font-size:9px;color:var(--muted);margin-top:1px">' + escHtml(hint) + '</div>' : '') +
588
708
  '</div>';
589
709
  }
590
710
 
711
+ // ── Settings tab + search helpers ─────────────────────────────────────────
712
+ // Extracted from openSettings so the tab switch / search-filter logic can be
713
+ // unit-tested without rebuilding the whole modal. Both operate on whatever
714
+ // DOM is currently mounted under #modal-body — no module state required.
715
+ function _selectSettingsTab(id) {
716
+ const body = document.getElementById('modal-body');
717
+ if (!body) return;
718
+ body.querySelectorAll('[data-settings-tab]').forEach(function(btn) {
719
+ btn.classList.toggle('active', btn.getAttribute('data-settings-tab') === id);
720
+ });
721
+ body.querySelectorAll('[data-settings-pane]').forEach(function(pane) {
722
+ pane.classList.toggle('active', pane.getAttribute('data-settings-pane') === id);
723
+ });
724
+ }
725
+
726
+ function _applySettingsSearch(rawQuery) {
727
+ const body = document.getElementById('modal-body');
728
+ if (!body) return;
729
+ const q = String(rawQuery || '').trim().toLowerCase();
730
+ const searching = q.length > 0;
731
+ body.classList.toggle('searching', searching);
732
+
733
+ // Reset every row + every rail badge before re-applying.
734
+ body.querySelectorAll('[data-search]').forEach(function(row) {
735
+ row.classList.remove('no-match', 'match-highlight');
736
+ });
737
+ const panes = body.querySelectorAll('[data-settings-pane]');
738
+ const railBtns = body.querySelectorAll('[data-settings-tab]');
739
+
740
+ if (!searching) {
741
+ // Restore single-active-pane mode.
742
+ panes.forEach(function(p) { p.classList.remove('no-match'); });
743
+ railBtns.forEach(function(btn) {
744
+ btn.classList.remove('no-match');
745
+ const badge = btn.querySelector('.match-count');
746
+ if (badge) badge.style.display = 'none';
747
+ });
748
+ return;
749
+ }
750
+
751
+ // Per-pane match counts drive both the rail badge and the pane visibility.
752
+ // Folding row.textContent into the haystack means operators can search by
753
+ // any visible label/hint string, not just the curated data-search tokens.
754
+ panes.forEach(function(pane) {
755
+ const paneId = pane.getAttribute('data-settings-pane');
756
+ let matches = 0;
757
+ pane.querySelectorAll('[data-search]').forEach(function(row) {
758
+ const haystack = (row.getAttribute('data-search') || '').toLowerCase();
759
+ const txt = (row.textContent || '').toLowerCase();
760
+ if (haystack.includes(q) || txt.includes(q)) {
761
+ row.classList.add('match-highlight');
762
+ matches++;
763
+ } else {
764
+ row.classList.add('no-match');
765
+ }
766
+ });
767
+ pane.classList.toggle('no-match', matches === 0);
768
+ const btn = body.querySelector('[data-settings-tab="' + paneId + '"]');
769
+ if (btn) {
770
+ btn.classList.toggle('no-match', matches === 0);
771
+ const badge = btn.querySelector('.match-count');
772
+ if (badge) {
773
+ badge.textContent = String(matches);
774
+ badge.style.display = matches > 0 ? 'inline-block' : 'none';
775
+ }
776
+ }
777
+ });
778
+ }
779
+
591
780
  async function saveSettings() {
592
781
  const status = document.getElementById('settings-status');
593
782
  const saveBtn = document.getElementById('modal-settings-save');
@@ -827,4 +1016,4 @@ async function removeProject(name) {
827
1016
  }
828
1017
  }
829
1018
 
830
- window.MinionsSettings = { openSettings, saveSettings, addProject, removeProject, resetSettingsToDefaults };
1019
+ window.MinionsSettings = { openSettings, saveSettings, addProject, removeProject, resetSettingsToDefaults, _selectSettingsTab, _applySettingsSearch };
@@ -710,6 +710,41 @@
710
710
 
711
711
  .modal-body { padding: var(--space-7) var(--space-8); overflow-y: auto; overflow-x: auto; white-space: pre-wrap; font-size: var(--text-md); line-height: 1.7; color: var(--muted); font-family: Consolas, monospace; }
712
712
 
713
+ /* ── Settings: left-rail tabbed layout (W-mpmwxkcn000646cc) ─────────────
714
+ Replaces the prior flat scroll of every engine.* knob with a Slack/VS-Code-style
715
+ vertical nav + per-tab pane. Search input filters control rows across all tabs
716
+ by data-search attribute. .modal.modal-wide is added by openSettings() so the
717
+ rail + content fit comfortably side-by-side. */
718
+ .modal-body.settings-body { padding: 0; white-space: normal; font-size: var(--text-md); line-height: 1.45; color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; }
719
+ .settings-layout { display: flex; min-height: 480px; max-height: calc(80vh - 64px); }
720
+ .settings-rail { width: 220px; min-width: 220px; background: var(--surface2); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
721
+ .settings-search-wrap { padding: var(--space-5) var(--space-5) var(--space-4); border-bottom: 1px solid var(--border); }
722
+ .settings-search { width: 100%; padding: var(--space-3) var(--space-4); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); font-size: var(--text-md); font-family: inherit; }
723
+ .settings-search:focus { outline: none; border-color: var(--blue); }
724
+ .settings-rail-nav { flex: 1; overflow-y: auto; padding: var(--space-3) 0; }
725
+ .settings-rail-btn { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: var(--space-3) var(--space-6); background: transparent; border: none; border-left: 3px solid transparent; color: var(--muted); font-size: var(--text-md); font-family: inherit; cursor: pointer; text-align: left; transition: all var(--transition-fast); }
726
+ .settings-rail-btn:hover { color: var(--text); background: var(--surface); }
727
+ .settings-rail-btn.active { color: var(--blue); border-left-color: var(--blue); background: var(--surface); font-weight: 600; }
728
+ .settings-rail-btn.featured { color: var(--text); }
729
+ .settings-rail-btn.featured.active { color: var(--blue); }
730
+ .settings-rail-btn.no-match { opacity: 0.3; }
731
+ .settings-rail-btn .match-count { font-size: var(--text-xs); color: var(--blue); background: var(--bg); border-radius: var(--radius-xl); padding: 1px 6px; min-width: 18px; text-align: center; }
732
+ .settings-content { flex: 1; overflow-y: auto; padding: var(--space-7) var(--space-8); min-width: 0; }
733
+ .settings-pane { display: none; }
734
+ .settings-pane.active { display: block; }
735
+ .settings-pane h3 { font-size: var(--text-xl); color: var(--blue); margin-bottom: var(--space-3); font-weight: 600; }
736
+ .settings-pane .settings-pane-sub { font-size: var(--text-base); color: var(--muted); margin-bottom: var(--space-7); }
737
+ .settings-pane h4 { font-size: var(--text-lg); color: var(--text); margin: var(--space-7) 0 var(--space-4); font-weight: 600; }
738
+ .settings-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
739
+ .settings-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--space-4); }
740
+ .settings-stack { display: flex; flex-direction: column; gap: var(--space-3); }
741
+ .settings-row { padding: 2px 0; }
742
+ .settings-row.no-match { display: none; }
743
+ .settings-row.match-highlight { background: rgba(88,166,255,0.08); border-left: 2px solid var(--blue); padding-left: var(--space-3); border-radius: var(--radius-sm); }
744
+ .settings-body.searching .settings-pane { display: block; }
745
+ .settings-body.searching .settings-pane.no-match { display: none; }
746
+ .settings-pane-empty { font-size: var(--text-base); color: var(--muted); padding: var(--space-6) 0; font-style: italic; }
747
+
713
748
  /* Responsive: tablet / narrow window */
714
749
  @media (max-width: 1100px) {
715
750
  .layout { grid-template-columns: 1fr; }
@@ -1,19 +1,19 @@
1
1
  [
2
2
  {
3
3
  "id": "config-poll-key-migration",
4
- "location": "engine/queries.js:123-162",
4
+ "location": "engine/queries.js:126-163",
5
5
  "function": "migrateDeprecatedConfigPollKeysOnce",
6
6
  "reason": "One-time boot migration that renames legacy engine config keys (adoPollStatusEvery → prPollStatusEvery, adoPollCommentsEvery → prPollCommentsEvery). Keys were renamed on 2026-04-16; this shim ensures any persisted config.json on a long-lived host gets transparently rewritten on next boot.",
7
7
  "targetRemovalDate": "2026-08-09",
8
- "notes": "Safe to delete on or after 2026-08-09 (3 months after the rename) once we are confident every engine has rebooted at least once and the closure-flagged migration (_configPollKeyMigrationChecked at line 123) is no longer reachable on any persisted config. Removal is one function + the call site in getConfig() at engine/queries.js:165."
8
+ "notes": "Safe to delete on or after 2026-08-09 (3 months after the rename) once we are confident every engine has rebooted at least once and the closure-flagged migration (_configPollKeyMigrationChecked at line 124) is no longer reachable on any persisted config. Removal is one function + the call site in getConfig() at engine/queries.js:166."
9
9
  },
10
10
  {
11
11
  "id": "legacy-done-aliases",
12
- "location": "engine/cleanup.js:970-972",
12
+ "location": "engine/cleanup.js:1052-1054",
13
13
  "constants": ["LEGACY_DONE_ALIASES", "LEGACY_NEEDS_REVIEW_STATUS"],
14
14
  "reason": "Read-side tolerance: cleanup sweep auto-migrates four obsolete work-item / PRD status strings ('in-pr', 'implemented', 'complete', 'needs-human-review') to the canonical 'done' / 'failed' values. The aliases are no longer written anywhere in the engine; the constants exist only to repair stale on-disk values from old engine versions.",
15
15
  "targetRemovalDate": null,
16
- "notes": "Keep indefinitely until telemetry / a sweep log shows zero migrations performed for 30 consecutive days across all known projects (work-items.json + prd/*.json). At that point the constants and both _migrateLegacyItem branches in engine/cleanup.js (definitions at :970-972; usage at :973-1000 for work items and :1051-1057 for PRD missing_features) can be deleted. Total cost on disk today: 4 strings."
16
+ "notes": "Keep indefinitely until telemetry / a sweep log shows zero migrations performed for 30 consecutive days across all known projects (work-items.json + prd/*.json). At that point the constants and both _migrateLegacyItem branches in engine/cleanup.js (definitions at :1052-1054; usage at :1055-1071 for work items and :1156-1162 for PRD missing_features) can be deleted. Total cost on disk today: 4 strings."
17
17
  },
18
18
  {
19
19
  "id": "completion-fallback-parsers",
@@ -27,6 +27,51 @@
27
27
  "enforcingTest": "test/unit/completion-fallback-telemetry.test.js:217-234",
28
28
  "enforcingSweepWindowTest": "test/unit/completion-fallback-sweep-window.test.js",
29
29
  "notes": "Do NOT set removedAt until telemetry confirms zero usage across the sweepWindowDays from sweepStartDate. The follow-up code-removal PR (dropping parseStructuredCompletion at engine/lifecycle.js:2856, parseCompletionFieldSummary at :3054, and the gated fallback at :3852-3877) is dispatched separately once the window is observed clean."
30
+ },
31
+ {
32
+ "id": "config-claude-binary-override",
33
+ "description": "Legacy `config.claude.binary` runtime-resolution override. Older `minions init` versions persisted a `config.claude.binary` field that pointed the Claude runtime at a specific binary path. The runtime adapter still honors this override on every Claude spawn; the engine emits a `deprecated-config-claude` warning at config-load time but does NOT delete the override, so the override branch in claude.js is load-bearing for any install that still carries a non-default value.",
34
+ "code": [
35
+ { "file": "engine/runtimes/claude.js", "lines": "82-93", "note": "resolveBinary() respects config.claude.binary on every Claude spawn (probes npm package dir or direct binary path)" },
36
+ { "file": "engine/shared.js", "lines": "2311-2326", "note": "warnings.push({ id: 'deprecated-config-claude' }) — surface-only; never deletes the override" },
37
+ { "file": "engine/shared.js", "lines": "2770-2774", "note": "DEFAULT_CLAUDE.binary baseline that the warning + prune logic compares against" }
38
+ ],
39
+ "removalGate": "Telemetry: the `deprecated-config-claude` warning emitted at engine/shared.js:2321-2324 must report zero hits across all known engines for >=30 consecutive days, AND a sweep of every persisted config.json must show no `config.claude.binary` value that diverges from DEFAULT_CLAUDE.binary. Only then is the override branch in resolveBinary() (engine/runtimes/claude.js:82-93) removable, along with the `_deprecatedConfigClaudeFields` membership for `binary` and the warning emitter at engine/shared.js:2311-2326.",
40
+ "targetRemovalDate": null,
41
+ "notes": "Do NOT set targetRemovalDate — removal must be signal-gated, not calendar-gated. This entry is paired with `prune-default-claude-config`: the prune strips DEFAULT-matching values but intentionally preserves user overrides, which is precisely why the override branch in claude.js stays reachable. Removing the override before the prune entry's gate clears would silently break installs that still rely on a custom binary path."
42
+ },
43
+ {
44
+ "id": "legacy-cc-model-migration",
45
+ "description": "applyLegacyCcModelMigration: in-memory shim that promotes legacy `engine.ccModel` to `engine.defaultModel` when defaultModel is unset, so single-model installs keep working after the runtime fleet refactor (P-3b8e5f1d). No on-disk rewrite — the persisted config.json continues to carry the legacy `ccModel` field. Called unconditionally on every engine boot from cli.start().",
46
+ "code": [
47
+ { "file": "engine/shared.js", "lines": "2236", "note": "applyLegacyCcModelMigration definition (function signature + once-per-process flag via _resetLegacyCcModelMigrationFlag)" },
48
+ { "file": "engine/cli.js", "lines": "471-472", "note": "Boot call site inside start(); wrapped in try/catch so a migration failure cannot block startup" },
49
+ { "file": "CLAUDE.md", "lines": "316", "note": "Architectural documentation calling out the in-memory promotion contract" },
50
+ { "file": "docs/slim-ux/concepts.md", "lines": "671", "note": "Surface-level concepts doc cross-reference" },
51
+ { "file": "test/unit.test.js", "lines": "19402", "note": "Source-inspection test pinning the CLAUDE.md description against the function name" },
52
+ { "file": "test/unit/runtime-fleet-helpers.test.js", "lines": "209-254", "note": "Behavioural unit tests (promotion, no-op when defaultModel set, no-op when ccModel unset, empty-string handling, once-only logging, null-safety)" },
53
+ { "file": "test/unit/runtime-fleet-helpers.test.js", "lines": "500-505", "note": "Source-inspection test pinning the cli.js boot call site" }
54
+ ],
55
+ "removalGate": "Telemetry: the once-per-boot deprecation log line emitted by applyLegacyCcModelMigration (via the injected logger at engine/shared.js:2236) must show zero promotion events across all known engines for >=30 consecutive days, AND a sweep of every persisted config.json must confirm no `engine.ccModel` field remains. Once both conditions hold, removal deletes the function + _resetLegacyCcModelMigrationFlag export at engine/shared.js:4977, the boot call at engine/cli.js:471-472, the CLAUDE.md:316 paragraph and docs/slim-ux/concepts.md:671 reference, and the tests at runtime-fleet-helpers.test.js:209-254 + :500-505 + unit.test.js:19402.",
56
+ "targetRemovalDate": null,
57
+ "notes": "Do NOT set targetRemovalDate — gating is signal-based. The function is silent on no-op (returns false without logging), so the meaningful telemetry signal is the absence of the promotion log line over the sweep window, NOT the absence of function invocations (cli.js calls it every boot regardless)."
58
+ },
59
+ {
60
+ "id": "prune-default-claude-config",
61
+ "description": "pruneDefaultClaudeConfig: active sanitizer that strips generated `config.claude.{binary,outputFormat,allowedTools,permissionMode}` defaults from persisted config.json so the `deprecated-config-claude` warning stops tripping on stale defaults left by older `minions init` versions. Sub-cluster of `config-claude-binary-override` — the prune deliberately preserves non-default user overrides (binary/allowedTools), which is what keeps the override branch in engine/runtimes/claude.js load-bearing.",
62
+ "code": [
63
+ { "file": "engine/shared.js", "lines": "2776-2809", "note": "pruneDefaultClaudeConfig definition: preserves non-default binary/allowedTools, always strips permissionMode + outputFormat" },
64
+ { "file": "engine/shared.js", "lines": "4989", "note": "Module export entry" },
65
+ { "file": "dashboard.js", "lines": "196", "note": "Called when loading config for the dashboard UI" },
66
+ { "file": "dashboard.js", "lines": "8753", "note": "Called during first config save handler" },
67
+ { "file": "dashboard.js", "lines": "8983", "note": "Called during second config save path" },
68
+ { "file": "dashboard.js", "lines": "9102", "note": "Called during third config save path" },
69
+ { "file": "minions.js", "lines": "385", "note": "Called during CLI init/update flow" },
70
+ { "file": "test/unit.test.js", "lines": "2153-2196", "note": "Behavioural unit tests (default strip, override preservation, outputFormat unconditional strip) + dashboard call-site source pin" },
71
+ { "file": "test/unit/runtime-fleet-helpers.test.js", "lines": "546", "note": "Source-inspection test pinning the dashboard handler call site" }
72
+ ],
73
+ "removalGate": "Telemetry: pruneDefaultClaudeConfig must return false (no mutation) for every call across all known engines for >=30 consecutive days (add an `_engine.pruneDefaultClaudeConfigStrips` counter if needed to observe this), AND the parent `config-claude-binary-override` entry must have already cleared its own gate. The dependency is strict: removing the prune while users still rely on the override branch would surface the `deprecated-config-claude` warning on every stale generated default. Once both conditions hold, removal is the function definition (engine/shared.js:2776-2809), the export at :4989, all 5 call sites (dashboard.js:196, :8753, :8983, :9102; minions.js:385), and the tests at unit.test.js:2153-2196 + runtime-fleet-helpers.test.js:546.",
74
+ "targetRemovalDate": null,
75
+ "notes": "Do NOT set targetRemovalDate — gating is signal-based AND ordered. This entry MUST NOT be removed before `config-claude-binary-override` clears its gate, otherwise installs with stale defaults will flood the deprecation channel until their next config save. The 5 call sites form a complete coverage net: load (dashboard.js:196 + minions.js:385) + save (dashboard.js:8753/8983/9102), so any code path that touches config.json runs the sanitizer."
30
76
  }
31
77
  ]
32
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2049",
3
+ "version": "0.1.2050",
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"