@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.
Files changed (40) hide show
  1. package/dashboard/js/command-center.js +9 -0
  2. package/dashboard/js/modal-qa.js +10 -0
  3. package/dashboard/js/refresh.js +4 -0
  4. package/dashboard/js/render-dispatch.js +25 -0
  5. package/dashboard/js/render-other.js +109 -2
  6. package/dashboard/js/settings.js +1 -1
  7. package/dashboard/layout.html +2 -2
  8. package/dashboard/pages/engine.html +6 -0
  9. package/dashboard/slim.html +1987 -0
  10. package/dashboard/styles.css +8 -0
  11. package/dashboard.js +450 -40
  12. package/docs/completion-reports.md +25 -0
  13. package/docs/design-state-storage.md +1 -1
  14. package/docs/slim-ux/architecture-suggestions.md +467 -0
  15. package/docs/slim-ux/concepts.md +824 -0
  16. package/engine/ado-mcp-wrapper.js +33 -7
  17. package/engine/ado.js +123 -15
  18. package/engine/cc-worker-pool.js +41 -0
  19. package/engine/cleanup.js +71 -34
  20. package/engine/cli.js +37 -0
  21. package/engine/dispatch.js +32 -9
  22. package/engine/features.js +6 -0
  23. package/engine/gh-token.js +137 -0
  24. package/engine/github.js +166 -29
  25. package/engine/issues.js +29 -0
  26. package/engine/keep-process-sweep.js +397 -0
  27. package/engine/lifecycle.js +150 -33
  28. package/engine/playbook.js +17 -0
  29. package/engine/queries.js +71 -0
  30. package/engine/recovery.js +6 -0
  31. package/engine/shared.js +446 -14
  32. package/engine/spawn-agent.js +44 -2
  33. package/engine/timeout.js +34 -11
  34. package/engine/worktree-pool.js +410 -0
  35. package/engine.js +643 -119
  36. package/package.json +6 -3
  37. package/playbooks/review.md +2 -0
  38. package/playbooks/shared-rules.md +3 -1
  39. package/prompts/cc-system.md +24 -0
  40. 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) {
@@ -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() {
@@ -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.description || 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">' +
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.description ? '<span style="color:var(--muted);font-weight:400;font-size:10px">' + escHtml(p.description.slice(0, 60)) + (p.description.length > 60 ? '...' : '') + '</span>' : '') +
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)\'">&times;</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
+
@@ -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 — restart engine to pick it up', true); } catch { /* expected */ }
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
  }
@@ -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'>👽</text></svg>">
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>