@yemi33/minions 0.1.2044 → 0.1.2046
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/README.md +2 -2
- package/dashboard/js/command-center.js +64 -7
- package/dashboard/js/fre.js +3 -2
- package/dashboard/js/refresh.js +143 -2
- package/dashboard/js/render-prs.js +43 -9
- package/dashboard/js/settings.js +9 -5
- package/dashboard/styles.css +21 -0
- package/dashboard.js +308 -164
- package/docs/auto-discovery.md +3 -1
- package/docs/qa-runbook-lifecycle.md +71 -0
- package/docs/qa-runbooks.md +6 -5
- package/docs/runtime-adapters.md +9 -4
- package/docs/security.md +2 -1
- package/docs/watches.md +19 -19
- package/engine/cc-worker-pool.js +87 -11
- package/engine/cleanup.js +84 -2
- package/engine/dispatch.js +6 -0
- package/engine/kb-sweep.js +127 -0
- package/engine/lifecycle.js +18 -0
- package/engine/llm.js +148 -2
- package/engine/preflight.js +5 -5
- package/engine/queries.js +133 -27
- package/engine/shared.js +40 -3
- package/engine/timeout.js +4 -0
- package/engine.js +240 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ Inspired by and initially scaffolded from [Brady Gaster's Squad](https://bradyga
|
|
|
11
11
|
## Prerequisites
|
|
12
12
|
|
|
13
13
|
- **Node.js** 18+ (LTS recommended)
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
14
|
+
- **A supported runtime CLI** — Minions defaults to GitHub Copilot CLI (`npm install -g @github/copilot`). Claude Code CLI (`npm install -g @anthropic-ai/claude-code`) is also supported; switch with `minions config set-cli claude` or per-agent `cli` overrides.
|
|
15
|
+
- **Auth for your runtime** — GitHub Copilot subscription (Copilot CLI handles its own auth) or an Anthropic API key / Claude Max subscription
|
|
16
16
|
- **Git** — agents create worktrees for all code changes
|
|
17
17
|
|
|
18
18
|
> **Note:** you do **not** need to configure your CLI for "autopilot" / "bypass permissions" / "dangerous mode". Minions passes the right bypass flag per spawn (`--dangerously-skip-permissions` for Claude; `--autopilot --allow-all --no-ask-user` for Copilot), independent of your global CLI config. Run `minions doctor` to verify your installed CLI accepts those flags.
|
|
@@ -194,6 +194,35 @@ function _ccIsNewDashboardInstance(preRestartDashId, newDashId, clickTimeMs) {
|
|
|
194
194
|
return Number.isFinite(parsed) && parsed > clickTimeMs;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Shown when ccRestartMinions's POST to /api/dashboard/restart can't be
|
|
198
|
+
// delivered — i.e. the dashboard process itself is dead, not just stale. In
|
|
199
|
+
// that state the in-browser button cannot bring anything back up: a detached
|
|
200
|
+
// `minions restart` child is only spawned if the POST reaches the dashboard.
|
|
201
|
+
// The recovery path lives outside the browser. Surface that fact explicitly
|
|
202
|
+
// instead of polling for a new dashboardStartedAt that never arrives.
|
|
203
|
+
function _ccShowDashboardDeadFallback(btn, reason) {
|
|
204
|
+
var humanReason = reason || 'connection refused';
|
|
205
|
+
var msg = 'The dashboard process appears to be down — the Restart button can\'t reach it (' + humanReason + ').\n\nRun this in your terminal to recover:\n\n minions restart';
|
|
206
|
+
if (btn) {
|
|
207
|
+
try {
|
|
208
|
+
btn.disabled = false;
|
|
209
|
+
btn.textContent = 'Run `minions restart` in terminal';
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
var copyOk = function() {
|
|
213
|
+
if (typeof showToast === 'function') showToast('cmd-toast', '`minions restart` copied — paste in your terminal', true);
|
|
214
|
+
};
|
|
215
|
+
var copyFail = function() {
|
|
216
|
+
try { window.prompt('Copy this command, then run it in your terminal:', 'minions restart'); }
|
|
217
|
+
catch { try { alert(msg); } catch {} }
|
|
218
|
+
};
|
|
219
|
+
try {
|
|
220
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
221
|
+
navigator.clipboard.writeText('minions restart').then(copyOk).catch(copyFail);
|
|
222
|
+
} else { copyFail(); }
|
|
223
|
+
} catch { copyFail(); }
|
|
224
|
+
}
|
|
225
|
+
|
|
197
226
|
// Triggered by the CC "Restart Minions" recovery button when a stale dashboard
|
|
198
227
|
// connection is killing CC streams with "Failed to fetch". Spawns the same
|
|
199
228
|
// `minions restart` flow as the CLI command (kills + respawns engine AND
|
|
@@ -242,14 +271,42 @@ async function ccRestartMinions(btn) {
|
|
|
242
271
|
} catch { /* best-effort — clickTime fallback inside helper still covers us */ }
|
|
243
272
|
}
|
|
244
273
|
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
274
|
+
// Briefly await the POST so we can distinguish two failure shapes that look
|
|
275
|
+
// identical from a fire-and-forget callsite:
|
|
276
|
+
//
|
|
277
|
+
// (a) Dashboard alive when POST landed but died mid-response. The spawned
|
|
278
|
+
// restart child is running — polling /api/status WILL eventually see
|
|
279
|
+
// a new dashboardStartedAt and the existing reload path works.
|
|
280
|
+
//
|
|
281
|
+
// (b) Dashboard already dead when the user clicked. ECONNREFUSED before
|
|
282
|
+
// any byte hits the wire. No restart child was spawned, polling would
|
|
283
|
+
// wait the full 90 s and time out into a doomed reload (browser
|
|
284
|
+
// lands on a port-not-listening error). This is the case operators
|
|
285
|
+
// most often hit — the user just witnessed it today.
|
|
286
|
+
//
|
|
287
|
+
// 4 s is enough for the POST to deliver headers on a healthy box. If we
|
|
288
|
+
// saw EITHER res.ok OR a delayed-disconnect-with-headers-seen, assume the
|
|
289
|
+
// spawned child is on its way and start the polling loop. Otherwise fall
|
|
290
|
+
// back to the terminal-runnable command.
|
|
291
|
+
var postCtl = new AbortController();
|
|
292
|
+
var postTimer = setTimeout(function() { postCtl.abort(); }, 4000);
|
|
293
|
+
var postDelivered = false;
|
|
294
|
+
var postError = null;
|
|
249
295
|
try {
|
|
250
|
-
fetch('/api/dashboard/restart', { method: 'POST', headers: { 'Content-Type': 'application/json' } })
|
|
251
|
-
|
|
252
|
-
|
|
296
|
+
var postRes = await fetch('/api/dashboard/restart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: postCtl.signal });
|
|
297
|
+
if (postRes && postRes.ok) postDelivered = true;
|
|
298
|
+
else postError = 'HTTP ' + (postRes ? postRes.status : '?');
|
|
299
|
+
} catch (e) {
|
|
300
|
+
postError = String((e && e.message) || e);
|
|
301
|
+
} finally {
|
|
302
|
+
clearTimeout(postTimer);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!postDelivered) {
|
|
306
|
+
_ccShowDashboardDeadFallback(btn, postError);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
253
310
|
if (btn) { try { btn.textContent = 'Restarting Minions — waiting for new dashboard...'; } catch {} }
|
|
254
311
|
|
|
255
312
|
var startedAt = Date.now();
|
package/dashboard/js/fre.js
CHANGED
|
@@ -80,9 +80,10 @@ function renderFre(statusOrProjects) {
|
|
|
80
80
|
|
|
81
81
|
// Resolve the currently-configured runtime CLI for the explainer copy.
|
|
82
82
|
// /api/status surfaces this as autoMode.defaultCli (resolveAgentCli(null, engine)).
|
|
83
|
-
// Fall back to autoMode.ccCli (also defaultCli-derived when ccCli unset) then '
|
|
83
|
+
// Fall back to autoMode.ccCli (also defaultCli-derived when ccCli unset) then 'copilot'
|
|
84
|
+
// (matches ENGINE_DEFAULTS.defaultCli — W-mpmwxkk40007c995).
|
|
84
85
|
const auto = (status && status.autoMode) || {};
|
|
85
|
-
const runtimeCli = String(auto.defaultCli || auto.ccCli || '
|
|
86
|
+
const runtimeCli = String(auto.defaultCli || auto.ccCli || 'copilot');
|
|
86
87
|
|
|
87
88
|
const cardStyle = [
|
|
88
89
|
'margin:12px 24px',
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -71,7 +71,7 @@ const RENDER_VERSIONS = {
|
|
|
71
71
|
projects: 1,
|
|
72
72
|
notes: 1,
|
|
73
73
|
prd: 1,
|
|
74
|
-
prs:
|
|
74
|
+
prs: 2,
|
|
75
75
|
archivedPrds: 1,
|
|
76
76
|
engine: 2,
|
|
77
77
|
version: 1,
|
|
@@ -206,7 +206,16 @@ function _processStatusUpdate(data) {
|
|
|
206
206
|
|
|
207
207
|
|
|
208
208
|
// Render only changed sections
|
|
209
|
-
|
|
209
|
+
// Agents is exempt from the _changed gate: real-time status correctness on
|
|
210
|
+
// the Minions Members grid (status badge, running timer, Last-run line)
|
|
211
|
+
// beats the cost of re-rendering 5 cards every poll tick. The gate was
|
|
212
|
+
// causing visible staleness when the ref-eq / JSON-stringify short-circuit
|
|
213
|
+
// falsely matched across ticks (W-mpn7keq9000302c9). Still call _changed
|
|
214
|
+
// here so the _lastChangedFlags diag ring-buffer keeps recording whether
|
|
215
|
+
// the agents payload actually moved this tick.
|
|
216
|
+
_changed('agents', data.agents);
|
|
217
|
+
renderAgents(data.agents);
|
|
218
|
+
cmdUpdateAgentList(data.agents);
|
|
210
219
|
if (_changed('prdProgress', data.prdProgress) || _changed('prdPrs', data.pullRequests?.length)) { renderPrdProgress(data.prdProgress); _cachePrdItems(data.prdProgress); }
|
|
211
220
|
if (_changed('inbox', data.inbox)) renderInbox(data.inbox || []);
|
|
212
221
|
if (_changed('projects', data.projects)) { cmdUpdateProjectList(data.projects || []); renderProjects(data.projects || []); }
|
|
@@ -343,6 +352,116 @@ let _lastStatusData = null;
|
|
|
343
352
|
// fresh state anyway.
|
|
344
353
|
let _refreshInFlight = false;
|
|
345
354
|
|
|
355
|
+
// ── Dashboard-unreachable detector ───────────────────────────────────────
|
|
356
|
+
// When the dashboard process dies, /api/status throws or 5xxs and the
|
|
357
|
+
// existing catch block just console.errors — the page keeps painting the
|
|
358
|
+
// last successful snapshot. Operators have reported the resulting symptom
|
|
359
|
+
// many times: badge says "running", CC POST throws "Failed to fetch", and
|
|
360
|
+
// it's not obvious the dashboard itself is dead (vs. wedged, vs. the
|
|
361
|
+
// engine being down).
|
|
362
|
+
//
|
|
363
|
+
// Trip conditions: 2 consecutive failed polls OR >12 s since the last
|
|
364
|
+
// success (3× the 4 s poll cadence, so a single flaky tick doesn't fire).
|
|
365
|
+
// On trip we show a sticky red banner with two recovery actions:
|
|
366
|
+
// 1. "Restart Minions" → ccRestartMinions (works when dashboard is
|
|
367
|
+
// alive-but-stale; falls through to copy-to-clipboard when the POST
|
|
368
|
+
// itself fails)
|
|
369
|
+
// 2. "Copy minions restart" → terminal fallback for the dashboard-is-
|
|
370
|
+
// truly-dead case the in-browser button can't fix on its own
|
|
371
|
+
// Also overrides the engine badge to UNKNOWN/muted so the misleading
|
|
372
|
+
// "RUNNING" pill stops showing while data is frozen.
|
|
373
|
+
let _lastStatusOkAt = Date.now();
|
|
374
|
+
let _consecutiveStatusFails = 0;
|
|
375
|
+
let _unreachableSince = 0; // 0 = currently reachable
|
|
376
|
+
let _unreachableAgeTimer = null;
|
|
377
|
+
const _UNREACHABLE_FAIL_THRESHOLD = 2;
|
|
378
|
+
const _UNREACHABLE_AGE_MS = 12000;
|
|
379
|
+
|
|
380
|
+
function _formatAge(ms) {
|
|
381
|
+
if (ms < 1000) return 'just now';
|
|
382
|
+
const s = Math.round(ms / 1000);
|
|
383
|
+
if (s < 60) return s + 's ago';
|
|
384
|
+
const m = Math.floor(s / 60);
|
|
385
|
+
const rem = s % 60;
|
|
386
|
+
return rem ? m + 'm ' + rem + 's ago' : m + 'm ago';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function _refreshUnreachableAgeText() {
|
|
390
|
+
if (!_unreachableSince) return;
|
|
391
|
+
// The age span is rendered into the shared #engine-alert element by
|
|
392
|
+
// _markDashboardUnreachable; lookup by ID survives any inner-HTML rebuild
|
|
393
|
+
// as long as the markup keeps the span around (it does — single source).
|
|
394
|
+
const el = document.getElementById('dashboard-unreachable-age');
|
|
395
|
+
if (el) el.textContent = _formatAge(Date.now() - _lastStatusOkAt);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function _markDashboardUnreachable(err) {
|
|
399
|
+
if (_unreachableSince) {
|
|
400
|
+
_refreshUnreachableAgeText();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
_unreachableSince = Date.now();
|
|
404
|
+
window._dashboardUnreachable = {
|
|
405
|
+
since: _unreachableSince,
|
|
406
|
+
lastSuccessAt: _lastStatusOkAt,
|
|
407
|
+
lastError: String(err && err.message || err || 'unknown'),
|
|
408
|
+
};
|
|
409
|
+
// Reuse the existing #engine-alert surface (red-tinted banner already wired
|
|
410
|
+
// for engine-stale, ado-throttle, gh-throttle) instead of introducing a
|
|
411
|
+
// second red banner. Engine-stale and dashboard-unreachable are mutually
|
|
412
|
+
// exclusive in practice: engine-stale needs a fresh heartbeat from a
|
|
413
|
+
// successful poll, and a successful poll means the dashboard IS reachable.
|
|
414
|
+
// When the dashboard recovers, _markDashboardReachable hides this element
|
|
415
|
+
// explicitly; the next renderEngineAlert pass (driven by the recovered
|
|
416
|
+
// poll's engine state) then takes over normally.
|
|
417
|
+
const el = document.getElementById('engine-alert');
|
|
418
|
+
if (el) {
|
|
419
|
+
el.innerHTML =
|
|
420
|
+
'<span class="engine-alert-msg">⚠️ Dashboard unreachable — stale <span id="dashboard-unreachable-age">just now</span></span>' +
|
|
421
|
+
'<span class="engine-alert-action" onclick="ccRestartMinions(this)">Restart Minions</span>';
|
|
422
|
+
el.style.display = 'flex';
|
|
423
|
+
}
|
|
424
|
+
_refreshUnreachableAgeText();
|
|
425
|
+
if (_unreachableAgeTimer) clearInterval(_unreachableAgeTimer);
|
|
426
|
+
_unreachableAgeTimer = setInterval(_refreshUnreachableAgeText, 1000);
|
|
427
|
+
// Override engine badge so the cached "RUNNING" doesn't keep misleading
|
|
428
|
+
// the user. renderEngineStatus reads engine state from the next
|
|
429
|
+
// successful poll — when reachable recovers, the override clears.
|
|
430
|
+
const badge = document.getElementById('engine-badge');
|
|
431
|
+
if (badge) {
|
|
432
|
+
badge.className = 'engine-badge stopped';
|
|
433
|
+
badge.textContent = 'UNKNOWN';
|
|
434
|
+
badge.title = 'Dashboard unreachable — engine state is unknown. UI data is stale.';
|
|
435
|
+
}
|
|
436
|
+
console.warn('Dashboard unreachable:', window._dashboardUnreachable.lastError);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function _markDashboardReachable() {
|
|
440
|
+
if (!_unreachableSince) return;
|
|
441
|
+
const downForMs = Date.now() - _unreachableSince;
|
|
442
|
+
_unreachableSince = 0;
|
|
443
|
+
delete window._dashboardUnreachable;
|
|
444
|
+
if (_unreachableAgeTimer) { clearInterval(_unreachableAgeTimer); _unreachableAgeTimer = null; }
|
|
445
|
+
// Hand #engine-alert back to renderEngineAlert. We can't leave our content
|
|
446
|
+
// sitting in there because the next renderEngineAlert call only runs when
|
|
447
|
+
// `_changed('engine', data.engine)` returns true — if engine state hasn't
|
|
448
|
+
// shifted, our stale banner would persist past recovery.
|
|
449
|
+
const el = document.getElementById('engine-alert');
|
|
450
|
+
if (el) { el.style.display = 'none'; el.innerHTML = ''; }
|
|
451
|
+
console.log('Dashboard recovered after', _formatAge(downForMs).replace(' ago', ''));
|
|
452
|
+
// Badge restoration happens automatically on the next renderEngineStatus
|
|
453
|
+
// call (triggered by the successful refresh that brought us here).
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Test seam — reset detector state between scenarios.
|
|
457
|
+
window._resetDashboardUnreachableForTest = function() {
|
|
458
|
+
_lastStatusOkAt = Date.now();
|
|
459
|
+
_consecutiveStatusFails = 0;
|
|
460
|
+
_unreachableSince = 0;
|
|
461
|
+
if (_unreachableAgeTimer) { clearInterval(_unreachableAgeTimer); _unreachableAgeTimer = null; }
|
|
462
|
+
delete window._dashboardUnreachable;
|
|
463
|
+
};
|
|
464
|
+
|
|
346
465
|
// ── Refresh diagnostics (W-mphejzx100081972) ─────────────────────────────
|
|
347
466
|
// Ring buffer capturing the last 50 /api/status poll cycles so a user
|
|
348
467
|
// reporting "the dashboard didn't auto-update when X changed" can paste
|
|
@@ -418,6 +537,11 @@ async function refresh() {
|
|
|
418
537
|
const headers = {};
|
|
419
538
|
if (_lastStatusEtag) headers['If-None-Match'] = _lastStatusEtag;
|
|
420
539
|
const res = await safeFetch('/api/status', { headers });
|
|
540
|
+
if (!res || (!res.ok && res.status !== 304)) {
|
|
541
|
+
// Dashboard responded but with an error status. Treat as a fail tick so
|
|
542
|
+
// a 5xx-storm trips the unreachable banner just like a network error.
|
|
543
|
+
throw new Error('HTTP ' + (res ? res.status : '?'));
|
|
544
|
+
}
|
|
421
545
|
let data;
|
|
422
546
|
if (res.status === 304 && _lastStatusData) {
|
|
423
547
|
// Cache hit — reuse last payload, skip parsing entirely.
|
|
@@ -425,6 +549,12 @@ async function refresh() {
|
|
|
425
549
|
if (_diagEntry) {
|
|
426
550
|
_diagEntry.response_status = '304';
|
|
427
551
|
_diagEntry.bytes_received = 0;
|
|
552
|
+
// D2: capture etag on 304 so the diag table can show whether the
|
|
553
|
+
// server's ETag advanced even when we're reusing the cached body.
|
|
554
|
+
// Without this the "etag↓" column is blank on every 304 row and the
|
|
555
|
+
// operator can't tell server-side advancement from a pinned cache.
|
|
556
|
+
const etag304 = res.headers && (res.headers.get ? res.headers.get('etag') : null);
|
|
557
|
+
if (etag304) _diagEntry.etag_received = etag304;
|
|
428
558
|
}
|
|
429
559
|
} else {
|
|
430
560
|
data = await res.json();
|
|
@@ -458,6 +588,12 @@ async function refresh() {
|
|
|
458
588
|
return;
|
|
459
589
|
}
|
|
460
590
|
if (buildId) _knownDashboardBuildId = buildId;
|
|
591
|
+
// Successful poll — clear unreachable state. Placed AFTER the reload
|
|
592
|
+
// guards above so a dashboard restart still triggers location.reload()
|
|
593
|
+
// instead of just dismissing the banner.
|
|
594
|
+
_lastStatusOkAt = Date.now();
|
|
595
|
+
_consecutiveStatusFails = 0;
|
|
596
|
+
if (_unreachableSince) _markDashboardReachable();
|
|
461
597
|
const _renderStart = _diagOn ? Date.now() : 0;
|
|
462
598
|
let _diagChanges = null;
|
|
463
599
|
if (_diagOn) {
|
|
@@ -484,6 +620,11 @@ async function refresh() {
|
|
|
484
620
|
_diagEntry.response_status = (e && e.name === 'AbortError') ? 'abort' : 'error';
|
|
485
621
|
_diagEntry.error_message = String((e && e.message) || e);
|
|
486
622
|
}
|
|
623
|
+
_consecutiveStatusFails++;
|
|
624
|
+
const ageMs = Date.now() - _lastStatusOkAt;
|
|
625
|
+
if (_consecutiveStatusFails >= _UNREACHABLE_FAIL_THRESHOLD || ageMs > _UNREACHABLE_AGE_MS) {
|
|
626
|
+
_markDashboardUnreachable(e);
|
|
627
|
+
}
|
|
487
628
|
}
|
|
488
629
|
finally {
|
|
489
630
|
_refreshInFlight = false;
|
|
@@ -51,22 +51,56 @@ function prRow(pr) {
|
|
|
51
51
|
var followupChip = followupCount > 0
|
|
52
52
|
? ' <span class="pr-badge draft" style="font-size:8px" title="' + followupCount + ' follow-up work item(s) dispatched from comments on this PR">+' + followupCount + ' follow-up' + (followupCount === 1 ? '' : 's') + '</span>'
|
|
53
53
|
: '';
|
|
54
|
+
const titleText = pr.title || 'Untitled';
|
|
55
|
+
const agentText = pr.agent || '—';
|
|
56
|
+
const reviewerCell = sq.reviewer && sq.status !== 'waiting'
|
|
57
|
+
? '<span class="pr-agent" title="' + escapeHtml(sq.note || sq.reviewer) + '">' + escapeHtml(sq.reviewer) + '</span>'
|
|
58
|
+
: sq.reviewer && sq.status === 'waiting'
|
|
59
|
+
? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escapeHtml(sq.reviewer) + '…</span>'
|
|
60
|
+
: pr.reviewedBy && pr.reviewedBy.length
|
|
61
|
+
? '<span class="pr-agent" title="' + escapeHtml(pr.reviewedBy.join(', ')) + '">' + escapeHtml(pr.reviewedBy.join(', ')) + '</span>'
|
|
62
|
+
: '<span style="color:var(--muted);font-size:11px">—</span>';
|
|
63
|
+
const createdLabel = (pr.created || '—').slice(0, 16).replace('T', ' ');
|
|
64
|
+
// Title attrs live on the inner element (link/span/badge) so hovering the
|
|
65
|
+
// ellipsis-truncated content reveals the full text. Cell tags stay bare so
|
|
66
|
+
// the header-to-cell count assertion in test/unit.test.js continues to
|
|
67
|
+
// balance.
|
|
54
68
|
return '<tr>' +
|
|
55
|
-
'<td><span class="pr-id">' + escapeHtml(String(prId)) + '</span></td>' +
|
|
56
|
-
'<td><a class="pr-title" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(
|
|
57
|
-
'<td><span class="pr-agent"
|
|
69
|
+
'<td><span class="pr-id" title="' + escapeHtml(String(prId)) + '">' + escapeHtml(String(prId)) + '</span></td>' +
|
|
70
|
+
'<td><a class="pr-title" title="' + escapeHtml(titleText) + '" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(titleText) + '</a>' + followupChip + (pr.description ? '<div class="pr-desc" title="' + escapeHtml(pr.description) + '">' + escapeHtml(pr.description.length > 120 ? pr.description.slice(0, 120) + '...' : pr.description) + '</div>' : '') + '</td>' +
|
|
71
|
+
'<td><span class="pr-agent" title="' + escapeHtml(agentText) + '">' + escapeHtml(agentText) + '</span></td>' +
|
|
58
72
|
'<td><span class="' + branchClass + '" title="' + escapeHtml(branchError || branchLabel) + '">' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
|
|
59
|
-
'<td><span class="pr-badge ' + reviewClass + '"
|
|
60
|
-
'<td>' +
|
|
61
|
-
'<td><span class="pr-badge ' + buildClass + '"
|
|
62
|
-
'<td><span class="pr-badge ' + statusClass + '">' + escapeHtml(statusLabel) + '</span></td>' +
|
|
63
|
-
'<td><span class="pr-date"
|
|
73
|
+
'<td><span class="pr-badge ' + reviewClass + '" title="' + escapeHtml(reviewTitle || reviewLabel) + '">' + escapeHtml(reviewLabel) + '</span></td>' +
|
|
74
|
+
'<td>' + reviewerCell + '</td>' +
|
|
75
|
+
'<td><span class="pr-badge ' + buildClass + '" title="' + escapeHtml(buildTitle || buildLabel) + '">' + escapeHtml(buildLabel) + '</span></td>' +
|
|
76
|
+
'<td><span class="pr-badge ' + statusClass + '" title="' + escapeHtml(statusLabel) + '">' + escapeHtml(statusLabel) + '</span></td>' +
|
|
77
|
+
'<td><span class="pr-date" title="' + escapeHtml(createdLabel) + '">' + escapeHtml(createdLabel) + '</span></td>' +
|
|
64
78
|
'<td><button class="pr-pager-btn" style="font-size:9px;padding:1px 5px;color:var(--red);border-color:var(--red)" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
|
|
65
79
|
'</tr>';
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
// Explicit per-column widths keep the PR table from ballooning when titles or
|
|
83
|
+
// branches are long. Total ≈1420px → table grows past viewport on narrow
|
|
84
|
+
// windows and the .pr-table-wrap--prs container scrolls horizontally inside
|
|
85
|
+
// the viewport (sticky scrollbar — see styles.css).
|
|
86
|
+
const PRS_COLGROUP =
|
|
87
|
+
'<colgroup>' +
|
|
88
|
+
'<col style="width:75px">' + // PR id
|
|
89
|
+
'<col style="width:320px">' + // Title
|
|
90
|
+
'<col style="width:140px">' + // Agent
|
|
91
|
+
'<col style="width:200px">' + // Branch
|
|
92
|
+
'<col style="width:130px">' + // Review
|
|
93
|
+
'<col style="width:140px">' + // Signed Off By
|
|
94
|
+
'<col style="width:130px">' + // Build
|
|
95
|
+
'<col style="width:110px">' + // Status
|
|
96
|
+
'<col style="width:130px">' + // Created
|
|
97
|
+
'<col style="width:50px">' + // Actions
|
|
98
|
+
'</colgroup>';
|
|
99
|
+
|
|
68
100
|
function prTableHtml(rows) {
|
|
69
|
-
return '<div class="pr-table-wrap"><table class="pr-table"
|
|
101
|
+
return '<div class="pr-table-wrap pr-table-wrap--prs"><table class="pr-table pr-table--prs">' +
|
|
102
|
+
PRS_COLGROUP +
|
|
103
|
+
'<thead><tr>' +
|
|
70
104
|
'<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th>Signed Off By</th><th>Build</th><th>Status</th><th>Created</th><th></th>' +
|
|
71
105
|
'</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
|
72
106
|
}
|
package/dashboard/js/settings.js
CHANGED
|
@@ -49,7 +49,7 @@ async function openSettings() {
|
|
|
49
49
|
// Per-agent override placeholders surface the inherited fleet defaults as
|
|
50
50
|
// muted text — operators see exactly what each agent will resolve to without
|
|
51
51
|
// chasing config files. Empty input clears the override → re-inherit fleet.
|
|
52
|
-
const fleetCliLabel = e.defaultCli || '
|
|
52
|
+
const fleetCliLabel = e.defaultCli || 'copilot';
|
|
53
53
|
const fleetModelLabel = e.defaultModel ? String(e.defaultModel) : 'CLI default';
|
|
54
54
|
const agentRows = Object.entries(agents).map(function([id, a]) {
|
|
55
55
|
return '<tr>' +
|
|
@@ -98,6 +98,7 @@ async function openSettings() {
|
|
|
98
98
|
settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks') +
|
|
99
99
|
settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
|
|
100
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.') +
|
|
101
102
|
settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in)') +
|
|
102
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.') +
|
|
103
104
|
'</div>' +
|
|
@@ -127,6 +128,7 @@ async function openSettings() {
|
|
|
127
128
|
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
|
|
128
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)') +
|
|
129
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).') +
|
|
130
132
|
settingsField('Version Check Interval', 'set-versionCheckInterval', e.versionCheckInterval || 3600000, 'ms', 'How often to check npm for updates (default: 1 hour)') +
|
|
131
133
|
settingsField('Ignored Comment Authors', 'set-ignoredCommentAuthors', (e.ignoredCommentAuthors || []).join(', '), '', 'Comma-separated usernames — comments auto-closed, never trigger fixes') +
|
|
132
134
|
'</div>' +
|
|
@@ -404,10 +406,10 @@ async function initRuntimeFleetUI(engineCfg, agentsCfg) {
|
|
|
404
406
|
runtimes = Array.isArray(d.runtimes) ? d.runtimes : [];
|
|
405
407
|
} catch { /* ignore — we'll surface a free-text-only path below */ }
|
|
406
408
|
|
|
407
|
-
// Always include '
|
|
409
|
+
// Always include 'copilot' as a fallback option even if /api/runtimes is empty;
|
|
408
410
|
// legacy installs without the registry endpoint should still see something pickable.
|
|
409
|
-
const names = runtimes.length ? runtimes.map(rt => rt.name) : ['
|
|
410
|
-
const currentDefault = engineCfg.defaultCli || '
|
|
411
|
+
const names = runtimes.length ? runtimes.map(rt => rt.name) : ['copilot'];
|
|
412
|
+
const currentDefault = engineCfg.defaultCli || 'copilot';
|
|
411
413
|
const currentCc = engineCfg.ccCli || '';
|
|
412
414
|
cliSelect.innerHTML = names.map(n =>
|
|
413
415
|
'<option value="' + escHtml(n) + '"' + (n === currentDefault ? ' selected' : '') + '>' + escHtml(n) + '</option>'
|
|
@@ -438,7 +440,7 @@ async function initRuntimeFleetUI(engineCfg, agentsCfg) {
|
|
|
438
440
|
// this the input was free-text and a user could (and did) save an agent
|
|
439
441
|
// with cli=claude + model=<some gpt> — invalid combination that crashed
|
|
440
442
|
// dispatch. Refreshing on CLI change clears stale model values.
|
|
441
|
-
const fleetDefaultCli = engineCfg.defaultCli || '
|
|
443
|
+
const fleetDefaultCli = engineCfg.defaultCli || 'copilot';
|
|
442
444
|
for (const cell of cliCells) {
|
|
443
445
|
const agentId = cell.getAttribute('data-runtime-cli');
|
|
444
446
|
const agent = (agentsCfg || {})[agentId] || {};
|
|
@@ -606,6 +608,7 @@ async function saveSettings() {
|
|
|
606
608
|
autoDecompose: document.getElementById('set-autoDecompose').checked,
|
|
607
609
|
allowTempAgents: document.getElementById('set-allowTempAgents').checked,
|
|
608
610
|
autoArchive: document.getElementById('set-autoArchive').checked,
|
|
611
|
+
autoConsolidateMemory: document.getElementById('set-autoConsolidateMemory').checked,
|
|
609
612
|
autoApplyReviewVote: document.getElementById('set-autoApplyReviewVote').checked,
|
|
610
613
|
autoFixBuilds: document.getElementById('set-autoFixBuilds').checked,
|
|
611
614
|
autoFixConflicts: document.getElementById('set-autoFixConflicts').checked,
|
|
@@ -621,6 +624,7 @@ async function saveSettings() {
|
|
|
621
624
|
prPollCommentsEvery: document.getElementById('set-prPollCommentsEvery').value,
|
|
622
625
|
evalMaxCost: document.getElementById('set-evalMaxCost').value || null,
|
|
623
626
|
agentBusyReassignMs: document.getElementById('set-agentBusyReassignMs').value,
|
|
627
|
+
maxRetriesPerAgent: document.getElementById('set-maxRetriesPerAgent').value,
|
|
624
628
|
ignoredCommentAuthors: document.getElementById('set-ignoredCommentAuthors').value,
|
|
625
629
|
versionCheckInterval: document.getElementById('set-versionCheckInterval').value,
|
|
626
630
|
// Runtime fleet (P-7a5c1f8e). Empty strings are intentional — they signal
|
package/dashboard/styles.css
CHANGED
|
@@ -260,6 +260,27 @@
|
|
|
260
260
|
.pr-table-wrap { overflow-x: auto; }
|
|
261
261
|
.pr-table { width: 100%; border-collapse: collapse; font-size: var(--text-md); table-layout: auto; }
|
|
262
262
|
.pr-table th:last-child, .pr-table td:last-child { width: 36px; min-width: 36px; text-align: center; }
|
|
263
|
+
|
|
264
|
+
/* PR-page table variant (W-mpmwxn9h000bd2c2): fixed column widths with
|
|
265
|
+
ellipsis overflow, and the horizontal scrollbar is pinned inside the
|
|
266
|
+
viewport via a bounded-height container on the standalone /prs page so
|
|
267
|
+
it stays reachable without scrolling to the bottom of a tall table. */
|
|
268
|
+
.pr-table--prs { table-layout: fixed; width: 100%; min-width: 1420px; }
|
|
269
|
+
.pr-table--prs th, .pr-table--prs td { overflow: hidden; text-overflow: ellipsis; }
|
|
270
|
+
.pr-table--prs th:last-child, .pr-table--prs td:last-child { width: auto; min-width: 0; }
|
|
271
|
+
.pr-table--prs .pr-title { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
272
|
+
.pr-table--prs .pr-agent { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: middle; white-space: nowrap; }
|
|
273
|
+
.pr-table--prs .pr-branch { max-width: 100%; }
|
|
274
|
+
.pr-table--prs .pr-desc { max-width: 100%; }
|
|
275
|
+
.pr-table--prs .pr-date { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
|
|
276
|
+
|
|
277
|
+
/* Standalone /prs page only: bound the table-wrap height so the horizontal
|
|
278
|
+
scrollbar (bottom of the wrap) and the table header (sticky inside the
|
|
279
|
+
wrap) stay visible while the user is on this page. The modal "see all"
|
|
280
|
+
view uses the same colgroup but is unaffected — modal-body handles its
|
|
281
|
+
own scrolling. */
|
|
282
|
+
#pr-content .pr-table-wrap--prs { max-height: calc(100vh - 200px); overflow: auto; }
|
|
283
|
+
#pr-content .pr-table--prs thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
|
|
263
284
|
.pr-table th { text-align: left; color: var(--muted); font-weight: 500; font-size: var(--text-base); text-transform: uppercase; letter-spacing: 0.5px; padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border); }
|
|
264
285
|
.pr-table td { padding: var(--space-5); border-bottom: 1px solid var(--border); vertical-align: middle; white-space: nowrap; }
|
|
265
286
|
.pr-table tr:last-child td { border-bottom: none; }
|