@yemi33/minions 0.1.327 → 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,15 @@
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
+
8
+ ## 0.1.328 (2026-04-03)
9
+
10
+ ### Fixes
11
+ - import PR_STATUS in engine.js — was causing discoverWork to fail
12
+
3
13
  ## 0.1.327 (2026-04-03)
4
14
 
5
15
  ### 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') {
@@ -23,9 +23,8 @@ function checkPlanCompletion(meta, config) {
23
23
  const planPath = path.join(PRD_DIR, planFile);
24
24
  const plan = safeJson(planPath);
25
25
  if (!plan?.missing_features) return;
26
- if (plan.status === 'completed') {
26
+ if (plan.status === PLAN_STATUS.COMPLETED) {
27
27
  if (plan._completionNotified) return;
28
- // Crash recovery: status=completed but _completionNotified not set — fall through
29
28
  }
30
29
 
31
30
  const projects = shared.getProjects(config);
@@ -81,7 +80,7 @@ function checkPlanCompletion(meta, config) {
81
80
  const failedItems = planItems.filter(w => w.status === WI_STATUS.FAILED);
82
81
 
83
82
  // 1. Mark plan as completed
84
- plan.status = 'completed';
83
+ plan.status = PLAN_STATUS.COMPLETED;
85
84
  plan.completedAt = ts();
86
85
 
87
86
  // Compute timing
@@ -137,30 +136,15 @@ function checkPlanCompletion(meta, config) {
137
136
  ...uniquePrs.map(pr => `- ${pr.id}: ${pr.title || ''} ${pr.url || ''}`),
138
137
  ].filter(Boolean).join('\n');
139
138
 
140
- // Write summary to notes/inbox (slug-based dedup prevents duplicates on same day)
141
- const slug = `prd-completion-${planFile.replace('.json', '')}`;
142
- const wrote = shared.writeToInbox('engine', slug, summary);
143
- if (wrote) log('info', `PRD completion summary written to notes/inbox/`);
144
-
145
- // Persist status and _completionNotified atomically BEFORE creating work items
146
- // NOTE: Do NOT set plan._completionNotified in-memory before persist —
147
- // if persist fails, the in-memory flag would prevent retry on next tick.
148
- mutateJsonFileLocked(planPath, (data) => {
149
- data.status = 'completed';
150
- data.completedAt = plan.completedAt;
151
- data._completionNotified = true;
152
- if (plan._timing) data._timing = plan._timing;
153
- return data;
154
- });
139
+ // Write summary to notes/inbox
140
+ const summaryFile = `prd-completion-${planFile.replace('.json', '')}-${ts().slice(0, 10)}.md`;
141
+ shared.safeWrite(shared.uniquePath(path.join(MINIONS_DIR, 'notes', 'inbox', summaryFile)), summary);
142
+ log('info', `PRD completion summary written to notes/inbox/${summaryFile}`);
155
143
 
156
144
  // Resolve the primary project for writing new work items (PR, verify)
157
145
  const projectName = plan.project;
158
146
  const primaryProject = projectName
159
- ? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : (projects[0] || null);
160
- if (!primaryProject) {
161
- log('warn', `checkPlanCompletion: no project available (projects array ${projects.length === 0 ? 'empty' : 'no match for ' + projectName}) — skipping PR/verify creation for ${planFile}`);
162
- return;
163
- }
147
+ ? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : projects[0];
164
148
  const wiPath = primaryProject ? shared.projectWorkItemsPath(primaryProject) : null;
165
149
  const workItems = wiPath ? (safeJson(wiPath) || []) : [];
166
150
 
@@ -176,7 +160,7 @@ function checkPlanCompletion(meta, config) {
176
160
  id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
177
161
  type: 'implement', priority: 'high',
178
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}`,
179
- status: 'pending', created: ts(), createdBy: 'engine:plan-completion',
163
+ status: WI_STATUS.PENDING, created: ts(), createdBy: 'engine:plan-completion',
180
164
  sourcePlan: planFile, itemType: 'pr',
181
165
  branch: featureBranch, branchStrategy: 'shared-branch', project: projectName,
182
166
  });
@@ -206,12 +190,11 @@ function checkPlanCompletion(meta, config) {
206
190
 
207
191
  // Build per-project checkout commands: one worktree, merge all PR branches into it
208
192
  const checkoutBlocks = Object.entries(projectPrs).map(([name, { project: p, prs, mainBranch }]) => {
209
- const localPath = p.localPath.replace(/\\/g, '/');
210
- const wtPath = `${localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
193
+ const wtPath = `${p.localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
211
194
  const branches = prs.map(pr => pr.branch).filter(Boolean);
212
195
  const lines = [
213
196
  `# ${name} — merge ${branches.length} PR branch(es) into one worktree`,
214
- `cd "${localPath}"`,
197
+ `cd "${p.localPath.replace(/\\/g, '/')}"`,
215
198
  `git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`,
216
199
  `git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
217
200
  `cd "${wtPath}"`,
@@ -232,10 +215,9 @@ function checkPlanCompletion(meta, config) {
232
215
  ).join('\n');
233
216
 
234
217
  // List projects and their worktree paths for the agent
235
- const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) => {
236
- const lp = p.localPath.replace(/\\/g, '/');
237
- return `- **${name}**: see setup commands below (\`${lp}/../worktrees/verify-${name}-${planSlug}-*\`)`;
238
- }).join('\n');
218
+ const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) =>
219
+ `- **${name}**: \`${p.localPath}/../worktrees/verify-${planSlug}\``
220
+ ).join('\n');
239
221
 
240
222
  const description = [
241
223
  `Verification task for completed plan \`${planFile}\`.`,
@@ -269,7 +251,7 @@ function checkPlanCompletion(meta, config) {
269
251
  type: 'verify',
270
252
  priority: 'high',
271
253
  description,
272
- status: 'pending',
254
+ status: WI_STATUS.PENDING,
273
255
  created: ts(),
274
256
  createdBy: 'engine:plan-verification',
275
257
  sourcePlan: planFile,
@@ -280,75 +262,46 @@ function checkPlanCompletion(meta, config) {
280
262
  log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
281
263
  }
282
264
 
283
- // 5. Archive deferred until verify completes (see runPostCompletionHooks).
284
- // Plan stays active until verification finishes so artifacts are visible.
285
-
286
- log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
287
- }
288
-
289
- // ─── Plan Archiving (called after verify completes) ─────────────────────────
290
-
291
- function archivePlan(planFile, plan, projects, config) {
292
- const planPath = path.join(PRD_DIR, planFile);
293
- const projectName = plan.project || '';
294
-
295
- // Archive PRD .json to prd/archive/
265
+ // 5. Archive: move PRD .json to prd/archive/ and source .md plan to plans/archive/
296
266
  const prdArchiveDir = path.join(PRD_DIR, 'archive');
297
267
  if (!fs.existsSync(prdArchiveDir)) fs.mkdirSync(prdArchiveDir, { recursive: true });
298
- if (fs.existsSync(planPath)) {
268
+ shared.safeWrite(planPath, plan); // save completed status first
269
+ try {
270
+ fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
271
+ log('info', `Archived completed PRD: prd/archive/${planFile}`);
272
+ } catch (err) {
273
+ log('warn', `Failed to archive PRD ${planFile}: ${err.message}`);
299
274
  shared.safeWrite(planPath, plan);
300
- try {
301
- fs.renameSync(planPath, path.join(prdArchiveDir, planFile));
302
- log('info', `Archived completed PRD: prd/archive/${planFile}`);
303
- } catch (err) { log('warn', `Failed to archive PRD ${planFile}: ${err.message}`); }
304
275
  }
305
276
 
306
- // Archive the source .md plan
277
+ // Also archive the source .md plan if it exists
307
278
  const planArchiveDir = path.join(PLANS_DIR, 'archive');
308
279
  if (!fs.existsSync(planArchiveDir)) fs.mkdirSync(planArchiveDir, { recursive: true });
309
- if (plan.source_plan) {
310
- const mdPath = path.join(PLANS_DIR, plan.source_plan);
311
- if (fs.existsSync(mdPath)) {
312
- try {
313
- fs.renameSync(mdPath, path.join(planArchiveDir, plan.source_plan));
314
- log('info', `Archived source plan: plans/archive/${plan.source_plan}`);
315
- } catch (err) { log('warn', `Failed to archive source plan ${plan.source_plan}: ${err.message}`); }
316
- }
317
- } else {
318
- try {
319
- const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
320
- for (const md of mdFiles) {
321
- const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
322
- if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
323
- try {
324
- fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
325
- log('info', `Archived source plan: plans/archive/${md}`);
326
- } catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
327
- break;
328
- }
329
- }
330
- } catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
331
- }
332
-
333
- // Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
334
280
  try {
335
- let allWi = [];
336
- for (const p of projects) {
337
- try { allWi = allWi.concat(safeJson(shared.projectWorkItemsPath(p)) || []); } catch {}
338
- }
339
- const planWi = allWi.filter(w => w.sourcePlan === planFile && w.itemType !== 'verify');
340
- const allPrs = [];
341
- for (const p of projects) {
342
- try { allPrs.push(...(safeJson(shared.projectPrPath(p)) || [])); } catch {}
281
+ const mdFiles = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
282
+ for (const md of mdFiles) {
283
+ const mdContent = shared.safeRead(path.join(PLANS_DIR, md)) || '';
284
+ // Match by project name or plan summary appearing in the .md content
285
+ if (mdContent.includes(projectName) || mdContent.includes(plan.plan_summary?.slice(0, 40) || '___nomatch___')) {
286
+ try {
287
+ fs.renameSync(path.join(PLANS_DIR, md), path.join(planArchiveDir, md));
288
+ log('info', `Archived source plan: plans/archive/${md}`);
289
+ } catch (err) { log('warn', `Failed to archive plan ${md}: ${err.message}`); }
290
+ break;
291
+ }
343
292
  }
293
+ } catch (err) { log('warn', `Plan archive scan: ${err.message}`); }
344
294
 
295
+ // 6. Clean up ALL worktrees created for this plan's work items (shared-branch + per-item)
296
+ try {
297
+ // Collect all branch slugs: shared-branch + per-item branches + item IDs
345
298
  const branchSlugs = new Set();
346
299
  if (plan.feature_branch) branchSlugs.add(shared.sanitizeBranch(plan.feature_branch).toLowerCase());
347
- for (const w of planWi) {
300
+ for (const w of doneItems) {
348
301
  if (w.branch) branchSlugs.add(shared.sanitizeBranch(w.branch).toLowerCase());
349
302
  if (w.id) branchSlugs.add(w.id.toLowerCase());
350
303
  }
351
- for (const pr of allPrs.filter(pr => (pr.prdItems || []).some(id => planWi.find(w => w.id === id)))) {
304
+ for (const pr of uniquePrs) {
352
305
  if (pr.branch) branchSlugs.add(shared.sanitizeBranch(pr.branch).toLowerCase());
353
306
  }
354
307
 
@@ -370,8 +323,10 @@ function archivePlan(planFile, plan, projects, config) {
370
323
  }
371
324
  }
372
325
  }
373
- if (cleanedWt > 0) log('info', `Plan archive: cleaned ${cleanedWt} worktree(s)`);
326
+ if (cleanedWt > 0) log('info', `Plan completion: cleaned ${cleanedWt} worktree(s)`);
374
327
  } catch (err) { log('warn', `Worktree cleanup: ${err.message}`); }
328
+
329
+ log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
375
330
  }
376
331
 
377
332
  // ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
@@ -430,10 +385,6 @@ function chainPlanToPrd(dispatchItem, meta, config) {
430
385
 
431
386
  const projectName = meta?.item?.project || meta?.project?.name;
432
387
  const projects = shared.getProjects(config);
433
- if (projects.length === 0) {
434
- log('error', 'Plan chaining: no projects configured — cannot chain plan to PRD');
435
- return;
436
- }
437
388
  const targetProject = projectName
438
389
  ? projects.find(p => p.name === projectName) || projects[0]
439
390
  : projects[0];
@@ -446,17 +397,14 @@ function chainPlanToPrd(dispatchItem, meta, config) {
446
397
  log('info', `Plan chaining: queuing plan-to-prd for next tick (chained from ${dispatchItem.id})`);
447
398
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
448
399
  let items = [];
449
- try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch (err) {
450
- log('warn', `Failed to parse ${wiPath}: ${err.message} — creating .bak and starting fresh`);
451
- try { fs.copyFileSync(wiPath, wiPath + '.bak'); } catch {}
452
- }
400
+ try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {}
453
401
  items.push({
454
402
  id: 'W-' + shared.uid(),
455
403
  title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
456
404
  type: 'plan-to-prd',
457
405
  priority: meta?.item?.priority || 'high',
458
406
  description: `Plan file: plans/${planFile.name}\nChained from plan task ${dispatchItem.id}`,
459
- status: 'pending',
407
+ status: WI_STATUS.PENDING,
460
408
  created: ts(),
461
409
  createdBy: 'engine:chain',
462
410
  project: targetProject.name,
@@ -466,10 +414,15 @@ function chainPlanToPrd(dispatchItem, meta, config) {
466
414
  }
467
415
 
468
416
  // ─── Work Item Status ────────────────────────────────────────────────────────
417
+ const _VALID_WI_STATUSES = new Set(Object.values(WI_STATUS));
469
418
  function updateWorkItemStatus(meta, status, reason) {
470
419
 
471
420
  const itemId = meta.item?.id;
472
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
+ }
473
426
 
474
427
  let wiPath;
475
428
  if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
@@ -530,8 +483,10 @@ function updateWorkItemStatus(meta, status, reason) {
530
483
  }
531
484
  }
532
485
 
486
+ const _VALID_PRD_STATUSES = new Set([...Object.values(WI_STATUS), 'missing']);
533
487
  function syncPrdItemStatus(itemId, status, sourcePlan) {
534
488
  if (!itemId) return;
489
+ if (!_VALID_PRD_STATUSES.has(status)) return;
535
490
  try {
536
491
  const prdDir = path.join(MINIONS_DIR, 'prd');
537
492
  const files = sourcePlan ? [sourcePlan] : require('fs').readdirSync(prdDir).filter(f => f.endsWith('.json'));
@@ -594,11 +549,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
594
549
  if (prMatches.size === 0) return 0;
595
550
 
596
551
  const projects = shared.getProjects(config);
597
- if (projects.length === 0 && !meta?.project?.name) {
598
- log('warn', `syncPrsFromOutput: no projects configured and no project in meta — cannot sync PRs`);
599
- return 0;
600
- }
601
- const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || (projects[0] || null);
552
+ const defaultProject = (meta?.project?.name && projects.find(p => p.name === meta.project.name)) || projects[0];
602
553
  const useCentral = !defaultProject;
603
554
 
604
555
  // Match each PR to its correct project by finding which repo URL appears near the PR number in output
@@ -626,54 +577,47 @@ function syncPrsFromOutput(output, agentId, meta, config) {
626
577
  const agentName = config.agents?.[agentId]?.name || agentId;
627
578
  let added = 0;
628
579
  const centralPrPath = path.join(MINIONS_DIR, 'pull-requests.json');
580
+ // Track which PR files need writing — keyed by target name
581
+ const dirtyTargets = new Map(); // name -> { prs, prPath }
629
582
 
630
- // Group PR matches by target file so we take one lock per target
631
- const targetPrIds = new Map(); // targetName -> { prPath, prIds: [{ prId, fullId }] }
632
583
  for (const prId of prMatches) {
633
584
  const fullId = `PR-${prId}`;
634
585
  const targetProject = useCentral ? null : resolveProjectForPr(prId);
635
586
  const targetName = targetProject ? targetProject.name : '_central';
636
587
  const prPath = targetProject ? shared.projectPrPath(targetProject) : centralPrPath;
637
- if (!targetPrIds.has(targetName)) {
638
- targetPrIds.set(targetName, { prPath, prIds: [] });
588
+
589
+ // Load PRs for this target (cache per target)
590
+ if (!dirtyTargets.has(targetName)) {
591
+ dirtyTargets.set(targetName, { prs: safeJson(prPath) || [], prPath });
639
592
  }
640
- targetPrIds.get(targetName).prIds.push({ prId, fullId });
593
+ const entry = dirtyTargets.get(targetName);
594
+ if (entry.prs.some(p => p.id === fullId || String(p.id).includes(prId))) continue;
595
+
596
+ let title = meta?.item?.title || '';
597
+ const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
598
+ if (titleMatch) title = titleMatch[1].trim();
599
+ if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
600
+ title = meta?.item?.title || '';
601
+ }
602
+ entry.prs.push({
603
+ id: fullId,
604
+ title: (title || `PR created by ${agentName}`).slice(0, 120),
605
+ agent: agentName,
606
+ branch: meta?.branch || '',
607
+ reviewStatus: 'pending',
608
+ status: PR_STATUS.ACTIVE,
609
+ created: dateStamp(),
610
+ url: extractPrUrl(prId),
611
+ prdItems: meta?.item?.id ? [meta.item.id] : [],
612
+ sourcePlan: meta?.item?.sourcePlan || '',
613
+ itemType: meta?.item?.itemType || ''
614
+ });
615
+ if (meta?.item?.id) addPrLink(fullId, meta.item.id);
616
+ added++;
641
617
  }
642
618
 
643
- // For each target, read+modify+write inside a single locked callback
644
- for (const [name, { prPath, prIds }] of targetPrIds) {
645
- mutateJsonFileLocked(prPath, (prs) => {
646
- if (!Array.isArray(prs)) prs = [];
647
- // Deduplicate any existing entries with same id (case-insensitive agent name race)
648
- const seen = new Set();
649
- prs = prs.filter(p => { const k = String(p.id); if (seen.has(k)) return false; seen.add(k); return true; });
650
- for (const { prId, fullId } of prIds) {
651
- if (prs.some(p => p.id === fullId || String(p.id) === String(prId))) continue;
652
-
653
- let title = meta?.item?.title || '';
654
- const titleMatch = output.match(new RegExp(`${prId}[^\\n]*?[—–-]\\s*([^\\n]+)`, 'i'));
655
- if (titleMatch) title = titleMatch[1].trim();
656
- if (title.includes('session_id') || title.includes('is_error') || title.includes('uuid') || title.length > 120) {
657
- title = meta?.item?.title || '';
658
- }
659
- prs.push({
660
- id: fullId,
661
- title: (title || `PR created by ${agentName}`).slice(0, 120),
662
- agent: agentName,
663
- branch: meta?.branch || '',
664
- reviewStatus: 'pending',
665
- status: 'active',
666
- created: dateStamp(),
667
- url: extractPrUrl(prId),
668
- prdItems: meta?.item?.id ? [meta.item.id] : [],
669
- sourcePlan: meta?.item?.sourcePlan || '',
670
- itemType: meta?.item?.itemType || ''
671
- });
672
- if (meta?.item?.id) addPrLink(fullId, meta.item.id);
673
- added++;
674
- }
675
- return prs;
676
- }, { defaultValue: [] });
619
+ for (const [name, entry] of dirtyTargets) {
620
+ shared.safeWrite(entry.prPath, entry.prs);
677
621
  log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}/pull-requests.json`);
678
622
  }
679
623
  return added;
@@ -693,7 +637,7 @@ function updatePrAfterReview(agentId, pr, project) {
693
637
  // Record the reviewer — actual verdict comes from ADO/GitHub votes via pollPrStatus.
694
638
  // Set to 'waiting' so pollPrStatus updates it with the real vote on next cycle.
695
639
  const dispatch = getDispatch();
696
- const completedEntry = (dispatch.completed || []).find(d => d.agent === agentId && d.type === WORK_TYPE.REVIEW);
640
+ const completedEntry = (dispatch.completed || []).find(d => d.agent === agentId && d.type === 'review');
697
641
 
698
642
  // Set reviewStatus to 'waiting' (single source of truth — synced from ADO/GitHub votes on next poll)
699
643
  target.reviewStatus = 'waiting';
@@ -718,7 +662,7 @@ function updatePrAfterReview(agentId, pr, project) {
718
662
  }
719
663
 
720
664
  shared.safeWrite(project ? shared.projectPrPath(project) : path.join(path.resolve(MINIONS_DIR, '..'), '.minions', 'pull-requests.json'), prs);
721
- log('info', `Updated ${pr.id} → minions review: ${target.reviewStatus} by ${reviewerName}`);
665
+ log('info', `Updated ${pr.id} → minions review: ${minionsVerdict} by ${reviewerName}`);
722
666
  createReviewFeedbackForAuthor(agentId, { ...pr, ...target }, config);
723
667
  }
724
668
 
@@ -770,7 +714,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
770
714
  } catch (err) { log('warn', `Post-merge worktree cleanup: ${err.message}`); }
771
715
  }
772
716
 
773
- if (newStatus !== 'merged') return;
717
+ if (newStatus !== PR_STATUS.MERGED) return;
774
718
 
775
719
  // Resolve linked work item from pr-links or PR branch name
776
720
  let mergedItemId = getPrLinks()[pr.id];
@@ -894,12 +838,12 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config) {
894
838
  if (proj) {
895
839
  const centralPath = path.join(MINIONS_DIR, 'work-items.json');
896
840
  const items = safeJson(centralPath) || [];
897
- 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);
898
842
  if (!alreadyExists) {
899
843
  const skillId = `SK${String(items.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
900
844
  items.push({ id: skillId, type: 'implement', title: `Add skill: ${name}`,
901
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\`\`\``,
902
- 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}` });
903
847
  shared.safeWrite(centralPath, items);
904
848
  log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
905
849
  }
@@ -1084,7 +1028,7 @@ function handleDecompositionResult(stdout, meta, config) {
1084
1028
  type: (sub.estimated_complexity === 'large') ? 'implement:large' : 'implement',
1085
1029
  priority: sub.priority || parent.priority || 'medium',
1086
1030
  description: sub.description || '',
1087
- status: 'pending',
1031
+ status: WI_STATUS.PENDING,
1088
1032
  complexity: sub.estimated_complexity || 'medium',
1089
1033
  depends_on: sub.depends_on || [],
1090
1034
  parent_id: parentId,
@@ -1124,13 +1068,13 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1124
1068
 
1125
1069
  // Handle decomposition results — create sub-items from decompose agent output
1126
1070
  let skipDoneStatus = false;
1127
- if (type === 'decompose' && isSuccess && meta?.item?.id) {
1071
+ if (type === WORK_TYPE.DECOMPOSE && isSuccess && meta?.item?.id) {
1128
1072
  const subCount = handleDecompositionResult(stdout, meta, config);
1129
1073
  if (subCount > 0) skipDoneStatus = true; // parent already marked 'decomposed' by handler
1130
1074
  // If decomposition produced nothing, fall through to mark parent as done
1131
1075
  }
1132
1076
 
1133
- if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, 'done', '');
1077
+ if (isSuccess && meta?.item?.id && !skipDoneStatus) updateWorkItemStatus(meta, WI_STATUS.DONE, '');
1134
1078
  if (!isSuccess && meta?.item?.id) {
1135
1079
  // Auto-retry: read fresh _retryCount from file (not stale dispatch-time snapshot)
1136
1080
  let retries = (meta.item._retryCount || 0);
@@ -1145,9 +1089,8 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1145
1089
  }
1146
1090
  } catch { /* optional */ }
1147
1091
 
1148
- const maxRetries = ENGINE_DEFAULTS.maxRetries;
1149
- if (retries < maxRetries) {
1150
- log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${maxRetries}`);
1092
+ if (retries < ENGINE_DEFAULTS.maxRetries) {
1093
+ log('info', `Agent failed for ${meta.item.id} — auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries}`);
1151
1094
  updateWorkItemStatus(meta, WI_STATUS.PENDING, '');
1152
1095
  try {
1153
1096
  const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
@@ -1158,16 +1101,16 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1158
1101
  const wi = items.find(i => i.id === meta.item.id);
1159
1102
  if (wi) {
1160
1103
  wi._retryCount = retries + 1; wi.status = WI_STATUS.PENDING; delete wi.dispatched_at; delete wi.dispatched_to;
1161
- if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing;
1104
+ if (type === WORK_TYPE.DECOMPOSE) delete wi._decomposing; // clear so item can retry decomposition
1162
1105
  shared.safeWrite(wiPath, items);
1163
1106
  }
1164
1107
  }
1165
1108
  } catch (err) { log('warn', `Retry update: ${err.message}`); }
1166
1109
  } else {
1167
- updateWorkItemStatus(meta, WI_STATUS.FAILED, `Agent failed (${maxRetries} retries exhausted)`);
1110
+ updateWorkItemStatus(meta, WI_STATUS.FAILED, `Agent failed (${ENGINE_DEFAULTS.maxRetries} retries exhausted)`);
1168
1111
  }
1169
1112
  // Clear _decomposing flag on failure so item doesn't get permanently stuck
1170
- if (type === 'decompose') {
1113
+ if (type === WORK_TYPE.DECOMPOSE) {
1171
1114
  try {
1172
1115
  const wiPath = meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout'
1173
1116
  ? path.join(MINIONS_DIR, 'work-items.json')
@@ -1181,7 +1124,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1181
1124
  }
1182
1125
  }
1183
1126
  // Meeting post-completion: collect findings/debate/conclusion
1184
- if (type === 'meeting' && meta?.meetingId) {
1127
+ if (type === WORK_TYPE.MEETING && meta?.meetingId) {
1185
1128
  try {
1186
1129
  const { collectMeetingFindings } = require('./meeting');
1187
1130
  collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout);
@@ -1194,19 +1137,6 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1194
1137
  let prsCreatedCount = 0;
1195
1138
  if (isSuccess) prsCreatedCount = syncPrsFromOutput(stdout, agentId, meta, config) || 0;
1196
1139
 
1197
- // Archive plan after verify task completes (AFTER PR sync so E2E PR is linked)
1198
- if (meta?.item?.itemType === 'verify' && meta?.item?.sourcePlan) {
1199
- try {
1200
- const vPlanFile = meta.item.sourcePlan;
1201
- const vPlanPath = path.join(PRD_DIR, vPlanFile);
1202
- const vPlan = safeJson(vPlanPath);
1203
- if (vPlan) {
1204
- const vProjects = shared.getProjects(config);
1205
- archivePlan(vPlanFile, vPlan, vProjects, config);
1206
- }
1207
- } catch (err) { log('warn', `Verify archive: ${err.message}`); }
1208
- }
1209
-
1210
1140
  // Clean up worktree for non-shared-branch tasks after completion
1211
1141
  if (meta?.branch && meta?.branchStrategy !== 'shared-branch') {
1212
1142
  try {
@@ -1229,7 +1159,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1229
1159
  for (const dir of dirs) {
1230
1160
  const wtPath = path.join(worktreeRoot, dir);
1231
1161
  try {
1232
- execSilent(`git worktree remove "${wtPath}" --force`, { cwd: rootDir, timeout: 15000 });
1162
+ shared.exec(`git worktree remove "${wtPath}" --force`, { cwd: rootDir, stdio: 'pipe', timeout: 15000, windowsHide: true });
1233
1163
  log('info', `Post-completion: removed worktree ${dir}`);
1234
1164
  } catch (err) {
1235
1165
  log('warn', `Post-completion: failed to remove worktree ${dir}: ${err.message}`);
@@ -1263,16 +1193,15 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1263
1193
  wi.noPr = true;
1264
1194
  wi.failReason = 'Completed without creating a pull request';
1265
1195
  const retries = wi._retryCount || 0;
1266
- const maxR = ENGINE_DEFAULTS.maxRetries;
1267
- if (retries < maxR) {
1196
+ if (retries < ENGINE_DEFAULTS.maxRetries) {
1268
1197
  wi.status = WI_STATUS.PENDING;
1269
1198
  wi._retryCount = retries + 1;
1270
1199
  delete wi.dispatched_at;
1271
1200
  delete wi.dispatched_to;
1272
- log('info', `Auto-retry ${retries + 1}/${maxR} for ${meta.item.id} (no PR created)`);
1201
+ log('info', `Auto-retry ${retries + 1}/${ENGINE_DEFAULTS.maxRetries} for ${meta.item.id} (no PR created)`);
1273
1202
  } else {
1274
1203
  wi.status = WI_STATUS.FAILED;
1275
- log('warn', `${meta.item.id} failed after ${maxR} retries — no PR created`);
1204
+ log('warn', `${meta.item.id} failed after ${ENGINE_DEFAULTS.maxRetries} retries — no PR created`);
1276
1205
  }
1277
1206
  shared.safeWrite(wiPath, items);
1278
1207
  }
@@ -1286,7 +1215,7 @@ function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config) {
1286
1215
  if (isSuccess) extractSkillsFromOutput(stdout, agentId, dispatchItem, config);
1287
1216
  updateAgentHistory(agentId, dispatchItem, result);
1288
1217
  // Don't count auto-retries as errors in metrics — only count final outcomes
1289
- 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;
1290
1219
  const metricsResult = isAutoRetry ? 'retry' : result;
1291
1220
  updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
1292
1221
 
@@ -1318,7 +1247,7 @@ function syncPrdFromPrs(config) {
1318
1247
  safeWrite(wiPath, items);
1319
1248
  // Sync done status to PRD JSON for each newly reconciled item
1320
1249
  for (const wi of items) {
1321
- if (wi.status === WI_STATUS.DONE) syncPrdItemStatus(wi.id, 'done', wi.sourcePlan);
1250
+ if (wi.status === WI_STATUS.DONE) syncPrdItemStatus(wi.id, WI_STATUS.DONE, wi.sourcePlan);
1322
1251
  }
1323
1252
  totalReconciled += reconciled;
1324
1253
  }
@@ -1334,7 +1263,6 @@ function syncPrdFromPrs(config) {
1334
1263
 
1335
1264
  module.exports = {
1336
1265
  checkPlanCompletion,
1337
- archivePlan,
1338
1266
  updateWorkItemStatus,
1339
1267
  syncPrdItemStatus,
1340
1268
  syncPrsFromOutput,
@@ -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/engine.js CHANGED
@@ -25,7 +25,7 @@ const fs = require('fs');
25
25
  const path = require('path');
26
26
  const shared = require('./engine/shared');
27
27
  const { exec, execSilent, runFile, ENGINE_DEFAULTS: DEFAULTS,
28
- WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, DISPATCH_RESULT } = shared;
28
+ WI_STATUS, DONE_STATUSES, WORK_TYPE, PLAN_STATUS, PR_STATUS, DISPATCH_RESULT } = shared;
29
29
  const queries = require('./engine/queries');
30
30
 
31
31
  // ─── Paths ──────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.327",
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"