@yemi33/minions 0.1.2054 → 0.1.2056
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/dashboard/js/refresh.js +68 -8
- package/dashboard/js/settings.js +44 -1
- package/dashboard/layout.html +13 -0
- package/dashboard/styles.css +17 -3
- package/dashboard.js +9 -0
- package/engine/lifecycle.js +97 -0
- package/engine/shared.js +16 -1
- package/engine.js +10 -9
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -73,7 +73,7 @@ const RENDER_VERSIONS = {
|
|
|
73
73
|
prd: 1,
|
|
74
74
|
prs: 2,
|
|
75
75
|
archivedPrds: 1,
|
|
76
|
-
engine:
|
|
76
|
+
engine: 4,
|
|
77
77
|
version: 1,
|
|
78
78
|
adoThrottle: 1,
|
|
79
79
|
ghThrottle: 1,
|
|
@@ -147,18 +147,57 @@ function _formatCcDrawerLabel(autoMode) {
|
|
|
147
147
|
return runtimeLabel + (model ? ' (' + model + ')' : '') + '-powered. Full minions context. Enter to send, Shift+Enter for newline.';
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
// W-mpnc4u8c001d9d6c — #engine-quick-stats "Next tick in
|
|
150
|
+
// W-mpnc4u8c001d9d6c / W-mpodheao0006a37a — #engine-quick-stats "Next tick in"
|
|
151
|
+
// countdown.
|
|
151
152
|
//
|
|
152
153
|
// Origin is engine.lastTickAt (stamped by tickInner at the start of every
|
|
153
154
|
// tick, see engine.js) plus engine.tickInterval (server-side config, surfaced
|
|
154
155
|
// via _buildStatusFastState). The chip updates every 1s without re-rendering
|
|
155
156
|
// the surrounding row — mirrors the _tickAgentRuntimes pattern in
|
|
156
|
-
// dashboard/js/render-agents.js.
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
157
|
+
// dashboard/js/render-agents.js.
|
|
158
|
+
//
|
|
159
|
+
// Cadence semantics (W-mpodheao0006a37a): tickInterval is the MINIMUM gap
|
|
160
|
+
// between tick starts (engine.js#cli sets `setInterval(() => e.tick(), interval)`
|
|
161
|
+
// and engine.tick() short-circuits if a previous tick is still running). Slow
|
|
162
|
+
// ticks (PR polls, reconcilers) delay the next slot, so actual tick-to-tick
|
|
163
|
+
// gaps can exceed tickInterval. Once the countdown crosses zero the chip
|
|
164
|
+
// shows "running" (not "due" — the old label implied the engine was overdue,
|
|
165
|
+
// which lies about the floor-not-ceiling semantics). When we have ≥3
|
|
166
|
+
// observed deltas in _engineCadenceDeltas, the running label is augmented
|
|
167
|
+
// with the observed median cadence: "running (~Ns cadence)".
|
|
168
|
+
//
|
|
169
|
+
// When the engine isn't running, show "—" so a stale lastTickAt doesn't
|
|
170
|
+
// render a ticking countdown the engine can't honor.
|
|
160
171
|
var _engineCountdown = { lastTickAt: 0, tickInterval: 0, engineState: 'stopped' };
|
|
161
172
|
var _engineNextTickTimer = null;
|
|
173
|
+
// Ring buffer of the most recent observed tick-to-tick deltas (ms). Updated
|
|
174
|
+
// inside _processStatusUpdate whenever data.engine.lastTickAt advances. Used
|
|
175
|
+
// by _formatNextTickText to surface an observed cadence in the overshoot
|
|
176
|
+
// path. Capped at _engineCadenceMax entries (FIFO) so the median stays
|
|
177
|
+
// representative of recent behavior.
|
|
178
|
+
var _engineCadenceDeltas = [];
|
|
179
|
+
var _engineCadenceMax = 5;
|
|
180
|
+
var _engineCadenceLastSeenTickAt = 0;
|
|
181
|
+
|
|
182
|
+
function _recordEngineTickObservation(lastTickAt) {
|
|
183
|
+
if (!lastTickAt) return;
|
|
184
|
+
if (!_engineCadenceLastSeenTickAt) {
|
|
185
|
+
_engineCadenceLastSeenTickAt = lastTickAt;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (lastTickAt <= _engineCadenceLastSeenTickAt) return;
|
|
189
|
+
var delta = lastTickAt - _engineCadenceLastSeenTickAt;
|
|
190
|
+
_engineCadenceLastSeenTickAt = lastTickAt;
|
|
191
|
+
_engineCadenceDeltas.push(delta);
|
|
192
|
+
if (_engineCadenceDeltas.length > _engineCadenceMax) _engineCadenceDeltas.shift();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _medianCadenceMs() {
|
|
196
|
+
if (_engineCadenceDeltas.length < 3) return 0;
|
|
197
|
+
var sorted = _engineCadenceDeltas.slice().sort(function(a, b) { return a - b; });
|
|
198
|
+
var mid = Math.floor(sorted.length / 2);
|
|
199
|
+
return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
200
|
+
}
|
|
162
201
|
|
|
163
202
|
function _formatNextTickText() {
|
|
164
203
|
var state = _engineCountdown.engineState;
|
|
@@ -167,8 +206,11 @@ function _formatNextTickText() {
|
|
|
167
206
|
var interval = _engineCountdown.tickInterval;
|
|
168
207
|
if (!last || !interval) return '—';
|
|
169
208
|
var remainingMs = last + interval - Date.now();
|
|
170
|
-
if (remainingMs
|
|
171
|
-
|
|
209
|
+
if (remainingMs > 0) return Math.ceil(remainingMs / 1000) + 's';
|
|
210
|
+
if (state !== 'running') return '—';
|
|
211
|
+
var medianMs = _medianCadenceMs();
|
|
212
|
+
if (medianMs > 0) return 'running (~' + Math.round(medianMs / 1000) + 's cadence)';
|
|
213
|
+
return 'running';
|
|
172
214
|
}
|
|
173
215
|
|
|
174
216
|
function _updateNextTickChip() {
|
|
@@ -297,6 +339,9 @@ function _processStatusUpdate(data) {
|
|
|
297
339
|
_engineCountdown.lastTickAt = Number(data.engine.lastTickAt) || 0;
|
|
298
340
|
_engineCountdown.tickInterval = Number(data.engine.tickInterval) || 0;
|
|
299
341
|
_engineCountdown.engineState = data.engine.state || 'stopped';
|
|
342
|
+
// Feed the cadence ring buffer so the overshoot label can surface the
|
|
343
|
+
// observed tick-to-tick gap (W-mpodheao0006a37a).
|
|
344
|
+
_recordEngineTickObservation(_engineCountdown.lastTickAt);
|
|
300
345
|
// eslint-disable-next-line no-unsanitized/property -- reason: composed from internal engine metrics (pid, lastTickAt/tickInterval, worktreeCount) and a literal id; no user data flows in
|
|
301
346
|
qs.innerHTML = '<span>PID: <b>' + pid + '</b></span>' +
|
|
302
347
|
'<span>Next tick in: <b id="engine-next-tick">' + _formatNextTickText() + '</b></span>' +
|
|
@@ -693,6 +738,21 @@ async function refresh() {
|
|
|
693
738
|
|
|
694
739
|
refresh();
|
|
695
740
|
_syncPinsFromServer(); // Load server-side pins on startup
|
|
741
|
+
// W-mpmwxkrw000872ec — reconcile the font-size preference from the server
|
|
742
|
+
// once on cold load. The inline bootstrap in layout.html has already applied
|
|
743
|
+
// localStorage's value (if any) to avoid flash; this fetch promotes the
|
|
744
|
+
// server value when it differs (e.g. fresh browser, different machine).
|
|
745
|
+
(function _reconcileFontSizeFromServer() {
|
|
746
|
+
fetch('/api/settings').then(function(r) { return r.ok ? r.json() : null; }).then(function(data) {
|
|
747
|
+
if (!data || !data.engine) return;
|
|
748
|
+
var serverValue = data.engine.fontSize || 'small';
|
|
749
|
+
var localValue = 'small';
|
|
750
|
+
try { localValue = localStorage.getItem('minions:font-size') || 'small'; } catch (e) { /* private mode */ }
|
|
751
|
+
if (serverValue !== localValue && typeof window.applyFontSizePreference === 'function') {
|
|
752
|
+
window.applyFontSizePreference(serverValue);
|
|
753
|
+
}
|
|
754
|
+
}).catch(function() { /* offline / 4xx — keep localStorage value */ });
|
|
755
|
+
})();
|
|
696
756
|
|
|
697
757
|
// Poll for status updates (SSE caused HTTP/1.1 connection exhaustion — CC fetch would fail)
|
|
698
758
|
setInterval(refresh, 4000);
|
package/dashboard/js/settings.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
// settings.js — Settings panel functions extracted from dashboard.html
|
|
2
2
|
|
|
3
|
+
// W-mpmwxkrw000872ec — single source of truth for applying the font-size
|
|
4
|
+
// preference at runtime. Mirrors the inline bootstrap script in
|
|
5
|
+
// layout.html (which runs before this file loads). Exposed on window so the
|
|
6
|
+
// refresh.js reconciler can call it after the first /api/settings fetch.
|
|
7
|
+
const FONT_SIZE_VALUES = ['small', 'medium', 'large', 'xlarge'];
|
|
8
|
+
function applyFontSizePreference(value) {
|
|
9
|
+
const v = FONT_SIZE_VALUES.includes(value) ? value : 'small';
|
|
10
|
+
try { document.documentElement.setAttribute('data-font-size', v); } catch (e) { /* no DOM */ }
|
|
11
|
+
try { localStorage.setItem('minions:font-size', v); } catch (e) { /* private mode / disabled */ }
|
|
12
|
+
}
|
|
13
|
+
try { window.applyFontSizePreference = applyFontSizePreference; } catch (e) { /* non-browser */ }
|
|
14
|
+
|
|
3
15
|
let _settingsData = null;
|
|
4
16
|
// Async runtime/model discovery can resolve out of order when the operator
|
|
5
17
|
// flips between runtimes quickly. Without a per-target token, a slower Copilot
|
|
@@ -166,6 +178,29 @@ async function openSettings() {
|
|
|
166
178
|
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)') +
|
|
167
179
|
'</div>';
|
|
168
180
|
|
|
181
|
+
// W-mpmwxkrw000872ec — Appearance pane. Hosts dashboard-wide visual
|
|
182
|
+
// preferences (font-size scale today; reserved for future theme picks).
|
|
183
|
+
// Kept narrow + featured near the rail top so the setting is discoverable
|
|
184
|
+
// rather than buried under generic Engine headers.
|
|
185
|
+
const paneAppearance =
|
|
186
|
+
'<h3>Appearance</h3>' +
|
|
187
|
+
'<div class="settings-pane-sub">Dashboard-wide visual preferences. Persisted in browser localStorage + server-side settings so they survive a reload from a cold cache.</div>' +
|
|
188
|
+
'<div class="settings-grid-2">' +
|
|
189
|
+
'<div data-search="font size scale appearance dashboard">' +
|
|
190
|
+
'<label style="font-size:10px;color:var(--muted);display:block;margin-bottom:2px">Font Size</label>' +
|
|
191
|
+
'<select id="set-fontSize" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:12px">' +
|
|
192
|
+
(function() {
|
|
193
|
+
var current = (e.fontSize || 'small');
|
|
194
|
+
var opts = [['small', 'Small (current default)'], ['medium', 'Medium'], ['large', 'Large'], ['xlarge', 'Extra Large']];
|
|
195
|
+
return opts.map(function(o) {
|
|
196
|
+
return '<option value="' + o[0] + '"' + (current === o[0] ? ' selected' : '') + '>' + o[1] + '</option>';
|
|
197
|
+
}).join('');
|
|
198
|
+
})() +
|
|
199
|
+
'</select>' +
|
|
200
|
+
'<div style="font-size:9px;color:var(--muted);margin-top:1px">Scales the whole dashboard. Persisted in browser + server.</div>' +
|
|
201
|
+
'</div>' +
|
|
202
|
+
'</div>';
|
|
203
|
+
|
|
169
204
|
const projectsHtml = (data.projects || []).map(function(p) {
|
|
170
205
|
// Cross-reference live /api/status to pull in the auto-detected
|
|
171
206
|
// remoteDefaultBranch (read-only) — settings already has localPath +
|
|
@@ -230,7 +265,7 @@ async function openSettings() {
|
|
|
230
265
|
'<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>' +
|
|
231
266
|
'<div class="settings-grid-2">' +
|
|
232
267
|
settingsField('Max Concurrent Agents', 'set-maxConcurrent', e.maxConcurrent || 5, '', 'Max agents working simultaneously') +
|
|
233
|
-
settingsField('Tick Interval', 'set-tickInterval', e.tickInterval || 60000, 'ms', '
|
|
268
|
+
settingsField('Min Tick Interval', 'set-tickInterval', e.tickInterval || 60000, 'ms', 'Minimum gap between tick starts. Slow ticks (PR polls, reconcilers) may delay the next slot — see `running` indicator on /engine.') +
|
|
234
269
|
settingsField('Agent Timeout', 'set-agentTimeout', e.agentTimeout || 18000000, 'ms', 'Kill agent after this duration') +
|
|
235
270
|
settingsField('Heartbeat Timeout', 'set-heartbeatTimeout', e.heartbeatTimeout || 300000, 'ms', 'No output = dead after this') +
|
|
236
271
|
settingsField('Idle Alert', 'set-idleAlertMinutes', e.idleAlertMinutes || 15, 'min', 'Alert after agent idle this long') +
|
|
@@ -378,6 +413,7 @@ async function openSettings() {
|
|
|
378
413
|
const sections = [
|
|
379
414
|
{ id: 'runtime', label: 'Runtime & Models', featured: true, html: paneRuntime },
|
|
380
415
|
{ id: 'autofix', label: 'Auto-fix & Review Loop', featured: true, html: paneAutoFix },
|
|
416
|
+
{ id: 'appearance', label: 'Appearance', html: paneAppearance },
|
|
381
417
|
{ id: 'projects', label: 'Projects', html: paneProjects },
|
|
382
418
|
{ id: 'polling', label: 'Polling', html: panePolling },
|
|
383
419
|
{ id: 'concurrency', label: 'Concurrency & Timeouts', html: paneConcurrency },
|
|
@@ -848,6 +884,9 @@ async function saveSettings() {
|
|
|
848
884
|
copilotReasoningSummaries: !!document.getElementById('set-copilotReasoningSummaries')?.checked,
|
|
849
885
|
maxBudgetUsd: (document.getElementById('set-maxBudgetUsd')?.value ?? '').trim(),
|
|
850
886
|
disableModelDiscovery: !!document.getElementById('set-disableModelDiscovery')?.checked,
|
|
887
|
+
// W-mpmwxkrw000872ec — global font-size scale. Allowlist validation
|
|
888
|
+
// (and clamp messaging) lives server-side in handleSettingsUpdate.
|
|
889
|
+
fontSize: document.getElementById('set-fontSize')?.value || 'small',
|
|
851
890
|
maxTurnsByType: (function() {
|
|
852
891
|
var mbt = {};
|
|
853
892
|
var types = ['explore', 'ask', 'review', 'implement', 'fix', 'test', 'verify', 'plan', 'decompose'];
|
|
@@ -912,6 +951,10 @@ async function saveSettings() {
|
|
|
912
951
|
status.style.color = 'var(--green)';
|
|
913
952
|
showToast('cmd-toast', 'Settings saved', true);
|
|
914
953
|
}
|
|
954
|
+
// W-mpmwxkrw000872ec — apply font-size immediately + persist to
|
|
955
|
+
// localStorage so a hard reload still reflects the new pick (server is
|
|
956
|
+
// the cold-load authority; localStorage is the no-flash fast path).
|
|
957
|
+
try { applyFontSizePreference(enginePayload.fontSize); } catch (err) { /* tolerated — change shows on next reload */ }
|
|
915
958
|
if (saveBtn) { saveBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Saved'; saveBtn.style.color = 'var(--green)'; saveBtn.style.borderColor = 'var(--green)'; }
|
|
916
959
|
setTimeout(function() {
|
|
917
960
|
if (saveBtn) { saveBtn.disabled = false; saveBtn.style.opacity = ''; saveBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Save'; }
|
package/dashboard/layout.html
CHANGED
|
@@ -5,6 +5,19 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Minions Mission Control{{title_suffix}}</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>{{favicon_emoji}}</text></svg>">
|
|
8
|
+
<script>
|
|
9
|
+
// W-mpmwxkrw000872ec — apply the saved font-size BEFORE the stylesheet
|
|
10
|
+
// parses so the first paint already reflects the user's preference (no
|
|
11
|
+
// flash of unstyled font size). localStorage is the fast path; the server
|
|
12
|
+
// value is reconciled later in refresh.js's first tick.
|
|
13
|
+
(function() {
|
|
14
|
+
try {
|
|
15
|
+
var v = localStorage.getItem('minions:font-size');
|
|
16
|
+
var valid = { small: 1, medium: 1, large: 1, xlarge: 1 };
|
|
17
|
+
document.documentElement.setAttribute('data-font-size', (v && valid[v]) ? v : 'small');
|
|
18
|
+
} catch (e) { /* private mode / disabled storage — fall through to default */ }
|
|
19
|
+
})();
|
|
20
|
+
</script>
|
|
8
21
|
<style>/* __CSS__ */</style>
|
|
9
22
|
</head>
|
|
10
23
|
<body>
|
package/dashboard/styles.css
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
:root {
|
|
2
|
+
/* W-mpmwxkrw000872ec — global dashboard font-size scale. Driven by
|
|
3
|
+
[data-font-size] on <html>. 'small' is the historic default (1.0) so
|
|
4
|
+
existing users see no change. Body-level CSS `zoom` is the cheapest
|
|
5
|
+
way to scale every px/em/rem rule across the SPA (typography tokens
|
|
6
|
+
below, slim.html, and the many inline `font-size:11px` styles) without
|
|
7
|
+
rewriting every rule. Scales modals, drawers, and fixed-position
|
|
8
|
+
elements because they all live inside <body>. */
|
|
9
|
+
--minions-font-scale: 1;
|
|
2
10
|
--bg: #0d1117; --surface: #161b22; --surface2: #21262d; --border: #30363d;
|
|
3
11
|
--text: #e6edf3; --muted: #8b949e; --green: #3fb950; --yellow: #d29922;
|
|
4
12
|
--blue: #58a6ff; --purple: #bc8cff; --red: #f85149; --orange: #e3b341;
|
|
@@ -23,9 +31,17 @@
|
|
|
23
31
|
/* Transitions */
|
|
24
32
|
--transition-fast: 0.15s; --transition-base: 0.2s; --transition-slow: 0.3s;
|
|
25
33
|
}
|
|
34
|
+
/* W-mpmwxkrw000872ec — font-size scale overrides. Values picked so the
|
|
35
|
+
largest tier (~1.30) tops out near 18px effective for the default 14px
|
|
36
|
+
body without overflowing chips/badges/tables at /, /pull-requests, /qa,
|
|
37
|
+
/settings (spot-checked). */
|
|
38
|
+
:root[data-font-size="small"] { --minions-font-scale: 1; }
|
|
39
|
+
:root[data-font-size="medium"] { --minions-font-scale: 1.08; }
|
|
40
|
+
:root[data-font-size="large"] { --minions-font-scale: 1.18; }
|
|
41
|
+
:root[data-font-size="xlarge"] { --minions-font-scale: 1.30; }
|
|
26
42
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
27
43
|
html, body { height: 100%; margin: 0; overflow: hidden; }
|
|
28
|
-
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: var(--text-xl); display: flex; flex-direction: column; }
|
|
44
|
+
body { background: var(--bg); color: var(--text); font-family: 'Segoe UI', system-ui, sans-serif; font-size: var(--text-xl); display: flex; flex-direction: column; zoom: var(--minions-font-scale, 1); }
|
|
29
45
|
|
|
30
46
|
header {
|
|
31
47
|
background: var(--surface); border-bottom: 1px solid var(--border);
|
|
@@ -730,8 +746,6 @@
|
|
|
730
746
|
.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-weight: 400; font-family: inherit; cursor: pointer; text-align: left; transition: all var(--transition-fast); }
|
|
731
747
|
.settings-rail-btn:hover { color: var(--text); background: var(--surface); }
|
|
732
748
|
.settings-rail-btn.active { color: var(--blue); border-left-color: var(--blue); background: var(--surface); font-weight: 600; }
|
|
733
|
-
.settings-rail-btn.featured { color: var(--text); }
|
|
734
|
-
.settings-rail-btn.featured.active { color: var(--blue); }
|
|
735
749
|
.settings-rail-btn.no-match { opacity: 0.3; }
|
|
736
750
|
.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; }
|
|
737
751
|
.settings-content { flex: 1; overflow-y: auto; padding: var(--space-7) var(--space-8); min-width: 0; }
|
package/dashboard.js
CHANGED
|
@@ -9100,6 +9100,15 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
9100
9100
|
else if (valid.includes(e.copilotStreamMode)) _setEngineConfig('copilotStreamMode', e.copilotStreamMode);
|
|
9101
9101
|
else _clamped.push(`copilotStreamMode: "${e.copilotStreamMode}" not in [on, off] (kept previous value)`);
|
|
9102
9102
|
}
|
|
9103
|
+
// W-mpmwxkrw000872ec — fontSize allowlist. Clamps invalid values
|
|
9104
|
+
// (rather than silently failing) so the dashboard bootstrap never
|
|
9105
|
+
// ends up with an unknown data-font-size attribute.
|
|
9106
|
+
if (e.fontSize !== undefined) {
|
|
9107
|
+
const valid = ['small', 'medium', 'large', 'xlarge'];
|
|
9108
|
+
if (_isClear(e.fontSize)) _deleteEngineConfig('fontSize');
|
|
9109
|
+
else if (valid.includes(e.fontSize)) _setEngineConfig('fontSize', e.fontSize);
|
|
9110
|
+
else _clamped.push(`fontSize: "${e.fontSize}" not in [${valid.join(', ')}] (kept previous value)`);
|
|
9111
|
+
}
|
|
9103
9112
|
// maxBudgetUsd uses ?? semantics — 0 is a valid cap (read-only / dry-run agents).
|
|
9104
9113
|
if (e.maxBudgetUsd !== undefined) {
|
|
9105
9114
|
if (_isClear(e.maxBudgetUsd)) _deleteEngineConfig('maxBudgetUsd');
|
package/engine/lifecycle.js
CHANGED
|
@@ -2067,6 +2067,90 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
2067
2067
|
delete next.fixedAt;
|
|
2068
2068
|
target.minionsReview = next;
|
|
2069
2069
|
};
|
|
2070
|
+
// W-mpoeirqx0007712a — Build-fix push verification. The agent may report
|
|
2071
|
+
// SUCCESS while the git push silently failed to advance the remote head
|
|
2072
|
+
// (stale-worktree push rejected non-fast-forward, agent ignores non-zero
|
|
2073
|
+
// `git push` exit, etc). detectPrFixBranchChange falls back to
|
|
2074
|
+
// local-head / worktree-diff evidence in those scenarios and returns
|
|
2075
|
+
// `changed: true` even though origin/<branch> never moved. Without a
|
|
2076
|
+
// guard here, the optimistic stamp + 10-min buildFixGracePeriod
|
|
2077
|
+
// suppresses re-dispatch against a still-failing build that was never
|
|
2078
|
+
// actually fixed (live repro: opg-microsoft/minions PR #57).
|
|
2079
|
+
//
|
|
2080
|
+
// Only `evidence: 'remote-head'` proves the push landed. For
|
|
2081
|
+
// BUILD_FAILURE with changed=true AND evidence explicitly set to one of
|
|
2082
|
+
// the unverified types, increment `_buildFixPushFailedCount`, write an
|
|
2083
|
+
// inbox alert, route through recordPrNoOpFixAttempt so the cause stays
|
|
2084
|
+
// unhandled, and never write `_buildFixPushedAt`. When the counter
|
|
2085
|
+
// reaches `engine.maxBuildFixRetries`, flip `_buildFixNeedsHumanRebase`
|
|
2086
|
+
// so the engine stops retrying.
|
|
2087
|
+
//
|
|
2088
|
+
// Note: callers that omit `branchChange.evidence` (legacy / tests
|
|
2089
|
+
// predating evidence plumbing) still hit the trusted-push path below to
|
|
2090
|
+
// preserve backward compatibility — only the explicitly unverified
|
|
2091
|
+
// evidence kinds trigger this guard.
|
|
2092
|
+
const _unverifiedPushEvidence = new Set(['local-head', 'worktree-diff']);
|
|
2093
|
+
if (cause === shared.PR_FIX_CAUSE.BUILD_FAILURE
|
|
2094
|
+
&& explicitlyChangedBranch
|
|
2095
|
+
&& options.branchChange?.changed === true
|
|
2096
|
+
&& _unverifiedPushEvidence.has(options.branchChange?.evidence)) {
|
|
2097
|
+
const maxRetries = options.config?.engine?.maxBuildFixRetries
|
|
2098
|
+
?? ENGINE_DEFAULTS.maxBuildFixRetries;
|
|
2099
|
+
target._buildFixPushFailedCount = (Number(target._buildFixPushFailedCount) || 0) + 1;
|
|
2100
|
+
const reachedCap = target._buildFixPushFailedCount >= maxRetries;
|
|
2101
|
+
if (reachedCap) {
|
|
2102
|
+
target._buildFixNeedsHumanRebase = ts();
|
|
2103
|
+
}
|
|
2104
|
+
const beforeHeadStr = String(options.branchChange?.beforeHead || '').slice(0, 40);
|
|
2105
|
+
const afterHeadStr = String(options.branchChange?.afterHead || '').slice(0, 40);
|
|
2106
|
+
const evidenceStr = String(options.branchChange?.evidence || 'unknown');
|
|
2107
|
+
try {
|
|
2108
|
+
const wiId = options.dispatchItem?.meta?.item?.id || null;
|
|
2109
|
+
const noteBody = `# Build-fix push not verified for ${pr.id}\n\n`
|
|
2110
|
+
+ `**PR:** ${pr.url || pr.id}\n`
|
|
2111
|
+
+ `**Branch:** ${pr.branch || '(unknown)'}\n`
|
|
2112
|
+
+ `**Cause:** build-failure\n`
|
|
2113
|
+
+ `**Pre-dispatch head:** ${beforeHeadStr || '(unknown)'}\n`
|
|
2114
|
+
+ `**Post-completion head (live):** ${afterHeadStr || '(unknown)'}\n`
|
|
2115
|
+
+ `**Branch-change evidence:** ${evidenceStr}\n`
|
|
2116
|
+
+ `**Attempt:** ${target._buildFixPushFailedCount}/${maxRetries}\n\n`
|
|
2117
|
+
+ (reachedCap
|
|
2118
|
+
? `⚠️ **Reached \`engine.maxBuildFixRetries\` (${maxRetries}).** PR flagged \`_buildFixNeedsHumanRebase\` — engine will stop auto-retrying. Likely root cause: worktree stale vs origin/master, push rejected non-fast-forward, or branch protection blocks the engine identity.\n`
|
|
2119
|
+
: `_Engine will re-dispatch on the next \`discoverFromPrs\` pass (counter < cap)._\n`)
|
|
2120
|
+
+ `\nThe agent reported SUCCESS but the remote head did not advance — the optimistic \`_buildFixPushedAt\` stamp was suppressed to avoid the ${(ENGINE_DEFAULTS.buildFixGracePeriod / 60000) | 0}-minute grace-period blackout.\n`;
|
|
2121
|
+
shared.writeToInbox(
|
|
2122
|
+
'engine',
|
|
2123
|
+
`build-fix-push-unverified-${pr.prNumber || pr.id}`,
|
|
2124
|
+
noteBody,
|
|
2125
|
+
null,
|
|
2126
|
+
{ wi: wiId, pr: pr.id, cause: shared.PR_FIX_CAUSE.BUILD_FAILURE }
|
|
2127
|
+
);
|
|
2128
|
+
} catch (err) {
|
|
2129
|
+
log('warn', `build-fix push-verify inbox alert for ${pr.id}: ${err.message}`);
|
|
2130
|
+
}
|
|
2131
|
+
// Route through the noop path so the cause stays unhandled, the noop
|
|
2132
|
+
// counter advances symmetrically with the genuine-noop case, and the
|
|
2133
|
+
// existing `delete target._buildFixPushedAt` cleanup (line ~2016) runs.
|
|
2134
|
+
const verifyBranchChange = {
|
|
2135
|
+
changed: false,
|
|
2136
|
+
beforeHead: options.branchChange?.beforeHead,
|
|
2137
|
+
afterHead: options.branchChange?.afterHead,
|
|
2138
|
+
evidence: 'push-unverified',
|
|
2139
|
+
};
|
|
2140
|
+
const noopReason = `build-fix push unverified (evidence: ${evidenceStr}); attempt ${target._buildFixPushFailedCount}/${maxRetries}${reachedCap ? ' — needs-human-rebase' : ''}`;
|
|
2141
|
+
const record = recordPrNoOpFixAttempt(target, cause, source, options.dispatchItem, verifyBranchChange, options.config, noopReason);
|
|
2142
|
+
result = {
|
|
2143
|
+
noOp: true,
|
|
2144
|
+
cause,
|
|
2145
|
+
paused: !!record.paused,
|
|
2146
|
+
count: record.count,
|
|
2147
|
+
pushUnverified: true,
|
|
2148
|
+
pushFailedCount: target._buildFixPushFailedCount,
|
|
2149
|
+
needsHumanRebase: reachedCap,
|
|
2150
|
+
};
|
|
2151
|
+
log('warn', `Updated ${pr.id} → build-fix push unverified (${target._buildFixPushFailedCount}/${maxRetries}, evidence=${evidenceStr})${reachedCap ? ' [needs-human-rebase]' : ''}; remote head ${beforeHeadStr.slice(0, 8)} did not advance — inbox alert written, cause left unhandled for re-dispatch`);
|
|
2152
|
+
return prs;
|
|
2153
|
+
}
|
|
2070
2154
|
if (explicitlyChangedBranch && options.branchChange?.changed === false) {
|
|
2071
2155
|
const record = recordPrNoOpFixAttempt(target, cause, source, options.dispatchItem, options.branchChange, options.config, options.noopReason);
|
|
2072
2156
|
result = { noOp: true, cause, paused: !!record.paused, count: record.count };
|
|
@@ -2086,6 +2170,19 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
2086
2170
|
return prs;
|
|
2087
2171
|
}
|
|
2088
2172
|
clearPrNoOpFixAttempt(target, cause);
|
|
2173
|
+
// W-mpoeirqx0007712a — verified-push stamping for BUILD_FAILURE. Reaching
|
|
2174
|
+
// this point with explicitlyChangedBranch=true means the unverified-push
|
|
2175
|
+
// guard above did NOT trigger, so either evidence === 'remote-head'
|
|
2176
|
+
// (live remote refs prove the branch advanced) OR no branchChange info
|
|
2177
|
+
// was supplied (legacy callers that didn't pass branchChange — keep
|
|
2178
|
+
// existing behavior of trusting the agent's branchChanged claim).
|
|
2179
|
+
// Clear the push-failure counter on confirmed success so future
|
|
2180
|
+
// regressions start fresh.
|
|
2181
|
+
if (cause === shared.PR_FIX_CAUSE.BUILD_FAILURE && explicitlyChangedBranch) {
|
|
2182
|
+
target._buildFixPushedAt = ts();
|
|
2183
|
+
delete target._buildFixPushFailedCount;
|
|
2184
|
+
delete target._buildFixNeedsHumanRebase;
|
|
2185
|
+
}
|
|
2089
2186
|
if (source === 'pr-human-feedback') {
|
|
2090
2187
|
const clearPendingFix = shouldClearHumanFeedbackPendingFix(target, pr, automationCauseKey);
|
|
2091
2188
|
if (target.humanFeedback && clearPendingFix) target.humanFeedback.pendingFix = false;
|
package/engine/shared.js
CHANGED
|
@@ -1800,7 +1800,15 @@ const ENGINE_DEFAULTS = {
|
|
|
1800
1800
|
logBufferSize: 50, // flush immediately when buffer exceeds this many entries
|
|
1801
1801
|
lockRetries: 0, // no retries — single 5s timeout window with 25ms polling (200 attempts) is sufficient; stale lock recovery at 60s handles crashes
|
|
1802
1802
|
lockRetryBackoffMs: 500, // base backoff between lock retries (doubles each attempt: 500ms, 1s, 2s, ...)
|
|
1803
|
-
buildFixGracePeriod: 600000, // 10min — wait for CI to run after build
|
|
1803
|
+
buildFixGracePeriod: 600000, // 10min — wait for CI to run after a verified build-fix push before re-dispatching
|
|
1804
|
+
// W-mpoeirqx0007712a: cap re-dispatch attempts when build-fix pushes
|
|
1805
|
+
// silently fail to advance the remote head (stale-worktree push rejected,
|
|
1806
|
+
// agent ignores non-zero git push exit and reports SUCCESS, etc).
|
|
1807
|
+
// updatePrAfterFix increments `_buildFixPushFailedCount` whenever the
|
|
1808
|
+
// post-completion branchChange has non-remote-head evidence; when the
|
|
1809
|
+
// counter reaches this cap, the PR is flagged `_buildFixNeedsHumanRebase`
|
|
1810
|
+
// so the dispatcher stops auto-retrying and a human can rescue the branch.
|
|
1811
|
+
maxBuildFixRetries: 3,
|
|
1804
1812
|
adoPollEnabled: true, // poll ADO PR status, comments, and reconciliation on each tick cycle
|
|
1805
1813
|
ghPollEnabled: true, // poll GitHub PR status, comments, and reconciliation on each tick cycle
|
|
1806
1814
|
prPollStatusEvery: 12, // poll PR build/review/merge status every N ticks for both ADO and GitHub (~12 min at default interval)
|
|
@@ -1870,6 +1878,13 @@ const ENGINE_DEFAULTS = {
|
|
|
1870
1878
|
ccUseWorkerPool: false, // Sub-task C of W-mp2w003600196c51 (CC perf): when true AND CC runtime is copilot, _invokeCcStream routes through engine/cc-worker-pool.js (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI per turn. Off by default — opt-in feature flag. **Structurally copilot-only**: the pool spawns `copilot --acp` (Agent Client Protocol); Claude Code does not implement ACP, so resolveCcUseWorkerPool returns false on non-copilot CC runtimes even with explicit-true (W-mphlriic00095f69 — prevents silent runtime switch). Engine/agent dispatch path stays per-process regardless.
|
|
1871
1879
|
maxBudgetUsd: undefined, // fleet USD ceiling for --max-budget-usd (per-agent override: agents.<id>.maxBudgetUsd). Honors 0 via ?? so a literal cap of $0 works
|
|
1872
1880
|
disableModelDiscovery: false, // skip runtime.listModels() REST calls fleet-wide (settings UI falls back to free-text)
|
|
1881
|
+
// W-mpmwxkrw000872ec — dashboard global font-size scale. Drives the
|
|
1882
|
+
// [data-font-size] attribute on <html> via the inline bootstrap script in
|
|
1883
|
+
// layout.html (localStorage fast path) and is reconciled from the server
|
|
1884
|
+
// value on cold loads. Valid: 'small' (current default), 'medium', 'large',
|
|
1885
|
+
// 'xlarge'. Allowlist validation lives in handleSettingsUpdate so invalid
|
|
1886
|
+
// values are clamped rather than silently breaking the bootstrap.
|
|
1887
|
+
fontSize: 'small',
|
|
1873
1888
|
maxPendingContexts: 20, // cap pendingContexts arrays in cooldowns.json to prevent unbounded growth
|
|
1874
1889
|
maxPendingContextEntryBytes: 256 * 1024, // 256 KB — cap each pendingContexts entry to prevent huge PR comments from bloating cooldowns.json
|
|
1875
1890
|
maxDispatchPromptBytes: 1024 * 1024, // 1 MB — dispatch items with prompts larger than this sidecar to engine/contexts/ to prevent dispatch.json OOM (#1167)
|
package/engine.js
CHANGED
|
@@ -4929,15 +4929,16 @@ async function discoverFromPrs(config, project) {
|
|
|
4929
4929
|
}, `Fix build failure on ${pr.id}: ${pr.title || ''}`, { dispatchKey: key, cooldownKey: key, automationCauseKey: buildCauseKey, source: 'pr', pr, branch: prBranch, project: projMeta });
|
|
4930
4930
|
if (item) {
|
|
4931
4931
|
newWork.push(item); fixDispatched = true;
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4932
|
+
// W-mpoeirqx0007712a — DO NOT stamp `_buildFixPushedAt` at dispatch
|
|
4933
|
+
// time. The optimistic stamp here used to suppress re-dispatch for
|
|
4934
|
+
// the buildFixGracePeriod window even when the agent never pushed
|
|
4935
|
+
// (stale-worktree push silently rejected, agent reported SUCCESS
|
|
4936
|
+
// anyway). `_buildFixPushedAt` is now written only by
|
|
4937
|
+
// lifecycle.updatePrAfterFix after the post-completion branchChange
|
|
4938
|
+
// confirms the remote head actually advanced (evidence ===
|
|
4939
|
+
// 'remote-head'). In-flight dispatches are already deduplicated by
|
|
4940
|
+
// `isPrAutomationCausePending` + `isAlreadyDispatched` above, so no
|
|
4941
|
+
// race window opens by removing the optimistic stamp.
|
|
4941
4942
|
}
|
|
4942
4943
|
|
|
4943
4944
|
if (pr.agent && !pr._buildFailNotified) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2056",
|
|
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"
|