@yemi33/minions 0.1.152 → 0.1.154

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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.154 (2026-04-02)
4
+
5
+ ### Engine
6
+ - engine.js
7
+ - engine/lifecycle.js
8
+ - engine/playbook.js
9
+ - engine/queries.js
10
+
11
+ ### Dashboard
12
+ - dashboard.html
13
+ - dashboard.js
14
+ - dashboard/js/command-center.js
15
+ - dashboard/js/modal-qa.js
16
+ - dashboard/js/modal.js
17
+ - dashboard/js/refresh.js
18
+ - dashboard/js/render-inbox.js
19
+ - dashboard/js/render-kb.js
20
+ - dashboard/js/render-plans.js
21
+ - dashboard/js/render-prd.js
22
+ - dashboard/js/render-work-items.js
23
+ - dashboard/styles.css
24
+
25
+ ### Documentation
26
+ - deprecated.json
27
+
28
+ ### Other
29
+ - test/unit.test.js
30
+
3
31
  ## 0.1.152 (2026-04-02)
4
32
 
5
33
  ### Dashboard
@@ -88,7 +88,7 @@ function ccAddMessage(role, html, skipSave) {
88
88
  const isUser = role === 'user';
89
89
  const div = document.createElement('div');
90
90
  const isAssistant = !isUser;
91
- div.className = isAssistant ? 'cc-msg-assistant' : '';
91
+ div.className = isAssistant ? 'cc-msg-assistant md-content' : '';
92
92
  div.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;' +
93
93
  (isUser ? 'background:var(--blue);color:#fff;align-self:flex-end' : 'background:var(--surface2);color:var(--text);align-self:flex-start;border:1px solid var(--border);position:relative');
94
94
  div.innerHTML = (isAssistant && !html.includes('color:var(--red)') && !html.includes('cc-queued-pill') ? llmCopyBtn() : '') + html;
@@ -205,11 +205,9 @@ async function _ccDoSend(message, skipUserMsg) {
205
205
  ccUpdateSessionIndicator();
206
206
  }
207
207
 
208
- // Render markdown-ish response
208
+ // Render markdown response
209
209
  const ccElapsed = Math.round((Date.now() - ccStartTime) / 1000);
210
- const rendered = (data.text || '').replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
211
- .replace(/`([^`]+)`/g, '<code style="background:var(--surface);padding:1px 4px;border-radius:3px;font-size:11px">$1</code>')
212
- .replace(/\n/g, '<br>');
210
+ const rendered = renderMd(data.text || '');
213
211
  ccAddMessage('assistant', rendered + '<div style="font-size:9px;color:var(--muted);margin-top:6px;display:flex;justify-content:flex-end;padding-right:30px">' + ccElapsed + 's</div>');
214
212
 
215
213
  // Execute actions
@@ -2,7 +2,6 @@
2
2
 
3
3
  let _modalDocContext = { title: '', content: '', selection: '' };
4
4
  let _modalFilePath = null; // file path for steering (null = read-only Q&A only)
5
- let _modalOriginalPlan = null; // tracks original plan file when editing a forked version
6
5
 
7
6
  function showModalQa() {
8
7
  document.getElementById('modal-qa').style.display = '';
@@ -235,9 +234,9 @@ async function _processQaMessage(message, selection) {
235
234
  actionDiv.style.cssText = 'margin:8px 0;padding:8px 12px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);border-radius:6px;display:flex;flex-wrap:wrap;align-items:center;gap:8px';
236
235
  actionDiv.innerHTML =
237
236
  '<span style="color:var(--orange);font-weight:600;font-size:12px;width:100%">Execution paused — plan was updated</span>' +
238
- '<button onclick="qaReplacePrd(\'' + esc + '\')" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer">Re-execute with new PRD</button>' +
237
+ '<button onclick="planExecute(\'' + esc + '\', \'\', null)" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer">Re-execute plan</button>' +
239
238
  '<button onclick="this.closest(\'div\').innerHTML=\'<span style=color:var(--muted);font-size:11px>Paused. No work dispatched.</span>\'" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer">Keep paused</button>' +
240
- '<span style="color:var(--muted);font-size:10px;width:100%">Re-execute replaces the old PRD with a fresh one from the updated plan.</span>';
239
+ '<span style="color:var(--muted);font-size:10px;width:100%">Re-execute creates a new PRD from the updated plan.</span>';
241
240
  thread.appendChild(actionDiv);
242
241
  }
243
242
  } else {
@@ -289,45 +288,4 @@ async function _processQaMessage(message, selection) {
289
288
  }
290
289
  }
291
290
 
292
- async function qaNewPrd(planFile) {
293
- qaDisablePrdButtons();
294
- const allPlans = window._lastStatus?.plans || [];
295
- const mdPlan = allPlans.find(p => p.file === planFile);
296
- const project = mdPlan?.project || '';
297
-
298
- planExecute(planFile, project, null);
299
-
300
- const btn = document.getElementById('qa-generate-prd-btn');
301
- if (btn) btn.innerHTML = '<span style="color:var(--green);font-size:12px">New PRD dispatched — existing work continues, agent creating fresh PRD from revised plan.</span>';
302
- }
303
-
304
- async function qaReplacePrd(planFile) {
305
- qaDisablePrdButtons();
306
- const allPlans = window._lastStatus?.plans || [];
307
- const mdPlan = allPlans.find(p => p.file === planFile);
308
- const project = mdPlan?.project || '';
309
- const existingPrd = allPlans.find(p => p.file.endsWith('.json') && p.project === project);
310
-
311
- if (existingPrd) {
312
- // Pause first to stop materialization, then clean pending items
313
- try {
314
- await fetch('/api/plans/pause', {
315
- method: 'POST', headers: { 'Content-Type': 'application/json' },
316
- body: JSON.stringify({ file: existingPrd.file })
317
- });
318
- } catch (e) { console.error('plan pause:', e.message); }
319
- try {
320
- await fetch('/api/plans/regenerate', {
321
- method: 'POST', headers: { 'Content-Type': 'application/json' },
322
- body: JSON.stringify({ source: existingPrd.file })
323
- });
324
- } catch (e) { console.error('plan regenerate:', e.message); }
325
- }
326
-
327
- planExecute(planFile, project, null);
328
-
329
- const btn = document.getElementById('qa-generate-prd-btn');
330
- if (btn) btn.innerHTML = '<span style="color:var(--orange);font-size:12px">Replacing PRD — old items paused, agent regenerating from revised plan.</span>';
331
- }
332
-
333
- window.MinionsQA = { showModalQa, modalAskAboutSelection, clearQaSelection, clearQaConversation, modalSend, qaNewPrd, qaReplacePrd };
291
+ window.MinionsQA = { showModalQa, modalAskAboutSelection, clearQaSelection, clearQaConversation, modalSend };
@@ -35,8 +35,6 @@ function closeModal() {
35
35
  // Clear edit/steer state
36
36
  _modalEditable = null;
37
37
  _modalFilePath = null;
38
- _modalOriginalPlan = null;
39
- // steer btn removed — unified send
40
38
  const body = document.getElementById('modal-body');
41
39
  body.contentEditable = 'false';
42
40
  body.style.border = '';
@@ -2,6 +2,7 @@
2
2
 
3
3
  // Sidebar activity indicators — detect changes between refreshes
4
4
  let _prevCounts = {};
5
+ let _prevEngineAlert = false;
5
6
  function _detectPageChanges(data) {
6
7
  const counts = {
7
8
  completions: (data.dispatch?.completed || []).length,
@@ -22,9 +23,52 @@ function _detectPageChanges(data) {
22
23
  if (counts.meetingRounds > _prevCounts.meetingRounds) changes.meetings = true;
23
24
  }
24
25
  _prevCounts = counts;
26
+
27
+ // Engine page — only badge for genuine problems, not routine activity
28
+ const engineAlert = _isEngineAlertWorthy(data);
29
+ if (engineAlert && !_prevEngineAlert) changes.engine = true;
30
+ // Clear the engine badge when alert condition resolves
31
+ if (!engineAlert && _prevEngineAlert) {
32
+ const engineLink = document.querySelector('.sidebar-link[data-page="engine"]');
33
+ if (engineLink) clearNotifBadge(engineLink);
34
+ }
35
+ _prevEngineAlert = engineAlert;
36
+
25
37
  return changes;
26
38
  }
27
39
 
40
+ /**
41
+ * Determine if the engine state warrants a notification dot.
42
+ * Returns true only for genuine problems:
43
+ * - Engine stopped, stale, or in error state
44
+ * - 3+ failed work items in the last hour
45
+ * - Agent timeout/crash detected (error results in recent completions)
46
+ */
47
+ function _isEngineAlertWorthy(data) {
48
+ // 1. Engine not running (stopped, stale, or error)
49
+ const engineState = data.engine?.state || 'stopped';
50
+ if (engineState === 'stopped' || engineState === 'error') return true;
51
+ // Stale heartbeat (>2 min old while claiming running)
52
+ if (engineState === 'running' && data.engine?.heartbeat) {
53
+ if (Date.now() - data.engine.heartbeat > 120000) return true;
54
+ }
55
+
56
+ // 2. 3+ failed work items in the last hour
57
+ const oneHourAgo = Date.now() - 3600000;
58
+ const recentFailures = (data.workItems || []).filter(w =>
59
+ w.status === 'failed' && w.updated_at && new Date(w.updated_at).getTime() > oneHourAgo
60
+ );
61
+ if (recentFailures.length >= 3) return true;
62
+
63
+ // 3. Agent timeout/crash — 3+ error results in recent completed dispatches
64
+ const recentErrors = (data.dispatch?.completed || []).filter(d =>
65
+ d.result === 'error' && d.completed_at && new Date(d.completed_at).getTime() > oneHourAgo
66
+ );
67
+ if (recentErrors.length >= 3) return true;
68
+
69
+ return false;
70
+ }
71
+
28
72
  function _processStatusUpdate(data) {
29
73
  // Detect fresh install — clear stale browser state if install ID changed
30
74
  if (data.installId) {
@@ -72,7 +72,6 @@ function openNotesModal() {
72
72
  _modalEditable = 'notes.md';
73
73
  _modalFilePath = 'notes.md'; showModalQa();
74
74
  document.getElementById('modal-edit-btn').style.display = '';
75
- // steer btn removed — unified send
76
75
  document.getElementById('modal').classList.add('open');
77
76
  }
78
77
 
@@ -169,7 +169,6 @@ async function kbOpenItem(category, file) {
169
169
  // Clear notification badge when opening this document
170
170
  const card = findCardForFile(_modalFilePath);
171
171
  if (card) clearNotifBadge(card);
172
- // steer btn removed — unified send
173
172
  document.getElementById('modal').classList.add('open');
174
173
  } catch (e) {
175
174
  console.error('Failed to load KB item:', e);
@@ -67,7 +67,7 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
67
67
  const hasPendingPrd = wi.some(w => w.type === 'plan-to-prd' && (w.status === 'pending' || w.status === 'dispatched'));
68
68
  const hasActiveWork = implementWi.some(w => w.status === 'pending' || w.status === 'dispatched');
69
69
  const allDone = implementWi.length > 0 && implementWi.every(w =>
70
- w.status === 'done' || w.status === 'in-pr' || w.status === 'implemented' || w.status === 'complete'
70
+ w.status === 'done'
71
71
  );
72
72
  const hasFailed = implementWi.some(w => w.status === 'failed');
73
73
 
@@ -304,48 +304,6 @@ function openArchivedPlansModal() {
304
304
  document.getElementById('modal').classList.add('open');
305
305
  }
306
306
 
307
- // Disable all PRD action buttons to prevent double-clicks
308
- function qaDisablePrdButtons() {
309
- const container = document.getElementById('qa-generate-prd-btn');
310
- if (container) container.querySelectorAll('button').forEach(b => { b.disabled = true; b.style.opacity = '0.5'; });
311
- }
312
-
313
- // Show plan version action buttons (Run alongside / Replace / Just save)
314
- function showPlanVersionActions(thread, newFile, originalFile) {
315
- const esc = newFile.replace(/'/g, "\\'");
316
- // Look up existing PRD for the original plan's project
317
- const allPlans = window._lastStatus?.plans || [];
318
- const origPlan = allPlans.find(p => p.file === originalFile);
319
- const project = origPlan?.project || '';
320
- const existingPrd = allPlans.find(p => p.file.endsWith('.json') && p.project === project && p.status !== 'completed');
321
-
322
- const btn = document.createElement('div');
323
- btn.id = 'qa-generate-prd-btn';
324
- btn.style.cssText = 'margin:8px 0;padding:8px 12px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:6px;display:flex;flex-wrap:wrap;align-items:center;gap:8px';
325
-
326
- if (existingPrd) {
327
- btn.innerHTML = '<span style="color:var(--green);font-weight:600;font-size:12px;width:100%">New plan version created — existing PRD running</span>' +
328
- '<button onclick="qaNewPrd(\'' + esc + '\')" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer" title="Execute this plan as a separate PRD alongside the current one">Run alongside</button>' +
329
- '<button onclick="qaReplacePrd(\'' + esc + '\')" style="background:var(--orange);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer" title="Pause existing PRD, clean pending items, execute this plan instead">Replace old PRD</button>' +
330
- '<button onclick="qaJustSave(this)" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer" title="Keep the new version saved without dispatching any work">Just save</button>' +
331
- '<span style="color:var(--muted);font-size:10px;width:100%">Run alongside keeps current work going. Replace pauses it and starts fresh.</span>';
332
- } else {
333
- btn.innerHTML = '<span style="color:var(--green);font-weight:600;font-size:12px;width:100%">New plan version created</span>' +
334
- '<button onclick="qaNewPrd(\'' + esc + '\')" style="background:var(--green);color:#fff;border:none;border-radius:4px;padding:4px 12px;font-size:11px;font-weight:600;cursor:pointer">Execute plan</button>' +
335
- '<button onclick="qaJustSave(this)" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 12px;font-size:11px;cursor:pointer">Just save</button>' +
336
- '<span style="color:var(--muted);font-size:10px">Execute dispatches an agent to create PRD items from this plan</span>';
337
- }
338
- // Remove any previous action buttons
339
- const old = thread.querySelector('#qa-generate-prd-btn');
340
- if (old) old.remove();
341
- thread.appendChild(btn);
342
- }
343
-
344
- function qaJustSave(el) {
345
- const container = el.closest('#qa-generate-prd-btn');
346
- if (container) container.innerHTML = '<span style="color:var(--muted);font-size:11px">Saved. No work dispatched.</span>';
347
- }
348
-
349
307
  async function planExecute(file, project, btn) {
350
308
  if (btn) { btn.textContent = 'Executing...'; btn.disabled = true; btn.style.color = 'var(--blue)'; }
351
309
  try {
@@ -736,4 +694,4 @@ async function planUnarchive(file, btn) {
736
694
  } catch (e) { resetBtn(); alert('Error: ' + e.message); }
737
695
  }
738
696
 
739
- window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, qaDisablePrdButtons, showPlanVersionActions, qaJustSave, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planUnarchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
697
+ window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planUnarchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
@@ -24,7 +24,7 @@ function renderPrd(prd, prog) {
24
24
  if (existing.length <= 1) {
25
25
  // Single PRD — show status + actions in header
26
26
  const implementItems = allWi.filter(w => prdItems.some(pi => pi.id === w.id));
27
- const allDone = implementItems.length > 0 && implementItems.every(w => w.status === 'done' || w.status === 'in-pr' || w.status === 'implemented' || w.status === 'complete');
27
+ const allDone = implementItems.length > 0 && implementItems.every(w => w.status === 'done');
28
28
  const hasActive = implementItems.some(w => w.status === 'pending' || w.status === 'dispatched');
29
29
  const prdFile = existing[0]?.file || '';
30
30
  const prdStatus = existing[0]?.status || '';
@@ -52,7 +52,7 @@ function renderPrd(prd, prog) {
52
52
  for (const p of existing) {
53
53
  const items = prdItems.filter(i => i.source === p.file);
54
54
  const wiForPrd = allWi.filter(w => items.some(pi => pi.id === w.id));
55
- const allDone = wiForPrd.length > 0 && wiForPrd.every(w => w.status === 'done' || w.status === 'in-pr' || w.status === 'implemented' || w.status === 'complete');
55
+ const allDone = wiForPrd.length > 0 && wiForPrd.every(w => w.status === 'done');
56
56
  const hasActive = wiForPrd.some(w => w.status === 'pending' || w.status === 'dispatched');
57
57
  const s = allDone && !hasActive ? 'completed' : hasActive ? 'in-progress' : p.status || 'active';
58
58
  counts[s] = (counts[s] || 0) + 1;
@@ -73,7 +73,7 @@ function renderPrdProgress(prog) {
73
73
  // Compute overall progress from active (non-archived) items
74
74
  const activeItems = (prog.items || []).filter(i => !i._archived);
75
75
  if (activeItems.length > 0) {
76
- const activeDone = activeItems.filter(i => i.status === 'done' || i.status === 'implemented' || i.status === 'in-pr').length;
76
+ const activeDone = activeItems.filter(i => i.status === 'done').length;
77
77
  countEl.textContent = Math.round((activeDone / activeItems.length) * 100) + '%';
78
78
  } else {
79
79
  countEl.textContent = '—';
@@ -82,7 +82,7 @@ function renderPrdProgress(prog) {
82
82
  function renderGroupStats(items) {
83
83
  const total = items.length;
84
84
  if (total === 0) return '';
85
- const done = items.filter(i => i.status === 'done' || i.status === 'implemented' || i.status === 'in-pr').length; // in-pr counted as done for backward compat
85
+ const done = items.filter(i => i.status === 'done').length;
86
86
  const inProgress = items.filter(i => i.status === 'in-progress').length;
87
87
  const failed = items.filter(i => i.status === 'failed').length;
88
88
  const paused = items.filter(i => i.status === 'paused').length;
@@ -112,13 +112,11 @@ function renderPrdProgress(prog) {
112
112
  const statusBadge = (s) => {
113
113
  const styles = {
114
114
  'done': 'background:rgba(63,185,80,0.15);color:var(--green)',
115
- 'implemented': 'background:rgba(63,185,80,0.15);color:var(--green)', /* legacy alias */
116
- 'in-pr': 'background:rgba(63,185,80,0.15);color:var(--green)', /* backward compat: displayed as done */
117
115
  'in-progress': 'background:rgba(210,153,34,0.15);color:var(--yellow);animation:wipPulse 1.5s infinite',
118
116
  'failed': 'background:rgba(248,81,73,0.15);color:var(--red)',
119
117
  'paused': 'background:rgba(139,148,158,0.15);color:var(--muted)',
120
118
  };
121
- const labels = { 'done': 'DONE', 'implemented': 'DONE', 'in-pr': 'DONE', 'in-progress': 'WIP', 'failed': 'FAIL', 'paused': 'PAUSED', 'missing': '—' };
119
+ const labels = { 'done': 'DONE', 'in-progress': 'WIP', 'failed': 'FAIL', 'paused': 'PAUSED', 'missing': '—' };
122
120
  const style = styles[s] || 'background:var(--surface);color:var(--muted)';
123
121
  const label = labels[s] || '—';
124
122
  return '<span style="font-size:9px;font-weight:700;padding:2px 6px;border-radius:3px;letter-spacing:0.5px;white-space:nowrap;' + style + '">' + label + '</span>';
@@ -197,7 +195,7 @@ function renderPrdProgress(prog) {
197
195
  const timings = prog.planTimings || {};
198
196
 
199
197
  const renderGroupHeader = (g) => {
200
- const done = g.items.filter(i => i.status === 'done' || i.status === 'complete' || i.status === 'implemented').length;
198
+ const done = g.items.filter(i => i.status === 'done').length;
201
199
  const wip = g.items.filter(i => i.status === 'in-progress' || i.status === 'dispatched').length;
202
200
  const summary = (g.summary || '').replace(/^Convert plan to PRD:\s*/i, '').slice(0, 80);
203
201
  const isAwaitingApproval = g.planStatus === 'awaiting-approval';
@@ -304,7 +302,7 @@ function renderPrdProgress(prog) {
304
302
  });
305
303
 
306
304
  const statusColor = (s) => {
307
- if (s === 'done' || s === 'implemented' || s === 'in-pr') return 'var(--green)'; // in-pr treated as done
305
+ if (s === 'done') return 'var(--green)';
308
306
  if (s === 'in-progress') return 'var(--yellow)';
309
307
  if (s === 'failed') return 'var(--red)';
310
308
  if (s === 'paused') return 'var(--muted)';
@@ -475,7 +473,7 @@ function openArchivedPrdModal() {
475
473
  // Picker: list archived plans, click to expand
476
474
  html = '<div style="margin-bottom:12px;font-size:12px;color:var(--muted)">Select an archived PRD to view:</div>';
477
475
  html += groups.map((g, i) => {
478
- const done = g.items.filter(it => it.status === 'done' || it.status === 'implemented' || it.status === 'complete').length;
476
+ const done = g.items.filter(it => it.status === 'done').length;
479
477
  const failed = g.items.filter(it => it.status === 'failed').length;
480
478
  return '<div class="plan-card" style="cursor:pointer;margin-bottom:8px" onclick="showArchivedPrdDetail(' + i + ')">' +
481
479
  '<div class="plan-card-title" style="font-size:13px">' + escHtml(g.summary || g.file) + '</div>' +
@@ -538,7 +536,7 @@ async function prdItemEdit(source, itemId) {
538
536
 
539
537
  // Build completion summary section
540
538
  let completionHtml = '';
541
- const isDone = item.status === 'done' || item.status === 'implemented' || item.status === 'complete' || item.status === 'in-pr'; // in-pr treated as done for backward compat
539
+ const isDone = item.status === 'done';
542
540
  const isFailed = item.status === 'failed';
543
541
  const isActive = item.status === 'in-progress' || item.status === 'dispatched';
544
542
 
@@ -711,7 +709,7 @@ function openArchive(i) {
711
709
  html += '<div class="archive-feature">' +
712
710
  '<span class="feat-id">' + escHtml(f.id) + '</span> ' +
713
711
  '<span class="prd-item-priority ' + pClass + '">' + escHtml(f.priority || '') + '</span>' +
714
- (f.status ? ' <span class="pr-badge ' + (f.status === 'complete' || f.status === 'done' || f.status === 'in-pr' ? 'approved' : 'draft') + '" style="font-size:9px">' + escHtml(f.status === 'in-pr' ? 'done' : f.status) + '</span>' : '') +
712
+ (f.status ? ' <span class="pr-badge ' + (f.status === 'done' ? 'approved' : 'draft') + '" style="font-size:9px">' + escHtml(f.status) + '</span>' : '') +
715
713
  '<div class="feat-name">' + escHtml(f.name) + '</div>' +
716
714
  '<div class="feat-desc">' + escHtml(f.description || '') + '</div>' +
717
715
  (f.rationale ? '<div class="feat-desc" style="margin-top:4px;color:var(--yellow)">Rationale: ' + escHtml(f.rationale) + '</div>' : '') +
@@ -4,6 +4,30 @@ let allWorkItems = [];
4
4
  let wiPage = 0;
5
5
  const WI_PER_PAGE = 20;
6
6
 
7
+ // Track retry state per work item so loading/success/error survives re-renders
8
+ const _wiRetryState = {}; // { [id]: { status: 'pending'|'done'|'error', message?, until? } }
9
+ function setWiRetryState(id, state) { _wiRetryState[id] = state; }
10
+ function getWiRetryState(id) {
11
+ const s = _wiRetryState[id];
12
+ if (!s) return null;
13
+ if (s.until && Date.now() > s.until) { delete _wiRetryState[id]; return null; }
14
+ return s;
15
+ }
16
+
17
+ function wiRetryBtn(item) {
18
+ const rs = getWiRetryState(item.id);
19
+ if (rs && rs.status === 'pending') {
20
+ return '<span style="font-size:9px;padding:1px 6px;color:var(--yellow);border:1px solid rgba(210,153,34,0.35);background:rgba(210,153,34,0.1);border-radius:3px;cursor:wait;margin-left:4px">Retrying\u2026</span>';
21
+ }
22
+ if (rs && rs.status === 'done') {
23
+ return '<span style="font-size:9px;padding:1px 6px;color:var(--green);border:1px solid rgba(63,185,80,0.35);background:rgba(63,185,80,0.1);border-radius:3px;margin-left:4px">Requeued</span>';
24
+ }
25
+ if (rs && rs.status === 'error') {
26
+ return '<span style="font-size:9px;padding:1px 6px;color:var(--red);border:1px solid rgba(248,81,73,0.35);background:rgba(248,81,73,0.1);border-radius:3px;margin-left:4px;cursor:pointer" title="' + escHtml(rs.message || 'Retry failed') + ' — click to try again" onclick="event.stopPropagation();retryWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')">Retry failed</span>';
27
+ }
28
+ return '<button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="event.stopPropagation();retryWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\')">Retry</button>';
29
+ }
30
+
7
31
  function wiRow(item) {
8
32
  const statusBadge = (s) => {
9
33
  const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
@@ -23,7 +47,7 @@ function wiRow(item) {
23
47
  '<td>' + priBadge(item.priority) + '</td>' +
24
48
  '<td>' + statusBadge(item.status || 'pending') +
25
49
  (item._pendingReason ? ' <span style="font-size:9px;color:var(--muted);margin-left:4px" title="Pending reason: ' + escHtml(item._pendingReason) + '">' + escHtml(item._pendingReason.replace(/_/g, ' ')) + '</span>' : '') +
26
- (item.status === 'failed' ? ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="event.stopPropagation();retryWorkItem(\'' + escHtml(item.id) + '\',\'' + escHtml(item._source || '') + '\',this)">Retry</button>' : '') +
50
+ (item.status === 'failed' ? ' ' + wiRetryBtn(item) : '') +
27
51
  '</td>' +
28
52
  '<td>' +
29
53
  (item.completedAgents && item.completedAgents.length > 0
@@ -92,7 +116,7 @@ function renderWorkItems(items) {
92
116
  function editWorkItem(id, source) {
93
117
  const item = allWorkItems.find(i => i.id === id);
94
118
  if (!item) return;
95
- const types = ['implement', 'fix', 'review', 'plan', 'verify', 'investigate', 'refactor', 'test', 'docs'];
119
+ const types = ['implement', 'fix', 'review', 'plan', 'verify', 'evaluate', 'decompose', 'meeting', 'investigate', 'refactor', 'test', 'explore', 'ask', 'docs'];
96
120
  const priorities = ['critical', 'high', 'medium', 'low'];
97
121
  const agentOpts = (cmdAgents || []).map(a => '<option value="' + escHtml(a.id) + '"' + (item.agent === a.id ? ' selected' : '') + '>' + escHtml(a.name) + '</option>').join('');
98
122
  const typeOpts = types.map(t => '<option value="' + t + '"' + ((item.type || 'implement') === t ? ' selected' : '') + '>' + t + '</option>').join('');
@@ -211,23 +235,33 @@ async function toggleWorkItemArchive() {
211
235
  } catch (e) { el.innerHTML = '<p class="empty">Failed to load archive.</p>'; }
212
236
  }
213
237
 
214
- async function retryWorkItem(id, source, btn) {
215
- if (btn) { btn.dataset.origText = btn.textContent; btn.textContent = 'Retrying...'; btn.style.pointerEvents = 'none'; btn.style.opacity = '0.6'; }
238
+ async function retryWorkItem(id, source) {
239
+ // Prevent double-click: if already retrying, ignore
240
+ const existing = getWiRetryState(id);
241
+ if (existing && existing.status === 'pending') return;
242
+
243
+ setWiRetryState(id, { status: 'pending' });
244
+ renderWorkItems(allWorkItems);
216
245
  try {
217
246
  const res = await fetch('/api/work-items/retry', {
218
247
  method: 'POST', headers: { 'Content-Type': 'application/json' },
219
248
  body: JSON.stringify({ id, source: source || undefined })
220
249
  });
221
250
  if (res.ok) {
251
+ setWiRetryState(id, { status: 'done', until: Date.now() + 8000 });
222
252
  showToast('cmd-toast', 'Work item ' + id + ' reset to pending', true);
223
253
  wakeEngine();
224
254
  refresh();
225
255
  } else {
226
- if (btn) { btn.textContent = btn.dataset.origText || 'Retry'; btn.style.pointerEvents = ''; btn.style.opacity = ''; }
227
256
  const d = await res.json().catch(() => ({}));
228
- alert('Retry failed: ' + (d.error || 'unknown'));
257
+ const msg = d.error || 'unknown';
258
+ setWiRetryState(id, { status: 'error', message: msg, until: Date.now() + 10000 });
259
+ renderWorkItems(allWorkItems);
229
260
  }
230
- } catch (e) { if (btn) { btn.textContent = btn.dataset.origText || 'Retry'; btn.style.pointerEvents = ''; btn.style.opacity = ''; } alert('Retry error: ' + e.message); }
261
+ } catch (e) {
262
+ setWiRetryState(id, { status: 'error', message: e.message, until: Date.now() + 10000 });
263
+ renderWorkItems(allWorkItems);
264
+ }
231
265
  }
232
266
 
233
267
  function wiPrev() { if (wiPage > 0) { wiPage--; renderWorkItems(allWorkItems); } }
@@ -282,7 +316,7 @@ async function submitFeedback(id, source) {
282
316
  }
283
317
 
284
318
  function openCreateWorkItemModal() {
285
- const typeOpts = ['implement', 'fix', 'explore', 'test', 'review', 'ask', 'plan'].map(t =>
319
+ const typeOpts = ['implement', 'fix', 'explore', 'test', 'review', 'ask', 'plan', 'verify', 'evaluate', 'decompose', 'meeting'].map(t =>
286
320
  '<option value="' + t + '"' + (t === 'implement' ? ' selected' : '') + '>' + t + '</option>'
287
321
  ).join('');
288
322
  const priOpts = ['high', 'medium', 'low'].map(p =>
@@ -181,8 +181,6 @@
181
181
  .prd-items-list { display: flex; flex-direction: column; gap: 3px; max-height: 400px; overflow-y: auto; padding: 0 8px; }
182
182
  .prd-item-row { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-radius: var(--radius-sm); font-size: var(--text-base); background: var(--surface2); border: 1px solid var(--border); border-left: 3px solid var(--border); }
183
183
  .prd-item-row.st-done { border-left-color: var(--green); }
184
- .prd-item-row.st-implemented { border-left-color: var(--green); } /* legacy alias */
185
- .prd-item-row.st-in-pr { border-left-color: var(--green); } /* backward compat: treated as done */
186
184
  .prd-item-row.st-in-progress { border-left-color: var(--yellow); animation: prdWipPulse 2s infinite; }
187
185
  @keyframes prdWipPulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(210,153,34,0); } 50% { box-shadow: 0 0 0 4px rgba(210,153,34,0.2); } }
188
186
  .prd-item-row.st-failed { border-left-color: var(--red); }
@@ -507,6 +505,10 @@
507
505
  .dispatch-type.plan { background: rgba(168,85,247,0.15); color: #a855f7; }
508
506
  .dispatch-type.plan-to-prd { background: rgba(168,85,247,0.1); color: #a855f7; }
509
507
  .dispatch-type.ask { background: rgba(63,185,80,0.15); color: var(--green); }
508
+ .dispatch-type.evaluate { background: rgba(248,81,73,0.15); color: var(--red); }
509
+ .dispatch-type.verify { background: rgba(63,185,80,0.2); color: var(--green); }
510
+ .dispatch-type.meeting { background: rgba(88,166,255,0.2); color: var(--blue); }
511
+ .dispatch-type.decompose { background: rgba(188,140,255,0.2); color: var(--purple); }
510
512
  .dispatch-type.manual { background: rgba(139,148,158,0.15); color: var(--muted); }
511
513
  .dispatch-agent { font-weight: 600; color: var(--text); }
512
514
  .dispatch-task { flex: 1; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }