@yemi33/minions 0.1.2380 → 0.1.2382
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/bin/minions.js +41 -2
- package/dashboard/js/refresh.js +9 -3
- package/dashboard/js/render-other.js +81 -23
- package/dashboard/js/render-work-items.js +61 -20
- package/dashboard/js/settings.js +47 -25
- package/dashboard/pages/engine.html +6 -0
- package/dashboard.js +126 -62
- package/docs/README.md +1 -0
- package/docs/claude-md-propagation.md +118 -0
- package/docs/engine-restart.md +10 -0
- package/docs/live-checkout-mode.md +45 -26
- package/docs/worktree-lifecycle.md +9 -0
- package/engine/claude-md-context.js +195 -0
- package/engine/consolidation.js +47 -3
- package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
- package/engine/dispatch.js +78 -24
- package/engine/lifecycle.js +129 -84
- package/engine/live-checkout.js +193 -149
- package/engine/pipeline.js +147 -20
- package/engine/playbook.js +47 -15
- package/engine/prd-store.js +14 -6
- package/engine/quarantine-refs.js +33 -0
- package/engine/queries.js +8 -6
- package/engine/restart-health.js +69 -4
- package/engine/routing.js +1 -1
- package/engine/runtimes/claude.js +6 -0
- package/engine/runtimes/codex.js +18 -0
- package/engine/runtimes/copilot.js +7 -0
- package/engine/shared.js +199 -106
- package/engine/supervisor.js +72 -16
- package/engine/timeout.js +87 -16
- package/engine/untrusted-fence.js +2 -1
- package/engine.js +301 -31
- package/package.json +1 -1
- package/prompts/cc-system.md +7 -7
package/bin/minions.js
CHANGED
|
@@ -336,7 +336,11 @@ function _sqliteSpawnFlags() {
|
|
|
336
336
|
* CLI's intent (EADDRINUSE retries still apply inside the dashboard if a
|
|
337
337
|
* race opens between the CLI's port-free check and the actual bind). */
|
|
338
338
|
function spawnDashboard(requestedPort) {
|
|
339
|
-
const env = {
|
|
339
|
+
const env = {
|
|
340
|
+
...process.env,
|
|
341
|
+
MINIONS_NO_AUTO_OPEN: '1',
|
|
342
|
+
MINIONS_REQUIRE_DASHBOARD_PORT: '1',
|
|
343
|
+
};
|
|
340
344
|
const out = _openStdioLog('dashboard-stdio.log');
|
|
341
345
|
const err = _openStdioLog('dashboard-stdio.log');
|
|
342
346
|
const args = [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'dashboard.js')];
|
|
@@ -444,19 +448,43 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
|
|
|
444
448
|
// failure rather than a false "healthy".
|
|
445
449
|
const actualPort = await _waitForDashboardPortFile(MINIONS_HOME, 12000, 150, dashProc.pid) || requested.port;
|
|
446
450
|
if (actualPort !== requested.port) {
|
|
447
|
-
console.
|
|
451
|
+
console.error(`\n ERROR: Dashboard bound to unexpected port ${actualPort}; required ${requested.port}.`);
|
|
452
|
+
console.error(' Refusing to verify a split-port Minions topology. Run `minions restart` again.');
|
|
453
|
+
_cleanupFailedStackSpawn({
|
|
454
|
+
enginePid: engineProc.pid,
|
|
455
|
+
dashboardPid: dashProc.pid,
|
|
456
|
+
supervisorPid: supProc.pid,
|
|
457
|
+
dashboardPort: actualPort,
|
|
458
|
+
});
|
|
459
|
+
process.exit(1);
|
|
448
460
|
}
|
|
461
|
+
const supervisorInternals = require(path.join(PKG_ROOT, 'engine', 'supervisor'))._internals;
|
|
449
462
|
const result = await waitForRestartHealth({
|
|
450
463
|
minionsHome: MINIONS_HOME,
|
|
464
|
+
expectedEnginePid: engineProc.pid,
|
|
451
465
|
dashboardPid: dashProc.pid,
|
|
452
466
|
dashboardPort: actualPort,
|
|
467
|
+
supervisorPid: supProc.pid,
|
|
453
468
|
timeoutMs: _resolveRestartHealthTimeoutMs(),
|
|
454
469
|
// Verify the process bound to the port IS the dashboard we just spawned
|
|
455
470
|
// (beacon pid match), not a stale leftover still holding it.
|
|
456
471
|
requireBeaconOwner: true,
|
|
472
|
+
requireExactTopology: true,
|
|
473
|
+
listProcessPids: supervisorInternals.listNodePidsMatching,
|
|
474
|
+
topologyScripts: {
|
|
475
|
+
engine: path.join(MINIONS_HOME, 'engine.js'),
|
|
476
|
+
dashboard: path.join(MINIONS_HOME, 'dashboard.js'),
|
|
477
|
+
supervisor: path.join(MINIONS_HOME, 'engine', 'supervisor.js'),
|
|
478
|
+
},
|
|
457
479
|
});
|
|
458
480
|
if (!result.ok) {
|
|
459
481
|
console.error(formatRestartHealthError(result));
|
|
482
|
+
_cleanupFailedStackSpawn({
|
|
483
|
+
enginePid: engineProc.pid,
|
|
484
|
+
dashboardPid: dashProc.pid,
|
|
485
|
+
supervisorPid: supProc.pid,
|
|
486
|
+
dashboardPort: actualPort,
|
|
487
|
+
});
|
|
460
488
|
process.exit(1);
|
|
461
489
|
}
|
|
462
490
|
console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.`);
|
|
@@ -479,6 +507,17 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
|
|
|
479
507
|
});
|
|
480
508
|
}
|
|
481
509
|
|
|
510
|
+
function _cleanupFailedStackSpawn({ enginePid, dashboardPid, supervisorPid, dashboardPort }) {
|
|
511
|
+
writeStopIntent('restart verification failed');
|
|
512
|
+
for (const pid of [supervisorPid, dashboardPid, enginePid]) killPidOnly(pid);
|
|
513
|
+
for (const pid of [supervisorPid, dashboardPid, enginePid]) waitForPidDeath(pid, 5000);
|
|
514
|
+
if (dashboardPort) {
|
|
515
|
+
killByPort(dashboardPort);
|
|
516
|
+
waitForPortRelease(dashboardPort, 5000);
|
|
517
|
+
}
|
|
518
|
+
shared.clearDashboardPortFile(MINIONS_HOME);
|
|
519
|
+
}
|
|
520
|
+
|
|
482
521
|
/** Poll engine/dashboard-port.json until it appears (dashboard wrote it on
|
|
483
522
|
* listen) or the timeout elapses. Returns the bound port or null on
|
|
484
523
|
* timeout. Stale files left behind by a crashed dashboard are tolerated —
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -138,7 +138,7 @@ const RENDER_VERSIONS = {
|
|
|
138
138
|
// Bumped 4→5 for the clickable checkout-mode pill + picker (W-mr1b67zi0006b788).
|
|
139
139
|
// Bumped 5→6 for multi-select hybrid liveValidation.type support (W-mr2m1ute000a9c01).
|
|
140
140
|
// Bumped 6→7 for the liveValidation.autoDispatch toggle + pill "· auto-validate" suffix (W-mr2q361a00097e5c).
|
|
141
|
-
projects:
|
|
141
|
+
projects: 9,
|
|
142
142
|
notes: 1,
|
|
143
143
|
prd: 3,
|
|
144
144
|
prs: 3,
|
|
@@ -151,8 +151,11 @@ const RENDER_VERSIONS = {
|
|
|
151
151
|
dispatch: 2,
|
|
152
152
|
engineLog: 2,
|
|
153
153
|
metrics: 1,
|
|
154
|
-
// Bumped 8
|
|
155
|
-
|
|
154
|
+
// Bumped 7→8: WI detail modal's branch pill moved out of the Artifacts
|
|
155
|
+
// chip row into its own non-interactive "Branch" field (W-mr2qjr3b0002522f).
|
|
156
|
+
// Bumped 8→9 for paused pre-dispatch evaluation guidance.
|
|
157
|
+
// Bumped 9→10: row actions now preserve work-item source scope when IDs collide.
|
|
158
|
+
workItems: 10,
|
|
156
159
|
skills: 1,
|
|
157
160
|
commands: 1,
|
|
158
161
|
mcpServers: 1,
|
|
@@ -840,6 +843,9 @@ function _processStatusUpdate(data, opts) {
|
|
|
840
843
|
.catch(function () { /* keep render even if managed fetch failed — getLastItems() returns the last good cache (or []) */ })
|
|
841
844
|
.then(function () { try { renderKeepProcesses(); } catch {} });
|
|
842
845
|
}
|
|
846
|
+
if (typeof renderWorktreeQuarantineRefs === 'function') {
|
|
847
|
+
_safeRender('worktreeQuarantineRefs', function () { renderWorktreeQuarantineRefs(); });
|
|
848
|
+
}
|
|
843
849
|
// Work items now come from /api/work-items — a dedicated fresh-JSON
|
|
844
850
|
// endpoint that re-runs getWorkItems() server-side on every request
|
|
845
851
|
// with input-mtime ETag (issue #2949). The previous /state/ + stale-
|
|
@@ -105,13 +105,13 @@ function _renderProjectBranch(p) {
|
|
|
105
105
|
// "Live checkout" (live — agents run in-place inside the operator's working
|
|
106
106
|
// tree). Driven by p.checkoutMode (shared.CHECKOUT_MODES; engine defaults
|
|
107
107
|
// unset → 'worktree'). Live is the riskier/special mode (capped to one mutating
|
|
108
|
-
// dispatch per project,
|
|
109
|
-
// color than the muted worktree pill.
|
|
108
|
+
// dispatch per project, with configurable dirty-tree recovery) so it gets a
|
|
109
|
+
// more prominent color than the muted worktree pill.
|
|
110
110
|
//
|
|
111
111
|
// Hybrid live-validation: when checkoutMode is 'live' AND p.liveValidationType
|
|
112
|
-
// is set,
|
|
113
|
-
//
|
|
114
|
-
//
|
|
112
|
+
// is set, only selected WI types run in-place and all others use isolated
|
|
113
|
+
// worktrees. This is a materially different dispatch profile from full-live,
|
|
114
|
+
// so it gets its own pill.
|
|
115
115
|
// W-mr1b67zi0006b788: the pill is clickable — opens an inline picker (see
|
|
116
116
|
// _openCheckoutModeMenu below) to switch between Worktrees / Live checkout /
|
|
117
117
|
// Hybrid dispatch modes without leaving the Projects view. Shared
|
|
@@ -125,6 +125,10 @@ function _renderWorktreeModePill(p) {
|
|
|
125
125
|
const common = ' data-checkout-pill="' + name + '" role="button" tabindex="0" aria-haspopup="true" aria-label="Change checkout mode"';
|
|
126
126
|
if (p.checkoutMode === 'live') {
|
|
127
127
|
const types = _liveValidationTypesArray(p);
|
|
128
|
+
if (p.liveValidationInvalid) {
|
|
129
|
+
const effectiveTypes = types.length > 0 ? types.join(', ') : 'none';
|
|
130
|
+
return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable project-warn"' + common + ' title="Invalid hybrid configuration — unsupported settings are ignored. Effective live work-item types: ' + escHtml(effectiveTypes) + '. Click to repair.">⚠ Hybrid · invalid</span>';
|
|
131
|
+
}
|
|
128
132
|
if (types.length > 0) {
|
|
129
133
|
const typesText = types.join(', ');
|
|
130
134
|
const vt = escapeHtml(typesText);
|
|
@@ -134,20 +138,19 @@ function _renderWorktreeModePill(p) {
|
|
|
134
138
|
const autoDispatch = p.liveValidationAutoDispatch === true;
|
|
135
139
|
const autoSuffix = autoDispatch ? ' · auto-validate' : '';
|
|
136
140
|
const autoTitleBit = autoDispatch
|
|
137
|
-
? ' A
|
|
138
|
-
: ' Auto-validation is OFF — validation must be dispatched manually
|
|
139
|
-
return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid
|
|
141
|
+
? ' A test work item is automatically dispatched after each eligible coding work item completes.'
|
|
142
|
+
: ' Auto-validation is OFF — validation must be dispatched manually.';
|
|
143
|
+
return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid dispatch — only the "' + vt + '" work-item type(s) run in-place; all others use isolated worktrees. Live work is capped to one mutating dispatch, and dirty-tree handling follows the configured auto-stash/reset policy.' + escHtml(autoTitleBit) + ' Click to change.">⚡ Hybrid · ' + vt + escHtml(autoSuffix) + '</span>';
|
|
140
144
|
}
|
|
141
|
-
return ' <span class="project-mode-pill project-mode-live project-mode-pill-clickable"' + common + ' title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch
|
|
145
|
+
return ' <span class="project-mode-pill project-mode-live project-mode-pill-clickable"' + common + ' title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch. Dirty-tree handling follows the configured auto-stash/reset policy. Click to change.">⚡ Live checkout</span>';
|
|
142
146
|
}
|
|
143
147
|
return ' <span class="project-mode-pill project-mode-isolated project-mode-pill-clickable"' + common + ' title="Worktree dispatch mode (default) — each agent runs in its own git worktree. Click to change.">Worktrees</span>';
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
const CHECKOUT_MODE_HYBRID_TYPES = ['implement', 'fix', 'docs', 'decompose', 'explore', 'ask', 'review', 'test', 'verify', 'setup'];
|
|
150
|
+
// Canonical work-item types that can meaningfully use the live checkout.
|
|
151
|
+
// Read-only root tasks are omitted because hybrid routing cannot change where
|
|
152
|
+
// they run. `build-and-test` is a playbook alias; `test` is the WI type.
|
|
153
|
+
const CHECKOUT_MODE_HYBRID_TYPES = ['test', 'implement', 'implement:large', 'fix', 'build-fix-complex', 'docs', 'decompose', 'review', 'verify', 'setup'];
|
|
151
154
|
const CHECKOUT_MODE_HYBRID_DEFAULT_TYPE = 'test';
|
|
152
155
|
|
|
153
156
|
function _findProjectInLastStatus(name) {
|
|
@@ -230,9 +233,10 @@ function _openCheckoutModeMenu(projectName, anchorEl) {
|
|
|
230
233
|
desc.textContent = descText;
|
|
231
234
|
item.appendChild(desc);
|
|
232
235
|
}
|
|
236
|
+
const liveTypes = _liveValidationTypesArray(project);
|
|
233
237
|
const isActive = (mode === 'worktree' && project.checkoutMode !== 'live')
|
|
234
|
-
|| (mode === 'live' && project.checkoutMode === 'live' &&
|
|
235
|
-
|| (mode === 'hybrid' && project.checkoutMode === 'live' &&
|
|
238
|
+
|| (mode === 'live' && project.checkoutMode === 'live' && liveTypes.length === 0 && !project.liveValidationInvalid)
|
|
239
|
+
|| (mode === 'hybrid' && project.checkoutMode === 'live' && (liveTypes.length > 0 || project.liveValidationInvalid));
|
|
236
240
|
if (isActive) {
|
|
237
241
|
item.setAttribute('aria-checked', 'true');
|
|
238
242
|
item.classList.add('checkout-mode-menu-item-active');
|
|
@@ -254,8 +258,8 @@ function _openCheckoutModeMenu(projectName, anchorEl) {
|
|
|
254
258
|
}
|
|
255
259
|
|
|
256
260
|
addItem('Worktrees (default)', 'worktree', 'Each dispatch runs in its own isolated git worktree. Fully parallel, never touches your working directory.');
|
|
257
|
-
addItem('Live checkout', 'live', 'Agents run in-place in the project folder. One mutating dispatch at a time;
|
|
258
|
-
addItem('Hybrid dispatch…', 'hybrid', '
|
|
261
|
+
addItem('Live checkout', 'live', 'Agents run in-place in the project folder. One mutating dispatch at a time; dirty-tree handling follows the auto-stash/reset policy.');
|
|
262
|
+
addItem('Hybrid dispatch…', 'hybrid', 'Only chosen work-item types (normally test) use the real checkout; all others use isolated worktrees.');
|
|
259
263
|
|
|
260
264
|
document.body.appendChild(menu);
|
|
261
265
|
const rect = anchorEl.getBoundingClientRect();
|
|
@@ -286,7 +290,7 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
|
286
290
|
|
|
287
291
|
const label = document.createElement('div');
|
|
288
292
|
label.className = 'checkout-mode-menu-item';
|
|
289
|
-
label.textContent = 'Work-item type(s) to
|
|
293
|
+
label.textContent = 'Work-item type(s) to run live:';
|
|
290
294
|
menu.appendChild(label);
|
|
291
295
|
|
|
292
296
|
const currentTypes = _liveValidationTypesArray(project);
|
|
@@ -325,10 +329,23 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
|
325
329
|
autoDispatchCb.className = 'checkout-mode-menu-checkbox checkout-mode-menu-autodispatch-checkbox';
|
|
326
330
|
autoDispatchCb.checked = project.liveValidationAutoDispatch === true;
|
|
327
331
|
const autoDispatchText = document.createElement('span');
|
|
328
|
-
autoDispatchText.textContent = 'Automatically
|
|
332
|
+
autoDispatchText.textContent = 'Automatically run a test validation after each eligible coding work item completes';
|
|
329
333
|
autoDispatchRow.appendChild(autoDispatchCb);
|
|
330
334
|
autoDispatchRow.appendChild(autoDispatchText);
|
|
331
335
|
menu.appendChild(autoDispatchRow);
|
|
336
|
+
const testTypeCheckbox = checkboxes.find(function(cb) {
|
|
337
|
+
return cb.value === CHECKOUT_MODE_HYBRID_DEFAULT_TYPE;
|
|
338
|
+
});
|
|
339
|
+
function syncAutoDispatchType() {
|
|
340
|
+
if (autoDispatchCb.checked && testTypeCheckbox) testTypeCheckbox.checked = true;
|
|
341
|
+
}
|
|
342
|
+
autoDispatchCb.addEventListener('change', syncAutoDispatchType);
|
|
343
|
+
if (testTypeCheckbox) {
|
|
344
|
+
testTypeCheckbox.addEventListener('change', function() {
|
|
345
|
+
if (!testTypeCheckbox.checked) autoDispatchCb.checked = false;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
syncAutoDispatchType();
|
|
332
349
|
|
|
333
350
|
const actions = document.createElement('div');
|
|
334
351
|
actions.className = 'checkout-mode-menu-actions';
|
|
@@ -349,9 +366,9 @@ function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
|
349
366
|
applyBtn.className = 'checkout-mode-menu-btn checkout-mode-menu-btn-primary';
|
|
350
367
|
applyBtn.textContent = 'Apply';
|
|
351
368
|
applyBtn.addEventListener('click', function() {
|
|
369
|
+
const autoDispatch = autoDispatchCb.checked;
|
|
352
370
|
const types = checkboxes.filter(function(cb) { return cb.checked; }).map(function(cb) { return cb.value; });
|
|
353
371
|
if (types.length === 0) return; // require at least one selected type
|
|
354
|
-
const autoDispatch = autoDispatchCb.checked;
|
|
355
372
|
_closeCheckoutModeMenu();
|
|
356
373
|
_applyCheckoutModeChange(projectName, 'live', types, autoDispatch);
|
|
357
374
|
});
|
|
@@ -392,8 +409,8 @@ async function _applyCheckoutModeChange(projectName, newMode, liveValidationType
|
|
|
392
409
|
// explicitly in the confirm message rather than burying it in the label.
|
|
393
410
|
const autoDispatchNote = typesForLabel.length > 0
|
|
394
411
|
? (autoDispatchOn
|
|
395
|
-
? ' Auto-validation will be turned ON — a
|
|
396
|
-
: ' Auto-validation will be OFF — you will need to dispatch validation manually
|
|
412
|
+
? ' Auto-validation will be turned ON — a test work item will be dispatched automatically after each eligible coding work item completes, and eligible coding agents will skip local builds/tests.'
|
|
413
|
+
: ' Auto-validation will be OFF — you will need to dispatch validation manually.')
|
|
397
414
|
: '';
|
|
398
415
|
const ok = await confirmDialog({
|
|
399
416
|
title: 'Change checkout mode?',
|
|
@@ -407,12 +424,14 @@ async function _applyCheckoutModeChange(projectName, newMode, liveValidationType
|
|
|
407
424
|
const prevMode = project.checkoutMode;
|
|
408
425
|
const prevType = project.liveValidationType;
|
|
409
426
|
const prevAutoDispatch = project.liveValidationAutoDispatch;
|
|
427
|
+
const prevInvalid = project.liveValidationInvalid;
|
|
410
428
|
const nextType = (newMode === 'live' && liveValidationType) ? liveValidationType : null;
|
|
411
429
|
// Optimistic flip BEFORE awaiting the API call (per dashboard convention —
|
|
412
430
|
// see projectChipRemove / removePinnedNote); reverted in the catch below.
|
|
413
431
|
project.checkoutMode = newMode;
|
|
414
432
|
project.liveValidationType = nextType;
|
|
415
433
|
project.liveValidationAutoDispatch = nextType ? autoDispatchOn : false;
|
|
434
|
+
project.liveValidationInvalid = false;
|
|
416
435
|
if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
|
|
417
436
|
|
|
418
437
|
try {
|
|
@@ -434,6 +453,7 @@ async function _applyCheckoutModeChange(projectName, newMode, liveValidationType
|
|
|
434
453
|
project.checkoutMode = prevMode;
|
|
435
454
|
project.liveValidationType = prevType;
|
|
436
455
|
project.liveValidationAutoDispatch = prevAutoDispatch;
|
|
456
|
+
project.liveValidationInvalid = prevInvalid;
|
|
437
457
|
if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
|
|
438
458
|
showToast('cmd-toast', 'Failed to update checkout mode: ' + (err && err.message || err), false);
|
|
439
459
|
}
|
|
@@ -1230,3 +1250,41 @@ async function killKeepPid(agentId, pid) {
|
|
|
1230
1250
|
}
|
|
1231
1251
|
|
|
1232
1252
|
window.MinionsKeepProcesses = { renderKeepProcesses, killKeepPid, mountKeepProcessesPanel, unmountKeepProcessesPanel };
|
|
1253
|
+
|
|
1254
|
+
async function renderWorktreeQuarantineRefs() {
|
|
1255
|
+
const root = document.getElementById('worktree-quarantine-refs-content');
|
|
1256
|
+
const count = document.getElementById('worktree-quarantine-refs-count');
|
|
1257
|
+
if (!root) return;
|
|
1258
|
+
let html;
|
|
1259
|
+
try {
|
|
1260
|
+
const res = await fetch('/api/worktree-quarantine-refs');
|
|
1261
|
+
const data = await res.json();
|
|
1262
|
+
if (!res.ok) throw new Error(data.error || String(res.status));
|
|
1263
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
1264
|
+
if (count) count.textContent = String(items.length);
|
|
1265
|
+
if (!items.length) {
|
|
1266
|
+
html = '<p class="empty">No quarantined work refs found.</p>';
|
|
1267
|
+
} else {
|
|
1268
|
+
html = '<table style="width:100%;border-collapse:collapse"><thead><tr>' +
|
|
1269
|
+
'<th style="text-align:left">Project</th><th style="text-align:left">Backup ref</th>' +
|
|
1270
|
+
'<th style="text-align:left">SHA</th><th style="text-align:left">Created</th></tr></thead><tbody>' +
|
|
1271
|
+
items.map(function (item) {
|
|
1272
|
+
if (item.error) {
|
|
1273
|
+
return '<tr><td>' + escHtml(item.project || '') + '</td><td colspan="3" style="color:var(--red)">' + escHtml(item.error) + '</td></tr>';
|
|
1274
|
+
}
|
|
1275
|
+
return '<tr><td>' + escHtml(item.project || '') + '</td>' +
|
|
1276
|
+
'<td><code>' + escHtml(item.ref || '') + '</code></td>' +
|
|
1277
|
+
'<td><code>' + escHtml(String(item.sha || '').slice(0, 12)) + '</code></td>' +
|
|
1278
|
+
'<td>' + escHtml(item.createdAt || '') + '</td></tr>';
|
|
1279
|
+
}).join('') +
|
|
1280
|
+
'</tbody></table>';
|
|
1281
|
+
}
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
if (count) count.textContent = '?';
|
|
1284
|
+
html = '<span style="color:var(--red)">Failed to load: ' + escHtml(e.message) + '</span>';
|
|
1285
|
+
}
|
|
1286
|
+
// eslint-disable-next-line no-unsanitized/method -- structural HTML only; API fields are escaped above
|
|
1287
|
+
root.replaceChildren(document.createRange().createContextualFragment(html));
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
window.MinionsWorktreeQuarantineRefs = { renderWorktreeQuarantineRefs };
|
|
@@ -52,10 +52,17 @@ function needsAttentionInfo(item) {
|
|
|
52
52
|
if (item._preDispatchEval && item._preDispatchEval.valid === false) {
|
|
53
53
|
var rawReason = String(item._preDispatchEval.reason || '(no reason recorded)');
|
|
54
54
|
var firstLine = rawReason.split(/\r?\n/, 1)[0] || rawReason;
|
|
55
|
+
var exhausted = item._preDispatchEval.exhausted === true;
|
|
56
|
+
var prefix = exhausted
|
|
57
|
+
? 'Pre-dispatch evaluation paused after ' + (item._preDispatchEval.rejectCount || '?') + ' unchanged rejections: '
|
|
58
|
+
: 'Pre-dispatch evaluation rejected: ';
|
|
59
|
+
var fullReason = exhausted
|
|
60
|
+
? rawReason + '\n\nEdit the work item description to resume evaluation, or cancel it.'
|
|
61
|
+
: rawReason;
|
|
55
62
|
return {
|
|
56
63
|
kind: 'pre_dispatch_eval',
|
|
57
|
-
short:
|
|
58
|
-
full:
|
|
64
|
+
short: prefix + firstLine,
|
|
65
|
+
full: fullReason,
|
|
59
66
|
};
|
|
60
67
|
}
|
|
61
68
|
|
|
@@ -1065,7 +1072,9 @@ function viewAgentOutput(logPath) {
|
|
|
1065
1072
|
});
|
|
1066
1073
|
}
|
|
1067
1074
|
|
|
1075
|
+
let _inboxNoteOpenSeq = 0;
|
|
1068
1076
|
function openInboxNote(filename) {
|
|
1077
|
+
var openSeq = ++_inboxNoteOpenSeq;
|
|
1069
1078
|
var idx = (inboxData || []).findIndex(function(item) { return item.name === filename; });
|
|
1070
1079
|
if (idx >= 0) {
|
|
1071
1080
|
openModal(idx);
|
|
@@ -1083,29 +1092,61 @@ function openInboxNote(filename) {
|
|
|
1083
1092
|
// 'archive:<name>' token). Fetch it from notes/archive/ and show it in the
|
|
1084
1093
|
// modal instead of bouncing to the inbox page. Falls back to the inbox page
|
|
1085
1094
|
// only if the file is genuinely gone (e.g. merged into notes.md).
|
|
1095
|
+
var title = filename + ' (archived)';
|
|
1096
|
+
var filePath = 'notes/archive/' + filename;
|
|
1097
|
+
var titleEl = document.getElementById('modal-title');
|
|
1098
|
+
var bodyEl = document.getElementById('modal-body');
|
|
1099
|
+
if (titleEl) titleEl.textContent = title;
|
|
1100
|
+
if (bodyEl) {
|
|
1101
|
+
bodyEl.innerHTML = '<p style="color:var(--muted)">Loading...</p>';
|
|
1102
|
+
bodyEl.style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
1103
|
+
bodyEl.style.whiteSpace = 'normal';
|
|
1104
|
+
}
|
|
1105
|
+
_modalDocContext = { title: title, content: '', selection: '' };
|
|
1106
|
+
_modalFilePath = filePath;
|
|
1107
|
+
var qaEl = document.getElementById('modal-qa');
|
|
1108
|
+
if (qaEl) qaEl.style.display = 'none';
|
|
1109
|
+
var modalEl = document.getElementById('modal');
|
|
1110
|
+
if (modalEl) modalEl.classList.add('open');
|
|
1111
|
+
|
|
1112
|
+
// A note can be superseded by another note before its archive fetch resolves,
|
|
1113
|
+
// or by a different artifact stacked above it. Guard both cases so delayed
|
|
1114
|
+
// success and failure handlers only affect the selection that started them.
|
|
1115
|
+
var applyIfCurrent = function(fn) {
|
|
1116
|
+
if (openSeq !== _inboxNoteOpenSeq ||
|
|
1117
|
+
_modalFilePath !== filePath ||
|
|
1118
|
+
!modalEl ||
|
|
1119
|
+
!modalEl.classList.contains('open')) return false;
|
|
1120
|
+
if (typeof withTopFrame === 'function') return withTopFrame('note', filename, fn);
|
|
1121
|
+
fn();
|
|
1122
|
+
return true;
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1086
1125
|
fetch('/state/notes/archive/' + encodeURIComponent(filename))
|
|
1087
1126
|
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
|
|
1088
1127
|
.then(function(content) {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1128
|
+
applyIfCurrent(function() {
|
|
1129
|
+
if (!titleEl || !bodyEl) return;
|
|
1130
|
+
titleEl.textContent = title;
|
|
1131
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes note content before assembling HTML; filename is set via textContent
|
|
1132
|
+
bodyEl.innerHTML = '<div style="font-size:var(--text-md);line-height:1.7;color:var(--muted)">' + renderMd(content) + '</div>';
|
|
1133
|
+
// Wire Doc Chat for the deeplinked archive note, mirroring the live-inbox
|
|
1134
|
+
// openModal() path (render-prs.js). Without this the archive branch opened
|
|
1135
|
+
// the note as plain text with no Q&A panel, so deeplinking into a note
|
|
1136
|
+
// from a (completed) work item sometimes silently dropped doc-chat.
|
|
1137
|
+
_modalDocContext = { title: title, content: content, selection: '' };
|
|
1138
|
+
_modalFilePath = filePath;
|
|
1139
|
+
showModalQa();
|
|
1140
|
+
// P-34fa5d79 — Source-WI chip for archive notes. Same convention as the
|
|
1141
|
+
// live-inbox branch above; parsed off the freshly-fetched content.
|
|
1142
|
+
_wiInjectSourceChipFromFrontmatter(content, 'modal-body');
|
|
1143
|
+
});
|
|
1105
1144
|
})
|
|
1106
1145
|
.catch(function() {
|
|
1107
|
-
|
|
1108
|
-
|
|
1146
|
+
applyIfCurrent(function() {
|
|
1147
|
+
closeModal();
|
|
1148
|
+
switchPage('inbox');
|
|
1149
|
+
});
|
|
1109
1150
|
});
|
|
1110
1151
|
}
|
|
1111
1152
|
|
package/dashboard/js/settings.js
CHANGED
|
@@ -277,16 +277,18 @@ async function openSettings() {
|
|
|
277
277
|
// (honors the legacy worktreeMode field). Chip is hidden by default and
|
|
278
278
|
// toggled reactively below.
|
|
279
279
|
// W-mr9mx93d000x1dda — third option "Hybrid dispatch": checkoutMode:'live'
|
|
280
|
-
// PLUS a liveValidation block (
|
|
281
|
-
//
|
|
282
|
-
//
|
|
280
|
+
// PLUS a liveValidation block (only chosen work-item types run in-place;
|
|
281
|
+
// all others use isolated worktrees, with an optional test auto-dispatch
|
|
282
|
+
// after eligible coding work). This mirrors the Projects-bar
|
|
283
283
|
// picker (render-other.js _openCheckoutModeMenu / _renderCheckoutModeHybridStep)
|
|
284
284
|
// so both surfaces configure the SAME liveValidation shape identically.
|
|
285
285
|
var currentWtMode = (p.checkoutMode === 'live') ? 'live' : 'worktree';
|
|
286
286
|
var lvTypes = (window._liveValidationTypesArray ? window._liveValidationTypesArray(p) : []);
|
|
287
|
-
// 3-way UI mode: worktree / live / hybrid.
|
|
287
|
+
// 3-way UI mode: worktree / live / hybrid. Keep invalid persisted hybrid
|
|
288
|
+
// blocks in the hybrid editor so saving can repair rather than hide them as
|
|
289
|
+
// full-live.
|
|
288
290
|
var currentCheckoutMode = (currentWtMode === 'live')
|
|
289
|
-
? (lvTypes.length > 0 ? 'hybrid' : 'live')
|
|
291
|
+
? ((lvTypes.length > 0 || p.liveValidationInvalid) ? 'hybrid' : 'live')
|
|
290
292
|
: 'worktree';
|
|
291
293
|
var hybridTypes = window.CHECKOUT_MODE_HYBRID_TYPES || [];
|
|
292
294
|
var hybridDefaultType = window.CHECKOUT_MODE_HYBRID_DEFAULT_TYPE || 'test';
|
|
@@ -303,26 +305,26 @@ async function openSettings() {
|
|
|
303
305
|
var hybridPanel =
|
|
304
306
|
'<div data-checkout-hybrid-panel="' + escHtml(p.name) + '" style="' + (currentCheckoutMode === 'hybrid' ? '' : 'display:none;') + 'margin-top:6px">' +
|
|
305
307
|
'<div style="padding:6px 8px;background:rgba(234,179,8,0.12);border:1px solid var(--yellow);border-radius:4px;color:var(--yellow);font-size:var(--text-xs);line-height:1.4;margin-bottom:6px">' +
|
|
306
|
-
'⚠ Hybrid mode:
|
|
308
|
+
'⚠ Hybrid mode: only the chosen work-item type(s) run in-place on the live checkout; all others use isolated worktrees (one mutating dispatch at a time; dirty-tree recovery follows the live-checkout auto-stash/reset settings).' +
|
|
307
309
|
'</div>' +
|
|
308
|
-
'<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px">Work-item type(s) to
|
|
310
|
+
'<div style="font-size:var(--text-sm);color:var(--muted);margin-bottom:2px">Work-item type(s) to run live:</div>' +
|
|
309
311
|
'<div style="margin-bottom:4px">' + hybridTypeChecks + '</div>' +
|
|
310
312
|
'<label style="display:inline-flex;align-items:center;gap:6px;font-size:var(--text-sm);color:var(--text);margin-top:2px">' +
|
|
311
313
|
'<input type="checkbox" id="set-liveValidationAutoDispatch-' + escHtml(p.name) + '"' + (hybridAutoPre ? ' checked' : '') + '>' +
|
|
312
|
-
'<span>Automatically
|
|
314
|
+
'<span>Automatically run a test validation after each eligible coding work item completes</span>' +
|
|
313
315
|
'</label>' +
|
|
314
|
-
'<div data-checkout-hybrid-error="' + escHtml(p.name) + '" style="display:none;margin-top:6px;color:var(--red);font-size:var(--text-xs)">Select at least one work-item type to
|
|
316
|
+
'<div data-checkout-hybrid-error="' + escHtml(p.name) + '" style="display:none;margin-top:6px;color:var(--red);font-size:var(--text-xs)">Select at least one work-item type to run live.</div>' +
|
|
315
317
|
'</div>';
|
|
316
318
|
var worktreeModeBlock =
|
|
317
319
|
'<div data-search="' + escHtml(wtModeSearch) + '" style="margin-bottom:6px">' +
|
|
318
320
|
'<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Checkout mode</label>' +
|
|
319
321
|
'<select id="set-checkoutMode-' + escHtml(p.name) + '" data-checkout-mode-select="' + escHtml(p.name) + '" style="width:100%;padding:4px 6px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:var(--text-md)">' +
|
|
320
322
|
'<option value="worktree" title="Each dispatch runs in its own isolated git worktree. Fully parallel, never touches your working directory."' + (currentCheckoutMode === 'worktree' ? ' selected' : '') + '>Worktree (default)</option>' +
|
|
321
|
-
'<option value="live" title="Agents run in-place in the project folder. One mutating dispatch at a time;
|
|
322
|
-
'<option value="hybrid" title="
|
|
323
|
+
'<option value="live" title="Agents run in-place in the project folder. One mutating dispatch at a time; dirty-tree handling follows the auto-stash/reset policy."' + (currentCheckoutMode === 'live' ? ' selected' : '') + '>Live checkout</option>' +
|
|
324
|
+
'<option value="hybrid" title="Only chosen work-item types run on the real checkout; all others use isolated worktrees."' + (currentCheckoutMode === 'hybrid' ? ' selected' : '') + '>Hybrid dispatch</option>' +
|
|
323
325
|
'</select>' +
|
|
324
326
|
'<div data-checkout-mode-chip="' + escHtml(p.name) + '" style="' + (currentCheckoutMode === 'live' ? '' : 'display:none;') + 'margin-top:6px;padding:6px 8px;background:rgba(234,179,8,0.12);border:1px solid var(--yellow);border-radius:4px;color:var(--yellow);font-size:var(--text-xs);line-height:1.4">' +
|
|
325
|
-
'⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty
|
|
327
|
+
'⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty-tree behavior follows the auto-stash/reset settings below.' +
|
|
326
328
|
'</div>' +
|
|
327
329
|
hybridPanel +
|
|
328
330
|
'</div>';
|
|
@@ -474,6 +476,7 @@ async function openSettings() {
|
|
|
474
476
|
// Tooltip on copilotDisableBuiltinMcps MUST warn about the split-brain risk
|
|
475
477
|
settingsToggle('Copilot: disable built-in MCPs', 'set-copilotDisableBuiltinMcps', e.copilotDisableBuiltinMcps !== false,
|
|
476
478
|
'⚠ When OFF, Copilot agents can autonomously create PRs/labels/comments via the github-mcp-server, bypassing pull-requests.json tracking — Minions and Copilot end up with split views of the same PR. Keep ON unless you understand the risk.') +
|
|
479
|
+
settingsToggle('Propagate CLAUDE.md to non-Claude runtimes', 'set-propagateClaudeMdForNonClaudeRuntimes', e.propagateClaudeMdForNonClaudeRuntimes !== false, 'Copilot/Codex have NO native CLAUDE.md auto-load. When ON (default), the engine bounded-discovers the nearest applicable repo-authored CLAUDE.md files for a dispatch and injects them as a "Project instructions (CLAUDE.md)" context layer so load-bearing "keep in sync" rules and blessed tooling reach the agent. Claude is always skipped (its CLI reads CLAUDE.md itself). Orthogonal to each runtime\'s native AGENTS.md discovery — CLAUDE.md carries no split-brain risk because Copilot/Codex never read it themselves. Per-project override via project.propagateClaudeMdForNonClaudeRuntimes.') +
|
|
477
480
|
settingsToggle('Copilot: reasoning summaries', 'set-copilotReasoningSummaries', !!e.copilotReasoningSummaries, '--enable-reasoning-summaries (Anthropic-family models only)') +
|
|
478
481
|
'</div>' +
|
|
479
482
|
'<div class="settings-grid-2">' +
|
|
@@ -774,6 +777,17 @@ async function openSettings() {
|
|
|
774
777
|
if (hybridPanel) hybridPanel.style.display = (sel.value === 'hybrid') ? '' : 'none';
|
|
775
778
|
});
|
|
776
779
|
});
|
|
780
|
+
document.querySelectorAll('[id^="set-liveValidationAutoDispatch-"]').forEach(function(autoDispatch) {
|
|
781
|
+
autoDispatch.addEventListener('change', function() {
|
|
782
|
+
if (!autoDispatch.checked) return;
|
|
783
|
+
const projectName = autoDispatch.id.slice('set-liveValidationAutoDispatch-'.length);
|
|
784
|
+
const esc = (window.CSS && CSS.escape ? CSS.escape(projectName) : projectName);
|
|
785
|
+
const testType = document.querySelector(
|
|
786
|
+
'[data-live-validation-type="' + esc + '"][value="test"]',
|
|
787
|
+
);
|
|
788
|
+
if (testType) testType.checked = true;
|
|
789
|
+
});
|
|
790
|
+
});
|
|
777
791
|
}
|
|
778
792
|
|
|
779
793
|
// Lazily open/close the per-agent charter editor in the Settings Agents table.
|
|
@@ -960,19 +974,16 @@ async function loadModelsForAgent(agentId, runtimeName, currentValue) {
|
|
|
960
974
|
cell.innerHTML = '<input ' + baseAttrs + ' value="' + escHtml(currentValue || '') + '" placeholder="' + escHtml(runtimeName) + ' default" style="' + baseStyle + '">';
|
|
961
975
|
return;
|
|
962
976
|
}
|
|
963
|
-
|
|
977
|
+
const listId = 'agent-model-options-' + agentId;
|
|
978
|
+
let opts = '';
|
|
964
979
|
for (const m of models) {
|
|
965
980
|
const id = m.id || m.name || '';
|
|
966
981
|
if (!id) continue;
|
|
967
|
-
const label = m.name && m.name !== id ?
|
|
968
|
-
opts += '<option value="' + escHtml(id) + '"' + (
|
|
969
|
-
}
|
|
970
|
-
// Preserve unknown saved values so a user-set custom ID survives the next save.
|
|
971
|
-
if (currentValue && !models.some(m => (m.id || m.name) === currentValue)) {
|
|
972
|
-
opts += '<option value="' + escHtml(currentValue) + '" selected>' + escHtml(currentValue) + ' (custom — invalid for ' + escHtml(runtimeName) + '?)</option>';
|
|
982
|
+
const label = m.name && m.name !== id ? m.name : '';
|
|
983
|
+
opts += '<option value="' + escHtml(id) + '"' + (label ? ' label="' + escHtml(label) + '"' : '') + '></option>';
|
|
973
984
|
}
|
|
974
985
|
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: agentId, model id, model label, currentValue, runtimeName)
|
|
975
|
-
cell.innerHTML = '<
|
|
986
|
+
cell.innerHTML = '<input ' + baseAttrs + ' list="' + escHtml(listId) + '" value="' + escHtml(currentValue || '') + '" placeholder="' + escHtml(runtimeName) + ' default" autocomplete="off" style="' + baseStyle + '"><datalist id="' + escHtml(listId) + '">' + opts + '</datalist>';
|
|
976
987
|
}
|
|
977
988
|
|
|
978
989
|
function settingsToggle(label, id, checked, hint) {
|
|
@@ -1178,6 +1189,7 @@ async function saveSettings() {
|
|
|
1178
1189
|
claudeFallbackModel: (document.getElementById('set-claudeFallbackModel')?.value ?? '').trim(),
|
|
1179
1190
|
copilotFallbackModel: (document.getElementById('set-copilotFallbackModel')?.value ?? '').trim(),
|
|
1180
1191
|
copilotDisableBuiltinMcps: !!document.getElementById('set-copilotDisableBuiltinMcps')?.checked,
|
|
1192
|
+
propagateClaudeMdForNonClaudeRuntimes: !!document.getElementById('set-propagateClaudeMdForNonClaudeRuntimes')?.checked,
|
|
1181
1193
|
copilotStreamMode: document.getElementById('set-copilotStreamMode')?.value || 'on',
|
|
1182
1194
|
copilotReasoningSummaries: !!document.getElementById('set-copilotReasoningSummaries')?.checked,
|
|
1183
1195
|
claudePreApproveWorkspaceMcps: !!document.getElementById('set-claudePreApproveWorkspaceMcps')?.checked,
|
|
@@ -1244,13 +1256,23 @@ async function saveSettings() {
|
|
|
1244
1256
|
const types = [];
|
|
1245
1257
|
typeEls.forEach(function(el) { if (el && el.checked) types.push(el.value); });
|
|
1246
1258
|
const errEl = document.querySelector('[data-checkout-hybrid-error="' + esc + '"]');
|
|
1259
|
+
const autoEl = document.getElementById('set-liveValidationAutoDispatch-' + p.name);
|
|
1260
|
+
const autoDispatch = autoEl?.checked === true;
|
|
1247
1261
|
if (types.length === 0) {
|
|
1248
|
-
hybridValidationErrors.push(p.name);
|
|
1249
|
-
if (errEl)
|
|
1262
|
+
hybridValidationErrors.push(p.name + ': select at least one live type');
|
|
1263
|
+
if (errEl) {
|
|
1264
|
+
errEl.textContent = 'Select at least one work-item type to run live.';
|
|
1265
|
+
errEl.style.display = '';
|
|
1266
|
+
}
|
|
1267
|
+
} else if (autoDispatch && types.indexOf('test') === -1) {
|
|
1268
|
+
hybridValidationErrors.push(p.name + ': auto-validation requires test');
|
|
1269
|
+
if (errEl) {
|
|
1270
|
+
errEl.textContent = 'Automatic validation requires the test type.';
|
|
1271
|
+
errEl.style.display = '';
|
|
1272
|
+
}
|
|
1250
1273
|
} else {
|
|
1251
1274
|
if (errEl) errEl.style.display = 'none';
|
|
1252
|
-
|
|
1253
|
-
liveValidation = { type: types, autoDispatch: !!(autoEl && autoEl.checked) };
|
|
1275
|
+
liveValidation = { type: types, autoDispatch };
|
|
1254
1276
|
}
|
|
1255
1277
|
}
|
|
1256
1278
|
// W-mqtvnnj1000357fa — per-project live-checkout auto-stash override.
|
|
@@ -1280,7 +1302,7 @@ async function saveSettings() {
|
|
|
1280
1302
|
});
|
|
1281
1303
|
|
|
1282
1304
|
if (hybridValidationErrors.length > 0) {
|
|
1283
|
-
throw new Error('
|
|
1305
|
+
throw new Error('Invalid hybrid dispatch configuration: ' + hybridValidationErrors.join(', '));
|
|
1284
1306
|
}
|
|
1285
1307
|
|
|
1286
1308
|
// Save config
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
</h2>
|
|
27
27
|
<div id="keep-processes-content"><p class="empty">No agents have left processes running. Set <code>meta.keep_processes: true</code> on a work item to enable.</p></div>
|
|
28
28
|
</section>
|
|
29
|
+
<section id="worktree-quarantine-refs-section">
|
|
30
|
+
<h2>Quarantined Work <span class="count" id="worktree-quarantine-refs-count">0</span>
|
|
31
|
+
<span class="qa-section-subtitle">local backup refs available for manual recovery</span>
|
|
32
|
+
</h2>
|
|
33
|
+
<div id="worktree-quarantine-refs-content"><p class="empty">No quarantined work refs found.</p></div>
|
|
34
|
+
</section>
|
|
29
35
|
<section id="managed-processes-section">
|
|
30
36
|
<h2>Managed Processes <span class="count" id="managed-processes-count">0</span>
|
|
31
37
|
<span class="qa-section-subtitle">engine-managed long-running services</span>
|