@yemi33/minions 0.1.2305 → 0.1.2307
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 +1 -0
- 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 +30 -3
- package/dashboard/slim/body.html +6 -22
- package/dashboard/slim/js/modals-tiles.js +32 -22
- package/dashboard/slim/js/plans.js +24 -516
- package/dashboard/slim/styles.css +10 -7
- package/dashboard/styles.css +58 -0
- package/dashboard.js +66 -9
- package/docs/completion-reports.md +27 -2
- package/docs/copilot-cli-schema.md +31 -1
- package/docs/design-state-storage.md +1 -1
- package/docs/harness-transparency.md +18 -0
- package/docs/live-checkout-mode.md +57 -8
- package/engine/ado-comment.js +5 -2
- package/engine/ado.js +37 -0
- package/engine/cli.js +58 -3
- package/engine/comment-format.js +82 -2
- package/engine/consolidation.js +59 -2
- package/engine/discover-project-skills.js +4 -0
- package/engine/dispatch.js +36 -1
- package/engine/gh-comment.js +8 -3
- package/engine/lifecycle.js +241 -25
- package/engine/live-checkout.js +335 -7
- package/engine/playbook.js +2 -2
- package/engine/pre-dispatch-eval.js +54 -5
- package/engine/queries.js +11 -7
- package/engine/shared.js +96 -6
- package/engine/supervisor.js +144 -19
- package/engine/watchdog.js +10 -0
- package/engine.js +210 -5
- package/package.json +1 -1
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +9 -0
package/bin/minions.js
CHANGED
|
@@ -1580,6 +1580,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1580
1580
|
isPortListening,
|
|
1581
1581
|
confirmPortUp: tcpPortAccepts,
|
|
1582
1582
|
isStopIntentSet: shared.isStopIntentSet || (() => false),
|
|
1583
|
+
recordRespawn: shared.recordEngineRespawn,
|
|
1583
1584
|
}).then(result => {
|
|
1584
1585
|
// ALWAYS exit 0 — the scheduler must never see a failure for the
|
|
1585
1586
|
// recovery we're trying to make boring. The `result` is logged by tick
|
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,13 +282,30 @@ 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.' +
|
|
290
290
|
'</div>' +
|
|
291
291
|
'</div>';
|
|
292
|
+
// W-mqtvnnj1000357fa — per-project live-checkout auto-stash override.
|
|
293
|
+
// Tri-state: "" (use fleet default → engine.liveCheckoutAutoStash), "on"
|
|
294
|
+
// (force stash), "off" (force fail-on-dirty). Only an explicit on/off
|
|
295
|
+
// overrides the fleet setting; the default option leaves p.liveCheckoutAutoStash
|
|
296
|
+
// unset so resolveLiveCheckoutAutoStash falls back to engine.
|
|
297
|
+
var autoStashRaw = (p.liveCheckoutAutoStash === true) ? 'on' : (p.liveCheckoutAutoStash === false) ? 'off' : '';
|
|
298
|
+
var autoStashSearch = 'live checkout auto-stash dirty tree stash dispatch';
|
|
299
|
+
var autoStashBlock =
|
|
300
|
+
'<div data-search="' + escHtml(autoStashSearch) + '" style="margin-bottom:6px">' +
|
|
301
|
+
'<label style="font-size:var(--text-sm);color:var(--muted);display:block;margin-bottom:2px">Live-checkout auto-stash</label>' +
|
|
302
|
+
'<select id="set-liveCheckoutAutoStash-' + 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)">' +
|
|
303
|
+
'<option value=""' + (autoStashRaw === '' ? ' selected' : '') + '>Use fleet default</option>' +
|
|
304
|
+
'<option value="on"' + (autoStashRaw === 'on' ? ' selected' : '') + '>On — auto-stash dirty tree</option>' +
|
|
305
|
+
'<option value="off"' + (autoStashRaw === 'off' ? ' selected' : '') + '>Off — fail on dirty tree</option>' +
|
|
306
|
+
'</select>' +
|
|
307
|
+
'<div style="font-size:var(--text-xs);color:var(--muted);margin-top:2px;line-height:1.4">Live mode only. On = `git stash push --include-untracked` a dirty tree before dispatch instead of failing; the engine never pops the stash (run `git stash pop` manually). Overrides the fleet-wide setting under Worktrees.</div>' +
|
|
308
|
+
'</div>';
|
|
292
309
|
return '<div data-settings-project="' + escHtml(p.name) + '" data-search="project ' + escHtml(p.name.toLowerCase()) + '" style="border:1px solid var(--border);border-radius:6px;padding:10px 12px;margin-bottom:12px">' +
|
|
293
310
|
'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">' +
|
|
294
311
|
'<div style="font-size:var(--text-md);font-weight:600">' + escHtml(p.name) + '</div>' +
|
|
@@ -297,6 +314,7 @@ async function openSettings() {
|
|
|
297
314
|
pathRow +
|
|
298
315
|
branchGrid +
|
|
299
316
|
worktreeModeBlock +
|
|
317
|
+
autoStashBlock +
|
|
300
318
|
driftNote +
|
|
301
319
|
'<div style="display:flex;flex-direction:column;gap:6px;margin-top:8px">' +
|
|
302
320
|
settingsToggle('Discover from PRs', 'set-ws-prs-' + p.name, p.workSources.pullRequests.enabled, 'Discovery gate: scan repo for open PRs and surface them as review tasks. Independent of ADO/GitHub polling — does not affect already-tracked PRs.') +
|
|
@@ -384,7 +402,7 @@ async function openSettings() {
|
|
|
384
402
|
settingsToggle('Auto-reap orphan worktree holders', 'set-autoReapOrphanWorktreeHolders', !!e.autoReapOrphanWorktreeHolders,
|
|
385
403
|
'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
404
|
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
|
|
405
|
+
'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
406
|
'</div>' +
|
|
389
407
|
'<div class="settings-grid-2">' +
|
|
390
408
|
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.') +
|
|
@@ -1050,6 +1068,7 @@ async function saveSettings() {
|
|
|
1050
1068
|
autoCompletePrs: document.getElementById('set-autoCompletePrs').checked,
|
|
1051
1069
|
ccUseWorkerPool: !!document.getElementById('set-ccUseWorkerPool')?.checked,
|
|
1052
1070
|
autoReapOrphanWorktreeHolders: !!document.getElementById('set-autoReapOrphanWorktreeHolders')?.checked,
|
|
1071
|
+
liveCheckoutAutoStash: !!document.getElementById('set-liveCheckoutAutoStash')?.checked,
|
|
1053
1072
|
liveCheckoutAutoReset: !!document.getElementById('set-liveCheckoutAutoReset')?.checked,
|
|
1054
1073
|
orphanHolderScanTimeoutMs: document.getElementById('set-orphanHolderScanTimeoutMs')?.value,
|
|
1055
1074
|
statusProbeKillTimeoutMs: document.getElementById('set-statusProbeKillTimeoutMs')?.value,
|
|
@@ -1155,10 +1174,18 @@ async function saveSettings() {
|
|
|
1155
1174
|
// values.
|
|
1156
1175
|
const wtModeInput = document.getElementById('set-checkoutMode-' + p.name);
|
|
1157
1176
|
const wtModeValue = (wtModeInput && wtModeInput.value === 'live') ? 'live' : 'worktree';
|
|
1177
|
+
// W-mqtvnnj1000357fa — per-project live-checkout auto-stash override.
|
|
1178
|
+
// '' → null (clear override, fall back to engine fleet setting); 'on' →
|
|
1179
|
+
// true; 'off' → false. The server deletes the field on null and validates
|
|
1180
|
+
// booleans.
|
|
1181
|
+
const autoStashInput = document.getElementById('set-liveCheckoutAutoStash-' + p.name);
|
|
1182
|
+
const autoStashRaw = autoStashInput ? autoStashInput.value : '';
|
|
1183
|
+
const autoStashValue = autoStashRaw === 'on' ? true : autoStashRaw === 'off' ? false : null;
|
|
1158
1184
|
return {
|
|
1159
1185
|
name: p.name,
|
|
1160
1186
|
mainBranch: mainBranchValue || null,
|
|
1161
1187
|
checkoutMode: wtModeValue,
|
|
1188
|
+
liveCheckoutAutoStash: autoStashValue,
|
|
1162
1189
|
workSources: {
|
|
1163
1190
|
pullRequests: { enabled: document.getElementById('set-ws-prs-' + p.name)?.checked ?? true },
|
|
1164
1191
|
workItems: { enabled: document.getElementById('set-ws-wi-' + p.name)?.checked ?? true }
|
package/dashboard/slim/body.html
CHANGED
|
@@ -226,28 +226,12 @@
|
|
|
226
226
|
</div>
|
|
227
227
|
</div>
|
|
228
228
|
|
|
229
|
-
<!-- Plans + PRD control panel —
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
archive,delete,unarchive}).
|
|
236
|
-
PRD — the .json entries from /api/plans plus /api/prd and the
|
|
237
|
-
read-only subset of /api/prd-items. -->
|
|
238
|
-
<div class="modal-bg" id="slim-plans-modal">
|
|
239
|
-
<div class="modal slim-plans-modal-inner">
|
|
240
|
-
<div class="modal-header">
|
|
241
|
-
<h3>Plans</h3>
|
|
242
|
-
<div class="kn-tabs" id="slim-plans-tabs" role="tablist">
|
|
243
|
-
<button class="kn-tab active" id="slim-plans-tab-plans" data-plans-tab="plans" type="button" role="tab" aria-selected="true" aria-controls="slim-plans-body">Plans</button>
|
|
244
|
-
<button class="kn-tab" id="slim-plans-tab-prd" data-plans-tab="prd" type="button" role="tab" aria-selected="false" aria-controls="slim-plans-body">PRD</button>
|
|
245
|
-
</div>
|
|
246
|
-
<button id="slim-plans-close" class="icon-btn" title="Close" style="margin-left:auto">×</button>
|
|
247
|
-
</div>
|
|
248
|
-
<div class="modal-body" id="slim-plans-body" role="tabpanel" tabindex="0" aria-labelledby="slim-plans-tab-plans"></div>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
229
|
+
<!-- Plans + PRD control panel — the "Plans" cockpit tile opens the LITERAL
|
|
230
|
+
classic /plans screen in a chrome-off iframe (/plans?embed=1), routed through
|
|
231
|
+
the standard TILE_VIEWS path in modals-tiles.js#renderPlansBody. There is no
|
|
232
|
+
slim-specific Plans/PRD flyout markup anymore (W-mr28ko750010199f / "reuse,
|
|
233
|
+
don't fork"): the classic /plans screen is the single source of truth for the
|
|
234
|
+
Plans + PRD tabs and every lifecycle action. -->
|
|
251
235
|
|
|
252
236
|
<!-- Pin editor — create (Pin Content action / "+ Pin") or edit an existing
|
|
253
237
|
note. Submits to POST /api/pinned (create) or /api/pinned/update (edit). -->
|