@yemi33/minions 0.1.2179 → 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.
package/README.md CHANGED
@@ -57,7 +57,7 @@ node ~/.minions/minions.js init
57
57
  | `minions remove <dir-or-name> [--keep-data \| --purge --force]` | Unlink a project: cancels pending work items, drains dispatch + kills active agents, cleans worktrees, disables linked schedules, archives `projects/<name>/` to `projects/.archived/<name>-YYYYMMDD/`. Use `--keep-data` to leave the data dir in place, or `--purge --force` to delete it. |
58
58
  | `minions list` | List all linked projects with descriptions |
59
59
  | `minions restart` | Start engine and dashboard together (recommended after reboot) |
60
- | `minions start` | Start engine daemon (ticks every 10s, auto-syncs MCP servers) |
60
+ | `minions start` | Start engine daemon (ticks every 10s) |
61
61
  | `minions stop` | Stop the engine |
62
62
  | `minions status` | Show agents, projects, dispatch queue, quality metrics |
63
63
  | `minions pause` / `resume` | Pause/resume dispatching |
@@ -71,7 +71,7 @@ node ~/.minions/minions.js init
71
71
  | `minions kill` | Kill all active agents and reset their dispatches to pending |
72
72
  | `minions complete <dispatch-id>` | Manually mark a dispatch as completed |
73
73
  | `minions config set-cli <R> [--model M]` | Persist the default runtime/model without starting the engine |
74
- | `minions mcp-sync` | Sync MCP servers from `~/.claude.json` |
74
+ | `minions mcp-sync` | Print harness propagation diagnostic (read-only; same source as `minions doctor --harness`) |
75
75
  | `minions cleanup` | Run cleanup manually (temp files, worktrees, zombies) |
76
76
  | `minions dash` | Open dashboard (starts if not already running, port 7331) |
77
77
  | `minions nuke --confirm` | Factory reset runtime state and reset config to defaults |
@@ -322,9 +322,11 @@ When dispatching agents, the engine reads each project's `CLAUDE.md` and injects
322
322
 
323
323
  Agents need repo-host tooling to create PRs, post review comments, check status, and handle review feedback. GitHub repos use `gh`. Azure DevOps repos should use the `az` CLI first and keep the Azure DevOps MCP server available only as a fallback when `az` is unavailable or does not support the required operation.
324
324
 
325
- Agents inherit MCP servers directly from `~/.claude.json` as Claude Code processes add fallback servers there and they're immediately available to all agents on next spawn.
325
+ Agents inherit MCP servers, skills, and slash-commands directly from each runtime's native config — Claude reads `~/.claude.json` + `~/.claude/{skills,commands}/`, Copilot reads `~/.copilot/` + `~/.agents/`, Codex reads `~/.codex/`. Add fallback servers, skills, or commands there and they're immediately available to all agents on next spawn. No sync step is required.
326
326
 
327
- Manually refresh with `minions mcp-sync`.
327
+ **What your agent sees.** On every dispatch, the engine merges three layers around the agent's worktree: (1) **user-scope** MCP servers, skills, and slash-commands from the runtime's home dir (e.g. `~/.claude/`, `~/.copilot/`); (2) **project-scope** equivalents from `<repo>/.claude/`, `<repo>/.copilot/`, `<repo>/.mcp.json` — propagated even when the worktree is on a different branch than your live checkout (gate: `engine.harnessPropagateProjectLocal`, default on); (3) **workspace MCPs** from the worktree's `.mcp.json`, pre-approved for Claude so the agent never has to prompt (gate: `engine.claudePreApproveWorkspaceMcps`, default on). Per-agent opt-out via `agent.hermeticHarness: true` (or fleet-wide `engine.hermeticHarness: true`) strips everything except `minionsDir`, giving you a known-empty harness for reproducible runs.
328
+
329
+ To audit which propagation surfaces (asset dirs, skill roots, slash-command dirs, MCP config files) each runtime sees on this host, run `minions mcp-sync` (or `minions doctor --harness` for the same diagnostic with a non-zero exit on registry errors). Both commands are read-only — they print the per-runtime survey without mutating any state. See [docs/harness-propagation.md](docs/harness-propagation.md) for the propagation contract.
328
330
 
329
331
  ### GitHub Users
330
332
 
@@ -346,7 +348,7 @@ az login
346
348
  az devops configure --defaults organization=https://dev.azure.com/YOUR_ORG project=YOUR_PROJECT
347
349
  ```
348
350
 
349
- Optionally add the [Azure DevOps MCP server](https://github.com/microsoft/azure-devops-mcp) to your Claude Code settings (`~/.claude.json`) as a fallback. The engine will auto-sync it to all agents on next start.
351
+ Optionally add the [Azure DevOps MCP server](https://github.com/microsoft/azure-devops-mcp) to your Claude Code settings (`~/.claude.json`) as a fallback. Agents inherit it on next spawn no sync step needed.
350
352
 
351
353
  ## Work Items
352
354
 
package/bin/minions.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * minions list List linked projects
11
11
  * minions update [--no-wait] Update to latest version (--no-wait backgrounds the post-update restart)
12
12
  * minions version Show installed and package versions
13
- * minions doctor Check prerequisites and runtime health
13
+ * minions doctor [--harness] Check prerequisites and runtime health (--harness: print harness propagation diagnostic)
14
14
  * minions restart [--open] Stop + start engine + dashboard (--open forces a new browser tab)
15
15
  * minions start [--open] Start engine + dashboard if not already running (no-op when up)
16
16
  * minions stop Stop the engine
@@ -29,7 +29,7 @@
29
29
  * minions config set-cli <R> [--model M] Persist default runtime/model
30
30
  * minions bridge <subcmd> Constellation bridge: status|health|enable|disable
31
31
  * minions plan <file|text> [proj] Run a plan
32
- * minions mcp-sync Sync MCP servers from ~/.claude.json
32
+ * minions mcp-sync Print harness propagation diagnostic (read-only; same source as `doctor --harness`)
33
33
  * minions nuke --confirm Factory reset runtime state/config
34
34
  * minions uninstall --confirm Remove Minions and uninstall package
35
35
  */
@@ -1100,7 +1100,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
1100
1100
  minions init Bootstrap ~/.minions/ (first time)
1101
1101
  minions update [--no-wait] Update to latest version (--no-wait backgrounds the restart)
1102
1102
  minions version Show installed vs package version
1103
- minions doctor Check prerequisites and runtime health
1103
+ minions doctor [--harness] Check prerequisites and runtime health (--harness: print harness propagation diagnostic)
1104
1104
  minions add <project-dir> Link a project (interactive)
1105
1105
  minions remove <project-dir> Unlink a project
1106
1106
  minions list List linked projects
@@ -1123,7 +1123,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
1123
1123
  minions config set-cli <R> [--model M]
1124
1124
  Persist default runtime/model without starting
1125
1125
  minions bridge <subcmd> Constellation bridge: status|health|enable|disable
1126
- minions mcp-sync Sync MCP servers from ~/.claude.json
1126
+ minions mcp-sync Print harness propagation diagnostic (read-only; same source as 'doctor --harness')
1127
1127
  minions cleanup Clean temp files, worktrees, zombies
1128
1128
  minions pr comment <repo> <n> Post a marker-prepended PR comment via gh
1129
1129
  --agent <id> --kind <k> [--wi <id>] (--body-file <f> | --body <text>)
@@ -1450,8 +1450,17 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
1450
1450
  console.log(' npm install -g @yemi33/minions && minions init\n');
1451
1451
  } else if (cmd === 'doctor') {
1452
1452
  ensureInstalled();
1453
- const { doctor } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
1454
- doctor(MINIONS_HOME).then(ok => process.exit(ok ? 0 : 1));
1453
+ const { doctor, runHarnessDoctor } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
1454
+ if (rest.includes('--harness')) {
1455
+ // P-a3f9b2c1 — print the harness propagation diagnostic and exit. The
1456
+ // legacy `minions doctor` still runs the full preflight + runtime fleet
1457
+ // sweep; `--harness` is a one-shot survey of the asset-propagation
1458
+ // surfaces only (see docs/harness-propagation.md).
1459
+ const ok = runHarnessDoctor(MINIONS_HOME);
1460
+ process.exit(ok ? 0 : 1);
1461
+ } else {
1462
+ doctor(MINIONS_HOME).then(ok => process.exit(ok ? 0 : 1));
1463
+ }
1455
1464
  } else if (cmd === 'watchdog') {
1456
1465
  // External recovery scheduled by the OS. Survives cluster-kill scenarios
1457
1466
  // the in-process supervisor can't (Windows job-object teardown when the
@@ -211,6 +211,66 @@ async function _memRefreshHistory() {
211
211
  }));
212
212
  }
213
213
 
214
+ // P-e5f6a7b8 — operator-driven heap snapshot capture button. Gates the
215
+ // POST behind a window.confirm() that calls out the multi-second stall
216
+ // and the 50–200 MB per-process disk write. Renders the resulting paths
217
+ // (and any engine timeout) into #memory-heap-snapshot-result via
218
+ // textContent so the XSS gate remains clean.
219
+ const HEAP_SNAPSHOT_CONFIRM_TOKEN = 'YES_I_UNDERSTAND_THIS_STALLS_THE_ENGINE';
220
+ const HEAP_SNAPSHOT_CONFIRM_MSG =
221
+ 'Capture a heap snapshot from BOTH the engine and dashboard processes?\n\n' +
222
+ '⚠️ Each process will be stalled for several seconds and will write a 50–200 MB ' +
223
+ '.heapsnapshot file under engine/diagnostics/. Only do this when actively ' +
224
+ 'debugging a leak. Rate-limited to 1 capture per 60 seconds.';
225
+
226
+ function _memSetHeapResult(text) {
227
+ const el = document.getElementById('memory-heap-snapshot-result');
228
+ if (el) el.textContent = text;
229
+ }
230
+
231
+ async function _memCaptureHeapSnapshot() {
232
+ const btn = document.getElementById('memory-heap-snapshot-btn');
233
+ if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
234
+ if (!window.confirm(HEAP_SNAPSHOT_CONFIRM_MSG)) return;
235
+ }
236
+ if (btn) { btn.disabled = true; }
237
+ _memSetHeapResult('Capturing… (engine + dashboard each stall for several seconds)');
238
+ try {
239
+ const res = await fetch(
240
+ '/api/diagnostics/heap-snapshot?confirm=' + encodeURIComponent(HEAP_SNAPSHOT_CONFIRM_TOKEN),
241
+ { method: 'POST' });
242
+ let data = null;
243
+ try { data = await res.json(); } catch { /* empty body */ }
244
+ if (!res.ok) {
245
+ const msg = (data && (data.error || data.hint)) || ('HTTP ' + res.status);
246
+ _memSetHeapResult('Capture failed: ' + msg);
247
+ return;
248
+ }
249
+ const lines = [];
250
+ lines.push('Dashboard snapshot: ' + (data && data.dashboardSnapshot ? data.dashboardSnapshot : '(none)'));
251
+ if (data && data.engineTimedOut) {
252
+ lines.push('Engine snapshot: TIMED OUT after ' + ((data.timeoutMs || 30000) / 1000) + 's ' +
253
+ '— the engine may have captured it after the dashboard gave up; check engine/diagnostics/.');
254
+ } else {
255
+ lines.push('Engine snapshot: ' + (data && data.engineSnapshot ? data.engineSnapshot : '(none)'));
256
+ }
257
+ lines.push('');
258
+ lines.push('Load these in Chrome DevTools → Memory → Load to inspect.');
259
+ _memSetHeapResult(lines.join('\n'));
260
+ } catch (e) {
261
+ _memSetHeapResult('Capture failed: ' + (e && e.message ? e.message : String(e)));
262
+ } finally {
263
+ if (btn) { btn.disabled = false; }
264
+ }
265
+ }
266
+
267
+ function _memWireHeapSnapshotButton() {
268
+ const btn = document.getElementById('memory-heap-snapshot-btn');
269
+ if (!btn || btn.dataset.heapSnapshotWired === '1') return;
270
+ btn.dataset.heapSnapshotWired = '1';
271
+ btn.addEventListener('click', _memCaptureHeapSnapshot);
272
+ }
273
+
214
274
  function mountMemoryPanel() {
215
275
  if (_memoryPanelMounted) return;
216
276
  // No-op when the engine page hasn't been assembled yet (defensive — the
@@ -222,6 +282,7 @@ function mountMemoryPanel() {
222
282
  _memRefreshHistory();
223
283
  _memoryPanelLiveInterval = setInterval(_memRefreshLive, MEMORY_POLL_LIVE_MS);
224
284
  _memoryPanelHistoryInterval = setInterval(_memRefreshHistory, MEMORY_POLL_HISTORY_MS);
285
+ _memWireHeapSnapshotButton();
225
286
  }
226
287
 
227
288
  function unmountMemoryPanel() {
@@ -259,4 +320,5 @@ window.MinionsMemoryPanel = {
259
320
  mountMemoryPanel,
260
321
  unmountMemoryPanel,
261
322
  _buildSparkline: _memBuildSparkline,
323
+ _captureHeapSnapshot: _memCaptureHeapSnapshot,
262
324
  };
@@ -149,7 +149,9 @@ const RENDER_VERSIONS = {
149
149
  metrics: 1,
150
150
  workItems: 3,
151
151
  skills: 1,
152
+ commands: 1,
152
153
  mcpServers: 1,
154
+ harnessDiag: 1,
153
155
  schedules: 1,
154
156
  watches: 2,
155
157
  meetings: 1,
@@ -854,8 +856,24 @@ function _processStatusUpdate(data, opts) {
854
856
  });
855
857
  _changed('skills', data.skills);
856
858
  _safeRender('skills', function() { renderSkills(data.skills || []); });
859
+ _changed('commands', data.commands);
860
+ _safeRender('commands', function() { renderCommands(data.commands || []); });
857
861
  _changed('mcpServers', data.mcpServers);
858
862
  _safeRender('mcpServers', function() { renderMcpServers(data.mcpServers || []); });
863
+ // Harness propagation diagnostic comes from /api/harness/diagnostics
864
+ // (rare-change, fetched on each /api/status refresh so the tools page is
865
+ // always up to date when an operator drops a project-local skill).
866
+ _safeRender('harnessDiag', function() {
867
+ fetch('/api/harness/diagnostics')
868
+ .then(function (r) { return r.ok ? r.json() : Promise.reject(); })
869
+ .then(function (fresh) {
870
+ window._lastHarnessDiag = fresh;
871
+ _safeRender('harnessDiag', function() { renderHarnessDiagnostics(fresh); });
872
+ })
873
+ .catch(function () {
874
+ if (window._lastHarnessDiag) _safeRender('harnessDiag', function() { renderHarnessDiagnostics(window._lastHarnessDiag); });
875
+ });
876
+ });
859
877
  // Schedule definitions stay on /api/status (config-derived), but the
860
878
  // _lastRun/_lastResult overlay is re-applied client-side from
861
879
  // /state/engine/schedule-runs.json so a freshly-fired schedule lights up
@@ -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
@@ -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,
@@ -46,4 +46,11 @@
46
46
  <div id="memory-dashboard-sparkline" style="height:60px;width:100%;color:var(--fg)"></div>
47
47
  </div>
48
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>
49
56
  </section>
@@ -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>