@yemi33/minions 0.1.2177 → 0.1.2179

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 (44) hide show
  1. package/bin/minions.js +24 -11
  2. package/dashboard/js/command-parser.js +1 -1
  3. package/dashboard/js/memory-panel.js +262 -0
  4. package/dashboard/js/qa.js +2 -2
  5. package/dashboard/js/refresh.js +9 -1
  6. package/dashboard/js/render-dispatch.js +92 -0
  7. package/dashboard/js/render-other.js +1 -1
  8. package/dashboard/js/render-plans.js +82 -13
  9. package/dashboard/js/render-prs.js +2 -1
  10. package/dashboard/js/render-schedules.js +1 -1
  11. package/dashboard/js/render-watches.js +1 -1
  12. package/dashboard/js/settings.js +100 -11
  13. package/dashboard/layout.html +6 -0
  14. package/dashboard/pages/engine-memory-panel.html +49 -0
  15. package/dashboard/pages/engine.html +1 -0
  16. package/dashboard/slim/js/link-pr.js +5 -5
  17. package/dashboard/slim/js/modals-tiles.js +44 -3
  18. package/dashboard/slim/js/projects.js +8 -6
  19. package/dashboard/slim/styles.css +20 -0
  20. package/dashboard/styles.css +39 -0
  21. package/dashboard-build.js +17 -2
  22. package/dashboard.js +469 -21
  23. package/docs/README.md +8 -1
  24. package/docs/auto-discovery.md +40 -0
  25. package/docs/branch-derivation.md +13 -1
  26. package/docs/cross-repo-plans.md +292 -0
  27. package/docs/deprecated.json +4 -4
  28. package/docs/pr-auto-fix-dispatch.md +64 -0
  29. package/docs/pr-review-fix-loop.md +1 -1
  30. package/docs/watches.md +1 -0
  31. package/engine/ado.js +1 -10
  32. package/engine/diagnostics-memory.js +190 -0
  33. package/engine/dispatch.js +53 -0
  34. package/engine/lifecycle.js +155 -191
  35. package/engine/meeting.js +30 -0
  36. package/engine/playbook.js +15 -0
  37. package/engine/queries.js +165 -5
  38. package/engine/runtimes/copilot.js +19 -0
  39. package/engine/shared.js +303 -3
  40. package/engine/watchdog.js +6 -0
  41. package/engine.js +576 -113
  42. package/package.json +2 -2
  43. package/playbooks/plan-to-prd.md +25 -2
  44. package/playbooks/plan.md +4 -2
@@ -8,7 +8,10 @@ function _plansNext() { _plansPage++; refresh(); }
8
8
 
9
9
  function openCreatePlanModal() {
10
10
  const projOpts = (typeof cmdProjects !== 'undefined' ? cmdProjects : []).map(p =>
11
- '<option value="' + escapeHtml(p) + '">' + escapeHtml(p) + '</option>'
11
+ '<label style="display:flex;align-items:center;gap:6px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;font-size:var(--text-md);color:var(--text)">' +
12
+ '<input type="checkbox" class="plan-new-project-cb" value="' + escapeHtml(p) + '" onchange="_updatePlanProjectHint()">' +
13
+ escapeHtml(p) +
14
+ '</label>'
12
15
  ).join('');
13
16
  const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
14
17
 
@@ -17,7 +20,11 @@ function openCreatePlanModal() {
17
20
  document.getElementById('modal-body').innerHTML =
18
21
  '<div style="display:flex;flex-direction:column;gap:10px">' +
19
22
  '<label style="color:var(--text);font-size:var(--text-md)">Title <input id="plan-new-title" style="' + inputStyle + '" placeholder="e.g. Add user authentication with JWT"></label>' +
20
- '<label style="color:var(--text);font-size:var(--text-md)">Project <select id="plan-new-project" style="' + inputStyle + '"><option value="">Multiple / cross-repo (agent routes per item)</option>' + projOpts + '</select><span style="display:block;font-size:var(--text-sm);color:var(--muted);margin-top:2px">Pick a project to scope every item to one repo. Leave on cross-repo for plans that span multiple projects.</span></label>' +
23
+ '<div style="color:var(--text);font-size:var(--text-md)">Projects' +
24
+ '<div id="plan-new-project-list" style="display:flex;flex-wrap:wrap;gap:6px;margin-top:4px">' + projOpts + '</div>' +
25
+ '<span style="display:block;font-size:var(--text-sm);color:var(--muted);margin-top:2px">Tick one project to scope every item to that repo. Leave all unticked for plans where the agent should route each item per the PRD.</span>' +
26
+ '<p id="plan-new-project-hint" style="display:none;font-size:var(--text-sm);color:var(--muted);margin:4px 0 0 0;padding:6px 8px;background:var(--bg);border:1px dashed var(--border);border-radius:var(--radius-sm)">Cross-repo plan — each PRD item will route to the project you assign in the PRD.</p>' +
27
+ '</div>' +
21
28
  '<label style="color:var(--text);font-size:var(--text-md)">Plan Content <textarea id="plan-new-content" rows="12" style="' + inputStyle + ';resize:vertical;font-family:monospace;font-size:var(--text-md)" placeholder="Write your plan in markdown...\n\nDescribe what needs to be built, the approach, requirements, and any constraints.\n\nThe squad will convert this into a PRD with structured work items."></textarea></label>' +
22
29
  '<div style="font-size:var(--text-base);color:var(--muted)">After creating, click Execute on the plan card to have an agent convert it into a PRD with work items.</div>' +
23
30
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
@@ -29,13 +36,34 @@ function openCreatePlanModal() {
29
36
  setTimeout(() => document.getElementById('plan-new-title')?.focus(), 100);
30
37
  }
31
38
 
39
+ // Toggle the cross-repo hint under the project picker. Visible only when
40
+ // ≥2 boxes are checked — a single check is still a regular single-project
41
+ // plan, so the hint would be misleading.
42
+ function _updatePlanProjectHint() {
43
+ const boxes = document.querySelectorAll('.plan-new-project-cb');
44
+ let checkedCount = 0;
45
+ for (const cb of boxes) { if (cb.checked) checkedCount++; }
46
+ const hint = document.getElementById('plan-new-project-hint');
47
+ if (hint) hint.style.display = checkedCount >= 2 ? 'block' : 'none';
48
+ }
49
+
32
50
  async function _submitCreatePlan(e) {
33
51
  var btn = (e || window.event)?.target; if (btn) { btn.disabled = true; btn.textContent = 'Creating...'; }
34
52
  const title = document.getElementById('plan-new-title')?.value?.trim();
35
53
  const content = document.getElementById('plan-new-content')?.value?.trim();
36
54
  if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Create Plan'; } alert('Title is required'); return; }
37
55
  if (!content) { if (btn) { btn.disabled = false; btn.textContent = 'Create Plan'; } alert('Plan content is required'); return; }
38
- const project = document.getElementById('plan-new-project')?.value || '';
56
+ // Collect every checked .plan-new-project-cb into a name array. Server
57
+ // accepts string OR array (P-2e9b54d1) but we preserve today's contract
58
+ // for the 0/1-selected cases: 0 → '' (omit-equivalent), 1 → string,
59
+ // ≥2 → array. Order follows DOM order of the checkboxes.
60
+ const boxes = document.querySelectorAll('.plan-new-project-cb');
61
+ const selected = [];
62
+ for (const cb of boxes) { if (cb.checked) selected.push(cb.value); }
63
+ let project;
64
+ if (selected.length === 0) project = '';
65
+ else if (selected.length === 1) project = selected[0];
66
+ else if (selected.length > 1) project = selected;
39
67
 
40
68
  try {
41
69
  const res = await fetch('/api/plans/create', {
@@ -313,8 +341,8 @@ function renderPlans(plans) {
313
341
  const showPause = effectiveStatus === 'dispatched' && prdFile && !isArchived;
314
342
  // Resume pill not needed — paused state is handled by the actions block above
315
343
  const showResume = false;
316
- const verifyWi = allWi.find(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
317
- const hasVerifyWi = !!verifyWi;
344
+ const verifyWis = allWi.filter(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
345
+ const hasVerifyWi = verifyWis.length > 0;
318
346
  const showVerify = effectiveStatus === 'completed' && prdFile && !isArchived && !hasVerifyWi;
319
347
  const pauseBtn = showPause ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:2px 8px;color:var(--yellow)" ' +
320
348
  'onclick="event.stopPropagation();planPause(\'' + escapeHtml(prdFile) + '\',this)">Pause</button>' : '';
@@ -338,17 +366,48 @@ function renderPlans(plans) {
338
366
  const versionBadge = p.version ? ' <span style="font-size:var(--text-xs);font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue);vertical-align:middle">v' + p.version + '</span>' : '';
339
367
  const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--green)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)' };
340
368
  const cardClass = effectiveStatus === 'dispatched' || effectiveStatus === 'converting' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
369
+ // P-e8d49105 — cross-repo plans surface every touched project as its
370
+ // own badge. Single-project plans (and old PRDs without _projects)
371
+ // fall back to the legacy `p.project` plain-text span so existing
372
+ // visuals stay identical.
373
+ const projectsRollup = Array.isArray(p._projects) ? p._projects : [];
374
+ const projectMeta = projectsRollup.length >= 2
375
+ ? projectsRollup.map(function(pn) { return '<span class="prd-project-badge">' + escapeHtml(pn) + '</span>'; }).join(' ')
376
+ : (p.project ? '<span>' + escapeHtml(p.project) + '</span>' : (projectsRollup.length === 1 ? '<span>' + escapeHtml(projectsRollup[0]) + '</span>' : ''));
377
+ // P-66b1faec — when a plan touches >= 2 projects, surface a tiny pill
378
+ // per project in the meta line: `<name> <complete>/<total>` plus ✓ when
379
+ // all items in that project are done, ⏳ otherwise. Single-project plans
380
+ // (or PRD records that omit the rollup, e.g. MD drafts) render nothing
381
+ // extra so the existing meta line is visually unchanged.
382
+ const perProjectProgress = (p && p._perProjectProgress && typeof p._perProjectProgress === 'object')
383
+ ? p._perProjectProgress : {};
384
+ const perProjectKeys = Object.keys(perProjectProgress);
385
+ const perProjectPills = perProjectKeys.length >= 2
386
+ ? perProjectKeys.map(function(pn) {
387
+ const entry = perProjectProgress[pn] || { complete: 0, total: 0 };
388
+ const complete = Number(entry.complete) || 0;
389
+ const total = Number(entry.total) || 0;
390
+ const isComplete = total > 0 && complete === total;
391
+ const tail = isComplete ? ' ✓' : ' ⏳';
392
+ const color = isComplete ? 'var(--green)' : 'var(--muted)';
393
+ return '<span title="' + escapeHtml(pn) + ': ' + complete + '/' + total +
394
+ (isComplete ? ' complete' : ' in progress') + '" ' +
395
+ 'style="font-size:var(--text-xs);font-weight:600;padding:1px 6px;border-radius:3px;background:rgba(110,118,129,0.15);color:' + color + '">' +
396
+ escapeHtml(pn) + ' ' + complete + '/' + total + tail + '</span>';
397
+ }).join(' ')
398
+ : '';
341
399
  return '<div class="plan-card ' + cardClass + '" data-file="plans/' + escapeHtml(p.file) + '" style="cursor:pointer' + (isArchived ? ';opacity:0.7' : '') + '" onclick="if(shouldIgnoreSelectionClick(event))return;planView(\'' + escapeHtml(p.file) + '\')">' +
342
400
  '<div class="plan-card-header">' +
343
401
  '<div><div class="plan-card-title">' + escapeHtml(p.summary || p.file) + versionBadge + '</div>' +
344
402
  '<div class="plan-card-meta">' +
345
403
  '<span style="font-weight:600;color:' + (statusColors[effectiveStatus] || 'var(--muted)') + '">' + label + '</span>' +
346
- (p.project ? '<span>' + escapeHtml(p.project) + '</span>' : '') +
404
+ projectMeta +
347
405
  '<span>' + p.itemCount + ' items</span>' +
406
+ perProjectPills +
348
407
  (p.updatedAt ? '<span title="Last updated: ' + p.updatedAt + '">Updated ' + timeAgo(p.updatedAt) + '</span>' : '') +
349
408
  (p.completedAt ? '<span>' + p.completedAt.slice(0, 10) + '</span>' : '') +
350
409
  (p.generatedBy ? '<span>by ' + escapeHtml(p.generatedBy) + '</span>' : '') +
351
- executeBtn + pauseBtn + resumeBtn + verifyBtn + (hasVerifyWi ? _renderVerifyBadge(verifyWi) : '') + archiveReadyBadge + archiveBtn + deleteBtn +
410
+ executeBtn + pauseBtn + resumeBtn + verifyBtn + (verifyWis.length >= 2 ? verifyWis.map(v => _renderVerifyBadge(v, { projectLabel: v.project || '' })).join(' ') : (hasVerifyWi ? _renderVerifyBadge(verifyWis[0]) : '')) + archiveReadyBadge + archiveBtn + deleteBtn +
352
411
  '</div>' +
353
412
  '</div>' +
354
413
  '</div>' +
@@ -620,12 +679,18 @@ function _renderPlanModal(normalizedFile, raw, lastMod) {
620
679
  if (effectiveStatus === 'dispatched') {
621
680
  modalActions += '<span style="' + bs + ';color:var(--blue)">In Progress</span> ';
622
681
  }
623
- // Verify / Verified badge
624
- const modalVerifyWi = (window._lastWorkItems || []).find(w => w.itemType === 'verify' && w.sourcePlan === (prdFile || normalizedFile));
625
- if (effectiveStatus === 'completed' && prdFile && !isArchived && !modalVerifyWi) {
682
+ // Verify / Verified badge — cross-repo plans fan out one verify WI per
683
+ // project (engine/lifecycle.js); collect them all and render one badge
684
+ // per project so operators can see every repo's verify status.
685
+ const modalVerifyWis = (window._lastWorkItems || []).filter(w => w.itemType === 'verify' && w.sourcePlan === (prdFile || normalizedFile));
686
+ if (effectiveStatus === 'completed' && prdFile && !isArchived && modalVerifyWis.length === 0) {
626
687
  modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--green)" onclick="triggerVerify(\'' + escapeHtml(prdFile) + '\',this)">Verify</button> ';
627
688
  }
628
- if (modalVerifyWi) modalActions += _renderVerifyBadge(modalVerifyWi);
689
+ if (modalVerifyWis.length >= 2) {
690
+ modalActions += modalVerifyWis.map(v => _renderVerifyBadge(v, { projectLabel: v.project || '' })).join(' ');
691
+ } else if (modalVerifyWis.length === 1) {
692
+ modalActions += _renderVerifyBadge(modalVerifyWis[0]);
693
+ }
629
694
  // Archive + Delete (always, unless archived)
630
695
  if (!isArchived) {
631
696
  modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--muted)" onclick="planArchive(\'' + escapeHtml(prdFile || normalizedFile) + '\')">Archive</button> ';
@@ -918,10 +983,14 @@ async function planRegeneratePRD(source) {
918
983
  } catch (e) { alert('Error: ' + e.message); }
919
984
  }
920
985
 
921
- function _renderVerifyBadge(verifyWi) {
986
+ function _renderVerifyBadge(verifyWi, opts) {
922
987
  const statusColors = { pending: 'var(--muted)', dispatched: 'var(--blue)', done: 'var(--green)', failed: 'var(--red)' };
923
988
  const color = statusColors[verifyWi.status] || 'var(--muted)';
924
- const label = verifyWi.status === 'dispatched' ? 'Verifying...' : verifyWi.status === 'done' ? '\u2714 Verified' : verifyWi.status === 'failed' ? 'Verify failed' : 'Verify pending';
989
+ const baseLabel = verifyWi.status === 'dispatched' ? 'Verifying...' : verifyWi.status === 'done' ? '\u2714 Verified' : verifyWi.status === 'failed' ? 'Verify failed' : 'Verify pending';
990
+ // Cross-repo plans fan out one verify WI per project (lifecycle.js); the
991
+ // call-site passes opts.projectLabel so each badge identifies its repo.
992
+ const projectLabel = (opts && opts.projectLabel) ? opts.projectLabel : '';
993
+ const label = projectLabel ? baseLabel + ' (' + escapeHtml(projectLabel) + ')' : baseLabel;
925
994
  // E2E PR — check by prdItems, branch, or title. Issue #2949 — pullRequests
926
995
  // moved off /api/status to /api/pull-requests (window._lastPullRequests).
927
996
  const allPrs = window._lastPullRequests || [];
@@ -320,7 +320,8 @@ function openAddPrModal() {
320
320
  const projects = (typeof cmdProjects !== 'undefined' ? cmdProjects : []) || [];
321
321
  const projOpts = projects.map(p => {
322
322
  const name = typeof p === 'object' ? p.name : p;
323
- return '<option value="' + escapeHtml(name) + '">' + escapeHtml(name) + '</option>';
323
+ const label = (typeof p === 'object' && (p.displayName || p.name)) || name;
324
+ return '<option value="' + escapeHtml(name) + '">' + escapeHtml(label) + '</option>';
324
325
  }).join('');
325
326
  const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
326
327
 
@@ -422,7 +422,7 @@ function _scheduleFormHtml(sched, isEdit) {
422
422
  const priorities = ['high', 'medium', 'low'];
423
423
  const typeOpts = types.map(t => '<option value="' + t + '"' + ((sched.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
424
424
  const priOpts = priorities.map(p => '<option value="' + p + '"' + ((sched.priority || 'medium') === p ? ' selected' : '') + '>' + p + '</option>').join('');
425
- const projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(p => '<option value="' + escHtml(p.name) + '"' + (sched.project === p.name ? ' selected' : '') + '>' + escHtml(p.name) + '</option>').join('');
425
+ const projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(p => '<option value="' + escHtml(p.name) + '"' + (sched.project === p.name ? ' selected' : '') + '>' + escHtml(p.displayName || p.name) + '</option>').join('');
426
426
  const agentOpts = '<option value="">Auto</option>' + (cmdAgents || []).map(a => '<option value="' + escHtml(a.id) + '"' + (sched.agent === a.id ? ' selected' : '') + '>' + escHtml(a.name) + '</option>').join('');
427
427
 
428
428
  const inputStyle = 'display:block;width:100%;margin-top:4px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:var(--text-md);font-family:inherit';
@@ -486,7 +486,7 @@ function _watchFormHtml() {
486
486
  }).join('');
487
487
 
488
488
  var agentOpts = '<option value="">human</option>' + (cmdAgents || []).map(function(a) { return '<option value="' + escHtml(a.id) + '">' + escHtml(a.name) + '</option>'; }).join('');
489
- var projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(function(p) { return '<option value="' + escHtml(p.name) + '">' + escHtml(p.name) + '</option>'; }).join('');
489
+ var projOpts = '<option value="">Any</option>' + (cmdProjects || []).map(function(p) { return '<option value="' + escHtml(p.name) + '">' + escHtml(p.displayName || p.name) + '</option>'; }).join('');
490
490
 
491
491
  return '<div style="display:flex;flex-direction:column;gap:12px;font-family:inherit">' +
492
492
  '<div id="watch-form-error" style="display:none;color:var(--red);font-size:var(--text-md);padding:6px 10px;background:rgba(255,50,50,0.1);border-radius:var(--radius-sm)"></div>' +
@@ -158,7 +158,11 @@ async function openSettings() {
158
158
 
159
159
  const paneAutoFix =
160
160
  '<h3>Auto-fix &amp; Review Loop</h3>' +
161
- '<div class="settings-pane-sub">Dispatch gates that decide when an agent is auto-spawned in response to PR build failures, merge conflicts, review verdicts, and human comments. All require the matching provider polling toggle (Polling tab).</div>' +
161
+ '<div class="settings-pane-sub">PR-triggered dispatch gates only. Each toggle below decides when an agent is auto-spawned in response to a PR signal (build failure, merge conflict, review verdict, human comment). All require the matching provider polling (Polling tab). The <em>Pause All Auto-fix</em> kill-switch below inerts every per-cause gate at once. Non-dispatch knobs (auto-merge, auto-vote, plan/decompose defaults) moved to the new <strong>PR Lifecycle</strong> and <strong>Workflow Defaults</strong> panes.</div>' +
162
+ '<div class="settings-stack" style="margin-bottom:16px">' +
163
+ settingsToggle('🛑 Pause ALL PR auto-fix dispatches', 'set-autoFixPaused', !!e.autoFixPaused, 'Halts every PR-triggered dispatch (review, re-review, build-failure fix, review-feedback fix, human-comment fix, merge-conflict fix) on the next tick. In-flight agents keep running. Reversible without restart. Per-cause flags below are inert while this is set.') +
164
+ '</div>' +
165
+ '<div class="settings-pane-sub" style="margin-bottom:8px">Per-cause dispatch gates — each one names the exact discoverFromPrs site it gates. When <em>Pause All Auto-fix</em> is ON these are inert; when it is OFF, each gate decides independently whether its site fires.</div>' +
162
166
  '<div class="settings-stack" style="margin-bottom:16px">' +
163
167
  settingsToggle('Auto-fix Builds', 'set-autoFixBuilds', e.autoFixBuilds !== false, 'Shared dispatch gate: auto-fix agent when a PR build fails; also requires that PR provider polling is enabled') +
164
168
  settingsToggle('Auto-fix Conflicts', 'set-autoFixConflicts', e.autoFixConflicts !== false, 'Shared dispatch gate: auto-fix agent when a PR merge conflict is detected; also requires that PR provider polling is enabled') +
@@ -166,16 +170,42 @@ async function openSettings() {
166
170
  settingsToggle('Auto-re-review PRs', 'set-autoReReviewPrs', e.autoReReviewPrs !== false, 'Shared dispatch gate: review agent after a fix push is awaiting re-review; also requires that PR provider polling is enabled') +
167
171
  settingsToggle('Auto-fix Review Feedback', 'set-autoFixReviewFeedback', e.autoFixReviewFeedback !== false, 'Shared dispatch gate: fix agent for minions changes-requested verdicts; also requires that PR provider polling is enabled') +
168
172
  settingsToggle('Auto-fix Human Comments', 'set-autoFixHumanComments', e.autoFixHumanComments !== false, 'Shared dispatch gate: fix agent for actionable human PR comments; also requires that PR provider polling is enabled') +
169
- settingsToggle('Auto-apply review vote to PR', 'set-autoApplyReviewVote', !!e.autoApplyReviewVote, 'When ON, Minions review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote on ADO/GitHub. When OFF (default), verdicts are informational only and the human casts the final vote.') +
170
- settingsToggle('Eval Loop', 'set-evalLoop', e.evalLoop !== false, 'Auto-review implementations and iterate fix cycles until pass') +
171
- settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks') +
172
- settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in)') +
173
- settingsToggle('Auto-approve Plans', 'set-autoApprovePlans', !!e.autoApprovePlans, 'PRDs are approved automatically without human review') +
174
- settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard)') +
175
- 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.') +
173
+ settingsToggle('Eval Loop', 'set-evalLoop', e.evalLoop !== false, 'Gates the review→fix iteration loop only (first review, re-review, review-feedback fix). Does NOT gate build-failure, merge-conflict, or human-comment fixes. Use the emergency stop above to halt everything.') +
174
+ settingsToggle('Pre-dispatch Eval (cheap LLM gate)', 'set-enablePreDispatchEval', e.enablePreDispatchEval !== false, 'P-d2a9f6e5: cheap LLM gate that screens work items for clear/actionable/testable criteria BEFORE queueing the agent. Catches noop dispatches authored from impossible/ambiguous WIs. Fail-open on any validator error.') +
175
+ settingsToggle('Pre-dispatch Eval: skip PRD-sourced items', 'set-preDispatchEvalSkipPrdSourced', e.preDispatchEvalSkipPrdSourced !== false, 'W-mq9acoo800177bcb: short-circuit the validator for items materialized from an approved/active PRD — plan-to-prd already LLM-vets them. Reduces queue-time on an N-item approved PRD from ~N×25s to nearly instant. OFF = always re-validate even PRD-sourced items.') +
176
176
  '</div>' +
177
177
  '<div class="settings-grid-2">' +
178
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)') +
179
+ settingsField('Pre-dispatch Eval Concurrency', 'set-preDispatchEvalConcurrency', e.preDispatchEvalConcurrency || 6, '', 'W-mq9acoo800177bcb: max parallel pre-dispatch validator calls per discovery tick. Default 6. Clamped to [1, 20]. Raises throughput when multiple PRDs queue at once; lower if the LLM provider throttles. 1 = sequential (pre-fix behavior).') +
180
+ '</div>' +
181
+ '<div style="margin-top:12px;padding:6px 8px;border:1px solid var(--border);border-radius:4px;background:rgba(130,160,210,0.06);font-size:var(--text-sm);color:var(--muted)">' +
182
+ 'Moved to <strong>PR Lifecycle</strong>: Auto-complete PRs, Auto-apply review vote. ' +
183
+ 'Moved to <strong>Workflow Defaults</strong>: Auto-archive Plans, Auto-approve Plans, Auto-decompose, Auto-consolidate Memory.' +
184
+ '</div>';
185
+
186
+ // P-g7a2b4c5 — new "PR Lifecycle" pane. Owns the two PR-lifecycle knobs
187
+ // that historically lived under Auto-fix but do NOT gate any dispatch:
188
+ // autoApplyReviewVote (lifecycle vote-posting), autoCompletePrs (auto-merge).
189
+ const paneLifecycle =
190
+ '<h3>PR Lifecycle</h3>' +
191
+ '<div class="settings-pane-sub">Post-merge / post-review lifecycle knobs. These are not dispatch gates — they control what Minions does with a PR once review or build state lands.</div>' +
192
+ '<div class="settings-stack">' +
193
+ settingsToggle('Auto-apply review vote to PR', 'set-autoApplyReviewVote', !!e.autoApplyReviewVote, 'When ON, Minions review verdicts (APPROVE / REQUEST_CHANGES) automatically flip the platform vote on ADO/GitHub. When OFF (default), verdicts are informational only and the human casts the final vote.') +
194
+ settingsToggle('Auto-complete PRs', 'set-autoCompletePrs', !!e.autoCompletePrs, 'Auto-merge PRs when builds pass and review is approved (opt-in). Independent of the per-cause auto-fix gates above.') +
195
+ '</div>';
196
+
197
+ // P-g7a2b4c5 — new "Workflow Defaults" pane. Owns the four workflow-level
198
+ // automation defaults that historically lived under Auto-fix: plan-flow
199
+ // (approve / decompose / archive) and memory consolidation. None of these
200
+ // gate a PR-triggered dispatch.
201
+ const paneWorkflow =
202
+ '<h3>Workflow Defaults</h3>' +
203
+ '<div class="settings-pane-sub">Workflow-level automation defaults. Independent of PR dispatch and PR lifecycle — these control plan flow and memory upkeep.</div>' +
204
+ '<div class="settings-stack">' +
205
+ settingsToggle('Auto-approve Plans', 'set-autoApprovePlans', !!e.autoApprovePlans, 'PRDs are approved automatically without human review.') +
206
+ settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks.') +
207
+ settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard).') +
208
+ 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 under Advanced); this toggle controls only the KB sweep that was previously dashboard-button-only.') +
179
209
  '</div>';
180
210
 
181
211
  // W-mpmwxkrw000872ec — Appearance pane. Hosts dashboard-wide visual
@@ -268,11 +298,35 @@ async function openSettings() {
268
298
 
269
299
  const panePolling =
270
300
  '<h3>Polling</h3>' +
271
- '<div class="settings-pane-sub">Cadence for fetching PR build status, votes, and comments from the platforms. Disabling a provider here turns the matching Auto-fix gates into no-ops.</div>' +
301
+ '<div class="settings-pane-sub">Cadence for fetching PR build status, votes, and comments from the platforms. Disabling a provider here turns the matching Auto-fix gates into no-ops. The <em>Pause All Polling</em> kill-switch below overrides both provider toggles and inerts every PR auto-dispatch gate (Auto-fix Builds / Conflicts / Review / etc.) until cleared.</div>' +
302
+ '<div class="settings-stack" style="margin-bottom:16px">' +
303
+ settingsToggle('🛑 Pause ALL polling', 'set-pollingPaused', !!e.pollingPaused, 'Halts every PR poll, reconciliation, rebase processing, and work-discovery scan on the next tick. Reversible without restart. Use when hitting API rate limits.') +
304
+ '</div>' +
305
+ '<div class="settings-pane-sub" style="margin-bottom:8px">Legacy bundle toggles — granular controls below override these when set.</div>' +
272
306
  '<div class="settings-stack" style="margin-bottom:12px">' +
273
- settingsToggle('ADO Polling', 'set-adoPollEnabled', e.adoPollEnabled !== false, 'Keeps ADO PR build results, votes, and comments fresh each tick; ADO PR dispatch gates are inert when this is off') +
274
- settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Keeps GitHub PR build results, votes, and comments fresh each tick; GitHub PR dispatch gates are inert when this is off') +
307
+ settingsToggle('ADO Polling', 'set-adoPollEnabled', e.adoPollEnabled !== false, 'Legacy bundle macro — when OFF, silences all three ADO axes (status, comments, reconcile) and ADO PR dispatch gates are inert when this is off. Per-axis flags below take priority when explicitly set. Keep ON unless you want a one-knob ADO kill.') +
308
+ settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Legacy bundle macro — when OFF, silences all three GitHub axes (status, comments, reconcile) and GitHub PR dispatch gates are inert when this is off. Per-axis flags below take priority when explicitly set. Keep ON unless you want a one-knob GitHub kill.') +
275
309
  '</div>' +
310
+ '<details class="settings-collapsible" style="margin-bottom:12px"><summary>Granular per-poller controls (P-c4d8e1a3)</summary>' +
311
+ '<div class="settings-pane-sub" style="margin-top:8px">Override individual ADO/GitHub poll axes + the pending-rebase processor. Explicit values here win; if unset, the legacy ADO/GitHub Polling macros above apply; otherwise the default is ON. <strong>Status</strong> + <strong>Comments</strong> still honor the <em>Pause All Polling</em> kill-switch above; <strong>Reconcile</strong> and <strong>Process Pending Rebases</strong> are recovery sweeps and ignore it (by design).</div>' +
312
+ '<div class="settings-stack" style="margin-top:8px">' +
313
+ settingsToggle('ADO PR Status Poll', 'set-adoPrStatusPollEnabled', e.adoPrStatusPollEnabled !== false, 'Granular: ADO PR build/merge/review status poll (section 2.6). Default ON. When OFF, ADO PR status will not refresh, but reconcile + comments still run (unless their own granular flag is OFF).') +
314
+ settingsToggle('ADO PR Comments Poll', 'set-adoPrCommentsPollEnabled', e.adoPrCommentsPollEnabled !== false, 'Granular: ADO PR human-comments poll (section 2.7). Default ON. When OFF, ADO PR comments will not surface — auto-fix-human-comments still composes against this flag for ADO.') +
315
+ settingsToggle('ADO PR Reconcile', 'set-adoPrReconcileEnabled', e.adoPrReconcileEnabled !== false, 'Granular: ADO PR reconciliation recovery sweep (section 2.7 tail). Default ON. Setting OFF stops engine from healing missed PR state transitions for ADO — only do this if reconcile is misbehaving.') +
316
+ settingsToggle('GitHub PR Status Poll', 'set-ghPrStatusPollEnabled', e.ghPrStatusPollEnabled !== false, 'Granular: GitHub PR build/merge/review status poll. Default ON. When OFF, GitHub PR status will not refresh, but reconcile + comments still run (unless their own granular flag is OFF).') +
317
+ settingsToggle('GitHub PR Comments Poll', 'set-ghPrCommentsPollEnabled', e.ghPrCommentsPollEnabled !== false, 'Granular: GitHub PR human-comments poll. Default ON. When OFF, GitHub PR comments will not surface — auto-fix-human-comments still composes against this flag for GitHub.') +
318
+ settingsToggle('GitHub PR Reconcile', 'set-ghPrReconcileEnabled', e.ghPrReconcileEnabled !== false, 'Granular: GitHub PR reconciliation recovery sweep. Default ON. Setting OFF stops engine from healing missed PR state transitions for GitHub — only do this if reconcile is misbehaving.') +
319
+ settingsToggle('Process Pending Rebases', 'set-processPendingRebasesEnabled', e.processPendingRebasesEnabled !== false, 'Granular: pending-rebase processor that runs after each status poll cycle. Default ON. Setting OFF freezes the rebase queue — useful if rebase-on-tick is causing platform churn or you want manual control. Has no legacy macro counterpart.') +
320
+ '</div></details>' +
321
+ '<details class="settings-collapsible" style="margin-bottom:12px"><summary>Granular work-discovery controls (P-d6f0a2b5)</summary>' +
322
+ '<div class="settings-pane-sub" style="margin-top:8px">Silence individual discovery phases inside the per-tick <code>engine.discoverWork()</code> sweep. Each flag complements (not replaces) the per-project <code>project.workSources.*.enabled</code> toggles — a <strong>false</strong> at either the global or per-project level skips the matching discovery call. There is no legacy macro to fall back to; defaults are ON. Use these for incident-response (e.g. <em>turn off PR discovery while triaging a runaway auto-fix loop</em>) or long migrations that must not auto-create work items.</div>' +
323
+ '<div class="settings-stack" style="margin-top:8px">' +
324
+ settingsToggle('PR Discovery', 'set-prDiscoveryEnabled', e.prDiscoveryEnabled !== false, 'Granular: discoverFromPrs per-project — gates the PR-driven fix / review / build-test work queue. Default ON. OFF stops the engine from queuing new fix/review/test work from PRs at all (per-project workSources.pullRequests.enabled still composes independently).') +
325
+ settingsToggle('Work Items Discovery', 'set-workItemsDiscoveryEnabled', e.workItemsDiscoveryEnabled !== false, 'Granular: discoverFromWorkItems per-project — gates the project-local work-items.json scan (includes items auto-filed from plans, design docs, build failures). Default ON. OFF freezes per-project work-item pickup; per-project workSources.workItems.enabled still composes independently.') +
326
+ settingsToggle('Central Work Discovery', 'set-centralWorkDiscoveryEnabled', e.centralWorkDiscoveryEnabled !== false, 'Granular: discoverCentralWorkItems — gates the top-level project-agnostic work-items.json scan. Default ON. OFF keeps centralWork iterable as [] so the downstream dispatch path stays safe.') +
327
+ settingsToggle('Scheduled Work Discovery', 'set-scheduledWorkDiscoveryEnabled', e.scheduledWorkDiscoveryEnabled !== false, 'Granular: discoverScheduledWork — gates the cron-style scheduled tasks + scheduled meetings block. Default ON. OFF stops cron-style scheduled tasks from firing.') +
328
+ settingsToggle('Plan Materialization', 'set-planMaterializationEnabled', e.planMaterializationEnabled !== false, 'Granular: reconcilePrdStatuses + materializePlansAsWorkItems pair — gates the PRD reconcile backward-scan and plan-to-work-item materialization. Default ON. OFF suppresses the pair atomically so a long migration does not race new PRD items into the queue.') +
329
+ '</div></details>' +
276
330
  '<div class="settings-grid-2">' +
277
331
  settingsField('PR Status Poll Frequency', 'set-prPollStatusEvery', e.prPollStatusEvery ?? 12, 'ticks', 'Poll PR build/review/merge status every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
278
332
  settingsField('PR Comments Poll Frequency', 'set-prPollCommentsEvery', e.prPollCommentsEvery ?? 12, 'ticks', 'Poll PR human comments every N ticks for both ADO and GitHub (~12 min at default tick rate)') +
@@ -344,6 +398,12 @@ async function openSettings() {
344
398
  '</select>' +
345
399
  '</div>' +
346
400
  settingsField('Copilot fallback model', 'set-copilotFallbackModel', e.copilotFallbackModel || '', 'e.g. gpt-5.4', 'Copilot has no --fallback-model flag. On a MODEL_UNAVAILABLE (overloaded/503) retry, the engine OVERRIDES --model with this value (Copilot only).') +
401
+ '</div>' +
402
+ '<div class="settings-stack" style="margin-top:12px">' +
403
+ settingsField('Copilot: disable agent MCP servers', 'set-copilotAgentDisabledMcpServers',
404
+ Array.isArray(e.copilotAgentDisabledMcpServers) ? e.copilotAgentDisabledMcpServers.join(', ') : (e.copilotAgentDisabledMcpServers || ''),
405
+ 'e.g. playwright, maestro, loop',
406
+ 'Comma-separated MCP server names (from ~/.copilot/mcp-config.json) the engine disables for autonomous Copilot agent dispatches via --disable-mcp-server. Copilot loads your user MCP config on every spawn regardless of --add-dir/hermeticHarness; list the noisy local/UI servers (e.g. playwright, maestro) to keep them out of agents. Empty = inherit all.') +
347
407
  '</div>';
348
408
 
349
409
  const paneClaude =
@@ -446,9 +506,15 @@ async function openSettings() {
446
506
 
447
507
  // Section registry — order is intentional (Runtime + Auto-fix surface first
448
508
  // per Caleb's feedback). Each entry maps a rail-button id → pane HTML.
509
+ // P-g7a2b4c5 — added "PR Lifecycle" and "Workflow Defaults" panes for the
510
+ // non-dispatch toggles that historically lived under Auto-fix; placed
511
+ // adjacent to Auto-fix so operators following the legacy mental model find
512
+ // the moved flags quickly.
449
513
  const sections = [
450
514
  { id: 'runtime', label: 'Runtime & Models', featured: true, html: paneRuntime },
451
515
  { id: 'autofix', label: 'Auto-fix & Review Loop', featured: true, html: paneAutoFix },
516
+ { id: 'lifecycle', label: 'PR Lifecycle', html: paneLifecycle },
517
+ { id: 'workflow', label: 'Workflow Defaults', html: paneWorkflow },
452
518
  { id: 'appearance', label: 'Appearance', html: paneAppearance },
453
519
  { id: 'projects', label: 'Projects', html: paneProjects },
454
520
  { id: 'polling', label: 'Polling', html: panePolling },
@@ -904,6 +970,7 @@ async function saveSettings() {
904
970
  autoApplyReviewVote: document.getElementById('set-autoApplyReviewVote').checked,
905
971
  autoFixBuilds: document.getElementById('set-autoFixBuilds').checked,
906
972
  autoFixConflicts: document.getElementById('set-autoFixConflicts').checked,
973
+ autoFixPaused: document.getElementById('set-autoFixPaused').checked,
907
974
  autoReviewPrs: document.getElementById('set-autoReviewPrs').checked,
908
975
  autoReReviewPrs: document.getElementById('set-autoReReviewPrs').checked,
909
976
  autoFixReviewFeedback: document.getElementById('set-autoFixReviewFeedback').checked,
@@ -914,9 +981,31 @@ async function saveSettings() {
914
981
  orphanHolderScanTimeoutMs: document.getElementById('set-orphanHolderScanTimeoutMs')?.value,
915
982
  adoPollEnabled: document.getElementById('set-adoPollEnabled').checked,
916
983
  ghPollEnabled: document.getElementById('set-ghPollEnabled').checked,
984
+ pollingPaused: document.getElementById('set-pollingPaused').checked,
985
+ // P-c4d8e1a3 — granular per-poller flags (default true). All seven submit
986
+ // booleans; the server stores them on config.engine and resolvePollFlag
987
+ // resolves precedence at engine runtime.
988
+ adoPrStatusPollEnabled: document.getElementById('set-adoPrStatusPollEnabled').checked,
989
+ adoPrCommentsPollEnabled: document.getElementById('set-adoPrCommentsPollEnabled').checked,
990
+ adoPrReconcileEnabled: document.getElementById('set-adoPrReconcileEnabled').checked,
991
+ ghPrStatusPollEnabled: document.getElementById('set-ghPrStatusPollEnabled').checked,
992
+ ghPrCommentsPollEnabled: document.getElementById('set-ghPrCommentsPollEnabled').checked,
993
+ ghPrReconcileEnabled: document.getElementById('set-ghPrReconcileEnabled').checked,
994
+ processPendingRebasesEnabled: document.getElementById('set-processPendingRebasesEnabled').checked,
995
+ // P-d6f0a2b5 — granular work-discovery flags (default true). Each one
996
+ // gates a single phase inside engine.discoverWork via the inline
997
+ // `config.engine?.<flag> !== false` pattern. No legacy macro counterpart.
998
+ prDiscoveryEnabled: document.getElementById('set-prDiscoveryEnabled').checked,
999
+ workItemsDiscoveryEnabled: document.getElementById('set-workItemsDiscoveryEnabled').checked,
1000
+ centralWorkDiscoveryEnabled: document.getElementById('set-centralWorkDiscoveryEnabled').checked,
1001
+ scheduledWorkDiscoveryEnabled: document.getElementById('set-scheduledWorkDiscoveryEnabled').checked,
1002
+ planMaterializationEnabled: document.getElementById('set-planMaterializationEnabled').checked,
917
1003
  prPollStatusEvery: document.getElementById('set-prPollStatusEvery').value,
918
1004
  prPollCommentsEvery: document.getElementById('set-prPollCommentsEvery').value,
919
1005
  evalMaxCost: document.getElementById('set-evalMaxCost').value || null,
1006
+ enablePreDispatchEval: document.getElementById('set-enablePreDispatchEval').checked,
1007
+ preDispatchEvalSkipPrdSourced: document.getElementById('set-preDispatchEvalSkipPrdSourced').checked,
1008
+ preDispatchEvalConcurrency: document.getElementById('set-preDispatchEvalConcurrency').value,
920
1009
  agentBusyReassignMs: document.getElementById('set-agentBusyReassignMs').value,
921
1010
  maxRetriesPerAgent: document.getElementById('set-maxRetriesPerAgent').value,
922
1011
  ignoredCommentAuthors: document.getElementById('set-ignoredCommentAuthors').value,
@@ -35,6 +35,12 @@
35
35
  </div>
36
36
  </header>
37
37
  <div class="engine-alert" id="engine-alert"></div>
38
+ <!-- P-g7a2b4c5 — sticky cross-page banner for the two operator kill-switches
39
+ (engine.pollingPaused, engine.autoFixPaused). Rendered by
40
+ renderPausedBanner() in dashboard/js/render-dispatch.js whenever either
41
+ flag is true; hidden otherwise. Lives outside .page-layout so it persists
42
+ across page nav. -->
43
+ <div class="paused-banner" id="paused-banner" hidden></div>
38
44
 
39
45
  <!-- Command Center Drawer -->
40
46
  <div id="cc-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:340" onclick="toggleCommandCenter()"></div>
@@ -0,0 +1,49 @@
1
+ <!--
2
+ dashboard/pages/engine-memory-panel.html — Memory panel fragment (P-d4e5f6a7).
3
+
4
+ Side-by-side cards for the engine and dashboard processes. Live values are
5
+ refreshed every 10 s by dashboard/js/memory-panel.js (GET /api/diagnostics/memory);
6
+ the inline SVG sparkline is refreshed every 60 s
7
+ (GET /api/diagnostics/memory/history?process=...). The yellow
8
+ "engine sample stale" badge is unhidden by the JS module when
9
+ engineStale === true.
10
+
11
+ dashboard-build.js substitutes this fragment into dashboard/pages/engine.html
12
+ at assembly time (see pageSubFragments).
13
+ -->
14
+ <section id="memory-panel-section">
15
+ <h2>Memory <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">RSS / heap / event-loop / GC for engine + dashboard processes</span></h2>
16
+ <div id="memory-panel-content" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:12px">
17
+ <div id="memory-card-engine" class="memory-card" style="border:1px solid var(--border);border-radius:4px;padding:10px;background:var(--surface2)">
18
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
19
+ <div style="font-weight:600;font-size:var(--text-md);font-family:monospace">engine</div>
20
+ <span id="memory-engine-stale-badge" style="display:none;background:var(--yellow);color:#000;padding:2px 8px;border-radius:3px;font-size:var(--text-sm);font-weight:600">engine sample stale</span>
21
+ </div>
22
+ <dl style="display:grid;grid-template-columns:max-content 1fr;gap:3px 12px;margin:0 0 8px 0;font-size:var(--text-base)">
23
+ <dt style="color:var(--muted);margin:0">RSS</dt><dd id="memory-engine-rss" style="margin:0;font-family:monospace">—</dd>
24
+ <dt style="color:var(--muted);margin:0">Heap used / total</dt><dd id="memory-engine-heap" style="margin:0;font-family:monospace">—</dd>
25
+ <dt style="color:var(--muted);margin:0">External</dt><dd id="memory-engine-external" style="margin:0;font-family:monospace">—</dd>
26
+ <dt style="color:var(--muted);margin:0">Event-loop lag p50 / p99</dt><dd id="memory-engine-lag" style="margin:0;font-family:monospace">—</dd>
27
+ <dt style="color:var(--muted);margin:0">Last GC pause</dt><dd id="memory-engine-gc" style="margin:0;font-family:monospace">—</dd>
28
+ <dt style="color:var(--muted);margin:0">Uptime</dt><dd id="memory-engine-uptime" style="margin:0;font-family:monospace">—</dd>
29
+ </dl>
30
+ <div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px"><span style="color:var(--blue,#4ea1ff)">●</span> RSS &nbsp; <span style="color:var(--green,#4caf50)">●</span> heapUsed &nbsp;<span style="opacity:0.7">(last hour)</span></div>
31
+ <div id="memory-engine-sparkline" style="height:60px;width:100%;color:var(--fg)"></div>
32
+ </div>
33
+ <div id="memory-card-dashboard" class="memory-card" style="border:1px solid var(--border);border-radius:4px;padding:10px;background:var(--surface2)">
34
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
35
+ <div style="font-weight:600;font-size:var(--text-md);font-family:monospace">dashboard</div>
36
+ </div>
37
+ <dl style="display:grid;grid-template-columns:max-content 1fr;gap:3px 12px;margin:0 0 8px 0;font-size:var(--text-base)">
38
+ <dt style="color:var(--muted);margin:0">RSS</dt><dd id="memory-dashboard-rss" style="margin:0;font-family:monospace">—</dd>
39
+ <dt style="color:var(--muted);margin:0">Heap used / total</dt><dd id="memory-dashboard-heap" style="margin:0;font-family:monospace">—</dd>
40
+ <dt style="color:var(--muted);margin:0">External</dt><dd id="memory-dashboard-external" style="margin:0;font-family:monospace">—</dd>
41
+ <dt style="color:var(--muted);margin:0">Event-loop lag p50 / p99</dt><dd id="memory-dashboard-lag" style="margin:0;font-family:monospace">—</dd>
42
+ <dt style="color:var(--muted);margin:0">Last GC pause</dt><dd id="memory-dashboard-gc" style="margin:0;font-family:monospace">—</dd>
43
+ <dt style="color:var(--muted);margin:0">Uptime</dt><dd id="memory-dashboard-uptime" style="margin:0;font-family:monospace">—</dd>
44
+ </dl>
45
+ <div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px"><span style="color:var(--blue,#4ea1ff)">●</span> RSS &nbsp; <span style="color:var(--green,#4caf50)">●</span> heapUsed &nbsp;<span style="opacity:0.7">(last hour)</span></div>
46
+ <div id="memory-dashboard-sparkline" style="height:60px;width:100%;color:var(--fg)"></div>
47
+ </div>
48
+ </div>
49
+ </section>
@@ -3,6 +3,7 @@
3
3
  <section>
4
4
  <div id="engine-quick-stats" style="display:flex;gap:16px;margin-bottom:12px;font-size:var(--text-base);color:var(--muted)"></div>
5
5
  </section>
6
+ <!-- __ENGINE_MEMORY_PANEL__ -->
6
7
  <section>
7
8
  <h2>Engine Log <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">tick-by-tick audit trail of engine operations</span></h2>
8
9
  <div class="log-list" id="engine-log">No log entries yet.</div>
@@ -6,18 +6,18 @@
6
6
  var modal = document.getElementById('slim-linkpr-modal');
7
7
  if (!modal) return;
8
8
  var projects = (lastStatusData && Array.isArray(lastStatusData.projects) ? lastStatusData.projects : [])
9
- .map(function(p) { return p && p.name ? String(p.name) : null; })
10
- .filter(Boolean);
9
+ .filter(function(p) { return p && p.name; })
10
+ .map(function(p) { return { name: String(p.name), label: String(p.displayName || p.name) }; });
11
11
  var sel = document.getElementById('slim-linkpr-project');
12
12
  sel.textContent = '';
13
13
  var auto = document.createElement('option');
14
14
  auto.value = '';
15
15
  auto.textContent = 'Auto-detect from URL (central if no unique match)';
16
16
  sel.appendChild(auto);
17
- projects.forEach(function(name) {
17
+ projects.forEach(function(proj) {
18
18
  var o = document.createElement('option');
19
- o.value = name;
20
- o.textContent = name;
19
+ o.value = proj.name;
20
+ o.textContent = proj.label;
21
21
  sel.appendChild(o);
22
22
  });
23
23
  var warn = document.getElementById('slim-linkpr-noproj');
@@ -159,7 +159,7 @@
159
159
  }
160
160
  var prs = Array.isArray(data.pullRequests) ? data.pullRequests : [];
161
161
  if (!prs.length) { tileEmpty(body, 'No tracked pull requests.'); return; }
162
- prs.forEach(function(p) {
162
+ function prCard(p) {
163
163
  var num = p.prNumber || p.number || p.id;
164
164
  var build = p.buildStatus || '';
165
165
  var review = p.reviewStatus || '';
@@ -168,7 +168,7 @@
168
168
  : ((p.status === 'active' || p.status === 'linked') ? 'blue' : ''));
169
169
  var statusText = document.createElement('span');
170
170
  statusText.textContent = p.status || 'unknown';
171
- body.appendChild(tileCard({
171
+ return tileCard({
172
172
  title: (num ? '#' + num + ' ' : '') + (p.title || '(untitled PR)'),
173
173
  // Project slug first (under title), then build/review row below — the
174
174
  // project is the strongest disambiguator across PRs and benefits from
@@ -178,7 +178,48 @@
178
178
  metaNodes: [statusText, statusSpan('build', build, BUILD_STATUS), statusSpan('review', review, REVIEW_STATUS)],
179
179
  chip: { text: p.status || '—', cls: cls },
180
180
  href: p.url || null,
181
- }));
181
+ });
182
+ }
183
+ // Partition once into three fixed buckets (status normalized to lowercase),
184
+ // preserving each PR's relative order within its bucket. Mirrors shared
185
+ // PR_STATUS (active/merged/abandoned/closed/linked); anything that does not
186
+ // match the merged/abandoned buckets defaults to Active so a PR with an
187
+ // unexpected status never silently disappears.
188
+ var buckets = { active: [], merged: [], abandoned: [] };
189
+ prs.forEach(function(p) {
190
+ var s = String(p.status || '').toLowerCase();
191
+ if (s === 'abandoned') buckets.abandoned.push(p);
192
+ else if (s === 'merged' || s === 'completed' || s === 'closed') buckets.merged.push(p);
193
+ else buckets.active.push(p);
194
+ });
195
+ // Fixed section order: Active (open) → Merged / Completed → Abandoned.
196
+ // Empty sections are omitted entirely.
197
+ var sections = [
198
+ { label: 'Active', prs: buckets.active, open: true },
199
+ { label: 'Merged / Completed', prs: buckets.merged, open: false },
200
+ { label: 'Abandoned', prs: buckets.abandoned, open: false },
201
+ ];
202
+ sections.forEach(function(section) {
203
+ if (!section.prs.length) return;
204
+ var details = document.createElement('details');
205
+ details.className = 'tile-section';
206
+ if (section.open) details.open = true;
207
+ var summary = document.createElement('summary');
208
+ summary.className = 'tile-section-summary';
209
+ var label = document.createElement('span');
210
+ label.className = 'tile-section-label';
211
+ label.textContent = section.label;
212
+ var count = document.createElement('span');
213
+ count.className = 'tile-section-count';
214
+ count.textContent = '(' + section.prs.length + ')';
215
+ summary.appendChild(label);
216
+ summary.appendChild(count);
217
+ details.appendChild(summary);
218
+ var sectionBody = document.createElement('div');
219
+ sectionBody.className = 'tile-section-body';
220
+ section.prs.forEach(function(p) { sectionBody.appendChild(prCard(p)); });
221
+ details.appendChild(sectionBody);
222
+ body.appendChild(details);
182
223
  });
183
224
  }
184
225
 
@@ -31,7 +31,7 @@
31
31
  // chosen, so the indicator never auto-defaults and the user can always clear
32
32
  // their choice back to it. Returns the select so the caller can attach a
33
33
  // change listener.
34
- function makeContextSelect(projects, selected) {
34
+ function makeContextSelect(projects, selected, displayByName) {
35
35
  var sel = document.createElement('select');
36
36
  sel.id = 'chat-context-select';
37
37
  sel.setAttribute('aria-label', 'Working in project');
@@ -44,7 +44,7 @@
44
44
  var p = projects[i];
45
45
  var opt = document.createElement('option');
46
46
  opt.value = p;
47
- opt.textContent = p;
47
+ opt.textContent = (displayByName && displayByName[p]) || p;
48
48
  if (p === selected) opt.selected = true;
49
49
  sel.appendChild(opt);
50
50
  }
@@ -59,9 +59,11 @@
59
59
  var res = await fetch('/api/status');
60
60
  if (!res.ok) throw new Error('HTTP ' + res.status);
61
61
  var data = await res.json();
62
- var projects = (data && Array.isArray(data.projects) ? data.projects : [])
63
- .map(function(p) { return p && p.name ? String(p.name) : null; })
64
- .filter(Boolean);
62
+ var projectList = (data && Array.isArray(data.projects) ? data.projects : [])
63
+ .filter(function(p) { return p && p.name; });
64
+ var projects = projectList.map(function(p) { return String(p.name); });
65
+ var displayByName = {};
66
+ projectList.forEach(function(p) { displayByName[String(p.name)] = String(p.displayName || p.name); });
65
67
  if (projects.length === 0) {
66
68
  stripEl.style.display = '';
67
69
  controlsEl.replaceChildren(makeContextEmptyNode(), makeContextAddBtn());
@@ -79,7 +81,7 @@
79
81
  // (italicized via CSS). We deliberately do NOT fall back to projects[0]
80
82
  // here (W-mqayzsj3) so CC turns and project-scoped actions don't
81
83
  // silently target the wrong project.
82
- var sel = makeContextSelect(projects, currentProject);
84
+ var sel = makeContextSelect(projects, currentProject, displayByName);
83
85
  stripEl.style.display = '';
84
86
  controlsEl.replaceChildren(sel, makeContextAddBtn());
85
87
  sel.addEventListener('change', function(ev) {