@yemi33/minions 0.1.328 → 0.1.329

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.329 (2026-04-03)
4
+
5
+ ### Fixes
6
+ - remove orphan statuses, add validation, replace in-progress with dispatched
7
+
3
8
  ## 0.1.328 (2026-04-03)
4
9
 
5
10
  ### Fixes
@@ -83,7 +83,7 @@ function derivePlanStatus(prdFile, mdFile, prdJsonStatus, workItems) {
83
83
 
84
84
  // Derive from work item progress
85
85
  if (allDone && !hasActiveWork) return 'completed';
86
- if (hasActiveWork || hasPendingPrd) return 'in-progress';
86
+ if (hasActiveWork || hasPendingPrd) return 'dispatched';
87
87
  if (hasFailed && !hasActiveWork) return 'has-failures';
88
88
 
89
89
  if (prdJsonStatus === 'awaiting-approval' && implementWi.length === 0) return 'awaiting-approval';
@@ -192,7 +192,7 @@ function renderPlans(plans) {
192
192
  const effectiveStatus = isArchived ? 'completed' : derivePlanStatus(prdFile, p.file, prdJsonStatus, allWi);
193
193
 
194
194
  const statusLabelsMap = {
195
- 'completed': 'Completed', 'in-progress': 'In Progress', 'paused': 'Paused',
195
+ 'completed': 'Completed', 'dispatched': 'In Progress', 'paused': 'Paused',
196
196
  'awaiting-approval': 'Awaiting Approval', 'approved': 'Approved', 'rejected': 'Rejected',
197
197
  'revision-requested': 'Revision Requested', 'has-failures': 'Has Failures', 'active': 'Active'
198
198
  };
@@ -225,7 +225,7 @@ function renderPlans(plans) {
225
225
 
226
226
  const executeBtn = isDraft && (effectiveStatus === 'active' || effectiveStatus === 'draft') && !isArchived && !prdFile ? '<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);font-weight:600" ' +
227
227
  'onclick="event.stopPropagation();planExecute(\'' + escHtml(p.file) + '\',\'' + escHtml(p.project) + '\',this)">Execute</button>' : '';
228
- const showPause = effectiveStatus === 'in-progress' && prdFile && !isArchived;
228
+ const showPause = effectiveStatus === 'dispatched' && prdFile && !isArchived;
229
229
  const showResume = (effectiveStatus === 'paused' || effectiveStatus === 'awaiting-approval') && prdFile && !isArchived;
230
230
  const verifyWi = allWi.find(w => w.itemType === 'verify' && w.sourcePlan === prdFile);
231
231
  const hasVerifyWi = !!verifyWi;
@@ -246,8 +246,8 @@ function renderPlans(plans) {
246
246
  'onclick="event.stopPropagation();planDelete(\'' + escHtml(p.file) + '\')">Delete</button>' : '';
247
247
 
248
248
  const versionBadge = p.version ? ' <span style="font-size:9px;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>' : '';
249
- const statusColors = { 'completed': 'var(--green)', 'in-progress': 'var(--blue)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--green)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)' };
250
- const cardClass = effectiveStatus === 'in-progress' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
249
+ const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'paused': 'var(--muted)', 'awaiting-approval': 'var(--yellow)', 'approved': 'var(--green)', 'rejected': 'var(--red)', 'has-failures': 'var(--red)', 'revision-requested': 'var(--purple,#a855f7)', 'active': 'var(--muted)' };
250
+ const cardClass = effectiveStatus === 'dispatched' ? 'working' : effectiveStatus === 'awaiting-approval' || effectiveStatus === 'paused' ? 'awaiting' : effectiveStatus;
251
251
  return '<div class="plan-card ' + cardClass + '" data-file="plans/' + escHtml(p.file) + '" style="cursor:pointer' + (isArchived ? ';opacity:0.7' : '') + '" onclick="planView(\'' + escHtml(p.file) + '\')">' +
252
252
  '<div class="plan-card-header">' +
253
253
  '<div><div class="plan-card-title">' + escHtml(p.summary || p.file) + versionBadge + '</div>' +
@@ -13,8 +13,8 @@ function renderPrd(prd, prog) {
13
13
  return;
14
14
  }
15
15
 
16
- const statusColors = { 'completed': 'var(--green)', 'in-progress': 'var(--blue)', 'awaiting-approval': 'var(--yellow)', 'paused': 'var(--muted)', 'approved': 'var(--green)' };
17
- const statusLabels = { 'completed': 'Completed', 'in-progress': 'In Progress', 'awaiting-approval': 'Awaiting Approval', 'paused': 'Paused', 'approved': 'Approved' };
16
+ const statusColors = { 'completed': 'var(--green)', 'dispatched': 'var(--blue)', 'awaiting-approval': 'var(--yellow)', 'paused': 'var(--muted)', 'approved': 'var(--green)' };
17
+ const statusLabels = { 'completed': 'Completed', 'dispatched': 'In Progress', 'awaiting-approval': 'Awaiting Approval', 'paused': 'Paused', 'approved': 'Approved' };
18
18
 
19
19
  // Show per-PRD status summary in header when multiple PRDs exist
20
20
  const existing = prd.existing || [];
@@ -28,7 +28,7 @@ function renderPrd(prd, prog) {
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 || '';
31
- const effectiveStatus = allDone && !hasActive ? 'completed' : hasActive ? 'in-progress' : prdStatus || 'active';
31
+ const effectiveStatus = allDone && !hasActive ? 'completed' : hasActive ? 'dispatched' : prdStatus || 'active';
32
32
 
33
33
  let actions = '';
34
34
  if (prdFile) {
@@ -37,7 +37,7 @@ function renderPrd(prd, prog) {
37
37
  } else if (effectiveStatus === 'completed') {
38
38
  actions = ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:4px" onclick="triggerVerify(\'' + escHtml(prdFile) + '\',this)">Verify</button>' +
39
39
  ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;margin-left:4px" onclick="planArchive(\'' + escHtml(prdFile) + '\',this)">Archive</button>';
40
- } else if (effectiveStatus === 'in-progress') {
40
+ } else if (effectiveStatus === 'dispatched') {
41
41
  actions = ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--yellow);border-color:var(--yellow);margin-left:4px" onclick="planPause(\'' + escHtml(prdFile) + '\',this)">Pause</button>';
42
42
  } else if (effectiveStatus === 'paused') {
43
43
  actions = ' <button class="pr-pager-btn" style="font-size:9px;padding:1px 6px;color:var(--green);border-color:var(--green);margin-left:4px" onclick="planApprove(\'' + escHtml(prdFile) + '\',this)">Resume</button>' +
@@ -48,13 +48,13 @@ function renderPrd(prd, prog) {
48
48
  ' <span style="color:var(--muted);font-size:10px">' + (prd.age || '') + '</span>' + actions;
49
49
  } else {
50
50
  // Multiple PRDs — show count summary, per-PRD details are in renderPrdProgress groups
51
- const counts = { completed: 0, 'in-progress': 0, 'awaiting-approval': 0, paused: 0 };
51
+ const counts = { completed: 0, 'dispatched': 0, 'awaiting-approval': 0, paused: 0 };
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
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
- const s = allDone && !hasActive ? 'completed' : hasActive ? 'in-progress' : p.status || 'active';
57
+ const s = allDone && !hasActive ? 'completed' : hasActive ? 'dispatched' : p.status || 'active';
58
58
  counts[s] = (counts[s] || 0) + 1;
59
59
  }
60
60
  const parts = Object.entries(counts).filter(([, n]) => n > 0).map(([s, n]) =>
@@ -83,7 +83,7 @@ function renderPrdProgress(prog) {
83
83
  const total = items.length;
84
84
  if (total === 0) return '';
85
85
  const done = items.filter(i => i.status === 'done').length;
86
- const inProgress = items.filter(i => i.status === 'in-progress').length;
86
+ const inProgress = items.filter(i => i.status === 'dispatched').length;
87
87
  const failed = items.filter(i => i.status === 'failed').length;
88
88
  const paused = items.filter(i => i.status === 'paused').length;
89
89
  const missing = items.filter(i => i.status === 'missing' || !i.status).length;
@@ -100,7 +100,7 @@ function renderPrdProgress(prog) {
100
100
 
101
101
  const bar = '<div class="prd-progress-bar">' +
102
102
  '<div class="seg complete" style="width:' + pct(done) + '%"></div>' +
103
- '<div class="seg in-progress" style="width:' + pct(inProgress) + '%"></div>' +
103
+ '<div class="seg dispatched" style="width:' + pct(inProgress) + '%"></div>' +
104
104
  '<div class="seg paused" style="width:' + pct(paused) + '%"></div>' +
105
105
  '<div class="seg missing" style="width:' + pct(missing) + '%"></div>' +
106
106
  '</div>';
@@ -108,15 +108,15 @@ function renderPrdProgress(prog) {
108
108
  return '<div style="margin:6px 0 8px 0;padding:0 8px">' + stats + '<div style="margin-top:8px">' + bar + '</div></div>';
109
109
  }
110
110
 
111
- // PRD item statuses: missing → in-progress → done
111
+ // PRD item statuses: missing → dispatched → done
112
112
  const statusBadge = (s) => {
113
113
  const styles = {
114
114
  'done': 'background:rgba(63,185,80,0.15);color:var(--green)',
115
- 'in-progress': 'background:rgba(210,153,34,0.15);color:var(--yellow);animation:wipPulse 1.5s infinite',
115
+ 'dispatched': 'background:rgba(210,153,34,0.15);color:var(--yellow);animation:wipPulse 1.5s infinite',
116
116
  'failed': 'background:rgba(248,81,73,0.15);color:var(--red)',
117
117
  'paused': 'background:rgba(139,148,158,0.15);color:var(--muted)',
118
118
  };
119
- const labels = { 'done': 'DONE', 'in-progress': 'WIP', 'failed': 'FAIL', 'paused': 'PAUSED', 'missing': '\u2014' };
119
+ const labels = { 'done': 'DONE', 'dispatched': 'WIP', 'failed': 'FAIL', 'paused': 'PAUSED', 'missing': '\u2014' };
120
120
  const style = styles[s] || 'background:var(--surface);color:var(--muted)';
121
121
  const label = labels[s] || '—';
122
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>';
@@ -196,7 +196,7 @@ function renderPrdProgress(prog) {
196
196
 
197
197
  const renderGroupHeader = (g) => {
198
198
  const done = g.items.filter(i => i.status === 'done').length;
199
- const wip = g.items.filter(i => i.status === 'in-progress' || i.status === 'dispatched').length;
199
+ const wip = g.items.filter(i => i.status === 'dispatched' || i.status === 'dispatched').length;
200
200
  const summary = (g.summary || '').replace(/^Convert plan to PRD:\s*/i, '').slice(0, 80);
201
201
  const isAwaitingApproval = g.planStatus === 'awaiting-approval';
202
202
  const isPaused = g.planStatus === 'paused';
@@ -303,7 +303,7 @@ function renderPrdProgress(prog) {
303
303
 
304
304
  const statusColor = (s) => {
305
305
  if (s === 'done') return 'var(--green)';
306
- if (s === 'in-progress') return 'var(--yellow)';
306
+ if (s === 'dispatched') return 'var(--yellow)';
307
307
  if (s === 'failed') return 'var(--red)';
308
308
  if (s === 'paused') return 'var(--muted)';
309
309
  return 'var(--border)';
@@ -322,7 +322,7 @@ function renderPrdProgress(prog) {
322
322
  const iid = escHtml(i.id || '');
323
323
  const agent = wi[i.id]?.dispatched_to || '';
324
324
  const deps = (i.depends_on || []).join(', ');
325
- const wipAnim = i.status === 'in-progress' ? 'animation:prdWipPulse 2s infinite;' : '';
325
+ const wipAnim = i.status === 'dispatched' ? 'animation:prdWipPulse 2s infinite;' : '';
326
326
  html += '<div onclick="prdItemEdit(\'' + src + '\',\'' + iid + '\')" ' +
327
327
  'style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid ' + borderColor + ';' + wipAnim +
328
328
  'border-radius:4px;padding:6px 8px;margin-bottom:6px;cursor:pointer;font-size:11px">' +
@@ -557,7 +557,7 @@ async function prdItemEdit(source, itemId) {
557
557
  let completionHtml = '';
558
558
  const isDone = item.status === 'done';
559
559
  const isFailed = item.status === 'failed';
560
- const isActive = item.status === 'in-progress' || item.status === 'dispatched';
560
+ const isActive = item.status === 'dispatched' || item.status === 'dispatched';
561
561
 
562
562
  if (isDone || isFailed || isActive) {
563
563
  const agent = wi?.dispatched_to || completedEntry?.agent || '';
@@ -30,7 +30,7 @@ function wiRetryBtn(item) {
30
30
 
31
31
  function wiRow(item) {
32
32
  const statusBadge = (s) => {
33
- const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : 'draft';
33
+ const cls = s === 'failed' ? 'rejected' : s === 'needs-human-review' ? 'needs-review' : s === 'dispatched' ? 'building' : s === 'pending' || s === 'queued' ? 'active' : s === 'done' ? 'approved' : s === 'decomposed' ? 'building' : 'draft';
34
34
  return '<span class="pr-badge ' + cls + '">' + escHtml(s) + '</span>';
35
35
  };
36
36
  const typeBadge = (t) => '<span class="dispatch-type ' + (t || 'implement') + '">' + escHtml(t || 'implement') + '</span>';
@@ -168,7 +168,7 @@
168
168
  .prd-progress-bar { width: 100%; height: 20px; background: var(--bg); border-radius: var(--radius-xl); overflow: hidden; display: flex; margin-bottom: var(--space-6); border: 1px solid var(--border); }
169
169
  .prd-progress-bar .seg { height: 100%; transition: width 0.5s ease; }
170
170
  .prd-progress-bar .seg.complete { background: var(--green); }
171
- .prd-progress-bar .seg.in-progress { background: var(--yellow); }
171
+ .prd-progress-bar .seg.dispatched { background: var(--yellow); }
172
172
  .prd-progress-bar .seg.paused { background: var(--muted); opacity: 0.5; }
173
173
  .prd-progress-bar .seg.missing { background: var(--border); }
174
174
  .prd-progress-pct { font-size: 22px; font-weight: 700; color: var(--green); margin-bottom: var(--space-4); }
@@ -176,7 +176,7 @@
176
176
  .prd-legend-item { display: flex; align-items: center; gap: 5px; font-size: var(--text-base); color: var(--muted); }
177
177
  .prd-legend-dot { width: 10px; height: 10px; border-radius: 2px; }
178
178
  .prd-legend-dot.complete { background: var(--green); }
179
- .prd-legend-dot.in-progress { background: var(--yellow); }
179
+ .prd-legend-dot.dispatched { background: var(--yellow); }
180
180
  .prd-legend-dot.missing { background: var(--border); }
181
181
 
182
182
  /* Pipeline Step-Progress */
@@ -193,7 +193,7 @@
193
193
  .prd-items-list { display: flex; flex-direction: column; gap: 3px; max-height: 400px; overflow-y: auto; padding: 0 8px; }
194
194
  .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); }
195
195
  .prd-item-row.st-done { border-left-color: var(--green); }
196
- .prd-item-row.st-in-progress { border-left-color: var(--yellow); animation: prdWipPulse 2s infinite; }
196
+ .prd-item-row.st-dispatched { border-left-color: var(--yellow); animation: prdWipPulse 2s infinite; }
197
197
  @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); } }
198
198
  .prd-item-row.st-failed { border-left-color: var(--red); }
199
199
  .prd-item-row.st-needs-human-review { border-left-color: var(--orange); }
package/engine/cli.js CHANGED
@@ -809,8 +809,8 @@ const commands = {
809
809
  if (exists && name === 'prd') {
810
810
  const prd = safeJson(filePath);
811
811
  if (prd) {
812
- const missing = (prd.missing_features || []).filter(f => ['missing', 'planned'].includes(f.status));
813
- console.log(` Items: ${missing.length} missing/planned features`);
812
+ const missing = (prd.missing_features || []).filter(f => f.status === 'missing' || !f.status);
813
+ console.log(` Items: ${missing.length} missing features`);
814
814
  }
815
815
  }
816
816
  if (exists && name === 'pullRequests') {
@@ -5,9 +5,11 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const os = require('os');
8
9
  const shared = require('./shared');
9
- const { safeRead, safeJson, safeWrite, execSilent, projectPrPath, getPrLinks, addPrLink,
10
- log, ts, dateStamp } = shared;
10
+ const { safeRead, safeJson, safeWrite, mutateJsonFileLocked, execSilent, projectPrPath, getPrLinks, addPrLink,
11
+ log, ts, dateStamp, WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT,
12
+ ENGINE_DEFAULTS } = shared;
11
13
  const { trackEngineUsage } = require('./llm');
12
14
  const queries = require('./queries');
13
15
  const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
@@ -21,7 +23,9 @@ function checkPlanCompletion(meta, config) {
21
23
  const planPath = path.join(PRD_DIR, planFile);
22
24
  const plan = safeJson(planPath);
23
25
  if (!plan?.missing_features) return;
24
- if (plan.status === 'completed') return;
26
+ if (plan.status === PLAN_STATUS.COMPLETED) {
27
+ if (plan._completionNotified) return;
28
+ }
25
29
 
26
30
  const projects = shared.getProjects(config);
27
31
 
@@ -53,7 +57,7 @@ function checkPlanCompletion(meta, config) {
53
57
  const unmaterialized = [...planFeatureIds].filter(id => {
54
58
  if (workItemById[id]) return false;
55
59
  const prdItem = (plan.missing_features || []).find(f => f.id === id);
56
- return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
60
+ return !(prdItem && DONE_STATUSES.has(prdItem.status));
57
61
  });
58
62
  if (unmaterialized.length > 0) {
59
63
  log('info', `Plan ${planFile}: ${unmaterialized.length}/${planFeatureIds.size} feature(s) not yet materialized as work items: ${unmaterialized.join(', ')}`);
@@ -63,20 +67,20 @@ function checkPlanCompletion(meta, config) {
63
67
  // Check 2: every feature's work item must be done (or PRD item marked done externally)
64
68
  const notDone = [...planFeatureIds].filter(id => {
65
69
  const w = workItemById[id];
66
- if (w && (w.status === 'done' || w.status === 'in-pr')) return false; // in-pr accepted for backward compat
70
+ if (w && DONE_STATUSES.has(w.status)) return false;
67
71
  const prdItem = (plan.missing_features || []).find(f => f.id === id);
68
- return !(prdItem && (prdItem.status === 'done' || prdItem.status === 'in-pr'));
72
+ return !(prdItem && DONE_STATUSES.has(prdItem.status));
69
73
  });
70
74
  if (notDone.length > 0) {
71
75
  log('info', `Plan ${planFile}: waiting for done on ${notDone.length}/${planFeatureIds.size} item(s): ${notDone.join(', ')}`);
72
76
  return;
73
77
  }
74
78
 
75
- const doneItems = planItems.filter(w => w.status === 'done' || w.status === 'in-pr');
76
- const failedItems = planItems.filter(w => w.status === 'failed');
79
+ const doneItems = planItems.filter(w => DONE_STATUSES.has(w.status));
80
+ const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED);
77
81
 
78
82
  // 1. Mark plan as completed
79
- plan.status = 'completed';
83
+ plan.status = PLAN_STATUS.COMPLETED;
80
84
  plan.completedAt = ts();
81
85
 
82
86
  // Compute timing
@@ -156,7 +160,7 @@ function checkPlanCompletion(meta, config) {
156
160
  id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
157
161
  type: 'implement', priority: 'high',
158
162
  description: `All plan items from \`${planFile}\` are complete on branch \`${featureBranch}\`.\n\n**Branch:** \`${featureBranch}\`\n**Target:** \`${mainBranch}\`\n\n## Completed Items\n${itemSummary}`,
159
- status: 'pending', created: ts(), createdBy: 'engine:plan-completion',
163
+ status: WI_STATUS.PENDING, created: ts(), createdBy: 'engine:plan-completion',
160
164
  sourcePlan: planFile, itemType: 'pr',
161
165
  branch: featureBranch, branchStrategy: 'shared-branch', project: projectName,
162
166
  });
@@ -177,7 +181,7 @@ function checkPlanCompletion(meta, config) {
177
181
  const prs = (safeJson(shared.projectPrPath(p)) || [])
178
182
  .filter(pr => {
179
183
  const linkedId = prLinks[pr.id];
180
- return pr.status === 'active' && linkedId && doneItems.find(w => w.id === linkedId);
184
+ return pr.status === PR_STATUS.ACTIVE && linkedId && doneItems.find(w => w.id === linkedId);
181
185
  });
182
186
  if (prs.length > 0) {
183
187
  projectPrs[p.name] = { project: p, prs, mainBranch: p.mainBranch || 'main' };
@@ -190,7 +194,7 @@ function checkPlanCompletion(meta, config) {
190
194
  const branches = prs.map(pr => pr.branch).filter(Boolean);
191
195
  const lines = [
192
196
  `# ${name} — merge ${branches.length} PR branch(es) into one worktree`,
193
- `cd "${p.localPath}"`,
197
+ `cd "${p.localPath.replace(/\\/g, '/')}"`,
194
198
  `git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`,
195
199
  `git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
196
200
  `cd "${wtPath}"`,
@@ -247,7 +251,7 @@ function checkPlanCompletion(meta, config) {
247
251
  type: 'verify',
248
252
  priority: 'high',
249
253
  description,
250
- status: 'pending',
254
+ status: WI_STATUS.PENDING,
251
255
  created: ts(),
252
256
  createdBy: 'engine:plan-verification',
253
257
  sourcePlan: planFile,
@@ -400,7 +404,7 @@ function chainPlanToPrd(dispatchItem, meta, config) {
400
404
  type: 'plan-to-prd',
401
405
  priority: meta?.item?.priority || 'high',
402
406
  description: `Plan file: plans/${planFile.name}\nChained from plan task ${dispatchItem.id}`,
403
- status: 'pending',
407
+ status: WI_STATUS.PENDING,
404
408
  created: ts(),
405
409
  createdBy: 'engine:chain',
406
410
  project: targetProject.name,
@@ -410,10 +414,15 @@ function chainPlanToPrd(dispatchItem, meta, config) {
410
414
  }
411
415
 
412
416
  // ─── Work Item Status ────────────────────────────────────────────────────────
417
+ const _VALID_WI_STATUSES = new Set(Object.values(WI_STATUS));
413
418
  function updateWorkItemStatus(meta, status, reason) {
414
419
 
415
420
  const itemId = meta.item?.id;
416
421
  if (!itemId) return;
422
+ if (!_VALID_WI_STATUSES.has(status)) {
423
+ log('warn', `Invalid work item status '${status}' for ${itemId} — ignoring`);
424
+ return;
425
+ }
417
426
 
418
427
  let wiPath;
419
428
  if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
@@ -435,20 +444,20 @@ function updateWorkItemStatus(meta, status, reason) {
435
444
  target.agentResults[agent] = { status, completedAt: ts(), reason: reason || undefined };
436
445
 
437
446
  const results = Object.values(target.agentResults);
438
- const anySuccess = results.some(r => r.status === 'done');
447
+ const anySuccess = results.some(r => r.status === WI_STATUS.DONE);
439
448
  const allDone = Array.isArray(target.fanOutAgents) && target.fanOutAgents.length > 0 ? results.length >= target.fanOutAgents.length : false;
440
449
  const dispatchAge = target.dispatched_at ? Date.now() - new Date(target.dispatched_at).getTime() : 0;
441
450
  const timedOut = !allDone && dispatchAge > 6 * 60 * 60 * 1000 && results.length > 0;
442
451
 
443
452
  if (anySuccess) {
444
- target.status = 'done';
453
+ target.status = WI_STATUS.DONE;
445
454
  delete target.failReason;
446
455
  delete target.failedAt;
447
456
  target.completedAgents = Object.entries(target.agentResults)
448
- .filter(([, r]) => r.status === 'done')
457
+ .filter(([, r]) => r.status === WI_STATUS.DONE)
449
458
  .map(([a]) => a);
450
459
  } else if (allDone || timedOut) {
451
- target.status = 'failed';
460
+ target.status = WI_STATUS.FAILED;
452
461
  target.failReason = timedOut
453
462
  ? `Fan-out timed out: ${results.length}/${(target.fanOutAgents || []).length} agents reported (all failed)`
454
463
  : 'All fan-out agents failed';
@@ -456,11 +465,11 @@ function updateWorkItemStatus(meta, status, reason) {
456
465
  }
457
466
  } else {
458
467
  target.status = status;
459
- if (status === 'done') {
468
+ if (status === WI_STATUS.DONE) {
460
469
  delete target.failReason;
461
470
  delete target.failedAt;
462
471
  target.completedAt = ts();
463
- } else if (status === 'failed') {
472
+ } else if (status === WI_STATUS.FAILED) {
464
473
  if (reason) target.failReason = reason;
465
474
  target.failedAt = ts();
466
475
  }
@@ -474,8 +483,10 @@ function updateWorkItemStatus(meta, status, reason) {
474
483
  }
475
484
  }
476
485
 
486
+ const _VALID_PRD_STATUSES = new Set([...Object.values(WI_STATUS), 'missing']);
477
487
  function syncPrdItemStatus(itemId, status, sourcePlan) {
478
488
  if (!itemId) return;
489
+ if (!_VALID_PRD_STATUSES.has(status)) return;
479
490
  try {
480
491
  const prdDir = path.join(MINIONS_DIR, 'prd');
481
492
  const files = sourcePlan ? [sourcePlan] : require('fs').readdirSync(prdDir).filter(f => f.endsWith('.json'));
@@ -594,7 +605,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
594
605
  agent: agentName,
595
606
  branch: meta?.branch || '',
596
607
  reviewStatus: 'pending',
597
- status: 'active',
608
+ status: PR_STATUS.ACTIVE,
598
609
  created: dateStamp(),
599
610
  url: extractPrUrl(prId),
600
611
  prdItems: meta?.item?.id ? [meta.item.id] : [],
@@ -703,7 +714,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
703
714
  } catch (err) { log('warn', `Post-merge worktree cleanup: ${err.message}`); }
704
715
  }
705
716
 
706
- if (newStatus !== 'merged') return;
717
+ if (newStatus !== PR_STATUS.MERGED) return;
707
718
 
708
719
  // Resolve linked work item from pr-links or PR branch name
709
720
  let mergedItemId = getPrLinks()[pr.id];
@@ -722,13 +733,13 @@ async function handlePostMerge(pr, project, config, newStatus) {
722
733
  const plan = safeJson(path.join(prdDir, pf));
723
734
  if (!plan?.missing_features) continue;
724
735
  const feature = plan.missing_features.find(f => f.id === mergedItemId);
725
- if (feature && feature.status !== 'implemented') {
726
- feature.status = 'implemented';
736
+ if (feature && feature.status !== WI_STATUS.DONE) {
737
+ feature.status = WI_STATUS.DONE;
727
738
  shared.safeWrite(path.join(prdDir, pf), plan);
728
739
  updated++;
729
740
  }
730
741
  }
731
- if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as implemented for ${pr.id}`);
742
+ if (updated > 0) log('info', `Post-merge: marked ${mergedItemId} as done for ${pr.id}`);
732
743
  } catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
733
744
 
734
745
  // Mark work item as done
@@ -739,10 +750,10 @@ async function handlePostMerge(pr, project, config, newStatus) {
739
750
  const items = safeJson(wiPath);
740
751
  if (!items) continue;
741
752
  const item = items.find(i => i.id === mergedItemId);
742
- if (item && item.status !== 'done') {
753
+ if (item && item.status !== WI_STATUS.DONE) {
743
754
  log('info', `Post-merge: marking work item ${mergedItemId} as done (was ${item.status}) for ${pr.id}`);
744
- item.status = 'done';
745
- item.completedAt = e.ts();
755
+ item.status = WI_STATUS.DONE;
756
+ item.completedAt = ts();
746
757
  item._mergedVia = pr.id;
747
758
  shared.safeWrite(wiPath, items);
748
759
  break;
@@ -827,19 +838,19 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
827
838
  if (proj) {
828
839
  const centralPath = path.join(MINIONS_DIR, 'work-items.json');
829
840
  const items = safeJson(centralPath) || [];
830
- const alreadyExists = items.some(i => i.title === `Add skill: ${name}` && i.status !== 'failed');
841
+ const alreadyExists = items.some(i => i.title === `Add skill: ${name}` && i.status !== WI_STATUS.FAILED);
831
842
  if (!alreadyExists) {
832
843
  const skillId = `SK${String(items.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
833
844
  items.push({ id: skillId, type: 'implement', title: `Add skill: ${name}`,
834
845
  description: `Create project-level skill \`${filename}\` in ${project}.\n\nWrite this file to \`${proj.localPath}/.claude/skills/${filename}\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
835
- priority: 'low', status: 'queued', created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
846
+ priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
836
847
  shared.safeWrite(centralPath, items);
837
848
  log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
838
849
  }
839
850
  }
840
851
  } else {
841
852
  // Write in Claude Code native format: ~/.claude/skills/<name>/SKILL.md
842
- const claudeSkillsDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.claude', 'skills');
853
+ const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
843
854
  const skillDir = path.join(claudeSkillsDir, name.replace(/[^a-z0-9-]/g, '-'));
844
855
  const skillPath = path.join(skillDir, 'SKILL.md');
845
856
  if (!fs.existsSync(skillPath)) {
@@ -918,10 +929,10 @@ function updateMetrics(agentId, dispatchItem, result, taskUsage, prsCreatedCount
918
929
  m.lastTask = dispatchItem.task;
919
930
  m.lastCompleted = ts();
920
931
  if (model) m.model = model;
921
- if (result === 'success') {
932
+ if (result === DISPATCH_RESULT.SUCCESS) {
922
933
  m.tasksCompleted++;
923
934
  if (prsCreatedCount > 0) m.prsCreated = (m.prsCreated || 0) + prsCreatedCount;
924
- if (dispatchItem.type === 'review') m.reviewsDone++;
935
+ if (dispatchItem.type === WORK_TYPE.REVIEW) m.reviewsDone++;
925
936
  } else if (result === 'retry') {
926
937
  // Auto-retry: count cost but not as a final outcome
927
938
  m.tasksRetried = (m.tasksRetried || 0) + 1;
@@ -1003,7 +1014,7 @@ function handleDecompositionResult(stdout, meta, config) {
1003
1014
  if (!parent) continue;
1004
1015
 
1005
1016
  // Mark parent as decomposed
1006
- parent.status = 'decomposed';
1017
+ parent.status = WI_STATUS.DECOMPOSED;
1007
1018
  parent._decomposed = true;
1008
1019
  delete parent._decomposing;
1009
1020
  parent._subItemIds = subItems.map(s => s.id);
@@ -1017,7 +1028,7 @@ function handleDecompositionResult(stdout, meta, config) {
1017
1028
  type: (sub.estimated_complexity === 'large') ? 'implement:large' : 'implement',
1018
1029
  priority: sub.priority || parent.priority || 'medium',
1019
1030
  description: sub.description || '',
1020
- status: 'pending',
1031
+ status: WI_STATUS.PENDING,
1021
1032
  complexity: sub.estimated_complexity || 'medium',
1022
1033
  depends_on: sub.depends_on || [],
1023
1034
  parent_id: parentId,
@@ -1042,7 +1053,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1042
1053
  const type = dispatchItem.type;
1043
1054
  const meta = dispatchItem.meta;
1044
1055
  const isSuccess = code === 0;
1045
- const result = isSuccess ? 'success' : 'error';
1056
+ const result = isSuccess ? DISPATCH_RESULT.SUCCESS : DISPATCH_RESULT.ERROR;
1046
1057
  const { resultSummary, taskUsage, sessionId, model } = parseAgentOutput(stdout);
1047
1058
 
1048
1059
  // Save session for potential resume on next dispatch
@@ -1057,13 +1068,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1057
1068
 
1058
1069
  // Handle decomposition results — create sub-items from decompose agent output
1059
1070
  let skipDoneStatus = false;
1060
- if (type === 'decompose' && isSuccess && meta?.item?.id) {
1071
+ if (type === WORK_TYPE.DECOMPOSE && isSuccess && meta?.item?.id) {
1061
1072
  const subCount = handleDecompositionResult(stdout, meta, config);
1062
1073
  if (subCount > 0) skipDoneStatus = true; // parent already marked 'decomposed' by handler
1063
1074
  // If decomposition produced nothing, fall through to mark parent as done
1064
1075
  }
1065
1076
 
1066
- if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, 'done', '');
1077
+ if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, WI_STATUS.DONE, '');
1067
1078
  if (!isSuccess && meta?.item?.id) {
1068
1079
  // Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
1069
1080
  let retries = (meta.item._retryCount || 0);
@@ -1078,9 +1089,9 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1078
1089
  }
1079
1090
  } catch { /* optional */ }
1080
1091
 
1081
- if (retries < 3) {
1082
- log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/3`);
1083
- updateWorkItemStatus(meta, 'pending', '');
1092
+ if (retries < ENGINE_DEFAULTS.maxRetries) {
1093
+ log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
1094
+ updateWorkItemStatus(meta, WI_STATUS.PENDING, '');
1084
1095
  try {
1085
1096
  const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
1086
1097
  ? path.join(MINIONS_DIR, 'work-items.json')
@@ -1089,17 +1100,17 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1089
1100
  const items = safeJson(wiPath) || [];
1090
1101
  const wi = items.find(i => i.id === meta.item.id);
1091
1102
  if (wi) {
1092
- wi._retryCount = retries + 1; wi.status = 'pending'; delete wi.dispatched_at; delete wi.dispatched_to;
1093
- if (type === 'decompose') delete wi._decomposing; // clear so item can retry decomposition
1103
+ wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to;
1104
+ if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing; // clear so item can retry decomposition
1094
1105
  shared.safeWrite(wiPath, items);
1095
1106
  }
1096
1107
  }
1097
1108
  } catch (err) { log('warn', `Retry update: ${err.message}`); }
1098
1109
  } else {
1099
- updateWorkItemStatus(meta, 'failed', 'Agent failed (3 retries exhausted)');
1110
+ updateWorkItemStatus(meta, WI_STATUS.FAILED, `Agent failed (${ENGINE_DEFAULTS.maxRetries} retries exhausted)`);
1100
1111
  }
1101
1112
  // Clear _decomposing flag on failure so item doesn't get permanently stuck
1102
- if (type === 'decompose') {
1113
+ if (type === WORK_TYPE.DECOMPOSE) {
1103
1114
  try {
1104
1115
  const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
1105
1116
  ? path.join(MINIONS_DIR, 'work-items.json')
@@ -1113,7 +1124,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1113
1124
  }
1114
1125
  }
1115
1126
  // Meeting post-completion: collect findings/debate/conclusion
1116
- if (type === 'meeting' && meta?.meetingId) {
1127
+ if (type === WORK_TYPE.MEETING && meta?.meetingId) {
1117
1128
  try {
1118
1129
  const { collectMeetingFindings } = require('./meeting');
1119
1130
  collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout);
@@ -1140,7 +1151,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1140
1151
  return d.includes(branchSlug) && fs.statSync(path.join(worktreeRoot, d)).isDirectory();
1141
1152
  });
1142
1153
  // Only remove if no other active dispatch uses this branch
1143
- const dispatch = e.getDispatch();
1154
+ const dispatch = getDispatch();
1144
1155
  const otherActive = ((dispatch.active || []).concat(dispatch.pending || [])).some(d =>
1145
1156
  d.id !== dispatchItem.id && d.meta?.branch && shared.sanitizeBranch && shared.sanitizeBranch(d.meta.branch) === branchSlug
1146
1157
  );
@@ -1162,7 +1173,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1162
1173
  }
1163
1174
 
1164
1175
  // Detect implement tasks that completed without creating a PR
1165
- if (isSuccess && (type === 'implement' || type === 'implement:large' || type === 'fix') && prsCreatedCount === 0 && meta?.item?.id) {
1176
+ if (isSuccess && (type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type === WORK_TYPE.FIX) && prsCreatedCount === 0 && meta?.item?.id) {
1166
1177
  // Check if a PR already exists linked to this work item (from a previous attempt)
1167
1178
  const projects = shared.getProjects(config);
1168
1179
  const existingPrFound = Object.values(getPrLinks()).includes(meta.item.id);
@@ -1182,15 +1193,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1182
1193
  wi.noPr = true;
1183
1194
  wi.failReason = 'Completed without creating a pull request';
1184
1195
  const retries = wi._retryCount || 0;
1185
- if (retries < 3) {
1186
- wi.status = 'pending';
1196
+ if (retries < ENGINE_DEFAULTS.maxRetries) {
1197
+ wi.status = WI_STATUS.PENDING;
1187
1198
  wi._retryCount = retries + 1;
1188
1199
  delete wi.dispatched_at;
1189
1200
  delete wi.dispatched_to;
1190
- e.log('info', `Auto-retry ${retries + 1}/3 for ${meta.item.id} (no PR created)`);
1201
+ log('info', `Auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no PR created)`);
1191
1202
  } else {
1192
- wi.status = 'failed';
1193
- e.log('warn', `${meta.item.id} failed after 3 retries — no PR created`);
1203
+ wi.status = WI_STATUS.FAILED;
1204
+ log('warn', `${meta.item.id} failed after ${ENGINE_DEFAULTS.maxRetries} retries — no PR created`);
1194
1205
  }
1195
1206
  shared.safeWrite(wiPath, items);
1196
1207
  }
@@ -1198,13 +1209,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1198
1209
  }
1199
1210
  }
1200
1211
 
1201
- if (type === 'review') updatePrAfterReview(agentId, meta?.pr, meta?.project);
1202
- if (type === 'fix') updatePrAfterFix(meta?.pr, meta?.project, meta?.source);
1212
+ if (type === WORK_TYPE.REVIEW) updatePrAfterReview(agentId, meta?.pr, meta?.project);
1213
+ if (type === WORK_TYPE.FIX) updatePrAfterFix(meta?.pr, meta?.project, meta?.source);
1203
1214
  checkForLearnings(agentId, config.agents[agentId], dispatchItem.task);
1204
1215
  if (isSuccess) extractSkillsFromOutput(stdout, agentId, dispatchItem, config);
1205
1216
  updateAgentHistory(agentId, dispatchItem, result);
1206
1217
  // Don't count auto-retries as errors in metrics — only count final outcomes
1207
- const isAutoRetry = !isSuccess && meta?.item?.id && (meta.item._retryCount || 0) < 3;
1218
+ const isAutoRetry = !isSuccess && meta?.item?.id && (meta.item._retryCount || 0) < ENGINE_DEFAULTS.maxRetries;
1208
1219
  const metricsResult = isAutoRetry ? 'retry' : result;
1209
1220
  updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
1210
1221
 
@@ -1229,14 +1240,14 @@ function syncPrdFromPrs(config) {
1229
1240
  for (const project of allProjects) {
1230
1241
  const wiPath = projectWorkItemsPath(project);
1231
1242
  const items = safeJson(wiPath) || [];
1232
- const hasPending = items.some(wi => wi.status === 'pending' && !wi._pr);
1243
+ const hasPending = items.some(wi => wi.status === WI_STATUS.PENDING && !wi._pr);
1233
1244
  if (!hasPending) continue;
1234
1245
  const reconciled = reconcileItemsWithPrs(items, allPrs);
1235
1246
  if (reconciled > 0) {
1236
1247
  safeWrite(wiPath, items);
1237
1248
  // Sync done status to PRD JSON for each newly reconciled item
1238
1249
  for (const wi of items) {
1239
- if (wi.status === 'done') syncPrdItemStatus(wi.id, 'done', wi.sourcePlan);
1250
+ if (wi.status === WI_STATUS.DONE) syncPrdItemStatus(wi.id, WI_STATUS.DONE, wi.sourcePlan);
1240
1251
  }
1241
1252
  totalReconciled += reconciled;
1242
1253
  }
@@ -400,19 +400,19 @@ function buildAgentContext(agentId, config, project) {
400
400
  const projects = getProjects(config);
401
401
  const allPrs = [];
402
402
  for (const p of projects) {
403
- const prs = getPrs(p).filter(pr => pr.status === PR_STATUS.ACTIVE || pr.status === 'linked');
403
+ const prs = getPrs(p).filter(pr => pr.status === PR_STATUS.ACTIVE);
404
404
  for (const pr of prs) allPrs.push({ ...pr, _project: p.name });
405
405
  }
406
406
  // Also check central pull-requests.json
407
407
  try {
408
408
  const centralPrs = safeJson(path.join(MINIONS_DIR, 'pull-requests.json')) || [];
409
- for (const pr of centralPrs.filter(pr => pr.status === PR_STATUS.ACTIVE || pr.status === 'linked')) {
409
+ for (const pr of centralPrs.filter(pr => pr.status === PR_STATUS.ACTIVE)) {
410
410
  if (!allPrs.some(p => p.id === pr.id)) allPrs.push({ ...pr, _project: 'central' });
411
411
  }
412
412
  } catch (e) { log('warn', 'read central pull-requests: ' + e.message); }
413
413
  if (allPrs.length > 0) {
414
414
  const prLines = allPrs.map(pr =>
415
- `- **${pr.id}** (${pr._project}): ${(pr.title || '').slice(0, 80)} [${pr.status === 'linked' ? 'context-only' : (pr.reviewStatus || 'pending')}${pr.buildStatus === 'failing' ? ', BUILD FAILING' : ''}]${pr.branch ? ' branch: `' + pr.branch + '`' : ''}${pr._context ? ' — ' + pr._context.slice(0, 100) : ''}`
415
+ `- **${pr.id}** (${pr._project}): ${(pr.title || '').slice(0, 80)} [${(pr.reviewStatus || 'pending')}${pr.buildStatus === 'failing' ? ', BUILD FAILING' : ''}]${pr.branch ? ' branch: `' + pr.branch + '`' : ''}${pr._context ? ' — ' + pr._context.slice(0, 100) : ''}`
416
416
  );
417
417
  context += `## Active Pull Requests\n\n${prLines.join('\n')}\n\n`;
418
418
  }
package/engine/queries.js CHANGED
@@ -177,7 +177,7 @@ function getAgentStatus(agentId) {
177
177
  const latestInFlight = allItems
178
178
  .filter(w =>
179
179
  (w.dispatched_to || '').toLowerCase() === String(agentId).toLowerCase() &&
180
- (w.status === 'dispatched' || w.status === 'in-progress')
180
+ w.status === 'dispatched'
181
181
  )
182
182
  .sort((a, b) => (b.dispatched_at || '').localeCompare(a.dispatched_at || ''))[0];
183
183
  if (latestInFlight) {
@@ -679,9 +679,9 @@ function getPrdInfo(config) {
679
679
  }
680
680
 
681
681
  // PRD JSON status is the source of truth — kept in sync with work item by syncPrdItemStatus.
682
- // Map from PRD JSON values to display values (dispatchedin-progress etc.)
682
+ // Map from PRD JSON values to display values (pendingmissing for undispatched items)
683
683
  // Augment each item with execution metadata from the work item.
684
- const statusDisplay = { dispatched: 'in-progress', pending: 'missing' };
684
+ const statusDisplay = { pending: 'missing' };
685
685
  for (const item of items) {
686
686
  const wi = wiById[item.id];
687
687
  // Work item status is source of truth when available (PRD JSON may lag behind)
@@ -697,7 +697,7 @@ function getPrdInfo(config) {
697
697
  const byStatus = {};
698
698
  items.forEach(item => { const s = item.status || 'missing'; byStatus[s] = byStatus[s] || []; byStatus[s].push(item); });
699
699
  const complete = (byStatus['done'] || []).length;
700
- const inProgress = (byStatus['in-progress'] || []).length;
700
+ const inProgress = (byStatus['dispatched'] || []).length;
701
701
  const paused = (byStatus['paused'] || []).length;
702
702
  const missing = (byStatus['missing'] || []).length;
703
703
  const donePercent = total > 0 ? Math.round((complete / total) * 100) : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.328",
3
+ "version": "0.1.329",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"