@yemi33/minions 0.1.2178 → 0.1.2180

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 (39) hide show
  1. package/README.md +7 -5
  2. package/bin/minions.js +39 -17
  3. package/dashboard/js/command-parser.js +1 -1
  4. package/dashboard/js/memory-panel.js +324 -0
  5. package/dashboard/js/qa.js +2 -2
  6. package/dashboard/js/refresh.js +19 -1
  7. package/dashboard/js/render-other.js +143 -2
  8. package/dashboard/js/render-prs.js +2 -1
  9. package/dashboard/js/render-schedules.js +1 -1
  10. package/dashboard/js/render-watches.js +1 -1
  11. package/dashboard/js/render-work-items.js +18 -1
  12. package/dashboard/js/settings.js +23 -0
  13. package/dashboard/pages/engine-memory-panel.html +56 -0
  14. package/dashboard/pages/engine.html +1 -0
  15. package/dashboard/pages/tools.html +8 -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-build.js +17 -2
  21. package/dashboard.js +693 -19
  22. package/docs/branch-derivation.md +13 -1
  23. package/docs/diagnostics-memory.md +446 -0
  24. package/docs/harness-propagation.md +273 -0
  25. package/docs/human-vs-automated.md +1 -1
  26. package/docs/runtime-adapters.md +5 -0
  27. package/engine/cli.js +24 -5
  28. package/engine/diagnostics-memory.js +190 -0
  29. package/engine/lifecycle.js +111 -1
  30. package/engine/preflight.js +265 -0
  31. package/engine/queries.js +331 -19
  32. package/engine/runtimes/claude.js +36 -0
  33. package/engine/runtimes/codex.js +19 -0
  34. package/engine/runtimes/copilot.js +27 -36
  35. package/engine/shared.js +390 -15
  36. package/engine/spawn-agent.js +178 -12
  37. package/engine/watchdog.js +6 -0
  38. package/engine.js +277 -4
  39. package/package.json +2 -2
@@ -12,7 +12,7 @@ function renderProjects(projects) {
12
12
  // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: project name, path, branch metadata)
13
13
  list.innerHTML = visible.map(p =>
14
14
  '<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">' +
15
- escHtml(p.name) +
15
+ escHtml(p.displayName || p.name) +
16
16
  _renderProjectBranch(p) +
17
17
  '<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>' +
18
18
  '</span>'
@@ -523,7 +523,148 @@ async function _addSelectedProjects() {
523
523
  }
524
524
  }
525
525
 
526
- window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderMetrics, renderLlmPerf, renderTokenUsage, _aggregateEngineUsageForTokenTile, openScanProjectsModal };
526
+ function _scopeBadge(scope, projectName, pluginName) {
527
+ // PRD-shaped scope label (user|project:<name>|plugin|agent-skill). The
528
+ // commands feed today only emits user-roots (per-runtime), plugin, and
529
+ // project; agent-skill is reserved so when a runtime starts shipping
530
+ // agent-scoped commands they don't need a new badge style.
531
+ let label = scope;
532
+ let color = 'var(--muted)';
533
+ if (scope === 'project' && projectName) { label = 'project:' + projectName; color = 'var(--blue)'; }
534
+ else if (scope === 'plugin') { label = pluginName ? 'plugin:' + pluginName : 'plugin'; color = 'var(--purple)'; }
535
+ else if (scope === 'agent-skill') { color = 'var(--orange)'; }
536
+ else if (scope === 'claude-code' || scope === 'copilot' || scope === 'codex') {
537
+ label = 'user:' + scope;
538
+ }
539
+ return '<span style="font-size:var(--text-xs);color:' + color + ';margin-left:6px;font-weight:500">[' + escHtml(label) + ']</span>';
540
+ }
541
+
542
+ function renderCommands(commands) {
543
+ const el = document.getElementById('commands-list');
544
+ const countEl = document.getElementById('commands-count');
545
+ const arr = Array.isArray(commands) ? commands : [];
546
+ if (countEl) countEl.textContent = arr.length;
547
+ if (!el) return;
548
+ if (!arr.length) {
549
+ el.innerHTML = '<p class="empty">No slash commands found. Drop a <code>.md</code> file into <code>~/.claude/commands/</code>, <code>~/.copilot/commands/</code>, a plugin\'s commands dir, or a registered project\'s <code>.claude/commands/</code> — they\'ll appear here automatically.</p>';
550
+ return;
551
+ }
552
+
553
+ // Group by scope bucket so the list reads "user roots first, then plugins, then projects".
554
+ const buckets = new Map();
555
+ for (const c of arr) {
556
+ const key = c.scope === 'project' ? 'project:' + (c.projectName || '?')
557
+ : c.scope === 'plugin' ? 'plugin:' + (c.pluginName || '?')
558
+ : c.scope; // user-roots use their runtime name
559
+ if (!buckets.has(key)) buckets.set(key, []);
560
+ buckets.get(key).push(c);
561
+ }
562
+
563
+ const sections = [];
564
+ for (const [key, list] of buckets) {
565
+ const header = '<div style="font-size:var(--text-sm);color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin:10px 0 4px">' + escHtml(key) + ' <span style="color:var(--muted);text-transform:none;letter-spacing:0">(' + list.length + ')</span></div>';
566
+ const rows = list.map(c =>
567
+ '<div style="font-size:var(--text-base);padding:4px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--text);display:inline-flex;align-items:center;gap:6px;margin:2px 4px 2px 0" title="' + escHtml((c.title || '') + (c.rel ? ' • ' + c.rel : '')) + '">' +
568
+ '<code style="color:var(--blue);background:transparent;padding:0">/' + escHtml(c.commandName || '') + '</code>' +
569
+ _scopeBadge(c.scope, c.projectName, c.pluginName) +
570
+ '</div>'
571
+ ).join('');
572
+ sections.push(header + '<div style="display:flex;flex-wrap:wrap;gap:4px">' + rows + '</div>');
573
+ }
574
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: scope key, command name, project name, plugin name, title, rel path)
575
+ el.innerHTML = sections.join('') +
576
+ '<p style="font-size:var(--text-sm);color:var(--muted);margin:10px 0 0">Mirrors the runtime-native command roots that adapters expose via <code>getCommandRoots()</code>. Projects and plugins are aggregated alongside user-scope dirs.</p>';
577
+ }
578
+
579
+ function _harnessRowsBlock(label, rows) {
580
+ if (!rows || !rows.length) {
581
+ return '<div style="font-size:var(--text-sm);color:var(--muted);margin:4px 0 0 12px">' + escHtml(label) + ': <em>(adapter does not expose this)</em></div>';
582
+ }
583
+ const body = rows.map(r => {
584
+ const marker = r.exists ? '<span style="color:var(--green)">✓</span>' : '<span style="color:var(--orange)">⚠</span>';
585
+ const missing = r.exists ? '' : ' <span style="color:var(--orange);font-size:var(--text-xs)">(missing on disk)</span>';
586
+ return '<div style="font-family:monospace;font-size:var(--text-sm);margin-left:18px;display:flex;gap:8px;align-items:baseline">' +
587
+ marker + ' <span>' + escHtml(r.path) + '</span> <span style="color:var(--muted)">[' + escHtml(r.scope) + ']</span>' + missing +
588
+ '</div>';
589
+ }).join('');
590
+ return '<div style="margin:4px 0 0 12px"><div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px">' + escHtml(label) + ':</div>' + body + '</div>';
591
+ }
592
+
593
+ function renderHarnessDiagnostics(diag) {
594
+ const el = document.getElementById('harness-diag');
595
+ if (!el) return;
596
+ if (!diag || typeof diag !== 'object') {
597
+ el.innerHTML = '<p class="empty">Harness diagnostics unavailable.</p>';
598
+ return;
599
+ }
600
+ const parts = [];
601
+ parts.push('<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:8px">Fleet-default runtime: <code style="color:var(--blue)">' + escHtml(diag.fleetDefaultCli || '?') + '</code></div>');
602
+
603
+ // Per-runtime adapter rows
604
+ for (const r of diag.runtimes || []) {
605
+ parts.push('<details style="margin:6px 0;border:1px solid var(--border);border-radius:6px;padding:8px 10px;background:var(--surface2)"><summary style="font-weight:600;cursor:pointer">Runtime: ' + escHtml(r.name) + '</summary>' +
606
+ _harnessRowsBlock('User asset dirs (--add-dir)', r.userAssetDirs) +
607
+ _harnessRowsBlock('Skill roots (CLI native discovery)', r.skillRoots) +
608
+ _harnessRowsBlock('Skill write targets', r.skillWriteTargets) +
609
+ _harnessRowsBlock('Slash command roots', r.commandRoots) +
610
+ _harnessRowsBlock('MCP config files', r.mcpConfigPaths) +
611
+ '</details>');
612
+ }
613
+
614
+ // --add-dir snapshot for fleet default
615
+ const snap = diag.addDirSnapshot || [];
616
+ parts.push('<details open style="margin:6px 0;border:1px solid var(--border);border-radius:6px;padding:8px 10px;background:var(--surface2)"><summary style="font-weight:600;cursor:pointer">--add-dir snapshot for ' + escHtml(diag.fleetDefaultCli || 'fleet default') + ' <span style="color:var(--muted);font-weight:400">(' + snap.length + ' dir' + (snap.length === 1 ? '' : 's') + ')</span></summary>' +
617
+ (snap.length === 0
618
+ ? '<div style="font-size:var(--text-sm);color:var(--muted);margin:6px 0 0 12px"><em>(no dirs attached — engine/spawn-agent.js unavailable or adapter has no asset dirs)</em></div>'
619
+ : snap.map(d =>
620
+ '<div style="font-family:monospace;font-size:var(--text-sm);margin-left:18px;display:flex;gap:8px;align-items:baseline"><span style="color:var(--green)">✓</span> <span>' + escHtml(d.path) + '</span> <span style="color:var(--muted)">[' + escHtml(d.scope) + ']</span></div>'
621
+ ).join('')
622
+ ) +
623
+ '</details>');
624
+
625
+ // Suppressed assets
626
+ const suppressed = diag.suppressed || [];
627
+ if (suppressed.length > 0) {
628
+ parts.push('<details open style="margin:6px 0;border:1px solid var(--border);border-radius:6px;padding:8px 10px;background:var(--surface2)"><summary style="font-weight:600;cursor:pointer">Suppressed assets <span style="color:var(--muted);font-weight:400">(' + suppressed.length + ')</span></summary>' +
629
+ suppressed.map(s =>
630
+ '<div style="margin:6px 0 0 12px"><code style="color:var(--orange)">' + escHtml(s.flag) + '</code> <span style="color:var(--muted);font-size:var(--text-sm)">(' + escHtml(String(s.value)) + ')</span>' +
631
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin-left:18px">' + escHtml(s.effect || '') + '</div></div>'
632
+ ).join('') +
633
+ '</details>');
634
+ }
635
+
636
+ // Project-local-on-main footgun
637
+ const footgun = (diag.projectLocalOnMain || []).filter(e => e && e.uncommittedAssets && e.uncommittedAssets.length > 0);
638
+ if (footgun.length > 0) {
639
+ parts.push('<details open style="margin:6px 0;border:1px solid var(--orange);border-radius:6px;padding:8px 10px;background:var(--surface2)"><summary style="font-weight:600;cursor:pointer;color:var(--orange)">⚠ Project-local assets on main checkout (won\'t propagate to worktree)</summary>' +
640
+ footgun.map(entry =>
641
+ '<div style="margin:8px 0 0 12px">' +
642
+ '<div style="font-weight:600">' + escHtml(entry.project) + ' <span style="font-family:monospace;color:var(--muted);font-weight:400">' + escHtml(entry.localPath) + '</span></div>' +
643
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin:4px 0 6px">' + escHtml(entry.footgunWarning || '') + '</div>' +
644
+ entry.uncommittedAssets.map(a =>
645
+ '<div style="font-family:monospace;font-size:var(--text-sm);margin-left:12px;display:flex;gap:8px;align-items:baseline"><span style="color:var(--orange)">⚠</span> <span>' + escHtml(a.file) + '</span> <span style="color:var(--muted)">[' + escHtml(a.kind) + ']</span></div>'
646
+ ).join('') +
647
+ '</div>'
648
+ ).join('') +
649
+ '</details>');
650
+ }
651
+
652
+ // Missing dirs summary (aggregate)
653
+ const missing = diag.missingDirs || [];
654
+ if (missing.length > 0) {
655
+ parts.push('<details style="margin:6px 0;border:1px solid var(--border);border-radius:6px;padding:8px 10px;background:var(--surface2)"><summary style="font-weight:600;cursor:pointer">Missing on-disk dirs <span style="color:var(--muted);font-weight:400">(' + missing.length + ')</span></summary>' +
656
+ '<div style="font-size:var(--text-sm);color:var(--muted);margin:4px 0 6px 12px">Adapters declare these paths but they do not exist on this host. Warnings, not failures — create them if the runtime should look there.</div>' +
657
+ missing.map(m =>
658
+ '<div style="font-family:monospace;font-size:var(--text-sm);margin-left:18px"><span style="color:var(--orange)">⚠</span> ' + escHtml(m.path) + ' <span style="color:var(--muted)">[' + escHtml(m.scope) + ' · ' + escHtml(m.kind) + ' · ' + escHtml(m.runtime) + ']</span></div>'
659
+ ).join('') +
660
+ '</details>');
661
+ }
662
+
663
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: runtime name, path, scope, flag, effect, project name, localPath, file, kind)
664
+ el.innerHTML = parts.join('');
665
+ }
666
+
667
+ window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderCommands, renderHarnessDiagnostics, renderMetrics, renderLlmPerf, renderTokenUsage, _aggregateEngineUsageForTokenTile, openScanProjectsModal };
527
668
 
528
669
  // ─── keep_processes panel (W-mp68q6ke0010de68) ─────────────────────────────
529
670
  // Polls /api/keep-processes every refresh (engine page only) and renders a
@@ -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>' +
@@ -372,6 +372,10 @@ async function editWorkItem(id, source) {
372
372
  '<label style="color:var(--text);font-size:var(--text-md)">Depends On (work-item ids, comma- or newline-separated)' +
373
373
  '<textarea id="wi-edit-depends-on" rows="2" placeholder="W-foo, W-bar" style="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;resize:vertical">' + escapeHtml((Array.isArray(item.depends_on) ? item.depends_on : []).join(', ')) + '</textarea>' +
374
374
  '</label>' +
375
+ '<label style="color:var(--text);font-size:var(--text-md)">Working directory (subpath, optional)' +
376
+ '<input id="wi-edit-workdir" placeholder="packages/foo" value="' + escapeHtml((item.meta && item.meta.workdir) || '') + '" style="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">' +
377
+ '<small style="display:block;margin-top:4px;color:var(--muted);font-size:var(--text-sm)">Relative POSIX subpath under the project root (or worktree, for code-mutating types). Leave empty to dispatch at the project root. Examples: <code>packages/foo</code>, <code>apps/dashboard</code>. Absolute paths and <code>..</code> segments are rejected.</small>' +
378
+ '</label>' +
375
379
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:8px">' +
376
380
  '<button onclick="closeModal()" class="pr-pager-btn" style="padding:6px 16px;font-size:var(--text-md)">Cancel</button>' +
377
381
  '<button onclick="submitWorkItemEdit(\'' + escapeHtml(id) + '\',\'' + escapeHtml(source || '') + '\',event)" class="btn-primary-lg">Save</button>' +
@@ -396,13 +400,14 @@ async function submitWorkItemEdit(id, source, e) {
396
400
  const acceptanceCriteria = acRaw.split('\n').filter(function(l) { return l.trim(); });
397
401
  const dependsRaw = document.getElementById('wi-edit-depends-on')?.value || '';
398
402
  const depends_on = dependsRaw.split(/[\n,]/).map(function(s) { return s.trim(); }).filter(Boolean);
403
+ const workdirRaw = (document.getElementById('wi-edit-workdir')?.value || '').trim();
399
404
  if (!title) { if (btn) { btn.disabled = false; btn.textContent = 'Save'; } alert('Title is required'); return; }
400
405
  try { closeModal(); } catch { /* may not be open */ }
401
406
  showToast('cmd-toast', 'Work item updated', true);
402
407
  try {
403
408
  const res = await fetch('/api/work-items/update', {
404
409
  method: 'POST', headers: { 'Content-Type': 'application/json' },
405
- body: JSON.stringify({ id, source: source || undefined, title, description, type, priority, agent, references, acceptanceCriteria, depends_on })
410
+ body: JSON.stringify({ id, source: source || undefined, title, description, type, priority, agent, references, acceptanceCriteria, depends_on, workdir: workdirRaw || null })
406
411
  });
407
412
  if (res.ok) { refresh(); } else { const d = await res.json().catch(() => ({})); alert('Update failed: ' + (d.error || 'unknown')); editWorkItem(id, source); }
408
413
  } catch (e) { alert('Update error: ' + e.message); editWorkItem(id, source); }
@@ -592,6 +597,9 @@ function openCreateWorkItemModal() {
592
597
  '<label style="color:var(--text);font-size:var(--text-md)">Acceptance Criteria <textarea id="wi-new-ac" rows="2" style="' + inputStyle + ';resize:vertical" placeholder="One criterion per line (optional)"></textarea></label>' +
593
598
  '<label style="color:var(--text);font-size:var(--text-md)">Depends On <textarea id="wi-new-depends-on" rows="2" style="' + inputStyle + ';resize:vertical" placeholder="W-foo, W-bar — comma- or newline-separated work-item ids (optional)"></textarea></label>' +
594
599
  '<label style="color:var(--text);font-size:var(--text-md)">References <textarea id="wi-new-refs" rows="2" style="' + inputStyle + ';resize:vertical" placeholder="url | title | type — one per line (optional)"></textarea></label>' +
600
+ '<label style="color:var(--text);font-size:var(--text-md)">Working directory (subpath, optional) <input id="wi-new-workdir" style="' + inputStyle + '" placeholder="packages/foo — leave empty for project root">' +
601
+ '<small style="display:block;margin-top:4px;color:var(--muted);font-size:var(--text-sm)">Relative subpath under the project root. Use for monorepo subpackage dispatches so the agent\'s cwd lands inside <code>packages/foo</code> instead of the project root.</small>' +
602
+ '</label>' +
595
603
  '<label id="wi-new-skippr-row" style="color:var(--text);font-size:var(--text-md);display:flex;gap:8px;align-items:center;cursor:pointer"><input type="checkbox" id="wi-new-skippr"> Skip PR creation (push branch only)</label>' +
596
604
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
597
605
  '<button onclick="closeModal()" class="pr-pager-btn">Cancel</button>' +
@@ -629,6 +637,7 @@ async function _submitCreateWorkItem(e) {
629
637
  const parts = l.split('|').map(s => s.trim());
630
638
  return { url: parts[0] || '', title: parts[1] || parts[0] || '', type: parts[2] || 'link' };
631
639
  }).filter(r => r.url);
640
+ const workdir = (document.getElementById('wi-new-workdir')?.value || '').trim();
632
641
 
633
642
  try {
634
643
  const body = { title, description: desc, type, priority };
@@ -637,6 +646,7 @@ async function _submitCreateWorkItem(e) {
637
646
  if (acceptanceCriteria.length) body.acceptanceCriteria = acceptanceCriteria;
638
647
  if (references.length && references[0].url) body.references = references;
639
648
  if (depends_on.length) body.depends_on = depends_on;
649
+ if (workdir) body.meta = Object.assign({}, body.meta, { workdir: workdir });
640
650
  const skipPr = document.getElementById('wi-new-skippr')?.checked || false;
641
651
  if (skipPr) body.skipPr = true;
642
652
 
@@ -707,6 +717,13 @@ function _wiRenderDetail(item) {
707
717
  html += field('Description', '<div id="wi-detail-desc" style="font-size:var(--text-md);max-height:320px;overflow-y:auto;padding:8px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm)">' + _descHtml + '</div>');
708
718
  html += field('Agent', escapeHtml(item.dispatched_to || item.agent || 'Auto'));
709
719
  html += field('Source', escapeHtml(item._source || 'central'));
720
+ // P-714ef144 — surface meta.workdir in the detail modal so operators can
721
+ // confirm the agent's actual cwd. Renders nothing when unset (which is
722
+ // the back-compat default = project root). Code-tagged so the path is
723
+ // visually clearly distinct from prose fields.
724
+ if (item.meta && typeof item.meta.workdir === 'string' && item.meta.workdir) {
725
+ html += field('Working directory', '<code style="font-size:var(--text-sm);background:var(--surface2);padding:2px 6px;border-radius:var(--radius-sm)">' + escapeHtml(item.meta.workdir) + '</code>');
726
+ }
710
727
  if (item.created) html += field('Created', escapeHtml(formatLocalDateTime(item.created)));
711
728
  if (item.dispatched_at) html += field('Dispatched', escapeHtml(formatLocalDateTime(item.dispatched_at)) + ' to ' + escapeHtml(item.dispatched_to || '?'));
712
729
  if (item.completedAt) html += field('Completed', escapeHtml(formatLocalDateTime(item.completedAt)));
@@ -422,6 +422,22 @@ async function openSettings() {
422
422
  'Permission bypass is runtime-owned: Claude agents use <code>--dangerously-skip-permissions</code>; Copilot agents use <code>--autopilot --allow-all --no-ask-user</code>. There is no dashboard permission-mode setting.' +
423
423
  '</div>';
424
424
 
425
+ // Harness propagation knobs (PL-seamless-harness-invocation, P-2bd84e91).
426
+ // All three flags govern what the spawned agent inherits from the operator's
427
+ // local CLI harness (user-scope skills/commands/MCPs, project-local-on-main
428
+ // assets, Claude workspace .mcp.json pre-approval). See docs/harness-propagation.md.
429
+ const paneHarness =
430
+ '<h3>Harness Propagation</h3>' +
431
+ '<div class="settings-pane-sub">Controls what the spawned agent sees from your local CLI harness (skills, commands, MCPs). Defaults match the docs/harness-propagation.md contract — flip these only if you understand the tradeoff. The Tools tab\'s Harness diagnostics view shows the live propagated surface.</div>' +
432
+ '<div class="settings-stack" style="margin-bottom:12px">' +
433
+ settingsToggle('Propagate project-local harness on main', 'set-harnessPropagateProjectLocal', e.harnessPropagateProjectLocal !== false,
434
+ 'P-08b62d49: surface uncommitted <repo>/.claude/skills, <repo>/.copilot/commands, etc. into the worktree dispatch via extra --add-dir entries. Default ON closes the worktree-uncommitted footgun. Turn OFF to fall back to the legacy "only committed assets are visible" behavior.') +
435
+ settingsToggle('Claude: pre-approve workspace .mcp.json', 'set-claudePreApproveWorkspaceMcps', e.claudePreApproveWorkspaceMcps !== false,
436
+ 'P-7d31a06b: pre-warms ~/.claude.json projects.<worktree>.enabledMcpjsonServers on every Claude spawn so the first invocation in a brand-new worktree skips the "trust this server?" prompt — which would otherwise be invisible behind --dangerously-skip-permissions and silently drop workspace MCPs. Claude-only (no-op on Copilot/Codex). Best-effort: failures never block dispatch.') +
437
+ settingsToggle('Hermetic harness (drop user-scope assets)', 'set-hermeticHarness', !!e.hermeticHarness,
438
+ '⚠ P-49e1c8b7 fleet-wide opt-out: when ON, --add-dir is exactly [minionsDir] (user-scope skill/command/MCP roots dropped), project-local propagation skipped, and Claude workspace .mcp.json pre-approval skipped. Use when you want a known-empty harness surface (reproducible CI runs, debugging "works in my CLI" bugs). Per-agent override available via agent.hermeticHarness. Default OFF.') +
439
+ '</div>';
440
+
425
441
  const paneBudget =
426
442
  '<h3>Budget</h3>' +
427
443
  '<div class="settings-pane-sub">Fleet-wide spend ceiling. Per-agent monthly caps live in the Agents table on the Runtime &amp; Models tab.</div>' +
@@ -522,6 +538,7 @@ async function openSettings() {
522
538
  { id: 'worktree', label: 'Worker Pool & Worktrees', html: paneWorktree },
523
539
  { id: 'copilot', label: 'Copilot Tuning', html: paneCopilot },
524
540
  { id: 'claude', label: 'Claude Tuning', html: paneClaude },
541
+ { id: 'harness', label: 'Harness Propagation', html: paneHarness },
525
542
  { id: 'budget', label: 'Budget', html: paneBudget },
526
543
  { id: 'maxturns', label: 'Max Turns', html: paneMaxTurns },
527
544
  { id: 'features', label: 'Feature Flags', html: paneFeatures },
@@ -1025,6 +1042,12 @@ async function saveSettings() {
1025
1042
  copilotSuppressAgentsMd: !!document.getElementById('set-copilotSuppressAgentsMd')?.checked,
1026
1043
  copilotStreamMode: document.getElementById('set-copilotStreamMode')?.value || 'on',
1027
1044
  copilotReasoningSummaries: !!document.getElementById('set-copilotReasoningSummaries')?.checked,
1045
+ // Harness propagation knobs (P-2bd84e91). Booleans round-trip through
1046
+ // handleSettingsUpdate's auto boolean-fields loop because all three live
1047
+ // in ENGINE_DEFAULTS as booleans.
1048
+ harnessPropagateProjectLocal: !!document.getElementById('set-harnessPropagateProjectLocal')?.checked,
1049
+ claudePreApproveWorkspaceMcps: !!document.getElementById('set-claudePreApproveWorkspaceMcps')?.checked,
1050
+ hermeticHarness: !!document.getElementById('set-hermeticHarness')?.checked,
1028
1051
  maxBudgetUsd: (document.getElementById('set-maxBudgetUsd')?.value ?? '').trim(),
1029
1052
  disableModelDiscovery: !!document.getElementById('set-disableModelDiscovery')?.checked,
1030
1053
  qaDualWriteJson: !!document.getElementById('set-qaDualWriteJson')?.checked,
@@ -0,0 +1,56 @@
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
+ <div id="memory-heap-snapshot-panel" style="margin-top:12px;padding:10px;border:1px solid var(--border);border-radius:4px;background:var(--surface2)">
50
+ <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
51
+ <button type="button" id="memory-heap-snapshot-btn" class="btn" style="cursor:pointer">Capture heap snapshot</button>
52
+ <span style="font-size:var(--text-sm);color:var(--muted)">Stalls engine + dashboard for several seconds, writes a 50–200 MB <code>.heapsnapshot</code> per process under <code>engine/diagnostics/</code> (load in Chrome DevTools → Memory → Load). Rate-limited to 1 / 60 s; only the 5 most-recent per process are kept.</span>
53
+ </div>
54
+ <div id="memory-heap-snapshot-result" style="margin-top:8px;font-family:monospace;font-size:var(--text-sm);color:var(--muted);white-space:pre-wrap"></div>
55
+ </div>
56
+ </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>
@@ -2,7 +2,15 @@
2
2
  <h2>Minions Skills <span class="count" id="skills-count">0</span> <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">discovered from runtime native dirs, plugin installs, and configured project repos</span></h2>
3
3
  <div id="skills-list"><p class="empty">No skills yet. Agents create these when they discover repeatable workflows.</p></div>
4
4
  </section>
5
+ <section>
6
+ <h2>Slash Commands <span class="count" id="commands-count">0</span> <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">discovered from runtime native command dirs, plugin installs, and configured project repos</span></h2>
7
+ <div id="commands-list"><p class="empty">No slash commands discovered.</p></div>
8
+ </section>
5
9
  <section>
6
10
  <h2>MCP Servers <span class="count" id="mcp-count">0</span></h2>
7
11
  <div id="mcp-list"><p class="empty">No MCP servers synced.</p></div>
8
12
  </section>
13
+ <section>
14
+ <h2>Harness Propagation <span style="font-size:var(--text-sm);color:var(--muted);font-weight:400;text-transform:none;letter-spacing:0">what each runtime sees, what's attached via --add-dir, and which assets won't propagate to a fresh worktree</span></h2>
15
+ <div id="harness-diag"><p class="empty">Loading harness diagnostics…</p></div>
16
+ </section>
@@ -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) {
@@ -681,6 +681,26 @@
681
681
  .tile-chip.blue { background: rgba(88, 166, 255, 0.12); color: var(--blue); border-color: var(--blue); }
682
682
  .tile-empty { color: var(--muted); font-style: italic; font-size: var(--text-md); }
683
683
 
684
+ /* Collapsible PR sections (Active / Merged-Completed / Abandoned). Uses
685
+ native <details>/<summary> — no JS wiring needed for the toggle. */
686
+ .tile-section { margin-bottom: 10px; }
687
+ .tile-section:last-child { margin-bottom: 0; }
688
+ .tile-section-summary {
689
+ display: flex; align-items: center; gap: 6px;
690
+ cursor: pointer; user-select: none;
691
+ padding: 4px 2px; margin-bottom: 6px;
692
+ list-style: none;
693
+ font-size: var(--text-base); color: var(--muted);
694
+ }
695
+ .tile-section-summary::-webkit-details-marker { display: none; }
696
+ .tile-section-summary::before {
697
+ content: "▸"; color: var(--muted); font-size: var(--text-sm);
698
+ transition: transform 0.15s ease;
699
+ }
700
+ .tile-section[open] > .tile-section-summary::before { content: "▾"; }
701
+ .tile-section-label { font-weight: 700; letter-spacing: 0.3px; text-transform: uppercase; color: var(--text); }
702
+ .tile-section-count { color: var(--muted); }
703
+
684
704
  /* Pinned-context list rows (slim-pinned-modal). */
685
705
  .pinned-row {
686
706
  border: 1px solid var(--border);
@@ -21,9 +21,24 @@ function buildDashboardHtml() {
21
21
  const css = safeRead(path.join(dashDir, 'styles.css'));
22
22
 
23
23
  const pages = ['home', 'work', 'prs', 'plans', 'inbox', 'tools', 'schedule', 'watches', 'pipelines', 'meetings', 'qa', 'engine'];
24
+ // Sub-fragments substituted into a parent page at assembly time via
25
+ // <!-- __MARKER__ --> tokens. Lets large panels live in their own
26
+ // fragment file without inflating the parent page. P-d4e5f6a7 introduced
27
+ // engine-memory-panel.html as the first such sub-fragment; add more here
28
+ // by mapping marker -> fragment basename.
29
+ const pageSubFragments = {
30
+ engine: { '<!-- __ENGINE_MEMORY_PANEL__ -->': 'engine-memory-panel' },
31
+ };
24
32
  let pageHtml = '';
25
33
  for (const p of pages) {
26
- const content = safeRead(path.join(dashDir, 'pages', p + '.html'));
34
+ let content = safeRead(path.join(dashDir, 'pages', p + '.html'));
35
+ const subs = pageSubFragments[p];
36
+ if (subs) {
37
+ for (const [marker, basename] of Object.entries(subs)) {
38
+ const fragment = safeRead(path.join(dashDir, 'pages', basename + '.html'));
39
+ content = content.replace(marker, () => fragment);
40
+ }
41
+ }
27
42
  const activeClass = p === 'home' ? ' active' : '';
28
43
  pageHtml += ` <div class="page${activeClass}" id="page-${p}">\n${content}\n </div>\n\n`;
29
44
  }
@@ -32,7 +47,7 @@ function buildDashboardHtml() {
32
47
  'utils', 'state', 'features-client', 'render-utils', 'detail-panel', 'live-stream',
33
48
  'render-agents', 'render-dispatch', 'render-work-items', 'render-prd',
34
49
  'render-prs', 'render-plans', 'render-inbox', 'render-kb', 'render-skills',
35
- 'render-other', 'render-managed', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
50
+ 'render-other', 'render-managed', 'memory-panel', 'render-schedules', 'render-watches', 'render-pipelines', 'render-meetings', 'render-pinned',
36
51
  'command-parser', 'command-input', 'command-center', 'command-history',
37
52
  'confirm-dialog', 'modal', 'modal-qa', 'settings', 'qa', 'fre', 'refresh'
38
53
  ];