@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.
- package/README.md +7 -5
- package/bin/minions.js +39 -17
- package/dashboard/js/command-parser.js +1 -1
- package/dashboard/js/memory-panel.js +324 -0
- package/dashboard/js/qa.js +2 -2
- package/dashboard/js/refresh.js +19 -1
- package/dashboard/js/render-other.js +143 -2
- package/dashboard/js/render-prs.js +2 -1
- package/dashboard/js/render-schedules.js +1 -1
- package/dashboard/js/render-watches.js +1 -1
- package/dashboard/js/render-work-items.js +18 -1
- package/dashboard/js/settings.js +23 -0
- package/dashboard/pages/engine-memory-panel.html +56 -0
- package/dashboard/pages/engine.html +1 -0
- package/dashboard/pages/tools.html +8 -0
- package/dashboard/slim/js/link-pr.js +5 -5
- package/dashboard/slim/js/modals-tiles.js +44 -3
- package/dashboard/slim/js/projects.js +8 -6
- package/dashboard/slim/styles.css +20 -0
- package/dashboard-build.js +17 -2
- package/dashboard.js +693 -19
- package/docs/branch-derivation.md +13 -1
- package/docs/diagnostics-memory.md +446 -0
- package/docs/harness-propagation.md +273 -0
- package/docs/human-vs-automated.md +1 -1
- package/docs/runtime-adapters.md +5 -0
- package/engine/cli.js +24 -5
- package/engine/diagnostics-memory.js +190 -0
- package/engine/lifecycle.js +111 -1
- package/engine/preflight.js +265 -0
- package/engine/queries.js +331 -19
- package/engine/runtimes/claude.js +36 -0
- package/engine/runtimes/codex.js +19 -0
- package/engine/runtimes/copilot.js +27 -36
- package/engine/shared.js +390 -15
- package/engine/spawn-agent.js +178 -12
- package/engine/watchdog.js +6 -0
- package/engine.js +277 -4
- package/package.json +2 -2
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
|
|
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` |
|
|
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`
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
*/
|
|
@@ -217,9 +217,9 @@ async function _waitForBrowserReconnect(minionsHome, { afterMs, timeoutMs = 5000
|
|
|
217
217
|
return false;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
function _openInBrowser(url) {
|
|
221
|
-
const result = openUrlInBrowser(url);
|
|
222
|
-
if (!result.ok) {
|
|
220
|
+
function _openInBrowser(url, reason, callerHint) {
|
|
221
|
+
const result = openUrlInBrowser(url, { reason, callerHint });
|
|
222
|
+
if (!result.ok && !result.suppressed) {
|
|
223
223
|
console.log(` Could not auto-open browser: ${result.error}`);
|
|
224
224
|
console.log(` Please open ${url} manually.`);
|
|
225
225
|
}
|
|
@@ -400,11 +400,15 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
|
|
|
400
400
|
}
|
|
401
401
|
console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.`);
|
|
402
402
|
|
|
403
|
-
const shouldOpen = forceOpen ||
|
|
404
|
-
|
|
403
|
+
const shouldOpen = forceOpen || (
|
|
404
|
+
process.env.MINIONS_NO_AUTO_OPEN !== '1' && (
|
|
405
|
+
!dashWasUp ||
|
|
406
|
+
!(await _waitForBrowserReconnect(MINIONS_HOME, { afterMs: restartStartMs, timeoutMs: 5000 }))
|
|
407
|
+
)
|
|
408
|
+
);
|
|
405
409
|
if (shouldOpen) {
|
|
406
410
|
console.log(` Opening dashboard in browser...`);
|
|
407
|
-
_openInBrowser(`http://localhost:${actualPort}
|
|
411
|
+
_openInBrowser(`http://localhost:${actualPort}`, 'cli-restart-no-beacon', 'bin/minions.js:407');
|
|
408
412
|
}
|
|
409
413
|
console.log('');
|
|
410
414
|
})().catch(err => {
|
|
@@ -861,11 +865,20 @@ function init() {
|
|
|
861
865
|
|
|
862
866
|
void (async () => {
|
|
863
867
|
const actualPort = await _waitForDashboardPortFile(MINIONS_HOME, 8000) || upgradeRequested.port;
|
|
864
|
-
|
|
865
|
-
|
|
868
|
+
// W-mqb9y83o — same hard kill-switch as spawnFullStackAndVerify:
|
|
869
|
+
// MINIONS_NO_AUTO_OPEN=1 in the parent env (set by the watchdog spawn
|
|
870
|
+
// and /api/dashboard/restart spawn) suppresses auto-open regardless of
|
|
871
|
+
// the beacon heuristic. `forceOpen` (`--open` / MINIONS_FORCE_OPEN=1)
|
|
872
|
+
// still wins as the explicit operator override.
|
|
873
|
+
const shouldOpen = forceOpen || (
|
|
874
|
+
process.env.MINIONS_NO_AUTO_OPEN !== '1' && (
|
|
875
|
+
!dashWasUp ||
|
|
876
|
+
!(await _waitForBrowserReconnect(MINIONS_HOME, { afterMs: restartStartMs, timeoutMs: 5000 }))
|
|
877
|
+
)
|
|
878
|
+
);
|
|
866
879
|
if (shouldOpen) {
|
|
867
880
|
console.log(` Opening dashboard in browser...`);
|
|
868
|
-
_openInBrowser(`http://localhost:${actualPort}
|
|
881
|
+
_openInBrowser(`http://localhost:${actualPort}`, 'cli-upgrade-no-beacon', 'bin/minions.js:868');
|
|
869
882
|
}
|
|
870
883
|
})().catch(err => {
|
|
871
884
|
console.log(` Could not open dashboard: ${err.message}`);
|
|
@@ -1087,7 +1100,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
1087
1100
|
minions init Bootstrap ~/.minions/ (first time)
|
|
1088
1101
|
minions update [--no-wait] Update to latest version (--no-wait backgrounds the restart)
|
|
1089
1102
|
minions version Show installed vs package version
|
|
1090
|
-
minions doctor
|
|
1103
|
+
minions doctor [--harness] Check prerequisites and runtime health (--harness: print harness propagation diagnostic)
|
|
1091
1104
|
minions add <project-dir> Link a project (interactive)
|
|
1092
1105
|
minions remove <project-dir> Unlink a project
|
|
1093
1106
|
minions list List linked projects
|
|
@@ -1110,7 +1123,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
1110
1123
|
minions config set-cli <R> [--model M]
|
|
1111
1124
|
Persist default runtime/model without starting
|
|
1112
1125
|
minions bridge <subcmd> Constellation bridge: status|health|enable|disable
|
|
1113
|
-
minions mcp-sync
|
|
1126
|
+
minions mcp-sync Print harness propagation diagnostic (read-only; same source as 'doctor --harness')
|
|
1114
1127
|
minions cleanup Clean temp files, worktrees, zombies
|
|
1115
1128
|
minions pr comment <repo> <n> Post a marker-prepended PR comment via gh
|
|
1116
1129
|
--agent <id> --kind <k> [--wi <id>] (--body-file <f> | --body <text>)
|
|
@@ -1209,7 +1222,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1209
1222
|
console.log(`\n Minions is already running (engine PID ${enginePid}; dashboard http://localhost:${startResolved.port}).`);
|
|
1210
1223
|
if (forceOpen) {
|
|
1211
1224
|
console.log(` Opening dashboard in browser...`);
|
|
1212
|
-
_openInBrowser(`http://localhost:${startResolved.port}
|
|
1225
|
+
_openInBrowser(`http://localhost:${startResolved.port}`, 'cli-start-force-open', 'bin/minions.js:1212');
|
|
1213
1226
|
} else {
|
|
1214
1227
|
console.log(` Run \`minions dash\` to open the dashboard, or \`minions start --open\` to force a new browser tab.\n`);
|
|
1215
1228
|
}
|
|
@@ -1437,8 +1450,17 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1437
1450
|
console.log(' npm install -g @yemi33/minions && minions init\n');
|
|
1438
1451
|
} else if (cmd === 'doctor') {
|
|
1439
1452
|
ensureInstalled();
|
|
1440
|
-
const { doctor } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
|
|
1441
|
-
|
|
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
|
+
}
|
|
1442
1464
|
} else if (cmd === 'watchdog') {
|
|
1443
1465
|
// External recovery scheduled by the OS. Survives cluster-kill scenarios
|
|
1444
1466
|
// the in-process supervisor can't (Windows job-object teardown when the
|
|
@@ -1547,7 +1569,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1547
1569
|
handled = true;
|
|
1548
1570
|
const url = `http://localhost:${dashResolved.port}`;
|
|
1549
1571
|
console.log(`\n Dashboard already running: ${url}\n`);
|
|
1550
|
-
openUrlInBrowser(url);
|
|
1572
|
+
openUrlInBrowser(url, { reason: 'cli-dash-warm', callerHint: 'bin/minions.js:1559' });
|
|
1551
1573
|
});
|
|
1552
1574
|
sock.on('error', () => {
|
|
1553
1575
|
sock.destroy();
|
|
@@ -8,7 +8,7 @@ function cmdUpdateAgentList(agents) {
|
|
|
8
8
|
cmdAgents = (agents || []).map(a => ({ id: a.id, name: a.name, emoji: a.emoji, role: a.role }));
|
|
9
9
|
}
|
|
10
10
|
function cmdUpdateProjectList(projects) {
|
|
11
|
-
cmdProjects = (projects || []).map(p => ({ name: p.name, description: p.description || '' }));
|
|
11
|
+
cmdProjects = (projects || []).map(p => ({ name: p.name, displayName: p.displayName || p.name, description: p.description || '' }));
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function showToast(id, msg, ok, durationMs) {
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// dashboard/js/memory-panel.js — Memory panel poller + inline SVG sparkline.
|
|
2
|
+
// P-d4e5f6a7 (memory + perf audit plan, dashboard surface).
|
|
3
|
+
//
|
|
4
|
+
// Lifecycle: mountMemoryPanel() registers two intervals — a 10 s poll against
|
|
5
|
+
// /api/diagnostics/memory for live values, and a 60 s poll against
|
|
6
|
+
// /api/diagnostics/memory/history?process={engine,dashboard} for the inline
|
|
7
|
+
// SVG sparkline (RSS + heapUsed over the last hour). unmountMemoryPanel()
|
|
8
|
+
// clears both intervals.
|
|
9
|
+
//
|
|
10
|
+
// Mount/unmount registration uses the canonical PAGE_LAZY_LOADERS +
|
|
11
|
+
// PAGE_LEAVE_HOOKS maps declared in state.js — engine page enter triggers
|
|
12
|
+
// mount, every page-leave call triggers unmount (idempotent). state.js
|
|
13
|
+
// evaluates earlier in the assembled bundle so the maps already exist when
|
|
14
|
+
// this file pushes its hooks; the initial switchPage(currentPage) call at
|
|
15
|
+
// the end of refresh.js (last file in the bundle) is what fires the first
|
|
16
|
+
// mount when a user direct-loads /engine.
|
|
17
|
+
//
|
|
18
|
+
// XSS safety: nothing here writes into innerHTML / adjacent-html sinks. Live
|
|
19
|
+
// values land via textContent; the sparkline is constructed via
|
|
20
|
+
// createElementNS so it stays clean against eslint-plugin-no-unsanitized.
|
|
21
|
+
|
|
22
|
+
const MEMORY_POLL_LIVE_MS = 10_000;
|
|
23
|
+
const MEMORY_POLL_HISTORY_MS = 60_000;
|
|
24
|
+
const MEMORY_SPARKLINE_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
|
25
|
+
|
|
26
|
+
let _memoryPanelLiveInterval = null;
|
|
27
|
+
let _memoryPanelHistoryInterval = null;
|
|
28
|
+
let _memoryPanelMounted = false;
|
|
29
|
+
|
|
30
|
+
function _memFmtBytes(n) {
|
|
31
|
+
if (!Number.isFinite(n)) return '—';
|
|
32
|
+
const mb = n / (1024 * 1024);
|
|
33
|
+
if (mb < 1024) return mb.toFixed(1) + ' MB';
|
|
34
|
+
return (mb / 1024).toFixed(2) + ' GB';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _memFmtMs(n) {
|
|
38
|
+
if (!Number.isFinite(n)) return '—';
|
|
39
|
+
if (n < 1) return n.toFixed(2) + ' ms';
|
|
40
|
+
if (n < 100) return n.toFixed(1) + ' ms';
|
|
41
|
+
return Math.round(n) + ' ms';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function _memFmtUptime(seconds) {
|
|
45
|
+
if (!Number.isFinite(seconds) || seconds < 0) return '—';
|
|
46
|
+
const s = Math.floor(seconds);
|
|
47
|
+
if (s < 60) return s + 's';
|
|
48
|
+
const m = Math.floor(s / 60);
|
|
49
|
+
if (m < 60) return m + 'm ' + (s % 60) + 's';
|
|
50
|
+
const h = Math.floor(m / 60);
|
|
51
|
+
if (h < 24) return h + 'h ' + (m % 60) + 'm';
|
|
52
|
+
const d = Math.floor(h / 24);
|
|
53
|
+
return d + 'd ' + (h % 24) + 'h';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _memSetText(id, val) {
|
|
57
|
+
const el = document.getElementById(id);
|
|
58
|
+
if (el) el.textContent = val;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _memRenderCard(prefix, sample, stale) {
|
|
62
|
+
if (!sample) {
|
|
63
|
+
const fallback = stale ? 'stale' : '—';
|
|
64
|
+
_memSetText('memory-' + prefix + '-rss', fallback);
|
|
65
|
+
_memSetText('memory-' + prefix + '-heap', '—');
|
|
66
|
+
_memSetText('memory-' + prefix + '-external', '—');
|
|
67
|
+
_memSetText('memory-' + prefix + '-lag', '—');
|
|
68
|
+
_memSetText('memory-' + prefix + '-gc', '—');
|
|
69
|
+
_memSetText('memory-' + prefix + '-uptime', '—');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
_memSetText('memory-' + prefix + '-rss', _memFmtBytes(sample.rss));
|
|
73
|
+
_memSetText('memory-' + prefix + '-heap', _memFmtBytes(sample.heapUsed) + ' / ' + _memFmtBytes(sample.heapTotal));
|
|
74
|
+
_memSetText('memory-' + prefix + '-external', _memFmtBytes(sample.external));
|
|
75
|
+
_memSetText('memory-' + prefix + '-lag', _memFmtMs(sample.eventLoopLagP50) + ' / ' + _memFmtMs(sample.eventLoopLagP99));
|
|
76
|
+
const gcVal = (Number.isFinite(sample.lastGcPauseMs) && sample.lastGcPauseMs > 0)
|
|
77
|
+
? _memFmtMs(sample.lastGcPauseMs) + (sample.lastGcKind ? ' (' + sample.lastGcKind + ')' : '')
|
|
78
|
+
: '—';
|
|
79
|
+
_memSetText('memory-' + prefix + '-gc', gcVal);
|
|
80
|
+
_memSetText('memory-' + prefix + '-uptime', _memFmtUptime(sample.uptime));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _memSetStaleBadge(id, stale) {
|
|
84
|
+
const el = document.getElementById(id);
|
|
85
|
+
if (!el) return;
|
|
86
|
+
el.style.display = stale ? '' : 'none';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pure builder: returns an <svg> element rendering two polylines (RSS +
|
|
90
|
+
// heapUsed) over the last MEMORY_SPARKLINE_WINDOW_MS of samples, or a
|
|
91
|
+
// "no samples" label when the buffer is empty. Pure DOM via createElementNS
|
|
92
|
+
// keeps eslint-plugin-no-unsanitized happy.
|
|
93
|
+
function _memBuildSparkline(samples, opts) {
|
|
94
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
95
|
+
const width = (opts && opts.width) || 320;
|
|
96
|
+
const height = (opts && opts.height) || 60;
|
|
97
|
+
const pad = 2;
|
|
98
|
+
|
|
99
|
+
const svg = document.createElementNS(svgNS, 'svg');
|
|
100
|
+
svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
|
|
101
|
+
svg.setAttribute('preserveAspectRatio', 'none');
|
|
102
|
+
svg.setAttribute('width', '100%');
|
|
103
|
+
svg.setAttribute('height', String(height));
|
|
104
|
+
svg.style.display = 'block';
|
|
105
|
+
|
|
106
|
+
if (!Array.isArray(samples) || samples.length === 0) {
|
|
107
|
+
const label = document.createElementNS(svgNS, 'text');
|
|
108
|
+
label.setAttribute('x', String(width / 2));
|
|
109
|
+
label.setAttribute('y', String(height / 2));
|
|
110
|
+
label.setAttribute('text-anchor', 'middle');
|
|
111
|
+
label.setAttribute('dominant-baseline', 'middle');
|
|
112
|
+
label.setAttribute('fill', 'currentColor');
|
|
113
|
+
label.setAttribute('opacity', '0.5');
|
|
114
|
+
label.setAttribute('font-size', '11');
|
|
115
|
+
label.textContent = 'no samples yet';
|
|
116
|
+
svg.appendChild(label);
|
|
117
|
+
return svg;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Clip to the last hour, falling back to the full ring when nothing recent.
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const tMin = now - MEMORY_SPARKLINE_WINDOW_MS;
|
|
123
|
+
const windowed = samples.filter(function (s) {
|
|
124
|
+
return s && Number.isFinite(s.capturedAt) && s.capturedAt >= tMin;
|
|
125
|
+
});
|
|
126
|
+
const useSamples = windowed.length ? windowed : samples.filter(function (s) {
|
|
127
|
+
return s && Number.isFinite(s.capturedAt);
|
|
128
|
+
});
|
|
129
|
+
if (!useSamples.length) return svg;
|
|
130
|
+
|
|
131
|
+
const t0 = useSamples[0].capturedAt;
|
|
132
|
+
const t1 = useSamples[useSamples.length - 1].capturedAt;
|
|
133
|
+
const tSpan = Math.max(1, t1 - t0);
|
|
134
|
+
|
|
135
|
+
let yMin = Infinity;
|
|
136
|
+
let yMax = -Infinity;
|
|
137
|
+
for (const s of useSamples) {
|
|
138
|
+
for (const v of [s.rss, s.heapUsed]) {
|
|
139
|
+
if (Number.isFinite(v)) {
|
|
140
|
+
if (v < yMin) yMin = v;
|
|
141
|
+
if (v > yMax) yMax = v;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!Number.isFinite(yMin) || !Number.isFinite(yMax)) { yMin = 0; yMax = 1; }
|
|
146
|
+
const ySpan = Math.max(1, yMax - yMin);
|
|
147
|
+
|
|
148
|
+
function _series(field) {
|
|
149
|
+
const pts = [];
|
|
150
|
+
for (const s of useSamples) {
|
|
151
|
+
const v = s[field];
|
|
152
|
+
if (!Number.isFinite(v)) continue;
|
|
153
|
+
const x = pad + ((s.capturedAt - t0) / tSpan) * (width - 2 * pad);
|
|
154
|
+
const y = height - pad - ((v - yMin) / ySpan) * (height - 2 * pad);
|
|
155
|
+
pts.push(x.toFixed(1) + ',' + y.toFixed(1));
|
|
156
|
+
}
|
|
157
|
+
return pts.join(' ');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _addLine(field, color) {
|
|
161
|
+
const pts = _series(field);
|
|
162
|
+
if (!pts) return;
|
|
163
|
+
const line = document.createElementNS(svgNS, 'polyline');
|
|
164
|
+
line.setAttribute('points', pts);
|
|
165
|
+
line.setAttribute('fill', 'none');
|
|
166
|
+
line.setAttribute('stroke', color);
|
|
167
|
+
line.setAttribute('stroke-width', '1.5');
|
|
168
|
+
line.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
169
|
+
svg.appendChild(line);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_addLine('rss', 'var(--blue, #4ea1ff)');
|
|
173
|
+
_addLine('heapUsed', 'var(--green, #4caf50)');
|
|
174
|
+
return svg;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _memRenderSparkline(containerId, samples) {
|
|
178
|
+
const el = document.getElementById(containerId);
|
|
179
|
+
if (!el) return;
|
|
180
|
+
const width = el.clientWidth || 320;
|
|
181
|
+
const svg = _memBuildSparkline(samples, { width, height: 60 });
|
|
182
|
+
el.replaceChildren(svg);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function _memRefreshLive() {
|
|
186
|
+
let data;
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetch('/api/diagnostics/memory');
|
|
189
|
+
if (!res.ok) return;
|
|
190
|
+
data = await res.json();
|
|
191
|
+
} catch {
|
|
192
|
+
return; // network blip — leave previous values in place
|
|
193
|
+
}
|
|
194
|
+
if (!data || typeof data !== 'object') return;
|
|
195
|
+
_memRenderCard('engine', data.engine, !!data.engineStale);
|
|
196
|
+
_memRenderCard('dashboard', data.dashboard, false);
|
|
197
|
+
_memSetStaleBadge('memory-engine-stale-badge', !!data.engineStale);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function _memRefreshHistory() {
|
|
201
|
+
await Promise.all(['engine', 'dashboard'].map(async function (proc) {
|
|
202
|
+
try {
|
|
203
|
+
const res = await fetch('/api/diagnostics/memory/history?process=' + encodeURIComponent(proc));
|
|
204
|
+
if (!res.ok) return;
|
|
205
|
+
const data = await res.json();
|
|
206
|
+
const samples = (data && Array.isArray(data.samples)) ? data.samples : [];
|
|
207
|
+
_memRenderSparkline('memory-' + proc + '-sparkline', samples);
|
|
208
|
+
} catch {
|
|
209
|
+
// network blip — keep the previous sparkline
|
|
210
|
+
}
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
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
|
+
|
|
274
|
+
function mountMemoryPanel() {
|
|
275
|
+
if (_memoryPanelMounted) return;
|
|
276
|
+
// No-op when the engine page hasn't been assembled yet (defensive — the
|
|
277
|
+
// fragment is always part of the bundle, but this keeps the hook safe
|
|
278
|
+
// against partial DOM states during test harnesses).
|
|
279
|
+
if (!document.getElementById('memory-panel-content')) return;
|
|
280
|
+
_memoryPanelMounted = true;
|
|
281
|
+
_memRefreshLive();
|
|
282
|
+
_memRefreshHistory();
|
|
283
|
+
_memoryPanelLiveInterval = setInterval(_memRefreshLive, MEMORY_POLL_LIVE_MS);
|
|
284
|
+
_memoryPanelHistoryInterval = setInterval(_memRefreshHistory, MEMORY_POLL_HISTORY_MS);
|
|
285
|
+
_memWireHeapSnapshotButton();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function unmountMemoryPanel() {
|
|
289
|
+
if (!_memoryPanelMounted) return;
|
|
290
|
+
_memoryPanelMounted = false;
|
|
291
|
+
if (_memoryPanelLiveInterval) {
|
|
292
|
+
clearInterval(_memoryPanelLiveInterval);
|
|
293
|
+
_memoryPanelLiveInterval = null;
|
|
294
|
+
}
|
|
295
|
+
if (_memoryPanelHistoryInterval) {
|
|
296
|
+
clearInterval(_memoryPanelHistoryInterval);
|
|
297
|
+
_memoryPanelHistoryInterval = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Register the lifecycle hooks against the canonical maps in state.js.
|
|
302
|
+
// `const` declarations don't block mutating the underlying object/array,
|
|
303
|
+
// so we can splice in our entries without editing state.js. Best-effort:
|
|
304
|
+
// any failure leaves the panel unmounted but does not break navigation.
|
|
305
|
+
try {
|
|
306
|
+
if (typeof PAGE_LAZY_LOADERS === 'object' && PAGE_LAZY_LOADERS) {
|
|
307
|
+
if (!Array.isArray(PAGE_LAZY_LOADERS.engine)) PAGE_LAZY_LOADERS.engine = [];
|
|
308
|
+
if (PAGE_LAZY_LOADERS.engine.indexOf('mountMemoryPanel') < 0) {
|
|
309
|
+
PAGE_LAZY_LOADERS.engine.push('mountMemoryPanel');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (typeof PAGE_LEAVE_HOOKS !== 'undefined' && Array.isArray(PAGE_LEAVE_HOOKS)) {
|
|
313
|
+
if (PAGE_LEAVE_HOOKS.indexOf('unmountMemoryPanel') < 0) {
|
|
314
|
+
PAGE_LEAVE_HOOKS.push('unmountMemoryPanel');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch { /* registration is best-effort */ }
|
|
318
|
+
|
|
319
|
+
window.MinionsMemoryPanel = {
|
|
320
|
+
mountMemoryPanel,
|
|
321
|
+
unmountMemoryPanel,
|
|
322
|
+
_buildSparkline: _memBuildSparkline,
|
|
323
|
+
_captureHeapSnapshot: _memCaptureHeapSnapshot,
|
|
324
|
+
};
|
package/dashboard/js/qa.js
CHANGED
|
@@ -620,7 +620,7 @@ async function loadQaProjectsSelect() {
|
|
|
620
620
|
const res = await fetch('/api/status');
|
|
621
621
|
const json = res.ok ? await res.json() : {};
|
|
622
622
|
if (Array.isArray(json && json.projects)) {
|
|
623
|
-
projects = json.projects.map(p => ({ name: p.name, description: p.description || '' }));
|
|
623
|
+
projects = json.projects.map(p => ({ name: p.name, displayName: p.displayName || p.name, description: p.description || '' }));
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
} catch { projects = []; }
|
|
@@ -631,7 +631,7 @@ async function loadQaProjectsSelect() {
|
|
|
631
631
|
if (!p || !p.name) continue;
|
|
632
632
|
const opt = document.createElement('option');
|
|
633
633
|
opt.value = p.name;
|
|
634
|
-
opt.textContent = p.name;
|
|
634
|
+
opt.textContent = p.displayName || p.name;
|
|
635
635
|
if (previouslySelected.has(p.name)) opt.selected = true;
|
|
636
636
|
sel.appendChild(opt);
|
|
637
637
|
}
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -134,7 +134,7 @@ const RENDER_VERSIONS = {
|
|
|
134
134
|
prdProgress: 1,
|
|
135
135
|
prdPrs: 1,
|
|
136
136
|
inbox: 2,
|
|
137
|
-
projects:
|
|
137
|
+
projects: 2,
|
|
138
138
|
notes: 1,
|
|
139
139
|
prd: 1,
|
|
140
140
|
prs: 3,
|
|
@@ -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
|