@yemi33/minions 0.1.2287 → 0.1.2289

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.
@@ -189,8 +189,9 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems, opts) {
189
189
  if (prdJsonStatus === 'rejected') return 'rejected';
190
190
  if (prdJsonStatus === 'paused' && !allDone) return 'paused';
191
191
  if (prdJsonStatus === 'revision-requested') return 'revision-requested';
192
- // PRD awaiting approval overrides WI-derived completionnew items may need materialization (#970)
193
- if (prdJsonStatus === 'awaiting-approval') return 'awaiting-approval';
192
+ // PRD awaiting approval overrides only when no work has started yet if work is active or
193
+ // done, the approval gate was already implicitly cleared and the status is stale (#970).
194
+ if (prdJsonStatus === 'awaiting-approval' && !hasActiveWork && !allDone) return 'awaiting-approval';
194
195
 
195
196
  // Derive from work item progress
196
197
  if (allDone && !hasActiveWork) return 'completed';
@@ -268,6 +269,10 @@ function renderPlans(plans) {
268
269
  for (const p of plans) {
269
270
  if (p.format === 'prd' && !p.archived && p.sourcePlan) {
270
271
  const sourceKeys = [p.sourcePlan, normalizeSourcePlanKey(p.sourcePlan)];
272
+ // Also register the bare basename without directory prefix ('plans/foo.md' → 'foo.md')
273
+ // so that plans whose p.file has no prefix can still find their PRD.
274
+ const bare = (p.sourcePlan || '').replace(/^[^/]+\//, '');
275
+ if (bare && bare !== p.sourcePlan) sourceKeys.push(bare);
271
276
  for (const sourceKey of sourceKeys) {
272
277
  if (!sourceKey) continue;
273
278
  planToPrdFile[sourceKey] = p.file;
@@ -410,6 +410,8 @@ async function openSettings() {
410
410
  // OFF because killing a foreign process is destructive.
411
411
  settingsToggle('Auto-reap orphan worktree holders', 'set-autoReapOrphanWorktreeHolders', !!e.autoReapOrphanWorktreeHolders,
412
412
  '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.') +
413
+ settingsToggle('Live-checkout auto-reset (fleet-wide)', 'set-liveCheckoutAutoReset', !!e.liveCheckoutAutoReset,
414
+ '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 OFF because the reset is DESTRUCTIVE — it discards the operator\'s uncommitted changes in the live checkout (a `live-checkout-autoreset-<workItem>` inbox note records exactly what was discarded for reflog recovery). Only affects projects running in live/in-place checkout mode.') +
413
415
  '</div>' +
414
416
  '<div class="settings-grid-2">' +
415
417
  settingsField('Worktree Create Timeout', 'set-worktreeCreateTimeout', e.worktreeCreateTimeout || 300000, 'ms', 'Timeout for git worktree add (increase for large repos/Windows)') +
@@ -1082,6 +1084,7 @@ async function saveSettings() {
1082
1084
  autoCompletePrs: document.getElementById('set-autoCompletePrs').checked,
1083
1085
  ccUseWorkerPool: !!document.getElementById('set-ccUseWorkerPool')?.checked,
1084
1086
  autoReapOrphanWorktreeHolders: !!document.getElementById('set-autoReapOrphanWorktreeHolders')?.checked,
1087
+ liveCheckoutAutoReset: !!document.getElementById('set-liveCheckoutAutoReset')?.checked,
1085
1088
  orphanHolderScanTimeoutMs: document.getElementById('set-orphanHolderScanTimeoutMs')?.value,
1086
1089
  statusProbeKillTimeoutMs: document.getElementById('set-statusProbeKillTimeoutMs')?.value,
1087
1090
  adoPollEnabled: document.getElementById('set-adoPollEnabled').checked,
@@ -89,6 +89,10 @@
89
89
  <div class="cockpit-label"><span class="cockpit-dot"></span> Engine</div>
90
90
  <div class="cockpit-value dim">—</div>
91
91
  <div class="cockpit-detail">checking&hellip;</div>
92
+ <!-- Shown by status.js only when the engine is down/stale. Mirrors the
93
+ classic dashboard's stale-banner restart action; both surfaces POST
94
+ /api/engine/restart. -->
95
+ <button id="slim-engine-restart" class="engine-restart-btn" type="button" hidden>Restart engine</button>
92
96
  </div>
93
97
  <div class="cockpit-tile" data-tile="queued">
94
98
  <div class="cockpit-label"><span class="cockpit-dot"></span> Queued work</div>
@@ -124,17 +128,6 @@
124
128
  </div>
125
129
  </div>
126
130
  </div>
127
- <!-- Pending work items sub-section: lists work items with
128
- status === 'pending'. Populated by renderPendingWorkItems() in
129
- status.js from the /api/work-items poll slice; hidden via the
130
- `hidden` attribute when there are zero pending items. -->
131
- <div class="pending-section" id="pending-section" hidden>
132
- <div class="pending-head">
133
- Pending Work Items
134
- <span class="pending-count" id="pending-count"></span>
135
- </div>
136
- <div class="pending-list" id="pending-list"></div>
137
- </div>
138
131
  </div>
139
132
  </div>
140
133
 
@@ -189,6 +189,71 @@
189
189
  body.appendChild(section);
190
190
  }
191
191
 
192
+ // Collapsible "Pending work items" section appended to the queued popout
193
+ // (W-mqv1xbcm). Sources data.workItems filtered to status === 'pending' — the
194
+ // same /api/work-items slice the slim poll merges onto the snapshot — so it
195
+ // adds no new API calls and refreshes whenever the popout is re-opened.
196
+ // Expanded by DEFAULT (more prominent than the collapsed Completed section)
197
+ // since these are the queued WIs the operator most wants to scan. Mirrors
198
+ // renderCompletedSection's structure + tileCard usage; all text via
199
+ // textContent (no innerHTML) to keep the SEC-03 baseline clean. Each card
200
+ // shows title, id, project (project || _source), type, and the engine's
201
+ // _pendingReason as a chip when set.
202
+ function renderPendingSection(body, data) {
203
+ var pending = (Array.isArray(data.workItems) ? data.workItems : []).filter(function(w) {
204
+ return w && w.status === 'pending';
205
+ });
206
+
207
+ var section = document.createElement('div');
208
+ section.className = 'tile-completed-section';
209
+
210
+ var head = document.createElement('div');
211
+ head.className = 'tile-completed-section-head';
212
+ head.setAttribute('role', 'button');
213
+ head.setAttribute('tabindex', '0');
214
+ head.setAttribute('aria-expanded', 'true');
215
+ var caret = document.createElement('span');
216
+ caret.className = 'tile-caret';
217
+ caret.textContent = '▾';
218
+ var label = document.createElement('span');
219
+ label.className = 'tile-completed-section-title';
220
+ label.textContent = 'Pending work items (' + pending.length + ')';
221
+ head.appendChild(caret);
222
+ head.appendChild(label);
223
+
224
+ var listWrap = document.createElement('div');
225
+ listWrap.className = 'tile-completed-section-body';
226
+
227
+ if (!pending.length) {
228
+ tileEmpty(listWrap, 'No pending work items.');
229
+ } else {
230
+ pending.forEach(function(w) {
231
+ var project = w.project || w._source;
232
+ var meta = [w.id || null, project || null, w.type || null].filter(Boolean).join(' · ');
233
+ var card = { title: w.title || w.id || '(untitled)', meta: meta };
234
+ if (w._pendingReason) {
235
+ card.chip = { text: String(w._pendingReason).replace(/_/g, ' '), cls: 'amber' };
236
+ }
237
+ listWrap.appendChild(tileCard(card));
238
+ });
239
+ }
240
+
241
+ function toggle() {
242
+ var open = !section.classList.toggle('collapsed');
243
+ head.setAttribute('aria-expanded', open ? 'true' : 'false');
244
+ caret.textContent = open ? '▾' : '▸';
245
+ listWrap.hidden = !open;
246
+ }
247
+ head.addEventListener('click', toggle);
248
+ head.addEventListener('keydown', function(ev) {
249
+ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); toggle(); }
250
+ });
251
+
252
+ section.appendChild(head);
253
+ section.appendChild(listWrap);
254
+ body.appendChild(section);
255
+ }
256
+
192
257
  function renderPrTileBody(body, data) {
193
258
  // Emoji glyphs make build/review status scannable at a glance; the
194
259
  // accompanying label keeps the row readable when the glyph alone is
@@ -310,7 +375,7 @@
310
375
  var TILE_VIEWS = {
311
376
  engine: { title: 'Engine', render: renderEngineTileBody },
312
377
  dispatches: { title: 'Active dispatches', render: function(body, data) { renderDispatchTileBody(body, data, true); } },
313
- queued: { title: 'Queued work', render: function(body, data) { renderDispatchTileBody(body, data, false); renderCompletedSection(body, data); } },
378
+ queued: { title: 'Queued work', render: function(body, data) { renderDispatchTileBody(body, data, false); renderPendingSection(body, data); renderCompletedSection(body, data); } },
314
379
  prs: { title: 'Pull requests', render: renderPrTileBody },
315
380
  watches: { title: 'Watches', render: renderWatchTileBody },
316
381
  };
@@ -54,6 +54,68 @@
54
54
  // Latest /api/status snapshot — read by the cockpit-tile detail modals.
55
55
  var lastStatusData = null;
56
56
 
57
+ // ── Engine restart affordance ──────────────────────────────────
58
+ // Shows a "Restart engine" button on the engine tile when the engine reports
59
+ // down/stale and calls the same POST /api/engine/restart endpoint the classic
60
+ // dashboard uses (dashboard/js/render-dispatch.js#_wireEngineRestartClick).
61
+ // Optimistic UX parity: disable while pending, re-enable + alert on error,
62
+ // re-poll on success (the button hides itself once the engine reports
63
+ // 'running' again). engineRestartPending guards the in-flight label so the
64
+ // 5s status poll doesn't stomp "Restarting…" back to the idle text.
65
+ var engineRestartPending = false;
66
+
67
+ function updateEngineRestartButton(engineDown) {
68
+ var btn = document.getElementById('slim-engine-restart');
69
+ if (!btn) return;
70
+ if (!engineDown) {
71
+ // Engine recovered — clear pending state and hide the button.
72
+ engineRestartPending = false;
73
+ btn.hidden = true;
74
+ btn.disabled = false;
75
+ btn.textContent = 'Restart engine';
76
+ return;
77
+ }
78
+ btn.hidden = false;
79
+ if (!engineRestartPending) {
80
+ btn.disabled = false;
81
+ btn.textContent = 'Restart engine';
82
+ }
83
+ }
84
+
85
+ async function onEngineRestartClick(ev) {
86
+ if (ev) ev.stopPropagation(); // don't trigger the engine tile's detail modal
87
+ var btn = document.getElementById('slim-engine-restart');
88
+ if (!btn || engineRestartPending) return;
89
+ engineRestartPending = true;
90
+ btn.disabled = true;
91
+ btn.textContent = 'Restarting…';
92
+ try {
93
+ var res = await fetch('/api/engine/restart', { method: 'POST' });
94
+ var data = await res.json().catch(function() { return {}; });
95
+ if (res.ok && data.ok) {
96
+ // Re-poll sooner than the 5s cadence so the engine tile flips to
97
+ // 'running' (and the button auto-hides) shortly after the new PID's
98
+ // first heartbeat. Ongoing 5s polling catches it regardless.
99
+ scheduleStatusRefresh(1500);
100
+ } else {
101
+ engineRestartPending = false;
102
+ btn.disabled = false;
103
+ btn.textContent = 'Restart engine';
104
+ alert('Engine restart failed: ' + (data.error || ('HTTP ' + res.status)));
105
+ }
106
+ } catch (e) {
107
+ engineRestartPending = false;
108
+ btn.disabled = false;
109
+ btn.textContent = 'Restart engine';
110
+ alert('Engine restart failed: ' + (e && e.message ? e.message : 'unknown error'));
111
+ }
112
+ }
113
+
114
+ (function bindEngineRestart() {
115
+ var btn = document.getElementById('slim-engine-restart');
116
+ if (btn) btn.addEventListener('click', onEngineRestartClick);
117
+ })();
118
+
57
119
  // Reflect the Command Center's resolved runtime + model in the Actions panel
58
120
  // header (slim-ux only — classic chrome is untouched). Source is the same
59
121
  // /api/status payload the cockpit polls: autoMode.ccModel (config.engine
@@ -116,12 +178,20 @@
116
178
 
117
179
  updateTile('engine', engineValue, engineDetail, engineLit);
118
180
 
181
+ // Surface a "Restart engine" button on the engine tile whenever the engine
182
+ // is down/unreachable (stopped or stale heartbeat) — mirrors the classic
183
+ // dashboard's stale/stopped restart affordance
184
+ // (dashboard/js/render-dispatch.js#_wireEngineRestartClick). Both front-ends
185
+ // are separate IIFE bundles never loaded together, so the reused unit is the
186
+ // POST /api/engine/restart contract, consistent with the rest of slim (e.g.
187
+ // command-send.js reuses /api/command-center/* rather than the classic fn).
188
+ updateEngineRestartButton(engineState === 'stopped' || engineState === 'stale');
189
+
119
190
  // ── Dispatches ─────────────────────────────────────────────
120
191
  // The per-minion "working" view moved to the Team cards (renderMembers);
121
192
  // this section only drives the system tiles now.
122
193
  var dispatch = data.dispatch || {};
123
194
  var active = Array.isArray(dispatch.active) ? dispatch.active : [];
124
- var pending = Array.isArray(dispatch.pending) ? dispatch.pending : [];
125
195
  var completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
126
196
  updateTile(
127
197
  'dispatches',
@@ -129,11 +199,19 @@
129
199
  active.length ? 'agents working' : 'no agents working',
130
200
  active.length ? 'amber' : null
131
201
  );
202
+ // "Queued work" reflects ALL work items waiting to be dispatched — both
203
+ // status 'pending' (not yet picked up) and status 'queued' (W-mqveaj1h).
204
+ // Sourced from the /api/work-items poll slice merged in by history.js, not
205
+ // the dispatch queue, so a pending WI that has not produced a dispatch yet
206
+ // is still counted. Supersedes the standalone "Pending Work Items" section.
207
+ var waitingItems = (Array.isArray(data.workItems) ? data.workItems : []).filter(function(w) {
208
+ return w && (w.status === 'pending' || w.status === 'queued');
209
+ });
132
210
  updateTile(
133
211
  'queued',
134
- pending.length,
135
- pending.length ? 'waiting to spawn' : 'queue empty',
136
- pending.length ? 'orange' : null
212
+ waitingItems.length,
213
+ waitingItems.length ? 'waiting to spawn' : 'queue empty',
214
+ waitingItems.length ? 'orange' : null
137
215
  );
138
216
 
139
217
  // ── PR tile (status snapshot from cached pull-requests) ─────
@@ -179,11 +257,6 @@
179
257
  // ── Team member cards ──────────────────────────────────────
180
258
  renderMembers(Array.isArray(data.agents) ? data.agents : []);
181
259
 
182
- // ── Pending work items section ─────────────────────────────
183
- // Surfaces WIs with status === 'pending' (W-mqtx516q). Sourced from the
184
- // /api/work-items slice merged in by the slim poll (history.js).
185
- renderPendingWorkItems(Array.isArray(data.workItems) ? data.workItems : []);
186
-
187
260
  // ── Stamps + history ───────────────────────────────────────
188
261
  var stamp = document.getElementById('cockpit-stamp');
189
262
  if (stamp) stamp.textContent = 'updated ' + relTime(data.timestamp || Date.now());
@@ -193,94 +266,5 @@
193
266
  if (hStamp) hStamp.textContent = relTime(data.timestamp || Date.now());
194
267
  }
195
268
 
196
- // ── Pending work items renderer ────────────────────────────────
197
- // Lists every work item with status === 'pending' in a dedicated section
198
- // under the cockpit (W-mqtx516q). The section is hidden via the `hidden`
199
- // attribute when there are zero pending items. Each row shows id, title,
200
- // project, type, and the engine's _pendingReason as a small badge when set.
201
- // Everything goes through textContent (no innerHTML) to keep the SEC-03
202
- // baseline clean. A JSON fingerprint of the visible fields skips the DOM
203
- // rebuild when nothing changed between 5s polls.
204
- var _lastPendingJson = null;
205
-
206
- function renderPendingWorkItems(items) {
207
- var section = document.getElementById('pending-section');
208
- var list = document.getElementById('pending-list');
209
- if (!section || !list) return;
210
-
211
- var pending = (Array.isArray(items) ? items : []).filter(function(w) {
212
- return w && w.status === 'pending';
213
- });
214
-
215
- var fingerprint = JSON.stringify(pending.map(function(w) {
216
- return [w.id, w.title, w.project || w._source, w.type, w._pendingReason];
217
- }));
218
- if (fingerprint === _lastPendingJson) return;
219
- _lastPendingJson = fingerprint;
220
-
221
- if (!pending.length) {
222
- section.setAttribute('hidden', '');
223
- list.textContent = '';
224
- return;
225
- }
226
- section.removeAttribute('hidden');
227
-
228
- var countEl = document.getElementById('pending-count');
229
- if (countEl) countEl.textContent = String(pending.length);
230
-
231
- list.textContent = '';
232
- var frag = document.createDocumentFragment();
233
- pending.forEach(function(w) {
234
- var row = document.createElement('div');
235
- row.className = 'pending-row';
236
-
237
- // Top line: title + optional pending-reason badge.
238
- var main = document.createElement('div');
239
- main.className = 'pending-row-main';
240
-
241
- var title = document.createElement('span');
242
- title.className = 'pending-title';
243
- title.textContent = w.title || w.id || '(untitled)';
244
- title.title = (w.title || w.id || '') + (w.id ? ' (' + w.id + ')' : '');
245
- main.appendChild(title);
246
-
247
- if (w._pendingReason) {
248
- var badge = document.createElement('span');
249
- badge.className = 'pending-reason-badge';
250
- badge.textContent = String(w._pendingReason).replace(/_/g, ' ');
251
- badge.title = 'Pending reason: ' + w._pendingReason;
252
- main.appendChild(badge);
253
- }
254
- row.appendChild(main);
255
-
256
- // Meta line: id + project + type chips.
257
- var meta = document.createElement('div');
258
- meta.className = 'pending-row-meta';
259
-
260
- var idEl = document.createElement('span');
261
- idEl.className = 'pending-meta-id';
262
- idEl.textContent = w.id || '';
263
- meta.appendChild(idEl);
264
-
265
- var project = w.project || w._source;
266
- if (project) {
267
- var projEl = document.createElement('span');
268
- projEl.className = 'pending-meta-chip';
269
- projEl.textContent = project;
270
- meta.appendChild(projEl);
271
- }
272
-
273
- if (w.type) {
274
- var typeEl = document.createElement('span');
275
- typeEl.className = 'pending-meta-chip pending-meta-type';
276
- typeEl.textContent = w.type;
277
- meta.appendChild(typeEl);
278
- }
279
- row.appendChild(meta);
280
-
281
- frag.appendChild(row);
282
- });
283
- list.appendChild(frag);
284
- }
285
269
 
286
270
 
@@ -1100,6 +1100,19 @@
1100
1100
  margin-left: auto;
1101
1101
  text-align: right;
1102
1102
  }
1103
+ /* Restart-engine affordance on the engine tile — toggled visible via the
1104
+ `hidden` attribute in status.js only when the engine reports down/stale.
1105
+ Mirrors the classic dashboard's stale-banner "Restart engine" action
1106
+ (dashboard/js/render-dispatch.js); both POST /api/engine/restart. */
1107
+ .engine-restart-btn {
1108
+ font-size: var(--text-base); font-weight: 600;
1109
+ padding: 2px 10px; border-radius: 10px;
1110
+ background: rgba(248, 81, 73, 0.12); color: var(--red); border: 1px solid var(--red);
1111
+ cursor: pointer; font-family: inherit; white-space: nowrap;
1112
+ }
1113
+ .engine-restart-btn:hover { filter: brightness(1.2); }
1114
+ .engine-restart-btn:disabled { opacity: 0.6; cursor: default; }
1115
+ .engine-restart-btn[hidden] { display: none; }
1103
1116
  /* Cockpit tiles are clickable -> detail modal. */
1104
1117
  .cockpit-tile { cursor: pointer; position: relative; }
1105
1118
  .cockpit-tile:hover { border-color: var(--blue); }
@@ -1139,93 +1152,6 @@
1139
1152
  .team-section { margin-bottom: 14px; }
1140
1153
  .cockpit-section { padding-top: 14px; border-top: 1px solid var(--border); }
1141
1154
 
1142
- /* Pending work items section (W-mqtx516q). Sits under the cockpit tiles;
1143
- hidden via the `hidden` attribute when there are no pending WIs. */
1144
- .pending-section {
1145
- padding-top: 14px;
1146
- margin-top: 14px;
1147
- border-top: 1px solid var(--border);
1148
- }
1149
- .pending-section[hidden] { display: none; }
1150
- .pending-head {
1151
- font-size: var(--text-sm);
1152
- font-weight: 600;
1153
- color: var(--text);
1154
- letter-spacing: 0.02em;
1155
- text-transform: uppercase;
1156
- margin-bottom: 8px;
1157
- display: flex;
1158
- align-items: center;
1159
- gap: 6px;
1160
- }
1161
- .pending-count {
1162
- font-size: var(--text-xs);
1163
- font-weight: 600;
1164
- color: var(--orange);
1165
- background: rgba(214, 153, 34, 0.14);
1166
- border-radius: 9px;
1167
- padding: 0 7px;
1168
- line-height: 16px;
1169
- }
1170
- .pending-list {
1171
- display: flex;
1172
- flex-direction: column;
1173
- gap: 6px;
1174
- }
1175
- .pending-row {
1176
- background: var(--surface2);
1177
- border: 1px solid var(--border);
1178
- border-left: 3px solid var(--orange);
1179
- border-radius: var(--radius);
1180
- padding: 7px 9px;
1181
- }
1182
- .pending-row-main {
1183
- display: flex;
1184
- align-items: center;
1185
- gap: 6px;
1186
- }
1187
- .pending-title {
1188
- font-size: var(--text-md);
1189
- color: var(--text);
1190
- overflow: hidden;
1191
- text-overflow: ellipsis;
1192
- white-space: nowrap;
1193
- min-width: 0;
1194
- flex: 1;
1195
- }
1196
- .pending-reason-badge {
1197
- flex: none;
1198
- font-size: var(--text-xs);
1199
- color: var(--orange);
1200
- border: 1px solid rgba(214, 153, 34, 0.5);
1201
- border-radius: 6px;
1202
- padding: 0 5px;
1203
- line-height: 15px;
1204
- white-space: nowrap;
1205
- }
1206
- .pending-row-meta {
1207
- display: flex;
1208
- align-items: center;
1209
- flex-wrap: wrap;
1210
- gap: 6px;
1211
- margin-top: 4px;
1212
- }
1213
- .pending-meta-id {
1214
- font-size: var(--text-xs);
1215
- color: var(--muted);
1216
- font-family: var(--mono, monospace);
1217
- }
1218
- .pending-meta-chip {
1219
- font-size: var(--text-xs);
1220
- color: var(--muted);
1221
- background: var(--surface);
1222
- border: 1px solid var(--border);
1223
- border-radius: 6px;
1224
- padding: 0 6px;
1225
- line-height: 15px;
1226
- }
1227
- .pending-meta-type { color: var(--blue); }
1228
-
1229
1155
  /* Square member cards. Auto-fill so the column count adapts to the
1230
1156
  panel width; aspect-ratio keeps each card square regardless. */
1231
1157
  .member-grid {