@yemi33/minions 0.1.1950 → 0.1.1951
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/command-center.js +9 -0
- package/dashboard/js/modal-qa.js +10 -0
- package/dashboard/js/refresh.js +4 -0
- package/dashboard/js/render-dispatch.js +25 -0
- package/dashboard/js/render-other.js +109 -2
- package/dashboard/js/settings.js +1 -1
- package/dashboard/layout.html +2 -2
- package/dashboard/pages/engine.html +6 -0
- package/dashboard/slim.html +1987 -0
- package/dashboard/styles.css +8 -0
- package/dashboard.js +450 -40
- package/docs/completion-reports.md +25 -0
- package/docs/design-state-storage.md +1 -1
- package/docs/slim-ux/architecture-suggestions.md +467 -0
- package/docs/slim-ux/concepts.md +824 -0
- package/engine/ado-mcp-wrapper.js +33 -7
- package/engine/ado.js +123 -15
- package/engine/cc-worker-pool.js +41 -0
- package/engine/cleanup.js +71 -34
- package/engine/cli.js +37 -0
- package/engine/dispatch.js +32 -9
- package/engine/features.js +6 -0
- package/engine/gh-token.js +137 -0
- package/engine/github.js +166 -29
- package/engine/issues.js +29 -0
- package/engine/keep-process-sweep.js +397 -0
- package/engine/lifecycle.js +150 -33
- package/engine/playbook.js +17 -0
- package/engine/queries.js +71 -0
- package/engine/recovery.js +6 -0
- package/engine/shared.js +446 -14
- package/engine/spawn-agent.js +44 -2
- package/engine/timeout.js +34 -11
- package/engine/worktree-pool.js +410 -0
- package/engine.js +643 -119
- package/package.json +6 -3
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +3 -1
- package/prompts/cc-system.md +24 -0
- package/engine/copilot-models.json +0 -5
|
@@ -373,6 +373,15 @@ function ccNewTab(skipServerReset) {
|
|
|
373
373
|
ccSaveState();
|
|
374
374
|
var input = document.getElementById('cc-input');
|
|
375
375
|
if (input) input.focus();
|
|
376
|
+
// Fire-and-forget: pre-warm the worker pool so the first message skips the
|
|
377
|
+
// ~18-21 s Copilot cold-spawn. Server no-ops when the pool is off.
|
|
378
|
+
try {
|
|
379
|
+
fetch('/api/cc-sessions/warm', {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: { 'Content-Type': 'application/json' },
|
|
382
|
+
body: JSON.stringify({ tabId: tabId }),
|
|
383
|
+
}).catch(function() { /* swallow — warming is opportunistic */ });
|
|
384
|
+
} catch (_e) { /* swallow */ }
|
|
376
385
|
}
|
|
377
386
|
|
|
378
387
|
function ccSwitchTab(id) {
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -504,6 +504,16 @@ function _initQaSession() {
|
|
|
504
504
|
if (wrap) wrap.style.display = 'none';
|
|
505
505
|
if (expandBar) expandBar.style.display = 'none';
|
|
506
506
|
}
|
|
507
|
+
// Fire-and-forget: pre-warm the worker pool so the user's first question
|
|
508
|
+
// skips the ~18-21 s Copilot cold-spawn. Server no-ops when the pool is off
|
|
509
|
+
// or when a worker for this sessionKey is already warm.
|
|
510
|
+
try {
|
|
511
|
+
fetch('/api/doc-chat/warm', {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers: { 'Content-Type': 'application/json' },
|
|
514
|
+
body: JSON.stringify({ filePath: _modalFilePath || '', title: _modalDocContext.title || '' }),
|
|
515
|
+
}).catch(function() { /* swallow — warming is opportunistic */ });
|
|
516
|
+
} catch (_e) { /* swallow */ }
|
|
507
517
|
}
|
|
508
518
|
|
|
509
519
|
function clearQaConversation() {
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -103,6 +103,10 @@ function _processStatusUpdate(data) {
|
|
|
103
103
|
prunePrdRequeueState(window._lastWorkItems);
|
|
104
104
|
if (_changed('engineLog', data.engineLog)) renderEngineLog(data.engineLog || []);
|
|
105
105
|
if (_changed('metrics', data.metrics)) renderMetrics(data.metrics || {});
|
|
106
|
+
// keep_processes panel only relevant when on engine page; cheap call (one fetch)
|
|
107
|
+
if (typeof renderKeepProcesses === 'function') {
|
|
108
|
+
try { renderKeepProcesses(); } catch {}
|
|
109
|
+
}
|
|
106
110
|
if (_changed('workItems', data.workItems)) renderWorkItems(data.workItems || []);
|
|
107
111
|
if (_changed('skills', data.skills)) renderSkills(data.skills || []);
|
|
108
112
|
if (_changed('mcpServers', data.mcpServers)) renderMcpServers(data.mcpServers || []);
|
|
@@ -114,6 +114,7 @@ function renderDispatch(dispatch) {
|
|
|
114
114
|
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
115
115
|
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
116
116
|
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
117
|
+
renderStuckChip(d) +
|
|
117
118
|
'<span class="dispatch-time">' + shortTime(d.started_at) + '</span>' +
|
|
118
119
|
'</div>'
|
|
119
120
|
).join('') + '</div>';
|
|
@@ -130,6 +131,7 @@ function renderDispatch(dispatch) {
|
|
|
130
131
|
'<span class="dispatch-type ' + (d.type || '') + '">' + escHtml(d.type || '') + '</span>' +
|
|
131
132
|
'<span class="dispatch-agent">' + escHtml(d.agentName || d.agent || '') + '</span>' +
|
|
132
133
|
'<span class="dispatch-task" title="' + escHtml(d.task || '') + '">' + escHtml(d.task || '') + '</span>' +
|
|
134
|
+
renderStuckChip(d) +
|
|
133
135
|
(d.skipReason ? '<span style="font-size:9px;color:var(--muted);margin-left:6px" title="' + escHtml(d.skipReason) + '">' + escHtml(d.skipReason.replace(/_/g, ' ')) + '</span>' : '') +
|
|
134
136
|
'</div>'
|
|
135
137
|
).join('') + '</div>';
|
|
@@ -215,6 +217,29 @@ function shortTime(t) {
|
|
|
215
217
|
return formatLocalTime(t);
|
|
216
218
|
}
|
|
217
219
|
|
|
220
|
+
// Stuck-dispatch warning chip (W-mp62taw2000ubcc3): when a dispatch sits in a
|
|
221
|
+
// pre-spawn state (worktree-setup, spawning, ready) without progressing for >2
|
|
222
|
+
// minutes, the agent appears idle but is silently re-dispatching every tick.
|
|
223
|
+
// Surface a STUCK chip so the operator notices instead of waiting for the
|
|
224
|
+
// engine log. The threshold is 2 minutes — engine ticks every 60s, so 2 ticks
|
|
225
|
+
// is enough to distinguish a slow spawn from a wedged one.
|
|
226
|
+
const _STUCK_PRE_SPAWN_STATES = new Set(['spawning', 'worktree-setup', 'ready']);
|
|
227
|
+
const _STUCK_THRESHOLD_MS = 2 * 60 * 1000;
|
|
228
|
+
|
|
229
|
+
function renderStuckChip(d) {
|
|
230
|
+
const state = d && d.workerState;
|
|
231
|
+
if (!state || !_STUCK_PRE_SPAWN_STATES.has(state)) return '';
|
|
232
|
+
const at = d.workerStateAt ? Date.parse(d.workerStateAt) : 0;
|
|
233
|
+
if (!at || isNaN(at)) return '';
|
|
234
|
+
const ageMs = Date.now() - at;
|
|
235
|
+
if (ageMs < _STUCK_THRESHOLD_MS) return '';
|
|
236
|
+
const mins = Math.max(1, Math.round(ageMs / 60000));
|
|
237
|
+
const detail = d.workerStateDetail ? ' — ' + d.workerStateDetail : '';
|
|
238
|
+
const title = 'Dispatch stuck in "' + state + '" for ' + mins + 'm' + detail +
|
|
239
|
+
'. The agent appears idle but the dispatch loop may be silently re-spawning each tick. Check engine/log.json for spawnAgent errors.';
|
|
240
|
+
return '<span class="dispatch-stuck" title="' + escHtml(title) + '">STUCK ' + mins + 'm</span>';
|
|
241
|
+
}
|
|
242
|
+
|
|
218
243
|
async function showErrorDetails(agentId, reason, task) {
|
|
219
244
|
document.getElementById('modal-title').textContent = 'Error: ' + task;
|
|
220
245
|
document.getElementById('modal-body').textContent = 'Reason: ' + reason + '\n\nLoading agent output...';
|
|
@@ -10,9 +10,9 @@ function renderProjects(projects) {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
list.innerHTML = visible.map(p =>
|
|
13
|
-
'<span data-project="' + escHtml(p.name) + '" title="' + escHtml(p.
|
|
13
|
+
'<span data-project="' + escHtml(p.name) + '" title="' + escHtml(p.path || '') + '" style="display:inline-flex;align-items:center;gap:6px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:3px 10px;color:var(--blue);font-weight:500;cursor:help">' +
|
|
14
14
|
escHtml(p.name) +
|
|
15
|
-
(p
|
|
15
|
+
_renderProjectBranch(p) +
|
|
16
16
|
'<span onclick="event.stopPropagation();projectChipRemove(\'' + escHtml(p.name) + '\')" title="Remove project (cancels pending work, archives data dir)" style="color:var(--muted);font-weight:600;cursor:pointer;padding:0 2px;line-height:1" onmouseover="this.style.color=\'var(--red)\'" onmouseout="this.style.color=\'var(--muted)\'">×</span>' +
|
|
17
17
|
'</span>'
|
|
18
18
|
).join('') +
|
|
@@ -21,6 +21,21 @@ function renderProjects(projects) {
|
|
|
21
21
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// Renders the per-project current-branch indicator. Driven by the gitState
|
|
25
|
+
// classifier emitted by getProjectGitStatus() in engine/queries.js.
|
|
26
|
+
function _renderProjectBranch(p) {
|
|
27
|
+
if (!p) return '';
|
|
28
|
+
if (p.gitState === 'missing') return '<span class="project-warn" title="Project localPath does not exist on disk">(path not found)</span>';
|
|
29
|
+
if (p.gitState === 'non-git') return '<span class="project-muted" title="Project path exists but is not a git repository">(not a git repo)</span>';
|
|
30
|
+
if (p.gitState !== 'ok' || !p.gitBranch) return '';
|
|
31
|
+
const branch = escHtml(p.gitBranch);
|
|
32
|
+
const dirty = p.gitDirty ? ' <span class="dot-dirty" title="Working tree has uncommitted changes">●</span>' : '';
|
|
33
|
+
if (p.gitDetached) {
|
|
34
|
+
return '<span class="project-branch">on: ' + branch + ' <span class="muted">(detached)</span>' + dirty + '</span>';
|
|
35
|
+
}
|
|
36
|
+
return '<span class="project-branch">on: ' + branch + dirty + '</span>';
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
function _projectCachePath(project) {
|
|
25
40
|
return String((project && (project.localPath || project.path)) || '').replace(/\\/g, '/');
|
|
26
41
|
}
|
|
@@ -465,3 +480,95 @@ async function _addSelectedProjects() {
|
|
|
465
480
|
}
|
|
466
481
|
|
|
467
482
|
window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderMetrics, renderLlmPerf, renderTokenUsage, _aggregateEngineUsageForTokenTile, openScanProjectsModal };
|
|
483
|
+
|
|
484
|
+
// ─── keep_processes panel (W-mp68q6ke0010de68) ─────────────────────────────
|
|
485
|
+
// Polls /api/keep-processes every refresh (engine page only) and renders a
|
|
486
|
+
// table of active agents/<id>/keep-pids.json declarations with one-click
|
|
487
|
+
// "Kill PID" buttons that hit POST /api/keep-processes/kill.
|
|
488
|
+
|
|
489
|
+
async function renderKeepProcesses() {
|
|
490
|
+
const root = document.getElementById('keep-processes-content');
|
|
491
|
+
const countEl = document.getElementById('keep-processes-count');
|
|
492
|
+
if (!root) return;
|
|
493
|
+
let html;
|
|
494
|
+
let items;
|
|
495
|
+
let fetchErr = null;
|
|
496
|
+
try {
|
|
497
|
+
const res = await fetch('/api/keep-processes');
|
|
498
|
+
const data = await res.json();
|
|
499
|
+
items = (data && Array.isArray(data.items)) ? data.items : [];
|
|
500
|
+
} catch (e) {
|
|
501
|
+
fetchErr = e;
|
|
502
|
+
}
|
|
503
|
+
if (fetchErr) {
|
|
504
|
+
if (countEl) countEl.textContent = '?';
|
|
505
|
+
html = '<span style="color:var(--red)">Failed to load: ' + escHtml(fetchErr.message) + '</span>';
|
|
506
|
+
} else if (!items.length) {
|
|
507
|
+
if (countEl) countEl.textContent = '0';
|
|
508
|
+
html = '<p class="empty">No agents have left processes running. Set <code>meta.keep_processes: true</code> on a work item to enable.</p>';
|
|
509
|
+
} else {
|
|
510
|
+
if (countEl) countEl.textContent = String(items.length);
|
|
511
|
+
html = items.map(function (it) {
|
|
512
|
+
if (!it.valid) {
|
|
513
|
+
return '<div style="border:1px solid var(--border);border-radius:4px;padding:8px;margin-bottom:8px;background:var(--surface2)">' +
|
|
514
|
+
'<div style="color:var(--red);font-weight:600">' + escHtml(it.agentId) + ' INVALID</div>' +
|
|
515
|
+
'<div style="font-size:11px;color:var(--muted)">reason: ' + escHtml(it.reason || '?') + '</div>' +
|
|
516
|
+
'<div style="font-size:10px;color:var(--muted)">file: ' + escHtml(it.filePath || '') + '</div>' +
|
|
517
|
+
'</div>';
|
|
518
|
+
}
|
|
519
|
+
const pidRows = (it.pids || []).map(function (p) {
|
|
520
|
+
const aliveBadge = p.alive
|
|
521
|
+
? '<span style="color:var(--green)">●</span> alive'
|
|
522
|
+
: '<span style="color:var(--muted)">○</span> dead';
|
|
523
|
+
return '<tr>' +
|
|
524
|
+
'<td style="padding:2px 8px;font-family:monospace">' + p.pid + '</td>' +
|
|
525
|
+
'<td style="padding:2px 8px;font-size:11px">' + aliveBadge + '</td>' +
|
|
526
|
+
'<td style="padding:2px 8px;text-align:right">' +
|
|
527
|
+
(p.alive
|
|
528
|
+
? '<button onclick="killKeepPid(\'' + escHtml(it.agentId) + '\',' + p.pid + ')" style="font-size:10px;padding:2px 6px;color:var(--red);border:1px solid var(--red);background:transparent;border-radius:3px;cursor:pointer">Kill now</button>'
|
|
529
|
+
: '') +
|
|
530
|
+
'</td>' +
|
|
531
|
+
'</tr>';
|
|
532
|
+
}).join('');
|
|
533
|
+
const portsLine = (it.ports && it.ports.length) ? ' ports: ' + it.ports.join(', ') : '';
|
|
534
|
+
return '<div style="border:1px solid var(--border);border-radius:4px;padding:8px;margin-bottom:8px;background:var(--surface2)">' +
|
|
535
|
+
'<div style="font-weight:600">' + escHtml(it.agentId) +
|
|
536
|
+
(it.wi_id ? ' <span style="color:var(--muted);font-weight:400">(' + escHtml(it.wi_id) + ')</span>' : '') +
|
|
537
|
+
'</div>' +
|
|
538
|
+
'<div style="font-size:11px;color:var(--muted)">' + escHtml(it.purpose || '(no purpose set)') + portsLine + '</div>' +
|
|
539
|
+
'<div style="font-size:10px;color:var(--muted)">cwd: ' + escHtml(it.cwd || '?') +
|
|
540
|
+
' expires in ' + (it.expires_in_minutes != null ? it.expires_in_minutes : '?') + 'min age ' + (it.age_minutes != null ? it.age_minutes : '?') + 'min</div>' +
|
|
541
|
+
'<table style="margin-top:6px;width:100%;border-collapse:collapse">' + pidRows + '</table>' +
|
|
542
|
+
'</div>';
|
|
543
|
+
}).join('');
|
|
544
|
+
}
|
|
545
|
+
// Use DocumentFragment instead of innerHTML assignment to keep this
|
|
546
|
+
// function out of the dynamic-innerHTML regression gate (see
|
|
547
|
+
// test/unit.test.js DYNAMIC_INNERHTML_BASELINE). All embedded
|
|
548
|
+
// user-controlled fields above are wrapped in escHtml().
|
|
549
|
+
const range = document.createRange();
|
|
550
|
+
const frag = range.createContextualFragment(html);
|
|
551
|
+
root.replaceChildren(frag);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function killKeepPid(agentId, pid) {
|
|
555
|
+
if (!confirm('Kill PID ' + pid + ' (agent ' + agentId + ')? This is immediate.')) return;
|
|
556
|
+
try {
|
|
557
|
+
const res = await fetch('/api/keep-processes/kill', {
|
|
558
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
559
|
+
body: JSON.stringify({ agentId, pid }),
|
|
560
|
+
});
|
|
561
|
+
const data = await res.json().catch(() => ({}));
|
|
562
|
+
if (!res.ok) {
|
|
563
|
+
alert('Failed: ' + (data.error || res.status));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (typeof showToast === 'function') showToast('cmd-toast', 'Killed PID ' + pid + ' (' + (data.action || 'ok') + ')', true);
|
|
567
|
+
} catch (e) {
|
|
568
|
+
alert('Network error: ' + e.message);
|
|
569
|
+
}
|
|
570
|
+
renderKeepProcesses();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
window.MinionsKeepProcesses = { renderKeepProcesses, killKeepPid };
|
|
574
|
+
|
package/dashboard/js/settings.js
CHANGED
|
@@ -696,7 +696,7 @@ async function addProject() {
|
|
|
696
696
|
if (!exists) _settingsData.projects = _settingsData.projects.concat([addedProject]);
|
|
697
697
|
}
|
|
698
698
|
if (typeof optimisticallyAddProject === 'function') optimisticallyAddProject(addedProject);
|
|
699
|
-
try { showToast('cmd-toast', 'Project "' + addData.name + '" added
|
|
699
|
+
try { showToast('cmd-toast', 'Project "' + addData.name + '" added', true); } catch { /* expected */ }
|
|
700
700
|
refresh();
|
|
701
701
|
} catch (e) { alert('Error: ' + e.message); }
|
|
702
702
|
}
|
package/dashboard/layout.html
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Minions Mission Control</title>
|
|
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'
|
|
6
|
+
<title>Minions Mission Control{{title_suffix}}</title>
|
|
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
8
|
<style>/* __CSS__ */</style>
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -19,3 +19,9 @@
|
|
|
19
19
|
<h2>Token Usage</h2>
|
|
20
20
|
<div id="token-usage-content"><p class="empty">No usage data yet.</p></div>
|
|
21
21
|
</section>
|
|
22
|
+
<section id="keep-processes-section">
|
|
23
|
+
<h2>Keep-Processes <span class="count" id="keep-processes-count">0</span>
|
|
24
|
+
<span style="font-size:10px;color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">processes left running by agents (W-mp68q6ke0010de68 — opt-in keep_processes flag)</span>
|
|
25
|
+
</h2>
|
|
26
|
+
<div id="keep-processes-content"><p class="empty">No agents have left processes running. Set <code>meta.keep_processes: true</code> on a work item to enable.</p></div>
|
|
27
|
+
</section>
|