@yemi33/minions 0.1.2179 → 0.1.2181
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 +15 -6
- package/dashboard/js/memory-panel.js +62 -0
- package/dashboard/js/refresh.js +59 -61
- package/dashboard/js/render-other.js +142 -1
- package/dashboard/js/render-work-items.js +18 -1
- package/dashboard/js/settings.js +23 -0
- package/dashboard/pages/engine-memory-panel.html +7 -0
- package/dashboard/pages/tools.html +8 -0
- package/dashboard-build.js +44 -8
- package/dashboard.js +466 -3
- 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/features.js +0 -18
- package/engine/preflight.js +265 -0
- package/engine/queries.js +204 -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 +293 -38
- package/engine/spawn-agent.js +178 -12
- package/engine.js +232 -3
- package/package.json +1 -1
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
|
*/
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
};
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -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
|
|
@@ -1149,21 +1167,16 @@ document.addEventListener('visibilitychange', function() {
|
|
|
1149
1167
|
// Ring buffer capturing the last 50 /api/status poll cycles so a user
|
|
1150
1168
|
// reporting "the dashboard didn't auto-update when X changed" can paste
|
|
1151
1169
|
// `window._refreshDiagnostics()` from devtools or click the "diag" footer
|
|
1152
|
-
// chip to surface a table.
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
//
|
|
1170
|
+
// chip to surface a table. The capture is cheap (in-memory ring buffer,
|
|
1171
|
+
// no network, no DOM writes) and always on (P-c1d4e8b6 deleted the
|
|
1172
|
+
// per-flag gate that had shipped default ON since v0.1.2034) so the next
|
|
1173
|
+
// staleness complaint can be diagnosed immediately from
|
|
1174
|
+
// window._refreshDiagnostics without a settings flip.
|
|
1157
1175
|
const _DIAG_RING_SIZE = 50;
|
|
1158
1176
|
const _refreshDiagBuf = [];
|
|
1159
1177
|
let _prevRefreshTs = 0;
|
|
1160
1178
|
let _diagHiddenSinceMs = 0; // ts when tab last went hidden; 0 when visible
|
|
1161
1179
|
let _lastVisibilityChangeAt = 0;
|
|
1162
|
-
function _isRefreshDiagOn() {
|
|
1163
|
-
try {
|
|
1164
|
-
return !!(window.MinionsFeatures && window.MinionsFeatures.isOn('dashboard-refresh-diagnostics'));
|
|
1165
|
-
} catch { return false; }
|
|
1166
|
-
}
|
|
1167
1180
|
function _pushDiagEntry(entry) {
|
|
1168
1181
|
_refreshDiagBuf.push(entry);
|
|
1169
1182
|
if (_refreshDiagBuf.length > _DIAG_RING_SIZE) _refreshDiagBuf.shift();
|
|
@@ -1173,7 +1186,6 @@ function _pushDiagEntry(entry) {
|
|
|
1173
1186
|
window._refreshDiagnostics = function() { return _refreshDiagBuf.slice(); };
|
|
1174
1187
|
window._refreshDiagnosticsClear = function() { _refreshDiagBuf.length = 0; };
|
|
1175
1188
|
document.addEventListener('visibilitychange', function() {
|
|
1176
|
-
if (!_isRefreshDiagOn()) return;
|
|
1177
1189
|
const now = Date.now();
|
|
1178
1190
|
const vs = document.visibilityState;
|
|
1179
1191
|
let hiddenForMs = null;
|
|
@@ -1202,11 +1214,10 @@ async function refresh(opts) {
|
|
|
1202
1214
|
// console-spam and adds load to whatever's wedged).
|
|
1203
1215
|
if (_nextPollAllowedAt && Date.now() < _nextPollAllowedAt) return;
|
|
1204
1216
|
_refreshInFlight = true;
|
|
1205
|
-
const
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const _diagEntry = _diagOn ? {
|
|
1217
|
+
const _t0 = Date.now();
|
|
1218
|
+
const _gap = _prevRefreshTs ? _t0 - _prevRefreshTs : null;
|
|
1219
|
+
_prevRefreshTs = _t0;
|
|
1220
|
+
const _diagEntry = {
|
|
1210
1221
|
ts: _t0,
|
|
1211
1222
|
kind: 'refresh',
|
|
1212
1223
|
gap_since_prev_ms: _gap,
|
|
@@ -1221,7 +1232,7 @@ async function refresh(opts) {
|
|
|
1221
1232
|
changed: null,
|
|
1222
1233
|
render_duration_ms: null,
|
|
1223
1234
|
error_message: null,
|
|
1224
|
-
}
|
|
1235
|
+
};
|
|
1225
1236
|
try {
|
|
1226
1237
|
const headers = {};
|
|
1227
1238
|
if (_lastStatusEtag) headers['If-None-Match'] = _lastStatusEtag;
|
|
@@ -1235,16 +1246,14 @@ async function refresh(opts) {
|
|
|
1235
1246
|
if (res.status === 304 && _lastStatusData) {
|
|
1236
1247
|
// Cache hit — reuse last payload, skip parsing entirely.
|
|
1237
1248
|
data = _lastStatusData;
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (etag304) _diagEntry.etag_received = etag304;
|
|
1247
|
-
}
|
|
1249
|
+
_diagEntry.response_status = '304';
|
|
1250
|
+
_diagEntry.bytes_received = 0;
|
|
1251
|
+
// D2: capture etag on 304 so the diag table can show whether the
|
|
1252
|
+
// server's ETag advanced even when we're reusing the cached body.
|
|
1253
|
+
// Without this the "etag↓" column is blank on every 304 row and the
|
|
1254
|
+
// operator can't tell server-side advancement from a pinned cache.
|
|
1255
|
+
const etag304 = res.headers && (res.headers.get ? res.headers.get('etag') : null);
|
|
1256
|
+
if (etag304) _diagEntry.etag_received = etag304;
|
|
1248
1257
|
} else {
|
|
1249
1258
|
data = await res.json();
|
|
1250
1259
|
const etag = res.headers && (res.headers.get ? res.headers.get('etag') : null);
|
|
@@ -1252,12 +1261,10 @@ async function refresh(opts) {
|
|
|
1252
1261
|
_lastStatusEtag = etag;
|
|
1253
1262
|
_lastStatusData = data;
|
|
1254
1263
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
_diagEntry.bytes_received = cl != null && cl !== '' ? Number(cl) : null;
|
|
1260
|
-
}
|
|
1264
|
+
_diagEntry.response_status = String(res.status);
|
|
1265
|
+
_diagEntry.etag_received = etag || null;
|
|
1266
|
+
const cl = res.headers && (res.headers.get ? res.headers.get('content-length') : null);
|
|
1267
|
+
_diagEntry.bytes_received = cl != null && cl !== '' ? Number(cl) : null;
|
|
1261
1268
|
}
|
|
1262
1269
|
// Auto-reload policy (2026-05-29):
|
|
1263
1270
|
// - dashboardStartedAt change → reload (covers `minions restart`)
|
|
@@ -1284,31 +1291,26 @@ async function refresh(opts) {
|
|
|
1284
1291
|
_consecutiveStatusFails = 0;
|
|
1285
1292
|
_nextPollAllowedAt = 0;
|
|
1286
1293
|
if (_unreachableSince) _markDashboardReachable();
|
|
1287
|
-
const _renderStart =
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
_diagChanges = {};
|
|
1291
|
-
_lastChangedFlags = _diagChanges;
|
|
1292
|
-
}
|
|
1294
|
+
const _renderStart = Date.now();
|
|
1295
|
+
const _diagChanges = {};
|
|
1296
|
+
_lastChangedFlags = _diagChanges;
|
|
1293
1297
|
try {
|
|
1294
1298
|
_processStatusUpdate(data, opts);
|
|
1295
1299
|
} finally {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
: null;
|
|
1307
|
-
}
|
|
1300
|
+
_lastChangedFlags = null;
|
|
1301
|
+
_diagEntry.render_duration_ms = Date.now() - _renderStart;
|
|
1302
|
+
_diagEntry.changed = _diagChanges;
|
|
1303
|
+
_diagEntry.workItems_changed = !!(_diagChanges && _diagChanges.workItems);
|
|
1304
|
+
// Post-slim, data.workItems is undefined; read from the dedicated
|
|
1305
|
+
// endpoint cache.
|
|
1306
|
+
_diagEntry.workItems_count = (window._lastWorkItems || []).length;
|
|
1307
|
+
_diagEntry.statusCacheVersion = (data.version && data.version.statusCacheVersion) != null
|
|
1308
|
+
? data.version.statusCacheVersion
|
|
1309
|
+
: null;
|
|
1308
1310
|
}
|
|
1309
1311
|
} catch(e) {
|
|
1310
1312
|
console.error('refresh error', e);
|
|
1311
|
-
if (
|
|
1313
|
+
if (!_diagEntry.response_status) {
|
|
1312
1314
|
_diagEntry.response_status = (e && e.name === 'AbortError') ? 'abort' : 'error';
|
|
1313
1315
|
_diagEntry.error_message = String((e && e.message) || e);
|
|
1314
1316
|
}
|
|
@@ -1328,7 +1330,7 @@ async function refresh(opts) {
|
|
|
1328
1330
|
}
|
|
1329
1331
|
finally {
|
|
1330
1332
|
_refreshInFlight = false;
|
|
1331
|
-
|
|
1333
|
+
_pushDiagEntry(_diagEntry);
|
|
1332
1334
|
}
|
|
1333
1335
|
}
|
|
1334
1336
|
|
|
@@ -1376,15 +1378,11 @@ switchPage(currentPage);
|
|
|
1376
1378
|
window.MinionsRefresh = { refresh };
|
|
1377
1379
|
|
|
1378
1380
|
// ── Refresh-diagnostic footer chip + modal (W-mphejzx100081972) ───────────
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
// touched (and no listeners installed beyond the visibilitychange handler
|
|
1384
|
-
// above, which itself short-circuits when the flag is off) when the flag is
|
|
1385
|
-
// disabled — production behaviour stays untouched.
|
|
1381
|
+
// Always installed (P-c1d4e8b6 deleted the per-flag gate that previously
|
|
1382
|
+
// guarded this). The chip floats bottom-left and opens a simple table view
|
|
1383
|
+
// of the ring buffer plus a "send to engine" button that POSTs the
|
|
1384
|
+
// snapshot to /api/diagnostics/refresh.
|
|
1386
1385
|
(function installRefreshDiagChip() {
|
|
1387
|
-
if (!_isRefreshDiagOn()) return;
|
|
1388
1386
|
try {
|
|
1389
1387
|
const chip = document.createElement('button');
|
|
1390
1388
|
chip.id = 'refresh-diag-chip';
|
|
@@ -523,7 +523,148 @@ async function _addSelectedProjects() {
|
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
-
|
|
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)));
|