@yemi33/minions 0.1.2305 → 0.1.2306
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/dashboard/js/refresh.js +17 -13
- package/dashboard/js/render-other.js +251 -3
- package/dashboard/js/render-plans.js +1 -1
- package/dashboard/js/render-prs.js +4 -1
- package/dashboard/js/render-utils.js +13 -0
- package/dashboard/js/render-work-items.js +11 -13
- package/dashboard/js/settings.js +3 -3
- package/dashboard/styles.css +58 -0
- package/dashboard.js +32 -9
- package/docs/completion-reports.md +25 -0
- package/docs/copilot-cli-schema.md +31 -1
- package/docs/design-state-storage.md +1 -1
- package/docs/live-checkout-mode.md +5 -3
- package/engine/ado.js +37 -0
- package/engine/consolidation.js +59 -2
- package/engine/lifecycle.js +149 -0
- package/engine/live-checkout.js +128 -7
- package/engine/playbook.js +2 -2
- package/engine/queries.js +11 -7
- package/engine/shared.js +6 -5
- package/engine/supervisor.js +131 -19
- package/engine.js +2 -2
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -134,7 +134,8 @@ const RENDER_VERSIONS = {
|
|
|
134
134
|
prdProgress: 2,
|
|
135
135
|
prdPrs: 1,
|
|
136
136
|
inbox: 2,
|
|
137
|
-
|
|
137
|
+
// Bumped 4→5 for the clickable checkout-mode pill + picker (W-mr1b67zi0006b788).
|
|
138
|
+
projects: 5,
|
|
138
139
|
notes: 1,
|
|
139
140
|
prd: 3,
|
|
140
141
|
prs: 3,
|
|
@@ -147,7 +148,7 @@ const RENDER_VERSIONS = {
|
|
|
147
148
|
dispatch: 2,
|
|
148
149
|
engineLog: 2,
|
|
149
150
|
metrics: 1,
|
|
150
|
-
workItems:
|
|
151
|
+
workItems: 7,
|
|
151
152
|
skills: 1,
|
|
152
153
|
commands: 1,
|
|
153
154
|
mcpServers: 1,
|
|
@@ -1066,8 +1067,17 @@ let _refreshInFlight = false;
|
|
|
1066
1067
|
// it's not obvious the dashboard itself is dead (vs. wedged, vs. the
|
|
1067
1068
|
// engine being down).
|
|
1068
1069
|
//
|
|
1069
|
-
// Trip conditions:
|
|
1070
|
-
//
|
|
1070
|
+
// Trip conditions (2026-06-30): SUSTAINED failure only — the banner must never
|
|
1071
|
+
// flash while the dashboard is merely slow. 3 consecutive failed polls
|
|
1072
|
+
// (timeout/abort or 5xx). At the 4s cadence a wedged dashboard aborts each poll
|
|
1073
|
+
// at safeFetch's 15s timeout, so 3 in a row is ~45s of continuous
|
|
1074
|
+
// unresponsiveness — well beyond the legitimate 15-25s cold getStatus() rebuild
|
|
1075
|
+
// documented in restart-health.js. A truly-dead dashboard still trips promptly
|
|
1076
|
+
// (~12s) because connection-refused rejects fast rather than waiting out the
|
|
1077
|
+
// 15s timeout. The old "> N seconds since last success" OR side was REMOVED:
|
|
1078
|
+
// aborts during a healthy cold rebuild pushed elapsed-time past the budget and
|
|
1079
|
+
// surfaced the banner while the dashboard was alive-but-busy. Consecutive
|
|
1080
|
+
// failures are the only reliable "actually unreachable" signal.
|
|
1071
1081
|
// On trip we show a sticky red banner with two recovery actions:
|
|
1072
1082
|
// 1. "Restart Minions" → ccRestartMinions (works when dashboard is
|
|
1073
1083
|
// alive-but-stale; falls through to copy-to-clipboard when the POST
|
|
@@ -1080,14 +1090,7 @@ let _lastStatusOkAt = Date.now();
|
|
|
1080
1090
|
let _consecutiveStatusFails = 0;
|
|
1081
1091
|
let _unreachableSince = 0; // 0 = currently reachable
|
|
1082
1092
|
let _unreachableAgeTimer = null;
|
|
1083
|
-
// 3 + 20s (was 2 + 12s) — the prior thresholds tripped a banner on a single
|
|
1084
|
-
// safeFetch abort (timeout 15s in state.js) because the OR'd age side was
|
|
1085
|
-
// already satisfied. With safeFetch=15s, the age budget needs >15s to avoid
|
|
1086
|
-
// the trip-on-first-slow-response footgun. 3 consecutive fails ≈ 12s of real
|
|
1087
|
-
// outage at the 4s poll cadence, which still surfaces the banner promptly
|
|
1088
|
-
// during a real dashboard crash but tolerates one slow rebuild + retry.
|
|
1089
1093
|
const _UNREACHABLE_FAIL_THRESHOLD = 3;
|
|
1090
|
-
const _UNREACHABLE_AGE_MS = 20000;
|
|
1091
1094
|
// Once the banner is up we throttle polls (exponential, capped at 30s) to
|
|
1092
1095
|
// avoid hammering a struggling dashboard / network. Reset on recovery.
|
|
1093
1096
|
let _nextPollAllowedAt = 0;
|
|
@@ -1345,8 +1348,9 @@ async function refresh(opts) {
|
|
|
1345
1348
|
_diagEntry.error_message = String((e && e.message) || e);
|
|
1346
1349
|
}
|
|
1347
1350
|
_consecutiveStatusFails++;
|
|
1348
|
-
|
|
1349
|
-
|
|
1351
|
+
// Sustained failure only — see the detector header for why elapsed time
|
|
1352
|
+
// alone must not trip (it flashed the banner on healthy-but-slow rebuilds).
|
|
1353
|
+
if (_consecutiveStatusFails >= _UNREACHABLE_FAIL_THRESHOLD) {
|
|
1350
1354
|
_markDashboardUnreachable(e);
|
|
1351
1355
|
}
|
|
1352
1356
|
// Backoff: once we've tripped the banner, throttle subsequent polls
|
|
@@ -96,18 +96,266 @@ function _renderProjectBranch(p) {
|
|
|
96
96
|
// is set, coding WIs author in isolated worktrees and only the named validation
|
|
97
97
|
// type runs in-place on the live checkout. This is a materially different
|
|
98
98
|
// dispatch profile from full-live, so it gets its own pill.
|
|
99
|
+
// W-mr1b67zi0006b788: the pill is clickable — opens an inline picker (see
|
|
100
|
+
// _openCheckoutModeMenu below) to switch between Worktrees / Live checkout /
|
|
101
|
+
// Hybrid dispatch modes without leaving the Projects view. Shared
|
|
102
|
+
// data-checkout-pill / role="button" / tabindex / aria-haspopup attributes
|
|
103
|
+
// are attached to all three pill variants; the delegated listener wired at
|
|
104
|
+
// the bottom of this file reads data-checkout-pill to know which project to
|
|
105
|
+
// open the menu for.
|
|
99
106
|
function _renderWorktreeModePill(p) {
|
|
100
107
|
if (!p) return '';
|
|
108
|
+
const name = escHtml(p.name);
|
|
109
|
+
const common = ' data-checkout-pill="' + name + '" role="button" tabindex="0" aria-haspopup="true" aria-label="Change checkout mode"';
|
|
101
110
|
if (p.checkoutMode === 'live') {
|
|
102
111
|
if (p.liveValidationType) {
|
|
103
112
|
const vt = escapeHtml(p.liveValidationType);
|
|
104
|
-
return ' <span class="project-mode-pill project-mode-hybrid" title="Hybrid live-validation — coding work items author in isolated worktrees; only the "' + vt + '" validation type runs in-place on the live checkout (capped to one mutating dispatch, refused on a dirty tree)">⚡ Hybrid · ' + vt + '</span>';
|
|
113
|
+
return ' <span class="project-mode-pill project-mode-hybrid project-mode-pill-clickable"' + common + ' title="Hybrid live-validation — coding work items author in isolated worktrees; only the "' + vt + '" validation type runs in-place on the live checkout (capped to one mutating dispatch, refused on a dirty tree). Click to change.">⚡ Hybrid · ' + vt + '</span>';
|
|
105
114
|
}
|
|
106
|
-
return ' <span class="project-mode-pill project-mode-live" title="Live-checkout dispatch mode — agents run in-place inside the project working tree (no isolated worktree); capped to one mutating dispatch and refused on a dirty tree">⚡ Live checkout</span>';
|
|
115
|
+
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 and refused on a dirty tree. Click to change.">⚡ Live checkout</span>';
|
|
107
116
|
}
|
|
108
|
-
return ' <span class="project-mode-pill project-mode-isolated" title="Worktree dispatch mode (default) — each agent runs in its own git worktree">Worktrees</span>';
|
|
117
|
+
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>';
|
|
109
118
|
}
|
|
110
119
|
|
|
120
|
+
// Work-item types eligible for hybrid live-validation (W-mr1b67zi0006b788).
|
|
121
|
+
// Mirrors the set the server documents for project.liveValidation.type;
|
|
122
|
+
// intentionally excludes the illustrative "build-and-test" example from the
|
|
123
|
+
// server comments since that isn't a real WORK_TYPE value.
|
|
124
|
+
const CHECKOUT_MODE_HYBRID_TYPES = ['implement', 'fix', 'docs', 'decompose', 'explore', 'ask', 'review', 'test', 'verify', 'setup'];
|
|
125
|
+
const CHECKOUT_MODE_HYBRID_DEFAULT_TYPE = 'test';
|
|
126
|
+
|
|
127
|
+
function _findProjectInLastStatus(name) {
|
|
128
|
+
const projects = window._lastStatus && window._lastStatus.projects;
|
|
129
|
+
if (!Array.isArray(projects)) return null;
|
|
130
|
+
return projects.find(function(p) { return p.name === name; }) || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _closeCheckoutModeMenu() {
|
|
134
|
+
const existing = document.getElementById('checkout-mode-menu');
|
|
135
|
+
if (existing) existing.remove();
|
|
136
|
+
document.removeEventListener('mousedown', _checkoutModeMenuOutsideClick, true);
|
|
137
|
+
document.removeEventListener('keydown', _checkoutModeMenuKeydown, true);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function _checkoutModeMenuOutsideClick(e) {
|
|
141
|
+
const menu = document.getElementById('checkout-mode-menu');
|
|
142
|
+
if (!menu) return;
|
|
143
|
+
if (menu.contains(e.target)) return;
|
|
144
|
+
if (e.target.closest && e.target.closest('[data-checkout-pill]')) return;
|
|
145
|
+
_closeCheckoutModeMenu();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function _checkoutModeMenuKeydown(e) {
|
|
149
|
+
if (e.key === 'Escape') _closeCheckoutModeMenu();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Builds and positions the checkout-mode picker popover beneath the clicked
|
|
153
|
+
// pill. No innerHTML is used here (createElement/textContent throughout) to
|
|
154
|
+
// stay clear of the innerHTML lint gate (npm run lint).
|
|
155
|
+
function _openCheckoutModeMenu(projectName, anchorEl) {
|
|
156
|
+
_closeCheckoutModeMenu();
|
|
157
|
+
const project = _findProjectInLastStatus(projectName);
|
|
158
|
+
if (!project) return;
|
|
159
|
+
|
|
160
|
+
const menu = document.createElement('div');
|
|
161
|
+
menu.id = 'checkout-mode-menu';
|
|
162
|
+
menu.className = 'checkout-mode-menu';
|
|
163
|
+
menu.setAttribute('role', 'menu');
|
|
164
|
+
|
|
165
|
+
const title = document.createElement('div');
|
|
166
|
+
title.className = 'checkout-mode-menu-title';
|
|
167
|
+
title.textContent = 'Checkout mode — ' + projectName;
|
|
168
|
+
menu.appendChild(title);
|
|
169
|
+
|
|
170
|
+
function addItem(labelText, mode, descText) {
|
|
171
|
+
const item = document.createElement('div');
|
|
172
|
+
item.className = 'checkout-mode-menu-item';
|
|
173
|
+
item.setAttribute('role', 'menuitemradio');
|
|
174
|
+
item.tabIndex = 0;
|
|
175
|
+
const label = document.createElement('div');
|
|
176
|
+
label.className = 'checkout-mode-menu-item-label';
|
|
177
|
+
label.textContent = labelText;
|
|
178
|
+
item.appendChild(label);
|
|
179
|
+
if (descText) {
|
|
180
|
+
const desc = document.createElement('div');
|
|
181
|
+
desc.className = 'checkout-mode-menu-item-desc';
|
|
182
|
+
desc.textContent = descText;
|
|
183
|
+
item.appendChild(desc);
|
|
184
|
+
}
|
|
185
|
+
const isActive = (mode === 'worktree' && project.checkoutMode !== 'live')
|
|
186
|
+
|| (mode === 'live' && project.checkoutMode === 'live' && !project.liveValidationType)
|
|
187
|
+
|| (mode === 'hybrid' && project.checkoutMode === 'live' && !!project.liveValidationType);
|
|
188
|
+
if (isActive) {
|
|
189
|
+
item.setAttribute('aria-checked', 'true');
|
|
190
|
+
item.classList.add('checkout-mode-menu-item-active');
|
|
191
|
+
} else {
|
|
192
|
+
item.setAttribute('aria-checked', 'false');
|
|
193
|
+
}
|
|
194
|
+
item.addEventListener('click', function() {
|
|
195
|
+
if (mode === 'hybrid') {
|
|
196
|
+
_renderCheckoutModeHybridStep(menu, projectName);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
_closeCheckoutModeMenu();
|
|
200
|
+
_applyCheckoutModeChange(projectName, mode === 'live' ? 'live' : 'worktree', null);
|
|
201
|
+
});
|
|
202
|
+
item.addEventListener('keydown', function(e) {
|
|
203
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); item.click(); }
|
|
204
|
+
});
|
|
205
|
+
menu.appendChild(item);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
addItem('Worktrees (default)', 'worktree', 'Each dispatch runs in its own isolated git worktree. Fully parallel, never touches your working directory.');
|
|
209
|
+
addItem('Live checkout', 'live', 'Agents run in-place in the project folder. One mutating dispatch at a time; refuses to run on a dirty tree.');
|
|
210
|
+
addItem('Hybrid dispatch…', 'hybrid', 'Coding work authors in isolated worktrees; only a chosen validation step (e.g. build-and-test) runs on the real checkout.');
|
|
211
|
+
|
|
212
|
+
document.body.appendChild(menu);
|
|
213
|
+
const rect = anchorEl.getBoundingClientRect();
|
|
214
|
+
menu.style.position = 'absolute';
|
|
215
|
+
menu.style.top = (window.scrollY + rect.bottom + 4) + 'px';
|
|
216
|
+
menu.style.left = (window.scrollX + rect.left) + 'px';
|
|
217
|
+
|
|
218
|
+
document.addEventListener('mousedown', _checkoutModeMenuOutsideClick, true);
|
|
219
|
+
document.addEventListener('keydown', _checkoutModeMenuKeydown, true);
|
|
220
|
+
const first = menu.querySelector('.checkout-mode-menu-item');
|
|
221
|
+
if (first) first.focus();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Second step of the picker for hybrid mode: pick which work-item type runs
|
|
225
|
+
// in-place on the live checkout while everything else authors in isolated
|
|
226
|
+
// worktrees. Renders a <select> + Back/Apply into the existing menu element.
|
|
227
|
+
function _renderCheckoutModeHybridStep(menu, projectName) {
|
|
228
|
+
const project = _findProjectInLastStatus(projectName);
|
|
229
|
+
while (menu.firstChild) menu.removeChild(menu.firstChild);
|
|
230
|
+
|
|
231
|
+
const title = document.createElement('div');
|
|
232
|
+
title.className = 'checkout-mode-menu-title';
|
|
233
|
+
title.textContent = 'Hybrid dispatch — ' + projectName;
|
|
234
|
+
menu.appendChild(title);
|
|
235
|
+
|
|
236
|
+
const label = document.createElement('div');
|
|
237
|
+
label.className = 'checkout-mode-menu-item';
|
|
238
|
+
label.textContent = 'Work-item type to validate live:';
|
|
239
|
+
menu.appendChild(label);
|
|
240
|
+
|
|
241
|
+
const select = document.createElement('select');
|
|
242
|
+
select.className = 'checkout-mode-menu-select';
|
|
243
|
+
CHECKOUT_MODE_HYBRID_TYPES.forEach(function(t) {
|
|
244
|
+
const opt = document.createElement('option');
|
|
245
|
+
opt.value = t;
|
|
246
|
+
opt.textContent = t;
|
|
247
|
+
select.appendChild(opt);
|
|
248
|
+
});
|
|
249
|
+
select.value = (project && project.liveValidationType && CHECKOUT_MODE_HYBRID_TYPES.includes(project.liveValidationType))
|
|
250
|
+
? project.liveValidationType
|
|
251
|
+
: CHECKOUT_MODE_HYBRID_DEFAULT_TYPE;
|
|
252
|
+
menu.appendChild(select);
|
|
253
|
+
|
|
254
|
+
const actions = document.createElement('div');
|
|
255
|
+
actions.className = 'checkout-mode-menu-actions';
|
|
256
|
+
|
|
257
|
+
const backBtn = document.createElement('button');
|
|
258
|
+
backBtn.type = 'button';
|
|
259
|
+
backBtn.className = 'checkout-mode-menu-btn';
|
|
260
|
+
backBtn.textContent = 'Back';
|
|
261
|
+
backBtn.addEventListener('click', function() {
|
|
262
|
+
const anchor = document.querySelector('[data-checkout-pill="' + CSS.escape(projectName) + '"]');
|
|
263
|
+
_closeCheckoutModeMenu();
|
|
264
|
+
if (anchor) _openCheckoutModeMenu(projectName, anchor);
|
|
265
|
+
});
|
|
266
|
+
actions.appendChild(backBtn);
|
|
267
|
+
|
|
268
|
+
const applyBtn = document.createElement('button');
|
|
269
|
+
applyBtn.type = 'button';
|
|
270
|
+
applyBtn.className = 'checkout-mode-menu-btn checkout-mode-menu-btn-primary';
|
|
271
|
+
applyBtn.textContent = 'Apply';
|
|
272
|
+
applyBtn.addEventListener('click', function() {
|
|
273
|
+
const type = select.value;
|
|
274
|
+
_closeCheckoutModeMenu();
|
|
275
|
+
_applyCheckoutModeChange(projectName, 'live', type);
|
|
276
|
+
});
|
|
277
|
+
actions.appendChild(applyBtn);
|
|
278
|
+
|
|
279
|
+
menu.appendChild(actions);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Applies a checkout-mode change: confirms via confirmDialog(), optimistically
|
|
283
|
+
// flips the cached project entry and re-renders BEFORE awaiting the network
|
|
284
|
+
// call (mirrors projectChipRemove / removePinnedNote), then POSTs to
|
|
285
|
+
// /api/settings. On failure, reverts the optimistic flip, re-renders, and
|
|
286
|
+
// shows an error toast.
|
|
287
|
+
async function _applyCheckoutModeChange(projectName, newMode, liveValidationType) {
|
|
288
|
+
const project = _findProjectInLastStatus(projectName);
|
|
289
|
+
if (!project) return;
|
|
290
|
+
const label = newMode === 'worktree'
|
|
291
|
+
? 'Worktrees'
|
|
292
|
+
: (liveValidationType ? ('Hybrid (' + liveValidationType + ')') : 'Live checkout');
|
|
293
|
+
const ok = await confirmDialog({
|
|
294
|
+
title: 'Change checkout mode?',
|
|
295
|
+
message: 'Switch "' + projectName + '" to ' + label + '? This changes how every future dispatch on this project runs.',
|
|
296
|
+
confirmLabel: 'Change mode',
|
|
297
|
+
cancelLabel: 'Cancel',
|
|
298
|
+
danger: newMode !== 'worktree',
|
|
299
|
+
});
|
|
300
|
+
if (!ok) return;
|
|
301
|
+
|
|
302
|
+
const prevMode = project.checkoutMode;
|
|
303
|
+
const prevType = project.liveValidationType;
|
|
304
|
+
const nextType = (newMode === 'live' && liveValidationType) ? liveValidationType : null;
|
|
305
|
+
// Optimistic flip BEFORE awaiting the API call (per dashboard convention —
|
|
306
|
+
// see projectChipRemove / removePinnedNote); reverted in the catch below.
|
|
307
|
+
project.checkoutMode = newMode;
|
|
308
|
+
project.liveValidationType = nextType;
|
|
309
|
+
if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const res = await safeFetch('/api/settings', {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: { 'Content-Type': 'application/json' },
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
projects: [{
|
|
317
|
+
name: projectName,
|
|
318
|
+
checkoutMode: newMode,
|
|
319
|
+
liveValidation: nextType ? { type: nextType } : null,
|
|
320
|
+
}],
|
|
321
|
+
}),
|
|
322
|
+
});
|
|
323
|
+
const result = await res.json().catch(function() { return {}; });
|
|
324
|
+
if (!res.ok) throw new Error((result && result.error) || ('HTTP ' + res.status));
|
|
325
|
+
showToast('cmd-toast', 'Checkout mode updated for "' + projectName + '".', true);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
project.checkoutMode = prevMode;
|
|
328
|
+
project.liveValidationType = prevType;
|
|
329
|
+
if (window._lastStatus && Array.isArray(window._lastStatus.projects)) renderProjects(window._lastStatus.projects);
|
|
330
|
+
showToast('cmd-toast', 'Failed to update checkout mode: ' + (err && err.message || err), false);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Delegated click/keydown on the persistent #projects-list container (its
|
|
335
|
+
// children are replaced wholesale on every renderProjects() call, but the
|
|
336
|
+
// container itself survives re-renders — see the Long-Running Commands /
|
|
337
|
+
// event-delegation convention referenced in the playbook).
|
|
338
|
+
(function _wireCheckoutModePillDelegation() {
|
|
339
|
+
const list = document.getElementById('projects-list');
|
|
340
|
+
if (!list) return;
|
|
341
|
+
list.addEventListener('click', function(e) {
|
|
342
|
+
const pill = e.target.closest('[data-checkout-pill]');
|
|
343
|
+
if (!pill) return;
|
|
344
|
+
e.stopPropagation();
|
|
345
|
+
const name = pill.getAttribute('data-checkout-pill');
|
|
346
|
+
_openCheckoutModeMenu(name, pill);
|
|
347
|
+
});
|
|
348
|
+
list.addEventListener('keydown', function(e) {
|
|
349
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
350
|
+
const pill = e.target.closest('[data-checkout-pill]');
|
|
351
|
+
if (!pill) return;
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
e.stopPropagation();
|
|
354
|
+
const name = pill.getAttribute('data-checkout-pill');
|
|
355
|
+
_openCheckoutModeMenu(name, pill);
|
|
356
|
+
});
|
|
357
|
+
})();
|
|
358
|
+
|
|
111
359
|
function _projectCachePath(project) {
|
|
112
360
|
return String((project && (project.localPath || project.path)) || '').replace(/\\/g, '/');
|
|
113
361
|
}
|
|
@@ -371,7 +371,7 @@ function renderPlans(plans) {
|
|
|
371
371
|
'onclick="event.stopPropagation();planDelete(\'' + escapeHtml(p.file) + '\')">Delete</button>' : '';
|
|
372
372
|
|
|
373
373
|
const versionBadge = p.version ? ' <span style="font-size:var(--text-xs);font-weight:700;padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue);vertical-align:middle">v' + p.version + '</span>' : '';
|
|
374
|
-
const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--
|
|
374
|
+
const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'converting': 'var(--yellow)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--purple,#a855f7)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)', 'draft': 'var(--muted)' };
|
|
375
375
|
const cardClass = effectiveStatus === 'dispatched' || effectiveStatus === 'converting' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
|
|
376
376
|
// P-e8d49105 — cross-repo plans surface every touched project as its
|
|
377
377
|
// own badge. Single-project plans (and old PRDs without _projects)
|
|
@@ -646,7 +646,10 @@ async function togglePrObserve(btn) {
|
|
|
646
646
|
try {
|
|
647
647
|
const res = await fetch('/api/pull-requests/observe', {
|
|
648
648
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
649
|
-
|
|
649
|
+
// Canonical contextOnly is the primary payload (P-2b6e4d81); the
|
|
650
|
+
// legacy `observe` body param remains a server-side back-compat
|
|
651
|
+
// fallback for any other caller, so the client no longer sends it.
|
|
652
|
+
body: JSON.stringify({ host, slug, number, contextOnly: !next })
|
|
650
653
|
});
|
|
651
654
|
if (!res.ok) {
|
|
652
655
|
const d = await res.json().catch(() => ({}));
|
|
@@ -114,6 +114,19 @@ function ccSegmentsApplyChunk(segments, text, segmentId) {
|
|
|
114
114
|
var last = segments.length ? segments[segments.length - 1] : null;
|
|
115
115
|
if (hasId) {
|
|
116
116
|
if (last && last.kind === 'text' && last.id === segmentId) { last.text = text; return segments; }
|
|
117
|
+
// W-mr1bgfyr — a reconnect replay (or any other id-less push, see the
|
|
118
|
+
// heuristic branch below) leaves the trailing text segment with `id:
|
|
119
|
+
// null` even though the resumed live stream now carries a real
|
|
120
|
+
// segmentId for that SAME logical block. Without this, the id
|
|
121
|
+
// mismatch (null !== segmentId) opened a brand-new duplicate text
|
|
122
|
+
// segment on every following chunk, rendering as a second bubble/block
|
|
123
|
+
// stacked under the first with a little more text each time instead of
|
|
124
|
+
// updating in place. Adopt the id and merge into the existing segment.
|
|
125
|
+
if (last && last.kind === 'text' && last.id === null) {
|
|
126
|
+
last.text = _ccSegMergeText(last.text, text);
|
|
127
|
+
last.id = segmentId;
|
|
128
|
+
return segments;
|
|
129
|
+
}
|
|
117
130
|
segments.push({ kind: 'text', id: segmentId, text: text });
|
|
118
131
|
return segments;
|
|
119
132
|
}
|
|
@@ -876,10 +876,17 @@ function _wiRenderDetail(item) {
|
|
|
876
876
|
// Artifacts — branch (non-navigation) + navigation chips routed through
|
|
877
877
|
// renderArtifactLink so chip click → openArtifact() → modal-stack push +
|
|
878
878
|
// URL hash + browser-Back integration come for free (P-e265cd31).
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
//
|
|
879
|
+
// NOTE (issue #603): item._source is the WI's *project name* (stamped by
|
|
880
|
+
// fetchWorkItemsFromDisk above / used by every action button to route
|
|
881
|
+
// retry/cancel/delete/etc.), NOT a plan/PRD file id. A prior "Source" chip
|
|
882
|
+
// here treated item._source as if it held a genuine source-plan/PRD
|
|
883
|
+
// filename, which rendered a bogus chip labeled with the project name on
|
|
884
|
+
// nearly every work item and 404'd when clicked (there is no plan/PRD
|
|
885
|
+
// named e.g. "src"). The genuine source-plan/PRD case is already fully
|
|
886
|
+
// covered by arts.plan / arts.prd / arts.sourcePlan below, which are
|
|
887
|
+
// populated server-side from the WI's real sourcePlan/_planFileName/
|
|
888
|
+
// planFile fields (engine/queries.js) — no separate item._source-based
|
|
889
|
+
// chip is needed.
|
|
883
890
|
var arts = item._artifacts || {};
|
|
884
891
|
var artPills = '';
|
|
885
892
|
var branchStyle = 'display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:10px;font-size:var(--text-sm);background:var(--surface2);border:1px solid var(--border);color:var(--text);cursor:default';
|
|
@@ -887,15 +894,6 @@ function _wiRenderDetail(item) {
|
|
|
887
894
|
if (arts.plan) artPills += renderArtifactLink({ type: 'plan', id: arts.plan, label: 'Plan' }) + ' ';
|
|
888
895
|
if (arts.prd) artPills += renderArtifactLink({ type: 'prd', id: arts.prd, label: 'PRD' }) + ' ';
|
|
889
896
|
if (arts.sourcePlan) artPills += renderArtifactLink({ type: 'plan', id: arts.sourcePlan, label: 'Source Plan' }) + ' ';
|
|
890
|
-
// Source chip (item._source) — render when it's a file path that isn't
|
|
891
|
-
// already represented in arts.plan / arts.prd / arts.sourcePlan.
|
|
892
|
-
if (item._source && typeof item._source === 'string' && item._source !== 'central') {
|
|
893
|
-
var sourceAlreadyShown = arts.plan === item._source || arts.prd === item._source || arts.sourcePlan === item._source;
|
|
894
|
-
if (!sourceAlreadyShown) {
|
|
895
|
-
var sourceType = item._source.endsWith('.json') ? 'prd' : 'plan';
|
|
896
|
-
artPills += renderArtifactLink({ type: sourceType, id: item._source, label: 'Source', title: item._source }) + ' ';
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
897
|
if (arts.notes && arts.notes.length > 0) {
|
|
900
898
|
arts.notes.forEach(function(n) {
|
|
901
899
|
var noteFile = (n && typeof n === 'object') ? (n.file || n) : String(n || '');
|
package/dashboard/js/settings.js
CHANGED
|
@@ -282,8 +282,8 @@ async function openSettings() {
|
|
|
282
282
|
'<div data-search="' + escHtml(wtModeSearch) + '" style="margin-bottom:6px">' +
|
|
283
283
|
'<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Checkout mode</label>' +
|
|
284
284
|
'<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)">' +
|
|
285
|
-
'<option value="worktree"' + (currentWtMode === 'worktree' ? ' selected' : '') + '>Worktree (default)</option>' +
|
|
286
|
-
'<option value="live"' + (currentWtMode === 'live' ? ' selected' : '') + '>Live checkout</option>' +
|
|
285
|
+
'<option value="worktree" title="Each dispatch runs in its own isolated git worktree. Fully parallel, never touches your working directory."' + (currentWtMode === 'worktree' ? ' selected' : '') + '>Worktree (default)</option>' +
|
|
286
|
+
'<option value="live" title="Agents run in-place in the project folder. One mutating dispatch at a time; refuses to run on a dirty tree."' + (currentWtMode === 'live' ? ' selected' : '') + '>Live checkout</option>' +
|
|
287
287
|
'</select>' +
|
|
288
288
|
'<div data-checkout-mode-chip="' + escHtml(p.name) + '" style="' + (currentWtMode === '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">' +
|
|
289
289
|
'⚠ Live mode: dispatches run directly in this repo\'s checkout. Only one mutating dispatch runs at a time. Dirty working trees block dispatch — commit or stash before running.' +
|
|
@@ -384,7 +384,7 @@ async function openSettings() {
|
|
|
384
384
|
settingsToggle('Auto-reap orphan worktree holders', 'set-autoReapOrphanWorktreeHolders', !!e.autoReapOrphanWorktreeHolders,
|
|
385
385
|
'When ON, the periodic orphan-worktree sweep automatically kills unambiguous orphan `spawn-agent.js` processes holding a stuck worktree\'s cwd (Windows file-lock pattern, e.g. W-mq1k8z6o003acd89 / PID 16828). Safe-reap criteria: cmdline must match spawn-agent.js, reference the worktree basename, AND process age must exceed `engine.agentTimeout * 2`. Default OFF because killing a foreign process is destructive. The holder SCAN runs either way and is appended to the escalation inbox note.') +
|
|
386
386
|
settingsToggle('Live-checkout auto-reset (fleet-wide)', 'set-liveCheckoutAutoReset', !!e.liveCheckoutAutoReset,
|
|
387
|
-
'When ON, a dirty or broken live-checkout (in-place) tree is force-recovered before dispatch via `git fetch origin` + `git reset --hard origin/<branch>` instead of failing the dispatch with non-retryable LIVE_CHECKOUT_DIRTY. This is the FLEET-WIDE fallback; a per-project `liveCheckoutAutoReset` in config.json overrides it. Default
|
|
387
|
+
'When ON, a dirty or broken live-checkout (in-place) tree is force-recovered before dispatch via `git fetch origin` + `git reset --hard origin/<branch>` instead of failing the dispatch with non-retryable LIVE_CHECKOUT_DIRTY. This is the FLEET-WIDE fallback; a per-project `liveCheckoutAutoReset` in config.json overrides it. Default ON — discarding disposable dirt keeps live-checkout branches aligned with origin instead of accumulating auto-save WIP commits; the reset is DESTRUCTIVE (it discards the operator\'s uncommitted changes in the live checkout, and a `live-checkout-autoreset-<workItem>` inbox note records exactly what was discarded for reflog recovery), so set it OFF for live checkouts whose local drift you want preserved. Only affects projects running in live/in-place checkout mode.') +
|
|
388
388
|
'</div>' +
|
|
389
389
|
'<div class="settings-grid-2">' +
|
|
390
390
|
settingsField('CC Session Idle Timeout', 'set-ccWorkerIdleTimeoutMs', Math.round((e.ccWorkerIdleTimeoutMs || 1800000) / 60000), 'minutes', 'How long a persistent `copilot --acp` CC worker stays warm with no activity before the idle reaper kills it. After a reap the next message cold-spawns a FRESH session with no memory of prior turns and CC shows a "context cleared after inactivity" notice. Shorter = less idle memory/process footprint; longer = more context durability across gaps between messages. Only affects the Copilot worker-pool path (CC Worker Pool ON). Clamped 1–480 minutes.') +
|
package/dashboard/styles.css
CHANGED
|
@@ -1288,6 +1288,64 @@
|
|
|
1288
1288
|
.project-mode-live { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid var(--yellow); }
|
|
1289
1289
|
.project-mode-hybrid { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid var(--blue); }
|
|
1290
1290
|
|
|
1291
|
+
/* Clickable checkout-mode pill (W-mr1b67zi0006b788) — swaps the "help"
|
|
1292
|
+
* cursor for "pointer" and adds a hover/focus highlight so it reads as
|
|
1293
|
+
* interactive, opening the picker in _openCheckoutModeMenu(). */
|
|
1294
|
+
.project-mode-pill-clickable { cursor: pointer; }
|
|
1295
|
+
.project-mode-pill-clickable:hover,
|
|
1296
|
+
.project-mode-pill-clickable:focus-visible {
|
|
1297
|
+
filter: brightness(1.2);
|
|
1298
|
+
outline: 1px solid var(--blue);
|
|
1299
|
+
outline-offset: 1px;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/* Checkout-mode picker popover (W-mr1b67zi0006b788). Rendered via
|
|
1303
|
+
* document.createElement in _openCheckoutModeMenu (no innerHTML), appended
|
|
1304
|
+
* to <body> and positioned under the clicked pill. */
|
|
1305
|
+
.checkout-mode-menu {
|
|
1306
|
+
z-index: 1000;
|
|
1307
|
+
min-width: 220px;
|
|
1308
|
+
background: var(--surface2);
|
|
1309
|
+
border: 1px solid var(--border);
|
|
1310
|
+
border-radius: var(--radius-md, 6px);
|
|
1311
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
|
1312
|
+
padding: 6px;
|
|
1313
|
+
font-size: var(--text-sm);
|
|
1314
|
+
}
|
|
1315
|
+
.checkout-mode-menu-title {
|
|
1316
|
+
font-weight: 600; color: var(--muted);
|
|
1317
|
+
padding: 4px 8px 6px; border-bottom: 1px solid var(--border);
|
|
1318
|
+
margin-bottom: 4px;
|
|
1319
|
+
}
|
|
1320
|
+
.checkout-mode-menu-item {
|
|
1321
|
+
padding: 6px 8px; border-radius: 4px; cursor: pointer;
|
|
1322
|
+
color: var(--text);
|
|
1323
|
+
}
|
|
1324
|
+
.checkout-mode-menu-item:hover,
|
|
1325
|
+
.checkout-mode-menu-item:focus {
|
|
1326
|
+
background: var(--surface); outline: none;
|
|
1327
|
+
}
|
|
1328
|
+
.checkout-mode-menu-item-active { color: var(--blue); font-weight: 600; }
|
|
1329
|
+
.checkout-mode-menu-item-desc {
|
|
1330
|
+
font-size: var(--text-xs); color: var(--muted); font-weight: 400;
|
|
1331
|
+
margin-top: 2px; line-height: 1.35;
|
|
1332
|
+
}
|
|
1333
|
+
.checkout-mode-menu-select {
|
|
1334
|
+
width: 100%; margin: 4px 0 8px; padding: 4px 6px;
|
|
1335
|
+
background: var(--surface); color: var(--text); border: 1px solid var(--border);
|
|
1336
|
+
border-radius: 4px;
|
|
1337
|
+
}
|
|
1338
|
+
.checkout-mode-menu-actions {
|
|
1339
|
+
display: flex; justify-content: flex-end; gap: 6px; padding-top: 4px;
|
|
1340
|
+
}
|
|
1341
|
+
.checkout-mode-menu-btn {
|
|
1342
|
+
padding: 4px 10px; border-radius: 4px; border: 1px solid var(--border);
|
|
1343
|
+
background: var(--surface); color: var(--text); cursor: pointer; font-size: var(--text-sm);
|
|
1344
|
+
}
|
|
1345
|
+
.checkout-mode-menu-btn-primary {
|
|
1346
|
+
background: var(--blue); color: #fff; border-color: var(--blue);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1291
1349
|
/* QA tab (W-mpeiwz6k0005bf34-d) — targets / runbooks / runs sections.
|
|
1292
1350
|
* Reuses surface/border/text tokens defined in :root so the QA page
|
|
1293
1351
|
* blends with the rest of the dashboard. */
|
package/dashboard.js
CHANGED
|
@@ -1350,12 +1350,15 @@ function linkPullRequestForTracking({ url, title, project: projectName, contextO
|
|
|
1350
1350
|
// W-mpmwxkzm0009ba0b — Per-row auto-observe toggle backing helper for
|
|
1351
1351
|
// POST /api/pull-requests/observe. Flips canonical `contextOnly` on an
|
|
1352
1352
|
// existing tracked PR record under a lock (per CLAUDE.md mutate convention).
|
|
1353
|
-
// Body shape: { host: 'github'|'ado', slug, number,
|
|
1354
|
-
//
|
|
1355
|
-
// (W-mq5s5ttx000j7ab8-c)
|
|
1353
|
+
// Body shape: { host: 'github'|'ado', slug, number, contextOnly: boolean }.
|
|
1354
|
+
// `contextOnly` is canonical and read first; the legacy `observe` body
|
|
1355
|
+
// param (W-mq5s5ttx000j7ab8-c) is accepted as a documented back-compat
|
|
1356
|
+
// fallback (`contextOnly = !observe`) for any caller that still sends it —
|
|
1357
|
+
// the dashboard client itself now sends `contextOnly` exclusively
|
|
1358
|
+
// (P-2b6e4d81; see docs/deprecated.json#pr-observe-observe-body-param).
|
|
1356
1359
|
// Returns the updated record + the PR path that was touched. Throws an
|
|
1357
1360
|
// Error with `statusCode` for the route handler to map to an HTTP status.
|
|
1358
|
-
function updatePullRequestObserveFlag({ host, slug, number, observe } = {}, config = CONFIG, minionsDir = MINIONS_DIR) {
|
|
1361
|
+
function updatePullRequestObserveFlag({ host, slug, number, observe, contextOnly } = {}, config = CONFIG, minionsDir = MINIONS_DIR) {
|
|
1359
1362
|
const hostStr = String(host || '').trim().toLowerCase();
|
|
1360
1363
|
const slugStr = String(slug || '').trim();
|
|
1361
1364
|
const numberInt = Number.parseInt(number, 10);
|
|
@@ -1374,8 +1377,13 @@ function updatePullRequestObserveFlag({ host, slug, number, observe } = {}, conf
|
|
|
1374
1377
|
err.statusCode = 400;
|
|
1375
1378
|
throw err;
|
|
1376
1379
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1380
|
+
let contextOnlyValue;
|
|
1381
|
+
if (typeof contextOnly === 'boolean') {
|
|
1382
|
+
contextOnlyValue = contextOnly;
|
|
1383
|
+
} else if (typeof observe === 'boolean') {
|
|
1384
|
+
contextOnlyValue = !observe;
|
|
1385
|
+
} else {
|
|
1386
|
+
const err = new Error('contextOnly (or legacy observe) must be a boolean');
|
|
1379
1387
|
err.statusCode = 400;
|
|
1380
1388
|
throw err;
|
|
1381
1389
|
}
|
|
@@ -1394,7 +1402,7 @@ function updatePullRequestObserveFlag({ host, slug, number, observe } = {}, conf
|
|
|
1394
1402
|
shared.mutatePullRequests(prPath, (prs) => {
|
|
1395
1403
|
const pr = prs.find(p => p && p.id === canonicalId);
|
|
1396
1404
|
if (!pr) return prs;
|
|
1397
|
-
pr.contextOnly =
|
|
1405
|
+
pr.contextOnly = contextOnlyValue;
|
|
1398
1406
|
updated = { id: pr.id, contextOnly: pr.contextOnly };
|
|
1399
1407
|
updatedPath = prPath;
|
|
1400
1408
|
return prs;
|
|
@@ -13086,6 +13094,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13086
13094
|
const inputs = [
|
|
13087
13095
|
CONFIG_PATH,
|
|
13088
13096
|
path.join(ENGINE_DIR, 'dispatch.json'),
|
|
13097
|
+
// Central work-items.json at MINIONS_DIR root — backs the 'central'
|
|
13098
|
+
// scope (schedule/pipeline/CC-created rootless WIs, per
|
|
13099
|
+
// work-items-store.js#_filePathForScope). Without this, a new
|
|
13100
|
+
// central-scope WI never busts the ETag until dispatch.json changes.
|
|
13101
|
+
shared.centralWorkItemsPath(MINIONS_DIR),
|
|
13089
13102
|
];
|
|
13090
13103
|
for (const p of projects) {
|
|
13091
13104
|
if (p && p.name) {
|
|
@@ -13106,7 +13119,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13106
13119
|
{ method: 'GET', path: '/api/pull-requests', desc: 'Fully-enriched pull requests (per-project files joined + url backfill + _project stamp)', handler: (req, res) => {
|
|
13107
13120
|
const config = queries.getConfig();
|
|
13108
13121
|
const projects = config.projects || [];
|
|
13109
|
-
const inputs = [
|
|
13122
|
+
const inputs = [
|
|
13123
|
+
CONFIG_PATH,
|
|
13124
|
+
// Central pull-requests.json at MINIONS_DIR root — backs the
|
|
13125
|
+
// 'central' scope per pull-requests-store.js#_filePathForScope.
|
|
13126
|
+
// Mirrors the same class of staleness fix as /api/work-items.
|
|
13127
|
+
shared.centralPullRequestsPath(MINIONS_DIR),
|
|
13128
|
+
];
|
|
13110
13129
|
for (const p of projects) {
|
|
13111
13130
|
if (p && p.name) inputs.push(path.join(MINIONS_DIR, 'projects', p.name, 'pull-requests.json'));
|
|
13112
13131
|
}
|
|
@@ -13137,6 +13156,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13137
13156
|
const inputs = [
|
|
13138
13157
|
path.join(ENGINE_DIR, 'metrics.json'),
|
|
13139
13158
|
path.join(ENGINE_DIR, 'dispatch.json'),
|
|
13159
|
+
// getMetrics() enriches from getPullRequests(), which includes the
|
|
13160
|
+
// central pull-requests.json scope — include it here too so a new
|
|
13161
|
+
// central-scope PR busts the ETag (same class of bug as /api/work-items).
|
|
13162
|
+
shared.centralPullRequestsPath(MINIONS_DIR),
|
|
13140
13163
|
];
|
|
13141
13164
|
for (const p of projects) {
|
|
13142
13165
|
if (p && p.name) inputs.push(path.join(MINIONS_DIR, 'projects', p.name, 'pull-requests.json'));
|
|
@@ -13721,7 +13744,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
13721
13744
|
}
|
|
13722
13745
|
}},
|
|
13723
13746
|
|
|
13724
|
-
{ method: 'POST', path: '/api/pull-requests/observe', desc: 'Toggle canonical contextOnly flag on a tracked PR (
|
|
13747
|
+
{ method: 'POST', path: '/api/pull-requests/observe', desc: 'Toggle canonical contextOnly flag on a tracked PR (legacy `observe` body param is preserved as the inverse for backward compat)', params: 'host (github|ado), slug, number, contextOnly (boolean; legacy observe (boolean) also accepted)', handler: async (req, res) => {
|
|
13725
13748
|
const body = await readBody(req);
|
|
13726
13749
|
reloadConfig();
|
|
13727
13750
|
try {
|